Ajoute la correction sur fichier aux exercices BioCID et FDS

Reprend le fonctionnement du TP Nuisibles (« Corriger » à partir du PDF
généré avec les réponses du groupe) pour les deux exercices.

- BioCID et FDS : carte « Identification du groupe » (groupe, date,
  formateur) sur l'écran d'intro, reprise dans l'en-tête du PDF exporté.
- FDS : ajout d'un export PDF (inexistant) récapitulant réponse du groupe
  et réponse attendue, partie par partie, avec le score global.
- Bouton « Corriger un rendu » sur l'écran final des deux exercices.
- Nouvelle page correction-biocid.html : lecture du PDF, réponses
  regroupées par exercice avec badge renseigné / sans réponse.
- Nouvelle page correction-fds.html : notation automatique du QCM
  (anneau de réussite, score par partie, ✓/✗/? par question).
This commit is contained in:
2026-07-15 23:07:45 +02:00
parent c1bd61c10c
commit a418558393
4 changed files with 1298 additions and 4 deletions

View File

@@ -0,0 +1,507 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Correction BioCID — Certibiocide</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@600;700;800&family=DM+Sans:ital,wght@0,400;0,500;1,400&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../header-style.css"/>
<style>
body {
padding: calc(36px + 70px) 20px 80px;
}
.wrap { max-width: 820px; margin: 0 auto; }
.syne { font-family: 'Syne', sans-serif; }
.eyebrow {
font-family: 'Syne', sans-serif;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--amber);
margin-bottom: 6px;
}
h1 {
font-family: 'Syne', sans-serif;
font-size: 28px;
font-weight: 800;
letter-spacing: -0.5px;
margin-bottom: 28px;
}
/* DROPZONE */
#dropzone {
background: #fff;
border: 2px dashed rgba(186,117,23,0.35);
border-radius: 16px;
padding: 56px 24px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
#dropzone.drag { border-color: var(--amber); background: var(--amber-light); }
#dropzone .icon { font-size: 40px; margin-bottom: 12px; }
#dropzone p.title { font-family: 'Syne', sans-serif; font-weight: 700; font-size: 16px; margin-bottom: 6px; }
#dropzone p.sub { color: var(--text-muted); font-size: 13.5px; margin-bottom: 20px; }
#filePicker { display: none; }
.btn-pick {
font-family: 'Syne', sans-serif;
display: inline-block;
background: var(--amber);
color: #fff;
font-weight: 700;
font-size: 13px;
padding: 10px 26px;
border-radius: 30px;
cursor: pointer;
border: none;
}
#status { margin-top: 16px; font-size: 13px; }
#status.loading { color: var(--amber); }
#status.error { color: var(--coral); }
/* RESULT */
#result { display: none; }
.toprow {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
}
#fileName { font-size: 12.5px; color: var(--text-muted); }
.btn-reset {
font-family: 'Syne', sans-serif;
background: transparent;
border: 1px solid var(--border);
border-radius: 20px;
padding: 6px 16px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
color: var(--text-muted);
}
.btn-reset:hover { border-color: var(--amber); color: var(--amber); }
/* BANDEAU PARTICIPANTS */
#participantsBanner {
background: linear-gradient(135deg, var(--amber) 0%, #D8923A 100%);
border-radius: 16px;
padding: 20px 26px;
margin-bottom: 24px;
display: flex;
align-items: center;
gap: 16px;
color: #fff;
flex-wrap: wrap;
}
#participantsBanner .pb-icon {
width: 48px; height: 48px;
border-radius: 12px;
background: rgba(255,255,255,0.2);
display: flex; align-items: center; justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
#participantsBanner .pb-group {
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.85;
margin-bottom: 4px;
}
#participantsBanner .pb-names {
font-family: 'Syne', sans-serif;
font-size: 18px;
font-weight: 700;
}
#participantsBanner .pb-meta {
margin-left: auto;
font-size: 12.5px;
text-align: right;
opacity: 0.92;
line-height: 1.5;
}
.progress-row { display: flex; align-items: center; gap: 10px; margin-bottom: 30px; font-size: 13px; color: var(--text-muted); }
.progress-track { flex: 1; height: 7px; background: rgba(0,0,0,0.08); border-radius: 6px; overflow: hidden; }
.progress-fill { height: 100%; background: var(--amber); transition: width 0.4s ease; }
.progress-row strong { font-family: 'Syne', sans-serif; color: var(--text); }
/* CARROUSEL DE THEMES */
.carousel-wrap { margin-bottom: 36px; }
.carousel-nav {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.nav-btn {
width: 38px; height: 38px;
border-radius: 50%;
border: 1px solid var(--border);
background: #fff;
font-size: 16px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, border-color 0.15s;
}
.nav-btn:hover { background: var(--amber-light); border-color: var(--amber); }
.nav-btn:disabled { opacity: 0.3; cursor: default; }
.nav-btn:disabled:hover { background: #fff; border-color: var(--border); }
.dots { display: flex; gap: 7px; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(0,0,0,0.15); cursor: pointer; transition: background 0.15s, transform 0.15s; }
.dot.active { background: var(--amber); transform: scale(1.3); }
.theme-panel {
background: #fff;
border: 1px solid var(--border);
border-radius: 18px;
padding: 26px 28px;
min-height: 360px;
}
.theme-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 18px;
}
.theme-icon {
width: 44px; height: 44px;
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 22px;
flex-shrink: 0;
}
.theme-title { font-family: 'Syne', sans-serif; font-weight: 800; font-size: 17px; }
.theme-count { font-size: 12.5px; color: var(--text-muted); margin-left: auto; }
.cards { display: flex; flex-direction: column; gap: 10px; }
.qcard {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 18px;
display: flex;
gap: 12px;
align-items: flex-start;
transition: box-shadow 0.2s;
}
.qcard:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.06); }
.qcard .badge {
width: 22px; height: 22px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 12px;
flex-shrink: 0;
margin-top: 1px;
}
.qcard .badge.ok { background: var(--teal-light); color: var(--teal); }
.qcard .badge.miss { background: var(--coral-light); color: var(--coral); }
.qcard .qtext { flex: 1; }
.qcard .qlabel { font-weight: 600; font-size: 13.5px; margin-bottom: 4px; }
.qcard .qanswer { font-size: 13.5px; white-space: pre-wrap; }
.qcard .qanswer.empty { color: var(--coral); font-style: italic; }
</style>
</head>
<body>
<header>
<div class="logo">Gauthier Chombart & Nathan Chauwin</div>
<nav>
<a href="../index.html">Accueil</a>
<a href="../index.html#formations">Exercices</a>
<a href="../index.html#documentation">Documentation</a>
<a href="../index.html#rapports">Rapports</a>
<a href="../index.html#about">À propos</a>
<a href="../index.html#contact">Contact</a>
</nav>
</header>
<div class="wrap">
<div id="participantsBanner" style="display:none;"></div>
<div class="eyebrow">Certibiocide — Correction d'exercice</div>
<h1>Plateforme BioCID — Lecture du rendu</h1>
<div id="dropzone">
<div class="icon">📄</div>
<p class="title">Déposez le PDF du groupe ici</p>
<p class="sub">ou cliquez pour sélectionner le fichier exporté depuis l'exercice BioCID</p>
<label class="btn-pick">Choisir un fichier
<input type="file" id="filePicker" accept="application/pdf">
</label>
<div id="status"></div>
</div>
<div id="result">
<div class="toprow">
<span id="fileName"></span>
<button class="btn-reset" onclick="resetAll()">↺ Charger un autre PDF</button>
</div>
<div class="progress-row">
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
<span><strong id="progressCount"></strong> questions renseignées</span>
</div>
<div class="carousel-wrap">
<div class="carousel-nav">
<button class="nav-btn" id="prevBtn" onclick="goTo(currentTheme - 1)"></button>
<div class="dots" id="dots"></div>
<button class="nav-btn" id="nextBtn" onclick="goTo(currentTheme + 1)"></button>
</div>
<div id="themePanel"></div>
</div>
</div>
</div>
<footer>
<p>&copy; 2026 Gauthier Chombart & Nathan Chauwin<br>Tous droits réservés — Formations Certibiocide & Techniques — Formateurs indépendants</p>
<p style="margin-top: 10px; opacity: 0.6;">Tous droits réservés — Gauthier Chombart & Nathan Chauwin — Formateurs indépendants</p>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
// Les 4 exercices, avec les intitulés exacts exportés (data-question) comme clés.
const THEMES = [
{
title: "Exercice 1 — Recherche par nom",
icon: "🔎", color: "amber",
items: [
"Ex1 — Produit : type, substance active, fabricant",
"Ex1 — Date d'autorisation",
"Ex1 — Conditions d'utilisation",
]
},
{
title: "Exercice 2 — Recherche par type",
icon: "🗂️", color: "blue",
items: [
"Ex2 — Nombre de produits dans la catégorie",
"Ex2 — Comparaison entre catégories",
]
},
{
title: "Exercice 3 — Recherche par substance",
icon: "🧪", color: "teal",
items: [
"Ex3 — 3 produits contenant la substance",
"Ex3 — Formulations et concentrations comparées",
]
},
{
title: "Exercice 4 — Cas de synthèse",
icon: "📞", color: "coral",
items: [
"Ex4 — 3 produits applicables",
"Ex4 — Fiche de chaque produit",
"Ex4 — Argumentaire (+ bonus FDS)",
]
},
];
const ALL_SECTIONS = THEMES.flatMap(t => t.items);
const COLOR_MAP = {
amber: { bg: "var(--amber-light)", fg: "var(--amber-dark)" },
blue: { bg: "var(--blue-light)", fg: "var(--blue)" },
teal: { bg: "var(--teal-light)", fg: "var(--teal)" },
coral: { bg: "var(--coral-light)", fg: "var(--coral)" },
};
function escapeRx(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }
function escapeHtml(s) {
if (!s) return "";
return s.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}
function isEmpty(v) {
if (!v) return true;
const t = v.trim();
return t === "" || /^\(sans réponse\)/i.test(t);
}
function captureBetween(text, label, nextLabels) {
const startRx = new RegExp(escapeRx(label) + "\\s*:?\\s*", "i");
const m = startRx.exec(text);
if (!m) return "";
const from = m.index + m[0].length;
let to = text.length;
for (const nl of nextLabels) {
const endRx = new RegExp(escapeRx(nl), "i");
const m2 = endRx.exec(text.slice(from));
if (m2 && from + m2.index < to) to = from + m2.index;
}
return text.slice(from, to).trim();
}
function parsePdfText(raw) {
let clean = raw.replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").replace(/\s+/g, " ").trim();
// Retire les en-têtes/pieds d'impression du navigateur (date, titre, about:blank) sans toucher au corps
clean = clean.replace(/\d{1,2}\/\d{1,2}\/\d{4},?\s+\d{1,2}:\d{2}/g, " ");
clean = clean.replace(/Exercices BioCID\s*[—–-]\s*Réponses\s*\([^)]*\)/gi, " ");
clean = clean.replace(/about:blank\s*\d{1,3}\s*\/\s*\d{1,3}/gi, " ");
clean = clean.replace(/about:blank/gi, " ");
clean = clean.replace(/\s+\d{1,3}\/\d{1,3}\s*$/, " ");
clean = clean.replace(/\s+/g, " ").trim();
const meta = {
nom: captureBetween(clean, "Groupe / Stagiaire", ["Date de session"]),
date_session: captureBetween(clean, "Date de session", ["Formateur"]),
formateur: captureBetween(clean, "Formateur", ["Réponses aux exercices"]),
};
// Sépare "Groupe N" du nom des participants
const groupMatch = /^Groupe\s*(\d+)\s*[—\-:]*\s*(.*)$/i.exec(meta.nom.trim());
if (groupMatch) {
meta.groupe = "Groupe " + groupMatch[1];
meta.participants = groupMatch[2].trim();
} else {
meta.groupe = "";
meta.participants = meta.nom.trim();
}
const startIdx = clean.search(/Réponses aux exercices/i);
const block = startIdx >= 0 ? clean.slice(startIdx) : clean;
const answers = {};
for (let i = 0; i < ALL_SECTIONS.length; i++) {
const label = ALL_SECTIONS[i];
const nextLabel = ALL_SECTIONS[i + 1];
answers[label] = captureBetween(block, label, nextLabel ? [nextLabel] : []);
}
return { meta, answers };
}
let currentThemeData = null;
let currentTheme = 0;
function renderThemePanel() {
const theme = THEMES[currentTheme];
const data = currentThemeData;
const colors = COLOR_MAP[theme.color];
const themeAnswered = theme.items.filter(s => !isEmpty(data.answers[s])).length;
document.getElementById("themePanel").innerHTML = `
<div class="theme-panel">
<div class="theme-head">
<div class="theme-icon" style="background:${colors.bg};color:${colors.fg};">${theme.icon}</div>
<div class="theme-title">${theme.title}</div>
<div class="theme-count">${themeAnswered}/${theme.items.length}</div>
</div>
<div class="cards">
${theme.items.map(label => {
const value = data.answers[label] || "";
const empty = isEmpty(value);
const displayLabel = label.replace(/^Ex\d+\s*[—-]\s*/, "");
return `
<div class="qcard">
<div class="badge ${empty ? 'miss' : 'ok'}">${empty ? '!' : '✓'}</div>
<div class="qtext">
<div class="qlabel">${escapeHtml(displayLabel)}</div>
<div class="qanswer ${empty ? 'empty' : ''}">${empty ? 'Sans réponse' : escapeHtml(value)}</div>
</div>
</div>
`;
}).join("")}
</div>
</div>
`;
document.getElementById("prevBtn").disabled = currentTheme === 0;
document.getElementById("nextBtn").disabled = currentTheme === THEMES.length - 1;
document.querySelectorAll(".dot").forEach((d, i) => d.classList.toggle("active", i === currentTheme));
}
function goTo(idx) {
if (idx < 0 || idx >= THEMES.length) return;
currentTheme = idx;
renderThemePanel();
}
function renderResult(data) {
currentThemeData = data;
currentTheme = 0;
const banner = document.getElementById("participantsBanner");
const noms = data.meta.participants || data.meta.nom;
if (noms) {
banner.style.display = "flex";
banner.innerHTML = `
<div class="pb-icon">👥</div>
<div>
<div class="pb-group">${escapeHtml(data.meta.groupe) || "Participants"}</div>
<div class="pb-names">${escapeHtml(noms)}</div>
</div>
<div class="pb-meta">
${data.meta.date_session ? "Session : " + escapeHtml(data.meta.date_session) + "<br>" : ""}
${data.meta.formateur ? "Formateur : " + escapeHtml(data.meta.formateur) : ""}
</div>
`;
} else {
banner.style.display = "none";
}
const answeredCount = ALL_SECTIONS.filter(s => !isEmpty(data.answers[s])).length;
document.getElementById("progressFill").style.width = (answeredCount / ALL_SECTIONS.length * 100) + "%";
document.getElementById("progressCount").textContent = `${answeredCount} / ${ALL_SECTIONS.length}`;
document.getElementById("dots").innerHTML = THEMES.map((_, i) =>
`<div class="dot${i === 0 ? ' active' : ''}" onclick="goTo(${i})"></div>`
).join("");
renderThemePanel();
document.getElementById("dropzone").style.display = "none";
document.getElementById("result").style.display = "block";
}
async function handleFile(file) {
if (!file) return;
const statusEl = document.getElementById("status");
statusEl.className = "loading";
statusEl.textContent = "Lecture du PDF en cours…";
document.getElementById("fileName").textContent = file.name;
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let fullText = "";
for (let p = 1; p <= pdf.numPages; p++) {
const page = await pdf.getPage(p);
const content = await page.getTextContent();
fullText += content.items.map(it => it.str).join(" ") + "\n";
}
const data = parsePdfText(fullText);
renderResult(data);
statusEl.textContent = "";
} catch (e) {
console.error(e);
statusEl.className = "error";
statusEl.textContent = "Impossible de lire ce PDF. Vérifiez qu'il s'agit bien d'un export de l'exercice BioCID.";
}
}
function resetAll() {
document.getElementById("result").style.display = "none";
document.getElementById("dropzone").style.display = "block";
document.getElementById("participantsBanner").style.display = "none";
document.getElementById("status").textContent = "";
document.getElementById("filePicker").value = "";
}
const dropzone = document.getElementById("dropzone");
dropzone.addEventListener("dragover", e => { e.preventDefault(); dropzone.classList.add("drag"); });
dropzone.addEventListener("dragleave", () => dropzone.classList.remove("drag"));
dropzone.addEventListener("drop", e => {
e.preventDefault();
dropzone.classList.remove("drag");
const file = e.dataTransfer.files?.[0];
if (file) handleFile(file);
});
document.getElementById("filePicker").addEventListener("change", e => handleFile(e.target.files?.[0]));
</script>
</body>
</html>