// Main application entry point import ClozeGame from './clozeGameEngine.js'; import ChatUI from './chatInterface.js'; import WelcomeOverlay from './welcomeOverlay.js'; class App { constructor() { this.game = new ClozeGame(); this.chatUI = new ChatUI(this.game); this.welcomeOverlay = new WelcomeOverlay(); this.elements = { loading: document.getElementById('loading'), gameArea: document.getElementById('game-area'), stickyControls: document.getElementById('sticky-controls'), bookInfo: document.getElementById('book-info'), roundInfo: document.getElementById('round-info'), contextualization: document.getElementById('contextualization'), passageContent: document.getElementById('passage-content'), hintsSection: document.getElementById('hints-section'), hintsList: document.getElementById('hints-list'), submitBtn: document.getElementById('submit-btn'), nextBtn: document.getElementById('next-btn'), hintBtn: document.getElementById('hint-btn'), result: document.getElementById('result') }; this.currentResults = null; this.setupEventListeners(); } async initialize() { try { this.showLoading(true); await this.game.initialize(); await this.startNewGame(); this.showLoading(false); } catch (error) { console.error('Failed to initialize app:', error); this.showError('Failed to load the game. Please refresh and try again.'); } } setupEventListeners() { this.elements.submitBtn.addEventListener('click', () => this.handleSubmit()); this.elements.nextBtn.addEventListener('click', () => this.handleNext()); this.elements.hintBtn.addEventListener('click', () => this.toggleHints()); // Allow Enter key to submit when focused on an input document.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.target.classList.contains('cloze-input')) { this.handleSubmit(); } }); } async startNewGame() { try { const roundData = await this.game.startNewRound(); this.displayRound(roundData); this.resetUI(); } catch (error) { console.error('Error starting new game:', error); this.showError('Could not load a new passage. Please try again.'); } } displayRound(roundData) { // Show book information this.elements.bookInfo.innerHTML = ` ${roundData.title} by ${roundData.author} `; // Show level information const blanksCount = roundData.blanks.length; const levelInfo = `Level ${this.game.currentLevel} • ${blanksCount} blank${blanksCount > 1 ? 's' : ''}`; this.elements.roundInfo.innerHTML = levelInfo; // Show contextualization from AI agent this.elements.contextualization.innerHTML = `
${clozeHtml}
`; // Store hints for later display this.currentHints = roundData.hints || []; this.populateHints(); // Hide hints initially this.elements.hintsSection.style.display = 'none'; // Set up input field listeners this.setupInputListeners(); // Set up chat buttons this.chatUI.setupChatButtons(); } setupInputListeners() { const inputs = this.elements.passageContent.querySelectorAll('.cloze-input'); inputs.forEach((input, index) => { input.addEventListener('input', () => { // Remove any previous styling input.classList.remove('correct', 'incorrect'); this.updateSubmitButton(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); // Move to next input or submit if last const nextInput = inputs[index + 1]; if (nextInput) { nextInput.focus(); } else { this.handleSubmit(); } } }); }); // Focus first input if (inputs.length > 0) { inputs[0].focus(); } } updateSubmitButton() { const inputs = this.elements.passageContent.querySelectorAll('.cloze-input'); const allFilled = Array.from(inputs).every(input => input.value.trim() !== ''); this.elements.submitBtn.disabled = !allFilled; } handleSubmit() { const inputs = this.elements.passageContent.querySelectorAll('.cloze-input'); const answers = Array.from(inputs).map(input => input.value.trim()); // Check if all fields are filled if (answers.some(answer => answer === '')) { alert('Please fill in all blanks before submitting.'); return; } // Submit answers and get results this.currentResults = this.game.submitAnswers(answers); this.displayResults(this.currentResults); } displayResults(results) { let message = `Score: ${results.correct}/${results.total} (${results.percentage}%)`; // Show "Required" information at all levels for consistency message += ` - Required: ${results.requiredCorrect}/${results.total}`; if (results.passed) { // Check if this completes the requirements for level advancement const roundsCompleted = this.game.roundsPassedAtCurrentLevel + 1; // +1 for this round if (roundsCompleted >= 2) { message += ` - Excellent! Advancing to Level ${this.game.currentLevel + 1}! 🎉`; } else { message += ` - Great job! ${roundsCompleted}/2 rounds completed for Level ${this.game.currentLevel}`; } this.elements.result.className = 'mt-4 text-center font-semibold text-green-600'; } else { message += ` - Need ${results.requiredCorrect} correct to advance. Keep practicing! 💪`; this.elements.result.className = 'mt-4 text-center font-semibold text-red-600'; } this.elements.result.textContent = message; // Always reveal answers at the end of each round this.revealAnswersInPlace(results.results); // Show next button and hide submit button this.elements.submitBtn.style.display = 'none'; this.elements.nextBtn.classList.remove('hidden'); } highlightAnswers(results) { const inputs = this.elements.passageContent.querySelectorAll('.cloze-input'); results.forEach((result, index) => { const input = inputs[index]; if (input) { if (result.isCorrect) { input.classList.add('correct'); } else { input.classList.add('incorrect'); // Show correct answer as placeholder or title input.title = `Correct answer: ${result.correctAnswer}`; } input.disabled = true; } }); } async handleNext() { try { // Show loading immediately with specific message this.showLoading(true, 'Loading passages...'); // Clear chat history when starting new passage/round this.chatUI.clearChatHistory(); // Always show loading for at least 1 second for smooth UX const startTime = Date.now(); // Check if we should load next passage or next round let roundData; if (this.game.currentPassageIndex === 0) { // Load second passage in current round roundData = await this.game.nextPassage(); } else { // Load next round (two new passages) roundData = await this.game.nextRound(); } // Ensure loading is shown for at least half a second const elapsedTime = Date.now() - startTime; if (elapsedTime < 500) { await new Promise(resolve => setTimeout(resolve, 500 - elapsedTime)); } this.displayRound(roundData); this.resetUI(); this.showLoading(false); } catch (error) { console.error('Error loading next passage:', error); this.showError('Could not load next passage. Please try again.'); } } // Reveal correct answers immediately after submission revealAnswersInPlace(results) { const inputs = this.elements.passageContent.querySelectorAll('.cloze-input'); results.forEach((result, index) => { const input = inputs[index]; if (input) { if (result.isCorrect) { input.classList.add('correct'); input.style.backgroundColor = '#dcfce7'; // Light green input.style.borderColor = '#16a34a'; // Green border } else { input.classList.add('incorrect'); input.style.backgroundColor = '#fef2f2'; // Light red input.style.borderColor = '#dc2626'; // Red border // Show correct answer below the input (only if not already shown) const existingAnswer = input.parentNode.querySelector('.correct-answer-reveal'); if (!existingAnswer) { const correctAnswerSpan = document.createElement('span'); correctAnswerSpan.className = 'correct-answer-reveal text-sm text-green-600 font-semibold ml-2'; correctAnswerSpan.textContent = `✓ ${result.correctAnswer}`; input.parentNode.appendChild(correctAnswerSpan); } } input.disabled = true; } }); } populateHints() { if (!this.currentHints || this.currentHints.length === 0) { this.elements.hintsList.innerHTML = '${message}
${message}