Session 2026-04-21: SEO, Wiki-Anreicherung, Training, Lober
SEO & Crawler:
- robots.txt, llms.txt, sitemap.xml (508 Seiten bei Google)
- SSR-Seiten: /info, /wiki/rassen, /wiki/rasse/{slug}, /knigge
- Open Graph, JSON-LD, Breadcrumbs in index.html
Navigation:
- Training unter "Mein Hund", Wissen collapsible
- Welcome-Seite und Landing-Page auf 5-Gruppen-Struktur
Wiki:
- KI-Anreicherung (Claude API): beschreibung, vorkommen_de, Steckbrief
- "So einen hab ich" / Züchter-Verzeichnis
- Scheduler: 50 Rassen beim Start, 20/Nacht
Training:
- Session-Logging (Erfolgsquote, Stimmung, Zufriedenheit)
- Virtueller KI-Trainer (6h-Cache)
- Trainingskalender (Habit-Tracker)
- Top-Training → automatischer Tagebucheintrag
- Gamification ohne Druck: Badges, Streak, Stats
Fortschritts-Lober:
- Jeden Montag 09:00: Claude schreibt Lob-Text pro Hund
- Push + Karte im Tagebuch
Monitoring:
- 4× täglich Status-Mail mit Scheduler-Status + Wiki-Fortschritt
This commit is contained in:
parent
65d1cf6c7f
commit
180de32e57
22 changed files with 4351 additions and 189 deletions
|
|
@ -9,6 +9,34 @@ window.Page_uebungen = (() => {
|
|||
let _appState = null;
|
||||
let _activeTab = 'grundkommandos';
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// API HELPERS
|
||||
// ----------------------------------------------------------
|
||||
function _dogId() {
|
||||
return window.App?.state?.activeDogId || null;
|
||||
}
|
||||
|
||||
async function _apiPost(url, body) {
|
||||
const r = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
async function _apiGet(url) {
|
||||
const r = await fetch(url, {credentials: 'include'});
|
||||
return r.ok ? r.json() : null;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STATS STATE
|
||||
// ----------------------------------------------------------
|
||||
let _statsData = null; // cached stats from /api/training/stats
|
||||
let _badgesData = null; // cached badges from /api/achievements
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// DATEN
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -387,10 +415,29 @@ window.Page_uebungen = (() => {
|
|||
API.training.getSuggestions().then(suggestions => {
|
||||
if (suggestions.length) _showSuggestions(suggestions);
|
||||
}).catch(() => {});
|
||||
|
||||
// Stats + Badges laden
|
||||
_loadStatsAndBadges();
|
||||
}
|
||||
|
||||
async function _loadStatsAndBadges() {
|
||||
const dogId = _dogId();
|
||||
if (!dogId) return;
|
||||
const [stats, achievements] = await Promise.all([
|
||||
_apiGet(`/api/training/stats?dog_id=${dogId}`),
|
||||
_apiGet('/api/achievements'),
|
||||
]);
|
||||
_statsData = stats;
|
||||
_badgesData = achievements;
|
||||
_renderStatsBanner();
|
||||
}
|
||||
|
||||
function refresh() {}
|
||||
function onDogChange() {}
|
||||
function onDogChange() {
|
||||
_statsData = null;
|
||||
_badgesData = null;
|
||||
_loadStatsAndBadges();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// HAUPT-RENDER
|
||||
|
|
@ -399,12 +446,64 @@ window.Page_uebungen = (() => {
|
|||
_container.innerHTML = `
|
||||
<div id="ueb-wrap">
|
||||
${_renderTabs()}
|
||||
<div id="ueb-stats-banner" style="padding:0 var(--space-4);margin-bottom:var(--space-2)"></div>
|
||||
<div id="ueb-suggestions" style="padding:0 var(--space-4);display:flex;flex-direction:column;gap:var(--space-2);margin-bottom:var(--space-2)"></div>
|
||||
<div id="ueb-content"></div>
|
||||
</div>
|
||||
`;
|
||||
_bindTabs();
|
||||
_renderContent();
|
||||
_renderStatsBanner();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// STATS BANNER
|
||||
// ----------------------------------------------------------
|
||||
function _renderStatsBanner() {
|
||||
const el = _container && _container.querySelector('#ueb-stats-banner');
|
||||
if (!el) return;
|
||||
if (!_statsData || !_statsData.total_sessions) { el.innerHTML = ''; return; }
|
||||
|
||||
const s = _statsData;
|
||||
const streakHtml = (s.streak_days >= 2)
|
||||
? ` · ${s.streak_days}-Tage-Streak 🔥`
|
||||
: '';
|
||||
const avgHtml = s.avg_erfolgsquote != null
|
||||
? ` · Ø ${Math.round(s.avg_erfolgsquote)}% Erfolg`
|
||||
: '';
|
||||
|
||||
// Training-Badges filtern
|
||||
let badgesHtml = '';
|
||||
if (_badgesData && Array.isArray(_badgesData.user_badges)) {
|
||||
const trainingBadges = _badgesData.user_badges.filter(b => b.badge_id && b.badge_id.startsWith('training_'));
|
||||
if (trainingBadges.length) {
|
||||
const visible = trainingBadges.slice(0, 3);
|
||||
const rest = trainingBadges.length - visible.length;
|
||||
const pills = visible.map(b => `
|
||||
<span style="display:inline-flex;align-items:center;gap:3px;padding:2px 8px;
|
||||
border-radius:var(--radius-full,999px);
|
||||
background:var(--c-primary-subtle);color:var(--c-primary);
|
||||
font-size:var(--text-xs);font-weight:var(--weight-semibold)">
|
||||
${b.icon || '🏅'} ${_esc(b.name || b.badge_id)}
|
||||
</span>
|
||||
`).join('');
|
||||
const more = rest > 0
|
||||
? `<span style="font-size:var(--text-xs);color:var(--c-text-secondary)">+${rest} weitere</span>`
|
||||
: '';
|
||||
badgesHtml = `<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-2)">${pills}${more}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div style="background:var(--c-surface-2);border:1px solid var(--c-border);
|
||||
border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);
|
||||
font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
<span style="font-weight:var(--weight-semibold);color:var(--c-text)">
|
||||
${s.total_sessions} Einheit${s.total_sessions !== 1 ? 'en' : ''}
|
||||
</span>${avgHtml}${streakHtml}
|
||||
${badgesHtml}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function _renderTabs() {
|
||||
|
|
@ -492,7 +591,8 @@ window.Page_uebungen = (() => {
|
|||
}
|
||||
_bindAccordions();
|
||||
_bindStatusButtons();
|
||||
if (_activeTab === 'ki-trainer') _bindKiTrainer();
|
||||
_bindLogButtons();
|
||||
if (_activeTab === 'ki-trainer') _loadKiTrainerFeedback();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -523,6 +623,21 @@ window.Page_uebungen = (() => {
|
|||
${_esc(u.name)}
|
||||
</span>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
||||
<!-- Log-Button -->
|
||||
<button class="ueb-log-btn"
|
||||
data-tab="${_esc(_activeTab)}"
|
||||
data-name="${_esc(u.name)}"
|
||||
title="Trainingseinheit loggen"
|
||||
style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
|
||||
color:var(--c-primary);cursor:pointer;padding:2px 7px;
|
||||
display:flex;align-items:center;gap:3px;
|
||||
font-size:var(--text-xs);font-weight:var(--weight-semibold);
|
||||
border-radius:var(--radius-sm)">
|
||||
<svg class="ph-icon" style="width:13px;height:13px;flex-shrink:0" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#plus"></use>
|
||||
</svg>
|
||||
Einheit
|
||||
</button>
|
||||
<!-- Status-Button -->
|
||||
<button class="ueb-status-btn"
|
||||
data-tab="${_esc(_activeTab)}"
|
||||
|
|
@ -652,143 +767,432 @@ window.Page_uebungen = (() => {
|
|||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-TRAINER
|
||||
// LOG SESSION MODAL
|
||||
// ----------------------------------------------------------
|
||||
function _bindLogButtons() {
|
||||
_container.querySelectorAll('.ueb-log-btn').forEach(btn => {
|
||||
btn.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
_openLogModal(btn.dataset.tab, btn.dataset.name);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _openLogModal(tab, exerciseName) {
|
||||
// Build the modal HTML
|
||||
const modalId = 'ueb-log-modal';
|
||||
const formId = 'ueb-log-form';
|
||||
// Remove existing if present
|
||||
document.getElementById(modalId)?.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = modalId;
|
||||
overlay.style.cssText = `
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
display:flex;align-items:flex-end;justify-content:center;
|
||||
background:rgba(0,0,0,0.45);
|
||||
`;
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div style="background:var(--c-surface);border-radius:var(--radius-lg) var(--radius-lg) 0 0;
|
||||
width:100%;max-width:480px;max-height:92vh;overflow-y:auto;
|
||||
padding:var(--space-5) var(--space-4) var(--space-6);box-shadow:0 -4px 24px rgba(0,0,0,0.15)">
|
||||
<!-- Handle -->
|
||||
<div style="width:40px;height:4px;background:var(--c-border);border-radius:2px;margin:0 auto var(--space-4)"></div>
|
||||
|
||||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);
|
||||
margin:0 0 var(--space-4);text-align:center">
|
||||
Einheit loggen: ${_esc(exerciseName)}
|
||||
</h3>
|
||||
|
||||
<form id="${formId}" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||||
|
||||
<!-- Wiederholungen -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Wiederholungen</label>
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3);justify-content:center">
|
||||
<button type="button" id="ueb-rep-minus"
|
||||
style="width:36px;height:36px;border-radius:50%;border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);font-size:1.2rem;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;color:var(--c-text)">−</button>
|
||||
<span id="ueb-rep-val" style="font-size:var(--text-xl);font-weight:700;color:var(--c-text);min-width:32px;text-align:center">5</span>
|
||||
<button type="button" id="ueb-rep-plus"
|
||||
style="width:36px;height:36px;border-radius:50%;border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);font-size:1.2rem;cursor:pointer;
|
||||
display:flex;align-items:center;justify-content:center;color:var(--c-text)">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Wie lief's -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Wie lief's?</label>
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:center;flex-wrap:wrap">
|
||||
${[['😓','0'],['😐','25'],['🙂','50'],['😊','75'],['🎉','100']].map(([emoji, val]) => `
|
||||
<button type="button" class="ueb-erfolg-btn"
|
||||
data-val="${val}"
|
||||
style="font-size:1.5rem;padding:var(--space-2) var(--space-3);
|
||||
border-radius:var(--radius-md);border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);cursor:pointer;transition:all 0.15s"
|
||||
title="${val}%">${emoji}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stimmung des Hundes -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Stimmung des Hundes</label>
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:center;flex-wrap:wrap">
|
||||
${[['🎯','aufmerksam'],['😴','müde'],['🌪️','abgelenkt'],['⚡','super']].map(([emoji, val]) => `
|
||||
<button type="button" class="ueb-stimmung-btn"
|
||||
data-val="${val}"
|
||||
style="display:flex;flex-direction:column;align-items:center;gap:2px;
|
||||
font-size:1.2rem;padding:var(--space-2);min-width:60px;
|
||||
border-radius:var(--radius-md);border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);cursor:pointer;transition:all 0.15s">
|
||||
${emoji}
|
||||
<span style="font-size:9px;color:var(--c-text-secondary)">${_esc(val)}</span>
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zufriedenheit -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Wie zufrieden bist du?</label>
|
||||
<div style="display:flex;gap:var(--space-2);justify-content:center">
|
||||
${[1,2,3,4,5].map(n => `
|
||||
<button type="button" class="ueb-stern-btn"
|
||||
data-val="${n}"
|
||||
style="font-size:1.5rem;background:none;border:none;cursor:pointer;
|
||||
padding:2px;opacity:0.35;transition:opacity 0.15s">⭐</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notiz -->
|
||||
<div>
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">Notiz</label>
|
||||
<textarea id="ueb-log-notiz" rows="2" placeholder="Optional: Was ist aufgefallen?"
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-2) var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;resize:none;
|
||||
background:var(--c-surface);color:var(--c-text);line-height:1.5"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Meilenstein-Checkbox (initially hidden) -->
|
||||
<label id="ueb-log-milestone-wrap" hidden
|
||||
style="display:flex;align-items:flex-start;gap:var(--space-2);cursor:pointer;
|
||||
padding:var(--space-3);background:var(--c-primary-subtle);
|
||||
border:1px solid var(--c-primary-light);border-radius:var(--radius-md)">
|
||||
<input type="checkbox" id="ueb-log-milestone"
|
||||
style="margin-top:2px;flex-shrink:0;accent-color:var(--c-primary);width:16px;height:16px">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text);line-height:1.5">
|
||||
📖 Als Meilenstein ins Tagebuch eintragen
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- Footer Buttons -->
|
||||
<div style="display:flex;gap:var(--space-3);margin-top:var(--space-5)">
|
||||
<button id="ueb-log-cancel"
|
||||
style="flex:1;padding:var(--space-3);border:1.5px solid var(--c-border);
|
||||
background:var(--c-surface-2);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);cursor:pointer;color:var(--c-text)">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button id="ueb-log-save" form="${formId}"
|
||||
class="btn btn-primary" style="flex:2"
|
||||
type="button">
|
||||
Einheit speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// State
|
||||
let wiederholungen = 5;
|
||||
let erfolgsquote = null; // must be selected
|
||||
let stimmung = null;
|
||||
let zufriedenheit = null;
|
||||
|
||||
// Close helpers
|
||||
function _closeModal() { overlay.remove(); }
|
||||
overlay.addEventListener('click', e => { if (e.target === overlay) _closeModal(); });
|
||||
overlay.querySelector('#ueb-log-cancel').addEventListener('click', _closeModal);
|
||||
|
||||
// Stepper
|
||||
const repVal = overlay.querySelector('#ueb-rep-val');
|
||||
overlay.querySelector('#ueb-rep-minus').addEventListener('click', () => {
|
||||
if (wiederholungen > 1) { wiederholungen--; repVal.textContent = wiederholungen; }
|
||||
});
|
||||
overlay.querySelector('#ueb-rep-plus').addEventListener('click', () => {
|
||||
wiederholungen++;
|
||||
repVal.textContent = wiederholungen;
|
||||
});
|
||||
|
||||
// Erfolg-Buttons
|
||||
overlay.querySelectorAll('.ueb-erfolg-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
erfolgsquote = parseInt(btn.dataset.val, 10);
|
||||
overlay.querySelectorAll('.ueb-erfolg-btn').forEach(b => {
|
||||
b.style.background = 'var(--c-surface-2)';
|
||||
b.style.borderColor = 'var(--c-border)';
|
||||
b.style.transform = '';
|
||||
});
|
||||
btn.style.background = 'var(--c-primary-subtle)';
|
||||
btn.style.borderColor = 'var(--c-primary)';
|
||||
btn.style.transform = 'scale(1.15)';
|
||||
_checkMilestoneVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
// Stimmung-Buttons
|
||||
overlay.querySelectorAll('.ueb-stimmung-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
stimmung = btn.dataset.val;
|
||||
overlay.querySelectorAll('.ueb-stimmung-btn').forEach(b => {
|
||||
b.style.background = 'var(--c-surface-2)';
|
||||
b.style.borderColor = 'var(--c-border)';
|
||||
});
|
||||
btn.style.background = 'var(--c-primary-subtle)';
|
||||
btn.style.borderColor = 'var(--c-primary)';
|
||||
});
|
||||
});
|
||||
|
||||
// Stern-Buttons
|
||||
overlay.querySelectorAll('.ueb-stern-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
zufriedenheit = parseInt(btn.dataset.val, 10);
|
||||
overlay.querySelectorAll('.ueb-stern-btn').forEach(b => {
|
||||
b.style.opacity = parseInt(b.dataset.val, 10) <= zufriedenheit ? '1' : '0.35';
|
||||
});
|
||||
_checkMilestoneVisibility();
|
||||
});
|
||||
});
|
||||
|
||||
function _checkMilestoneVisibility() {
|
||||
const wrap = overlay.querySelector('#ueb-log-milestone-wrap');
|
||||
if (!wrap) return;
|
||||
const show = erfolgsquote != null && erfolgsquote >= 75 && zufriedenheit != null && zufriedenheit >= 4;
|
||||
wrap.hidden = !show;
|
||||
}
|
||||
|
||||
// Save
|
||||
overlay.querySelector('#ueb-log-save').addEventListener('click', async () => {
|
||||
const dogId = _dogId();
|
||||
if (!dogId) { UI.toast('Kein Hund ausgewählt.', 'warning'); return; }
|
||||
if (erfolgsquote === null) { UI.toast('Bitte wähle aus, wie es gelaufen ist.', 'warning'); return; }
|
||||
|
||||
const saveBtn = overlay.querySelector('#ueb-log-save');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Speichern…';
|
||||
|
||||
const exerciseId = `${tab}_${exerciseName.replace(/[\s/]+/g, '_')}`;
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const tagebuch = !overlay.querySelector('#ueb-log-milestone-wrap').hidden &&
|
||||
overlay.querySelector('#ueb-log-milestone').checked;
|
||||
|
||||
const body = {
|
||||
dog_id: dogId,
|
||||
exercise_id: exerciseId,
|
||||
exercise_name: exerciseName,
|
||||
datum: today,
|
||||
wiederholungen: wiederholungen,
|
||||
erfolgsquote: erfolgsquote,
|
||||
hund_stimmung: stimmung || null,
|
||||
zufriedenheit: zufriedenheit || null,
|
||||
notiz: overlay.querySelector('#ueb-log-notiz').value.trim() || null,
|
||||
tagebuch_eintrag: tagebuch,
|
||||
};
|
||||
|
||||
try {
|
||||
const resp = await _apiPost('/api/training/sessions', body);
|
||||
_closeModal();
|
||||
if (!resp) { UI.toast.error('Speichern fehlgeschlagen.'); return; }
|
||||
|
||||
if (resp.ist_top) {
|
||||
UI.toast.success('🎉 Top-Training! Das war eine eurer besten Einheiten.');
|
||||
} else {
|
||||
UI.toast.success('Einheit gespeichert!');
|
||||
}
|
||||
|
||||
if (resp.badges && resp.badges.length) {
|
||||
resp.badges.forEach((badge, idx) => {
|
||||
setTimeout(() => {
|
||||
UI.toast.success(`🏅 Neues Abzeichen: "${badge.name || badge}"!`);
|
||||
}, 1000 * (idx + 1));
|
||||
});
|
||||
}
|
||||
|
||||
if (resp.diary_entry_id) {
|
||||
setTimeout(() => {
|
||||
UI.toast.success('📖 Als Meilenstein im Tagebuch gespeichert.');
|
||||
}, resp.badges?.length ? (resp.badges.length + 1) * 1000 : 1000);
|
||||
}
|
||||
|
||||
// Stats-Banner aktualisieren
|
||||
_statsData = null;
|
||||
_loadStatsAndBadges();
|
||||
} catch (err) {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Einheit speichern';
|
||||
UI.toast.error('Speichern fehlgeschlagen.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// KI-TRAINER (neu: hundebasiertes Feedback)
|
||||
// ----------------------------------------------------------
|
||||
function _renderKiTrainer() {
|
||||
return `
|
||||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)">
|
||||
const dogId = _dogId();
|
||||
if (!dogId) {
|
||||
return `
|
||||
<div style="padding:var(--space-6) var(--space-4);text-align:center;color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" style="width:40px;height:40px;margin-bottom:var(--space-3);color:var(--c-border)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#robot"></use>
|
||||
</svg>
|
||||
<p style="font-size:var(--text-sm);margin:0">Wähle einen Hund aus um den KI-Trainer zu nutzen.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
<!-- Intro -->
|
||||
<div class="card" style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light)">
|
||||
<div style="display:flex;gap:var(--space-3);align-items:flex-start">
|
||||
<svg class="ph-icon" style="width:24px;height:24px;flex-shrink:0;color:var(--c-primary);margin-top:2px" aria-hidden="true">
|
||||
return `
|
||||
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-4)" id="ki-trainer-panel">
|
||||
|
||||
<!-- Header -->
|
||||
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
||||
<div style="width:48px;height:48px;border-radius:50%;background:var(--c-primary-subtle);
|
||||
display:flex;align-items:center;justify-content:center;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:26px;height:26px;color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#robot"></use>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text);margin:0 0 var(--space-1)">
|
||||
KI-Hundetrainer
|
||||
</h3>
|
||||
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0;line-height:1.5">
|
||||
Beschreibe ein konkretes Problem oder Verhalten deines Hundes —
|
||||
du bekommst individuelle Trainingstipps.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size:var(--text-base);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||||
KI-Trainer
|
||||
</div>
|
||||
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
||||
Personalisiertes Feedback basierend auf deinen Trainingseinheiten
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Eingabe -->
|
||||
<div class="card" style="padding:var(--space-4)">
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">
|
||||
Rasse & Alter (optional)
|
||||
</label>
|
||||
<div style="display:flex;gap:var(--space-2);margin-bottom:var(--space-4)">
|
||||
<input id="ki-rasse" type="text" placeholder="z.B. Labrador"
|
||||
style="flex:1;padding:var(--space-2) var(--space-3);border:1.5px solid var(--c-border);
|
||||
border-radius:var(--radius-md);font-size:var(--text-sm);
|
||||
background:var(--c-surface);color:var(--c-text);font-family:inherit">
|
||||
<input id="ki-alter" type="text" placeholder="z.B. 2 Jahre"
|
||||
style="flex:1;padding:var(--space-2) var(--space-3);border:1.5px solid var(--c-border);
|
||||
border-radius:var(--radius-md);font-size:var(--text-sm);
|
||||
background:var(--c-surface);color:var(--c-text);font-family:inherit">
|
||||
</div>
|
||||
<!-- Lade-Spinner -->
|
||||
<div id="ki-loading" style="text-align:center;padding:var(--space-6);color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" style="width:32px;height:32px;animation:spin 1s linear infinite;margin-bottom:var(--space-2)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#spinner-gap"></use>
|
||||
</svg>
|
||||
<p style="font-size:var(--text-sm);margin:0">Lade KI-Feedback…</p>
|
||||
</div>
|
||||
|
||||
<label style="display:block;font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-text);margin-bottom:var(--space-2)">
|
||||
Problem beschreiben *
|
||||
</label>
|
||||
<textarea id="ki-problem" rows="4"
|
||||
placeholder="z.B. Mein Hund bellt bei jedem Klingeln an der Tür und lässt sich kaum beruhigen. Er springt Besucher an und ist sehr aufgedreht..."
|
||||
style="width:100%;box-sizing:border-box;padding:var(--space-3);
|
||||
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||
font-size:var(--text-sm);font-family:inherit;resize:vertical;
|
||||
background:var(--c-surface);color:var(--c-text);line-height:1.5;
|
||||
min-height:100px"></textarea>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:var(--space-3)">
|
||||
<span id="ki-char-count" style="font-size:var(--text-xs);color:var(--c-text-muted)">0 / 1000</span>
|
||||
<button id="ki-submit" class="btn btn-primary">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
|
||||
Tipps holen
|
||||
<!-- Kein Sessions-Hinweis -->
|
||||
<div id="ki-no-sessions" hidden
|
||||
style="text-align:center;padding:var(--space-6) var(--space-4);color:var(--c-text-secondary)">
|
||||
<svg class="ph-icon" style="width:40px;height:40px;margin-bottom:var(--space-3);color:var(--c-border)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#clipboard-text"></use>
|
||||
</svg>
|
||||
<p style="font-size:var(--text-sm);margin:0">
|
||||
Logge deine erste Trainingseinheit um KI-Feedback zu erhalten.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feedback-Card -->
|
||||
<div id="ki-feedback-card" hidden>
|
||||
<div class="card" style="border-left:3px solid var(--c-primary);background:var(--c-surface)">
|
||||
<div id="ki-feedback-text"
|
||||
style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7;white-space:pre-wrap"></div>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-top:var(--space-3)">
|
||||
<span id="ki-feedback-meta" style="font-size:var(--text-xs);color:var(--c-text-muted)"></span>
|
||||
<button id="ki-regenerate"
|
||||
style="font-size:var(--text-xs);padding:var(--space-1) var(--space-3);
|
||||
border-radius:var(--radius-md);border:1px solid var(--c-border);
|
||||
background:var(--c-surface-2);cursor:pointer;color:var(--c-text-secondary)">
|
||||
Neu generieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Antwort -->
|
||||
<div id="ki-result" hidden></div>
|
||||
|
||||
<!-- Hinweis -->
|
||||
<p style="font-size:var(--text-xs);color:var(--c-text-muted);text-align:center;margin:0">
|
||||
KI-Tipps ersetzen keinen professionellen Hundetrainer. Bei Aggression oder starker Angst
|
||||
wende dich an einen zertifizierten Trainer vor Ort.
|
||||
KI-Tipps ersetzen keinen professionellen Hundetrainer.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
|
||||
function _bindKiTrainer() {
|
||||
const textarea = _container.querySelector('#ki-problem');
|
||||
const charCount = _container.querySelector('#ki-char-count');
|
||||
const submitBtn = _container.querySelector('#ki-submit');
|
||||
const result = _container.querySelector('#ki-result');
|
||||
if (!textarea || !submitBtn) return;
|
||||
async function _loadKiTrainerFeedback(forceRefresh) {
|
||||
const loading = _container.querySelector('#ki-loading');
|
||||
const noSessions = _container.querySelector('#ki-no-sessions');
|
||||
const feedbackCard = _container.querySelector('#ki-feedback-card');
|
||||
const feedbackText = _container.querySelector('#ki-feedback-text');
|
||||
const feedbackMeta = _container.querySelector('#ki-feedback-meta');
|
||||
const regenBtn = _container.querySelector('#ki-regenerate');
|
||||
if (!loading) return; // not on ki-trainer tab
|
||||
|
||||
textarea.addEventListener('input', () => {
|
||||
charCount.textContent = `${textarea.value.length} / 1000`;
|
||||
});
|
||||
const dogId = _dogId();
|
||||
if (!dogId) return;
|
||||
|
||||
submitBtn.addEventListener('click', async () => {
|
||||
const problem = textarea.value.trim();
|
||||
if (problem.length < 10) {
|
||||
UI.toast('Bitte beschreibe das Problem etwas genauer.', 'warning');
|
||||
// Show loading
|
||||
loading.hidden = false;
|
||||
noSessions.hidden = true;
|
||||
feedbackCard.hidden = true;
|
||||
|
||||
// Check if there are any sessions
|
||||
const stats = _statsData || await _apiGet(`/api/training/stats?dog_id=${dogId}`);
|
||||
if (!stats || !stats.total_sessions) {
|
||||
loading.hidden = true;
|
||||
noSessions.hidden = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await _apiPost('/api/training/ki-feedback', { dog_id: dogId });
|
||||
loading.hidden = true;
|
||||
if (!resp || !resp.feedback) {
|
||||
noSessions.hidden = false;
|
||||
return;
|
||||
}
|
||||
const rasse = _container.querySelector('#ki-rasse')?.value.trim() || null;
|
||||
const alter = _container.querySelector('#ki-alter')?.value.trim() || null;
|
||||
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg> Denke nach…`;
|
||||
|
||||
result.hidden = true;
|
||||
result.innerHTML = '';
|
||||
|
||||
try {
|
||||
const resp = await API.post('/ki/training', { problem, rasse, alter });
|
||||
const text = resp.antwort || '';
|
||||
|
||||
// Render with simple markdown-like formatting (text already escaped by API)
|
||||
const safeText = text
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
const html = safeText
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/^(\d+)\. (.+)$/gm, '<li><strong>$1.</strong> $2</li>')
|
||||
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
result.innerHTML = `
|
||||
<div class="card" style="border-left:3px solid var(--c-primary)">
|
||||
<div style="display:flex;gap:var(--space-2);align-items:center;margin-bottom:var(--space-3)">
|
||||
<svg class="ph-icon" style="color:var(--c-primary)" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#robot"></use>
|
||||
</svg>
|
||||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);color:var(--c-text)">
|
||||
Empfehlung des KI-Trainers
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:var(--text-sm);color:var(--c-text);line-height:1.7">
|
||||
<p>${html}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
result.hidden = false;
|
||||
} catch (err) {
|
||||
UI.toast(err.message || 'KI momentan nicht verfügbar.', 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg> Tipps holen`;
|
||||
feedbackText.textContent = resp.feedback;
|
||||
const cachedInfo = resp.cached ? 'Gespeichertes Feedback' : 'Gerade generiert';
|
||||
const sessionInfo = stats.total_sessions
|
||||
? ` · Basiert auf ${stats.total_sessions} Einheit${stats.total_sessions !== 1 ? 'en' : ''}`
|
||||
: '';
|
||||
feedbackMeta.textContent = `Aktualisiert alle 6 Stunden · ${cachedInfo}${sessionInfo}`;
|
||||
if (regenBtn) {
|
||||
regenBtn.textContent = resp.cached ? 'Neu generieren' : 'Aktualisieren';
|
||||
regenBtn.style.opacity = resp.cached ? '0.6' : '1';
|
||||
}
|
||||
});
|
||||
feedbackCard.hidden = false;
|
||||
} catch (err) {
|
||||
loading.hidden = true;
|
||||
noSessions.hidden = false;
|
||||
}
|
||||
|
||||
// Bind regenerate button
|
||||
if (regenBtn) {
|
||||
regenBtn.addEventListener('click', async () => {
|
||||
regenBtn.disabled = true;
|
||||
regenBtn.textContent = 'Generiere…';
|
||||
loading.hidden = false;
|
||||
feedbackCard.hidden = true;
|
||||
await _loadKiTrainerFeedback(true);
|
||||
regenBtn.disabled = false;
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
function _bindAccordions() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue