- 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>
554 lines
18 KiB
JavaScript
554 lines
18 KiB
JavaScript
// === CONTENT FACTORY - GÉNÉRATEUR UNIVERSEL DE CONTENU ===
|
|
|
|
class ContentFactory {
|
|
constructor() {
|
|
this.parsers = new Map();
|
|
this.generators = new Map();
|
|
this.templates = new Map();
|
|
this.mediaProcessor = new MediaProcessor();
|
|
this.validator = new ContentValidator();
|
|
|
|
this.initializeParsers();
|
|
this.initializeGenerators();
|
|
this.initializeTemplates();
|
|
}
|
|
|
|
// === PARSERS - ANALYSE DE CONTENU BRUT ===
|
|
|
|
initializeParsers() {
|
|
// Parser pour texte libre
|
|
this.parsers.set('text', new TextParser());
|
|
|
|
// Parser pour fichiers CSV
|
|
this.parsers.set('csv', new CSVParser());
|
|
|
|
// Parser pour JSON structuré
|
|
this.parsers.set('json', new JSONParser());
|
|
|
|
// Parser pour dialogue/script
|
|
this.parsers.set('dialogue', new DialogueParser());
|
|
|
|
// Parser pour séquences/histoires
|
|
this.parsers.set('sequence', new SequenceParser());
|
|
|
|
// Parser pour média (audio/image)
|
|
this.parsers.set('media', new MediaParser());
|
|
}
|
|
|
|
// === GÉNÉRATEURS - CRÉATION D'EXERCICES ===
|
|
|
|
initializeGenerators() {
|
|
// Générateur de vocabulaire
|
|
this.generators.set('vocabulary', new VocabularyGenerator());
|
|
|
|
// Générateur de phrases
|
|
this.generators.set('sentence', new SentenceGenerator());
|
|
|
|
// Générateur de dialogues
|
|
this.generators.set('dialogue', new DialogueGenerator());
|
|
|
|
// Générateur de séquences
|
|
this.generators.set('sequence', new SequenceGenerator());
|
|
|
|
// Générateur de scénarios
|
|
this.generators.set('scenario', new ScenarioGenerator());
|
|
|
|
// Générateur automatique (détection de type)
|
|
this.generators.set('auto', new AutoGenerator());
|
|
}
|
|
|
|
// === TEMPLATES - MODÈLES DE CONTENU ===
|
|
|
|
initializeTemplates() {
|
|
this.templates.set('vocabulary_simple', {
|
|
name: 'Vocabulaire Simple',
|
|
description: 'Mots avec traduction',
|
|
requiredFields: ['english', 'french'],
|
|
optionalFields: ['image', 'audio', 'phonetic', 'category'],
|
|
interactions: ['click', 'drag_drop', 'type'],
|
|
games: ['whack-a-mole', 'memory-game', 'temp-games']
|
|
});
|
|
|
|
this.templates.set('dialogue_conversation', {
|
|
name: 'Dialogue Conversationnel',
|
|
description: 'Conversation entre personnages',
|
|
requiredFields: ['speakers', 'conversation'],
|
|
optionalFields: ['scenario', 'context', 'audio_files'],
|
|
interactions: ['role_play', 'click', 'build_sentence'],
|
|
games: ['story-builder', 'temp-games']
|
|
});
|
|
|
|
this.templates.set('sequence_story', {
|
|
name: 'Histoire Séquentielle',
|
|
description: 'Étapes chronologiques',
|
|
requiredFields: ['title', 'steps'],
|
|
optionalFields: ['images', 'times', 'context'],
|
|
interactions: ['chronological_order', 'drag_drop'],
|
|
games: ['story-builder', 'memory-game']
|
|
});
|
|
|
|
this.templates.set('scenario_context', {
|
|
name: 'Scénario Contextuel',
|
|
description: 'Situation réelle complexe',
|
|
requiredFields: ['setting', 'vocabulary', 'phrases'],
|
|
optionalFields: ['roles', 'objectives', 'media'],
|
|
interactions: ['simulation', 'role_play', 'click'],
|
|
games: ['story-builder', 'temp-games']
|
|
});
|
|
}
|
|
|
|
// === MÉTHODE PRINCIPALE - CRÉATION DE CONTENU ===
|
|
|
|
async createContent(input, options = {}) {
|
|
try {
|
|
console.log('🏭 Content Factory - Début création contenu');
|
|
|
|
// 1. Analyser l'input
|
|
const parsedContent = await this.parseInput(input, options);
|
|
|
|
// 2. Détecter le type de contenu
|
|
const contentType = this.detectContentType(parsedContent, options);
|
|
|
|
// 3. Générer les exercices
|
|
const exercises = await this.generateExercises(parsedContent, contentType, options);
|
|
|
|
// 4. Traiter les médias
|
|
const processedMedia = await this.processMedia(parsedContent.media || [], options);
|
|
|
|
// 5. Assembler le module final
|
|
const contentModule = this.assembleModule(exercises, processedMedia, options);
|
|
|
|
// 6. Valider le résultat
|
|
if (!this.validator.validate(contentModule)) {
|
|
throw new Error('Contenu généré invalide');
|
|
}
|
|
|
|
console.log('✅ Content Factory - Contenu créé avec succès');
|
|
return contentModule;
|
|
|
|
} catch (error) {
|
|
console.error('❌ Content Factory - Erreur:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// === PARSING - ANALYSE DES INPUTS ===
|
|
|
|
async parseInput(input, options) {
|
|
const inputType = this.detectInputType(input, options);
|
|
const parser = this.parsers.get(inputType);
|
|
|
|
if (!parser) {
|
|
throw new Error(`Parser non trouvé pour le type: ${inputType}`);
|
|
}
|
|
|
|
return await parser.parse(input, options);
|
|
}
|
|
|
|
detectInputType(input, options) {
|
|
// Type explicite fourni
|
|
if (options.inputType) {
|
|
return options.inputType;
|
|
}
|
|
|
|
// Détection automatique
|
|
if (typeof input === 'string') {
|
|
if (input.includes(',') && input.includes('=')) {
|
|
return 'csv';
|
|
}
|
|
if (input.includes(':') && (input.includes('A:') || input.includes('B:'))) {
|
|
return 'dialogue';
|
|
}
|
|
if (input.includes('1.') || input.includes('First') || input.includes('Then')) {
|
|
return 'sequence';
|
|
}
|
|
return 'text';
|
|
}
|
|
|
|
if (typeof input === 'object') {
|
|
if (input.contentItems) return 'json';
|
|
if (input.conversation) return 'dialogue';
|
|
if (input.steps) return 'sequence';
|
|
}
|
|
|
|
if (Array.isArray(input)) {
|
|
if (input[0]?.english && input[0]?.french) return 'vocabulary';
|
|
if (input[0]?.speaker) return 'dialogue';
|
|
if (input[0]?.order || input[0]?.step) return 'sequence';
|
|
}
|
|
|
|
return 'text'; // Fallback
|
|
}
|
|
|
|
detectContentType(parsedContent, options) {
|
|
// Type explicite
|
|
if (options.contentType) {
|
|
return options.contentType;
|
|
}
|
|
|
|
// Détection basée sur la structure
|
|
if (parsedContent.vocabulary && parsedContent.vocabulary.length > 0) {
|
|
return 'vocabulary';
|
|
}
|
|
|
|
if (parsedContent.conversation || parsedContent.dialogue) {
|
|
return 'dialogue';
|
|
}
|
|
|
|
if (parsedContent.steps || parsedContent.sequence) {
|
|
return 'sequence';
|
|
}
|
|
|
|
if (parsedContent.scenario || parsedContent.setting) {
|
|
return 'scenario';
|
|
}
|
|
|
|
if (parsedContent.sentences) {
|
|
return 'sentence';
|
|
}
|
|
|
|
return 'auto'; // Génération automatique
|
|
}
|
|
|
|
// === GÉNÉRATION D'EXERCICES ===
|
|
|
|
async generateExercises(parsedContent, contentType, options) {
|
|
const generator = this.generators.get(contentType);
|
|
|
|
if (!generator) {
|
|
throw new Error(`Générateur non trouvé pour le type: ${contentType}`);
|
|
}
|
|
|
|
return await generator.generate(parsedContent, options);
|
|
}
|
|
|
|
// === ASSEMBLAGE DU MODULE ===
|
|
|
|
assembleModule(exercises, media, options) {
|
|
const moduleId = options.id || this.generateId();
|
|
const moduleName = options.name || 'Contenu Généré';
|
|
|
|
return {
|
|
id: moduleId,
|
|
name: moduleName,
|
|
description: options.description || `Contenu généré automatiquement - ${new Date().toLocaleDateString()}`,
|
|
version: "2.0",
|
|
format: "unified",
|
|
difficulty: options.difficulty || this.inferDifficulty(exercises),
|
|
|
|
metadata: {
|
|
totalItems: exercises.length,
|
|
categories: this.extractCategories(exercises),
|
|
contentTypes: this.extractContentTypes(exercises),
|
|
generatedAt: new Date().toISOString(),
|
|
sourceType: options.inputType || 'auto-detected'
|
|
},
|
|
|
|
config: {
|
|
defaultInteraction: options.defaultInteraction || "click",
|
|
supportedGames: options.supportedGames || ["whack-a-mole", "memory-game", "temp-games", "story-builder"],
|
|
adaptiveEnabled: true,
|
|
difficultyProgression: true
|
|
},
|
|
|
|
contentItems: exercises,
|
|
|
|
media: media,
|
|
|
|
categories: this.generateCategories(exercises),
|
|
|
|
gameSettings: this.generateGameSettings(exercises, options),
|
|
|
|
// Métadonnées de génération
|
|
generation: {
|
|
timestamp: Date.now(),
|
|
factory_version: "1.0",
|
|
options: options,
|
|
stats: {
|
|
parsing_time: 0, // À implémenter
|
|
generation_time: 0,
|
|
total_time: 0
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// === UTILITAIRES ===
|
|
|
|
generateId() {
|
|
return 'generated_' + Date.now() + '_' + Math.random().toString(36).substr(2, 5);
|
|
}
|
|
|
|
inferDifficulty(exercises) {
|
|
// Analyser la complexité des exercices
|
|
const complexities = exercises.map(ex => {
|
|
if (ex.type === 'vocabulary') return 1;
|
|
if (ex.type === 'sentence') return 2;
|
|
if (ex.type === 'dialogue') return 3;
|
|
if (ex.type === 'scenario') return 4;
|
|
return 2;
|
|
});
|
|
|
|
const avgComplexity = complexities.reduce((a, b) => a + b, 0) / complexities.length;
|
|
|
|
if (avgComplexity <= 1.5) return 'easy';
|
|
if (avgComplexity <= 2.5) return 'medium';
|
|
return 'hard';
|
|
}
|
|
|
|
extractCategories(exercises) {
|
|
const categories = new Set();
|
|
exercises.forEach(ex => {
|
|
if (ex.category) categories.add(ex.category);
|
|
if (ex.content?.tags) {
|
|
ex.content.tags.forEach(tag => categories.add(tag));
|
|
}
|
|
});
|
|
return Array.from(categories);
|
|
}
|
|
|
|
extractContentTypes(exercises) {
|
|
const types = new Set();
|
|
exercises.forEach(ex => types.add(ex.type));
|
|
return Array.from(types);
|
|
}
|
|
|
|
generateCategories(exercises) {
|
|
const categories = {};
|
|
const categoryGroups = this.groupByCategory(exercises);
|
|
|
|
Object.keys(categoryGroups).forEach(cat => {
|
|
categories[cat] = {
|
|
name: this.beautifyCategory(cat),
|
|
icon: this.getCategoryIcon(cat),
|
|
description: `Contenu généré pour ${cat}`,
|
|
difficulty: this.inferCategoryDifficulty(categoryGroups[cat]),
|
|
estimatedTime: Math.ceil(categoryGroups[cat].length * 1.5)
|
|
};
|
|
});
|
|
|
|
return categories;
|
|
}
|
|
|
|
generateGameSettings(exercises, options) {
|
|
const types = this.extractContentTypes(exercises);
|
|
|
|
return {
|
|
whackAMole: {
|
|
recommendedWords: Math.min(15, exercises.length),
|
|
timeLimit: 60,
|
|
maxErrors: 5,
|
|
supportedTypes: types.filter(t => ['vocabulary', 'sentence'].includes(t))
|
|
},
|
|
memoryGame: {
|
|
recommendedPairs: Math.min(8, Math.floor(exercises.length / 2)),
|
|
timeLimit: 120,
|
|
maxFlips: 30,
|
|
supportedTypes: types
|
|
},
|
|
storyBuilder: {
|
|
recommendedScenes: Math.min(6, exercises.length),
|
|
timeLimit: 180,
|
|
supportedTypes: types.filter(t => ['dialogue', 'sequence', 'scenario'].includes(t))
|
|
},
|
|
tempGames: {
|
|
recommendedItems: Math.min(10, exercises.length),
|
|
timeLimit: 90,
|
|
supportedTypes: types
|
|
}
|
|
};
|
|
}
|
|
|
|
// === API PUBLIQUE SIMPLIFIÉE ===
|
|
|
|
// Créer contenu depuis texte libre
|
|
async fromText(text, options = {}) {
|
|
return this.createContent(text, { ...options, inputType: 'text' });
|
|
}
|
|
|
|
// Créer contenu depuis liste vocabulaire
|
|
async fromVocabulary(words, options = {}) {
|
|
return this.createContent(words, { ...options, contentType: 'vocabulary' });
|
|
}
|
|
|
|
// Créer contenu depuis dialogue
|
|
async fromDialogue(dialogue, options = {}) {
|
|
return this.createContent(dialogue, { ...options, contentType: 'dialogue' });
|
|
}
|
|
|
|
// Créer contenu depuis séquence
|
|
async fromSequence(steps, options = {}) {
|
|
return this.createContent(steps, { ...options, contentType: 'sequence' });
|
|
}
|
|
|
|
// Créer contenu avec médias
|
|
async fromMediaBundle(content, mediaFiles, options = {}) {
|
|
return this.createContent(content, {
|
|
...options,
|
|
media: mediaFiles,
|
|
processMedia: true
|
|
});
|
|
}
|
|
|
|
// === TEMPLATE HELPERS ===
|
|
|
|
getAvailableTemplates() {
|
|
return Array.from(this.templates.entries()).map(([key, template]) => ({
|
|
id: key,
|
|
...template
|
|
}));
|
|
}
|
|
|
|
async fromTemplate(templateId, data, options = {}) {
|
|
const template = this.templates.get(templateId);
|
|
if (!template) {
|
|
throw new Error(`Template non trouvé: ${templateId}`);
|
|
}
|
|
|
|
// Valider les données selon le template
|
|
this.validateTemplateData(data, template);
|
|
|
|
return this.createContent(data, {
|
|
...options,
|
|
template: template,
|
|
requiredFields: template.requiredFields
|
|
});
|
|
}
|
|
|
|
validateTemplateData(data, template) {
|
|
for (const field of template.requiredFields) {
|
|
if (!data[field]) {
|
|
throw new Error(`Champ requis manquant: ${field}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// === MÉTHODES UTILITAIRES ===
|
|
|
|
groupByCategory(exercises) {
|
|
const groups = {};
|
|
exercises.forEach(ex => {
|
|
const cat = ex.category || 'general';
|
|
if (!groups[cat]) groups[cat] = [];
|
|
groups[cat].push(ex);
|
|
});
|
|
return groups;
|
|
}
|
|
|
|
beautifyCategory(category) {
|
|
const beautified = {
|
|
'family': 'Famille',
|
|
'animals': 'Animaux',
|
|
'colors': 'Couleurs',
|
|
'numbers': 'Nombres',
|
|
'food': 'Nourriture',
|
|
'school': 'École',
|
|
'daily': 'Quotidien',
|
|
'greetings': 'Salutations'
|
|
};
|
|
return beautified[category] || category.charAt(0).toUpperCase() + category.slice(1);
|
|
}
|
|
|
|
getCategoryIcon(category) {
|
|
const icons = {
|
|
'family': '👨👩👧👦',
|
|
'animals': '🐱',
|
|
'colors': '🎨',
|
|
'numbers': '🔢',
|
|
'food': '🍎',
|
|
'school': '📚',
|
|
'daily': '🌅',
|
|
'greetings': '👋',
|
|
'general': '📝'
|
|
};
|
|
return icons[category] || '📝';
|
|
}
|
|
|
|
inferCategoryDifficulty(exercises) {
|
|
const difficulties = exercises.map(ex => {
|
|
switch(ex.difficulty) {
|
|
case 'easy': return 1;
|
|
case 'medium': return 2;
|
|
case 'hard': return 3;
|
|
default: return 2;
|
|
}
|
|
});
|
|
|
|
const avg = difficulties.reduce((a, b) => a + b, 0) / difficulties.length;
|
|
|
|
if (avg <= 1.3) return 'easy';
|
|
if (avg <= 2.3) return 'medium';
|
|
return 'hard';
|
|
}
|
|
}
|
|
|
|
// === PROCESSEUR DE MÉDIAS ===
|
|
|
|
class MediaProcessor {
|
|
constructor() {
|
|
this.supportedAudioFormats = ['mp3', 'wav', 'ogg'];
|
|
this.supportedImageFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
|
|
}
|
|
|
|
async processMedia(mediaFiles, options = {}) {
|
|
const processedMedia = {
|
|
audio: {},
|
|
images: {},
|
|
metadata: {
|
|
totalFiles: mediaFiles.length,
|
|
processedAt: new Date().toISOString()
|
|
}
|
|
};
|
|
|
|
for (const file of mediaFiles) {
|
|
try {
|
|
const processed = await this.processFile(file, options);
|
|
|
|
if (this.isAudioFile(file.name)) {
|
|
processedMedia.audio[file.id || file.name] = processed;
|
|
} else if (this.isImageFile(file.name)) {
|
|
processedMedia.images[file.id || file.name] = processed;
|
|
}
|
|
} catch (error) {
|
|
console.warn(`Erreur traitement fichier ${file.name}:`, error);
|
|
}
|
|
}
|
|
|
|
return processedMedia;
|
|
}
|
|
|
|
async processFile(file, options) {
|
|
// Dans un environnement réel, ici on ferait :
|
|
// - Validation du format
|
|
// - Optimisation (compression, resize)
|
|
// - Upload vers CDN
|
|
// - Génération de thumbnails
|
|
// - Extraction de métadonnées
|
|
|
|
return {
|
|
originalName: file.name,
|
|
path: file.path || `assets/${this.getFileCategory(file.name)}/${file.name}`,
|
|
size: file.size,
|
|
type: file.type,
|
|
processedAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
isAudioFile(filename) {
|
|
const ext = filename.split('.').pop().toLowerCase();
|
|
return this.supportedAudioFormats.includes(ext);
|
|
}
|
|
|
|
isImageFile(filename) {
|
|
const ext = filename.split('.').pop().toLowerCase();
|
|
return this.supportedImageFormats.includes(ext);
|
|
}
|
|
|
|
getFileCategory(filename) {
|
|
return this.isAudioFile(filename) ? 'sounds' : 'images';
|
|
}
|
|
}
|
|
|
|
// Export global
|
|
window.ContentFactory = ContentFactory;
|
|
window.MediaProcessor = MediaProcessor; |