Sarolanda commited on
Commit
e39d48e
Β·
1 Parent(s): 5e6a147

adds error treat

Browse files
Files changed (7) hide show
  1. app.py +82 -2
  2. core/database.py +44 -7
  3. data/viralata.db +0 -0
  4. db/schema.sql +11 -9
  5. index.html +97 -30
  6. static/help-proof.js +125 -0
  7. static/style.css +101 -0
app.py CHANGED
@@ -280,15 +280,95 @@ def _cleanup_sessions():
280
 
281
  @app.post("/api/animal/{animal_id}/helped")
282
  async def mark_helped(animal_id: int):
283
- """Registra que alguΓ©m ajudou este animal (sem foto)."""
284
  animal = db.get_animal(animal_id)
285
  if not animal:
286
  return JSONResponse(content={"error": "not found"}, status_code=404)
287
- db.add_sighting(animal_id, None, None, None, "[HELPED]")
288
  db.update_animal(animal_id)
289
  return JSONResponse(content={"ok": True})
290
 
291
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  # ─── Admin ────────────────────────────────────────────
293
 
294
  @app.get("/admin/push-traces")
 
280
 
281
  @app.post("/api/animal/{animal_id}/helped")
282
  async def mark_helped(animal_id: int):
283
+ """Legacy β€” mantido por compatibilidade. Prefira submit_help_proof."""
284
  animal = db.get_animal(animal_id)
285
  if not animal:
286
  return JSONResponse(content={"error": "not found"}, status_code=404)
287
+ db.add_sighting(animal_id, None, None, None, "", is_help_event=True, help_type="other")
288
  db.update_animal(animal_id)
289
  return JSONResponse(content={"ok": True})
290
 
291
 
292
+ @app.api(name="submit_help_proof")
293
+ def submit_help_proof(
294
+ animal_id: int,
295
+ help_type: str = "other",
296
+ notes: str = "",
297
+ image_path: FileData = None,
298
+ ) -> dict:
299
+ """
300
+ Registra que alguΓ©m ajudou o animal, com foto de prova opcional.
301
+ A IA verifica se a foto Γ© do mesmo animal e detecta melhora de condiΓ§Γ£o.
302
+ """
303
+ from PIL import Image as PILImage
304
+ import json as _json
305
+
306
+ photo_path = None
307
+ ai_verified = False
308
+ condition_update = None
309
+ match_score = None
310
+
311
+ if image_path and image_path.get("path"):
312
+ img = PILImage.open(image_path["path"]).convert("RGB")
313
+
314
+ # Analisa a foto com IA
315
+ description = ai.analyze_image(img)
316
+
317
+ if description.get("is_animal") is not False and description.get("_ai_success"):
318
+ embedding = ai.get_embedding(description)
319
+ candidates = db.get_all_animals_with_embeddings()
320
+
321
+ # Verifica se Γ© o mesmo animal
322
+ match = matcher.find_match(embedding, candidates)
323
+ if match:
324
+ matched_id, score = match
325
+ ai_verified = (matched_id == animal_id)
326
+ match_score = round(score * 100)
327
+
328
+ # Detecta melhora de condiΓ§Γ£o
329
+ animal_data = db.get_animal(animal_id)
330
+ if animal_data:
331
+ prev_desc = _json.loads(animal_data.get("description") or "{}")
332
+ prev_condition = prev_desc.get("condition", "")
333
+ new_condition = description.get("condition", "")
334
+ condition_rank = {"injured": 0, "thin": 1, "healthy": 2}
335
+ if (condition_rank.get(new_condition, -1) >
336
+ condition_rank.get(prev_condition, -1)):
337
+ condition_update = new_condition
338
+
339
+ photo_path = db.save_photo(img, animal_id=animal_id)
340
+
341
+ db.add_sighting(
342
+ animal_id,
343
+ photo_path,
344
+ None, None,
345
+ notes,
346
+ is_help_event=True,
347
+ help_type=help_type,
348
+ )
349
+ db.update_animal(animal_id)
350
+
351
+ log_trace({
352
+ "event": "help_proof",
353
+ "animal_id": animal_id,
354
+ "help_type": help_type,
355
+ "has_photo": photo_path is not None,
356
+ "ai_verified": ai_verified,
357
+ "match_score": match_score,
358
+ "condition_update": condition_update,
359
+ })
360
+
361
+ return {
362
+ "ok": True,
363
+ "animal_id": animal_id,
364
+ "help_type": help_type,
365
+ "ai_verified": ai_verified,
366
+ "match_score": match_score,
367
+ "condition_update": condition_update,
368
+ "photo_url": _photo_url(photo_path) if photo_path else "",
369
+ }
370
+
371
+
372
  # ─── Admin ────────────────────────────────────────────
373
 
374
  @app.get("/admin/push-traces")
core/database.py CHANGED
@@ -142,31 +142,53 @@ class Database:
142
  lat: Optional[float],
143
  lng: Optional[float],
144
  notes: Optional[str],
 
 
145
  ):
146
  with self._conn() as conn:
