Feature: Hundeernährungs-Feature — Kalorien-Rechner, Futter-Guide, Giftliste, KI-Berater (SW by-v698)
This commit is contained in:
parent
b1d9fb4f54
commit
6e4bf25581
7 changed files with 838 additions and 8 deletions
603
backend/static/js/pages/ernaehrung.js
Normal file
603
backend/static/js/pages/ernaehrung.js
Normal file
|
|
@ -0,0 +1,603 @@
|
|||
/* ============================================================
|
||||
BAN YARO — Ernährung
|
||||
Tabs: Kalorien-Rechner | Futter-Guide | Giftliste | KI-Berater
|
||||
============================================================ */
|
||||
|
||||
window.Page_ernaehrung = (() => {
|
||||
|
||||
let _container = null;
|
||||
let _appState = null;
|
||||
let _activeTab = 'rechner';
|
||||
let _profil = {};
|
||||
|
||||
const TABS = [
|
||||
{ key: 'rechner', label: 'Kalorien', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calculator"></use></svg>' },
|
||||
{ key: 'guide', label: 'Futter-Guide', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#book-open"></use></svg>' },
|
||||
{ key: 'gift', label: 'Giftliste', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#warning"></use></svg>' },
|
||||
{ key: 'ki', label: 'KI-Berater', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>' },
|
||||
];
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Escape helper
|
||||
// ------------------------------------------------------------------
|
||||
function _esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// LIFECYCLE
|
||||
// ------------------------------------------------------------------
|
||||
async function init(container, appState, params) {
|
||||
_container = container;
|
||||
_appState = appState;
|
||||
if (params?.tab && TABS.some(t => t.key === params.tab)) {
|
||||
_activeTab = params.tab;
|
||||
}
|
||||
await _render();
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await _render();
|
||||
}
|
||||
|
||||
async function onDogChange() {
|
||||
_profil = {};
|
||||
await _render();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// RENDER
|
||||
// ------------------------------------------------------------------
|
||||
async function _render() {
|
||||
if (!_appState.activeDog) {
|
||||
_container.innerHTML = UI.emptyState({
|
||||
icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#bowl-food"></use></svg>',
|
||||
title: 'Noch kein Hund angelegt',
|
||||
text: 'Erstelle zuerst ein Hundeprofil.',
|
||||
action: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Profil erstellen</button>`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Profil laden
|
||||
const dog = _appState.activeDog;
|
||||
try {
|
||||
_profil = await API.get(`/dogs/${dog.id}/ernaehrung`);
|
||||
} catch (_) {
|
||||
_profil = {};
|
||||
}
|
||||
|
||||
_container.innerHTML = `
|
||||
<div class="by-tabs" id="ern-tabs"></div>
|
||||
<div id="ern-tab-content"></div>
|
||||
`;
|
||||
|
||||
_renderTabBar();
|
||||
_renderTab();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB-BAR
|
||||
// ------------------------------------------------------------------
|
||||
function _renderTabBar() {
|
||||
const el = _container.querySelector('#ern-tabs');
|
||||
if (!el) return;
|
||||
el.innerHTML = TABS.map(t => `
|
||||
<button class="by-tab${t.key === _activeTab ? ' active' : ''}"
|
||||
data-tab="${t.key}">
|
||||
${t.icon} ${t.label}
|
||||
</button>`).join('');
|
||||
el.querySelectorAll('.by-tab').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
_activeTab = btn.dataset.tab;
|
||||
el.querySelectorAll('.by-tab').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
_renderTab();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _renderTab() {
|
||||
const el = _container.querySelector('#ern-tab-content');
|
||||
if (!el) return;
|
||||
switch (_activeTab) {
|
||||
case 'rechner': _renderRechner(el); break;
|
||||
case 'guide': _renderGuide(el); break;
|
||||
case 'gift': _renderGift(el); break;
|
||||
case 'ki': _renderKi(el); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 1: KALORIEN-RECHNER
|
||||
// ------------------------------------------------------------------
|
||||
function _renderRechner(el) {
|
||||
const dog = _appState.activeDog;
|
||||
|
||||
// Auto-Werte aus Hundeprofil
|
||||
const gewichtDefault = dog?.gewicht || '';
|
||||
const alterDefault = dog?.alter || '';
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4) 0">
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||||
Berechne den täglichen Kalorienbedarf deines Hundes.
|
||||
</p>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Gewicht (kg)</label>
|
||||
<input id="ern-gewicht" type="number" step="0.1" min="0.5" max="100"
|
||||
class="by-input" value="${_esc(gewichtDefault)}" placeholder="z. B. 15">
|
||||
</div>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Alter (Jahre)</label>
|
||||
<input id="ern-alter" type="number" step="0.5" min="0" max="25"
|
||||
class="by-input" value="${_esc(alterDefault)}" placeholder="z. B. 3">
|
||||
</div>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Aktivität</label>
|
||||
<select id="ern-aktivitaet" class="by-select">
|
||||
<option value="gering">Gering (Couch-Hund)</option>
|
||||
<option value="normal" selected>Normal</option>
|
||||
<option value="aktiv">Aktiv</option>
|
||||
<option value="sport">Sehr aktiv (Sport)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="by-form-group">
|
||||
<label class="by-label">Kastriert</label>
|
||||
<div style="display:flex;gap:var(--space-3)">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="ern-kastriert" value="ja"> Ja
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="radio" name="ern-kastriert" value="nein" checked> Nein
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" id="ern-rechner-btn" style="width:100%">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calculator"></use></svg>
|
||||
Berechnen
|
||||
</button>
|
||||
|
||||
<div id="ern-rechner-result" style="display:none;margin-top:var(--space-5)"></div>
|
||||
|
||||
<!-- Profil speichern -->
|
||||
<div id="ern-profil-speichern" style="display:none;margin-top:var(--space-4)">
|
||||
<h4 style="font-size:var(--text-sm);margin-bottom:var(--space-2)">Profil speichern</h4>
|
||||
<div style="display:grid;gap:var(--space-3)">
|
||||
<div class="by-form-group" style="margin:0">
|
||||
<label class="by-label">Futter-Typ</label>
|
||||
<select id="ern-prof-typ" class="by-select">
|
||||
<option value="">-- wählen --</option>
|
||||
<option value="trocken"${_profil.futter_typ === 'trocken' ? ' selected' : ''}>Trockenfutter</option>
|
||||
<option value="nass"${_profil.futter_typ === 'nass' ? ' selected' : ''}>Nassfutter</option>
|
||||
<option value="barf"${_profil.futter_typ === 'barf' ? ' selected' : ''}>BARF</option>
|
||||
<option value="mix"${_profil.futter_typ === 'mix' ? ' selected' : ''}>Mix</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="by-form-group" style="margin:0">
|
||||
<label class="by-label">Marke / Produkt</label>
|
||||
<input id="ern-prof-marke" type="text" class="by-input"
|
||||
value="${_esc(_profil.marke)}" placeholder="z. B. Royal Canin">
|
||||
</div>
|
||||
<div class="by-form-group" style="margin:0">
|
||||
<label class="by-label">Portionen pro Tag</label>
|
||||
<input id="ern-prof-portionen" type="number" min="1" max="6"
|
||||
class="by-input" value="${_profil.portionen || 2}">
|
||||
</div>
|
||||
<div class="by-form-group" style="margin:0">
|
||||
<label class="by-label">Notizen</label>
|
||||
<textarea id="ern-prof-notizen" class="by-input" rows="2"
|
||||
placeholder="Besonderheiten, Allergien...">${_esc(_profil.notizen)}</textarea>
|
||||
</div>
|
||||
<button class="btn btn-secondary" id="ern-prof-save-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#floppy-disk"></use></svg>
|
||||
Profil speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.querySelector('#ern-rechner-btn').addEventListener('click', () => _berechne(el));
|
||||
}
|
||||
|
||||
function _berechne(el) {
|
||||
const gewicht = parseFloat(el.querySelector('#ern-gewicht').value);
|
||||
const aktivitaet = el.querySelector('#ern-aktivitaet').value;
|
||||
const kastriert = el.querySelector('input[name="ern-kastriert"]:checked')?.value === 'ja';
|
||||
|
||||
if (!gewicht || gewicht < 0.5) {
|
||||
UI.toast.warning('Bitte ein gültiges Gewicht eingeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
const rer = 70 * Math.pow(gewicht, 0.75);
|
||||
const faktoren = {
|
||||
gering: { intakt: 1.2, kastriert: 1.0 },
|
||||
normal: { intakt: 1.6, kastriert: 1.4 },
|
||||
aktiv: { intakt: 1.8, kastriert: 1.6 },
|
||||
sport: { intakt: 2.1, kastriert: 1.9 },
|
||||
};
|
||||
const kcal = Math.round(rer * faktoren[aktivitaet][kastriert ? 'kastriert' : 'intakt']);
|
||||
|
||||
// Umrechnung in Futtermengen
|
||||
const trocken = Math.round(kcal / 3.5); // ~350 kcal/100g
|
||||
const nass = Math.round(kcal / 0.85); // ~85 kcal/100g
|
||||
const barf = Math.round(kcal / 1.5); // ~150 kcal/100g
|
||||
|
||||
const kcalFormatted = kcal.toLocaleString('de-DE');
|
||||
|
||||
const resultEl = el.querySelector('#ern-rechner-result');
|
||||
resultEl.style.display = '';
|
||||
resultEl.innerHTML = `
|
||||
<div style="text-align:center;padding:var(--space-4);
|
||||
background:var(--c-primary);color:#fff;
|
||||
border-radius:var(--radius-lg);margin-bottom:var(--space-4)">
|
||||
<div style="font-size:var(--text-2xl);font-weight:700">ca. ${kcalFormatted} kcal</div>
|
||||
<div style="font-size:var(--text-sm);opacity:0.85">pro Tag</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;gap:var(--space-3)">
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-md);
|
||||
padding:var(--space-3);border:1px solid var(--c-border)">
|
||||
<div style="font-weight:600;margin-bottom:4px">🌾 Trockenfutter</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
(~350 kcal/100g)
|
||||
</div>
|
||||
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
|
||||
${trocken} g / Tag
|
||||
</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
= ${Math.round(trocken/2)} g morgens + ${Math.round(trocken/2)} g abends
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-md);
|
||||
padding:var(--space-3);border:1px solid var(--c-border)">
|
||||
<div style="font-weight:600;margin-bottom:4px">🥫 Nassfutter</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
(~85 kcal/100g)
|
||||
</div>
|
||||
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
|
||||
${nass} g / Tag
|
||||
</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
= ${Math.round(nass/2)} g morgens + ${Math.round(nass/2)} g abends
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-md);
|
||||
padding:var(--space-3);border:1px solid var(--c-border)">
|
||||
<div style="font-weight:600;margin-bottom:4px">🥩 BARF</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
(~150 kcal/100g)
|
||||
</div>
|
||||
<div style="font-size:var(--text-lg);font-weight:600;margin-top:6px">
|
||||
${barf} g / Tag
|
||||
</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
= ${Math.round(barf/2)} g morgens + ${Math.round(barf/2)} g abends
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-3)">
|
||||
Richtwert nach Nationaler Forschungsratsformel (NRC). Immer den Körperzustand beobachten.
|
||||
</p>
|
||||
`;
|
||||
|
||||
// Profil-Speichern einblenden und kcal vorbelegen
|
||||
const profilSection = el.querySelector('#ern-profil-speichern');
|
||||
profilSection.style.display = '';
|
||||
|
||||
// kcal für Speichern merken
|
||||
profilSection.dataset.kcal = kcal;
|
||||
|
||||
el.querySelector('#ern-prof-save-btn').onclick = () => _speichereProfil(el, kcal);
|
||||
}
|
||||
|
||||
async function _speichereProfil(el, kcal) {
|
||||
const dog = _appState.activeDog;
|
||||
const futter_typ = el.querySelector('#ern-prof-typ').value || null;
|
||||
const marke = el.querySelector('#ern-prof-marke').value.trim() || null;
|
||||
const portionen = parseInt(el.querySelector('#ern-prof-portionen').value) || 2;
|
||||
const notizen = el.querySelector('#ern-prof-notizen').value.trim() || null;
|
||||
|
||||
const btn = el.querySelector('#ern-prof-save-btn');
|
||||
await UI.asyncButton(btn, async () => {
|
||||
try {
|
||||
_profil = await API.put(`/dogs/${dog.id}/ernaehrung`, {
|
||||
futter_typ, marke, kcal_tag: kcal, portionen, notizen,
|
||||
});
|
||||
UI.toast.success('Profil gespeichert.');
|
||||
} catch (err) {
|
||||
UI.toast.error(err.message || 'Fehler beim Speichern.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 2: FUTTER-GUIDE
|
||||
// ------------------------------------------------------------------
|
||||
function _renderGuide(el) {
|
||||
const cards = [
|
||||
{
|
||||
id: 'barf',
|
||||
emoji: '🥩',
|
||||
titel: 'BARF (Rohfütterung)',
|
||||
inhalt: `
|
||||
<p><strong>Zusammensetzung:</strong> 70 % Muskelfleisch, 10 % rohe Knochen, 10 % Organe, 10 % Gemüse & Obst</p>
|
||||
<p><strong>Vorteile:</strong> Naturnahste Ernährungsform, glänzendes Fell, weniger Kot, keine Zusatzstoffe</p>
|
||||
<p><strong>Risiken:</strong> Keimbelastung durch rohes Fleisch, Calcium-Phosphor-Balance muss stimmen, zeitaufwändig und teurer</p>
|
||||
<p><strong>Tipp:</strong> Niemals BARF und Trockenfutter in derselben Mahlzeit mischen — unterschiedliche Verdauungszeiten können zu Problemen führen.</p>
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'nass',
|
||||
emoji: '🥫',
|
||||
titel: 'Nassfutter',
|
||||
inhalt: `
|
||||
<p><strong>Zusammensetzung:</strong> 70–80 % Wasseranteil, meist höherer Fleischanteil als Trockenfutter</p>
|
||||
<p><strong>Vorteile:</strong> Hunde trinken automatisch mehr (gut für die Niere), schmackhafter, gut für wählerische Hunde</p>
|
||||
<p><strong>Worauf achten:</strong> Erste Zutat auf der Liste = Fleisch (nicht „Tierische Nebenerzeugnisse"), kein Zucker, kein Karamell</p>
|
||||
<p><strong>Zähne:</strong> Schlechter für die Zahngesundheit als Trockenfutter — öfter Zähne putzen oder Kauartikel geben.</p>
|
||||
`,
|
||||
},
|
||||
{
|
||||
id: 'trocken',
|
||||
emoji: '🌾',
|
||||
titel: 'Trockenfutter',
|
||||
inhalt: `
|
||||
<p><strong>Zusammensetzung:</strong> 6–10 % Wasser, ca. 350–400 kcal/100 g, konzentrierte Nährstoffe</p>
|
||||
<p><strong>Gute Zutaten:</strong> Benanntes Fleisch an erster Stelle (Huhn, Lachs), mind. 40 % Tierprotein, kein Getreide als Hauptzutat</p>
|
||||
<p><strong>Schlechte Zutaten:</strong> „Getreide" als erste Zutat, Zucker, Karamell, Konservierungsstoffe E320 / E321</p>
|
||||
<p><strong>Wichtig:</strong> Immer frisches Wasser bereitstellen — Trockenfutter enthält kaum Feuchtigkeit.</p>
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4) 0">
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-4)">
|
||||
Klicke auf eine Karte für Details.
|
||||
</p>
|
||||
${cards.map(c => `
|
||||
<div class="ern-guide-card" data-id="${c.id}"
|
||||
style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-lg);margin-bottom:var(--space-3);
|
||||
overflow:hidden;cursor:pointer">
|
||||
<div class="ern-guide-head"
|
||||
style="display:flex;align-items:center;justify-content:space-between;
|
||||
padding:var(--space-3) var(--space-4)">
|
||||
<span style="font-weight:600;font-size:var(--text-base)">
|
||||
${c.emoji} ${c.titel}
|
||||
</span>
|
||||
<svg class="ph-icon ern-guide-chevron" aria-hidden="true"
|
||||
style="transition:transform .2s">
|
||||
<use href="/icons/phosphor.svg#caret-down"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ern-guide-body"
|
||||
style="display:none;padding:0 var(--space-4) var(--space-3);
|
||||
font-size:var(--text-sm);color:var(--c-text-secondary);
|
||||
line-height:1.6">
|
||||
${c.inhalt}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
el.querySelectorAll('.ern-guide-card').forEach(card => {
|
||||
card.querySelector('.ern-guide-head').addEventListener('click', () => {
|
||||
const body = card.querySelector('.ern-guide-body');
|
||||
const chevron = card.querySelector('.ern-guide-chevron');
|
||||
const open = body.style.display !== 'none';
|
||||
body.style.display = open ? 'none' : '';
|
||||
chevron.style.transform = open ? '' : 'rotate(180deg)';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 3: GIFTLISTE
|
||||
// ------------------------------------------------------------------
|
||||
function _renderGift(el) {
|
||||
const items = [
|
||||
{ emoji: '🍫', name: 'Schokolade', grund: 'Theobromin → Herzrasen, Krämpfe, kann tödlich sein' },
|
||||
{ emoji: '🍇', name: 'Trauben & Rosinen', grund: 'Nierenversagen — auch kleinste Mengen gefährlich' },
|
||||
{ emoji: '🧅', name: 'Zwiebeln & Knoblauch', grund: 'Zerstören rote Blutkörperchen → Anämie' },
|
||||
{ emoji: '🥑', name: 'Avocado', grund: 'Persin → Erbrechen, Durchfall, Atemnot' },
|
||||
{ emoji: '🌰', name: 'Macadamia-Nüsse', grund: 'Lähmungserscheinungen, Zittern, Erbrechen' },
|
||||
{ emoji: '🍬', name: 'Xylitol (Süßstoff)', grund: 'Schwere Leberschäden, Unterzucker — oft in Kaugummi' },
|
||||
{ emoji: '🥛', name: 'Milch & Milchprodukte', grund: 'Laktose-Intoleranz bei vielen Hunden → Durchfall' },
|
||||
{ emoji: '🦴', name: 'Gekochte Knochen', grund: 'Splitter → innere Verletzungen, Darmverschluss' },
|
||||
{ emoji: '☕', name: 'Koffein (Kaffee, Tee)', grund: 'Herzrasen, Zittern, Nervensystem' },
|
||||
{ emoji: '🧂', name: 'Salz', grund: 'Natriumvergiftung → Erbrechen, Krämpfe' },
|
||||
];
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4) 0">
|
||||
<div style="background:#fff3cd;border:1px solid #ffc107;border-radius:var(--radius-md);
|
||||
padding:var(--space-3);margin-bottom:var(--space-4);
|
||||
font-size:var(--text-sm)">
|
||||
<strong>⚠️ Notfall-Tierarzt:</strong> Bei Verdacht auf Vergiftung sofort zum Tierarzt.
|
||||
Nicht abwarten, auch wenn noch keine Symptome sichtbar sind.
|
||||
</div>
|
||||
|
||||
<div style="display:grid;gap:var(--space-2)">
|
||||
${items.map(item => `
|
||||
<div style="background:#fff5f5;border:1px solid #fed7d7;
|
||||
border-radius:var(--radius-md);padding:var(--space-3)">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||||
<span style="font-size:1.4rem">${item.emoji}</span>
|
||||
<div>
|
||||
<div style="font-weight:600;font-size:var(--text-sm)">${_esc(item.name)}</div>
|
||||
<div style="font-size:var(--text-xs);color:#c53030">${_esc(item.grund)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:var(--space-4)">
|
||||
Diese Liste ist nicht vollständig. Im Zweifel gilt: lieber weglassen.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// TAB 4: KI-FUTTERBERATER
|
||||
// ------------------------------------------------------------------
|
||||
function _renderKi(el) {
|
||||
const dog = _appState.activeDog;
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="padding:var(--space-4) 0">
|
||||
<div style="background:var(--c-surface-2,var(--c-surface));border-radius:var(--radius-md);
|
||||
padding:var(--space-3);margin-bottom:var(--space-4);
|
||||
font-size:var(--text-sm);color:var(--c-text-secondary);
|
||||
border:1px solid var(--c-border)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#robot"></use></svg>
|
||||
Der KI-Futterberater beantwortet Ernährungsfragen für
|
||||
<strong>${_esc(dog?.name || 'deinen Hund')}</strong>.
|
||||
Bei Gesundheitsfragen immer den Tierarzt zurate ziehen.
|
||||
</div>
|
||||
|
||||
<!-- Vorschläge -->
|
||||
<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||||
${[
|
||||
'Welches Futter empfiehlst du für meine Rasse?',
|
||||
'Wie oft soll ich meinen Hund füttern?',
|
||||
'Ist Getreide im Futter schlecht?',
|
||||
'Welche Leckerlis sind gesund?',
|
||||
].map(q => `
|
||||
<button class="btn btn-sm btn-secondary ern-ki-vorschlag"
|
||||
data-q="${_esc(q)}"
|
||||
style="font-size:var(--text-xs)">${_esc(q)}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<!-- Chat-Verlauf -->
|
||||
<div id="ern-ki-chat" style="min-height:80px;margin-bottom:var(--space-3)"></div>
|
||||
|
||||
<!-- Eingabe -->
|
||||
<div style="display:flex;gap:var(--space-2)">
|
||||
<textarea id="ern-ki-frage" class="by-input" rows="2"
|
||||
placeholder="Deine Frage zur Ernährung..."
|
||||
style="flex:1;resize:vertical"></textarea>
|
||||
<button class="btn btn-primary" id="ern-ki-send-btn"
|
||||
style="align-self:flex-end">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Vorschläge
|
||||
el.querySelectorAll('.ern-ki-vorschlag').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
el.querySelector('#ern-ki-frage').value = btn.dataset.q;
|
||||
el.querySelector('#ern-ki-frage').focus();
|
||||
});
|
||||
});
|
||||
|
||||
// Senden
|
||||
el.querySelector('#ern-ki-send-btn').addEventListener('click', () => _kiSenden(el));
|
||||
el.querySelector('#ern-ki-frage').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) _kiSenden(el);
|
||||
});
|
||||
}
|
||||
|
||||
async function _kiSenden(el) {
|
||||
const dog = _appState.activeDog;
|
||||
const frageEl = el.querySelector('#ern-ki-frage');
|
||||
const frage = frageEl.value.trim();
|
||||
if (!frage) {
|
||||
UI.toast.warning('Bitte eine Frage eingeben.');
|
||||
return;
|
||||
}
|
||||
|
||||
const chatEl = el.querySelector('#ern-ki-chat');
|
||||
const sendBtn = el.querySelector('#ern-ki-send-btn');
|
||||
|
||||
// Userfrage anzeigen
|
||||
chatEl.insertAdjacentHTML('beforeend', `
|
||||
<div style="display:flex;justify-content:flex-end;margin-bottom:var(--space-2)">
|
||||
<div style="background:var(--c-primary);color:#fff;border-radius:var(--radius-md);
|
||||
padding:var(--space-2) var(--space-3);max-width:80%;font-size:var(--text-sm)">
|
||||
${_esc(frage)}
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
frageEl.value = '';
|
||||
|
||||
// KI-Antwort Placeholder
|
||||
const placeholderId = `ern-ki-placeholder-${Date.now()}`;
|
||||
chatEl.insertAdjacentHTML('beforeend', `
|
||||
<div id="${placeholderId}" style="margin-bottom:var(--space-3)">
|
||||
<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
|
||||
font-size:var(--text-sm);color:var(--c-text-muted)">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#circle-notch"></use></svg>
|
||||
Denke nach…
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
chatEl.scrollTop = chatEl.scrollHeight;
|
||||
|
||||
await UI.asyncButton(sendBtn, async () => {
|
||||
let antwort = '';
|
||||
try {
|
||||
const result = await API.post(`/dogs/${dog.id}/ernaehrung/ki-beratung`, {
|
||||
frage,
|
||||
dog_name: dog?.name || null,
|
||||
rasse: dog?.rasse || null,
|
||||
alter: dog?.alter != null ? String(dog.alter) : null,
|
||||
gewicht: dog?.gewicht || null,
|
||||
aktiv: false,
|
||||
});
|
||||
antwort = result.antwort || 'Keine Antwort erhalten.';
|
||||
} catch (err) {
|
||||
if (err.status === 503) {
|
||||
antwort = 'Die KI ist momentan nicht verfügbar. Bitte später versuchen.';
|
||||
} else {
|
||||
antwort = 'Fehler bei der KI-Anfrage. Bitte später erneut versuchen.';
|
||||
}
|
||||
}
|
||||
|
||||
const antwortHtml = _esc(antwort)
|
||||
.replace(/\n\n/g, '</p><p style="margin:var(--space-1) 0">')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
const placeholder = document.getElementById(placeholderId);
|
||||
if (placeholder) {
|
||||
placeholder.innerHTML = `
|
||||
<div style="background:var(--c-surface);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);
|
||||
font-size:var(--text-sm);line-height:1.6;max-width:90%">
|
||||
<p style="margin:0">${antwortHtml}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
chatEl.scrollTop = chatEl.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// PUBLIC API
|
||||
// ------------------------------------------------------------------
|
||||
return { init, refresh, onDogChange };
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue