- 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>
379 lines
13 KiB
JavaScript
379 lines
13 KiB
JavaScript
// === SCANNER AUTOMATIQUE DE CONTENU ===
|
|
|
|
class ContentScanner {
|
|
constructor() {
|
|
this.discoveredContent = new Map();
|
|
this.contentFiles = [
|
|
// Liste des fichiers de contenu à scanner automatiquement
|
|
'sbs-level-7-8-new.js',
|
|
'basic-chinese.js',
|
|
'english-class-demo.js'
|
|
];
|
|
}
|
|
|
|
async scanAllContent() {
|
|
console.log('🔍 ContentScanner - Scan automatique du contenu...');
|
|
|
|
const results = {
|
|
found: [],
|
|
errors: [],
|
|
total: 0
|
|
};
|
|
|
|
for (const filename of this.contentFiles) {
|
|
try {
|
|
const contentInfo = await this.scanContentFile(filename);
|
|
if (contentInfo) {
|
|
this.discoveredContent.set(contentInfo.id, contentInfo);
|
|
results.found.push(contentInfo);
|
|
}
|
|
} catch (error) {
|
|
console.warn(`⚠️ Erreur scan ${filename}:`, error.message);
|
|
results.errors.push({ filename, error: error.message });
|
|
}
|
|
}
|
|
|
|
results.total = results.found.length;
|
|
console.log(`✅ Scan terminé: ${results.total} modules trouvés`);
|
|
|
|
return results;
|
|
}
|
|
|
|
async scanContentFile(filename) {
|
|
const contentId = this.extractContentId(filename);
|
|
const moduleName = this.getModuleName(contentId);
|
|
|
|
try {
|
|
// Charger le script si pas déjà fait
|
|
await this.loadScript(`js/content/${filename}`);
|
|
|
|
// Vérifier si le module existe
|
|
if (!window.ContentModules || !window.ContentModules[moduleName]) {
|
|
throw new Error(`Module ${moduleName} non trouvé après chargement`);
|
|
}
|
|
|
|
const module = window.ContentModules[moduleName];
|
|
|
|
// Extraire les métadonnées
|
|
const contentInfo = this.extractContentInfo(module, contentId, filename);
|
|
|
|
console.log(`📦 Contenu découvert: ${contentInfo.name}`);
|
|
return contentInfo;
|
|
|
|
} catch (error) {
|
|
throw new Error(`Impossible de charger ${filename}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
extractContentInfo(module, contentId, filename) {
|
|
return {
|
|
id: contentId,
|
|
filename: filename,
|
|
name: module.name || this.beautifyContentId(contentId),
|
|
description: module.description || 'Contenu automatiquement détecté',
|
|
icon: this.getContentIcon(module, contentId),
|
|
difficulty: module.difficulty || 'medium',
|
|
enabled: true,
|
|
|
|
// Métadonnées détaillées
|
|
metadata: {
|
|
version: module.version || '1.0',
|
|
format: module.format || 'legacy',
|
|
totalItems: this.countItems(module),
|
|
categories: this.extractCategories(module),
|
|
contentTypes: this.extractContentTypes(module),
|
|
estimatedTime: this.calculateEstimatedTime(module),
|
|
lastScanned: new Date().toISOString()
|
|
},
|
|
|
|
// Statistiques
|
|
stats: {
|
|
vocabularyCount: this.countByType(module, 'vocabulary'),
|
|
sentenceCount: this.countByType(module, 'sentence'),
|
|
dialogueCount: this.countByType(module, 'dialogue'),
|
|
grammarCount: this.countByType(module, 'grammar')
|
|
},
|
|
|
|
// Configuration pour les jeux
|
|
gameCompatibility: this.analyzeGameCompatibility(module)
|
|
};
|
|
}
|
|
|
|
extractContentId(filename) {
|
|
return filename.replace('.js', '').toLowerCase();
|
|
}
|
|
|
|
getModuleName(contentId) {
|
|
const mapping = {
|
|
'sbs-level-7-8-new': 'SBSLevel78New'
|
|
};
|
|
return mapping[contentId] || this.toPascalCase(contentId);
|
|
}
|
|
|
|
toPascalCase(str) {
|
|
return str.split('-').map(word =>
|
|
word.charAt(0).toUpperCase() + word.slice(1)
|
|
).join('');
|
|
}
|
|
|
|
beautifyContentId(contentId) {
|
|
const beautified = {
|
|
'sbs-level-7-8-new': 'SBS Level 7-8 (Simple Format)'
|
|
};
|
|
return beautified[contentId] || contentId.charAt(0).toUpperCase() + contentId.slice(1);
|
|
}
|
|
|
|
getContentIcon(module, contentId) {
|
|
// Icône du module si disponible
|
|
if (module.icon) return module.icon;
|
|
|
|
// Icônes par défaut selon l'ID
|
|
const defaultIcons = {
|
|
'sbs-level-7-8-new': '✨'
|
|
};
|
|
|
|
return defaultIcons[contentId] || '📝';
|
|
}
|
|
|
|
countItems(module) {
|
|
let count = 0;
|
|
|
|
// Format moderne (contentItems)
|
|
if (module.contentItems && Array.isArray(module.contentItems)) {
|
|
return module.contentItems.length;
|
|
}
|
|
|
|
// Format simple (vocabulary object + sentences array)
|
|
if (module.vocabulary && typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) {
|
|
count += Object.keys(module.vocabulary).length;
|
|
}
|
|
// Format legacy (vocabulary array)
|
|
else if (module.vocabulary && Array.isArray(module.vocabulary)) {
|
|
count += module.vocabulary.length;
|
|
}
|
|
|
|
// Autres contenus
|
|
if (module.sentences && Array.isArray(module.sentences)) count += module.sentences.length;
|
|
if (module.dialogues && Array.isArray(module.dialogues)) count += module.dialogues.length;
|
|
if (module.phrases && Array.isArray(module.phrases)) count += module.phrases.length;
|
|
if (module.texts && Array.isArray(module.texts)) count += module.texts.length;
|
|
|
|
return count;
|
|
}
|
|
|
|
extractCategories(module) {
|
|
const categories = new Set();
|
|
|
|
if (module.categories) {
|
|
Object.keys(module.categories).forEach(cat => categories.add(cat));
|
|
}
|
|
|
|
if (module.metadata && module.metadata.categories) {
|
|
module.metadata.categories.forEach(cat => categories.add(cat));
|
|
}
|
|
|
|
// Extraire des contenus si format moderne
|
|
if (module.contentItems) {
|
|
module.contentItems.forEach(item => {
|
|
if (item.category) categories.add(item.category);
|
|
});
|
|
}
|
|
|
|
// Extraire du vocabulaire selon le format
|
|
if (module.vocabulary) {
|
|
// Format simple (vocabulary object)
|
|
if (typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) {
|
|
// Pour l'instant, pas de catégories dans le format simple
|
|
categories.add('vocabulary');
|
|
}
|
|
// Format legacy (vocabulary array)
|
|
else if (Array.isArray(module.vocabulary)) {
|
|
module.vocabulary.forEach(word => {
|
|
if (word.category) categories.add(word.category);
|
|
});
|
|
}
|
|
}
|
|
|
|
return Array.from(categories);
|
|
}
|
|
|
|
extractContentTypes(module) {
|
|
const types = new Set();
|
|
|
|
if (module.contentItems) {
|
|
module.contentItems.forEach(item => {
|
|
if (item.type) types.add(item.type);
|
|
});
|
|
} else {
|
|
// Format legacy - deviner les types
|
|
if (module.vocabulary) types.add('vocabulary');
|
|
if (module.sentences) types.add('sentence');
|
|
if (module.dialogues || module.dialogue) types.add('dialogue');
|
|
if (module.phrases) types.add('sentence');
|
|
}
|
|
|
|
return Array.from(types);
|
|
}
|
|
|
|
calculateEstimatedTime(module) {
|
|
if (module.metadata && module.metadata.estimatedTime) {
|
|
return module.metadata.estimatedTime;
|
|
}
|
|
|
|
// Calcul basique : 1 minute par 3 éléments
|
|
const itemCount = this.countItems(module);
|
|
return Math.max(5, Math.ceil(itemCount / 3));
|
|
}
|
|
|
|
countByType(module, type) {
|
|
if (module.contentItems) {
|
|
return module.contentItems.filter(item => item.type === type).length;
|
|
}
|
|
|
|
// Format simple et legacy
|
|
switch(type) {
|
|
case 'vocabulary':
|
|
// Format simple (vocabulary object)
|
|
if (module.vocabulary && typeof module.vocabulary === 'object' && !Array.isArray(module.vocabulary)) {
|
|
return Object.keys(module.vocabulary).length;
|
|
}
|
|
// Format legacy (vocabulary array)
|
|
return module.vocabulary ? module.vocabulary.length : 0;
|
|
|
|
case 'sentence':
|
|
return (module.sentences ? module.sentences.length : 0) +
|
|
(module.phrases ? module.phrases.length : 0);
|
|
|
|
case 'dialogue':
|
|
return module.dialogues ? module.dialogues.length : 0;
|
|
|
|
case 'grammar':
|
|
// Format simple (grammar object avec sous-propriétés)
|
|
if (module.grammar && typeof module.grammar === 'object') {
|
|
return Object.keys(module.grammar).length;
|
|
}
|
|
return module.grammar && Array.isArray(module.grammar) ? module.grammar.length : 0;
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
analyzeGameCompatibility(module) {
|
|
const compatibility = {
|
|
'whack-a-mole': { compatible: false, score: 0 },
|
|
'memory-game': { compatible: false, score: 0 },
|
|
'story-builder': { compatible: false, score: 0 },
|
|
'temp-games': { compatible: false, score: 0 }
|
|
};
|
|
|
|
const vocabCount = this.countByType(module, 'vocabulary');
|
|
const sentenceCount = this.countByType(module, 'sentence');
|
|
const dialogueCount = this.countByType(module, 'dialogue');
|
|
|
|
// Whack-a-Mole - aime le vocabulaire et phrases simples
|
|
if (vocabCount > 5 || sentenceCount > 3) {
|
|
compatibility['whack-a-mole'].compatible = true;
|
|
compatibility['whack-a-mole'].score = Math.min(100, vocabCount * 5 + sentenceCount * 3);
|
|
}
|
|
|
|
// Memory Game - parfait pour vocabulaire avec images
|
|
if (vocabCount > 4) {
|
|
compatibility['memory-game'].compatible = true;
|
|
compatibility['memory-game'].score = Math.min(100, vocabCount * 8);
|
|
}
|
|
|
|
// Story Builder - aime les dialogues et séquences
|
|
if (dialogueCount > 0 || sentenceCount > 5) {
|
|
compatibility['story-builder'].compatible = true;
|
|
compatibility['story-builder'].score = Math.min(100, dialogueCount * 15 + sentenceCount * 2);
|
|
}
|
|
|
|
// Temp Games - accepte tout
|
|
if (this.countItems(module) > 3) {
|
|
compatibility['temp-games'].compatible = true;
|
|
compatibility['temp-games'].score = Math.min(100, this.countItems(module) * 2);
|
|
}
|
|
|
|
return compatibility;
|
|
}
|
|
|
|
async loadScript(src) {
|
|
return new Promise((resolve, reject) => {
|
|
// Vérifier si déjà chargé
|
|
const existingScript = document.querySelector(`script[src="${src}"]`);
|
|
if (existingScript) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const script = document.createElement('script');
|
|
script.src = src;
|
|
script.onload = resolve;
|
|
script.onerror = () => reject(new Error(`Impossible de charger ${src}`));
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
// === API PUBLIQUE ===
|
|
|
|
async getAvailableContent() {
|
|
if (this.discoveredContent.size === 0) {
|
|
await this.scanAllContent();
|
|
}
|
|
return Array.from(this.discoveredContent.values());
|
|
}
|
|
|
|
async getContentById(id) {
|
|
if (this.discoveredContent.size === 0) {
|
|
await this.scanAllContent();
|
|
}
|
|
return this.discoveredContent.get(id);
|
|
}
|
|
|
|
async getContentByGame(gameType) {
|
|
const allContent = await this.getAvailableContent();
|
|
|
|
return allContent.filter(content => {
|
|
const compat = content.gameCompatibility[gameType];
|
|
return compat && compat.compatible;
|
|
}).sort((a, b) => {
|
|
// Trier par score de compatibilité
|
|
const scoreA = a.gameCompatibility[gameType].score;
|
|
const scoreB = b.gameCompatibility[gameType].score;
|
|
return scoreB - scoreA;
|
|
});
|
|
}
|
|
|
|
async refreshContent() {
|
|
this.discoveredContent.clear();
|
|
return await this.scanAllContent();
|
|
}
|
|
|
|
getContentStats() {
|
|
const stats = {
|
|
totalModules: this.discoveredContent.size,
|
|
totalItems: 0,
|
|
categories: new Set(),
|
|
contentTypes: new Set(),
|
|
difficulties: new Set()
|
|
};
|
|
|
|
for (const content of this.discoveredContent.values()) {
|
|
stats.totalItems += content.metadata.totalItems;
|
|
content.metadata.categories.forEach(cat => stats.categories.add(cat));
|
|
content.metadata.contentTypes.forEach(type => stats.contentTypes.add(type));
|
|
stats.difficulties.add(content.difficulty);
|
|
}
|
|
|
|
return {
|
|
...stats,
|
|
categories: Array.from(stats.categories),
|
|
contentTypes: Array.from(stats.contentTypes),
|
|
difficulties: Array.from(stats.difficulties)
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export global
|
|
window.ContentScanner = ContentScanner; |