Spaces:
Sleeping
Sleeping
dave commited on
feat: poster match shows poster only + admin OTP 20 chars + FR/EN toggle
Browse files- backend/app/models.py +3 -0
- backend/app/routers/admin.py +1 -1
- backend/app/routers/cats.py +4 -1
- backend/app/schemas.py +1 -0
- backend/app/serialize.py +1 -0
- backend/migrations/versions/a3f1c8e92d47_add_cat_source.py +28 -0
- web/index.html +585 -193
backend/app/models.py
CHANGED
|
@@ -48,6 +48,9 @@ class Cat(Base):
|
|
| 48 |
# lost | found | reunited
|
| 49 |
status: Mapped[str] = mapped_column(String(20), default="lost")
|
| 50 |
|
|
|
|
|
|
|
|
|
|
| 51 |
# CONFIDENTIALITÉ : code secret remis au propriétaire à la déclaration. Seul
|
| 52 |
# moyen d'accéder à SA fiche et à SES signalements reçus. Jamais exposé ailleurs.
|
| 53 |
claim_token: Mapped[str | None] = mapped_column(
|
|
|
|
| 48 |
# lost | found | reunited
|
| 49 |
status: Mapped[str] = mapped_column(String(20), default="lost")
|
| 50 |
|
| 51 |
+
# owner | poster (affiches partagées par des passants, sans compte propriétaire)
|
| 52 |
+
source: Mapped[str] = mapped_column(String(20), default="owner")
|
| 53 |
+
|
| 54 |
# CONFIDENTIALITÉ : code secret remis au propriétaire à la déclaration. Seul
|
| 55 |
# moyen d'accéder à SA fiche et à SES signalements reçus. Jamais exposé ailleurs.
|
| 56 |
claim_token: Mapped[str | None] = mapped_column(
|
backend/app/routers/admin.py
CHANGED
|
@@ -52,7 +52,7 @@ def admin_request_code(db: Session = Depends(get_db)):
|
|
| 52 |
raise HTTPException(403, "Admin désactivé (CHATS_ADMIN_KEY non configuré)")
|
| 53 |
for old in db.scalars(select(models.EmailOTP).where(models.EmailOTP.email == _OTP_KEY)):
|
| 54 |
db.delete(old)
|
| 55 |
-
code =
|
| 56 |
db.add(models.EmailOTP(
|
| 57 |
email=_OTP_KEY, code_hash=_hash(code),
|
| 58 |
expires_at=dt.datetime.now(dt.timezone.utc) + dt.timedelta(minutes=config.OTP_TTL_MIN),
|
|
|
|
| 52 |
raise HTTPException(403, "Admin désactivé (CHATS_ADMIN_KEY non configuré)")
|
| 53 |
for old in db.scalars(select(models.EmailOTP).where(models.EmailOTP.email == _OTP_KEY)):
|
| 54 |
db.delete(old)
|
| 55 |
+
code = secrets.token_hex(10) # 20 caractères hexadécimaux
|
| 56 |
db.add(models.EmailOTP(
|
| 57 |
email=_OTP_KEY, code_hash=_hash(code),
|
| 58 |
expires_at=dt.datetime.now(dt.timezone.utc) + dt.timedelta(minutes=config.OTP_TTL_MIN),
|
backend/app/routers/cats.py
CHANGED
|
@@ -94,11 +94,13 @@ def create_cat(
|
|
| 94 |
phone: str | None = Form(None),
|
| 95 |
last_seen_lat: float | None = Form(None),
|
| 96 |
last_seen_lng: float | None = Form(None),
|
|
|
|
| 97 |
photos: list[UploadFile] = File(default=[]),
|
| 98 |
db: Session = Depends(get_db),
|
| 99 |
):
|
| 100 |
"""Déclare un chat perdu (email = identifiant pour le suivi ; téléphone optionnel,
|
| 101 |
-
privé). Plusieurs photos fortement recommandées.
|
|
|
|
| 102 |
storage.guard_count(photos)
|
| 103 |
cat = models.Cat(
|
| 104 |
name=name, description=description, color=color, pattern=pattern, breed=breed,
|
|
@@ -109,6 +111,7 @@ def create_cat(
|
|
| 109 |
last_seen_lat=last_seen_lat, last_seen_lng=last_seen_lng,
|
| 110 |
last_seen_at=dt.datetime.now(dt.timezone.utc),
|
| 111 |
claim_token=secrets.token_urlsafe(16),
|
|
|
|
| 112 |
)
|
| 113 |
for f in photos:
|
| 114 |
if f and f.filename:
|
|
|
|
| 94 |
phone: str | None = Form(None),
|
| 95 |
last_seen_lat: float | None = Form(None),
|
| 96 |
last_seen_lng: float | None = Form(None),
|
| 97 |
+
source: str | None = Form(None),
|
| 98 |
photos: list[UploadFile] = File(default=[]),
|
| 99 |
db: Session = Depends(get_db),
|
| 100 |
):
|
| 101 |
"""Déclare un chat perdu (email = identifiant pour le suivi ; téléphone optionnel,
|
| 102 |
+
privé). Plusieurs photos fortement recommandées.
|
| 103 |
+
`source=poster` signale une affiche partagée par un passant (sans compte propriétaire)."""
|
| 104 |
storage.guard_count(photos)
|
| 105 |
cat = models.Cat(
|
| 106 |
name=name, description=description, color=color, pattern=pattern, breed=breed,
|
|
|
|
| 111 |
last_seen_lat=last_seen_lat, last_seen_lng=last_seen_lng,
|
| 112 |
last_seen_at=dt.datetime.now(dt.timezone.utc),
|
| 113 |
claim_token=secrets.token_urlsafe(16),
|
| 114 |
+
source="poster" if source == "poster" else "owner",
|
| 115 |
)
|
| 116 |
for f in photos:
|
| 117 |
if f and f.filename:
|
backend/app/schemas.py
CHANGED
|
@@ -92,6 +92,7 @@ class CandidateOut(BaseModel):
|
|
| 92 |
pattern: str | None
|
| 93 |
confidence: float
|
| 94 |
photos: list[PhotoOut] = []
|
|
|
|
| 95 |
|
| 96 |
|
| 97 |
class SightingMatchResult(BaseModel):
|
|
|
|
| 92 |
pattern: str | None
|
| 93 |
confidence: float
|
| 94 |
photos: list[PhotoOut] = []
|
| 95 |
+
is_poster: bool = False # True = affiche partagée (pas de compte propriétaire)
|
| 96 |
|
| 97 |
|
| 98 |
class SightingMatchResult(BaseModel):
|
backend/app/serialize.py
CHANGED
|
@@ -105,6 +105,7 @@ def candidate_out(cat: models.Cat, confidence: float,
|
|
| 105 |
# On ne montre que quelques photos (assez pour reconnaître) — pas tout
|
| 106 |
# l'enrôlement : moins d'exposition, payload plus léger.
|
| 107 |
photos=[photo_out(p) for p in cat.photos[:3]],
|
|
|
|
| 108 |
)
|
| 109 |
|
| 110 |
|
|
|
|
| 105 |
# On ne montre que quelques photos (assez pour reconnaître) — pas tout
|
| 106 |
# l'enrôlement : moins d'exposition, payload plus léger.
|
| 107 |
photos=[photo_out(p) for p in cat.photos[:3]],
|
| 108 |
+
is_poster=getattr(cat, "source", "owner") == "poster",
|
| 109 |
)
|
| 110 |
|
| 111 |
|
backend/migrations/versions/a3f1c8e92d47_add_cat_source.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""add cat source column
|
| 2 |
+
|
| 3 |
+
Revision ID: a3f1c8e92d47
|
| 4 |
+
Revises: 5c7fa081b68a
|
| 5 |
+
Create Date: 2026-06-12 00:00:00.000000
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
# revision identifiers, used by Alembic.
|
| 15 |
+
revision: str = 'a3f1c8e92d47'
|
| 16 |
+
down_revision: Union[str, Sequence[str], None] = '5c7fa081b68a'
|
| 17 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 18 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def upgrade() -> None:
|
| 22 |
+
with op.batch_alter_table('cats', schema=None) as batch_op:
|
| 23 |
+
batch_op.add_column(sa.Column('source', sa.String(20), nullable=False, server_default='owner'))
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def downgrade() -> None:
|
| 27 |
+
with op.batch_alter_table('cats', schema=None) as batch_op:
|
| 28 |
+
batch_op.drop_column('source')
|
web/index.html
CHANGED
|
@@ -17,7 +17,11 @@
|
|
| 17 |
padding:14px 16px;display:flex;align-items:center;gap:10px;box-shadow:var(--shadow)}
|
| 18 |
header .back{background:rgba(255,255,255,.2);border:none;color:#fff;font-size:20px;width:40px;height:40px;
|
| 19 |
border-radius:10px;display:none}
|
| 20 |
-
header h1{font-size:19px;margin:0;font-weight:800;letter-spacing:.2px}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
main{max-width:680px;margin:0 auto;padding:16px}
|
| 22 |
.sub{color:var(--muted);font-size:15px;margin:2px 0 18px}
|
| 23 |
/* Home action cards */
|
|
@@ -98,9 +102,8 @@
|
|
| 98 |
#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:#16202c;color:#fff;
|
| 99 |
padding:12px 18px;border-radius:24px;opacity:0;transition:.3s;z-index:99;font-weight:600;max-width:90%}
|
| 100 |
#toast.show{opacity:1}
|
| 101 |
-
/* Visionneuse plein écran
|
| 102 |
.candidate img,.lead img,.game-pair img{cursor:zoom-in}
|
| 103 |
-
/* Affordance mobile : badge loupe en coin de la vignette agrandissable */
|
| 104 |
.zwrap{position:relative;display:inline-block;flex:none;line-height:0}
|
| 105 |
.zwrap .zbadge{position:absolute;right:4px;bottom:4px;background:rgba(8,12,18,.66);color:#fff;
|
| 106 |
border-radius:9px;font-size:12px;padding:1px 6px;pointer-events:none;line-height:1.5}
|
|
@@ -115,13 +118,13 @@
|
|
| 115 |
footer{max-width:680px;margin:8px auto 28px;padding:18px 16px 0;border-top:1px solid var(--line);
|
| 116 |
text-align:center;color:var(--muted);font-size:12.5px;line-height:1.7}
|
| 117 |
footer a{color:var(--muted);text-decoration:underline;cursor:pointer}
|
| 118 |
-
/* Page légale
|
| 119 |
.legal p{font-size:14.5px;color:#2b3a48;margin:10px 0}
|
| 120 |
.legal ul{font-size:14.5px;color:#2b3a48;padding-left:18px;margin:8px 0}
|
| 121 |
.legal li{margin:7px 0}
|
| 122 |
.legal a{color:var(--accent2)}
|
| 123 |
-
/* FAQ
|
| 124 |
-
.faq{margin-top:168px}
|
| 125 |
.faq h3{font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.6px;text-transform:uppercase;margin:0 0 2px}
|
| 126 |
.faq details{border-bottom:1px solid var(--line)}
|
| 127 |
.faq summary{font-weight:600;cursor:pointer;font-size:14px;color:var(--ink);list-style:none;display:flex;gap:9px;padding:12px 2px}
|
|
@@ -136,64 +139,65 @@
|
|
| 136 |
<header>
|
| 137 |
<button class="back" id="backBtn" onclick="goHome()">‹</button>
|
| 138 |
<h1 id="hTitle">🐱 Chats perdus (version Beta)</h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
</header>
|
| 140 |
<main>
|
| 141 |
|
| 142 |
<!-- ===================== ACCUEIL ===================== -->
|
| 143 |
<section id="home">
|
| 144 |
-
<p class="sub">Entraide communautaire pour retrouver nos compagnons.</p>
|
| 145 |
|
| 146 |
-
<!-- Deux flux d'urgence, massifs -->
|
| 147 |
<div class="home">
|
| 148 |
<button class="action hero hero-lost" onclick="show('report')">
|
| 149 |
<div class="ico">🚨</div><div>
|
| 150 |
-
<span class="tag tag-lost">Propriétaire</span>
|
| 151 |
-
<div class="t">J'ai perdu mon chat</div>
|
| 152 |
-
<div class="d">Déclare sa disparition en envoyant quelques photos (et son affiche, si tu en as une). Si un passant le croise et le prend en photo, notre IA le reconnaîtra.<br><br>
|
| 153 |
• Alerte immédiate : Tu reçois un courriel dès qu'il est repéré.<br>
|
| 154 |
• Vie privée protégée : Tu es le seul à décider de contacter le trouveur. Ton courriel et tes coordonnées ne sont jamais divulgués.</div></div>
|
| 155 |
</button>
|
| 156 |
<button class="action hero hero-seen" onclick="show('check')">
|
| 157 |
<div class="ico">📷</div><div>
|
| 158 |
-
<span class="tag tag-seen">Passant / Trouveur</span>
|
| 159 |
-
<div class="t">J'ai vu un chat</div>
|
| 160 |
-
<div class="d">Prends une photo du chat croisé qui semble perdu. L'IA compare instantanément sa ressemblance avec notre base de données et te montre les profils similaires.<br><br>
|
| 161 |
• Signalement rapide : Si tu reconnais le chat, valide la correspondance pour envoyer une notification automatique et anonyme à sa famille.</div></div>
|
| 162 |
</button>
|
| 163 |
<button class="action hero hero-follow" onclick="show('follow')">
|
| 164 |
<div class="ico">ℹ️</div><div>
|
| 165 |
-
<span class="tag tag-follow">Suivi</span>
|
| 166 |
-
<div class="t">Suivre mon signalement</div>
|
| 167 |
-
<div class="d">Accède à ton espace pour gérer ton alerte ou clôturer la recherche.</div></div>
|
| 168 |
</button>
|
| 169 |
</div>
|
| 170 |
|
| 171 |
-
<
|
| 172 |
-
<p class="sub" style="margin-top:24px;margin-bottom:8px">Aider la communauté :</p>
|
| 173 |
<div class="secondary">
|
| 174 |
<button class="action mini" onclick="show('poster')">
|
| 175 |
-
<div class="ico">📋</div><div><div class="t">Partager une affiche vue</div>
|
| 176 |
-
<div class="d">Enrichir la banque</div></div></button>
|
| 177 |
<button class="action mini" onclick="show('train')">
|
| 178 |
-
<div class="ico">🧠</div><div><div class="t">Aider l'IA</div>
|
| 179 |
-
<div class="d">Photos + jeu de reconnaissance</div></div></button>
|
| 180 |
</div>
|
| 181 |
|
| 182 |
-
<!-- FAQ -->
|
| 183 |
<div class="faq">
|
| 184 |
-
<h3>Questions fréquentes</h3>
|
| 185 |
-
<details><summary>Comment ça fonctionne ?</summary>
|
| 186 |
-
<p>Tu déclares ton chat perdu avec ses photos. Quand quelqu'un croise un chat, il en
|
| 187 |
prend une photo : l'IA la compare aux chats perdus et montre les ressemblances. Si la
|
| 188 |
personne reconnaît ton chat, tu reçois un signalement par courriel.</p></details>
|
| 189 |
-
<details><summary>Mes informations restent-elles privées ?</summary>
|
| 190 |
-
<p>Oui. Aucun annuaire public : la personne qui repère un chat ne voit jamais tes
|
| 191 |
coordonnées. On te rejoint par un code envoyé à ton courriel, et tu peux tout
|
| 192 |
supprimer quand tu veux.</p></details>
|
| 193 |
-
<details><summary>Est-ce gratuit ?</summary>
|
| 194 |
-
<p>Oui, entièrement. C'est un projet d'entraide communautaire, sans publicité.</p></details>
|
| 195 |
-
<details><summary>Quelle technologie derrière ?</summary>
|
| 196 |
-
<p>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
|
| 197 |
chaque photo en une « empreinte » visuelle. On compare ces empreintes pour repérer les
|
| 198 |
chats qui se ressemblent, puis un humain confirme toujours la correspondance.</p></details>
|
| 199 |
</div>
|
|
@@ -201,62 +205,62 @@
|
|
| 201 |
|
| 202 |
<!-- ===================== 1. VÉRIFIER ===================== -->
|
| 203 |
<section id="check" class="hidden">
|
| 204 |
-
<div class="privacy">🔒 On ne montre que les chats qui ressemblent à ta photo. Le contact du
|
| 205 |
propriétaire reste privé — c'est lui qui te recontacte.</div>
|
| 206 |
<div class="card">
|
| 207 |
-
<h2>Tu as croisé un chat ?</h2>
|
| 208 |
-
<label>Photo du chat</label>
|
| 209 |
-
<label class="photo-btn" for="c_photo">📷 Prendre / choisir une photo</label>
|
| 210 |
<input class="file-hidden" id="c_photo" type="file" accept="image/*" onchange="prev('c_photo','c_prev')" />
|
| 211 |
<div class="thumbs" id="c_prev"></div>
|
| 212 |
-
<div class="hint" id="c_gps">📍 Position : non captée (optionnel, aide au tri)</div>
|
| 213 |
-
<button class="btn btn-ghost" onclick="getPos('c')">Capter ma position</button>
|
| 214 |
-
<button class="btn btn-go" onclick="doCheck()">🔍 Vérifier</button>
|
| 215 |
<div class="search-prog" id="c_prog"><div class="bar"><div id="c_prog_fill"></div></div>
|
| 216 |
<div class="lbl"><span class="dot"></span><span id="c_prog_txt"></span></div></div>
|
| 217 |
</div>
|
| 218 |
<div id="c_result"></div>
|
| 219 |
<div id="c_leadbox" class="card hidden">
|
| 220 |
-
<h2 id="c_leadtitle">Prévenir le propriétaire 🐾</h2>
|
| 221 |
-
<p class="hint">Laisse TES coordonnées : le propriétaire te recontactera.</p>
|
| 222 |
-
<label>Ton contact (téléphone / courriel)</label>
|
| 223 |
<input id="c_contact" placeholder="514-555-1234" />
|
| 224 |
-
<label>Message (où, quand, état du chat)</label>
|
| 225 |
-
<textarea id="c_msg" rows="2" placeholder="Vu ce matin près du parc"></textarea>
|
| 226 |
-
<button class="btn btn-primary" onclick="sendLead()">Envoyer au propriétaire</button>
|
| 227 |
</div>
|
| 228 |
</section>
|
| 229 |
|
| 230 |
<!-- ===================== 2. SIGNALER ===================== -->
|
| 231 |
<section id="report" class="hidden">
|
| 232 |
-
<div class="privacy">🔒 Ton contact reste privé. Tu reçois un <b>code secret</b> pour suivre les
|
| 233 |
signalements. Plus tu mets de photos, mieux l'IA reconnaît ton chat.</div>
|
| 234 |
<div class="card">
|
| 235 |
-
<h2>Signaler mon chat perdu</h2>
|
| 236 |
-
<label>Tu as une affiche ? (photo ou PDF — optionnel)</label>
|
| 237 |
-
<label class="photo-btn" for="r_poster">🖼️ Charger l'affiche (remplit le formulaire)</label>
|
| 238 |
<input class="file-hidden" id="r_poster" type="file" accept="image/*,application/pdf" onchange="posterPrefill('r_poster')" />
|
| 239 |
<div class="hint" id="r_poster_note"></div>
|
| 240 |
|
| 241 |
-
<label>Nom du chat</label>
|
| 242 |
-
<input id="r_name" placeholder="Tigrou" />
|
| 243 |
-
<label>Description (robe, signes distinctifs)</label>
|
| 244 |
-
<textarea id="r_desc" rows="2" placeholder="Tortie, médaillon blanc, timide"></textarea>
|
| 245 |
-
<label>Ton courriel (sert à suivre les signalements — jamais montré aux autres)</label>
|
| 246 |
-
<input id="r_email" type="email" placeholder="toi@exemple.com" />
|
| 247 |
-
<label>Ton téléphone (optionnel, privé)</label>
|
| 248 |
<input id="r_phone" type="tel" placeholder="514-555-1234" />
|
| 249 |
-
<label>Détails supplémentaires (optionnel)</label>
|
| 250 |
-
<textarea id="r_details" rows="2" placeholder="Disparu le 3 juin, secteur Rosemont, collier bleu…"></textarea>
|
| 251 |
|
| 252 |
-
<label>Photos du chat — <b>5 à 10 recommandé</b> 🎯</label>
|
| 253 |
-
<label class="photo-btn" for="r_photos">📷 Ajouter des photos du chat</label>
|
| 254 |
<input class="file-hidden" id="r_photos" type="file" accept="image/*" multiple onchange="prev('r_photos','r_prev'); photoHint()" />
|
| 255 |
<div class="thumbs" id="r_prev"></div>
|
| 256 |
-
<div class="hint" id="r_hint">Astuce : plusieurs angles et éclairages = bien plus fiable.</div>
|
| 257 |
-
<div class="hint" id="r_gps">📍 Dernière position connue : non captée</div>
|
| 258 |
-
<button class="btn btn-ghost" onclick="getPos('r')">Capter la position</button>
|
| 259 |
-
<button class="btn btn-primary" onclick="doReport()">🚨 Déclarer (obtenir mon code)</button>
|
| 260 |
<div class="search-prog" id="r_prog"><div class="bar"><div id="r_prog_fill"></div></div>
|
| 261 |
<div class="lbl"><span class="dot"></span><span id="r_prog_txt"></span></div></div>
|
| 262 |
<div id="r_token"></div>
|
|
@@ -265,18 +269,18 @@
|
|
| 265 |
|
| 266 |
<!-- ===================== SUIVRE UN SIGNALEMENT ===================== -->
|
| 267 |
<section id="follow" class="hidden">
|
| 268 |
-
<div class="privacy">🔒 Accès sécurisé : on t'envoie un code par courriel pour vérifier
|
| 269 |
que c'est bien toi. Tu vois alors quand tu as déclaré ton chat, combien de fois l'IA
|
| 270 |
l'a comparé, et les signalements reçus.</div>
|
| 271 |
<div class="card">
|
| 272 |
-
<h2>Suivre mon signalement</h2>
|
| 273 |
-
<label>Ton courriel (celui de la déclaration)</label>
|
| 274 |
-
<input id="r_track_email" type="email" placeholder="toi@exemple.com" />
|
| 275 |
-
<button class="btn btn-ghost" onclick="requestCode()">📩 Recevoir mon code</button>
|
| 276 |
<div id="r_code_box" class="hidden">
|
| 277 |
-
<label>Code reçu</label>
|
| 278 |
<input id="r_track_code" inputmode="numeric" placeholder="123456" />
|
| 279 |
-
<button class="btn btn-go" onclick="verifyCode()">Voir mon suivi</button>
|
| 280 |
</div>
|
| 281 |
<div id="r_track"></div>
|
| 282 |
</div>
|
|
@@ -284,51 +288,51 @@
|
|
| 284 |
|
| 285 |
<!-- ===================== 3. PARTAGER UNE AFFICHE ===================== -->
|
| 286 |
<section id="poster" class="hidden">
|
| 287 |
-
<div class="privacy">📋 Les affiches sont publiques. En les partageant, tu enrichis la banque et
|
| 288 |
aides d'autres personnes à retrouver leur chat.</div>
|
| 289 |
<div class="card">
|
| 290 |
-
<h2>Partager une affiche trouvée</h2>
|
| 291 |
-
<label>Photo ou PDF de l'affiche</label>
|
| 292 |
-
<label class="photo-btn" for="p_file">📷 Prendre / charger l'affiche</label>
|
| 293 |
<input class="file-hidden" id="p_file" type="file" accept="image/*,application/pdf" onchange="prev('p_file','p_prev')" />
|
| 294 |
<div class="thumbs" id="p_prev"></div>
|
| 295 |
-
<label>Infos visibles sur l'affiche (optionnel)</label>
|
| 296 |
-
<textarea id="p_text" rows="3" placeholder="Nom, secteur, description… (ce qui est écrit sur l'affiche)"></textarea>
|
| 297 |
-
<div class="hint" id="p_gps">📍 Où l'as-tu vue ? (optionnel)</div>
|
| 298 |
-
<button class="btn btn-ghost" onclick="getPos('p')">Capter
|
| 299 |
-
<button class="btn btn-go" onclick="doPoster()">📋 Ajouter à la banque</button>
|
| 300 |
<div id="p_result"></div>
|
| 301 |
</div>
|
| 302 |
</section>
|
| 303 |
|
| 304 |
<!-- ===================== 4. ENTRAÎNER L'IA ===================== -->
|
| 305 |
<section id="train" class="hidden">
|
| 306 |
-
<div class="privacy">🧠 Aide volontaire. Ces photos servent UNIQUEMENT à entraîner l'IA — elles
|
| 307 |
ne sont jamais mêlées aux fiches privées de chats perdus.</div>
|
| 308 |
<div class="card">
|
| 309 |
-
<h2>Donner des photos</h2>
|
| 310 |
-
<p class="hint">Ajoute des photos de chats (le tien, ou des chats croisés) pour aider l'IA à mieux reconnaître.</p>
|
| 311 |
-
<label class="photo-btn" for="t_photos">📷 Ajouter des photos</label>
|
| 312 |
<input class="file-hidden" id="t_photos" type="file" accept="image/*" multiple onchange="prev('t_photos','t_prev')" />
|
| 313 |
<div class="thumbs" id="t_prev"></div>
|
| 314 |
<label style="display:flex;gap:10px;align-items:flex-start;font-weight:400;font-size:13.5px;margin-top:12px">
|
| 315 |
<input id="t_consent" type="checkbox" style="width:auto;margin-top:2px" />
|
| 316 |
-
<span>J'accepte que ces images soient utilisées pour <b>entraîner le modèle de
|
| 317 |
reconnaissance</b>. Elles ne seront pas mêlées aux fiches de chats perdus.</span>
|
| 318 |
</label>
|
| 319 |
-
<button class="btn btn-go" onclick="doContribute()">Donner ces photos</button>
|
| 320 |
</div>
|
| 321 |
<div class="card">
|
| 322 |
-
<h2>Jouer : même chat ou pas ?</h2>
|
| 323 |
-
<p class="hint">Regarde deux photos et dis si c'est le même chat. Chaque réponse aide l'IA.</p>
|
| 324 |
-
<div id="game"><button class="btn btn-go" onclick="nextPair()">Commencer à jouer</button></div>
|
| 325 |
<div class="hint" id="t_stats"></div>
|
| 326 |
</div>
|
| 327 |
</section>
|
| 328 |
|
| 329 |
<!-- ===================== AVIS LÉGAL & CONFIDENTIALITÉ ===================== -->
|
| 330 |
<section id="legal" class="hidden">
|
| 331 |
-
<div class="card legal">
|
| 332 |
<h2>Avis de non-responsabilité</h2>
|
| 333 |
<p>Allo Chat est un service communautaire <b>gratuit</b> d'entraide pour aider à
|
| 334 |
retrouver des chats perdus, à Montréal et ailleurs.</p>
|
|
@@ -346,7 +350,6 @@
|
|
| 346 |
<p><b>Conduite des utilisateurs.</b> Vous êtes responsable des renseignements que vous
|
| 347 |
publiez et de vos échanges. Faites preuve de prudence lors d'un contact avec une
|
| 348 |
personne inconnue, et ne tentez pas d'approcher un animal craintif ou en détresse.</p>
|
| 349 |
-
|
| 350 |
<h2 style="margin-top:22px">Confidentialité — Loi 25 (Québec)</h2>
|
| 351 |
<p>Allo Chat respecte la <i>Loi sur la protection des renseignements personnels dans le
|
| 352 |
secteur privé</i> (Loi 25). Voici l'essentiel.</p>
|
|
@@ -386,7 +389,7 @@
|
|
| 386 |
|
| 387 |
</main>
|
| 388 |
|
| 389 |
-
<footer>
|
| 390 |
🐾 Fait à Montréal · © 2026 Allo Chat ·
|
| 391 |
<a onclick="show('legal')">Avis légal & confidentialité</a><br>
|
| 392 |
Contact : <a href="mailto:info@allochat.org">info@allochat.org</a>
|
|
@@ -396,24 +399,410 @@
|
|
| 396 |
<div id="lightbox" onclick="closeZoom(event)">
|
| 397 |
<button class="x" onclick="closeZoom(event)" aria-label="Fermer">✕</button>
|
| 398 |
<img id="lightbox_img" alt="Photo du chat en grand">
|
| 399 |
-
<div class="hint-zoom">Touche l'image ou appuie sur ✕ pour fermer</div>
|
| 400 |
</div>
|
| 401 |
|
| 402 |
<script>
|
| 403 |
const API = location.origin;
|
| 404 |
const pos = {c:null,r:null,p:null};
|
| 405 |
let lastSightingId=null, chosenCandidateToken=null, gamePair=null;
|
| 406 |
-
let lastMatches=[], ownerCats=[];
|
| 407 |
|
| 408 |
-
// Échappement HTML : noms/descriptions/messages viennent d'inconnus → jamais
|
| 409 |
-
// injectés bruts dans innerHTML (anti-XSS). Sert pour tout texte affiché.
|
| 410 |
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>(
|
| 411 |
{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
// ---- navigation ----
|
| 414 |
-
const TITLES=
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
| 417 |
function show(v){
|
| 418 |
['home','check','report','follow','poster','train','legal'].forEach(id=>document.getElementById(id).classList.toggle('hidden',id!==v));
|
| 419 |
document.getElementById('hTitle').textContent=TITLES[v];
|
|
@@ -424,21 +813,19 @@
|
|
| 424 |
function goHome(){ show('home'); }
|
| 425 |
|
| 426 |
function toast(m){const t=document.getElementById('toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2800);}
|
| 427 |
-
// Visionneuse : agrandit une photo. On lit l'URL depuis l'élément <img>
|
| 428 |
-
// cliqué (this.src) — aucune donnée n'est injectée dans un handler.
|
| 429 |
function zoom(src){ document.getElementById('lightbox_img').src=src;
|
| 430 |
document.getElementById('lightbox').classList.add('on'); }
|
| 431 |
-
// Vignette agrandissable + badge loupe (cue visible y compris sur mobile).
|
| 432 |
const zimg=url=>`<span class="zwrap"><img src="${encodeURI(url)}" onclick="zoom(this.src)" title="Voir en grand"><span class="zbadge">🔍</span></span>`;
|
| 433 |
function closeZoom(e){ if(e) e.stopPropagation();
|
| 434 |
const lb=document.getElementById('lightbox'); lb.classList.remove('on');
|
| 435 |
document.getElementById('lightbox_img').removeAttribute('src'); }
|
| 436 |
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeZoom(); });
|
| 437 |
const val=id=>document.getElementById(id).value;
|
| 438 |
-
function getPos(p){
|
|
|
|
| 439 |
navigator.geolocation.getCurrentPosition(g=>{pos[p]={lat:g.coords.latitude,lng:g.coords.longitude};
|
| 440 |
document.getElementById(p+'_gps').textContent=`📍 ${g.coords.latitude.toFixed(5)}, ${g.coords.longitude.toFixed(5)}`;
|
| 441 |
-
},()=>toast(
|
| 442 |
}
|
| 443 |
function prev(inputId,boxId){
|
| 444 |
const box=document.getElementById(boxId); box.innerHTML='';
|
|
@@ -448,14 +835,10 @@
|
|
| 448 |
}
|
| 449 |
}
|
| 450 |
|
| 451 |
-
|
| 452 |
-
// Nom du moteur IA réellement actif (depuis /health) — pour des messages honnêtes.
|
| 453 |
-
let AI_NAME='l\'IA';
|
| 454 |
fetch(API+'/health').then(r=>r.json()).then(h=>{
|
| 455 |
const e=(h.embedder||'');
|
| 456 |
-
AI_NAME = e.includes('miewid') ? 'MiewID
|
| 457 |
-
: e.includes('megadescriptor') ? 'MegaDescriptor'
|
| 458 |
-
: 'l\'analyse colorimétrique';
|
| 459 |
}).catch(()=>{});
|
| 460 |
|
| 461 |
function progress(id){
|
|
@@ -464,10 +847,9 @@
|
|
| 464 |
let timer=null, finished=false;
|
| 465 |
const set=(p,t)=>{ fill.style.width=Math.min(p,98)+'%'; if(t) txt.textContent=t; };
|
| 466 |
return {
|
| 467 |
-
show(){ finished=false; box.classList.add('on'); set(2,'Préparation…'); },
|
| 468 |
set,
|
| 469 |
-
creep(target,t){
|
| 470 |
-
if(finished) return; if(t) txt.textContent=t;
|
| 471 |
clearInterval(timer);
|
| 472 |
timer=setInterval(()=>{ if(finished) return clearInterval(timer);
|
| 473 |
const cur=parseFloat(fill.style.width)||0;
|
|
@@ -481,14 +863,13 @@
|
|
| 481 |
};
|
| 482 |
}
|
| 483 |
|
| 484 |
-
// POST avec vraie progression d'envoi (XHR) puis étapes serveur.
|
| 485 |
function postWithProgress(url, fd, prog, stages){
|
| 486 |
return new Promise((resolve,reject)=>{
|
| 487 |
const xhr=new XMLHttpRequest();
|
| 488 |
xhr.open('POST', url);
|
| 489 |
xhr.upload.onprogress=e=>{ if(e.lengthComputable){
|
| 490 |
const pct=e.loaded/e.total;
|
| 491 |
-
prog.set(2+Math.round(pct*38),
|
| 492 |
xhr.upload.onload=()=>{ prog.creep(72, stages[0]);
|
| 493 |
if(stages[1]) setTimeout(()=>prog.creep(93, stages[1]), 2600); };
|
| 494 |
xhr.onload=()=>resolve({ok:xhr.status>=200&&xhr.status<300, status:xhr.status,
|
|
@@ -501,55 +882,65 @@
|
|
| 501 |
// ---------- 1. VÉRIFIER ----------
|
| 502 |
async function doCheck(){
|
| 503 |
const f=document.getElementById('c_photo').files[0];
|
| 504 |
-
if(!f) return toast(
|
| 505 |
const fd=new FormData(); fd.append('photos',f);
|
| 506 |
if(pos.c){fd.append('lat',pos.c.lat);fd.append('lng',pos.c.lng);}
|
| 507 |
const prog=progress('c_prog'); prog.show();
|
|
|
|
| 508 |
let r;
|
| 509 |
try{
|
| 510 |
r=await postWithProgress(API+'/sightings', fd, prog, [
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
prog.done('✅ Analyse terminée');
|
| 516 |
const d=r.json(); lastSightingId=d.sighting_id; lastMatches=d.matches||[];
|
| 517 |
const box=document.getElementById('c_result'); document.getElementById('c_leadbox').classList.add('hidden');
|
| 518 |
if(!d.match_found||!d.matches.length){
|
| 519 |
-
box.innerHTML=`<div class="result none"><div class="big">🔍</div><b>
|
| 520 |
<p class="hint">${esc(d.message)}</p></div>`; return;
|
| 521 |
}
|
| 522 |
-
box.innerHTML=`<div class="card"><b>${d.matches.length
|
| 523 |
-
<p class="hint">
|
| 524 |
d.matches.map(m=>{
|
| 525 |
const pct=Math.round(m.confidence*100);
|
| 526 |
-
const c=pct>=70?[
|
| 527 |
const ph=(m.photos[0]&&m.photos[0].url)?API+m.photos[0].url:'';
|
|
|
|
|
|
|
|
|
|
| 528 |
return `<div class="candidate">${ph?zimg(ph):''}<div style="flex:1">
|
| 529 |
-
<b>${esc(m.name||
|
| 530 |
<span class="hint">${esc(m.description||'')}</span><br>
|
| 531 |
-
|
| 532 |
</div></div>`;
|
| 533 |
}).join('')+`</div>`;
|
| 534 |
}
|
| 535 |
function chooseCat(id){ chosenCandidateToken=id;
|
| 536 |
const m=lastMatches.find(x=>x.candidate_token===id);
|
| 537 |
-
const name=(m&&m.name)?m.name:'ce chat';
|
| 538 |
-
document.getElementById('c_leadtitle').textContent=
|
| 539 |
const b=document.getElementById('c_leadbox'); b.classList.remove('hidden'); b.scrollIntoView({behavior:'smooth'});
|
| 540 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
async function sendLead(){
|
| 542 |
-
if(!chosenCandidateToken) return toast(
|
| 543 |
const r=await fetch(`${API}/sightings/${lastSightingId}/lead`,{method:'POST',headers:{'Content-Type':'application/json'},
|
| 544 |
body:JSON.stringify({candidate_token:chosenCandidateToken,finder_contact:val('c_contact'),message:val('c_msg')})});
|
| 545 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 546 |
-
document.getElementById('c_leadbox').innerHTML=
|
| 547 |
}
|
| 548 |
|
| 549 |
// ---------- 2. SIGNALER ----------
|
| 550 |
async function posterPrefill(inputId){
|
| 551 |
const f=document.getElementById(inputId).files[0]; if(!f) return;
|
| 552 |
-
const note=document.getElementById('r_poster_note');
|
|
|
|
| 553 |
const fd=new FormData(); fd.append('file',f);
|
| 554 |
try{
|
| 555 |
const r=await fetch(API+'/posters',{method:'POST',body:fd}); const d=await r.json();
|
|
@@ -557,21 +948,22 @@
|
|
| 557 |
const phEl=document.getElementById('r_phone');
|
| 558 |
if(d.suggested_contact&&phEl) phEl.value=d.suggested_contact;
|
| 559 |
if(d.suggested_description) document.getElementById('r_desc').value=d.suggested_description;
|
| 560 |
-
note.textContent=d.note||
|
| 561 |
-
}catch(e){ note.textContent=
|
| 562 |
}
|
| 563 |
function photoHint(){
|
| 564 |
-
const n=document.getElementById('r_photos').files.length,h=document.getElementById('r_hint');
|
| 565 |
-
|
| 566 |
-
if(n
|
| 567 |
-
|
| 568 |
-
else{h.style.color='var(--
|
|
|
|
| 569 |
}
|
| 570 |
async function doReport(){
|
| 571 |
const photos=document.getElementById('r_photos').files;
|
| 572 |
-
if(!photos.length) return toast(
|
| 573 |
const email=val('r_email').trim();
|
| 574 |
-
if(!email||!email.includes('@')) return toast(
|
| 575 |
const fd=new FormData();
|
| 576 |
fd.append('name',val('r_name'));
|
| 577 |
const desc=[val('r_desc'),val('r_details')].filter(Boolean).join(' — ');
|
|
@@ -579,44 +971,43 @@
|
|
| 579 |
if(pos.r){fd.append('last_seen_lat',pos.r.lat);fd.append('last_seen_lng',pos.r.lng);}
|
| 580 |
for(const f of photos) fd.append('photos',f);
|
| 581 |
const prog=progress('r_prog'); prog.show();
|
|
|
|
| 582 |
let r;
|
| 583 |
try{
|
| 584 |
r=await postWithProgress(API+'/cats', fd, prog, [
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
}catch(e){ return prog.fail(
|
| 588 |
-
if(!r.ok){ return prog.fail('Erreur '+r.status+(r.status===429?
|
| 589 |
-
prog.done(
|
| 590 |
const d=r.json(); localStorage.setItem('chats_email',email);
|
| 591 |
document.getElementById('r_track_email').value=email;
|
| 592 |
-
document.getElementById('r_token').innerHTML=
|
| 593 |
-
`<div class="result match" style="margin-top:14px"><b>✅ ${esc(d.name||'Chat')} déclaré (${d.photos_count} photos)</b>
|
| 594 |
-
<p class="hint">Pour voir les signalements et le suivi : retourne à l'accueil → « Suivre mon
|
| 595 |
-
signalement », entre ton courriel (${email}) et le code qu'on t'enverra. Rien à mémoriser 🙂</p></div>`;
|
| 596 |
document.getElementById('r_token').scrollIntoView({behavior:'smooth'});
|
| 597 |
}
|
| 598 |
async function requestCode(){
|
| 599 |
-
const email=val('r_track_email').trim();
|
|
|
|
| 600 |
const fd=new FormData(); fd.append('email',email);
|
| 601 |
const r=await fetch(API+'/cats/request-code',{method:'POST',body:fd});
|
| 602 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 603 |
const d=await r.json();
|
| 604 |
document.getElementById('r_code_box').classList.remove('hidden');
|
| 605 |
-
if(d.dev_code){
|
| 606 |
document.getElementById('r_track_code').value=d.dev_code;
|
| 607 |
-
toast('
|
| 608 |
-
} else toast(
|
| 609 |
}
|
| 610 |
async function verifyCode(){
|
| 611 |
const email=val('r_track_email').trim(), code=val('r_track_code').trim();
|
| 612 |
-
if(!code) return toast(
|
| 613 |
const fd=new FormData(); fd.append('email',email); fd.append('code',code);
|
| 614 |
const r=await fetch(API+'/cats/verify-code',{method:'POST',body:fd});
|
| 615 |
const box=document.getElementById('r_track');
|
| 616 |
-
if(!r.ok){ box.innerHTML=
|
| 617 |
const acc=await r.json(); ownerCats=acc.cats||[];
|
| 618 |
-
if(!acc.cats.length){ box.innerHTML=
|
| 619 |
-
box.innerHTML=
|
| 620 |
+ acc.cats.map(renderOwnerCat).join('');
|
| 621 |
}
|
| 622 |
function followLogout(){
|
|
@@ -625,65 +1016,64 @@
|
|
| 625 |
document.getElementById('r_code_box').classList.add('hidden');
|
| 626 |
document.getElementById('r_track_email').value='';
|
| 627 |
localStorage.removeItem('chats_email');
|
| 628 |
-
toast(
|
| 629 |
}
|
| 630 |
function renderOwnerCat(c){
|
|
|
|
| 631 |
const leads=c.leads.map(l=>{
|
| 632 |
const pct=l.confidence?Math.round(l.confidence*100):null;
|
| 633 |
const img=l.sighting_photo_url?zimg(API+l.sighting_photo_url):'';
|
| 634 |
-
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)}">
|
| 635 |
-
return `<div class="lead">${img}<div style="flex:1"><b>
|
| 636 |
-
<span class="hint">${esc(l.message||
|
| 637 |
-
<span class="hint">📞 ${esc(l.finder_contact||
|
| 638 |
-
}).join('')||
|
| 639 |
const declared = c.cat.last_seen_at || c.cat.created_at;
|
| 640 |
-
const dDate = declared ? new Date(declared).toLocaleDateString(
|
| 641 |
const stats = `<div class="follow-stats">
|
| 642 |
-
<div class="fs"><b>${dDate}</b><span>
|
| 643 |
-
<div class="fs"><b>${(c.comparisons??0).toLocaleString(
|
| 644 |
-
<div class="fs"><b>${c.leads.length}</b><span>
|
| 645 |
</div>`;
|
| 646 |
return `<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:12px">
|
| 647 |
-
<b>${esc(c.cat.name||
|
| 648 |
${stats}
|
| 649 |
-
<button class="btn btn-ghost" onclick="reunited('${encodeURIComponent(c.claim_token)}')">
|
| 650 |
<button class="btn btn-ghost" style="color:var(--bad);border-color:var(--bad)"
|
| 651 |
-
onclick="deleteCat('${encodeURIComponent(c.claim_token)}')">
|
| 652 |
-
<h2 style="margin-top:12px">
|
| 653 |
}
|
| 654 |
async function deleteCat(tok){
|
| 655 |
const c=ownerCats.find(x=>x.claim_token===tok);
|
| 656 |
-
const name=(c&&c.cat&&c.cat.name)?c.cat.name:
|
| 657 |
-
if(!confirm(
|
| 658 |
const fd=new FormData(); fd.append('token',tok);
|
| 659 |
const r=await fetch(API+'/cats/track/delete',{method:'POST',body:fd});
|
| 660 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 661 |
-
toast(
|
| 662 |
-
document.getElementById('r_track').innerHTML=
|
| 663 |
}
|
| 664 |
async function reunited(tok){
|
| 665 |
const fd=new FormData();fd.append('token',tok);fd.append('status','reunited');
|
| 666 |
const r=await fetch(API+'/cats/track/status',{method:'POST',body:fd});
|
| 667 |
if(!r.ok) return toast('Erreur');
|
| 668 |
-
const c=await r.json(); toast(
|
| 669 |
document.getElementById('r_track').innerHTML=renderOwnerCat(c);
|
| 670 |
}
|
| 671 |
|
| 672 |
// ---------- 3. PARTAGER UNE AFFICHE ----------
|
| 673 |
async function doPoster(){
|
| 674 |
const f=document.getElementById('p_file').files[0];
|
| 675 |
-
if(!f) return toast(
|
| 676 |
-
// 1) lecture OCR (pré-remplissage / note)
|
| 677 |
const fd0=new FormData(); fd0.append('file',f);
|
| 678 |
try{ await fetch(API+'/posters',{method:'POST',body:fd0}); }catch(e){}
|
| 679 |
-
|
| 680 |
-
const fd=new FormData(); fd.append('name','Affiche partagée');
|
| 681 |
fd.append('description',val('p_text'));
|
| 682 |
if(pos.p){fd.append('last_seen_lat',pos.p.lat);fd.append('last_seen_lng',pos.p.lng);}
|
| 683 |
if(f.type.startsWith('image/')) fd.append('photos',f);
|
| 684 |
const r=await fetch(API+'/cats',{method:'POST',body:fd});
|
| 685 |
const box=document.getElementById('p_result');
|
| 686 |
-
if(r.ok){ box.innerHTML=
|
| 687 |
document.getElementById('p_file').value=''; document.getElementById('p_prev').innerHTML=''; document.getElementById('p_text').value='';
|
| 688 |
} else box.innerHTML='<p class="hint">Erreur '+r.status+'.</p>';
|
| 689 |
}
|
|
@@ -691,38 +1081,40 @@
|
|
| 691 |
// ---------- 4. ENTRAÎNER ----------
|
| 692 |
async function doContribute(){
|
| 693 |
const photos=document.getElementById('t_photos').files;
|
| 694 |
-
if(!photos.length) return toast(
|
| 695 |
-
if(!document.getElementById('t_consent').checked) return toast(
|
| 696 |
const fd=new FormData(); fd.append('consent','1');
|
| 697 |
for(const f of photos) fd.append('photos',f);
|
| 698 |
-
toast(
|
| 699 |
const r=await fetch(API+'/training/photos',{method:'POST',body:fd});
|
| 700 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 701 |
-
const d=await r.json(); toast(
|
| 702 |
document.getElementById('t_photos').value=''; document.getElementById('t_prev').innerHTML=''; loadTrainStats();
|
| 703 |
}
|
| 704 |
async function nextPair(){
|
| 705 |
-
const g=document.getElementById('game'); g.innerHTML=
|
| 706 |
const r=await fetch(API+'/training/pair');
|
| 707 |
-
if(!r.ok){ g.innerHTML=
|
| 708 |
gamePair=await r.json();
|
| 709 |
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>
|
| 710 |
<div class="seg" style="margin-top:12px">
|
| 711 |
-
<button onclick="sendLabel('same')">
|
| 712 |
-
<button onclick="sendLabel('different')">
|
| 713 |
</div>`;
|
| 714 |
}
|
| 715 |
async function sendLabel(v){
|
| 716 |
if(!gamePair) return;
|
| 717 |
await fetch(API+'/training/label',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 718 |
body:JSON.stringify({photo_a_id:gamePair.a.id,photo_b_id:gamePair.b.id,verdict:v})});
|
| 719 |
-
toast(
|
| 720 |
}
|
| 721 |
async function loadTrainStats(){
|
| 722 |
try{const d=await (await fetch(API+'/training/stats')).json();
|
| 723 |
-
document.getElementById('t_stats').textContent=
|
| 724 |
}
|
| 725 |
|
|
|
|
|
|
|
| 726 |
const savedEmail=localStorage.getItem('chats_email'); if(savedEmail) document.getElementById('r_track_email').value=savedEmail;
|
| 727 |
</script>
|
| 728 |
</body>
|
|
|
|
| 17 |
padding:14px 16px;display:flex;align-items:center;gap:10px;box-shadow:var(--shadow)}
|
| 18 |
header .back{background:rgba(255,255,255,.2);border:none;color:#fff;font-size:20px;width:40px;height:40px;
|
| 19 |
border-radius:10px;display:none}
|
| 20 |
+
header h1{font-size:19px;margin:0;font-weight:800;letter-spacing:.2px;flex:1}
|
| 21 |
+
.lang-toggle{display:flex;gap:5px;flex-shrink:0}
|
| 22 |
+
.lang-btn{background:rgba(255,255,255,.18);border:none;color:rgba(255,255,255,.8);font-size:12px;
|
| 23 |
+
font-weight:800;padding:5px 10px;border-radius:8px;cursor:pointer;letter-spacing:.4px}
|
| 24 |
+
.lang-btn.on{background:rgba(255,255,255,.45);color:#fff}
|
| 25 |
main{max-width:680px;margin:0 auto;padding:16px}
|
| 26 |
.sub{color:var(--muted);font-size:15px;margin:2px 0 18px}
|
| 27 |
/* Home action cards */
|
|
|
|
| 102 |
#toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%);background:#16202c;color:#fff;
|
| 103 |
padding:12px 18px;border-radius:24px;opacity:0;transition:.3s;z-index:99;font-weight:600;max-width:90%}
|
| 104 |
#toast.show{opacity:1}
|
| 105 |
+
/* Visionneuse plein écran */
|
| 106 |
.candidate img,.lead img,.game-pair img{cursor:zoom-in}
|
|
|
|
| 107 |
.zwrap{position:relative;display:inline-block;flex:none;line-height:0}
|
| 108 |
.zwrap .zbadge{position:absolute;right:4px;bottom:4px;background:rgba(8,12,18,.66);color:#fff;
|
| 109 |
border-radius:9px;font-size:12px;padding:1px 6px;pointer-events:none;line-height:1.5}
|
|
|
|
| 118 |
footer{max-width:680px;margin:8px auto 28px;padding:18px 16px 0;border-top:1px solid var(--line);
|
| 119 |
text-align:center;color:var(--muted);font-size:12.5px;line-height:1.7}
|
| 120 |
footer a{color:var(--muted);text-decoration:underline;cursor:pointer}
|
| 121 |
+
/* Page légale */
|
| 122 |
.legal p{font-size:14.5px;color:#2b3a48;margin:10px 0}
|
| 123 |
.legal ul{font-size:14.5px;color:#2b3a48;padding-left:18px;margin:8px 0}
|
| 124 |
.legal li{margin:7px 0}
|
| 125 |
.legal a{color:var(--accent2)}
|
| 126 |
+
/* FAQ */
|
| 127 |
+
.faq{margin-top:168px;margin-bottom:90px}
|
| 128 |
.faq h3{font-size:12px;font-weight:700;color:var(--muted);letter-spacing:.6px;text-transform:uppercase;margin:0 0 2px}
|
| 129 |
.faq details{border-bottom:1px solid var(--line)}
|
| 130 |
.faq summary{font-weight:600;cursor:pointer;font-size:14px;color:var(--ink);list-style:none;display:flex;gap:9px;padding:12px 2px}
|
|
|
|
| 139 |
<header>
|
| 140 |
<button class="back" id="backBtn" onclick="goHome()">‹</button>
|
| 141 |
<h1 id="hTitle">🐱 Chats perdus (version Beta)</h1>
|
| 142 |
+
<div class="lang-toggle">
|
| 143 |
+
<button class="lang-btn on" id="btn_lang_fr" onclick="setLang('fr')">FR</button>
|
| 144 |
+
<button class="lang-btn" id="btn_lang_en" onclick="setLang('en')">EN</button>
|
| 145 |
+
</div>
|
| 146 |
</header>
|
| 147 |
<main>
|
| 148 |
|
| 149 |
<!-- ===================== ACCUEIL ===================== -->
|
| 150 |
<section id="home">
|
| 151 |
+
<p class="sub" data-i18n="home_sub">Entraide communautaire pour retrouver nos compagnons.</p>
|
| 152 |
|
|
|
|
| 153 |
<div class="home">
|
| 154 |
<button class="action hero hero-lost" onclick="show('report')">
|
| 155 |
<div class="ico">🚨</div><div>
|
| 156 |
+
<span class="tag tag-lost" data-i18n="tag_owner">Propriétaire</span>
|
| 157 |
+
<div class="t" data-i18n="hero_lost_title">J'ai perdu mon chat</div>
|
| 158 |
+
<div class="d" data-i18n-html="hero_lost_desc">Déclare sa disparition en envoyant quelques photos (et son affiche, si tu en as une). Si un passant le croise et le prend en photo, notre IA le reconnaîtra.<br><br>
|
| 159 |
• Alerte immédiate : Tu reçois un courriel dès qu'il est repéré.<br>
|
| 160 |
• Vie privée protégée : Tu es le seul à décider de contacter le trouveur. Ton courriel et tes coordonnées ne sont jamais divulgués.</div></div>
|
| 161 |
</button>
|
| 162 |
<button class="action hero hero-seen" onclick="show('check')">
|
| 163 |
<div class="ico">📷</div><div>
|
| 164 |
+
<span class="tag tag-seen" data-i18n="tag_finder">Passant / Trouveur</span>
|
| 165 |
+
<div class="t" data-i18n="hero_seen_title">J'ai vu un chat</div>
|
| 166 |
+
<div class="d" data-i18n-html="hero_seen_desc">Prends une photo du chat croisé qui semble perdu. L'IA compare instantanément sa ressemblance avec notre base de données et te montre les profils similaires.<br><br>
|
| 167 |
• Signalement rapide : Si tu reconnais le chat, valide la correspondance pour envoyer une notification automatique et anonyme à sa famille.</div></div>
|
| 168 |
</button>
|
| 169 |
<button class="action hero hero-follow" onclick="show('follow')">
|
| 170 |
<div class="ico">ℹ️</div><div>
|
| 171 |
+
<span class="tag tag-follow" data-i18n="tag_follow">Suivi</span>
|
| 172 |
+
<div class="t" data-i18n="hero_follow_title">Suivre mon signalement</div>
|
| 173 |
+
<div class="d" data-i18n="hero_follow_desc">Accède à ton espace pour gérer ton alerte ou clôturer la recherche.</div></div>
|
| 174 |
</button>
|
| 175 |
</div>
|
| 176 |
|
| 177 |
+
<p class="sub" style="margin-top:24px;margin-bottom:8px" data-i18n="community_sub">Aider la communauté :</p>
|
|
|
|
| 178 |
<div class="secondary">
|
| 179 |
<button class="action mini" onclick="show('poster')">
|
| 180 |
+
<div class="ico">📋</div><div><div class="t" data-i18n="mini_poster_title">Partager une affiche vue</div>
|
| 181 |
+
<div class="d" data-i18n="mini_poster_desc">Enrichir la banque</div></div></button>
|
| 182 |
<button class="action mini" onclick="show('train')">
|
| 183 |
+
<div class="ico">🧠</div><div><div class="t" data-i18n="mini_train_title">Aider l'IA</div>
|
| 184 |
+
<div class="d" data-i18n="mini_train_desc">Photos + jeu de reconnaissance</div></div></button>
|
| 185 |
</div>
|
| 186 |
|
|
|
|
| 187 |
<div class="faq">
|
| 188 |
+
<h3 data-i18n="faq_title">Questions fréquentes</h3>
|
| 189 |
+
<details><summary data-i18n="faq_q1">Comment ça fonctionne ?</summary>
|
| 190 |
+
<p data-i18n="faq_a1">Tu déclares ton chat perdu avec ses photos. Quand quelqu'un croise un chat, il en
|
| 191 |
prend une photo : l'IA la compare aux chats perdus et montre les ressemblances. Si la
|
| 192 |
personne reconnaît ton chat, tu reçois un signalement par courriel.</p></details>
|
| 193 |
+
<details><summary data-i18n="faq_q2">Mes informations restent-elles privées ?</summary>
|
| 194 |
+
<p data-i18n="faq_a2">Oui. Aucun annuaire public : la personne qui repère un chat ne voit jamais tes
|
| 195 |
coordonnées. On te rejoint par un code envoyé à ton courriel, et tu peux tout
|
| 196 |
supprimer quand tu veux.</p></details>
|
| 197 |
+
<details><summary data-i18n="faq_q3">Est-ce gratuit ?</summary>
|
| 198 |
+
<p data-i18n="faq_a3">Oui, entièrement. C'est un projet d'entraide communautaire, sans publicité.</p></details>
|
| 199 |
+
<details><summary data-i18n="faq_q4">Quelle technologie derrière ?</summary>
|
| 200 |
+
<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
|
| 201 |
chaque photo en une « empreinte » visuelle. On compare ces empreintes pour repérer les
|
| 202 |
chats qui se ressemblent, puis un humain confirme toujours la correspondance.</p></details>
|
| 203 |
</div>
|
|
|
|
| 205 |
|
| 206 |
<!-- ===================== 1. VÉRIFIER ===================== -->
|
| 207 |
<section id="check" class="hidden">
|
| 208 |
+
<div class="privacy" data-i18n="check_privacy">🔒 On ne montre que les chats qui ressemblent à ta photo. Le contact du
|
| 209 |
propriétaire reste privé — c'est lui qui te recontacte.</div>
|
| 210 |
<div class="card">
|
| 211 |
+
<h2 data-i18n="check_h2">Tu as croisé un chat ?</h2>
|
| 212 |
+
<label data-i18n="check_photo_label">Photo du chat</label>
|
| 213 |
+
<label class="photo-btn" for="c_photo" data-i18n="check_photo_btn">📷 Prendre / choisir une photo</label>
|
| 214 |
<input class="file-hidden" id="c_photo" type="file" accept="image/*" onchange="prev('c_photo','c_prev')" />
|
| 215 |
<div class="thumbs" id="c_prev"></div>
|
| 216 |
+
<div class="hint" id="c_gps" data-i18n="check_gps">📍 Position : non captée (optionnel, aide au tri)</div>
|
| 217 |
+
<button class="btn btn-ghost" onclick="getPos('c')" data-i18n="getpos_btn">Capter ma position</button>
|
| 218 |
+
<button class="btn btn-go" onclick="doCheck()" data-i18n="check_btn">🔍 Vérifier</button>
|
| 219 |
<div class="search-prog" id="c_prog"><div class="bar"><div id="c_prog_fill"></div></div>
|
| 220 |
<div class="lbl"><span class="dot"></span><span id="c_prog_txt"></span></div></div>
|
| 221 |
</div>
|
| 222 |
<div id="c_result"></div>
|
| 223 |
<div id="c_leadbox" class="card hidden">
|
| 224 |
+
<h2 id="c_leadtitle" data-i18n="lead_title">Prévenir le propriétaire 🐾</h2>
|
| 225 |
+
<p class="hint" data-i18n="lead_hint">Laisse TES coordonnées : le propriétaire te recontactera.</p>
|
| 226 |
+
<label data-i18n="lead_contact_label">Ton contact (téléphone / courriel)</label>
|
| 227 |
<input id="c_contact" placeholder="514-555-1234" />
|
| 228 |
+
<label data-i18n="lead_msg_label">Message (où, quand, état du chat)</label>
|
| 229 |
+
<textarea id="c_msg" rows="2" data-i18n-placeholder="lead_msg_placeholder" placeholder="Vu ce matin près du parc"></textarea>
|
| 230 |
+
<button class="btn btn-primary" onclick="sendLead()" data-i18n="lead_send_btn">Envoyer au propriétaire</button>
|
| 231 |
</div>
|
| 232 |
</section>
|
| 233 |
|
| 234 |
<!-- ===================== 2. SIGNALER ===================== -->
|
| 235 |
<section id="report" class="hidden">
|
| 236 |
+
<div class="privacy" data-i18n-html="report_privacy">🔒 Ton contact reste privé. Tu reçois un <b>code secret</b> pour suivre les
|
| 237 |
signalements. Plus tu mets de photos, mieux l'IA reconnaît ton chat.</div>
|
| 238 |
<div class="card">
|
| 239 |
+
<h2 data-i18n="report_h2">Signaler mon chat perdu</h2>
|
| 240 |
+
<label data-i18n="report_poster_label">Tu as une affiche ? (photo ou PDF — optionnel)</label>
|
| 241 |
+
<label class="photo-btn" for="r_poster" data-i18n="report_poster_btn">🖼️ Charger l'affiche (remplit le formulaire)</label>
|
| 242 |
<input class="file-hidden" id="r_poster" type="file" accept="image/*,application/pdf" onchange="posterPrefill('r_poster')" />
|
| 243 |
<div class="hint" id="r_poster_note"></div>
|
| 244 |
|
| 245 |
+
<label data-i18n="report_name_label">Nom du chat</label>
|
| 246 |
+
<input id="r_name" data-i18n-placeholder="report_name_placeholder" placeholder="Tigrou" />
|
| 247 |
+
<label data-i18n="report_desc_label">Description (robe, signes distinctifs)</label>
|
| 248 |
+
<textarea id="r_desc" rows="2" data-i18n-placeholder="report_desc_placeholder" placeholder="Tortie, médaillon blanc, timide"></textarea>
|
| 249 |
+
<label data-i18n="report_email_label">Ton courriel (sert à suivre les signalements — jamais montré aux autres)</label>
|
| 250 |
+
<input id="r_email" type="email" data-i18n-placeholder="report_email_placeholder" placeholder="toi@exemple.com" />
|
| 251 |
+
<label data-i18n="report_phone_label">Ton téléphone (optionnel, privé)</label>
|
| 252 |
<input id="r_phone" type="tel" placeholder="514-555-1234" />
|
| 253 |
+
<label data-i18n="report_details_label">Détails supplémentaires (optionnel)</label>
|
| 254 |
+
<textarea id="r_details" rows="2" data-i18n-placeholder="report_details_placeholder" placeholder="Disparu le 3 juin, secteur Rosemont, collier bleu…"></textarea>
|
| 255 |
|
| 256 |
+
<label data-i18n-html="report_photos_label">Photos du chat — <b>5 à 10 recommandé</b> 🎯</label>
|
| 257 |
+
<label class="photo-btn" for="r_photos" data-i18n="report_photos_btn">📷 Ajouter des photos du chat</label>
|
| 258 |
<input class="file-hidden" id="r_photos" type="file" accept="image/*" multiple onchange="prev('r_photos','r_prev'); photoHint()" />
|
| 259 |
<div class="thumbs" id="r_prev"></div>
|
| 260 |
+
<div class="hint" id="r_hint" data-i18n="report_photo_hint">Astuce : plusieurs angles et éclairages = bien plus fiable.</div>
|
| 261 |
+
<div class="hint" id="r_gps" data-i18n="report_gps">📍 Dernière position connue : non captée</div>
|
| 262 |
+
<button class="btn btn-ghost" onclick="getPos('r')" data-i18n="report_getpos_btn">Capter la position</button>
|
| 263 |
+
<button class="btn btn-primary" onclick="doReport()" data-i18n="report_btn">🚨 Déclarer (obtenir mon code)</button>
|
| 264 |
<div class="search-prog" id="r_prog"><div class="bar"><div id="r_prog_fill"></div></div>
|
| 265 |
<div class="lbl"><span class="dot"></span><span id="r_prog_txt"></span></div></div>
|
| 266 |
<div id="r_token"></div>
|
|
|
|
| 269 |
|
| 270 |
<!-- ===================== SUIVRE UN SIGNALEMENT ===================== -->
|
| 271 |
<section id="follow" class="hidden">
|
| 272 |
+
<div class="privacy" data-i18n="follow_privacy">🔒 Accès sécurisé : on t'envoie un code par courriel pour vérifier
|
| 273 |
que c'est bien toi. Tu vois alors quand tu as déclaré ton chat, combien de fois l'IA
|
| 274 |
l'a comparé, et les signalements reçus.</div>
|
| 275 |
<div class="card">
|
| 276 |
+
<h2 data-i18n="follow_h2">Suivre mon signalement</h2>
|
| 277 |
+
<label data-i18n="follow_email_label">Ton courriel (celui de la déclaration)</label>
|
| 278 |
+
<input id="r_track_email" type="email" data-i18n-placeholder="follow_email_placeholder" placeholder="toi@exemple.com" />
|
| 279 |
+
<button class="btn btn-ghost" onclick="requestCode()" data-i18n="follow_request_btn">📩 Recevoir mon code</button>
|
| 280 |
<div id="r_code_box" class="hidden">
|
| 281 |
+
<label data-i18n="follow_code_label">Code reçu</label>
|
| 282 |
<input id="r_track_code" inputmode="numeric" placeholder="123456" />
|
| 283 |
+
<button class="btn btn-go" onclick="verifyCode()" data-i18n="follow_verify_btn">Voir mon suivi</button>
|
| 284 |
</div>
|
| 285 |
<div id="r_track"></div>
|
| 286 |
</div>
|
|
|
|
| 288 |
|
| 289 |
<!-- ===================== 3. PARTAGER UNE AFFICHE ===================== -->
|
| 290 |
<section id="poster" class="hidden">
|
| 291 |
+
<div class="privacy" data-i18n="poster_privacy">📋 Les affiches sont publiques. En les partageant, tu enrichis la banque et
|
| 292 |
aides d'autres personnes à retrouver leur chat.</div>
|
| 293 |
<div class="card">
|
| 294 |
+
<h2 data-i18n="poster_h2">Partager une affiche trouvée</h2>
|
| 295 |
+
<label data-i18n="poster_file_label">Photo ou PDF de l'affiche</label>
|
| 296 |
+
<label class="photo-btn" for="p_file" data-i18n="poster_file_btn">📷 Prendre / charger l'affiche</label>
|
| 297 |
<input class="file-hidden" id="p_file" type="file" accept="image/*,application/pdf" onchange="prev('p_file','p_prev')" />
|
| 298 |
<div class="thumbs" id="p_prev"></div>
|
| 299 |
+
<label data-i18n="poster_text_label">Infos visibles sur l'affiche (optionnel)</label>
|
| 300 |
+
<textarea id="p_text" rows="3" data-i18n-placeholder="poster_text_placeholder" placeholder="Nom, secteur, description… (ce qui est écrit sur l'affiche)"></textarea>
|
| 301 |
+
<div class="hint" id="p_gps" data-i18n="poster_gps">📍 Où l'as-tu vue ? (optionnel)</div>
|
| 302 |
+
<button class="btn btn-ghost" onclick="getPos('p')" data-i18n="getpos_btn">Capter ma position</button>
|
| 303 |
+
<button class="btn btn-go" onclick="doPoster()" data-i18n="poster_btn">📋 Ajouter à la banque</button>
|
| 304 |
<div id="p_result"></div>
|
| 305 |
</div>
|
| 306 |
</section>
|
| 307 |
|
| 308 |
<!-- ===================== 4. ENTRAÎNER L'IA ===================== -->
|
| 309 |
<section id="train" class="hidden">
|
| 310 |
+
<div class="privacy" data-i18n="train_privacy">🧠 Aide volontaire. Ces photos servent UNIQUEMENT à entraîner l'IA — elles
|
| 311 |
ne sont jamais mêlées aux fiches privées de chats perdus.</div>
|
| 312 |
<div class="card">
|
| 313 |
+
<h2 data-i18n="train_photos_h2">Donner des photos</h2>
|
| 314 |
+
<p class="hint" data-i18n="train_photos_hint">Ajoute des photos de chats (le tien, ou des chats croisés) pour aider l'IA à mieux reconnaître.</p>
|
| 315 |
+
<label class="photo-btn" for="t_photos" data-i18n="train_photos_btn">📷 Ajouter des photos</label>
|
| 316 |
<input class="file-hidden" id="t_photos" type="file" accept="image/*" multiple onchange="prev('t_photos','t_prev')" />
|
| 317 |
<div class="thumbs" id="t_prev"></div>
|
| 318 |
<label style="display:flex;gap:10px;align-items:flex-start;font-weight:400;font-size:13.5px;margin-top:12px">
|
| 319 |
<input id="t_consent" type="checkbox" style="width:auto;margin-top:2px" />
|
| 320 |
+
<span data-i18n-html="train_consent">J'accepte que ces images soient utilisées pour <b>entraîner le modèle de
|
| 321 |
reconnaissance</b>. Elles ne seront pas mêlées aux fiches de chats perdus.</span>
|
| 322 |
</label>
|
| 323 |
+
<button class="btn btn-go" onclick="doContribute()" data-i18n="train_photos_submit">Donner ces photos</button>
|
| 324 |
</div>
|
| 325 |
<div class="card">
|
| 326 |
+
<h2 data-i18n="train_game_h2">Jouer : même chat ou pas ?</h2>
|
| 327 |
+
<p class="hint" data-i18n="train_game_hint">Regarde deux photos et dis si c'est le même chat. Chaque réponse aide l'IA.</p>
|
| 328 |
+
<div id="game"><button class="btn btn-go" onclick="nextPair()" data-i18n="train_game_start">Commencer à jouer</button></div>
|
| 329 |
<div class="hint" id="t_stats"></div>
|
| 330 |
</div>
|
| 331 |
</section>
|
| 332 |
|
| 333 |
<!-- ===================== AVIS LÉGAL & CONFIDENTIALITÉ ===================== -->
|
| 334 |
<section id="legal" class="hidden">
|
| 335 |
+
<div class="card legal" id="legal_content">
|
| 336 |
<h2>Avis de non-responsabilité</h2>
|
| 337 |
<p>Allo Chat est un service communautaire <b>gratuit</b> d'entraide pour aider à
|
| 338 |
retrouver des chats perdus, à Montréal et ailleurs.</p>
|
|
|
|
| 350 |
<p><b>Conduite des utilisateurs.</b> Vous êtes responsable des renseignements que vous
|
| 351 |
publiez et de vos échanges. Faites preuve de prudence lors d'un contact avec une
|
| 352 |
personne inconnue, et ne tentez pas d'approcher un animal craintif ou en détresse.</p>
|
|
|
|
| 353 |
<h2 style="margin-top:22px">Confidentialité — Loi 25 (Québec)</h2>
|
| 354 |
<p>Allo Chat respecte la <i>Loi sur la protection des renseignements personnels dans le
|
| 355 |
secteur privé</i> (Loi 25). Voici l'essentiel.</p>
|
|
|
|
| 389 |
|
| 390 |
</main>
|
| 391 |
|
| 392 |
+
<footer id="footer_el">
|
| 393 |
🐾 Fait à Montréal · © 2026 Allo Chat ·
|
| 394 |
<a onclick="show('legal')">Avis légal & confidentialité</a><br>
|
| 395 |
Contact : <a href="mailto:info@allochat.org">info@allochat.org</a>
|
|
|
|
| 399 |
<div id="lightbox" onclick="closeZoom(event)">
|
| 400 |
<button class="x" onclick="closeZoom(event)" aria-label="Fermer">✕</button>
|
| 401 |
<img id="lightbox_img" alt="Photo du chat en grand">
|
| 402 |
+
<div class="hint-zoom" data-i18n="lightbox_hint">Touche l'image ou appuie sur ✕ pour fermer</div>
|
| 403 |
</div>
|
| 404 |
|
| 405 |
<script>
|
| 406 |
const API = location.origin;
|
| 407 |
const pos = {c:null,r:null,p:null};
|
| 408 |
let lastSightingId=null, chosenCandidateToken=null, gamePair=null;
|
| 409 |
+
let lastMatches=[], ownerCats=[];
|
| 410 |
|
|
|
|
|
|
|
| 411 |
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>(
|
| 412 |
{'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 413 |
|
| 414 |
+
// ---- TRADUCTIONS / TRANSLATIONS ----
|
| 415 |
+
const T = {
|
| 416 |
+
fr: {
|
| 417 |
+
home_sub: 'Entraide communautaire pour retrouver nos compagnons.',
|
| 418 |
+
tag_owner: 'Propriétaire',
|
| 419 |
+
hero_lost_title: "J'ai perdu mon chat",
|
| 420 |
+
hero_lost_desc: "Déclare sa disparition en envoyant quelques photos (et son affiche, si tu en as une). Si un passant le croise et le prend en photo, notre IA le reconnaîtra.<br><br>• Alerte immédiate : Tu reçois un courriel dès qu'il est repéré.<br>• Vie privée protégée : Tu es le seul à décider de contacter le trouveur. Ton courriel et tes coordonnées ne sont jamais divulgués.",
|
| 421 |
+
tag_finder: 'Passant / Trouveur',
|
| 422 |
+
hero_seen_title: "J'ai vu un chat",
|
| 423 |
+
hero_seen_desc: "Prends une photo du chat croisé qui semble perdu. L'IA compare instantanément sa ressemblance avec notre base de données et te montre les profils similaires.<br><br>• Signalement rapide : Si tu reconnais le chat, valide la correspondance pour envoyer une notification automatique et anonyme à sa famille.",
|
| 424 |
+
tag_follow: 'Suivi',
|
| 425 |
+
hero_follow_title: 'Suivre mon signalement',
|
| 426 |
+
hero_follow_desc: 'Accède à ton espace pour gérer ton alerte ou clôturer la recherche.',
|
| 427 |
+
community_sub: 'Aider la communauté :',
|
| 428 |
+
mini_poster_title: 'Partager une affiche vue',
|
| 429 |
+
mini_poster_desc: 'Enrichir la banque',
|
| 430 |
+
mini_train_title: "Aider l'IA",
|
| 431 |
+
mini_train_desc: 'Photos + jeu de reconnaissance',
|
| 432 |
+
faq_title: 'Questions fréquentes',
|
| 433 |
+
faq_q1: 'Comment ça fonctionne\u00a0?',
|
| 434 |
+
faq_a1: "Tu déclares ton chat perdu avec ses photos. Quand quelqu'un croise un chat, il en prend une photo : l'IA la compare aux chats perdus et montre les ressemblances. Si la personne reconnaît ton chat, tu reçois un signalement par courriel.",
|
| 435 |
+
faq_q2: 'Mes informations restent-elles privées\u00a0?',
|
| 436 |
+
faq_a2: "Oui. Aucun annuaire public : la personne qui repère un chat ne voit jamais tes coordonnées. On te rejoint par un code envoyé à ton courriel, et tu peux tout supprimer quand tu veux.",
|
| 437 |
+
faq_q3: 'Est-ce gratuit\u00a0?',
|
| 438 |
+
faq_a3: "Oui, entièrement. C'est un projet d'entraide communautaire, sans publicité.",
|
| 439 |
+
faq_q4: 'Quelle technologie derrière\u00a0?',
|
| 440 |
+
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.',
|
| 441 |
+
title_home: '🐱 Chats perdus (version Beta)',
|
| 442 |
+
title_check: "📷 J'ai vu un chat",
|
| 443 |
+
title_report: "🚨 J'ai perdu mon chat",
|
| 444 |
+
title_follow: 'ℹ️ Suivre mon signalement',
|
| 445 |
+
title_poster: '📋 Partager une affiche',
|
| 446 |
+
title_train: "🧠 Aider l'IA",
|
| 447 |
+
title_legal: '⚖️ Avis légal & confidentialité',
|
| 448 |
+
check_privacy: "🔒 On ne montre que les chats qui ressemblent à ta photo. Le contact du propriétaire reste privé — c'est lui qui te recontacte.",
|
| 449 |
+
check_h2: 'Tu as croisé un chat\u00a0?',
|
| 450 |
+
check_photo_label: 'Photo du chat',
|
| 451 |
+
check_photo_btn: '📷 Prendre / choisir une photo',
|
| 452 |
+
check_gps: '📍 Position : non captée (optionnel, aide au tri)',
|
| 453 |
+
getpos_btn: 'Capter ma position',
|
| 454 |
+
check_btn: '🔍 Vérifier',
|
| 455 |
+
lead_title: 'Prévenir le propriétaire 🐾',
|
| 456 |
+
lead_hint: 'Laisse TES coordonnées : le propriétaire te recontactera.',
|
| 457 |
+
lead_contact_label: 'Ton contact (téléphone / courriel)',
|
| 458 |
+
lead_msg_label: 'Message (où, quand, état du chat)',
|
| 459 |
+
lead_msg_placeholder: 'Vu ce matin près du parc',
|
| 460 |
+
lead_send_btn: 'Envoyer au propriétaire',
|
| 461 |
+
report_privacy: "🔒 Ton contact reste privé. Tu reçois un <b>code secret</b> pour suivre les signalements. Plus tu mets de photos, mieux l'IA reconnaît ton chat.",
|
| 462 |
+
report_h2: 'Signaler mon chat perdu',
|
| 463 |
+
report_poster_label: 'Tu as une affiche\u00a0? (photo ou PDF — optionnel)',
|
| 464 |
+
report_poster_btn: "🖼️ Charger l'affiche (remplit le formulaire)",
|
| 465 |
+
report_name_label: 'Nom du chat',
|
| 466 |
+
report_name_placeholder: 'Tigrou',
|
| 467 |
+
report_desc_label: 'Description (robe, signes distinctifs)',
|
| 468 |
+
report_desc_placeholder: 'Tortie, médaillon blanc, timide',
|
| 469 |
+
report_email_label: 'Ton courriel (sert à suivre les signalements — jamais montré aux autres)',
|
| 470 |
+
report_email_placeholder: 'toi@exemple.com',
|
| 471 |
+
report_phone_label: 'Ton téléphone (optionnel, privé)',
|
| 472 |
+
report_details_label: 'Détails supplémentaires (optionnel)',
|
| 473 |
+
report_details_placeholder: 'Disparu le 3 juin, secteur Rosemont, collier bleu…',
|
| 474 |
+
report_photos_label: 'Photos du chat — <b>5 à 10 recommandé</b> 🎯',
|
| 475 |
+
report_photos_btn: '📷 Ajouter des photos du chat',
|
| 476 |
+
report_photo_hint: 'Astuce : plusieurs angles et éclairages = bien plus fiable.',
|
| 477 |
+
report_gps: '📍 Dernière position connue : non captée',
|
| 478 |
+
report_getpos_btn: 'Capter la position',
|
| 479 |
+
report_btn: '🚨 Déclarer (obtenir mon code)',
|
| 480 |
+
follow_privacy: "🔒 Accès sécurisé : on t'envoie un code par courriel pour vérifier que c'est bien toi. Tu vois alors quand tu as déclaré ton chat, combien de fois l'IA l'a comparé, et les signalements reçus.",
|
| 481 |
+
follow_h2: 'Suivre mon signalement',
|
| 482 |
+
follow_email_label: 'Ton courriel (celui de la déclaration)',
|
| 483 |
+
follow_email_placeholder: 'toi@exemple.com',
|
| 484 |
+
follow_request_btn: '📩 Recevoir mon code',
|
| 485 |
+
follow_code_label: 'Code reçu',
|
| 486 |
+
follow_verify_btn: 'Voir mon suivi',
|
| 487 |
+
poster_privacy: "📋 Les affiches sont publiques. En les partageant, tu enrichis la banque et aides d'autres personnes à retrouver leur chat.",
|
| 488 |
+
poster_h2: 'Partager une affiche trouvée',
|
| 489 |
+
poster_file_label: "Photo ou PDF de l'affiche",
|
| 490 |
+
poster_file_btn: "📷 Prendre / charger l'affiche",
|
| 491 |
+
poster_text_label: "Infos visibles sur l'affiche (optionnel)",
|
| 492 |
+
poster_text_placeholder: "Nom, secteur, description… (ce qui est écrit sur l'affiche)",
|
| 493 |
+
poster_gps: "📍 Où l'as-tu vue ? (optionnel)",
|
| 494 |
+
poster_btn: '📋 Ajouter à la banque',
|
| 495 |
+
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.",
|
| 496 |
+
train_photos_h2: 'Donner des photos',
|
| 497 |
+
train_photos_hint: "Ajoute des photos de chats (le tien, ou des chats croisés) pour aider l'IA à mieux reconnaître.",
|
| 498 |
+
train_photos_btn: '📷 Ajouter des photos',
|
| 499 |
+
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.",
|
| 500 |
+
train_photos_submit: 'Donner ces photos',
|
| 501 |
+
train_game_h2: 'Jouer : même chat ou pas\u00a0?',
|
| 502 |
+
train_game_hint: "Regarde deux photos et dis si c'est le même chat. Chaque réponse aide l'IA.",
|
| 503 |
+
train_game_start: 'Commencer à jouer',
|
| 504 |
+
lightbox_hint: 'Touche l\'image ou appuie sur ✕ pour fermer',
|
| 505 |
+
footer_html: "🐾 Fait à Montréal · © 2026 Allo Chat · <a onclick=\"show('legal')\">Avis légal & confidentialité</a><br>Contact : <a href=\"mailto:info@allochat.org\">info@allochat.org</a>",
|
| 506 |
+
legal_html: `<h2>Avis de non-responsabilité</h2>
|
| 507 |
+
<p>Allo Chat est un service communautaire <b>gratuit</b> d'entraide pour aider à retrouver des chats perdus, à Montréal et ailleurs.</p>
|
| 508 |
+
<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>
|
| 509 |
+
<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>
|
| 510 |
+
<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>
|
| 511 |
+
<h2 style="margin-top:22px">Confidentialité — Loi 25 (Québec)</h2>
|
| 512 |
+
<ul>
|
| 513 |
+
<li><b>Responsable :</b> Équipe Allo Chat — <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
|
| 514 |
+
<li><b>Données recueillies :</b> photos de chats, courriel, téléphone (facultatif), description, position (facultative).</li>
|
| 515 |
+
<li><b>Finalité unique :</b> mettre en relation propriétaires et trouveurs. <b>Aucune revente, aucune publicité, aucun profilage.</b></li>
|
| 516 |
+
<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>
|
| 517 |
+
<li><b>Droit à l'oubli :</b> suppression définitive de votre fiche à tout moment via « Suivre mon signalement ».</li>
|
| 518 |
+
<li><b>Hébergement :</b> serveurs aux États-Unis, protégés (accès restreint, liens signés, chiffrement en transit).</li>
|
| 519 |
+
<li><b>Droits :</b> accès, rectification, suppression → <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
|
| 520 |
+
</ul>
|
| 521 |
+
<p class="hint" style="margin-top:14px">Dernière mise à jour : juin 2026.</p>`,
|
| 522 |
+
// JS dynamic strings
|
| 523 |
+
js_add_photo: 'Ajoute une photo du chat',
|
| 524 |
+
js_net_err: 'Erreur réseau — réessaie',
|
| 525 |
+
js_rate: ' — trop de requêtes, patiente une minute',
|
| 526 |
+
js_analysis_done: '✅ Analyse terminée',
|
| 527 |
+
js_no_match: 'Aucune correspondance',
|
| 528 |
+
js_match_q: 'Reconnais-tu l\'un d\'eux\u00a0?',
|
| 529 |
+
js_match_count: n => `${n} chat(s) qui ressemble(nt)`,
|
| 530 |
+
js_conf_strong: 'Forte', js_conf_medium: 'Moyenne', js_conf_weak: 'Faible',
|
| 531 |
+
js_its_him: "C'est lui ✅",
|
| 532 |
+
js_view_poster: '🖼️ Voir l\'affiche',
|
| 533 |
+
js_no_poster_photo: 'Aucune photo d\'affiche disponible.',
|
| 534 |
+
js_cat_default: 'Chat perdu',
|
| 535 |
+
js_choose_cat_prompt: 'Choisis un chat',
|
| 536 |
+
js_lead_sent: '<div class="result match"><div class="big">🙏</div><b>Merci !</b><p class="hint">Le propriétaire a reçu ton signalement.</p></div>',
|
| 537 |
+
js_email_required: 'Entre ton courriel (pour suivre les signalements)',
|
| 538 |
+
js_email_short: 'Entre ton courriel',
|
| 539 |
+
js_code_invalid: '<p class="hint">Code invalide ou expiré. Redemande un code.</p>',
|
| 540 |
+
js_no_cats: '<p class="hint">Aucun chat rattaché à ce courriel.</p>',
|
| 541 |
+
js_logout: 'Déconnecté 🔒',
|
| 542 |
+
js_logout_btn: '🔒 Se déconnecter',
|
| 543 |
+
js_report_label: 'Signalement',
|
| 544 |
+
js_no_message: '(pas de message)',
|
| 545 |
+
js_no_contact: '(pas de contact)',
|
| 546 |
+
js_no_leads: 'Aucun signalement pour l\'instant.',
|
| 547 |
+
js_declared_on: 'déclaré le',
|
| 548 |
+
js_ai_compare: 'comparaisons IA 🔎',
|
| 549 |
+
js_leads_received: 'signalement(s) reçu(s)',
|
| 550 |
+
js_my_cat: 'Mon chat',
|
| 551 |
+
js_status: 'statut',
|
| 552 |
+
js_mark_found: 'Marquer « retrouvé » 🎉',
|
| 553 |
+
js_delete: '🗑️ Supprimer définitivement',
|
| 554 |
+
js_leads_title: n => `Signalements reçus (${n})`,
|
| 555 |
+
js_map: '📍 carte',
|
| 556 |
+
js_delete_confirm: (name) => `Supprimer définitivement la fiche de ${name} ?\nPhotos, signalements et contact seront effacés. Irréversible.`,
|
| 557 |
+
js_this_profile: 'cette fiche',
|
| 558 |
+
js_deleted_toast: 'Fiche supprimée 🗑️',
|
| 559 |
+
js_deleted_html: '<p class="hint">Fiche supprimée définitivement (droit à l\'oubli).</p>',
|
| 560 |
+
js_found_toast: 'Marqué retrouvé 🎉',
|
| 561 |
+
js_add_poster: 'Charge une affiche',
|
| 562 |
+
js_poster_added: '<div class="result match"><div class="big">🙏</div><b>Merci !</b><p class="hint">Affiche ajoutée à la banque.</p></div>',
|
| 563 |
+
js_add_photos: 'Ajoute des photos',
|
| 564 |
+
js_consent_required: 'Coche la case de consentement d\'abord 🙏',
|
| 565 |
+
js_sending: 'Envoi…',
|
| 566 |
+
js_photos_donated: photos => `Merci ! ${photos} photos dans le pool 🙏`,
|
| 567 |
+
js_loading: 'Chargement…',
|
| 568 |
+
js_not_enough_photos: 'Pas encore assez de photos données. Ajoute-en d\'abord ci-dessus 🙂',
|
| 569 |
+
js_same_cat: '😺 Même chat',
|
| 570 |
+
js_diff_cat: '🙀 Différents',
|
| 571 |
+
js_label_thanks: 'Merci ! 🎓',
|
| 572 |
+
js_stats: (photos, labels) => `Pool : ${photos} photos · ${labels} réponses données`,
|
| 573 |
+
js_poster_reading: "Lecture de l'affiche…",
|
| 574 |
+
js_poster_loaded: 'Affiche chargée (lecture auto indisponible).',
|
| 575 |
+
js_photo_hint_few: n => `${n} photo(s) — ajoutes-en plus (vise 5+) pour bien reconnaître.`,
|
| 576 |
+
js_photo_hint_ok: n => `${n} photos — bien ! quelques-unes de plus = idéal.`,
|
| 577 |
+
js_photo_hint_great: n => `${n} photos — excellent ✅`,
|
| 578 |
+
js_gps_unavail: 'Géolocalisation indisponible',
|
| 579 |
+
js_gps_refused: 'Position refusée',
|
| 580 |
+
js_report_success: (name, photos, email) => `<div class="result match" style="margin-top:14px"><b>✅ ${esc(name||'Chat')} déclaré (${photos} photos)</b><p class="hint">Pour voir les signalements et le suivi : retourne à l'accueil → « Suivre mon signalement », entre ton courriel (${email}) et le code qu'on t'enverra. Rien à mémoriser 🙂</p></div>`,
|
| 581 |
+
js_choose_cat_title: name => `Tu reconnais ${name} ? Préviens le propriétaire 🐾`,
|
| 582 |
+
js_prog_embed: name => `🧠 ${name} crée l'empreinte visuelle du chat…`,
|
| 583 |
+
js_prog_compare: '🔎 Comparaison avec les chats perdus enregistrés…',
|
| 584 |
+
js_prog_save: photos => `🧠 ${photos} crée l'empreinte de ${photos}…`,
|
| 585 |
+
js_prog_saving: '💾 Enregistrement sécurisé de la fiche…',
|
| 586 |
+
js_date_locale: 'fr-CA',
|
| 587 |
+
js_num_locale: 'fr-CA',
|
| 588 |
+
js_code_sent: "Un code vient de t'être envoyé par courriel — vérifie ta boîte 📬",
|
| 589 |
+
},
|
| 590 |
+
en: {
|
| 591 |
+
home_sub: 'Community help to find our companions.',
|
| 592 |
+
tag_owner: 'Owner',
|
| 593 |
+
hero_lost_title: 'I lost my cat',
|
| 594 |
+
hero_lost_desc: 'Report their disappearance by sending a few photos (and a poster, if you have one). If a passerby takes a photo of them, our AI will recognize them.<br><br>• Immediate alert: You receive an email as soon as they\'re spotted.<br>• Privacy protected: You\'re the only one who decides to contact the finder. Your email and contact info are never shared.',
|
| 595 |
+
tag_finder: 'Passerby / Finder',
|
| 596 |
+
hero_seen_title: 'I saw a cat',
|
| 597 |
+
hero_seen_desc: 'Take a photo of the cat you came across who seems lost. The AI instantly compares their likeness with our database and shows you similar profiles.<br><br>• Quick report: If you recognize the cat, confirm the match to send an automatic and anonymous notification to their family.',
|
| 598 |
+
tag_follow: 'Tracking',
|
| 599 |
+
hero_follow_title: 'Track my report',
|
| 600 |
+
hero_follow_desc: 'Access your space to manage your alert or close the search.',
|
| 601 |
+
community_sub: 'Help the community:',
|
| 602 |
+
mini_poster_title: 'Share a found poster',
|
| 603 |
+
mini_poster_desc: 'Enrich the database',
|
| 604 |
+
mini_train_title: 'Help the AI',
|
| 605 |
+
mini_train_desc: 'Photos + recognition game',
|
| 606 |
+
faq_title: 'Frequently asked questions',
|
| 607 |
+
faq_q1: 'How does it work?',
|
| 608 |
+
faq_a1: 'You report your lost cat with photos. When someone comes across a cat, they take a photo: the AI compares it to lost cats and shows similarities. If the person recognizes your cat, you receive a report by email.',
|
| 609 |
+
faq_q2: 'Is my information private?',
|
| 610 |
+
faq_a2: 'Yes. No public directory: the person who spots a cat never sees your contact info. We reach you through a code sent to your email, and you can delete everything at any time.',
|
| 611 |
+
faq_q3: 'Is it free?',
|
| 612 |
+
faq_a3: 'Yes, entirely. It\'s a free community project with no advertising.',
|
| 613 |
+
faq_q4: 'What technology is behind it?',
|
| 614 |
+
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.',
|
| 615 |
+
title_home: '🐱 Lost Cats (Beta)',
|
| 616 |
+
title_check: '📷 I saw a cat',
|
| 617 |
+
title_report: '🚨 I lost my cat',
|
| 618 |
+
title_follow: 'ℹ️ Track my report',
|
| 619 |
+
title_poster: '📋 Share a poster',
|
| 620 |
+
title_train: '🧠 Help the AI',
|
| 621 |
+
title_legal: '⚖️ Legal notice & privacy',
|
| 622 |
+
check_privacy: "🔒 We only show cats that look like your photo. The owner's contact stays private — they will reach out to you.",
|
| 623 |
+
check_h2: 'Did you come across a cat?',
|
| 624 |
+
check_photo_label: 'Cat photo',
|
| 625 |
+
check_photo_btn: '📷 Take / choose a photo',
|
| 626 |
+
check_gps: '📍 Location: not captured (optional, helps sorting)',
|
| 627 |
+
getpos_btn: 'Get my location',
|
| 628 |
+
check_btn: '🔍 Check',
|
| 629 |
+
lead_title: 'Notify the owner 🐾',
|
| 630 |
+
lead_hint: 'Leave YOUR contact info: the owner will reach out to you.',
|
| 631 |
+
lead_contact_label: 'Your contact (phone / email)',
|
| 632 |
+
lead_msg_label: 'Message (where, when, cat\'s condition)',
|
| 633 |
+
lead_msg_placeholder: 'Seen this morning near the park',
|
| 634 |
+
lead_send_btn: 'Send to owner',
|
| 635 |
+
report_privacy: '🔒 Your contact stays private. You\'ll receive a <b>secret code</b> to track reports. More photos = better AI recognition.',
|
| 636 |
+
report_h2: 'Report my lost cat',
|
| 637 |
+
report_poster_label: 'Do you have a poster? (photo or PDF — optional)',
|
| 638 |
+
report_poster_btn: '🖼️ Upload poster (auto-fills form)',
|
| 639 |
+
report_name_label: "Cat's name",
|
| 640 |
+
report_name_placeholder: 'Whiskers',
|
| 641 |
+
report_desc_label: 'Description (coat, distinctive signs)',
|
| 642 |
+
report_desc_placeholder: 'Tortie, white locket, shy',
|
| 643 |
+
report_email_label: 'Your email (used to track reports — never shown to others)',
|
| 644 |
+
report_email_placeholder: 'you@example.com',
|
| 645 |
+
report_phone_label: 'Your phone (optional, private)',
|
| 646 |
+
report_details_label: 'Additional details (optional)',
|
| 647 |
+
report_details_placeholder: 'Missing since June 3, Rosemont area, blue collar…',
|
| 648 |
+
report_photos_label: 'Cat photos — <b>5 to 10 recommended</b> 🎯',
|
| 649 |
+
report_photos_btn: '📷 Add cat photos',
|
| 650 |
+
report_photo_hint: 'Tip: multiple angles and lighting = much more reliable.',
|
| 651 |
+
report_gps: '📍 Last known location: not captured',
|
| 652 |
+
report_getpos_btn: 'Get location',
|
| 653 |
+
report_btn: '🚨 Report (get my code)',
|
| 654 |
+
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.",
|
| 655 |
+
follow_h2: 'Track my report',
|
| 656 |
+
follow_email_label: 'Your email (the one used to report)',
|
| 657 |
+
follow_email_placeholder: 'you@example.com',
|
| 658 |
+
follow_request_btn: '📩 Get my code',
|
| 659 |
+
follow_code_label: 'Code received',
|
| 660 |
+
follow_verify_btn: 'View my tracking',
|
| 661 |
+
poster_privacy: '📋 Posters are public. By sharing them, you enrich the database and help others find their cat.',
|
| 662 |
+
poster_h2: 'Share a found poster',
|
| 663 |
+
poster_file_label: 'Photo or PDF of the poster',
|
| 664 |
+
poster_file_btn: '📷 Take / upload poster',
|
| 665 |
+
poster_text_label: 'Info visible on the poster (optional)',
|
| 666 |
+
poster_text_placeholder: "Name, area, description… (what's written on the poster)",
|
| 667 |
+
poster_gps: '📍 Where did you see it? (optional)',
|
| 668 |
+
poster_btn: '📋 Add to database',
|
| 669 |
+
train_privacy: '🧠 Voluntary help. These photos are used ONLY to train the AI — they are never mixed with private lost cat profiles.',
|
| 670 |
+
train_photos_h2: 'Donate photos',
|
| 671 |
+
train_photos_hint: "Add cat photos (yours, or cats you've come across) to help the AI recognize better.",
|
| 672 |
+
train_photos_btn: '📷 Add photos',
|
| 673 |
+
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.',
|
| 674 |
+
train_photos_submit: 'Donate these photos',
|
| 675 |
+
train_game_h2: 'Game: same cat or not?',
|
| 676 |
+
train_game_hint: "Look at two photos and say if it's the same cat. Each answer helps the AI.",
|
| 677 |
+
train_game_start: 'Start playing',
|
| 678 |
+
lightbox_hint: 'Tap the image or press ✕ to close',
|
| 679 |
+
footer_html: "🐾 Made in Montréal · © 2026 Allo Chat · <a onclick=\"show('legal')\">Legal notice & privacy</a><br>Contact: <a href=\"mailto:info@allochat.org\">info@allochat.org</a>",
|
| 680 |
+
legal_html: `<h2>Disclaimer</h2>
|
| 681 |
+
<p>Allo Chat is a <b>free</b> community service to help find lost cats in Montréal and beyond.</p>
|
| 682 |
+
<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>
|
| 683 |
+
<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>
|
| 684 |
+
<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>
|
| 685 |
+
<h2 style="margin-top:22px">Privacy</h2>
|
| 686 |
+
<ul>
|
| 687 |
+
<li><b>Privacy officer:</b> Allo Chat team — <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
|
| 688 |
+
<li><b>Data collected:</b> cat photos, email (for tracking), phone (optional), description, approximate location (optional).</li>
|
| 689 |
+
<li><b>Sole purpose:</b> connect owners with people who spotted their cat. <b>No resale, no advertising, no profiling.</b></li>
|
| 690 |
+
<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>
|
| 691 |
+
<li><b>Right to erasure:</b> permanently delete your profile (photos, reports, contact) at any time via "Track my report".</li>
|
| 692 |
+
<li><b>Hosting:</b> servers in the United States, protected by security measures (restricted access, signed photo links, encrypted data in transit).</li>
|
| 693 |
+
<li><b>Your rights:</b> access, correction, deletion → <a href="mailto:info@allochat.org">info@allochat.org</a>.</li>
|
| 694 |
+
</ul>
|
| 695 |
+
<p class="hint" style="margin-top:14px">Last updated: June 2026.</p>`,
|
| 696 |
+
// JS dynamic strings
|
| 697 |
+
js_add_photo: 'Add a photo of the cat',
|
| 698 |
+
js_net_err: 'Network error — please retry',
|
| 699 |
+
js_rate: ' — too many requests, please wait a minute',
|
| 700 |
+
js_analysis_done: '✅ Analysis complete',
|
| 701 |
+
js_no_match: 'No match found',
|
| 702 |
+
js_match_q: 'Do you recognize any of them?',
|
| 703 |
+
js_match_count: n => `${n} cat(s) that look similar`,
|
| 704 |
+
js_conf_strong: 'Strong', js_conf_medium: 'Medium', js_conf_weak: 'Weak',
|
| 705 |
+
js_its_him: "That's them ✅",
|
| 706 |
+
js_view_poster: '🖼️ View poster',
|
| 707 |
+
js_no_poster_photo: 'No poster photo available.',
|
| 708 |
+
js_cat_default: 'Lost cat',
|
| 709 |
+
js_choose_cat_prompt: 'Choose a cat',
|
| 710 |
+
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>',
|
| 711 |
+
js_email_required: 'Enter your email (to track reports)',
|
| 712 |
+
js_email_short: 'Enter your email',
|
| 713 |
+
js_code_invalid: '<p class="hint">Invalid or expired code. Request a new one.</p>',
|
| 714 |
+
js_no_cats: '<p class="hint">No cat linked to this email.</p>',
|
| 715 |
+
js_logout: 'Logged out 🔒',
|
| 716 |
+
js_logout_btn: '🔒 Log out',
|
| 717 |
+
js_report_label: 'Report',
|
| 718 |
+
js_no_message: '(no message)',
|
| 719 |
+
js_no_contact: '(no contact)',
|
| 720 |
+
js_no_leads: 'No reports yet.',
|
| 721 |
+
js_declared_on: 'reported on',
|
| 722 |
+
js_ai_compare: 'AI comparisons 🔎',
|
| 723 |
+
js_leads_received: 'report(s) received',
|
| 724 |
+
js_my_cat: 'My cat',
|
| 725 |
+
js_status: 'status',
|
| 726 |
+
js_mark_found: 'Mark as "found" 🎉',
|
| 727 |
+
js_delete: '🗑️ Delete permanently',
|
| 728 |
+
js_leads_title: n => `Reports received (${n})`,
|
| 729 |
+
js_map: '📍 map',
|
| 730 |
+
js_delete_confirm: (name) => `Permanently delete the profile for ${name}?\nPhotos, reports and contact will be erased. Irreversible.`,
|
| 731 |
+
js_this_profile: 'this profile',
|
| 732 |
+
js_deleted_toast: 'Profile deleted 🗑️',
|
| 733 |
+
js_deleted_html: '<p class="hint">Profile permanently deleted (right to erasure).</p>',
|
| 734 |
+
js_found_toast: 'Marked as found 🎉',
|
| 735 |
+
js_add_poster: 'Upload a poster',
|
| 736 |
+
js_poster_added: '<div class="result match"><div class="big">🙏</div><b>Thank you!</b><p class="hint">Poster added to the database.</p></div>',
|
| 737 |
+
js_add_photos: 'Add photos',
|
| 738 |
+
js_consent_required: 'Please check the consent box first 🙏',
|
| 739 |
+
js_sending: 'Sending…',
|
| 740 |
+
js_photos_donated: photos => `Thank you! ${photos} photos in the pool 🙏`,
|
| 741 |
+
js_loading: 'Loading…',
|
| 742 |
+
js_not_enough_photos: 'Not enough photos yet. Add some above first 🙂',
|
| 743 |
+
js_same_cat: '😺 Same cat',
|
| 744 |
+
js_diff_cat: '🙀 Different',
|
| 745 |
+
js_label_thanks: 'Thank you! 🎓',
|
| 746 |
+
js_stats: (photos, labels) => `Pool: ${photos} photos · ${labels} answers given`,
|
| 747 |
+
js_poster_reading: 'Reading poster…',
|
| 748 |
+
js_poster_loaded: 'Poster uploaded (auto-reading unavailable).',
|
| 749 |
+
js_photo_hint_few: n => `${n} photo(s) — add more (aim for 5+) for better recognition.`,
|
| 750 |
+
js_photo_hint_ok: n => `${n} photos — good! A few more would be ideal.`,
|
| 751 |
+
js_photo_hint_great: n => `${n} photos — excellent ✅`,
|
| 752 |
+
js_gps_unavail: 'Geolocation unavailable',
|
| 753 |
+
js_gps_refused: 'Location denied',
|
| 754 |
+
js_report_success: (name, photos, email) => `<div class="result match" style="margin-top:14px"><b>✅ ${esc(name||'Cat')} reported (${photos} photos)</b><p class="hint">To view reports and tracking: go back to home → "Track my report", enter your email (${email}) and the code we'll send. Nothing to memorize 🙂</p></div>`,
|
| 755 |
+
js_choose_cat_title: name => `Do you recognize ${name}? Notify the owner 🐾`,
|
| 756 |
+
js_prog_embed: name => `🧠 ${name} creating visual fingerprint…`,
|
| 757 |
+
js_prog_compare: '🔎 Comparing with registered lost cats…',
|
| 758 |
+
js_prog_saving: '💾 Securely saving your profile…',
|
| 759 |
+
js_date_locale: 'en-CA',
|
| 760 |
+
js_num_locale: 'en-CA',
|
| 761 |
+
js_code_sent: 'A code has been sent to your email — check your inbox 📬',
|
| 762 |
+
}
|
| 763 |
+
};
|
| 764 |
+
|
| 765 |
+
let LANG = localStorage.getItem('chats_lang') || 'fr';
|
| 766 |
+
|
| 767 |
+
function setLang(lang) {
|
| 768 |
+
LANG = lang;
|
| 769 |
+
localStorage.setItem('chats_lang', lang);
|
| 770 |
+
document.documentElement.lang = lang;
|
| 771 |
+
document.title = lang === 'en' ? 'Lost Cats (Beta) — community' : 'Chats perdus (Beta) — entraide';
|
| 772 |
+
const t = T[lang];
|
| 773 |
+
document.querySelectorAll('[data-i18n]').forEach(el => {
|
| 774 |
+
const k = el.dataset.i18n; if (t[k] !== undefined) el.textContent = t[k];
|
| 775 |
+
});
|
| 776 |
+
document.querySelectorAll('[data-i18n-html]').forEach(el => {
|
| 777 |
+
const k = el.dataset.i18nHtml; if (t[k] !== undefined) el.innerHTML = t[k];
|
| 778 |
+
});
|
| 779 |
+
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
|
| 780 |
+
const k = el.dataset.i18nPlaceholder; if (t[k] !== undefined) el.placeholder = t[k];
|
| 781 |
+
});
|
| 782 |
+
// Legal section (full innerHTML)
|
| 783 |
+
document.getElementById('legal_content').innerHTML = t.legal_html;
|
| 784 |
+
// Footer
|
| 785 |
+
document.getElementById('footer_el').innerHTML = t.footer_html;
|
| 786 |
+
// Lang buttons
|
| 787 |
+
document.getElementById('btn_lang_fr').classList.toggle('on', lang === 'fr');
|
| 788 |
+
document.getElementById('btn_lang_en').classList.toggle('on', lang === 'en');
|
| 789 |
+
// Nav titles
|
| 790 |
+
Object.assign(TITLES, {
|
| 791 |
+
home: t.title_home, check: t.title_check, report: t.title_report,
|
| 792 |
+
follow: t.title_follow, poster: t.title_poster, train: t.title_train, legal: t.title_legal
|
| 793 |
+
});
|
| 794 |
+
const cur = ['home','check','report','follow','poster','train','legal']
|
| 795 |
+
.find(id => !document.getElementById(id).classList.contains('hidden')) || 'home';
|
| 796 |
+
document.getElementById('hTitle').textContent = TITLES[cur];
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
// ---- navigation ----
|
| 800 |
+
const TITLES = {
|
| 801 |
+
home:'🐱 Chats perdus (version Beta)', check:"📷 J'ai vu un chat",
|
| 802 |
+
report:"🚨 J'ai perdu mon chat", follow:'ℹ️ Suivre mon signalement',
|
| 803 |
+
poster:'📋 Partager une affiche', train:"🧠 Aider l'IA",
|
| 804 |
+
legal:'⚖️ Avis légal & confidentialité'
|
| 805 |
+
};
|
| 806 |
function show(v){
|
| 807 |
['home','check','report','follow','poster','train','legal'].forEach(id=>document.getElementById(id).classList.toggle('hidden',id!==v));
|
| 808 |
document.getElementById('hTitle').textContent=TITLES[v];
|
|
|
|
| 813 |
function goHome(){ show('home'); }
|
| 814 |
|
| 815 |
function toast(m){const t=document.getElementById('toast');t.textContent=m;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2800);}
|
|
|
|
|
|
|
| 816 |
function zoom(src){ document.getElementById('lightbox_img').src=src;
|
| 817 |
document.getElementById('lightbox').classList.add('on'); }
|
|
|
|
| 818 |
const zimg=url=>`<span class="zwrap"><img src="${encodeURI(url)}" onclick="zoom(this.src)" title="Voir en grand"><span class="zbadge">🔍</span></span>`;
|
| 819 |
function closeZoom(e){ if(e) e.stopPropagation();
|
| 820 |
const lb=document.getElementById('lightbox'); lb.classList.remove('on');
|
| 821 |
document.getElementById('lightbox_img').removeAttribute('src'); }
|
| 822 |
document.addEventListener('keydown',e=>{ if(e.key==='Escape') closeZoom(); });
|
| 823 |
const val=id=>document.getElementById(id).value;
|
| 824 |
+
function getPos(p){
|
| 825 |
+
if(!navigator.geolocation) return toast(T[LANG].js_gps_unavail);
|
| 826 |
navigator.geolocation.getCurrentPosition(g=>{pos[p]={lat:g.coords.latitude,lng:g.coords.longitude};
|
| 827 |
document.getElementById(p+'_gps').textContent=`📍 ${g.coords.latitude.toFixed(5)}, ${g.coords.longitude.toFixed(5)}`;
|
| 828 |
+
},()=>toast(T[LANG].js_gps_refused));
|
| 829 |
}
|
| 830 |
function prev(inputId,boxId){
|
| 831 |
const box=document.getElementById(boxId); box.innerHTML='';
|
|
|
|
| 835 |
}
|
| 836 |
}
|
| 837 |
|
| 838 |
+
let AI_NAME = LANG === 'en' ? 'the AI' : "l'IA";
|
|
|
|
|
|
|
| 839 |
fetch(API+'/health').then(r=>r.json()).then(h=>{
|
| 840 |
const e=(h.embedder||'');
|
| 841 |
+
AI_NAME = e.includes('miewid') ? 'MiewID' : e.includes('megadescriptor') ? 'MegaDescriptor' : (LANG==='en'?'the AI':"l'IA");
|
|
|
|
|
|
|
| 842 |
}).catch(()=>{});
|
| 843 |
|
| 844 |
function progress(id){
|
|
|
|
| 847 |
let timer=null, finished=false;
|
| 848 |
const set=(p,t)=>{ fill.style.width=Math.min(p,98)+'%'; if(t) txt.textContent=t; };
|
| 849 |
return {
|
| 850 |
+
show(){ finished=false; box.classList.add('on'); set(2, LANG==='en'?'Preparing…':'Préparation…'); },
|
| 851 |
set,
|
| 852 |
+
creep(target,t){ if(finished) return; if(t) txt.textContent=t;
|
|
|
|
| 853 |
clearInterval(timer);
|
| 854 |
timer=setInterval(()=>{ if(finished) return clearInterval(timer);
|
| 855 |
const cur=parseFloat(fill.style.width)||0;
|
|
|
|
| 863 |
};
|
| 864 |
}
|
| 865 |
|
|
|
|
| 866 |
function postWithProgress(url, fd, prog, stages){
|
| 867 |
return new Promise((resolve,reject)=>{
|
| 868 |
const xhr=new XMLHttpRequest();
|
| 869 |
xhr.open('POST', url);
|
| 870 |
xhr.upload.onprogress=e=>{ if(e.lengthComputable){
|
| 871 |
const pct=e.loaded/e.total;
|
| 872 |
+
prog.set(2+Math.round(pct*38), (LANG==='en'?'📤 Sending… ':'📤 Envoi… ')+Math.round(pct*100)+'%'); } };
|
| 873 |
xhr.upload.onload=()=>{ prog.creep(72, stages[0]);
|
| 874 |
if(stages[1]) setTimeout(()=>prog.creep(93, stages[1]), 2600); };
|
| 875 |
xhr.onload=()=>resolve({ok:xhr.status>=200&&xhr.status<300, status:xhr.status,
|
|
|
|
| 882 |
// ---------- 1. VÉRIFIER ----------
|
| 883 |
async function doCheck(){
|
| 884 |
const f=document.getElementById('c_photo').files[0];
|
| 885 |
+
if(!f) return toast(T[LANG].js_add_photo);
|
| 886 |
const fd=new FormData(); fd.append('photos',f);
|
| 887 |
if(pos.c){fd.append('lat',pos.c.lat);fd.append('lng',pos.c.lng);}
|
| 888 |
const prog=progress('c_prog'); prog.show();
|
| 889 |
+
const tl=T[LANG];
|
| 890 |
let r;
|
| 891 |
try{
|
| 892 |
r=await postWithProgress(API+'/sightings', fd, prog, [
|
| 893 |
+
tl.js_prog_embed(AI_NAME), tl.js_prog_compare]);
|
| 894 |
+
}catch(e){ return prog.fail(tl.js_net_err); }
|
| 895 |
+
if(!r.ok){ return prog.fail('Erreur '+r.status+(r.status===429?tl.js_rate:'')); }
|
| 896 |
+
prog.done(tl.js_analysis_done);
|
|
|
|
| 897 |
const d=r.json(); lastSightingId=d.sighting_id; lastMatches=d.matches||[];
|
| 898 |
const box=document.getElementById('c_result'); document.getElementById('c_leadbox').classList.add('hidden');
|
| 899 |
if(!d.match_found||!d.matches.length){
|
| 900 |
+
box.innerHTML=`<div class="result none"><div class="big">🔍</div><b>${tl.js_no_match}</b>
|
| 901 |
<p class="hint">${esc(d.message)}</p></div>`; return;
|
| 902 |
}
|
| 903 |
+
box.innerHTML=`<div class="card"><b>${tl.js_match_count(d.matches.length)}</b>
|
| 904 |
+
<p class="hint">${tl.js_match_q}</p>`+
|
| 905 |
d.matches.map(m=>{
|
| 906 |
const pct=Math.round(m.confidence*100);
|
| 907 |
+
const c=pct>=70?[tl.js_conf_strong,'var(--ok)']:pct>=40?[tl.js_conf_medium,'var(--warn)']:[tl.js_conf_weak,'var(--bad)'];
|
| 908 |
const ph=(m.photos[0]&&m.photos[0].url)?API+m.photos[0].url:'';
|
| 909 |
+
const actionBtn = m.is_poster
|
| 910 |
+
? `<button class="btn btn-primary" style="margin-top:8px" onclick="viewPoster('${m.candidate_token}')">${tl.js_view_poster}</button>`
|
| 911 |
+
: `<button class="btn btn-primary" style="margin-top:8px" onclick="chooseCat('${m.candidate_token}')">${tl.js_its_him}</button>`;
|
| 912 |
return `<div class="candidate">${ph?zimg(ph):''}<div style="flex:1">
|
| 913 |
+
<b>${esc(m.name||tl.js_cat_default)}</b> <span class="pill" style="background:${c[1]}">${c[0]} ${pct}%</span><br>
|
| 914 |
<span class="hint">${esc(m.description||'')}</span><br>
|
| 915 |
+
${actionBtn}
|
| 916 |
</div></div>`;
|
| 917 |
}).join('')+`</div>`;
|
| 918 |
}
|
| 919 |
function chooseCat(id){ chosenCandidateToken=id;
|
| 920 |
const m=lastMatches.find(x=>x.candidate_token===id);
|
| 921 |
+
const name=(m&&m.name)?m.name:(LANG==='en'?'this cat':'ce chat');
|
| 922 |
+
document.getElementById('c_leadtitle').textContent=T[LANG].js_choose_cat_title(name);
|
| 923 |
const b=document.getElementById('c_leadbox'); b.classList.remove('hidden'); b.scrollIntoView({behavior:'smooth'});
|
| 924 |
}
|
| 925 |
+
function viewPoster(token){
|
| 926 |
+
const m=lastMatches.find(x=>x.candidate_token===token); if(!m) return;
|
| 927 |
+
const ph=m.photos&&m.photos[0]&&m.photos[0].url ? API+m.photos[0].url : null;
|
| 928 |
+
if(ph) zoom(ph);
|
| 929 |
+
else toast(T[LANG].js_no_poster_photo);
|
| 930 |
+
}
|
| 931 |
async function sendLead(){
|
| 932 |
+
if(!chosenCandidateToken) return toast(T[LANG].js_choose_cat_prompt);
|
| 933 |
const r=await fetch(`${API}/sightings/${lastSightingId}/lead`,{method:'POST',headers:{'Content-Type':'application/json'},
|
| 934 |
body:JSON.stringify({candidate_token:chosenCandidateToken,finder_contact:val('c_contact'),message:val('c_msg')})});
|
| 935 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 936 |
+
document.getElementById('c_leadbox').innerHTML=T[LANG].js_lead_sent;
|
| 937 |
}
|
| 938 |
|
| 939 |
// ---------- 2. SIGNALER ----------
|
| 940 |
async function posterPrefill(inputId){
|
| 941 |
const f=document.getElementById(inputId).files[0]; if(!f) return;
|
| 942 |
+
const note=document.getElementById('r_poster_note');
|
| 943 |
+
note.textContent=T[LANG].js_poster_reading;
|
| 944 |
const fd=new FormData(); fd.append('file',f);
|
| 945 |
try{
|
| 946 |
const r=await fetch(API+'/posters',{method:'POST',body:fd}); const d=await r.json();
|
|
|
|
| 948 |
const phEl=document.getElementById('r_phone');
|
| 949 |
if(d.suggested_contact&&phEl) phEl.value=d.suggested_contact;
|
| 950 |
if(d.suggested_description) document.getElementById('r_desc').value=d.suggested_description;
|
| 951 |
+
note.textContent=d.note||T[LANG].js_poster_loaded;
|
| 952 |
+
}catch(e){ note.textContent=T[LANG].js_poster_loaded; }
|
| 953 |
}
|
| 954 |
function photoHint(){
|
| 955 |
+
const n=document.getElementById('r_photos').files.length, h=document.getElementById('r_hint');
|
| 956 |
+
const tl=T[LANG];
|
| 957 |
+
if(!n){h.style.color='';h.textContent=tl.report_photo_hint;return;}
|
| 958 |
+
if(n<3){h.style.color='var(--bad)';h.textContent=tl.js_photo_hint_few(n);}
|
| 959 |
+
else if(n<5){h.style.color='var(--warn)';h.textContent=tl.js_photo_hint_ok(n);}
|
| 960 |
+
else{h.style.color='var(--ok)';h.textContent=tl.js_photo_hint_great(n);}
|
| 961 |
}
|
| 962 |
async function doReport(){
|
| 963 |
const photos=document.getElementById('r_photos').files;
|
| 964 |
+
if(!photos.length) return toast(T[LANG].js_add_photo);
|
| 965 |
const email=val('r_email').trim();
|
| 966 |
+
if(!email||!email.includes('@')) return toast(T[LANG].js_email_required);
|
| 967 |
const fd=new FormData();
|
| 968 |
fd.append('name',val('r_name'));
|
| 969 |
const desc=[val('r_desc'),val('r_details')].filter(Boolean).join(' — ');
|
|
|
|
| 971 |
if(pos.r){fd.append('last_seen_lat',pos.r.lat);fd.append('last_seen_lng',pos.r.lng);}
|
| 972 |
for(const f of photos) fd.append('photos',f);
|
| 973 |
const prog=progress('r_prog'); prog.show();
|
| 974 |
+
const tl=T[LANG];
|
| 975 |
let r;
|
| 976 |
try{
|
| 977 |
r=await postWithProgress(API+'/cats', fd, prog, [
|
| 978 |
+
tl.js_prog_embed(AI_NAME+(photos.length>1?(LANG==='en'?` (${photos.length} photos)`:` (${photos.length} photos)`):'') ),
|
| 979 |
+
tl.js_prog_saving]);
|
| 980 |
+
}catch(e){ return prog.fail(tl.js_net_err); }
|
| 981 |
+
if(!r.ok){ return prog.fail('Erreur '+r.status+(r.status===429?tl.js_rate:'')); }
|
| 982 |
+
prog.done(tl.js_analysis_done);
|
| 983 |
const d=r.json(); localStorage.setItem('chats_email',email);
|
| 984 |
document.getElementById('r_track_email').value=email;
|
| 985 |
+
document.getElementById('r_token').innerHTML=tl.js_report_success(d.name, d.photos_count, email);
|
|
|
|
|
|
|
|
|
|
| 986 |
document.getElementById('r_token').scrollIntoView({behavior:'smooth'});
|
| 987 |
}
|
| 988 |
async function requestCode(){
|
| 989 |
+
const email=val('r_track_email').trim();
|
| 990 |
+
if(!email||!email.includes('@')) return toast(T[LANG].js_email_short);
|
| 991 |
const fd=new FormData(); fd.append('email',email);
|
| 992 |
const r=await fetch(API+'/cats/request-code',{method:'POST',body:fd});
|
| 993 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 994 |
const d=await r.json();
|
| 995 |
document.getElementById('r_code_box').classList.remove('hidden');
|
| 996 |
+
if(d.dev_code){
|
| 997 |
document.getElementById('r_track_code').value=d.dev_code;
|
| 998 |
+
toast('Demo: '+(LANG==='en'?'code pre-filled':'code pré-rempli')+' ('+d.dev_code+')');
|
| 999 |
+
} else toast(T[LANG].js_code_sent);
|
| 1000 |
}
|
| 1001 |
async function verifyCode(){
|
| 1002 |
const email=val('r_track_email').trim(), code=val('r_track_code').trim();
|
| 1003 |
+
if(!code) return toast(T[LANG].follow_code_label);
|
| 1004 |
const fd=new FormData(); fd.append('email',email); fd.append('code',code);
|
| 1005 |
const r=await fetch(API+'/cats/verify-code',{method:'POST',body:fd});
|
| 1006 |
const box=document.getElementById('r_track');
|
| 1007 |
+
if(!r.ok){ box.innerHTML=T[LANG].js_code_invalid; return; }
|
| 1008 |
const acc=await r.json(); ownerCats=acc.cats||[];
|
| 1009 |
+
if(!acc.cats.length){ box.innerHTML=T[LANG].js_no_cats; return; }
|
| 1010 |
+
box.innerHTML=`<button class="btn btn-ghost" style="margin-top:0" onclick="followLogout()">${T[LANG].js_logout_btn}</button>`
|
| 1011 |
+ acc.cats.map(renderOwnerCat).join('');
|
| 1012 |
}
|
| 1013 |
function followLogout(){
|
|
|
|
| 1016 |
document.getElementById('r_code_box').classList.add('hidden');
|
| 1017 |
document.getElementById('r_track_email').value='';
|
| 1018 |
localStorage.removeItem('chats_email');
|
| 1019 |
+
toast(T[LANG].js_logout); show('home');
|
| 1020 |
}
|
| 1021 |
function renderOwnerCat(c){
|
| 1022 |
+
const tl=T[LANG];
|
| 1023 |
const leads=c.leads.map(l=>{
|
| 1024 |
const pct=l.confidence?Math.round(l.confidence*100):null;
|
| 1025 |
const img=l.sighting_photo_url?zimg(API+l.sighting_photo_url):'';
|
| 1026 |
+
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>`:'';
|
| 1027 |
+
return `<div class="lead">${img}<div style="flex:1"><b>${tl.js_report_label}</b>${pct?` · ${pct}%`:''}<br>
|
| 1028 |
+
<span class="hint">${esc(l.message||tl.js_no_message)}</span><br>
|
| 1029 |
+
<span class="hint">📞 ${esc(l.finder_contact||tl.js_no_contact)} · ${loc}</span></div></div>`;
|
| 1030 |
+
}).join('')||`<p class="hint">${tl.js_no_leads}</p>`;
|
| 1031 |
const declared = c.cat.last_seen_at || c.cat.created_at;
|
| 1032 |
+
const dDate = declared ? new Date(declared).toLocaleDateString(tl.js_date_locale,{day:'numeric',month:'long',year:'numeric'}) : '?';
|
| 1033 |
const stats = `<div class="follow-stats">
|
| 1034 |
+
<div class="fs"><b>${dDate}</b><span>${tl.js_declared_on}</span></div>
|
| 1035 |
+
<div class="fs"><b>${(c.comparisons??0).toLocaleString(tl.js_num_locale)}</b><span>${tl.js_ai_compare}</span></div>
|
| 1036 |
+
<div class="fs"><b>${c.leads.length}</b><span>${tl.js_leads_received}</span></div>
|
| 1037 |
</div>`;
|
| 1038 |
return `<div style="margin-top:14px;border-top:1px solid var(--line);padding-top:12px">
|
| 1039 |
+
<b>${esc(c.cat.name||tl.js_my_cat)}</b> — ${tl.js_status} : ${esc(c.cat.status)}
|
| 1040 |
${stats}
|
| 1041 |
+
<button class="btn btn-ghost" onclick="reunited('${encodeURIComponent(c.claim_token)}')">${tl.js_mark_found}</button>
|
| 1042 |
<button class="btn btn-ghost" style="color:var(--bad);border-color:var(--bad)"
|
| 1043 |
+
onclick="deleteCat('${encodeURIComponent(c.claim_token)}')">${tl.js_delete}</button>
|
| 1044 |
+
<h2 style="margin-top:12px">${tl.js_leads_title(c.leads.length)}</h2>${leads}</div>`;
|
| 1045 |
}
|
| 1046 |
async function deleteCat(tok){
|
| 1047 |
const c=ownerCats.find(x=>x.claim_token===tok);
|
| 1048 |
+
const name=(c&&c.cat&&c.cat.name)?c.cat.name:T[LANG].js_this_profile;
|
| 1049 |
+
if(!confirm(T[LANG].js_delete_confirm(name))) return;
|
| 1050 |
const fd=new FormData(); fd.append('token',tok);
|
| 1051 |
const r=await fetch(API+'/cats/track/delete',{method:'POST',body:fd});
|
| 1052 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 1053 |
+
toast(T[LANG].js_deleted_toast);
|
| 1054 |
+
document.getElementById('r_track').innerHTML=T[LANG].js_deleted_html;
|
| 1055 |
}
|
| 1056 |
async function reunited(tok){
|
| 1057 |
const fd=new FormData();fd.append('token',tok);fd.append('status','reunited');
|
| 1058 |
const r=await fetch(API+'/cats/track/status',{method:'POST',body:fd});
|
| 1059 |
if(!r.ok) return toast('Erreur');
|
| 1060 |
+
const c=await r.json(); toast(T[LANG].js_found_toast);
|
| 1061 |
document.getElementById('r_track').innerHTML=renderOwnerCat(c);
|
| 1062 |
}
|
| 1063 |
|
| 1064 |
// ---------- 3. PARTAGER UNE AFFICHE ----------
|
| 1065 |
async function doPoster(){
|
| 1066 |
const f=document.getElementById('p_file').files[0];
|
| 1067 |
+
if(!f) return toast(T[LANG].js_add_poster);
|
|
|
|
| 1068 |
const fd0=new FormData(); fd0.append('file',f);
|
| 1069 |
try{ await fetch(API+'/posters',{method:'POST',body:fd0}); }catch(e){}
|
| 1070 |
+
const fd=new FormData(); fd.append('name', LANG==='en'?'Shared poster':'Affiche partagée'); fd.append('source','poster');
|
|
|
|
| 1071 |
fd.append('description',val('p_text'));
|
| 1072 |
if(pos.p){fd.append('last_seen_lat',pos.p.lat);fd.append('last_seen_lng',pos.p.lng);}
|
| 1073 |
if(f.type.startsWith('image/')) fd.append('photos',f);
|
| 1074 |
const r=await fetch(API+'/cats',{method:'POST',body:fd});
|
| 1075 |
const box=document.getElementById('p_result');
|
| 1076 |
+
if(r.ok){ box.innerHTML=T[LANG].js_poster_added;
|
| 1077 |
document.getElementById('p_file').value=''; document.getElementById('p_prev').innerHTML=''; document.getElementById('p_text').value='';
|
| 1078 |
} else box.innerHTML='<p class="hint">Erreur '+r.status+'.</p>';
|
| 1079 |
}
|
|
|
|
| 1081 |
// ---------- 4. ENTRAÎNER ----------
|
| 1082 |
async function doContribute(){
|
| 1083 |
const photos=document.getElementById('t_photos').files;
|
| 1084 |
+
if(!photos.length) return toast(T[LANG].js_add_photos);
|
| 1085 |
+
if(!document.getElementById('t_consent').checked) return toast(T[LANG].js_consent_required);
|
| 1086 |
const fd=new FormData(); fd.append('consent','1');
|
| 1087 |
for(const f of photos) fd.append('photos',f);
|
| 1088 |
+
toast(T[LANG].js_sending);
|
| 1089 |
const r=await fetch(API+'/training/photos',{method:'POST',body:fd});
|
| 1090 |
if(!r.ok) return toast('Erreur '+r.status);
|
| 1091 |
+
const d=await r.json(); toast(T[LANG].js_photos_donated(d.photos));
|
| 1092 |
document.getElementById('t_photos').value=''; document.getElementById('t_prev').innerHTML=''; loadTrainStats();
|
| 1093 |
}
|
| 1094 |
async function nextPair(){
|
| 1095 |
+
const g=document.getElementById('game'); g.innerHTML=`<p class="hint">${T[LANG].js_loading}</p>`;
|
| 1096 |
const r=await fetch(API+'/training/pair');
|
| 1097 |
+
if(!r.ok){ g.innerHTML=`<p class="hint">${T[LANG].js_not_enough_photos}</p>`; return; }
|
| 1098 |
gamePair=await r.json();
|
| 1099 |
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>
|
| 1100 |
<div class="seg" style="margin-top:12px">
|
| 1101 |
+
<button onclick="sendLabel('same')">${T[LANG].js_same_cat}</button>
|
| 1102 |
+
<button onclick="sendLabel('different')">${T[LANG].js_diff_cat}</button>
|
| 1103 |
</div>`;
|
| 1104 |
}
|
| 1105 |
async function sendLabel(v){
|
| 1106 |
if(!gamePair) return;
|
| 1107 |
await fetch(API+'/training/label',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 1108 |
body:JSON.stringify({photo_a_id:gamePair.a.id,photo_b_id:gamePair.b.id,verdict:v})});
|
| 1109 |
+
toast(T[LANG].js_label_thanks); loadTrainStats(); nextPair();
|
| 1110 |
}
|
| 1111 |
async function loadTrainStats(){
|
| 1112 |
try{const d=await (await fetch(API+'/training/stats')).json();
|
| 1113 |
+
document.getElementById('t_stats').textContent=T[LANG].js_stats(d.photos, d.labels);}catch(e){}
|
| 1114 |
}
|
| 1115 |
|
| 1116 |
+
// Init
|
| 1117 |
+
setLang(LANG);
|
| 1118 |
const savedEmail=localStorage.getItem('chats_email'); if(savedEmail) document.getElementById('r_track_email').value=savedEmail;
|
| 1119 |
</script>
|
| 1120 |
</body>
|