allocate / web /index.html
studio-d-o's picture
Upload folder using huggingface_hub
b19e5ac verified
Raw
History Blame Contribute Delete
104 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<!-- ============ SEO ============ -->
<title>Chat perdu à Montréal ? Allochat.org — retrouvez-le avec l'IA</title>
<meta name="description" content="Service gratuit d'entraide pour retrouver les chats perdus à Montréal et au Québec. Déclarez votre chat perdu avec ses photos : l'IA de reconnaissance compare les signalements et vous alerte par courriel si quelqu'un le croise. Vos coordonnées restent privées." />
<link rel="canonical" href="https://allochat.org/" />
<meta name="robots" content="index, follow, max-image-preview:large" />
<meta name="author" content="Allo Chat" />
<meta name="theme-color" content="#ff5a3c" />
<!-- Référencement local (Montréal / Québec) -->
<meta name="geo.region" content="CA-QC" />
<meta name="geo.placename" content="Montréal, Québec, Canada" />
<meta name="geo.position" content="45.5019;-73.5674" />
<meta name="ICBM" content="45.5019, -73.5674" />
<!-- Partage social -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Allochat.org" />
<meta property="og:url" content="https://allochat.org/" />
<meta property="og:title" content="Chat perdu à Montréal ? Retrouvez-le avec l'IA — Allochat.org" />
<meta property="og:description" content="Entraide communautaire gratuite pour retrouver les chats perdus. La reconnaissance visuelle par IA compare les photos, vous êtes alerté par courriel. Vos coordonnées restent privées." />
<meta property="og:locale" content="fr_CA" />
<meta property="og:locale:alternate" content="en_CA" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Chat perdu à Montréal ? Retrouvez-le avec l'IA — Allochat.org" />
<meta name="twitter:description" content="Entraide communautaire gratuite pour retrouver les chats perdus, avec reconnaissance visuelle par IA." />
<!-- Icône (SVG inline : aucun fichier externe, aucune requête réseau) -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%90%B1%3C/text%3E%3C/svg%3E" />
<link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='.9em' font-size='90'%3E%F0%9F%90%B1%3C/text%3E%3C/svg%3E" />
<!-- Données structurées : aident Google (résultats enrichis) ET les assistants IA
à comprendre/citer le service. Uniquement des faits vérifiables — aucune note
ni avis inventés. -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "WebApplication",
"@id": "https://allochat.org/#app",
"name": "Allochat.org",
"alternateName": "Allo Chat",
"url": "https://allochat.org/",
"description": "Service communautaire gratuit pour retrouver les chats perdus. Les propriétaires déclarent leur chat avec des photos ; l'intelligence artificielle de ré-identification compare les photos envoyées par les passants et alerte le propriétaire par courriel en cas de ressemblance. Chaque correspondance est confirmée par un humain.",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "Web (tout navigateur)",
"browserRequirements": "Navigateur moderne avec JavaScript",
"inLanguage": ["fr-CA", "en-CA"],
"isAccessibleForFree": true,
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "CAD" },
"featureList": [
"Déclarer un chat perdu avec plusieurs photos",
"Signaler un chat croisé dans la rue",
"Reconnaissance visuelle par IA (MiewID) avec confirmation humaine",
"Alerte par courriel au propriétaire",
"Partage d'affiches de chats perdus",
"Confidentialité : coordonnées jamais montrées aux autres utilisateurs"
],
"areaServed": {
"@type": "City",
"name": "Montréal",
"containedInPlace": { "@type": "AdministrativeArea", "name": "Québec, Canada" }
},
"publisher": { "@id": "https://allochat.org/#org" }
},
{
"@type": "Organization",
"@id": "https://allochat.org/#org",
"name": "Allo Chat",
"url": "https://allochat.org/",
"email": "info@allochat.org",
"foundingLocation": { "@type": "Place", "name": "Montréal, Québec, Canada" },
"contactPoint": {
"@type": "ContactPoint",
"email": "info@allochat.org",
"contactType": "Support et vie privée",
"availableLanguage": ["French", "English"]
}
},
{
"@type": "FAQPage",
"@id": "https://allochat.org/#faq",
"mainEntity": [
{
"@type": "Question",
"name": "Comment fonctionne Allochat.org pour retrouver un chat perdu ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Le propriétaire déclare son chat perdu en ajoutant plusieurs photos. Quand une personne croise un chat qui semble perdu, elle le prend en photo : l'IA compare cette photo aux chats déclarés. Si la ressemblance dépasse 50 %, les candidats sont proposés pour confirmation visuelle, puis le propriétaire reçoit une alerte par courriel."
}
},
{
"@type": "Question",
"name": "Mes informations personnelles restent-elles privées ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui. Il n'existe aucun annuaire public : la personne qui signale un chat ne voit jamais vos coordonnées. L'accès à vos données exige un code de vérification envoyé à votre courriel. Aucune revente de données, aucune publicité. Vous pouvez supprimer définitivement votre fiche à tout moment."
}
},
{
"@type": "Question",
"name": "Allochat.org est-il gratuit ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Oui, le service est entièrement gratuit et sans publicité. C'est un projet d'entraide communautaire créé à Montréal."
}
},
{
"@type": "Question",
"name": "Quelle technologie d'intelligence artificielle est utilisée ?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Allochat.org utilise MiewID, un modèle d'IA de ré-identification animale qui transforme chaque photo en une empreinte visuelle. Les empreintes sont comparées pour repérer les chats qui se ressemblent, puis un humain confirme toujours la correspondance. Plusieurs photos par chat améliorent nettement la fiabilité."
}
}
]
}
]
}
</script>
<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}
/* En-tête : titre CENTRÉ, retour et langues en absolu pour ne pas décaler le centre */
header{position:sticky;top:0;z-index:10;background:var(--accent);color:#fff;
padding:12px 16px;box-shadow:var(--shadow);text-align:center}
header .back{position:absolute;left:12px;top:50%;transform:translateY(-50%);
background:rgba(255,255,255,.2);border:none;color:#fff;font-size:20px;width:40px;height:40px;
border-radius:10px;display:none}
.brand{padding:0 56px}
header h1{font-size:18px;margin:0;font-weight:800;letter-spacing:.2px;line-height:1.3}
.tagline{font-size:12.5px;line-height:1.4;margin-top:4px;color:rgba(255,255,255,.93);font-weight:500}
/* Réassurance : plus discrète que l'accroche (hiérarchie visuelle) */
.tagline2{font-size:11.5px;line-height:1.35;margin-top:3px;color:rgba(255,255,255,.78);font-weight:500}
.lang-toggle{position:absolute;right:12px;top:50%;transform:translateY(-50%);display:flex;gap:5px}
.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) — icône + titre + 1 ligne, lisible d'un coup d'œil */
.action.hero{padding:20px 18px;align-items:center}
.action.hero .ico{font-size:36px;width:50px}
.action .txt{flex:1;min-width:0}
.action.hero .t{font-size:19px;line-height:1.25}
.action.hero .d{margin-top:4px;font-size:14px;line-height:1.4;color:var(--muted)}
.action .chev{font-size:26px;color:#b9c6d3;flex:none;line-height:1}
.hero-lost{border:2px solid #ffc4b3;background:#fff8f5}
.hero-seen{border:2px solid #aee3cc;background:#f5fbf8}
.hero-follow{border:2px solid #c4cbff;background:#f6f7ff}
.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)}
/* Actions communautaires (secondaires) — libellé seul, discret */
.secondary{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.action.mini{padding:14px 12px;box-shadow:none;background:#fbfdff;gap:9px}
.action.mini .ico{font-size:20px;width:26px}
.action.mini .t{font-size:14px;font-weight:700}
/* Options repliées dans les formulaires : disponibles sans encombrer */
.opt{border-top:1px solid var(--line);margin-top:16px;padding-top:4px}
.opt summary{list-style:none;cursor:pointer;font-size:14px;font-weight:600;color:var(--accent2);
padding:10px 2px;display:flex;gap:8px;align-items:center}
.opt summary::-webkit-details-marker{display:none}
.opt summary::before{content:'+';font-weight:800;color:var(--muted)}
.opt[open] summary::before{content:'–'}
.opt-tag{font-weight:500;font-size:12.5px;color:var(--muted)}
/* 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)}
/* Bouton « en cours » : montre que le serveur travaille + bloque le double-envoi */
.btn:disabled{opacity:.65;cursor:progress;filter:none}
.btn.working{position:relative}
.btn.working::after{content:'';display:inline-block;width:14px;height:14px;margin-left:9px;
vertical-align:-2px;border:2.5px solid rgba(255,255,255,.45);border-top-color:#fff;
border-radius:50%;animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.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:10px;margin-top:10px;margin-bottom:10px}
.thumbs img{width:72px;height:72px;object-fit:cover;border-radius:12px;border:2px solid #cbd5e1;box-shadow:0 2px 6px rgba(0,0,0,0.08)}
.thumb-wrap{position:relative;display:inline-block;line-height:0}
.thumb-wrap .thumb-doc{display:inline-flex;align-items:center;height:72px;padding:0 12px;font-size:13px;font-weight:600;
line-height:1.3;color:var(--ink);background:#f8fafc;border:2px dashed #cbd5e1;border-radius:12px}
.thumb-x{position:absolute;top:-6px;right:-6px;width:24px;height:24px;border:2px solid #fff;border-radius:50%;
background:#e11d48;color:#fff;font-size:13px;font-weight:bold;line-height:1;cursor:pointer;padding:0;
display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.25);z-index:5}
.thumb-x:hover{background:#be123c;transform:scale(1.08)}
.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}
.match-legend{font-size:12.5px;color:var(--muted);background:#f1f4f7;border:1px solid var(--line);
border-radius:10px;padding:8px 11px;margin:8px 0 0;line-height:1.45}
.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}
/* Bannière « serveur en démarrage » */
.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()" aria-label="Retour"></button>
<div class="brand">
<h1 id="hTitle">🐱 Allochat.org — Beta</h1>
<div class="tagline" id="tagline" data-i18n="tagline">Entraide communautaire pour retrouver les chats perdus par reconnaissance visuelle (IA).</div>
<div class="tagline2" id="tagline2" data-i18n="tagline2">Gratuit, sans publicité, vos données restent privées.</div>
</div>
<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">
<div class="home">
<button class="action hero hero-lost" onclick="show('report')">
<div class="ico">🚨</div>
<div class="txt">
<div class="t" data-i18n="hero_lost_title">J'ai perdu mon chat</div>
<div class="d" data-i18n="hero_lost_desc">Déclarez-le avec ses photos — on vous alerte si quelqu'un le croise.</div>
</div>
<div class="chev"></div>
</button>
<button class="action hero hero-seen" onclick="show('check')">
<div class="ico">📷</div>
<div class="txt">
<div class="t" data-i18n="hero_seen_title">J'ai vu un chat</div>
<div class="d" data-i18n="hero_seen_desc">Prenez-le en photo — on vérifie s'il est déclaré perdu.</div>
</div>
<div class="chev"></div>
</button>
<button class="action hero hero-follow" onclick="show('follow')">
<div class="ico">🔎</div>
<div class="txt">
<div class="t" data-i18n="hero_follow_title">Suivre mon signalement</div>
<div class="d" data-i18n="hero_follow_desc">Voir les signalements reçus pour votre chat.</div>
</div>
<div class="chev"></div>
</button>
</div>
<p class="sub" style="margin-top:26px;margin-bottom:8px" data-i18n="community_sub">Aider la communauté :</p>
<div class="secondary">
<button class="action mini" onclick="show('poster')">
<div class="ico">📋</div>
<div class="txt"><div class="t" data-i18n="mini_poster_title">Partager une affiche</div></div>
</button>
<button class="action mini" onclick="show('train')">
<div class="ico">🧠</div>
<div class="txt"><div class="t" data-i18n="mini_train_title">Aider l'IA</div></div>
</button>
</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">🔒 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="check_photo_label">Photos du chat</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">Plusieurs angles = reconnaissance plus fiable.</div>
<details class="opt">
<summary data-i18n="check_opt_sum">Recevoir une confirmation / ajouter ma position</summary>
<label data-i18n="check_email_label">Votre courriel</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</div>
<button class="btn btn-ghost" onclick="getPos('c')" data-i18n="getpos_btn">Capter ma position</button>
</details>
<button class="btn btn-go" id="c_btn" 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="report_privacy">🔒 Vos coordonnées restent privées — jamais montrées aux autres.</div>
<div class="card">
<h2 data-i18n="report_h2">Signaler mon chat perdu</h2>
<p class="sub" data-i18n="report_intro">Ajoutez ses photos. Si quelqu'un le croise, vous recevez une alerte par courriel.</p>
<!-- L'essentiel d'abord : les photos -->
<label data-i18n="report_photos_label">Photos du chat</label>
<label class="photo-btn" for="r_photos" data-i18n="report_photos_btn">📷 Ajouter des photos</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">5 à 10 photos, sous plusieurs angles = bien plus fiable.</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</label>
<textarea id="r_desc" rows="3" data-i18n-placeholder="report_desc_placeholder" placeholder="Tortie, médaillon blanc, timide. Disparu le 3 juin, secteur Rosemont."></textarea>
<label data-i18n="report_email_label">Votre courriel</label>
<input id="r_email" type="email" data-i18n-placeholder="report_email_placeholder" placeholder="vous@exemple.com" />
<div class="hint" data-i18n="report_email_hint">Sert à recevoir les alertes. Jamais montré aux autres.</div>
<label data-i18n-html="report_phone_label">Téléphone <span class="opt-tag">optionnel</span></label>
<input id="r_phone" type="tel" placeholder="514-555-1234" />
<!-- Options replieés : n'encombrent pas l'écran par défaut -->
<details class="opt">
<summary data-i18n="report_poster_sum">J'ai une affiche — la charger remplit le formulaire</summary>
<label class="photo-btn" for="r_poster" data-i18n="report_poster_btn">🖼️ Charger l'affiche</label>
<input class="file-hidden" id="r_poster" type="file" accept="image/*,application/pdf" onchange="prev('r_poster','r_poster_prev'); posterPrefill('r_poster')" />
<div class="thumbs" id="r_poster_prev"></div>
<div class="hint" id="r_poster_note"></div>
</details>
<details class="opt">
<summary data-i18n="report_pos_sum">Ajouter la dernière position connue</summary>
<div class="hint" id="r_gps" data-i18n="report_gps">📍 Non captée</div>
<button class="btn btn-ghost" onclick="getPos('r')" data-i18n="report_getpos_btn">Capter la position</button>
</details>
<button class="btn btn-primary" id="r_btn" 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:6px" 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">🔒 On vous envoie un code par courriel pour vérifier que c'est bien vous.</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" id="p_btn" 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 class="search-prog" id="p_prog"><div class="bar"><div id="p_prog_fill"></div></div>
<div class="lbl"><span class="dot"></span><span id="p_prog_txt"></span></div></div>
<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" id="t_btn" onclick="doContribute()" data-i18n="train_photos_submit">Donner ces photos</button>
<div class="search-prog" id="t_prog"><div class="bar"><div id="t_prog_fill"></div></div>
<div class="lbl"><span class="dot"></span><span id="t_prog_txt"></span></div></div>
</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=[];
// Accumulation multi-photos par input : chaque ré-ouverture du sélecteur
// AJOUTE au tableau au lieu d'écraser. Les fonctions d'envoi lisent
// input.files (réassigné via DataTransfer).
const filesStore={c_photo:[], r_photos:[], r_poster:[], p_file:[], t_photos:[]};
const previewBoxes={c_photo:'c_prev', r_photos:'r_prev', r_poster:'r_poster_prev', p_file:'p_prev', t_photos:'t_prev'};
function fileKey(f){ return f.name+'|'+f.size+'|'+f.lastModified; }
// Réassigne input.files depuis le tableau accumulé (via DataTransfer) pour
// que le reste du code (qui lit input.files) fonctionne inchangé.
function syncInputFiles(inputId){
const dt=new DataTransfer();
for(const f of filesStore[inputId]) dt.items.add(f);
document.getElementById(inputId).files=dt.files;
}
function clearFiles(inputId){
filesStore[inputId]=[];
const inp=document.getElementById(inputId); if(inp) inp.value='';
const boxId=previewBoxes[inputId];
if(boxId){ const box=document.getElementById(boxId); if(box) box.innerHTML=''; }
}
function removePhoto(inputId, idx){
filesStore[inputId].splice(idx,1);
syncInputFiles(inputId);
renderThumbs(inputId);
if(inputId==='r_photos' || inputId==='r_poster') photoHint();
}
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>(
{'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
// ---- TRADUCTIONS / TRANSLATIONS ----
const T = {
fr: {
tagline: "Entraide communautaire pour retrouver les chats perdus par reconnaissance visuelle (IA).",
tagline2: "Gratuit, sans publicité, vos données restent privées.",
hero_lost_title: "J'ai perdu mon chat",
hero_lost_desc: "Déclarez-le avec ses photos — on vous alerte si quelqu'un le croise.",
hero_seen_title: "J'ai vu un chat",
hero_seen_desc: "Prenez-le en photo — on vérifie s'il est déclaré perdu.",
hero_follow_title: 'Suivre mon signalement',
hero_follow_desc: 'Voir les signalements reçus pour votre chat.',
community_sub: 'Aider la communauté :',
mini_poster_title: 'Partager une affiche',
mini_train_title: "Aider l'IA",
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 — Beta',
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: "🔒 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',
check_opt_sum: 'Recevoir une confirmation / ajouter ma position',
check_photo_btn: '📷 Ajouter des photos',
check_photo_hint: 'Plusieurs angles = reconnaissance plus fiable.',
check_gps: '📍 Position : non captée',
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: "Ajoutez ses photos. Si quelqu'un le croise, vous recevez une alerte par courriel.",
report_privacy: "🔒 Vos coordonnées restent privées — jamais montrées aux autres.",
report_h2: 'Signaler mon chat perdu',
report_poster_btn: "🖼️ Charger l'affiche",
report_name_label: 'Nom du chat',
report_name_placeholder: 'Tigrou',
report_desc_label: 'Description',
report_desc_placeholder: 'Tortie, médaillon blanc, timide. Disparu le 3 juin, secteur Rosemont.',
report_email_label: 'Votre courriel',
report_email_hint: 'Sert à recevoir les alertes. Jamais montré aux autres.',
report_poster_sum: "J'ai une affiche — la charger remplit le formulaire",
report_pos_sum: 'Ajouter la dernière position connue',
report_email_placeholder: 'vous@exemple.com',
report_phone_label: 'Téléphone <span class="opt-tag">optionnel</span>',
report_photos_label: 'Photos du chat',
report_photos_btn: '📷 Ajouter des photos',
report_photo_hint: '5 à 10 photos, sous plusieurs angles = bien plus fiable.',
report_gps: '📍 Non captée',
report_getpos_btn: 'Capter la position',
report_btn: 'Déclarer mon chat perdu',
follow_privacy: "🔒 On vous envoie un code par courriel pour vérifier que c'est bien vous.",
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 après une minute ? Vérifiez vos 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_match_legend: '💡 Le % = ressemblance visuelle estimée par l\'IA. Plus il est élevé, plus c\'est probable — mais c\'est à vous de confirmer en regardant les photos 👀',
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',
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_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_received: '🧠 Photos reçues — analyse en cours…',
js_btn_working: '⏳ Envoi en cours…',
js_prog_poster_read: "📤 Envoi de l'affiche — lecture du texte…",
js_prog_poster_save: "🧠 Le serveur analyse l'affiche et crée les empreintes… (jusqu'à 1 min)",
js_prog_train: "🧠 Le serveur reçoit vos photos et crée les empreintes…",
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_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) 📬",
js_code_required: 'Entrez le code reçu par courriel',
js_remove_photo: 'Retirer cette photo',
startup_failed: "Le serveur ne répond pas pour l'instant. Réessayez dans quelques instants.",
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: {
tagline: 'Community help to find lost cats through AI visual recognition.',
tagline2: 'Free, ad-free, your data stays private.',
hero_lost_title: 'I lost my cat',
hero_lost_desc: 'Report it with photos — we alert you if someone spots it.',
hero_seen_title: 'I saw a cat',
hero_seen_desc: 'Take a photo — we check if it was reported lost.',
hero_follow_title: 'Track my report',
hero_follow_desc: 'See the reports received for your cat.',
community_sub: 'Help the community:',
mini_poster_title: 'Share a poster',
mini_train_title: 'Help the AI',
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 — Beta',
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: "🔒 The owner's contact stays private — they reach out to you.",
check_h2: 'Did you come across a cat?',
check_photo_label: 'Photos of the cat',
check_opt_sum: 'Get a confirmation / add my location',
check_photo_btn: '📷 Add photos',
check_photo_hint: 'Several angles = more reliable recognition.',
check_gps: '📍 Location: not captured',
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: 'Add their photos. If someone spots them, you get an email alert.',
report_privacy: '🔒 Your contact details stay private — never shown to others.',
report_h2: 'Report my lost cat',
report_poster_btn: '🖼️ Upload poster',
report_name_label: "Cat's name",
report_name_placeholder: 'Whiskers',
report_desc_label: 'Description',
report_desc_placeholder: 'Tortie, white locket, shy. Missing since June 3, Rosemont area.',
report_email_label: 'Your email',
report_email_hint: 'Used to send you alerts. Never shown to others.',
report_poster_sum: 'I have a poster — uploading it fills the form',
report_pos_sum: 'Add the last known location',
report_email_placeholder: 'you@example.com',
report_phone_label: 'Phone <span class="opt-tag">optional</span>',
report_photos_label: 'Cat photos',
report_photos_btn: '📷 Add photos',
report_photo_hint: '5 to 10 photos from several angles = far more reliable.',
report_gps: '📍 Not captured',
report_getpos_btn: 'Get location',
report_btn: 'Report my lost cat',
follow_privacy: '🔒 We email you a code to verify it is really you.',
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_match_legend: '💡 The % = AI-estimated visual resemblance. Higher means more likely — but it\'s up to you to confirm by looking at the photos 👀',
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',
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_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_received: '🧠 Photos received — analyzing…',
js_btn_working: '⏳ Sending…',
js_prog_poster_read: '📤 Uploading the poster — reading its text…',
js_prog_poster_save: '🧠 The server is analyzing the poster and building fingerprints… (up to 1 min)',
js_prog_train: '🧠 The server is receiving your photos and building fingerprints…',
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 📬',
js_code_required: 'Enter the code you received by email',
js_remove_photo: 'Remove this photo',
startup_failed: "The server isn't responding right now. Please try again in a few moments.",
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 = 'Allochat.org — Beta';
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:'🐱 Allochat.org', 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){
const prev=['home','check','report','follow','poster','train','legal']
.find(id=>!document.getElementById(id).classList.contains('hidden'));
// Nettoyage d'état pour éviter les fuites entre flux : on repart propre à
// chaque changement de section. On préserve le flux 'follow' si on y reste.
const clr=el=>{ if(el) el.innerHTML=''; };
clr(document.getElementById('c_result'));
clr(document.getElementById('p_result'));
clr(document.getElementById('r_token'));
document.getElementById('c_leadbox').classList.add('hidden');
['c_photo','r_photos','r_poster','p_file','t_photos'].forEach(clearFiles);
lastMatches=[]; chosenCandidateToken=null; lastSightingId=null;
// Session de suivi : on ne la ferme que si on QUITTE 'follow' (sinon on
// casserait un utilisateur déjà connecté qui navigue dans son suivi).
if(prev==='follow' && v!=='follow'){
clr(document.getElementById('r_track'));
document.getElementById('r_code_box').classList.add('hidden');
}
['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';
// Le sous-titre du bandeau n'a de sens que sur l'accueil.
document.getElementById('tagline').style.display = v==='home' ? 'block':'none';
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));
}
// onchange : AJOUTE les fichiers nouvellement choisis au tableau accumulé
// (dé-duplication par nom+taille+date), réassigne input.files, puis rerend.
function prev(inputId,boxId){
const inp=document.getElementById(inputId);
const store=filesStore[inputId];
const seen=new Set(store.map(fileKey));
for(const f of inp.files){
const k=fileKey(f);
if(seen.has(k)) continue;
seen.add(k); store.push(f);
}
syncInputFiles(inputId);
renderThumbs(inputId);
}
// Rerend les vignettes depuis le tableau accumulé, avec un bouton ✕ par photo.
function renderThumbs(inputId){
const box=document.getElementById(previewBoxes[inputId]);
if(!box) return;
box.innerHTML='';
filesStore[inputId].forEach((f,idx)=>{
const wrap=document.createElement('span');
wrap.className='thumb-wrap';
if(f.type.startsWith('image/')){
const img=document.createElement('img'); img.src=URL.createObjectURL(f);
wrap.appendChild(img);
} else {
const doc=document.createElement('span');
doc.className='thumb-doc'; doc.textContent='📄 '+esc(f.name);
wrap.appendChild(doc);
}
const rm=document.createElement('button');
rm.type='button'; rm.className='thumb-x'; rm.textContent='✕';
rm.title=T[LANG].js_remove_photo;
rm.setAttribute('aria-label',T[LANG].js_remove_photo);
rm.onclick=()=>removePhoto(inputId, idx);
wrap.appendChild(rm);
box.appendChild(wrap);
});
}
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
// Bouton « en cours » : le serveur peut mettre plusieurs secondes (upload +
// IA). On désactive le bouton, on affiche un spinner et un libellé explicite
// → l'utilisateur voit que ça travaille et ne clique pas deux fois.
function busy(btnId, on, label){
const b=document.getElementById(btnId); if(!b) return;
if(on){
if(b.dataset.lbl===undefined) b.dataset.lbl=b.textContent;
b.disabled=true; b.classList.add('working');
b.textContent=label||T[LANG].js_btn_working;
}else{
b.disabled=false; b.classList.remove('working');
if(b.dataset.lbl!==undefined) b.textContent=b.dataset.lbl;
}
}
// `btnId` (optionnel) : le bouton déclencheur est mis en « occupé » pendant
// toute la durée et restauré par done()/fail() — quel que soit le chemin de
// sortie (succès, erreur réseau, timeout).
function progress(id, btnId){
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'); if(btnId) busy(btnId,true);
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(btnId) busy(btnId,false);
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';
if(btnId) busy(btnId,false);
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=filesStore['c_photo']||[];
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','c_btn'); 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:'')); }
let a;
try{ a=r.json(); }catch(e){ return prog.fail(tl.js_net_err); }
// 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`, {headers:{'X-Result-Token':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);
clearFiles('c_photo');
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>
<p class="match-legend">${tl.js_match_legend}</p>${refline}`+
d.matches.map(m=>{
const pct=Math.round(m.confidence*100);
// Le serveur ne renvoie plus rien sous 50 % (choix produit : pas de faux
// espoirs). Deux bandes suffisent : forte (≥70 %) et moyenne (50-69 %).
const c=pct>=70?[tl.js_conf_strong,'var(--ok)']:[tl.js_conf_medium,'var(--warn)'];
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 posterFiles = filesStore['r_poster'] || [];
const photoFiles = filesStore['r_photos'] || [];
const n = posterFiles.length + photoFiles.length;
const 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 posterFiles = filesStore['r_poster'] || [];
const photoFiles = filesStore['r_photos'] || [];
const allPhotos = [...posterFiles, ...photoFiles];
if(!allPhotos.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');
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 allPhotos){ if(f.type.startsWith('image/')) fd.append('photos',f); }
const prog=progress('r_prog','r_btn'); 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);
let d;
try{ d=r.json(); }catch(e){ return prog.fail(tl.js_net_err); }
localStorage.setItem('chats_email',email);
clearFiles('r_photos'); photoHint();
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].js_code_required);
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=filesStore['p_file']||[];
if(!files.length) return toast(T[LANG].js_add_poster);
const tl=T[LANG];
const box=document.getElementById('p_result'); box.innerHTML='';
// Retour visuel : l'envoi + l'OCR + l'IA prennent plusieurs secondes.
const prog=progress('p_prog','p_btn'); prog.show();
try{
// 1) OCR best-effort sur le 1er fichier (affiche).
prog.creep(45, tl.js_prog_poster_read);
const fd0=new FormData(); fd0.append('file',files[0]);
try{ await fetch(API+'/posters',{method:'POST',body:fd0}); }catch(e){}
// 2) Enregistrement + empreintes IA.
prog.work(tl.js_prog_poster_save);
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});
if(r.ok){
prog.done(tl.js_analysis_done);
const pd=await r.json().catch(()=>({}));
const pref=pd.ref?`<p class="hint">${tl.js_ref_label} : <b>${esc(pd.ref)}</b></p>`:'';
box.innerHTML=tl.js_poster_added+pref;
clearFiles('p_file'); document.getElementById('p_text').value='';
} else {
prog.fail('Erreur '+r.status);
box.innerHTML='<p class="hint">Erreur '+r.status+'.</p>';
}
}catch(e){
prog.fail(tl.js_net_err);
box.innerHTML=`<p class="hint">${esc(tl.js_net_err)}</p>`;
}finally{ busy('p_btn', false); }
}
// ---------- 4. ENTRAÎNER ----------
async function doContribute(){
const photos=filesStore['t_photos']||[];
if(!photos.length) return toast(T[LANG].js_add_photos);
if(!document.getElementById('t_consent').checked) return toast(T[LANG].js_consent_required);
const tl=T[LANG];
const fd=new FormData(); fd.append('consent','1');
for(const f of photos) fd.append('photos',f);
// Retour visuel : l'envoi + les empreintes IA prennent plusieurs secondes.
const prog=progress('t_prog','t_btn'); prog.show();
try{
prog.work(tl.js_prog_train);
const r=await fetch(API+'/training/photos',{method:'POST',body:fd});
if(!r.ok){ prog.fail('Erreur '+r.status); return toast('Erreur '+r.status); }
const d=await r.json();
prog.done(tl.js_analysis_done);
toast(tl.js_photos_donated(d.photos));
clearFiles('t_photos'); loadTrainStats();
}catch(e){
prog.fail(tl.js_net_err); toast(tl.js_net_err);
}finally{ busy('t_btn', false); }
}
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.
let startupTries=0;
const STARTUP_MAX_TRIES=20;
function startupFailed(){
const el=document.getElementById('startup');
el.style.display='block';
const inner=el.querySelector('div');
if(inner) inner.innerHTML=`<span>${esc(T[LANG].startup_failed)}</span>`;
}
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'; return; }
el.style.display='block';
if(++startupTries>=STARTUP_MAX_TRIES){ startupFailed(); return; }
setTimeout(checkServerReady, 3000);
})
.catch(()=>{
document.getElementById('startup').style.display='block';
if(++startupTries>=STARTUP_MAX_TRIES){ startupFailed(); return; }
setTimeout(checkServerReady, 4000);
});
}
// Init
setLang(LANG);
initCookieBanner();
checkServerReady();
const savedEmail=localStorage.getItem('chats_email'); if(savedEmail) document.getElementById('r_track_email').value=savedEmail;
</script>
<!-- Default Statcounter code for Allochat.org
https://studio-d-o-allocate.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>