import { GeminiProvider } from '@/add-log/food-detection/providers/GeminiProvider'; import { formatDateKey } from '@/common/utils/format'; import type { FoodLog, FoodRecommendation, UserProfile } from '@/types/foodlog'; import type { NutrientKey, Nutrition } from '@/types/nutrition'; import { strings } from '@/common/strings'; import { BASE_DAILY_TARGETS } from '@/common/constants/nutrition'; type NutrientGap = { key: NutrientKey; label: string; consumed: number; target: number; fillPercent: number; weight: number; priority: number; }; const SCORED_NUTRIENTS: { key: NutrientKey; label: string; weight: number }[] = [ // Level 1 - MUST { key: 'protein_g', label: 'Protein', weight: 12 }, { key: 'carbs_g', label: 'Carbs', weight: 6 }, { key: 'fat_g', label: 'Fat', weight: 6 }, { key: 'fiber_g', label: 'Fiber', weight: 10 }, { key: 'soluble_fiber_g', label: 'Soluble Fiber', weight: 4 }, { key: 'insoluble_fiber_g', label: 'Insoluble Fiber', weight: 4 }, { key: 'a_mcg', label: 'Vitamin A', weight: 6 }, { key: 'retinol_mcg', label: 'Retinol', weight: 3 }, { key: 'b1_mg', label: 'Thiamin (B1)', weight: 4 }, { key: 'b2_mg', label: 'Riboflavin (B2)', weight: 4 }, { key: 'b3_mg', label: 'Niacin (B3)', weight: 4 }, { key: 'b5_mg', label: 'Pantothenic Acid (B5)', weight: 4 }, { key: 'b6_mg', label: 'Vitamin B6', weight: 5 }, { key: 'b9_mcg', label: 'Folate (B9)', weight: 6 }, { key: 'b12_mcg', label: 'Vitamin B12', weight: 7 }, { key: 'c_mg', label: 'Vitamin C', weight: 6 }, { key: 'e_mg', label: 'Vitamin E', weight: 5 }, { key: 'k_mcg', label: 'Vitamin K', weight: 5 }, { key: 'calcium_mg', label: 'Calcium', weight: 7 }, { key: 'iron_mg', label: 'Iron', weight: 7 }, { key: 'magnesium_mg', label: 'Magnesium', weight: 7 }, { key: 'zinc_mg', label: 'Zinc', weight: 6 }, { key: 'potassium_mg', label: 'Potassium', weight: 6 }, { key: 'phosphorus_mg', label: 'Phosphorus', weight: 4 }, { key: 'selenium_mcg', label: 'Selenium', weight: 5 }, { key: 'chromium_mcg', label: 'Chromium', weight: 3 }, { key: 'molybdenum_mcg', label: 'Molybdenum', weight: 3 }, { key: 'iodine_mcg', label: 'Iodine', weight: 5 }, { key: 'omega_3_mg', label: 'Omega-3', weight: 10 }, { key: 'ala_mg', label: 'ALA', weight: 4 }, { key: 'epa_mg', label: 'EPA', weight: 6 }, { key: 'dha_mg', label: 'DHA', weight: 6 }, { key: 'biotin_mcg', label: 'Biotin', weight: 4 }, { key: 'probiotics_cfu', label: 'Probiotics', weight: 5 }, { key: 'copper_mg', label: 'Copper', weight: 4 }, { key: 'manganese_mg', label: 'Manganese', weight: 4 }, { key: 'choline_mg', label: 'Choline', weight: 6 }, { key: 'usa_gov_dga_protein_g', label: 'Protein Foods', weight: 6 }, { key: 'usa_gov_dga_healthy_fats_tsp_eq', label: 'Healthy Fats', weight: 4 }, { key: 'usa_gov_dga_vegetables_cup_eq', label: 'Vegetables', weight: 8 }, { key: 'usa_gov_dga_fruits_cup_eq', label: 'Fruits', weight: 6 }, { key: 'usa_gov_dga_dairy_cup_eq', label: 'Dairy', weight: 5 }, { key: 'usa_gov_dga_whole_grains_oz_eq', label: 'Whole Grains', weight: 5 }, { key: 'usa_gov_dga_dark_green_veg_cup_eq', label: 'Dark Green Vegetables', weight: 5 }, { key: 'usa_gov_dga_red_orange_veg_cup_eq', label: 'Red/Orange Vegetables', weight: 4 }, { key: 'usa_gov_dga_beans_peas_lentils_veg_cup_eq', label: 'Legumes', weight: 5 }, { key: 'usa_gov_dga_starchy_veg_cup_eq', label: 'Starchy Vegetables', weight: 3 }, { key: 'usa_gov_dga_other_veg_cup_eq', label: 'Other Vegetables', weight: 3 }, { key: 'omega_6_mg', label: 'Omega-6', weight: 3 }, { key: 'lycopene_mcg', label: 'Lycopene', weight: 4 }, { key: 'lutein_zeaxanthin_mcg', label: 'Lutein/Zeaxanthin', weight: 4 }, { key: 'beta_carotene_mcg', label: 'Beta Carotene', weight: 4 }, { key: 'tryptophan_mg', label: 'Tryptophan', weight: 3 }, { key: 'tyrosine_mg', label: 'Tyrosine', weight: 3 }, { key: 'alpha_carotene_mcg', label: 'Alpha Carotene', weight: 2 }, { key: 'cryptoxanthin_mcg', label: 'Cryptoxanthin', weight: 2 }, { key: 'anthocyanins_mg', label: 'Anthocyanins', weight: 3 }, { key: 'monounsaturated_fat_g', label: 'Monounsaturated Fat', weight: 3 }, { key: 'polyunsaturated_fat_g', label: 'Polyunsaturated Fat', weight: 3 }, { key: 'phytosterols_mg', label: 'Phytosterols', weight: 2 }, { key: 'dpa_mg', label: 'DPA', weight: 3 }, { key: 'valine_g', label: 'Valine', weight: 3 }, { key: 'glutamine_g', label: 'Glutamine', weight: 3 }, { key: 'betaine_mg', label: 'Betaine', weight: 2 }, // Level 2 - CANNOT AS FOOD (supplement-only) { key: 'vitamin_d_mcg', label: 'Vitamin D', weight: 8 }, { key: 'collagen_g', label: 'Collagen', weight: 3 }, { key: 'coq10_mg', label: 'CoQ10', weight: 3 }, { key: 'curcumin_mg', label: 'Curcumin', weight: 3 }, { key: 'creatine_g', label: 'Creatine', weight: 3 }, { key: 'resveratrol_mg', label: 'Resveratrol', weight: 2 }, { key: 'astaxanthin_mcg', label: 'Astaxanthin', weight: 2 }, // Level 3 - BONUS { key: 'water_ml', label: 'Water', weight: 2 }, { key: 'caffeine_mg', label: 'Caffeine', weight: 1 }, { key: 'theobromine_mg', label: 'Theobromine', weight: 1 }, { key: 'taurine_mg', label: 'Taurine', weight: 2 }, ]; export const DAYS_TO_REVIEW = 3; function getRecentDates(today: Date, days: number): string[] { return Array.from({ length: days }, (_, i) => { const d = new Date(today); d.setDate(d.getDate() - i); return formatDateKey(d); }); } function extractRecentIngredients(logs: FoodLog[], dates: string[]): string[] { const ingredients = new Set(); for (const foodLog of logs) { if (!dates.includes(foodLog.date)) continue; if (foodLog.processingStatus !== 'completed') continue; for (const ing of foodLog.ingredients || []) { ingredients.add(ing.toLowerCase()); } } return Array.from(ingredients); } function computeRecentNutrition(logs: FoodLog[], dates: string[]): Nutrition { const consumed: Nutrition = {}; for (const foodLog of logs) { if (!dates.includes(foodLog.date) || foodLog.processingStatus !== 'completed') continue; if (!foodLog.nutrition) continue; for (const [k, v] of Object.entries(foodLog.nutrition)) { const key = k as NutrientKey; consumed[key] = (consumed[key] ?? 0) + (v ?? 0); } } return consumed; } function identifyNutritionGaps(consumed: Nutrition, days: number): NutrientGap[] { const gaps: NutrientGap[] = []; for (const { key, label, weight } of SCORED_NUTRIENTS) { const c = consumed[key] ?? 0; const t = (BASE_DAILY_TARGETS[key] ?? 0) * days; if (t <= 0) continue; const fillPercent = Math.min((c / t) * 100, 100); const priority = weight * (1 - fillPercent / 100); gaps.push({ key, label, consumed: c, target: t, fillPercent, weight, priority }); } return gaps.sort((a, b) => b.priority - a.priority); } function buildPrompt( gaps: NutrientGap[], recentIngredients: string[], profile: UserProfile | null, days: number ): string { const topGaps = gaps.filter(g => g.fillPercent < 80).slice(0, 8); const gapLines = topGaps .map( (g, i) => `${i + 1}. ${g.label}: ${Math.round(g.consumed)} / ${Math.round(g.target)} (${Math.round(g.fillPercent)}%)` ) .join('\n'); const restrictions = profile?.food_restrictions?.length ? profile.food_restrictions.join(', ') : 'none'; const preferences = profile?.food_preferences?.length ? profile.food_preferences.join(', ') : 'none'; const conditions = profile?.health_conditions?.length ? profile.health_conditions.join(', ') : 'none'; const recentList = recentIngredients.length > 0 ? recentIngredients.slice(0, 20).join(', ') : 'none'; return strings.ai.recommendIngredients({ days, gapLines, recentIngredients: recentList, restrictions, preferences, conditions, }); } type RecommendationResult = { ingredients: string[]; foods: FoodRecommendation[]; }; function parseResponse(response: string): RecommendationResult { try { const cleaned = response.replace(/```json\n?|\n?```/g, '').trim(); const parsed = JSON.parse(cleaned); const ingredients = Array.isArray(parsed.ingredients) ? parsed.ingredients : []; const foods: FoodRecommendation[] = []; if (Array.isArray(parsed.foods)) { for (const f of parsed.foods) { if (f.name && Array.isArray(f.ingredients)) { foods.push({ recommendedFoodName: f.name, ingredients: f.ingredients }); } } } return { ingredients, foods }; } catch { return { ingredients: [], foods: [] }; } } export async function generateRecommendations( logs: FoodLog[], today: Date, profile: UserProfile | null, days: number = DAYS_TO_REVIEW ): Promise { const dates = getRecentDates(today, days); const recentIngredients = extractRecentIngredients(logs, dates); const consumed = computeRecentNutrition(logs, dates); const gaps = identifyNutritionGaps(consumed, days); if (gaps.every(g => g.fillPercent >= 80)) { return { ingredients: [], foods: [] }; } const prompt = buildPrompt(gaps, recentIngredients, profile, days); const provider = new GeminiProvider(); try { const response = await provider.query(prompt); return parseResponse(response); } catch (error) { console.error('Recommendation generation failed:', error); return { ingredients: [], foods: [] }; } } export { computeRecentNutrition, identifyNutritionGaps };