import sys import os sys.path.append(os.path.dirname(os.path.abspath(__file__))) import streamlit as st import json import re from prompt_builder import build_prompt from model_api import query_model from diet_builder import build_diet_prompt from auth import generate_otp, create_jwt, verify_jwt, hash_password, verify_password from database import init_database, user_exists, register_user, get_user_by_email from email_utils import send_otp_email from dotenv import load_dotenv load_dotenv() init_database() st.set_page_config(page_title="FitPlan AI", layout="wide", page_icon="⚡") # --- MASTER UI STYLING --- st.markdown("""
2026
Personalised — fitness — Plan
Intelligence · Performance · Results
""", unsafe_allow_html=True) # --- UTILITY FUNCTIONS --- def parse_plan_to_json(text, is_diet=False): sections = [] pattern = r'(Day \d+:.*)' if not is_diet else r'(Meal \d+:.*|Breakfast:|Lunch:|Dinner:|Snack:)' parts = re.split(pattern, text) if len(parts) > 1: for i in range(1, len(parts), 2): header = parts[i].strip() content = parts[i+1].strip() items = [] for line in content.split('\n'): if line.strip().startswith(('-', '*')): main_part = line.strip('- *').split('|')[0].split(':')[0].strip() items.append({ "name": main_part, "val1": "3 sets" if not is_diet else "1 bowl", "val2": "10 reps" if not is_diet else "300 kcal", "val3": "60s" if not is_diet else "Protein" }) sections.append({"header": header, "items": items}) return sections def parse_diet_plan(text): """Parse dietary plan to extract days and meals with time, calories, and dishes.""" days_data = [] # Split by Day pattern day_pattern = r'Day (\d+)' day_matches = list(re.finditer(day_pattern, text)) if not day_matches: return days_data for idx, day_match in enumerate(day_matches): day_num = int(day_match.group(1)) # Get content until next day or end of text content_start = day_match.end() content_end = day_matches[idx + 1].start() if idx + 1 < len(day_matches) else len(text) day_content = text[content_start:content_end].strip() meals = [] meal_names = ['Breakfast', 'Lunch', 'Dinner', 'Snack'] # Split content by newlines and process each line for line in day_content.split('\n'): line = line.strip() if not line: continue # Check if line contains a meal name meal_name = None for name in meal_names: if name.lower() in line.lower(): meal_name = name break if meal_name: # Remove meal name from the start meal_content = re.sub(rf'^{meal_name}[:\-\s]*', '', line, flags=re.IGNORECASE).strip() # Extract time from parentheses or standalone time pattern time = "--:--" time_match = re.search(r'\(?(\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?)\)?', meal_content) if time_match: time = time_match.group(1).strip() # Extract calories - look for pattern: comma, space, number (with or without kcal) # Pattern like: "Something, 500" or "Something, 500 kcal" calories = "-- kcal" cal_match = re.search(r',\s*(\d+)\s*(?:kcal|cal|calories)?', meal_content, re.IGNORECASE) if cal_match: cal_value = cal_match.group(1) calories = f"{cal_value} kcal" # Extract dish name - remove everything except the main dish description dish = meal_content # Remove time patterns dish = re.sub(r'\(?(\d{1,2}:\d{2}\s*(?:AM|PM|am|pm)?)\)?', '', dish) # Remove calories (with or without kcal keyword) dish = re.sub(r',\s*\d+\s*(?:kcal|cal|calories)?', '', dish, flags=re.IGNORECASE) # Remove "Meal Name:" prefix if it exists dish = re.sub(r'Meal\s+Name\s*:\s*', '', dish, flags=re.IGNORECASE) # Remove extra punctuation and spaces dish = re.sub(r'[\-\|]', '', dish) dish = ' '.join(dish.split()) dish = dish.strip() # Remove trailing comma or special chars dish = dish.rstrip(',-:') # Limit to first meaningful words if len(dish) > 30: words = [w for w in dish.split() if len(w) > 1] dish = ' '.join(words[:3]) if dish and dish.lower() not in ['', 'none', 'n/a']: meals.append({ "meal": meal_name, "time": time, "calories": calories, "dish": dish }) if meals: days_data.append({ "day": day_num, "meals": meals }) return days_data[:5] # Limit to 5 days def render_diet_cards(diet_data): """Render dietary plan as cards with Day, Time, Calories, and Dish.""" for day_info in diet_data: day_num = day_info["day"] st.markdown(f"""
◆ Day {day_num}
""", unsafe_allow_html=True) for meal in day_info["meals"]: st.markdown(f"""
{meal['meal']}
{meal['time']}TIME
{meal['calories']}CALORIES
{meal['dish']}DISH
""", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) def render_cards(data, label1, label2, label3): for section in data: # Check if this is a rest day is_rest_day = 'rest day' in section['header'].lower() or (len(section['items']) == 1 and 'rest' in section['items'][0]['name'].lower()) if is_rest_day: st.markdown(f"""
🛋️ {section['header']}
Take it easy today! Focus on recovery, light stretching, or active rest activities like walking.
""", unsafe_allow_html=True) else: st.markdown(f"""
◆ {section['header']}
""", unsafe_allow_html=True) for item in section['items']: st.markdown(f"""
{item['name']}
{item['val1']}{label1}
{item['val2']}{label2}
{item['val3']}{label3}
""", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # --- SESSION STATE --- for key, val in [("otp", None), ("authenticated", False), ("token", None), ("page", "dashboard"), ("user_data", None), ("workout_plan", None), ("diet_plan", None), ("edit_mode", False), ("temp_signup_name", None), ("temp_signup_email", None), ("temp_signup_password", None)]: if key not in st.session_state: st.session_state[key] = val # ══════════════════════════════════════════ # PHASE 1: LOGIN # ══════════════════════════════════════════ if not st.session_state.authenticated: st.markdown("""
⚡ Intelligence-Powered Fitness ⚡
""", unsafe_allow_html=True) col_l, col_m, col_r = st.columns([1, 2, 1]) with col_m: # Toggle between Sign In and Sign Up tab1, tab2 = st.tabs(["SIGN IN", "SIGN UP"]) # ═══════════════════════════════════════ # SIGN IN TAB # ═══════════════════════════════════════ with tab1: st.markdown("
", unsafe_allow_html=True) signin_email = st.text_input("Email Address", placeholder="your@email.com", key="signin_email", label_visibility="collapsed") signin_password = st.text_input("Password", placeholder="Enter your password", type="password", key="signin_password", label_visibility="collapsed") if st.button("🔓 Sign In →", use_container_width=True, key="signin_btn"): if signin_email and signin_password: user = get_user_by_email(signin_email) if user and verify_password(signin_password, user["password_hash"]): st.session_state.token = create_jwt(user["email"], user["name"]) st.session_state.authenticated = True st.success("✓ Welcome back!") st.rerun() else: st.error("✗ Invalid email or password") else: st.error("Please enter both email and password") # ═══════════════════════════════════════ # SIGN UP TAB # ═══════════════════════════════════════ with tab2: st.markdown("
", unsafe_allow_html=True) signup_name = st.text_input("Full Name", placeholder="Your full name", key="signup_name", label_visibility="collapsed") signup_email = st.text_input("Email Address", placeholder="your@email.com", key="signup_email", label_visibility="collapsed") signup_password = st.text_input("Password", placeholder="Create a password", type="password", key="signup_password", label_visibility="collapsed") if st.button("📧 Send OTP →", use_container_width=True, key="otp_btn"): if signup_name and signup_email and signup_password: if user_exists(signup_email): st.error("✗ Email already registered") else: otp = generate_otp() st.session_state.otp = otp st.session_state.temp_signup_name = signup_name st.session_state.temp_signup_email = signup_email st.session_state.temp_signup_password = signup_password try: send_otp_email(signup_email, otp) st.success("✓ OTP sent to your email") except Exception: st.error("✗ Email delivery failed") else: st.error("Please fill all fields") if st.session_state.otp and st.session_state.temp_signup_email: st.markdown("
", unsafe_allow_html=True) otp_in = st.text_input("OTP Code", placeholder="6-digit code", type="password", key="otp_input", label_visibility="collapsed") if st.button("✅ Complete Sign Up →", use_container_width=True, key="verify_btn"): if str(otp_in).strip() == str(st.session_state.otp).strip(): # Register the user password_hash = hash_password(st.session_state.temp_signup_password) if register_user(st.session_state.temp_signup_name, st.session_state.temp_signup_email, password_hash): st.session_state.token = create_jwt(st.session_state.temp_signup_email, st.session_state.temp_signup_name) st.session_state.authenticated = True # Clear temp data st.session_state.otp = None st.session_state.temp_signup_name = None st.session_state.temp_signup_email = None st.session_state.temp_signup_password = None st.success("✓ Account created successfully!") st.rerun() else: st.error("✗ Registration failed") else: st.error("✗ Invalid OTP code") # ══════════════════════════════════════════ # PHASE 2: MAIN APP # ══════════════════════════════════════════ else: decoded = verify_jwt(st.session_state.token) if not decoded: st.session_state.authenticated = False st.error("Session expired. Please log in again.") st.rerun() # ── SIDEBAR ── with st.sidebar: st.markdown(f""" """, unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) if st.button("◈ Dashboard", use_container_width=True): st.session_state.page = "dashboard"; st.rerun() if st.button("◈ Workout Plan", use_container_width=True): st.session_state.page = "result" if st.session_state.workout_plan else "input"; st.rerun() if st.button("◈ Dietary Plan", use_container_width=True): st.session_state.page = "diet"; st.rerun() st.markdown("
", unsafe_allow_html=True) if st.button("✏️ Edit Profile", use_container_width=True): st.session_state.page = "input" st.session_state.edit_mode = True st.rerun() if st.button("⏻ Logout", use_container_width=True): st.session_state.authenticated = False st.session_state.token = None st.session_state.otp = None; st.rerun() # ══ DASHBOARD ══ if st.session_state.page == "dashboard": st.markdown("""
Personalized
Fitness
Planner
— powered by ai · built for athletes
""", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) if st.session_state.user_data: d = st.session_state.user_data c1, c2, c3, c4 = st.columns(4) with c1: st.markdown(f'
{d["name"][:6]}
Athlete
', unsafe_allow_html=True) with c2: st.markdown(f'
{d["bmi"]:.1f}
BMI Index
', unsafe_allow_html=True) with c3: st.markdown(f'
{d["status"]}
Status
', unsafe_allow_html=True) with c4: st.markdown(f'
{d["goal"]}
', unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) st.markdown(f"""
Active Plan

