Class_generator/js/tools/content-creator.js
StillHammer 1f8688c4aa Fix WebSocket logging system and add comprehensive network features
- 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>
2025-09-15 23:05:14 +08:00

919 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// === INTERFACE CRÉATEUR DE CONTENU ===
class ContentCreator {
constructor() {
this.factory = new ContentFactory();
this.previewContainer = null;
this.currentContent = null;
}
init() {
this.createInterface();
this.setupEventListeners();
this.loadExamples();
}
createInterface() {
// Créer l'interface dans le container principal
const existingInterface = document.getElementById('content-creator');
if (existingInterface) {
existingInterface.remove();
}
const interface = document.createElement('div');
interface.id = 'content-creator';
interface.className = 'content-creator-interface';
interface.innerHTML = `
<div class="creator-header">
<h2>🏭 Créateur de Contenu Universel</h2>
<p>Transformez n'importe quel contenu en exercices interactifs</p>
</div>
<div class="creator-tabs">
<button class="tab-btn active" data-tab="text">📝 Texte Libre</button>
<button class="tab-btn" data-tab="vocabulary">📚 Vocabulaire</button>
<button class="tab-btn" data-tab="dialogue">💬 Dialogue</button>
<button class="tab-btn" data-tab="sequence">📋 Séquence</button>
</div>
<div class="creator-content">
<!-- TAB: Texte Libre -->
<div class="tab-content active" id="text-tab">
<h3>Collez votre texte ici</h3>
<textarea id="text-input" placeholder="Exemple:
cat = chat
dog = chien
bird = oiseau
ou
1. Wake up at 7am
2. Brush teeth
3. Eat breakfast
4. Go to school
ou
Alice: Hello! What's your name?
Bob: Hi! I'm Bob. Nice to meet you!"></textarea>
<div class="input-options">
<label>
<input type="checkbox" id="auto-detect"> Détection automatique du type
</label>
<select id="content-type-select">
<option value="auto">Auto</option>
<option value="vocabulary">Vocabulaire</option>
<option value="dialogue">Dialogue</option>
<option value="sequence">Séquence</option>
<option value="sentence">Phrases</option>
</select>
</div>
</div>
<!-- TAB: Vocabulaire -->
<div class="tab-content" id="vocabulary-tab">
<h3>Liste de Vocabulaire</h3>
<div class="vocab-builder">
<div class="vocab-entry">
<input type="text" placeholder="Anglais" class="english-input">
<span>=</span>
<input type="text" placeholder="Français" class="french-input">
<input type="text" placeholder="Catégorie" class="category-input">
<button class="remove-entry">×</button>
</div>
</div>
<button id="add-vocab-entry">+ Ajouter un mot</button>
</div>
<!-- TAB: Dialogue -->
<div class="tab-content" id="dialogue-tab">
<h3>Créateur de Dialogue</h3>
<input type="text" id="dialogue-scenario" placeholder="Scénario (ex: Au restaurant)">
<div class="dialogue-builder">
<div class="dialogue-line">
<input type="text" placeholder="Personnage" class="speaker-input">
<textarea placeholder="Réplique en anglais" class="line-input"></textarea>
<button class="remove-line">×</button>
</div>
</div>
<button id="add-dialogue-line">+ Ajouter une réplique</button>
</div>
<!-- TAB: Séquence -->
<div class="tab-content" id="sequence-tab">
<h3>Créateur de Séquence</h3>
<input type="text" id="sequence-title" placeholder="Titre de la séquence">
<div class="sequence-builder">
<div class="sequence-step">
<span class="step-number">1</span>
<input type="text" placeholder="Étape en anglais" class="step-input">
<input type="time" class="time-input" title="Heure (optionnel)">
<button class="remove-step">×</button>
</div>
</div>
<button id="add-sequence-step">+ Ajouter une étape</button>
</div>
</div>
<div class="creator-actions">
<div class="generation-options">
<label>Nom du module:
<input type="text" id="module-name" placeholder="Mon Contenu Personnalisé">
</label>
<label>Difficulté:
<select id="difficulty-select">
<option value="auto">Automatique</option>
<option value="easy">Facile</option>
<option value="medium">Moyen</option>
<option value="hard">Difficile</option>
</select>
</label>
</div>
<div class="action-buttons">
<button id="preview-btn" class="btn secondary">👁️ Aperçu</button>
<button id="generate-btn" class="btn primary">🚀 Générer Module</button>
<button id="test-game-btn" class="btn success" style="display:none;">🎮 Tester dans un Jeu</button>
</div>
</div>
<div class="creator-examples">
<h4>💡 Exemples Rapides</h4>
<div class="example-buttons">
<button class="example-btn" data-example="vocabulary">Vocabulaire Animaux</button>
<button class="example-btn" data-example="dialogue">Dialogue Restaurant</button>
<button class="example-btn" data-example="sequence">Routine Matinale</button>
</div>
</div>
<div class="creator-preview" id="content-preview" style="display:none;">
<h4>📋 Aperçu du Contenu Généré</h4>
<div class="preview-content"></div>
</div>
`;
// Injecter dans la page
const container = document.getElementById('game-container') || document.body;
container.appendChild(interface);
this.previewContainer = interface.querySelector('#content-preview .preview-content');
}
setupEventListeners() {
// Gestion des onglets
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.switchTab(btn.dataset.tab);
});
});
// Bouton générer
document.getElementById('generate-btn').addEventListener('click', () => {
this.generateContent();
});
// Bouton aperçu
document.getElementById('preview-btn').addEventListener('click', () => {
this.previewContent();
});
// Bouton test
document.getElementById('test-game-btn').addEventListener('click', () => {
this.testInGame();
});
// Exemples
document.querySelectorAll('.example-btn').forEach(btn => {
btn.addEventListener('click', () => {
this.loadExample(btn.dataset.example);
});
});
// Builders dynamiques
this.setupVocabularyBuilder();
this.setupDialogueBuilder();
this.setupSequenceBuilder();
}
switchTab(tabName) {
// Désactiver tous les onglets et contenu
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Activer le bon onglet
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
document.getElementById(`${tabName}-tab`).classList.add('active');
}
setupVocabularyBuilder() {
document.getElementById('add-vocab-entry').addEventListener('click', () => {
this.addVocabularyEntry();
});
// Supprimer entries
document.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-entry')) {
e.target.parentElement.remove();
}
});
}
addVocabularyEntry() {
const builder = document.querySelector('.vocab-builder');
const entry = document.createElement('div');
entry.className = 'vocab-entry';
entry.innerHTML = `
<input type="text" placeholder="Anglais" class="english-input">
<span>=</span>
<input type="text" placeholder="Français" class="french-input">
<input type="text" placeholder="Catégorie" class="category-input">
<button class="remove-entry">×</button>
`;
builder.appendChild(entry);
}
setupDialogueBuilder() {
document.getElementById('add-dialogue-line').addEventListener('click', () => {
this.addDialogueLine();
});
document.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-line')) {
e.target.parentElement.remove();
}
});
}
addDialogueLine() {
const builder = document.querySelector('.dialogue-builder');
const line = document.createElement('div');
line.className = 'dialogue-line';
line.innerHTML = `
<input type="text" placeholder="Personnage" class="speaker-input">
<textarea placeholder="Réplique en anglais" class="line-input"></textarea>
<button class="remove-line">×</button>
`;
builder.appendChild(line);
}
setupSequenceBuilder() {
document.getElementById('add-sequence-step').addEventListener('click', () => {
this.addSequenceStep();
});
document.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-step')) {
e.target.parentElement.remove();
this.updateStepNumbers();
}
});
}
addSequenceStep() {
const builder = document.querySelector('.sequence-builder');
const stepCount = builder.children.length + 1;
const step = document.createElement('div');
step.className = 'sequence-step';
step.innerHTML = `
<span class="step-number">${stepCount}</span>
<input type="text" placeholder="Étape en anglais" class="step-input">
<input type="time" class="time-input" title="Heure (optionnel)">
<button class="remove-step">×</button>
`;
builder.appendChild(step);
}
updateStepNumbers() {
document.querySelectorAll('.step-number').forEach((num, index) => {
num.textContent = index + 1;
});
}
async generateContent() {
try {
const input = this.collectInput();
const options = this.collectOptions();
logSh('🏭 Génération de contenu...', { input, options }, 'INFO');
const content = await this.factory.createContent(input, options);
this.currentContent = content;
this.showSuccess('Contenu généré avec succès !');
this.displayPreview(content);
document.getElementById('test-game-btn').style.display = 'inline-block';
} catch (error) {
logSh('Erreur génération:', error, 'ERROR');
this.showError('Erreur lors de la génération: ' + error.message);
}
}
collectInput() {
const activeTab = document.querySelector('.tab-content.active').id;
switch (activeTab) {
case 'text-tab':
return document.getElementById('text-input').value;
case 'vocabulary-tab':
return this.collectVocabularyData();
case 'dialogue-tab':
return this.collectDialogueData();
case 'sequence-tab':
return this.collectSequenceData();
default:
throw new Error('Type de contenu non supporté');
}
}
collectVocabularyData() {
const entries = document.querySelectorAll('.vocab-entry');
const vocabulary = [];
entries.forEach(entry => {
const english = entry.querySelector('.english-input').value;
const french = entry.querySelector('.french-input').value;
const category = entry.querySelector('.category-input').value || 'general';
if (english && french) {
vocabulary.push({ english, french, category });
}
});
return { vocabulary };
}
collectDialogueData() {
const scenario = document.getElementById('dialogue-scenario').value || 'conversation';
const lines = document.querySelectorAll('.dialogue-line');
const conversation = [];
lines.forEach(line => {
const speaker = line.querySelector('.speaker-input').value;
const text = line.querySelector('.line-input').value;
if (speaker && text) {
conversation.push({ speaker, english: text });
}
});
return {
dialogue: {
scenario,
conversation
}
};
}
collectSequenceData() {
const title = document.getElementById('sequence-title').value || 'Sequence';
const steps = document.querySelectorAll('.sequence-step');
const sequence = [];
steps.forEach((step, index) => {
const english = step.querySelector('.step-input').value;
const time = step.querySelector('.time-input').value;
if (english) {
sequence.push({
order: index + 1,
english,
time: time || null
});
}
});
return {
sequence: {
title,
steps: sequence
}
};
}
collectOptions() {
return {
name: document.getElementById('module-name').value || 'Contenu Généré',
difficulty: document.getElementById('difficulty-select').value === 'auto' ? null : document.getElementById('difficulty-select').value,
contentType: document.getElementById('content-type-select').value === 'auto' ? null : document.getElementById('content-type-select').value
};
}
async previewContent() {
try {
const input = this.collectInput();
const options = this.collectOptions();
// Génération rapide pour aperçu
const content = await this.factory.createContent(input, options);
this.displayPreview(content);
document.getElementById('content-preview').style.display = 'block';
} catch (error) {
this.showError('Erreur lors de l\'aperçu: ' + error.message);
}
}
displayPreview(content) {
if (!this.previewContainer) return;
this.previewContainer.innerHTML = `
<div class="preview-summary">
<h5>${content.name}</h5>
<p>${content.description}</p>
<div class="preview-stats">
<span class="stat">📊 ${content.contentItems.length} exercices</span>
<span class="stat">🎯 Difficulté: ${content.difficulty}</span>
<span class="stat">🏷️ Types: ${content.metadata.contentTypes.join(', ')}</span>
</div>
</div>
<div class="preview-items">
${content.contentItems.slice(0, 3).map(item => `
<div class="preview-item">
<span class="item-type">${item.type}</span>
<span class="item-content">${item.content.english} = ${item.content.french}</span>
<span class="item-interaction">${item.interaction.type}</span>
</div>
`).join('')}
${content.contentItems.length > 3 ? `<div class="preview-more">... et ${content.contentItems.length - 3} autres</div>` : ''}
</div>
`;
document.getElementById('content-preview').style.display = 'block';
}
async testInGame() {
if (!this.currentContent) {
this.showError('Aucun contenu généré à tester');
return;
}
try {
// Sauvegarder temporairement le contenu
const contentId = 'generated_test';
window.ContentModules = window.ContentModules || {};
window.ContentModules.GeneratedTest = this.currentContent;
// Naviguer vers un jeu pour tester
this.showSuccess('Contenu prêt ! Redirection vers le jeu...');
setTimeout(() => {
// Fermer l'interface
document.getElementById('content-creator').remove();
// Lancer le jeu avec le contenu généré
AppNavigation.navigateTo('play', 'whack-a-mole', 'generated-test');
}, 1500);
} catch (error) {
this.showError('Erreur lors du test: ' + error.message);
}
}
loadExamples() {
// Les exemples sont gérés par les boutons
}
loadExample(type) {
switch (type) {
case 'vocabulary':
this.switchTab('vocabulary');
setTimeout(() => {
// Effacer contenu existant
document.querySelector('.vocab-builder').innerHTML = '';
// Ajouter exemples
const animals = [
{ english: 'cat', french: 'chat', category: 'animals' },
{ english: 'dog', french: 'chien', category: 'animals' },
{ english: 'bird', french: 'oiseau', category: 'animals' },
{ english: 'fish', french: 'poisson', category: 'animals' }
];
animals.forEach(animal => {
this.addVocabularyEntry();
const entries = document.querySelectorAll('.vocab-entry');
const lastEntry = entries[entries.length - 1];
lastEntry.querySelector('.english-input').value = animal.english;
lastEntry.querySelector('.french-input').value = animal.french;
lastEntry.querySelector('.category-input').value = animal.category;
});
document.getElementById('module-name').value = 'Animaux Domestiques';
}, 100);
break;
case 'dialogue':
this.switchTab('dialogue');
setTimeout(() => {
document.getElementById('dialogue-scenario').value = 'Au Restaurant';
document.querySelector('.dialogue-builder').innerHTML = '';
const dialogueLines = [
{ speaker: 'Serveur', text: 'What would you like to order?' },
{ speaker: 'Client', text: 'I would like a pizza, please.' },
{ speaker: 'Serveur', text: 'What size?' },
{ speaker: 'Client', text: 'Large, please.' }
];
dialogueLines.forEach(line => {
this.addDialogueLine();
const lines = document.querySelectorAll('.dialogue-line');
const lastLine = lines[lines.length - 1];
lastLine.querySelector('.speaker-input').value = line.speaker;
lastLine.querySelector('.line-input').value = line.text;
});
document.getElementById('module-name').value = 'Dialogue Restaurant';
}, 100);
break;
case 'sequence':
this.switchTab('sequence');
setTimeout(() => {
document.getElementById('sequence-title').value = 'Morning Routine';
document.querySelector('.sequence-builder').innerHTML = '';
const steps = [
{ text: 'Wake up', time: '07:00' },
{ text: 'Brush teeth', time: '07:15' },
{ text: 'Get dressed', time: '07:30' },
{ text: 'Eat breakfast', time: '07:45' },
{ text: 'Go to school', time: '08:00' }
];
steps.forEach(step => {
this.addSequenceStep();
const stepElements = document.querySelectorAll('.sequence-step');
const lastStep = stepElements[stepElements.length - 1];
lastStep.querySelector('.step-input').value = step.text;
lastStep.querySelector('.time-input').value = step.time;
});
document.getElementById('module-name').value = 'Routine du Matin';
}, 100);
break;
}
}
showSuccess(message) {
Utils.showToast(message, 'success');
}
showError(message) {
Utils.showToast(message, 'error');
}
}
// CSS pour l'interface
const contentCreatorStyles = `
<style>
.content-creator-interface {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 8px 25px rgba(0,0,0,0.1);
margin: 20px auto;
max-width: 900px;
}
.creator-header {
text-align: center;
margin-bottom: 30px;
}
.creator-header h2 {
color: var(--primary-color);
margin-bottom: 10px;
}
.creator-tabs {
display: flex;
gap: 5px;
margin-bottom: 30px;
border-bottom: 2px solid #e5e7eb;
}
.tab-btn {
background: transparent;
border: none;
padding: 12px 20px;
border-radius: 8px 8px 0 0;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
}
.tab-btn:hover {
background: #f3f4f6;
}
.tab-btn.active {
background: var(--primary-color);
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
#text-input {
width: 100%;
min-height: 200px;
padding: 15px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-family: monospace;
resize: vertical;
}
.input-options {
display: flex;
gap: 20px;
align-items: center;
margin-top: 15px;
}
.vocab-builder, .dialogue-builder, .sequence-builder {
margin: 20px 0;
}
.vocab-entry, .dialogue-line, .sequence-step {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.vocab-entry input, .dialogue-line input, .sequence-step input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
}
.dialogue-line textarea {
flex: 2;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
resize: vertical;
min-height: 40px;
}
.step-number {
background: var(--primary-color);
color: white;
width: 30px;
height: 30px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
}
.remove-entry, .remove-line, .remove-step {
background: var(--error-color);
color: white;
border: none;
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.creator-actions {
margin-top: 30px;
padding-top: 30px;
border-top: 2px solid #e5e7eb;
}
.generation-options {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.generation-options label {
display: flex;
flex-direction: column;
gap: 5px;
}
.generation-options input, .generation-options select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 6px;
min-width: 200px;
}
.action-buttons {
display: flex;
gap: 15px;
justify-content: center;
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 25px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-size: 16px;
}
.btn.primary {
background: var(--primary-color);
color: white;
}
.btn.secondary {
background: var(--neutral-color);
color: white;
}
.btn.success {
background: var(--secondary-color);
color: white;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.creator-examples {
margin-top: 25px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.example-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.example-btn {
background: white;
border: 2px solid var(--primary-color);
color: var(--primary-color);
padding: 8px 16px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.example-btn:hover {
background: var(--primary-color);
color: white;
}
.creator-preview {
margin-top: 25px;
padding: 20px;
background: #f0f9ff;
border-radius: 8px;
border-left: 4px solid var(--primary-color);
}
.preview-summary h5 {
color: var(--primary-color);
margin-bottom: 8px;
font-size: 1.2rem;
}
.preview-stats {
display: flex;
gap: 20px;
margin: 15px 0;
}
.stat {
background: white;
padding: 5px 10px;
border-radius: 15px;
font-size: 14px;
font-weight: 500;
}
.preview-items {
margin-top: 15px;
}
.preview-item {
display: flex;
gap: 15px;
padding: 8px 12px;
background: white;
border-radius: 6px;
margin-bottom: 8px;
align-items: center;
}
.item-type {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
min-width: 70px;
text-align: center;
}
.item-content {
flex: 1;
font-weight: 500;
}
.item-interaction {
background: var(--secondary-color);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
min-width: 80px;
text-align: center;
}
.preview-more {
text-align: center;
color: var(--text-secondary);
font-style: italic;
margin-top: 10px;
}
@media (max-width: 768px) {
.content-creator-interface {
margin: 10px;
padding: 20px;
}
.creator-tabs {
flex-wrap: wrap;
}
.tab-btn {
padding: 10px 15px;
font-size: 14px;
}
.generation-options {
flex-direction: column;
gap: 10px;
}
.action-buttons {
flex-direction: column;
}
.vocab-entry, .dialogue-line, .sequence-step {
flex-direction: column;
align-items: stretch;
}
.preview-stats {
flex-direction: column;
gap: 10px;
}
.preview-item {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
}
</style>
`;
// Ajouter les styles
document.head.insertAdjacentHTML('beforeend', contentCreatorStyles);
// Export global
window.ContentCreator = ContentCreator;