File size: 25,770 Bytes
e21a5a1 1f8b86f e21a5a1 1f8b86f e21a5a1 9faa19a e21a5a1 1f8b86f e21a5a1 1f8b86f e21a5a1 1f8b86f e21a5a1 9faa19a e21a5a1 9faa19a e21a5a1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Générateur de Quiz IA</title>
<style>
/* Styles généraux */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
padding: 20px;
max-width: 800px;
margin: 40px auto;
background-color: #f4f7f9; /* Couleur de fond légèrement différente */
color: #333;
}
.container {
background-color: #fff;
padding: 30px 40px; /* Plus de padding horizontal */
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08); /* Ombre plus douce */
}
h1 {
text-align: center;
color: #2c3e50;
margin-bottom: 35px; /* Plus d'espace sous le titre */
}
/* Section d'entrée */
.input-section {
display: flex;
flex-wrap: wrap; /* Permet au bouton de passer en dessous sur petit écran */
gap: 10px; /* Espace entre l'input et le bouton */
margin-bottom: 25px;
}
.input-section label {
flex-basis: 100%; /* Le label prend toute la largeur */
margin-bottom: 8px;
font-weight: bold;
color: #555;
}
.input-section input[type="text"] {
flex-grow: 1; /* L'input prend l'espace restant */
padding: 12px 15px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
}
.input-section button {
padding: 12px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
transition: background-color 0.3s ease;
white-space: nowrap; /* Empêche le texte du bouton de se casser */
}
.input-section button:hover {
background-color: #2980b9;
}
.input-section button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
/* Styles pour les messages de statut */
#statusMessage {
margin-top: 20px;
margin-bottom: 20px; /* Espace après le message */
padding: 12px 15px; /* Padding un peu plus grand */
border-radius: 4px;
text-align: center;
font-weight: bold;
display: none; /* Caché par défaut */
border-left: 5px solid; /* Barre latérale pour accentuer */
}
#statusMessage.loading { background-color: #eaf2f8; color: #3498db; border-left-color: #3498db;}
#statusMessage.error { background-color: #fdedec; color: #c0392b; border-left-color: #c0392b;} /* Rouge plus doux */
#statusMessage.success { background-color: #e8f6f3; color: #1abc9c; border-left-color: #1abc9c;}
#statusMessage.info { background-color: #fef9e7; color: #f39c12; border-left-color: #f39c12;} /* Jaune/Orange doux */
/* Styles pour le Quiz */
#quizContainer {
margin-top: 30px;
border-top: 1px solid #eee;
padding-top: 25px;
}
.quiz-question {
background-color: #ffffff; /* Fond blanc pour les questions */
border: 1px solid #e0e0e0; /* Bordure plus légère */
border-radius: 6px; /* Coins légèrement plus arrondis */
padding: 20px 25px;
margin-bottom: 25px; /* Plus d'espace entre les questions */
box-shadow: 0 2px 5px rgba(0,0,0,0.05); /* Légère ombre portée */
}
.quiz-question p.question-text { /* Classe spécifique pour le texte de la question */
font-weight: 600; /* Semi-bold */
margin: 0 0 18px 0; /* Ajustement des marges */
color: #34495e;
font-size: 1.1em;
line-height: 1.4;
}
.options-list {
list-style: none;
padding: 0;
margin: 0;
}
.option-item {
margin-bottom: 12px; /* Espacement des options */
padding: 10px 12px; /* Padding interne */
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s ease, border-color 0.2s ease;
border: 1px solid #eee; /* Bordure légère par défaut */
background-color: #f9fafb; /* Fond très léger pour les options */
}
.option-item:hover:not(.disabled) { /* Ne pas appliquer le hover si vérifié */
background-color: #f0f4f8; /* Hover subtil */
border-color: #dbe4f0;
}
.option-item input[type="radio"] {
margin-right: 12px; /* Plus d'espace avant le texte */
vertical-align: middle;
/* Cacher la radio native pour la personnaliser (plus avancé) */
/* appearance: none; -webkit-appearance: none; */
}
.option-item label {
font-weight: normal;
display: inline;
margin-bottom: 0;
vertical-align: middle;
color: #333;
cursor: pointer;
width: calc(100% - 30px); /* S'assurer que le label remplit l'espace */
}
/* Styles pour le feedback (après vérification) */
.option-item.correct {
background-color: #d4efdf;
border-color: #2ecc71;
font-weight: 500; /* Medium weight */
color: #145a32; /* Texte plus foncé */
}
.option-item.incorrect {
background-color: #fdedec;
border-color: #e74c3c;
color: #943126; /* Texte plus foncé */
/* Ajouter un style pour l'élément sélectionné incorrect */
/* text-decoration: line-through; */
}
/* Style spécifique pour la *bonne* réponse quand une *mauvaise* a été choisie */
.option-item.missed-correct {
/* Similaire à correct mais peut-être moins prononcé ou juste une bordure */
border: 2px solid #2ecc71;
box-shadow: 0 0 0 2px rgba(46, 204, 113, 0.3); /* Halo léger */
}
/* Désactiver le hover et le curseur après vérification */
.option-item.disabled {
cursor: default;
opacity: 0.9; /* Légère transparence */
}
.option-item.disabled:hover {
background-color: inherit; /* Annule le hover */
border-color: inherit;
}
/* Style pour l'explication */
.explanation {
margin-top: 18px; /* Espace avant l'explication */
padding: 12px 15px;
background-color: #eaf2f8;
border-left: 4px solid #3498db;
font-size: 0.95em;
color: #2c3e50; /* Couleur de texte pour l'explication */
display: none; /* Caché jusqu'à la vérification */
border-radius: 0 4px 4px 0; /* Arrondi sur les coins droits */
}
.explanation strong {
color: #2980b9; /* Couleur pour le mot "Explication" */
}
.explanation.visible {
display: block;
}
/* Style pour le bouton de soumission et le score */
#submitQuizButton {
display: block;
margin: 35px auto 15px auto; /* Centré, plus d'espace au-dessus */
padding: 12px 30px; /* Bouton plus large */
background-color: #2ecc71; /* Vert */
color: white;
border: none;
border-radius: 5px; /* Coins un peu plus arrondis */
cursor: pointer;
font-size: 1.1rem;
font-weight: 500;
transition: background-color 0.3s ease, transform 0.1s ease;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#submitQuizButton:hover:not(:disabled) {
background-color: #27ae60;
transform: translateY(-1px); /* Léger effet de soulèvement */
}
#submitQuizButton:disabled {
background-color: #95a5a6; /* Gris quand désactivé */
cursor: not-allowed;
box-shadow: none;
transform: none;
}
#scoreContainer {
text-align: center;
font-size: 1.3em; /* Score plus grand */
font-weight: bold;
margin-top: 25px;
padding: 18px; /* Plus de padding */
background-color: #ecf0f1; /* Fond gris clair */
border-radius: 5px;
color: #2c3e50;
border: 1px solid #dce4ec;
display: none; /* Caché initialement */
}
#scoreContainer .score-value { /* Pour styliser le score lui-même */
color: #3498db; /* Score en bleu */
font-size: 1.1em; /* Encore plus grand */
}
/* Responsive */
@media (max-width: 600px) {
.container {
padding: 20px; /* Moins de padding sur petit écran */
}
.input-section {
flex-direction: column; /* Input et bouton l'un sous l'autre */
}
.input-section input[type="text"],
.input-section button {
width: 100%; /* Pleine largeur */
}
h1 {
font-size: 1.8em; /* Titre légèrement plus petit */
}
.quiz-question {
padding: 15px;
}
#submitQuizButton {
width: 90%;
padding: 12px 20px;
}
#scoreContainer {
font-size: 1.1em;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Générateur de Quiz IA</h1>
<div class="input-section">
<label for="topicInput">Entrez un sujet pour le quiz :</label>
<input type="text" id="topicInput" placeholder="Ex: La Révolution Française">
<button id="generateButton">Générer le Quiz</button>
</div>
<div id="statusMessage"></div> <!-- Zone pour les messages (chargement, erreurs, succès) -->
<div id="quizContainer">
<!-- Le quiz généré apparaîtra ici -->
</div>
<!-- Bouton pour soumettre/vérifier les réponses (initialement caché) -->
<button id="submitQuizButton" style="display: none;">Vérifier mes réponses</button>
<!-- Zone pour afficher le score (initialement cachée) -->
<div id="scoreContainer" style="display: none;"></div>
</div>
<script>
// Références aux éléments du DOM
const topicInput = document.getElementById('topicInput');
const generateButton = document.getElementById('generateButton');
const statusMessage = document.getElementById('statusMessage');
const quizContainer = document.getElementById('quizContainer');
const submitQuizButton = document.getElementById('submitQuizButton');
const scoreContainer = document.getElementById('scoreContainer');
// Variable pour stocker les données du quiz actuel (questions, réponses, explications)
let currentQuizData = [];
// --- ÉCOUTEURS D'ÉVÉNEMENTS ---
generateButton.addEventListener('click', handleGenerateClick);
// Permet de lancer la génération avec la touche Entrée
topicInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
event.preventDefault(); // Empêche le comportement par défaut (qui pourrait être une soumission de formulaire)
handleGenerateClick();
}
});
// Vérification des réponses au clic
submitQuizButton.addEventListener('click', checkAnswers);
// --- FONCTIONS PRINCIPALES ---
/**
* Gère le clic sur le bouton "Générer le Quiz".
* Lance la requête vers le backend Flask.
*/
async function handleGenerateClick() {
const topic = topicInput.value.trim();
if (!topic) {
displayMessage('Veuillez entrer un sujet.', 'error');
return;
}
// Réinitialisation de l'interface et état de chargement
generateButton.disabled = true;
topicInput.disabled = true; // Désactiver aussi l'input pendant le chargement
quizContainer.innerHTML = ''; // Vider l'ancien quiz
submitQuizButton.style.display = 'none'; // Cacher le bouton de soumission
submitQuizButton.disabled = true; // Désactiver au cas où il serait déjà visible
submitQuizButton.textContent = 'Vérifier mes réponses'; // Reset text
scoreContainer.style.display = 'none'; // Cacher le score précédent
displayMessage('Génération du quiz en cours... Cela peut prendre quelques instants.', 'loading');
try {
// Appel à l'API backend
const response = await fetch('/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topic: topic }),
});
const data = await response.json(); // Tenter de parser JSON même si erreur HTTP
clearMessage(); // Effacer le message de chargement
if (!response.ok) {
// Gérer les erreurs HTTP et les erreurs retournées par le backend
const errorMsg = data?.error || `Erreur serveur ${response.status}.`;
console.error("Erreur backend:", data); // Log l'erreur complète pour le débogage
throw new Error(errorMsg);
}
// Traitement de la réponse réussie
if (data.success && data.quiz && Array.isArray(data.quiz)) {
if (data.quiz.length > 0) {
currentQuizData = data.quiz; // Stocker les données du quiz
displayQuiz(currentQuizData); // Afficher le quiz
displayMessage(`Quiz sur "${topic}" généré ! Répondez aux questions ci-dessous.`, 'success');
submitQuizButton.style.display = 'block'; // Afficher le bouton Vérifier
submitQuizButton.disabled = false; // Activer le bouton
} else {
// L'IA a réussi mais n'a retourné aucune question
displayMessage("Aucune question n'a pu être générée pour ce sujet. Essayez d'être plus précis ou un sujet différent.", 'info');
}
} else {
// Réponse OK mais contenu inattendu ou erreur signalée dans 'data'
throw new Error(data.error || 'Format de réponse inattendu du serveur.');
}
} catch (error) {
// Gérer les erreurs réseau ou les erreurs levées pendant le traitement
console.error('Erreur lors de la génération du quiz:', error);
clearMessage(); // S'assurer que le message de chargement est parti
// Afficher un message d'erreur plus générique à l'utilisateur
displayMessage(`Erreur: ${error.message}. Veuillez réessayer ou contacter le support si le problème persiste.`, 'error');
} finally {
// Réactiver les contrôles d'entrée dans tous les cas
generateButton.disabled = false;
topicInput.disabled = false;
}
}
/**
* Affiche les questions du quiz dans le conteneur HTML.
* @param {Array} quizData - Le tableau d'objets questions/réponses.
*/
function displayQuiz(quizData) {
quizContainer.innerHTML = ''; // Nettoyer avant d'ajouter
quizData.forEach((q, index) => {
// Validation robuste de la structure de chaque question
if (!q || typeof q.question !== 'string' || !Array.isArray(q.options) || q.options.length < 2 || typeof q.correct_answer !== 'string' || !q.options.includes(q.correct_answer)) {
console.warn("Format de question de quiz invalide ou incomplet ignoré:", q);
// Afficher un message à l'utilisateur pourrait être utile ici
// displayMessage(`Erreur interne: Une des questions reçues est mal formée (index ${index}).`, 'warning'); // Créez un style .warning si besoin
return; // Passer à la question suivante
}
const questionElement = document.createElement('div');
questionElement.classList.add('quiz-question');
// Stocker l'index pour une référence facile lors de la vérification
questionElement.dataset.questionIndex = index;
// Texte de la question
const questionText = document.createElement('p');
questionText.classList.add('question-text'); // Utiliser la classe spécifique
// Sanitize potentially harmful HTML, although usually not needed if source is trusted AI
// questionText.textContent = `${index + 1}. ${q.question}`; // Safer option
questionText.innerHTML = `${index + 1}. ${q.question}`; // Allows basic formatting if needed
questionElement.appendChild(questionText);
// Liste des options
const optionsList = document.createElement('div');
optionsList.classList.add('options-list');
// Mélanger les options pour éviter que la bonne réponse soit toujours à la même place
const shuffledOptions = [...q.options].sort(() => Math.random() - 0.5);
shuffledOptions.forEach((option, optionIndex) => {
const optionId = `q${index}_option${optionIndex}`; // ID unique pour le label et l'input
// Conteneur pour chaque option (radio + label)
const optionItem = document.createElement('div');
optionItem.classList.add('option-item');
const radioInput = document.createElement('input');
radioInput.type = 'radio';
radioInput.id = optionId;
radioInput.name = `question_${index}`; // Nom de groupe unique pour cette question
radioInput.value = option; // La valeur est le texte de l'option (important pour la vérification)
const radioLabel = document.createElement('label');
radioLabel.htmlFor = optionId; // Lie le label à la radio
// radioLabel.textContent = option; // Safer
radioLabel.innerHTML = option; // Allows basic formatting in options
// Ajoute un écouteur pour cliquer sur tout l'item sélectionne la radio
optionItem.addEventListener('click', () => {
if (!radioInput.disabled) { // Ne pas sélectionner si déjà vérifié/désactivé
radioInput.checked = true;
}
});
optionItem.appendChild(radioInput);
optionItem.appendChild(radioLabel);
optionsList.appendChild(optionItem);
});
questionElement.appendChild(optionsList);
// Ajouter l'élément pour l'explication (caché initialement)
// Vérifier que l'explication existe et n'est pas vide
if (q.explanation && typeof q.explanation === 'string' && q.explanation.trim() !== '') {
const explanationElement = document.createElement('div');
explanationElement.classList.add('explanation');
// explanationElement.textContent = `Explication : ${q.explanation}`; // Safer
explanationElement.innerHTML = `<strong>Explication :</strong> ${q.explanation}`; // Permet le gras
questionElement.appendChild(explanationElement);
}
quizContainer.appendChild(questionElement);
});
}
/**
* Vérifie les réponses sélectionnées par l'utilisateur, affiche le feedback et le score.
*/
function checkAnswers() {
let score = 0;
const totalQuestions = currentQuizData.length;
if (totalQuestions === 0) return; // Ne rien faire s'il n'y a pas de quiz chargé
// Désactiver le bouton pendant la vérification
submitQuizButton.disabled = true;
submitQuizButton.textContent = 'Vérification...'; // Feedback visuel
// Optionnel: Faire défiler la page vers le haut pour voir le début du quiz/score
window.scrollTo({ top: quizContainer.offsetTop - 20, behavior: 'smooth' });
currentQuizData.forEach((qData, index) => {
const questionElement = quizContainer.querySelector(`.quiz-question[data-question-index="${index}"]`);
if (!questionElement) return; // Sécurité
const optionsItems = questionElement.querySelectorAll('.option-item');
// Trouver la radio qui est cochée pour cette question
const selectedRadio = questionElement.querySelector(`input[name="question_${index}"]:checked`);
const correctAnswerValue = qData.correct_answer;
const explanationElement = questionElement.querySelector('.explanation');
let questionAnswered = false; // Pour savoir si l'utilisateur a répondu
optionsItems.forEach(item => {
const radio = item.querySelector('input[type="radio"]');
item.classList.add('disabled'); // Désactiver visuellement l'item
radio.disabled = true; // Désactiver la radio fonctionnellement
// Retirer les anciens styles de feedback au cas où (ne devrait pas arriver normalement)
item.classList.remove('correct', 'incorrect', 'missed-correct');
// Vérifier si cette option est la bonne réponse
if (radio.value === correctAnswerValue) {
// C'est la bonne réponse
if (selectedRadio && selectedRadio.value === correctAnswerValue) {
// Et l'utilisateur l'a sélectionnée
item.classList.add('correct');
score++;
questionAnswered = true;
} else {
// C'est la bonne réponse, mais l'utilisateur a choisi autre chose ou rien
item.classList.add('missed-correct'); // Mettre en évidence la bonne réponse manquée
}
} else if (selectedRadio && selectedRadio.value === radio.value) {
// C'est une mauvaise réponse, et l'utilisateur l'a sélectionnée
item.classList.add('incorrect');
questionAnswered = true;
}
});
// Afficher l'explication s'il y en a une et si la question a été répondue (ou toujours l'afficher ?)
// On choisit de toujours l'afficher après vérification
if (explanationElement) {
explanationElement.classList.add('visible');
}
// Optionnel : Ajouter un style si la question n'a pas été répondue
// if (!questionAnswered) { questionElement.style.opacity = '0.7'; }
});
// Afficher le score final
scoreContainer.innerHTML = `Votre score : <span class="score-value">${score} / ${totalQuestions}</span>`;
scoreContainer.style.display = 'block';
// Mettre à jour le bouton de soumission (par ex., indiquer que c'est terminé)
submitQuizButton.textContent = 'Quiz Terminé !';
// Le bouton reste désactivé pour empêcher une nouvelle vérification
}
// --- FONCTIONS UTILITAIRES ---
/**
* Affiche un message dans la zone de statut.
* @param {string} message - Le texte du message.
* @param {'info' | 'error' | 'loading' | 'success'} type - Le type de message pour le style CSS.
*/
function displayMessage(message, type = 'info') {
statusMessage.textContent = message;
// Assigne la classe CSS correspondante (remplace les précédentes)
statusMessage.className = type;
statusMessage.style.display = 'block'; // Rendre visible
}
/**
* Efface le message de la zone de statut.
*/
function clearMessage() {
statusMessage.textContent = '';
statusMessage.className = ''; // Retire toutes les classes de type
statusMessage.style.display = 'none'; // Cacher la zone
}
</script>
</body>
</html> |