Your personalised AI fitness program is live. Navigate using the sidebar to view your Workout Plan or Dietary Plan.

""", unsafe_allow_html=True) else: st.markdown("""
No Data Yet

Generate a workout plan to see your stats

""", unsafe_allow_html=True) # ══ INPUT ══ elif st.session_state.page == "input": if st.session_state.edit_mode: st.markdown("
Edit Your Profile
", unsafe_allow_html=True) st.markdown("
— update your information
", unsafe_allow_html=True) else: st.markdown("
Build Your Profile
", unsafe_allow_html=True) st.markdown("
— tell us about yourself
", unsafe_allow_html=True) # Get existing data for editing existing_data = st.session_state.user_data if st.session_state.edit_mode and st.session_state.user_data else {} with st.container(): c1, c2, c3 = st.columns([2, 1, 1]) with c1: name = st.text_input("Full Name", value=existing_data.get("name", "")) with c2: gender = st.selectbox("Gender", ["Male", "Female", "Other"], index=["Male", "Female", "Other"].index(existing_data.get("gender", "Male")) if existing_data.get("gender") in ["Male", "Female", "Other"] else 0) with c3: age = st.number_input("Age", min_value=1, value=int(existing_data.get("age", 19))) h, w = st.columns(2) with h: height = st.number_input("Height (cm)", value=float(existing_data.get("height", 170.0))) with w: weight = st.number_input("Weight (kg)", value=float(existing_data.get("weight", 70.0))) goal = st.selectbox("Training Goal", ["Build Muscle", "Weight Loss", "Strength", "Flexibility"], index=["Build Muscle", "Weight Loss", "Strength", "Flexibility"].index(existing_data.get("goal", "Build Muscle")) if existing_data.get("goal") in ["Build Muscle", "Weight Loss", "Strength", "Flexibility"] else 0) level = st.selectbox("Fitness Level", ["Beginner", "Intermediate", "Advanced"], index=["Beginner", "Intermediate", "Advanced"].index(existing_data.get("level", "Beginner")) if existing_data.get("level") in ["Beginner", "Intermediate", "Advanced"] else 0) equip = st.multiselect("Available Equipment", ["Dumbbells", "Kettlebells", "Pull-up Bar", "Resistance Bands", "Yoga Mat", "No Equipment"], default=existing_data.get("equip", [])) st.markdown("
", unsafe_allow_html=True) button_text = "Update Profile →" if st.session_state.edit_mode else "Generate Complete Plan →" if st.button(button_text): prompt, bmi, status, color = build_prompt(name, gender, age, height, weight, goal, level, equip) prompt += "\nFormat: Day X: [Name]\n- [Exercise]: [Sets], [Reps], [Rest]" with st.spinner("AI is engineering your plan..."): st.session_state.workout_plan = query_model(prompt) st.session_state.user_data = {"name": name, "gender": gender, "age": age, "height": height, "weight": weight, "bmi": bmi, "status": status, "color": color, "goal": goal, "level": level, "equip": equip} st.session_state.page = "result" st.session_state.edit_mode = False # Reset edit mode st.rerun() # ══ RESULT ══ elif st.session_state.page == "result": d = st.session_state.user_data st.markdown(f"""

◆ {d['name']}'s Program

BMI {d['bmi']:.2f}  ·  {d['status']}  ·  {d['goal']}

""", unsafe_allow_html=True) st.download_button("↓ Export Workout Plan", st.session_state.workout_plan, file_name="workout_plan.txt") st.markdown("
", unsafe_allow_html=True) plan_json = parse_plan_to_json(st.session_state.workout_plan) if plan_json: render_cards(plan_json, "SETS", "REPS", "REST") else: st.markdown(f"""
{st.session_state.workout_plan}
                    
""", unsafe_allow_html=True) # ══ DIET ══ elif st.session_state.page == "diet": st.markdown("
Nutrition Protocol
", unsafe_allow_html=True) st.markdown("
— fuel your performance
", unsafe_allow_html=True) if st.session_state.user_data: d = st.session_state.user_data st.markdown(f"""

◆ {d['name']}'s Nutrition Plan

BMI {d['bmi']:.2f}  ·  {d['status']}  ·  {d['goal']}

""", unsafe_allow_html=True) if not st.session_state.diet_plan: d_prompt = build_diet_prompt(d["name"], "User", 20, 170, 70, d["goal"]) with st.spinner("Formulating your nutrition plan..."): st.session_state.diet_plan = query_model(d_prompt) st.download_button("↓ Export Diet Plan", st.session_state.diet_plan, file_name="diet_plan.txt") st.markdown("
", unsafe_allow_html=True) diet_data = parse_diet_plan(st.session_state.diet_plan) if diet_data and len(diet_data[0]["meals"]) > 0: render_diet_cards(diet_data) else: st.markdown(f"""
{st.session_state.diet_plan}
                    
""", unsafe_allow_html=True) else: st.markdown("""
No Profile

Generate a workout plan first

""", unsafe_allow_html=True)