diff --git a/package-lock.json b/package-lock.json index fd23906..2c9a5c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "antigravity-app", "version": "0.1.0", "dependencies": { + "@google/generative-ai": "^0.24.1", "clsx": "^2.1.1", "framer-motion": "^12.35.0", "lucide-react": "^0.577.0", @@ -459,6 +460,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/package.json b/package.json index 538a7f9..6da6bba 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "@google/generative-ai": "^0.24.1", "clsx": "^2.1.1", "framer-motion": "^12.35.0", "lucide-react": "^0.577.0", diff --git a/src/app/api/analyze-meal/route.ts b/src/app/api/analyze-meal/route.ts new file mode 100644 index 0000000..f3bc3b5 --- /dev/null +++ b/src/app/api/analyze-meal/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { GoogleGenerativeAI } from '@google/generative-ai'; + +export async function POST(req: NextRequest) { + try { + const { mealDescription } = await req.json(); + + if (!mealDescription || typeof mealDescription !== 'string' || !mealDescription.trim()) { + return NextResponse.json( + { success: false, error: 'Please provide a valid meal description.' }, + { status: 400 } + ); + } + + const apiKey = process.env.GEMINI_API_KEY; + if (!apiKey) { + return NextResponse.json( + { + success: false, + error: 'GEMINI_API_KEY is not configured in .env.local on the server.', + }, + { status: 500 } + ); + } + + const genAI = new GoogleGenerativeAI(apiKey); + + // Use gemini-1.5-flash with structured JSON response + const model = genAI.getGenerativeModel({ + model: 'gemini-1.5-flash', + generationConfig: { + responseMimeType: 'application/json', + temperature: 0.2, + }, + }); + + const prompt = `You are an expert clinical sports nutritionist and dietitian specializing in evidence-based macronutrient tracking. +Analyze the following meal description and accurately estimate the macronutrients and total energy intake. + +Meal description: +"${mealDescription.trim()}" + +Return ONLY a valid JSON object matching this schema: +{ + "mealName": "Short descriptive title of the meal", + "calories": number (integer total kcal), + "protein": number (integer grams of protein), + "carbs": number (integer grams of carbohydrates), + "fats": number (integer grams of total fat), + "items": [ + { + "name": "Food item name and portion", + "calories": number, + "protein": number, + "carbs": number, + "fats": number + } + ], + "insight": "One concise sentence on the nutritional value or timing (e.g., 'Excellent post-workout ratio providing >3g leucine for MPS stimulus.')" +}`; + + const result = await model.generateContent(prompt); + const responseText = result.response.text(); + const data = JSON.parse(responseText); + + return NextResponse.json({ + success: true, + data: { + mealName: data.mealName || 'Logged Meal', + calories: Math.round(Number(data.calories) || 0), + protein: Math.round(Number(data.protein) || 0), + carbs: Math.round(Number(data.carbs) || 0), + fats: Math.round(Number(data.fats) || 0), + items: Array.isArray(data.items) ? data.items : [], + insight: data.insight || 'Meal analyzed successfully.', + }, + }); + } catch (error: unknown) { + console.error('Error analyzing meal with Gemini:', error); + const message = error instanceof Error ? error.message : 'Failed to analyze meal'; + return NextResponse.json( + { success: false, error: message }, + { status: 500 } + ); + } +} diff --git a/src/components/modules/FitnessView.tsx b/src/components/modules/FitnessView.tsx index c1aaa6e..757e59e 100644 --- a/src/components/modules/FitnessView.tsx +++ b/src/components/modules/FitnessView.tsx @@ -19,7 +19,9 @@ import { TrendingUp, Target, Utensils, - Check + Check, + Loader2, + Bot } from 'lucide-react'; import { WorkoutSession, DailyNutrition, MuscleRecovery, WorkoutExercise } from '@/lib/types'; import { SCIENCE_TIPS, getFormattedDate } from '@/lib/initialData'; @@ -95,6 +97,68 @@ export const FitnessView: React.FC = ({ setTimeout(() => setIsSynced(false), 2500); }; + // AI Smart Macro Logger State + const [mealInput, setMealInput] = useState(''); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [aiError, setAiError] = useState(null); + const [lastAnalyzedMeal, setLastAnalyzedMeal] = useState<{ + mealName: string; + calories: number; + protein: number; + carbs: number; + fats: number; + items?: { name: string; calories: number; protein: number; carbs: number; fats: number }[]; + insight?: string; + } | null>(null); + + const sampleMealChips = [ + '3 boiled eggs, 2 rotis with dal, 1 scoop whey', + '200g grilled chicken, 1 cup brown rice & broccoli', + '1 scoop whey isolate with 250ml milk & 1 banana', + '1 cup rolled oats, 1 tbsp peanut butter & Greek yogurt', + 'Paneer bhurji (150g) with 2 multigrain rotis' + ]; + + const handleAnalyzeMeal = async (customPrompt?: string) => { + const promptToUse = customPrompt || mealInput; + if (!promptToUse.trim() || isAnalyzing) return; + + setIsAnalyzing(true); + setAiError(null); + + try { + const res = await fetch('/api/analyze-meal', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mealDescription: promptToUse.trim() }), + }); + + const json = await res.json(); + if (!res.ok || !json.success) { + throw new Error(json.error || 'Failed to analyze meal.'); + } + + const meal = json.data; + setLastAnalyzedMeal(meal); + + // Auto-fill calculated macros directly into the macro tracker! + onUpdateNutrition({ + calories: Math.max(0, nutrition.calories + meal.calories), + protein: Math.max(0, nutrition.protein + meal.protein), + carbs: Math.max(0, nutrition.carbs + meal.carbs), + fats: Math.max(0, nutrition.fats + meal.fats), + }); + + setMealInput(''); + playNotificationChime('streak'); + } catch (err: unknown) { + console.error('AI meal analyze error:', err); + setAiError(err instanceof Error ? err.message : 'Error analyzing meal. Check API key or network.'); + } finally { + setIsAnalyzing(false); + } + }; + // Log Workout Form State const [workoutName, setWorkoutName] = useState('Push Hypertrophy'); const [workoutDate, setWorkoutDate] = useState(getFormattedDate(0)); @@ -244,6 +308,168 @@ export const FitnessView: React.FC = ({ Bodyweight: {nutrition.bodyweightKg} kg + {/* AI SMART MEAL INTAKE CARD */} +
+
+
+ + {/* Card Header */} +
+
+
+ +
+
+

+ AI Smart Meal Intake Logger + + Gemini AI + +

+

+ Describe what you ate in natural language—AI calculates the macros and updates your tracker automatically. +

+
+
+
+ + {/* Meal Input and Submit Action */} +
{ + e.preventDefault(); + handleAnalyzeMeal(); + }} + className="space-y-3 relative z-10" + > +
+ setMealInput(e.target.value)} + disabled={isAnalyzing} + placeholder="e.g. 3 boiled eggs, 2 rotis with dal, 1 scoop whey protein" + className="flex-1 px-4 py-3 rounded-xl bg-slate-950/80 border border-slate-800 text-sm text-slate-100 placeholder-slate-500 focus:outline-none focus:border-lime-500 transition-colors disabled:opacity-50" + /> + +
+ + {/* Sample Prompt Chips */} +
+ + + Quick ideas: + + {sampleMealChips.map((chip, idx) => ( + + ))} +
+
+ + {/* Error Banner */} + {aiError && ( +
+ ⚠️ {aiError} + +
+ )} + + {/* Last Analyzed Meal Results Card */} + {lastAnalyzedMeal && ( +
+
+
+ + + {lastAnalyzedMeal.mealName} + +
+ + ✓ Auto-filled to Today's Intake + +
+ + {/* Added Macro Badges */} +
+
+ Calories + +{lastAnalyzedMeal.calories} + kcal +
+
+ Protein + +{lastAnalyzedMeal.protein}g + Leucine rich +
+
+ Carbohydrates + +{lastAnalyzedMeal.carbs}g + Glycogen fuel +
+
+ Healthy Fats + +{lastAnalyzedMeal.fats}g + Hormone balance +
+
+ + {/* Items Breakdown & Scientific Insight */} + {lastAnalyzedMeal.insight && ( +

+ 💡 {lastAnalyzedMeal.insight} +

+ )} + + {lastAnalyzedMeal.items && lastAnalyzedMeal.items.length > 0 && ( +
+ + Itemized Portion Estimates + +
+ {lastAnalyzedMeal.items.map((it, i) => ( +
+ {it.name} + {it.calories} kcal ({it.protein}g P) +
+ ))} +
+
+ )} +
+ )} +
+
{/* Calories Card */}