// ── State ────────────────────────────────────────────────────────────────── const state = { activeTab: 'pantry', pantry: [], meals: [], mealStats: null, currentMenu: null, currentGrocery: null, ollamaStatus: 'unknown', currentGroceryId: null, editingIngredientId: null, chatHistory: [], }; let pendingSwap = null; // ── API Utilities ────────────────────────────────────────────────────────── async function api(method, path, body = null) { const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (body) opts.body = JSON.stringify(body); try { const res = await fetch(`/api${path}`, opts); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail || 'Request failed'); } if (res.status === 204) return null; return res.json(); } catch (err) { throw err; } } // ── Toast Notifications ─────────────────────────────────────────────────── function showToast(message, type = 'success') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const icon = type === 'success' ? '✓' : '✕'; toast.innerHTML = `
${icon}
${message}
`; container.appendChild(toast); setTimeout(() => { toast.classList.add('exiting'); setTimeout(() => toast.remove(), 300); }, 4000); } // ── Formatting Utilities ─────────────────────────────────────────────────── function formatDate(isoStr) { if (!isoStr) return ''; const date = new Date(isoStr + 'T00:00:00Z'); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); const dateOnly = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const todayStr = today.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const yesterdayStr = yesterday.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); if (dateOnly === todayStr) return 'Today'; if (dateOnly === yesterdayStr) return 'Yesterday'; return dateStr; } function formatDateTime(isoStr) { if (!isoStr) return ''; const date = new Date(isoStr); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + ', ' + date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); } function daysUntilExpiry(expiryDateStr) { if (!expiryDateStr) return null; const expiry = new Date(expiryDateStr + 'T00:00:00Z'); const today = new Date(); today.setUTCHours(0, 0, 0, 0); const days = Math.floor((expiry - today) / (1000 * 60 * 60 * 24)); return days; } // ── Tab Switching ────────────────────────────────────────────────────────── function switchTab(tabName) { state.activeTab = tabName; // Update button states document.querySelectorAll('.tab-btn').forEach(btn => { btn.classList.toggle('active', btn.dataset.tab === tabName); }); // Update content visibility document.querySelectorAll('.tab-content').forEach(content => { content.classList.toggle('hidden', content.id !== `tab-${tabName}`); content.classList.toggle('active', content.id === `tab-${tabName}`); }); // Load tab content if (tabName === 'pantry') loadPantry(); else if (tabName === 'meals') loadMeals(); else if (tabName === 'menu') loadMenu(); else if (tabName === 'grocery') loadGrocery(); else if (tabName === 'chat') loadChat(); } // ── Pantry Tab ───────────────────────────────────────────────────────────── async function loadPantry() { try { state.pantry = await api('GET', '/pantry'); renderPantryTable(); updateExpiryWarning(); } catch (err) { showToast(err.message, 'error'); } } function renderPantryTable() { const tbody = document.getElementById('pantry-tbody'); const emptyState = document.getElementById('pantry-empty'); const table = document.getElementById('pantry-table'); if (state.pantry.length === 0) { tbody.innerHTML = ''; table.classList.add('hidden'); emptyState.classList.remove('hidden'); return; } table.classList.remove('hidden'); emptyState.classList.add('hidden'); tbody.innerHTML = state.pantry.map(ing => { const days = daysUntilExpiry(ing.expiry_date); let rowClass = ''; if (days !== null && days < 2) rowClass = 'expiry-danger'; else if (days !== null && days < 7) rowClass = 'expiry-warn'; const expiryText = ing.expiry_date ? formatDate(ing.expiry_date) : '—'; return ` ${ing.name} ${ing.quantity} ${ing.unit} ${ing.category || '—'} ${expiryText} `; }).join(''); } function updateExpiryWarning() { const banner = document.getElementById('expiry-warning-banner'); const expiring = state.pantry.filter(ing => { const days = daysUntilExpiry(ing.expiry_date); return days !== null && days < 3 && days >= 0; }); if (expiring.length === 0) { banner.classList.add('hidden'); return; } banner.classList.remove('hidden'); banner.innerHTML = ` ⚠️ Expiring Soon: ${expiring.map(ing => `${ing.name} (${formatDate(ing.expiry_date)})`).join(', ')} `; } async function addIngredient() { const name = document.getElementById('ing-name').value.trim(); const qty = parseFloat(document.getElementById('ing-qty').value); const unit = document.getElementById('ing-unit').value.trim(); const category = document.getElementById('ing-category').value; const expiry = document.getElementById('ing-expiry').value; if (!name) { showToast('Please enter ingredient name', 'error'); return; } try { await api('POST', '/pantry', { name, quantity: qty || 0, unit, category: category || null, expiry_date: expiry || null, }); document.getElementById('add-ingredient-form').classList.add('hidden'); document.getElementById('ing-name').value = ''; document.getElementById('ing-qty').value = ''; document.getElementById('ing-unit').value = ''; document.getElementById('ing-category').value = ''; document.getElementById('ing-expiry').value = ''; await loadPantry(); showToast('Ingredient added!'); } catch (err) { showToast(err.message, 'error'); } } function editIngredient(id) { const ing = state.pantry.find(i => i.id === id); if (!ing) return; state.editingIngredientId = id; document.getElementById('ing-name').value = ing.name; document.getElementById('ing-qty').value = ing.quantity || ''; document.getElementById('ing-unit').value = ing.unit || ''; document.getElementById('ing-category').value = ing.category || ''; document.getElementById('ing-expiry').value = ing.expiry_date || ''; document.getElementById('add-ingredient-form').classList.remove('hidden'); document.getElementById('btn-save-ingredient').textContent = 'Update'; } async function deleteIngredient(id) { if (!confirm('Delete this ingredient?')) return; try { await api('DELETE', `/pantry/${id}`); await loadPantry(); showToast('Ingredient deleted'); } catch (err) { showToast(err.message, 'error'); } } // ── Meals Tab ────────────────────────────────────────────────────────────── async function loadMeals() { try { state.meals = await api('GET', '/meals?days=30'); state.mealStats = await api('GET', '/meals/stats'); renderMealsStats(); renderMealsTimeline(); } catch (err) { showToast(err.message, 'error'); } } function renderMealsStats() { const statsDiv = document.getElementById('meals-stats'); if (!state.mealStats || !state.mealStats.top_meals || state.mealStats.top_meals.length === 0) { statsDiv.innerHTML = ''; return; } const maxCount = Math.max(...state.mealStats.top_meals.map(m => m.count)); statsDiv.innerHTML = `
Top Meals (Last 30 Days)
${state.mealStats.top_meals.slice(0, 5).map(meal => { const percentage = (meal.count / maxCount) * 100; return `
${meal.meal_name}
${meal.count}x
${meal.count}
`; }).join('')} `; } function renderMealsTimeline() { const timelineDiv = document.getElementById('meals-timeline'); const emptyState = document.getElementById('meals-empty'); if (state.meals.length === 0) { timelineDiv.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); // Group meals by date const grouped = {}; state.meals.forEach(meal => { const dateKey = meal.eaten_at.split('T')[0]; if (!grouped[dateKey]) grouped[dateKey] = []; grouped[dateKey].push(meal); }); // Sort dates descending const sortedDates = Object.keys(grouped).sort().reverse(); timelineDiv.innerHTML = `
${sortedDates.map(dateKey => { const meals = grouped[dateKey]; const dateLabel = formatDate(dateKey); return `
${dateLabel}
${meals.map(meal => { const badgeClass = `meal-type-${meal.meal_type}`; const timeStr = new Date(meal.eaten_at).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); return `
${meal.meal_type}
${meal.meal_name}
${timeStr}
${meal.notes ? `
${meal.notes}
` : ''}
`; }).join('')}
`; }).join('')}
`; } async function logMeal() { const mealName = document.getElementById('meal-name').value.trim(); const mealType = document.getElementById('meal-type').value; const eatenAt = document.getElementById('meal-datetime').value; const notes = document.getElementById('meal-notes').value.trim(); const servings = parseInt(document.getElementById('meal-servings').value) || 1; if (!mealName || !eatenAt) { showToast('Please fill meal name and date/time', 'error'); return; } try { await api('POST', '/meals', { meal_name: mealName, meal_type: mealType, eaten_at: eatenAt, notes: notes || null, servings, }); document.getElementById('log-meal-form').classList.add('hidden'); document.getElementById('meal-name').value = ''; document.getElementById('meal-notes').value = ''; document.getElementById('meal-servings').value = '1'; await loadMeals(); showToast('Meal logged!'); } catch (err) { showToast(err.message, 'error'); } } async function deleteMeal(id) { if (!confirm('Delete this meal log?')) return; try { await api('DELETE', `/meals/${id}`); await loadMeals(); showToast('Meal deleted'); } catch (err) { showToast(err.message, 'error'); } } // ── Menu Tab ─────────────────────────────────────────────────────────────── async function loadMenu() { try { const res = await api('GET', '/menus/current').catch(err => { if (err.message.includes('404')) return null; throw err; }); if (!res) { document.getElementById('recipe-list').innerHTML = ''; document.getElementById('menu-empty').classList.remove('hidden'); document.getElementById('menu-notes').classList.add('hidden'); document.getElementById('btn-clear-menu').classList.add('hidden'); return; } let planIds = []; try { const parsed = typeof res.plan === 'string' ? JSON.parse(res.plan) : res.plan; planIds = Array.isArray(parsed) ? parsed : []; } catch { planIds = []; } const allRecipes = await api('GET', '/recipes'); const recipeMap = Object.fromEntries(allRecipes.map(r => [r.id, r])); const planRecipes = planIds.map(id => recipeMap[id]).filter(Boolean); state.currentMenu = { ...res, recipes: planRecipes }; document.getElementById('menu-empty').classList.add('hidden'); document.getElementById('btn-clear-menu').classList.remove('hidden'); if (res.notes) { document.getElementById('menu-notes').textContent = res.notes; document.getElementById('menu-notes').classList.remove('hidden'); } else { document.getElementById('menu-notes').classList.add('hidden'); } renderRecipeList(planRecipes); } catch (err) { showToast(err.message, 'error'); } } function renderRecipeList(recipes) { const list = document.getElementById('recipe-list'); if (!recipes || recipes.length === 0) { list.innerHTML = ''; return; } list.innerHTML = recipes.map(recipe => { let ingredients = recipe.ingredients || []; if (typeof ingredients === 'string') { try { ingredients = JSON.parse(ingredients); } catch { ingredients = []; } } const ingredientText = Array.isArray(ingredients) ? ingredients.map(i => { if (typeof i === 'string') return i; return [i.quantity, i.unit, i.name].filter(Boolean).join(' '); }).join(', ') : ''; const mealType = recipe.meal_type || 'dinner'; const typeDisplay = mealType.charAt(0).toUpperCase() + mealType.slice(1); const time = recipe.time_minutes || recipe.estimated_time_minutes; const serves = recipe.serves || recipe.servings; const meta = [time ? `${time} min` : '', serves ? `serves ${serves}` : ''].filter(Boolean).join(' · '); return `
${recipe.name}
${typeDisplay}
${meta ? `
${meta}
` : ''} ${ingredientText ? `
Ingredients: ${ingredientText}
` : ''} ${recipe.instructions ? `
${recipe.instructions}
` : ''}
`; }).join(''); } async function removeRecipe(recipeId, btn) { btn.disabled = true; try { await api('DELETE', `/menus/current/recipes/${recipeId}`, null); state.currentMenu.recipes = state.currentMenu.recipes.filter(r => r.id !== recipeId); renderRecipeList(state.currentMenu.recipes); if (state.currentMenu.recipes.length === 0) { document.getElementById('menu-empty').classList.remove('hidden'); document.getElementById('btn-clear-menu').classList.add('hidden'); } } catch (err) { showToast(err.message, 'error'); btn.disabled = false; } } function swapRecipe(recipeId, mealType, btn) { pendingSwap = { recipeId, mealType, btn }; const recipe = state.currentMenu.recipes.find(r => r.id === recipeId); document.getElementById('swap-modal-subtitle').textContent = recipe ? `Replacing: ${recipe.name}` : ''; document.getElementById('swap-notes').value = ''; document.getElementById('swap-modal-overlay').classList.remove('hidden'); document.getElementById('swap-notes').focus(); } function cancelSwap() { document.getElementById('swap-modal-overlay').classList.add('hidden'); pendingSwap = null; } async function confirmSwap() { if (!pendingSwap) return; const { recipeId, btn } = pendingSwap; const userNotes = document.getElementById('swap-notes').value.trim(); document.getElementById('swap-modal-overlay').classList.add('hidden'); btn.disabled = true; btn.textContent = '...'; pendingSwap = null; try { const res = await api('POST', `/menus/current/recipes/${recipeId}/swap`, userNotes ? { user_notes: userNotes } : {} ); const newRecipe = res.recipe; state.currentMenu.recipes = state.currentMenu.recipes.map(r => r.id === recipeId ? newRecipe : r ); renderRecipeList(state.currentMenu.recipes); } catch (err) { showToast(err.message, 'error'); btn.disabled = false; btn.textContent = 'Swap'; } } async function generateMenu() { const btn = document.getElementById('btn-generate-menu'); const spinner = document.getElementById('menu-spinner'); btn.disabled = true; spinner.classList.remove('hidden'); try { const userNotes = document.getElementById('menu-user-notes').value.trim(); const res = await api('POST', '/menus/generate', userNotes ? { user_notes: userNotes } : {}); state.currentMenu = { ...res.menu_plan, recipes: res.recipes }; document.getElementById('menu-empty').classList.add('hidden'); document.getElementById('btn-clear-menu').classList.remove('hidden'); if (res.notes) { document.getElementById('menu-notes').textContent = res.notes; document.getElementById('menu-notes').classList.remove('hidden'); } renderRecipeList(res.recipes); showToast('Menu generated!'); } catch (err) { if (err.message.includes('503')) { showToast('Ollama is offline or not responding', 'error'); } else { showToast(err.message, 'error'); } } finally { btn.disabled = false; spinner.classList.add('hidden'); } } async function clearMenu() { try { await api('DELETE', '/menus/current', null); state.currentMenu = null; document.getElementById('recipe-list').innerHTML = ''; document.getElementById('menu-empty').classList.remove('hidden'); document.getElementById('menu-notes').classList.add('hidden'); document.getElementById('btn-clear-menu').classList.add('hidden'); showToast('Menu cleared'); } catch (err) { showToast(err.message, 'error'); } } async function makeThisMeal(mealName, mealType) { try { const now = new Date(); await api('POST', '/meals', { meal_name: mealName, meal_type: mealType, eaten_at: now.toISOString(), notes: null, servings: 1, }); showToast(`Logged: ${mealName}`); } catch (err) { showToast(err.message, 'error'); } } // ── Grocery Tab ──────────────────────────────────────────────────────────── async function loadGrocery() { try { const res = await api('GET', '/grocery/current').catch(err => { if (err.message.includes('404')) return null; throw err; }); if (!res) { document.getElementById('grocery-sections').innerHTML = ''; document.getElementById('grocery-total').classList.add('hidden'); document.getElementById('grocery-empty').classList.remove('hidden'); document.getElementById('btn-mark-purchased').classList.add('hidden'); document.getElementById('btn-clear-grocery').classList.add('hidden'); return; } state.currentGrocery = res; state.currentGroceryId = res.id; document.getElementById('grocery-empty').classList.add('hidden'); document.getElementById('btn-clear-grocery').classList.remove('hidden'); if (res.notes) { document.getElementById('grocery-notes').textContent = res.notes; document.getElementById('grocery-notes').classList.remove('hidden'); } else { document.getElementById('grocery-notes').classList.add('hidden'); } if (!res.is_purchased) { document.getElementById('btn-mark-purchased').classList.remove('hidden'); } else { document.getElementById('btn-mark-purchased').classList.add('hidden'); } renderGroceryList(); } catch (err) { showToast(err.message, 'error'); } } function renderGroceryList() { const sectionsDiv = document.getElementById('grocery-sections'); const totalDiv = document.getElementById('grocery-total'); if (!state.currentGrocery) return; let items = state.currentGrocery.items; if (typeof items === 'string') { try { items = JSON.parse(items); } catch { items = []; } } if (!Array.isArray(items)) items = []; // Group by store_section const grouped = {}; items.forEach(item => { const section = item.store_section || 'Other'; if (!grouped[section]) grouped[section] = []; grouped[section].push(item); }); // Render sections let html = ''; Object.keys(grouped).sort().forEach(section => { const titleCased = section.charAt(0).toUpperCase() + section.slice(1); html += `
${titleCased}
${grouped[section].map(item => { const usedIn = item.used_in ? item.used_in.join(', ') : ''; return `
${item.name}
${item.quantity} ${item.unit} ${item.estimated_cost ? ` - $${item.estimated_cost.toFixed(2)}` : ''} ${usedIn ? ` - Used in: ${usedIn}` : ''}
`; }).join('')}
`; }); sectionsDiv.innerHTML = html; // Show total const total = items.reduce((sum, item) => sum + (item.estimated_cost || 0), 0); if (total > 0) { totalDiv.innerHTML = `
Estimated Total
$${total.toFixed(2)}
`; totalDiv.classList.remove('hidden'); } } async function checkGroceryItem(checkbox) { if (!checkbox.checked) return; checkbox.disabled = true; try { await api('POST', '/pantry', { name: checkbox.dataset.name, quantity: parseFloat(checkbox.dataset.quantity) || 1, unit: checkbox.dataset.unit, category: checkbox.dataset.category || null, }); checkbox.closest('.grocery-item').classList.add('grocery-item-checked'); } catch (err) { showToast('Failed to add to pantry: ' + err.message, 'error'); checkbox.checked = false; checkbox.disabled = false; } } async function generateGrocery() { const btn = document.getElementById('btn-generate-grocery'); const spinner = document.getElementById('grocery-spinner'); btn.disabled = true; spinner.classList.remove('hidden'); try { const userNotes = document.getElementById('grocery-user-notes').value.trim(); const res = await api('POST', '/grocery/generate', userNotes ? { user_notes: userNotes } : {}); state.currentGrocery = res; state.currentGroceryId = res.grocery_list.id; document.getElementById('grocery-empty').classList.add('hidden'); document.getElementById('btn-mark-purchased').classList.remove('hidden'); document.getElementById('btn-clear-grocery').classList.remove('hidden'); if (res.shopping_notes) { document.getElementById('grocery-notes').textContent = res.shopping_notes; document.getElementById('grocery-notes').classList.remove('hidden'); } renderGroceryList(); showToast('Grocery list generated!'); } catch (err) { if (err.message.includes('503')) { showToast('Ollama is offline or not responding', 'error'); } else { showToast(err.message, 'error'); } } finally { btn.disabled = false; spinner.classList.add('hidden'); } } async function clearGrocery() { try { await api('DELETE', '/grocery/current', null); state.currentGrocery = null; state.currentGroceryId = null; document.getElementById('grocery-sections').innerHTML = ''; document.getElementById('grocery-total').classList.add('hidden'); document.getElementById('grocery-empty').classList.remove('hidden'); document.getElementById('grocery-notes').classList.add('hidden'); document.getElementById('btn-mark-purchased').classList.add('hidden'); document.getElementById('btn-clear-grocery').classList.add('hidden'); showToast('Grocery list cleared'); } catch (err) { showToast(err.message, 'error'); } } async function markPurchased() { try { await api('PUT', `/grocery/${state.currentGroceryId}/purchased`, {}); await loadPantry(); await loadGrocery(); showToast('Pantry updated!'); } catch (err) { showToast(err.message, 'error'); } } // ── Chat Tab ─────────────────────────────────────────────────────────────── function loadChat() { renderChat(); } function renderChat() { const messagesDiv = document.getElementById('chat-messages'); if (state.chatHistory.length === 0) { messagesDiv.innerHTML = '
Hi! I\'m Commis, your personal chef assistant. Ask me anything about your pantry, recipes, or meal planning.
'; return; } messagesDiv.innerHTML = state.chatHistory.map(msg => `
${msg.content.replace(/\n/g, '
')}
`).join(''); messagesDiv.scrollTop = messagesDiv.scrollHeight; } async function sendChatMessage() { const input = document.getElementById('chat-input'); const btn = document.getElementById('btn-send-chat'); const message = input.value.trim(); if (!message) return; input.value = ''; input.disabled = true; btn.disabled = true; state.chatHistory.push({ role: 'user', content: message }); renderChat(); // Show typing indicator const messagesDiv = document.getElementById('chat-messages'); const typingEl = document.createElement('div'); typingEl.className = 'chat-message chat-message-assistant'; typingEl.id = 'chat-typing'; typingEl.innerHTML = '
...
'; messagesDiv.appendChild(typingEl); messagesDiv.scrollTop = messagesDiv.scrollHeight; try { const res = await api('POST', '/ai/chat', { message, history: state.chatHistory.slice(-20).slice(0, -1), }); document.getElementById('chat-typing')?.remove(); state.chatHistory.push({ role: 'assistant', content: res.response }); renderChat(); } catch (err) { document.getElementById('chat-typing')?.remove(); state.chatHistory.pop(); showToast(err.message, 'error'); input.value = message; renderChat(); } finally { input.disabled = false; btn.disabled = false; input.focus(); } } // ── Initialization ───────────────────────────────────────────────────────── async function init() { // Check Ollama status try { const health = await api('GET', '/health'); const badge = document.getElementById('ollama-status'); if (health.ollama === 'connected') { badge.textContent = `✓ ${health.model}`; badge.className = 'status-badge status-ok'; } else { badge.textContent = '✗ Ollama offline'; badge.className = 'status-badge status-error'; } state.ollamaStatus = health.ollama; } catch { document.getElementById('ollama-status').textContent = '✗ Ollama offline'; document.getElementById('ollama-status').className = 'status-badge status-error'; } // Set up tab switching document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => switchTab(btn.dataset.tab)); }); // Set up pantry form document.getElementById('btn-add-ingredient').addEventListener('click', () => { state.editingIngredientId = null; document.getElementById('ing-name').value = ''; document.getElementById('ing-qty').value = ''; document.getElementById('ing-unit').value = ''; document.getElementById('ing-category').value = ''; document.getElementById('ing-expiry').value = ''; document.getElementById('btn-save-ingredient').textContent = 'Save'; document.getElementById('add-ingredient-form').classList.remove('hidden'); }); document.getElementById('btn-cancel-ingredient').addEventListener('click', () => { document.getElementById('add-ingredient-form').classList.add('hidden'); }); document.getElementById('btn-save-ingredient').addEventListener('click', async () => { if (state.editingIngredientId) { const ing = state.pantry.find(i => i.id === state.editingIngredientId); if (!ing) return; const name = document.getElementById('ing-name').value.trim(); const qty = parseFloat(document.getElementById('ing-qty').value); const unit = document.getElementById('ing-unit').value.trim(); const category = document.getElementById('ing-category').value; const expiry = document.getElementById('ing-expiry').value; if (!name) { showToast('Please enter ingredient name', 'error'); return; } try { await api('PUT', `/pantry/${state.editingIngredientId}`, { name, quantity: qty || 0, unit, category: category || null, expiry_date: expiry || null, }); document.getElementById('add-ingredient-form').classList.add('hidden'); await loadPantry(); showToast('Ingredient updated!'); } catch (err) { showToast(err.message, 'error'); } } else { addIngredient(); } }); // Set up meals form document.getElementById('btn-log-meal').addEventListener('click', () => { const now = new Date(); document.getElementById('meal-datetime').value = now.toISOString().slice(0, 16); document.getElementById('meal-name').value = ''; document.getElementById('meal-type').value = 'dinner'; document.getElementById('meal-notes').value = ''; document.getElementById('meal-servings').value = '1'; document.getElementById('log-meal-form').classList.remove('hidden'); }); document.getElementById('btn-cancel-meal').addEventListener('click', () => { document.getElementById('log-meal-form').classList.add('hidden'); }); document.getElementById('btn-save-meal').addEventListener('click', logMeal); // Set up menu form document.getElementById('btn-generate-menu').addEventListener('click', generateMenu); document.getElementById('btn-clear-menu').addEventListener('click', clearMenu); // Set up grocery form document.getElementById('btn-generate-grocery').addEventListener('click', generateGrocery); document.getElementById('btn-clear-grocery').addEventListener('click', clearGrocery); document.getElementById('btn-mark-purchased').addEventListener('click', markPurchased); // Set up chat document.getElementById('btn-send-chat').addEventListener('click', sendChatMessage); document.getElementById('chat-input').addEventListener('keydown', e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendChatMessage(); } }); // Load initial tab await loadPantry(); } document.addEventListener('DOMContentLoaded', init);