You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

838 lines
27 KiB

<!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;
}
}
</style>
</head>
<body>
<div class="container">
<h1>📝 Quiz Interactif ESP32</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 ESP32</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');
// Vérifier si Web Bluetooth est disponible
if (!navigator.bluetooth) {
alert('Web Bluetooth non supporté. Utilisez Chrome, Edge ou Opera.');
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 ESP32...');
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE_UUID] }]
});
console.log('ESP32 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);
alert(`Erreur: ${error.message}`);
}
}
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;
parseQuizFile(content);
if (questions.length > 0) {
startQuiz();
}
};
reader.readAsText(file);
}
}
function parseQuizFile(content) {
questions = [];
const lines = content.trim().split('\n');
let currentQuestion = null;
for (let line of lines) {
line = line.trim();
if (!line) continue;
if (line.startsWith('* ')) {
if (currentQuestion) {
questions.push(currentQuestion);
}
currentQuestion = {
question: line.substring(2).trim(),
answers: [],
correctAnswer: -1
};
} else if (line.startsWith('+ ')) {
if (currentQuestion) {
currentQuestion.correctAnswer = currentQuestion.answers.length;
currentQuestion.answers.push(line.substring(2).trim());
}
} else if (line.startsWith('- ')) {
if (currentQuestion) {
currentQuestion.answers.push(line.substring(2).trim());
}
}
}
if (currentQuestion) {
questions.push(currentQuestion);
}
}
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>