Feature: Sprint31 — 9 Features merged (Streak, Ausgaben, KI-Tierarzt, Rückrufe, Adoption, Vet+Befunde, Hundepass, Playdate, Rassenerkennung)
- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
This commit is contained in:
parent
031c6028ac
commit
742ad189e8
26 changed files with 5734 additions and 27 deletions
|
|
@ -255,6 +255,15 @@ window.Page_wiki = (() => {
|
|||
<option value="">Alle Gruppen</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="padding:0 0 var(--space-3)">
|
||||
<button class="btn btn-secondary w-full" id="wiki-rasse-erkennen-btn"
|
||||
style="font-size:var(--text-sm)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#magnifying-glass"></use></svg>
|
||||
Welche Rasse ist das? — Foto analysieren
|
||||
</button>
|
||||
<input type="file" accept="image/jpeg,image/png,image/webp"
|
||||
id="wiki-rasse-foto-input" style="display:none">
|
||||
</div>
|
||||
<div class="wiki-breed-grid" id="wiki-breed-grid"></div>
|
||||
<div id="wiki-mehr-wrap" style="text-align:center;padding:var(--space-4) 0;display:none">
|
||||
<button class="btn btn-secondary" id="wiki-mehr-btn">Mehr laden</button>
|
||||
|
|
@ -264,6 +273,9 @@ window.Page_wiki = (() => {
|
|||
// Load initial batch (also populates gruppen)
|
||||
await _loadBreeds(el, true);
|
||||
|
||||
// Rassen-Erkennung per KI
|
||||
_bindWikiRasseErkennung(el);
|
||||
|
||||
// Search handler with debounce
|
||||
let _searchTimer;
|
||||
el.querySelector('#wiki-rassen-search').addEventListener('input', e => {
|
||||
|
|
@ -1265,6 +1277,130 @@ window.Page_wiki = (() => {
|
|||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// RASSEN-ERKENNUNG PER KI (Wiki-Tab)
|
||||
// ----------------------------------------------------------
|
||||
function _bindWikiRasseErkennung(el) {
|
||||
const btn = el.querySelector('#wiki-rasse-erkennen-btn');
|
||||
const fileInput = el.querySelector('#wiki-rasse-foto-input');
|
||||
if (!btn || !fileInput) return;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
if (!_appState.user) {
|
||||
UI.toast('Bitte melde dich an, um diese Funktion zu nutzen.', 'info');
|
||||
return;
|
||||
}
|
||||
fileInput.value = '';
|
||||
fileInput.click();
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', async () => {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
UI.toast('Bild zu groß (max. 5 MB).', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const origHtml = btn.innerHTML;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg> KI analysiert das Bild…`;
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const token = localStorage.getItem('by_token');
|
||||
const resp = await fetch('/api/ki/rasse-erkennung', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
|
||||
body: fd,
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (!resp.ok) throw new Error(data.detail || 'Fehler bei der Erkennung.');
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
_showWikiRasseErgebnis(data);
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = origHtml;
|
||||
UI.toast(e.message || 'Fehler bei der Rassen-Erkennung.', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _showWikiRasseErgebnis(data) {
|
||||
if (!data.ist_hund) {
|
||||
UI.modal.open({
|
||||
title: 'Kein Hund erkannt',
|
||||
body: `<div style="text-align:center;padding:var(--space-6) var(--space-2)">
|
||||
<div style="font-size:3rem;margin-bottom:var(--space-3)">🐾</div>
|
||||
<p style="color:var(--c-text-secondary)">
|
||||
Auf diesem Foto konnte kein Hund erkannt werden.<br>
|
||||
Bitte lade ein deutlicheres Foto hoch.
|
||||
</p>
|
||||
${data.hinweis ? `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-3)">${_esc(data.hinweis)}</p>` : ''}
|
||||
</div>`,
|
||||
footer: `<button class="btn btn-secondary" onclick="UI.modal.close()">Schließen</button>`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rassen = data.rassen || [];
|
||||
const cardsHtml = rassen.map((r, i) => {
|
||||
const isTop = i === 0;
|
||||
return `
|
||||
<div class="rasse-result-card${isTop ? ' rasse-result-card--top' : ''}">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||||
<div class="rasse-result-name">${isTop ? '🐕 ' : ''}${_esc(r.name)}</div>
|
||||
<span class="rasse-result-pct${isTop ? '' : ' rasse-result-pct--dim'}">${r.sicherheit}%</span>
|
||||
</div>
|
||||
<div class="rasse-result-bar-wrap">
|
||||
<div class="rasse-result-bar${isTop ? '' : ' rasse-result-bar--dim'}"
|
||||
style="width:${r.sicherheit}%"></div>
|
||||
</div>
|
||||
${r.beschreibung ? `<div class="rasse-result-desc">${_esc(r.beschreibung)}</div>` : ''}
|
||||
${r.wiki_slug ? `
|
||||
<div style="margin-top:var(--space-3)">
|
||||
<button class="btn btn-${isTop ? 'primary' : 'secondary'} btn-sm w-full"
|
||||
data-action="wiki" data-slug="${_esc(r.wiki_slug)}">
|
||||
Im Wiki nachschlagen
|
||||
</button>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
UI.modal.open({
|
||||
title: 'Erkannte Rasse',
|
||||
body: `
|
||||
<div style="padding-bottom:var(--space-2)">
|
||||
${data.hinweis ? `<div style="background:var(--c-surface-2);border-radius:var(--radius-md);
|
||||
padding:var(--space-3);margin-bottom:var(--space-3);font-size:var(--text-sm);
|
||||
color:var(--c-text-secondary)">ℹ️ ${_esc(data.hinweis)}</div>` : ''}
|
||||
${cardsHtml}
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-2);
|
||||
text-align:center">
|
||||
Noch ${data.verbleibende_anfragen} Erkennung${data.verbleibende_anfragen !== 1 ? 'en' : ''} heute verfügbar
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
footer: `<button class="btn btn-secondary" id="wiki-rasse-modal-schliessen">Schließen</button>`,
|
||||
});
|
||||
|
||||
document.getElementById('wiki-rasse-modal-schliessen')
|
||||
?.addEventListener('click', UI.modal.close);
|
||||
|
||||
document.querySelectorAll('[data-action="wiki"]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
UI.modal.close();
|
||||
setTimeout(() => _openBreedDetail(btn.dataset.slug), 300);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUBLIC
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue