// === SCANNER AUTOMATIQUE DE CONTENU === class ContentScanner { constructor() { this.discoveredContent = new Map(); this.contentDirectory = 'js/content/'; // Configuration depuis EnvConfig this.envConfig = window.envConfig; logSh('🔧 ContentScanner configuré avec:', 'INFO'); } async scanAllContent() { logSh('🔍 ContentScanner - Scan automatique du dossier content...', 'INFO'); const results = { found: [], errors: [], total: 0 }; try { // Découvrir tous les fichiers .js dans le dossier content const contentFiles = await this.discoverContentFiles(); logSh(`📁 Fichiers trouvés: ${contentFiles.join(', ')}`, 'INFO'); for (const filename of contentFiles) { try { logSh(`🔍 Scanning content file: ${filename}`, 'DEBUG'); const contentInfo = await this.scanContentFile(filename); if (contentInfo) { logSh(`✅ Successfully scanned: ${contentInfo.id} (${contentInfo.name})`, 'INFO'); this.discoveredContent.set(contentInfo.id, contentInfo); results.found.push(contentInfo); } else { logSh(`⚠️ scanContentFile returned null for ${filename}`, 'WARN'); } } catch (error) { logSh(`⚠️ Erreur scan ${filename}: ${error.message}`, 'WARN'); results.errors.push({ filename, error: error.message }); } } } catch (error) { logSh('❌ Erreur lors de la découverte des fichiers:', error.message, 'ERROR'); results.errors.push({ error: `Scan directory failed: ${error.message}` }); } results.total = results.found.length; // Analyser les capacités globales du contenu découvert this.analyzeGlobalCapabilities(results.found); logSh(`✅ Scan terminé: ${results.total} modules trouvés`, 'INFO'); return results; } async discoverContentFiles() { // Détecter si on est en mode file:// ou serveur web const isFileProtocol = window.location.protocol === 'file:'; if (isFileProtocol) { logSh('📂 Mode fichier local - chargement des fichiers connus', 'INFO'); // D'abord essayer de charger les fichiers connus await this.preloadKnownFiles(); // Puis scanner les modules chargés return this.scanLoadedModules(); } let allFiles = []; // Méthode 1: Récupérer le listing local try { const response = await fetch(this.contentDirectory); if (response.ok) { const html = await response.text(); // Parser les liens .js dans le HTML du directory listing const jsFiles = this.parseDirectoryListing(html); if (jsFiles.length > 0) { logSh('📂 Méthode directory listing réussie', 'INFO'); allFiles = [...jsFiles]; } } } catch (error) { logSh('📂 Directory listing failed', 'INFO'); } // Méthode 2: Toujours essayer les fichiers distants connus logSh('📂 Tentative de scan des fichiers distants...', 'INFO'); const commonFiles = await this.tryCommonFiles(); // Combiner les résultats locaux et distants sans doublons for (const file of commonFiles) { if (!allFiles.includes(file)) { allFiles.push(file); } } logSh(`📁 Fichiers trouvés: ${allFiles.join(', ')}`, 'INFO'); return allFiles; } async preloadKnownFiles() { const knownFiles = [ 'sbs-level-7-8-new.js', // Local JS file 'SBS-level-1.js', // Side by Side Level 1 - English introduction with Chinese translation 'english-class-demo.json', // Remote JSON file 'english-exemple-commented.js', // Module JS complet nouvellement créé 'story-test.js', // Story test module 'story-prototype-1000words.js', // 1000-word story prototype 'story-complete-1000words.js', // Complete 1000-word story with pronunciation 'chinese-long-story.js', // Chinese story with English translation and pinyin 'french-beginner-story.js', // French beginner story for English speakers 'WTA1B1.js', // English letters and pets story with Chinese translation 'story-prototype-optimized.js', // Optimized story with centralized vocabulary 'test-compatibility.js', // Test content for compatibility system 'test-minimal.js', // Minimal test content 'test-rich.js', // Rich test content 'NCE1-Lesson63-64.js', // New Concept English Book 1 - Lessons 63-64 'NCE2-Lesson3.js', // New Concept English Book 2 - Lesson 3 'NCE2-Lesson30.js' // New Concept English Book 2 - Lesson 30 ]; logSh('📂 Préchargement des fichiers connus...', 'INFO'); this.updateConnectionStatus('loading'); let remoteSuccess = 0; let localSuccess = 0; for (const filename of knownFiles) { try { if (filename.endsWith('.json')) { // Essayer d'abord le contenu distant, puis local en fallback const success = await this.loadJsonWithFallback(filename); if (success === 'remote') remoteSuccess++; else if (success === 'local') localSuccess++; } else { // Fichiers JS en local uniquement await this.loadScript(`${this.contentDirectory}${filename}`); localSuccess++; } logSh(`✓ Chargé: ${filename}`, 'INFO'); } catch (error) { logSh(`⚠️ Ignoré: ${filename} (${error.message})`, 'INFO'); } } // Mise à jour du statut de connexion if (remoteSuccess > 0) { this.updateConnectionStatus('online', `${remoteSuccess} contenus distants`); } else if (localSuccess > 0) { this.updateConnectionStatus('offline', `${localSuccess} contenus locaux`); } else { this.updateConnectionStatus('error', 'Aucun contenu chargé'); } } async loadJsonWithFallback(filename) { const tryRemoteFirst = this.envConfig?.get('TRY_REMOTE_FIRST') && this.shouldTryRemote(); if (tryRemoteFirst) { // Mode distant prioritaire (rare) const remoteResult = await this.tryRemoteLoad(filename); if (remoteResult.success) return remoteResult.source; // Fallback local const localResult = await this.tryLocalLoad(filename); if (localResult.success) return localResult.source; throw new Error(`Impossible de charger ${filename}: Remote (${remoteResult.error}) et Local (${localResult.error})`); } else { // Mode LOCAL PRIORITAIRE (par défaut) const localResult = await this.tryLocalLoad(filename); if (localResult.success) return localResult.source; // Fallback distant seulement si configuré et réseau disponible if (this.shouldTryRemote()) { const remoteResult = await this.tryRemoteLoad(filename); if (remoteResult.success) return remoteResult.source; throw new Error(`Impossible de charger ${filename}: Local (${localResult.error}) et Remote (${remoteResult.error})`); } else { throw new Error(`Impossible de charger ${filename}: ${localResult.error} (distant désactivé)`); } } } async tryLocalLoad(filename) { try { const localUrl = `${this.contentDirectory}${filename}`; const response = await fetch(localUrl); if (response.ok) { const jsonData = await response.json(); const moduleName = this.jsonFilenameToModuleName(filename); window.ContentModules = window.ContentModules || {}; window.ContentModules[moduleName] = jsonData; logSh(`💾 Chargé depuis local: ${filename}`, 'INFO'); return { success: true, source: 'local' }; } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { logSh(`💾 Local échoué pour ${filename}: ${error.message}`, 'INFO'); return { success: false, error: error.message }; } } async tryRemoteLoad(filename) { // Double vérification: ne pas essayer en mode file:// if (window.location.protocol === 'file:') { logSh(`📁 Mode file:// - Saut du chargement distant pour ${filename}`, 'DEBUG'); return { success: false, error: 'Mode file:// - distant désactivé' }; } try { // UTILISER LE PROXY LOCAL au lieu de DigitalOcean directement const remoteUrl = `http://localhost:8083/do-proxy/${filename}`; logSh(`🔍 Tentative de chargement distant via proxy: ${remoteUrl}`, 'INFO'); logSh(`🔧 Protocol: ${window.location.protocol}, Remote enabled: ${this.envConfig?.isRemoteContentEnabled()}`, 'DEBUG'); // Fetch avec timeout court const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.envConfig.get('REMOTE_TIMEOUT')); await this.loadJsonContentWithTimeout(remoteUrl, controller.signal); clearTimeout(timeoutId); logSh(`🌐 Chargé depuis distant: ${filename}`, 'INFO'); return { success: true, source: 'remote' }; } catch (error) { logSh(`💥 Distant échoué pour ${filename}: ${error.message}`, 'ERROR'); logSh(`🔍 Stack trace: ${error.stack}`, 'DEBUG'); return { success: false, error: error.message }; } } async loadJsonContentWithTimeout(url, signal) { const response = await fetch(url, { signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const jsonData = await response.json(); const moduleName = this.jsonFilenameToModuleName(url); window.ContentModules = window.ContentModules || {}; window.ContentModules[moduleName] = jsonData; } shouldTryRemote() { // Utiliser la configuration de EnvConfig return this.envConfig?.isRemoteContentEnabled() && (window.location.protocol !== 'file:' || this.envConfig.get('FORCE_REMOTE_ON_FILE')); } updateConnectionStatus(status, details = '') { // Émettre un événement personnalisé pour l'UI const event = new CustomEvent('contentConnectionStatus', { detail: { status, details, timestamp: new Date() } }); window.dispatchEvent(event); } scanLoadedModules() { // Scanner les modules déjà présents dans window.ContentModules const loadedFiles = []; if (window.ContentModules) { logSh(`📂 Modules already loaded in window.ContentModules:`, 'DEBUG'); for (const moduleName in window.ContentModules) { // Convertir le nom du module en nom de fichier probable const filename = this.moduleNameToFilename(moduleName); loadedFiles.push(filename); logSh(`✓ Module découvert: ${moduleName} → ${filename}`, 'INFO'); // Debug : vérifier si le module a un id const module = window.ContentModules[moduleName]; if (module.id) { logSh(` Module ID: ${module.id}`, 'DEBUG'); } else { logSh(` ⚠️ Module ${moduleName} has no ID!`, 'WARN'); } } } else { logSh(`⚠️ window.ContentModules is undefined!`, 'WARN'); } logSh(`📂 Total loaded files from modules: ${loadedFiles.length}`, 'INFO'); return loadedFiles; } moduleNameToFilename(moduleName) { // Mapping des noms de modules vers les noms de fichiers const mapping = { 'SBSLevel78New': 'sbs-level-7-8-new.js', 'EnglishClassDemo': 'english-class-demo.json', 'EnglishExemple': 'english_exemple.json', 'EnglishExempleFixed': 'english_exemple_fixed.json', 'EnglishExempleUltraCommented': 'english_exemple_ultra_commented.json', // AJOUT: Fichiers générés par le système de conversion 'SbsLevel78GeneratedFromJs': 'sbs-level-7-8-GENERATED-from-js.json', 'EnglishExempleCommentedGenerated': 'english-exemple-commented-GENERATED.json', // AJOUT: Module JS complet nouvellement créé 'EnglishExempleCommented': 'english-exemple-commented.js', // AJOUT: Story modules 'StoryTest': 'story-test.js', 'StoryPrototype1000words': 'story-prototype-1000words.js', 'StoryComplete1000words': 'story-complete-1000words.js', 'ChineseLongStory': 'chinese-long-story.js', 'FrenchBeginnerStory': 'french-beginner-story.js', 'WTA1B1': 'WTA1B1.js', 'SBSLevel1': 'SBS-level-1.js', 'StoryPrototypeOptimized': 'story-prototype-optimized.js', // AJOUT: Test compatibility modules 'TestMinimalContent': 'test-compatibility.js', 'TestRichContent': 'test-compatibility.js', 'TestSentenceOnly': 'test-compatibility.js', // AJOUT: NCE modules 'NCE1Lesson6364': 'NCE1-Lesson63-64.js', 'NCE2Lesson3': 'NCE2-Lesson3.js', 'NCE2Lesson30': 'NCE2-Lesson30.js' }; if (mapping[moduleName]) { return mapping[moduleName]; } // Conversion générique PascalCase → kebab-case // Essayer d'abord .json puis .js const baseName = moduleName .replace(/([a-z])([A-Z])/g, '$1-$2') .toLowerCase(); return baseName + '.json'; // Préférer JSON en premier } parseDirectoryListing(html) { const contentFiles = []; // Regex pour trouver les liens vers des fichiers .js ET .json const linkRegex = /]+href="([^"]+\.(js|json))"[^>]*>/gi; let match; while ((match = linkRegex.exec(html)) !== null) { const filename = match[1]; // Éviter les fichiers système ou temporaires if (!filename.startsWith('.') && !filename.includes('test') && !filename.includes('backup')) { contentFiles.push(filename); } } return contentFiles; } async tryCommonFiles() { // FIXME: Liste temporaire - À remplacer par listing dynamique DigitalOcean // Pour l'instant on met juste les fichiers qu'on sait qui existent const possibleFiles = [ 'sbs-level-7-8-new.js', // Local JS legacy 'english-class-demo.json', // Remote JSON example 'english_exemple.json', // Local JSON basic 'english_exemple_fixed.json', // Local JSON modular 'english_exemple_ultra_commented.json', // Local JSON ultra-modular 'example-with-images.js', // Local JS with image support for Word Discovery // AJOUT: Fichiers générés par le système de conversion 'sbs-level-7-8-GENERATED-from-js.json', 'english-exemple-commented-GENERATED.json', // AJOUT: Fichiers NCE (New Concept English) 'NCE1-Lesson63-64.js', // New Concept English Book 1 - Lessons 63-64 'NCE2-Lesson3.js', // New Concept English Book 2 - Lesson 3 'NCE2-Lesson30.js' // New Concept English Book 2 - Lesson 30 ]; const existingFiles = []; for (const filename of possibleFiles) { try { // Tester local d'abord const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 1000); const response = await fetch(`${this.contentDirectory}${filename}`, { method: 'HEAD', signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) { existingFiles.push(filename); logSh(`✓ Trouvé (local): ${filename}`, 'INFO'); continue; } } catch (error) { // Fichier local n'existe pas } // Si pas trouvé en local et remote activé, tester remote via proxy if (this.envConfig.isRemoteContentEnabled() && window.location.protocol !== 'file:') { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 3000); // UTILISER LE PROXY LOCAL au lieu de DigitalOcean directement const remoteUrl = `http://localhost:8083/do-proxy/${filename}`; logSh(`🔍 Test existence via proxy: ${remoteUrl}`, 'DEBUG'); const response = await fetch(remoteUrl, { method: 'HEAD', signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) { existingFiles.push(filename); logSh(`✓ Trouvé (remote): ${filename}`, 'INFO'); } else if (response.status === 403) { logSh(`🔒 Trouvé mais privé (remote): ${filename}`, 'INFO'); // Même si privé, on considère que le fichier existe existingFiles.push(filename); } } catch (error) { // Fichier remote n'existe pas ou timeout } } } return existingFiles; } async scanContentFile(filename) { const contentId = this.extractContentId(filename); const moduleName = this.getModuleName(contentId); try { // Vérifier d'abord si le module est déjà chargé if (!window.ContentModules || !window.ContentModules[moduleName]) { // Le module n'est pas encore chargé, on doit le charger // Détecter le type de fichier et charger en conséquence if (filename.endsWith('.json')) { // Fichier JSON - essayer de le charger via proxy ou local await this.loadJsonContent(filename); } else { // Fichier JS - charger le script classique await this.loadScript(`js/content/${filename}`); } // Vérifier si le module existe après chargement if (!window.ContentModules || !window.ContentModules[moduleName]) { throw new Error(`Module ${moduleName} non trouvé après chargement`); } } else { logSh(`✓ Module ${moduleName} déjà chargé, pas besoin de recharger`, 'INFO'); } const module = window.ContentModules[moduleName]; // Extraire les métadonnées const contentInfo = this.extractContentInfo(module, contentId, filename); logSh(`📦 Contenu découvert: ${contentInfo.name}`, 'INFO'); return contentInfo; } catch (error) { throw new Error(`Impossible de charger ${filename}: ${error.message}`); } } extractContentInfo(module, contentId, filename) { // Analyser les capacités du contenu ultra-modulaire let capabilities; try { capabilities = this.analyzeContentCapabilities(module); } catch (error) { logSh(`⚠️ Erreur dans analyzeContentCapabilities pour ${contentId}: ${error.message}`, 'WARN'); // Fallback avec capacités basiques capabilities = { hasVocabulary: !!module.vocabulary, hasSentences: !!module.sentences, hasGrammar: !!module.grammar, hasBasicContent: true }; } return { id: module.id || contentId, filename: filename, name: module.name || this.beautifyContentId(contentId), description: module.description || 'Contenu automatiquement détecté', // Ultra-modular metadata difficulty_level: module.difficulty_level, original_lang: module.original_lang, user_lang: module.user_lang, tags: module.tags || [], skills_covered: module.skills_covered || [], target_audience: module.target_audience || {}, estimated_duration: module.estimated_duration, // Content capabilities analysis capabilities: capabilities, compatibility: this.safeCalculateGameCompatibility(capabilities), icon: this.safeGetContentIcon(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.safeAnalyzeGameCompatibility(module) }; } safeCalculateGameCompatibility(capabilities) { try { return this.calculateGameCompatibility(capabilities); } catch (error) { logSh(`⚠️ Erreur dans calculateGameCompatibility: ${error.message}`, 'WARN'); return {}; // Retourner un objet vide en cas d'erreur } } safeGetContentIcon(module, contentId) { try { return this.getContentIcon(module, contentId); } catch (error) { logSh(`⚠️ Erreur dans getContentIcon: ${error.message}`, 'WARN'); return '📚'; // Icône par défaut } } safeAnalyzeGameCompatibility(module) { try { return this.analyzeGameCompatibility(module); } catch (error) { logSh(`⚠️ Erreur dans analyzeGameCompatibility: ${error.message}`, 'WARN'); return {}; // Retourner un objet vide en cas d'erreur } } extractContentId(filename) { return filename.replace('.js', '').toLowerCase(); } getModuleName(contentId) { const mapping = { 'sbs-level-7-8-new': 'SBSLevel78New', 'sbs-level-1': 'SBSLevel1', 'chinese-long-story': 'ChineseLongStory', 'french-beginner-story': 'FrenchBeginnerStory', 'wta1b1': 'WTA1B1', 'story-prototype-optimized': 'StoryPrototypeOptimized', 'test-compatibility': 'TestMinimalContent', 'test-minimal': 'TestMinimal', 'test-rich': 'TestRich', // Ajout des modules NCE 'nce1-lesson63-64': 'NCE1Lesson6364', 'nce2-lesson3': 'NCE2Lesson3', 'nce2-lesson30': 'NCE2Lesson30' }; 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 }, }; 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); } return compatibility; } async loadJsonContent(filename) { logSh(`🔍 Chargement JSON: ${filename}`, 'INFO'); let loaded = false; let lastError = null; // Méthode 1: Essayer distant via proxy (si configuré) if (this.shouldTryRemote()) { try { const result = await this.tryRemoteLoad(filename); if (result.success) { logSh(`✅ JSON chargé depuis distant: ${filename}`, 'INFO'); loaded = true; } } catch (error) { logSh(`⚠️ Distant échoué pour ${filename}: ${error.message}`, 'WARN'); lastError = error; } } // Méthode 2: Essayer local if (!loaded) { try { const localUrl = `js/content/${filename}`; const response = await fetch(localUrl); if (response.ok) { const jsonData = await response.json(); const moduleName = this.jsonFilenameToModuleName(filename); window.ContentModules = window.ContentModules || {}; window.ContentModules[moduleName] = jsonData; logSh(`✅ JSON chargé depuis local: ${filename}`, 'INFO'); loaded = true; } } catch (error) { logSh(`⚠️ Local échoué pour ${filename}: ${error.message}`, 'WARN'); lastError = error; } } if (!loaded) { throw new Error(`Impossible de charger JSON ${filename}: ${lastError?.message || 'toutes les méthodes ont échoué'}`); } } jsonFilenameToModuleName(src) { // Extraire le nom du fichier et le convertir en PascalCase compatible const filename = src.split('/').pop().replace('.json', ''); // Mapping spécifique pour certains noms de fichiers const specialMappings = { 'sbs-level-7-8-new': 'SBSLevel78New', 'sbs-level-1': 'SBSLevel1', 'english-class-demo': 'EnglishClassDemo', 'chinese-long-story': 'ChineseLongStory', 'french-beginner-story': 'FrenchBeginnerStory', 'wta1b1': 'WTA1B1', 'story-prototype-optimized': 'StoryPrototypeOptimized', // Ajout des modules NCE 'nce1-lesson63-64': 'NCE1Lesson6364', 'nce2-lesson3': 'NCE2Lesson3', 'nce2-lesson30': 'NCE2Lesson30' }; return specialMappings[filename] || this.toPascalCase(filename); } 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) }; } /** * Analyse les capacités d'un module ultra-modulaire */ analyzeContentCapabilities(module) { const capabilities = { // Core content analysis hasVocabulary: this.hasContent(module, 'vocabulary'), hasSentences: this.hasContent(module, 'sentences'), hasGrammar: this.hasContent(module, 'grammar'), hasAudio: this.hasContent(module, 'audio'), hasDialogues: this.hasContent(module, 'dialogues'), hasPoems: this.hasContent(module, 'poems'), hasCulture: this.hasContent(module, 'culture'), // Advanced features hasExercises: this.hasExercises(module), hasMatching: this.hasContent(module, 'matching'), hasFillInBlanks: this.hasContent(module, 'fillInBlanks'), hasCorrections: this.hasContent(module, 'corrections'), hasComprehension: this.hasContent(module, 'comprehension'), hasParametricSentences: this.hasContent(module, 'parametric_sentences'), // Multimedia capabilities hasAudioFiles: this.hasAudioFiles(module), hasImages: this.hasImages(module), hasVideos: this.hasVideos(module), hasIPA: this.hasIPA(module), // Metadata richness hasDetailedMetadata: this.hasDetailedMetadata(module), hasProgressiveDifficulty: this.hasProgressiveDifficulty(module), hasMultipleLanguages: this.hasMultipleLanguages(module), hasCulturalContext: this.hasCulturalContext(module), // Content depth levels vocabularyDepth: this.analyzeVocabularyDepth(module), contentRichness: this.analyzeContentRichness(module) }; return capabilities; } /** * Calcule la compatibilité avec les jeux selon les capacités */ calculateGameCompatibility(capabilities) { const games = { 'whack-a-mole': this.calculateWhackAMoleCompat(capabilities), 'memory-match': this.calculateMemoryMatchCompat(capabilities), 'quiz-game': this.calculateQuizGameCompat(capabilities), 'fill-the-blank': this.calculateFillBlankCompat(capabilities), 'adventure-reader': this.calculateAdventureCompat(capabilities), 'sentence-builder': this.calculateSentenceBuilderCompat(capabilities), 'pronunciation-game': this.calculatePronunciationCompat(capabilities), 'culture-explorer': this.calculateCultureCompat(capabilities) }; return games; } /** * Vérifie si un module a un type de contenu spécifique */ hasContent(module, contentType) { const content = module[contentType]; if (!content) return false; if (Array.isArray(content)) return content.length > 0; if (typeof content === 'object') return Object.keys(content).length > 0; return !!content; } /** * Vérifie si un module a des exercices */ hasExercises(module) { return this.hasContent(module, 'exercises') || this.hasContent(module, 'fillInBlanks') || this.hasContent(module, 'corrections') || this.hasContent(module, 'comprehension') || this.hasContent(module, 'matching'); } /** * Vérifie la présence de fichiers audio */ hasAudioFiles(module) { // Vérifier dans le vocabulaire if (module.vocabulary) { for (const word of Object.values(module.vocabulary)) { if (typeof word === 'object' && (word.audio_file || word.audio || word.pronunciation)) { return true; } } } // Vérifier dans l'audio dédié return this.hasContent(module, 'audio'); } /** * Analyse la profondeur du vocabulaire (niveaux 1-6) */ analyzeVocabularyDepth(module) { if (!module.vocabulary) return 0; let maxDepth = 1; // Au minimum niveau 1 (string simple) for (const definition of Object.values(module.vocabulary)) { if (typeof definition === 'object') { maxDepth = Math.max(maxDepth, 2); // Niveau 2: objet de base if (definition.examples || definition.grammar_notes) maxDepth = Math.max(maxDepth, 3); if (definition.etymology || definition.word_family) maxDepth = Math.max(maxDepth, 4); if (definition.cultural_significance) maxDepth = Math.max(maxDepth, 5); if (definition.memory_techniques || definition.visual_associations) maxDepth = Math.max(maxDepth, 6); } } return maxDepth; } /** * Calculs de compatibilité spécifiques par jeu */ calculateWhackAMoleCompat(capabilities) { let score = 0; if (capabilities.hasVocabulary) score += 40; if (capabilities.hasSentences) score += 30; if (capabilities.hasAudioFiles) score += 20; if (capabilities.vocabularyDepth >= 2) score += 10; return { compatible: score >= 40, score, reason: 'Nécessite vocabulaire ou phrases' }; } calculateMemoryMatchCompat(capabilities) { let score = 0; if (capabilities.hasVocabulary) score += 50; if (capabilities.hasImages) score += 30; if (capabilities.hasAudioFiles) score += 20; return { compatible: score >= 50, score, reason: 'Optimisé pour vocabulaire visuel' }; } calculateQuizGameCompat(capabilities) { let score = 0; if (capabilities.hasVocabulary) score += 30; if (capabilities.hasGrammar) score += 25; if (capabilities.hasExercises) score += 45; return { compatible: score >= 30, score, reason: 'Fonctionne avec tout contenu' }; } calculateFillBlankCompat(capabilities) { let score = 0; if (capabilities.hasFillInBlanks) score += 70; if (capabilities.hasSentences) score += 30; return { compatible: score >= 30, score, reason: 'Nécessite phrases à trous' }; } calculateAdventureCompat(capabilities) { let score = 0; if (capabilities.hasDialogues) score += 60; if (capabilities.hasCulture) score += 30; if (capabilities.contentRichness >= 5) score += 10; return { compatible: score >= 50, score, reason: 'Nécessite dialogues riches' }; } calculateSentenceBuilderCompat(capabilities) { let score = 0; if (capabilities.hasParametricSentences) score += 70; if (capabilities.hasSentences) score += 30; return { compatible: score >= 30, score, reason: 'Construit des phrases' }; } calculatePronunciationCompat(capabilities) { let score = 0; if (capabilities.hasAudioFiles) score += 60; if (capabilities.hasIPA) score += 40; return { compatible: score >= 60, score, reason: 'Nécessite fichiers audio' }; } calculateCultureCompat(capabilities) { let score = 0; if (capabilities.hasCulture) score += 60; if (capabilities.hasPoems) score += 30; if (capabilities.hasCulturalContext) score += 10; return { compatible: score >= 30, score, reason: 'Nécessite contenu culturel' }; } /** * Analyse les capacités globales de tous les modules */ analyzeGlobalCapabilities(contentList) { const globalStats = { totalCapabilities: new Set(), averageRichness: 0, recommendedGames: new Map(), contentGaps: [] }; let totalRichness = 0; for (const content of contentList) { // Compiler toutes les capacités Object.keys(content.capabilities).forEach(cap => { if (content.capabilities[cap]) globalStats.totalCapabilities.add(cap); }); totalRichness += content.capabilities.contentRichness || 0; // Analyser la compatibilité des jeux Object.entries(content.compatibility || {}).forEach(([game, compat]) => { if (compat.compatible) { const current = globalStats.recommendedGames.get(game) || { count: 0, avgScore: 0 }; current.count++; current.avgScore = (current.avgScore + compat.score) / current.count; globalStats.recommendedGames.set(game, current); } }); } globalStats.averageRichness = totalRichness / contentList.length; globalStats.totalCapabilities = Array.from(globalStats.totalCapabilities); logSh(`📊 Analyse globale: ${globalStats.totalCapabilities.length} capacités, richesse moyenne: ${globalStats.averageRichness.toFixed(1)}`, 'INFO'); return globalStats; } // Helper methods pour les analyses spécialisées hasImages(module) { return false; } // TODO: implémenter hasVideos(module) { return false; } hasIPA(module) { return module.vocabulary && Object.values(module.vocabulary).some(w => typeof w === 'object' && w.ipa); } hasDetailedMetadata(module) { return !!(module.tags && module.skills_covered && module.target_audience); } hasProgressiveDifficulty(module) { return !!(module.difficulty_level && typeof module.difficulty_level === 'number'); } hasMultipleLanguages(module) { return !!(module.original_lang && module.user_lang); } hasCulturalContext(module) { return this.hasContent(module, 'culture') || this.hasContent(module, 'poems'); } analyzeContentRichness(module) { return this.countItems(module) / 10; } // Score sur 10 basé sur le nombre d'items } // Export global window.ContentScanner = ContentScanner;