- Fix WebSocket server to properly broadcast logs to all connected clients - Integrate professional logging system with real-time WebSocket interface - Add network status indicator with DigitalOcean Spaces connectivity - Implement AWS Signature V4 authentication for private bucket access - Add JSON content loader with backward compatibility to JS modules - Restore navigation breadcrumb system with comprehensive logging - Add multiple content formats: JSON + JS with automatic discovery - Enhance top bar with logger toggle and network status indicator - Remove deprecated temp-games module and clean up unused files 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
391 lines
12 KiB
JavaScript
391 lines
12 KiB
JavaScript
// === JSON CONTENT LOADER ===
|
|
// Transforme les contenus JSON en format compatible avec les jeux existants
|
|
|
|
class JSONContentLoader {
|
|
constructor() {
|
|
this.loadedContent = new Map();
|
|
}
|
|
|
|
/**
|
|
* Charge et adapte un contenu JSON pour les jeux
|
|
* @param {Object} jsonContent - Le contenu JSON brut
|
|
* @returns {Object} - Contenu adapté au format legacy
|
|
*/
|
|
adapt(jsonContent) {
|
|
if (!jsonContent) {
|
|
logSh('⚠️ JSONContentLoader - Contenu vide reçu', 'WARN');
|
|
return this.createEmptyContent();
|
|
}
|
|
|
|
logSh(`🔄 JSONContentLoader - Adaptation du contenu: ${jsonContent.name || 'Sans nom'}`, 'INFO');
|
|
|
|
// Créer l'objet de base au format legacy
|
|
const adaptedContent = {
|
|
// Métadonnées (préservées mais adaptées)
|
|
name: jsonContent.name || 'Contenu Sans Nom',
|
|
description: jsonContent.description || '',
|
|
difficulty: jsonContent.difficulty || 'medium',
|
|
language: jsonContent.language || 'english',
|
|
icon: jsonContent.icon || '📝',
|
|
|
|
// === VOCABULAIRE ===
|
|
vocabulary: this.adaptVocabulary(jsonContent.vocabulary),
|
|
|
|
// === PHRASES ET SENTENCES ===
|
|
sentences: this.adaptSentences(jsonContent.sentences),
|
|
|
|
// === TEXTES ===
|
|
texts: this.adaptTexts(jsonContent.texts),
|
|
|
|
// === DIALOGUES ===
|
|
dialogues: this.adaptDialogues(jsonContent.dialogues),
|
|
|
|
// === GRAMMAIRE ===
|
|
grammar: this.adaptGrammar(jsonContent.grammar),
|
|
|
|
// === AUDIO ===
|
|
audio: this.adaptAudio(jsonContent.audio),
|
|
|
|
// === POÈMES ET CULTURE ===
|
|
poems: this.adaptPoems(jsonContent.poems),
|
|
|
|
// === EXERCICES AVANCÉS ===
|
|
fillInBlanks: this.adaptFillInBlanks(jsonContent.fillInBlanks),
|
|
corrections: this.adaptCorrections(jsonContent.corrections),
|
|
comprehension: this.adaptComprehension(jsonContent.comprehension),
|
|
matching: this.adaptMatching(jsonContent.matching),
|
|
|
|
// === EXERCICES GÉNÉRIQUES ===
|
|
exercises: this.adaptExercises(jsonContent.exercises),
|
|
|
|
// === LISTENING (format SBS) ===
|
|
listening: this.adaptListening(jsonContent.listening),
|
|
|
|
// === MÉTADONNÉES DE COMPATIBILITÉ ===
|
|
_adapted: true,
|
|
_source: 'json',
|
|
_adaptedAt: new Date().toISOString()
|
|
};
|
|
|
|
// Nettoyage des valeurs undefined
|
|
this.cleanUndefinedValues(adaptedContent);
|
|
|
|
logSh(`✅ JSONContentLoader - Contenu adapté avec succès`, 'INFO');
|
|
logSh(`📊 Stats: ${Object.keys(adaptedContent.vocabulary || {}).length} mots, ${(adaptedContent.sentences || []).length} phrases`, 'INFO');
|
|
|
|
return adaptedContent;
|
|
}
|
|
|
|
/**
|
|
* Adapte le vocabulaire (format JSON → Legacy)
|
|
*/
|
|
adaptVocabulary(vocabulary) {
|
|
if (!vocabulary || typeof vocabulary !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
const adapted = {};
|
|
|
|
for (const [word, definition] of Object.entries(vocabulary)) {
|
|
if (typeof definition === 'string') {
|
|
// Format simple: "cat": "chat"
|
|
adapted[word] = definition;
|
|
} else if (typeof definition === 'object') {
|
|
// Format enrichi: "cat": { translation: "chat", type: "noun", ... }
|
|
adapted[word] = {
|
|
translation: definition.translation || definition.french || definition.chinese || '',
|
|
english: word,
|
|
type: definition.type || 'word',
|
|
pronunciation: definition.pronunciation || definition.prononciation,
|
|
difficulty: definition.difficulty,
|
|
examples: definition.examples,
|
|
grammarNotes: definition.grammarNotes,
|
|
// Compatibilité avec anciens formats
|
|
french: definition.french || definition.translation,
|
|
chinese: definition.chinese || definition.translation,
|
|
image: definition.image,
|
|
audio: definition.audio || definition.pronunciation
|
|
};
|
|
}
|
|
}
|
|
|
|
return adapted;
|
|
}
|
|
|
|
/**
|
|
* Adapte les phrases
|
|
*/
|
|
adaptSentences(sentences) {
|
|
if (!Array.isArray(sentences)) {
|
|
return [];
|
|
}
|
|
|
|
return sentences.map(sentence => {
|
|
if (typeof sentence === 'string') {
|
|
return { english: sentence, translation: '' };
|
|
}
|
|
|
|
return {
|
|
english: sentence.english || '',
|
|
french: sentence.french || sentence.translation,
|
|
chinese: sentence.chinese || sentence.translation,
|
|
translation: sentence.translation || sentence.french || sentence.chinese,
|
|
prononciation: sentence.prononciation || sentence.pinyin,
|
|
audio: sentence.audio,
|
|
difficulty: sentence.difficulty
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adapte les textes
|
|
*/
|
|
adaptTexts(texts) {
|
|
if (!Array.isArray(texts)) {
|
|
return [];
|
|
}
|
|
|
|
return texts.map(text => ({
|
|
title: text.title || 'Sans titre',
|
|
content: text.content || '',
|
|
translation: text.translation || '',
|
|
french: text.french || text.translation,
|
|
chinese: text.chinese || text.translation,
|
|
audio: text.audio,
|
|
difficulty: text.difficulty
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Adapte les dialogues
|
|
*/
|
|
adaptDialogues(dialogues) {
|
|
if (!Array.isArray(dialogues)) {
|
|
return [];
|
|
}
|
|
|
|
return dialogues.map(dialogue => ({
|
|
title: dialogue.title || 'Dialogue',
|
|
conversation: Array.isArray(dialogue.conversation)
|
|
? dialogue.conversation.map(line => ({
|
|
speaker: line.speaker || 'Speaker',
|
|
english: line.english || '',
|
|
french: line.french || line.translation,
|
|
chinese: line.chinese || line.translation,
|
|
translation: line.translation || line.french || line.chinese,
|
|
prononciation: line.prononciation || line.pinyin,
|
|
audio: line.audio
|
|
}))
|
|
: []
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Adapte la grammaire
|
|
*/
|
|
adaptGrammar(grammar) {
|
|
if (!grammar || typeof grammar !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
// La grammaire reste en format objet (compatible)
|
|
return grammar;
|
|
}
|
|
|
|
/**
|
|
* Adapte le contenu audio
|
|
*/
|
|
adaptAudio(audio) {
|
|
if (!audio || typeof audio !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
return {
|
|
withText: Array.isArray(audio.withText) ? audio.withText : [],
|
|
withoutText: Array.isArray(audio.withoutText) ? audio.withoutText : []
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Adapte les poèmes
|
|
*/
|
|
adaptPoems(poems) {
|
|
if (!Array.isArray(poems)) {
|
|
return [];
|
|
}
|
|
|
|
return poems.map(poem => ({
|
|
title: poem.title || 'Poème',
|
|
content: poem.content || '',
|
|
translation: poem.translation || '',
|
|
audioFile: poem.audioFile,
|
|
culturalContext: poem.culturalContext
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices Fill-in-the-Blanks
|
|
*/
|
|
adaptFillInBlanks(fillInBlanks) {
|
|
if (!Array.isArray(fillInBlanks)) {
|
|
return [];
|
|
}
|
|
|
|
return fillInBlanks;
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices de correction
|
|
*/
|
|
adaptCorrections(corrections) {
|
|
if (!Array.isArray(corrections)) {
|
|
return [];
|
|
}
|
|
|
|
return corrections;
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices de compréhension
|
|
*/
|
|
adaptComprehension(comprehension) {
|
|
if (!Array.isArray(comprehension)) {
|
|
return [];
|
|
}
|
|
|
|
return comprehension;
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices de matching
|
|
*/
|
|
adaptMatching(matching) {
|
|
if (!Array.isArray(matching)) {
|
|
return [];
|
|
}
|
|
|
|
return matching;
|
|
}
|
|
|
|
/**
|
|
* Adapte les exercices génériques
|
|
*/
|
|
adaptExercises(exercises) {
|
|
if (!exercises || typeof exercises !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
return exercises;
|
|
}
|
|
|
|
/**
|
|
* Adapte le contenu listening (format SBS)
|
|
*/
|
|
adaptListening(listening) {
|
|
if (!listening || typeof listening !== 'object') {
|
|
return {};
|
|
}
|
|
|
|
return listening;
|
|
}
|
|
|
|
/**
|
|
* Nettoie les valeurs undefined de l'objet
|
|
*/
|
|
cleanUndefinedValues(obj) {
|
|
Object.keys(obj).forEach(key => {
|
|
if (obj[key] === undefined) {
|
|
delete obj[key];
|
|
} else if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
|
this.cleanUndefinedValues(obj[key]);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Crée un contenu vide par défaut
|
|
*/
|
|
createEmptyContent() {
|
|
return {
|
|
name: 'Contenu Vide',
|
|
description: 'Aucun contenu disponible',
|
|
difficulty: 'easy',
|
|
vocabulary: {},
|
|
sentences: [],
|
|
texts: [],
|
|
dialogues: [],
|
|
grammar: {},
|
|
exercises: {},
|
|
_adapted: true,
|
|
_source: 'empty',
|
|
_adaptedAt: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Analyse la richesse du contenu et génère des statistiques
|
|
*/
|
|
analyzeContent(adaptedContent) {
|
|
const stats = {
|
|
vocabularyCount: Object.keys(adaptedContent.vocabulary || {}).length,
|
|
sentenceCount: (adaptedContent.sentences || []).length,
|
|
textCount: (adaptedContent.texts || []).length,
|
|
dialogueCount: (adaptedContent.dialogues || []).length,
|
|
grammarTopics: Object.keys(adaptedContent.grammar || {}).length,
|
|
audioContent: !!(adaptedContent.audio && (adaptedContent.audio.withText || adaptedContent.audio.withoutText)),
|
|
poemCount: (adaptedContent.poems || []).length,
|
|
exerciseTypes: Object.keys(adaptedContent.exercises || {}).length,
|
|
totalItems: 0
|
|
};
|
|
|
|
stats.totalItems = stats.vocabularyCount + stats.sentenceCount +
|
|
stats.textCount + stats.dialogueCount + stats.poemCount;
|
|
|
|
stats.richness = stats.totalItems > 50 ? 'rich' :
|
|
stats.totalItems > 20 ? 'medium' : 'basic';
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Teste si un objet est au format JSON (vs legacy JS)
|
|
*/
|
|
isJSONFormat(content) {
|
|
return content && (
|
|
content.hasOwnProperty('name') ||
|
|
content.hasOwnProperty('description') ||
|
|
content.hasOwnProperty('language') ||
|
|
content._adapted === true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Point d'entrée principal - décide s'il faut adapter ou pas
|
|
*/
|
|
loadContent(content) {
|
|
if (!content) {
|
|
return this.createEmptyContent();
|
|
}
|
|
|
|
// Si déjà adapté, retourner tel quel
|
|
if (content._adapted === true) {
|
|
return content;
|
|
}
|
|
|
|
// Si format JSON, adapter
|
|
if (this.isJSONFormat(content)) {
|
|
return this.adapt(content);
|
|
}
|
|
|
|
// Sinon, c'est du format legacy, retourner tel quel
|
|
return content;
|
|
}
|
|
}
|
|
|
|
// Export global
|
|
window.JSONContentLoader = JSONContentLoader;
|
|
|
|
// Export Node.js (optionnel)
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = JSONContentLoader;
|
|
} |