sara.mesquita commited on
Commit
094c5de
Β·
1 Parent(s): 0cfcf78

adjusts ia

Browse files
Files changed (1) hide show
  1. app.py +79 -247
app.py CHANGED
@@ -23,29 +23,30 @@ db = Database()
23
  ai = AnimalAI()
24
  matcher = AnimalMatcher()
25
 
26
- C_GREEN = "#388C59"
27
- C_RED = "#E53935"
28
- C_ORANGE = "#FB8C00"
 
 
29
 
30
  HEAD_HTML = """
31
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous">
32
  <script>
33
  (function() {
34
- // ── GPS ────────────────────────────────────────────────────────────────────
35
- function setGradioValue(id, val) {
36
  var attempts = 0;
37
- function trySet() {
38
- var el = document.getElementById(id);
39
- if (!el) { if (attempts++ < 20) setTimeout(trySet, 300); return; }
40
- var tb = el.querySelector('textarea') || el.querySelector('input[type="text"]');
41
- if (!tb) { if (attempts++ < 20) setTimeout(trySet, 300); return; }
42
- var proto = tb.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
43
  var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
44
- setter.call(tb, val);
45
  tb.dispatchEvent(new Event('input', { bubbles: true }));
46
  tb.dispatchEvent(new Event('change', { bubbles: true }));
47
  }
48
- trySet();
49
  }
50
 
51
  function requestGPS() {
@@ -57,53 +58,46 @@ HEAD_HTML = """
57
  btn.disabled = true;
58
  btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Detectando...';
59
  icon.innerHTML = '<i class="fa-solid fa-spinner fa-spin" style="color:#388C59"></i>';
 
60
  navigator.geolocation.getCurrentPosition(
61
  function(pos) {
62
  var lat = parseFloat(pos.coords.latitude.toFixed(5));
63
  var lng = parseFloat(pos.coords.longitude.toFixed(5));
64
  icon.innerHTML = '<i class="fa-solid fa-check" style="color:#388C59"></i>';
65
- text.innerHTML = '<strong>LocalizaΓ§Γ£o:</strong> ' + lat + ', ' + lng;
66
  btn.innerHTML = '<i class="fa-solid fa-rotate"></i> Atualizar';
67
  btn.disabled = false;
68
  btn.style.background = '#2d7a4a';
69
- setGradioValue('gps-coords', JSON.stringify({ lat: lat, lng: lng }));
70
  },
71
  function(err) {
72
  icon.innerHTML = '<i class="fa-solid fa-triangle-exclamation" style="color:#E53935"></i>';
73
  btn.innerHTML = '<i class="fa-solid fa-location-dot"></i> Tentar novamente';
74
  btn.disabled = false;
75
  btn.style.background = '#E53935';
76
- var msgs = { 1:'PermissΓ£o negada.', 2:'LocalizaΓ§Γ£o indisponΓ­vel.', 3:'Timeout GPS.' };
77
  text.textContent = msgs[err.code] || 'Erro: ' + err.message;
78
- setGradioValue('gps-coords', JSON.stringify({}));
79
  },
80
  { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
81
  );
82
  }
 
83
  window.animalVistoRequestGPS = requestGPS;
 
84
  document.addEventListener('click', function(e) {
85
- var t = e.target.closest('.tab-nav button');
86
- if (t && t.textContent.includes('Registrar')) setTimeout(requestGPS, 600);
87
  });
88
 
89
- // ── CΓ’mera mobile ─────────────────────────────────────────────────────────
90
- function patchCamera() {
91
  var el = document.getElementById('photo-upload');
92
- if (!el) { setTimeout(patchCamera, 800); return; }
93
  var inp = el.querySelector('input[type=file]');
94
  if (inp) { inp.setAttribute('capture', 'environment'); inp.setAttribute('accept', 'image/*'); }
95
- else setTimeout(patchCamera, 800);
96
  }
97
- setTimeout(patchCamera, 1500);
98
-
99
- // ── Ficha modal ───────────────────────────────────────────────────────────
100
- window.openFicha = function(animalId) {
101
- document.getElementById('ficha-overlay').style.display = 'flex';
102
- setGradioValue('selected-animal-id', String(animalId));
103
- };
104
- window.closeFicha = function() {
105
- document.getElementById('ficha-overlay').style.display = 'none';
106
- };
107
  })();
108
  </script>
