# Project export: FormFriend

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: FormFriend: Your AI workout ally. Lift smarter, not harder.
- Devpost: https://devpost.com/software/formfriend-vbw2et
- GitHub: https://github.com/julialaforet/form-friend-fitness-ai
- Demo: https://form-friend-fitness-ai.lovable.app/
- Video: https://www.youtube.com/embed/Lk8hsyS9zC8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 5 GitHub contributor(s) — kaden tang (14 commits), julialaforet (13 commits), Meera-14 (8 commits), gpt-engineer-app[bot] (7 commits), kbudhapu (3 commits)

## Devpost submission (written by the team)

### Inspiration

As a team of STEM interns, we spend a lot of time sitting at our desks and understand how crucial it is to stay physically active. We were all interested in fitness for its incredible health benefits, but we quickly ran into the common barriers: the high cost of personal trainers, the risk of injury from improper form, and the feeling of self-consciousness that can come with going to a gym. We asked ourselves: Could we use AI to democratize personal fitness, making expert guidance accessible and affordable? That question was the inspiration for FormFriend. We wanted to build a tool that could give anyone the confidence to start their fitness journey safely and effectively, right from the comfort of their own home.

### What it does

FormFriend acts as a comprehensive, AI-powered personal trainer that guides users from initial setup to a complete, form-corrected workout. It seamlessly integrates three core features to create a supportive and effective fitness experience. 1. Personalized Workout Planning The user journey begins with a simple, intuitive onboarding process where they input their personal stats (like age and weight) and fitness goals (such as 'build strength' or 'weight loss'). Based on this data, our backend service leverages the Gemini API to generate a dynamic, tailored workout plan, providing a clear roadmap for the user's fitness journey and eliminating the guesswork of what exercises to do. 2. Interactive AI Coaching Once a plan is created, our conversational AI coach, "Jackie," greets the user with a personalized audio introduction using the Vapi API. This creates an engaging and supportive experience from the start. The conversational interface allows users to ask questions and receive on-demand guidance, making the experience feel closer to working with a real trainer. 3. Real-Time Form Correction This is the core feature of FormFriend. When a user starts their workout, the app uses their webcam and the TensorFlow.js MoveNet model to analyze their exercise form in real-time. It visually maps a skeleton onto the user's body, calculates the angles of their joints, and provides immediate, on-screen feedback to help them correct their posture and technique. This significantly reduces the risk of injury and ensures every repetition is effective, building a strong and safe foundation for their fitness.

### How we built it

FormFriend is built on a modern web stack designed for a rich, interactive user experience. 1. The User Foundation: We chose a robust frontend using React and TypeScript, powered by the Vite build tool for a fast and efficient development workflow. The user interface was crafted with a combination of shadcn/ui and styled-components for a clean, polished, and responsive design. 2. The AI Form Coach: The core of FormFriend is the real-time form analysis. We integrated TensorFlow.js with the lightweight MoveNet pose detection model. This allows the application to track 17 key body joints through the user's webcam. Our custom algorithms then calculate the angles between these joints in real-time to provide immediate, actionable feedback on exercises like squats and lunges. 3. The Conversational Trainer: To make the experience more personal and engaging, we implemented a conversational AI coach using Vapi. This voice agent, "Jackie," provides a personalized audio introduction based on the user's goals and is the foundation for future real-time, voice-based interactions. Personalized Plans: The workout plans are generated by a custom backend service built with Node.js and hosted on Render, which takes the user's stats and goals to create a tailored week-by-week fitness schedule.

### Challenges we ran into

A main challenge we faced was ensuring the TensorFlow pose detection ran smoothly without lagging the user's browser. We had to be mindful of our state updates and the frequency of our detection loop to balance accuracy with performance, creating a seamless user experience.

### Accomplishments we're proud of

Mastering Frontend Development from Scratch: As a team with no prior frontend experience, we are proud to have built a complete, responsive, and modern web application using React and TypeScript. We learned and applied concepts like component-based architecture, state management with React Hooks, and asynchronous data fetching. Integrating Complex AI in the Browser: We successfully implemented two advanced AI features. The core of our project is the real-time pose detection using TensorFlow.js, which required us to process a live video feed and perform calculations directly on the user's device. Implementing a Conversational Voice Agent: We went beyond a visual-only interface by integrating the Vapi API to create "Jackie," our AI audio coach. This taught us how to work with real-time, event-driven APIs and manage complex states like an active call. Resilient Problem-Solving: The journey was filled with challenges, particularly in debugging the APIs and ensuring all services worked together seamlessly. We are proud of the persistence and systematic approach we developed to diagnose and solve these complex issues, which has made us better developers.

### What we learned

This project was a deep dive into the practical application of machine learning in the browser. Implementing TensorFlow.js for real-time video processing taught us a great deal about performance optimization and managing complex state in a React application. Integrating a sophisticated voice agent like Vapi was another major learning experience. We learned how to handle event-driven communication, manage sensitive API keys securely using environment variables (.env.local), and debug complex, asynchronous API errors. This process reinforced the importance of meticulous documentation, reading, and systematically isolating variables to find a root cause. Beyond the code, we learned about user-centric design, thinking through the entire user journey from their initial hesitation about fitness to feeling empowered and confident in their workout.

### What's next

FormFriend is a powerful foundation, and we're excited about the many ways we can expand its capabilities to create an even more comprehensive and immersive fitness experience. Our roadmap is focused on making the AI coach smarter, more interactive, and more personalized. Here are the key features we plan to build next: Fully Interactive Real-Time Coaching: Our top priority is to evolve our AI coach, "Jackie," from an introduction-only agent to a fully interactive workout companion. By feeding the real-time form feedback from TensorFlow.js into our Vapi voice agent, Jackie will be able to provide spoken encouragement and corrections throughout the entire workout, with phrases like, "Great depth on that squat!" or "Two more reps, you've got this!" Expanded Exercise Library: We plan to significantly increase the breadth of supported exercises. This includes adding more complex weightlifting movements (like cleans and overhead presses), popular bodyweight exercises, yoga poses, and even physical therapy movements to make FormFriend a versatile tool for a wider range of users and fitness goals. Automatic Rep and Set Counting: By analyzing the angle data from the user's movements over time, we will implement automatic rep and set counting. The app will not only know if your form is correct, but it will also track your progress through the workout without requiring any manual input from the user. Historical Progress Tracking: To help users see their improvements, we plan to add user accounts and a database to store workout history. Users will be able to look back at their past sessions, see charts of their strength gains over time, and stay motivated by tracking their long-term achievements.

## README (from the GitHub repository)

# Welcome to your Lovable project

## Project info

**URL**: https://lovable.dev/projects/bbce9bc6-b559-41b8-9679-e22371789655

## How can I edit this code?

There are several ways of editing your application.

**Use Lovable**

Simply visit the [Lovable Project](https://lovable.dev/projects/bbce9bc6-b559-41b8-9679-e22371789655) and start prompting.

Changes made via Lovable will be committed automatically to this repo.

**Use your preferred IDE**

If you want to work locally using your own IDE, you can clone this repo and push changes. Pushed changes will also be reflected in Lovable.

The only requirement is having Node.js & npm installed - [install with nvm](https://github.com/nvm-sh/nvm#installing-and-updating)

Follow these steps:

```sh
# Step 1: Clone the repository using the project's Git URL.
git clone <YOUR_GIT_URL>

# Step 2: Navigate to the project directory.
cd <YOUR_PROJECT_NAME>

# Step 3: Install the necessary dependencies.
npm i

# Step 4: Start the development server with auto-reloading and an instant preview.
npm run dev
```

**Edit a file directly in GitHub**

- Navigate to the desired file(s).
- Click the "Edit" button (pencil icon) at the top right of the file view.
- Make your changes and commit the changes.

**Use GitHub Codespaces**

- Navigate to the main page of your repository.
- Click on the "Code" button (green button) near the top right.
- Select the "Codespaces" tab.
- Click on "New codespace" to launch a new Codespace environment.
- Edit files directly within the Codespace and commit and push your changes once you're done.

## What technologies are used for this project?

This project is built with:

- Vite
- TypeScript
- React
- shadcn-ui
- Tailwind CSS

## How can I deploy this project?

Simply open [Lovable](https://lovable.dev/projects/bbce9bc6-b559-41b8-9679-e22371789655) and click on Share -> Publish.

## Can I connect a custom domain to my Lovable project?

Yes, you can!

To connect a domain, navigate to Project > Settings > Domains and click Connect Domain.

Read more here: [Setting up a custom domain](https://docs.lovable.dev/tips-tricks/custom-domain#step-by-step-guide)


## Detected evidence (automated analysis)

Indexed codebase: 72 recognized source files, 287 KB.
- CSS (language) — detected in the code
- Express (technology) — detected in the code
- Google Gemini (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Node.js (technology) — claimed on Devpost, not found in the code
- TensorFlow (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (82 of 82)

```
.env.local
.gitignore
backend/.env
backend/package.json
backend/server.js
components.json
eslint.config.js
index.html
package.json
postcss.config.js
public/robots.txt
README.md
src/App.css
src/App.tsx
src/components/AICoachDemo.tsx
src/components/GoalsForm.tsx
src/components/HealthStatsForm.tsx
src/components/Hero.tsx
src/components/ui/accordion.tsx
src/components/ui/alert-dialog.tsx
src/components/ui/alert.tsx
src/components/ui/aspect-ratio.tsx
src/components/ui/avatar.tsx
src/components/ui/badge.tsx
src/components/ui/breadcrumb.tsx
src/components/ui/button.tsx
src/components/ui/calendar.tsx
src/components/ui/card.tsx
src/components/ui/carousel.tsx
src/components/ui/chart.tsx
src/components/ui/checkbox.tsx
src/components/ui/collapsible.tsx
src/components/ui/command.tsx
src/components/ui/context-menu.tsx
src/components/ui/dialog.tsx
src/components/ui/drawer.tsx
src/components/ui/dropdown-menu.tsx
src/components/ui/form.tsx
src/components/ui/hover-card.tsx
src/components/ui/input-otp.tsx
src/components/ui/input.tsx
src/components/ui/label.tsx
src/components/ui/menubar.tsx
src/components/ui/navigation-menu.tsx
src/components/ui/pagination.tsx
src/components/ui/popover.tsx
src/components/ui/progress.tsx
src/components/ui/radio-group.tsx
src/components/ui/resizable.tsx
src/components/ui/scroll-area.tsx
src/components/ui/select.tsx
src/components/ui/separator.tsx
src/components/ui/sheet.tsx
src/components/ui/sidebar.tsx
src/components/ui/skeleton.tsx
src/components/ui/slider.tsx
src/components/ui/sonner.tsx
src/components/ui/switch.tsx
src/components/ui/table.tsx
src/components/ui/tabs.tsx
src/components/ui/textarea.tsx
src/components/ui/toast.tsx
src/components/ui/toaster.tsx
src/components/ui/toggle-group.tsx
src/components/ui/toggle.tsx
src/components/ui/tooltip.tsx
src/components/ui/use-toast.ts
src/components/WorkoutPlan.tsx
src/hooks/use-mobile.tsx
src/hooks/use-toast.ts
src/index.css
src/lib/utils.ts
src/main.tsx
src/pages/api/generate-workout.ts
src/pages/Index.tsx
src/pages/NotFound.tsx
src/vite-env.d.ts
tailwind.config.ts
tsconfig.app.json
tsconfig.json
tsconfig.node.json
vite.config.ts
```

### Dependencies

- backend/package.json: @google/generative-ai@^0.24.1, cors@^2.8.5, dotenv@^16.4.5, express@^4.19.2, node-fetch@^2.7.0
- package.json: @eslint/js@^9.9.0, @hookform/resolvers@^3.9.0, @mediapipe/camera_utils@^0.3.1675466862, @mediapipe/pose@^0.5.1675469404, @radix-ui/react-accordion@^1.2.0, @radix-ui/react-alert-dialog@^1.1.1, @radix-ui/react-aspect-ratio@^1.1.0, @radix-ui/react-avatar@^1.1.0, @radix-ui/react-checkbox@^1.1.1, @radix-ui/react-collapsible@^1.1.0, @radix-ui/react-context-menu@^2.2.1, @radix-ui/react-dialog@^1.1.2, @radix-ui/react-dropdown-menu@^2.1.1, @radix-ui/react-hover-card@^1.1.1, @radix-ui/react-label@^2.1.0, @radix-ui/react-menubar@^1.1.1, @radix-ui/react-navigation-menu@^1.2.0, @radix-ui/react-popover@^1.1.1, @radix-ui/react-progress@^1.1.0, @radix-ui/react-radio-group@^1.2.0, @radix-ui/react-scroll-area@^1.1.0, @radix-ui/react-select@^2.1.1, @radix-ui/react-separator@^1.1.0, @radix-ui/react-slider@^1.2.0, @radix-ui/react-slot@^1.1.0, @radix-ui/react-switch@^1.1.0, @radix-ui/react-tabs@^1.1.0, @radix-ui/react-toast@^1.2.1, @radix-ui/react-toggle@^1.1.0, @radix-ui/react-toggle-group@^1.1.0, @radix-ui/react-tooltip@^1.1.4, @tailwindcss/typography@^0.5.15, @tanstack/react-query@^5.56.2, @tensorflow-models/pose-detection@^2.1.3, @tensorflow/tfjs@^4.22.0, @types/node@^22.5.5, @types/react@^18.3.3, @types/react-dom@^18.3.0, @vapi-ai/web@^2.3.6, @vitejs/plugin-react-swc@^3.5.0, autoprefixer@^10.4.20, class-variance-authority@^0.7.1, clsx@^2.1.1, cmdk@^1.0.0, date-fns@^3.6.0, embla-carousel-react@^8.3.0, eslint@^9.9.0, eslint-plugin-react-hooks@^5.1.0-rc.0, eslint-plugin-react-refresh@^0.4.9, globals@^15.9.0, input-otp@^1.2.4, lovable-tagger@^1.1.7, lucide-react@^0.462.0, next-themes@^0.3.0, postcss@^8.4.47, react@^18.3.1, react-day-picker@^8.10.1, react-dom@^18.3.1, react-hook-form@^7.53.0, react-resizable-panels@^2.1.3, react-router-dom@^6.26.2, recharts@^2.12.7, sonner@^1.5.0, styled-components@^6.1.19, tailwind-merge@^2.5.2, tailwindcss@^3.4.11, tailwindcss-animate@^1.0.7, typescript@^5.5.3, typescript-eslint@^8.0.1, vaul@^0.9.3, vite@^5.4.1, zod@^3.23.8

### Recent commits (newest first)

- Changing button textt
- making things mandatory
- change button text
- new videos
- got the skeleton back
- Added buttons and routing to be able to go back and forth from AI to workout plan
- Edited UI
- added demo animations
- Merge branch 'main' of github.com:julialaforet/form-friend-fitness-ai
- added demo animations
- vapi improvements
- Merge remote-tracking branch 'refs/remotes/origin/main'
- vapi
- added demo animations
- changed labels
- make ui pop
- added exercises
- fixed situp analyzer
- added situp analyzer
- Merge branch 'main' of https://github.com/julialaforet/form-friend-fitness-ai

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "vite_react_shadcn_ts",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:dev": "vite build --mode development",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "@hookform/resolvers": "^3.9.0",
    "@mediapipe/camera_utils": "^0.3.1675466862",
    "@mediapipe/pose": "^0.5.1675469404",
    "@radix-ui/react-accordion": "^1.2.0",
    "@radix-ui/react-alert-dialog": "^1.1.1",
    "@radix-ui/react-aspect-ratio": "^1.1.0",
    "@radix-ui/react-avatar": "^1.1.0",
    "@radix-ui/react-checkbox": "^1.1.1",
    "@radix-ui/react-collapsible": "^1.1.0",
    "@radix-ui/react-context-menu": "^2.2.1",
    "@radix-ui/react-dialog": "^1.1.2",
    "@radix-ui/react-dropdown-menu": "^2.1.1",
    "@radix-ui/react-hover-card": "^1.1.1",
    "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-menubar": "^1.1.1",
    "@radix-ui/react-navigation-menu": "^1.2.0",
    "@radix-ui/react-popover": "^1.1.1",
    "@radix-ui/react-progress": "^1.1.0",
    "@radix-ui/react-radio-group": "^1.2.0",
    "@radix-ui/react-scroll-area": "^1.1.0",
    "@radix-ui/react-select": "^2.1.1",
    "@radix-ui/react-separator": "^1.1.0",
    "@radix-ui/react-slider": "^1.2.0",
    "@radix-ui/react-slot": "^1.1.0",
    "@radix-ui/react-switch": "^1.1.0",
    "@radix-ui/react-tabs": "^1.1.0",
    "@radix-ui/react-toast": "^1.2.1",
    "@radix-ui/react-toggle": "^1.1.0",
    "@radix-ui/react-toggle-group": "^1.1.0",
    "@radix-ui/react-tooltip": "^1.1.4",
    "@tanstack/react-query": "^5.56.2",
    "@tensorflow-models/pose-detection": "^2.1.3",
    "@tensorflow/tfjs": "^4.22.0",
    "@vapi-ai/web": "^2.3.6",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.0.0",
    "date-fns": "^3.6.0",
    "embla-carousel-react": "^8.3.0",
    "input-otp": "^1.2.4",
    "lucide-react": "^0.462.0",
    "next-themes": "^0.3.0",
    "react": "^18.3.1",
    "react-day-picker": "^8.10.1",
    "react-dom": "^18.3.1",
    "react-hook-form": "^7.53.0",
    "react-resizable-panels": "^2.1.3",
    "react-router-dom": "^6.26.2",
    "recharts": "^2.12.7",
    "sonner": "^1.5.0",
    "styled-components": "^6.1.19",
    "tailwind-merge": "^2.5.2",
    "tailwindcss-animate": "^1.0.7",
    "vaul": "^0.9.3",
    "zod": "^3.23.8"
  },
  "devDependencies": {
    "@eslint/js": "^9.9.0",
    "@tailwindcss/typography": "^0.5.15",
    "@types/node": "^22.5.5",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react-swc": "^3.5.0",
    "autoprefixer": "^10.4.20",
    "eslint": "^9.9.0",
    "eslint-plugin-react-hooks": "^5.1.0-rc.0",
    "eslint-plugin-react-refresh": "^0.4.9",
    "globals": "^15.9.0",
    "lovable-tagger": "^1.1.7",
    "postcss": "^8.4.47",
    "tailwindcss": "^3.4.11",
    "typescript": "^5.5.3",
    "typescript-eslint": "^8.0.1",
    "vite": "^5.4.1"
  }
}

```

### backend/package.json

```
{
  "name": "backend",
  "version": "1.0.0",
  "description": "Backend server for AI Fitness Coach",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "fitness",
    "AI",
    "workout-generator",
    "huggingface"
  ],
  "author": "Your Name/Team Name",
  "license": "MIT",
  "dependencies": {
    "@google/generative-ai": "^0.24.1",
    "cors": "^2.8.5",
    "dotenv": "^16.4.5",
    "express": "^4.19.2",
    "node-fetch": "^2.7.0"
  }
}

```

### src/main.tsx

```typescript
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById("root")!).render(<App />);

```

### src/App.tsx

```typescript
// App.tsx
import { useState } from 'react';
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route, useNavigate } from "react-router-dom";

// Import your page components
import Hero from "./components/Hero";
import HealthStatsForm from "./components/HealthStatsForm";
import GoalsForm from "./components/GoalsForm";
import WorkoutPlan from "./components/WorkoutPlan";
import AICoachDemo from "./components/AICoachDemo";
import NotFound from "./pages/NotFound";
// Ensure Index.tsx is either removed or repurposed if Hero is your new home page
// import Index from "./pages/Index"; // You might not need this anymore if Hero is '/'

const queryClient = new QueryClient();

// Define interfaces for data types (as before)
interface HealthStatsData {
    name?: string;
    age?: number;
    gender?: string;
    weight?: number;
    height?: number;
    medicalConditions?: string;
}

interface GoalsData {
    primaryGoal?: string;
    targetWeight?: number;
    workoutFrequency?: string;
    muscleGroups?: string[];
    timeframe?: string;
    fitnessLevel?: string;
    availableEquipment?: string;
}

const App = () => (
    <QueryClientProvider client={queryClient}>
        <TooltipProvider>
            <Toaster />
            <Sonner />
            <BrowserRouter>
                <AppRoutes /> {/* New component to hold routes and state */}
            </BrowserRouter>
        </TooltipProvider>
    </QueryClientProvider>
);

const AppRoutes = () => {
    const navigate = useNavigate();

    const [healthStats, setHealthStats] = useState<HealthStatsData>({});
    const [goals, setGoals] = useState<GoalsData>({});

    // This function is no longer needed since Hero handles navigation internally
    // const handleGetStarted = () => {
    //     navigate("/health-stats");
    // };

    const handleHealthStatsComplete = (stats: any) => {
        const processedStats: HealthStatsData = stats ? {
            name: stats.name || undefined,
            age: stats.age ? parseInt(stats.age) : undefined,
            gender: stats.gender || undefined,
            weight: stats.weight ? parseInt(stats.weight) : undefined,
            height: stats.height ? parseInt(stats.height) : undefined,
            medicalConditions: stats.medicalConditions || undefined,
        } : {};
        setHealthStats(processedStats);
        navigate("/goals");
    };

    const handleGoalsComplete = (userGoals: any) => {
        const processedGoals: GoalsData = userGoals ? {
            primaryGoal: userGoals.primaryGoal || undefined,
            targetWeight: userGoals.targetWeight ? parseInt(userGoals.targetWeight) : undefined,
            workoutFrequency: userGoals.workoutFrequency || undefined,
            muscleGroups: userGoals.muscleGroups || undefined,
            timeframe: userGoals.timeframe || undefined,
            fitnessLevel: userGoals.fitnessLevel || undefined,
            availableEquipment: userGoals.availableEquipment || undefined,
        } : {};
        setGoals(processedGoals);
        navigate("/workout-plan");
    };

    const handleStartWorkout = () => {
        navigate("/ai-coach");
    };

    const handleGoBackToPlan = () => {
        navigate("/workout-plan");
    };

    return (
        <Routes>
            {/* Hero Page - NO onGetStarted prop needed here anymore */}
            <Route path="/" element={<Hero />} /> {/* <-- FIX IS HERE */}

            {/* Health Stats Form Page */}
            <Route path="/health-stats" element={<HealthStatsForm onNext={handleHealthStatsComplete} />} />

            {/* Goals Form Page */}
            <Route path="/goals" element={<GoalsForm onComplete={handleGoalsComplete} />} />

            {/* Workout Plan Page - receives collected data */}
            <Route path="/workout-plan" element={<WorkoutPlan healthStats={healthStats} goals={goals} onStartWorkout={handleStartWorkout} />} />

            {/* AI Coach Demo Page - has button to go back to Workout Plan */}
            <Route path="/ai-coach" element={<AICoachDemo onGoBack={handleGoBackToPlan} />} />

            {/* Catch-all for 404 */}
            <Route path="*" element={<NotFound />} />
        </Routes>
    );
};

export default App;
```

### backend/server.js

```javascript
// server.js
// This file sets up a Node.js backend server using Express to interact with the Google Gemini API for generating personalized workout plans.
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import dotenv from 'dotenv';
dotenv.config();

// Import the Google Generative AI SDK
import { GoogleGenerativeAI } from '@google/generative-ai';

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware
app.use(bodyParser.json());
app.use(cors());

// Your Google Gemini API Key (replace OPENAI_API_KEY)
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;

if (!GEMINI_API_KEY) {
    console.error("Error: GEMINI_API_KEY environment variable not set. Please update your .env file.");
    process.exit(1);
}

// Initialize the Google Generative AI client
const genAI = new GoogleGenerativeAI(GEMINI_API_KEY);

// Define the system instruction (AI's persona and rules)
const SYSTEM_INSTRUCTION = "You are a highly experienced, knowledgeable, and motivating personal fitness trainer. Your primary goal is to create safe, effective, and personalized workout plans. You should provide clear instructions, emphasize proper form, and consider the user's goals, current fitness level, available equipment, time commitment, and any preferences or limitations (like injuries). Always respond with the workout plan strictly in JSON format as requested by the user, and nothing else. Ensure the JSON is valid and matches the specified structure.";

// --- Gemini API Call Utility Function ---
async function callGeminiChat(userPromptContent, model = "gemini-1.5-flash", temperature = 0.7, max_tokens = 1200) {
    try {
        const geminiModel = genAI.getGenerativeModel({
            model: model,
            // Pass the system instruction directly here for models that support it (like 1.5-flash)
            systemInstruction: SYSTEM_INSTRUCTION
        });

        // For single-turn requests, you can use generateContent.
        // For multi-turn conversations, you would use model.startChat and pass history.
        // For this use case (generating a plan based on a single comprehensive prompt), generateContent is fine.
        const result = await geminiModel.generateContent({
            contents: [{ role: "user", parts: [{ text: userPromptContent }] }],
            generationConfig: {
                temperature: temperature,
                maxOutputTokens: max_tokens,
                responseMimeType: "application/json" // Request JSON output explicitly
            }
        });

        const response = result.response;
        const text = response.text(); // This should be your JSON string
        return text;

    } catch (error) {
        console.error("Failed to call Google Gemini API:", error);
        if (error.response && error.response.status === 429) {
            throw new Error("Gemini API Rate Limit Exceeded. Please wait and try again.");
        }
        throw error;
    }
}

// --- Helper to build the prompt for Gemini (largely same as before) ---
function buildWorkoutPlanPrompt(healthStats, goals) {
    const { age, gender, weight, height, medicalConditions } = healthStats;
    const { primaryGoal, workoutFrequency, timeframe, fitnessLevel, availableEquipment } = goals;

    let prompt = `Generate a personalized workout plan in JSON format.
    The plan should be an array of objects, where each object represents a day.
    Each day object MUST have the following properties: 'day' (e.g., "Day 1"), 'focus' (e.g., "Upper Body Strength"), 'exercises' (an array of strings, e.g., ["Bench Press 3x8", "Overhead Press 3x10"]), 'duration' (e.g., "60 min"), and 'difficulty' (e.g., "Intermediate").
    Ensure the 'exercises' array contains specific exercises with suggested sets and reps.
    Include a warm-up and cool-down for each day within the exercise list, clearly labeled.
    
    Here are the user's details:
    - Age: ${age || 'N/A'}
    - Gender: ${gender || 'N/A'}
    - Weight: ${weight ? `${weight} kg` : 'N/A'}
    - Height: ${height ? `${height} cm` : 'N/A'}
    - Medical Conditions: ${medicalConditions || 'None'}
    
    User's Goals:
    - Primary Goal: ${primaryGoal || 'General Fitness'}
    - Workout Frequency: ${workoutFrequency || 'N/A'}
    - Timeframe (for goal): ${timeframe || 'N/A'}
    - Current Fitness Level: ${fitnessLevel || 'N/A'}
    - Available Equipment: ${availableEquipment || 'N/A'}
    
    Based on these details, create a plan for the specified workout frequency.
    Example JSON structure:
    [
      {
        "day": "Day 1",
        "focus": "Full Body Strength",
        "exercises": ["Warm-up: 5 min light cardio", "Squats 3x10", "Push-ups 3xAMRAP", "Bent-over Rows 3x10", "Plank 3x30s", "Cool-down: 5 min stretching"],
        "duration": "60 min",
        "difficulty": "Beginner"
      }
    ]
    `;

    return prompt;
}

// --- API Endpoints ---

app.get('/health', (req, res) => {
    res.json({ status: 'healthy', message: 'Node.js backend with Gemini API is running.' });
});

app.post('/generate-workout', async (req, res) => {
    const { healthStats, goals } = req.body;

    if (!healthStats || !goals) {
        return res.status(400).json({ message: "Missing healthStats or goals in request body." });
    }

    const userPrompt = buildWorkoutPlanPrompt(healthStats, goals);
    // Note: With Gemini, the system instruction is passed to the model initialization,
    // not directly in the messages array for single-turn content generation.

    try {
        const geminiResponse = await callGeminiChat(userPrompt);
        console.log("Raw Gemini Response:", geminiResponse);

        let workoutPlan;
        try {
            workoutPlan = JSON.parse(geminiResponse);
            // Basic validation for the expected array structure
            if (!Array.isArray(workoutPlan) || workoutPlan.some(item => !item.day || !item.focus || !Array.isArray(item.exercises))) {
                throw new Error("Parsed JSON
[truncated — 772 more characters]
```

### src/pages/Index.tsx

```typescript

import { useState } from "react";
import Hero from "@/components/Hero";
import HealthStatsForm from "@/components/HealthStatsForm";
import GoalsForm from "@/components/GoalsForm";
import WorkoutPlan from "@/components/WorkoutPlan";
import AICoachDemo from "@/components/AICoachDemo";

type Step = "hero" | "health-stats" | "goals" | "workout-plan" | "ai-coach";

const Index = () => {
  const [currentStep, setCurrentStep] = useState<Step>("hero");
  const [healthStats, setHealthStats] = useState(null);
  const [goals, setGoals] = useState(null);

  const handleGetStarted = () => {
    setCurrentStep("health-stats");
  };

  const handleHealthStatsNext = (stats: any) => {
    setHealthStats(stats);
    if (stats === null) {
      // User skipped health stats, also skip goals and go to workout plan
      setCurrentStep("workout-plan");
    } else {
      setCurrentStep("goals");
    }
  };

  const handleGoalsComplete = (goalData: any) => {
    setGoals(goalData);
    setCurrentStep("workout-plan");
  };

  const handleStartWorkout = () => {
    setCurrentStep("ai-coach");
  };

  const handleBackToWorkoutPlan = () => {
    setCurrentStep("workout-plan");
  };

  const renderCurrentStep = () => {
    switch (currentStep) {
      case "hero":
        return <Hero onGetStarted={handleGetStarted} />;
      case "health-stats":
        return <HealthStatsForm onNext={handleHealthStatsNext} />;
      case "goals":
        return <GoalsForm onComplete={handleGoalsComplete} />;
      case "workout-plan":
        return (
          <WorkoutPlan 
            healthStats={healthStats} 
            goals={goals} 
            onStartWorkout={handleStartWorkout}
          />
        );
      case "ai-coach":
        return <AICoachDemo onBack={handleBackToWorkoutPlan} />;
      default:
        return <Hero onGetStarted={handleGetStarted} />;
    }
  };

  return <div className="min-h-screen">{renderCurrentStep()}</div>;
};

export default Index;

```

### postcss.config.js

```javascript
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

```

### vite.config.ts

```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import path from "path";
import { componentTagger } from "lovable-tagger";

// https://vitejs.dev/config/
export default defineConfig(({ mode }) => ({
  server: {
    host: "::",
    port: 8080,
  },
  plugins: [
    react(),
    mode === 'development' &&
    componentTagger(),
  ].filter(Boolean),
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
}));

```

### eslint.config.js

```javascript
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
      "@typescript-eslint/no-unused-vars": "off",
    },
  }
);

```

### index.html

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>form-friend-fitness-ai</title>
    <meta name="description" content="Lovable Generated Project" />
    <meta name="author" content="Lovable" />

    <meta property="og:title" content="form-friend-fitness-ai" />
    <meta property="og:description" content="Lovable Generated Project" />
    <meta property="og:type" content="website" />
    <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />

    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@lovable_dev" />
    <meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
  </head>

  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

```

[62 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]