147
  conn.execute(
148
- "INSERT INTO sightings (animal_id, photo_path, latitude, longitude, notes)"
149
- " VALUES (?, ?, ?, ?, ?)",
150
- (animal_id, photo_path, lat, lng, notes or ""),
 
 
151
  )
152
 
153
  def get_animal_sightings(self, animal_id: int) -> list:
154
  with self._conn() as conn:
155
  rows = conn.execute(
156
- "SELECT * FROM sightings WHERE animal_id = ? ORDER BY created_at DESC",
 
 
157
  (animal_id,),
158
  ).fetchall()
159
  return [dict(r) for r in rows]
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  def get_animal_detail(self, animal_id: int) -> Optional[dict]:
162
- """Retorna animal + todos os avistamentos com URLs de foto."""
163
  animal = self.get_animal(animal_id)
164
  if not animal:
165
  return None
166
- sightings = self.get_animal_sightings(animal_id)
 
167
  for s in sightings:
168
  s["photo_url"] = self.photo_url(s.get("photo_path")) or ""
169
- return {"animal": animal, "sightings": sightings}
170
 
171
  # ─── Map data ────────────────────────────────────────────────────────────
172
 
@@ -273,3 +295,18 @@ class Database:
273
  def total_animals(self) -> int:
274
  with self._conn() as conn:
275
  return conn.execute("SELECT COUNT(*) FROM animals").fetchone()[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  lat: Optional[float],
143
  lng: Optional[float],
144
  notes: Optional[str],
145
+ is_help_event: bool = False,
146
+ help_type: Optional[str] = None,
147
  ):
148
  with self._conn() as conn:
149
  conn.execute(
150
+ "INSERT INTO sightings"
151
+ " (animal_id, photo_path, latitude, longitude, notes, is_help_event, help_type)"
152
+ " VALUES (?, ?, ?, ?, ?, ?, ?)",
153
+ (animal_id, photo_path, lat, lng, notes or "",
154
+ 1 if is_help_event else 0, help_type),
155
  )
156
 
157
  def get_animal_sightings(self, animal_id: int) -> list:
158
  with self._conn() as conn:
159
  rows = conn.execute(
160
+ "SELECT *, CAST(julianday('now') - julianday(created_at) AS INTEGER) AS days_ago"
161
+ " FROM sightings WHERE animal_id = ? AND is_help_event = 0"
162
+ " ORDER BY created_at DESC",
163
  (animal_id,),
164
  ).fetchall()
165
  return [dict(r) for r in rows]
166
 
167
+ def get_help_events(self, animal_id: int) -> list:
168
+ """Retorna apenas registros de ajuda (is_help_event=1) com URL de foto."""
169
+ with self._conn() as conn:
170
+ rows = conn.execute(
171
+ "SELECT * FROM sightings WHERE animal_id = ? AND is_help_event = 1"
172
+ " ORDER BY created_at DESC",
173
+ (animal_id,),
174
+ ).fetchall()
175
+ result = []
176
+ for row in rows:
177
+ d = dict(row)
178
+ d["photo_url"] = self.photo_url(d.get("photo_path")) or ""
179
+ result.append(d)
180
+ return result
181
+
182
  def get_animal_detail(self, animal_id: int) -> Optional[dict]:
183
+ """Retorna animal + avistamentos normais + help_events separados."""
184
  animal = self.get_animal(animal_id)
185
  if not animal:
186
  return None
187
+ sightings = self.get_animal_sightings(animal_id)
188
+ help_events = self.get_help_events(animal_id)
189
  for s in sightings:
190
  s["photo_url"] = self.photo_url(s.get("photo_path")) or ""
191
+ return {"animal": animal, "sightings": sightings, "help_events": help_events}
192
 
193
  # ─── Map data ────────────────────────────────────────────────────────────
194
 
 
295
  def total_animals(self) -> int:
296
  with self._conn() as conn:
297
  return conn.execute("SELECT COUNT(*) FROM animals").fetchone()[0]
298
+ (limit,)).fetchall()
299
+ result = []
300
+ for row in rows:
301
+ d = dict(row)
302
+ d["last_photo_url"] = self.photo_url(d.get("last_photo_path")) or ""
303
+ result.append(d)
304
+ return result
305
+
306
+ def total_sightings(self) -> int:
307
+ with self._conn() as conn:
308
+ return conn.execute("SELECT COUNT(*) FROM sightings").fetchone()[0]
309
+
310
+ def total_animals(self) -> int:
311
+ with self._conn() as conn:
312
+ return conn.execute("SELECT COUNT(*) FROM animals").fetchone()[0]
data/viralata.db CHANGED
Binary files a/data/viralata.db and b/data/viralata.db differ
 
db/schema.sql CHANGED
@@ -1,21 +1,23 @@
1
  CREATE TABLE IF NOT EXISTS animals (
2
  id INTEGER PRIMARY KEY AUTOINCREMENT,
3
  species TEXT NOT NULL CHECK (species IN ('dog', 'cat')),
4
- description TEXT, -- JSON com atributos do Nemotron
5
- embedding BLOB, -- vetor numpy serializado (float32, 384-dim)
6
  first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
7
  last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
8
  sighting_count INTEGER NOT NULL DEFAULT 1
9
  );
10
 
11
  CREATE TABLE IF NOT EXISTS sightings (
12
- id INTEGER PRIMARY KEY AUTOINCREMENT,
13
- animal_id INTEGER NOT NULL REFERENCES animals(id) ON DELETE CASCADE,
14
- photo_path TEXT, -- caminho relativo em /data/photos/
15
- latitude REAL,
16
- longitude REAL,
17
- notes TEXT,
18
- created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
 
 
19
  );
20
 
21
  CREATE INDEX IF NOT EXISTS idx_sightings_animal ON sightings(animal_id);
 
1
  CREATE TABLE IF NOT EXISTS animals (
2
  id INTEGER PRIMARY KEY AUTOINCREMENT,
3
  species TEXT NOT NULL CHECK (species IN ('dog', 'cat')),
4
+ description TEXT,
5
+ embedding BLOB,
6
  first_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
7
  last_seen DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
8
  sighting_count INTEGER NOT NULL DEFAULT 1
9
  );
10
 
11
  CREATE TABLE IF NOT EXISTS sightings (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ animal_id INTEGER NOT NULL REFERENCES animals(id) ON DELETE CASCADE,
14
+ photo_path TEXT,
15
+ latitude REAL,
16
+ longitude REAL,
17
+ notes TEXT,
18
+ is_help_event INTEGER NOT NULL DEFAULT 0,
19
+ help_type TEXT,
20
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
21
  );
22
 
23
  CREATE INDEX IF NOT EXISTS idx_sightings_animal ON sightings(animal_id);
index.html CHANGED
@@ -303,6 +303,15 @@
303
  <div class="gallery-scroll" id="profile-gallery"></div>
304
  </div>
305
 
 
 
 
 
 
 
 
 
 
306
  <!-- Trajectory -->
307
  <div class="profile-section">
308
  <div class="section-title">Recent Path</div>
@@ -367,6 +376,57 @@
367
  </div>
368
  </div>
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  <!-- ══ PHOTO CONTEXT MENU ══ -->
371
  <div id="photo-menu" class="photo-menu hidden">
372
  <button id="photo-menu-download">
@@ -461,7 +521,7 @@
461
  const bottomNav = document.getElementById('bottom-nav');
462
  const photoInput= document.getElementById('photo-input');
463
 
464
- const FLOW_SCREENS = ['analysis', 'confirm', 'profile'];
465
 
466
  // ── Navigation ─────────────────────────────────────────────────────────────
467
  function showScreen(name) {
@@ -662,6 +722,7 @@
662
  const data = await fetch(`/api/animal/${id}`).then(r => r.json());
663
  const animal = data.animal;
664
  const sightings = data.sightings || [];
 
665
  let desc = {};
666
  try { desc = JSON.parse(animal.description || '{}'); } catch(e){}
667
 
@@ -716,9 +777,8 @@
716
  }
717
 
718
  // Helped banner
719
- const helpedSightings = sightings.filter(s => (s.notes || '').startsWith('[HELPED]'));
720
  const helpedBanner = document.getElementById('profile-helped-banner');
721
- helpedBanner.style.display = helpedSightings.length ? 'flex' : 'none';
722
 
723
  // Gallery
724
  const gallery = document.getElementById('profile-gallery');
@@ -756,20 +816,33 @@
756
  gallery.innerHTML = `<div style="font-size:13px;color:#aaa;padding:8px 0;">No photos recorded.</div>`;
757
  }
758
 
759
- // Append helped events below gallery
760
- if (helpedSightings.length) {
761
- const helpedHtml = helpedSightings.map(s => {
762
- const dt = s.created_at ? new Date(s.created_at) : null;
 
 
 
 
 
 
 
 
 
 
763
  const dateStr = dt ? dt.toLocaleDateString('en-US', {day:'2-digit', month:'short', year:'numeric'}) : '';
764
- return `<div class="helped-event">
765
- <div class="helped-event-icon">${svgIcon('heart', 16, 'var(--green)')}</div>
766
- <div class="helped-event-text">
767
- <span>Someone helped this animal</span>
768
- ${dateStr ? `<small>${dateStr}</small>` : ''}
769
- </div>
770
- </div>`;
 
771
  }).join('');
772
- gallery.insertAdjacentHTML('afterend', `<div class="helped-events">${helpedHtml}</div>`);
 
 
773
  }
774
 
775
  // Trajectory mini-map
@@ -1184,22 +1257,10 @@
1184
  closeHelpSheet();
1185
  });
1186
 
1187
- // I helped
1188
- document.getElementById('help-helped').addEventListener('click', async () => {
1189
  closeHelpSheet();
1190
- try {
1191
- await fetch(`/api/animal/${profileAnimalId}/helped`, { method: 'POST' });
1192
- // Refresh profile to show helped banner
1193
- await openProfile(profileAnimalId);
1194
- } catch(e) {
1195
- console.error(e);
1196
- }
1197
- const confirmEl = document.getElementById('helped-confirm');
1198
- confirmEl.classList.remove('hidden');
1199
- requestAnimationFrame(() => {
1200
- confirmEl.classList.add('open');
1201
- if (typeof lucide !== 'undefined') lucide.createIcons();
1202
- });
1203
  });
1204
 
1205
  document.getElementById('helped-ok').addEventListener('click', () => {
@@ -1208,11 +1269,17 @@
1208
  setTimeout(() => confirm.classList.add('hidden'), 300);
1209
  });
1210
 
 
 
 
 
 
1211
  // ── BOOT ───────────────────────────────────────────────────────────────────
1212
  initMap();
1213
  loadMapData();
1214
  if (typeof lucide !== 'undefined') lucide.createIcons();
1215
  })();
1216
  </script>
 
1217
  </body>
1218
  </html>
 
303
  <div class="gallery-scroll" id="profile-gallery"></div>
304
  </div>
305
 
306
+ <!-- Community Help -->
307
+ <div class="profile-section" id="profile-help-section" style="display:none">
308
+ <div class="section-title help-section-title">
309
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="var(--green)" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
310
+ Community Help
311
+ </div>
312
+ <div id="profile-help-list"></div>
313
+ </div>
314
+
315
  <!-- Trajectory -->
316
  <div class="profile-section">
317
  <div class="section-title">Recent Path</div>
 
376
  </div>
377
  </div>
378
 
379
+ <!-- ══ SCREEN: HELP PROOF ══ -->
380
+ <div id="screen-help-proof" class="screen">
381
+ <div class="flow-header">
382
+ <button class="back-btn" id="help-proof-back"><svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg></button>
383
+ <h2>I Helped This Animal</h2>
384
+ </div>
385
+ <div id="help-proof-animal-row">
386
+ <div id="help-proof-animal-photo" class="help-proof-thumb"></div>
387
+ <div>
388
+ <div id="help-proof-animal-name" class="help-proof-animal-name">β€”</div>
389
+ <div id="help-proof-animal-sub" class="help-proof-animal-sub">β€”</div>
390
+ </div>
391
+ </div>
392
+ <div class="help-proof-section">
393
+ <div class="help-proof-label">What did you do?</div>
394
+ <div class="help-type-chips" id="help-type-chips">
395
+ <button class="help-type-chip active" data-type="fed"><i data-lucide="utensils"></i> Fed</button>
396
+ <button class="help-type-chip" data-type="vet"><i data-lucide="stethoscope"></i> Took to vet</button>
397
+ <button class="help-type-chip" data-type="adopted"><i data-lucide="home"></i> Adopted</button>
398
+ <button class="help-type-chip" data-type="rescued"><i data-lucide="shield-check"></i> Rescued</button>
399
+ <button class="help-type-chip" data-type="other"><i data-lucide="heart"></i> Other</button>
400
+ </div>
401
+ </div>
402
+ <div class="help-proof-section">
403
+ <div class="help-proof-label">Add a photo <span class="optional-tag">optional</span></div>
404
+ <div id="help-proof-viewfinder" class="help-proof-viewfinder">
405
+ <input type="file" id="help-proof-input" accept="image/*" capture="environment"/>
406
+ <img id="help-proof-preview" src="" alt="" class="hidden"/>
407
+ <div id="help-proof-placeholder">
408
+ <svg viewBox="0 0 24 24"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4"/></svg>
409
+ <span>Tap to add photo proof</span>
410
+ </div>
411
+ <div id="help-proof-ai-badge" class="help-proof-ai-badge hidden">
412
+ <span class="dot"></span><span id="help-proof-ai-text">Verifying with AI…</span>
413
+ </div>
414
+ <div id="help-proof-verified-badge" class="help-proof-verified-badge hidden">
415
+ <svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>
416
+ <span id="help-proof-verified-text">Same animal confirmed βœ“</span>
417
+ </div>
418
+ </div>
419
+ </div>
420
+ <div class="help-proof-section">
421
+ <div class="help-proof-label">Tell the story <span class="optional-tag">optional</span></div>
422
+ <textarea id="help-proof-notes" rows="3" placeholder="What happened? Where did you take them?"></textarea>
423
+ </div>
424
+ <button class="btn-primary help-proof-submit" id="help-proof-submit">
425
+ <svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg>
426
+ Register Help
427
+ </button>
428
+ </div>
429
+
430
  <!-- ══ PHOTO CONTEXT MENU ══ -->
431
  <div id="photo-menu" class="photo-menu hidden">
432
  <button id="photo-menu-download">
 
521
  const bottomNav = document.getElementById('bottom-nav');
522
  const photoInput= document.getElementById('photo-input');
523
 
524
+ const FLOW_SCREENS = ['analysis', 'confirm', 'profile', 'help-proof'];
525
 
526
  // ── Navigation ─────────────────────────────────────────────────────────────
