diff --git a/src/lib/components/ProfileCard.svelte b/src/lib/components/ProfileCard.svelte index e69de29..a8f4723 100644 --- a/src/lib/components/ProfileCard.svelte +++ b/src/lib/components/ProfileCard.svelte @@ -0,0 +1,18 @@ + diff --git a/src/lib/components/WorkoutPlanCard.svelte b/src/lib/components/WorkoutPlanCard.svelte new file mode 100644 index 0000000..3191289 --- /dev/null +++ b/src/lib/components/WorkoutPlanCard.svelte @@ -0,0 +1,124 @@ + + + + +
+
+ + {workoutPlan.plan_name} + + +
+ + {capitalize(workoutPlan.fitness_level)} Level + + + + {workoutPlan.days_per_week} days/week + +
+
+
+
+ +
+
+
+ + +
+
+ + General Guidelines +
+

+ {workoutPlan.general_guidelines} +

+
+ + + + + {#each workoutPlan.weekly_schedule as day} + + +
+ {day.day} + {day.focus} +
+
+ +
+
+ {#each day.exercises as exercise} +
+
+

{exercise.name}

+ {exercise.load} +
+
+
+ Sets: + {exercise.sets} +
+
+ Reps: + {exercise.reps} +
+
+ Rest: + {exercise.rest} +
+
+ {#if exercise.tips} +

💡 {exercise.tips}

+ {/if} +
+ {/each} +
+
+ Cooldown: + {day.cooldown} +
+
+
+
+ {/each} +
+
+
diff --git a/src/routes/(app)/(components)/chat-agent.svelte b/src/routes/(app)/(components)/chat-agent.svelte index 6835b68..76599ba 100644 --- a/src/routes/(app)/(components)/chat-agent.svelte +++ b/src/routes/(app)/(components)/chat-agent.svelte @@ -5,6 +5,8 @@ import { Input } from '$lib/components/ui/input'; import { Badge } from '$lib/components/ui/badge'; import * as Alert from '$lib/components/ui/alert'; + import { Button } from '$lib/components/ui/button'; + import { Avatar, AvatarFallback } from '$lib/components/ui/avatar'; import { Avatar, AvatarFallback } from '$lib/components/ui/avatar'; import { Button } from '$lib/components/ui/button'; import { ScrollArea } from '$lib/components/ui/scroll-area'; @@ -13,6 +15,8 @@ type Message = { id: string; content: string; + timestamp: Date; + sender: 'user' | 'bot'; sender: 'user' | 'bot'; timestamp: Date; }; @@ -50,6 +54,31 @@ let isOpen = $state(false); let isMinimized = $state(false); + let isCheckingOnboarding = $state(true); + let hasCompletedOnboarding = $state(false); + let userOnboardingData = $state(null); + type WorkoutPlan = { + fitness_level: string; + plan_name: string; + general_guidelines: string; + weekly_schedule: { + day: string; + focus: string; + exercises: { + name: string; + sets: number; + reps: string; + load: string; + rest: string; + tips: string; + }[]; + cooldown: string; + }[]; + days_per_week: number; + }; + + let workoutplanData = $state(null); + $inspect(workoutplanData); let hasCompletedOnboarding = $state(false); let isCheckingOnboarding = $state(true); let userOnboardingData = $state(null); @@ -104,6 +133,11 @@ function renderMessage(message: Message) { if (message.content.includes('Please confirm your fitness profile:') && userOnboardingData) { return ` +
+

${message.content}

+ ${ProfileCard(formatOnboardingDataToProfile(userOnboardingData))} +
+ `;

${message.content}

${ProfileCard(formatOnboardingDataToProfile(userOnboardingData))} @@ -113,6 +147,16 @@ try { const startIndex = message.content.indexOf('{'); const endIndex = message.content.lastIndexOf('}') + 1; + + // Extract the JSON string from the message + const workoutPlanJson = message.content.slice(startIndex, endIndex); + + // Parse the JSON to get the workoutPlan object + const workoutPlanData = JSON.parse(workoutPlanJson); + workoutplanData = workoutPlanData; + + // Return just the intro text, the component will be rendered separately + return `

Here's your personalized workout plan based on your profile:

`; const workoutPlanJson = message.content.slice(startIndex, endIndex); const workoutPlan = JSON.parse(workoutPlanJson); @@ -127,6 +171,149 @@ return message.content; } } + return message.content; + } + + function WorkoutPlan({ workoutPlan }: { workoutPlan: WorkoutPlan }) { + function capitalize(str: string) { + return str + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + } + + return ` +
+
+
+
+

${workoutPlan.plan_name}

+
+ + ${capitalize(workoutPlan.fitness_level)} Level + + + + + + + + + ${workoutPlan.days_per_week} days/week + +
+
+
+ + + + +
+
+
+ +
+
+
+ + + + + General Guidelines +
+

${workoutPlan.general_guidelines}

+
+ +
+ +
+ ${workoutPlan.weekly_schedule + .map( + (day, dayIndex) => ` +
+ + +
+ ` + ) + .join('')} +
+
+
+ `; return `

${message.content}

`; } @@ -297,6 +484,51 @@ } async function getBotResponse(message: string): Promise { + try { + const userData = { + fitnessLevel: userOnboardingData?.fitnessLevel || 'intermediate', + fitnessGoal: userOnboardingData?.fitnessGoal || 'muscle gain', + age: userOnboardingData?.age || 32, + gender: userOnboardingData?.gender || 'male', + weight: { + value: userOnboardingData?.weight || 75, + unit: 'kg' + }, + height: { + value: userOnboardingData?.height || 180, + unit: 'cm' + }, + daysPerWeek: userOnboardingData?.daysPerWeek || 4, + availableEquipment: userOnboardingData?.availableEquipment || [ + 'dumbbells', + 'barbell', + 'bench', + 'pull-up bar', + 'resistance bands' + ] + }; + + // Call the workout plan API + const response = await fetch('/api/workout-plan', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(userData) + }); + + if (!response.ok) { + throw new Error('Failed to generate workout plan'); + } + + const data = await response.json(); + + return `Here's your personalized workout plan based on your fitness profile: + ${WorkoutPlan(data)}`; + } catch (error) { + console.error('Error generating workout plan:', error); + return 'I apologize, but I encountered an error while creating your workout plan. Would you like to try again?'; + } const lowerMessage = message.toLowerCase(); // Handle workout plan request @@ -456,6 +688,7 @@ Would you like me to explain any part of this workout plan in more detail?`; {#if isOpen && !isMinimized}
@@ -486,6 +719,99 @@ Would you like me to explain any part of this workout plan in more detail?`;
+
+
+
+ {#if isCheckingOnboarding} +
+
+ +

Loading your profile...

+
+
+ {:else if !hasCompletedOnboarding} +
+ + + Onboarding Required + + To get personalized fitness assistance, we need some information about you and + your fitness goals. + + + +
+ {:else} +
+ {#each messages as message (message.id)} +
+ {#if message.sender === 'bot'} + + + AI + + + {/if} + +
+ {#if message.sender === 'bot'} + {@html renderMessage(message)} + {:else} +

{message.content}

+ {/if} + + {formatTime(message.timestamp)} + +
+ + {#if message.sender === 'user'} + + + You + + + {/if} +
+ {/each} + + {#if isLoading} +
+ + + AI + + +
+
+ + + +
+
+
+ {/if} +
+ {/if} +
+
+
{#if isCheckingOnboarding} @@ -612,3 +938,28 @@ Would you like me to explain any part of this workout plan in more detail?`;
{/if} + + \ No newline at end of file diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index c273f2a..7037a90 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -1,4 +1,5 @@ stream?.getTracks().forEach((t) => t.stop())} /> +
+ + + + + Nutrition Label Scanner + + + Point your camera at a nutrition label to extract nutritional information + + + + + {#if !cameraPermissionGranted} +
+ +
+

Camera Access Required

+

+ Allow camera access to scan nutrition labels +

+
+ +
+ {:else} +
+ {#if loading} +
+
+
+

Processing label...

+
+
+ {/if} + + + + +
+
+
+ Center the nutrition label +
+
+
+
+ +
+ + + +
+ + {#if error} +
+ + {error} +
+ {/if} + {/if} +
+
+
+ + + +
+ +
+ + + Scanned Nutrition Info + +
+ + +
+
+
+ +
+
+
+

Nutritional Information

+ Per 100g +
+ +
+
+ {parsedNutrition.calories || '0'} + CALORIES +
+
+ {parsedNutrition.protein || '0'}g + PROTEIN +
+
+ {parsedNutrition.carbs || '0'}g + CARBS +
+
+ {parsedNutrition.fat || '0'}g + FAT +
+
+
+
+
+
+
Scan Nutrition Label @@ -277,4 +505,4 @@ - + \ No newline at end of file diff --git a/src/routes/(app)/onboarding/+page.svelte b/src/routes/(app)/onboarding/+page.svelte index 3345605..a152157 100644 --- a/src/routes/(app)/onboarding/+page.svelte +++ b/src/routes/(app)/onboarding/+page.svelte @@ -156,7 +156,6 @@ throw new Error('Network response was not ok'); } const data = await response.text(); - console.log(data); isSuccess = true; toast.success('Your fitness profile has been created!'); } catch (error) { diff --git a/src/routes/api/gemini/+server.ts b/src/routes/api/gemini/+server.ts deleted file mode 100644 index 6fc76b2..0000000 --- a/src/routes/api/gemini/+server.ts +++ /dev/null @@ -1 +0,0 @@ -export async function GET() {} diff --git a/src/routes/api/macros/+server.ts b/src/routes/api/macros/+server.ts index 5d4e54a..78a4aaf 100644 --- a/src/routes/api/macros/+server.ts +++ b/src/routes/api/macros/+server.ts @@ -35,7 +35,6 @@ export async function POST({ request }) { const { macros_data } = await request.json(); - console.log(macros_data); try { await prisma.macros.create({ data: { diff --git a/src/routes/api/onboarding/+server.ts b/src/routes/api/onboarding/+server.ts index 4f7c073..8d73afd 100644 --- a/src/routes/api/onboarding/+server.ts +++ b/src/routes/api/onboarding/+server.ts @@ -31,6 +31,4 @@ export async function POST({ request }) { } catch (e) { return new Response('Error saving onboarding data', { status: 500 }); } - - console.log(onboarding); } diff --git a/src/routes/api/workout-plan/+server.ts b/src/routes/api/workout-plan/+server.ts index caa94ca..b11bac9 100644 --- a/src/routes/api/workout-plan/+server.ts +++ b/src/routes/api/workout-plan/+server.ts @@ -1,10 +1,10 @@ +import { GoogleGenAI } from '@google/genai'; import { GEMINI_API_KEY } from '$env/static/private'; import { createWorkoutPlanFunctionDeclaration } from '$lib/toolspecs/fitness-planner'; -import { GoogleGenAI } from '@google/genai'; export async function POST({ request }) { const userData = await request.json(); - + const config = { tools: [ { @@ -13,46 +13,45 @@ export async function POST({ request }) { ] }; - // Configure the client const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY }); - // Define a simple user prompt const contents = [ { role: 'user', parts: [ - { - text: `Create a workout routine that fits my profile: - - I'm a ${userData.fitness_level || 'intermediate'} level fitness enthusiast, ${userData.age || 35} years old, - ${userData.height || 175}cm tall, weighing ${userData.weight || 75}kg. + { + text: userData.message ? userData.message : `Create a workout routine that fits my profile: + + I'm a ${userData.fitness_level || 'intermediate'} level fitness enthusiast, ${userData.age || 35} years old, + ${userData.height || 175}cm tall, weighing ${userData.weight || 75}kg. I can commit to working out ${userData.days_per_week || 4} days per week. ${userData.injuries?.length ? `I have the following injuries to consider: ${userData.injuries.join(', ')}.` : ''} My main goals are ${userData.goals?.join(', ') || 'overall fitness and weight loss'}. - - Please create a complete, structured workout plan for me.` + + Please create a complete, structured workout plan for me.`, } ] } ]; - // Send request with function declarations const response = await ai.models.generateContent({ - model: 'gemini-2.0-flash', + model: 'gemini-2.5-pro-exp-03-25', contents: contents, config: config }); - // Get the structured workout plan from the function call const workoutPlan = response.functionCalls?.at(0)?.args; - return new Response(JSON.stringify({ - workoutPlan, - fullResponse: response - }), { - status: 200, - headers: { - 'Content-Type': 'application/json' + return new Response( + JSON.stringify({ + workoutPlan, + fullResponse: response + }), + { + status: 200, + headers: { + 'Content-Type': 'application/json' + } } - }); + ); }