109
  """
@@ -128,52 +122,25 @@ footer { display: none !important; }
128
  .reg-label i { color: #388C59; }
129
  #photo-upload { border: 2px dashed #cfcfcf !important; border-radius: 12px !important; background: #f9f9f9 !important; min-height: 180px; }
130
  #submit-btn { margin-top: 10px !important; width: 100% !important; background: #388C59 !important; border: none !important; border-radius: 12px !important; font-size: 16px !important; font-weight: 600 !important; padding: 14px !important; color: white !important; }
131
- #status-card { margin-top: 16px; border-radius: 16px; font-size: 14px; line-height: 1.5; }
 
 
132
  #gps-coords { display: none !important; }
133
- #selected-animal-id { display: none !important; }
134
  #animals-tab { padding: 0 0 80px; background: white; }
135
- .animal-card { display: flex; align-items: center; gap: 12px; background: white; border-radius: 12px; padding: 14px 16px; margin: 8px 12px; box-shadow: 0 2px 8px rgba(0,0,0,.07); border: 1px solid #f0f0f0; cursor: pointer; transition: transform .1s; }
136
- .animal-card:active { transform: scale(.98); }
137
- .animal-thumb { width: 56px; height: 56px; border-radius: 12px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 28px; overflow: hidden; background: #eee; }
138
- .animal-thumb img { width: 100%; height: 100%; object-fit: cover; }
139
  .animal-info { flex: 1; min-width: 0; }
140
  .animal-name { font-weight: 600; font-size: 14px; color: #212121; }
141
  .animal-meta { font-size: 12px; color: #888; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
142
  .animal-badge { background: #D9EBD9; color: #388C59; border-radius: 20px; padding: 3px 9px; font-size: 12px; font-weight: 700; flex-shrink: 0; }
143
  .animal-badge.urgent { background: #ffebee; color: #E53935; }
144
  .section-header { padding: 14px 16px 6px; font-size: 13px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: .5px; }
145
- /* Ficha overlay */
146
- #ficha-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.5); z-index: 1000; align-items: flex-end; justify-content: center; }
147
- #ficha-sheet { background: white; width: 100%; max-width: 480px; max-height: 90vh; border-radius: 20px 20px 0 0; overflow-y: auto; padding-bottom: 32px; }
148
- #ficha-sheet .ficha-header { display: flex; align-items: center; gap: 12px; padding: 16px 16px 12px; border-bottom: 1px solid #f0f0f0; position: sticky; top: 0; background: white; z-index: 10; }
149
- #ficha-sheet .ficha-header h2 { margin: 0; font-size: 17px; font-weight: 700; flex: 1; }
150
- #ficha-sheet .ficha-header small { font-size: 12px; color: #888; display: block; }
151
- .ficha-close { background: #f5f5f5; border: none; border-radius: 50%; width: 32px; height: 32px; font-size: 16px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
152
- .photo-strip { display: flex; gap: 8px; padding: 12px 16px; overflow-x: auto; scrollbar-width: none; }
153
- .photo-strip-item { flex-shrink: 0; text-align: center; position: relative; }
154
- .photo-strip-item img { width: 72px; height: 72px; object-fit: cover; border-radius: 10px; border: 2px solid #eee; }
155
- .photo-strip-item.novo img { border-color: #388C59; }
156
- .photo-strip-item .novo-badge { position: absolute; top: -6px; right: -6px; background: #388C59; color: white; font-size: 9px; font-weight: 700; padding: 2px 5px; border-radius: 8px; }
157
- .photo-strip-item .photo-date { font-size: 10px; color: #888; margin-top: 4px; }
158
- .ficha-section { margin: 0 16px 16px; }
159
- .ficha-section h3 { font-size: 13px; font-weight: 700; color: #212121; margin: 0 0 10px; display: flex; align-items: center; gap: 6px; }
160
- .id-table { width: 100%; border-collapse: collapse; }
161
- .id-table td { padding: 7px 0; font-size: 13px; border-bottom: 1px solid #f5f5f5; }
162
- .id-table td:first-child { color: #888; width: 40%; }
163
- .id-table td:last-child { color: #212121; font-weight: 500; }
164
- .marks-card { background: #fff8e1; border-radius: 10px; padding: 10px 12px; font-size: 13px; color: #795548; border-left: 3px solid #FFC107; }
165
- .trajectory { display: flex; align-items: center; gap: 0; padding: 8px 0; position: relative; }
166
- .traj-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
167
- .traj-line { flex: 1; height: 2px; background: #ddd; }
168
- .traj-label { font-size: 10px; color: #888; text-align: center; margin-top: 4px; }
169
- .adopt-btn { display: block; width: calc(100% - 32px); margin: 8px 16px; padding: 14px; background: #D9EBD9; border: none; border-radius: 12px; color: #388C59; font-size: 15px; font-weight: 600; cursor: pointer; text-align: center; }
170
- .adopt-btn:hover { background: #c8e6c9; }
171
  """
172
 
173
  GPS_HTML = """
174
  <div id="gps-box" style="display:flex;align-items:center;gap:10px;background:#D9EBD9;border-radius:10px;padding:12px 14px;font-size:13px;color:#2e7d32;font-weight:500;border:1px solid #c8e6c9;">
175
  <span id="gps-icon" style="font-size:18px"><i class="fa-solid fa-location-crosshairs"></i></span>
176
- <span id="gps-text" style="flex:1">Toque para detectar localizaΓ§Γ£o</span>
177
  <button id="gps-btn" onclick="window.animalVistoRequestGPS && window.animalVistoRequestGPS()"
178
  style="background:#388C59;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;">
179
  <i class="fa-solid fa-location-dot"></i> Localizar
@@ -181,34 +148,32 @@ GPS_HTML = """
181
  </div>
182
  """
183
 
184
- FICHA_OVERLAY_HTML = """
185
- <div id="ficha-overlay" onclick="if(event.target===this)window.closeFicha()">
186
- <div id="ficha-sheet">
187
- <div id="ficha-content" style="min-height:200px;display:flex;align-items:center;justify-content:center;color:#aaa;">
188
- <i class="fa-solid fa-spinner fa-spin" style="font-size:24px"></i>
189
- </div>
190
- </div>
191
- </div>
192
- """
193
-
194
 
195
  def build_map_html(species="all", timeframe="all"):
196
  import base64 as _b64
197
  data = db.get_map_data(species, timeframe)
 
198
  data_b64 = _b64.b64encode(json.dumps(data, ensure_ascii=False).encode()).decode()
 
199
  inner = (
200
  '<!DOCTYPE html><html><head>'
201
  '<meta charset="utf-8"/>'
202
  '<meta name="viewport" content="width=device-width,initial-scale=1"/>'
203
  '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"/>'
204
- '<style>body{margin:0;padding:0;}#map{height:100vh;width:100%;}</style></head><body>'
 
 
205
  '<div id="map"></div>'
206
  '<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>'
207
  '<script>'
208
  'var animals=JSON.parse(atob("' + data_b64 + '"));'
209
  'var map=L.map("map").setView([-23.0316,-46.9785],13);'
210
  'L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:"OSM",maxZoom:19}).addTo(map);'
211
- 'if(!animals.length){var info=L.control({position:"topright"});info.onAdd=function(){var d=L.DomUtil.create("div");d.style="background:white;padding:8px 12px;border-radius:8px;font-size:13px;color:#888;";d.innerHTML="Nenhum avistamento ainda";return d;};info.addTo(map);}'
 
 
 
 
212
  'animals.forEach(function(a){'
213
  ' var color=a.days_since>30?"#E53935":a.count>1?"#FB8C00":"#388C59";'
214
  ' var em=a.species==="dog"?"D":"G";'
@@ -216,10 +181,12 @@ def build_map_html(species="all", timeframe="all"):
216
  ' var ico=L.divIcon({html:"<div style=\'position:relative;background:"+color+";width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:white;box-shadow:0 2px 8px rgba(0,0,0,.3);border:2.5px solid white;\'>"+em+badge+"</div>",className:"",iconSize:[38,38],iconAnchor:[19,19],popupAnchor:[0,-22]});'
217
  ' var sp=a.species==="dog"?"Cao":"Gato";'
218
  ' var urg=a.days_since>30?"<br><small style=\'color:#E53935\'>Nao visto ha "+a.days_since+" dias</small>":"";'
219
- ' L.marker([a.lat,a.lng],{icon:ico}).addTo(map).bindPopup("<b>"+sp+" #"+a.id+"</b>"+(a.desc?"<br><small>"+a.desc+"</small>":"")+"<br><small>"+a.count+"x - "+a.last_seen+"</small>"+urg,{maxWidth:220});'
 
220
  '});'
221
  '</script></body></html>'
222
  )
 
223
  srcdoc = inner.replace('&', '&amp;').replace('"', '&quot;')
224
  return f'<iframe srcdoc="{srcdoc}" style="width:100%;height:500px;border:none;"></iframe>'
225
 
@@ -227,36 +194,37 @@ def build_map_html(species="all", timeframe="all"):
227
  def build_animals_html():
228
  animals = db.get_recent_animals(limit=30)
229
  if not animals:
230
- return '<div style="padding:40px 16px;text-align:center;color:#aaa;"><div style="font-size:48px">🐾</div><div style="font-size:15px;font-weight:500;margin-top:12px;">Nenhum animal registrado ainda</div><div style="font-size:13px;margin-top:6px;">VÑ para Registrar e tire a primeira foto!</div></div>'
231
  cards = []
232
  for a in animals:
233
  try:
234
  desc = json.loads(a.get("description") or "{}")
235
  except Exception:
236
  desc = {}
237
- is_dog = a["species"] == "dog"
238
- em = "πŸ•" if is_dog else "🐈"
239
- sp_pt = "CΓ£o" if is_dog else "Gato"
240
- urgent = a.get("days_since", 0) > 30
241
- color = C_RED if urgent else C_GREEN
242
  badge_cls = "animal-badge urgent" if urgent else "animal-badge"
243
- breed = desc.get("breed_estimate", "raΓ§a desconhecida")
244
  color_coat = desc.get("primary_color", "")
245
- meta = f"{breed}{' Β· ' + color_coat if color_coat else ''}"
246
- count = a["sighting_count"]
247
  last_seen = a.get("last_seen_short", "")
248
  photo_url = a.get("last_photo_url") or ""
249
- urgent_icon = '<i class="fa-solid fa-triangle-exclamation"></i> ' if urgent else ""
250
  if photo_url:
251
- thumb = f'<img src="{photo_url}" style="width:56px;height:56px;border-radius:12px;object-fit:cover;" onerror="this.parentElement.innerHTML=\'{em}\'">'
252
  else:
253
  thumb = em
254
- cards.append(f'''<div class="animal-card" onclick="window.openFicha({a['id']})">
 
 
255
  <div class="animal-thumb">{thumb}</div>
256
  <div class="animal-info">
257
- <div class="animal-name">{sp_pt} #{a['id']}</div>
258
  <div class="animal-meta">{meta}</div>
259
- <div class="animal-meta" style="color:{color};">{urgent_icon}Visto {count}x Β· ΓΊltimo: {last_seen}</div>
260
  </div>
261
  <div class="{badge_cls}">{count}x</div>
262
  </div>''')
@@ -265,145 +233,25 @@ def build_animals_html():
265
  return f'<div class="section-header">🐾 {total_a} animais Β· {total_s} avistamentos</div>' + "".join(cards)
266
 
267
 
268
- def build_ficha_html(animal_id_str: str) -> str:
269
- try:
270
- animal_id = int(animal_id_str.strip())
271
- except Exception:
272
- return ""
273
- detail = db.get_animal_detail(animal_id)
274
- if not detail:
275
- return ""
276
- animal = detail["animal"]
277
- sightings = detail["sightings"]
278
- try:
279
- desc = json.loads(animal.get("description") or "{}")
280
- except Exception:
281
- desc = {}
282
-
283
- is_dog = animal["species"] == "dog"
284
- em = "πŸ•" if is_dog else "🐈"
285
- sp_pt = "Cachorro" if is_dog else "Gato"
286
- count = animal["sighting_count"]
287
- breed = desc.get("breed_estimate", "Vira-lata")
288
- size_map = {"small": "Pequeno", "medium": "MΓ©dio", "large": "Grande"}
289
- size_pt = size_map.get(desc.get("size", ""), desc.get("size", "β€”"))
290
- colors = desc.get("primary_color", "β€”")
291
- if desc.get("secondary_colors"):
292
- colors += " com " + ", ".join(desc["secondary_colors"])
293
- condition_map = {"healthy": "Aparentemente saudΓ‘vel", "thin": "Magro", "injured": "Ferido"}
294
- condition_pt = condition_map.get(desc.get("condition", ""), "β€”")
295
- marks = desc.get("distinctive_marks", [])
296
-
297
- # Tira foto strip
298
- photos_html = ""
299
- for i, s in enumerate(sightings[:6]):
300
- date_str = (s.get("created_at") or "")[:10]
301
- try:
302
- from datetime import date
303
- d = date.fromisoformat(date_str)
304
- date_label = f"{d.day:02d}/{d.month:02d}"
305
- except Exception:
306
- date_label = date_str[-5:] if date_str else "β€”"
307
- is_novo = (i == 0)
308
- novo_badge = '<span class="novo-badge">Novo</span>' if is_novo else ""
309
- novo_cls = "novo" if is_novo else ""
310
- if s["photo_url"]:
311
- img_tag = f'<img src="{s["photo_url"]}" onerror="this.src=\'\'"/>'
312
- else:
313
- img_tag = f'<div style="width:72px;height:72px;border-radius:10px;background:#f0f0f0;display:flex;align-items:center;justify-content:center;font-size:28px;border:2px solid #eee;">{em}</div>'
314
- photos_html += f'<div class="photo-strip-item {novo_cls}">{novo_badge}{img_tag}<div class="photo-date">{date_label}</div></div>'
315
-
316
- # TrajetΓ³ria
317
- traj_html = ""
318
- if len(sightings) > 1:
319
- colors_dot = ["#388C59", "#FB8C00", "#E53935"]
320
- items = list(reversed(sightings[:5]))
321
- for i, s in enumerate(items):
322
- date_str = (s.get("created_at") or "")[:10]
323
- try:
324
- from datetime import date
325
- d = date.fromisoformat(date_str)
326
- lbl = f"{d.day:02d}/{d.month:02d}"
327
- except Exception:
328
- lbl = date_str[-5:] if date_str else ""
329
- dot_color = colors_dot[min(i, 2)]
330
- traj_html += f'<div style="display:flex;flex-direction:column;align-items:center;gap:4px;">'
331
- if i > 0:
332
- traj_html += f'<div style="flex:1;height:2px;background:#ddd;width:40px;margin-top:6px;"></div>'
333
- traj_html += f'<div style="width:12px;height:12px;border-radius:50%;background:{dot_color};"></div>'
334
- traj_html += f'<div style="font-size:10px;color:#888;">{lbl}</div></div>'
335
-
336
- # Marcas
337
- marks_html = ""
338
- if marks:
339
- marks_list = " Β· ".join(marks)
340
- marks_html = f'<div class="ficha-section"><h3>✨ Características marcantes</h3><div class="marks-card">{marks_list}</div></div>'
341
-
342
- ficha = f'''
343
- <div class="ficha-header">
344
- <button class="ficha-close" onclick="window.closeFicha()"><i class="fa-solid fa-xmark"></i></button>
345
- <div>
346
- <h2>Ficha do animal</h2>
347
- <small>{count} avistamento{'s' if count != 1 else ''} na regiΓ£o</small>
348
- </div>
349
- </div>
350
- <div class="photo-strip">{photos_html}</div>
351
- <div class="ficha-section">
352
- <h3>🐾 Identificação</h3>
353
- <table class="id-table">
354
- <tr><td>EspΓ©cie</td><td>{sp_pt}</td></tr>
355
- <tr><td>RaΓ§a</td><td>{breed}</td></tr>
356
- <tr><td>Porte</td><td>{size_pt}</td></tr>
357
- <tr><td>Cor</td><td>{colors}</td></tr>
358
- <tr><td>CondiΓ§Γ£o</td><td>{condition_pt}</td></tr>
359
- </table>
360
- </div>
361
- {marks_html}
362
- {'<div class="ficha-section"><h3>πŸ“ TrajetΓ³ria observada</h3><div style="display:flex;align-items:flex-start;gap:0;">' + traj_html + '</div></div>' if traj_html else ''}
363
- <button class="adopt-btn">🏠 Quero adotar β€” entrar em contato</button>
364
- '''
365
- return f'<div id="ficha-content">{ficha}</div>'
366
-
367
-
368
  def build_confirmation_html(animal_id, is_new, count, species, photo_url=""):
369
  em = "πŸ•" if species == "dog" else "🐈"
370
- sp_pt = "Cachorro" if species == "dog" else "Gato"
371
- try:
372
- desc_raw = (db.get_animal(animal_id) or {}).get("description") or "{}"
373
- desc = json.loads(desc_raw)
374
- breed = desc.get("breed_estimate", "Vira-lata")
375
- except Exception:
376
- breed = "Vira-lata"
377
- title = "Avistamento registrado!" if is_new else f"{em} Animal reconhecido!"
378
- sub = "Obrigado por ajudar os animais da sua região 🐾"
379
- ord_pt = {1:"1Β°",2:"2Β°",3:"3Β°",4:"4Β°"}.get(count, f"{count}Β°")
380
  return f'''
381
- <div id="status-card" style="background:white;border-radius:16px;overflow:hidden;box-shadow:0 4px 20px rgba(0,0,0,0.1);">
382
- <div style="background:#D9EBD9;padding:32px 16px 24px;text-align:center;">
383
- <div style="width:80px;height:80px;border-radius:50%;background:#388C59;display:inline-flex;align-items:center;justify-content:center;font-size:36px;position:relative;margin-bottom:12px;">
384
- {em}
385
- <div style="position:absolute;bottom:-4px;right:-4px;width:26px;height:26px;border-radius:50%;background:white;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.2);">
386
- <i class="fa-solid fa-check" style="color:#388C59;font-size:12px;"></i>
387
- </div>
388
- </div>
389
- <div style="font-weight:700;font-size:20px;color:#212121;margin-bottom:4px;">{title}</div>
390
- <div style="font-size:13px;color:#555;">{sub}</div>
391
- </div>
392
- <div style="padding:16px;">
393
- <div style="background:#f8f9fa;border-radius:12px;padding:12px 14px;font-size:13px;margin-bottom:16px;">
394
- <div style="display:flex;gap:10px;padding:6px 0;border-bottom:1px solid #eee;"><span style="color:#888;width:90px;flex-shrink:0;">Animal</span><span style="font-weight:500;">{sp_pt} Β· {breed}</span></div>
395
- <div style="display:flex;gap:10px;padding:6px 0;border-bottom:1px solid #eee;"><span style="color:#888;width:90px;flex-shrink:0;">Registrado</span><span style="font-weight:500;">Agora hΓ‘ pouco</span></div>
396
- <div style="display:flex;gap:10px;padding:6px 0;"><span style="color:#888;width:90px;flex-shrink:0;">Na regiΓ£o</span><span style="font-weight:500;">{ord_pt} avistamento</span></div>
397
- </div>
398
- <button onclick="document.querySelector('.tab-nav button').click()" style="width:100%;padding:13px;background:#388C59;color:white;border:none;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;margin-bottom:8px;">Ver no mapa</button>
399
- <button onclick="document.querySelectorAll('.tab-nav button')[1].click()" style="width:100%;padding:13px;background:transparent;color:#388C59;border:2px solid #388C59;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;">Registrar outro animal</button>
400
- </div>
401
  </div>'''
402
 
403
 
404
  def process_sighting(image, gps_json, notes, progress=gr.Progress()):
405
  if image is None:
406
- return gr.update(), '<div style="padding:16px;background:#ffebee;border-radius:12px;color:#b71c1c;border-left:4px solid #E53935;">Tire uma foto do animal antes de registrar.</div>'
407
  progress(0.15, desc="Analisando imagem...")
408
  try:
409
  coords = json.loads(gps_json) if gps_json and gps_json.strip() else {}
@@ -429,8 +277,6 @@ def process_sighting(image, gps_json, notes, progress=gr.Progress()):
429
  is_new = False
430
  else:
431
  animal_id = db.create_animal(description, embedding)
432
- photo_path = db.save_photo(image, animal_id=animal_id)
433
- photo_url = db.photo_url(photo_path) or ""
434
  db.add_sighting(animal_id, photo_path, lat, lng, notes)
435
  count = 1
436
  species = description.get("species", "dog")
@@ -439,10 +285,9 @@ def process_sighting(image, gps_json, notes, progress=gr.Progress()):
439
  return gr.update(value=build_map_html()), build_confirmation_html(animal_id, is_new, count, species, photo_url)
440
 
441
 
442
- with gr.Blocks(css=CSS, head=HEAD_HTML, theme=gr.themes.Base(), title="Animal Visto") as demo:
443
 
444
  gr.HTML('<div id="top-bar"><h1>🐾 Animal Visto</h1><small>Vinhedo, SP</small></div>')
445
- gr.HTML(FICHA_OVERLAY_HTML)
446
 
447
  species_st = gr.State("all")
448
  timeframe_st = gr.State("all")
@@ -452,7 +297,7 @@ with gr.Blocks(css=CSS, head=HEAD_HTML, theme=gr.themes.Base(), title="Animal Vi
452
  with gr.Tab("πŸ—ΊοΈ Mapa"):
453
  with gr.Row(elem_classes="filter-row"):
454
  btn_all = gr.Button("Todos", elem_classes="filter-row")
455
- btn_dogs = gr.Button("πŸ• CΓ£es", elem_classes="filter-row")
456
  btn_cats = gr.Button("🐈 Gatos", elem_classes="filter-row")
457
  btn_today = gr.Button("Hoje", elem_classes="filter-row secondary")
458
  btn_week = gr.Button("Esta semana", elem_classes="filter-row secondary")
@@ -460,12 +305,12 @@ with gr.Blocks(css=CSS, head=HEAD_HTML, theme=gr.themes.Base(), title="Animal Vi
460
 
461
  with gr.Tab("πŸ“· Registrar", elem_id="register-tab"):
462
  with gr.Column(elem_classes="form-card"):
463
- gr.HTML('<div class="reg-label"><i class="fa-solid fa-location-dot"></i> Passo 1: LocalizaΓ§Γ£o</div>')
464
  gr.HTML(GPS_HTML)
465
- gps_coords = gr.Textbox(value="", elem_id="gps-coords", interactive=True, visible=False, label="gps")
466
  gr.HTML('<div class="reg-label"><i class="fa-solid fa-camera"></i> Passo 2: Foto do Animal</div>')
467
- photo_input = gr.Image(label="", type="pil", sources=["upload","webcam"], interactive=True, show_label=False, elem_id="photo-upload")
468
- gr.HTML('<div class="reg-label"><i class="fa-solid fa-file-signature"></i> Passo 3: ObservaΓ§Γ΅es (opcional)</div>')
469
  notes_input = gr.Textbox(label="", placeholder="Ex.: parece ferido, tem coleira...", lines=2, max_lines=4, show_label=False)
470
  submit_btn = gr.Button("Registrar avistamento", variant="primary", elem_id="submit-btn")
471
  status_html = gr.HTML("")
@@ -474,11 +319,6 @@ with gr.Blocks(css=CSS, head=HEAD_HTML, theme=gr.themes.Base(), title="Animal Vi
474
  refresh_btn = gr.Button("Atualizar lista", size="sm", variant="secondary")
475
  animals_display = gr.HTML(build_animals_html())
476
 
477
- # Ficha modal: campo oculto recebe animal_id via JS β†’ carrega ficha
478
- selected_animal_id = gr.Textbox(value="", elem_id="selected-animal-id", visible=False, interactive=True, label="sel")
479
- ficha_html = gr.HTML("", elem_id="ficha-content-gradio")
480
-
481
- # Eventos
482
  submit_btn.click(process_sighting, inputs=[photo_input, gps_coords, notes_input], outputs=[map_html, status_html])
483
  btn_all.click(lambda: (build_map_html("all","all"),"all","all"), outputs=[map_html, species_st, timeframe_st])
484
  btn_dogs.click(lambda t: (build_map_html("dog",t),"dog",t), inputs=[timeframe_st], outputs=[map_html, species_st, timeframe_st])
@@ -488,21 +328,13 @@ with gr.Blocks(css=CSS, head=HEAD_HTML, theme=gr.themes.Base(), title="Animal Vi
488
  refresh_btn.click(build_animals_html, outputs=[animals_display])
489
  demo.load(build_map_html, outputs=[map_html])
490
 
491
- # Ficha: quando animal selecionado muda β†’ carrega HTML e injeta no overlay
492
- def load_ficha_and_inject(animal_id_str):
493
- html = build_ficha_html(animal_id_str)
494
- # Injeta via JS no overlay
495
- js = f"<script>var fc=document.getElementById('ficha-content');if(fc){{fc.outerHTML=`{html.replace('`','\'')}`;}}</script>"
496
- return js
497
- selected_animal_id.change(load_ficha_and_inject, inputs=[selected_animal_id], outputs=[ficha_html])
498
-
499
 
500
  if __name__ == "__main__":
501
  import gradio.blocks as _gb_mod
502
  _orig_gai = _gb_mod.Blocks.get_api_info
503
  def _safe_get_api_info(self):
504
  try: return _orig_gai(self)
505
- except Exception as e: logging.warning(f"get_api_info: {e}"); return {}
506
  _gb_mod.Blocks.get_api_info = _safe_get_api_info
507
  from core.database import DATA_DIR, PHOTOS_DIR
508
  PHOTOS_DIR.mkdir(parents=True, exist_ok=True)
 
23
  ai = AnimalAI()
24
  matcher = AnimalMatcher()
25
 
26
+ C_GREEN = "#388C59"
27
+ C_GREEN_L = "#D9EBD9"
28
+ C_TEXT = "#212121"
29
+ C_RED = "#E53935"
30
+ C_ORANGE = "#FB8C00"
31
 
32
  HEAD_HTML = """