527
  function showScreen(name) {
 
722
  const data = await fetch(`/api/animal/${id}`).then(r => r.json());
723
  const animal = data.animal;
724
  const sightings = data.sightings || [];
725
+ const helpEvents = data.help_events || [];
726
  let desc = {};
727
  try { desc = JSON.parse(animal.description || '{}'); } catch(e){}
728
 
 
777
  }
778
 
779
  // Helped banner
 
780
  const helpedBanner = document.getElementById('profile-helped-banner');
781
+ helpedBanner.style.display = helpEvents.length ? 'flex' : 'none';
782
 
783
  // Gallery
784
  const gallery = document.getElementById('profile-gallery');
 
816
  gallery.innerHTML = `<div style="font-size:13px;color:#aaa;padding:8px 0;">No photos recorded.</div>`;
817
  }
818
 
819
+ // Community Help section
820
+ const helpSection = document.getElementById('profile-help-section');
821
+ const helpList = document.getElementById('profile-help-list');
822
+ const helpTypeLabels = {
823
+ fed: '<i data-lucide="utensils"></i> Fed',
824
+ vet: '<i data-lucide="stethoscope"></i> Took to vet',
825
+ adopted: '<i data-lucide="home"></i> Adopted',
826
+ rescued: '<i data-lucide="shield-check"></i> Rescued',
827
+ other: '<i data-lucide="heart"></i> Helped',
828
+ };
829
+ if (helpEvents.length) {
830
+ helpSection.style.display = '';
831
+ helpList.innerHTML = helpEvents.map(h => {
832
+ const dt = h.created_at ? new Date(h.created_at) : null;
833
  const dateStr = dt ? dt.toLocaleDateString('en-US', {day:'2-digit', month:'short', year:'numeric'}) : '';
834
+ const label = helpTypeLabels[h.help_type] || '<i data-lucide="heart"></i> Helped';
835
+ return '<div class="help-event-card">'
836
+ + (h.photo_url ? '<img class="help-event-photo" src="' + h.photo_url + '" alt="help proof" onerror="this.style.display='none'"/>' : '')
837
+ + '<div class="help-event-body">'
838
+ + '<div class="help-event-label">' + label + '</div>'
839
+ + (h.notes ? '<div class="help-event-notes">' + h.notes + '</div>' : '')
840
+ + (dateStr ? '<div class="help-event-date">' + dateStr + '</div>' : '')
841
+ + '</div></div>';
842
  }).join('');
843
+ if (typeof lucide !== 'undefined') lucide.createIcons();
844
+ } else {
845
+ helpSection.style.display = 'none';
846
  }
847
 
848
  // Trajectory mini-map
 
1257
  closeHelpSheet();
1258
  });
1259
 
1260
+ // I helped β†’ nova tela
1261
+ document.getElementById('help-helped').addEventListener('click', () => {
1262
  closeHelpSheet();
1263
+ window.openHelpProofScreen();
 
 
 
 
 
 
 
 
 
 
 
 
1264
  });
1265
 
1266
  document.getElementById('helped-ok').addEventListener('click', () => {
 
1269
  setTimeout(() => confirm.classList.add('hidden'), 300);
1270
  });
1271
 
1272
+ // Expor para help-proof.js
1273
+ window.showScreen = showScreen;
1274
+ window.openProfile = openProfile;
1275
+ window.getGradioClient = getClient;
1276
+
1277
  // ── BOOT ───────────────────────────────────────────────────────────────────
1278
  initMap();
1279
  loadMapData();
1280
  if (typeof lucide !== 'undefined') lucide.createIcons();
1281
  })();
1282
  </script>
1283
+ <script src="/static/help-proof.js"></script>
1284
  </body>
1285
  </html>
static/help-proof.js ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ // ── HELP PROOF SCREEN ──────────────────────────────────────────────────────
3
+ (function() {
4
+ let helpProofAnimalId = null;
5
+ let helpProofSelType = 'fed';
6
+
7
+ window.openHelpProofScreen = function() {
8
+ helpProofAnimalId = window.profileAnimalId;
9
+ helpProofSelType = 'fed';
10
+
11
+ const preview = document.getElementById('help-proof-preview');
12
+ preview.src = ''; preview.classList.add('hidden');
13
+ document.getElementById('help-proof-placeholder').style.display = '';
14
+ document.getElementById('help-proof-ai-badge').classList.add('hidden');
15
+ document.getElementById('help-proof-verified-badge').classList.add('hidden');
16
+ document.getElementById('help-proof-notes').value = '';
17
+ document.getElementById('help-proof-input').value = '';
18
+ document.querySelectorAll('.help-type-chip').forEach(c => c.classList.remove('active'));
19
+ document.querySelector('.help-type-chip[data-type="fed"]').classList.add('active');
20
+
21
+ const name = document.getElementById('profile-title').textContent || 'Animal';
22
+ const sub = document.getElementById('profile-status-text').textContent || '';
23
+ document.getElementById('help-proof-animal-name').textContent = name;
24
+ document.getElementById('help-proof-animal-sub').textContent = sub;
25
+ const heroSrc = document.getElementById('profile-hero-img').src;
26
+ const thumb = document.getElementById('help-proof-animal-photo');
27
+ thumb.style.backgroundImage = heroSrc ? 'url(' + heroSrc + ')' : '';
28
+ thumb.style.backgroundSize = 'cover';
29
+
30
+ window.showScreen('help-proof');
31
+ };
32
+
33
+ document.getElementById('help-type-chips').addEventListener('click', e => {
34
+ const chip = e.target.closest('.help-type-chip');
35
+ if (!chip) return;
36
+ document.querySelectorAll('.help-type-chip').forEach(c => c.classList.remove('active'));
37
+ chip.classList.add('active');
38
+ helpProofSelType = chip.dataset.type;
39
+ });
40
+
41
+ document.getElementById('help-proof-viewfinder').addEventListener('click', () => {
42
+ document.getElementById('help-proof-input').click();
43
+ });
44
+
45
+ document.getElementById('help-proof-input').addEventListener('change', async (e) => {
46
+ const file = e.target.files[0];
47
+ if (!file) return;
48
+
49
+ const preview = document.getElementById('help-proof-preview');
50
+ preview.src = URL.createObjectURL(file);
51
+ preview.classList.remove('hidden');
52
+ document.getElementById('help-proof-placeholder').style.display = 'none';
53
+
54
+ const aiBadge = document.getElementById('help-proof-ai-badge');
55
+ const verifiedBadge = document.getElementById('help-proof-verified-badge');
56
+ aiBadge.classList.remove('hidden');
57
+ document.getElementById('help-proof-ai-text').textContent = 'Verifying with AI…';
58
+ verifiedBadge.classList.add('hidden');
59
+
60
+ try {
61
+ const { client, handleFile } = await window.getGradioClient();
62
+ const res = await client.predict('/analyze_image', { image_path: handleFile(file) });
63
+ const data = res.data[0] || {};
64
+
65
+ aiBadge.classList.add('hidden');
66
+ verifiedBadge.classList.remove('hidden');
67
+ const verifiedText = document.getElementById('help-proof-verified-text');
68
+
69
+ if (data.error) {
70
+ verifiedBadge.style.background = 'rgba(220,53,69,0.85)';
71
+ verifiedText.textContent = 'No animal detected in photo';
72
+ } else if (data.similar && data.similar.some(s => s.id === helpProofAnimalId && s.score_pct >= 60)) {
73
+ verifiedBadge.style.background = 'rgba(56,140,89,0.9)';
74
+ verifiedText.textContent = 'Same animal confirmed βœ“';
75
+ } else {
76
+ verifiedBadge.style.background = 'rgba(255,165,0,0.85)';
77
+ verifiedText.textContent = 'Could not confirm β€” register anyway?';
78
+ }
79
+ } catch(err) {
80
+ console.warn('AI verify failed:', err);
81
+ document.getElementById('help-proof-ai-badge').classList.add('hidden');
82
+ }
83
+ });
84
+
85
+ document.getElementById('help-proof-submit').addEventListener('click', async () => {
86
+ const btn = document.getElementById('help-proof-submit');
87
+ const notes = document.getElementById('help-proof-notes').value.trim();
88
+ const file = document.getElementById('help-proof-input').files[0];
89
+
90
+ btn.disabled = true;
91
+ btn.innerHTML = '<div class="spinner"></div> Saving…';
92
+
93
+ try {
94
+ const { client, handleFile } = await window.getGradioClient();
95
+ const res = await client.predict('/submit_help_proof', {
96
+ animal_id: helpProofAnimalId,
97
+ help_type: helpProofSelType,
98
+ notes: notes,
99
+ image_path: file ? handleFile(file) : null,
100
+ });
101
+ const result = res.data[0] || {};
102
+
103
+ if (result.ok) {
104
+ window.showScreen('profile');
105
+ await window.openProfile(helpProofAnimalId);
106
+ const confirmEl = document.getElementById('helped-confirm');
107
+ confirmEl.classList.remove('hidden');
108
+ requestAnimationFrame(() => {
109
+ confirmEl.classList.add('open');
110
+ if (typeof lucide !== 'undefined') lucide.createIcons();
111
+ });
112
+ } else {
113
+ alert('Erro ao registrar. Tente novamente.');
114
+ }
115
+ } catch(err) {
116
+ console.error(err);
117
+ alert('Erro ao registrar. Tente novamente.');
118
+ } finally {
119
+ btn.disabled = false;
120
+ btn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"/></svg> Register Help';
121
+ }
122
+ });
123
+
124
+ document.getElementById('help-proof-back').addEventListener('click', () => window.showScreen('profile'));
125
+ })();
static/style.css CHANGED
@@ -443,3 +443,104 @@
443
  line-height: 1.4;
444
  padding: 10px 16px;
445
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  line-height: 1.4;
444
  padding: 10px 16px;
445
  }
