allo-chat / web /index.html
dave
Upload folder using huggingface_hub
172fee6 verified
Raw
History Blame Contribute Delete
94.6 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<title>Chats perdus (Beta) — entraide</title>
<style>
:root{
--bg:#f3f5f8; --card:#ffffff; --ink:#16202c; --muted:#5b6b7b;
--line:#e2e8f0; --accent:#ff5a3c; --accent2:#0b7; --ok:#0a9d5a; --warn:#e08a00; --bad:#d23b2e;
--radius:16px; --shadow:0 2px 14px rgba(20,40,60,.08);
}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{margin:0;font-family:-apple-system,system-ui,"Segoe UI",sans-serif;background:var(--bg);color:var(--ink);
font-size:17px;line-height:1.45}
header{position:sticky;top:0;z-index:10;background:var(--accent);color:#fff;
padding:14px 16px;display:flex;align-items:center;gap:10px;box-shadow:var(--shadow)}
header .back{background:rgba(255,255,255,.2);border:none;color:#fff;font-size:20px;width:40px;height:40px;
border-radius:10px;display:none}
header h1{font-size:19px;margin:0;font-weight:800;letter-spacing:.2px;flex:1}
.lang-toggle{display:flex;gap:5px;flex-shrink:0}
.lang-btn{background:rgba(255,255,255,.18);border:none;color:rgba(255,255,255,.8);font-size:12px;
font-weight:800;padding:5px 10px;border-radius:8px;cursor:pointer;letter-spacing:.4px}
.lang-btn.on{background:rgba(255,255,255,.45);color:#fff}
main{max-width:680px;margin:0 auto;padding:18px 16px 36px}
.sub{color:var(--muted);font-size:15px;line-height:1.5;margin:2px 0 18px}
/* Home action cards — aéré pour une lecture rapide en situation d'urgence */
.home{display:grid;gap:18px;margin-top:12px}
.action{display:flex;gap:14px;align-items:center;background:var(--card);border:1px solid var(--line);
border-radius:var(--radius);padding:18px;box-shadow:var(--shadow);cursor:pointer;text-align:left;width:100%}
.action:active{transform:scale(.99)}
.action .ico{font-size:34px;width:56px;text-align:center;flex:none}
.action .t{font-size:18px;font-weight:800}
.action .d{color:var(--muted);font-size:14px;margin-top:2px}
/* Flux d'urgence (primaires) */
.action.hero{padding:24px 22px;display:block}
.action.hero .head{display:flex;gap:14px;align-items:center}
.action.hero .ico{font-size:44px;width:64px}
.action.hero .t{font-size:22px;margin-top:4px}
.action.hero .d{margin-top:10px;font-size:14.5px;line-height:1.45;color:var(--muted)}
.action.hero .feats{display:flex;flex-wrap:wrap;gap:8px;margin-top:18px}
.action.hero .feat{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:700;
background:rgba(0,0,0,.05);border-radius:999px;padding:6px 12px;color:var(--ink);line-height:1.2}
.hero-lost{border:2px solid #ffc4b3;background:#fff8f5}
.hero-seen{border:2px solid #aee3cc;background:#f5fbf8}
.hero-follow{border:2px solid #c4cbff;background:#f6f7ff}
.tag-follow{background:#e0e3ff;color:#3a44b8}
.follow-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:12px 0}
.follow-stats .fs{background:#f3f4ff;border-radius:10px;padding:10px;text-align:center}
.follow-stats .fs b{display:block;font-size:17px;color:#3a44b8}
.follow-stats .fs span{font-size:11.5px;color:var(--muted)}
.tag{display:inline-block;font-size:11.5px;font-weight:800;padding:3px 10px;border-radius:12px;letter-spacing:.3px}
.tag-lost{background:#ffe3da;color:#b3360f}
.tag-seen{background:#d8f3e6;color:#067a4e}
/* Actions communautaires (secondaires) */
.secondary{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.action.mini{padding:12px 14px;box-shadow:none;background:#fbfdff}
.action.mini .ico{font-size:24px;width:36px}
.action.mini .t{font-size:14.5px}
.action.mini .d{font-size:12.5px}
/* Cards / forms */
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:18px;
box-shadow:var(--shadow);margin-bottom:16px}
h2{font-size:21px;margin:2px 0 4px;font-weight:800}
label{display:block;font-weight:700;font-size:14px;margin:14px 0 6px}
input,textarea{width:100%;padding:13px;border:1.5px solid var(--line);border-radius:12px;font-size:16px;background:#fbfdff}
textarea{resize:vertical}
.btn{display:block;width:100%;padding:16px;border:none;border-radius:14px;font-size:17px;font-weight:800;
margin-top:14px;cursor:pointer}
.btn-primary{background:var(--accent);color:#fff}
.btn-go{background:var(--accent2);color:#fff}
.btn-ghost{background:#eef2f6;color:var(--ink);border:1.5px solid var(--line)}
.btn:active{filter:brightness(.95)}
.photo-btn{display:flex;align-items:center;justify-content:center;gap:10px;background:#eef2f6;border:2px dashed #b9c6d3;
color:var(--ink);padding:20px;border-radius:14px;font-weight:700;font-size:16px;cursor:pointer;text-align:center}
.file-hidden{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden}
.hint{font-size:13.5px;color:var(--muted);margin-top:8px}
.privacy{background:#eaf5ee;color:#0a6b3d;border:1px solid #bfe3cd;border-radius:12px;padding:10px 12px;
font-size:13.5px;margin-bottom:14px}
.thumbs{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.thumbs img{width:64px;height:64px;object-fit:cover;border-radius:10px;border:1px solid var(--line)}
.result{text-align:center;padding:22px;border-radius:14px;margin-top:6px}
.result.match{background:#eaf5ee;border:1px solid #bfe3cd}
.result.none{background:#f1f4f7;border:1px solid var(--line)}
.big{font-size:46px;line-height:1}
.candidate{display:flex;gap:14px;background:#fbfdff;border:1px solid var(--line);border-radius:14px;padding:12px;margin-top:12px}
.candidate img{width:96px;height:96px;object-fit:cover;border-radius:12px;flex:none}
.pill{display:inline-block;font-size:12px;font-weight:800;padding:3px 10px;border-radius:20px;color:#fff}
.token{font-family:ui-monospace,monospace;font-size:20px;font-weight:800;background:#fff3ef;border:2px dashed var(--accent);
border-radius:12px;padding:14px;text-align:center;user-select:all;word-break:break-all;letter-spacing:1px}
.seg{display:flex;gap:8px;margin:6px 0 4px}
.seg button{flex:1;padding:12px;border:1.5px solid var(--line);background:#fff;border-radius:12px;font-weight:700;font-size:15px}
.seg button.on{background:var(--accent);color:#fff;border-color:var(--accent)}
.game-pair{display:flex;gap:10px}
.game-pair img{width:50%;height:190px;object-fit:cover;border-radius:12px;border:1px solid var(--line)}
.hidden{display:none}
/* Barre de progression des recherches (discrète) */
.status-card{margin-top:24px}
.stat-row{display:flex;align-items:center;gap:10px;padding:8px 0;font-size:14.5px;border-bottom:1px solid var(--line)}
.stat-row:last-child{border-bottom:none}
.stat-dot{width:10px;height:10px;border-radius:50%;flex:none;background:#cbd5e1}
.stat-dot.ok{background:var(--ok)} .stat-dot.warn{background:var(--warn)} .stat-dot.down{background:var(--bad)}
.stat-val{margin-left:auto;font-weight:700;font-size:13px;color:var(--muted)}
.ind-bar{height:4px;background:#ffe0d4;border-radius:3px;overflow:hidden;margin-top:6px}
.ind-bar>div{height:100%;width:40%;background:var(--accent);border-radius:3px;animation:indet 1.2s infinite ease-in-out}
@keyframes indet{0%{margin-left:-40%}100%{margin-left:100%}}
.search-prog{margin-top:12px;display:none}
.search-prog.on{display:block}
.search-prog .bar{height:5px;background:#e2e8f0;border-radius:3px;overflow:hidden}
.search-prog .bar>div{height:100%;width:0%;background:var(--accent2);transition:width .3s ease}
.search-prog .lbl{font-size:12.5px;color:var(--muted);margin-top:6px;display:flex;gap:7px;align-items:center}
.search-prog .dot{width:7px;height:7px;border-radius:50%;background:var(--accent2);flex:none;animation:pulse 1.1s infinite}
@keyframes pulse{0%,100%{opacity:.25}50%{opacity:1}}
.lead{display:flex;gap:12px;background:#fbfdff;border:1px solid var(--line);border-radius:12px;padding:12px;margin-top:10px}
.lead img{width:96px;height:96px;object-fit:cover;border-radius:10px;border:1px solid var(--line)}
#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:#16202c;color:#fff;
padding:12px 18px;border-radius:24px;opacity:0;transition:.3s;z-index:99;font-weight:600;max-width:90%}
#toast.show{opacity:1}
/* Visionneuse plein écran */
.candidate img,.lead img,.game-pair img{cursor:zoom-in}
.zwrap{position:relative;display:inline-block;flex:none;line-height:0}
.zwrap .zbadge{position:absolute;right:4px;bottom:4px;background:rgba(8,12,18,.66);color:#fff;
border-radius:9px;font-size:12px;padding:1px 6px;pointer-events:none;line-height:1.5}
#lightbox{position:fixed;inset:0;background:rgba(8,12,18,.94);display:none;align-items:center;
justify-content:center;z-index:200;padding:20px;cursor:zoom-out}
#lightbox.on{display:flex}
#lightbox img{max-width:100%;max-height:88vh;border-radius:14px;box-shadow:0 8px 40px rgba(0,0,0,.6)}
#lightbox .x{position:absolute;top:16px;right:18px;width:44px;height:44px;border:none;border-radius:50%;
background:rgba(255,255,255,.16);color:#fff;font-size:24px;cursor:pointer;line-height:1}
#lightbox .hint-zoom{position:absolute;bottom:20px;left:0;right:0;text-align:center;color:#cdd6e0;font-size:13px}
/* Pied de page */
footer{max-width:680px;margin:8px auto 28px;padding:18px 16px 0;border-top:1px solid var(--line);
text-align:center;color:var(--muted);font-size:12.5px;line-height:1.7}
footer a{color:var(--muted);text-decoration:underline;cursor:pointer}
/* Page légale */
.legal p{font-size:14.5px;color:#2b3a48;margin:10px 0}
.legal ul{font-size:14.5px;color:#2b3a48;padding-left:18px;margin:8px 0}
.legal li{margin:7px 0}
.legal a{color:var(--accent2)}
/* FAQ */
.faq{margin-top:168px;margin-bottom:90px}
.faq h3{font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.6px;text-transform:uppercase;margin:0 0 2px}
.faq details{border-bottom:1px solid var(--line)}
.faq summary{font-weight:600;cursor:pointer;font-size:14px;color:var(--ink);list-style:none;display:flex;gap:9px;padding:12px 2px}
.faq summary::-webkit-details-marker{display:none}
.faq summary::before{content:'+';color:var(--muted);font-weight:700}
.faq details[open] summary::before{content:'–'}
.faq p{font-size:13.5px;color:var(--muted);margin:0 0 12px;line-height:1.5;padding:0 2px}
.faq a{color:var(--accent2);text-decoration:underline}
</style>
</head>
<body>
<header>
<button class="back" id="backBtn" onclick="goHome()"></button>
<h1 id="hTitle">🐱 Allochat.org - Entraide communautaire pour retrouver les chats perdus, avec l'IA</h1>
<div class="lang-toggle">
<button class="lang-btn on" id="btn_lang_fr" onclick="setLang('fr')">FR</button>
<button class="lang-btn" id="btn_lang_en" onclick="setLang('en')">EN</button>
</div>
</header>
<div id="startup" style="display:none;background:#fff6f3;border-bottom:1px solid #ffd9cd;padding:10px 16px">
<div style="max-width:680px;margin:0 auto;font-size:13.5px;color:#b3360f">
<span data-i18n="startup_note">Démarrage du serveur et de l'IA, cela peut prendre jusqu'à 45 secondes.</span>
<div class="ind-bar"><div></div></div>
</div>
</div>
<main>
<!-- ===================== ACCUEIL ===================== -->
<section id="home">
<p class="sub" data-i18n-html="home_sub"><b>Entraide communautaire pour retrouver les chats perdus à l'aide de l'IA.</b></p>
<div class="home">
<button class="action hero hero-lost" onclick="show('report')">
<div class="head"><div>
<span class="tag tag-lost" data-i18n="tag_owner">Propriétaire</span>
<div class="t" data-i18n="hero_lost_title">J'ai perdu mon chat</div></div></div>
<div class="d" data-i18n="hero_lost_desc">Vous avez perdu votre chat ? Déclarez-le ici avec des photos (et/ou son affiche). Notre IA analyse les images et vous envoie une alerte si quelqu'un du quartier le croise.</div>
<div class="feats">
<span class="feat" data-i18n="hero_lost_f1">📩 Alerte courriel</span>
<span class="feat" data-i18n="hero_lost_f2">🔒 Contact privé</span>
</div>
</button>
<button class="action hero hero-seen" onclick="show('check')">
<div class="head"><div>
<span class="tag tag-seen" data-i18n="tag_finder">Passant / Trouveur</span>
<div class="t" data-i18n="hero_seen_title">J'ai vu un chat</div></div></div>
<div class="d" data-i18n="hero_seen_desc">Vous avez croisé un chat qui semble perdu ? Signalez-le ici avec une photo. S'il correspond à un chat déclaré, le ou la propriétaire est prévenu(e) et pourra vous recontacter.</div>
<div class="feats">
<span class="feat" data-i18n="hero_seen_f1">⚡ Résultat immédiat</span>
<span class="feat" data-i18n="hero_seen_f2">🔒 Anonyme</span>
</div>
</button>
<button class="action hero hero-follow" onclick="show('follow')">
<div class="head"><div>
<span class="tag tag-follow" data-i18n="tag_follow">Suivi</span>
<div class="t" data-i18n="hero_follow_title">Suivre mon signalement</div></div></div>
<div class="d" data-i18n="hero_follow_desc">Accédez à votre espace pour gérer votre alerte ou clôturer la recherche.</div>
</button>
</div>
<p class="sub" style="margin-top:24px;margin-bottom:8px" data-i18n="community_sub">Aider la communauté :</p>
<div class="secondary">
<button class="action mini" onclick="show('poster')">
<div><div class="t" data-i18n="mini_poster_title">Partager une affiche vue</div>
<div class="d" data-i18n="mini_poster_desc">Partagez des photos d'affiches de chats perdus que vous croisez, pour enrichir la base de données et aidez plus de gens à retrouver leur chat.</div></div></button>
<button class="action mini" onclick="show('train')">
<div><div class="t" data-i18n="mini_train_title">Aider l'IA</div>
<div class="d" data-i18n="mini_train_desc">Entraînez l'IA à mieux reconnaître les chats perdus.</div></div></button>
</div>
<div class="card status-card">
<h3 data-i18n="status_title" style="margin-top:0">État des systèmes</h3>
<div class="stat-row"><span class="stat-dot" id="dot_app"></span><span data-i18n="status_app">Hébergement (Hugging Face)</span><span class="stat-val" id="val_app"></span></div>
<div class="stat-row"><span class="stat-dot" id="dot_ai"></span><span data-i18n="status_ai">Reconnaissance IA</span><span class="stat-val" id="val_ai"></span></div>
<div class="stat-row"><span class="stat-dot" id="dot_db"></span><span data-i18n="status_db">Base de données</span><span class="stat-val" id="val_db"></span></div>
<div class="stat-row"><span class="stat-dot" id="dot_mail"></span><span data-i18n="status_mail">Service courriel</span><span class="stat-val" id="val_mail"></span></div>
</div>
<div class="faq">
<h3 data-i18n="faq_title">Questions fréquentes</h3>
<details><summary data-i18n="faq_q1">Comment ça fonctionne&nbsp;?</summary>
<p data-i18n-html="faq_a1">Pour déclarer un chat perdu : cliquez sur « J'ai perdu mon chat » et suivez les instructions.<br><br>Pour vérifier si un chat croisé est perdu : utilisez « J'ai vu un chat ». S'il correspond à un chat déclaré, le ou la propriétaire reçoit une alerte.</p>
</details>
<details><summary data-i18n="faq_q2">Mes informations restent-elles privées&nbsp;?</summary>
<p data-i18n="faq_a2">Oui. Vos informations restent privées et ne servent qu'à vous aider à retrouver votre chat et à vous mettre en relation. Aucune revente de données, aucune utilisation à des fins marketing.</p>
</details>
<details><summary data-i18n="faq_q3">Est-ce gratuit&nbsp;?</summary>
<p data-i18n="faq_a3">Oui, entièrement. C'est un projet d'entraide communautaire, sans publicité.</p></details>
<details><summary data-i18n="faq_q4">Quelle technologie derrière&nbsp;?</summary>
<p data-i18n-html="faq_a4">Allo Chat utilise <a href="https://huggingface.co/conservationxlabs/miewid-msv3" target="_blank" rel="noopener noreferrer">MiewID</a>, un modèle d'IA de ré-identification animale qui transforme chaque photo en une « empreinte » visuelle. On compare ces empreintes pour repérer les chats qui se ressemblent, puis un humain confirme toujours la correspondance. Le service fonctionne via la plateforme <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer">Hugging Face</a>.</p>
</details>
</div>
</section>
<!-- ===================== 1. VÉRIFIER ===================== -->
<section id="check" class="hidden">
<div class="privacy" data-i18n="check_privacy">🔒 On ne montre que les chats qui ressemblent à votre photo. Le contact du
propriétaire reste privé — c'est lui qui vous recontacte.</div>
<div class="card">
<h2 data-i18n="check_h2">Vous avez croisé un chat&nbsp;?</h2>
<label data-i18n-html="check_photo_label">Photos du chat — <b>5 à 10 recommandé</b></label>
<label class="photo-btn" for="c_photo" data-i18n="check_photo_btn">📷 Ajouter des photos</label>
<input class="file-hidden" id="c_photo" type="file" accept="image/*" multiple onchange="prev('c_photo','c_prev')" />
<div class="thumbs" id="c_prev"></div>
<div class="hint" data-i18n="check_photo_hint">Astuce : partagez plusieurs angles pour faciliter la reconnaissance.</div>
<label data-i18n="check_email_label">Votre courriel (pour recevoir une confirmation — optionnel)</label>
<input id="c_email" type="email" data-i18n-placeholder="check_email_placeholder" placeholder="vous@exemple.com" />
<div class="hint" id="c_gps" data-i18n="check_gps">📍 Position : non captée (optionnel, aide au tri)</div>
<button class="btn btn-ghost" onclick="getPos('c')" data-i18n="getpos_btn">Capter ma position</button>
<button class="btn btn-go" onclick="doCheck()" data-i18n="check_btn">🔍 Vérifier</button>
<p class="hint" data-i18n="proc_time_note">L'analyse par l'IA peut prendre jusqu'à 1 minute — gardez la page ouverte.</p>
<div class="search-prog" id="c_prog"><div class="bar"><div id="c_prog_fill"></div></div>
<div class="lbl"><span class="dot"></span><span id="c_prog_txt"></span></div></div>
</div>
<div id="c_result"></div>
<div id="c_leadbox" class="card hidden">
<h2 id="c_leadtitle" data-i18n="lead_title">Prévenir le propriétaire 🐾</h2>
<p class="hint" data-i18n="lead_hint">Laissez vos coordonnées : le propriétaire vous recontactera.</p>
<label data-i18n="lead_contact_label">Votre contact (téléphone / courriel)</label>
<input id="c_contact" placeholder="514-555-1234" />
<label data-i18n="lead_msg_label">Message (où, quand, état du chat)</label>
<textarea id="c_msg" rows="2" data-i18n-placeholder="lead_msg_placeholder" placeholder="Vu ce matin près du parc"></textarea>
<button class="btn btn-primary" onclick="sendLead()" data-i18n="lead_send_btn">Envoyer au propriétaire</button>
<p class="hint" style="margin-top:8px" data-i18n-html="consent_note">En soumettant, vous acceptez l'<a onclick="show('legal')">avis légal et de confidentialité</a>.</p>
</div>
</section>
<!-- ===================== 2. SIGNALER ===================== -->
<section id="report" class="hidden">
<div class="privacy" data-i18n-html="report_privacy">🔒 Votre contact reste privé. Vous recevez un <b>code secret</b> pour suivre les
signalements. Plus vous ajoutez de photos, mieux l'IA reconnaît votre chat.</div>
<div class="card">
<h2 data-i18n="report_h2">Signaler mon chat perdu</h2>
<p class="sub" data-i18n="report_intro">Déclarez-le perdu en ajoutant quelques photos et/ou son affiche. L'IA analyse les images. Dès qu'une personne prend en photo un chat qu'elle croit perdu et qu'il correspond au vôtre, vous recevez une alerte par courriel, ainsi que les infos pour contacter la personne qui a vu votre chat.</p>
<label data-i18n="report_poster_label">Vous avez une affiche&nbsp;? (photo ou PDF — optionnel)</label>
<label class="photo-btn" for="r_poster" data-i18n="report_poster_btn">🖼️ Charger l'affiche (remplit le formulaire)</label>
<input class="file-hidden" id="r_poster" type="file" accept="image/*,application/pdf" onchange="posterPrefill('r_poster')" />
<div class="hint" id="r_poster_note"></div>
<label data-i18n="report_name_label">Nom du chat</label>
<input id="r_name" data-i18n-placeholder="report_name_placeholder" placeholder="Tigrou" />
<label data-i18n="report_desc_label">Description (robe, signes distinctifs)</label>
<textarea id="r_desc" rows="2" data-i18n-placeholder="report_desc_placeholder" placeholder="Tortie, médaillon blanc, timide"></textarea>
<label data-i18n="report_email_label">Votre courriel (sert à suivre les signalements — jamais montré aux autres)</label>
<input id="r_email" type="email" data-i18n-placeholder="report_email_placeholder" placeholder="vous@exemple.com" />
<label data-i18n="report_phone_label">Votre téléphone (optionnel, privé)</label>
<input id="r_phone" type="tel" placeholder="514-555-1234" />
<label data-i18n="report_details_label">Détails supplémentaires (optionnel)</label>
<textarea id="r_details" rows="2" data-i18n-placeholder="report_details_placeholder" placeholder="Disparu le 3 juin, secteur Rosemont, collier bleu…"></textarea>
<label data-i18n-html="report_photos_label">Photos du chat — <b>5 à 10 recommandé</b></label>
<label class="photo-btn" for="r_photos" data-i18n="report_photos_btn">📷 Ajouter des photos du chat</label>
<input class="file-hidden" id="r_photos" type="file" accept="image/*" multiple onchange="prev('r_photos','r_prev'); photoHint()" />
<div class="thumbs" id="r_prev"></div>
<div class="hint" id="r_hint" data-i18n="report_photo_hint">Astuce : variez les angles et les éclairages pour une reconnaissance plus fiable.</div>
<div class="hint" id="r_gps" data-i18n="report_gps">📍 Dernière position connue : non captée</div>
<button class="btn btn-ghost" onclick="getPos('r')" data-i18n="report_getpos_btn">Capter la position</button>
<button class="btn btn-primary" onclick="doReport()" data-i18n="report_btn">Déclarer mon chat perdu</button>
<p class="hint" data-i18n="proc_time_note">L'analyse par l'IA peut prendre jusqu'à 1 minute — gardez la page ouverte.</p>
<p class="hint" style="margin-top:8px" data-i18n="spam_note">Pas de code reçu après une minute ? Vérifiez vos courriels indésirables (spam).</p>
<p class="hint" style="margin-top:4px" data-i18n-html="consent_note">En soumettant, vous acceptez l'<a onclick="show('legal')">avis légal et de confidentialité</a>.</p>
<div class="search-prog" id="r_prog"><div class="bar"><div id="r_prog_fill"></div></div>
<div class="lbl"><span class="dot"></span><span id="r_prog_txt"></span></div></div>
<div id="r_token"></div>
</div>
</section>
<!-- ===================== SUIVRE UN SIGNALEMENT ===================== -->
<section id="follow" class="hidden">
<div class="privacy" data-i18n="follow_privacy">🔒 Accès sécurisé : nous vous envoyons un code par courriel pour vérifier
que c'est bien vous. Vous voyez alors quand vous avez déclaré votre chat, combien de fois l'IA
l'a comparé, et les signalements reçus.</div>
<div class="card">
<h2 data-i18n="follow_h2">Suivre mon signalement</h2>
<label data-i18n="follow_email_label">Votre courriel (celui de la déclaration)</label>
<input id="r_track_email" type="email" data-i18n-placeholder="follow_email_placeholder" placeholder="vous@exemple.com" />
<button class="btn btn-ghost" onclick="requestCode()" data-i18n="follow_request_btn">📩 Recevoir mon code</button>
<p class="hint" style="margin-top:8px" data-i18n="spam_note">Pas de code reçu après une minute ? Vérifiez vos courriels indésirables (spam).</p>
<div id="r_code_box" class="hidden">
<label data-i18n="follow_code_label">Code reçu</label>
<input id="r_track_code" inputmode="numeric" placeholder="123456" />
<button class="btn btn-go" onclick="verifyCode()" data-i18n="follow_verify_btn">Voir mon suivi</button>
</div>
<div id="r_track"></div>
</div>
</section>
<!-- ===================== 3. PARTAGER UNE AFFICHE ===================== -->
<section id="poster" class="hidden">
<div class="privacy" data-i18n="poster_privacy">📋 Les affiches sont publiques. En les partageant, vous enrichissez la banque et
aidez d'autres personnes à retrouver leur chat.</div>
<div class="card">
<h2 data-i18n="poster_h2">Partager une affiche trouvée</h2>
<label data-i18n="poster_file_label">Affiche + photos du chat (plusieurs possibles, jusqu'à 10)</label>
<label class="photo-btn" for="p_file" data-i18n="poster_file_btn">📷 Ajouter l'affiche / des photos</label>
<input class="file-hidden" id="p_file" type="file" accept="image/*,application/pdf" multiple onchange="prev('p_file','p_prev')" />
<div class="thumbs" id="p_prev"></div>
<label data-i18n="poster_text_label">Infos visibles sur l'affiche (optionnel)</label>
<textarea id="p_text" rows="3" data-i18n-placeholder="poster_text_placeholder" placeholder="Nom, secteur, description… (ce qui est écrit sur l'affiche)"></textarea>
<label data-i18n="poster_email_label">Votre courriel (pour recevoir une confirmation — optionnel)</label>
<input id="p_email" type="email" data-i18n-placeholder="poster_email_placeholder" placeholder="vous@exemple.com" />
<div class="hint" id="p_gps" data-i18n="poster_gps">📍 Où l'avez-vous vue ? (optionnel)</div>
<button class="btn btn-ghost" onclick="getPos('p')" data-i18n="getpos_btn">Capter ma position</button>
<button class="btn btn-go" onclick="doPoster()" data-i18n="poster_btn">📋 Ajouter à la banque</button>
<p class="hint" data-i18n="proc_time_note">L'analyse par l'IA peut prendre jusqu'à 1 minute — gardez la page ouverte.</p>
<div id="p_result"></div>
</div>
</section>
<!-- ===================== 4. ENTRAÎNER L'IA ===================== -->
<section id="train" class="hidden">
<div class="privacy" data-i18n="train_privacy">🧠 Aide volontaire. Ces photos servent UNIQUEMENT à entraîner l'IA — elles
ne sont jamais mêlées aux fiches privées de chats perdus.</div>
<div class="card">
<h2 data-i18n="train_photos_h2">Donner des photos</h2>
<p class="hint" data-i18n="train_photos_hint">Ajoutez des photos de chats (le vôtre, ou des chats croisés) pour aider l'IA à mieux reconnaître.</p>
<label class="photo-btn" for="t_photos" data-i18n="train_photos_btn">📷 Ajouter des photos</label>
<input class="file-hidden" id="t_photos" type="file" accept="image/*" multiple onchange="prev('t_photos','t_prev')" />
<div class="thumbs" id="t_prev"></div>
<label style="display:flex;gap:10px;align-items:flex-start;font-weight:400;font-size:13.5px;margin-top:12px">
<input id="t_consent" type="checkbox" style="width:auto;margin-top:2px" />
<span data-i18n-html="train_consent">J'accepte que ces images soient utilisées pour <b>entraîner le modèle de
reconnaissance</b>. Elles ne seront pas mêlées aux fiches de chats perdus et sont conservées <b>au plus 2 ans</b>.</span>
</label>
<button class="btn btn-go" onclick="doContribute()" data-i18n="train_photos_submit">Donner ces photos</button>
</div>
<div class="card">
<h2 data-i18n="train_game_h2">Jouer : même chat ou pas&nbsp;?</h2>
<p class="hint" data-i18n="train_game_hint">Regardez deux photos et dites si c'est le même chat. Chaque réponse aide l'IA.</p>
<div id="game"><button class="btn btn-go" onclick="nextPair()" data-i18n="train_game_start">Commencer à jouer</button></div>
<div class="hint" id="t_stats"></div>
</div>
</section>
<!-- ===================== AVIS LÉGAL & CONFIDENTIALITÉ ===================== -->
<section id="legal" class="hidden">
<div class="card legal" id="legal_content">
<h2>Avis de non-responsabilité</h2>
<p>Allo Chat est un service communautaire <b>gratuit</b> d'entraide pour aider à
retrouver des chats perdus, à Montréal et ailleurs.</p>
<p><b>Service fourni « tel quel », sans garantie.</b> Le service est offert sans
garantie d'aucune sorte, expresse ou implicite (notamment quant à la qualité,
l'exactitude, la disponibilité ou les résultats). Les correspondances proposées par
l'intelligence artificielle sont <b>seulement indicatives</b> et doivent toujours être
confirmées par un humain. Allo Chat ne garantit en aucun cas qu'un chat sera retrouvé.</p>
<p><b>Limitation de responsabilité.</b> Dans les limites permises par la loi,
l'exploitant d'Allo Chat ne pourra être tenu responsable de tout dommage direct ou
indirect résultant de l'utilisation (ou de l'impossibilité d'utiliser) le service, des
interactions entre utilisateurs, de l'exactitude des renseignements fournis, d'une
fausse correspondance ou d'une interruption du service. <b>L'utilisation se fait à vos
propres risques.</b></p>
<p><b>Conduite des utilisateurs.</b> Vous êtes responsable des renseignements que vous
publiez et de vos échanges. Faites preuve de prudence lors d'un contact avec une
personne inconnue, et ne tentez pas d'approcher un animal craintif ou en détresse.</p>
<h2 style="margin-top:22px">Confidentialité — Loi 25 (Québec)</h2>
<p>Allo Chat respecte la <i>Loi sur la protection des renseignements personnels dans le
secteur privé</i> (Loi 25). Voici l'essentiel.</p>
<ul>
<li><b>Responsable de la protection des renseignements personnels :</b>
Équipe Allo Chat — <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
<li><b>Renseignements recueillis :</b> photos de chats, courriel (pour le suivi),
téléphone (facultatif), description, et position approximative (facultative, si vous
l'autorisez — arrondie à environ 100 m dès la réception ; la position exacte n'est
jamais conservée).</li>
<li><b>Finalité unique :</b> mettre en relation propriétaires et personnes ayant repéré
un chat. <b>Aucune revente, aucune publicité, aucun profilage.</b></li>
<li><b>Vos photos ne sont ni vendues ni utilisées pour entraîner l'IA</b> — à la
<b>seule exception</b> des images que vous donnez volontairement via la section
« Aider l'IA » (consentement explicite et distinct).</li>
<li><b>Consentement :</b> la collecte se fait avec votre consentement. Les photos
destinées à entraîner l'IA exigent un consentement explicite distinct.</li>
<li><b>Confidentialité par défaut :</b> aucun annuaire public, aucune navigation libre.
La personne qui repère un chat ne voit jamais vos coordonnées ni votre adresse —
l'accès à vos données se fait par un code de vérification envoyé à votre courriel.</li>
<li><b>Conservation et droit à l'oubli :</b> vous pouvez <b>supprimer définitivement</b>
votre fiche (photos, signalements reçus, contact) à tout moment, via « Suivre mon
signalement ». Une purge automatique supprime les fiches résolues après
<b>90 jours</b>, les fiches inactives après <b>12 mois</b>, et les signalements
sans suite après <b>6 mois</b>. Les photos données pour entraîner l'IA
(section « Aider l'IA ») sont conservées <b>au plus 2 ans</b>.</li>
<li><b>Vos droits :</b> accès, rectification, retrait du consentement et suppression.
Pour les exercer, écrivez à
<a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
<li><b>Hébergement :</b> l'application et les données sont confiées à des
<b>fournisseurs de services externes, dont Hugging Face</b> (et notre base de
données infonuagique). Les données peuvent être traitées à l'extérieur du Québec
et demeurent chiffrées en transit et au repos.</li>
<li><b>Témoins et mesure d'audience :</b> stockage local minimal (votre courriel,
pour faciliter le suivi) et un outil de statistiques de visites (StatCounter) afin
d'améliorer le service. <b>Aucune publicité ; vos renseignements ne sont ni vendus
ni utilisés à des fins commerciales.</b></li>
<li><b>Incident de confidentialité :</b> en cas d'incident présentant un risque, les
personnes concernées et la Commission d'accès à l'information seront avisées
conformément à la Loi 25.</li>
</ul>
<p class="hint" style="margin-top:14px">Dernière mise à jour : juin 2026. Ce résumé est
fourni à titre informatif et ne constitue pas un avis juridique.</p>
</div>
</section>
</main>
<footer id="footer_el">
🐾 Fait à Montréal · © 2026 Allo Chat ·
<a onclick="show('legal')">Avis légal &amp; confidentialité</a><br>
Contact : <a href="mailto:info@allochat.org">info@allochat.org</a>
</footer>
<!-- Bannière de consentement aux témoins (Loi 25) -->
<div id="cookie_banner" style="display:none;position:fixed;left:0;right:0;bottom:0;z-index:80;
background:#16202c;color:#fff;padding:14px 16px;box-shadow:0 -2px 14px rgba(0,0,0,.25);
font-size:14px;line-height:1.45">
<div style="max-width:680px;margin:0 auto;display:flex;gap:12px;align-items:center;flex-wrap:wrap">
<span style="flex:1;min-width:220px" data-i18n-html="cookie_text">Ce site utilise un stockage local utile (mémoriser votre courriel) et un outil de <b>mesure d'audience</b> (statistiques de visites). <b>Aucune publicité ; vos informations ne sont ni vendues ni utilisées à des fins commerciales.</b> <a onclick="show('legal')" style="color:#7fb3ff">En savoir plus</a>.</span>
<button class="btn btn-go" style="margin:0;white-space:nowrap;width:auto;padding:10px 18px"
onclick="acceptCookies()" data-i18n="cookie_btn">J'ai compris</button>
</div>
</div>
<div id="toast"></div>
<div id="lightbox" onclick="closeZoom(event)">
<button class="x" onclick="closeZoom(event)" aria-label="Fermer"></button>
<img id="lightbox_img" alt="Photo du chat en grand">
<div class="hint-zoom" data-i18n="lightbox_hint">Touche l'image ou appuie sur ✕ pour fermer</div>
</div>
<script>
const API = location.origin;
const pos = {c:null,r:null,p:null};
let lastSightingId=null, chosenCandidateToken=null, gamePair=null;
let lastMatches=[], ownerCats=[];
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>(
{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// ---- TRADUCTIONS / TRANSLATIONS ----
const T = {
fr: {
home_sub: "<b>Entraide communautaire pour retrouver les chats perdus à l'aide de l'IA.</b>",
home_sub2: "Déclarez votre chat perdu ou signalez un chat que vous croisez. L'IA compare les photos et envoie une alerte par courriel en cas de correspondance. Vos informations restent privées.",
tag_owner: 'Propriétaire',
hero_lost_title: "J'ai perdu mon chat",
hero_lost_desc: "Vous avez perdu votre chat ? Déclarez-le ici avec des photos (et/ou son affiche). Notre IA analyse les images et vous envoie une alerte si quelqu'un du quartier le croise.",
hero_lost_f1: "📩 Alerte courriel",
hero_lost_f2: "🔒 Contact privé",
tag_finder: 'Passant / Trouveur',
hero_seen_title: "J'ai vu un chat",
hero_seen_desc: "Vous avez croisé un chat qui semble perdu ? Signalez-le ici avec une photo. S'il correspond à un chat déclaré, le ou la propriétaire est prévenu(e) et pourra vous recontacter.",
hero_seen_f1: "⚡ Résultat immédiat",
hero_seen_f2: "🔒 Anonyme",
tag_follow: 'Suivi',
hero_follow_title: 'Suivre mon signalement',
hero_follow_desc: 'Accédez à votre espace pour gérer votre alerte ou clôturer la recherche.',
community_sub: 'Aider la communauté :',
mini_poster_title: 'Partager une affiche vue',
mini_poster_desc: 'Partagez les affiches de chats perdus que vous croisez, pour enrichir la base et aider plus de gens.',
mini_train_title: "Aider l'IA",
mini_train_desc: "Entraînez l'IA à mieux reconnaître les chats perdus.",
status_title: 'État des systèmes',
status_app: 'Hébergement (Hugging Face)',
status_ai: 'Reconnaissance IA',
status_db: 'Base de données',
status_mail: 'Service courriel',
status_ok: 'Opérationnel',
status_starting: 'En démarrage',
status_down: 'Indisponible',
faq_title: 'Questions fréquentes',
faq_q1: 'Comment ça fonctionne\u00a0?',
faq_a1: "Pour déclarer un chat perdu : cliquez sur « J'ai perdu mon chat » et suivez les instructions.<br><br>Pour vérifier si un chat croisé est perdu : utilisez « J'ai vu un chat ». S'il correspond à un chat déclaré, le ou la propriétaire reçoit une alerte.",
faq_q2: 'Mes informations restent-elles privées\u00a0?',
faq_a2: "Oui. Vos informations restent privées et ne servent qu'à vous aider à retrouver votre chat et à vous mettre en relation. Aucune revente de données, aucune utilisation à des fins marketing.",
faq_q3: 'Est-ce gratuit\u00a0?',
faq_a3: "Oui, entièrement. C'est un projet d'entraide communautaire, sans publicité.",
faq_q4: 'Quelle technologie derrière\u00a0?',
faq_a4: 'Allo Chat utilise <a href="https://huggingface.co/conservationxlabs/miewid-msv3" target="_blank" rel="noopener noreferrer">MiewID</a>, un modèle d\'IA de ré-identification animale qui transforme chaque photo en une « empreinte » visuelle. On compare ces empreintes pour repérer les chats qui se ressemblent, puis un humain confirme toujours la correspondance. Le service fonctionne via la plateforme <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer">Hugging Face</a>.',
title_home: '🐱 Allochat.org',
title_check: "📷 J'ai vu un chat",
title_report: "J'ai perdu mon chat",
title_follow: 'ℹ️ Suivre mon signalement',
title_poster: '📋 Partager une affiche',
title_train: "🧠 Aider l'IA",
title_legal: '⚖️ Avis légal & confidentialité',
check_privacy: "🔒 On ne montre que les chats qui ressemblent à votre photo. Le contact du propriétaire reste privé — c'est lui qui vous recontacte.",
check_h2: 'Vous avez croisé un chat\u00a0?',
check_photo_label: 'Photos du chat — <b>5 à 10 recommandé</b>',
check_photo_btn: '📷 Ajouter des photos',
check_photo_hint: 'Astuce : partagez plusieurs angles pour faciliter la reconnaissance.',
check_gps: '📍 Position : non captée (optionnel, aide au tri)',
getpos_btn: 'Capter ma position',
check_btn: '🔍 Vérifier',
lead_title: 'Prévenir le propriétaire 🐾',
lead_hint: 'Laissez vos coordonnées : le propriétaire vous recontactera.',
lead_contact_label: 'Votre contact (téléphone / courriel)',
lead_msg_label: 'Message (où, quand, état du chat)',
lead_msg_placeholder: 'Vu ce matin près du parc',
lead_send_btn: 'Envoyer au propriétaire',
consent_note: "En soumettant, vous acceptez l'<a onclick=\"show('legal')\">avis légal et de confidentialité</a>.",
report_intro: "Déclarez-le perdu en ajoutant quelques photos et/ou son affiche. L'IA analyse les images. Dès qu'une personne prend en photo un chat qu'elle croit perdu et qu'il correspond au vôtre, vous recevez une alerte par courriel, ainsi que les infos pour contacter la personne qui a vu votre chat.",
report_privacy: "🔒 Votre contact reste privé. Vous recevez un <b>code secret</b> pour suivre les signalements. Plus vous ajoutez de photos, mieux l'IA reconnaît votre chat.",
report_h2: 'Signaler mon chat perdu',
report_poster_label: 'Vous avez une affiche\u00a0? (photo ou PDF — optionnel)',
report_poster_btn: "🖼️ Charger l'affiche (remplit le formulaire)",
report_name_label: 'Nom du chat',
report_name_placeholder: 'Tigrou',
report_desc_label: 'Description (robe, signes distinctifs)',
report_desc_placeholder: 'Tortie, médaillon blanc, timide',
report_email_label: 'Votre courriel (sert à suivre les signalements — jamais montré aux autres)',
report_email_placeholder: 'vous@exemple.com',
report_phone_label: 'Votre téléphone (optionnel, privé)',
report_details_label: 'Détails supplémentaires (optionnel)',
report_details_placeholder: 'Disparu le 3 juin, secteur Rosemont, collier bleu…',
report_photos_label: 'Photos du chat — <b>5 à 10 recommandé</b>',
report_photos_btn: '📷 Ajouter des photos du chat',
report_photo_hint: 'Astuce : variez les angles et les éclairages pour une reconnaissance plus fiable.',
report_gps: '📍 Dernière position connue : non captée',
report_getpos_btn: 'Capter la position',
report_btn: 'Déclarer mon chat perdu',
follow_privacy: "🔒 Accès sécurisé : nous vous envoyons un code par courriel pour vérifier que c'est bien vous. Vous voyez alors quand vous avez déclaré votre chat, combien de fois l'IA l'a comparé, et les signalements reçus.",
follow_h2: 'Suivre mon signalement',
follow_email_label: 'Votre courriel (celui de la déclaration)',
follow_email_placeholder: 'vous@exemple.com',
follow_request_btn: '📩 Recevoir mon code',
spam_note: "Pas de code reçu après une minute ? Vérifiez vos courriels indésirables (spam).",
follow_code_label: 'Code reçu',
follow_verify_btn: 'Voir mon suivi',
poster_privacy: "📋 Les affiches sont publiques. En les partageant, vous enrichissez la banque et aidez d'autres personnes à retrouver leur chat.",
poster_h2: 'Partager une affiche trouvée',
poster_file_label: "Affiche + photos du chat (plusieurs possibles, jusqu'à 10)",
poster_file_btn: "📷 Ajouter l'affiche / des photos",
poster_text_label: "Infos visibles sur l'affiche (optionnel)",
poster_text_placeholder: "Nom, secteur, description… (ce qui est écrit sur l'affiche)",
poster_gps: "📍 Où l'avez-vous vue ? (optionnel)",
poster_btn: '📋 Ajouter à la banque',
train_privacy: "🧠 Aide volontaire. Ces photos servent UNIQUEMENT à entraîner l'IA — elles ne sont jamais mêlées aux fiches privées de chats perdus.",
train_photos_h2: 'Donner des photos',
train_photos_hint: "Ajoutez des photos de chats (le vôtre, ou des chats croisés) pour aider l'IA à mieux reconnaître.",
train_photos_btn: '📷 Ajouter des photos',
train_consent: "J'accepte que ces images soient utilisées pour <b>entraîner le modèle de reconnaissance</b>. Elles ne seront pas mêlées aux fiches de chats perdus et sont conservées <b>au plus 2 ans</b>.",
train_photos_submit: 'Donner ces photos',
train_game_h2: 'Jouer : même chat ou pas\u00a0?',
train_game_hint: "Regardez deux photos et dites si c'est le même chat. Chaque réponse aide l'IA.",
train_game_start: 'Commencer à jouer',
lightbox_hint: 'Touchez l\'image ou appuyez sur ✕ pour fermer',
footer_html: "🐾 Fait à Montréal · © 2026 Allo Chat · <a onclick=\"show('legal')\">Avis légal &amp; confidentialité</a><br>Contact : <a href=\"mailto:info@allochat.org\">info@allochat.org</a>",
legal_html: `<h2>Avis de non-responsabilité</h2>
<p>Allo Chat est un service communautaire <b>gratuit</b> d'entraide pour aider à retrouver des chats perdus, à Montréal et ailleurs.</p>
<p><b>Service fourni « tel quel », sans garantie.</b> Les correspondances proposées par l'IA sont <b>seulement indicatives</b> et doivent toujours être confirmées par un humain. Allo Chat ne garantit en aucun cas qu'un chat sera retrouvé. <b>L'utilisation se fait à vos propres risques.</b></p>
<p><b>Limitation de responsabilité.</b> L'exploitant d'Allo Chat ne pourra être tenu responsable de tout dommage direct ou indirect résultant de l'utilisation du service, des interactions entre utilisateurs, d'une fausse correspondance ou d'une interruption du service.</p>
<p><b>Conduite des utilisateurs.</b> Vous êtes responsable des renseignements que vous publiez. Faites preuve de prudence lors d'un contact avec une personne inconnue.</p>
<h2 style="margin-top:22px">Confidentialité — Loi 25 (Québec)</h2>
<ul>
<li><b>Responsable :</b> Équipe Allo Chat — <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
<li><b>Données recueillies :</b> photos de chats, courriel, téléphone (facultatif), description, position (facultative).</li>
<li><b>Finalité unique :</b> mettre en relation propriétaires et trouveurs. <b>Aucune revente, aucune publicité, aucun profilage.</b></li>
<li><b>Vos photos ne sont ni vendues ni utilisées pour entraîner l'IA</b>, sauf celles données volontairement via « Aider l'IA » (consentement explicite distinct).</li>
<li><b>Confidentialité par défaut :</b> aucun annuaire public. Le trouveur ne voit jamais vos coordonnées — accès par code de vérification par courriel.</li>
<li><b>Droit à l'oubli :</b> suppression définitive de votre fiche à tout moment via « Suivre mon signalement ».</li>
<li><b>Hébergement :</b> fournisseurs de services externes (dont Hugging Face) ; données chiffrées en transit et au repos.</li>
<li><b>Droits :</b> accès, rectification, suppression → <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
</ul>
<p class="hint" style="margin-top:14px">Dernière mise à jour : juin 2026.</p>`,
// JS dynamic strings
js_add_photo: 'Ajoutez une photo du chat',
js_net_err: 'Erreur réseau — réessayez',
js_rate: ' — trop de requêtes, patiente une minute',
js_analysis_done: '✅ Analyse terminée',
js_no_match: 'Aucune correspondance',
js_match_q: 'Reconnaissez-vous l\'un d\'eux\u00a0?',
js_match_count: n => `${n} chat(s) qui ressemble(nt)`,
js_conf_strong: 'Forte', js_conf_medium: 'Moyenne', js_conf_weak: 'Faible',
js_its_him: "C'est lui ✅",
js_view_poster: '🖼️ Voir l\'affiche',
js_no_poster_photo: 'Aucune photo d\'affiche disponible.',
js_cat_default: 'Chat perdu',
js_choose_cat_prompt: 'Choisissez un chat',
js_lead_sent: '<div class="result match"><div class="big">🙏</div><b>Merci !</b><p class="hint">Le propriétaire a reçu votre signalement.</p></div>',
js_email_required: 'Entrez votre courriel (pour suivre les signalements)',
js_email_short: 'Entrez votre courriel',
js_code_invalid: '<p class="hint">Code invalide ou expiré. Redemande un code.</p>',
js_no_cats: '<p class="hint">Aucun chat rattaché à ce courriel.</p>',
js_logout: 'Déconnecté 🔒',
js_logout_btn: '🔒 Se déconnecter',
js_report_label: 'Signalement',
js_no_message: '(pas de message)',
js_no_contact: '(pas de contact)',
js_no_leads: 'Aucun signalement pour l\'instant.',
js_declared_on: 'déclaré le',
js_ai_compare: 'comparaisons IA 🔎',
js_leads_received: 'signalement(s) reçu(s)',
js_my_cat: 'Mon chat',
js_status: 'statut',
js_mark_found: 'Marquer « retrouvé » 🎉',
js_delete: '🗑️ Supprimer définitivement',
js_leads_title: n => `Signalements reçus (${n})`,
js_map: '📍 carte',
js_delete_confirm: (name) => `Supprimer définitivement la fiche de ${name} ?\nPhotos, signalements et contact seront effacés. Irréversible.`,
js_this_profile: 'cette fiche',
js_deleted_toast: 'Fiche supprimée 🗑️',
js_deleted_html: '<p class="hint">Fiche supprimée définitivement (droit à l\'oubli).</p>',
js_found_toast: 'Marqué retrouvé 🎉',
js_add_poster: 'Charge une affiche',
js_poster_added: '<div class="result match"><div class="big">🙏</div><b>Merci !</b><p class="hint">Affiche ajoutée à la banque. Si vous avez laissé votre courriel, une confirmation vous a été envoyée 📧</p></div>',
check_email_label: 'Votre courriel (pour recevoir une confirmation — optionnel)',
check_email_placeholder: 'vous@exemple.com',
poster_email_label: 'Votre courriel (pour recevoir une confirmation — optionnel)',
poster_email_placeholder: 'vous@exemple.com',
js_add_photos: 'Ajoutez des photos',
js_consent_required: 'Coche la case de consentement d\'abord 🙏',
js_sending: 'Envoi…',
js_photos_donated: photos => `Merci ! ${photos} photos dans le pool 🙏`,
js_loading: 'Chargement…',
js_not_enough_photos: 'Pas encore assez de photos données. Ajoutez-en d\'abord ci-dessus 🙂',
js_same_cat: '😺 Même chat',
js_diff_cat: '🙀 Différents',
js_label_thanks: 'Merci ! 🎓',
js_stats: (photos, labels) => `Pool : ${photos} photos · ${labels} réponses données`,
js_poster_reading: "Lecture de l'affiche…",
js_poster_loaded: 'Affiche chargée (lecture auto indisponible).',
js_photo_hint_few: n => `${n} photo(s) — ajoutez-en plus (visez 5+) pour bien reconnaître.`,
js_photo_hint_ok: n => `${n} photos — bien ! quelques-unes de plus = idéal.`,
js_photo_hint_great: n => `${n} photos — excellent ✅`,
js_gps_unavail: 'Géolocalisation indisponible',
js_gps_refused: 'Position refusée',
js_ref_label: 'Numéro de référence',
proc_time_note: "L'analyse par l'IA peut prendre jusqu'à 1 minute — gardez la page ouverte.",
startup_note: "Démarrage du serveur et de l'IA, cela peut prendre jusqu'à 45 secondes.",
js_detected_label: 'Chat détecté',
js_report_success: (name, photos, email, ref, detected) => `<div class="result match" style="margin-top:14px"><b>✅ ${esc(name||'Chat')} déclaré (${photos} photos)</b>${detected?`<p class="hint">🐱 Chat détecté : <b>${esc(detected)}</b></p>`:''}${ref?`<p class="hint">Numéro de suivi : <b>${esc(ref)}</b></p>`:''}<p class="hint">📧 Un courriel de confirmation vous a été envoyé à ${esc(email)} (vérifiez vos indésirables). Pour voir les signalements : accueil → « Suivre mon signalement », entrez votre courriel et le code reçu. Rien à mémoriser 🙂</p></div>`,
js_choose_cat_title: name => `Vous reconnaissez ${name} ? Prévenez le propriétaire 🐾`,
js_prog_embed: name => `🧠 ${name} crée l'empreinte visuelle du chat…`,
js_prog_compare: '🔎 Comparaison avec les chats perdus enregistrés…',
js_prog_working: "L'IA analyse vos photos et les compare aux chats perdus. Cela peut prendre jusqu'à une minute — gardez cette page ouverte.",
js_analysis_timeout: "L'analyse prend plus de temps que prévu. Réessayez dans un instant (le serveur démarrait peut-être).",
js_prog_save: photos => `🧠 ${photos} crée l'empreinte de ${photos}…`,
js_prog_saving: '💾 Enregistrement sécurisé de la fiche…',
js_date_locale: 'fr-CA',
js_num_locale: 'fr-CA',
js_code_sent: "Un code vient de vous être envoyé par courriel — vérifiez votre boîte, et vos indésirables (spam) 📬",
cookie_text: "Ce site utilise un stockage local utile (mémoriser votre courriel) et un outil de <b>mesure d'audience</b> (statistiques de visites). <b>Aucune publicité ; vos informations ne sont ni vendues ni utilisées à des fins commerciales.</b> <a onclick=\"show('legal')\" style=\"color:#7fb3ff\">En savoir plus</a>.",
cookie_btn: "J'ai compris",
},
en: {
home_sub: '<b>Community help to find lost cats, with AI.</b>',
home_sub2: "Report your lost cat or flag one you spot. The AI compares the photos and sends an email alert when there's a match. Your information stays private.",
tag_owner: 'Owner',
hero_lost_title: 'I lost my cat',
hero_lost_desc: 'Lost your cat? Report it here with photos (and/or its poster). Our AI analyzes the images and emails you an alert if someone nearby spots it.',
hero_lost_f1: '📩 Email alert',
hero_lost_f2: '🔒 Private contact',
tag_finder: 'Passerby / Finder',
hero_seen_title: 'I saw a cat',
hero_seen_desc: 'Spotted a cat that looks lost? Report it here with a photo. If it matches a reported cat, the owner is notified and can reach back out to you.',
hero_seen_f1: '⚡ Instant result',
hero_seen_f2: '🔒 Anonymous',
tag_follow: 'Tracking',
hero_follow_title: 'Track my report',
hero_follow_desc: 'Access your space to manage your alert or close the search.',
community_sub: 'Help the community:',
mini_poster_title: 'Share a found poster',
mini_poster_desc: 'Share posters of lost cats you come across, to enrich the database and help more people.',
mini_train_title: 'Help the AI',
mini_train_desc: 'Help the AI better recognize lost cats.',
status_title: 'System status',
status_app: 'Hosting (Hugging Face)',
status_ai: 'AI recognition',
status_db: 'Database',
status_mail: 'Email service',
status_ok: 'Operational',
status_starting: 'Starting up',
status_down: 'Unavailable',
faq_title: 'Frequently asked questions',
faq_q1: 'How does it work?',
faq_a1: 'To report a lost cat: tap "I lost my cat" and follow the steps.<br><br>To check whether a cat you spotted is lost: use "I saw a cat". If it matches a reported cat, the owner gets an alert.',
faq_q2: 'Is my information private?',
faq_a2: 'Yes. Your information stays private and is only used to help you find your cat and connect you with finders. No data resale, no marketing use.',
faq_q3: 'Is it free?',
faq_a3: 'Yes, entirely. It\'s a free community project with no advertising.',
faq_q4: 'What technology is behind it?',
faq_a4: 'Allo Chat uses <a href="https://huggingface.co/conservationxlabs/miewid-msv3" target="_blank" rel="noopener noreferrer">MiewID</a>, an AI model for animal re-identification that transforms each photo into a visual "fingerprint". We compare these fingerprints to spot similar-looking cats, then a human always confirms the match. The service runs on the <a href="https://huggingface.co" target="_blank" rel="noopener noreferrer">Hugging Face</a> platform.',
title_home: '🐱 Allochat.org',
title_check: '📷 I saw a cat',
title_report: 'I lost my cat',
title_follow: 'ℹ️ Track my report',
title_poster: '📋 Share a poster',
title_train: '🧠 Help the AI',
title_legal: '⚖️ Legal notice & privacy',
check_privacy: "🔒 We only show cats that look like your photo. The owner's contact stays private — they will reach out to you.",
check_h2: 'Did you come across a cat?',
check_photo_label: 'Photos of the cat — <b>5 to 10 recommended</b>',
check_photo_btn: '📷 Add photos',
check_photo_hint: 'Tip: several angles = a much better match.',
check_gps: '📍 Location: not captured (optional, helps sorting)',
getpos_btn: 'Get my location',
check_btn: '🔍 Check',
lead_title: 'Notify the owner 🐾',
lead_hint: 'Leave YOUR contact info: the owner will reach out to you.',
lead_contact_label: 'Your contact (phone / email)',
lead_msg_label: 'Message (where, when, cat\'s condition)',
lead_msg_placeholder: 'Seen this morning near the park',
lead_send_btn: 'Send to owner',
consent_note: 'By submitting, you accept the <a onclick="show(\'legal\')">legal &amp; privacy notice</a>.',
report_intro: 'Report them lost by adding a few photos (and/or their poster). The AI analyzes the images. As soon as someone photographs a cat they believe is lost and it matches yours, you get an email alert with the info to contact that person.',
report_privacy: '🔒 Your contact stays private. You\'ll receive a <b>secret code</b> to track reports. More photos = better AI recognition.',
report_h2: 'Report my lost cat',
report_poster_label: 'Do you have a poster? (photo or PDF — optional)',
report_poster_btn: '🖼️ Upload poster (auto-fills form)',
report_name_label: "Cat's name",
report_name_placeholder: 'Whiskers',
report_desc_label: 'Description (coat, distinctive signs)',
report_desc_placeholder: 'Tortie, white locket, shy',
report_email_label: 'Your email (used to track reports — never shown to others)',
report_email_placeholder: 'you@example.com',
report_phone_label: 'Your phone (optional, private)',
report_details_label: 'Additional details (optional)',
report_details_placeholder: 'Missing since June 3, Rosemont area, blue collar…',
report_photos_label: 'Cat photos — <b>5 to 10 recommended</b>',
report_photos_btn: '📷 Add cat photos',
report_photo_hint: 'Tip: multiple angles and lighting = much more reliable.',
report_gps: '📍 Last known location: not captured',
report_getpos_btn: 'Get location',
report_btn: 'Report my lost cat',
follow_privacy: "🔒 Secure access: we send you a code by email to verify your identity. You'll then see when you reported your cat, how many times the AI compared it, and reports received.",
follow_h2: 'Track my report',
follow_email_label: 'Your email (the one used to report)',
follow_email_placeholder: 'you@example.com',
follow_request_btn: '📩 Get my code',
spam_note: "No code after a minute? Check your spam folder.",
follow_code_label: 'Code received',
follow_verify_btn: 'View my tracking',
poster_privacy: '📋 Posters are public. By sharing them, you enrich the database and help others find their cat.',
poster_h2: 'Share a found poster',
poster_file_label: 'Poster + photos of the cat (several allowed, up to 10)',
poster_file_btn: '📷 Add poster / photos',
poster_text_label: 'Info visible on the poster (optional)',
poster_text_placeholder: "Name, area, description… (what's written on the poster)",
poster_gps: '📍 Where did you see it? (optional)',
poster_btn: '📋 Add to database',
train_privacy: '🧠 Voluntary help. These photos are used ONLY to train the AI — they are never mixed with private lost cat profiles.',
train_photos_h2: 'Donate photos',
train_photos_hint: "Add cat photos (yours, or cats you've come across) to help the AI recognize better.",
train_photos_btn: '📷 Add photos',
train_consent: 'I agree that these images will be used to <b>train the recognition model</b>. They will not be mixed with lost cat profiles and are kept for <b>at most 2 years</b>.',
train_photos_submit: 'Donate these photos',
train_game_h2: 'Game: same cat or not?',
train_game_hint: "Look at two photos and say if it's the same cat. Each answer helps the AI.",
train_game_start: 'Start playing',
lightbox_hint: 'Tap the image or press ✕ to close',
footer_html: "🐾 Made in Montréal · © 2026 Allo Chat · <a onclick=\"show('legal')\">Legal notice &amp; privacy</a><br>Contact: <a href=\"mailto:info@allochat.org\">info@allochat.org</a>",
legal_html: `<h2>Disclaimer</h2>
<p>Allo Chat is a <b>free</b> community service to help find lost cats in Montréal and beyond.</p>
<p><b>Service provided "as is", without warranty.</b> AI matches are <b>indicative only</b> and must always be confirmed by a human. Allo Chat does not guarantee that any cat will be found. <b>Use is at your own risk.</b></p>
<p><b>Limitation of liability.</b> Allo Chat shall not be liable for any direct or indirect damages resulting from the use of the service, user interactions, false matches, or service interruptions.</p>
<p><b>User conduct.</b> You are responsible for the information you publish. Exercise caution when contacting strangers, and do not approach a fearful or distressed animal.</p>
<h2 style="margin-top:22px">Privacy</h2>
<ul>
<li><b>Privacy officer:</b> Allo Chat team — <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
<li><b>Data collected:</b> cat photos, email (for tracking), phone (optional), description, approximate location (optional).</li>
<li><b>Sole purpose:</b> connect owners with people who spotted their cat. <b>No resale, no advertising, no profiling.</b></li>
<li><b>Privacy by default:</b> no public directory. The finder never sees your contact info — access requires a verification code sent to your email.</li>
<li><b>Right to erasure:</b> permanently delete your profile (photos, reports, contact) at any time via "Track my report".</li>
<li><b>Hosting:</b> servers in the United States, protected by security measures (restricted access, signed photo links, encrypted data in transit).</li>
<li><b>Your rights:</b> access, correction, deletion → <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
</ul>
<p class="hint" style="margin-top:14px">Last updated: June 2026.</p>`,
// JS dynamic strings
js_add_photo: 'Add a photo of the cat',
js_net_err: 'Network error — please retry',
js_rate: ' — too many requests, please wait a minute',
js_analysis_done: '✅ Analysis complete',
js_no_match: 'No match found',
js_match_q: 'Do you recognize any of them?',
js_match_count: n => `${n} cat(s) that look similar`,
js_conf_strong: 'Strong', js_conf_medium: 'Medium', js_conf_weak: 'Weak',
js_its_him: "That's them ✅",
js_view_poster: '🖼️ View poster',
js_no_poster_photo: 'No poster photo available.',
js_cat_default: 'Lost cat',
js_choose_cat_prompt: 'Choose a cat',
js_lead_sent: '<div class="result match"><div class="big">🙏</div><b>Thank you!</b><p class="hint">The owner has received your report.</p></div>',
js_email_required: 'Enter your email (to track reports)',
js_email_short: 'Enter your email',
js_code_invalid: '<p class="hint">Invalid or expired code. Request a new one.</p>',
js_no_cats: '<p class="hint">No cat linked to this email.</p>',
js_logout: 'Logged out 🔒',
js_logout_btn: '🔒 Log out',
js_report_label: 'Report',
js_no_message: '(no message)',
js_no_contact: '(no contact)',
js_no_leads: 'No reports yet.',
js_declared_on: 'reported on',
js_ai_compare: 'AI comparisons 🔎',
js_leads_received: 'report(s) received',
js_my_cat: 'My cat',
js_status: 'status',
js_mark_found: 'Mark as "found" 🎉',
js_delete: '🗑️ Delete permanently',
js_leads_title: n => `Reports received (${n})`,
js_map: '📍 map',
js_delete_confirm: (name) => `Permanently delete the profile for ${name}?\nPhotos, reports and contact will be erased. Irreversible.`,
js_this_profile: 'this profile',
js_deleted_toast: 'Profile deleted 🗑️',
js_deleted_html: '<p class="hint">Profile permanently deleted (right to erasure).</p>',
js_found_toast: 'Marked as found 🎉',
js_add_poster: 'Upload a poster',
js_poster_added: '<div class="result match"><div class="big">🙏</div><b>Thank you!</b><p class="hint">Poster added to the database. If you left your email, a confirmation was sent 📧</p></div>',
check_email_label: 'Your email (to receive a confirmation — optional)',
check_email_placeholder: 'you@example.com',
poster_email_label: 'Your email (to receive a confirmation — optional)',
poster_email_placeholder: 'you@example.com',
js_add_photos: 'Add photos',
js_consent_required: 'Please check the consent box first 🙏',
js_sending: 'Sending…',
js_photos_donated: photos => `Thank you! ${photos} photos in the pool 🙏`,
js_loading: 'Loading…',
js_not_enough_photos: 'Not enough photos yet. Add some above first 🙂',
js_same_cat: '😺 Same cat',
js_diff_cat: '🙀 Different',
js_label_thanks: 'Thank you! 🎓',
js_stats: (photos, labels) => `Pool: ${photos} photos · ${labels} answers given`,
js_poster_reading: 'Reading poster…',
js_poster_loaded: 'Poster uploaded (auto-reading unavailable).',
js_photo_hint_few: n => `${n} photo(s) — add more (aim for 5+) for better recognition.`,
js_photo_hint_ok: n => `${n} photos — good! A few more would be ideal.`,
js_photo_hint_great: n => `${n} photos — excellent ✅`,
js_gps_unavail: 'Geolocation unavailable',
js_gps_refused: 'Location denied',
js_ref_label: 'Reference number',
proc_time_note: 'AI analysis can take up to 1 minute — keep this page open.',
startup_note: 'Starting the server and AI — this can take up to 45 seconds.',
js_detected_label: 'Detected cat',
js_report_success: (name, photos, email, ref, detected) => `<div class="result match" style="margin-top:14px"><b>✅ ${esc(name||'Cat')} reported (${photos} photos)</b>${detected?`<p class="hint">🐱 Detected: <b>${esc(detected)}</b></p>`:''}${ref?`<p class="hint">Tracking number: <b>${esc(ref)}</b></p>`:''}<p class="hint">📧 A confirmation email was sent to ${esc(email)} (check your spam). To view reports: home → "Track my report", enter your email and the code we send. Nothing to memorize 🙂</p></div>`,
js_choose_cat_title: name => `Do you recognize ${name}? Notify the owner 🐾`,
js_prog_embed: name => `🧠 ${name} creating visual fingerprint…`,
js_prog_compare: '🔎 Comparing with registered lost cats…',
js_prog_working: 'The AI is analyzing your photos and comparing them with lost cats. This can take up to a minute — keep this page open.',
js_analysis_timeout: 'Analysis is taking longer than expected. Please try again shortly (the server may have been starting up).',
js_prog_saving: '💾 Securely saving your profile…',
js_date_locale: 'en-CA',
js_num_locale: 'en-CA',
js_code_sent: 'A code has been sent to your email — check your inbox and your spam folder 📬',
cookie_text: 'This site uses helpful local storage (remembering your email) and an <b>audience-measurement tool</b> (visit statistics). <b>No advertising; your information is never sold or used for commercial purposes.</b> <a onclick="show(\'legal\')" style="color:#7fb3ff">Learn more</a>.',
cookie_btn: 'Got it',
}
};
let LANG = localStorage.getItem('chats_lang') || 'fr';
function setLang(lang) {
LANG = lang;
localStorage.setItem('chats_lang', lang);
document.documentElement.lang = lang;
document.title = lang === 'en'
? 'Allochat.org — Community help to find lost cats'
: 'Allochat.org — Entraide communautaire pour retrouver les chats perdus';
const t = T[lang];
document.querySelectorAll('[data-i18n]').forEach(el => {
const k = el.dataset.i18n; if (t[k] !== undefined) el.textContent = t[k];
});
document.querySelectorAll('[data-i18n-html]').forEach(el => {
const k = el.dataset.i18nHtml; if (t[k] !== undefined) el.innerHTML = t[k];
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const k = el.dataset.i18nPlaceholder; if (t[k] !== undefined) el.placeholder = t[k];
});
// Legal section (full innerHTML)
document.getElementById('legal_content').innerHTML = t.legal_html;
// Footer
document.getElementById('footer_el').innerHTML = t.footer_html;
// Lang buttons
document.getElementById('btn_lang_fr').classList.toggle('on', lang === 'fr');
document.getElementById('btn_lang_en').classList.toggle('on', lang === 'en');
// Nav titles
Object.assign(TITLES, {
home: t.title_home, check: t.title_check, report: t.title_report,
follow: t.title_follow, poster: t.title_poster, train: t.title_train, legal: t.title_legal
});
const cur = ['home','check','report','follow','poster','train','legal']
.find(id => !document.getElementById(id).classList.contains('hidden')) || 'home';
document.getElementById('hTitle').textContent = TITLES[cur];
}
// ---- navigation ----
const TITLES = {
home:'🐱 Chats perdus (version Beta)', check:"📷 J'ai vu un chat",
report:"J'ai perdu mon chat", follow:'ℹ️ Suivre mon signalement',
poster:'📋 Partager une affiche', train:"🧠 Aider l'IA",
legal:'⚖️ Avis légal & confidentialité'
};
function show(v){
['home','check','report','follow','poster','train','legal'].forEach(id=>document.getElementById(id).classList.toggle('hidden',id!==v));
document.getElementById('hTitle').textContent=TITLES[v];
document.getElementById('backBtn').style.display = v==='home' ? 'none':'block';
window.scrollTo(0,0);
if(v==='train') loadTrainStats();
}
function goHome(){ show('home'); }
function toast(m){const t=document.getElementById('toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2800);}
function zoom(src){ document.getElementById('lightbox_img').src=src;
document.getElementById('lightbox').classList.add('on'); }
const zimg=url=>`<span class="zwrap"><img src="${encodeURI(url)}" onclick="zoom(this.src)" title="Voir en grand"><span class="zbadge">🔍</span></span>`;
function closeZoom(e){ if(e) e.stopPropagation();
const lb=document.getElementById('lightbox'); lb.classList.remove('on');
document.getElementById('lightbox_img').removeAttribute('src'); }
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeZoom(); });
const val=id=>document.getElementById(id).value;
function getPos(p){
if(!navigator.geolocation) return toast(T[LANG].js_gps_unavail);
navigator.geolocation.getCurrentPosition(g=>{pos[p]={lat:g.coords.latitude,lng:g.coords.longitude};
document.getElementById(p+'_gps').textContent=`📍 ${g.coords.latitude.toFixed(5)}, ${g.coords.longitude.toFixed(5)}`;
},()=>toast(T[LANG].js_gps_refused));
}
function prev(inputId,boxId){
const box=document.getElementById(boxId); box.innerHTML='';
for(const f of document.getElementById(inputId).files){
if(!f.type.startsWith('image/')){ box.innerHTML+=`<div class="thumbs" style="font-size:13px;color:var(--muted)">📄 ${f.name}</div>`; continue; }
const img=document.createElement('img'); img.src=URL.createObjectURL(f); box.appendChild(img);
}
}
let AI_NAME = LANG === 'en' ? 'the AI' : "l'IA";
fetch(API+'/health').then(r=>r.json()).then(h=>{
const e=(h.embedder||'');
AI_NAME = e.includes('miewid') ? 'MiewID' : e.includes('megadescriptor') ? 'MegaDescriptor' : (LANG==='en'?'the AI':"l'IA");
}).catch(()=>{});
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
function progress(id){
const box=document.getElementById(id), fill=document.getElementById(id+'_fill'),
txt=document.getElementById(id+'_txt');
let timer=null, finished=false;
const set=(p,t)=>{ fill.style.width=Math.min(p,98)+'%'; if(t) txt.textContent=t; };
return {
show(){ finished=false; box.classList.add('on'); set(2, LANG==='en'?'Preparing…':'Préparation…'); },
set,
creep(target,t){ if(finished) return; if(t) txt.textContent=t;
clearInterval(timer);
timer=setInterval(()=>{ if(finished) return clearInterval(timer);
const cur=parseFloat(fill.style.width)||0;
set(Math.min(cur+(target-cur)*0.07+0.15, target)); },220);
},
// Phase « IA au travail » : avance LENTEMENT vers 95 % sur ~1 min, ne
// recule JAMAIS. Message honnête (jusqu'à une minute), pas de faux %.
work(t){ finished=false; if(t) txt.textContent=t;
clearInterval(timer);
timer=setInterval(()=>{ if(finished) return clearInterval(timer);
const cur=parseFloat(fill.style.width)||0;
set(Math.min(cur+(95-cur)*0.015+0.25, 95)); },500);
},
done(msg){ finished=true; clearInterval(timer); fill.style.width='100%';
if(msg) txt.textContent=msg;
setTimeout(()=>{ box.classList.remove('on'); fill.style.width='0%'; },900); },
fail(msg){ finished=true; clearInterval(timer); txt.textContent=msg||'Erreur';
setTimeout(()=>box.classList.remove('on'),2500); }
};
}
function postWithProgress(url, fd, prog, stages){
return new Promise((resolve,reject)=>{
const xhr=new XMLHttpRequest();
xhr.open('POST', url);
xhr.upload.onprogress=e=>{ if(e.lengthComputable){
const pct=e.loaded/e.total;
prog.set(2+Math.round(pct*38), (LANG==='en'?'📤 Sending… ':'📤 Envoi… ')+Math.round(pct*100)+'%'); } };
xhr.upload.onload=()=>{ prog.creep(72, stages[0]);
if(stages[1]) setTimeout(()=>prog.creep(93, stages[1]), 2600); };
xhr.onload=()=>resolve({ok:xhr.status>=200&&xhr.status<300, status:xhr.status,
json:()=>JSON.parse(xhr.responseText)});
xhr.onerror=()=>reject(new Error('réseau'));
xhr.send(fd);
});
}
// ---------- 1. VÉRIFIER ----------
async function doCheck(){
const files=document.getElementById('c_photo').files;
if(!files.length) return toast(T[LANG].js_add_photo);
const fd=new FormData(); for(const f of files) fd.append('photos',f);
const cem=val('c_email').trim(); if(cem) fd.append('reporter_email',cem);
if(pos.c){fd.append('lat',pos.c.lat);fd.append('lng',pos.c.lng);}
const prog=progress('c_prog'); prog.show();
const tl=T[LANG];
let r;
try{
r=await postWithProgress(API+'/sightings', fd, prog, [tl.js_prog_received]);
}catch(e){ return prog.fail(tl.js_net_err); }
if(!r.ok){ return prog.fail('Erreur '+r.status+(r.status===429?tl.js_rate:'')); }
const a=r.json();
// ASYNCHRONE : le serveur a juste STOCKÉ les photos (rapide) ; l'IA tourne
// maintenant en arrière-plan. On sonde le résultat en affichant un état
// HONNÊTE (« jusqu'à une minute »), sans faux pourcentage qui oscille.
let d=a;
if(a.result_token && a.status==='analyzing'){
prog.work(tl.js_prog_working);
d=null;
for(let i=0;i<45;i++){ // ~90 s max
await sleep(2000);
let rr;
try{ rr=await fetch(`${API}/sightings/${a.sighting_id}/result?token=${encodeURIComponent(a.result_token)}`); }
catch(e){ continue; }
if(rr.ok){ const j=await rr.json(); if(j.status!=='analyzing'){ d=j; break; } }
}
if(!d){ return prog.fail(tl.js_analysis_timeout); }
}
prog.done(tl.js_analysis_done);
lastSightingId=d.sighting_id; lastMatches=d.matches||[];
renderSightingResult(d, tl);
}
function renderSightingResult(d, tl){
const detline=d.detected?`<div class="privacy" style="background:#eef4ff;color:#274690;border-color:#cfe0ff">🐱 ${tl.js_detected_label} : <b>${esc(d.detected)}</b></div>`:'';
const refline=d.ref?`<p class="hint">${tl.js_ref_label} : <b>${esc(d.ref)}</b></p>`:'';
const box=document.getElementById('c_result'); document.getElementById('c_leadbox').classList.add('hidden');
if(!d.match_found||!(d.matches&&d.matches.length)){
box.innerHTML=`${detline}<div class="result none"><div class="big">🔍</div><b>${tl.js_no_match}</b>
<p class="hint">${esc(d.message)}</p>${refline}</div>`; return;
}
box.innerHTML=detline+`<div class="card"><b>${tl.js_match_count(d.matches.length)}</b>
<p class="hint">${tl.js_match_q}</p>${refline}`+
d.matches.map(m=>{
const pct=Math.round(m.confidence*100);
const c=pct>=70?[tl.js_conf_strong,'var(--ok)']:pct>=40?[tl.js_conf_medium,'var(--warn)']:[tl.js_conf_weak,'var(--bad)'];
const ph=(m.photos[0]&&m.photos[0].url)?API+m.photos[0].url:'';
const actionBtn = m.is_poster
? `<button class="btn btn-primary" style="margin-top:8px" onclick="viewPoster('${m.candidate_token}')">${tl.js_view_poster}</button>`
: `<button class="btn btn-primary" style="margin-top:8px" onclick="chooseCat('${m.candidate_token}')">${tl.js_its_him}</button>`;
return `<div class="candidate">${ph?zimg(ph):''}<div style="flex:1">
<b>${esc(m.name||tl.js_cat_default)}</b> <span class="pill" style="background:${c[1]}">${c[0]} ${pct}%</span><br>
<span class="hint">${esc(m.description||'')}</span><br>
${actionBtn}
</div></div>`;
}).join('')+`</div>`;
}
function chooseCat(id){ chosenCandidateToken=id;
const m=lastMatches.find(x=>x.candidate_token===id);
const name=(m&&m.name)?m.name:(LANG==='en'?'this cat':'ce chat');
document.getElementById('c_leadtitle').textContent=T[LANG].js_choose_cat_title(name);
const b=document.getElementById('c_leadbox'); b.classList.remove('hidden'); b.scrollIntoView({behavior:'smooth'});
}
function viewPoster(token){
const m=lastMatches.find(x=>x.candidate_token===token); if(!m) return;
const ph=m.photos&&m.photos[0]&&m.photos[0].url ? API+m.photos[0].url : null;
if(ph) zoom(ph);
else toast(T[LANG].js_no_poster_photo);
}
async function sendLead(){
if(!chosenCandidateToken) return toast(T[LANG].js_choose_cat_prompt);
const r=await fetch(`${API}/sightings/${lastSightingId}/lead`,{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({candidate_token:chosenCandidateToken,finder_contact:val('c_contact'),message:val('c_msg')})});
if(!r.ok) return toast('Erreur '+r.status);
const ld=await r.json().catch(()=>({}));
const lref=ld.ref?`<p class="hint">${T[LANG].js_ref_label} : <b>${esc(ld.ref)}</b></p>`:'';
document.getElementById('c_leadbox').innerHTML=T[LANG].js_lead_sent+lref;
}
// ---------- 2. SIGNALER ----------
async function posterPrefill(inputId){
const f=document.getElementById(inputId).files[0]; if(!f) return;
const note=document.getElementById('r_poster_note');
note.textContent=T[LANG].js_poster_reading;
const fd=new FormData(); fd.append('file',f);
try{
const r=await fetch(API+'/posters',{method:'POST',body:fd}); const d=await r.json();
if(d.suggested_name) document.getElementById('r_name').value=d.suggested_name;
const phEl=document.getElementById('r_phone');
if(d.suggested_contact&&phEl) phEl.value=d.suggested_contact;
if(d.suggested_description) document.getElementById('r_desc').value=d.suggested_description;
note.textContent=d.note||T[LANG].js_poster_loaded;
}catch(e){ note.textContent=T[LANG].js_poster_loaded; }
}
function photoHint(){
const n=document.getElementById('r_photos').files.length, h=document.getElementById('r_hint');
const tl=T[LANG];
if(!n){h.style.color='';h.textContent=tl.report_photo_hint;return;}
if(n<3){h.style.color='var(--bad)';h.textContent=tl.js_photo_hint_few(n);}
else if(n<5){h.style.color='var(--warn)';h.textContent=tl.js_photo_hint_ok(n);}
else{h.style.color='var(--ok)';h.textContent=tl.js_photo_hint_great(n);}
}
async function doReport(){
const photos=document.getElementById('r_photos').files;
if(!photos.length) return toast(T[LANG].js_add_photo);
const email=val('r_email').trim();
if(!email||!email.includes('@')) return toast(T[LANG].js_email_required);
const fd=new FormData();
fd.append('name',val('r_name'));
const desc=[val('r_desc'),val('r_details')].filter(Boolean).join(' — ');
fd.append('description',desc); fd.append('email',email); fd.append('phone',val('r_phone'));
if(pos.r){fd.append('last_seen_lat',pos.r.lat);fd.append('last_seen_lng',pos.r.lng);}
for(const f of photos) fd.append('photos',f);
const prog=progress('r_prog'); prog.show();
const tl=T[LANG];
let r;
try{
// Déclaration ASYNCHRONE : le serveur enregistre vite et répond tout de
// suite (jeton + courriel) ; l'IA indexe les photos en arrière-plan.
r=await postWithProgress(API+'/cats', fd, prog, [tl.js_prog_saving]);
}catch(e){ return prog.fail(tl.js_net_err); }
if(!r.ok){ return prog.fail('Erreur '+r.status+(r.status===429?tl.js_rate:'')); }
prog.done(tl.js_analysis_done);
const d=r.json(); localStorage.setItem('chats_email',email);
document.getElementById('r_track_email').value=email;
document.getElementById('r_token').innerHTML=tl.js_report_success(d.name, d.photos_count, email, d.ref, d.detected);
document.getElementById('r_token').scrollIntoView({behavior:'smooth'});
}
async function requestCode(){
const email=val('r_track_email').trim();
if(!email||!email.includes('@')) return toast(T[LANG].js_email_short);
const fd=new FormData(); fd.append('email',email);
const r=await fetch(API+'/cats/request-code',{method:'POST',body:fd});
if(!r.ok) return toast('Erreur '+r.status);
const d=await r.json();
document.getElementById('r_code_box').classList.remove('hidden');
if(d.dev_code){
document.getElementById('r_track_code').value=d.dev_code;
toast('Demo: '+(LANG==='en'?'code pre-filled':'code pré-rempli')+' ('+d.dev_code+')');
} else toast(T[LANG].js_code_sent);
}
async function verifyCode(){
const email=val('r_track_email').trim(), code=val('r_track_code').trim();
if(!code) return toast(T[LANG].follow_code_label);
const fd=new FormData(); fd.append('email',email); fd.append('code',code);
const r=await fetch(API+'/cats/verify-code',{method:'POST',body:fd});
const box=document.getElementById('r_track');
if(!r.ok){ box.innerHTML=T[LANG].js_code_invalid; return; }
const acc=await r.json(); ownerCats=acc.cats||[];
if(!acc.cats.length){ box.innerHTML=T[LANG].js_no_cats; return; }
box.innerHTML=`<button class="btn btn-ghost" style="margin-top:0" onclick="followLogout()">${T[LANG].js_logout_btn}</button>`
+ acc.cats.map(renderOwnerCat).join('');
}
function followLogout(){
document.getElementById('r_track').innerHTML='';
document.getElementById('r_track_code').value='';
document.getElementById('r_code_box').classList.add('hidden');
document.getElementById('r_track_email').value='';
localStorage.removeItem('chats_email');
toast(T[LANG].js_logout); show('home');
}
function renderOwnerCat(c){
const tl=T[LANG];
const leads=c.leads.map(l=>{
const pct=l.confidence?Math.round(l.confidence*100):null;
const img=l.sighting_photo_url?zimg(API+l.sighting_photo_url):'';
const loc=(l.sighting_lat!=null)?`<a target="_blank" rel="noopener noreferrer" href="https://maps.google.com/?q=${Number(l.sighting_lat)},${Number(l.sighting_lng)}">${tl.js_map}</a>`:'';
return `<div class="lead">${img}<div style="flex:1"><b>${tl.js_report_label}</b>${pct?` · ${pct}%`:''}<br>
<span class="hint">${esc(l.message||tl.js_no_message)}</span><br>
<span class="hint">📞 ${esc(l.finder_contact||tl.js_no_contact)} · ${loc}</span></div></div>`;
}).join('')||`<p class="hint">${tl.js_no_leads}</p>`;
const declared = c.cat.last_seen_at || c.cat.created_at;
const dDate = declared ? new Date(declared).toLocaleDateString(tl.js_date_locale,{day:'numeric',month:'long',year:'numeric'}) : '?';
const stats = `<div class="follow-stats">
<div class="fs"><b>${dDate}</b><span>${tl.js_declared_on}</span></div>
<div class="fs"><b>${(c.comparisons??0).toLocaleString(tl.js_num_locale)}</b><span>${tl.js_ai_compare}</span></div>
<div class="fs"><b>${c.leads.length}</b><span>${tl.js_leads_received}</span></div>
</div>`;
return `<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:12px">
<b>${esc(c.cat.name||tl.js_my_cat)}</b> — ${tl.js_status} : ${esc(c.cat.status)}
${stats}
<button class="btn btn-ghost" onclick="reunited('${encodeURIComponent(c.claim_token)}')">${tl.js_mark_found}</button>
<button class="btn btn-ghost" style="color:var(--bad);border-color:var(--bad)"
onclick="deleteCat('${encodeURIComponent(c.claim_token)}')">${tl.js_delete}</button>
<h2 style="margin-top:12px">${tl.js_leads_title(c.leads.length)}</h2>${leads}</div>`;
}
async function deleteCat(tok){
const c=ownerCats.find(x=>x.claim_token===tok);
const name=(c&&c.cat&&c.cat.name)?c.cat.name:T[LANG].js_this_profile;
if(!confirm(T[LANG].js_delete_confirm(name))) return;
const fd=new FormData(); fd.append('token',tok);
const r=await fetch(API+'/cats/track/delete',{method:'POST',body:fd});
if(!r.ok) return toast('Erreur '+r.status);
toast(T[LANG].js_deleted_toast);
document.getElementById('r_track').innerHTML=T[LANG].js_deleted_html;
}
async function reunited(tok){
const fd=new FormData();fd.append('token',tok);fd.append('status','reunited');
const r=await fetch(API+'/cats/track/status',{method:'POST',body:fd});
if(!r.ok) return toast('Erreur');
const c=await r.json(); toast(T[LANG].js_found_toast);
document.getElementById('r_track').innerHTML=renderOwnerCat(c);
}
// ---------- 3. PARTAGER UNE AFFICHE ----------
async function doPoster(){
const files=document.getElementById('p_file').files;
if(!files.length) return toast(T[LANG].js_add_poster);
// OCR best-effort sur le 1er fichier (affiche).
const fd0=new FormData(); fd0.append('file',files[0]);
try{ await fetch(API+'/posters',{method:'POST',body:fd0}); }catch(e){}
const fd=new FormData(); fd.append('name', LANG==='en'?'Shared poster':'Affiche partagée'); fd.append('source','poster');
fd.append('description',val('p_text'));
const pem=val('p_email').trim(); if(pem) fd.append('email',pem);
if(pos.p){fd.append('last_seen_lat',pos.p.lat);fd.append('last_seen_lng',pos.p.lng);}
for(const f of files){ if(f.type.startsWith('image/')) fd.append('photos',f); }
const r=await fetch(API+'/cats',{method:'POST',body:fd});
const box=document.getElementById('p_result');
if(r.ok){ const pd=await r.json().catch(()=>({}));
const pref=pd.ref?`<p class="hint">${T[LANG].js_ref_label} : <b>${esc(pd.ref)}</b></p>`:'';
box.innerHTML=T[LANG].js_poster_added+pref;
document.getElementById('p_file').value=''; document.getElementById('p_prev').innerHTML=''; document.getElementById('p_text').value='';
} else box.innerHTML='<p class="hint">Erreur '+r.status+'.</p>';
}
// ---------- 4. ENTRAÎNER ----------
async function doContribute(){
const photos=document.getElementById('t_photos').files;
if(!photos.length) return toast(T[LANG].js_add_photos);
if(!document.getElementById('t_consent').checked) return toast(T[LANG].js_consent_required);
const fd=new FormData(); fd.append('consent','1');
for(const f of photos) fd.append('photos',f);
toast(T[LANG].js_sending);
const r=await fetch(API+'/training/photos',{method:'POST',body:fd});
if(!r.ok) return toast('Erreur '+r.status);
const d=await r.json(); toast(T[LANG].js_photos_donated(d.photos));
document.getElementById('t_photos').value=''; document.getElementById('t_prev').innerHTML=''; loadTrainStats();
}
async function nextPair(){
const g=document.getElementById('game'); g.innerHTML=`<p class="hint">${T[LANG].js_loading}</p>`;
const r=await fetch(API+'/training/pair');
if(!r.ok){ g.innerHTML=`<p class="hint">${T[LANG].js_not_enough_photos}</p>`; return; }
gamePair=await r.json();
g.innerHTML=`<div class="game-pair"><img src="${encodeURI(API+gamePair.a.url)}" onclick="zoom(this.src)"><img src="${encodeURI(API+gamePair.b.url)}" onclick="zoom(this.src)"></div>
<div class="seg" style="margin-top:12px">
<button onclick="sendLabel('same')">${T[LANG].js_same_cat}</button>
<button onclick="sendLabel('different')">${T[LANG].js_diff_cat}</button>
</div>`;
}
async function sendLabel(v){
if(!gamePair) return;
await fetch(API+'/training/label',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({photo_a_id:gamePair.a.id,photo_b_id:gamePair.b.id,verdict:v})});
toast(T[LANG].js_label_thanks); loadTrainStats(); nextPair();
}
async function loadTrainStats(){
try{const d=await (await fetch(API+'/training/stats')).json();
document.getElementById('t_stats').textContent=T[LANG].js_stats(d.photos, d.labels);}catch(e){}
}
// Bannière de consentement aux témoins (Loi 25)
function acceptCookies(){
try{ localStorage.setItem('chats_cookie_ack','1'); }catch(e){}
document.getElementById('cookie_banner').style.display='none';
}
function initCookieBanner(){
let ack=null; try{ ack=localStorage.getItem('chats_cookie_ack'); }catch(e){}
if(!ack) document.getElementById('cookie_banner').style.display='block';
}
// Bannière « serveur en démarrage » : tant que l'IA n'est pas chargée (cold
// start), on affiche une barre et on re-vérifie. Disparaît dès que prêt.
// État des systèmes (accueil) : voyants ok/starting/down, sans donnée sensible.
function setStat(key, state){
const tl=T[LANG];
const dot=document.getElementById('dot_'+key), val=document.getElementById('val_'+key);
if(!dot||!val) return;
let cls='down', label=tl.status_down;
if(state==='ok'||state==='ready'){ cls='ok'; label=tl.status_ok; }
else if(state==='starting'){ cls='warn'; label=tl.status_starting; }
dot.className='stat-dot '+cls; val.textContent=label;
}
function pollStatus(){
fetch(API+'/status',{cache:'no-store'}).then(r=>r.json()).then(s=>{
setStat('app', s.app); setStat('ai', s.ai); setStat('db', s.database); setStat('mail', s.email);
}).catch(()=>{ ['app','ai','db','mail'].forEach(k=>setStat(k,'down')); });
}
function checkServerReady(){
fetch(API+'/ready',{cache:'no-store'})
.then(r=>r.json())
.then(d=>{
const el=document.getElementById('startup');
if(d && d.ready){ el.style.display='none'; }
else { el.style.display='block'; setTimeout(checkServerReady, 3000); }
})
.catch(()=>{ document.getElementById('startup').style.display='block'; setTimeout(checkServerReady, 4000); });
}
// Init
setLang(LANG);
initCookieBanner();
checkServerReady();
pollStatus();
const savedEmail=localStorage.getItem('chats_email'); if(savedEmail) document.getElementById('r_track_email').value=savedEmail;
</script>
<!-- Default Statcounter code for Allochat.org
https://ddooooodododoodoooooo-allo-chat.hf.space -->
<script type="text/javascript">
var sc_project=13306134;
var sc_invisible=1;
var sc_security="359b17f9";
</script>
<script type="text/javascript"
src="https://www.statcounter.com/counter/counter.js"
async></script>
<noscript><div class="statcounter"><a title="Web Analytics"
href="https://statcounter.com/" target="_blank"><img
class="statcounter"
src="https://c.statcounter.com/13306134/0/359b17f9/1/"
alt="Web Analytics"
referrerPolicy="no-referrer-when-downgrade"></a></div></noscript>
<!-- End of Statcounter Code -->
</body>
</html>