Files
Site_web/exercices/correction-tp-nuisibles.html
gauthier.chombart 44be41b2b2 Renomme la marque en Gauthier Chombart & Nathan Chauwin et finalise le jeu Memory
Header, footer, titres et balises meta de toutes les pages actives : remplace NextGN Formation par Gauthier Chombart & Nathan Chauwin. Nouveau pied de page uniforme mentionnant les droits reserves et le statut de formateurs independants (dossier archives/ laisse inchange).

Integre aussi la refonte du jeu Memory (CSS/JS/HUD/leaderboard) et ajoute le rapport de veille Certibiocide semaine 28.
2026-07-14 23:02:09 +02:00

651 lines
22 KiB
HTML
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.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Correction TP Nuisibles — 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); }
.id-card { display: none; }
/* 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;
}
#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;
}
.id-card .label {
font-family: 'Syne', sans-serif;
font-size: 10.5px;
font-weight: 700;
color: var(--amber-dark);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.id-card .value { font-size: 14.5px; margin-top: 2px; }
.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(--teal); 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: 420px;
}
.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; }
.table-wrap {
background: #fff;
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 18px;
font-size: 13px;
margin-bottom: 14px;
}
/* TABLEAU ANALYSE DE RISQUES */
.risk-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.risk-table th {
font-family: 'Syne', sans-serif;
text-align: left;
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--amber-dark);
background: var(--amber-light);
padding: 10px 12px;
}
.risk-table th:first-child { border-radius: 8px 0 0 8px; }
.risk-table th:last-child { border-radius: 0 8px 8px 0; }
.risk-table td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
vertical-align: top;
white-space: pre-wrap;
}
.risk-table tr:last-child td { border-bottom: none; }
.meta-line { font-size: 12.5px; color: var(--text-muted); margin-top: 20px; }
.meta-line strong { color: var(--text); }
</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 de TP</div>
<h1>TP Nuisibles — Lecture du rendu</h1>
<div id="dropzone">
<div class="icon">📄</div>
<p class="title">Déposez le PDF du stagiaire ici</p>
<p class="sub">ou cliquez pour sélectionner le fichier exporté depuis le TP</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="id-card" id="idCard"></div>
<div class="progress-row">
<div class="progress-track"><div class="progress-fill" id="progressFill"></div></div>
<span><strong id="progressCount"></strong> questions répondues</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>
<h2 class="syne" style="font-size:16px;font-weight:800;margin-bottom:12px;color:var(--amber-dark);">Analyse de risques</h2>
<div class="table-wrap" id="tp3Meta"></div>
<div class="table-wrap" id="tp3Table" style="display:none;"></div>
<p class="meta-line" id="suiviLine"></p>
</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";
// Thèmes regroupant les sections du TP, avec icône et couleur
const THEMES = [
{
title: "Premier contact & diagnostic",
icon: "🗣️", color: "purple",
items: [
"Questions posées au client lors du premier contact",
"Conseils donnés en premier lieu",
"Actions de prévention menées sur le site",
"Éléments observés / preuve de présence du nuisible",
]
},
{
title: "Solutions envisagées",
icon: "🧰", color: "blue",
items: [
"Solutions alternatives non-biocides",
"Solutions biocides mises en place",
"Type de produit (TP) et mode de traitement",
"Suivi mis en place et durée",
"Gestion des déchets",
]
},
{
title: "Produit & traçabilité",
icon: "🧪", color: "teal",
items: [
"FDS trouvée (éléments principaux)",
"Nom du produit et fabricant/fournisseur",
"Numéro AMM",
]
},
{
title: "Sécurité & santé",
icon: "🛡️", color: "coral",
items: [
"Risques pour la santé",
"EPI à porter",
"Risques environnement / dispersion accidentelle",
"DL50 substance(s) active(s)",
"Gestes de premiers secours",
]
},
];
const ALL_SECTIONS = THEMES.flatMap(t => t.items);
const COLOR_MAP = {
purple: { bg: "var(--purple-light)", fg: "var(--purple)" },
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 répétitions d'en-tête de page (ex: "17/06/2026 15:42 Résultats TP — ... about:blank 1/2")
clean = clean.replace(/\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d{2}\s+Résultats TP[^]*?about:blank\s*\d\/\d/gi, " ");
const meta = {
nom: captureBetween(clean, "Groupe / Stagiaire", ["Date de session"]),
date_session: captureBetween(clean, "Date de session", ["Formateur"]),
formateur: captureBetween(clean, "Formateur", ["Cas pratique"]),
cas: captureBetween(clean, "Cas pratique", ["Réponses TP1"]),
};
// Sépare "Groupe N" du nom des participants (ex: "Groupe 1 Romain Kevin Valere Greg P")
const groupMatch = /^Groupe\s*(\d+)\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 TP1\s*&?\s*TP2/i);
const endIdx = clean.search(/TP3\s*[—-]\s*Analyse de risques/i);
const block = startIdx >= 0 ? clean.slice(startIdx, endIdx >= 0 ? endIdx : clean.length) : 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] : []);
}
const tp3Zone = endIdx >= 0 ? clean.slice(endIdx) : "";
const tp3 = {
intitule: captureBetween(tp3Zone, "Intitulé", ["Description"]),
description: captureBetween(tp3Zone, "Description", ["Lieu"]),
lieu: captureBetween(tp3Zone, "Lieu", ["Début prévu le"]),
debut: captureBetween(tp3Zone, "Début prévu le", ["Étapes"]),
};
// Tableau de risques : on découpe en s'appuyant sur les intitulés d'étapes connus du TP
const KNOWN_STEPS = [
"remplir la poudreuse",
"sécurisation du site",
"application traitements",
"rangement du matériel. purger la perche",
"retrait du nid",
"gestion des déchets. élimination par des filiaires spé.",
];
let riskRows = [];
const tableMatch = /Étapes\s*Risques\s*Mesures de prévention([\s\S]*?)(Réalisée par|$)/i.exec(tp3Zone);
if (tableMatch) {
let raw = tableMatch[1].trim();
if (raw) {
// Repère les positions de chaque étape connue dans le texte aplati
const positions = [];
KNOWN_STEPS.forEach(step => {
const idx = raw.toLowerCase().indexOf(step.toLowerCase());
if (idx >= 0) positions.push({ step, idx });
});
positions.sort((a, b) => a.idx - b.idx);
if (positions.length > 0) {
for (let i = 0; i < positions.length; i++) {
const from = positions[i].idx + positions[i].step.length;
const to = i + 1 < positions.length ? positions[i + 1].idx : raw.length;
let rest = raw.slice(from, to).trim();
// rest contient "risque... mesure..." -> on coupe sur les mots clés de mesures (gants/masques, barrieres, etc.)
const measureRx = /(gants\s*\/\s*masques\s*\/\s*lunettes[^.]*|barrieres\.?\s*epi)/i;
const mm = measureRx.exec(rest);
let risque = rest, mesure = "";
if (mm) {
risque = rest.slice(0, mm.index).trim();
mesure = rest.slice(mm.index).trim();
}
risque = risque.replace(/^[\s\-.]+/, "").trim();
riskRows.push({ etape: positions[i].step, risque: risque || "—", mesure: mesure || "—" });
}
} else {
riskRows.push({ etape: raw, risque: "", mesure: "" });
}
}
}
const mReal = clean.match(/Réalisée par\s*:\s*([^—\n]*)/i);
const mVerif = clean.match(/Vérifiée par\s*:\s*([^—\n]*)/i);
const mMaj = clean.match(/Mise à jour par\s*:\s*([^—\n]*)/i);
return {
meta, answers, tp3, riskRows,
suivi: {
realise: mReal ? mReal[1].trim() : "",
verifie: mVerif ? mVerif[1].trim() : "",
maj: mMaj ? mMaj[1].trim() : "",
}
};
}
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);
return `
<div class="qcard">
<div class="badge ${empty ? 'miss' : 'ok'}">${empty ? '!' : '✓'}</div>
<div class="qtext">
<div class="qlabel">${escapeHtml(label)}</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");
if (data.meta.participants) {
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(data.meta.participants)}</div>
</div>
`;
} else {
banner.style.display = "none";
}
document.getElementById("idCard").innerHTML = `
<div><div class="label">Groupe / Stagiaire</div><div class="value">${escapeHtml(data.meta.nom) || "—"}</div></div>
<div><div class="label">Date de session</div><div class="value">${escapeHtml(data.meta.date_session) || "—"}</div></div>
<div><div class="label">Formateur</div><div class="value">${escapeHtml(data.meta.formateur) || "—"}</div></div>
<div><div class="label">Cas pratique</div><div class="value">${escapeHtml(data.meta.cas) || "—"}</div></div>
`;
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("tp3Meta").innerHTML = `
<p style="margin-bottom:6px;"><strong>Intitulé :</strong> ${escapeHtml(data.tp3.intitule) || '<em style="color:var(--coral)">—</em>'}</p>
<p style="margin-bottom:6px;"><strong>Description :</strong> ${escapeHtml(data.tp3.description) || '<em style="color:var(--coral)">—</em>'}</p>
<p style="margin-bottom:6px;"><strong>Lieu :</strong> ${escapeHtml(data.tp3.lieu) || '<em style="color:var(--coral)">—</em>'}</p>
<p><strong>Début prévu le :</strong> ${escapeHtml(data.tp3.debut) || '<em style="color:var(--coral)">—</em>'}</p>
`;
const tp3TableEl = document.getElementById("tp3Table");
if (data.riskRows && data.riskRows.length > 0) {
tp3TableEl.style.display = "block";
tp3TableEl.innerHTML = `
<table class="risk-table">
<thead><tr><th>Étapes</th><th>Risques</th><th>Mesures de prévention</th></tr></thead>
<tbody>
${data.riskRows.map(r => `
<tr>
<td>${escapeHtml(r.etape) || "—"}</td>
<td>${escapeHtml(r.risque) || "—"}</td>
<td>${escapeHtml(r.mesure) || "—"}</td>
</tr>
`).join("")}
</tbody>
</table>
`;
} else {
tp3TableEl.style.display = "none";
}
document.getElementById("suiviLine").innerHTML =
`Réalisée par : <strong>${escapeHtml(data.suivi.realise) || "—"}</strong> — Vérifiée par : <strong>${escapeHtml(data.suivi.verifie) || "—"}</strong> — Mise à jour par : <strong>${escapeHtml(data.suivi.maj) || "—"}</strong>`;
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 du TP.";
}
}
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 = "";
document.getElementById("tp3Table").style.display = "none";
}
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>