Files
anonymisation/docs/gen_mockup.py
Domi31tls c427e2a3f4 chore(rgpd): replace CHCB/Bayonne refs in docs (D-12)
Anonymise les références aux entités réelles (CHCB, villes basques,
Saint-Denis, Réunion, etc.) dans la documentation projet, les maquettes
HTML/Python, les notes de coordination et les audits.

Conserve docs/coordination/decisions/2026-06-02_dom_mvp-pivots-strategiques.md
(table de mapping de référence) et docs/coordination/inbox/for-claude/
intacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 14:40:20 +02:00

915 lines
57 KiB
Python

#!/usr/bin/env python3
"""Génère ui_mockup_v6.html — logo embarqué en base64, JS sans apostrophes dans les strings."""
import base64
from pathlib import Path
LOGO_PATH = Path(__file__).parent.parent / "assets" / "logo_header.png"
OUT_PATH = Path(__file__).parent / "ui_mockup_v6.html"
logo_b64 = base64.b64encode(LOGO_PATH.read_bytes()).decode()
LOGO_SRC = "data:image/png;base64," + logo_b64
HTML = r"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>aivanonym v6 &mdash; Prototype UI</title>
<style>
:root{
--bg:#1a1a2e;--card:#16213e;--card-border:#0f3460;
--primary:#e94560;--primary-dim:#c73652;--accent:#f5a623;
--text:#e0e0e0;--text-dim:#9ca3af;--text-muted:#6b7280;
--success:#10b981;--warning:#f59e0b;--danger:#ef4444;--blue:#3b82f6;
--radius:8px;--shadow:0 2px 12px rgba(0,0,0,.4);
--divider:rgba(255,255,255,.06);--btn-sec-bg:rgba(255,255,255,.08);--btn-sec-border:rgba(255,255,255,.14);
}
/* ── CLAIR : fond gris moyen, cartes blanches, bordures visibles ── */
.theme-light{
--bg:#cdd2da;--card:#ffffff;--card-border:#9aa3b0;
--primary:#c93050;--primary-dim:#a82545;--accent:#b45309;
--text:#0d1117;--text-dim:#1f2937;--text-muted:#374151;
--success:#047857;--warning:#b45309;--danger:#b91c1c;--blue:#1d4ed8;
--shadow:0 2px 14px rgba(0,0,0,.22);
--divider:rgba(0,0,0,.09);--btn-sec-bg:rgba(0,0,0,.07);--btn-sec-border:#9aa3b0;
}
/* ── MÉDICAL : fond bleu structuré, cartes légèrement teintées ── */
.theme-medical{
--bg:#b8ceea;--card:#eef5ff;--card-border:#6897ca;
--primary:#1a56db;--primary-dim:#1340b0;--accent:#0369a1;
--text:#071427;--text-dim:#0f2a4a;--text-muted:#1e3a5f;
--success:#166534;--warning:#92400e;--danger:#991b1b;--blue:#1e40af;
--shadow:0 2px 14px rgba(0,50,160,.18);
--divider:rgba(0,50,160,.09);--btn-sec-bg:rgba(0,50,160,.07);--btn-sec-border:#6897ca;
}
/* ── NEUTRE sombre : inchangé, contraste déjà correct ── */
.theme-neutral{
--bg:#1f2937;--card:#374151;--card-border:#6b7280;
--primary:#818cf8;--primary-dim:#6366f1;--accent:#fbbf24;
--text:#f9fafb;--text-dim:#e5e7eb;--text-muted:#d1d5db;
--success:#34d399;--warning:#fbbf24;--danger:#f87171;--blue:#60a5fa;
--shadow:0 2px 12px rgba(0,0,0,.45);
--divider:rgba(255,255,255,.08);--btn-sec-bg:rgba(255,255,255,.08);--btn-sec-border:#6b7280;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);font-size:14px;display:flex;justify-content:center;min-height:100vh}
.app-shell{width:780px;min-height:820px;display:flex;flex-direction:column;background:var(--bg);box-shadow:0 0 40px rgba(0,0,0,.5)}
@media(max-width:800px){.app-shell{width:100%;min-height:100vh}}
/* HEADER */
.header{background:var(--card);border-bottom:3px solid var(--primary);padding:10px 20px;display:flex;align-items:center;gap:12px;flex-shrink:0}
.header img{height:34px;width:auto}
.hv{margin-left:auto;font-size:11px;color:var(--text-muted);background:rgba(128,128,128,.12);padding:3px 8px;border-radius:4px}
/* TABS */
.tabs-bar{display:flex;background:var(--card);border-bottom:1px solid var(--card-border);padding:0 20px;flex-shrink:0;gap:2px}
.tab-btn{padding:9px 16px;cursor:pointer;border:none;background:none;color:var(--text-dim);font-size:13px;font-weight:500;border-bottom:3px solid transparent;margin-bottom:-1px;transition:color .15s;white-space:nowrap}
.tab-btn:hover{color:var(--text)}
.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary);font-weight:600}
/* CONTENT */
.content{flex:1;overflow-y:auto;padding:20px}
.tab-pane{display:none}.tab-pane.active{display:block}
/* CARD */
.card{background:var(--card);border:1px solid var(--card-border);border-radius:var(--radius);padding:18px;margin-bottom:14px;box-shadow:var(--shadow)}
.ct{font-size:12px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:.06em;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.hbtn{margin-left:auto;cursor:pointer;font-size:16px;background:none;border:none;color:var(--text-muted);transition:color .15s;line-height:1;flex-shrink:0}
.hbtn:hover{color:var(--primary)}
/* DROP ZONE */
.dz{border:2px dashed var(--card-border);border-radius:var(--radius);padding:28px 16px;text-align:center;cursor:pointer;transition:border-color .2s,background .2s}
.dz:hover,.dz.over{border-color:var(--primary);background:rgba(233,69,96,.06)}
.dz-icon{font-size:32px;margin-bottom:8px}
.dz-txt{font-size:14px;margin-bottom:3px}
.dz-sub{font-size:12px;color:var(--text-muted)}
.dz-acts{display:flex;gap:8px;justify-content:center;margin-top:12px}
.file-list{margin-top:10px;display:flex;flex-direction:column;gap:5px}
.fi{display:flex;align-items:center;gap:8px;background:var(--divider);border-radius:6px;padding:7px 10px;border:1px solid var(--btn-sec-border)}
.fn{flex:1;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fs{font-size:11px;color:var(--text-muted);flex-shrink:0}
.fx{background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:15px;padding:0 3px}
.fx:hover{color:var(--danger)}
/* FORMAT */
.fmt-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.fmt-card{border:2px solid var(--card-border);border-radius:var(--radius);padding:14px;text-align:center;cursor:pointer;transition:all .15s}
.fmt-card.on{border-color:var(--primary);background:rgba(233,69,96,.07)}
.fi2{font-size:22px;margin-bottom:6px}
.fn2{font-size:13px;font-weight:600}
.fs2{font-size:11px;color:var(--text-muted);margin-top:2px}
/* THEME */
.theme-row{display:flex;gap:8px;flex-wrap:wrap}
.tp{padding:6px 14px;border-radius:99px;border:2px solid var(--card-border);cursor:pointer;font-size:12px;font-weight:500;background:none;color:var(--text-dim);transition:all .15s}
.tp:hover{border-color:var(--primary);color:var(--primary)}
.tp.on{border-color:var(--primary);background:var(--primary);color:#fff}
/* BUTTONS */
.btn{display:inline-flex;align-items:center;gap:5px;padding:8px 16px;border-radius:var(--radius);border:none;cursor:pointer;font-size:13px;font-weight:600;transition:all .15s}
.bp{background:var(--primary);color:#fff}
.bp:hover{background:var(--primary-dim);transform:translateY(-1px)}
.bs{background:var(--btn-sec-bg);color:var(--text);border:1px solid var(--btn-sec-border)}
.bs:hover{background:rgba(128,128,128,.2)}
.bsu{background:var(--success);color:#fff}
.blg{padding:11px 24px;font-size:14px}
.btn:disabled{opacity:.4;cursor:not-allowed;transform:none!important}
.brow{display:flex;justify-content:flex-end;gap:8px;margin-bottom:14px}
/* PROGRESS */
.psec{display:none}.psec.vis{display:block}
.ptrack{background:var(--divider);border:1px solid var(--btn-sec-border);border-radius:99px;height:9px;overflow:hidden;margin:8px 0}
.pfill{height:100%;background:linear-gradient(90deg,var(--primary),var(--accent));border-radius:99px;transition:width .4s ease}
.plbl{display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted)}
.psteps{display:flex;gap:5px;margin-top:10px;flex-wrap:wrap}
.sp{padding:3px 9px;border-radius:99px;font-size:11px;background:rgba(128,128,128,.1);color:var(--text-muted)}
.sp.done{background:rgba(16,185,129,.15);color:var(--success)}
.sp.act{background:rgba(233,69,96,.15);color:var(--primary);font-weight:600}
.log{background:var(--divider);border:1px solid var(--card-border);border-radius:6px;padding:8px;font-family:monospace;font-size:11px;color:var(--text-dim);height:90px;overflow-y:auto;line-height:1.6;margin-top:10px}
.lok{color:var(--success)}
/* RESULTS */
.rgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:12px}
.sc{background:var(--btn-sec-bg);border:1px solid var(--btn-sec-border);border-radius:var(--radius);padding:12px;text-align:center}
.sv{font-size:24px;font-weight:700;color:var(--primary)}
.sl{font-size:11px;color:var(--text-muted);margin-top:2px}
.qbar{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.qs{font-size:30px;font-weight:800;color:var(--success)}
.qg{font-size:16px;font-weight:700;color:var(--success)}
/* SUB-TABS */
.stabs{display:flex;gap:2px;border-bottom:1px solid var(--card-border);margin-bottom:16px}
.stab{padding:7px 14px;border:none;background:none;color:var(--text-dim);font-size:13px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px;transition:color .15s}
.stab:hover{color:var(--text)}
.stab.on{color:var(--primary);border-bottom-color:var(--primary);font-weight:600}
.spane{display:none}.spane.on{display:block}
/* SETTINGS */
.scols{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:600px){.scols{grid-template-columns:1fr}}
.srow{display:flex;align-items:center;justify-content:space-between;padding:9px 0;border-bottom:1px solid var(--divider);gap:12px}
.slbl{font-size:13px}
.shint{font-size:11px;color:var(--text-muted);margin-top:2px}
/* TOGGLE */
.tog{position:relative;display:inline-block;width:38px;height:21px;flex-shrink:0}
.tog input{opacity:0;width:0;height:0}
.tsl{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:rgba(128,128,128,.25);border-radius:99px;transition:.2s}
.tsl:before{content:'';position:absolute;width:15px;height:15px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s}
.tog input:checked+.tsl{background:var(--primary)}
.tog input:checked+.tsl:before{transform:translateX(17px)}
/* TAGS */
.tagrow{display:flex;gap:7px;margin-bottom:9px}
.taginput{flex:1;background:var(--btn-sec-bg);border:1px solid var(--btn-sec-border);border-radius:6px;padding:6px 10px;color:var(--text);font-size:13px;outline:none}
.taginput:focus{border-color:var(--primary)}
.tagcloud{display:flex;flex-wrap:wrap;gap:5px;min-height:28px}
.tag{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:99px;font-size:12px}
.tw{background:rgba(16,185,129,.12);color:var(--success);border:1px solid rgba(16,185,129,.25)}
.tb{background:rgba(233,69,96,.1);color:var(--primary);border:1px solid rgba(233,69,96,.2)}
.tx{cursor:pointer;font-size:13px;opacity:.6}
.tx:hover{opacity:1}
/* SWATCHES */
.swrow{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px}
.sw{width:30px;height:30px;border-radius:6px;cursor:pointer;border:3px solid transparent;transition:border-color .15s}
.sw.on{border-color:var(--primary)}
/* MASK PREVIEW */
.mprev{background:var(--divider);border:1px solid var(--card-border);border-radius:6px;padding:10px 14px;font-size:13px;margin-top:10px;line-height:2}
.mb{background:var(--primary);color:var(--primary);padding:0 6px;border-radius:3px}
.ms2{color:var(--text-muted)}
.mn{background:#000;color:#000;padding:0 6px;border-radius:2px}
/* RULES TABLE */
.rtbl{width:100%;border-collapse:collapse;font-size:12px}
.rtbl th{text-align:left;padding:7px 9px;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--text-muted);border-bottom:1px solid var(--card-border)}
.rtbl td{padding:8px 9px;border-bottom:1px solid rgba(128,128,128,.07);vertical-align:middle}
.rtbl tr:hover td{background:rgba(128,128,128,.04)}
.rst{display:inline-block;padding:2px 7px;border-radius:99px;font-size:10px;font-weight:600}
.ract{background:rgba(16,185,129,.15);color:var(--success)}
.rcand{background:rgba(245,158,11,.15);color:var(--warning)}
.rtyp{font-size:10px;padding:2px 5px;border-radius:4px;background:rgba(128,128,128,.12);color:var(--text-dim)}
/* ABOUT */
.agrid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.ai{display:flex;gap:9px}
.ak{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em}
.av{font-size:13px;font-weight:600;margin-top:2px}
/* NOTE */
.note{font-size:11px;color:var(--text-muted);font-style:italic;padding:5px 9px;background:rgba(59,130,246,.08);border-left:3px solid var(--blue);border-radius:0 4px 4px 0;margin-bottom:12px}
/* MODAL */
.mo{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.65);z-index:1000;align-items:center;justify-content:center}
.mo.open{display:flex}
.mbox{background:var(--card);border:1px solid var(--card-border);border-radius:12px;padding:24px;max-width:460px;width:90%;box-shadow:0 8px 40px rgba(0,0,0,.5);position:relative}
.mtit{font-size:15px;font-weight:700;margin-bottom:12px}
.mbody{font-size:13px;line-height:1.75;color:var(--text-dim)}
.mbody strong{color:var(--text)}
.mbody code{background:rgba(128,128,128,.15);padding:1px 5px;border-radius:4px;font-size:12px}
.mcls{position:absolute;top:14px;right:14px;background:none;border:none;font-size:20px;cursor:pointer;color:var(--text-muted)}
.mcls:hover{color:var(--text)}
/* PDF MASK EDITOR */
.me-panel{display:none;margin-top:14px;border:1px solid var(--card-border);border-radius:var(--radius);overflow:hidden}
.me-panel.open{display:block}
.me-toolbar{background:rgba(0,0,0,.15);border-bottom:1px solid var(--card-border);padding:8px 10px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}
.me-sep{width:1px;height:24px;background:var(--card-border);margin:0 2px;flex-shrink:0}
.me-canvas{background:rgba(0,0,0,.25);min-height:320px;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center}
.me-canvas-inner{position:relative;display:inline-block;box-shadow:0 2px 16px rgba(0,0,0,.5)}
.me-canvas-inner img{display:block;max-width:100%}
.me-overlay{position:absolute;top:0;left:0;width:100%;height:100%;cursor:crosshair}
.me-mask-rect{position:absolute;background:rgba(0,0,0,.85);border:1px solid rgba(255,0,0,.4);cursor:pointer}
.me-mask-rect:hover{border-color:var(--danger);background:#000}
.me-hint{color:var(--text-muted);font-size:12px;text-align:center;padding:20px}
.me-hint span{font-size:32px;display:block;margin-bottom:8px}
.me-status{background:rgba(0,0,0,.15);border-top:1px solid var(--card-border);padding:5px 10px;font-size:11px;color:var(--text-muted);display:flex;gap:12px}
/* TOOLTIP */
[title]{position:relative}
/* native title is enough for mockup */
</style>
</head>
<body>
<div class="app-shell" id="shell">
<!-- HEADER -->
<div class="header">
<img src="LOGO_PLACEHOLDER" alt="aivanonym">
<div class="hv">v6.0 &middot; prototype</div>
</div>
<!-- ONGLETS PRINCIPAUX (3 : Utilisation / Configuration / À propos) -->
<div class="tabs-bar">
<button class="tab-btn active" onclick="ST('use',this)">&#128196; Utilisation</button>
<button class="tab-btn" onclick="ST('cfg',this)">&#9881;&#65039; Configuration</button>
<button class="tab-btn" onclick="ST('about',this)">&#8505;&#65039; &Agrave; propos</button>
</div>
<div class="content">
<!-- ═══ UTILISATION ═══ -->
<div class="tab-pane active" id="tab-use">
<div class="card">
<div class="ct">&#127912; Apparence <button class="hbtn" onclick="H('theme')" title="Aide sur le th&egrave;me">&#10067;</button></div>
<div class="theme-row">
<button class="tp on" onclick="TH('',this)" title="Th&egrave;me sombre">&#127769; Sombre</button>
<button class="tp" onclick="TH('light',this)" title="Th&egrave;me clair">&#9728;&#65039; Clair</button>
<button class="tp" onclick="TH('medical',this)" title="Th&egrave;me hospitalier">&#127973;&#65039; M&eacute;dical</button>
<button class="tp" onclick="TH('neutral',this)" title="Th&egrave;me neutre gris">&#127807; Neutre</button>
</div>
</div>
<div class="card">
<div class="ct">&#128194; Documents &agrave; anonymiser <button class="hbtn" onclick="H('fich')" title="Aide sur les fichiers">&#10067;</button></div>
<div id="dropzone" class="dz" onclick="PICK()"
ondragover="event.preventDefault();this.classList.add('over')"
ondragleave="this.classList.remove('over')"
ondrop="DROP(event)">
<div class="dz-icon">&#11014;&#65039;</div>
<div class="dz-txt">Glissez-d&eacute;posez vos fichiers ici</div>
<div class="dz-sub">PDF &middot; Word &middot; Images &middot; Texte</div>
<div class="dz-acts">
<button class="btn bs" onclick="event.stopPropagation();PICK()" title="Choisir des fichiers individuels">&#128196; Fichiers</button>
<button class="btn bs" onclick="event.stopPropagation();PICKF()" title="Traiter tous les fichiers d'un dossier">&#128194; Dossier entier</button>
</div>
</div>
<div class="file-list" id="flist"></div>
</div>
<div class="card">
<div class="ct">&#128190; Format de sortie <button class="hbtn" onclick="H('fmt')" title="Aide sur les formats">&#10067;</button></div>
<div class="fmt-grid">
<div class="fmt-card on" onclick="this.classList.toggle('on')" title="Exporter un PDF avec les zones masqu&eacute;es en noir">
<div class="fi2">&#128196;</div><div class="fn2">PDF anonymis&eacute;</div><div class="fs2">Zones noircies</div>
</div>
<div class="fmt-card on" onclick="this.classList.toggle('on')" title="Exporter un fichier texte avec les PII remplac&eacute;s par des codes">
<div class="fi2">&#128221;</div><div class="fn2">Texte .txt</div><div class="fs2">Mots remplac&eacute;s par [NOM]&hellip;</div>
</div>
</div>
</div>
<div class="brow">
<button class="btn bs" onclick="CLR()" title="Vider la liste de fichiers">&#10006; Effacer</button>
<button class="btn bp blg" id="btnGo" onclick="GO()" title="Lancer le traitement d'anonymisation">&#9654; Lancer l&apos;anonymisation</button>
</div>
<div class="card psec" id="psec">
<div class="ct">&#8987; Traitement en cours&hellip;</div>
<div class="plbl"><span id="pf">Fichier 1 / 3</span><span id="pp">0 %</span></div>
<div class="ptrack"><div class="pfill" id="pb" style="width:0%"></div></div>
<div class="psteps">
<span class="sp act" id="s0">&#128214; Extraction</span>
<span class="sp" id="s1">&#129504; D&eacute;tection</span>
<span class="sp" id="s2">&#128274; Masquage</span>
<span class="sp" id="s3">&#128196; PDF final</span>
</div>
<div class="log" id="logb"></div>
<div style="text-align:right;margin-top:9px">
<button class="btn bs" onclick="STOP()" title="Arr&ecirc;ter le traitement en cours">&#9209; Arr&ecirc;ter</button>
</div>
</div>
<div class="card psec" id="rsec">
<div class="ct">&#9989; R&eacute;sultats</div>
<div class="rgrid">
<div class="sc"><div class="sv">3</div><div class="sl">Documents</div></div>
<div class="sc"><div class="sv">142</div><div class="sl">PII masqu&eacute;s</div></div>
<div class="sc"><div class="sv">4s</div><div class="sl">Dur&eacute;e</div></div>
<div class="sc"><div class="sv" style="color:var(--success)">A+</div><div class="sl">Qualit&eacute;</div></div>
</div>
<div class="qbar">
<div class="qs">100.0</div>
<div><div class="qg">/ 100 &middot; A+</div><div style="font-size:11px;color:var(--text-muted)">Aucune fuite d&eacute;tect&eacute;e</div></div>
</div>
<div class="log">
<span class="lok">&#10003;</span> CR_23456.pdf &rarr; 47 PII masqu&eacute;s<br>
<span class="lok">&#10003;</span> CRO_81234.pdf &rarr; 38 PII masqu&eacute;s<br>
<span class="lok">&#10003;</span> LETTRE_SORTIE.pdf &rarr; 57 PII masqu&eacute;s
</div>
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
<button class="btn bsu" onclick="alert('Ouverture du dossier de sortie')" title="Ouvrir le dossier contenant les fichiers anonymis&eacute;s">&#128193; Ouvrir le dossier</button>
<button class="btn bs" onclick="RRES()" title="Revenir au d&eacute;but pour traiter d'autres fichiers">&#128260; Nouveau traitement</button>
</div>
</div>
</div><!-- /use -->
<!-- ═══ CONFIGURATION ═══ -->
<div class="tab-pane" id="tab-cfg">
<div class="stabs">
<button class="stab on" onclick="SS('reg',this)">&#9881;&#65039; R&eacute;glages</button>
<button class="stab" onclick="SS('msk',this)">&#127917; Masquage</button>
<button class="stab" onclick="SS('shr',this)">&#128260; Partage</button>
<button class="stab" onclick="SS('rul',this)">&#128737;&#65039; R&egrave;gles <span style="display:inline-block;background:var(--primary);color:#fff;font-size:10px;padding:1px 5px;border-radius:9px;margin-left:4px">2</span></button>
</div>
<!-- RÉGLAGES -->
<div class="spane on" id="sp-reg">
<div class="scols">
<div>
<div class="card">
<div class="ct">&#128269; Donn&eacute;es &agrave; d&eacute;tecter <button class="hbtn" onclick="H('det')" title="Aide sur la d&eacute;tection">&#10067;</button></div>
<div class="srow"><div><div class="slbl">Noms et pr&eacute;noms</div><div class="shint">Gazetteers INSEE &middot; CamemBERT</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">Dates de naissance</div><div class="shint">Uniquement la date de naissance</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">Etablissements</div><div class="shint">R&eacute;pertoire FINESS + contexte</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">Adresses et codes postaux</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">N&deg; s&eacute;curit&eacute; sociale</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">T&eacute;l&eacute;phones et e-mails</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">N&deg; adh&eacute;rent mutuelle</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
</div>
<div class="card">
<div class="ct">&#129504; Moteurs NER <button class="hbtn" onclick="H('ner')" title="Aide sur les moteurs IA">&#10067;</button></div>
<div class="srow"><div><div class="slbl">CamemBERT-bio <span style="font-size:10px;color:var(--success)">RAPIDE</span></div><div class="shint">~10 ms/doc &middot; F1 = 0.963</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">EDS-Pseudo <span style="font-size:10px;color:var(--blue)">PRECIS</span></div><div class="shint">~200 ms/doc &middot; m&eacute;dical fran&ccedil;ais</div></div><label class="tog"><input type="checkbox" checked><span class="tsl"></span></label></div>
<div class="srow"><div><div class="slbl">GLiNER <span style="font-size:10px;color:var(--text-muted)">OPTIONNEL</span></div><div class="shint">~95 ms/doc &middot; vote crois&eacute;</div></div><label class="tog"><input type="checkbox"><span class="tsl"></span></label></div>
</div>
</div>
<div>
<div class="card">
<div class="ct">&#9989; Termes &agrave; toujours conserver <button class="hbtn" onclick="H('wl')" title="Aide liste blanche">&#10067;</button></div>
<div class="note">Ces termes ne seront <strong>jamais masqu&eacute;s</strong>, m&ecirc;me s&rsquo;ils ressemblent &agrave; un nom propre.</div>
<div class="tagrow"><input class="taginput" id="wIn" placeholder="Ex : FUROSEMIDE&hellip;" onkeydown="if(event.key==='Enter')AT('w')"><button class="btn bs" onclick="AT('w')" title="Ajouter ce terme &agrave; la liste blanche">+ Ajouter</button></div>
<div class="tagcloud" id="wTags">
<span class="tag tw">FUROSEMIDE <span class="tx" onclick="this.parentElement.remove()">&#215;</span></span>
<span class="tag tw">r&eacute;&eacute;ducation fonctionnelle <span class="tx" onclick="this.parentElement.remove()">&#215;</span></span>
<span class="tag tw">classification internationale <span class="tx" onclick="this.parentElement.remove()">&#215;</span></span>
</div>
</div>
<div class="card">
<div class="ct">&#128683; Termes &agrave; toujours masquer <button class="hbtn" onclick="H('bl')" title="Aide liste noire">&#10067;</button></div>
<div class="note">Ces termes seront <strong>toujours masqu&eacute;s</strong>, m&ecirc;me sans contexte m&eacute;dical autour.</div>
<div class="tagrow"><input class="taginput" id="bIn" placeholder="Ex : CHUXX, Dr Dupont&hellip;" onkeydown="if(event.key==='Enter')AT('b')"><button class="btn bs" onclick="AT('b')" title="Ajouter ce terme &agrave; la liste noire">+ Ajouter</button></div>
<div class="tagcloud" id="bTags">
<span class="tag tb">CHUXX <span class="tx" onclick="this.parentElement.remove()">&#215;</span></span>
</div>
</div>
</div>
</div>
</div>
<!-- MASQUAGE -->
<div class="spane" id="sp-msk">
<div class="scols">
<div>
<div class="card">
<div class="ct">&#11035; Couleur de masquage (PDF) <button class="hbtn" onclick="H('col')" title="Aide couleur de masquage">&#10067;</button></div>
<div class="note">Couleur des rectangles dans le PDF final.</div>
<div class="swrow">
<div class="sw on" style="background:#000" onclick="SW(this)" title="Noir (standard officiel)"></div>
<div class="sw" style="background:#1a1a2e" onclick="SW(this)" title="Bleu nuit"></div>
<div class="sw" style="background:#374151" onclick="SW(this)" title="Gris fonc&eacute;"></div>
<div class="sw" style="background:#92400e" onclick="SW(this)" title="Marron"></div>
<div class="sw" style="background:#1e3a5f" onclick="SW(this)" title="Bleu marine"></div>
</div>
</div>
<div class="card">
<div class="ct">&#127991;&#65039; Style des marqueurs (texte) <button class="hbtn" onclick="H('sty')" title="Aide style de marqueurs">&#10067;</button></div>
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:10px">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer" title="Remplace par [NOM], [DATE_NAISSANCE], etc.">
<input type="radio" name="ms" value="b" checked onchange="UP()">
Crochets &mdash; <code style="color:var(--primary)">[NOM]</code>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer" title="Remplace par des ast&eacute;risques discrets">
<input type="radio" name="ms" value="s" onchange="UP()">
Etoiles &mdash; <code style="color:var(--text-muted)">***</code>
</label>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer" title="Remplace par des blocs noirs comme dans un PDF">
<input type="radio" name="ms" value="n" onchange="UP()">
Noirci &mdash; <span style="background:#000;color:#000;padding:0 8px;border-radius:2px">NOM</span>
</label>
</div>
<div id="mprev" class="mprev"></div>
</div>
</div>
<div>
<div class="card">
<div class="ct">&#128208; Epaisseur du masque <button class="hbtn" onclick="H('ep')" title="Aide &eacute;paisseur">&#10067;</button></div>
<div class="note">Marge autour du texte masqu&eacute; (en points).</div>
<div class="srow"><div><div class="slbl">Marge horizontale</div></div><input type="range" min="0" max="6" value="2" style="width:120px;accent-color:var(--primary)" title="R&eacute;gler la marge gauche-droite du masque"></div>
<div class="srow"><div><div class="slbl">Marge verticale</div></div><input type="range" min="0" max="6" value="1" style="width:120px;accent-color:var(--primary)" title="R&eacute;gler la marge haut-bas du masque"></div>
<div class="srow"><div><div class="slbl">Coins arrondis</div></div><label class="tog"><input type="checkbox"><span class="tsl"></span></label></div>
</div>
<div class="card">
<div class="ct">&#128274; Codes de remplacement <button class="hbtn" onclick="H('ph')" title="Aide codes de remplacement">&#10067;</button></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;font-size:12px">
<div style="color:var(--text-muted)">Nom/Pr&eacute;nom</div><div style="color:var(--primary);font-weight:600">[NOM]</div>
<div style="color:var(--text-muted)">Date naissance</div><div style="color:var(--primary);font-weight:600">[DATE_NAISSANCE]</div>
<div style="color:var(--text-muted)">Etablissement</div><div style="color:var(--primary);font-weight:600">[ETABLISSEMENT]</div>
<div style="color:var(--text-muted)">Adresse</div><div style="color:var(--primary);font-weight:600">[ADRESSE]</div>
<div style="color:var(--text-muted)">T&eacute;l&eacute;phone</div><div style="color:var(--primary);font-weight:600">[TEL]</div>
<div style="color:var(--text-muted)">N&deg; s&eacute;cu</div><div style="color:var(--primary);font-weight:600">[NIR]</div>
<div style="color:var(--text-muted)">IPP</div><div style="color:var(--primary);font-weight:600">[IPP]</div>
<div style="color:var(--text-muted)">Email</div><div style="color:var(--primary);font-weight:600">[EMAIL]</div>
</div>
</div>
</div>
</div>
<!-- ÉDITEUR DE MASQUES PDF -->
<div class="card" style="margin-top:0">
<div class="ct">
&#127968; Masques de zones fixes (logos, en-t&ecirc;tes)
<button class="hbtn" onclick="H('medit')" title="Aide &eacute;diteur de masques">&#10067;</button>
<button class="btn bp" style="margin-left:8px;padding:5px 12px;font-size:12px" onclick="METOG()" id="meBtnOpen" title="Ouvrir l'&eacute;diteur pour dessiner des zones &agrave; masquer sur un PDF mod&egrave;le">
&#128393; Ouvrir l&apos;&eacute;diteur de masques
</button>
</div>
<div class="note">
Dessinez des rectangles sur un PDF mod&egrave;le pour masquer syst&eacute;matiquement les logos, en-t&ecirc;tes ou zones fixes &mdash;
ind&eacute;pendamment de l&rsquo;OCR.
</div>
<!-- Panneau éditeur (togglable) -->
<div class="me-panel" id="mePanel">
<!-- Barre d'outils -->
<div class="me-toolbar">
<button class="btn bs" style="padding:5px 10px;font-size:12px" onclick="ME_open()" title="Charger un PDF pour d&eacute;finir les zones &agrave; masquer">
&#128196; Ouvrir PDF&hellip;
</button>
<div class="me-sep"></div>
<button class="btn bs" style="padding:5px 8px;font-size:13px;min-width:30px" onclick="ME_zoom(-1)" title="Zoom arri&egrave;re">&#8722;</button>
<span id="meZoomLbl" style="font-size:12px;color:var(--text-muted);min-width:38px;text-align:center">100%</span>
<button class="btn bs" style="padding:5px 8px;font-size:13px;min-width:30px" onclick="ME_zoom(1)" title="Zoom avant">+</button>
<div class="me-sep"></div>
<span style="font-size:12px;color:var(--text-muted)">Template&nbsp;:</span>
<input id="meTplName" type="text" value="template_masques" style="width:130px;background:rgba(128,128,128,.12);border:1px solid var(--card-border);border-radius:5px;padding:4px 7px;color:var(--text);font-size:12px;outline:none" title="Nom du template de masques">
<button class="btn bs" style="padding:5px 10px;font-size:12px" onclick="ME_save()" title="Sauvegarder ce template de masques dans un fichier">&#128190; Sauver</button>
<button class="btn bs" style="padding:5px 10px;font-size:12px" onclick="ME_load()" title="Charger un template de masques existant">&#128193; Charger</button>
<div class="me-sep"></div>
<button class="btn bs" style="padding:5px 10px;font-size:12px;color:var(--danger)" onclick="ME_clearPage()" title="Effacer tous les masques de la page actuelle">
&#128465; Effacer page
</button>
</div>
<!-- Zone de dessin -->
<div class="me-canvas" id="meCanvas">
<div id="meHint" class="me-hint">
<span>&#128196;</span>
Ouvrez un PDF pour commencer &agrave; dessiner des zones de masquage.<br>
<span style="font-size:11px">Cliquez-glissez pour tracer un rectangle &bull; Clic sur un masque pour le supprimer</span>
</div>
<div id="meImgWrap" class="me-canvas-inner" style="display:none">
<img id="meImg" src="" alt="page PDF">
<canvas id="meOverlay" class="me-overlay"
onmousedown="ME_mdown(event)"
onmousemove="ME_mmove(event)"
onmouseup="ME_mup(event)">
</canvas>
</div>
</div>
<!-- Barre d'action inférieure -->
<div class="me-toolbar" style="border-top:1px solid var(--card-border);border-bottom:none;justify-content:space-between">
<div style="display:flex;gap:6px;align-items:center">
<span style="font-size:12px;color:var(--text-muted)">DPI raster&nbsp;:</span>
<input id="meDpi" type="number" value="200" min="72" max="600" step="10"
style="width:58px;background:rgba(128,128,128,.12);border:1px solid var(--card-border);border-radius:5px;padding:4px 6px;color:var(--text);font-size:12px;outline:none;text-align:center"
title="R&eacute;solution de rendu (200 DPI recommand&eacute; pour la pr&eacute;cision des masques)">
<button class="btn bs" style="padding:5px 10px;font-size:12px" onclick="ME_preview()" title="Pr&eacute;visualiser le r&eacute;sultat du masquage sur la page courante">
&#128065; Pr&eacute;visualiser
</button>
</div>
<button class="btn bp" style="padding:6px 14px;font-size:12px" onclick="ME_apply()" title="Appliquer ce template aux documents du traitement en cours">
&#9654; Appliquer le template
</button>
</div>
<!-- Barre de statut -->
<div class="me-status">
<span id="meStat">Aucun PDF charg&eacute;</span>
<span id="meMaskCount">0 masque(s)</span>
</div>
</div>
</div>
</div><!-- /sp-msk -->
<!-- PARTAGE -->
<div class="spane" id="sp-shr">
<div class="card">
<div class="ct">&#128228; Exporter la configuration <button class="hbtn" onclick="H('exp')" title="Aide export">&#10067;</button></div>
<div class="note">G&eacute;n&egrave;re un fichier .json avec vos listes, &agrave; envoyer par e-mail &agrave; d&rsquo;autres &eacute;tablissements.</div>
<button class="btn bs" onclick="alert('Export JSON simul&eacute;')" title="T&eacute;l&eacute;charger votre configuration en fichier .json">&#11015; Exporter (.json)</button>
</div>
<div class="card">
<div class="ct">&#128229; Importer une configuration <button class="hbtn" onclick="H('imp')" title="Aide import">&#10067;</button></div>
<div class="note">Importez un fichier re&ccedil;u. Vos r&eacute;glages locaux ne seront pas supprim&eacute;s.</div>
<button class="btn bs" onclick="alert('Import simul&eacute;')" title="Charger un fichier .json re&ccedil;u par e-mail">&#11014; Importer (.json)</button>
</div>
</div>
<!-- RÈGLES (sous-onglet) -->
<div class="spane" id="sp-rul">
<div class="card">
<div class="ct">&#128737;&#65039; R&egrave;gles actives <button class="hbtn" onclick="H('rul')" title="Aide sur les r&egrave;gles">&#10067;</button></div>
<div class="note">Ces r&egrave;gles adaptent le moteur &agrave; votre &eacute;tablissement. Chaque r&egrave;gle est valid&eacute;e avant activation.</div>
<table class="rtbl">
<thead><tr><th>Label</th><th>Type</th><th>Cible &rarr; R&eacute;sultat</th><th>Statut</th><th></th></tr></thead>
<tbody>
<tr>
<td>Masquer le sigle CHUXX</td>
<td><span class="rtyp">exact</span></td>
<td><code>CHUXX</code> &rarr; <code style="color:var(--primary)">[MASK]</code></td>
<td><span class="rst ract">Actif</span></td>
<td><button class="btn bs" style="padding:3px 8px;font-size:11px" onclick="SIM('CHUXX')" title="Tester cette r&egrave;gle sur un texte libre">&#9654; Tester</button></td>
</tr>
<tr>
<td>Pr&eacute;server &ldquo;classification internationale&rdquo;</td>
<td><span class="rtyp">preserve</span></td>
<td>conserv&eacute; tel quel</td>
<td><span class="rst ract">Actif</span></td>
<td><button class="btn bs" style="padding:3px 8px;font-size:11px" onclick="SIM('classification')" title="Tester cette r&egrave;gle sur un texte libre">&#9654; Tester</button></td>
</tr>
<tr>
<td>Identifier N&deg; 1234567</td>
<td><span class="rtyp">norm-id</span></td>
<td><code>N&deg; 1234567</code> &rarr; <code style="color:var(--primary)">[NDA]</code></td>
<td><span class="rst rcand">Candidat</span></td>
<td><button class="btn bs" style="padding:3px 8px;font-size:11px" onclick="SIM('1234567')" title="Tester cette r&egrave;gle sur un texte libre">&#9654; Tester</button></td>
</tr>
</tbody>
</table>
<div style="margin-top:12px;display:flex;gap:8px">
<button class="btn bp" onclick="alert('Editeur de r&egrave;gles (&agrave; venir)')" title="Cr&eacute;er une nouvelle r&egrave;gle d'administration">+ Nouvelle r&egrave;gle</button>
<button class="btn bs" title="Recharger les r&egrave;gles depuis la configuration">&#128260; Recharger</button>
</div>
</div>
<div class="card" id="simcard" style="display:none">
<div class="ct">&#129514; Testeur de r&egrave;gle</div>
<label style="font-size:12px;color:var(--text-muted);display:block;margin-bottom:5px">Texte de test</label>
<textarea id="simtxt" rows="3" style="width:100%;background:rgba(0,0,0,.2);border:1px solid var(--card-border);border-radius:6px;padding:8px;color:var(--text);font-size:13px;resize:vertical;outline:none"></textarea>
<div style="display:flex;gap:8px;margin:10px 0">
<button class="btn bp" onclick="RSIM()" title="Ex&eacute;cuter la simulation sur le texte saisi">&#9654; Tester</button>
<button class="btn bs" onclick="document.getElementById('simcard').style.display='none'" title="Fermer le testeur">&#10006; Fermer</button>
</div>
<div id="simout" style="display:none;background:rgba(0,0,0,.2);border:1px solid var(--card-border);border-radius:6px;padding:10px;font-size:13px;line-height:1.8"></div>
</div>
</div>
</div><!-- /cfg -->
<!-- ═══ À PROPOS ═══ -->
<div class="tab-pane" id="tab-about">
<div class="card">
<div class="ct">&#8505;&#65039; Informations</div>
<div class="agrid">
<div class="ai"><span style="font-size:20px">&#127991;&#65039;</span><div><div class="ak">Version</div><div class="av">v6.0 (prototype)</div></div></div>
<div class="ai"><span style="font-size:20px">&#128197;</span><div><div class="ak">Build</div><div class="av">2026-04-28</div></div></div>
<div class="ai"><span style="font-size:20px">&#129504;</span><div><div class="ak">Moteurs NER</div><div class="av">CamemBERT &middot; EDS-Pseudo &middot; GLiNER</div></div></div>
<div class="ai"><span style="font-size:20px">&#128274;</span><div><div class="ak">Traitement</div><div class="av">100 % local &mdash; aucune donn&eacute;e transmise</div></div></div>
<div class="ai"><span style="font-size:20px">&#128218;</span><div><div class="ak">Gazetteers</div><div class="av">INSEE 219K &middot; FINESS 108K &middot; BDPM 7K</div></div></div>
<div class="ai"><span style="font-size:20px">&#128193;</span><div><div class="ak">Formats</div><div class="av">PDF &middot; DOCX &middot; ODT &middot; RTF &middot; TXT &middot; Images</div></div></div>
</div>
</div>
<div class="card">
<div class="ct">&#128202; Derni&egrave;re session</div>
<div class="qbar">
<div class="qs">100.0</div>
<div><div class="qg">/ 100 &middot; A+</div><div style="font-size:11px;color:var(--text-muted)">22 PDFs &middot; 0 fuite d&eacute;tect&eacute;e</div></div>
</div>
</div>
</div><!-- /about -->
</div><!-- /content -->
</div><!-- /shell -->
<!-- MODAL AIDE -->
<div class="mo" id="mo" onclick="if(event.target===this)CH()">
<div class="mbox">
<button class="mcls" onclick="CH()">&#215;</button>
<div class="mtit" id="mt"></div>
<div class="mbody" id="mb"></div>
</div>
</div>
<script>
// Navigation onglets principaux
function ST(n,b){
document.querySelectorAll(".tab-pane").forEach(function(p){p.classList.remove("active");});
document.querySelectorAll(".tab-btn").forEach(function(x){x.classList.remove("active");});
document.getElementById("tab-"+n).classList.add("active");
b.classList.add("active");
}
// Sous-onglets configuration
function SS(n,b){
document.querySelectorAll(".spane").forEach(function(p){p.classList.remove("on");});
document.querySelectorAll(".stab").forEach(function(x){x.classList.remove("on");});
document.getElementById("sp-"+n).classList.add("on");
b.classList.add("on");
}
// Themes
function TH(t,b){
document.getElementById("shell").className="app-shell"+(t?" theme-"+t:"");
document.querySelectorAll(".tp").forEach(function(x){x.classList.remove("on");});
b.classList.add("on");
}
// Fichiers
var SAMP=[{n:"CR_23456.pdf",s:"142 Ko"},{n:"CRO_81234.pdf",s:"98 Ko"},{n:"LETTRE_SORTIE.pdf",s:"64 Ko"}];
var _f=[];
function RF(f){
_f=f;
var h="";
for(var i=0;i<f.length;i++){
h+="<div class=\"fi\" id=\"fi"+i+"\"><span style=\"font-size:16px\">&#128196;</span><span class=\"fn\">"+f[i].n+"</span><span class=\"fs\">"+f[i].s+"</span><button class=\"fx\" onclick=\"document.getElementById('fi"+i+"').remove()\">&#215;</button></div>";
}
document.getElementById("flist").innerHTML=h;
}
function PICK(){RF(SAMP);}
function PICKF(){RF(SAMP.concat([{n:"ANAPATH.pdf",s:"211 Ko"},{n:"BACTERIO.docx",s:"34 Ko"}]));}
function DROP(e){e.preventDefault();document.getElementById("dropzone").classList.remove("over");RF(SAMP);}
function CLR(){_f=[];document.getElementById("flist").innerHTML="";RRES();}
// Progression
var _t=null;
var STP=["s0","s1","s2","s3"];
var LG=[
["lok","OK Lecture CR_23456.pdf"],
["","- Extraction texte..."],
["lok","OK 47 entites detectees"],
["","- Masquage..."],
["lok","OK CR_23456.pdf termine"],
["","- CRO_81234.pdf..."],
["lok","OK CRO_81234.pdf termine"],
["","- LETTRE_SORTIE.pdf..."],
["lok","OK 142 PII masques au total"]
];
function GO(){
if(!_f.length)PICK();
document.getElementById("psec").classList.add("vis");
document.getElementById("rsec").classList.remove("vis");
document.getElementById("btnGo").disabled=true;
document.getElementById("logb").innerHTML="";
for(var i=0;i<STP.length;i++) document.getElementById(STP[i]).className="sp";
document.getElementById(STP[0]).className="sp act";
var p=0,si=0,li=0;
var lb=document.getElementById("logb");
_t=setInterval(function(){
p+=5; if(p>100)p=100;
document.getElementById("pb").style.width=p+"%";
document.getElementById("pp").textContent=p+" %";
document.getElementById("pf").textContent="Fichier "+Math.min(3,Math.ceil(p/34))+" / 3";
var ns=Math.min(STP.length-1,Math.floor(p/26));
if(ns!==si){document.getElementById(STP[si]).className="sp done";si=ns;document.getElementById(STP[si]).className="sp act";}
if(li<LG.length&&Math.random()>.5){
lb.innerHTML+="<span class=\""+LG[li][0]+"\">"+LG[li][1]+"</span>\n";
lb.scrollTop=lb.scrollHeight; li++;
}
if(p>=100){
clearInterval(_t);
for(var j=0;j<STP.length;j++){document.getElementById(STP[j]).className="sp done";}
setTimeout(function(){
document.getElementById("psec").classList.remove("vis");
document.getElementById("rsec").classList.add("vis");
document.getElementById("btnGo").disabled=false;
},600);
}
},100);
}
function STOP(){if(_t)clearInterval(_t);document.getElementById("psec").classList.remove("vis");document.getElementById("btnGo").disabled=false;}
function RRES(){document.getElementById("rsec").classList.remove("vis");document.getElementById("psec").classList.remove("vis");document.getElementById("pb").style.width="0%";}
// Tags
function AT(t){
var inp=document.getElementById(t==="w"?"wIn":"bIn");
var v=inp.value.trim(); if(!v)return;
var c=document.getElementById(t==="w"?"wTags":"bTags");
var s=document.createElement("span");
s.className="tag "+(t==="w"?"tw":"tb");
s.innerHTML=v+" <span class=\"tx\" onclick=\"this.parentElement.remove()\">&#215;</span>";
c.appendChild(s); inp.value="";
}
// Swatches
function SW(el){document.querySelectorAll(".sw").forEach(function(s){s.classList.remove("on");});el.classList.add("on");}
// Preview masquage
function UP(){
var v=document.querySelector("input[name='ms']:checked").value;
var nm,nd,ne;
if(v==="b"){nm="<span class=\"mb\">[NOM]</span>";nd="<span class=\"mb\">[DATE_NAISSANCE]</span>";ne="<span class=\"mb\">[ETABLISSEMENT]</span>";}
else if(v==="s"){nm="<span class=\"ms2\">***</span>";nd="<span class=\"ms2\">***</span>";ne="<span class=\"ms2\">***</span>";}
else{nm="<span class=\"mn\">&#9608;&#9608;&#9608;&#9608;</span>";nd="<span class=\"mn\">&#9608;&#9608;&#9608;&#9608;</span>";ne="<span class=\"mn\">&#9608;&#9608;&#9608;&#9608;</span>";}
document.getElementById("mprev").innerHTML="Patient : "+nm+"<br>N&eacute; le "+nd+"<br>Service du "+ne;
}
UP();
// Simulateur de regle
var _rt="";
function SIM(t){
_rt=t;
document.getElementById("simcard").style.display="block";
document.getElementById("simout").style.display="none";
document.getElementById("simtxt").value="Dossier "+t+" -- suivi au "+t+" de Chicago.\nCode CIM retient la classification internationale G56.8.";
document.getElementById("simcard").scrollIntoView({behavior:"smooth"});
}
function RSIM(){
var txt=document.getElementById("simtxt").value;
var re=new RegExp(_rt.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");
var out=txt.replace(re,function(m){return "<mark style=\"background:var(--primary);color:white;padding:1px 5px;border-radius:3px\">[MASK]</mark>";});
out=out.replace(/\n/g,"<br>");
var el=document.getElementById("simout");
el.innerHTML=out; el.style.display="block";
}
// ── ÉDITEUR DE MASQUES PDF ──────────────────────────────────────────────────
var _meZoom=1, _meMasks=[], _meDrawing=false, _meStart=null, _meTmpRect=null;
function METOG(){
var p=document.getElementById("mePanel");
var b=document.getElementById("meBtnOpen");
if(p.classList.contains("open")){
p.classList.remove("open");
b.innerHTML="&#128393; Ouvrir l'&eacute;diteur de masques";
} else {
p.classList.add("open");
b.innerHTML="&#215; Fermer l'&eacute;diteur";
p.scrollIntoView({behavior:"smooth",block:"nearest"});
}
}
function ME_open(){
// Simule le chargement d'un PDF : affiche une image de placeholder
var w=document.getElementById("meImgWrap");
var img=document.getElementById("meImg");
var hint=document.getElementById("meHint");
var cv=document.getElementById("meOverlay");
// Page simulée (rectangle blanc avec texte simulé)
var c2=document.createElement("canvas");
c2.width=595; c2.height=842;
var ctx=c2.getContext("2d");
ctx.fillStyle="#ffffff"; ctx.fillRect(0,0,595,842);
ctx.fillStyle="#e5e7eb"; ctx.fillRect(0,0,595,90);
ctx.fillStyle="#9ca3af"; ctx.font="bold 18px sans-serif"; ctx.fillText("EN-TETE ETABLISSEMENT [LOGO]",30,40);
ctx.fillStyle="#6b7280"; ctx.font="13px sans-serif";
ctx.fillText("Service de cardiologie | Tel : 05.59.XX.XX.XX",30,68);
ctx.fillStyle="#111827"; ctx.font="13px sans-serif";
ctx.fillText("Patient : Dupont Jean Ne le : 12/03/1955",30,130);
ctx.fillText("IPP : 1234567 NDA : 8901234",30,155);
ctx.fillText("Motif : Insuffisance cardiaque decompensee.",30,200);
ctx.fillText("Traitement : FUROSEMIDE 40mg, BISOPROLOL 5mg.",30,225);
ctx.fillStyle="#6b7280"; ctx.font="11px sans-serif";
ctx.fillText("Signe : Dr Martin RPPS 12345678 | mmartin@chuxx.fr",30,790);
img.src=c2.toDataURL();
img.onload=function(){
cv.width=img.naturalWidth;
cv.height=img.naturalHeight;
cv.style.width=Math.round(img.naturalWidth*_meZoom)+"px";
cv.style.height=Math.round(img.naturalHeight*_meZoom)+"px";
ME_redraw();
};
hint.style.display="none";
w.style.display="inline-block";
document.getElementById("meStat").textContent="page_modele.pdf — page 1/1";
ME_redraw();
}
function ME_zoom(d){
_meZoom=Math.min(3,Math.max(0.3,_meZoom+d*0.15));
document.getElementById("meZoomLbl").textContent=Math.round(_meZoom*100)+"%";
var img=document.getElementById("meImg");
var cv=document.getElementById("meOverlay");
img.style.width=Math.round(img.naturalWidth*_meZoom)+"px";
cv.style.width=Math.round(img.naturalWidth*_meZoom)+"px";
cv.style.height=Math.round(img.naturalHeight*_meZoom)+"px";
ME_redraw();
}
function _meCoord(e){
var cv=document.getElementById("meOverlay");
var r=cv.getBoundingClientRect();
return {x:(e.clientX-r.left)/_meZoom, y:(e.clientY-r.top)/_meZoom};
}
function ME_mdown(e){
_meDrawing=true;
_meStart=_meCoord(e);
}
function ME_mmove(e){
if(!_meDrawing)return;
ME_redraw();
var cur=_meCoord(e);
var cv=document.getElementById("meOverlay");
var ctx=cv.getContext("2d");
ctx.save();
ctx.scale(_meZoom,_meZoom);
ctx.fillStyle="rgba(0,0,0,0.55)";
ctx.strokeStyle="rgba(233,69,96,0.8)";
ctx.lineWidth=1.5/_meZoom;
var x=Math.min(_meStart.x,cur.x), y=Math.min(_meStart.y,cur.y);
var w=Math.abs(cur.x-_meStart.x), h=Math.abs(cur.y-_meStart.y);
ctx.fillRect(x,y,w,h);
ctx.strokeRect(x,y,w,h);
ctx.restore();
}
function ME_mup(e){
if(!_meDrawing)return;
_meDrawing=false;
var cur=_meCoord(e);
var x=Math.min(_meStart.x,cur.x), y=Math.min(_meStart.y,cur.y);
var w=Math.abs(cur.x-_meStart.x), h=Math.abs(cur.y-_meStart.y);
if(w>4&&h>4){
_meMasks.push({x:Math.round(x),y:Math.round(y),w:Math.round(w),h:Math.round(h)});
ME_updCount();
}
ME_redraw();
}
function ME_redraw(){
var cv=document.getElementById("meOverlay");
if(!cv.width)return;
var ctx=cv.getContext("2d");
ctx.clearRect(0,0,cv.width,cv.height);
ctx.save();
ctx.scale(_meZoom,_meZoom);
ctx.fillStyle="rgba(0,0,0,0.82)";
ctx.strokeStyle="rgba(233,69,96,0.6)";
ctx.lineWidth=1/_meZoom;
for(var i=0;i<_meMasks.length;i++){
var m=_meMasks[i];
ctx.fillRect(m.x,m.y,m.w,m.h);
ctx.strokeRect(m.x,m.y,m.w,m.h);
ctx.fillStyle="rgba(233,69,96,0.7)";
ctx.font="10px sans-serif";
ctx.fillText("x",m.x+m.w/2-4,m.y+m.h/2+4);
ctx.fillStyle="rgba(0,0,0,0.82)";
}
ctx.restore();
}
function ME_clearPage(){
_meMasks=[];
ME_redraw();
ME_updCount();
}
function ME_updCount(){
document.getElementById("meMaskCount").textContent=_meMasks.length+" masque(s)";
}
function ME_save(){
var name=document.getElementById("meTplName").value||"template_masques";
alert("Template \""+name+"\" sauvegardé ("+_meMasks.length+" masque(s)).\n\nFichier : config/mask_templates/"+name+".json");
}
function ME_load(){
alert("Sélection du fichier template simulée.\nDans l'application réelle : ouverture d'un sélecteur de fichier .json");
}
function ME_preview(){
var dpi=parseInt(document.getElementById("meDpi").value)||200;
alert("Prévisualisation raster à "+dpi+" DPI\nMasques appliqués : "+_meMasks.length+"\n\nDans l'application réelle : rendu de la page avec les zones noircies.");
}
function ME_apply(){
var name=document.getElementById("meTplName").value||"template_masques";
if(_meMasks.length===0){alert("Aucun masque défini. Dessinez au moins une zone.");return;}
alert("Template \""+name+"\" activé pour le prochain traitement.\n"+_meMasks.length+" zone(s) seront masquées systématiquement.");
}
// Aide contextuelle
var HELP={
"theme":["Apparence","Choisissez le th&egrave;me visuel adapt&eacute; &agrave; votre environnement.<br><br><strong>Sombre</strong> &mdash; fond fonc&eacute;, id&eacute;al en luminosit&eacute; r&eacute;duite.<br><strong>Clair</strong> &mdash; fond blanc, pour &eacute;crans bien &eacute;clair&eacute;s.<br><strong>M&eacute;dical</strong> &mdash; bleu et blanc, proche des interfaces hospitali&egrave;res.<br><strong>Neutre</strong> &mdash; tons gris discrets."],
"fich":["Documents","Glissez-d&eacute;posez vos fichiers dans la zone, ou cliquez sur <strong>Fichiers</strong>.<br><br>Cliquez sur <strong>Dossier entier</strong> pour traiter automatiquement tous les documents d'un r&eacute;pertoire.<br><br><strong>Formats accept&eacute;s :</strong> PDF, Word (.docx), ODT, RTF, Texte, JPEG, PNG, TIFF."],
"fmt":["Format de sortie","<strong>PDF anonymis&eacute;</strong> &mdash; le document avec les informations recouvertes de bandes noires. Recommand&eacute; pour l'archivage.<br><br><strong>Texte .txt</strong> &mdash; le contenu avec les donn&eacute;es remplac&eacute;es par des codes comme <code>[NOM]</code>, <code>[DATE_NAISSANCE]</code>. Utile pour l'analyse."],
"det":["D&eacute;tection","Ces options contr&ocirc;lent <strong>ce qui est recherch&eacute;</strong> lors du traitement.<br><br>D&eacute;sactiver une cat&eacute;gorie peut laisser passer des donn&eacute;es personnelles. <strong>En cas de doute, laissez tout activ&eacute;.</strong>"],
"ner":["Moteurs NER","Le logiciel utilise plusieurs moteurs d'intelligence artificielle qui <strong>se compl&egrave;tent mutuellement</strong>.<br><br><strong>CamemBERT-bio</strong> &mdash; tr&egrave;s rapide, entra&icirc;n&eacute; sur des dossiers m&eacute;dicaux fran&ccedil;ais.<br><strong>EDS-Pseudo</strong> &mdash; plus lent mais tr&egrave;s pr&eacute;cis sur le vocabulaire clinique.<br><strong>GLiNER</strong> &mdash; optionnel, apporte un vote crois&eacute;. D&eacute;sactivez-le pour acc&eacute;l&eacute;rer."],
"wl":["Termes &agrave; conserver","Ces termes ne seront <strong>jamais masqu&eacute;s</strong>, m&ecirc;me s'ils ressemblent &agrave; un nom propre.<br><br>Exemples :<br>&bull; <code>FUROSEMIDE</code> &mdash; m&eacute;dicament, pas un patient<br>&bull; <code>classification internationale</code> &mdash; formulation m&eacute;dicale<br><br>Ajoutez ici tout terme incorrectement masqu&eacute;."],
"bl":["Termes &agrave; toujours masquer","Ces termes seront <strong>syst&eacute;matiquement masqu&eacute;s</strong>, peu importe leur contexte.<br><br>Exemples :<br>&bull; <code>CHUXX</code> &mdash; sigle de l'&eacute;tablissement<br>&bull; <code>Dr Dupont</code> &mdash; m&eacute;decin &agrave; masquer dans tous les documents<br><br>Id&eacute;al pour les identifiants locaux non reconnus automatiquement."],
"col":["Couleur de masquage","Choisissez la couleur des rectangles qui recouvrent les donn&eacute;es dans le PDF.<br><br>Le <strong>noir</strong> est la norme pour les documents officiels. Les autres couleurs facilitent la relecture lors de la validation."],
"sty":["Style des marqueurs","Dans le fichier texte, chaque donn&eacute;e masqu&eacute;e est remplac&eacute;e par un marqueur.<br><br><strong>Crochets [NOM]</strong> &mdash; explicite sur le type de donn&eacute;e (recommand&eacute;).<br><strong>Etoiles ***</strong> &mdash; plus discret.<br><strong>Noirci</strong> &mdash; imite visuellement le masquage PDF."],
"ep":["Epaisseur du masque","La marge d&eacute;termine de combien le rectangle d&eacute;passe le texte.<br><br>Une marge de <strong>2 points</strong> est suffisante pour la plupart des PDF. Augmentez si des lettres d&eacute;passent l&eacute;g&egrave;rement du masque."],
"ph":["Codes de remplacement","Dans le texte anonymis&eacute;, chaque donn&eacute;e est remplac&eacute;e par un code entre crochets indiquant sa nature.<br><br>Cela permet de savoir <em>ce qui a &eacute;t&eacute; masqu&eacute;</em> sans r&eacute;v&eacute;ler <em>ce que c'&eacute;tait</em>."],
"medit":["&Eacute;diteur de masques de zones","Cet outil permet de <strong>masquer des zones fixes</strong> qui apparaissent au m&ecirc;me endroit sur tous les documents d'un mod&egrave;le.<br><br>Exemples : logo de l'&eacute;tablissement en haut de page, tampon de signature, en-t&ecirc;te avec num&eacute;ro de fax.<br><br><strong>Comment utiliser :</strong><br>1. Cliquez <em>Ouvrir PDF</em> pour charger un document mod&egrave;le.<br>2. Cliquez-glissez pour tracer des rectangles sur les zones &agrave; masquer.<br>3. Donnez un nom au template et cliquez <em>Sauver</em>.<br>4. Cliquez <em>Appliquer le template</em> pour l'activer sur les prochains traitements."],
"exp":["Exporter","G&eacute;n&egrave;re un fichier <strong>.json</strong> contenant vos listes personnalis&eacute;es.<br><br>Envoyez ce fichier par e-mail &agrave; d'autres &eacute;tablissements pour partager votre configuration."],
"imp":["Importer","Importez un fichier .json re&ccedil;u d'un autre &eacute;tablissement.<br><br>La configuration est <strong>fusionn&eacute;e</strong> avec la v&ocirc;tre &mdash; vos r&eacute;glages locaux ne sont pas supprim&eacute;s."],
"rul":["R&egrave;gles administrables","Ces r&egrave;gles adaptent le moteur &agrave; votre &eacute;tablissement.<br><br><strong>Actif</strong> &mdash; appliqu&eacute;e &agrave; chaque traitement.<br><strong>Candidat</strong> &mdash; en attente de validation qualit&eacute;.<br><strong>Brouillon</strong> &mdash; en cours de cr&eacute;ation, non appliqu&eacute;e.<br><br>Cliquez sur <strong>Tester</strong> pour simuler l'effet d'une r&egrave;gle avant de l'activer."]
};
function H(k){
var h=HELP[k]||["Aide","Information non disponible."];
document.getElementById("mt").innerHTML=h[0];
document.getElementById("mb").innerHTML=h[1];
document.getElementById("mo").classList.add("open");
}
function CH(){document.getElementById("mo").classList.remove("open");}
document.addEventListener("keydown",function(e){if(e.key==="Escape")CH();});
</script>
</body>
</html>"""
HTML = HTML.replace("LOGO_PLACEHOLDER", LOGO_SRC)
OUT_PATH.write_text(HTML, encoding="utf-8")
print("OK — {} ({} Ko)".format(OUT_PATH, OUT_PATH.stat().st_size // 1024))