🌿
Time to log your meal!
Tap to record what you've eaten.
Log Now
9:41
●●●
Using local storage
⬇ CSV
Sync
Today
+
🍽️
3
Meals
🔥
1,420
Calories
💧
4
Water
71%
1,420 / 2,000 kcal
580 kcal remaining today
💧
Quick Log Water
+
⚙️ Connect cloud storage to sync your data
Open this file and set
WORKER_URL
to your Cloudflare Worker URL.
Until then, entries are saved locally in this browser only.
🌅 Breakfast
8:12 AM
Oatmeal with berries & honey
1 bowl · 380 kcal
😊
☀️ Lunch
12:05 PM
Grilled chicken salad
Large plate · 520 kcal
😄
12:08 PM
Water
1 glass
💧
🍎 Snack
2:30 PM
Apple & almond butter
1 apple, 2 tbsp · 210 kcal
🙂
Insights
🧠
Weekly AI Recap
Tap to generate your personalized report
"You've logged 21 meals this week across all meal types. Tap to see your full AI-powered health insights…"
✨ Generate full recap →
7-Day Overview
🔥
1,580
Avg Cal / Day
🍽️
3.4
Avg Meals / Day
💧
5.2
Avg Water / Day
🎯
5/7
Goal Days Hit
Daily Calories vs Goal
Mood After Eating — This Week
😄 Great 😊 Good 😐 Okay 😮💨 Bloated
Settings
Appearance
🌙
Dark Mode
Daily Calorie Goal
Set your daily calorie target to track progress on the Today screen.
kcal / day
Save
Reminder Times
🔔
Meal Reminders
Tap times to toggle on/off. Active times shown in green.
Next reminder: 4:00 PM
Data
📤
Export to CSV
›
📋
Total Entries
21
About
🌿
Version
4.0.0
📱
Platform
iOS 17+ / macOS 14+
Log Meal
Save
Meal Type
🌅 Bfast
☀️ Lunch
🌙 Dinner
🍎 Snack
💧 Drink
✨
AI Meal Suggest
AI
✨
Suggest
Pick meal type above, add ingredients (optional), tap Suggest.
Food Item
Portion Size
Est. Calories (optional)
Hunger Before Eating
😐
🙂
😊
😋
🤤
Mood After Eating
😄
😊
😐
😮💨
😴
🤢
Notes (optional)
🧠 Weekly Recap
Done
🌿
Today
📊
Insights
⚙️
Settings
// ── Clock ── (function tick(){ const n=new Date(),h=n.getHours()%12||12,m=n.getMinutes(),ap=n.getHours()>=12?'PM':'AM'; document.getElementById('statusTime').textContent=`${h}:${String(m).padStart(2,'0')} ${ap}`; setTimeout(tick,30000); })(); // ── Tabs ── function switchTab(name,el){ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active')); document.querySelectorAll('.tab-item').forEach(t=>t.classList.remove('active')); document.getElementById('screen-'+name).classList.add('active'); el.classList.add('active'); if(name==='insights'){renderBars();renderMoodGrid();} } // ══════════════════════════════════════════ // DARK MODE // ══════════════════════════════════════════ function toggleDark(){ darkMode=!darkMode; document.getElementById('phone').setAttribute('data-theme',darkMode?'dark':''); const t=document.getElementById('darkToggle'); darkMode?t.classList.add('on'):t.classList.remove('on'); } // ══════════════════════════════════════════ // CALORIE GOAL RING // ══════════════════════════════════════════ function updateGoalRing(){ const pct=Math.min(calCount/calorieGoal,1); const circ=113; const offset=circ-(pct*circ); document.getElementById('ringFg').style.strokeDashoffset=offset; document.getElementById('ringPct').textContent=Math.round(pct*100)+'%'; document.getElementById('goalTitle').textContent=`${calCount.toLocaleString()} / ${calorieGoal.toLocaleString()} kcal`; const rem=Math.max(0,calorieGoal-calCount); document.getElementById('goalSub').textContent=rem>0?`${rem.toLocaleString()} kcal remaining`:`Goal reached! 🎉`; const ring=document.getElementById('ringFg'); ring.style.stroke=pct>=1?'#e74c3c':pct>=.8?'#f39c12':'var(--gold)'; } function saveGoal(){ const v=parseInt(document.getElementById('goalInput').value)||2000; calorieGoal=Math.max(500,Math.min(9999,v)); document.getElementById('goalInput').value=calorieGoal; updateGoalRing(); const btn=document.querySelector('.goal-save-btn'); btn.textContent='Saved ✓';btn.style.background='var(--sage)'; setTimeout(()=>{btn.textContent='Save';btn.style.background='';},1500); } // init ring setTimeout(()=>{ const circ=113,pct=calCount/calorieGoal; document.getElementById('ringFg').style.strokeDashoffset=circ-(pct*circ); document.getElementById('ringPct').textContent=Math.round(pct*100)+'%'; },300); // ══════════════════════════════════════════ // REMINDER CHIPS // ══════════════════════════════════════════ function initChips(){ const wrap=document.getElementById('reminderChips'); wrap.innerHTML=''; ALL_REMINDER_HOURS.forEach(h=>{ const chip=document.createElement('div'); chip.className='r-chip'+(activeReminderHours.has(h)?' active':''); chip.textContent=formatHour(h); chip.onclick=()=>toggleChip(h,chip); wrap.appendChild(chip); }); updateNextReminder(); } function toggleChip(h,chip){ if(activeReminderHours.has(h)){activeReminderHours.delete(h);chip.classList.remove('active');} else{activeReminderHours.add(h);chip.classList.add('active');} updateNextReminder(); } function formatHour(h){const h12=h>12?h-12:h,ap=h>=12?'PM':'AM';return `${h12}:00 ${ap}`;} function updateNextReminder(){ const now=new Date().getHours(); const next=ALL_REMINDER_HOURS.filter(h=>activeReminderHours.has(h)&&h>now)[0] ||ALL_REMINDER_HOURS.filter(h=>activeReminderHours.has(h))[0]; document.getElementById('nextReminderLabel').textContent= next?`Next reminder: ${formatHour(next)}`:'No reminders active'; } function toggleReminders(){ remindersOn=!remindersOn; const t=document.getElementById('remToggle'); remindersOn?t.classList.add('on'):t.classList.remove('on'); document.getElementById('reminderChips').style.opacity=remindersOn?1:.4; document.getElementById('nextReminderLabel').style.opacity=remindersOn?1:.4; } initChips(); // ══════════════════════════════════════════ // INSIGHTS — BAR CHART // ══════════════════════════════════════════ function renderBars(){ const container=document.getElementById('barChartRows'); if(container.children.length)return; const days=['Mon','Tue','Wed','Thu','Fri','Sat','Sun']; const cals=[1620,1380,1890,1540,1420,980,1650]; const max=Math.max(calorieGoal,...cals); cals.forEach((c,i)=>{ const r=document.createElement('div');r.className='bar-row'; const pct=Math.round(c/max*100); const color=c>=calorieGoal?'#e74c3c':c>=calorieGoal*.8?'var(--gold)':'var(--nav)'; r.innerHTML=`
${days[i]}
${c.toLocaleString()}
`; container.appendChild(r); }); // Goal line marker const goalPct=Math.round(calorieGoal/max*100); container.style.position='relative'; setTimeout(()=>container.querySelectorAll('.bar-fill').forEach(b=>b.style.width=b.dataset.w),80); } // ══════════════════════════════════════════ // INSIGHTS — MOOD GRID // ══════════════════════════════════════════ function renderMoodGrid(){ const grid=document.getElementById('moodGrid'); if(grid.children.length)return; const days=['M','T','W','T','F','S','S']; const moodData=[ ['😄','😊','😊'],['😊','😐','😄'],['😄','😊','😊'], ['😮💨','😊','😐'],['😊','😄','😊'],['😄','😄','😊'],['😊','😊','😄'] ]; const moodClass={'😄':'great','😊':'good','😐':'okay','😮💨':'bad','😴':'bad','🤢':'bad'}; days.forEach((d,i)=>{ const col=document.createElement('div');col.className='mood-col'; const lbl=document.createElement('div');lbl.className='mood-day-lbl';lbl.textContent=d; col.appendChild(lbl); (moodData[i]||['😊']).forEach(m=>{ const dot=document.createElement('div'); dot.className=`mood-dot ${moodClass[m]||''}`; dot.textContent=m; col.appendChild(dot); }); grid.appendChild(col); }); } // ══════════════════════════════════════════ // AI WEEKLY RECAP // ══════════════════════════════════════════ let recapGenerated=false; function openRecap(){ document.getElementById('recapOverlay').classList.add('open'); if(!recapGenerated) generateRecap(); } function closeRecap(){document.getElementById('recapOverlay').classList.remove('open');} function closeRecapOnBg(e){if(e.target===document.getElementById('recapOverlay'))closeRecap();} async function generateRecap(){ const el=document.getElementById('recapFullText'); el.innerHTML=`
`; const summaryData=` Weekly food log summary (sample data for demo): - Total entries: 21 meals logged across 7 days - Average daily calories: 1,580 kcal (goal: ${calorieGoal} kcal) - Average meals per day: 3.4 - Average water glasses per day: 5.2 - Mood breakdown: 60% Great/Good, 30% Okay, 10% Bloated/Tired - Most common breakfast: oatmeal, yogurt - Most common lunch: salads, wraps - Lightest day: Saturday (980 kcal) - Goal hit 5 out of 7 days`; try{ const res=await fetch('https://api.anthropic.com/v1/messages',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ model:'claude-sonnet-4-20250514', max_tokens:600, system:`You are a supportive, friendly nutrition coach inside a food logging app. Write a warm, motivating weekly recap in 4–5 short paragraphs. Use plain text only — no markdown, no asterisks, no bullet points, no headers. Highlight positives first, then 1-2 gentle suggestions. Keep it under 200 words. Address the user as "you". Be encouraging, not clinical.`, messages:[{role:'user',content:`Generate a weekly recap based on this data:\n${summaryData}`}] }) }); if(!res.ok)throw new Error('API '+res.status); const data=await res.json(); const text=sanitize(data.content?.map(b=>b.text||'').join('')||'',2000); const paras=text.split('\n').filter(p=>p.trim()); el.innerHTML=''; paras.forEach(p=>{ const div=document.createElement('p'); div.textContent=p.trim(); el.appendChild(div); }); recapGenerated=true; // Update preview card document.getElementById('recapPreview').textContent=`"${paras[0]?.slice(0,100)}…"`; document.querySelector('.recap-cta-text').textContent='✨ View full recap →'; }catch(err){ el.innerHTML=''; const p=document.createElement('p'); p.style.color='#c0392b'; p.textContent='⚠️ Could not generate recap. Check your connection and try again.'; el.appendChild(p); } } // ══════════════════════════════════════════ // LOG SHEET // ══════════════════════════════════════════ function openSheet(){document.getElementById('sheetOverlay').classList.add('open');} function closeSheetOnBg(e){if(e.target===document.getElementById('sheetOverlay'))closeSheet();} function closeSheet(){ document.getElementById('sheetOverlay').classList.remove('open'); resetSuggest(); } function openLogFromBanner(){ document.getElementById('reminderBanner').classList.remove('show'); openSheet(); } function selMeal(btn,meal){ selectedMeal=meal; document.querySelectorAll('#mealSegs .seg-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); resetSuggest(); } function selHunger(btn){ document.querySelectorAll('#hungerBtns .hunger-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); } function selMood(btn,mood){ selectedMood=mood; document.querySelectorAll('#moodSegs .seg-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); } function saveEntry(){ const food=sanitize(document.getElementById('inputFood').value,200); const portion=sanitize(document.getElementById('inputPortion').value,100); const cal=Math.max(0,Math.min(9999,parseInt(document.getElementById('inputCal').value)||0)); if(!food&&selectedMeal!=='Drink'){ document.getElementById('inputFood').style.outline='2px solid #c0392b'; setTimeout(()=>document.getElementById('inputFood').style.outline='',1500); return; } const now=new Date(); const timeStr=now.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'}); const mealEmoji={Breakfast:'🌅',Lunch:'☀️',Dinner:'🌙',Snack:'🍎',Drink:'💧'}[selectedMeal]||'🍽️'; const list=document.getElementById('entryList'); const row=document.createElement('div');row.className='entry-row'; const tEl=document.createElement('span');tEl.className='entry-time';tEl.textContent=timeStr; const iEl=document.createElement('div');iEl.className='entry-info'; const nEl=document.createElement('div');nEl.className='entry-name';nEl.textContent=food||'Water'; const mEl=document.createElement('div');mEl.className='entry-meta'; mEl.textContent=[portion,cal?cal+' kcal':''].filter(Boolean).join(' · ')||'—'; const eEl=document.createElement('span');eEl.className='entry-mood';eEl.textContent=selectedMood||mealEmoji; iEl.append(nEl,mEl);row.append(tEl,iEl,eEl); list.appendChild(row); if(selectedMeal!=='Drink'){mealCount++;calCount+=cal;}else waterCount++; document.getElementById('statMeals').textContent=mealCount; document.getElementById('statCal').textContent=calCount.toLocaleString(); document.getElementById('statWater').textContent=waterCount; updateGoalRing(); ['inputFood','inputPortion','inputCal','inputNotes','inputIngredients'].forEach(id=>document.getElementById(id).value=''); closeSheet(); } // ══════════════════════════════════════════ // AI SUGGEST // ══════════════════════════════════════════ function sanitize(str,max){ return String(str??'').replace(/<[^>]*>/g,'').replace(/[\x00-\x1F\x7F]/g,'').trim().slice(0,max); } async function getSuggestions(){ const btn=document.getElementById('suggestBtn'); const icon=document.getElementById('suggestIcon'); const results=document.getElementById('suggestResults'); const errEl=document.getElementById('suggestError'); const ingredients=sanitize(document.getElementById('inputIngredients').value,300); btn.classList.add('loading');icon.textContent='⏳'; errEl.classList.remove('visible'); results.classList.add('visible'); results.innerHTML=`
`; const prompt=ingredients ?`${selectedMeal} meal. I have: ${ingredients}. Suggest 3 meals.` :`Suggest 3 healthy ${selectedMeal} meal ideas.`; try{ const res=await fetch('https://api.anthropic.com/v1/messages',{ method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({ model:'claude-sonnet-4-20250514',max_tokens:800, system:`Nutrition assistant in a food logging app. Reply ONLY with raw JSON, no markdown. Schema: {"suggestions":[{"name":"string","description":"string","portion":"string","calories":number,"tags":["string"]}]} Exactly 3 items. Tags: 1-3 short labels.`, messages:[{role:'user',content:prompt}] }) }); if(!res.ok)throw new Error('API '+res.status); const data=await res.json(); const raw=data.content?.map(b=>b.text||'').join('')||''; const parsed=JSON.parse(raw.replace(/```json|```/g,'').trim()); renderCards(parsed.suggestions||[]); }catch(e){ results.classList.remove('visible');results.innerHTML=''; errEl.textContent='⚠️ Could not load suggestions. Try again.';errEl.classList.add('visible'); }finally{btn.classList.remove('loading');icon.textContent='✨';} }