// ── 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;
}
// ── Pantry Category Auto-Detection ──────────────────────────────────────
function autoCategory(name) {
const n = name.toLowerCase();
if (/\b(apple|banana|berry|berries|broccoli|carrot|celery|corn|cucumber|garlic|grape|greens|kale|lemon|lettuce|lime|mango|mushroom|onion|orange|pepper|potato|spinach|tomato|zucchini|avocado|cabbage|cauliflower|beet|pea|herb|basil|cilantro|parsley|thyme|rosemary|ginger|scallion|leek|squash|radish|arugula|asparagus|artichoke|eggplant)\b/.test(n)) return 'produce';
if (/\b(milk|cheese|butter|cream|yogurt|egg|eggs|cheddar|mozzarella|parmesan|ricotta|sour cream|half.and.half|heavy cream|kefir)\b/.test(n)) return 'dairy';
if (/\b(chicken|beef|pork|turkey|fish|salmon|tuna|shrimp|lamb|bacon|sausage|ham|steak|ground|tofu|tempeh|lentil|bean|beans|chickpea)\b/.test(n)) return 'protein';
if (/\b(bread|bun|baguette|tortilla|roll|bagel|muffin|croissant|pita)\b/.test(n)) return 'bakery';
if (/\b(ice cream|frozen|edamame)\b/.test(n)) return 'frozen';
return 'pantry';
}
// ── 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();
} 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 => {
return `
| ${ing.name} |
${ing.category || '—'} |
|
`;
}).join('');
}
async function addIngredient() {
const name = document.getElementById('ing-name').value.trim();
if (!name) {
showToast('Please enter ingredient name', 'error');
return;
}
try {
const category = autoCategory(name);
await api('POST', '/pantry', {
name,
quantity: 1,
category,
});
document.getElementById('add-ingredient-form').classList.add('hidden');
document.getElementById('ing-name').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('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}
`;
}).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 `
${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(', ') : '';
const isChecked = item.checked ? 'checked' : '';
const checkedClass = item.checked ? 'grocery-item-checked' : '';
return `
`;
}).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) {
const checked = checkbox.checked;
const row = checkbox.closest('.grocery-item');
if (checked) {
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,
});
row.classList.add('grocery-item-checked');
} catch (err) {
showToast('Failed to add to pantry: ' + err.message, 'error');
checkbox.checked = false;
checkbox.disabled = false;
return;
} finally {
checkbox.disabled = false;
}
} else {
row.classList.remove('grocery-item-checked');
}
api('PATCH', `/grocery/${state.currentGroceryId}/check-item`, {
name: checkbox.dataset.name,
checked,
}).catch(err => console.error('Failed to update check state:', err));
}
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('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();
if (!name) {
showToast('Please enter ingredient name', 'error');
return;
}
try {
const category = autoCategory(name);
await api('PUT', `/pantry/${state.editingIngredientId}`, {
name,
category,
});
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);