33
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" crossorigin="anonymous">
34
  <script>
35
  (function() {
36
+ function updateGradioTextbox(elementId, value) {
 
37
  var attempts = 0;
38
+ function tryUpdate() {
39
+ var container = document.getElementById(elementId);
40
+ if (!container) { if (attempts++ < 20) setTimeout(tryUpdate, 300); return; }
41
+ var tb = container.querySelector('textarea') || container.querySelector('input[type="text"]');
42
+ if (!tb) { if (attempts++ < 20) setTimeout(tryUpdate, 300); return; }
43
+ var proto = (tb.tagName === 'TEXTAREA') ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
44
  var setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
45
+ setter.call(tb, value);
46
  tb.dispatchEvent(new Event('input', { bubbles: true }));
47
  tb.dispatchEvent(new Event('change', { bubbles: true }));
48
  }
49
+ tryUpdate();
50
  }
51
 
52
  function requestGPS() {
 
58
  btn.disabled = true;
59
  btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Detectando...';
60
  icon.innerHTML = '<i class="fa-solid fa-spinner fa-spin" style="color:#388C59"></i>';
61
+ text.textContent = 'Aguardando GPS...';
62
  navigator.geolocation.getCurrentPosition(
63
  function(pos) {
64
  var lat = parseFloat(pos.coords.latitude.toFixed(5));
65
  var lng = parseFloat(pos.coords.longitude.toFixed(5));
66
  icon.innerHTML = '<i class="fa-solid fa-check" style="color:#388C59"></i>';
67
+ text.innerHTML = '<strong>Localizacao:</strong> ' + lat + ', ' + lng;
68
  btn.innerHTML = '<i class="fa-solid fa-rotate"></i> Atualizar';
69
  btn.disabled = false;
70
  btn.style.background = '#2d7a4a';
71
+ updateGradioTextbox('gps-coords', JSON.stringify({ lat: lat, lng: lng }));
72
  },
73
  function(err) {
74
  icon.innerHTML = '<i class="fa-solid fa-triangle-exclamation" style="color:#E53935"></i>';
75
  btn.innerHTML = '<i class="fa-solid fa-location-dot"></i> Tentar novamente';
76
  btn.disabled = false;
77
  btn.style.background = '#E53935';
78
+ var msgs = { 1:'Permissao negada.', 2:'Localizacao indisponivel.', 3:'Timeout GPS.' };
79
  text.textContent = msgs[err.code] || 'Erro: ' + err.message;
80
+ updateGradioTextbox('gps-coords', JSON.stringify({}));
81
  },
82
  { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
83
  );
84
  }
85
+
86
  window.animalVistoRequestGPS = requestGPS;
87
+
88
  document.addEventListener('click', function(e) {
89
+ var tabBtn = e.target.closest('.tab-nav button');
90
+ if (tabBtn && tabBtn.textContent.includes('Registrar')) setTimeout(requestGPS, 600);
91
  });
92
 
93
+ function patchCameraInput() {
 
94
  var el = document.getElementById('photo-upload');
95
+ if (!el) { setTimeout(patchCameraInput, 800); return; }
96
  var inp = el.querySelector('input[type=file]');
97
  if (inp) { inp.setAttribute('capture', 'environment'); inp.setAttribute('accept', 'image/*'); }
98
+ else setTimeout(patchCameraInput, 800);
99
  }
100
+ setTimeout(patchCameraInput, 1500);
 
 
 
 
 
 
 
 
 
101
  })();
102
  </script>
103
  """
 
122
  .reg-label i { color: #388C59; }
123
  #photo-upload { border: 2px dashed #cfcfcf !important; border-radius: 12px !important; background: #f9f9f9 !important; min-height: 180px; }
124
  #submit-btn { margin-top: 10px !important; width: 100% !important; background: #388C59 !important; border: none !important; border-radius: 12px !important; font-size: 16px !important; font-weight: 600 !important; padding: 14px !important; color: white !important; }
125
+ #status-card { margin-top: 16px; border-radius: 12px; font-size: 14px; line-height: 1.5; }
126
+ #status-card.ok { background: #e8f5e9; color: #2e7d32; border-left: 4px solid #388C59; padding: 14px 16px; }
127
+ #status-card.err { background: #ffebee; color: #b71c1c; border-left: 4px solid #E53935; padding: 14px 16px; }
128
  #gps-coords { display: none !important; }
 
129
  #animals-tab { padding: 0 0 80px; background: white; }
130
+ .animal-card { display: flex; align-items: center; gap: 12px; background: white; border-radius: 12px; padding: 14px 16px; margin: 8px 12px; box-shadow: 0 2px 8px rgba(0,0,0,.07); border: 1px solid #f0f0f0; }
131
+ .animal-thumb { width: 52px; height: 52px; border-radius: 10px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 24px; overflow: hidden; background: #eee; }
 
 
132
  .animal-info { flex: 1; min-width: 0; }
133
  .animal-name { font-weight: 600; font-size: 14px; color: #212121; }
134
  .animal-meta { font-size: 12px; color: #888; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
135
  .animal-badge { background: #D9EBD9; color: #388C59; border-radius: 20px; padding: 3px 9px; font-size: 12px; font-weight: 700; flex-shrink: 0; }
136
  .animal-badge.urgent { background: #ffebee; color: #E53935; }
137
  .section-header { padding: 14px 16px 6px; font-size: 13px; font-weight: 600; color: #888; text-transform: uppercase; letter-spacing: .5px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  """
139
 
140
  GPS_HTML = """
141
  <div id="gps-box" style="display:flex;align-items:center;gap:10px;background:#D9EBD9;border-radius:10px;padding:12px 14px;font-size:13px;color:#2e7d32;font-weight:500;border:1px solid #c8e6c9;">
142
  <span id="gps-icon" style="font-size:18px"><i class="fa-solid fa-location-crosshairs"></i></span>
143
+ <span id="gps-text" style="flex:1">Toque para detectar localizacao</span>
144
  <button id="gps-btn" onclick="window.animalVistoRequestGPS && window.animalVistoRequestGPS()"
145
  style="background:#388C59;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;">
146
  <i class="fa-solid fa-location-dot"></i> Localizar
 
148
  </div>
149
  """
150
 
 
 
 
 
 
 
 
 
 
 
151
 
152
  def build_map_html(species="all", timeframe="all"):
153
  import base64 as _b64
154
  data = db.get_map_data(species, timeframe)
155
+ # Base64 evita conflito de aspas e escaping no srcdoc
156
  data_b64 = _b64.b64encode(json.dumps(data, ensure_ascii=False).encode()).decode()
157
+ # Leaflet via cdnjs (mais estΓ‘vel que unpkg no HuggingFace)
158
  inner = (
159
  '<!DOCTYPE html><html><head>'
160
  '<meta charset="utf-8"/>'
161
  '<meta name="viewport" content="width=device-width,initial-scale=1"/>'
162
  '<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"/>'
163
+ '<style>body{margin:0;padding:0;}#map{height:100vh;width:100%;}'
164
+ '.popup-photo{width:100%;height:80px;object-fit:cover;border-radius:6px;margin-bottom:6px;}'
165
+ '</style></head><body>'
166
  '<div id="map"></div>'
167
  '<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>'
168
  '<script>'
169
  'var animals=JSON.parse(atob("' + data_b64 + '"));'
170
  'var map=L.map("map").setView([-23.0316,-46.9785],13);'
171
  'L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:"OSM",maxZoom:19}).addTo(map);'
172
+ 'if(!animals.length){'
173
+ ' var info=L.control({position:"topright"});'
174
+ ' info.onAdd=function(){var d=L.DomUtil.create("div");d.style="background:white;padding:8px 12px;border-radius:8px;font-size:13px;color:#888;";d.innerHTML="Nenhum avistamento ainda";return d;};'
175
+ ' info.addTo(map);'
176
+ '}'
177
  'animals.forEach(function(a){'
178
  ' var color=a.days_since>30?"#E53935":a.count>1?"#FB8C00":"#388C59";'
179
  ' var em=a.species==="dog"?"D":"G";'
 
181
  ' var ico=L.divIcon({html:"<div style=\'position:relative;background:"+color+";width:38px;height:38px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:700;color:white;box-shadow:0 2px 8px rgba(0,0,0,.3);border:2.5px solid white;\'>"+em+badge+"</div>",className:"",iconSize:[38,38],iconAnchor:[19,19],popupAnchor:[0,-22]});'
182
  ' var sp=a.species==="dog"?"Cao":"Gato";'
183
  ' var urg=a.days_since>30?"<br><small style=\'color:#E53935\'>Nao visto ha "+a.days_since+" dias</small>":"";'
184
+ ' var popup="<b>"+sp+" #"+a.id+"</b>"+(a.desc?"<br><small>"+a.desc+"</small>":"")+"<br><small>"+a.count+"x - "+a.last_seen+"</small>"+urg;'
185
+ ' L.marker([a.lat,a.lng],{icon:ico}).addTo(map).bindPopup(popup,{maxWidth:220});'
186
  '});'
187
  '</script></body></html>'
188
  )
189
+ # srcdoc: só escapa & e " (NÃO escapa < e > para não quebrar o HTML)
190
  srcdoc = inner.replace('&', '&amp;').replace('"', '&quot;')
191
  return f'<iframe srcdoc="{srcdoc}" style="width:100%;height:500px;border:none;"></iframe>'
192
 
 
194
  def build_animals_html():
195
  animals = db.get_recent_animals(limit=30)
196
  if not animals:
197
+ return '<div style="padding:40px 16px;text-align:center;color:#aaa;"><div style="font-size:48px">🐾</div><div style="font-size:15px;font-weight:500;margin-top:12px;">Nenhum animal registrado ainda</div></div>'
198
  cards = []
199
  for a in animals:
200
  try:
201
  desc = json.loads(a.get("description") or "{}")
202
  except Exception:
203
  desc = {}
204
+ is_dog = a["species"] == "dog"
205
+ em = "πŸ•" if is_dog else "🐈"
206
+ sp_pt = "Cao" if is_dog else "Gato"
207
+ urgent = a.get("days_since", 0) > 30
208
+ color = C_RED if urgent else C_GREEN
209
  badge_cls = "animal-badge urgent" if urgent else "animal-badge"
210
+ breed = desc.get("breed_estimate", "raca desconhecida")
211
  color_coat = desc.get("primary_color", "")
212
+ meta = f"{breed}{' Β· ' + color_coat if color_coat else ''}"
213
+ count = a["sighting_count"]
214
  last_seen = a.get("last_seen_short", "")
215
  photo_url = a.get("last_photo_url") or ""
 
216
  if photo_url:
217
+ thumb = f'<img src="{photo_url}" style="width:52px;height:52px;border-radius:10px;object-fit:cover;" onerror="this.parentElement.innerHTML=\'{em}\'">'
218
  else:
219
  thumb = em
220
+ urgent_icon = '<i class="fa-solid fa-triangle-exclamation"></i> ' if urgent else ''
221
+ cards.append(f'''
222
+ <div class="animal-card">
223
  <div class="animal-thumb">{thumb}</div>
224
  <div class="animal-info">
225
+ <div class="animal-name">{sp_pt} #{a["id"]}</div>
226
  <div class="animal-meta">{meta}</div>
227
+ <div class="animal-meta" style="color:{color};">{urgent_icon}Visto {count}x Β· ultimo: {last_seen}</div>
228
  </div>
229
  <div class="{badge_cls}">{count}x</div>
230
  </div>''')
 
233
  return f'<div class="section-header">🐾 {total_a} animais Β· {total_s} avistamentos</div>' + "".join(cards)
234
 
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  def build_confirmation_html(animal_id, is_new, count, species, photo_url=""):
237
  em = "πŸ•" if species == "dog" else "🐈"
238
+ sp_pt = "cao" if species == "dog" else "gato"
239
+ title = f"Novo {sp_pt} registrado!" if is_new else f"{em} Animal reconhecido!"
240
+ sub = "1o avistamento β€” obrigada por registrar!" if is_new else f"Este {sp_pt} ja foi avistado <b>{count}x</b> na regiao."
241
+ photo_html = f'<img src="{photo_url}" style="width:100%;max-height:160px;object-fit:cover;border-radius:8px;margin-bottom:8px;" onerror="this.style.display=\'none\'">' if photo_url else ""
 
 
 
 
 
 
242
  return f'''
243
+ <div id="status-card" class="ok">
244
+ {photo_html}
245
+ <div style="font-size:24px;margin-bottom:6px;color:#388C59;">βœ… {em}</div>
246
+ <div style="font-weight:700;font-size:15px;">{title}</div>
247
+ <div>{sub}</div>
248
+ <div style="font-size:12px;margin-top:6px;color:#555;">ID #{animal_id} Β· Avistamento salvo</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  </div>'''
250
 
251
 
252
  def process_sighting(image, gps_json, notes, progress=gr.Progress()):
253
  if image is None:
254
+ return gr.update(), '<div id="status-card" class="err">Tire uma foto do animal antes de registrar.</div>'
255
  progress(0.15, desc="Analisando imagem...")
256
  try:
257
  coords = json.loads(gps_json) if gps_json and gps_json.strip() else {}
 
277
  is_new = False
278
  else:
279
  animal_id = db.create_animal(description, embedding)
 
 
280
  db.add_sighting(animal_id, photo_path, lat, lng, notes)
281
  count = 1
282
  species = description.get("species", "dog")
 
285
  return gr.update(value=build_map_html()), build_confirmation_html(animal_id, is_new, count, species, photo_url)
286
 
287
 
288
+ with gr.Blocks(css=CSS, head=HEAD_HTML, theme=gr.themes.Base(), title="Animal Visto 🐾") as demo:
289
 
290
  gr.HTML('<div id="top-bar"><h1>🐾 Animal Visto</h1><small>Vinhedo, SP</small></div>')
 
291
 
292
  species_st = gr.State("all")
293
  timeframe_st = gr.State("all")
 
297
  with gr.Tab("πŸ—ΊοΈ Mapa"):
298
  with gr.Row(elem_classes="filter-row"):
299
  btn_all = gr.Button("Todos", elem_classes="filter-row")
300
+ btn_dogs = gr.Button("πŸ• Caes", elem_classes="filter-row")
301
  btn_cats = gr.Button("🐈 Gatos", elem_classes="filter-row")
302
  btn_today = gr.Button("Hoje", elem_classes="filter-row secondary")
303
  btn_week = gr.Button("Esta semana", elem_classes="filter-row secondary")
 
305
 
306
  with gr.Tab("πŸ“· Registrar", elem_id="register-tab"):
307
  with gr.Column(elem_classes="form-card"):
308
+ gr.HTML('<div class="reg-label"><i class="fa-solid fa-location-dot"></i> Passo 1: Localizacao</div>')
309
  gr.HTML(GPS_HTML)
310
+ gps_coords = gr.Textbox(value="", elem_id="gps-coords", interactive=True, visible=False, label="gps-coords")
311
  gr.HTML('<div class="reg-label"><i class="fa-solid fa-camera"></i> Passo 2: Foto do Animal</div>')
312
+ photo_input = gr.Image(label="", type="pil", sources=["upload", "webcam"], interactive=True, show_label=False, elem_id="photo-upload")
313
+ gr.HTML('<div class="reg-label"><i class="fa-solid fa-file-signature"></i> Passo 3: Observacoes (opcional)</div>')
314
  notes_input = gr.Textbox(label="", placeholder="Ex.: parece ferido, tem coleira...", lines=2, max_lines=4, show_label=False)
315
  submit_btn = gr.Button("Registrar avistamento", variant="primary", elem_id="submit-btn")
316
  status_html = gr.HTML("")
 
319
  refresh_btn = gr.Button("Atualizar lista", size="sm", variant="secondary")
320
  animals_display = gr.HTML(build_animals_html())
321
 
 
 
 
 
 
322
  submit_btn.click(process_sighting, inputs=[photo_input, gps_coords, notes_input], outputs=[map_html, status_html])
323
  btn_all.click(lambda: (build_map_html("all","all"),"all","all"), outputs=[map_html, species_st, timeframe_st])
324
  btn_dogs.click(lambda t: (build_map_html("dog",t),"dog",t), inputs=[timeframe_st], outputs=[map_html, species_st, timeframe_st])
 
328
  refresh_btn.click(build_animals_html, outputs=[animals_display])
329
  demo.load(build_map_html, outputs=[map_html])
330
 
 
 
 
 
 
 
 
 
331
 
332
  if __name__ == "__main__":
333
  import gradio.blocks as _gb_mod
334
  _orig_gai = _gb_mod.Blocks.get_api_info
335
  def _safe_get_api_info(self):
336
  try: return _orig_gai(self)
337
+ except Exception as _e: logging.warning(f"get_api_info: {_e}"); return {}
338
  _gb_mod.Blocks.get_api_info = _safe_get_api_info
339
  from core.database import DATA_DIR, PHOTOS_DIR
340
  PHOTOS_DIR.mkdir(parents=True, exist_ok=True)