/** * Spaced Repetition System for Spelling Practice App * * Based on the SM-2 algorithm (SuperMemo) with adaptations for spelling practice * This system schedules words for review based on user performance * Words are assigned difficulty levels and review intervals are adjusted accordingly */ // Module variables let spacedRepetitionData = null; let srCurrentUser = null; // Renamed to avoid conflicts let currentWords = []; let reviewQueue = []; // Initialize the spaced repetition system document.addEventListener('DOMContentLoaded', function() { // Event listener will be triggered when a user logs in document.addEventListener('userLoggedIn', function(e) { srCurrentUser = e.detail.username; initializeSpacedRepetition(srCurrentUser); }); }); // Main initialization function async function initializeSpacedRepetition(username) { try { await fetchSpacedRepetitionData(username); updateSpacedRepetitionUI(); console.log('🔄 Spaced repetition system initialized'); } catch (error) { console.error('Error initializing spaced repetition:', error); } } // Fetch spaced repetition data for the current user async function fetchSpacedRepetitionData(username) { try { const response = await fetch(`/getSpacedRepetitionData?username=${encodeURIComponent(username)}`); if (!response.ok) { throw new Error(`Failed to fetch spaced repetition data: ${response.status}`); } spacedRepetitionData = await response.json(); // If there are words due for review today, add them to the review queue buildReviewQueue(); return spacedRepetitionData; } catch (error) { console.error('Error fetching spaced repetition data:', error); return null; } } // Build a queue of words that are due for review function buildReviewQueue() { if (!spacedRepetitionData || !spacedRepetitionData.words) { reviewQueue = []; return; } const today = new Date().toISOString().split('T')[0]; reviewQueue = Object.entries(spacedRepetitionData.words) .filter(([_, wordData]) => { return wordData.nextReview && wordData.nextReview <= today; }) .map(([word, wordData]) => ({ word, easeFactor: wordData.easeFactor, interval: wordData.interval, repetitions: wordData.repetitions, lastReview: wordData.lastReview, nextReview: wordData.nextReview, priority: calculatePriority(wordData) })) .sort((a, b) => b.priority - a.priority); // Sort by priority (highest first) } // Calculate priority for a word based on how overdue it is and its difficulty function calculatePriority(wordData) { if (!wordData.nextReview) return 0; const today = new Date(); const nextReview = new Date(wordData.nextReview); const daysOverdue = Math.max(0, (today - nextReview) / (1000 * 60 * 60 * 24)); // Priority increases with days overdue and decreases with ease factor // This prioritizes difficult, overdue words return daysOverdue * (5 - wordData.easeFactor); } // Update the UI to show spaced repetition information function updateSpacedRepetitionUI() { const srStatsContainer = document.getElementById('srStatsContainer'); if (!srStatsContainer || !spacedRepetitionData) return; // Clear previous content srStatsContainer.innerHTML = ''; // Create stats container const statsDiv = document.createElement('div'); statsDiv.className = 'sr-stats'; const todayCount = reviewQueue.length; const masteredCount = Object.values(spacedRepetitionData.words || {}) .filter(word => word.easeFactor >= 2.5 && word.repetitions >= 5).length; const totalCount = Object.keys(spacedRepetitionData.words || {}).length; statsDiv.innerHTML = `
${todayCount}
Due Today
${masteredCount}
Mastered
${totalCount}
Total Words
`; srStatsContainer.appendChild(statsDiv); // Update review button state const reviewButton = document.getElementById('startSpacedReviewBtn'); if (reviewButton) { if (todayCount > 0) { reviewButton.classList.remove('disabled'); reviewButton.innerHTML = `🔄 Start Review (${todayCount})`; } else { reviewButton.classList.add('disabled'); reviewButton.innerHTML = '✓ All Caught Up'; } } // Update progress visualization updateProgressVisualization(); } // Update the progress visualization function updateProgressVisualization() { const progressContainer = document.getElementById('srProgressContainer'); if (!progressContainer || !spacedRepetitionData || !spacedRepetitionData.words) return; // Clear previous content progressContainer.innerHTML = ''; const words = Object.entries(spacedRepetitionData.words); if (words.length === 0) { progressContainer.innerHTML = '

No words in your spaced repetition system yet.

'; return; } // Create progress visualization const progressDiv = document.createElement('div'); progressDiv.className = 'sr-progress-visualization'; // Group words by mastery level const levels = { 'New': words.filter(([_, w]) => w.repetitions === 0).length, 'Learning': words.filter(([_, w]) => w.repetitions > 0 && w.repetitions < 3).length, 'Reviewing': words.filter(([_, w]) => w.repetitions >= 3 && w.repetitions < 5).length, 'Mastered': words.filter(([_, w]) => w.repetitions >= 5).length }; const totalWords = Object.values(levels).reduce((sum, count) => sum + count, 0); // Create progress bar const progressBar = document.createElement('div'); progressBar.className = 'sr-progress-bar'; const colors = { 'New': '#ff6b6b', 'Learning': '#feca57', 'Reviewing': '#54a0ff', 'Mastered': '#1dd1a1' }; Object.entries(levels).forEach(([level, count]) => { if (count === 0) return; const percentage = (count / totalWords) * 100; const segment = document.createElement('div'); segment.className = 'sr-progress-segment'; segment.style.width = `${percentage}%`; segment.style.backgroundColor = colors[level]; segment.title = `${level}: ${count} words (${percentage.toFixed(1)}%)`; progressBar.appendChild(segment); }); // Create legend const legend = document.createElement('div'); legend.className = 'sr-legend'; Object.entries(levels).forEach(([level, count]) => { const legendItem = document.createElement('div'); legendItem.className = 'sr-legend-item'; legendItem.innerHTML = ` ${level}: ${count} `; legend.appendChild(legendItem); }); progressDiv.appendChild(progressBar); progressDiv.appendChild(legend); progressContainer.appendChild(progressDiv); } // Start a spaced repetition review session async function startSpacedReview() { if (reviewQueue.length === 0) { showToast('No words to review right now!', 'info'); return; } // Hide student panel and show game section document.getElementById('studentPanel').classList.add('hidden'); document.getElementById('gameSection').classList.remove('hidden'); // Update UI to indicate this is a spaced repetition session document.querySelector('#gameSection h3').textContent = '🔄 Spaced Repetition Review'; // Prepare words for the session (take up to 10 words from the queue) currentWords = reviewQueue.slice(0, 10).map(item => item.word); // Start the game with these words await startGame(currentWords, true); } // Process results from a spaced repetition session function processSpacedRepetitionResults(results) { if (!srCurrentUser || !spacedRepetitionData || !results || results.length === 0) return; const now = new Date(); const today = now.toISOString().split('T')[0]; // Process each word from the results results.forEach(result => { const word = result.word; const correct = result.correct; // If word doesn't exist in the system yet, initialize it if (!spacedRepetitionData.words[word]) { spacedRepetitionData.words[word] = { easeFactor: 2.5, // Initial ease factor interval: 0, // Initial interval (0 = first time) repetitions: 0, // Number of successful reviews lastReview: null, nextReview: null }; } const wordData = spacedRepetitionData.words[word]; // Apply SM-2 algorithm if (correct) { // Update based on correct answer if (wordData.repetitions === 0) { // First successful review wordData.interval = 1; } else if (wordData.repetitions === 1) { // Second successful review wordData.interval = 6; } else { // Subsequent successful reviews - increase interval wordData.interval = Math.round(wordData.interval * wordData.easeFactor); } // Increment repetitions counter wordData.repetitions += 1; // Adjust ease factor (make it slightly easier if correct) wordData.easeFactor = Math.max(1.3, wordData.easeFactor + 0.1); } else { // Reset on incorrect answer wordData.repetitions = 0; wordData.interval = 1; // Make it harder if incorrect wordData.easeFactor = Math.max(1.3, wordData.easeFactor - 0.2); } // Update review dates wordData.lastReview = today; // Calculate next review date const nextDate = new Date(now); nextDate.setDate(nextDate.getDate() + wordData.interval); wordData.nextReview = nextDate.toISOString().split('T')[0]; }); // Save updated data to the server saveSpacedRepetitionData(); // Update the queue and UI buildReviewQueue(); updateSpacedRepetitionUI(); } // Save the updated spaced repetition data to the server async function saveSpacedRepetitionData() { if (!srCurrentUser || !spacedRepetitionData) return; try { const response = await fetch('/updateSpacedRepetitionData', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: srCurrentUser, data: spacedRepetitionData }) }); if (!response.ok) { throw new Error(`Failed to update spaced repetition data: ${response.status}`); } return true; } catch (error) { console.error('Error saving spaced repetition data:', error); return false; } } // Import new words to the spaced repetition system from a word list async function importWordsToSR() { if (!srCurrentUser || !spacedRepetitionData) return; try { // Get user's word lists const response = await fetch(`/getWordlistsForUser?username=${encodeURIComponent(srCurrentUser)}`); if (!response.ok) { throw new Error(`Failed to fetch wordlists: ${response.status}`); } const wordlists = await response.json(); if (!wordlists || !Array.isArray(wordlists) || wordlists.length === 0) { showToast('No word lists found', 'error'); return; } // For simplicity, let's import all words from the user's word list const allWords = [...new Set(wordlists)]; // Remove duplicates // Add each word to the spaced repetition system if not already present let newWordsCount = 0; allWords.forEach(word => { if (!spacedRepetitionData.words[word]) { spacedRepetitionData.words[word] = { easeFactor: 2.5, interval: 0, repetitions: 0, lastReview: null, nextReview: new Date().toISOString().split('T')[0] // Due immediately }; newWordsCount++; } }); // Save the updated data await saveSpacedRepetitionData(); // Update UI buildReviewQueue(); updateSpacedRepetitionUI(); showToast(`Added ${newWordsCount} new words to your spaced repetition system`, 'success'); } catch (error) { console.error('Error importing words:', error); showToast('Failed to import words', 'error'); } } // Export functions to window for use in HTML window.startSpacedReview = startSpacedReview; window.importWordsToSR = importWordsToSR; // Register a handler for when game results are available document.addEventListener('gameResultsAvailable', function(e) { // Only process if this was a spaced repetition session if (e.detail.isSpacedRepetition) { processSpacedRepetitionResults(e.detail.results); } });