// === MOTEUR DE CONTENU FLEXIBLE === class ContentEngine { constructor() { this.loadedContent = {}; this.migrator = new ContentMigrator(); this.validator = new ContentValidator(); } // Charger et traiter un module de contenu async loadContent(contentId) { if (this.loadedContent[contentId]) { return this.loadedContent[contentId]; } try { // Charger le contenu brut const rawContent = await this.loadRawContent(contentId); // Valider et migrer si nécessaire const processedContent = this.processContent(rawContent); // Mettre en cache this.loadedContent[contentId] = processedContent; return processedContent; } catch (error) { console.error(`Erreur chargement contenu ${contentId}:`, error); throw error; } } async loadRawContent(contentId) { // Charger depuis le module existant const moduleName = this.getModuleName(contentId); if (window.ContentModules && window.ContentModules[moduleName]) { return window.ContentModules[moduleName]; } // Charger dynamiquement le script await this.loadScript(`js/content/${contentId}.js`); return window.ContentModules[moduleName]; } processContent(rawContent) { // Vérifier le format if (this.isOldFormat(rawContent)) { console.log('Migration ancien format vers nouveau format...'); return this.migrator.migrateToNewFormat(rawContent); } // Valider le nouveau format if (!this.validator.validate(rawContent)) { throw new Error('Format de contenu invalide'); } return rawContent; } isOldFormat(content) { // Détecter l'ancien format (simple array vocabulary) return content.vocabulary && Array.isArray(content.vocabulary) && !content.contentItems && !content.version; } // Filtrer le contenu par critères filterContent(content, filters = {}) { if (!content.contentItems) return content; let filtered = [...content.contentItems]; // Filtrer par type if (filters.type) { filtered = filtered.filter(item => Array.isArray(filters.type) ? filters.type.includes(item.type) : item.type === filters.type ); } // Filtrer par difficulté if (filters.difficulty) { filtered = filtered.filter(item => item.difficulty === filters.difficulty); } // Filtrer par catégorie if (filters.category) { filtered = filtered.filter(item => item.category === filters.category); } // Filtrer par tags if (filters.tags) { filtered = filtered.filter(item => item.content.tags && filters.tags.some(tag => item.content.tags.includes(tag)) ); } return { ...content, contentItems: filtered }; } // Adapter le contenu pour un jeu spécifique adaptForGame(content, gameType) { const adapter = new GameContentAdapter(gameType); return adapter.adapt(content); } // Obtenir des éléments aléatoires getRandomItems(content, count = 10, filters = {}) { const filtered = this.filterContent(content, filters); const items = filtered.contentItems || []; const shuffled = [...items].sort(() => Math.random() - 0.5); return shuffled.slice(0, count); } // Obtenir des éléments par progression getItemsByProgression(content, userLevel = 1, count = 10) { const items = content.contentItems || []; // Calculer la difficulté appropriée const targetDifficulties = this.getDifficultiesForLevel(userLevel); const appropriateItems = items.filter(item => targetDifficulties.includes(item.difficulty) ); return this.shuffleArray(appropriateItems).slice(0, count); } getDifficultiesForLevel(level) { if (level <= 2) return ['easy']; if (level <= 4) return ['easy', 'medium']; return ['easy', 'medium', 'hard']; } // Utilitaires getModuleName(contentId) { const names = { 'sbs-level-8': 'SBSLevel8', 'animals': 'Animals', 'colors': 'Colors', 'family': 'Family', 'food': 'Food', 'house': 'House' }; return names[contentId] || contentId; } async loadScript(src) { return new Promise((resolve, reject) => { 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); }); } shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } } // === MIGRATEUR DE CONTENU === class ContentMigrator { migrateToNewFormat(oldContent) { const newContent = { id: oldContent.name || 'migrated-content', name: oldContent.name || 'Contenu Migré', description: oldContent.description || '', version: '2.0', format: 'unified', difficulty: oldContent.difficulty || 'intermediate', // Métadonnées metadata: { totalItems: oldContent.vocabulary ? oldContent.vocabulary.length : 0, categories: this.extractCategories(oldContent), contentTypes: ['vocabulary'], migrationDate: new Date().toISOString() }, // Configuration config: { defaultInteraction: 'click', supportedGames: ['whack-a-mole', 'memory-game', 'temp-games'], adaptiveEnabled: true }, // Contenu principal contentItems: [] }; // Migrer le vocabulaire if (oldContent.vocabulary) { oldContent.vocabulary.forEach((word, index) => { newContent.contentItems.push(this.migrateVocabularyItem(word, index)); }); } // Migrer les phrases si elles existent if (oldContent.phrases) { oldContent.phrases.forEach((phrase, index) => { newContent.contentItems.push(this.migratePhraseItem(phrase, index)); }); } return newContent; } migrateVocabularyItem(oldWord, index) { return { id: `vocab_${index + 1}`, type: 'vocabulary', difficulty: this.inferDifficulty(oldWord.english), category: oldWord.category || 'general', content: { english: oldWord.english, french: oldWord.french, context: oldWord.category || '', tags: oldWord.category ? [oldWord.category] : [] }, media: { image: oldWord.image || null, audio: null, icon: this.getIconForCategory(oldWord.category) }, pedagogy: { learningObjective: `Apprendre le mot "${oldWord.english}"`, prerequisites: [], followUp: [], grammarFocus: 'vocabulary' }, interaction: { type: 'click', validation: 'exact', hints: [oldWord.french], feedback: { correct: `Bien joué ! "${oldWord.english}" = "${oldWord.french}"`, incorrect: `Non, "${oldWord.english}" = "${oldWord.french}"` }, alternatives: [] // Sera rempli dynamiquement } }; } migratePhraseItem(oldPhrase, index) { return { id: `phrase_${index + 1}`, type: 'sentence', difficulty: 'medium', category: oldPhrase.category || 'general', content: { english: oldPhrase.english, french: oldPhrase.french, context: oldPhrase.category || '', tags: [oldPhrase.category || 'phrase'] }, media: { image: null, audio: null, icon: '💬' }, pedagogy: { learningObjective: `Comprendre la phrase "${oldPhrase.english}"`, prerequisites: [], followUp: [], grammarFocus: 'sentence_structure' }, interaction: { type: 'click', validation: 'exact', hints: [oldPhrase.french], feedback: { correct: `Parfait ! Cette phrase signifie "${oldPhrase.french}"`, incorrect: `Cette phrase signifie "${oldPhrase.french}"` } } }; } inferDifficulty(englishWord) { if (englishWord.length <= 4) return 'easy'; if (englishWord.length <= 8) return 'medium'; return 'hard'; } getIconForCategory(category) { const icons = { family: '👨‍👩‍👧‍👦', animals: '🐱', colors: '🎨', food: '🍎', school_objects: '📚', daily_activities: '🌅', numbers: '🔢' }; return icons[category] || '📝'; } extractCategories(oldContent) { const categories = new Set(); if (oldContent.vocabulary) { oldContent.vocabulary.forEach(word => { if (word.category) categories.add(word.category); }); } if (oldContent.categories) { Object.keys(oldContent.categories).forEach(cat => categories.add(cat)); } return Array.from(categories); } } // === VALIDATEUR DE CONTENU === class ContentValidator { validate(content) { try { // Vérifications de base if (!content.id || !content.name) { console.warn('Contenu manque ID ou nom'); return false; } if (!content.contentItems || !Array.isArray(content.contentItems)) { console.warn('contentItems manquant ou invalide'); return false; } // Valider chaque élément for (let item of content.contentItems) { if (!this.validateContentItem(item)) { return false; } } return true; } catch (error) { console.error('Erreur validation:', error); return false; } } validateContentItem(item) { // Champs requis const requiredFields = ['id', 'type', 'content']; for (let field of requiredFields) { if (!item[field]) { console.warn(`Champ requis manquant: ${field}`); return false; } } // Valider le contenu selon le type switch (item.type) { case 'vocabulary': return this.validateVocabulary(item); case 'sentence': return this.validateSentence(item); case 'dialogue': return this.validateDialogue(item); default: console.warn(`Type de contenu inconnu: ${item.type}`); return true; // Permettre types inconnus pour extensibilité } } validateVocabulary(item) { return item.content.english && item.content.french; } validateSentence(item) { return item.content.english && item.content.french; } validateDialogue(item) { return item.content.conversation && Array.isArray(item.content.conversation); } } // === ADAPTATEUR CONTENU/JEU === class GameContentAdapter { constructor(gameType) { this.gameType = gameType; } adapt(content) { switch (this.gameType) { case 'whack-a-mole': return this.adaptForWhackAMole(content); case 'memory-game': return this.adaptForMemoryGame(content); case 'temp-games': return this.adaptForTempGames(content); default: return content; } } adaptForWhackAMole(content) { // Convertir vers format attendu par Whack-a-Mole const vocabulary = content.contentItems .filter(item => item.type === 'vocabulary' || item.type === 'sentence') .map(item => ({ english: item.content.english, french: item.content.french, image: item.media?.image, category: item.category, difficulty: item.difficulty, interaction: item.interaction })); return { ...content, vocabulary: vocabulary, // Maintenir la compatibilité gameSettings: { whackAMole: { recommendedWords: Math.min(15, vocabulary.length), timeLimit: 60, maxErrors: 5 } } }; } adaptForMemoryGame(content) { const pairs = content.contentItems .filter(item => ['vocabulary', 'sentence'].includes(item.type)) .map(item => ({ english: item.content.english, french: item.content.french, image: item.media?.image, type: item.type })); return { ...content, pairs: pairs }; } adaptForTempGames(content) { // Format flexible pour les mini-jeux return { ...content, vocabulary: content.contentItems.map(item => ({ english: item.content.english, french: item.content.french, type: item.type, difficulty: item.difficulty })) }; } } // Export global window.ContentEngine = ContentEngine; window.ContentMigrator = ContentMigrator; window.ContentValidator = ContentValidator; window.GameContentAdapter = GameContentAdapter;