Skip to content

Commit ce74e62

Browse files
committed
Add Custom Ingredient Selection
1 parent bdc7abe commit ce74e62

File tree

2 files changed

+95
-79
lines changed

2 files changed

+95
-79
lines changed

β€Žsmart_pantry_manager/pages/all_recipes.pyβ€Ž

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,13 @@ def parse_ingredients(ingredients_str):
7575
st.markdown(f"**Diet Type:** {diet}")
7676
with col2:
7777
st.markdown("**πŸ§‚ Ingredients:**")
78+
# Parse ingredients safely
7879
ing_list = parse_ingredients(row.get("Ingredients", ""))
79-
for ing in ing_list[:10]:
80-
st.write(f"β€’ {ing}")
81-
if len(ing_list) > 10:
82-
st.write(f"*...and {len(ing_list) - 10} more*")
80+
if ing_list:
81+
for ing in ing_list:
82+
st.write(f"β€’ {ing}")
83+
else:
84+
st.write("No ingredient data available.")
8385
st.markdown("**πŸ‘©β€πŸ³ Instructions:**")
84-
st.write(str(row.get("Instructions", "No instructions available.")))
86+
# Show full instructions
87+
st.write(str(row.get("Instructions", "No instructions available.")))
Lines changed: 87 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
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

75
import ast
86
import os
@@ -17,9 +15,9 @@
1715
st.set_page_config(page_title="Recommended Recipes", page_icon="🍳", layout="wide")
1816

1917
st.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 ----------
2321
if "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()
@@ -29,56 +27,60 @@
2927
"smart_pantry_manager", "data", f"pantry_{username.replace(' ', '_').lower()}.xlsx"
3028
)
3129

30+
# ---------- Load pantry ----------
3231
try:
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", [])})
3834
except 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

7475
recipes = load_recipes()
7576
if 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 ----------
8182
def 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

9092
def 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
121126
def 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

156159
st.write("πŸ” Analyzing recipes...")
157160
progress_bar = st.progress(0)
158161
status_text = st.empty()
159-
160162
results = []
161163
total_recipes = len(recipes)
162-
pantry_key = tuple(pantry_products)
164+
selected_tuple = tuple(selected_ingredients)
163165

164166
for 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

185194
progress_bar.empty()
186195
status_text.empty()
187-
188196
results_df = pd.DataFrame(results)
189197
if 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.")
219232
else:
220233
st.info(f"No recipes found with at least {min_match}% match.")

0 commit comments

Comments
Β (0)