// === STORY READER GAME === // Prototype for reading long stories with sentence chunking and word-by-word translation class StoryReader { constructor(options) { this.container = options.container; this.content = options.content; this.onScoreUpdate = options.onScoreUpdate || (() => {}); this.onGameEnd = options.onGameEnd || (() => {}); // Reading state this.currentChapter = 0; this.currentSentence = 0; this.totalSentences = 0; this.readingSessions = 0; this.wordsRead = 0; this.comprehensionScore = 0; // Story data this.story = null; this.vocabulary = {}; // UI state this.showTranslations = false; this.showPronunciations = false; this.readingMode = 'sentence'; // 'sentence' or 'paragraph' this.fontSize = 'medium'; // Reading time tracking this.startTime = Date.now(); this.totalReadingTime = 0; this.readingTimer = null; this.init(); } init() { logSh(`πŸ” Story Reader content received:`, this.content, 'DEBUG'); logSh(`πŸ” Story field exists: ${!!this.content.story}`, 'DEBUG'); logSh(`πŸ” RawContent exists: ${!!this.content.rawContent}`, 'DEBUG'); // VΓ©rifier d'abord le contenu brut (rawContent) puis le contenu adaptΓ© const storyData = this.content.rawContent?.story || this.content.story; if (!storyData) { logSh('No story content found in content or rawContent', 'ERROR'); this.showError('This content does not contain a story for reading.'); return; } this.story = storyData; this.vocabulary = this.content.rawContent?.vocabulary || this.content.vocabulary || {}; this.calculateTotalSentences(); logSh(`πŸ“– Story Reader initialized: "${this.story.title}" (${this.totalSentences} sentences)`, 'INFO'); this.createInterface(); this.loadProgress(); this.renderCurrentSentence(); } showError(message) { this.container.innerHTML = `

❌ Error

${message}

`; } calculateTotalSentences() { this.totalSentences = 0; this.story.chapters.forEach(chapter => { this.totalSentences += chapter.sentences.length; }); } createInterface() { this.container.innerHTML = `

${this.story.title}

Sentence 1 of ${this.totalSentences}
Chapter 1: Loading...
Loading story...
Words Read: 0
Reading Time: 00:00
Progress: 0%
`; this.addStyles(); this.setupEventListeners(); } addStyles() { const style = document.createElement('style'); style.textContent = ` .story-reader-wrapper { max-width: 800px; margin: 0 auto; padding: 20px; font-family: 'Georgia', serif; line-height: 1.6; } .story-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 2px solid #e2e8f0; } .story-title h2 { margin: 0 0 10px 0; color: #2d3748; font-size: 1.8em; } .reading-progress { display: flex; align-items: center; gap: 10px; } .progress-bar { width: 200px; height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #10b981); width: 0%; transition: width 0.3s ease; } .story-controls { display: flex; gap: 10px; } .settings-panel { background: #f7fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 15px; margin-bottom: 20px; } .setting-group { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .setting-group label { font-weight: 600; min-width: 100px; } .chapter-info { background: #edf2f7; padding: 10px 15px; border-radius: 6px; margin-bottom: 20px; font-style: italic; color: #4a5568; } .reading-area { position: relative; background: white; border: 2px solid #e2e8f0; border-radius: 12px; padding: 30px; margin-bottom: 20px; min-height: 200px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); } .sentence-display { text-align: center; } .original-text { font-size: 1.2em; color: #2d3748; margin-bottom: 15px; cursor: pointer; padding: 15px; border-radius: 8px; transition: background-color 0.2s; } .original-text:hover { background-color: #f7fafc; } .original-text.small { font-size: 1em; } .original-text.medium { font-size: 1.2em; } .original-text.large { font-size: 1.4em; } .original-text.extra-large { font-size: 1.6em; } .translation-text { font-style: italic; color: #718096; font-size: 1em; padding: 10px; background: #f0fff4; border-radius: 6px; border-left: 4px solid #10b981; } .clickable-word { cursor: pointer; padding: 2px 4px; border-radius: 3px; transition: background-color 0.2s; position: relative; display: inline-block; } .clickable-word:hover { background-color: #fef5e7; color: #d69e2e; } .word-with-pronunciation { position: relative; display: inline-block; margin: 0 2px; vertical-align: top; line-height: 1.8; } .pronunciation-text { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 0.7em; color: #718096; font-style: italic; white-space: nowrap; pointer-events: none; z-index: 10; display: none; } .pronunciation-text.show { display: block; } .reading-area { position: relative; background: white; border: 2px solid #e2e8f0; border-radius: 12px; padding: 40px 30px 30px 30px; margin-bottom: 20px; min-height: 200px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); line-height: 2.2; } .word-popup { position: fixed; background: white; border: 2px solid #3b82f6; border-radius: 6px; padding: 8px 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); z-index: 9999; max-width: 200px; min-width: 120px; font-size: 0.9em; line-height: 1.3; } .word-original { font-weight: bold; color: #2d3748; font-size: 1em; margin-bottom: 3px; } .word-translation { color: #10b981; font-size: 0.9em; margin-bottom: 2px; } .word-type { font-size: 0.75em; color: #718096; font-style: italic; } .story-navigation { display: flex; justify-content: center; gap: 15px; margin-bottom: 20px; } .nav-btn { padding: 12px 24px; border: 2px solid #e2e8f0; background: white; border-radius: 8px; cursor: pointer; font-size: 1em; transition: all 0.2s; } .nav-btn:hover:not(:disabled) { background: #f7fafc; border-color: #cbd5e0; } .nav-btn:disabled { opacity: 0.5; cursor: not-allowed; } .nav-btn.primary { background: #3b82f6; color: white; border-color: #3b82f6; } .nav-btn.primary:hover { background: #2563eb; } .reading-stats { display: flex; justify-content: space-around; background: #f7fafc; padding: 15px; border-radius: 8px; border: 1px solid #e2e8f0; } .stat { text-align: center; } .stat-label { display: block; font-size: 0.9em; color: #718096; margin-bottom: 5px; } .stat-value { display: block; font-weight: bold; font-size: 1.1em; color: #2d3748; } @media (max-width: 768px) { .story-reader-wrapper { padding: 10px; } .story-header { flex-direction: column; gap: 15px; } .reading-stats { flex-direction: column; gap: 10px; } } `; document.head.appendChild(style); } setupEventListeners() { // Navigation document.getElementById('prev-btn').addEventListener('click', () => this.previousSentence()); document.getElementById('next-btn').addEventListener('click', () => this.nextSentence()); document.getElementById('bookmark-btn').addEventListener('click', () => this.saveBookmark()); // Controls document.getElementById('settings-btn').addEventListener('click', () => this.toggleSettings()); document.getElementById('toggle-translation-btn').addEventListener('click', () => this.toggleTranslations()); document.getElementById('pronunciation-toggle-btn').addEventListener('click', () => this.togglePronunciations()); // Settings document.getElementById('font-size-select').addEventListener('change', (e) => this.changeFontSize(e.target.value)); document.getElementById('reading-mode-select').addEventListener('change', (e) => this.changeReadingMode(e.target.value)); // Keyboard shortcuts document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft') this.previousSentence(); if (e.key === 'ArrowRight') this.nextSentence(); if (e.key === 'Space') { e.preventDefault(); this.nextSentence(); } if (e.key === 't' || e.key === 'T') this.toggleTranslations(); }); // Click outside to close word popup document.addEventListener('click', (e) => { if (!e.target.closest('.word-popup') && !e.target.closest('.clickable-word')) { this.hideWordPopup(); } }); } getCurrentSentenceData() { let sentenceCount = 0; for (let chapterIndex = 0; chapterIndex < this.story.chapters.length; chapterIndex++) { const chapter = this.story.chapters[chapterIndex]; if (sentenceCount + chapter.sentences.length > this.currentSentence) { const sentenceInChapter = this.currentSentence - sentenceCount; return { chapter: chapterIndex, sentence: sentenceInChapter, data: chapter.sentences[sentenceInChapter], chapterTitle: chapter.title }; } sentenceCount += chapter.sentences.length; } return null; } // Match words from sentence with centralized vocabulary matchWordsWithVocabulary(sentence) { const words = sentence.split(/(\s+|[.,!?;:"'()[\]{}\-–—])/); const matchedWords = []; words.forEach(token => { // Skip whitespace and punctuation tokens if (/^\s+$/.test(token) || /^[.,!?;:"'()[\]{}\-–—]+$/.test(token)) { matchedWords.push({ original: token, hasVocab: false, isWhitespace: true }); return; } // Clean word (remove punctuation for matching) const cleanWord = token.toLowerCase().replace(/[.,!?;:"'()[\]{}\-–—]/g, ''); // Skip empty tokens if (!cleanWord) return; // Check if word exists in vocabulary (try exact match first, then stems) let vocabEntry = this.content.vocabulary[cleanWord]; // Try common variations if exact match not found if (!vocabEntry) { // Try without 's' for plurals if (cleanWord.endsWith('s')) { vocabEntry = this.content.vocabulary[cleanWord.slice(0, -1)]; } // Try without 'ed' for past tense if (!vocabEntry && cleanWord.endsWith('ed')) { vocabEntry = this.content.vocabulary[cleanWord.slice(0, -2)]; } // Try without 'ing' for present participle if (!vocabEntry && cleanWord.endsWith('ing')) { vocabEntry = this.content.vocabulary[cleanWord.slice(0, -3)]; } } if (vocabEntry) { // Word found in vocabulary matchedWords.push({ original: token, hasVocab: true, word: cleanWord, translation: vocabEntry.translation || vocabEntry.user_language, pronunciation: vocabEntry.pronunciation, type: vocabEntry.type || 'unknown' }); } else { // Word not in vocabulary - render as plain text matchedWords.push({ original: token, hasVocab: false }); } }); return matchedWords; } renderCurrentSentence() { const sentenceData = this.getCurrentSentenceData(); if (!sentenceData) return; const { data, chapterTitle } = sentenceData; // Update chapter info document.getElementById('chapter-info').innerHTML = ` ${chapterTitle} `; // Update progress const progress = ((this.currentSentence + 1) / this.totalSentences) * 100; document.getElementById('progress-fill').style.width = `${progress}%`; document.getElementById('progress-text').textContent = `Sentence ${this.currentSentence + 1} of ${this.totalSentences}`; document.getElementById('reading-percentage').textContent = `${Math.round(progress)}%`; // Check if sentence has word-by-word data (old format) or needs automatic matching let wordsHtml; if (data.words && data.words.length > 0) { // Old format with word-by-word data wordsHtml = data.words.map(wordData => { const pronunciation = wordData.pronunciation || ''; const pronunciationHtml = pronunciation ? `${pronunciation}` : ''; return ` ${pronunciationHtml} ${wordData.word} `; }).join(' '); } else { // New format with centralized vocabulary - use automatic matching const matchedWords = this.matchWordsWithVocabulary(data.original); wordsHtml = matchedWords.map(wordInfo => { if (wordInfo.isWhitespace) { return wordInfo.original; } else if (wordInfo.hasVocab) { const pronunciation = this.showPronunciation && wordInfo.pronunciation ? wordInfo.pronunciation : ''; const pronunciationHtml = pronunciation ? `${pronunciation}` : ''; return ` ${pronunciationHtml} ${wordInfo.original} `; } else { // No vocabulary entry - render as plain text return wordInfo.original; } }).join(''); } document.getElementById('original-text').innerHTML = wordsHtml; document.getElementById('translation-text').textContent = data.translation; // Add word click listeners document.querySelectorAll('.clickable-word').forEach(word => { word.addEventListener('click', (e) => this.showWordPopup(e)); }); // Update navigation buttons document.getElementById('prev-btn').disabled = this.currentSentence === 0; document.getElementById('next-btn').disabled = this.currentSentence >= this.totalSentences - 1; // Update stats this.updateStats(); } showWordPopup(event) { const word = event.target.dataset.word; const translation = event.target.dataset.translation; const type = event.target.dataset.type; const pronunciation = event.target.dataset.pronunciation; logSh(`πŸ” Word clicked: ${word}, translation: ${translation}`, 'DEBUG'); const popup = document.getElementById('word-popup'); if (!popup) { logSh('❌ Word popup element not found!', 'ERROR'); return; } document.getElementById('popup-word').textContent = word; document.getElementById('popup-translation').textContent = translation; // Show pronunciation in popup if available const typeText = pronunciation ? `${pronunciation} (${type})` : `(${type})`; document.getElementById('popup-type').textContent = typeText; // Position popup ABOVE the clicked word const rect = event.target.getBoundingClientRect(); popup.style.display = 'block'; // Center horizontally on the word, show above it const popupLeft = rect.left + (rect.width / 2) - 100; // Center popup (200px wide / 2) const popupTop = rect.top - 10; // Above the word with small gap popup.style.left = `${popupLeft}px`; popup.style.top = `${popupTop}px`; popup.style.transform = 'translateY(-100%)'; // Move up by its own height // Ensure popup stays within viewport if (popupLeft < 10) { popup.style.left = '10px'; } if (popupLeft + 200 > window.innerWidth) { popup.style.left = `${window.innerWidth - 210}px`; } if (popupTop - 80 < 10) { // If no room above, show below instead popup.style.top = `${rect.bottom + 10}px`; popup.style.transform = 'translateY(0)'; } logSh(`πŸ“ Popup positioned at: ${rect.left}px, ${rect.bottom + 10}px`, 'DEBUG'); } hideWordPopup() { document.getElementById('word-popup').style.display = 'none'; } previousSentence() { if (this.currentSentence > 0) { this.currentSentence--; this.renderCurrentSentence(); this.saveProgress(); } } nextSentence() { if (this.currentSentence < this.totalSentences - 1) { this.currentSentence++; this.renderCurrentSentence(); this.saveProgress(); } else { this.completeReading(); } } toggleSettings() { const panel = document.getElementById('settings-panel'); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; } toggleTranslations() { this.showTranslations = !this.showTranslations; const translationText = document.getElementById('translation-text'); translationText.style.display = this.showTranslations ? 'block' : 'none'; const btn = document.getElementById('toggle-translation-btn'); btn.textContent = this.showTranslations ? '🌐 Hide Translations' : '🌐 Show Translations'; } togglePronunciations() { this.showPronunciations = !this.showPronunciations; const pronunciations = document.querySelectorAll('.pronunciation-text'); pronunciations.forEach(pronunciation => { if (this.showPronunciations) { pronunciation.classList.add('show'); } else { pronunciation.classList.remove('show'); } }); const btn = document.getElementById('pronunciation-toggle-btn'); btn.textContent = this.showPronunciations ? 'πŸ”‡ Hide Pronunciations' : 'πŸ”Š Show Pronunciations'; } changeFontSize(size) { this.fontSize = size; document.getElementById('original-text').className = `original-text ${size}`; } changeReadingMode(mode) { this.readingMode = mode; // Mode implementation can be extended later } updateStats() { const sentenceData = this.getCurrentSentenceData(); if (sentenceData) { this.wordsRead += sentenceData.data.words.length; document.getElementById('words-read').textContent = this.wordsRead; } } saveProgress() { const progressData = { currentSentence: this.currentSentence, wordsRead: this.wordsRead, timestamp: Date.now() }; localStorage.setItem(`story_progress_${this.content.name}`, JSON.stringify(progressData)); } loadProgress() { const saved = localStorage.getItem(`story_progress_${this.content.name}`); if (saved) { const data = JSON.parse(saved); this.currentSentence = data.currentSentence || 0; this.wordsRead = data.wordsRead || 0; } } saveBookmark() { this.saveProgress(); const toast = document.createElement('div'); toast.textContent = 'πŸ”– Bookmark saved!'; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #10b981; color: white; padding: 10px 20px; border-radius: 6px; z-index: 1000; `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 2000); } completeReading() { this.onGameEnd(this.wordsRead); const completionMessage = `

πŸŽ‰ Story Complete!

You've finished reading "${this.story.title}"

Words read: ${this.wordsRead}

Total sentences: ${this.totalSentences}

`; document.getElementById('reading-area').innerHTML = completionMessage; } start() { logSh('πŸ“– Story Reader: Starting', 'INFO'); this.startReadingTimer(); } startReadingTimer() { this.startTime = Date.now(); this.readingTimer = setInterval(() => { this.updateReadingTime(); }, 1000); } updateReadingTime() { const currentTime = Date.now(); this.totalReadingTime = Math.floor((currentTime - this.startTime) / 1000); const minutes = Math.floor(this.totalReadingTime / 60); const seconds = this.totalReadingTime % 60; const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; document.getElementById('reading-time').textContent = timeString; } restart() { this.currentSentence = 0; this.wordsRead = 0; // Restart reading timer if (this.readingTimer) { clearInterval(this.readingTimer); } this.startReadingTimer(); this.renderCurrentSentence(); this.saveProgress(); } destroy() { // Clean up timer if (this.readingTimer) { clearInterval(this.readingTimer); } this.container.innerHTML = ''; } } // Module registration window.GameModules = window.GameModules || {}; window.GameModules.StoryReader = StoryReader;