|
|
<!DOCTYPE html> |
|
|
<html lang="fr"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>Quiz Interactif</title> |
|
|
<style> |
|
|
* { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
body { |
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|
|
min-height: 100vh; |
|
|
padding: 20px; |
|
|
} |
|
|
|
|
|
.container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
h1 { |
|
|
color: white; |
|
|
text-align: center; |
|
|
margin-bottom: 30px; |
|
|
font-size: 2.5em; |
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.header-controls { |
|
|
background: white; |
|
|
padding: 20px; |
|
|
border-radius: 15px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
margin-bottom: 30px; |
|
|
display: flex; |
|
|
gap: 15px; |
|
|
align-items: center; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.file-input-container { |
|
|
display: flex; |
|
|
gap: 15px; |
|
|
align-items: center; |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.scoring-settings { |
|
|
display: flex; |
|
|
gap: 10px; |
|
|
align-items: center; |
|
|
padding: 10px 15px; |
|
|
background: #f5f5f5; |
|
|
border-radius: 8px; |
|
|
} |
|
|
|
|
|
.scoring-input { |
|
|
width: 50px; |
|
|
padding: 5px 8px; |
|
|
border: 2px solid #e0e0e0; |
|
|
border-radius: 5px; |
|
|
font-size: 14px; |
|
|
text-align: center; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.scoring-label { |
|
|
font-size: 14px; |
|
|
color: #666; |
|
|
font-weight: 500; |
|
|
} |
|
|
|
|
|
.file-input-label { |
|
|
display: inline-block; |
|
|
padding: 12px 24px; |
|
|
background: #667eea; |
|
|
color: white; |
|
|
border-radius: 8px; |
|
|
cursor: pointer; |
|
|
font-weight: 600; |
|
|
font-size: 16px; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.file-input-label:hover { |
|
|
background: #5568d3; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
|
|
|
#fileInput { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
.btn-connect { |
|
|
padding: 12px 24px; |
|
|
background: #4CAF50; |
|
|
color: white; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
cursor: pointer; |
|
|
font-weight: 600; |
|
|
font-size: 16px; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.btn-connect:hover { |
|
|
background: #45a049; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4); |
|
|
} |
|
|
|
|
|
.modules-status { |
|
|
color: #666; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.main-content { |
|
|
display: grid; |
|
|
grid-template-columns: 2fr 1fr; |
|
|
gap: 20px; |
|
|
} |
|
|
|
|
|
.quiz-card { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 30px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.question { |
|
|
font-size: 1.5em; |
|
|
font-weight: bold; |
|
|
color: #333; |
|
|
margin-bottom: 25px; |
|
|
line-height: 1.4; |
|
|
} |
|
|
|
|
|
.question-number { |
|
|
color: #667eea; |
|
|
margin-right: 10px; |
|
|
} |
|
|
|
|
|
.answers { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.answer { |
|
|
padding: 15px 20px; |
|
|
border: 2px solid #e0e0e0; |
|
|
border-radius: 10px; |
|
|
transition: all 0.3s ease; |
|
|
font-size: 1.1em; |
|
|
background: #f9f9f9; |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
} |
|
|
|
|
|
.answer-letter { |
|
|
background: #667eea; |
|
|
color: white; |
|
|
width: 35px; |
|
|
height: 35px; |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
font-weight: bold; |
|
|
margin-right: 15px; |
|
|
} |
|
|
|
|
|
.answer-content { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.answer.correct { |
|
|
border-color: #4CAF50; |
|
|
background: #e8f5e9; |
|
|
} |
|
|
|
|
|
.answer.correct .answer-letter { |
|
|
background: #4CAF50; |
|
|
} |
|
|
|
|
|
.modules-panel { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
.module-card { |
|
|
background: white; |
|
|
border-radius: 10px; |
|
|
padding: 12px 15px; |
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.module-header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-bottom: 8px; |
|
|
padding-bottom: 8px; |
|
|
border-bottom: 2px solid #667eea; |
|
|
} |
|
|
|
|
|
.module-name { |
|
|
font-weight: bold; |
|
|
color: #667eea; |
|
|
font-size: 0.95em; |
|
|
} |
|
|
|
|
|
.disconnect-btn { |
|
|
background: #f44336; |
|
|
color: white; |
|
|
padding: 4px 8px; |
|
|
font-size: 11px; |
|
|
border: none; |
|
|
border-radius: 4px; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
} |
|
|
|
|
|
.disconnect-btn:hover { |
|
|
background: #da190b; |
|
|
} |
|
|
|
|
|
.module-info { |
|
|
display: flex; |
|
|
justify-content: space-around; |
|
|
align-items: center; |
|
|
gap: 10px; |
|
|
} |
|
|
|
|
|
.module-info-item { |
|
|
text-align: center; |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.module-answer-label { |
|
|
font-size: 0.75em; |
|
|
color: #666; |
|
|
margin-bottom: 4px; |
|
|
} |
|
|
|
|
|
.module-answer-value { |
|
|
font-size: 1.3em; |
|
|
font-weight: bold; |
|
|
color: #667eea; |
|
|
} |
|
|
|
|
|
.module-answer-value.correct { |
|
|
color: #4CAF50; |
|
|
} |
|
|
|
|
|
.module-answer-value.incorrect { |
|
|
color: #f44336; |
|
|
} |
|
|
|
|
|
.module-waiting { |
|
|
color: #999; |
|
|
font-style: italic; |
|
|
} |
|
|
|
|
|
.module-answered { |
|
|
color: #667eea; |
|
|
font-style: normal; |
|
|
} |
|
|
|
|
|
.controls { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
margin-top: 30px; |
|
|
gap: 15px; |
|
|
} |
|
|
|
|
|
button { |
|
|
padding: 12px 30px; |
|
|
font-size: 16px; |
|
|
border: none; |
|
|
border-radius: 8px; |
|
|
cursor: pointer; |
|
|
transition: all 0.3s ease; |
|
|
font-weight: 600; |
|
|
} |
|
|
|
|
|
.btn-primary { |
|
|
background: #667eea; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.btn-primary:hover:not(:disabled) { |
|
|
background: #5568d3; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); |
|
|
} |
|
|
|
|
|
.btn-primary:disabled { |
|
|
background: #cccccc; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.btn-results { |
|
|
background: #ff9800; |
|
|
color: white; |
|
|
width: 100%; |
|
|
margin-top: 20px; |
|
|
} |
|
|
|
|
|
.btn-results:hover:not(:disabled) { |
|
|
background: #f57c00; |
|
|
transform: translateY(-2px); |
|
|
box-shadow: 0 5px 15px rgba(255, 152, 0, 0.4); |
|
|
} |
|
|
|
|
|
.btn-results:disabled { |
|
|
background: #cccccc; |
|
|
cursor: not-allowed; |
|
|
} |
|
|
|
|
|
.progress { |
|
|
text-align: center; |
|
|
color: #666; |
|
|
font-weight: 600; |
|
|
font-size: 1.1em; |
|
|
} |
|
|
|
|
|
.score-card { |
|
|
background: white; |
|
|
border-radius: 15px; |
|
|
padding: 40px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
text-align: center; |
|
|
} |
|
|
|
|
|
.score-title { |
|
|
font-size: 2em; |
|
|
color: #667eea; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
|
|
|
.score-value { |
|
|
font-size: 3em; |
|
|
font-weight: bold; |
|
|
color: #333; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.score-message { |
|
|
font-size: 1.3em; |
|
|
color: #666; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
|
|
|
.hidden { |
|
|
display: none; |
|
|
} |
|
|
|
|
|
@media (max-width: 968px) { |
|
|
.main-content { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
} |
|
|
|
|
|
/* Système de notifications */ |
|
|
.notification { |
|
|
position: fixed; |
|
|
top: 20px; |
|
|
right: 20px; |
|
|
min-width: 300px; |
|
|
max-width: 500px; |
|
|
padding: 20px 25px; |
|
|
border-radius: 12px; |
|
|
box-shadow: 0 10px 30px rgba(0,0,0,0.3); |
|
|
color: white; |
|
|
font-weight: 500; |
|
|
font-size: 15px; |
|
|
z-index: 10000; |
|
|
animation: slideIn 0.3s ease-out; |
|
|
display: flex; |
|
|
align-items: flex-start; |
|
|
gap: 12px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
|
|
|
.notification.success { |
|
|
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); |
|
|
} |
|
|
|
|
|
.notification.error { |
|
|
background: linear-gradient(135deg, #f44336 0%, #da190b 100%); |
|
|
} |
|
|
|
|
|
.notification.info { |
|
|
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%); |
|
|
} |
|
|
|
|
|
.notification-icon { |
|
|
font-size: 24px; |
|
|
flex-shrink: 0; |
|
|
} |
|
|
|
|
|
.notification-content { |
|
|
flex: 1; |
|
|
white-space: pre-line; |
|
|
} |
|
|
|
|
|
.notification-close { |
|
|
background: rgba(255,255,255,0.2); |
|
|
border: none; |
|
|
color: white; |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
border-radius: 50%; |
|
|
cursor: pointer; |
|
|
font-size: 16px; |
|
|
line-height: 1; |
|
|
padding: 0; |
|
|
flex-shrink: 0; |
|
|
transition: background 0.2s; |
|
|
} |
|
|
|
|
|
.notification-close:hover { |
|
|
background: rgba(255,255,255,0.3); |
|
|
} |
|
|
|
|
|
@keyframes slideIn { |
|
|
from { |
|
|
transform: translateX(400px); |
|
|
opacity: 0; |
|
|
} |
|
|
to { |
|
|
transform: translateX(0); |
|
|
opacity: 1; |
|
|
} |
|
|
} |
|
|
|
|
|
@keyframes slideOut { |
|
|
from { |
|
|
transform: translateX(0); |
|
|
opacity: 1; |
|
|
} |
|
|
to { |
|
|
transform: translateX(400px); |
|
|
opacity: 0; |
|
|
} |
|
|
} |
|
|
|
|
|
.notification.hiding { |
|
|
animation: slideOut 0.3s ease-in forwards; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<h1>📝 Quiz Interactif</h1> |
|
|
|
|
|
<div class="header-controls"> |
|
|
<div class="file-input-container"> |
|
|
<label for="fileInput" class="file-input-label"> |
|
|
📁 Charger un quiz (.txt) |
|
|
</label> |
|
|
<input type="file" id="fileInput" accept=".txt"> |
|
|
</div> |
|
|
<div class="scoring-settings"> |
|
|
<span class="scoring-label">✅</span> |
|
|
<input type="number" id="correctScore" class="scoring-input" value="1" min="-10" max="10"> |
|
|
<span class="scoring-label">❌</span> |
|
|
<input type="number" id="incorrectScore" class="scoring-input" value="0" min="-10" max="10"> |
|
|
</div> |
|
|
<button id="connectBtn" class="btn-connect">🔵 Connecter module</button> |
|
|
<div class="modules-status" id="modulesStatus">Aucun module</div> |
|
|
</div> |
|
|
|
|
|
<div id="quizContainer" class="hidden"> |
|
|
<div class="main-content"> |
|
|
<div> |
|
|
<div class="quiz-card" id="questionCard"> |
|
|
<div class="question"> |
|
|
<span class="question-number"></span> |
|
|
<span class="question-text"></span> |
|
|
</div> |
|
|
<div class="answers" id="answersContainer"></div> |
|
|
<button id="showResultsBtn" class="btn-results" disabled> |
|
|
🎯 Afficher les résultats |
|
|
</button> |
|
|
<div class="controls"> |
|
|
<button id="prevBtn" class="btn-primary">← Précédent</button> |
|
|
<div class="progress" id="progress"></div> |
|
|
<button id="nextBtn" class="btn-primary">Suivant →</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="modules-panel" id="modulesPanel"> |
|
|
<!-- Les modules connectés apparaîtront ici --> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div id="scoreContainer" class="hidden"> |
|
|
<div class="score-card"> |
|
|
<div class="score-title">🎉 Quiz terminé !</div> |
|
|
<div id="scoresDetails"></div> |
|
|
<button class="btn-primary" onclick="location.reload()">Recommencer</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
// Configuration BLE |
|
|
const SERVICE_UUID = '4fafc201-1fb5-459e-8fcc-c5c9c331914b'; |
|
|
const CHARACTERISTIC_UUID = 'beb5483e-36e1-4688-b7f5-ea07361b26a8'; |
|
|
|
|
|
let questions = []; |
|
|
let currentQuestionIndex = 0; |
|
|
let connectedModules = new Map(); // moduleName -> { device, characteristic, answer, scores } |
|
|
let resultsShown = false; |
|
|
|
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
const connectBtn = document.getElementById('connectBtn'); |
|
|
const quizContainer = document.getElementById('quizContainer'); |
|
|
const scoreContainer = document.getElementById('scoreContainer'); |
|
|
const modulesStatus = document.getElementById('modulesStatus'); |
|
|
const modulesPanel = document.getElementById('modulesPanel'); |
|
|
const prevBtn = document.getElementById('prevBtn'); |
|
|
const nextBtn = document.getElementById('nextBtn'); |
|
|
const showResultsBtn = document.getElementById('showResultsBtn'); |
|
|
const correctScoreInput = document.getElementById('correctScore'); |
|
|
const incorrectScoreInput = document.getElementById('incorrectScore'); |
|
|
|
|
|
// Fonction pour afficher une notification |
|
|
function showNotification(message, type = 'info', duration = 5000) { |
|
|
const notification = document.createElement('div'); |
|
|
notification.className = `notification ${type}`; |
|
|
|
|
|
const icons = { |
|
|
success: '✅', |
|
|
error: '❌', |
|
|
info: 'ℹ️' |
|
|
}; |
|
|
|
|
|
notification.innerHTML = ` |
|
|
<div class="notification-icon">${icons[type]}</div> |
|
|
<div class="notification-content">${message}</div> |
|
|
<button class="notification-close" onclick="this.parentElement.remove()">×</button> |
|
|
`; |
|
|
|
|
|
document.body.appendChild(notification); |
|
|
|
|
|
if (duration > 0) { |
|
|
setTimeout(() => { |
|
|
notification.classList.add('hiding'); |
|
|
setTimeout(() => notification.remove(), 300); |
|
|
}, duration); |
|
|
} |
|
|
} |
|
|
|
|
|
// Vérifier si Web Bluetooth est disponible |
|
|
if (!navigator.bluetooth) { |
|
|
showNotification('Web Bluetooth non supporté. Utilisez Chrome, Edge ou Opera.', 'error', 0); |
|
|
connectBtn.disabled = true; |
|
|
} |
|
|
|
|
|
// Fonction pour obtenir le scoring actuel |
|
|
function getScoringConfig() { |
|
|
return { |
|
|
correctAnswer: parseInt(correctScoreInput.value) || 1, |
|
|
incorrectAnswer: parseInt(incorrectScoreInput.value) || 0 |
|
|
}; |
|
|
} |
|
|
|
|
|
fileInput.addEventListener('change', handleFileSelect); |
|
|
connectBtn.addEventListener('click', connectModule); |
|
|
prevBtn.addEventListener('click', showPreviousQuestion); |
|
|
nextBtn.addEventListener('click', showNextQuestion); |
|
|
showResultsBtn.addEventListener('click', showResults); |
|
|
|
|
|
function updateModulesStatus() { |
|
|
const count = connectedModules.size; |
|
|
modulesStatus.textContent = count === 0 |
|
|
? 'Aucun module' |
|
|
: `${count} module${count > 1 ? 's' : ''}`; |
|
|
} |
|
|
|
|
|
async function connectModule() { |
|
|
try { |
|
|
console.log('Recherche module...'); |
|
|
|
|
|
const device = await navigator.bluetooth.requestDevice({ |
|
|
filters: [{ services: [SERVICE_UUID] }] |
|
|
}); |
|
|
|
|
|
console.log('Module trouvé:', device.name); |
|
|
|
|
|
const server = await device.gatt.connect(); |
|
|
const service = await server.getPrimaryService(SERVICE_UUID); |
|
|
const characteristic = await service.getCharacteristic(CHARACTERISTIC_UUID); |
|
|
|
|
|
// Lire l'état initial |
|
|
const initialValue = await characteristic.readValue(); |
|
|
const initialData = new TextDecoder().decode(initialValue); |
|
|
console.log('État initial:', initialData); |
|
|
|
|
|
const data = JSON.parse(initialData); |
|
|
const moduleName = data.module; |
|
|
|
|
|
// Stocker le module |
|
|
connectedModules.set(moduleName, { |
|
|
device: device, |
|
|
characteristic: characteristic, |
|
|
answer: null, |
|
|
totalScore: 0, |
|
|
gpios: data.gpios |
|
|
}); |
|
|
|
|
|
// Créer la carte du module |
|
|
createModuleCard(moduleName); |
|
|
updateModulesStatus(); |
|
|
|
|
|
// Activer les notifications |
|
|
await characteristic.startNotifications(); |
|
|
|
|
|
characteristic.addEventListener('characteristicvaluechanged', (event) => { |
|
|
const value = new TextDecoder().decode(event.target.value); |
|
|
console.log('🔔 Notification de', moduleName, ':', value); |
|
|
|
|
|
try { |
|
|
const data = JSON.parse(value); |
|
|
handleModuleData(moduleName, data); |
|
|
} catch (e) { |
|
|
console.error('Erreur parsing:', e); |
|
|
} |
|
|
}); |
|
|
|
|
|
// Gérer la déconnexion |
|
|
device.addEventListener('gattserverdisconnected', () => { |
|
|
console.log(moduleName, 'déconnecté'); |
|
|
connectedModules.delete(moduleName); |
|
|
document.getElementById(`module-${moduleName}`)?.remove(); |
|
|
updateModulesStatus(); |
|
|
}); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Erreur connexion:', error); |
|
|
showNotification(`Erreur de connexion: ${error.message}`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
function createModuleCard(moduleName) { |
|
|
const card = document.createElement('div'); |
|
|
card.className = 'module-card'; |
|
|
card.id = `module-${moduleName}`; |
|
|
|
|
|
card.innerHTML = ` |
|
|
<div class="module-header"> |
|
|
<div class="module-name">${moduleName}</div> |
|
|
<button class="disconnect-btn" onclick="disconnectModule('${moduleName}')"> |
|
|
✕ |
|
|
</button> |
|
|
</div> |
|
|
<div class="module-info"> |
|
|
<div class="module-info-item"> |
|
|
<div class="module-answer-label">Réponse</div> |
|
|
<div class="module-answer-value module-waiting" id="answer-${moduleName}"> |
|
|
- |
|
|
</div> |
|
|
</div> |
|
|
<div class="module-info-item"> |
|
|
<div class="module-answer-label">Score</div> |
|
|
<div class="module-answer-value" id="score-${moduleName}"> |
|
|
0 |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
modulesPanel.appendChild(card); |
|
|
} |
|
|
|
|
|
function handleModuleData(moduleName, data) { |
|
|
const module = connectedModules.get(moduleName); |
|
|
if (!module) return; |
|
|
|
|
|
// Mettre à jour les GPIO |
|
|
module.gpios = data.gpios; |
|
|
|
|
|
console.log('📊 GPIOs pour', moduleName, ':', data.gpios); |
|
|
|
|
|
// Détecter la réponse (A=GPIO15, B=GPIO4, C=GPIO5, D=GPIO18) |
|
|
// Réponse détectée si GPIO est à l'état BAS (1) |
|
|
let answer = null; |
|
|
data.gpios.forEach(gpio => { |
|
|
console.log(` GPIO ${gpio.pin} = ${gpio.state} (type: ${typeof gpio.state})`); |
|
|
if (String(gpio.state) === '1') { |
|
|
if (gpio.pin === 15) answer = 'A'; |
|
|
else if (gpio.pin === 4) answer = 'B'; |
|
|
else if (gpio.pin === 5) answer = 'C'; |
|
|
else if (gpio.pin === 18) answer = 'D'; |
|
|
} |
|
|
}); |
|
|
|
|
|
console.log('➡️ Réponse détectée:', answer); |
|
|
|
|
|
if (answer && !resultsShown) { |
|
|
// Permet de modifier la réponse tant que les résultats ne sont pas affichés |
|
|
if (module.answer !== answer) { |
|
|
module.answer = answer; |
|
|
updateModuleAnswer(moduleName, answer); |
|
|
checkAllAnswersReceived(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function updateModuleAnswer(moduleName, answer) { |
|
|
const answerDiv = document.getElementById(`answer-${moduleName}`); |
|
|
if (answerDiv) { |
|
|
answerDiv.textContent = '✓ Répondu'; |
|
|
answerDiv.classList.remove('module-waiting'); |
|
|
answerDiv.classList.add('module-answered'); |
|
|
} |
|
|
} |
|
|
|
|
|
function checkAllAnswersReceived() { |
|
|
let allAnswered = true; |
|
|
for (let [name, module] of connectedModules.entries()) { |
|
|
if (!module.answer) { |
|
|
allAnswered = false; |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
showResultsBtn.disabled = !allAnswered || connectedModules.size === 0; |
|
|
} |
|
|
|
|
|
function showResults() { |
|
|
if (resultsShown) return; |
|
|
resultsShown = true; |
|
|
|
|
|
const question = questions[currentQuestionIndex]; |
|
|
const correctAnswerIndex = question.correctAnswer; |
|
|
const correctLetter = ['A', 'B', 'C', 'D'][correctAnswerIndex]; |
|
|
const scoringConfig = getScoringConfig(); |
|
|
|
|
|
// Afficher la bonne réponse |
|
|
const answerDivs = document.querySelectorAll('.answer'); |
|
|
answerDivs[correctAnswerIndex].classList.add('correct'); |
|
|
|
|
|
// Vérifier les réponses de chaque module |
|
|
for (let [name, module] of connectedModules.entries()) { |
|
|
const answerDiv = document.getElementById(`answer-${name}`); |
|
|
const scoreDiv = document.getElementById(`score-${name}`); |
|
|
|
|
|
if (answerDiv) { |
|
|
// Afficher la réponse du module |
|
|
answerDiv.textContent = module.answer || '-'; |
|
|
answerDiv.classList.remove('module-answered'); |
|
|
|
|
|
if (module.answer === correctLetter) { |
|
|
answerDiv.classList.add('correct'); |
|
|
module.totalScore += scoringConfig.correctAnswer; |
|
|
} else { |
|
|
answerDiv.classList.add('incorrect'); |
|
|
module.totalScore += scoringConfig.incorrectAnswer; |
|
|
} |
|
|
|
|
|
// Mettre à jour l'affichage du score |
|
|
if (scoreDiv) { |
|
|
scoreDiv.textContent = module.totalScore; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
showResultsBtn.disabled = true; |
|
|
} |
|
|
|
|
|
function handleFileSelect(event) { |
|
|
const file = event.target.files[0]; |
|
|
if (file) { |
|
|
const reader = new FileReader(); |
|
|
reader.onload = function(e) { |
|
|
const content = e.target.result; |
|
|
try { |
|
|
parseQuizFile(content); |
|
|
if (questions.length > 0) { |
|
|
showNotification(`Quiz chargé avec succès!\n\n${questions.length} question(s) trouvée(s).`, 'success', 3000); |
|
|
startQuiz(); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Erreur lors du chargement du quiz:', error); |
|
|
questions = []; |
|
|
// L'erreur a déjà été affichée dans parseQuizFile |
|
|
} |
|
|
}; |
|
|
reader.readAsText(file); |
|
|
} |
|
|
} |
|
|
|
|
|
function parseQuizFile(content) { |
|
|
questions = []; |
|
|
const lines = content.trim().split('\n'); |
|
|
let currentQuestion = null; |
|
|
let lineNumber = 0; |
|
|
const errors = []; |
|
|
|
|
|
for (let line of lines) { |
|
|
lineNumber++; |
|
|
const originalLine = line; |
|
|
line = line.trim(); |
|
|
if (!line) continue; |
|
|
|
|
|
if (line.startsWith('* ')) { |
|
|
// Valider la question précédente avant d'en commencer une nouvelle |
|
|
if (currentQuestion) { |
|
|
const questionErrors = validateQuestion(currentQuestion, lineNumber - 1); |
|
|
if (questionErrors.length > 0) { |
|
|
errors.push(...questionErrors); |
|
|
} else { |
|
|
questions.push(currentQuestion); |
|
|
} |
|
|
} |
|
|
|
|
|
const questionText = line.substring(2).trim(); |
|
|
if (!questionText) { |
|
|
errors.push(`Ligne ${lineNumber}: Question vide après '*'`); |
|
|
} |
|
|
|
|
|
currentQuestion = { |
|
|
question: questionText, |
|
|
answers: [], |
|
|
correctAnswer: -1, |
|
|
lineNumber: lineNumber |
|
|
}; |
|
|
} else if (line.startsWith('+ ')) { |
|
|
if (!currentQuestion) { |
|
|
errors.push(`Ligne ${lineNumber}: Réponse correcte '+' trouvée sans question précédente`); |
|
|
continue; |
|
|
} |
|
|
|
|
|
const answerText = line.substring(2).trim(); |
|
|
if (!answerText) { |
|
|
errors.push(`Ligne ${lineNumber}: Réponse correcte vide après '+'`); |
|
|
} |
|
|
|
|
|
if (currentQuestion.correctAnswer !== -1) { |
|
|
errors.push(`Ligne ${lineNumber}: Question à la ligne ${currentQuestion.lineNumber} a déjà une réponse correcte (ligne avec '+')`); |
|
|
} |
|
|
|
|
|
currentQuestion.correctAnswer = currentQuestion.answers.length; |
|
|
currentQuestion.answers.push(answerText); |
|
|
} else if (line.startsWith('- ')) { |
|
|
if (!currentQuestion) { |
|
|
errors.push(`Ligne ${lineNumber}: Réponse '-' trouvée sans question précédente`); |
|
|
continue; |
|
|
} |
|
|
|
|
|
const answerText = line.substring(2).trim(); |
|
|
if (!answerText) { |
|
|
errors.push(`Ligne ${lineNumber}: Réponse vide après '-'`); |
|
|
} |
|
|
|
|
|
currentQuestion.answers.push(answerText); |
|
|
} else { |
|
|
// Ligne avec un format non reconnu |
|
|
errors.push(`Ligne ${lineNumber}: Format non reconnu. Utilisez '* ' pour une question, '+ ' pour la bonne réponse, '- ' pour les mauvaises réponses`); |
|
|
} |
|
|
} |
|
|
|
|
|
// Valider la dernière question |
|
|
if (currentQuestion) { |
|
|
const questionErrors = validateQuestion(currentQuestion, lineNumber); |
|
|
if (questionErrors.length > 0) { |
|
|
errors.push(...questionErrors); |
|
|
} else { |
|
|
questions.push(currentQuestion); |
|
|
} |
|
|
} |
|
|
|
|
|
// Vérifier s'il y a des erreurs |
|
|
if (errors.length > 0) { |
|
|
const errorMessage = `Erreurs détectées dans le fichier de quiz:\n\n${errors.join('\n')}`; |
|
|
showNotification(errorMessage, 'error', 10000); |
|
|
throw new Error('Format de quiz invalide'); |
|
|
} |
|
|
|
|
|
// Vérifier qu'il y a au moins une question |
|
|
if (questions.length === 0) { |
|
|
showNotification('Erreur: Aucune question valide trouvée dans le fichier.\n\nFormat attendu:\n* Question ?\n+ Bonne réponse\n- Mauvaise réponse\n- Mauvaise réponse', 'error', 8000); |
|
|
throw new Error('Aucune question trouvée'); |
|
|
} |
|
|
} |
|
|
|
|
|
function validateQuestion(question, lineNumber) { |
|
|
const errors = []; |
|
|
|
|
|
// Vérifier qu'il y a au moins 2 réponses |
|
|
if (question.answers.length < 2) { |
|
|
errors.push(`Ligne ${question.lineNumber}: Question "${question.question}" n'a que ${question.answers.length} réponse(s). Minimum requis: 2`); |
|
|
} |
|
|
|
|
|
// Vérifier qu'il y a maximum 4 réponses (A, B, C, D) |
|
|
if (question.answers.length > 4) { |
|
|
errors.push(`Ligne ${question.lineNumber}: Question "${question.question}" a ${question.answers.length} réponses. Maximum autorisé: 4 (A, B, C, D)`); |
|
|
} |
|
|
|
|
|
// Vérifier qu'il y a une réponse correcte |
|
|
if (question.correctAnswer === -1) { |
|
|
errors.push(`Ligne ${question.lineNumber}: Question "${question.question}" n'a pas de réponse correcte (utilisez '+' devant la bonne réponse)`); |
|
|
} |
|
|
|
|
|
return errors; |
|
|
} |
|
|
|
|
|
function startQuiz() { |
|
|
quizContainer.classList.remove('hidden'); |
|
|
currentQuestionIndex = 0; |
|
|
|
|
|
// Réinitialiser les scores |
|
|
for (let [name, module] of connectedModules.entries()) { |
|
|
module.totalScore = 0; |
|
|
} |
|
|
|
|
|
showQuestion(); |
|
|
} |
|
|
|
|
|
function showQuestion() { |
|
|
if (currentQuestionIndex >= questions.length) { |
|
|
showFinalScore(); |
|
|
return; |
|
|
} |
|
|
|
|
|
resultsShown = false; |
|
|
const question = questions[currentQuestionIndex]; |
|
|
|
|
|
// Afficher la question |
|
|
document.querySelector('.question-number').textContent = |
|
|
`Question ${currentQuestionIndex + 1}/${questions.length}`; |
|
|
document.querySelector('.question-text').textContent = question.question; |
|
|
|
|
|
// Afficher les réponses |
|
|
const answersContainer = document.getElementById('answersContainer'); |
|
|
answersContainer.innerHTML = ''; |
|
|
|
|
|
const letters = ['A', 'B', 'C', 'D']; |
|
|
question.answers.forEach((answer, index) => { |
|
|
const answerDiv = document.createElement('div'); |
|
|
answerDiv.className = 'answer'; |
|
|
|
|
|
answerDiv.innerHTML = ` |
|
|
<div class="answer-content"> |
|
|
<div class="answer-letter">${letters[index]}</div> |
|
|
<div>${answer}</div> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
answersContainer.appendChild(answerDiv); |
|
|
}); |
|
|
|
|
|
// Réinitialiser les réponses des modules |
|
|
for (let [name, module] of connectedModules.entries()) { |
|
|
module.answer = null; |
|
|
const answerDiv = document.getElementById(`answer-${name}`); |
|
|
if (answerDiv) { |
|
|
answerDiv.textContent = '-'; |
|
|
answerDiv.className = 'module-answer-value module-waiting'; |
|
|
} |
|
|
} |
|
|
|
|
|
// Mettre à jour les contrôles |
|
|
document.getElementById('progress').textContent = |
|
|
`${currentQuestionIndex + 1} / ${questions.length}`; |
|
|
prevBtn.disabled = currentQuestionIndex === 0; |
|
|
nextBtn.textContent = currentQuestionIndex === questions.length - 1 ? 'Voir les scores' : 'Suivant →'; |
|
|
showResultsBtn.disabled = true; |
|
|
} |
|
|
|
|
|
function showPreviousQuestion() { |
|
|
if (currentQuestionIndex > 0) { |
|
|
currentQuestionIndex--; |
|
|
showQuestion(); |
|
|
} |
|
|
} |
|
|
|
|
|
function showNextQuestion() { |
|
|
if (currentQuestionIndex < questions.length - 1) { |
|
|
currentQuestionIndex++; |
|
|
showQuestion(); |
|
|
} else { |
|
|
showFinalScore(); |
|
|
} |
|
|
} |
|
|
|
|
|
function showFinalScore() { |
|
|
quizContainer.classList.add('hidden'); |
|
|
scoreContainer.classList.remove('hidden'); |
|
|
|
|
|
const scoresDetails = document.getElementById('scoresDetails'); |
|
|
let html = ''; |
|
|
|
|
|
for (let [name, module] of connectedModules.entries()) { |
|
|
const percentage = Math.round((module.totalScore / questions.length) * 100); |
|
|
html += ` |
|
|
<div style="margin: 20px 0; padding: 20px; background: #f5f5f5; border-radius: 10px;"> |
|
|
<div style="font-size: 1.5em; font-weight: bold; color: #667eea; margin-bottom: 10px;"> |
|
|
${name} |
|
|
</div> |
|
|
<div style="font-size: 2em; font-weight: bold; color: #333;"> |
|
|
${module.totalScore} / ${questions.length} (${percentage}%) |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
if (html === '') { |
|
|
html = '<p style="color: #999;">Aucun module connecté</p>'; |
|
|
} |
|
|
|
|
|
scoresDetails.innerHTML = html; |
|
|
} |
|
|
|
|
|
window.disconnectModule = function(moduleName) { |
|
|
const module = connectedModules.get(moduleName); |
|
|
if (module && module.device.gatt.connected) { |
|
|
module.device.gatt.disconnect(); |
|
|
} |
|
|
}; |
|
|
</script> |
|
|
</body> |
|
|
</html> |