| import streamlit as st |
| from glob import glob |
| import pandas as pd |
| import utils |
| import plotly.graph_objects as go |
| from datetime import datetime |
| import numpy as np |
| from sentence_transformers import SentenceTransformer |
| import pyarrow.parquet as pq |
| import fastparquet |
|
|
| |
| |
| st.set_page_config( |
| page_title="Recipe Recommender", |
| page_icon="🍳", |
| layout="wide" |
| ) |
|
|
| with open("style.css") as f: |
| st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True) |
|
|
| |
| |
|
|
| if "data" not in st.session_state: |
| files = glob(r"data/*.parquet") |
| df = pd.read_parquet(files) |
| df["ingredients"] = ( |
| df["ingredients"] |
| .str.strip("[]") |
| .str.replace("'", "") |
| .str.replace('"', "") |
| .str.split(",") |
| .apply(lambda x: [y.strip() for y in x]) |
| ) |
| st.session_state["data"] = df |
|
|
| if "meal_history" not in st.session_state: |
| st.session_state["meal_history"] = { |
| "recipes": [], |
| "ratings": {}, |
| "last_cooked": {}, |
| "frequency": {}, |
| "preferences": { |
| "favorite_cuisines": set(), |
| "favorite_ingredients": set(), |
| "preferred_cooking_time": 0, |
| "preferred_meal_size": 0 |
| } |
| } |
|
|
| if "result" not in st.session_state: |
| st.session_state["result"] = None |
|
|
| if "use_history" not in st.session_state: |
| st.session_state["use_history"] = False |
|
|
| if "user_info" not in st.session_state: |
| st.session_state["user_info"] = None |
|
|
| if "has_diabetes" not in st.session_state: |
| st.session_state["has_diabetes"] = False |
|
|
| |
| |
|
|
| st.image(r"images/logo-no-background.png", width=400) |
|
|
| |
| st.header("User Information") |
| col1, col2 = st.columns(2) |
|
|
| with col1: |
| gender = st.selectbox("Gender", ["Male", "Female"]) |
| age = st.number_input("Age", min_value=1, max_value=120, value=30) |
| weight = st.number_input("Weight (kg)", min_value=30.0, max_value=300.0, value=70.0, step=0.1) |
| height = st.number_input("Height (cm)", min_value=100.0, max_value=250.0, value=170.0, step=0.1) |
| activity_level = st.selectbox( |
| "Activity Level", |
| ["Sedentary", "Lightly Active", "Moderately Active", "Very Active", "Extra Active"], |
| help="Sedentary: Little or no exercise\nLightly Active: Light exercise 1-3 days/week\nModerately Active: Moderate exercise 3-5 days/week\nVery Active: Hard exercise 6-7 days/week\nExtra Active: Very hard exercise & physical job or training twice/day" |
| ) |
| |
| |
| max_prep_time = st.number_input( |
| "Maximum preparation time (minutes)", |
| min_value=0, |
| max_value=int(st.session_state["data"]["minutes"].max()), |
| value=60, |
| step=5, |
| help="Maximum time you want to spend preparing the recipe" |
| ) |
| |
| |
| has_diabetes = st.checkbox( |
| "I have diabetes (will filter high-sugar recipes)", |
| key="has_diabetes", |
| help="Check this to filter out recipes with high sugar content and receive diabetic-friendly recommendations" |
| ) |
| |
| if has_diabetes: |
| st.info(""" |
| Diabetic-friendly filter limits sugar content to: |
| - Women: ≤ 25g (6 teaspoons) per day |
| - Men: ≤ 36g (9 teaspoons) per day |
| |
| This helps maintain stable blood sugar levels and follows ADA guidelines. |
| """) |
|
|
| |
| activity_multipliers = { |
| "Sedentary": 1.2, |
| "Lightly Active": 1.375, |
| "Moderately Active": 1.55, |
| "Very Active": 1.725, |
| "Extra Active": 1.9 |
| } |
|
|
| |
| fitness_goal = st.selectbox( |
| "Your Fitness Goal", |
| ["Loss weight and gain muscles", "Loss weight", "Gain weight", "Gain muscles", "Fitness (maintain)"], |
| help="Select your primary fitness goal to get personalized recipe recommendations" |
| ) |
|
|
| |
| st.subheader("Calorie Range") |
| col_min, col_max = st.columns(2) |
| with col_min: |
| min_calories = st.number_input( |
| label="Min Calories", |
| min_value=0, |
| max_value=2000, |
| value=0, |
| step=50, |
| help="Minimum calories per serving" |
| ) |
| with col_max: |
| max_calories = st.number_input( |
| label="Max Calories", |
| min_value=min_calories, |
| max_value=2000, |
| value=2000, |
| step=50, |
| help="Maximum calories per serving" |
| ) |
|
|
| |
| if st.button("Calculate My Nutritional Needs"): |
| |
| if weight <= 0 or height <= 0 or age <= 0: |
| st.error("Please enter valid values for weight, height, and age.") |
| else: |
| bmr = utils.calculate_bmr(weight, height, age, gender) |
| tdee = utils.calculate_tdee(bmr, activity_multipliers[activity_level]) |
| protein_target, carbs_target, fats_target = utils.get_macro_targets(tdee, fitness_goal, weight) |
| |
| |
| if fitness_goal == "Loss weight": |
| adjusted_calories = tdee - 500 |
| calorie_explanation = "500 calorie deficit for steady weight loss" |
| elif fitness_goal == "Loss weight and gain muscles": |
| adjusted_calories = tdee - 300 |
| calorie_explanation = "300 calorie deficit for muscle-preserving weight loss" |
| elif fitness_goal == "Gain weight": |
| adjusted_calories = tdee + 500 |
| calorie_explanation = "500 calorie surplus for weight gain" |
| elif fitness_goal == "Gain muscles": |
| adjusted_calories = tdee + 300 |
| calorie_explanation = "300 calorie surplus for muscle gain" |
| else: |
| adjusted_calories = tdee |
| calorie_explanation = "Maintenance calories" |
| |
| st.session_state["user_info"] = { |
| "bmr": bmr, |
| "tdee": tdee, |
| "adjusted_calories": adjusted_calories, |
| "protein_target": protein_target, |
| "carbs_target": carbs_target, |
| "fats_target": fats_target |
| } |
| |
| |
| st.success("Your Nutritional Targets Calculated!") |
| |
| |
| col1, col2 = st.columns(2) |
| with col1: |
| st.metric("Basal Metabolic Rate (BMR)", f"{bmr:.0f} kcal") |
| with col2: |
| st.metric("Total Daily Energy Expenditure (TDEE)", f"{tdee:.0f} kcal") |
| |
| |
| st.subheader("Goal-Adjusted Calories") |
| st.info(f""" |
| **Target Daily Calories: {adjusted_calories:.0f} kcal** |
| _{calorie_explanation}_ |
| """) |
| |
| |
| st.subheader("Macronutrient Targets") |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| protein_cals = protein_target * 4 |
| protein_pct = (protein_cals / adjusted_calories) * 100 |
| st.metric( |
| "Protein", |
| f"{protein_target:.0f}g", |
| f"{protein_cals:.0f} kcal ({protein_pct:.0f}%)" |
| ) |
| |
| with col2: |
| carbs_cals = carbs_target * 4 |
| carbs_pct = (carbs_cals / adjusted_calories) * 100 |
| st.metric( |
| "Carbohydrates", |
| f"{carbs_target:.0f}g", |
| f"{carbs_cals:.0f} kcal ({carbs_pct:.0f}%)" |
| ) |
| |
| with col3: |
| fats_cals = fats_target * 9 |
| fats_pct = (fats_cals / adjusted_calories) * 100 |
| st.metric( |
| "Fats", |
| f"{fats_target:.0f}g", |
| f"{fats_cals:.0f} kcal ({fats_pct:.0f}%)" |
| ) |
| |
| |
| st.subheader("Suggested Meal Distribution") |
| st.info(f""" |
| Based on your daily target of {adjusted_calories:.0f} kcal: |
| - Breakfast: {(adjusted_calories * 0.25):.0f} kcal (25%) |
| - Lunch: {(adjusted_calories * 0.35):.0f} kcal (35%) |
| - Dinner: {(adjusted_calories * 0.30):.0f} kcal (30%) |
| - Snacks: {(adjusted_calories * 0.10):.0f} kcal (10%) |
| """) |
|
|
| |
| if st.session_state["meal_history"]["recipes"]: |
| st.subheader("Your Meal History") |
| |
| |
| col1, col2, col3, col4, col5 = st.columns([2, 1, 1, 1, 1]) |
| with col1: |
| st.write("**Recipe Name**") |
| with col2: |
| st.write("**Calories**") |
| with col3: |
| st.write("**Rating**") |
| with col4: |
| st.write("**Last Cooked**") |
| with col5: |
| st.write("**Actions**") |
| |
| |
| for i, meal_name in enumerate(st.session_state["meal_history"]["recipes"]): |
| meal_data = st.session_state["data"][st.session_state["data"]["name"] == meal_name].iloc[0] |
| |
| col1, col2, col3, col4, col5 = st.columns([2, 1, 1, 1, 1]) |
| with col1: |
| st.write(meal_name) |
| with col2: |
| st.write(f"{meal_data['calories']} kcal") |
| with col3: |
| |
| current_rating = st.session_state["meal_history"]["ratings"].get(meal_name, 0) |
| new_rating = st.selectbox( |
| "Rating", |
| options=[0, 1, 2, 3, 4, 5], |
| index=current_rating, |
| key=f"rating_{i}", |
| label_visibility="collapsed" |
| ) |
| if new_rating != current_rating: |
| st.session_state["meal_history"]["ratings"][meal_name] = new_rating |
| st.experimental_rerun() |
| with col4: |
| last_cooked = st.session_state["meal_history"]["last_cooked"].get(meal_name, "Never") |
| st.write(last_cooked) |
| with col5: |
| if st.button("🗑️", key=f"remove_{i}"): |
| |
| st.session_state["meal_history"]["recipes"].remove(meal_name) |
| if meal_name in st.session_state["meal_history"]["ratings"]: |
| del st.session_state["meal_history"]["ratings"][meal_name] |
| if meal_name in st.session_state["meal_history"]["last_cooked"]: |
| del st.session_state["meal_history"]["last_cooked"][meal_name] |
| if meal_name in st.session_state["meal_history"]["frequency"]: |
| del st.session_state["meal_history"]["frequency"][meal_name] |
| st.experimental_rerun() |
| |
| |
| st.markdown("---") |
| st.session_state["use_history"] = st.checkbox( |
| "Use my meal history for recommendations", |
| value=st.session_state["use_history"], |
| help="When enabled, recommendations will be based on your ratings, cooking frequency, and preferences" |
| ) |
| |
| if st.session_state["use_history"]: |
| st.info(""" |
| **Enhanced History-Based Recommendations** |
| - Recommendations consider your ratings (1-5 stars) |
| - Frequent and recently cooked recipes influence suggestions |
| - Your preferred ingredients and cuisines are prioritized |
| - Results are filtered to match your current fitness goals and dietary preferences |
| """) |
|
|
| if st.session_state["use_history"]: |
| recipe = None |
| else: |
| recipe = st.selectbox( |
| label="Search the recipe:", |
| options=st.session_state["data"]["name"], |
| index=100 |
| ) |
|
|
| col1, col2 = st.columns([1, 3]) |
| with col1: |
| num_recipes = st.number_input( |
| label="Number of similar recipes", min_value=1, max_value=10, step=1 |
| ) |
|
|
| |
| |
|
|
| def assign_values(): |
| try: |
| macro_targets = None |
| min_cal = min_calories |
| max_cal = max_calories |
| |
| if st.session_state["user_info"] and not (min_calories or max_calories): |
| |
| tdee = st.session_state["user_info"]["tdee"] |
| macro_targets = ( |
| st.session_state["user_info"]["protein_target"], |
| st.session_state["user_info"]["carbs_target"], |
| st.session_state["user_info"]["fats_target"] |
| ) |
| |
| |
| if fitness_goal == "Loss weight": |
| min_cal = int(tdee * 0.20) |
| max_cal = int(tdee * 0.35) |
| elif fitness_goal == "Loss weight and gain muscles": |
| min_cal = int(tdee * 0.25) |
| max_cal = int(tdee * 0.40) |
| elif fitness_goal == "Gain weight": |
| min_cal = int(tdee * 0.30) |
| max_cal = int(tdee * 0.45) |
| elif fitness_goal == "Gain muscles": |
| min_cal = int(tdee * 0.25) |
| max_cal = int(tdee * 0.40) |
| else: |
| min_cal = int(tdee * 0.25) |
| max_cal = int(tdee * 0.35) |
| |
| st.info(f""" |
| **Suggested Calorie Range Per Meal** |
| - Minimum: {min_cal:.0f} kcal |
| - Maximum: {max_cal:.0f} kcal |
| |
| Based on your daily needs ({tdee:.0f} kcal) and goal ({fitness_goal}). |
| You can adjust this range using the inputs above. |
| """) |
| else: |
| |
| st.info(f""" |
| **Selected Calorie Range** |
| - Minimum: {min_cal} kcal |
| - Maximum: {max_cal} kcal |
| """) |
|
|
| |
| st.write("Using calorie range:", min_cal, "-", max_cal, "kcal") |
| |
| if st.session_state["use_history"]: |
| st.write("Using history-based recommendations") |
| st.session_state["result"] = utils.get_recommendations_from_history( |
| st.session_state["data"], |
| st.session_state["meal_history"]["recipes"], |
| num_recipes, |
| min_calories=min_cal, |
| max_calories=max_cal, |
| diabetic_friendly=st.session_state.get("has_diabetes", False), |
| macro_targets=macro_targets, |
| fitness_goal=fitness_goal, |
| max_prep_time=max_prep_time |
| ) |
| else: |
| st.write("Using specific recipe search:", recipe) |
| st.session_state["result"] = utils.find_similar_recipe( |
| recipe, |
| st.session_state["data"], |
| num_recipes, |
| min_calories=min_cal, |
| max_calories=max_cal, |
| meal_history=st.session_state["meal_history"]["recipes"], |
| diabetic_friendly=st.session_state.get("has_diabetes", False), |
| macro_targets=macro_targets, |
| fitness_goal=fitness_goal, |
| max_prep_time=max_prep_time |
| ) |
| |
| if st.session_state["result"] is not None: |
| st.write("Number of recommendations found:", len(st.session_state["result"])) |
| if len(st.session_state["result"]) == 0: |
| st.warning("No recipes found matching your criteria. Try adjusting your filters.") |
| else: |
| st.warning("No recommendations found. Try adjusting your filters or search criteria.") |
| |
| except Exception as e: |
| st.error(f"An error occurred: {str(e)}") |
| st.write("Debug information:") |
| st.write("Data shape:", st.session_state["data"].shape) |
| st.write("Data columns:", st.session_state["data"].columns.tolist()) |
|
|
| search = st.button(label="Search", on_click=assign_values) |
|
|
| |
| |
|
|
| if search: |
| if st.session_state["result"] is not None: |
| |
| if st.session_state["user_info"]: |
| st.subheader("Your Nutritional Targets") |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| st.metric("Daily Calories", f"{st.session_state['user_info']['tdee']:.0f} kcal") |
| with col2: |
| st.metric("Protein", f"{st.session_state['user_info']['protein_target']:.0f}g") |
| with col3: |
| st.metric("Carbohydrates", f"{st.session_state['user_info']['carbs_target']:.0f}g") |
| with col4: |
| st.metric("Fats", f"{st.session_state['user_info']['fats_target']:.0f}g") |
| st.markdown("---") |
| |
| for recipe_idx, row_index in enumerate(range(st.session_state["result"].shape[0])): |
| dfx = st.session_state["result"].iloc[row_index] |
|
|
| with st.expander( |
| f"{recipe_idx+1}. " |
| + f"{dfx['name'].capitalize()}" |
| ): |
| tab_1, tab_2, tab_3 = st.tabs(["Summary", "Ingredients", "Recipe"]) |
|
|
| with tab_1: |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| st.metric(label="Calories", value=dfx["calories"]) |
|
|
| with col2: |
| st.metric(label="Number of Steps", value=dfx["n_steps"]) |
|
|
| with col3: |
| st.metric(label="Number of Ingredients", value=dfx["n_ingredients"]) |
|
|
| with col4: |
| st.metric(label="Cooking Time", value=f"{dfx['minutes']} Mins") |
|
|
| |
| if st.session_state["user_info"]: |
| st.subheader("Nutritional Information (per serving)") |
| |
| try: |
| nutrition_cols = dfx.index[8:13] |
| |
| |
| if any(dfx[nutrition_cols].isna()): |
| st.warning("Some nutritional information is missing for this recipe.") |
| continue |
| |
| |
| protein_g = max(0, dfx[nutrition_cols[3]] * 0.5) |
| carbs_g = max(0, dfx[nutrition_cols[1]] * 3) |
| fats_g = max(0, dfx[nutrition_cols[0]] * 0.65) |
| sugar_g = max(0, dfx[nutrition_cols[1]] * 0.5) |
| |
| |
| st.write("### Macronutrients") |
| col1, col2, col3, col4 = st.columns(4) |
| with col1: |
| st.metric("Protein", f"{protein_g:.1f}g") |
| with col2: |
| st.metric("Carbs", f"{carbs_g:.1f}g") |
| with col3: |
| st.metric("Fats", f"{fats_g:.1f}g") |
| with col4: |
| st.metric("Sugar", f"{sugar_g:.1f}g") |
| |
| |
| if st.session_state["has_diabetes"] and sugar_g > 10: |
| st.warning(f"⚠️ High sugar content! This recipe contains {sugar_g:.1f}g of sugar per serving, which may not be suitable for diabetic patients.") |
| |
| except Exception as e: |
| st.error(f"Error calculating nutritional information: {str(e)}") |
| st.write("Please contact support if this error persists.") |
|
|
| fig = utils.plot_nutrition(dfx) |
| st.plotly_chart(fig) |
|
|
| with tab_2: |
| st.text(f"Number of Ingredients: {dfx['n_ingredients']}") |
| for ingredient_idx, step in enumerate(dfx["ingredients"]): |
| st.markdown(f"{ingredient_idx+1}. {step}") |
|
|
| with tab_3: |
| st.text(f"Recipe") |
| for step_idx, step in enumerate(dfx["steps"]): |
| st.markdown(f"{step_idx+1}. {step}") |
| |
| |
| if st.button(f"Add {dfx['name']} to Meal History", key=f"add_{dfx['name']}_{recipe_idx}"): |
| if dfx['name'] not in st.session_state["meal_history"]["recipes"]: |
| |
| st.session_state["meal_history"]["recipes"].append(dfx['name']) |
| |
| |
| st.session_state["meal_history"]["last_cooked"][dfx['name']] = datetime.now().strftime("%Y-%m-%d") |
| |
| |
| st.session_state["meal_history"]["frequency"][dfx['name']] = 1 |
| |
| |
| |
| cuisine = dfx['name'].split()[0].lower() |
| st.session_state["meal_history"]["preferences"]["favorite_cuisines"].add(cuisine) |
| |
| |
| for ingredient in dfx['ingredients']: |
| st.session_state["meal_history"]["preferences"]["favorite_ingredients"].add(ingredient.lower()) |
| |
| |
| current_avg = st.session_state["meal_history"]["preferences"]["preferred_cooking_time"] |
| n_recipes = len(st.session_state["meal_history"]["recipes"]) |
| new_avg = (current_avg * (n_recipes - 1) + dfx['minutes']) / n_recipes |
| st.session_state["meal_history"]["preferences"]["preferred_cooking_time"] = new_avg |
| |
| |
| current_avg = st.session_state["meal_history"]["preferences"]["preferred_meal_size"] |
| new_avg = (current_avg * (n_recipes - 1) + dfx['calories']) / n_recipes |
| st.session_state["meal_history"]["preferences"]["preferred_meal_size"] = new_avg |
| |
| st.experimental_rerun() |
| else: |
| st.warning("No recommendations found. Try adjusting your calorie range or adding more recipes to your history.") |
|
|
|
|