1- # spell-checker: disable
2- """
3- Recommended Recipes Page
4- Shows recipes matched with user's pantry, including diet type
5- """
1+ # recommended_recipes.py
2+ # Optimized Recommended Recipes page with ingredient selection and diet filter
3+ # Date: 2025-11-20
64
75import ast
86import os
1715st .set_page_config (page_title = "Recommended Recipes" , page_icon = "π³" , layout = "wide" )
1816
1917st .title ("π³ Recommended Recipes" )
20- st .caption ("Discover recipes you can cook with what's already in your pantry !" )
18+ st .caption ("Discover recipes you can cook with your selected ingredients !" )
2119
22- # ---------- Check Username ----------
20+ # ---------- Check username ----------
2321if "username" not in st .session_state or not st .session_state ["username" ]:
2422 st .warning ("Please go to Home page and enter your username first." )
2523 st .stop ()
2927 "smart_pantry_manager" , "data" , f"pantry_{ username .replace (' ' , '_' ).lower ()} .xlsx"
3028)
3129
30+ # ---------- Load pantry ----------
3231try :
3332 pantry = pd .read_excel (USER_FILE )
34- if "Product" in pantry .columns :
35- pantry ["Product" ] = pantry ["Product" ].astype (str ).str .lower ().str .strip ()
36- else :
37- pantry ["Product" ] = ""
33+ pantry_products = sorted ({p .lower ().strip () for p in pantry .get ("Product" , [])})
3834except FileNotFoundError :
3935 st .info ("Your pantry is empty. Add items on Home page first." )
4036 st .stop ()
4137
42- pantry_products = sorted ({p for p in pantry ["Product" ].tolist () if p and p .strip ()})
43- pantry_regexes = [
44- re .compile (rf"\b{ re .escape (p )} \b" , flags = re .IGNORECASE ) for p in pantry_products
45- ]
38+ # ---------- User ingredient selection ----------
39+ st .sidebar .header ("Select Ingredients to Use" )
40+ selected_ingredients = st .sidebar .multiselect (
41+ "Choose ingredients:" , options = pantry_products , default = pantry_products
42+ )
4643
47- # ---------- Load Recipes ----------
48- DB_PATH = os .path .join ("smart_pantry_manager" , "data" , "cleaned_data.sqlite" )
44+ st .sidebar .header ("Select Diet Type" )
45+ selected_diet = st .sidebar .selectbox (
46+ "Diet preference:" , ["Any" , "Vegan" , "Vegetarian" , "Non-Vegetarian" ]
47+ )
4948
5049
50+ # ---------- Load recipes ----------
5151@st .cache_data
52- def load_recipes ():
53- if not os .path .exists (DB_PATH ):
54- st .error ("Recipes database not found." )
52+ def load_recipes () -> pd .DataFrame :
53+ db_path = os .path .join ("smart_pantry_manager" , "data" , "cleaned_data.sqlite" )
54+ if not os .path .exists (db_path ):
55+ st .error ("Recipes database not found! Place cleaned_data.sqlite in data/." )
5556 return pd .DataFrame (
5657 columns = ["Title" , "Ingredients" , "Instructions" , "Diet_Type" ]
5758 )
58- conn = sqlite3 .connect (DB_PATH )
59+ conn = sqlite3 .connect (db_path )
5960 try :
60- df = pd .read_sql ("SELECT * FROM all_recipes" , conn )
61+ df = pd .read_sql_query ("SELECT * FROM all_recipes" , conn )
6162 except Exception as e :
62- st .error (f"Error reading recipes: { e } " )
6363 conn .close ()
64+ st .error (f"Error loading recipes: { e } " )
6465 return pd .DataFrame (
6566 columns = ["Title" , "Ingredients" , "Instructions" , "Diet_Type" ]
6667 )
6768 conn .close ()
6869 for col in ["Title" , "Ingredients" , "Instructions" , "Diet_Type" ]:
6970 if col not in df .columns :
7071 df [col ] = ""
71- return df
72+ return df [[ "Title" , "Ingredients" , "Instructions" , "Diet_Type" ]]
7273
7374
7475recipes = load_recipes ()
7576if recipes .empty :
76- st .info ("No recipes available in DB ." )
77+ st .warning ("No recipes found in the database ." )
7778 st .stop ()
7879
7980
8081# ---------- Utilities ----------
8182def normalize_text (s : str ) -> str :
83+ """Normalize unicode artifacts and strip."""
8284 if s is None :
8385 return ""
8486 s = str (s )
@@ -88,91 +90,98 @@ def normalize_text(s: str) -> str:
8890
8991
9092def parse_ingredients (ingredients_str : str ) -> List [str ]:
91- if not ingredients_str :
93+ """Parse ingredients stored as list string or comma-separated."""
94+ if pd .isna (ingredients_str ):
9295 return []
93- s = normalize_text (str ( ingredients_str ) )
96+ s = normalize_text (ingredients_str )
9497 try :
9598 if s .startswith ("[" ) and s .endswith ("]" ):
9699 parsed = ast .literal_eval (s )
97- return [normalize_text (str (x )) for x in parsed if str (x ).strip ()]
100+ if isinstance (parsed , (list , tuple )):
101+ return [normalize_text (str (x )) for x in parsed if str (x ).strip ()]
98102 if "," in s :
99103 return [normalize_text (x ) for x in s .split ("," ) if x .strip ()]
100104 return [s ]
101105 except Exception :
106+ if "|" in s :
107+ return [normalize_text (x ) for x in s .split ("|" ) if x .strip ()]
108+ if "\n " in s :
109+ return [normalize_text (x ) for x in s .split ("\n " ) if x .strip ()]
102110 return [s ]
103111
104112
105- def strip_leading_qty (s : str ) -> str :
113+ def clean_ingredient_name (s : str ) -> str :
114+ """Extract core ingredient name for matching."""
106115 if not s :
107116 return ""
108117 s = s .lower ()
109- s = re .sub (r"^\s*\(?\d+(?:[\/\u00BC-\u00BE\u2150-\u215E]?\d*)?\)?\s*" , "" , s )
110- s = re .sub (
111- r"^\s*\d+(\.\d+)?\s*(cup|cups|tbsp|tbsp.|tbsps|tsp|tsp.|oz|lb|lbs|g|kg|ml|l)\b" ,
112- "" ,
113- s ,
114- )
115- s = re .sub (r"^\s*(?:one|two|three|four|a|an)\s+" , "" , s )
116- s = re .sub (r"^[\-\β\β\s]+" , "" , s )
118+ s = re .sub (r"\([^)]*\)" , "" , s )
119+ s = re .sub (r"\d+[\/\d\s]*\s*(cup|cups|tbsp|tsp|oz|lb|lbs|g|kg|ml|l)?" , "" , s )
120+ s = re .sub (r"[^a-zA-Z\u00C0-\u017F\s]+" , "" , s )
121+ s = re .sub (r"\s+" , " " , s )
117122 return s .strip ()
118123
119124
120125@st .cache_data
121126def cached_check_availability (
122- recipe_ingredients : str , pantry_products_tuple : Tuple [str , ...]
123- ):
127+ recipe_ingredients : str , selected_tuple : Tuple [str , ...]
128+ ) -> Tuple [float , List [str ]]:
129+ """Return match % and missing items for selected ingredients."""
124130 ingredients = parse_ingredients (recipe_ingredients )
125131 if not ingredients :
126132 return 0.0 , []
133+
127134 total = len (ingredients )
128135 available_count = 0
129136 missing_items = []
130137 regexes = [
131- re .compile (rf"\b{ re .escape (p )} \b" , re .IGNORECASE ) for p in pantry_products_tuple
138+ re .compile (rf"\b{ re .escape (p )} \b" , flags = re .IGNORECASE ) for p in selected_tuple
132139 ]
140+
133141 for item in ingredients :
134- item_norm = normalize_text (str (item )).lower ()
135- name_candidate = strip_leading_qty (item_norm )
136- text_to_search = name_candidate or item_norm
142+ core_name = clean_ingredient_name (item )
143+ text_to_search = core_name or item
137144 matched = any (rx .search (text_to_search ) for rx in regexes )
138145 if matched :
139146 available_count += 1
140147 else :
141- words = text_to_search .split ()
142- short = " " .join (words [- 3 :] if len (words ) > 3 else words )
143- missing_items .append (short )
144- match_percentage = (available_count / total ) * 100 if total > 0 else 0.0
145- return round (match_percentage , 1 ), missing_items
148+ missing_items .append (core_name )
149+ match_percent = (available_count / total ) * 100 if total else 0.0
150+ return round (match_percent , 1 ), missing_items
146151
147152
148153# ---------- Filters ----------
149- st .subheader (f"π₯ Personalized Recipe Matches for { username } " )
150- col1 , col2 = st .columns (2 )
151- with col1 :
152- min_match = st .slider ("Minimum match percentage:" , 0 , 100 , 50 , 5 )
153- with col2 :
154- max_recipes = st .number_input ("Maximum recipes to show:" , 10 , 200 , 20 , 5 )
154+ st .subheader (f"π₯ Personalized Recipes for { username } " )
155+
156+ min_match = st .slider ("Minimum match %:" , 0 , 100 , 50 , 5 )
157+ max_recipes = st .number_input ("Max recipes to show:" , 10 , 200 , 20 , 5 )
155158
156159st .write ("π Analyzing recipes..." )
157160progress_bar = st .progress (0 )
158161status_text = st .empty ()
159-
160162results = []
161163total_recipes = len (recipes )
162- pantry_key = tuple (pantry_products )
164+ selected_tuple = tuple (selected_ingredients )
163165
164166for idx , (_ , row ) in enumerate (recipes .iterrows ()):
165- progress_bar .progress ((idx + 1 ) / total_recipes )
167+ progress = (idx + 1 ) / total_recipes
168+ progress_bar .progress (progress )
166169 status_text .text (f"Processing recipe { idx + 1 } of { total_recipes } ..." )
167- ingredients_raw = normalize_text (str (row .get ("Ingredients" , "" )))
168- match_percent , missing = cached_check_availability (ingredients_raw , pantry_key )
170+
171+ if selected_diet != "Any" and row ["Diet_Type" ].strip () != selected_diet :
172+ continue
173+
174+ ingredients_raw = row .get ("Ingredients" ) or ""
175+ match_percent , missing = cached_check_availability (ingredients_raw , selected_tuple )
176+
169177 if match_percent >= min_match :
170- instr = normalize_text (str (row .get ("Instructions" , "" )))
171- instr_preview = instr [:500 ] + "..." if len (instr ) > 500 else instr
178+ instr = normalize_text (row .get ("Instructions" ) or "" )
179+ instr_preview = instr # show full instructions
180+ diet = row .get ("Diet_Type" ) or "Unknown"
172181 results .append (
173182 {
174- "Title " : row .get ("Title" , "Unnamed Recipe" ) ,
175- "Diet_Type " : row . get ( "Diet_Type" , "Unknown" ) ,
183+ "Recipe " : row .get ("Title" ) or "Unnamed Recipe" ,
184+ "Diet " : diet ,
176185 "Match %" : match_percent ,
177186 "Missing" : ", " .join (missing [:3 ]) + ("..." if len (missing ) > 3 else "" )
178187 if missing
@@ -184,37 +193,41 @@ def cached_check_availability(
184193
185194progress_bar .empty ()
186195status_text .empty ()
187-
188196results_df = pd .DataFrame (results )
189197if not results_df .empty :
190- results_df = results_df .sort_values ("Match %" , ascending = False ).head (
198+ results_df = results_df .sort_values (by = "Match %" , ascending = False ).head (
191199 int (max_recipes )
192200 )
193201 st .success (f"β
Found { len (results_df )} matching recipes!" )
202+
194203 st .write ("### π Recipe Match Overview" )
195204 st .dataframe (
196- results_df [["Title " , "Diet_Type " , "Match %" , "Missing" ]].reset_index (drop = True ),
205+ results_df [["Recipe " , "Diet " , "Match %" , "Missing" ]].reset_index (drop = True ),
197206 use_container_width = True ,
198207 hide_index = True ,
199208 )
209+
200210 st .write ("### π Recipe Details" )
201211 for _ , row in results_df .iterrows ():
202212 match_color = (
203213 "π’" if row ["Match %" ] >= 80 else "π‘" if row ["Match %" ] >= 60 else "π "
204214 )
205- with st .expander (f"{ row ['Title ' ]} β { row ['Diet_Type' ] } { match_color } " ):
215+ with st .expander (f"{ match_color } { row ['Recipe ' ]} - { row ['Diet' ] } " ):
206216 col1 , col2 = st .columns ([1 , 2 ])
207217 with col1 :
208218 st .markdown (f"**Match:** { row ['Match %' ]} %" )
209219 st .markdown (f"**Missing:** { row ['Missing' ]} " )
210- ing_list = parse_ingredients (row ["Ingredients" ])
220+ ing_list = parse_ingredients (row ["Ingredients" ] or "" )
211221 with col2 :
212222 st .markdown ("**π§ Ingredients:**" )
213- for ing in ing_list [:10 ]:
214- st .write (f"β’ { ing } " )
215- if len (ing_list ) > 10 :
216- st .write (f"*...and { len (ing_list ) - 10 } more*" )
217- st .markdown ("**π©βπ³ Instructions:**" )
218- st .write (row ["Instructions" ] or "No instructions available." )
223+ if ing_list :
224+ for ing in ing_list [:10 ]:
225+ st .write (f"β’ { ing } " )
226+ if len (ing_list ) > 10 :
227+ st .write (f"*...and { len (ing_list ) - 10 } more*" )
228+ else :
229+ st .write ("No ingredient data available." )
230+ st .markdown ("**π©βπ³ Instructions:**" )
231+ st .write (row ["Instructions" ] or "No instructions available." )
219232else :
220233 st .info (f"No recipes found with at least { min_match } % match." )
0 commit comments