- Complete SPA architecture with dynamic module loading - 9 different educational games (whack-a-mole, memory, quiz, etc.) - Rich content system supporting multimedia (audio, images, video) - Chinese study mode with character recognition - Adaptive game system based on available content - Content types: vocabulary, grammar, poems, fill-blanks, corrections - AI-powered text evaluation for open-ended answers - Flexible content schema with backward compatibility 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
367 lines
13 KiB
JavaScript
367 lines
13 KiB
JavaScript
// === MODULE TEXT READER ===
|
|
|
|
class TextReaderGame {
|
|
constructor(options) {
|
|
this.container = options.container;
|
|
this.content = options.content;
|
|
this.onScoreUpdate = options.onScoreUpdate || (() => {});
|
|
this.onGameEnd = options.onGameEnd || (() => {});
|
|
|
|
// État du lecteur
|
|
this.currentTextIndex = 0;
|
|
this.currentSentenceIndex = 0;
|
|
this.isRunning = false;
|
|
|
|
// Données de lecture
|
|
this.texts = this.extractTexts(this.content);
|
|
this.currentText = null;
|
|
this.sentences = [];
|
|
this.showingFullText = false;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
// Vérifier que nous avons des textes
|
|
if (!this.texts || this.texts.length === 0) {
|
|
console.error('Aucun texte disponible pour Text Reader');
|
|
this.showInitError();
|
|
return;
|
|
}
|
|
|
|
this.createReaderInterface();
|
|
this.setupEventListeners();
|
|
this.loadText();
|
|
}
|
|
|
|
showInitError() {
|
|
this.container.innerHTML = `
|
|
<div class="game-error">
|
|
<h3>❌ Error loading</h3>
|
|
<p>This content doesn't contain texts compatible with Text Reader.</p>
|
|
<p>The reader needs texts to display.</p>
|
|
<button onclick="AppNavigation.goBack()" class="back-btn">← Back</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
extractTexts(content) {
|
|
let texts = [];
|
|
|
|
console.log('📖 Extracting texts from:', content?.name || 'content');
|
|
|
|
// Use raw module content if available
|
|
if (content.rawContent) {
|
|
console.log('📦 Using raw module content');
|
|
return this.extractTextsFromRaw(content.rawContent);
|
|
}
|
|
|
|
// Format with texts array
|
|
if (content.texts && Array.isArray(content.texts)) {
|
|
console.log('📝 Texts format detected');
|
|
texts = content.texts.filter(text =>
|
|
text.content && text.content.trim() !== ''
|
|
);
|
|
}
|
|
// Modern format with contentItems
|
|
else if (content.contentItems && Array.isArray(content.contentItems)) {
|
|
console.log('🆕 ContentItems format detected');
|
|
texts = content.contentItems
|
|
.filter(item => item.type === 'text' && item.content)
|
|
.map(item => ({
|
|
title: item.title || 'Text',
|
|
content: item.content
|
|
}));
|
|
}
|
|
|
|
return this.finalizeTexts(texts);
|
|
}
|
|
|
|
extractTextsFromRaw(rawContent) {
|
|
console.log('🔧 Extracting from raw content:', rawContent.name || 'Module');
|
|
let texts = [];
|
|
|
|
// Simple format (texts array)
|
|
if (rawContent.texts && Array.isArray(rawContent.texts)) {
|
|
texts = rawContent.texts.filter(text =>
|
|
text.content && text.content.trim() !== ''
|
|
);
|
|
console.log(`📝 ${texts.length} texts extracted from texts array`);
|
|
}
|
|
// ContentItems format
|
|
else if (rawContent.contentItems && Array.isArray(rawContent.contentItems)) {
|
|
texts = rawContent.contentItems
|
|
.filter(item => item.type === 'text' && item.content)
|
|
.map(item => ({
|
|
title: item.title || 'Text',
|
|
content: item.content
|
|
}));
|
|
console.log(`🆕 ${texts.length} texts extracted from contentItems`);
|
|
}
|
|
|
|
return this.finalizeTexts(texts);
|
|
}
|
|
|
|
finalizeTexts(texts) {
|
|
// Validation and cleanup
|
|
texts = texts.filter(text =>
|
|
text &&
|
|
typeof text.content === 'string' &&
|
|
text.content.trim() !== ''
|
|
);
|
|
|
|
if (texts.length === 0) {
|
|
console.error('❌ No valid texts found');
|
|
// Demo texts as fallback
|
|
texts = [
|
|
{
|
|
title: "Demo Text",
|
|
content: "This is a demo text. It has multiple sentences. Each sentence will be displayed one by one. You can navigate using the buttons below."
|
|
}
|
|
];
|
|
console.warn('🚨 Using demo texts');
|
|
}
|
|
|
|
console.log(`✅ Text Reader: ${texts.length} texts finalized`);
|
|
return texts;
|
|
}
|
|
|
|
createReaderInterface() {
|
|
this.container.innerHTML = `
|
|
<div class="text-reader-wrapper">
|
|
<!-- Text Selection -->
|
|
<div class="text-selection">
|
|
<label for="text-selector" class="text-selector-label">Choose a text:</label>
|
|
<select id="text-selector" class="text-selector">
|
|
<!-- Options will be generated here -->
|
|
</select>
|
|
<div class="text-progress">
|
|
<span id="sentence-counter">1 / 1</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reading Area -->
|
|
<div class="reading-area" id="reading-area">
|
|
<div class="sentence-display" id="sentence-display">
|
|
<!-- Current sentence will appear here -->
|
|
</div>
|
|
|
|
<div class="full-text-display" id="full-text-display" style="display: none;">
|
|
<!-- Full text will appear here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Controls -->
|
|
<div class="reader-controls">
|
|
<button class="control-btn secondary" id="prev-sentence-btn" disabled>← Previous</button>
|
|
<button class="control-btn primary" id="next-sentence-btn">Next →</button>
|
|
<button class="control-btn secondary" id="show-full-btn">📄 Full Text</button>
|
|
</div>
|
|
|
|
<!-- Full Text Navigation -->
|
|
<div class="full-text-navigation" id="full-text-navigation" style="display: none;">
|
|
<button class="control-btn secondary" id="back-to-reading-btn">📖 Back to Reading</button>
|
|
</div>
|
|
|
|
<!-- Feedback Area -->
|
|
<div class="feedback-area" id="feedback-area">
|
|
<div class="instruction">
|
|
Use Next/Previous buttons to navigate through sentences
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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() {
|
|
console.log('📖 Text Reader: Starting');
|
|
this.isRunning = true;
|
|
}
|
|
|
|
restart() {
|
|
console.log('🔄 Text Reader: Restarting');
|
|
this.reset();
|
|
this.start();
|
|
}
|
|
|
|
reset() {
|
|
this.currentTextIndex = 0;
|
|
this.currentSentenceIndex = 0;
|
|
this.isRunning = false;
|
|
this.showingFullText = false;
|
|
this.loadText();
|
|
}
|
|
|
|
loadText() {
|
|
if (this.currentTextIndex >= this.texts.length) {
|
|
this.currentTextIndex = 0;
|
|
}
|
|
|
|
this.currentText = this.texts[this.currentTextIndex];
|
|
this.sentences = this.splitIntoSentences(this.currentText.content);
|
|
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.content);
|
|
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 = `
|
|
<div class="full-text-content">
|
|
<p>${this.currentText.content}</p>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = `
|
|
<div class="current-sentence">
|
|
${currentSentence}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
updateUI() {
|
|
// Update counters
|
|
document.getElementById('sentence-counter').textContent = `${this.currentSentenceIndex + 1} / ${this.sentences.length}`;
|
|
|
|
// Update button states
|
|
document.getElementById('prev-sentence-btn').disabled = this.currentSentenceIndex === 0;
|
|
document.getElementById('next-sentence-btn').disabled = false;
|
|
document.getElementById('next-sentence-btn').textContent =
|
|
this.currentSentenceIndex === this.sentences.length - 1 ? 'Full Text →' : 'Next →';
|
|
}
|
|
|
|
// updateTextNavigation method removed - using dropdown instead
|
|
|
|
showFeedback(message, type = 'info') {
|
|
const feedbackArea = document.getElementById('feedback-area');
|
|
feedbackArea.innerHTML = `<div class="instruction ${type}">${message}</div>`;
|
|
}
|
|
|
|
destroy() {
|
|
this.isRunning = false;
|
|
this.container.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
// Module registration
|
|
window.GameModules = window.GameModules || {};
|
|
window.GameModules.TextReader = TextReaderGame; |