Spaces:
Running
Running
milwright
fix leaderboard initials entry readability with improved contrast and light background
e4e854a
| // Main application entry point | |
| import ClozeGame from './clozeGameEngine.js'; | |
| import ChatUI from './chatInterface.js'; | |
| import WelcomeOverlay from './welcomeOverlay.js'; | |
| import { LeaderboardUI } from './leaderboardUI.js'; | |
| class App { | |
| constructor() { | |
| this.game = new ClozeGame(); | |
| this.chatUI = new ChatUI(this.game); | |
| this.welcomeOverlay = new WelcomeOverlay(); | |
| this.leaderboardUI = new LeaderboardUI(this.game.leaderboardService); | |
| 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'), | |
| leaderboardBtn: document.getElementById('leaderboard-btn') | |
| }; | |
| 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()); | |
| // Leaderboard button | |
| if (this.elements.leaderboardBtn) { | |
| this.elements.leaderboardBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| this.leaderboardUI.show(); | |
| }); | |
| } | |
| // Note: Enter key handling is done per-input in setupInputListeners() | |
| } | |
| 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 = ` | |
| <strong>${roundData.title}</strong> 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 = ` | |
| <div class="flex items-start gap-2"> | |
| <span class="text-blue-600">📜</span> | |
| <span>${roundData.contextualization || 'Loading context...'}</span> | |
| </div> | |
| `; | |
| // Render the cloze text with input fields and chat buttons | |
| const clozeHtml = this.game.renderClozeTextWithChat(); | |
| this.elements.passageContent.innerHTML = `<p>${clozeHtml}</p>`; | |
| // 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}`; | |
| if (results.passed) { | |
| // Check if level was just advanced | |
| if (results.justAdvancedLevel) { | |
| message += ` - Level ${results.currentLevel} unlocked!`; | |
| // Check for milestone notification (every 5 levels) | |
| if (results.currentLevel % 5 === 0) { | |
| this.leaderboardUI.showMilestoneNotification(results.currentLevel); | |
| } | |
| // Check for high score | |
| this.checkForHighScore(); | |
| } else { | |
| message += ` - Passed!`; | |
| } | |
| this.elements.result.className = 'mt-4 text-center font-semibold text-green-600'; | |
| } else { | |
| message += ` - Failed (need ${results.requiredCorrect} correct)`; | |
| 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 round | |
| this.chatUI.clearChatHistory(); | |
| // Always show loading for at least 1 second for smooth UX | |
| const startTime = Date.now(); | |
| // Load next round | |
| const 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 round:', error); | |
| this.showError('Could not load next round. 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 = '<div class="text-yellow-600">No hints available for this passage.</div>'; | |
| return; | |
| } | |
| const hintsHtml = this.currentHints.map((hintData, index) => | |
| `<div class="flex items-start gap-2"> | |
| <span class="font-semibold text-yellow-800">${index + 1}.</span> | |
| <span>${hintData.hint}</span> | |
| </div>` | |
| ).join(''); | |
| this.elements.hintsList.innerHTML = hintsHtml; | |
| } | |
| toggleHints() { | |
| const isHidden = this.elements.hintsSection.style.display === 'none'; | |
| this.elements.hintsSection.style.display = isHidden ? 'block' : 'none'; | |
| this.elements.hintBtn.textContent = isHidden ? 'Hide Hints' : 'Show Hints'; | |
| } | |
| resetUI() { | |
| this.elements.result.textContent = ''; | |
| this.elements.submitBtn.style.display = 'inline-block'; | |
| this.elements.submitBtn.disabled = true; | |
| this.elements.nextBtn.classList.add('hidden'); | |
| this.elements.hintsSection.style.display = 'none'; | |
| this.elements.hintBtn.textContent = 'Show Hints'; | |
| this.currentResults = null; | |
| this.currentHints = []; | |
| } | |
| showLoading(show, message = 'Loading passages...') { | |
| if (show) { | |
| this.elements.loading.innerHTML = ` | |
| <div class="text-center py-8"> | |
| <p class="text-lg loading-text">${message}</p> | |
| </div> | |
| `; | |
| this.elements.loading.classList.remove('hidden'); | |
| this.elements.gameArea.classList.add('hidden'); | |
| this.elements.stickyControls.classList.add('hidden'); | |
| } else { | |
| this.elements.loading.classList.add('hidden'); | |
| this.elements.gameArea.classList.remove('hidden'); | |
| this.elements.stickyControls.classList.remove('hidden'); | |
| } | |
| } | |
| showError(message) { | |
| this.elements.loading.innerHTML = ` | |
| <div class="text-center py-8"> | |
| <p class="text-lg text-red-600 mb-4">${message}</p> | |
| <button onclick="location.reload()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"> | |
| Reload | |
| </button> | |
| </div> | |
| `; | |
| this.elements.loading.classList.remove('hidden'); | |
| this.elements.gameArea.classList.add('hidden'); | |
| } | |
| checkForHighScore() { | |
| // Check if current score qualifies for leaderboard | |
| if (this.game.checkForHighScore()) { | |
| const rank = this.game.getHighScoreRank(); | |
| const stats = this.game.leaderboardService.getStats(); | |
| // Always show initials entry when achieving a high score | |
| // Pre-fills with previous initials if available, allowing changes | |
| this.leaderboardUI.showInitialsEntry( | |
| stats.highestLevel, | |
| stats.roundAtHighestLevel, | |
| rank, | |
| (initials) => { | |
| // Save to leaderboard | |
| const finalRank = this.game.addToLeaderboard(initials); | |
| console.log(`Added to leaderboard at rank ${finalRank}`); | |
| } | |
| ); | |
| } | |
| } | |
| } | |
| // Initialize the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const app = new App(); | |
| // Show welcome overlay immediately before any loading | |
| app.welcomeOverlay.show(); | |
| app.initialize(); | |
| // Expose API key setter for browser console | |
| window.setOpenRouterKey = (key) => { | |
| app.game.chatService.aiService.setApiKey(key); | |
| console.log('OpenRouter API key updated'); | |
| }; | |
| }); |