import { useState, useEffect } from 'react' import { useNavigate, useLocation } from 'react-router-dom' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, ReferenceLine, PieChart, Pie, Cell } from 'recharts' import { ChevronRight, Brain, ChevronDown, ChevronUp } from 'lucide-react' import { api } from '../utils/api' import { getBfCategory } from '../utils/calc' import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret' import Markdown from '../utils/Markdown' import TrainingTypeDistribution from '../components/TrainingTypeDistribution' import dayjs from 'dayjs' import 'dayjs/locale/de' dayjs.locale('de') function rollingAvg(arr, key, window=7) { return arr.map((d,i) => { const s = arr.slice(Math.max(0,i-window+1),i+1).map(x=>x[key]).filter(v=>v!=null) return s.length ? {...d,[`${key}_avg`]:Math.round(s.reduce((a,b)=>a+b)/s.length*10)/10} : d }) } const fmtDate = d => dayjs(d).format('DD.MM') function NavToCaliper() { const nav = useNavigate() return } function NavToCircum() { const nav = useNavigate() return } function EmptySection({ text, to, toLabel }) { const nav = useNavigate() return (
{text}
{to && }
) } function SectionHeader({ title, to, toLabel, lastUpdated }) { const nav = useNavigate() return (

{title}

{lastUpdated && {dayjs(lastUpdated).format('DD.MM.YY')}} {to && ( )}
) } function RuleCard({ item }) { const [open, setOpen] = useState(false) const color = getStatusColor(item.status) return (
setOpen(o=>!o)}> {item.icon}
{item.category}
{item.title}
{item.value && {item.value}} {open ? : }
{open &&
{item.detail}
}
) } function InsightBox({ insights, slugs, onRequest, loading }) { const [expanded, setExpanded] = useState(null) const relevant = insights?.filter(i=>slugs.includes(i.scope))||[] const LABELS = {gesamt:'Gesamt',koerper:'Komposition',ernaehrung:'Ernährung', aktivitaet:'Aktivität',gesundheit:'Gesundheit',ziele:'Ziele', pipeline:'🔬 Mehrstufige Analyse', pipeline_body:'Pipeline Körper',pipeline_nutrition:'Pipeline Ernährung', pipeline_activity:'Pipeline Aktivität',pipeline_synthesis:'Pipeline Synthese', pipeline_goals:'Pipeline Ziele'} return (
🤖 KI-AUSWERTUNGEN
{slugs.map(slug=>( ))}
{relevant.length===0 && (
Noch keine Auswertung. Klicke oben um eine zu erstellen.
)} {relevant.map(ins=>(
setExpanded(expanded===ins.id?null:ins.id)}>
{dayjs(ins.created).format('DD. MMM YYYY, HH:mm')} · {LABELS[ins.scope]||ins.scope}
{expanded===ins.id?:}
{expanded===ins.id &&
}
))}
) } // ── Period selector ─────────────────────────────────────────────────────────── function PeriodSelector({ value, onChange }) { const opts = [{v:30,l:'30 Tage'},{v:90,l:'90 Tage'},{v:180,l:'6 Monate'},{v:365,l:'1 Jahr'},{v:9999,l:'Alles'}] return (
{opts.map(o=>( ))}
) } // ── Body Section (Weight + Composition combined) ────────────────────────────── function BodySection({ weights, calipers, circs, profile, insights, onRequest, loadingSlug, filterActiveSlugs }) { const [period, setPeriod] = useState(90) const sex = profile?.sex||'m' const height = profile?.height||178 const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') const filtW = [...(weights||[])].sort((a,b)=>a.date.localeCompare(b.date)) .filter(d=>period===9999||d.date>=cutoff) const filtCal = (calipers||[]).filter(d=>period===9999||d.date>=cutoff) const filtCir = (circs||[]).filter(d=>period===9999||d.date>=cutoff) const hasWeight = filtW.length >= 2 const hasCal = filtCal.length >= 1 const hasCir = filtCir.length >= 1 if (!hasWeight && !hasCal && !hasCir) return (
) // ── Weight chart ── const withAvg = rollingAvg(filtW,'weight') const withAvg14= rollingAvg(filtW,'weight',14) const wCd = withAvg.map((d,i)=>({ date:fmtDate(d.date), weight:d.weight, avg7: d.weight_avg, avg14: withAvg14[i]?.weight_avg, })) const ws = filtW.map(w=>w.weight) const minW = ws.length ? Math.min(...ws) : null const maxW = ws.length ? Math.max(...ws) : null const avgAll = ws.length ? Math.round(ws.reduce((a,b)=>a+b)/ws.length*10)/10 : null const trendPeriods = [7,30,90].map(days=>{ const cut = dayjs().subtract(days,'day').format('YYYY-MM-DD') const per = filtW.filter(d=>d.date>=cut) if (per.length<2) return null const diff = Math.round((per[per.length-1].weight-per[0].weight)*10)/10 return {label:`${days}T`,diff,count:per.length} }).filter(Boolean) // ── Caliper chart ── const bfCd = [...filtCal].filter(c=>c.body_fat_pct).reverse().map(c=>({ date:fmtDate(c.date),bf:c.body_fat_pct,lean:c.lean_mass,fat:c.fat_mass })) const latestCal = filtCal[0] const prevCal = filtCal[1] const latestCir = filtCir[0] const latestW2 = filtW[filtW.length-1] const bfCat = latestCal?.body_fat_pct ? getBfCategory(latestCal.body_fat_pct,sex) : null // ── Circ chart ── const cirCd = [...filtCir].filter(c=>c.c_waist||c.c_hip).reverse().map(c=>({ date:fmtDate(c.date),waist:c.c_waist,hip:c.c_hip,belly:c.c_belly })) // ── Indicators ── const whr = latestCir?.c_waist&&latestCir?.c_hip ? Math.round(latestCir.c_waist/latestCir.c_hip*100)/100 : null const whtr = latestCir?.c_waist&&height ? Math.round(latestCir.c_waist/height*100)/100 : null // ── Rules ── const combined = { ...(latestCal||{}), c_waist:latestCir?.c_waist, c_hip:latestCir?.c_hip, weight:latestW2?.weight } const rules = getInterpretation(combined, profile, prevCal||null) return (
{/* Summary stats */}
{latestW2 &&
{latestW2.weight} kg
Aktuell
} {latestCal?.body_fat_pct &&
{latestCal.body_fat_pct}%
KF {bfCat?.label}
} {latestCal?.lean_mass &&
{latestCal.lean_mass} kg
Mager
} {whr &&
{whr}
WHR
} {whtr &&
{whtr}
WHtR
}
{/* Weight chart – 3 lines like WeightScreen */} {hasWeight && (
Gewicht · {filtW.length} Einträge
{avgAll && } {profile?.goal_weight && } [`${v} kg`,n==='weight'?'Täglich':n==='avg7'?'Ø 7 Tage':'Ø 14 Tage']}/>
● Täglich Ø 7T Ø 14T Ø Gesamt
{/* Trend tiles */} {trendPeriods.length>0 && (
{trendPeriods.map(({label,diff})=>(
0?'var(--warn)':'var(--border)'}`}}>
0?'var(--warn)':'var(--text3)'}}> {diff>0?'+':''}{diff} kg
{label}
))} {minW &&
{minW}
{maxW}
Min/Max
}
)}
)} {/* KF + Magermasse chart */} {bfCd.length>=2 && (
KF% + Magermasse
[`${v}${n==='bf'?'%':' kg'}`,n==='bf'?'KF%':'Mager']}/> {profile?.goal_bf_pct && }
KF% Mager kg {profile?.goal_bf_pct && Ziel KF}
)} {/* Circ trend */} {cirCd.length>=2 && (
Umfänge Verlauf
[`${v} cm`,n]}/> {cirCd.some(d=>d.belly) && }
)} {/* WHR / WHtR detail */} {(whr||whtr) && (
{whr &&
{whr}
WHR
Taille ÷ Hüfte
Ziel <{sex==='m'?'0,90':'0,85'}
{whr<(sex==='m'?0.90:0.85)?'✓ Günstig':'⚠️ Erhöht'}
} {whtr &&
{whtr}
WHtR
Taille ÷ Körpergröße
Ziel <0,50
{whtr<0.5?'✓ Optimal':'⚠️ Erhöht'}
}
)} {rules.length>0 && (
BEWERTUNG
{rules.map((item,i)=>)}
)}
) } // ── Nutrition Section ───────────────────────────────────────────────────────── function NutritionSection({ nutrition, weights, profile, insights, onRequest, loadingSlug, filterActiveSlugs }) { const [period, setPeriod] = useState(30) if (!nutrition?.length) return ( ) const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') const filtN = nutrition.filter(d=>period===9999||d.date>=cutoff) const sorted = [...filtN].sort((a,b)=>a.date.localeCompare(b.date)) if (!filtN.length) return (
) const n = filtN.length const avgKcal = Math.round(filtN.reduce((s,d)=>s+(d.kcal||0),0)/n) const avgProtein = Math.round(filtN.reduce((s,d)=>s+(d.protein_g||0),0)/n*10)/10 const avgFat = Math.round(filtN.reduce((s,d)=>s+(d.fat_g||0),0)/n*10)/10 const avgCarbs = Math.round(filtN.reduce((s,d)=>s+(d.carbs_g||0),0)/n*10)/10 const latestW = weights?.[0]?.weight||80 const ptLow = Math.round(latestW*1.6) const ptHigh = Math.round(latestW*2.2) const proteinOk = avgProtein>=ptLow // Stacked macro bar (daily) const cdMacro = sorted.map(d=>({ date: fmtDate(d.date), Protein: Math.round(d.protein_g||0), KH: Math.round(d.carbs_g||0), Fett: Math.round(d.fat_g||0), kcal: Math.round(d.kcal||0), })) // Pie const totalMacroKcal = avgProtein*4+avgCarbs*4+avgFat*9 const pieData = [ {name:'Protein',value:Math.round(avgProtein*4/totalMacroKcal*100),color:'#1D9E75'}, {name:'KH', value:Math.round(avgCarbs*4/totalMacroKcal*100), color:'#D4537E'}, {name:'Fett', value:Math.round(avgFat*9/totalMacroKcal*100), color:'#378ADD'}, ] // Weekly macro bars const weeklyMap={} filtN.forEach(d=>{ const wk=dayjs(d.date).format('YYYY-WW') const weekNum = (() => { const dt=new Date(d.date); dt.setHours(0,0,0,0); dt.setDate(dt.getDate()+4-(dt.getDay()||7)); const y=new Date(dt.getFullYear(),0,1); return Math.ceil(((dt-y)/86400000+1)/7) })() if(!weeklyMap[wk]) weeklyMap[wk]={label:'KW'+weekNum,n:0,protein:0,carbs:0,fat:0,kcal:0} weeklyMap[wk].protein+=d.protein_g||0; weeklyMap[wk].carbs+=d.carbs_g||0 weeklyMap[wk].fat+=d.fat_g||0; weeklyMap[wk].kcal+=d.kcal||0; weeklyMap[wk].n++ }) const weeklyData=Object.values(weeklyMap).slice(-12).map(w=>({ label:w.label, Protein:Math.round(w.protein/w.n), KH:Math.round(w.carbs/w.n), Fett:Math.round(w.fat/w.n), kcal:Math.round(w.kcal/w.n), })) // Rules const macroRules=[] if(!proteinOk) macroRules.push({status:'bad',icon:'🥩',category:'Protein', title:`Unterversorgung: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`, detail:`1,6–2,2g/kg KG. Fehlend: ~${ptLow-Math.round(avgProtein)}g täglich. Konsequenz: Muskelverlust bei Defizit.`, value:avgProtein+'g'}) else macroRules.push({status:'good',icon:'🥩',category:'Protein', title:`Gut: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`, detail:`Ausreichend für Muskelerhalt und -aufbau.`,value:avgProtein+'g'}) const protPct=Math.round(avgProtein*4/totalMacroKcal*100) if(protPct<20) macroRules.push({status:'warn',icon:'📊',category:'Makro-Anteil', title:`Protein-Anteil niedrig: ${protPct}% der Kalorien`, detail:`Empfehlung: 25–35%. Aktuell: ${protPct}% P / ${Math.round(avgCarbs*4/totalMacroKcal*100)}% KH / ${Math.round(avgFat*9/totalMacroKcal*100)}% F`, value:protPct+'%'}) return (
{[['Ø Kalorien',avgKcal+' kcal','#EF9F27'],['Ø Protein',avgProtein+'g',proteinOk?'#1D9E75':'#D85A30'], ['Ø Fett',avgFat+'g','#378ADD'],['Ø KH',avgCarbs+'g','#D4537E'], ['Einträge',n+' T','var(--text3)']].map(([l,v,c])=>(
{v}
{l}
))}
{/* Stacked macro bars (daily) */}
Makroverteilung täglich (g) · {sorted[0]?.date?.slice(0,7)} – {sorted[sorted.length-1]?.date?.slice(0,7)}
[`${v}g`,n]}/>
Protein KH Fett Protein-Ziel
{/* Pie + macro breakdown */}
Ø Makroverteilung · {n} Tage ({sorted[0]?.date?.slice(0,10)} – {sorted[sorted.length-1]?.date?.slice(0,10)})
{pieData.map((e,i)=>)} [`${v}%`,n]}/>
{pieData.map(p=>(
{p.name}
{p.value}%
{Math.round(p.name==='Protein'?avgProtein:p.name==='KH'?avgCarbs:avgFat)}g
{p.name==='Protein' &&
{proteinOk?'✓':'⚠️'} Ziel {ptLow}g
}
))}
Gesamt: {avgKcal} kcal/Tag
{/* Weekly stacked bars */} {weeklyData.length>=2 && (
Makros pro Woche (Ø g/Tag)
[`${v}g`,n]}/>
)}
BEWERTUNG
{macroRules.map((item,i)=>)}
) } // ── Activity Section ────────────────────────────────────────────────────────── function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs }) { const [period, setPeriod] = useState(30) if (!activities?.length) return ( ) const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') const filtA = activities.filter(d=>period===9999||d.date>=cutoff) const byDate={} filtA.forEach(a=>{ byDate[a.date]=(byDate[a.date]||0)+(a.kcal_active||0) }) const cd=Object.entries(byDate).sort((a,b)=>a[0].localeCompare(b[0])).map(([date,kcal])=>({date:fmtDate(date),kcal:Math.round(kcal)})) const totalKcal=Math.round(filtA.reduce((s,a)=>s+(a.kcal_active||0),0)) const totalMin =Math.round(filtA.reduce((s,a)=>s+(a.duration_min||0),0)) const hrData =filtA.filter(a=>a.hr_avg) const avgHr =hrData.length?Math.round(hrData.reduce((s,a)=>s+a.hr_avg,0)/hrData.length):null const types={}; filtA.forEach(a=>{ types[a.activity_type]=(types[a.activity_type]||0)+1 }) const topTypes=Object.entries(types).sort((a,b)=>b[1]-a[1]) const daysWithAct=new Set(filtA.map(a=>a.date)).size const totalDays=Math.min(period,dayjs().diff(dayjs(filtA[filtA.length-1]?.date),'day')+1) const consistency=totalDays>0?Math.round(daysWithAct/totalDays*100):0 const actRules=[{ status:consistency>=70?'good':consistency>=40?'warn':'bad', icon:'📅', category:'Konsistenz', title:`${consistency}% aktive Tage (${daysWithAct}/${Math.min(period,30)} Tage)`, detail:consistency>=70?'Ausgezeichnete Regelmäßigkeit.':consistency>=40?'Ziel: 4–5 Einheiten/Woche.':'Mehr Regelmäßigkeit empfohlen.', value:consistency+'%' }] return (
{[['Trainings',filtA.length,'var(--text1)'],['Kcal',totalKcal,'#EF9F27'], ['Stunden',Math.round(totalMin/60*10)/10,'#378ADD'], avgHr?['Ø HF',avgHr+' bpm','#D85A30']:null].filter(Boolean).map(([l,v,c])=>(
{v}
{l}
))}
Aktive Kalorien / Tag
[`${v} kcal`]}/>
Trainingsarten
{topTypes.map(([type,count])=>(
{type}
{count}×
))}
Trainingstyp-Verteilung
BEWERTUNG
{actRules.map((item,i)=>)}
) } // ── Correlation Section ─────────────────────────────────────────────────────── function CorrelationSection({ corrData, insights, profile, onRequest, loadingSlug, filterActiveSlugs }) { const filtered = (corrData||[]).filter(d=>d.kcal&&d.weight) if (filtered.length < 5) return ( ) const sex = profile?.sex||'m' const height = profile?.height||178 const latestW = filtered[filtered.length-1]?.weight||80 const age = profile?.dob ? Math.floor((Date.now()-new Date(profile.dob))/(365.25*24*3600*1000)) : 35 const bmr = sex==='m' ? 10*latestW+6.25*height-5*age+5 : 10*latestW+6.25*height-5*age-161 const tdee = Math.round(bmr*1.4) // light activity baseline // Chart 1: Kcal vs Weight const kcalVsW = rollingAvg(filtered.map(d=>({...d,date:fmtDate(d.date)})),'kcal') // Chart 2: Protein vs Lean Mass (only days with both) const protVsLean = filtered.filter(d=>d.protein_g&&d.lean_mass) .map(d=>({date:fmtDate(d.date),protein:d.protein_g,lean:d.lean_mass})) // Chart 3: Activity kcal vs Weight change const actVsW = filtered.filter(d=>d.weight) .map((d,i,arr)=>{ const prev = arr[i-1] return { date: fmtDate(d.date), weight: d.weight, weightDelta: prev ? Math.round((d.weight-prev.weight)*10)/10 : null, kcal: d.kcal||0, } }).filter(d=>d.weightDelta!==null) // Chart 4: Calorie balance (intake - estimated TDEE) const balance = filtered.map(d=>({ date: fmtDate(d.date), balance: Math.round((d.kcal||0) - tdee), })) const balWithAvg = rollingAvg(balance,'balance') const avgBalance = Math.round(balance.reduce((s,d)=>s+d.balance,0)/balance.length) // ── Correlation insights ── const corrInsights = [] // 1. Kcal → Weight correlation if (filtered.length >= 14) { const highKcal = filtered.filter(d=>d.kcal>tdee+200) const lowKcal = filtered.filter(d=>d.kcal=3 && lowKcal.length>=3) { const avgWHigh = Math.round(highKcal.reduce((s,d)=>s+d.weight,0)/highKcal.length*10)/10 const avgWLow = Math.round(lowKcal.reduce((s,d)=>s+d.weight,0)/lowKcal.length*10)/10 corrInsights.push({ icon:'📊', status: avgWLow < avgWHigh ? 'good' : 'warn', title: avgWLow < avgWHigh ? `Kalorienreduktion wirkt: Ø ${avgWLow}kg bei Defizit vs. ${avgWHigh}kg bei Überschuss` : `Kein klarer Kalorieneffekt auf Gewicht erkennbar`, detail: `Tage mit Überschuss (>${tdee+200} kcal): Ø ${avgWHigh}kg · Tage mit Defizit (<${tdee-200} kcal): Ø ${avgWLow}kg`, }) } } // 2. Protein → Lean mass if (protVsLean.length >= 3) { const ptLow = Math.round(latestW*1.6) const highProt = protVsLean.filter(d=>d.protein>=ptLow) const lowProt = protVsLean.filter(d=>d.protein=2 && lowProt.length>=2) { const avgLH = Math.round(highProt.reduce((s,d)=>s+d.lean,0)/highProt.length*10)/10 const avgLL = Math.round(lowProt.reduce((s,d)=>s+d.lean,0)/lowProt.length*10)/10 corrInsights.push({ icon:'🥩', status: avgLH >= avgLL ? 'good' : 'warn', title: `Hohe Proteinzufuhr (≥${ptLow}g): Ø ${avgLH}kg Mager · Niedrig: Ø ${avgLL}kg`, detail: `${highProt.length} Messpunkte mit hoher vs. ${lowProt.length} mit niedriger Proteinzufuhr verglichen.`, }) } } // 3. Avg balance corrInsights.push({ icon: avgBalance < -100 ? '✅' : avgBalance > 200 ? '⬆️' : '➡️', status: avgBalance < -100 ? 'good' : avgBalance > 300 ? 'warn' : 'good', title: `Ø Kalorienbilanz: ${avgBalance>0?'+':''}${avgBalance} kcal/Tag`, detail: `Geschätzter TDEE: ${tdee} kcal (Mifflin-St Jeor ×1,4). ${ avgBalance<-500?'Starkes Defizit – Muskelerhalt durch ausreichend Protein sicherstellen.': avgBalance<-100?'Moderates Defizit – ideal für Fettabbau bei Muskelerhalt.': avgBalance>300?'Kalorienüberschuss – günstig für Muskelaufbau, Fettzunahme möglich.': 'Nahezu ausgeglichen – Gewicht sollte stabil bleiben.'}`, }) return (
{/* Chart 1: Kcal vs Weight */}
📉 Kalorien (Ø 7T) vs. Gewicht
[`${Math.round(v)} ${n==='weight'?'kg':'kcal'}`,n==='kcal_avg'?'Ø Kalorien':'Gewicht']}/>
Gestrichelt: geschätzter TDEE {tdee} kcal · — Kalorien · — Gewicht
{/* Chart 2: Calorie balance */}
⚖️ Kalorienbilanz (Aufnahme − TDEE {tdee} kcal)
[`${v>0?'+':''}${v} kcal`,n==='balance_avg'?'Ø 7T Bilanz':'Tagesbilanz']}/>
Über 0 = Überschuss · Unter 0 = Defizit · Ø {avgBalance>0?'+':''}{avgBalance} kcal/Tag
{/* Chart 3: Protein vs Lean Mass */} {protVsLean.length >= 3 && (
🥩 Protein vs. Magermasse
[`${v}${n==='protein'?'g':' kg'}`,n==='protein'?'Protein':'Mager']}/>
— Protein g/Tag · ● Magermasse kg
)} {/* Correlation insights */} {corrInsights.length > 0 && (
KORRELATIONSAUSSAGEN
{corrInsights.map((item,i) => (
{item.icon}
{item.title}
{item.detail}
))}
ℹ️ TDEE-Schätzung basiert auf Mifflin-St Jeor ×1,4 (leicht aktiv). Für genauere Werte Aktivitätsdaten erfassen.
)}
) } // ── Photo Grid ──────────────────────────────────────────────────────────────── function PhotoGrid() { const [photos,setPhotos]=useState([]) const [big,setBig]=useState(null) useEffect(()=>{ api.listPhotos().then(setPhotos) },[]) if(!photos.length) return return ( <> {big&&
setBig(null)}>
}
{photos.map(p=>(
setBig(p.id)} alt=""/>
{p.date?.slice(0,10)||p.created?.slice(0,10)}
))}
) } // ── Main ────────────────────────────────────────────────────────────────────── const TABS = [ { id:'body', label:'⚖️ Körper' }, { id:'nutrition', label:'🍽️ Ernährung' }, { id:'activity', label:'🏋️ Aktivität' }, { id:'correlation', label:'🔗 Korrelation' }, { id:'photos', label:'📷 Fotos' }, ] export default function History() { const location = useLocation?.() || {} const [tab, setTab] = useState((location.state?.tab)||'body') const [weights, setWeights] = useState([]) const [calipers, setCalipers] = useState([]) const [circs, setCircs] = useState([]) const [nutrition, setNutrition] = useState([]) const [activities, setActivities] = useState([]) const [corrData, setCorrData] = useState([]) const [insights, setInsights] = useState([]) const [prompts, setPrompts] = useState([]) const [profile, setProfile] = useState(null) const [loading, setLoading] = useState(true) const [loadingSlug,setLoadingSlug]= useState(null) const loadAll = () => Promise.all([ api.listWeight(365), api.listCaliper(), api.listCirc(), api.listNutrition(90), api.listActivity(200), api.nutritionCorrelations(), api.latestInsights(), api.getProfile(), api.listPrompts(), ]).then(([w,ca,ci,n,a,corr,ins,p,pr])=>{ setWeights(w); setCalipers(ca); setCircs(ci) setNutrition(n); setActivities(a); setCorrData(corr) setInsights(Array.isArray(ins)?ins:[]); setProfile(p) setPrompts(Array.isArray(pr)?pr:[]) setLoading(false) }) useEffect(()=>{ loadAll() },[]) const requestInsight = async (slug) => { setLoadingSlug(slug) try { const result = await api.runInsight(slug) // result is already JSON, not a Response object const ins = await api.latestInsights() setInsights(Array.isArray(ins)?ins:[]) } catch(e){ alert('KI-Fehler: '+e.message) } finally{ setLoadingSlug(null) } } if(loading) return
// Filter active prompts const activeSlugs = prompts.filter(p=>p.active).map(p=>p.slug) const filterActiveSlugs = (slugs) => slugs.filter(s=>activeSlugs.includes(s)) const sp={insights,onRequest:requestInsight,loadingSlug,filterActiveSlugs} return (

Verlauf & Auswertung

{TABS.map(t=>( ))}
{tab==='body' && } {tab==='nutrition' && } {tab==='activity' && } {tab==='correlation' && } {tab==='photos' && }
) }