// === MODULE TEXT READER ===
class TextReaderGame {
constructor(options) {
this.container = options.container;
this.content = options.content;
this.onScoreUpdate = options.onScoreUpdate || (() => {});
this.onGameEnd = options.onGameEnd || (() => {});
// Reader state
this.currentTextIndex = 0;
this.currentSentenceIndex = 0;
this.isRunning = false;
// Reading data
this.texts = this.extractTexts(this.content);
this.sentencesFromContent = this.extractSentences(this.content);
this.vocabulary = this.extractVocabulary(this.content);
this.currentText = null;
this.sentences = [];
this.showingFullText = false;
this.init();
}
init() {
// Check that we have texts, sentences, or vocabulary
if ((!this.texts || this.texts.length === 0) &&
(!this.sentencesFromContent || this.sentencesFromContent.length === 0) &&
(!this.vocabulary || this.vocabulary.length === 0)) {
logSh('No texts, sentences, or vocabulary available for Text Reader', 'ERROR');
this.showInitError();
return;
}
this.createReaderInterface();
this.setupEventListeners();
this.loadText();
}
showInitError() {
this.container.innerHTML = `
β Error loading
This content doesn't contain texts, sentences, or vocabulary compatible with Text Reader.
The reader needs content with ultra-modular format (original_language/user_language).
`;
}
extractTexts(content) {
let texts = [];
logSh('π Extracting texts from:', content?.name || 'content', 'INFO');
// Priority 1: Use raw module content (simple format)
if (content.rawContent) {
logSh('π¦ Using raw module content', 'INFO');
return this.extractTextsFromRaw(content.rawContent);
}
// Priority 2: Ultra-modular format (texts array) - ONLY format supported
if (content.texts && Array.isArray(content.texts)) {
logSh('β¨ Ultra-modular format detected (texts array)', 'INFO');
texts = content.texts
.filter(text => text && text.original_language && text.user_language)
.map(text => ({
id: text.id || `text_${Date.now()}_${Math.random()}`,
title: text.title || text.id || 'Text',
original: text.original_language,
translation: text.user_language
}));
logSh(`β¨ ${texts.length} texts extracted from ultra-modular format`, 'INFO');
}
return this.finalizeTexts(texts);
}
extractTextsFromRaw(rawContent) {
logSh('π§ Extracting from raw content:', rawContent.name || 'Module', 'INFO');
let texts = [];
// Ultra-modular format (texts array) - ONLY format supported
if (rawContent.texts && Array.isArray(rawContent.texts)) {
texts = rawContent.texts
.filter(text => text && text.original_language && text.user_language)
.map(text => ({
id: text.id || `text_${Date.now()}_${Math.random()}`,
title: text.title || text.id || 'Text',
original: text.original_language,
translation: text.user_language
}));
logSh(`β¨ ${texts.length} texts extracted from ultra-modular format`, 'INFO');
}
return this.finalizeTexts(texts);
}
finalizeTexts(texts) {
// Validation and cleanup
texts = texts.filter(text =>
text &&
typeof text.original === 'string' &&
text.original.trim() !== '' &&
typeof text.translation === 'string' &&
text.translation.trim() !== ''
);
if (texts.length === 0) {
logSh('β No valid texts found', 'ERROR');
// Demo texts as fallback
texts = [
{
id: "demo_text_1",
title: "Demo Text",
original: "This is a demo text. It has multiple sentences. Each sentence will be displayed one by one. You can navigate using the buttons below.",
translation: "Ceci est un texte de dΓ©monstration. Il contient plusieurs phrases. Chaque phrase sera affichΓ©e une par une. Vous pouvez naviguer avec les boutons ci-dessous."
}
];
logSh('π¨ Using demo texts', 'WARN');
}
logSh(`β
Text Reader: ${texts.length} texts finalized`, 'INFO');
return texts;
}
extractSentences(content) {
let sentences = [];
logSh('π Extracting sentences from:', content?.name || 'content', 'INFO');
// Priority 1: Use raw module content
if (content.rawContent) {
logSh('π¦ Using raw module content for sentences', 'INFO');
return this.extractSentencesFromRaw(content.rawContent);
}
// Priority 2: Ultra-modular format (sentences array) - ONLY format supported
if (content.sentences && Array.isArray(content.sentences)) {
logSh('β¨ Ultra-modular format detected (sentences array)', 'INFO');
sentences = content.sentences
.filter(sentence => sentence && sentence.original_language && sentence.user_language)
.map(sentence => ({
id: sentence.id || `sentence_${Date.now()}_${Math.random()}`,
original: sentence.original_language,
translation: sentence.user_language
}));
logSh(`β¨ ${sentences.length} sentences extracted from ultra-modular format`, 'INFO');
}
return this.finalizeSentences(sentences);
}
extractSentencesFromRaw(rawContent) {
logSh('π§ Extracting sentences from raw content:', rawContent.name || 'Module', 'INFO');
let sentences = [];
// Ultra-modular format (sentences array) - ONLY format supported
if (rawContent.sentences && Array.isArray(rawContent.sentences)) {
sentences = rawContent.sentences
.filter(sentence => sentence && sentence.original_language && sentence.user_language)
.map(sentence => ({
id: sentence.id || `sentence_${Date.now()}_${Math.random()}`,
original: sentence.original_language,
translation: sentence.user_language
}));
logSh(`β¨ ${sentences.length} sentences extracted from ultra-modular format`, 'INFO');
}
return this.finalizeSentences(sentences);
}
finalizeSentences(sentences) {
// Validation and cleanup
sentences = sentences.filter(sentence =>
sentence &&
typeof sentence.original === 'string' &&
sentence.original.trim() !== '' &&
typeof sentence.translation === 'string' &&
sentence.translation.trim() !== ''
);
logSh(`β
Text Reader: ${sentences.length} sentences finalized`, 'INFO');
return sentences;
}
extractVocabulary(content) {
let vocabulary = [];
logSh('π Extracting vocabulary from:', content?.name || 'content', 'INFO');
// Priority 1: Use raw module content (simple format)
if (content.rawContent) {
logSh('π¦ Using raw module content for vocabulary', 'INFO');
return this.extractVocabularyFromRaw(content.rawContent);
}
// Priority 2: Ultra-modular format (vocabulary object) - ONLY format supported
if (content.vocabulary && typeof content.vocabulary === 'object' && !Array.isArray(content.vocabulary)) {
logSh('β¨ Ultra-modular format detected (vocabulary object)', 'INFO');
vocabulary = Object.entries(content.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
original: word, // ClΓ© = original_language
translation: data.user_language,
type: data.type || 'unknown'
};
}
return null;
}).filter(item => item !== null);
logSh(`β¨ ${vocabulary.length} vocabulary words extracted from ultra-modular format`, 'INFO');
}
return this.finalizeVocabulary(vocabulary);
}
extractVocabularyFromRaw(rawContent) {
logSh('π§ Extracting vocabulary from raw content:', rawContent.name || 'Module', 'INFO');
let vocabulary = [];
// Ultra-modular format (vocabulary object) - ONLY format supported
if (rawContent.vocabulary && typeof rawContent.vocabulary === 'object' && !Array.isArray(rawContent.vocabulary)) {
vocabulary = Object.entries(rawContent.vocabulary).map(([word, data]) => {
// Support ultra-modular format ONLY
if (typeof data === 'object' && data.user_language) {
return {
original: word, // ClΓ© = original_language
translation: data.user_language,
type: data.type || 'unknown'
};
}
return null;
}).filter(item => item !== null);
logSh(`β¨ ${vocabulary.length} vocabulary words extracted from ultra-modular format`, 'INFO');
}
return this.finalizeVocabulary(vocabulary);
}
finalizeVocabulary(vocabulary) {
// Validation and cleanup
vocabulary = vocabulary.filter(word =>
word &&
typeof word.original === 'string' &&
word.original.trim() !== '' &&
typeof word.translation === 'string' &&
word.translation.trim() !== ''
);
logSh(`β
Text Reader: ${vocabulary.length} vocabulary words finalized`, 'INFO');
return vocabulary;
}
createReaderInterface() {
this.container.innerHTML = `
Use Next/Previous buttons to navigate through sentences
`;
}
setupEventListeners() {
document.getElementById('next-sentence-btn').addEventListener('click', () => this.nextSentence());
document.getElementById('prev-sentence-btn').addEventListener('click', () => this.prevSentence());
document.getElementById('show-full-btn').addEventListener('click', () => this.showFullText());
document.getElementById('back-to-reading-btn').addEventListener('click', () => this.backToReading());
document.getElementById('text-selector').addEventListener('change', (e) => this.selectText(parseInt(e.target.value)));
// Keyboard navigation
document.addEventListener('keydown', (e) => {
if (!this.isRunning) return;
if (this.showingFullText) {
if (e.key === 'Escape') this.backToReading();
} else {
if (e.key === 'ArrowLeft') this.prevSentence();
else if (e.key === 'ArrowRight') this.nextSentence();
else if (e.key === 'Enter' || e.key === ' ') this.showFullText();
}
});
}
start() {
logSh('π Text Reader: Starting', 'INFO');
this.isRunning = true;
}
restart() {
logSh('π Text Reader: Restarting', 'INFO');
this.reset();
this.start();
}
reset() {
this.currentTextIndex = 0;
this.currentSentenceIndex = 0;
this.isRunning = false;
this.showingFullText = false;
this.loadText();
}
loadText() {
// Use texts if available, otherwise use individual sentences, then vocabulary
if (this.texts && this.texts.length > 0) {
if (this.currentTextIndex >= this.texts.length) {
this.currentTextIndex = 0;
}
this.currentText = this.texts[this.currentTextIndex];
this.sentences = this.splitIntoSentences(this.currentText.original);
this.currentSentenceIndex = 0;
this.showingFullText = false;
this.populateTextSelector();
} else if (this.sentencesFromContent && this.sentencesFromContent.length > 0) {
// Use individual sentences as "mini-texts"
this.texts = this.sentencesFromContent.map((sentence, index) => ({
id: sentence.id || `sentence_text_${index}`,
title: `Sentence ${index + 1}`,
original: sentence.original,
translation: sentence.translation
}));
this.currentText = this.texts[0];
this.sentences = [this.currentText.original]; // Single sentence
this.currentSentenceIndex = 0;
this.showingFullText = false;
this.populateTextSelector();
} else if (this.vocabulary && this.vocabulary.length > 0) {
// Use vocabulary words as "mini-texts"
this.texts = this.vocabulary.map((word, index) => ({
id: `vocab_text_${index}`,
title: `Word: ${word.original}`,
original: word.original,
translation: word.translation
}));
this.currentText = this.texts[0];
this.sentences = [this.currentText.original]; // Single word
this.currentSentenceIndex = 0;
this.showingFullText = false;
this.populateTextSelector();
}
this.updateDisplay();
this.updateUI();
}
populateTextSelector() {
const selector = document.getElementById('text-selector');
selector.innerHTML = '';
this.texts.forEach((text, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = text.title || `Text ${index + 1}`;
if (index === this.currentTextIndex) {
option.selected = true;
}
selector.appendChild(option);
});
}
selectText(textIndex) {
if (textIndex >= 0 && textIndex < this.texts.length) {
this.currentTextIndex = textIndex;
this.currentText = this.texts[this.currentTextIndex];
this.sentences = this.splitIntoSentences(this.currentText.original);
this.currentSentenceIndex = 0;
// Always go back to sentence reading when changing text
if (this.showingFullText) {
this.backToReading();
} else {
this.updateDisplay();
this.updateUI();
}
this.showFeedback(`Switched to: ${this.currentText.title}`, 'info');
}
}
splitIntoSentences(text) {
// Split by periods, exclamation marks, and question marks
// Keep the punctuation with the sentence
const sentences = text.split(/(?<=[.!?])\s+/)
.filter(sentence => sentence.trim() !== '')
.map(sentence => sentence.trim());
return sentences.length > 0 ? sentences : [text];
}
nextSentence() {
if (this.currentSentenceIndex < this.sentences.length - 1) {
this.currentSentenceIndex++;
this.updateDisplay();
this.updateUI();
} else {
// End of sentences, show full text automatically
this.showFullText();
}
}
prevSentence() {
if (this.currentSentenceIndex > 0) {
this.currentSentenceIndex--;
this.updateDisplay();
this.updateUI();
}
}
showFullText() {
this.showingFullText = true;
document.getElementById('sentence-display').style.display = 'none';
document.getElementById('full-text-display').style.display = 'block';
document.getElementById('full-text-display').innerHTML = `
Original:
${this.currentText.original}
Translation:
${this.currentText.translation}
`;
// Show full text navigation controls
document.querySelector('.reader-controls').style.display = 'none';
document.getElementById('full-text-navigation').style.display = 'flex';
this.showFeedback('Full text displayed. Use dropdown to change text.', 'info');
}
backToReading() {
this.showingFullText = false;
document.getElementById('sentence-display').style.display = 'block';
document.getElementById('full-text-display').style.display = 'none';
// Show sentence navigation controls
document.querySelector('.reader-controls').style.display = 'flex';
document.getElementById('full-text-navigation').style.display = 'none';
this.updateDisplay();
this.updateUI();
this.showFeedback('Back to sentence-by-sentence reading.', 'info');
}
// Text navigation methods removed - using dropdown instead
updateDisplay() {
if (this.showingFullText) return;
const sentenceDisplay = document.getElementById('sentence-display');
const currentSentence = this.sentences[this.currentSentenceIndex];
sentenceDisplay.innerHTML = `