Class_generator/index.html
StillHammer 8ebc0b2334 Add TTS service, deployment docs, and refactor game modules
- Add TTSService.js for text-to-speech functionality
- Add comprehensive deployment documentation (guides, checklists, diagnostics)
- Add new SBS content (chapters 8 & 9)
- Refactor 14 game modules for better maintainability (-947 lines)
- Enhance SettingsDebug.js with improved debugging capabilities
- Update configuration files and startup scripts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 23:41:12 +08:00

3058 lines
143 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Class Generator - Educational Games Platform</title>
<!-- Styles -->
<link rel="stylesheet" href="src/styles/base.css?v=3">
<link rel="stylesheet" href="src/styles/components.css?v=17">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
</head>
<body>
<!-- Application Root -->
<div id="app">
<!-- Loading screen shown until app initializes -->
<div id="loading-screen" class="loading-screen">
<div class="loading-spinner"></div>
<h2>Loading Class Generator...</h2>
<p>Initializing educational games platform</p>
</div>
<!-- Main application container -->
<div id="app-container" class="app-container" style="display: none;">
<!-- Top navigation bar -->
<header id="app-header" class="app-header">
<div class="header-content">
<div id="connection-status" class="connection-status">
<span class="status-indicator"></span>
<span class="status-text">Online</span>
</div>
<div class="header-actions">
<a href="/test-drs-interface.html" target="_blank" class="test-link" title="Tests DRS">
🧪 Tests DRS
</a>
</div>
</div>
</header>
<!-- Main content area -->
<main id="app-main" class="app-main">
<!-- Content will be rendered here by modules -->
</main>
</div>
</div>
<!-- Application Bootstrap -->
<script type="module">
// Import and start the application
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;
let currentChapterId = null;
// Also expose globally for progress tracking
window.currentBookId = null;
window.currentChapterId = null;
// Initialize ContentLoader and make it global
const contentLoader = new ContentLoader();
window.contentLoader = contentLoader;
// Smart Preview Orchestrator will be initialized automatically by Application.js
// Wait for both DOM and application to be ready
let appReady = false;
let domReady = false;
function checkReadiness() {
if (appReady && domReady) {
setupEventListeners();
}
}
// DOM ready check
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
domReady = true;
checkReadiness();
});
} else {
domReady = true;
}
// App ready check with retry
function waitForApp() {
try {
// Try to access EventBus - if this works, app is ready
const { eventBus } = app.getCore();
if (eventBus && eventBus.getRegisteredModules().includes('Bootstrap')) {
appReady = true;
checkReadiness();
} else {
setTimeout(waitForApp, 50);
}
} catch (error) {
setTimeout(waitForApp, 50);
}
}
waitForApp();
function setupEventListeners() {
const { eventBus } = app.getCore();
// Hide tooltip on navigation
eventBus.on('router:route-changed', () => {
window.hideTooltip();
}, 'Bootstrap');
// Handle app ready event
eventBus.on('app:ready', async (event) => {
console.log('🎉 Application is ready!');
// Hide loading screen
const loadingScreen = document.getElementById('loading-screen');
const appContainer = document.getElementById('app-container');
if (loadingScreen) loadingScreen.style.display = 'none';
if (appContainer) appContainer.style.display = 'block';
// Smart Preview Orchestrator is automatically initialized by Application.js
}, 'Bootstrap');
// Handle navigation events
eventBus.on('navigation:home', () => {
updateMainContent(`
<div class="hero-container">
<div class="hero-background"></div>
<div class="hero-content">
<div class="hero-icon">🎓</div>
<h1 class="hero-title">Class Generator</h1>
<p class="hero-subtitle">Interactive Educational Learning Platform</p>
<p class="hero-description">
Develop your language skills with captivating and adaptive learning experiences
</p>
<div class="action-buttons">
<button class="btn btn-primary" onclick="navigateToBooks()">
<span class="btn-icon">📚</span>
<span class="btn-text">Explore Books</span>
</button>
</div>
<div class="secondary-buttons">
<button class="btn btn-outline" onclick="navigateToDynamicRevision()">
<span class="btn-icon">🧠</span>
<span class="btn-text">Dynamic Revision System</span>
</button>
<button class="btn btn-outline" onclick="openPlaceholder()">
<span class="btn-icon">🎮</span>
<span class="btn-text">Quick Play</span>
</button>
<button class="btn btn-outline" onclick="openSettings()">
<span class="btn-icon">⚙️</span>
<span class="btn-text">Settings</span>
</button>
</div>
</div>
</div>
`);
}, 'Bootstrap');
eventBus.on('navigation:books', async () => {
try {
const books = await apiService.getBooks();
const languages = [...new Set(books.map(book => book.language))];
const languageOptions = languages.map(lang =>
`<option value="${lang}">${lang === 'en-US' ? 'English' : lang}</option>`
).join('');
const booksHTML = books.map(book => {
const bookCover = book.id === 'sbs'
? `<img src="assets/SBSBook.jpg" alt="${book.name} Book Cover" />`
: `<div class="book-cover-placeholder">📚</div>`;
return `
<div class="book-card" data-language="${book.language}" onclick="openBook('${book.id}')">
<div class="book-cover">
${bookCover}
</div>
<div class="book-info">
<h3>${book.name}</h3>
<p class="book-language">${book.language === 'en-US' ? 'English' : book.language}</p>
<p class="book-description">${book.description}</p>
<span class="book-difficulty">${book.difficulty}</span>
</div>
</div>
`;
}).join('');
updateMainContent(`
<div class="books-container">
<div class="books-header">
<button class="btn btn-outline btn-sm" onclick="navigateToHome()">
<span class="btn-icon">🏠</span>
<span class="btn-text">Back to Home</span>
</button>
<h2>Available Books</h2>
<div class="language-selector">
<label for="language-select">Language:</label>
<select id="language-select" class="form-select" onchange="filterBooksByLanguage(this.value)">
<option value="all">All Languages</option>
${languageOptions}
</select>
</div>
</div>
<div class="books-grid" id="books-grid">
${booksHTML}
</div>
</div>
`);
} catch (error) {
console.error('Error loading books:', error);
updateMainContent(`
<div class="error-message">
<h2>Error Loading Books</h2>
<p>Could not load available books. Please try again.</p>
<button onclick="navigateToBooks()">Retry</button>
</div>
`);
}
}, 'Bootstrap');
eventBus.on('navigation:chapters', async (event) => {
const bookId = event.data.path.split('/')[2];
currentBookId = bookId;
window.currentBookId = bookId;
try {
// Load content using ContentLoader to get reports
const contentData = await contentLoader.loadContent(bookId);
// Generate chapter cards from contentData.chapters array
const chapters = contentData.chapters || [];
const chapterCards = chapters.map(chapter => `
<div class="chapter-card" onclick="selectChapter('${bookId}', '${chapter.id}')"
onmouseenter="showTooltip(event, '${chapter.id}')"
onmouseleave="hideTooltip()">
<div class="chapter-number">${chapter.chapter_number || '?'}</div>
<div class="chapter-info">
<h3>${chapter.name || chapter.title}</h3>
<p class="chapter-description">${chapter.description || ''}</p>
<div class="chapter-meta">
<span class="difficulty-badge difficulty-${chapter.difficulty}">${chapter.difficulty}</span>
<span class="language-badge">${chapter.language || contentData.language}</span>
</div>
<div class="chapter-progress">
<div class="progress">
<div class="progress-bar" style="width: 0%"></div>
</div>
<span class="progress-text">0% Complete</span>
</div>
</div>
</div>
`).join('');
updateMainContent(`
<div class="chapters-container">
<div class="chapters-header">
<button class="btn btn-outline btn-sm" onclick="navigateToHome()">
<span class="btn-icon">🏠</span>
<span class="btn-text">Back to Home</span>
</button>
<button class="btn btn-outline btn-sm" onclick="navigateToBooks()">
<span class="btn-icon">←</span>
<span class="btn-text">Back to Books</span>
</button>
<h2>${contentData.name}</h2>
</div>
<div class="chapters-grid">
${chapterCards || '<p>No chapters available</p>'}
</div>
</div>
`);
} catch (error) {
console.error('Error loading chapter data:', error);
updateMainContent(`
<div class="chapters-container">
<div class="chapters-header">
<button class="btn btn-outline btn-sm" onclick="navigateToBooks()">
<span class="btn-icon">←</span>
<span class="btn-text">Back to Books</span>
</button>
<h2>Chapters</h2>
</div>
<div class="error-message">
<p>Error loading chapter data. Please try again.</p>
</div>
</div>
`);
}
}, 'Bootstrap');
eventBus.on('navigation:games', async (event) => {
const chapterId = event.data.path.split('/')[2];
currentChapterId = chapterId;
try {
// Get GameLoader instance
const gameLoader = app.getModule('gameLoader');
if (!gameLoader) {
throw new Error('GameLoader not available');
}
// Load CHAPTER content (not book) for compatibility scoring
// Use module-based ContentLoader which loads chapter JSON
const moduleContentLoader = app.getModule('contentLoader');
if (moduleContentLoader) {
const chapterContent = await moduleContentLoader.getContent(chapterId);
if (chapterContent) {
// Module ContentLoader already emits content:loaded, no need to emit again
// Just wait for it to be processed
setTimeout(() => {
window._renderGamesInterface(gameLoader, chapterId);
}, 50);
return;
}
}
// If no module content loader or content, render immediately
window._renderGamesInterface(gameLoader, chapterId);
} catch (error) {
console.error('Error loading games:', error);
updateMainContent(`
<div class="games-container">
<div class="games-header">
<button class="btn btn-outline btn-sm" onclick="navigateToChapters('${chapterId}')">
<span class="btn-icon">←</span>
<span class="btn-text">Back to Chapters</span>
</button>
<h2>Games</h2>
</div>
<div class="error-message">
<p>Error loading games. Please try again.</p>
</div>
</div>
`);
}
}, 'Bootstrap');
// Helper function to render games interface
window._renderGamesInterface = function(gameLoader, chapterId) {
// Get compatible games
const availableGames = gameLoader.getAvailableGames();
console.log('🎮 Available games:', availableGames);
const compatibleGames = gameLoader.getCompatibleGames(0.1); // Low threshold to show most games
console.log('✅ Compatible games:', compatibleGames);
updateMainContent(`
<div class="games-container">
<div class="games-header">
<button class="btn btn-outline btn-sm" onclick="navigateToChapters('${chapterId}')">
<span class="btn-icon">←</span>
<span class="btn-text">Back to Chapters</span>
</button>
<h2>${chapterId} - Games</h2>
<p class="games-subtitle">${compatibleGames.length} compatible games found</p>
</div>
${compatibleGames.length > 0 ? `
<div class="games-grid">
${compatibleGames.map(game => `
<div class="game-card ${game.compatibility.score < 0.3 ? 'low-compatibility' : ''}"
onclick="launchGame('${game.id}')">
<div class="game-icon">🎮</div>
<div class="game-info">
<h3>${game.metadata.name}</h3>
<p class="game-description">${game.metadata.description}</p>
<div class="game-meta">
${typeof game.metadata.difficulty === 'string' ? `
<span class="difficulty-badge difficulty-${game.metadata.difficulty}">
${game.metadata.difficulty}
</span>
` : ''}
<span class="compatibility-score ${game.compatibility.score >= 0.7 ? 'high' : game.compatibility.score >= 0.3 ? 'medium' : 'low'}">
${Math.round(game.compatibility.score * 100)}% compatible
</span>
</div>
<div class="compatibility-details">
${game.compatibility.reason}
</div>
</div>
</div>
`).join('')}
</div>
` : `
<div class="no-games-container">
<div class="no-games-icon">🎮</div>
<h3>No Compatible Games</h3>
<p>No games are compatible with the current content.</p>
<div class="suggestions">
<p>Games typically require:</p>
<ul>
<li>At least 8 vocabulary words for Memory Match</li>
<li>Proper content structure</li>
<li>Translation data</li>
</ul>
</div>
</div>
`}
</div>
`);
};
// Handle dynamic revision navigation
eventBus.on('navigation:dynamic-revision', async () => {
try {
// Load books using ContentLoader
await contentLoader.loadBooks();
const books = contentLoader.getBooks();
updateMainContent(`
<div class="dynamic-revision-container">
<div class="revision-header">
<button class="btn btn-outline btn-sm" onclick="navigateToHome()">
<span class="btn-icon">🏠</span>
<span class="btn-text">Back to Home</span>
</button>
<h2>🧠 Dynamic Revision System</h2>
<p class="revision-subtitle">Intelligent course preparation with adaptive exercises</p>
</div>
<div class="revision-controls">
<div class="dropdown-container">
<div class="dropdown-group">
<label for="book-select">Select Book:</label>
<select id="book-select" class="form-select" onchange="onBookSelected(this.value)">
<option value="">Choose a book...</option>
${books.map(book =>
`<option value="${book.id}">${book.name}</option>`
).join('')}
</select>
</div>
<div class="dropdown-group">
<label for="chapter-select">Select Chapter:</label>
<select id="chapter-select" class="form-select" disabled onchange="onChapterSelected(this.value)">
<option value="">Choose a chapter...</option>
</select>
</div>
</div>
<div class="revision-start">
<button id="smart-guide-btn" class="btn btn-primary btn-lg btn-disabled" disabled onclick="startSmartGuide()">
<span class="btn-icon">🧠</span>
<span class="btn-text">Start Smart Guide</span>
</button>
</div>
<div class="manual-mode">
<details class="manual-controls">
<summary>Advanced Manual Mode</summary>
<div class="manual-content">
<div class="exercise-type-selection">
<h5>Exercise Type:</h5>
<div class="exercise-types">
<button class="exercise-type-btn active" data-type="text">
<span class="type-icon">📚</span>
<span class="type-name">Reading</span>
</button>
<button class="exercise-type-btn" data-type="audio">
<span class="type-icon">🎵</span>
<span class="type-name">Audio</span>
</button>
<button class="exercise-type-btn" data-type="image">
<span class="type-icon">🖼️</span>
<span class="type-name">Image</span>
</button>
<button class="exercise-type-btn" data-type="grammar">
<span class="type-icon">📝</span>
<span class="type-name">Grammar</span>
</button>
</div>
</div>
<div class="difficulty-selection">
<h5>Difficulty:</h5>
<div class="difficulty-buttons">
<button class="difficulty-btn" data-difficulty="easy">Easy</button>
<button class="difficulty-btn active" data-difficulty="medium">Medium</button>
<button class="difficulty-btn" data-difficulty="hard">Hard</button>
</div>
</div>
<button id="start-revision-btn" class="btn btn-outline btn-disabled" disabled onclick="startRevision()">
<span class="btn-icon">🚀</span>
<span class="btn-text">Start Manual Revision</span>
</button>
</div>
</details>
</div>
</div>
<div class="revision-viewport">
<div class="viewport-placeholder">
<div class="placeholder-icon">🧠</div>
<h3>Smart Preview Exercises</h3>
<p>Select a book and chapter to see available exercises</p>
<div class="exercise-types-preview">
<div class="exercise-type">📚 Vocabulary (groups of 5)</div>
<div class="exercise-type">💬 Phrase comprehension</div>
<div class="exercise-type">📖 Text processing</div>
<div class="exercise-type">🎵 Audio comprehension</div>
<div class="exercise-type">🖼️ Image description</div>
<div class="exercise-type">📝 Grammar exercises</div>
</div>
</div>
</div>
</div>
`);
// After loading the interface, try to load last used selection
await loadLastUsedSelection();
// Setup exercise type selection handlers
setupExerciseTypeHandlers();
setupDifficultyHandlers();
} catch (error) {
console.error('Error loading dynamic revision:', error);
updateMainContent(`
<div class="dynamic-revision-container">
<div class="revision-header">
<button class="btn btn-outline btn-sm" onclick="navigateToHome()">
<span class="btn-icon">🏠</span>
<span class="btn-text">Back to Home</span>
</button>
<h2>Dynamic Revision System</h2>
</div>
<div class="error-message">
<p>Error loading revision system. Please try again.</p>
</div>
</div>
`);
}
}, 'Bootstrap');
// Handle errors
eventBus.on('app:error', (event) => {
console.error('Application error:', event.data);
showError('An error occurred. Check console for details.');
}, 'Bootstrap');
// Set up keyboard shortcuts after app is ready
setupKeyboardShortcuts();
}
// Tooltip functions - Make them global
window.showTooltip = function(event, bookId) {
// Remove any existing tooltip
window.hideTooltip();
// Get tooltip content from ContentLoader
const tooltipHTML = contentLoader.generateTooltipHTML(bookId);
// Create tooltip element
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = tooltipHTML;
// Position au centre de l'écran - simple et efficace
tooltip.style.position = 'fixed';
tooltip.style.left = '50%';
tooltip.style.top = '50%';
tooltip.style.transform = 'translate(-50%, -50%)';
tooltip.style.zIndex = '1000';
tooltip.style.maxWidth = '400px';
// Add to DOM
document.body.appendChild(tooltip);
};
window.hideTooltip = function() {
const existingTooltip = document.querySelector('.tooltip');
if (existingTooltip) {
existingTooltip.remove();
}
};
// Utility functions
function updateMainContent(html) {
const mainElement = document.getElementById('app-main');
if (mainElement) {
mainElement.innerHTML = html;
}
}
function showError(message) {
const mainElement = document.getElementById('app-main');
if (mainElement) {
mainElement.innerHTML = `
<div class="error-message">
<h2>Error</h2>
<p>${message}</p>
<button onclick="window.location.reload()">Reload Page</button>
</div>
`;
}
}
function setupKeyboardShortcuts() {
document.addEventListener('keydown', (event) => {
try {
// ESC to go back
if (event.key === 'Escape') {
const { router } = app.getCore();
if (router && !router.isCurrentRoute('/')) {
router.back();
}
}
} catch (error) {
// Ignore errors in keyboard shortcuts to prevent crashes
console.log('Keyboard shortcut error (safe to ignore):', error.message);
}
});
}
// Connection status monitoring
function updateConnectionStatus() {
const statusIndicator = document.querySelector('.status-indicator');
const statusText = document.querySelector('.status-text');
if (navigator.onLine) {
if (statusIndicator) statusIndicator.className = 'status-indicator online';
if (statusText) statusText.textContent = 'Online';
} else {
if (statusIndicator) statusIndicator.className = 'status-indicator offline';
if (statusText) statusText.textContent = 'Offline';
}
}
// Monitor connection changes
window.addEventListener('online', updateConnectionStatus);
window.addEventListener('offline', updateConnectionStatus);
updateConnectionStatus(); // Initial check
// Export app globally for console access
window.app = app;
// Global navigation functions for buttons
window.navigateToHome = function() {
try {
const { router } = app.getCore();
router.navigate('/');
} catch (error) {
console.error('Navigation error:', error);
}
};
window.navigateToBooks = function() {
try {
const { router } = app.getCore();
router.navigate('/books');
} catch (error) {
console.error('Navigation error:', error);
}
};
window.navigateToChapters = function(chapterIdOrBookId) {
try {
const { router } = app.getCore();
// If a chapterId is passed (e.g., "sbs-7-8"), extract the bookId from it
// If a bookId is passed (e.g., "sbs"), use it directly
let bookId = chapterIdOrBookId;
// Check if window.currentBookId is already set (most reliable)
if (window.currentBookId) {
bookId = window.currentBookId;
} else if (chapterIdOrBookId && chapterIdOrBookId.includes('-')) {
// Extract bookId from chapterId (e.g., "sbs-7-8" -> "sbs")
const parts = chapterIdOrBookId.split('-');
bookId = parts[0];
}
console.log(`📚 Navigating to chapters for book: ${bookId}`);
router.navigate(`/chapters/${bookId || ''}`);
} catch (error) {
console.error('Navigation error:', error);
}
};
window.navigateToGames = function(chapterId) {
try {
const { router } = app.getCore();
router.navigate(`/games/${chapterId || ''}`);
} catch (error) {
console.error('Navigation error:', error);
}
};
window.selectChapter = function(bookId, chapterId) {
try {
// Set global chapter ID for Games/Smart Guide
window.currentBookId = bookId;
window.currentChapterId = chapterId;
console.log(`✅ Chapter selected: ${bookId}/${chapterId}`);
// Navigate to games for this chapter
const { router } = app.getCore();
router.navigate(`/games/${chapterId}`);
} catch (error) {
console.error('Chapter selection error:', error);
}
};
window.openPlaceholder = function() {
alert('Quick Play feature coming soon!');
};
window.openSettings = function() {
try {
const { router } = app.getCore();
if (router) {
router.navigate('/settings');
} else {
console.error('Router not available');
}
} catch (error) {
console.error('Error opening settings:', error);
}
};
window.navigateToDynamicRevision = function() {
try {
const { router } = app.getCore();
router.navigate('/dynamic-revision');
} catch (error) {
console.error('Navigation error:', error);
alert('Dynamic Revision System coming soon!');
}
};
window.filterBooksByLanguage = function(language) {
const books = document.querySelectorAll('.book-card');
books.forEach(book => {
const bookLanguage = book.getAttribute('data-language');
if (language === 'all' || bookLanguage === language) {
book.style.display = 'block';
} else {
book.style.display = 'none';
}
});
};
window.openBook = function(bookId) {
try {
const { router } = app.getCore();
router.navigate(`/chapters/${bookId}`);
} catch (error) {
console.error('Navigation error:', error);
}
};
window.launchGame = async function(gameId) {
try {
console.log('🎮 Launching game:', gameId);
// Get GameLoader instance
const gameLoader = app.getModule('gameLoader');
if (!gameLoader) {
throw new Error('GameLoader not available');
}
// Get current content using module ContentLoader (async)
const moduleContentLoader = app.getModule('contentLoader');
if (!moduleContentLoader) {
throw new Error('ContentLoader not available');
}
// Load chapter content asynchronously
const content = await moduleContentLoader.getContent(currentChapterId || window.currentChapterId);
if (!content) {
throw new Error('No content loaded for current chapter');
}
// Prepare game container
updateMainContent(`
<div class="game-container">
<div class="game-header">
<button class="btn btn-outline btn-sm" onclick="exitGame()">
<span class="btn-icon">←</span>
<span class="btn-text">Exit Game</span>
</button>
<h2>${gameId.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</h2>
<div class="game-loading">
<div class="loading-spinner">⚡</div>
<span>Loading game...</span>
</div>
</div>
<div id="game-area" class="game-area">
<!-- Le jeu sera rendu ici -->
</div>
</div>
`);
// Launch game through GameLoader
const instanceId = await gameLoader.launchGame(gameId, {
container: document.getElementById('game-area'),
config: {
container: document.getElementById('game-area')
},
dependencies: {
content: content
}
});
// Store instance ID for cleanup
window.currentGameInstance = instanceId;
// Hide loading indicator
const loadingElement = document.querySelector('.game-loading');
if (loadingElement) {
loadingElement.style.display = 'none';
}
console.log('✅ Game launched successfully:', instanceId);
} catch (error) {
console.error('Error launching game:', error);
updateMainContent(`
<div class="game-container">
<div class="game-header">
<button class="btn btn-outline btn-sm" onclick="navigateToGames(currentChapterId || 'unknown')">
<span class="btn-icon">←</span>
<span class="btn-text">Back to Games</span>
</button>
<h2>Game Error</h2>
</div>
<div class="error-message">
<p>Error loading game: ${gameId}</p>
<p>Error: ${error.message}</p>
</div>
</div>
`);
}
};
window.exitGame = function() {
try {
// Clean up current game instance
if (window.currentGameInstance) {
const gameLoader = app.getModule('gameLoader');
if (gameLoader) {
gameLoader.exitGame(window.currentGameInstance);
}
window.currentGameInstance = null;
}
// Navigate back to games
const { router } = app.getCore();
router.navigate(`/games/${currentChapterId || 'unknown'}`);
} catch (error) {
console.error('Navigation error:', error);
}
};
// Dynamic Revision System Functions
window.onBookSelected = async function(bookId) {
const chapterSelect = document.getElementById('chapter-select');
const startBtn = document.getElementById('start-revision-btn');
if (!bookId) {
chapterSelect.disabled = true;
chapterSelect.innerHTML = '<option value="">Choose a chapter...</option>';
startBtn.disabled = true;
startBtn.classList.add('btn-disabled');
return;
}
try {
// Load chapters using ContentLoader
const chapters = contentLoader.getBookChapters(bookId);
chapterSelect.disabled = false;
chapterSelect.innerHTML = `
<option value="">Choose a chapter...</option>
${chapters.map(chapter =>
`<option value="${chapter.id}">${chapter.name} (${chapter.chapter_number})</option>`
).join('')}
`;
console.log(`📚 Loaded ${chapters.length} chapters for book ${bookId}`);
} catch (error) {
console.error('Error loading chapters:', error);
chapterSelect.innerHTML = '<option value="">Error loading chapters</option>';
}
};
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) {
startBtn.disabled = true;
startBtn.classList.add('btn-disabled');
}
if (smartGuideBtn) {
smartGuideBtn.disabled = true;
smartGuideBtn.classList.add('btn-disabled');
}
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;
smartGuideBtn.classList.remove('btn-disabled');
}
// Manual revision is secondary
if (startBtn) {
startBtn.disabled = false;
startBtn.classList.remove('btn-disabled');
}
try {
// Load chapter content directly from APIService
const chapterData = await apiService.loadChapter(chapterId);
const stats = chapterData.statistics || {};
// Load progress information
const bookSelect = document.getElementById('book-select');
const bookId = bookSelect.value;
const progress = await getChapterProgress(bookId, chapterId);
const isCompleted = progress.completed;
const masteryCount = progress.masteryCount || 0;
// Count mastered items (handle both old string format and new object format)
const countMasteredItems = (items) => items ? items.length : 0;
const getLatestActivity = (items) => {
if (!items || items.length === 0) return null;
const latestEntry = items
.filter(entry => typeof entry === 'object' && entry.masteredAt)
.sort((a, b) => new Date(b.masteredAt) - new Date(a.masteredAt))[0];
return latestEntry ? latestEntry.masteredAt : null;
};
const vocabCount = countMasteredItems(progress.masteredVocabulary);
const phraseCount = countMasteredItems(progress.masteredPhrases);
const totalMastered = vocabCount + phraseCount;
const lastActivity = getLatestActivity([
...(progress.masteredVocabulary || []),
...(progress.masteredPhrases || [])
]);
// Create mastery badge and reset button
const masteryBadge = masteryCount > 0 ? `<span class="mastery-badge">🏆 Mastered ${masteryCount}x</span>` : '';
const completionBadge = isCompleted ? `<span class="completion-badge">✅ Completed</span>` : '';
const resetButton = isCompleted ? `
<button class="btn btn-warning btn-sm" onclick="resetChapterProgressUI('${bookId}', '${chapterId}')">
<span class="btn-icon">🔄</span>
<span class="btn-text">Reset & Master Again</span>
</button>
` : '';
// Update viewport with chapter preview and actual statistics
const viewport = document.querySelector('.revision-viewport');
if (viewport) {
viewport.innerHTML = `
<div class="chapter-preview">
<div class="preview-header">
<div class="chapter-title-row">
<h3>📚 Chapter: ${chapterData.name || chapterId}</h3>
<div class="chapter-badges">
${completionBadge}
${masteryBadge}
</div>
</div>
<p>Ready to start intelligent revision</p>
<div class="chapter-stats">
<span class="stat">📚 ${stats.vocabulary_count || 0} words (${vocabCount} mastered)</span>
<span class="stat">💬 ${stats.phrases_count || 0} phrases (${phraseCount} mastered)</span>
<span class="stat">📖 ${stats.dialogs_count || 0} dialogs</span>
<span class="stat">⏱️ ~${stats.estimated_completion_time || 25}min</span>
<span class="stat">📈 Total: ${totalMastered} items mastered</span>
${lastActivity ? `<span class="stat">🕐 Last activity: ${new Date(lastActivity).toLocaleDateString()}</span>` : ''}
</div>
<div class="chapter-actions">
${resetButton}
<button class="btn btn-outline btn-sm" onclick="showDataSyncUI('${bookId}', '${chapterId}')">
<span class="btn-icon">🔄</span>
<span class="btn-text">Sync Data</span>
</button>
<button class="btn btn-outline btn-sm" onclick="exportProgressData('${bookId}', '${chapterId}')">
<span class="btn-icon">💾</span>
<span class="btn-text">Export</span>
</button>
</div>
</div>
<div class="exercise-preview-grid">
<div class="exercise-preview-card">
<div class="exercise-icon">📚</div>
<h4>Vocabulary</h4>
<p>${stats.vocabulary_count || 0} words in groups of 5</p>
</div>
<div class="exercise-preview-card">
<div class="exercise-icon">💬</div>
<h4>Phrases</h4>
<p>${stats.phrases_count || 0} phrases with context</p>
</div>
<div class="exercise-preview-card">
<div class="exercise-icon">📖</div>
<h4>Texts</h4>
<p>${stats.dialogs_count || 0} dialogs sentence-by-sentence</p>
</div>
<div class="exercise-preview-card">
<div class="exercise-icon">🎵</div>
<h4>Audio</h4>
<p>Listening comprehension</p>
</div>
<div class="exercise-preview-card">
<div class="exercise-icon">🖼️</div>
<h4>Images</h4>
<p>Description exercises</p>
</div>
<div class="exercise-preview-card">
<div class="exercise-icon">📝</div>
<h4>Grammar</h4>
<p>Construction and validation</p>
</div>
</div>
</div>
`;
}
console.log(`📖 Loaded chapter data for ${chapterId}:`, stats);
} catch (error) {
console.error('Error loading chapter data:', error);
// Fallback to basic preview
const viewport = document.querySelector('.revision-viewport');
if (viewport) {
viewport.innerHTML = `
<div class="chapter-preview">
<div class="preview-header">
<h3>📚 Chapter: ${chapterId}</h3>
<p>Ready to start intelligent revision</p>
<div class="error-message">Could not load chapter statistics</div>
</div>
</div>
`;
}
}
};
window.startRevision = async function() {
const bookSelect = document.getElementById('book-select');
const chapterSelect = document.getElementById('chapter-select');
const bookId = bookSelect.value;
const chapterId = chapterSelect.value;
if (!bookId || !chapterId) {
alert('Please select both a book and a chapter first.');
return;
}
// Get selected exercise type and difficulty
const selectedTypeBtn = document.querySelector('.exercise-type-btn.active');
const selectedDifficultyBtn = document.querySelector('.difficulty-btn.active');
const exerciseType = selectedTypeBtn ? selectedTypeBtn.dataset.type : 'text';
const difficulty = selectedDifficultyBtn ? selectedDifficultyBtn.dataset.difficulty : 'medium';
// Get UnifiedDRS from the application
const moduleLoader = window.app.getCore().moduleLoader;
const unifiedDRS = moduleLoader.getModule('unifiedDRS');
if (!unifiedDRS) {
alert('UnifiedDRS is not initialized. Please refresh the page.');
return;
}
// Show loading state
const viewport = document.querySelector('.revision-viewport');
if (viewport) {
viewport.innerHTML = `
<div class="revision-active">
<div class="revision-status">
<h3>🚀 Starting Dynamic Revision...</h3>
<p><strong>Book:</strong> ${bookSelect.options[bookSelect.selectedIndex].text}</p>
<p><strong>Chapter:</strong> ${chapterSelect.options[chapterSelect.selectedIndex].text}</p>
<p><strong>Exercise:</strong> ${exerciseType.charAt(0).toUpperCase() + exerciseType.slice(1)} (${difficulty})</p>
<div class="loading-spinner">🧠</div>
<p>Initializing Unified DRS System...</p>
</div>
<div id="drs-exercise-container">
<!-- Exercise modules will be loaded here -->
</div>
</div>
`;
}
try {
// Save the selection as last used
saveLastUsedSelection(bookId, chapterId);
// Start revision session with UnifiedDRS
const exerciseContainer = document.getElementById('drs-exercise-container');
console.log(`🎯 Starting ${exerciseType} exercise (${difficulty}) for ${bookId}/${chapterId}`);
await unifiedDRS.start(exerciseContainer, {
type: exerciseType,
difficulty: difficulty,
bookId: bookId,
chapterId: chapterId,
context: 'dynamic-revision'
});
// Hide loading, show exercise container
const loadingDiv = viewport.querySelector('.revision-status');
if (loadingDiv) {
loadingDiv.style.display = 'none';
}
console.log('✅ Dynamic Revision Session started with UnifiedDRS');
} catch (error) {
console.error('❌ Error starting revision session:', error);
// Show error state
if (viewport) {
viewport.innerHTML = `
<div class="revision-error">
<div class="error-icon">❌</div>
<h3>Failed to Start Revision Session</h3>
<p><strong>Error:</strong> ${error.message}</p>
<p><strong>Type:</strong> ${exerciseType} (${difficulty})</p>
<button class="btn btn-outline" onclick="location.reload()">
<span class="btn-icon">🔄</span>
<span class="btn-text">Reload Page</span>
</button>
</div>
`;
}
}
};
// Exercise type and difficulty selection handlers
window.setupExerciseTypeHandlers = function() {
const exerciseTypeButtons = document.querySelectorAll('.exercise-type-btn');
exerciseTypeButtons.forEach(btn => {
btn.addEventListener('click', () => {
// Remove active class from all buttons
exerciseTypeButtons.forEach(b => b.classList.remove('active'));
// Add active class to clicked button
btn.classList.add('active');
console.log(`📋 Selected exercise type: ${btn.dataset.type}`);
});
});
};
window.setupDifficultyHandlers = function() {
const difficultyButtons = document.querySelectorAll('.difficulty-btn');
difficultyButtons.forEach(btn => {
btn.addEventListener('click', () => {
// Remove active class from all buttons
difficultyButtons.forEach(b => b.classList.remove('active'));
// Add active class to clicked button
btn.classList.add('active');
console.log(`🎯 Selected difficulty: ${btn.dataset.difficulty}`);
});
});
};
// Last Use functionality for DRS
window.saveLastUsedSelection = function(bookId, chapterId) {
const lastUse = {
bookId: bookId,
chapterId: chapterId,
timestamp: new Date().toISOString()
};
localStorage.setItem('drs-last-use', JSON.stringify(lastUse));
console.log('💾 Saved last used selection:', lastUse);
};
window.getLastUsedSelection = function() {
try {
const lastUse = localStorage.getItem('drs-last-use');
return lastUse ? JSON.parse(lastUse) : null;
} catch (error) {
console.error('Error loading last use:', error);
return null;
}
};
// Test timestamped progress tracking
window.testTimestamps = async function() {
console.log('🕐 Testing timestamped progress tracking...');
try {
const bookId = 'test-book';
const chapterId = 'test-chapter';
// Test adding items with timestamps and metadata
console.log('📤 Adding vocabulary item...');
await addMasteredItem(bookId, chapterId, 'vocabulary', 'hello', {
difficulty: 'easy',
responseTime: 1200
});
console.log('📤 Adding phrase item...');
await addMasteredItem(bookId, chapterId, 'phrases', 'how are you', {
difficulty: 'medium',
aiScore: 95,
responseTime: 3400
});
// Test updating existing item
console.log('🔄 Re-adding same vocabulary (should update)...');
await addMasteredItem(bookId, chapterId, 'vocabulary', 'hello', {
difficulty: 'easy',
responseTime: 800
});
// Load and display
console.log('📥 Loading timestamped progress...');
const progress = await getChapterProgress(bookId, chapterId);
console.log('✅ Timestamped progress loaded:', progress);
// Clean up
setTimeout(async () => {
try {
await fetch('/api/progress/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
system: 'drs',
bookId,
chapterId,
progressData: { masteredVocabulary: [], masteredPhrases: [], masteredGrammar: [] }
})
});
console.log('🧹 Test data cleaned');
} catch (e) { console.log('Clean up failed:', e); }
}, 5000);
return true;
} catch (error) {
console.error('❌ Timestamp test failed:', error);
return false;
}
};
// Test progress API functionality
window.testProgressAPI = async function() {
console.log('🧪 Testing Progress API...');
try {
// Test save
const testData = {
masteredVocabulary: ['hello', 'world', 'test'],
masteredPhrases: ['phrase1', 'phrase2'],
masteryCount: 3,
completed: true
};
console.log('📤 Testing save...');
await saveChapterProgress('test-book', 'test-chapter', testData);
// Test load
console.log('📥 Testing load...');
const loadedData = await getChapterProgress('test-book', 'test-chapter');
console.log('✅ Progress API test successful!', loadedData);
return true;
} catch (error) {
console.error('❌ Progress API test failed:', error);
return false;
}
};
// Test localStorage functionality
window.testLocalStorage = function() {
try {
// Test write
const testData = {
vocabulary: ['hello', 'world', 'test'],
phrases: ['phrase1', 'phrase2'],
masteryCount: 3,
completed: true,
timestamp: new Date().toISOString()
};
localStorage.setItem('drs-test', JSON.stringify(testData));
console.log('✅ localStorage WRITE successful:', testData);
// Test read
const readData = JSON.parse(localStorage.getItem('drs-test'));
console.log('✅ localStorage READ successful:', readData);
// Test existence check
const exists = localStorage.getItem('drs-test') !== null;
console.log('✅ localStorage EXISTS check:', exists);
// Cleanup
localStorage.removeItem('drs-test');
console.log('✅ localStorage DELETE successful');
return true;
} catch (error) {
console.error('❌ localStorage test failed:', error);
return false;
}
};
// DRS Progress Management Functions (Server-side storage)
window.saveChapterProgress = async function(bookId, chapterId, progressData) {
try {
const result = await apiService.saveProgress({
system: 'drs',
bookId,
chapterId,
progressData
});
console.log(`💾 Saved progress to file: ${result.filename}`, progressData);
return progressData;
} catch (error) {
console.error(`❌ Failed to save progress for ${bookId}/${chapterId}:`, error);
throw error;
}
};
window.getChapterProgress = async function(bookId, chapterId) {
try {
const progress = await apiService.loadDRSProgress(bookId, chapterId);
console.log(`📁 Loaded progress from file: ${bookId}/${chapterId}`, progress);
return progress;
} catch (error) {
console.error(`❌ Failed to load progress for ${bookId}/${chapterId}:`, error);
// Return empty progress on error
return {
masteredVocabulary: [],
masteredPhrases: [],
masteredGrammar: [],
completed: false,
masteryCount: 0,
system: 'drs',
bookId,
chapterId
};
}
};
window.isChapterCompleted = async function(bookId, chapterId) {
const progress = await getChapterProgress(bookId, chapterId);
return progress && progress.completed === true;
};
window.getChapterMasteryCount = async function(bookId, chapterId) {
const progress = await getChapterProgress(bookId, chapterId);
return progress ? progress.masteryCount || 0 : 0;
};
window.resetChapterProgress = async function(bookId, chapterId) {
const isCompleted = await isChapterCompleted(bookId, chapterId);
if (!isCompleted) {
throw new Error("Cannot reset: Chapter not completed yet");
}
const progress = await getChapterProgress(bookId, chapterId);
const resetProgress = {
masteredVocabulary: [],
masteredPhrases: [],
masteredGrammar: [],
completed: false,
masteryCount: (progress.masteryCount || 0) + 1,
resetAt: new Date().toISOString(),
createdAt: progress.createdAt,
previousCompletions: progress.previousCompletions || []
};
// Add completion to history
if (progress.completedAt) {
resetProgress.previousCompletions.push({
completedAt: progress.completedAt,
masteryNumber: progress.masteryCount || 0
});
}
await saveChapterProgress(bookId, chapterId, resetProgress);
console.log(`🔄 Reset progress for ${bookId}/${chapterId} - Mastery count: ${resetProgress.masteryCount}`);
return resetProgress;
};
window.markChapterCompleted = async function(bookId, chapterId) {
const progress = await getChapterProgress(bookId, chapterId);
const completedProgress = {
...progress,
completed: true,
completedAt: new Date().toISOString()
};
return await saveChapterProgress(bookId, chapterId, completedProgress);
};
window.addMasteredItem = async function(bookId, chapterId, type, item, metadata = {}) {
const progress = await getChapterProgress(bookId, chapterId);
const key = `mastered${type.charAt(0).toUpperCase() + type.slice(1)}`;
// Convert old format (strings) to new format (objects) if needed
if (!Array.isArray(progress[key])) {
progress[key] = [];
}
// Check if item exists (handle both old string format and new object format)
const existingItem = progress[key].find(entry => {
if (typeof entry === 'string') return entry === item;
return entry.item === item;
});
if (!existingItem) {
const masteredEntry = {
item: item,
masteredAt: new Date().toISOString(),
attempts: 1,
...metadata
};
progress[key].push(masteredEntry);
await saveChapterProgress(bookId, chapterId, progress);
console.log(`✅ Added ${type}: ${item} to ${bookId}/${chapterId} at ${masteredEntry.masteredAt}`);
} else if (typeof existingItem === 'object') {
// Update existing entry with new attempt
existingItem.lastReviewAt = new Date().toISOString();
existingItem.attempts = (existingItem.attempts || 1) + 1;
await saveChapterProgress(bookId, chapterId, progress);
console.log(`🔄 Updated ${type}: ${item} - attempt #${existingItem.attempts}`);
}
};
window.loadLastUsedSelection = async function() {
const lastUse = getLastUsedSelection();
if (!lastUse) {
console.log('📋 No last used selection found');
return;
}
console.log('🔄 Loading last used selection:', lastUse);
const bookSelect = document.getElementById('book-select');
const chapterSelect = document.getElementById('chapter-select');
if (!bookSelect || !chapterSelect) {
console.log('⚠️ Selection elements not found, skipping auto-load');
return;
}
// Set book selection
bookSelect.value = lastUse.bookId;
// Trigger book selection to load chapters
await onBookSelected(lastUse.bookId);
// Wait a moment for chapters to load, then set chapter
setTimeout(() => {
chapterSelect.value = lastUse.chapterId;
// Trigger chapter selection to load preview
onChapterSelected(lastUse.chapterId);
}, 100);
console.log('✅ Last used selection loaded successfully');
};
// Reset chapter progress with confirmation and UI update
window.resetChapterProgressUI = async function(bookId, chapterId) {
const progress = await getChapterProgress(bookId, chapterId);
if (!progress.completed) {
alert('This chapter is not completed yet. You can only reset completed chapters.');
return;
}
const confirmation = confirm(
`Are you sure you want to reset the progress for this chapter?\n\n` +
`This will:\n` +
`• Clear all mastered vocabulary (${progress.masteredVocabulary.length} items)\n` +
`• Clear all mastered phrases (${progress.masteredPhrases.length} items)\n` +
`• Increment mastery counter to ${(progress.masteryCount || 0) + 1}\n` +
`• Allow you to practice this chapter again\n\n` +
`This action cannot be undone.`
);
if (confirmation) {
try {
const resetResult = await resetChapterProgress(bookId, chapterId);
// Show success message
alert(`✅ Chapter progress reset successfully!\n\nMastery Count: ${resetResult.masteryCount}\nYou can now practice this chapter again.`);
// Refresh the chapter preview
await onChapterSelected(chapterId);
} catch (error) {
alert(`❌ Failed to reset progress: ${error.message}`);
console.error('Reset failed:', error);
}
}
};
// Data merge functions for combining local and external progress
window.mergeProgressData = async function(bookId, chapterId, externalData, mergeStrategy = 'timestamp') {
try {
console.log(`🔄 Merging progress data for ${bookId}/${chapterId} with strategy: ${mergeStrategy}`);
// Get current local data
const localData = await getChapterProgress(bookId, chapterId);
// Call merge API
const response = await fetch('/api/progress/merge', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
system: 'drs',
bookId,
chapterId,
localData,
externalData,
mergeStrategy
})
});
if (!response.ok) {
throw new Error(`Merge failed: ${response.statusText}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Merge operation failed');
}
console.log('✅ Data merge completed:', result.mergeInfo);
return result;
} catch (error) {
console.error('❌ Data merge failed:', error);
throw error;
}
};
window.getSyncStatus = async function() {
try {
const response = await fetch('/api/progress/sync-status');
if (!response.ok) {
throw new Error(`Sync status failed: ${response.statusText}`);
}
const status = await response.json();
console.log('📊 Sync status:', status);
return status;
} catch (error) {
console.error('❌ Sync status failed:', error);
throw error;
}
};
// Import external progress data (from file, URL, or text)
window.importExternalProgress = async function(bookId, chapterId, source, sourceData) {
try {
console.log(`📥 Importing external progress from ${source}`);
let externalData;
// Handle different source types
switch (source) {
case 'file':
// sourceData is File object
const fileContent = await sourceData.text();
externalData = JSON.parse(fileContent);
break;
case 'url':
// sourceData is URL string
const response = await fetch(sourceData);
if (!response.ok) throw new Error('Failed to fetch from URL');
externalData = await response.json();
break;
case 'text':
// sourceData is JSON string
externalData = JSON.parse(sourceData);
break;
default:
throw new Error('Invalid source type. Use: file, url, or text');
}
// Validate external data
if (!externalData || typeof externalData !== 'object') {
throw new Error('Invalid external data format');
}
// Show merge preview
const localData = await getChapterProgress(bookId, chapterId);
const mergePreview = {
local: {
vocab: localData.masteredVocabulary?.length || 0,
phrases: localData.masteredPhrases?.length || 0,
grammar: localData.masteredGrammar?.length || 0
},
external: {
vocab: externalData.masteredVocabulary?.length || 0,
phrases: externalData.masteredPhrases?.length || 0,
grammar: externalData.masteredGrammar?.length || 0
}
};
console.log('📋 Merge preview:', mergePreview);
// Confirm merge operation
const confirmation = confirm(
`Merge External Progress?\n\n` +
`Local Data:\n` +
`${mergePreview.local.vocab} vocabulary items\n` +
`${mergePreview.local.phrases} phrases\n` +
`${mergePreview.local.grammar} grammar concepts\n\n` +
`External Data:\n` +
`${mergePreview.external.vocab} vocabulary items\n` +
`${mergePreview.external.phrases} phrases\n` +
`${mergePreview.external.grammar} grammar concepts\n\n` +
`Merge strategy: Newest timestamps win\n` +
`Conflicts will be resolved automatically.\n\n` +
`Continue with merge?`
);
if (!confirmation) {
console.log('📋 Merge cancelled by user');
return { cancelled: true };
}
// Perform merge
const mergeResult = await mergeProgressData(bookId, chapterId, externalData, 'timestamp');
// Show merge results
const message = `✅ Merge Completed!\n\n` +
`Total Items: ${mergeResult.mergeInfo.totalItems}\n` +
`Conflicts Resolved: ${mergeResult.mergeInfo.conflicts.length}\n` +
`Merge Strategy: ${mergeResult.mergeInfo.strategy}\n\n` +
`Your progress has been updated with the merged data.`;
alert(message);
return mergeResult;
} catch (error) {
console.error('❌ Import failed:', error);
alert(`❌ Import Failed: ${error.message}`);
throw error;
}
};
// Export current progress for backup or sharing
window.exportProgressData = async function(bookId, chapterId) {
try {
const progressData = await getChapterProgress(bookId, chapterId);
// Add export metadata
const exportData = {
...progressData,
exportedAt: new Date().toISOString(),
exportVersion: '1.0',
source: 'class-generator-drs'
};
// Create downloadable file
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
// Create download link
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `drs-progress-${bookId}-${chapterId}.json`;
// Trigger download
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
console.log('✅ Progress exported successfully');
return exportData;
} catch (error) {
console.error('❌ Export failed:', error);
alert(`❌ Export Failed: ${error.message}`);
throw error;
}
};
// Show data sync UI modal
window.showDataSyncUI = async function(bookId, chapterId) {
try {
// Get sync status and current progress
const syncStatus = await getSyncStatus();
const currentProgress = await getChapterProgress(bookId, chapterId);
// Create modal HTML
const modalHTML = `
<div id="sync-modal" class="sync-modal" onclick="if(event.target === this) closeSyncModal()">
<div class="sync-modal-content">
<div class="sync-modal-header">
<h3>🔄 Data Synchronization</h3>
<button onclick="closeSyncModal()" class="close-btn">&times;</button>
</div>
<div class="sync-tabs">
<button class="sync-tab active" onclick="showSyncTab('import')">📥 Import</button>
<button class="sync-tab" onclick="showSyncTab('export')">📤 Export</button>
<button class="sync-tab" onclick="showSyncTab('status')">📊 Status</button>
</div>
<div id="import-tab" class="sync-tab-content">
<h4>Import External Progress</h4>
<p>Merge progress data from another device or backup:</p>
<div class="import-options">
<div class="import-option">
<h5>📁 From File</h5>
<input type="file" id="file-input" accept=".json" style="display: none;" onchange="handleFileImport(event, '${bookId}', '${chapterId}')">
<button class="btn btn-primary" onclick="document.getElementById('file-input').click()">
Choose File
</button>
</div>
<div class="import-option">
<h5>🌐 From URL</h5>
<input type="url" id="url-input" placeholder="https://example.com/progress.json" style="width: 100%; margin-bottom: 10px;">
<button class="btn btn-primary" onclick="handleUrlImport('${bookId}', '${chapterId}')">
Import from URL
</button>
</div>
<div class="import-option">
<h5>📝 From Text</h5>
<textarea id="text-input" placeholder="Paste JSON progress data here..." rows="4" style="width: 100%; margin-bottom: 10px;"></textarea>
<button class="btn btn-primary" onclick="handleTextImport('${bookId}', '${chapterId}')">
Import from Text
</button>
</div>
</div>
</div>
<div id="export-tab" class="sync-tab-content" style="display: none;">
<h4>Export Current Progress</h4>
<div class="current-progress-info">
<p><strong>Current Progress:</strong></p>
<ul>
<li>📚 ${currentProgress.masteredVocabulary?.length || 0} vocabulary items</li>
<li>💬 ${currentProgress.masteredPhrases?.length || 0} phrases</li>
<li>📝 ${currentProgress.masteredGrammar?.length || 0} grammar concepts</li>
<li>🏆 Mastery count: ${currentProgress.masteryCount || 0}</li>
</ul>
</div>
<button class="btn btn-primary" onclick="exportProgressData('${bookId}', '${chapterId}'); closeSyncModal();">
💾 Download Progress File
</button>
<div class="export-note">
<p><em>This will download a JSON file with all your progress data that can be imported on another device.</em></p>
</div>
</div>
<div id="status-tab" class="sync-tab-content" style="display: none;">
<h4>Synchronization Status</h4>
<div class="status-overview">
<p><strong>Local Files:</strong> ${syncStatus.totalFiles} saved progress files</p>
<p><strong>Last Activity:</strong> ${syncStatus.lastSync ? new Date(syncStatus.lastSync).toLocaleString() : 'Never'}</p>
</div>
<div class="saved-files-list">
<h5>Saved Progress Files:</h5>
${syncStatus.savedFiles.length > 0 ?
syncStatus.savedFiles.map(file => `
<div class="saved-file-item">
<div class="file-info">
<strong>${file.filename}</strong>
<span class="file-meta">
${file.itemCount} items |
${file.hasTimestamps ? '🕐 Timestamped' : '📝 Basic'} |
${new Date(file.savedAt).toLocaleDateString()}
</span>
</div>
</div>
`).join('') :
'<p><em>No saved files found</em></p>'
}
</div>
<button class="btn btn-secondary" onclick="testDataMerge()">
🧪 Test Merge Function
</button>
</div>
</div>
</div>
`;
// Add modal to page
document.body.insertAdjacentHTML('beforeend', modalHTML);
console.log('📱 Data sync UI opened');
} catch (error) {
console.error('❌ Failed to show sync UI:', error);
alert(`Failed to show sync interface: ${error.message}`);
}
};
// Close sync modal
window.closeSyncModal = function() {
const modal = document.getElementById('sync-modal');
if (modal) {
modal.remove();
}
};
// Switch sync tabs
window.showSyncTab = function(tabName) {
// Update tab buttons
document.querySelectorAll('.sync-tab').forEach(tab => {
tab.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.sync-tab-content').forEach(content => {
content.style.display = 'none';
});
document.getElementById(`${tabName}-tab`).style.display = 'block';
};
// Import handlers
window.handleFileImport = async function(event, bookId, chapterId) {
try {
const file = event.target.files[0];
if (!file) return;
const result = await importExternalProgress(bookId, chapterId, 'file', file);
if (!result.cancelled) {
closeSyncModal();
// Refresh chapter preview
await onChapterSelected(chapterId);
}
} catch (error) {
console.error('❌ File import failed:', error);
}
};
window.handleUrlImport = async function(bookId, chapterId) {
try {
const url = document.getElementById('url-input').value.trim();
if (!url) {
alert('Please enter a URL');
return;
}
const result = await importExternalProgress(bookId, chapterId, 'url', url);
if (!result.cancelled) {
closeSyncModal();
await onChapterSelected(chapterId);
}
} catch (error) {
console.error('❌ URL import failed:', error);
}
};
window.handleTextImport = async function(bookId, chapterId) {
try {
const text = document.getElementById('text-input').value.trim();
if (!text) {
alert('Please paste JSON data');
return;
}
const result = await importExternalProgress(bookId, chapterId, 'text', text);
if (!result.cancelled) {
closeSyncModal();
await onChapterSelected(chapterId);
}
} catch (error) {
console.error('❌ Text import failed:', error);
}
};
// Force Phrase Module (temporary testing)
window.forcePhraseModule = async function() {
try {
console.log('🔧 Forcing Phrase Module to load...');
// Get UnifiedDRS instance directly (not SmartPreviewOrchestrator)
const unifiedDRS = window.app?.getCore()?.moduleLoader?.getModule('unifiedDRS');
if (!unifiedDRS) {
console.error('❌ UnifiedDRS not found. Make sure DRS is initialized.');
console.log('Available modules:', Object.keys(window.app?.getCore()?.moduleLoader?._modules || {}));
return;
}
// Get current chapter content
const contentLoader = window.app?.getCore()?.moduleLoader?.getModule('contentLoader');
if (!contentLoader) {
console.error('❌ ContentLoader not found.');
return;
}
const chapterContent = await contentLoader.getContent(currentChapterId || 'test-heavy-stress');
// Check if chapter has phrases
if (!chapterContent?.phrases || Object.keys(chapterContent.phrases).length === 0) {
console.error('❌ No phrases found in current chapter.');
console.log('Available content types:', Object.keys(chapterContent || {}));
return;
}
// Get first phrase
const phraseKeys = Object.keys(chapterContent.phrases);
const firstPhraseKey = phraseKeys[0];
const firstPhrase = chapterContent.phrases[firstPhraseKey];
console.log(`✅ Found ${phraseKeys.length} phrases in chapter`);
console.log(`🎯 Loading first phrase: "${firstPhraseKey}"`);
// Create exercise config for phrase
const exerciseConfig = {
type: 'phrase',
phraseKey: firstPhraseKey,
phrase: {
id: firstPhraseKey,
english: firstPhraseKey,
user_language: firstPhrase.user_language,
context: firstPhrase.context,
pronunciation: firstPhrase.pronunciation
},
chapterContent: chapterContent,
metadata: {
forced: true,
testMode: true
}
};
console.log('📦 Exercise config:', exerciseConfig);
// Get the DRS container
const container = document.getElementById('drs-exercise-container');
if (!container) {
console.error('❌ DRS container not found');
return;
}
// Force start the exercise using UnifiedDRS.start()
await unifiedDRS.start(container, exerciseConfig);
console.log('✅ Phrase module loaded successfully!');
} catch (error) {
console.error('❌ Failed to force phrase module:', error);
console.error(error.stack);
}
};
// Test AI provider connectivity
window.testAIConnectivity = async function() {
try {
console.log('🔍 Testing AI provider connectivity...');
// Try to get the IAEngine instance from the DRS system
let iaEngine = null;
// Test if the DRS SmartPreviewOrchestrator has been created
if (window.drsOrchestrator && window.drsOrchestrator.llmValidator && window.drsOrchestrator.llmValidator.iaEngine) {
iaEngine = window.drsOrchestrator.llmValidator.iaEngine;
} else {
console.log('⚠️ DRS not initialized, creating temporary IAEngine for testing...');
// Dynamically import and create IAEngine for testing
const IAEngineModule = await import('./src/DRS/services/IAEngine.js');
const IAEngine = IAEngineModule.default;
iaEngine = new IAEngine({ debug: true });
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for API keys
}
// Test all providers
const results = await iaEngine.testAllProvidersConnectivity();
console.log('🧪 AI Connectivity Test Results:');
console.log('================================');
console.log(`AI System Status: ${results.aiDisabled ? '🚫 DISABLED' : '✅ ACTIVE'}`);
if (results.disableReason) {
console.log(`Disable Reason: ${results.disableReason}`);
}
console.log(`Available Providers: ${results.availableProviders.join(', ') || 'None'}`);
console.log('\nProvider Details:');
Object.entries(results.results).forEach(([provider, result]) => {
console.log(` ${provider.toUpperCase()}: ${result.success ? '✅ SUCCESS' : '❌ FAILED'} (${result.duration || 0}ms)`);
if (!result.success) {
console.log(` Error: ${result.error}`);
}
});
console.log('\nProvider Status:');
Object.entries(results.providerStatus).forEach(([provider, status]) => {
console.log(` ${provider.toUpperCase()}:`);
console.log(` Available: ${status.available !== null ? (status.available ? '✅ Yes' : '❌ No') : '❓ Unknown'}`);
console.log(` Last Tested: ${status.lastTested || 'Never'}`);
console.log(` Consecutive Failures: ${status.consecutiveFailures || 0}`);
if (status.lastError) {
console.log(` Last Error: ${status.lastError}`);
}
});
// Show user-friendly alert
const message = results.aiDisabled
? `🚫 AI System Disabled\n\nReason: ${results.disableReason}\n\nAll AI features will use mock mode.`
: `✅ AI System Active\n\nAvailable Providers:\n${results.availableProviders.map(p => `${p.toUpperCase()}`).join('\n')}\n\nAI validation will work normally.`;
alert(message);
return results;
} catch (error) {
console.error('❌ AI connectivity test failed:', error);
alert(`❌ AI Test Failed\n\nError: ${error.message}\n\nPlease check the console for more details.`);
throw error;
}
};
// Test data merge functionality
window.testDataMerge = async function() {
try {
const bookId = 'test-book';
const chapterId = 'test-chapter';
console.log('🧪 Testing data merge functionality...');
// Create test local data
await addMasteredItem(bookId, chapterId, 'vocabulary', 'local-word', {
sessionId: 'local-session',
difficulty: 'easy'
});
// Create test external data
const externalData = {
masteredVocabulary: [
{
item: 'external-word',
masteredAt: new Date().toISOString(),
attempts: 1,
sessionId: 'external-session',
difficulty: 'hard'
},
{
item: 'local-word', // Same word as local - will create conflict
masteredAt: new Date(Date.now() + 60000).toISOString(), // Newer timestamp
attempts: 2,
sessionId: 'external-session',
difficulty: 'good'
}
],
masteredPhrases: [
{
item: 'external phrase',
masteredAt: new Date().toISOString(),
attempts: 1,
moduleType: 'phrase'
}
],
masteredGrammar: [],
completed: false,
masteryCount: 0,
system: 'drs',
bookId,
chapterId
};
// Test merge
const mergeResult = await mergeProgressData(bookId, chapterId, externalData, 'timestamp');
console.log('✅ Data merge test completed:', mergeResult.mergeInfo);
alert(`✅ Data Merge Test Completed!\n\nTotal Items: ${mergeResult.mergeInfo.totalItems}\nConflicts: ${mergeResult.mergeInfo.conflicts.length}`);
return mergeResult;
} catch (error) {
console.error('❌ Data merge test failed:', error);
alert(`❌ Test Failed: ${error.message}`);
throw error;
}
};
// Smart Guide functionality - Intelligent exercise sequencing
let currentGuidedSession = null;
window.startSmartGuide = async function() {
const bookSelect = document.getElementById('book-select');
const chapterSelect = document.getElementById('chapter-select');
const bookId = bookSelect.value;
const chapterId = chapterSelect.value;
if (!bookId || !chapterId) {
alert('Please select both a book and a chapter first.');
return;
}
// Update global state for progress tracking
currentBookId = bookId;
currentChapterId = chapterId;
window.currentBookId = bookId;
window.currentChapterId = chapterId;
console.log(`📚 Smart Guide using: ${bookId}/${chapterId}`);
// Get modules from the application
const moduleLoader = window.app.getCore().moduleLoader;
// Load chapter data for progress tracking
const contentLoader = moduleLoader.getModule('contentLoader');
if (contentLoader) {
window.currentChapterData = await contentLoader.getContent(chapterId);
console.log(`📊 Chapter data loaded for progress tracking: ${Object.keys(window.currentChapterData.vocabulary || {}).length} words`);
}
// Get IntelligentSequencer
const intelligentSequencer = moduleLoader.getModule('intelligentSequencer');
if (!intelligentSequencer) {
alert('IntelligentSequencer is not initialized. Please refresh the page.');
return;
}
// Show Smart Guide mode in viewport
const viewport = document.querySelector('.revision-viewport');
if (viewport) {
viewport.innerHTML = `
<div class="smart-guide-active">
<div class="guide-header">
<div class="guide-title">
<h3>🧠 Smart Guide Active</h3>
<div class="guide-actions">
<button class="btn btn-info btn-sm" onclick="showVocabularyKnowledge()">
<span class="btn-icon">📖</span>
<span class="btn-text">Vocabulary Knowledge</span>
</button>
<button class="btn btn-warning btn-sm" onclick="stopSmartGuide()">
<span class="btn-icon">⏹️</span>
<span class="btn-text">Stop Guide</span>
</button>
</div>
</div>
<div class="guide-status" id="guide-status">
Analyzing performance and preparing intelligent sequence...
</div>
</div>
<div class="guide-controls">
<div class="session-progress" id="session-progress">
<div class="progress-bar-container">
<div class="progress-bar" id="guide-progress-bar" style="width: 0%"></div>
</div>
<div class="progress-text" id="guide-progress-text">Preparing session...</div>
</div>
</div>
<div class="current-exercise-info" id="current-exercise-info" style="display: none;">
<h4>Current Exercise</h4>
<div class="exercise-details" id="exercise-details"></div>
<div class="exercise-reasoning" id="exercise-reasoning"></div>
</div>
<div id="guide-exercise-container">
<!-- Exercise modules will be loaded here -->
</div>
<div class="guide-insights" id="guide-insights" style="display: none;">
<h4>🎯 Performance Insights</h4>
<div id="insights-content"></div>
</div>
</div>
`;
}
try {
// Start guided session
console.log('🧠 Starting Smart Guide session...');
currentGuidedSession = intelligentSequencer.startGuidedSession({
bookId: bookId,
chapterId: chapterId,
targetLength: 8 // Default 8 exercises per session
});
updateGuideStatus(`✅ Session started: ${currentGuidedSession.id}`);
updateProgressBar(0, currentGuidedSession.targetLength);
// Get first recommended exercise
const firstExercise = await intelligentSequencer.getNextExercise();
if (firstExercise) {
await startGuidedExercise(firstExercise);
} else {
// Session completed immediately (shouldn't happen)
completeGuidedSession();
}
} catch (error) {
console.error('❌ Error starting Smart Guide:', error);
updateGuideStatus(`❌ Error: ${error.message}`);
setTimeout(() => stopSmartGuide(), 3000);
}
};
window.stopSmartGuide = function() {
if (currentGuidedSession) {
const moduleLoader = window.app.getCore().moduleLoader;
const intelligentSequencer = moduleLoader.getModule('intelligentSequencer');
if (intelligentSequencer && intelligentSequencer.isGuiding()) {
intelligentSequencer.stopGuidedSession();
}
currentGuidedSession = null;
console.log('🛑 Smart Guide session stopped');
}
// Return to normal revision interface
const chapterSelect = document.getElementById('chapter-select');
if (chapterSelect && chapterSelect.value) {
onChapterSelected(chapterSelect.value);
}
};
// Helper function to get current book ID from chapter ID
window.getCurrentBookId = function() {
// Use the real currentBookId if available (set when loading chapters)
if (window.currentBookId) {
return window.currentBookId;
}
// Fallback: try to parse from chapterId (may be inaccurate for multi-part book IDs)
const chapterId = window.currentChapterId || 'sbs-7-8';
const parts = chapterId.split('-');
return parts[0] || 'sbs';
};
// Helper function to load persisted vocabulary data FROM UNIFIED DRS SYSTEM
window.loadPersistedVocabularyData = async function(chapterId) {
try {
const bookId = getCurrentBookId();
console.log(`📁 Loading unified vocabulary data for ${bookId}/${chapterId}`);
// Use unified VocabularyProgressManager
const VocabularyProgressManager = (await import('./src/DRS/services/VocabularyProgressManager.js')).default;
const progressManager = new VocabularyProgressManager();
// Get unified progress data (handles legacy conversion automatically)
const progressData = await progressManager.loadProgress(bookId, chapterId);
console.log('📚 Unified vocabulary progress loaded:', progressData);
// Extract words from unified system
const drsMasteredWords = Object.keys(progressData.mastered || {});
const drsDiscoveredWords = Object.keys(progressData.discovered || {});
console.log('📊 Unified vocabulary stats:', {
discovered: drsDiscoveredWords.length,
mastered: drsMasteredWords.length
});
// Return unified data only
return {
drsDiscovered: drsDiscoveredWords,
drsMastered: drsMasteredWords,
drsData: progressData,
// Keep legacy fields for compatibility
serverDiscovered: drsMasteredWords, // Legacy field mapped to mastered
serverData: progressData
};
} catch (error) {
console.warn('Failed to load unified vocabulary data:', error);
return {
drsDiscovered: [],
drsMastered: [],
drsData: {},
// Legacy fields
serverDiscovered: [],
serverData: {}
};
}
};
// Helper function to calculate combined vocabulary progress
window.calculateVocabularyProgress = async function(chapterId, persistedData, prerequisiteEngine) {
try {
console.log('🧮 Calculating enhanced vocabulary progress...');
// Get chapter content to know total vocabulary
const moduleLoader = window.app.getCore().moduleLoader;
const contentLoader = moduleLoader.getModule('contentLoader');
if (!contentLoader) {
throw new Error('ContentLoader not available');
}
const content = await contentLoader.getContent(chapterId);
if (!content?.vocabulary) {
throw new Error('No vocabulary content found');
}
const allWords = Object.keys(content.vocabulary);
const vocabCount = allWords.length;
// Use unified persistence data directly
const discoveredWords = persistedData.drsDiscovered || [];
const masteredWords = persistedData.drsMastered || [];
console.log('📊 Unified progress calculation:', {
total: vocabCount,
discovered: discoveredWords.length,
mastered: masteredWords.length
});
// Use unified data directly (no complex merging needed)
const combinedDiscovered = new Set([
...discoveredWords, // From unified persistence system
...masteredWords // Mastered words are also discovered
]);
const combinedMastered = new Set(masteredWords); // From unified persistence system
// Add current session data if prerequisiteEngine is available
if (prerequisiteEngine && prerequisiteEngine.isInitialized) {
allWords.forEach(word => {
if (prerequisiteEngine.isDiscovered(word)) {
combinedDiscovered.add(word);
}
if (prerequisiteEngine.isMastered(word)) {
combinedMastered.add(word);
}
});
console.log('📊 Added current session data to progress calculation');
}
// Count words that are in the current chapter vocabulary
const discoveredCount = allWords.filter(word => combinedDiscovered.has(word)).length;
const masteredCount = allWords.filter(word => combinedMastered.has(word)).length;
const masteredWordsList = allWords.filter(word => combinedMastered.has(word));
// Count other content types
const totalPhrases = Object.keys(content.phrases || {}).length;
const totalLessons = Object.keys(content.lessons || {}).length;
const totalDialogs = Array.isArray(content.dialogs) ? content.dialogs.length : Object.keys(content.dialogs || {}).length;
const totalGrammar = Object.keys(content.grammar || {}).length;
const totalAudio = Object.keys(content.audio || {}).length;
const totalImages = Object.keys(content.images || {}).length;
// TODO: Load completion data for other content types
const completedPhrases = 0; // TODO: Track phrase completion
const completedLessons = 0; // TODO: Track lesson completion
const completedDialogs = 0; // TODO: Track dialog completion
const completedGrammar = 0; // TODO: Track grammar completion
const result = {
total: vocabCount,
discovered: discoveredCount,
mastered: masteredCount,
discoveredPercentage: vocabCount > 0 ? Math.round((discoveredCount / vocabCount) * 100) : 0,
masteredPercentage: vocabCount > 0 ? Math.round((masteredCount / vocabCount) * 100) : 0,
masteredWordsList: masteredWordsList,
// Additional content types
phrases: {
total: totalPhrases,
completed: completedPhrases,
percentage: totalPhrases > 0 ? Math.round((completedPhrases / totalPhrases) * 100) : 0
},
lessons: {
total: totalLessons,
completed: completedLessons,
percentage: totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0
},
dialogs: {
total: totalDialogs,
completed: completedDialogs,
percentage: totalDialogs > 0 ? Math.round((completedDialogs / totalDialogs) * 100) : 0
},
grammar: {
total: totalGrammar,
completed: completedGrammar,
percentage: totalGrammar > 0 ? Math.round((completedGrammar / totalGrammar) * 100) : 0
}
};
console.log('✅ Combined progress calculated:', result);
return result;
} catch (error) {
console.warn('Failed to calculate vocabulary progress:', error);
return {
total: 0,
discovered: 0,
mastered: 0,
discoveredPercentage: 0,
masteredPercentage: 0,
masteredWordsList: []
};
}
};
window.showVocabularyKnowledge = async function() {
try {
console.log('📖 Refreshing vocabulary knowledge with latest data...');
// Close any existing modal first
const existingModal = document.querySelector('.vocabulary-knowledge-modal');
if (existingModal) {
existingModal.remove();
console.log('🗑️ Removed existing modal');
}
// Always start fresh - reload all data on every opening
const currentChapterId = window.currentChapterId || 'sbs-7-8';
console.log('🔄 Reloading data for chapter:', currentChapterId);
// Load fresh persisted data
const persistedData = await loadPersistedVocabularyData(currentChapterId);
console.log('📁 Fresh persisted data loaded:', persistedData);
// Create a temporary PrerequisiteEngine for the modal
let prerequisiteEngine = null;
try {
const moduleLoader = window.app.getCore().moduleLoader;
// Try to get PrerequisiteEngine from SmartPreviewOrchestrator first
const orchestrator = moduleLoader.getModule('smartPreviewOrchestrator');
if (orchestrator) {
const sharedServices = orchestrator.getSharedServices();
prerequisiteEngine = sharedServices.prerequisiteEngine;
console.log('🔗 PrerequisiteEngine from SmartPreviewOrchestrator:', !!prerequisiteEngine);
}
// If not available, create a temporary one
if (!prerequisiteEngine) {
console.log('🔧 Creating temporary PrerequisiteEngine for modal...');
const { default: PrerequisiteEngine } = await import('./src/DRS/services/PrerequisiteEngine.js');
prerequisiteEngine = new PrerequisiteEngine();
console.log('✅ Temporary PrerequisiteEngine created');
}
// Always ensure chapter is analyzed
if (prerequisiteEngine) {
// Load chapter content
const contentLoader = moduleLoader.getModule('contentLoader');
if (contentLoader) {
console.log('📥 Loading chapter content for:', currentChapterId);
const chapterContent = await contentLoader.getContent(currentChapterId);
console.log('📦 Chapter content loaded:', {
vocabulary: Object.keys(chapterContent.vocabulary || {}).length,
phrases: Object.keys(chapterContent.phrases || {}).length,
dialogs: Object.keys(chapterContent.dialogs || {}).length,
texts: chapterContent.texts?.length || 0,
audio: chapterContent.audio?.length || 0,
images: chapterContent.images?.length || 0,
grammar: Object.keys(chapterContent.grammar || {}).length
});
// Analyze chapter to populate contentAnalysis
console.log('🔬 Analyzing chapter...');
const analysis = prerequisiteEngine.analyzeChapter(chapterContent);
console.log('✅ Chapter analyzed:', analysis);
console.log('📊 contentAnalysis.phrases:', prerequisiteEngine.contentAnalysis?.phrases);
console.log('📊 contentAnalysis.dialogs:', prerequisiteEngine.contentAnalysis?.dialogs);
console.log('📊 contentAnalysis.texts:', prerequisiteEngine.contentAnalysis?.texts);
} else {
console.error('❌ ContentLoader not available');
}
}
} catch (error) {
console.error('❌ Error setting up PrerequisiteEngine:', error);
}
// Calculate fresh combined progress
const vocabularyProgress = await calculateVocabularyProgress(currentChapterId, persistedData, prerequisiteEngine);
console.log('📊 Fresh vocabulary progress:', vocabularyProgress);
// Get mastery progress from PrerequisiteEngine (includes all content types)
const masteryProgress = prerequisiteEngine ? prerequisiteEngine.getMasteryProgress() : {};
console.log('📊 Mastery progress from PrerequisiteEngine:', masteryProgress);
console.log('🔍 PrerequisiteEngine.contentAnalysis:', prerequisiteEngine?.contentAnalysis);
// Prepare data for modal
const progress = {
vocabulary: vocabularyProgress,
phrases: masteryProgress.phrases || { total: 0, mastered: 0, percentage: 0 },
texts: masteryProgress.texts || { total: 0, mastered: 0, percentage: 0 },
dialogs: masteryProgress.dialogs || { total: 0, mastered: 0, percentage: 0 },
audio: masteryProgress.audio || { total: 0, mastered: 0, percentage: 0 },
images: masteryProgress.images || { total: 0, mastered: 0, percentage: 0 },
grammar: masteryProgress.grammar || { total: 0, mastered: 0, percentage: 0 }
};
const masteredWords = vocabularyProgress.masteredWordsList;
// Create vocabulary knowledge modal
const modal = document.createElement('div');
modal.className = 'vocabulary-knowledge-modal';
modal.innerHTML = `
<div class="modal-content">
<div class="modal-header">
<h2>📖 Vocabulary Knowledge</h2>
<button class="modal-close" onclick="closeVocabularyModal()">✕</button>
</div>
<div class="knowledge-stats">
<div class="stat-card">
<div class="stat-number">${progress.vocabulary?.discovered || 0}</div>
<div class="stat-label">Words Discovered</div>
</div>
<div class="stat-card">
<div class="stat-number">${progress.vocabulary?.mastered || 0}</div>
<div class="stat-label">Words Mastered</div>
</div>
<div class="stat-card">
<div class="stat-number">${progress.vocabulary?.total || 0}</div>
<div class="stat-label">Total Words</div>
</div>
</div>
<div class="learning-details">
<h3>📚 Learning Progress Details</h3>
<div class="progress-sections">
<div class="progress-section">
<h4>👁️ Words Discovered</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.vocabulary?.discoveredPercentage || 0}%; background: linear-gradient(90deg, #3498db, #2980b9);"></div>
</div>
<span>${progress.vocabulary?.discovered || 0}/${progress.vocabulary?.total || 0} words (${progress.vocabulary?.discoveredPercentage || 0}%)</span>
</div>
<div class="progress-section">
<h4>🎯 Words Mastered</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.vocabulary?.masteredPercentage || 0}%; background: linear-gradient(90deg, #27ae60, #229954);"></div>
</div>
<span>${progress.vocabulary?.mastered || 0}/${progress.vocabulary?.total || 0} words (${progress.vocabulary?.masteredPercentage || 0}%)</span>
</div>
<div class="progress-section">
<h4>📝 Phrases</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.phrases?.percentage || 0}%; background: linear-gradient(90deg, #9b59b6, #8e44ad);"></div>
</div>
<span>${progress.phrases?.mastered || 0}/${progress.phrases?.total || 0} phrases (${progress.phrases?.percentage || 0}%)</span>
</div>
<div class="progress-section">
<h4>📄 Texts</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.texts?.percentage || 0}%; background: linear-gradient(90deg, #e67e22, #d35400);"></div>
</div>
<span>${progress.texts?.mastered || 0}/${progress.texts?.total || 0} texts (${progress.texts?.percentage || 0}%)</span>
</div>
<div class="progress-section">
<h4>💬 Dialogs</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.dialogs?.percentage || 0}%; background: linear-gradient(90deg, #1abc9c, #16a085);"></div>
</div>
<span>${progress.dialogs?.mastered || 0}/${progress.dialogs?.total || 0} dialogs (${progress.dialogs?.percentage || 0}%)</span>
</div>
<div class="progress-section">
<h4>🎧 Audio</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.audio?.percentage || 0}%; background: linear-gradient(90deg, #3498db, #2980b9);"></div>
</div>
<span>${progress.audio?.mastered || 0}/${progress.audio?.total || 0} audio (${progress.audio?.percentage || 0}%)</span>
</div>
<div class="progress-section">
<h4>🖼️ Images</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.images?.percentage || 0}%; background: linear-gradient(90deg, #e74c3c, #c0392b);"></div>
</div>
<span>${progress.images?.mastered || 0}/${progress.images?.total || 0} images (${progress.images?.percentage || 0}%)</span>
</div>
<div class="progress-section">
<h4>📚 Grammar</h4>
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress.grammar?.percentage || 0}%; background: linear-gradient(90deg, #f39c12, #e67e22);"></div>
</div>
<span>${progress.grammar?.mastered || 0}/${progress.grammar?.total || 0} concepts (${progress.grammar?.percentage || 0}%)</span>
</div>
</div>
</div>
<div class="mastered-words">
<h3>✅ Mastered Words</h3>
<div class="words-grid">
${masteredWords.length > 0
? masteredWords.map(word => `<span class="mastered-word">${word}</span>`).join('')
: '<p class="no-words">No words mastered yet. Start with flashcards to build your vocabulary!</p>'
}
</div>
</div>
</div>
`;
document.body.appendChild(modal);
console.log('Modal added to DOM:', modal);
// Force show modal with inline styles for debugging
modal.style.display = 'flex';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.width = '100%';
modal.style.height = '100%';
modal.style.zIndex = '999999';
} catch (error) {
console.error('Error showing vocabulary knowledge:', error);
alert('Error loading vocabulary knowledge. Please try again.');
}
};
window.closeVocabularyModal = function() {
const modal = document.querySelector('.vocabulary-knowledge-modal');
if (modal) {
modal.remove();
console.log('🗑️ Vocabulary modal closed and removed');
}
};
// Helper function to refresh modal if it's already open
window.refreshVocabularyModalIfOpen = function() {
const existingModal = document.querySelector('.vocabulary-knowledge-modal');
if (existingModal) {
console.log('🔄 Modal is open, refreshing with latest data...');
showVocabularyKnowledge(); // This will close the old one and create a new one
}
};
window.startGuidedExercise = async function(exerciseRecommendation) {
const moduleLoader = window.app.getCore().moduleLoader;
const unifiedDRS = moduleLoader.getModule('unifiedDRS');
if (!unifiedDRS) {
throw new Error('UnifiedDRS not available');
}
// Reset vocabulary override detection
window.vocabularyOverrideActive = null;
// Update UI with current exercise info (initial)
updateCurrentExerciseInfo(exerciseRecommendation);
updateGuideStatus(`🎯 Starting: ${exerciseRecommendation.type} (${exerciseRecommendation.difficulty})`);
const exerciseContainer = document.getElementById('guide-exercise-container');
console.log('🎯 Starting guided exercise:', exerciseRecommendation);
// Start the exercise
await unifiedDRS.start(exerciseContainer, {
type: exerciseRecommendation.type,
difficulty: exerciseRecommendation.difficulty,
bookId: exerciseRecommendation.bookId,
chapterId: exerciseRecommendation.chapterId,
context: 'smart-guide',
sessionId: currentGuidedSession.id
});
// Check if vocabulary override was activated
if (window.vocabularyOverrideActive) {
console.log('📚 Vocabulary override detected, updating Smart Guide UI:', window.vocabularyOverrideActive);
updateVocabularyOverrideUI(exerciseRecommendation, window.vocabularyOverrideActive);
} else {
// Update progress normally
updateProgressBar(exerciseRecommendation.sessionPosition, exerciseRecommendation.totalInSession);
}
};
window.updateGuideStatus = function(message) {
const statusElement = document.getElementById('guide-status');
if (statusElement) {
statusElement.textContent = message;
}
};
// Global chapter data cache
window.currentChapterData = null;
// Helper to get real content coverage percentage
window.getRealContentCoverage = async function() {
try {
if (!window.currentBookId || !window.currentChapterId || !window.currentChapterData) {
console.log('⚠️ Missing required data for coverage calculation');
return null;
}
const chapterData = window.currentChapterData;
// Load progress data
const { default: VocabularyProgressManager } = await import('./src/DRS/services/VocabularyProgressManager.js');
const progressManager = new VocabularyProgressManager();
const progressData = await progressManager.loadProgress(window.currentBookId, window.currentChapterId);
const discovered = Object.keys(progressData.discovered || {}).length;
const mastered = Object.keys(progressData.mastered || {}).length;
// Count all content types in chapter
const totalVocab = Object.keys(chapterData.vocabulary || {}).length;
const totalPhrases = Object.keys(chapterData.phrases || {}).length;
const totalLessons = Object.keys(chapterData.lessons || {}).length;
const totalDialogs = Array.isArray(chapterData.dialogs) ? chapterData.dialogs.length : Object.keys(chapterData.dialogs || {}).length;
const totalGrammar = Object.keys(chapterData.grammar || {}).length;
const totalAudio = Object.keys(chapterData.audio || {}).length;
const totalImages = Object.keys(chapterData.images || {}).length;
// Calculate weighted totals (from CLAUDE.md weights)
const totalWeight =
(totalVocab * 2) + // discovery (1) + mastery (1) = 2 per word
(totalPhrases * 6) +
(totalLessons * 15) +
(totalDialogs * 12) +
(totalGrammar * 6) +
(totalAudio * 12) +
(totalImages * 6);
// Calculate completed weight (for now, only vocabulary is tracked)
const completedWeight =
(discovered * 1) + // discovery = 1 point
(mastered * 1); // mastery = 1 point
// TODO: Add tracking for lessons, dialogs, grammar completion
const percentage = totalWeight > 0 ? Math.round((completedWeight / totalWeight) * 100) : 0;
console.log(`📊 Content inventory:`, {
vocab: totalVocab,
phrases: totalPhrases,
lessons: totalLessons,
dialogs: totalDialogs,
grammar: totalGrammar,
audio: totalAudio,
images: totalImages
});
console.log(`📊 Progress: ${completedWeight}/${totalWeight} points = ${percentage}%`);
console.log(`📊 Vocabulary: ${discovered} discovered, ${mastered} mastered`);
return percentage;
} catch (error) {
console.log('Could not get real content coverage:', error.message);
}
return null;
};
window.updateProgressBar = async function(current, total) {
const progressBar = document.getElementById('guide-progress-bar');
const progressText = document.getElementById('guide-progress-text');
// Get real content coverage from storage (async)
const percentage = await getRealContentCoverage() || 0;
if (progressBar) {
progressBar.style.width = percentage + '%';
}
if (progressText) {
progressText.textContent = `${percentage}% Content Coverage`;
}
};
window.updateVocabularyOverrideUI = function(originalExercise, overrideInfo) {
console.log('📚 Updating Smart Guide UI for vocabulary override');
// Distinguish between Word Discovery (passive) and Flashcards (active)
const isDiscovery = overrideInfo.mode === 'discovery';
const emoji = isDiscovery ? '📖' : '📚';
const typeName = isDiscovery ? 'Word Discovery' : 'Vocabulary Practice';
const modeName = isDiscovery ? 'Passive Learning' : 'Adaptive Flashcards';
// Update status with vocabulary override explanation
if (overrideInfo.missingWords && overrideInfo.missingWords.length > 0) {
// Content-based override: specific words needed
const wordList = overrideInfo.missingWords.slice(0, 3).join(', ');
const moreWords = overrideInfo.missingWords.length > 3 ? `... (${overrideInfo.missingWords.length - 3} more)` : '';
updateGuideStatus(`${emoji} ${typeName} (Required - Need: ${wordList}${moreWords})`);
updateCurrentExerciseInfo({
type: isDiscovery ? 'word-discovery' : 'vocabulary',
difficulty: 'adaptive',
mode: modeName,
sessionPosition: originalExercise.sessionPosition,
totalInSession: originalExercise.totalInSession,
reasoning: isDiscovery
? `First-time exposure to ${overrideInfo.totalMissing || overrideInfo.missingWords.length} new words. Discover: ${overrideInfo.missingWords.slice(0, 5).join(', ')}${overrideInfo.missingWords.length > 5 ? '...' : ''}`
: `Upcoming content requires ${overrideInfo.missingWords.length} vocabulary words. Practice: ${overrideInfo.missingWords.slice(0, 5).join(', ')}${overrideInfo.missingWords.length > 5 ? '...' : ''}`
});
} else {
// Fallback to old percentage-based display
updateGuideStatus(`${emoji} ${typeName} (Required - ${overrideInfo.reason || 'Building foundation'})`);
updateCurrentExerciseInfo({
type: isDiscovery ? 'word-discovery' : 'vocabulary',
difficulty: 'adaptive',
mode: modeName,
sessionPosition: originalExercise.sessionPosition,
totalInSession: originalExercise.totalInSession,
reasoning: overrideInfo.reason || `Vocabulary foundation required before ${originalExercise.type} exercises.`
});
}
// Update progress bar with real content coverage
updateProgressBar(originalExercise.sessionPosition, originalExercise.totalInSession);
};
window.updateCurrentExerciseInfo = function(exercise) {
const infoContainer = document.getElementById('current-exercise-info');
const detailsElement = document.getElementById('exercise-details');
const reasoningElement = document.getElementById('exercise-reasoning');
if (infoContainer && detailsElement && reasoningElement) {
infoContainer.style.display = 'block';
// Special handling for vocabulary exercises
if (exercise.type === 'vocabulary' || exercise.type === 'word-discovery') {
const emoji = exercise.type === 'word-discovery' ? '📖' : '📚';
const typeName = exercise.type === 'word-discovery' ? 'Word Discovery' : 'Vocabulary Practice';
const mode = exercise.mode || (exercise.difficulty === 'adaptive' ? 'Adaptive Flashcards' : exercise.difficulty.charAt(0).toUpperCase() + exercise.difficulty.slice(1));
detailsElement.innerHTML = `
<div class="exercise-detail">
<strong>Type:</strong> ${emoji} ${typeName}
</div>
<div class="exercise-detail">
<strong>Mode:</strong> ${mode}
</div>
<div class="exercise-detail">
<strong>Position:</strong> ${exercise.sessionPosition}/${exercise.totalInSession}
</div>
`;
} else {
// Normal exercise handling
detailsElement.innerHTML = `
<div class="exercise-detail">
<strong>Type:</strong> ${exercise.type.charAt(0).toUpperCase() + exercise.type.slice(1)}
</div>
<div class="exercise-detail">
<strong>Difficulty:</strong> ${exercise.difficulty.charAt(0).toUpperCase() + exercise.difficulty.slice(1)}
</div>
<div class="exercise-detail">
<strong>Position:</strong> ${exercise.sessionPosition}/${exercise.totalInSession}
</div>
`;
}
reasoningElement.innerHTML = `
<div class="reasoning-box">
<strong>Why this exercise?</strong><br>
${exercise.reasoning}
</div>
`;
}
};
window.completeGuidedSession = function() {
updateGuideStatus('🎉 Smart Guide session completed!');
// Get performance insights
const moduleLoader = window.app.getCore().moduleLoader;
const intelligentSequencer = moduleLoader.getModule('intelligentSequencer');
if (intelligentSequencer) {
const insights = intelligentSequencer.getPerformanceInsights();
showPerformanceInsights(insights);
}
// Reset session
setTimeout(() => {
currentGuidedSession = null;
}, 5000);
};
window.showPerformanceInsights = function(insights) {
const insightsContainer = document.getElementById('guide-insights');
const insightsContent = document.getElementById('insights-content');
if (insightsContainer && insightsContent) {
insightsContainer.style.display = 'block';
const overallStats = insights.overallStats;
const recommendations = insights.recommendations || [];
const recentTrends = insights.recentTrends;
insightsContent.innerHTML = `
<div class="insights-grid">
<div class="insight-card">
<h5>📊 Overall Performance</h5>
<ul>
<li>Total Exercises: ${overallStats.totalExercises}</li>
<li>Average Accuracy: ${Math.round(overallStats.averageAccuracy * 100)}%</li>
<li>Completion Rate: ${Math.round(overallStats.completionRate * 100)}%</li>
</ul>
</div>
<div class="insight-card">
<h5>📈 Recent Trend</h5>
<p>
${recentTrends.trend === 'improving' ? '📈 Improving' :
recentTrends.trend === 'declining' ? '📉 Declining' :
recentTrends.trend === 'stable' ? '📊 Stable' : '❓ Insufficient data'}
</p>
${recentTrends.recentAccuracy ?
`<p>Recent accuracy: ${Math.round(recentTrends.recentAccuracy * 100)}%</p>` : ''}
</div>
${recommendations.length > 0 ? `
<div class="insight-card">
<h5>💡 Recommendations</h5>
<ul>
${recommendations.slice(0, 3).map(rec =>
`<li><strong>${rec.type}:</strong> ${rec.message}</li>`
).join('')}
</ul>
</div>
` : ''}
</div>
<div class="insights-actions">
<button class="btn btn-primary" onclick="startSmartGuide()">
🧠 Start Another Session
</button>
<button class="btn btn-outline" onclick="stopSmartGuide()">
📖 Return to Manual Mode
</button>
</div>
`;
}
};
// Set up Smart Guide event listeners when app is ready
function setupSmartGuideEventListeners() {
if (!window.app || !window.app.getCore) {
setTimeout(setupSmartGuideEventListeners, 100);
return;
}
const { eventBus } = window.app.getCore();
// Register SmartGuide module for event listening
eventBus.registerModule({ name: 'SmartGuide' });
console.log('🎧 Setting up Smart Guide event listeners...');
// Set up event listeners for guided sessions
eventBus.on('drs:completed', (event) => {
if (!currentGuidedSession) {
console.log('📢 DRS completed but no guided session active');
return;
}
const data = event.data;
console.log('🎯 Exercise completed in guided session:', data);
// Record completion in sequencer
const moduleLoader = window.app.getCore().moduleLoader;
const intelligentSequencer = moduleLoader.getModule('intelligentSequencer');
if (intelligentSequencer && intelligentSequencer.isGuiding()) {
console.log('🔄 Getting next exercise recommendation...');
// Get next exercise recommendation
setTimeout(async () => {
try {
const nextExercise = await intelligentSequencer.getNextExercise();
if (nextExercise) {
updateGuideStatus('🔄 Preparing next exercise...');
console.log('🎯 Next exercise recommended:', nextExercise);
setTimeout(() => startGuidedExercise(nextExercise), 1500);
} else {
// Session completed
console.log('✅ No more exercises - session complete');
completeGuidedSession();
}
} catch (error) {
console.error('❌ Error getting next exercise:', error);
updateGuideStatus(`❌ Error: ${error.message}`);
}
}, 500);
} else {
console.warn('⚠️ IntelligentSequencer not guiding or not available');
}
}, 'SmartGuide');
eventBus.on('sequencer:session-completed', (event) => {
console.log('🎉 Guided session completed via sequencer:', event.data);
completeGuidedSession();
}, 'SmartGuide');
eventBus.on('drs:step-completed', (event) => {
if (!currentGuidedSession) return;
console.log('📝 Step completed in guided session:', event.data);
// Update progress for individual steps within an exercise
updateProgressBar(
currentGuidedSession.completed + (event.data.step + 1) / event.data.total,
currentGuidedSession.targetLength
);
}, 'SmartGuide');
console.log('✅ Smart Guide event listeners configured');
}
// Initialize event listeners when app is ready
setupSmartGuideEventListeners();
</script>
<!-- Integration Test Suite -->
<script src="test-integration.js"></script>
<script src="test-console-commands.js"></script>
<script src="test-uiux-integration.js"></script>
<script src="test-e2e-scenarios.js"></script>
</body>
</html>