446
+
447
+ /* ── Help Proof Screen ────────────────────────────────────────────────────── */
448
+ #screen-help-proof { display:flex; flex-direction:column; padding:0 0 100px; overflow-y:auto; background:var(--white); }
449
+ #screen-help-proof .flow-header { padding:12px 16px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--border); }
450
+ #screen-help-proof .flow-header h2 { font-size:17px; font-weight:700; }
451
+
452
+ #help-proof-animal-row { display:flex; align-items:center; gap:12px; padding:14px 16px; background:var(--green-soft); border-bottom:1px solid var(--border); }
453
+ .help-proof-thumb { width:48px; height:48px; border-radius:12px; background:var(--green-pale) center/cover no-repeat; flex-shrink:0; }
454
+ .help-proof-animal-name { font-size:15px; font-weight:700; color:var(--text); }
455
+ .help-proof-animal-sub { font-size:12px; color:var(--text-muted); margin-top:2px; }
456
+
457
+ .help-proof-section { padding:16px 16px 0; }
458
+ .help-proof-label { font-size:12px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:.5px; margin-bottom:10px; display:flex; align-items:center; gap:6px; }
459
+ .optional-tag { font-size:10px; font-weight:500; color:var(--green); background:var(--green-pale); padding:2px 7px; border-radius:20px; text-transform:none; letter-spacing:0; }
460
+
461
+ /* Help type chips */
462
+ .help-type-chips { display:flex; flex-wrap:wrap; gap:8px; }
463
+ .help-type-chip { border:1.5px solid var(--border); background:var(--white); border-radius:20px; padding:7px 14px; font-size:13px; font-weight:500; cursor:pointer; transition:all .18s; color:var(--text); }
464
+ .help-type-chip.active { border-color:var(--green); background:var(--green-pale); color:var(--green-dark); font-weight:700; }
465
+
466
+ /* Viewfinder */
467
+ .help-proof-viewfinder { position:relative; width:100%; height:200px; border-radius:14px; overflow:hidden; background:#f0f0f0; cursor:pointer; border:2px dashed var(--border); margin-top:0; }
468
+ .help-proof-viewfinder:active { opacity:.85; }
469
+ #help-proof-input { display:none; }
470
+ #help-proof-preview { width:100%; height:100%; object-fit:cover; display:block; }
471
+ #help-proof-placeholder { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; color:var(--text-muted); }
472
+ #help-proof-placeholder svg { width:32px; height:32px; stroke:var(--text-muted); fill:none; stroke-width:1.5; }
473
+ #help-proof-placeholder span { font-size:13px; font-weight:500; }
474
+ .help-proof-ai-badge { position:absolute; bottom:10px; left:10px; background:rgba(33,33,33,.82); color:#fff; border-radius:20px; padding:5px 12px; font-size:12px; font-weight:600; display:flex; align-items:center; gap:6px; }
475
+ .help-proof-ai-badge .dot { width:7px; height:7px; border-radius:50%; background:#4caf50; animation:pulse 1.2s infinite; }
476
+ .help-proof-verified-badge { position:absolute; bottom:10px; left:10px; color:#fff; border-radius:20px; padding:6px 12px; font-size:12px; font-weight:700; display:flex; align-items:center; gap:6px; }
477
+ .help-proof-verified-badge svg { width:14px; height:14px; stroke:#fff; fill:none; stroke-width:3; }
478
+
479
+ #help-proof-notes { width:100%; border:1.5px solid var(--border); border-radius:12px; padding:12px; font-size:14px; font-family:inherit; resize:none; outline:none; color:var(--text); }
480
+ #help-proof-notes:focus { border-color:var(--green); }
481
+
482
+ .help-proof-submit { margin:20px 16px 0; width:calc(100% - 32px); background:var(--green); color:#fff; border:none; border-radius:14px; padding:15px; font-size:15px; font-weight:700; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px; }
483
+ .help-proof-submit svg { width:18px; height:18px; stroke:#fff; fill:none; stroke-width:2; }
484
+ .help-proof-submit:active { background:var(--green-dark); }
485
+ .help-proof-submit:disabled { opacity:.6; cursor:not-allowed; }
486
+
487
+ /* ── Community Help section (ficha do animal) ──────────────────────────── */
488
+ .help-section-title { display:flex; align-items:center; gap:6px; }
489
+ #profile-help-list { display:flex; flex-direction:column; gap:12px; margin-top:4px; }
490
+ .help-event-card { border:1.5px solid var(--green-pale); border-radius:14px; overflow:hidden; background:var(--white); }
491
+ .help-event-photo { width:100%; height:160px; object-fit:cover; display:block; }
492
+ .help-event-body { padding:10px 14px 12px; }
493
+ .help-event-label { font-size:14px; font-weight:700; color:var(--green-dark); margin-bottom:4px; }
494
+ .help-event-notes { font-size:13px; color:var(--text); line-height:1.5; margin-bottom:4px; }
495
+ .help-event-date { font-size:11px; color:var(--text-muted); }
496
+
497
+ /* ── Help Proof Screen ───────────────────────────────────────────────────── */
498
+ #screen-help-proof { display:flex; flex-direction:column; padding:0 0 100px; overflow-y:auto; background:var(--white); }
499
+ #screen-help-proof .flow-header { padding:12px 16px; display:flex; align-items:center; gap:12px; border-bottom:1px solid var(--border); }
500
+ #screen-help-proof .flow-header h2 { font-size:17px; font-weight:700; }
501
+
502
+ #help-proof-animal-row { display:flex; align-items:center; gap:12px; padding:14px 16px; background:var(--green-soft); border-bottom:1px solid var(--border); }
503
+ .help-proof-thumb { width:48px; height:48px; border-radius:12px; background:var(--green-pale) center/cover no-repeat; flex-shrink:0; }
504
+ .help-proof-animal-name { font-size:15px; font-weight:700; }
505
+ .help-proof-animal-sub { font-size:12px; color:var(--text-muted); margin-top:2px; }
506
+
507
+ .help-proof-section { padding:16px 16px 0; }
508
+ .help-proof-label { font-size:12px; font-weight:700; color:var(--text-muted); text-transform:uppercase; letter-spacing:.5px; margin-bottom:10px; display:flex; align-items:center; gap:6px; }
509
+ .optional-tag { font-size:10px; font-weight:500; color:var(--green); background:var(--green-pale); padding:2px 7px; border-radius:20px; text-transform:none; letter-spacing:0; }
510
+
511
+ .help-type-chips { display:flex; flex-wrap:wrap; gap:8px; }
512
+ .help-type-chip { border:1.5px solid var(--border); background:var(--white); border-radius:20px; padding:7px 14px; font-size:13px; font-weight:500; cursor:pointer; color:var(--text); transition:all .15s; }
513
+ .help-type-chip.active { border-color:var(--green); background:var(--green-pale); color:var(--green-dark); font-weight:700; }
514
+
515
+ .help-proof-viewfinder { position:relative; width:100%; height:200px; border-radius:14px; overflow:hidden; background:#f0f0f0; cursor:pointer; border:2px dashed var(--border); }
516
+ #help-proof-input { display:none; }
517
+ #help-proof-preview { width:100%; height:100%; object-fit:cover; display:block; }
518
+ #help-proof-placeholder { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; color:var(--text-muted); pointer-events:none; }
519
+ #help-proof-placeholder svg { width:32px; height:32px; stroke:var(--text-muted); fill:none; stroke-width:1.5; }
520
+ #help-proof-placeholder span { font-size:13px; font-weight:500; }
521
+ .help-proof-ai-badge { position:absolute; bottom:10px; left:10px; background:rgba(33,33,33,.82); color:#fff; border-radius:20px; padding:5px 12px; font-size:12px; font-weight:600; display:flex; align-items:center; gap:6px; }
522
+ .help-proof-ai-badge .dot { width:7px; height:7px; border-radius:50%; background:#4caf50; animation:pulse 1.2s infinite; }
523
+ .help-proof-verified-badge { position:absolute; bottom:10px; left:10px; color:#fff; border-radius:20px; padding:6px 12px; font-size:12px; font-weight:700; display:flex; align-items:center; gap:6px; }
524
+ .help-proof-verified-badge svg { width:14px; height:14px; stroke:#fff; fill:none; stroke-width:3; }
525
+
526
+ #help-proof-notes { width:100%; border:1.5px solid var(--border); border-radius:12px; padding:12px; font-size:14px; font-family:inherit; resize:none; outline:none; color:var(--text); }
527
+ #help-proof-notes:focus { border-color:var(--green); }
528
+
529
+ .help-proof-submit { margin:20px 16px 0; width:calc(100% - 32px); background:var(--green); color:#fff; border:none; border-radius:14px; padding:15px; font-size:15px; font-weight:700; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:8px; }
530
+ .help-proof-submit svg { width:18px; height:18px; stroke:#fff; fill:none; stroke-width:2; }
531
+ .help-proof-submit:active { background:var(--green-dark); }
532
+ .help-proof-submit:disabled { opacity:.6; cursor:not-allowed; }
533
+
534
+ /* ── Community Help (ficha do animal) ────────────────────────────────────── */
535
+ .help-section-title { display:flex; align-items:center; gap:6px; }
536
+ #profile-help-list { display:flex; flex-direction:column; gap:12px; margin-top:4px; }
537
+ .help-event-card { border:1.5px solid var(--green-pale); border-radius:14px; overflow:hidden; background:var(--white); }
538
+ .help-event-photo { width:100%; height:160px; object-fit:cover; display:block; }
539
+ .help-event-body { padding:10px 14px 12px; }
540
+ .help-event-label { font-size:14px; font-weight:700; color:var(--green-dark); margin-bottom:4px; }
541
+ .help-event-notes { font-size:13px; color:var(--text); line-height:1.5; margin-bottom:4px; }
542
+ .help-event-date { font-size:11px; color:var(--text-muted); }
543
+
544
+ /* ── Lucide icons em chips e help labels ──────────────────────────────────── */
545
+ .help-type-chip i, .help-type-chip svg { width:14px; height:14px; vertical-align:-2px; }
546
+ .help-event-label i, .help-event-label svg { width:15px; height:15px; vertical-align:-3px; stroke:var(--green-dark); }