Spaces:
Build error
Build error
Update src/streamlit_app.py
Browse files- src/streamlit_app.py +787 -662
src/streamlit_app.py
CHANGED
|
@@ -1,10 +1,26 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
import pandas as pd
|
|
|
|
| 3 |
import json
|
|
|
|
| 4 |
import uuid
|
| 5 |
from datetime import datetime
|
| 6 |
-
from collections import Counter
|
| 7 |
import base64
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
# --- Page Configuration ---
|
| 10 |
st.set_page_config(
|
|
@@ -13,126 +29,310 @@ st.set_page_config(
|
|
| 13 |
layout="wide"
|
| 14 |
)
|
| 15 |
|
| 16 |
-
# ---
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
if image_file is not None:
|
| 24 |
-
try:
|
| 25 |
-
# Convert image to base64 for storage in memory
|
| 26 |
-
image_bytes = image_file.getvalue()
|
| 27 |
-
image_b64 = base64.b64encode(image_bytes).decode()
|
| 28 |
-
file_extension = image_file.name.split('.')[-1].lower()
|
| 29 |
-
|
| 30 |
-
# Store in session state
|
| 31 |
-
if 'recipe_images' not in st.session_state:
|
| 32 |
-
st.session_state['recipe_images'] = {}
|
| 33 |
-
|
| 34 |
-
image_key = f"{recipe_id}.{file_extension}"
|
| 35 |
-
st.session_state['recipe_images'][image_key] = {
|
| 36 |
-
'data': image_b64,
|
| 37 |
-
'type': image_file.type
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
return image_key
|
| 41 |
-
except Exception as e:
|
| 42 |
-
st.error(f"Error saving image: {e}")
|
| 43 |
-
return None
|
| 44 |
-
return None
|
| 45 |
-
|
| 46 |
-
def load_image_from_memory(image_filename):
|
| 47 |
-
"""Load image from session state"""
|
| 48 |
-
if image_filename and 'recipe_images' in st.session_state:
|
| 49 |
-
try:
|
| 50 |
-
if image_filename in st.session_state['recipe_images']:
|
| 51 |
-
image_data = st.session_state['recipe_images'][image_filename]
|
| 52 |
-
return base64.b64decode(image_data['data'])
|
| 53 |
-
except Exception as e:
|
| 54 |
-
st.error(f"Error loading image: {e}")
|
| 55 |
-
return None
|
| 56 |
-
|
| 57 |
-
def save_recipe_to_memory(recipe_data):
|
| 58 |
-
"""Save recipe to session state"""
|
| 59 |
try:
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
-
#
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
except Exception as e:
|
| 67 |
-
st.error(f"Error
|
| 68 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
def
|
| 71 |
-
"""
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
def
|
| 77 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 78 |
try:
|
| 79 |
-
#
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
-
#
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
|
|
|
|
|
|
| 95 |
return True
|
| 96 |
except Exception as e:
|
| 97 |
-
st.error(f"Error
|
| 98 |
return False
|
| 99 |
|
| 100 |
-
def
|
| 101 |
-
"""
|
|
|
|
| 102 |
try:
|
| 103 |
-
|
| 104 |
-
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
|
| 112 |
-
storage_size += len(image_data['data']) * 3 // 4 # Approximate decoded size
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
}
|
| 119 |
-
except:
|
| 120 |
-
return {'total_recipes': 0, 'total_images': 0, 'storage_size': 0}
|
| 121 |
|
| 122 |
# --- Language and Text ---
|
| 123 |
TEXT = {
|
| 124 |
"en": {
|
| 125 |
"title": "Ruchi Chudu",
|
| 126 |
-
"subtitle": "
|
| 127 |
"language_select": "Choose Language",
|
| 128 |
"sidebar_header": "Contribute Your Recipe!",
|
| 129 |
"tab_submit": "📝 Submit Your Recipe",
|
| 130 |
-
"tab_gallery": "🍲 Community Gallery",
|
|
|
|
| 131 |
"tab_analytics": "📊 Recipe Analytics",
|
| 132 |
-
"tab_manage": "⚙️ Manage Recipes",
|
| 133 |
"form_header": "Tell us about your dish",
|
| 134 |
"dish_name": "Dish Name",
|
| 135 |
-
"your_name": "Your Name",
|
| 136 |
"district": "Your District",
|
| 137 |
"select_district": "--- Select a District ---",
|
| 138 |
"recipe_type": "Recipe Type (e.g., Curry, Pickle, Snack)",
|
|
@@ -143,89 +343,41 @@ TEXT = {
|
|
| 143 |
"submit_button": "Submit Recipe",
|
| 144 |
"success_message": "Thank you! Your recipe has been submitted successfully.",
|
| 145 |
"gallery_header": "Explore Recipes from the Community",
|
| 146 |
-
"
|
| 147 |
-
"
|
| 148 |
-
"
|
| 149 |
-
"
|
| 150 |
-
"
|
| 151 |
-
"
|
| 152 |
-
"ingredients_title": "🌶️ Ingredients",
|
| 153 |
-
"instructions_title": "✍️ Instructions",
|
| 154 |
-
"tags_title": "AI-Generated Tags",
|
| 155 |
-
"error_dish_name": "Please enter a dish name before submitting.",
|
| 156 |
-
"search_placeholder": "Search recipes...",
|
| 157 |
-
"filter_by_district": "Filter by District",
|
| 158 |
-
"filter_by_type": "Filter by Recipe Type",
|
| 159 |
-
"all_districts": "All Districts",
|
| 160 |
-
"all_types": "All Recipe Types",
|
| 161 |
-
"total_recipes": "Total Recipes",
|
| 162 |
-
"total_images": "Total Images",
|
| 163 |
-
"storage_size": "Storage Used",
|
| 164 |
-
"top_districts": "Top Contributing Districts",
|
| 165 |
-
"popular_types": "Popular Recipe Types",
|
| 166 |
-
"recent_recipes": "Recent Submissions",
|
| 167 |
-
"clear_filters": "Clear All Filters",
|
| 168 |
-
"export_recipes": "Export All Recipes",
|
| 169 |
-
"manage_header": "Recipe Management",
|
| 170 |
-
"delete_recipe": "Delete Recipe",
|
| 171 |
-
"confirm_delete": "Are you sure you want to delete this recipe?",
|
| 172 |
-
"recipe_deleted": "Recipe deleted successfully!",
|
| 173 |
-
"backup_data": "Download All Data",
|
| 174 |
-
"backup_created": "Data downloaded successfully!",
|
| 175 |
-
"memory_storage_note": "Note: Data is stored in memory and will be lost when the session ends."
|
| 176 |
},
|
| 177 |
"te": {
|
| 178 |
-
"title": "రుచి చూడు",
|
| 179 |
-
"subtitle": "మన తెలుగు వంటల సంస్కృతిని కాపాడ
|
| 180 |
"language_select": "భాషను ఎంచుకోండి",
|
| 181 |
"sidebar_header": "మీ వంటకాన్ని పంపండి!",
|
| 182 |
"tab_submit": "📝 మీ వంటకాన్ని పంపండి",
|
| 183 |
"tab_gallery": "🍲 కమ్యూనిటీ గ్యాలరీ",
|
|
|
|
| 184 |
"tab_analytics": "📊 వంటకాల విశ్లేషణ",
|
| 185 |
-
"tab_manage": "⚙️ ��ంటకాలను నిర్వహించండి",
|
| 186 |
"form_header": "మీ వంటకం గురించి చెప్పండి",
|
| 187 |
"dish_name": "వంటకం పేరు",
|
| 188 |
"your_name": "మీ పేరు",
|
| 189 |
-
"district": "మీ జిల్లా",
|
| 190 |
"select_district": "--- జిల్లాను ఎంచుకోండి ---",
|
| 191 |
"recipe_type": "వంటకం రకం (ఉదా. కూర, పచ్చడి, స్నాక్)",
|
| 192 |
"ingredients": "కావలసినవి (ఒకదానికి ఒకటి)",
|
| 193 |
"instructions": "తయారీ విధానం",
|
| 194 |
"story": "ఈ వంటతో మీ కథ (మీ జ్ఞాపకాలు, కుటుంబ సంప్రదాయాలు మొదలైనవి)",
|
| 195 |
"image_upload": "వంటకం ఫోటోను అప్లోడ్ చేయండి (ఐచ్ఛికం)",
|
| 196 |
-
"submit_button": "వంటకాన్ని పంపండి",
|
| 197 |
"success_message": "ధన్యవాదాలు! మీ వంటకం విజయవంతంగా సమర్పించబడింది.",
|
| 198 |
-
"gallery_header": "కమ్యూనిటీ నుం
|
| 199 |
-
"
|
| 200 |
-
"
|
| 201 |
-
"
|
| 202 |
-
"
|
| 203 |
-
"
|
| 204 |
-
"
|
| 205 |
-
"ingredients_title": "🌶️ కావలసినవి",
|
| 206 |
-
"instructions_title": "✍️ తయారీ విధానం",
|
| 207 |
-
"tags_title": "AI ద్వారా రూపొందించబడిన ట్యాగ్లు",
|
| 208 |
-
"error_dish_name": "సమర్పించే ముందు దయచేసి వంటకం పేరును నమోదు చేయండి.",
|
| 209 |
-
"search_placeholder": "వంటకాలను వెతకండి...",
|
| 210 |
-
"filter_by_district": "జిల్లా ద్వారా ఫిల్టర్ చేయండి",
|
| 211 |
-
"filter_by_type": "వంటకం రకం ద్వారా ఫిల్టర్ చేయండి",
|
| 212 |
-
"all_districts": "అన్ని జిల్లాలు",
|
| 213 |
-
"all_types": "అన్ని వంటకాల రకాలు",
|
| 214 |
-
"total_recipes": "మొత్తం వంటకాలు",
|
| 215 |
-
"total_images": "మొత్తం చిత్రాలు",
|
| 216 |
-
"storage_size": "వాడిన స్టోరేజ్",
|
| 217 |
-
"top_districts": "అధిక సహకారం అందించిన జిల్లాలు",
|
| 218 |
-
"popular_types": "ప్రాచుర్యం పొందిన వంటకాల రకాలు",
|
| 219 |
-
"recent_recipes": "ఇటీవలి సమర్పణలు",
|
| 220 |
-
"clear_filters": "అన్ని ఫిల్టర్లను క్లియర్ చేయండి",
|
| 221 |
-
"export_recipes": "అన్ని వంటకాలను ఎగుమతి చేయండి",
|
| 222 |
-
"manage_header": "వంటకాల నిర్వహణ",
|
| 223 |
-
"delete_recipe": "వంటకాన్ని తొలగించండి",
|
| 224 |
-
"confirm_delete": "మీరు ఖచ్చితంగా ఈ వంటకాన్ని తొలగించాలనుకుంటున్నారా?",
|
| 225 |
-
"recipe_deleted": "వంటకం విజయవంతంగా తొలగించబడింది!",
|
| 226 |
-
"backup_data": "అన్ని డేటాను డౌన్లోడ్ చేయండి",
|
| 227 |
-
"backup_created": "డేటా విజయవంతంగా డౌన్లోడ్ చేయబడింది!",
|
| 228 |
-
"memory_storage_note": "గమనిక: డేటా మెమరీలో నిల్వ చేయబడుతుంది మరియు సెషన్ ముగిసినప్పుడు పోతుంది."
|
| 229 |
}
|
| 230 |
}
|
| 231 |
|
|
@@ -250,94 +402,36 @@ RECIPE_TYPES = ["Curry (కూర)", "Fry (వేపుడు)", "Pickle (పచ
|
|
| 250 |
if 'language' not in st.session_state:
|
| 251 |
st.session_state['language'] = 'en'
|
| 252 |
if 'recipes' not in st.session_state:
|
| 253 |
-
st.session_state['recipes'] =
|
| 254 |
-
if '
|
| 255 |
-
st.session_state['
|
| 256 |
|
| 257 |
# --- Helper Functions ---
|
| 258 |
def get_text(key):
|
| 259 |
"""Fetches text from the dictionary based on the selected language."""
|
| 260 |
return TEXT[st.session_state['language']][key]
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
"traditional": ["grandma", "ammamma", "nanamma", "traditional", "sampradayam"],
|
| 273 |
-
"summer": ["mango", "summer", "cool", "curd", "perugu"],
|
| 274 |
-
"winter": ["winter", "warm", "hot", "sheeta"],
|
| 275 |
-
"vegetarian": ["vegetable", "veg", "sabzi", "kura"],
|
| 276 |
-
"non-vegetarian": ["chicken", "mutton", "fish", "egg", "kodi", "meka"]
|
| 277 |
-
}
|
| 278 |
-
|
| 279 |
-
for tag, keywords in keyword_map.items():
|
| 280 |
-
if any(keyword in text_to_scan for keyword in keywords):
|
| 281 |
-
tags.add(tag.capitalize())
|
| 282 |
-
|
| 283 |
-
if not tags:
|
| 284 |
-
tags.add("Community Recipe")
|
| 285 |
-
|
| 286 |
-
return list(tags)
|
| 287 |
-
|
| 288 |
-
def filter_recipes(recipes, search_term="", district_filter="", type_filter=""):
|
| 289 |
-
"""Filter recipes based on search and filter criteria"""
|
| 290 |
-
filtered = recipes
|
| 291 |
-
|
| 292 |
-
if search_term:
|
| 293 |
-
filtered = [r for r in filtered if
|
| 294 |
-
search_term.lower() in r['dish_name'].lower() or
|
| 295 |
-
search_term.lower() in r.get('your_name', '').lower() or
|
| 296 |
-
search_term.lower() in r.get('ingredients', '').lower()]
|
| 297 |
-
|
| 298 |
-
if district_filter and district_filter != get_text('all_districts'):
|
| 299 |
-
filtered = [r for r in filtered if r.get('district') == district_filter]
|
| 300 |
-
|
| 301 |
-
if type_filter and type_filter != get_text('all_types'):
|
| 302 |
-
filtered = [r for r in filtered if r.get('recipe_type') == type_filter]
|
| 303 |
-
|
| 304 |
-
return filtered
|
| 305 |
-
|
| 306 |
-
def export_recipes_to_csv(recipes):
|
| 307 |
-
"""Export recipes to CSV format"""
|
| 308 |
-
if not recipes:
|
| 309 |
-
return None
|
| 310 |
-
|
| 311 |
-
export_data = []
|
| 312 |
-
for recipe in recipes:
|
| 313 |
-
export_data.append({
|
| 314 |
-
'Recipe ID': recipe.get('id', ''),
|
| 315 |
-
'Dish Name': recipe['dish_name'],
|
| 316 |
-
'Submitted By': recipe.get('your_name', ''),
|
| 317 |
-
'District': recipe.get('district', ''),
|
| 318 |
-
'Recipe Type': recipe.get('recipe_type', ''),
|
| 319 |
-
'Ingredients': recipe.get('ingredients', ''),
|
| 320 |
-
'Instructions': recipe.get('instructions', ''),
|
| 321 |
-
'Story': recipe.get('story', ''),
|
| 322 |
-
'Tags': ', '.join(recipe.get('tags', [])),
|
| 323 |
-
'Submitted Date': recipe.get('submitted_date', ''),
|
| 324 |
-
'Has Image': 'Yes' if recipe.get('image_filename') else 'No'
|
| 325 |
-
})
|
| 326 |
-
|
| 327 |
-
return pd.DataFrame(export_data)
|
| 328 |
-
|
| 329 |
-
def format_storage_size(size_bytes):
|
| 330 |
-
"""Convert bytes to human readable format"""
|
| 331 |
-
for unit in ['B', 'KB', 'MB', 'GB']:
|
| 332 |
-
if size_bytes < 1024.0:
|
| 333 |
-
return f"{size_bytes:.1f} {unit}"
|
| 334 |
-
size_bytes /= 1024.0
|
| 335 |
-
return f"{size_bytes:.1f} TB"
|
| 336 |
|
| 337 |
# --- Sidebar ---
|
| 338 |
with st.sidebar:
|
| 339 |
st.title(f"🍲 {get_text('title')}")
|
| 340 |
st.markdown(get_text('subtitle'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
st.markdown("---")
|
| 342 |
|
| 343 |
# Language selector
|
|
@@ -350,27 +444,19 @@ with st.sidebar:
|
|
| 350 |
st.session_state['language'] = 'te' if lang_choice == 'తెలుగు' else 'en'
|
| 351 |
|
| 352 |
st.info(get_text('sidebar_header'))
|
| 353 |
-
|
| 354 |
-
# Storage stats
|
| 355 |
-
stats = get_recipe_stats()
|
| 356 |
-
st.metric(get_text('total_recipes'), stats['total_recipes'])
|
| 357 |
-
st.metric(get_text('total_images'), stats['total_images'])
|
| 358 |
-
st.metric(get_text('storage_size'), format_storage_size(stats['storage_size']))
|
| 359 |
-
|
| 360 |
-
# Memory storage note
|
| 361 |
-
st.warning(get_text('memory_storage_note'))
|
| 362 |
|
| 363 |
# --- Main App Layout ---
|
| 364 |
st.title(get_text('title'))
|
| 365 |
|
| 366 |
-
|
|
|
|
| 367 |
get_text('tab_submit'),
|
| 368 |
get_text('tab_gallery'),
|
| 369 |
-
get_text('
|
| 370 |
-
get_text('
|
| 371 |
])
|
| 372 |
|
| 373 |
-
# --- Submission Form Tab ---
|
| 374 |
with tab_submit:
|
| 375 |
st.header(get_text('form_header'))
|
| 376 |
|
|
@@ -397,19 +483,12 @@ with tab_submit:
|
|
| 397 |
|
| 398 |
if submitted:
|
| 399 |
if not dish_name:
|
| 400 |
-
st.error(
|
| 401 |
else:
|
| 402 |
-
# Generate unique ID for this recipe
|
| 403 |
recipe_id = generate_recipe_id()
|
| 404 |
|
| 405 |
-
#
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
# Generate AI tags
|
| 409 |
-
ai_tags = simulate_ai_tagging(story, ingredients)
|
| 410 |
-
|
| 411 |
-
# Create new recipe with unique ID
|
| 412 |
-
new_recipe = {
|
| 413 |
"id": recipe_id,
|
| 414 |
"dish_name": dish_name,
|
| 415 |
"your_name": your_name,
|
|
@@ -418,444 +497,490 @@ with tab_submit:
|
|
| 418 |
"ingredients": ingredients,
|
| 419 |
"instructions": instructions,
|
| 420 |
"story": story,
|
| 421 |
-
"image_filename": image_filename,
|
| 422 |
-
"tags": ai_tags,
|
| 423 |
"submitted_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 424 |
}
|
| 425 |
|
| 426 |
-
#
|
| 427 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
st.success(f"{get_text('success_message')} (ID: {recipe_id})")
|
| 429 |
st.balloons()
|
| 430 |
-
|
| 431 |
-
# Show recipe details
|
| 432 |
-
with st.expander("Recipe Details", expanded=True):
|
| 433 |
-
st.write(f"**{get_text('recipe_id')}:** `{recipe_id}`")
|
| 434 |
-
st.write(f"**Storage:** In-memory (session-based)")
|
| 435 |
-
if image_filename:
|
| 436 |
-
st.write(f"**Image:** Stored as {image_filename}")
|
| 437 |
else:
|
| 438 |
st.error("Failed to save recipe. Please try again.")
|
| 439 |
|
| 440 |
-
# --- Community Gallery Tab ---
|
| 441 |
with tab_gallery:
|
| 442 |
st.header(get_text('gallery_header'))
|
| 443 |
|
| 444 |
-
#
|
| 445 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 446 |
|
| 447 |
with col1:
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
)
|
| 453 |
|
| 454 |
with col2:
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
)
|
| 461 |
|
| 462 |
-
with
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
type_options,
|
| 467 |
-
key="type_filter"
|
| 468 |
)
|
| 469 |
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
st.markdown("---")
|
| 478 |
|
| 479 |
-
#
|
| 480 |
-
|
| 481 |
-
filtered_recipes = filter_recipes(
|
| 482 |
-
recipes,
|
| 483 |
-
search_term,
|
| 484 |
-
district_filter,
|
| 485 |
-
type_filter
|
| 486 |
-
)
|
| 487 |
|
| 488 |
-
if
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
st.write(f"**{len(filtered_recipes)}** recipes found")
|
| 495 |
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
border-left: 5px solid #ff6b6b;
|
| 507 |
-
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 508 |
-
}
|
| 509 |
-
</style>
|
| 510 |
-
""", unsafe_allow_html=True)
|
| 511 |
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
with col_header2:
|
| 522 |
-
if recipe.get('recipe_type'):
|
| 523 |
-
st.info(recipe['recipe_type'])
|
| 524 |
-
|
| 525 |
-
with col_header3:
|
| 526 |
-
if recipe.get('submitted_date'):
|
| 527 |
-
st.caption(f"{get_text('submitted_on')}: {recipe['submitted_date'][:10]}")
|
| 528 |
-
|
| 529 |
-
# Metadata
|
| 530 |
-
metadata_parts = []
|
| 531 |
-
if recipe.get('your_name'):
|
| 532 |
-
metadata_parts.append(f"**{recipe['your_name']}**")
|
| 533 |
-
if recipe.get('district'):
|
| 534 |
-
metadata_parts.append(f"{get_text('from_district')} **{recipe['district']}**")
|
| 535 |
-
|
| 536 |
-
if metadata_parts:
|
| 537 |
-
st.caption(f"{get_text('submitted_by')} " + " • ".join(metadata_parts))
|
| 538 |
-
|
| 539 |
-
# Tags
|
| 540 |
-
if recipe.get('tags'):
|
| 541 |
-
tag_display = " ".join([f"`{tag}`" for tag in recipe['tags']])
|
| 542 |
-
st.markdown(tag_display)
|
| 543 |
-
|
| 544 |
-
# Content
|
| 545 |
-
left_col, right_col = st.columns([1, 1.2])
|
| 546 |
-
|
| 547 |
-
with left_col:
|
| 548 |
-
if recipe.get('image_filename'):
|
| 549 |
-
try:
|
| 550 |
-
image_data = load_image_from_memory(recipe['image_filename'])
|
| 551 |
-
if image_data:
|
| 552 |
-
st.image(image_data, caption=recipe['dish_name'], use_column_width=True)
|
| 553 |
-
else:
|
| 554 |
-
st.image("https://placehold.co/400x300/E8F4FD/31343C?text=Image+Not+Found")
|
| 555 |
-
except:
|
| 556 |
-
st.image("https://placehold.co/400x300/E8F4FD/31343C?text=Error+Loading+Image")
|
| 557 |
-
else:
|
| 558 |
-
st.image("https://placehold.co/400x300/E8F4FD/31343C?text=No+Image+Available")
|
| 559 |
-
|
| 560 |
-
with right_col:
|
| 561 |
-
if recipe.get('story'):
|
| 562 |
-
with st.expander(get_text('story_title')):
|
| 563 |
-
st.write(recipe['story'])
|
| 564 |
-
|
| 565 |
-
with st.expander(get_text('ingredients_title')):
|
| 566 |
-
if recipe.get('ingredients'):
|
| 567 |
-
ingredients_list = recipe['ingredients'].split('\n')
|
| 568 |
-
for ingredient in ingredients_list:
|
| 569 |
-
if ingredient.strip():
|
| 570 |
-
st.write(f"• {ingredient.strip()}")
|
| 571 |
-
else:
|
| 572 |
-
st.write("No ingredients listed.")
|
| 573 |
-
|
| 574 |
-
with st.expander(get_text('instructions_title'), expanded=True):
|
| 575 |
-
st.write(recipe.get('instructions', 'No instructions provided.'))
|
| 576 |
-
|
| 577 |
-
st.markdown('</div>', unsafe_allow_html=True)
|
| 578 |
|
| 579 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
|
| 581 |
# --- Analytics Tab ---
|
| 582 |
with tab_analytics:
|
| 583 |
-
st.header("📊 Recipe Analytics")
|
| 584 |
|
| 585 |
-
|
|
|
|
|
|
|
| 586 |
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
st.
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
st.
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
with col2:
|
| 644 |
-
st.subheader(get_text('popular_types'))
|
| 645 |
-
type_counts = Counter(r.get('recipe_type', 'Unknown') for r in recipes)
|
| 646 |
-
if type_counts:
|
| 647 |
-
type_df = pd.DataFrame(list(type_counts.items()), columns=['Type', 'Count'])
|
| 648 |
-
st.bar_chart(type_df.set_index('Type'))
|
| 649 |
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
|
|
|
|
|
|
| 655 |
|
| 656 |
-
|
| 657 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 658 |
with col1:
|
| 659 |
st.write(f"**{recipe['dish_name']}**")
|
|
|
|
|
|
|
| 660 |
with col2:
|
| 661 |
-
st.write(recipe.get('
|
|
|
|
|
|
|
| 662 |
with col3:
|
| 663 |
-
st.write(recipe.get('submitted_date', 'Unknown')[:10]
|
| 664 |
-
with col4:
|
| 665 |
-
st.code(recipe.get('id', 'N/A'), language=None)
|
| 666 |
-
|
| 667 |
-
# Export functionality
|
| 668 |
-
st.markdown("---")
|
| 669 |
-
col1, col2 = st.columns(2)
|
| 670 |
-
|
| 671 |
-
with col1:
|
| 672 |
-
if st.button(get_text('export_recipes')):
|
| 673 |
-
csv_data = export_recipes_to_csv(recipes)
|
| 674 |
-
if csv_data is not None:
|
| 675 |
-
csv_string = csv_data.to_csv(index=False)
|
| 676 |
-
st.download_button(
|
| 677 |
-
label="Download CSV",
|
| 678 |
-
data=csv_string,
|
| 679 |
-
file_name=f"ruchi_chudu_recipes_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 680 |
-
mime="text/csv"
|
| 681 |
-
)
|
| 682 |
-
else:
|
| 683 |
-
st.error("No recipes to export")
|
| 684 |
-
|
| 685 |
-
with col2:
|
| 686 |
-
if st.button(get_text('backup_data')):
|
| 687 |
-
try:
|
| 688 |
-
# Create a comprehensive JSON backup
|
| 689 |
-
backup_data = {
|
| 690 |
-
"recipes": recipes,
|
| 691 |
-
"images": st.session_state.get('recipe_images', {}),
|
| 692 |
-
"exported_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 693 |
-
"total_recipes": len(recipes),
|
| 694 |
-
"total_images": len(st.session_state.get('recipe_images', {}))
|
| 695 |
-
}
|
| 696 |
-
|
| 697 |
-
backup_json = json.dumps(backup_data, ensure_ascii=False, indent=2)
|
| 698 |
-
|
| 699 |
-
st.download_button(
|
| 700 |
-
label="Download JSON Backup",
|
| 701 |
-
data=backup_json,
|
| 702 |
-
file_name=f"ruchi_chudu_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
|
| 703 |
-
mime="application/json"
|
| 704 |
-
)
|
| 705 |
-
st.success(get_text('backup_created'))
|
| 706 |
-
except Exception as e:
|
| 707 |
-
st.error(f"Error creating backup: {e}")
|
| 708 |
-
|
| 709 |
-
# --- Management Tab ---
|
| 710 |
-
with tab_manage:
|
| 711 |
-
st.header(get_text('manage_header'))
|
| 712 |
-
|
| 713 |
-
recipes = load_all_recipes_from_memory()
|
| 714 |
-
|
| 715 |
-
if not recipes:
|
| 716 |
-
st.info("No recipes to manage yet.")
|
| 717 |
-
else:
|
| 718 |
-
st.write(f"Managing **{len(recipes)}** recipes")
|
| 719 |
-
|
| 720 |
-
# Search for specific recipe to manage
|
| 721 |
-
search_manage = st.text_input("Search for recipe to manage", placeholder="Enter recipe name or ID...")
|
| 722 |
-
|
| 723 |
-
# Filter recipes for management
|
| 724 |
-
manage_recipes = recipes
|
| 725 |
-
if search_manage:
|
| 726 |
-
manage_recipes = [r for r in recipes if
|
| 727 |
-
search_manage.lower() in r['dish_name'].lower() or
|
| 728 |
-
search_manage.lower() in r.get('id', '').lower()]
|
| 729 |
-
|
| 730 |
-
if not manage_recipes and search_manage:
|
| 731 |
-
st.warning("No recipes found matching your search.")
|
| 732 |
-
|
| 733 |
-
# Display recipes for management
|
| 734 |
-
for recipe in manage_recipes[:20]: # Limit to 20 for performance
|
| 735 |
-
with st.expander(f"🍽️ {recipe['dish_name']} (ID: {recipe.get('id', 'N/A')})"):
|
| 736 |
-
col1, col2, col3 = st.columns([2, 1, 1])
|
| 737 |
-
|
| 738 |
-
with col1:
|
| 739 |
-
st.write(f"**Submitted by:** {recipe.get('your_name', 'Anonymous')}")
|
| 740 |
-
st.write(f"**District:** {recipe.get('district', 'Not specified')}")
|
| 741 |
-
st.write(f"**Type:** {recipe.get('recipe_type', 'Not specified')}")
|
| 742 |
-
st.write(f"**Submitted:** {recipe.get('submitted_date', 'Unknown')}")
|
| 743 |
-
|
| 744 |
-
with col2:
|
| 745 |
-
st.write(f"**Storage Location:**")
|
| 746 |
-
st.code("Session Memory")
|
| 747 |
-
|
| 748 |
-
if recipe.get('image_filename'):
|
| 749 |
-
st.write(f"**Image:**")
|
| 750 |
-
st.code(f"Memory: {recipe['image_filename']}")
|
| 751 |
-
|
| 752 |
-
# Show approximate size
|
| 753 |
-
recipe_size = len(json.dumps(recipe, ensure_ascii=False).encode('utf-8'))
|
| 754 |
-
st.caption(f"Size: {format_storage_size(recipe_size)}")
|
| 755 |
-
|
| 756 |
-
with col3:
|
| 757 |
-
# Delete button with confirmation
|
| 758 |
-
delete_key = f"delete_{recipe.get('id', 'unknown')}"
|
| 759 |
-
confirm_key = f"confirm_delete_{recipe.get('id')}"
|
| 760 |
-
|
| 761 |
-
if st.button(f"🗑️ {get_text('delete_recipe')}", key=delete_key):
|
| 762 |
-
if st.session_state.get(confirm_key, False):
|
| 763 |
-
if delete_recipe_from_memory(recipe.get('id')):
|
| 764 |
-
st.success(get_text('recipe_deleted'))
|
| 765 |
-
st.rerun()
|
| 766 |
-
else:
|
| 767 |
-
st.error("Failed to delete recipe.")
|
| 768 |
-
else:
|
| 769 |
-
st.session_state[confirm_key] = True
|
| 770 |
-
st.warning(get_text('confirm_delete'))
|
| 771 |
-
|
| 772 |
-
if st.session_state.get(confirm_key, False):
|
| 773 |
-
if st.button(f"✅ Confirm Delete", key=f"confirm_{recipe.get('id')}"):
|
| 774 |
-
if delete_recipe_from_memory(recipe.get('id')):
|
| 775 |
-
st.success(get_text('recipe_deleted'))
|
| 776 |
-
# Clear the confirmation state
|
| 777 |
-
st.session_state[confirm_key] = False
|
| 778 |
-
st.rerun()
|
| 779 |
-
else:
|
| 780 |
-
st.error("Failed to delete recipe.")
|
| 781 |
-
|
| 782 |
-
if len(manage_recipes) > 20:
|
| 783 |
-
st.info(f"Showing first 20 recipes. Use search to find specific recipes. Total: {len(manage_recipes)}")
|
| 784 |
-
|
| 785 |
-
# Bulk operations
|
| 786 |
-
st.markdown("---")
|
| 787 |
-
st.subheader("🔧 Memory Management")
|
| 788 |
-
|
| 789 |
-
col1, col2, col3 = st.columns(3)
|
| 790 |
-
|
| 791 |
-
with col1:
|
| 792 |
-
if st.button("📊 Refresh Stats"):
|
| 793 |
-
stats = get_recipe_stats()
|
| 794 |
-
st.success("Stats refreshed!")
|
| 795 |
-
st.write(f"Total recipes: {stats['total_recipes']}")
|
| 796 |
-
st.write(f"Total images: {stats['total_images']}")
|
| 797 |
-
st.write(f"Memory usage: {format_storage_size(stats['storage_size'])}")
|
| 798 |
-
|
| 799 |
-
with col2:
|
| 800 |
-
if st.button("🔄 Reload Data"):
|
| 801 |
-
st.success("Data reloaded from session state!")
|
| 802 |
-
st.rerun()
|
| 803 |
-
|
| 804 |
-
with col3:
|
| 805 |
-
if st.button("🧹 Clear All Data", type="secondary"):
|
| 806 |
-
if st.button("⚠️ Confirm Clear All", type="secondary"):
|
| 807 |
-
st.session_state['recipes'] = []
|
| 808 |
-
st.session_state['recipe_images'] = {}
|
| 809 |
-
st.success("All data cleared!")
|
| 810 |
-
st.rerun()
|
| 811 |
-
|
| 812 |
-
# Session info
|
| 813 |
-
st.markdown("---")
|
| 814 |
-
st.subheader("📋 Session Information")
|
| 815 |
-
|
| 816 |
-
col1, col2 = st.columns(2)
|
| 817 |
-
|
| 818 |
-
with col1:
|
| 819 |
-
st.info("**Storage Type:** Session Memory")
|
| 820 |
-
st.caption("Data persists only during this browser session")
|
| 821 |
|
| 822 |
-
st.
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 835 |
st.markdown("---")
|
| 836 |
st.markdown("""
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
- **Export**: Use the export/backup features to save your data locally
|
| 844 |
-
- **Session Sharing**: Each user has their own independent session and data
|
| 845 |
-
|
| 846 |
-
**For Production Use**: Consider integrating with a database service or cloud storage for persistent data storage.
|
| 847 |
-
""")
|
| 848 |
-
|
| 849 |
-
# --- Debug Information (Optional) ---
|
| 850 |
-
if st.sidebar.checkbox("Show Debug Info"):
|
| 851 |
-
st.sidebar.markdown("---")
|
| 852 |
-
st.sidebar.subheader("🔧 Debug Info")
|
| 853 |
-
st.sidebar.write(f"Session recipes: {len(st.session_state.get('recipes', []))}")
|
| 854 |
-
st.sidebar.write(f"Session images: {len(st.session_state.get('recipe_images', {}))}")
|
| 855 |
-
st.sidebar.write(f"Current language: {st.session_state.get('language', 'en')}")
|
| 856 |
-
|
| 857 |
-
# Show session state keys
|
| 858 |
-
if st.sidebar.checkbox("Show all session keys"):
|
| 859 |
-
st.sidebar.write("Session state keys:")
|
| 860 |
-
for key in st.session_state.keys():
|
| 861 |
-
st.sidebar.caption(f"• {key}")
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
import pandas as pd
|
| 3 |
+
from collections import Counter
|
| 4 |
import json
|
| 5 |
+
import os
|
| 6 |
import uuid
|
| 7 |
from datetime import datetime
|
|
|
|
| 8 |
import base64
|
| 9 |
+
import glob
|
| 10 |
+
import re
|
| 11 |
+
from typing import List, Dict, Tuple
|
| 12 |
+
|
| 13 |
+
# AI/ML imports - lightweight transformers
|
| 14 |
+
try:
|
| 15 |
+
from sentence_transformers import SentenceTransformer
|
| 16 |
+
from transformers import pipeline, AutoTokenizer, AutoModelForSequenceClassification
|
| 17 |
+
import torch
|
| 18 |
+
import numpy as np
|
| 19 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 20 |
+
AI_AVAILABLE = True
|
| 21 |
+
except ImportError:
|
| 22 |
+
AI_AVAILABLE = False
|
| 23 |
+
st.warning("⚠️ AI features require additional packages. Install with: pip install sentence-transformers transformers torch scikit-learn")
|
| 24 |
|
| 25 |
# --- Page Configuration ---
|
| 26 |
st.set_page_config(
|
|
|
|
| 29 |
layout="wide"
|
| 30 |
)
|
| 31 |
|
| 32 |
+
# --- AI Model Management ---
|
| 33 |
+
@st.cache_resource
|
| 34 |
+
def load_ai_models():
|
| 35 |
+
"""Load AI models with caching to prevent reloading"""
|
| 36 |
+
if not AI_AVAILABLE:
|
| 37 |
+
return None, None, None
|
| 38 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
try:
|
| 40 |
+
# Lightweight sentence transformer for recipe similarity
|
| 41 |
+
similarity_model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 42 |
+
|
| 43 |
+
# Sentiment analysis for recipe stories
|
| 44 |
+
sentiment_analyzer = pipeline(
|
| 45 |
+
"sentiment-analysis",
|
| 46 |
+
model="cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 47 |
+
return_all_scores=True
|
| 48 |
+
)
|
| 49 |
|
| 50 |
+
# Text classification for cuisine type - using a lighter model
|
| 51 |
+
cuisine_classifier = pipeline(
|
| 52 |
+
"text-classification",
|
| 53 |
+
model="cardiffnlp/twitter-roberta-base-sentiment-latest" # Reuse sentiment model
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
return similarity_model, sentiment_analyzer, cuisine_classifier
|
| 57 |
except Exception as e:
|
| 58 |
+
st.error(f"Error loading AI models: {e}")
|
| 59 |
+
return None, None, None
|
| 60 |
+
|
| 61 |
+
# --- Enhanced AI Functions ---
|
| 62 |
+
def analyze_recipe_sentiment(story: str, sentiment_analyzer) -> Dict:
|
| 63 |
+
"""Analyze the emotional tone of recipe stories"""
|
| 64 |
+
if not story or not sentiment_analyzer:
|
| 65 |
+
return {"sentiment": "neutral", "confidence": 0.5, "emotion": "nostalgic"}
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
results = sentiment_analyzer(story[:500]) # Limit text length
|
| 69 |
+
|
| 70 |
+
# Map sentiment labels to readable format
|
| 71 |
+
sentiment_map = {
|
| 72 |
+
'LABEL_0': 'negative',
|
| 73 |
+
'LABEL_1': 'neutral',
|
| 74 |
+
'LABEL_2': 'positive',
|
| 75 |
+
'NEGATIVE': 'negative',
|
| 76 |
+
'NEUTRAL': 'neutral',
|
| 77 |
+
'POSITIVE': 'positive'
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
top_result = max(results[0], key=lambda x: x['score'])
|
| 81 |
+
sentiment = sentiment_map.get(top_result['label'], 'neutral')
|
| 82 |
+
confidence = top_result['score']
|
| 83 |
+
|
| 84 |
+
# Add emotion context for Telugu recipes
|
| 85 |
+
emotion_keywords = {
|
| 86 |
+
'joyful': ['celebration', 'festival', 'happy', 'joy', 'smile', 'laugh'],
|
| 87 |
+
'nostalgic': ['grandmother', 'childhood', 'memory', 'tradition', 'old', 'ammamma'],
|
| 88 |
+
'proud': ['special', 'unique', 'best', 'famous', 'perfect', 'excellent'],
|
| 89 |
+
'loving': ['family', 'mother', 'love', 'care', 'warmth', 'together']
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
story_lower = story.lower()
|
| 93 |
+
emotion = 'nostalgic' # default for recipe stories
|
| 94 |
+
for emotion_type, keywords in emotion_keywords.items():
|
| 95 |
+
if any(keyword in story_lower for keyword in keywords):
|
| 96 |
+
emotion = emotion_type
|
| 97 |
+
break
|
| 98 |
+
|
| 99 |
+
return {
|
| 100 |
+
"sentiment": sentiment,
|
| 101 |
+
"confidence": confidence,
|
| 102 |
+
"emotion": emotion
|
| 103 |
+
}
|
| 104 |
+
except Exception as e:
|
| 105 |
+
return {"sentiment": "neutral", "confidence": 0.5, "emotion": "nostalgic"}
|
| 106 |
|
| 107 |
+
def extract_smart_tags(recipe_data: Dict, similarity_model) -> List[str]:
|
| 108 |
+
"""Extract intelligent tags using NLP and pattern matching"""
|
| 109 |
+
tags = set()
|
| 110 |
+
|
| 111 |
+
# Combine text for analysis
|
| 112 |
+
text_content = f"{recipe_data.get('dish_name', '')} {recipe_data.get('ingredients', '')} {recipe_data.get('instructions', '')} {recipe_data.get('story', '')}"
|
| 113 |
+
|
| 114 |
+
# Traditional keyword-based tagging (enhanced)
|
| 115 |
+
keyword_categories = {
|
| 116 |
+
'spice_level': {
|
| 117 |
+
'mild': ['mild', 'less spicy', 'children', 'sweet'],
|
| 118 |
+
'medium': ['medium', 'moderate', 'balanced'],
|
| 119 |
+
'hot': ['spicy', 'hot', 'chili', 'pepper', 'karam', 'guntur'],
|
| 120 |
+
'very_hot': ['very spicy', 'extra hot', 'burning', 'fire']
|
| 121 |
+
},
|
| 122 |
+
'cooking_time': {
|
| 123 |
+
'quick': ['quick', 'fast', 'minutes', '15 min', '20 min', 'instant'],
|
| 124 |
+
'medium': ['30 min', '45 min', '1 hour', 'moderate time'],
|
| 125 |
+
'slow': ['hours', 'slow cook', 'overnight', 'patience']
|
| 126 |
+
},
|
| 127 |
+
'meal_type': {
|
| 128 |
+
'breakfast': ['breakfast', 'morning', 'tiffin', 'idli', 'dosa'],
|
| 129 |
+
'lunch': ['lunch', 'meal', 'rice', 'curry', 'sambar'],
|
| 130 |
+
'dinner': ['dinner', 'night', 'heavy'],
|
| 131 |
+
'snack': ['snack', 'evening', 'tea time', 'biscuit']
|
| 132 |
+
},
|
| 133 |
+
'health': {
|
| 134 |
+
'healthy': ['healthy', 'nutritious', 'vitamin', 'protein', 'fiber'],
|
| 135 |
+
'diabetic_friendly': ['sugar free', 'diabetic', 'low sugar', 'jaggery'],
|
| 136 |
+
'high_protein': ['protein', 'dal', 'lentils', 'sprouts'],
|
| 137 |
+
'low_fat': ['oil free', 'steamed', 'boiled', 'grilled']
|
| 138 |
+
},
|
| 139 |
+
'occasion': {
|
| 140 |
+
'festival': ['festival', 'celebration', 'ugadi', 'sankranti', 'diwali'],
|
| 141 |
+
'wedding': ['wedding', 'marriage', 'function', 'ceremony'],
|
| 142 |
+
'everyday': ['daily', 'regular', 'simple', 'routine'],
|
| 143 |
+
'special': ['special', 'guest', 'party', 'occasion']
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
text_lower = text_content.lower()
|
| 148 |
+
|
| 149 |
+
for category, subcategories in keyword_categories.items():
|
| 150 |
+
for tag, keywords in subcategories.items():
|
| 151 |
+
if any(keyword in text_lower for keyword in keywords):
|
| 152 |
+
tags.add(tag.replace('_', ' ').title())
|
| 153 |
+
|
| 154 |
+
# Telugu-specific patterns
|
| 155 |
+
telugu_patterns = {
|
| 156 |
+
'Regional': ['Andhra', 'Telangana', 'Hyderabad', 'Vijayawada', 'Guntur'],
|
| 157 |
+
'Traditional': ['traditional', 'authentic', 'original', 'sampradayam'],
|
| 158 |
+
'Modern': ['fusion', 'modern', 'new style', 'innovative'],
|
| 159 |
+
'Street Food': ['street', 'vendor', 'roadside', 'chat']
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
for tag, patterns in telugu_patterns.items():
|
| 163 |
+
if any(pattern.lower() in text_lower for pattern in patterns):
|
| 164 |
+
tags.add(tag)
|
| 165 |
+
|
| 166 |
+
# Ingredient-based classification
|
| 167 |
+
ingredient_text = recipe_data.get('ingredients', '').lower()
|
| 168 |
+
|
| 169 |
+
# Detect vegetarian/non-vegetarian
|
| 170 |
+
non_veg_ingredients = ['chicken', 'mutton', 'fish', 'egg', 'meat', 'prawn', 'crab']
|
| 171 |
+
if any(ingredient in ingredient_text for ingredient in non_veg_ingredients):
|
| 172 |
+
tags.add('Non-Vegetarian')
|
| 173 |
+
else:
|
| 174 |
+
tags.add('Vegetarian')
|
| 175 |
+
|
| 176 |
+
# Detect main ingredients
|
| 177 |
+
main_ingredients = {
|
| 178 |
+
'Rice Based': ['rice', 'biryani', 'pulao', 'annam'],
|
| 179 |
+
'Dal Based': ['dal', 'lentil', 'pappu', 'sambar'],
|
| 180 |
+
'Vegetable': ['vegetable', 'curry', 'fry', 'kura'],
|
| 181 |
+
'Sweet': ['sweet', 'sugar', 'jaggery', 'dessert', 'halwa'],
|
| 182 |
+
'Pickle': ['pickle', 'achar', 'pachadi']
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
for tag, ingredients in main_ingredients.items():
|
| 186 |
+
if any(ingredient in ingredient_text for ingredient in ingredients):
|
| 187 |
+
tags.add(tag)
|
| 188 |
+
|
| 189 |
+
return list(tags)
|
| 190 |
|
| 191 |
+
def find_similar_recipes(current_recipe: Dict, all_recipes: List[Dict], similarity_model, top_k=3) -> List[Tuple[Dict, float]]:
|
| 192 |
+
"""Find similar recipes using semantic similarity"""
|
| 193 |
+
if not similarity_model or len(all_recipes) < 2:
|
| 194 |
+
return []
|
| 195 |
+
|
| 196 |
try:
|
| 197 |
+
# Create text representations
|
| 198 |
+
current_text = f"{current_recipe.get('dish_name', '')} {current_recipe.get('ingredients', '')} {current_recipe.get('recipe_type', '')}"
|
| 199 |
+
|
| 200 |
+
recipe_texts = []
|
| 201 |
+
valid_recipes = []
|
| 202 |
+
|
| 203 |
+
for recipe in all_recipes:
|
| 204 |
+
if recipe.get('id') != current_recipe.get('id'): # Exclude current recipe
|
| 205 |
+
recipe_text = f"{recipe.get('dish_name', '')} {recipe.get('ingredients', '')} {recipe.get('recipe_type', '')}"
|
| 206 |
+
recipe_texts.append(recipe_text)
|
| 207 |
+
valid_recipes.append(recipe)
|
| 208 |
+
|
| 209 |
+
if not recipe_texts:
|
| 210 |
+
return []
|
| 211 |
|
| 212 |
+
# Generate embeddings
|
| 213 |
+
current_embedding = similarity_model.encode([current_text])
|
| 214 |
+
recipe_embeddings = similarity_model.encode(recipe_texts)
|
| 215 |
+
|
| 216 |
+
# Calculate similarities
|
| 217 |
+
similarities = cosine_similarity(current_embedding, recipe_embeddings)[0]
|
| 218 |
+
|
| 219 |
+
# Get top similar recipes
|
| 220 |
+
similar_indices = np.argsort(similarities)[::-1][:top_k]
|
| 221 |
+
|
| 222 |
+
similar_recipes = []
|
| 223 |
+
for idx in similar_indices:
|
| 224 |
+
if similarities[idx] > 0.3: # Minimum similarity threshold
|
| 225 |
+
similar_recipes.append((valid_recipes[idx], similarities[idx]))
|
| 226 |
+
|
| 227 |
+
return similar_recipes
|
| 228 |
+
except Exception as e:
|
| 229 |
+
st.error(f"Error finding similar recipes: {e}")
|
| 230 |
+
return []
|
| 231 |
+
|
| 232 |
+
def generate_cooking_tips(recipe_data: Dict) -> List[str]:
|
| 233 |
+
"""Generate contextual cooking tips based on recipe content"""
|
| 234 |
+
tips = []
|
| 235 |
+
|
| 236 |
+
ingredients = recipe_data.get('ingredients', '').lower()
|
| 237 |
+
instructions = recipe_data.get('instructions', '').lower()
|
| 238 |
+
dish_name = recipe_data.get('dish_name', '').lower()
|
| 239 |
+
|
| 240 |
+
# Ingredient-specific tips
|
| 241 |
+
if 'oil' in ingredients:
|
| 242 |
+
tips.append("🔥 Tip: Heat oil until it shimmers but doesn't smoke for best results")
|
| 243 |
+
|
| 244 |
+
if 'onion' in ingredients:
|
| 245 |
+
tips.append("🧅 Tip: Soak sliced onions in cold water for 10 minutes to reduce tears while cutting")
|
| 246 |
+
|
| 247 |
+
if any(spice in ingredients for spice in ['turmeric', 'chili', 'coriander']):
|
| 248 |
+
tips.append("🌶️ Tip: Dry roast spices lightly before grinding for enhanced flavor")
|
| 249 |
+
|
| 250 |
+
if 'rice' in ingredients:
|
| 251 |
+
tips.append("🍚 Tip: Soak rice for 20-30 minutes before cooking for better texture")
|
| 252 |
+
|
| 253 |
+
# Cooking method tips
|
| 254 |
+
if 'fry' in instructions or 'deep fry' in instructions:
|
| 255 |
+
tips.append("🍳 Tip: Maintain oil temperature at 350°F (175°C) for perfect frying")
|
| 256 |
+
|
| 257 |
+
if 'boil' in instructions:
|
| 258 |
+
tips.append("💧 Tip: Add salt to boiling water for vegetables to retain color and nutrients")
|
| 259 |
+
|
| 260 |
+
# Dish-specific tips
|
| 261 |
+
if 'curry' in dish_name:
|
| 262 |
+
tips.append("🍛 Tip: Let curry rest for 10 minutes after cooking to allow flavors to meld")
|
| 263 |
+
|
| 264 |
+
if 'pickle' in dish_name:
|
| 265 |
+
tips.append("🥒 Tip: Store pickle in airtight containers and use dry spoons to prevent spoilage")
|
| 266 |
+
|
| 267 |
+
# Regional tips
|
| 268 |
+
tips.append("🏠 Traditional Tip: Taste and adjust seasoning as family preferences vary by region")
|
| 269 |
+
|
| 270 |
+
return tips[:3] # Return top 3 tips
|
| 271 |
+
|
| 272 |
+
# --- Data Storage Functions ---
|
| 273 |
+
RECIPES_FOLDER = "recipes"
|
| 274 |
+
IMAGES_FOLDER = "recipe_images"
|
| 275 |
+
|
| 276 |
+
def ensure_folders_exist():
|
| 277 |
+
"""Create necessary folders if they don't exist"""
|
| 278 |
+
os.makedirs(RECIPES_FOLDER, exist_ok=True)
|
| 279 |
+
os.makedirs(IMAGES_FOLDER, exist_ok=True)
|
| 280 |
+
|
| 281 |
+
def generate_recipe_id():
|
| 282 |
+
"""Generate a unique ID for each recipe"""
|
| 283 |
+
return str(uuid.uuid4())[:8]
|
| 284 |
+
|
| 285 |
+
def save_recipe(recipe_data):
|
| 286 |
+
"""Save individual recipe to its own JSON file"""
|
| 287 |
+
try:
|
| 288 |
+
ensure_folders_exist()
|
| 289 |
+
recipe_id = recipe_data['id']
|
| 290 |
+
filename = f"recipe_{recipe_id}.json"
|
| 291 |
+
filepath = os.path.join(RECIPES_FOLDER, filename)
|
| 292 |
|
| 293 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
| 294 |
+
json.dump(recipe_data, f, ensure_ascii=False, indent=2)
|
| 295 |
return True
|
| 296 |
except Exception as e:
|
| 297 |
+
st.error(f"Error saving recipe: {e}")
|
| 298 |
return False
|
| 299 |
|
| 300 |
+
def load_all_recipes():
|
| 301 |
+
"""Load all recipes from individual JSON files"""
|
| 302 |
+
recipes = []
|
| 303 |
try:
|
| 304 |
+
ensure_folders_exist()
|
| 305 |
+
recipe_files = glob.glob(os.path.join(RECIPES_FOLDER, "recipe_*.json"))
|
| 306 |
|
| 307 |
+
for filepath in recipe_files:
|
| 308 |
+
try:
|
| 309 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 310 |
+
recipe = json.load(f)
|
| 311 |
+
recipes.append(recipe)
|
| 312 |
+
except Exception as e:
|
| 313 |
+
continue
|
| 314 |
|
| 315 |
+
recipes.sort(key=lambda x: x.get('submitted_date', ''), reverse=True)
|
|
|
|
| 316 |
|
| 317 |
+
except Exception as e:
|
| 318 |
+
st.error(f"Error loading recipes: {e}")
|
| 319 |
+
|
| 320 |
+
return recipes
|
|
|
|
|
|
|
|
|
|
| 321 |
|
| 322 |
# --- Language and Text ---
|
| 323 |
TEXT = {
|
| 324 |
"en": {
|
| 325 |
"title": "Ruchi Chudu",
|
| 326 |
+
"subtitle": "AI-Powered Community Cookbook to Preserve Telugu Cuisine",
|
| 327 |
"language_select": "Choose Language",
|
| 328 |
"sidebar_header": "Contribute Your Recipe!",
|
| 329 |
"tab_submit": "📝 Submit Your Recipe",
|
| 330 |
+
"tab_gallery": "🍲 Community Gallery",
|
| 331 |
+
"tab_ai_insights": "🤖 AI Recipe Insights",
|
| 332 |
"tab_analytics": "📊 Recipe Analytics",
|
|
|
|
| 333 |
"form_header": "Tell us about your dish",
|
| 334 |
"dish_name": "Dish Name",
|
| 335 |
+
"your_name": "Your Name",
|
| 336 |
"district": "Your District",
|
| 337 |
"select_district": "--- Select a District ---",
|
| 338 |
"recipe_type": "Recipe Type (e.g., Curry, Pickle, Snack)",
|
|
|
|
| 343 |
"submit_button": "Submit Recipe",
|
| 344 |
"success_message": "Thank you! Your recipe has been submitted successfully.",
|
| 345 |
"gallery_header": "Explore Recipes from the Community",
|
| 346 |
+
"ai_analysis": "🤖 AI Analysis",
|
| 347 |
+
"similar_recipes": "👥 Similar Recipes",
|
| 348 |
+
"cooking_tips": "💡 Smart Cooking Tips",
|
| 349 |
+
"recipe_sentiment": "😊 Recipe Story Mood",
|
| 350 |
+
"search_placeholder": "Search recipes or ask AI...",
|
| 351 |
+
"ai_search_help": "Try: 'spicy vegetarian curry' or 'quick breakfast recipe'",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
},
|
| 353 |
"te": {
|
| 354 |
+
"title": "రుచి చూడు",
|
| 355 |
+
"subtitle": "AI శక్తితో మన తెలుగు వంటల సంస్కృతిని కాపాడడం",
|
| 356 |
"language_select": "భాషను ఎంచుకోండి",
|
| 357 |
"sidebar_header": "మీ వంటకాన్ని పంపండి!",
|
| 358 |
"tab_submit": "📝 మీ వంటకాన్ని పంపండి",
|
| 359 |
"tab_gallery": "🍲 కమ్యూనిటీ గ్యాలరీ",
|
| 360 |
+
"tab_ai_insights": "🤖 AI వంటకాల విశ్లేషణ",
|
| 361 |
"tab_analytics": "📊 వంటకాల విశ్లేషణ",
|
|
|
|
| 362 |
"form_header": "మీ వంటకం గురించి చెప్పండి",
|
| 363 |
"dish_name": "వంటకం పేరు",
|
| 364 |
"your_name": "మీ పేరు",
|
| 365 |
+
"district": "మీ జిల్లా",
|
| 366 |
"select_district": "--- జిల్లాను ఎంచుకోండి ---",
|
| 367 |
"recipe_type": "వంటకం రకం (ఉదా. కూర, పచ్చడి, స్నాక్)",
|
| 368 |
"ingredients": "కావలసినవి (ఒకదానికి ఒకటి)",
|
| 369 |
"instructions": "తయారీ విధానం",
|
| 370 |
"story": "ఈ వంటతో మీ కథ (మీ జ్ఞాపకాలు, కుటుంబ సంప్రదాయాలు మొదలైనవి)",
|
| 371 |
"image_upload": "వంటకం ఫోటోను అప్లోడ్ చేయండి (ఐచ్ఛికం)",
|
| 372 |
+
"submit_button": "వంటకాన్ని పంపండి",
|
| 373 |
"success_message": "ధన్యవాదాలు! మీ వంటకం విజయవంతంగా సమర్పించబడింది.",
|
| 374 |
+
"gallery_header": "కమ్యూనిటీ నుంది వంటకాలను అన్వేషించండి",
|
| 375 |
+
"ai_analysis": "🤖 AI విశ్లేషణ",
|
| 376 |
+
"similar_recipes": "👥 సమాన వంటకాలు",
|
| 377 |
+
"cooking_tips": "💡 తెలివైన వంట చిట్కాలు",
|
| 378 |
+
"recipe_sentiment": "😊 వంటకం కథ భావం",
|
| 379 |
+
"search_placeholder": "వంటకాలను వెతకండి లేదా AI ని అడగండి...",
|
| 380 |
+
"ai_search_help": "ప్రయత్నించండ���: 'కారంగా వెజిటేరియన్ కూర' లేదా 'తొందర అల్పాహారం'",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
}
|
| 383 |
|
|
|
|
| 402 |
if 'language' not in st.session_state:
|
| 403 |
st.session_state['language'] = 'en'
|
| 404 |
if 'recipes' not in st.session_state:
|
| 405 |
+
st.session_state['recipes'] = load_all_recipes()
|
| 406 |
+
if 'ai_models_loaded' not in st.session_state:
|
| 407 |
+
st.session_state['ai_models_loaded'] = False
|
| 408 |
|
| 409 |
# --- Helper Functions ---
|
| 410 |
def get_text(key):
|
| 411 |
"""Fetches text from the dictionary based on the selected language."""
|
| 412 |
return TEXT[st.session_state['language']][key]
|
| 413 |
|
| 414 |
+
# --- Load AI Models ---
|
| 415 |
+
similarity_model, sentiment_analyzer, cuisine_classifier = None, None, None
|
| 416 |
+
if AI_AVAILABLE and not st.session_state.get('ai_models_loaded', False):
|
| 417 |
+
with st.spinner("🤖 Loading AI models for the first time... This may take a moment."):
|
| 418 |
+
similarity_model, sentiment_analyzer, cuisine_classifier = load_ai_models()
|
| 419 |
+
if similarity_model:
|
| 420 |
+
st.session_state['ai_models_loaded'] = True
|
| 421 |
+
st.success("✅ AI models loaded successfully!")
|
| 422 |
+
else:
|
| 423 |
+
similarity_model, sentiment_analyzer, cuisine_classifier = load_ai_models()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
|
| 425 |
# --- Sidebar ---
|
| 426 |
with st.sidebar:
|
| 427 |
st.title(f"🍲 {get_text('title')}")
|
| 428 |
st.markdown(get_text('subtitle'))
|
| 429 |
+
|
| 430 |
+
if AI_AVAILABLE and similarity_model:
|
| 431 |
+
st.success("🤖 AI Features: Active")
|
| 432 |
+
else:
|
| 433 |
+
st.warning("🤖 AI Features: Limited")
|
| 434 |
+
|
| 435 |
st.markdown("---")
|
| 436 |
|
| 437 |
# Language selector
|
|
|
|
| 444 |
st.session_state['language'] = 'te' if lang_choice == 'తెలుగు' else 'en'
|
| 445 |
|
| 446 |
st.info(get_text('sidebar_header'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
|
| 448 |
# --- Main App Layout ---
|
| 449 |
st.title(get_text('title'))
|
| 450 |
|
| 451 |
+
# Create tabs including the new AI insights tab
|
| 452 |
+
tab_submit, tab_gallery, tab_ai, tab_analytics = st.tabs([
|
| 453 |
get_text('tab_submit'),
|
| 454 |
get_text('tab_gallery'),
|
| 455 |
+
get_text('tab_ai_insights'),
|
| 456 |
+
get_text('tab_analytics')
|
| 457 |
])
|
| 458 |
|
| 459 |
+
# --- Submission Form Tab (Enhanced with AI) ---
|
| 460 |
with tab_submit:
|
| 461 |
st.header(get_text('form_header'))
|
| 462 |
|
|
|
|
| 483 |
|
| 484 |
if submitted:
|
| 485 |
if not dish_name:
|
| 486 |
+
st.error("Please enter a dish name before submitting.")
|
| 487 |
else:
|
|
|
|
| 488 |
recipe_id = generate_recipe_id()
|
| 489 |
|
| 490 |
+
# Create recipe data for AI analysis
|
| 491 |
+
recipe_data = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
"id": recipe_id,
|
| 493 |
"dish_name": dish_name,
|
| 494 |
"your_name": your_name,
|
|
|
|
| 497 |
"ingredients": ingredients,
|
| 498 |
"instructions": instructions,
|
| 499 |
"story": story,
|
|
|
|
|
|
|
| 500 |
"submitted_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 501 |
}
|
| 502 |
|
| 503 |
+
# AI Analysis
|
| 504 |
+
if AI_AVAILABLE and sentiment_analyzer:
|
| 505 |
+
with st.spinner("🤖 AI is analyzing your recipe..."):
|
| 506 |
+
# Sentiment analysis
|
| 507 |
+
sentiment_data = analyze_recipe_sentiment(story, sentiment_analyzer)
|
| 508 |
+
|
| 509 |
+
# Smart tag extraction
|
| 510 |
+
ai_tags = extract_smart_tags(recipe_data, similarity_model)
|
| 511 |
+
|
| 512 |
+
# Add AI analysis to recipe data
|
| 513 |
+
recipe_data.update({
|
| 514 |
+
"ai_tags": ai_tags,
|
| 515 |
+
"sentiment_analysis": sentiment_data,
|
| 516 |
+
"cooking_tips": generate_cooking_tips(recipe_data)
|
| 517 |
+
})
|
| 518 |
+
|
| 519 |
+
# Show AI insights
|
| 520 |
+
st.subheader("🤖 AI Analysis Results")
|
| 521 |
+
|
| 522 |
+
col1, col2 = st.columns(2)
|
| 523 |
+
with col1:
|
| 524 |
+
st.metric("Recipe Mood", sentiment_data['emotion'].title(), f"{sentiment_data['confidence']:.0%} confidence")
|
| 525 |
+
st.write("**AI Tags:**", ", ".join([f"`{tag}`" for tag in ai_tags]))
|
| 526 |
+
|
| 527 |
+
with col2:
|
| 528 |
+
st.write("**Smart Cooking Tips:**")
|
| 529 |
+
for tip in recipe_data['cooking_tips']:
|
| 530 |
+
st.write(f"• {tip}")
|
| 531 |
+
else:
|
| 532 |
+
# Fallback basic tagging
|
| 533 |
+
recipe_data["ai_tags"] = ["Community Recipe"]
|
| 534 |
+
recipe_data["sentiment_analysis"] = {"sentiment": "positive", "emotion": "nostalgic"}
|
| 535 |
+
recipe_data["cooking_tips"] = ["Follow the recipe step by step for best results."]
|
| 536 |
+
|
| 537 |
+
# Save recipe
|
| 538 |
+
if save_recipe(recipe_data):
|
| 539 |
+
st.session_state.recipes.insert(0, recipe_data)
|
| 540 |
st.success(f"{get_text('success_message')} (ID: {recipe_id})")
|
| 541 |
st.balloons()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
else:
|
| 543 |
st.error("Failed to save recipe. Please try again.")
|
| 544 |
|
| 545 |
+
# --- Community Gallery Tab (Enhanced with AI) ---
|
| 546 |
with tab_gallery:
|
| 547 |
st.header(get_text('gallery_header'))
|
| 548 |
|
| 549 |
+
# AI-powered search
|
| 550 |
+
if AI_AVAILABLE and similarity_model:
|
| 551 |
+
st.subheader("🔍 AI-Powered Recipe Search")
|
| 552 |
+
search_query = st.text_input(
|
| 553 |
+
"Search recipes with natural language:",
|
| 554 |
+
placeholder=get_text('ai_search_help'),
|
| 555 |
+
help="Ask in natural language like 'show me spicy vegetarian curries' or 'quick breakfast ideas'"
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
if search_query and st.session_state.recipes:
|
| 559 |
+
with st.spinner("🤖 AI is searching..."):
|
| 560 |
+
# Use semantic search
|
| 561 |
+
query_embedding = similarity_model.encode([search_query])
|
| 562 |
+
recipe_texts = []
|
| 563 |
+
valid_recipes = []
|
| 564 |
+
|
| 565 |
+
for recipe in st.session_state.recipes:
|
| 566 |
+
recipe_text = f"{recipe.get('dish_name', '')} {recipe.get('ingredients', '')} {recipe.get('recipe_type', '')} {' '.join(recipe.get('ai_tags', []))}"
|
| 567 |
+
recipe_texts.append(recipe_text)
|
| 568 |
+
valid_recipes.append(recipe)
|
| 569 |
+
|
| 570 |
+
if recipe_texts:
|
| 571 |
+
recipe_embeddings = similarity_model.encode(recipe_texts)
|
| 572 |
+
similarities = cosine_similarity(query_embedding, recipe_embeddings)[0]
|
| 573 |
+
|
| 574 |
+
# Get top matches
|
| 575 |
+
top_indices = np.argsort(similarities)[::-1][:10]
|
| 576 |
+
matching_recipes = []
|
| 577 |
+
|
| 578 |
+
for idx in top_indices:
|
| 579 |
+
if similarities[idx] > 0.2: # Minimum similarity
|
| 580 |
+
matching_recipes.append((valid_recipes[idx], similarities[idx]))
|
| 581 |
+
|
| 582 |
+
if matching_recipes:
|
| 583 |
+
st.success(f"🎯 Found {len(matching_recipes)} recipes matching your search")
|
| 584 |
+
display_recipes = [recipe for recipe, _ in matching_recipes]
|
| 585 |
+
else:
|
| 586 |
+
st.info("No recipes found matching your search. Try different keywords.")
|
| 587 |
+
display_recipes = st.session_state.recipes[:5]
|
| 588 |
+
else:
|
| 589 |
+
display_recipes = st.session_state.recipes
|
| 590 |
+
else:
|
| 591 |
+
display_recipes = st.session_state.recipes
|
| 592 |
+
else:
|
| 593 |
+
# Fallback to basic search
|
| 594 |
+
search_term = st.text_input("Search recipes:", placeholder=get_text('search_placeholder'))
|
| 595 |
+
if search_term:
|
| 596 |
+
display_recipes = [r for r in st.session_state.recipes if
|
| 597 |
+
search_term.lower() in r['dish_name'].lower() or
|
| 598 |
+
search_term.lower() in r.get('ingredients', '').lower()]
|
| 599 |
+
else:
|
| 600 |
+
display_recipes = st.session_state.recipes
|
| 601 |
+
|
| 602 |
+
# Display recipes with AI insights
|
| 603 |
+
for recipe in display_recipes[:10]: # Show top 10
|
| 604 |
+
with st.expander(f"🍽️ {recipe['dish_name']} (ID: {recipe.get('id', 'N/A')})"):
|
| 605 |
+
|
| 606 |
+
# Basic recipe info
|
| 607 |
+
col1, col2, col3 = st.columns(3)
|
| 608 |
+
with col1:
|
| 609 |
+
st.write(f"**By:** {recipe.get('your_name', 'Anonymous')}")
|
| 610 |
+
st.write(f"**District:** {recipe.get('district', 'Not specified')}")
|
| 611 |
+
st.write(f"**Type:** {recipe.get('recipe_type', 'Not specified')}")
|
| 612 |
+
|
| 613 |
+
with col2:
|
| 614 |
+
# AI Tags
|
| 615 |
+
if recipe.get('ai_tags'):
|
| 616 |
+
st.write("**AI Tags:**")
|
| 617 |
+
for tag in recipe['ai_tags'][:3]: # Show top 3 tags
|
| 618 |
+
st.badge(tag)
|
| 619 |
+
|
| 620 |
+
# Sentiment analysis
|
| 621 |
+
if recipe.get('sentiment_analysis'):
|
| 622 |
+
sentiment = recipe['sentiment_analysis']
|
| 623 |
+
st.write(f"**Story Mood:** {sentiment['emotion'].title()}")
|
| 624 |
+
|
| 625 |
+
with col3:
|
| 626 |
+
st.write(f"**Submitted:** {recipe.get('submitted_date', 'Unknown')}")
|
| 627 |
+
if recipe.get('cooking_tips'):
|
| 628 |
+
with st.popover("💡 Cooking Tips"):
|
| 629 |
+
for tip in recipe['cooking_tips']:
|
| 630 |
+
st.write(f"• {tip}")
|
| 631 |
+
|
| 632 |
+
# Recipe content
|
| 633 |
+
st.markdown("### Ingredients:")
|
| 634 |
+
st.text(recipe.get('ingredients', 'No ingredients provided'))
|
| 635 |
+
|
| 636 |
+
st.markdown("### Instructions:")
|
| 637 |
+
st.text(recipe.get('instructions', 'No instructions provided'))
|
| 638 |
+
|
| 639 |
+
if recipe.get('story'):
|
| 640 |
+
st.markdown("### Story:")
|
| 641 |
+
st.text(recipe['story'])
|
| 642 |
+
|
| 643 |
+
# AI-powered similar recipes
|
| 644 |
+
if AI_AVAILABLE and similarity_model and len(st.session_state.recipes) > 1:
|
| 645 |
+
similar_recipes = find_similar_recipes(recipe, st.session_state.recipes, similarity_model)
|
| 646 |
+
if similar_recipes:
|
| 647 |
+
st.markdown("### 👥 Similar Recipes:")
|
| 648 |
+
sim_cols = st.columns(len(similar_recipes))
|
| 649 |
+
for idx, (sim_recipe, similarity) in enumerate(similar_recipes):
|
| 650 |
+
with sim_cols[idx]:
|
| 651 |
+
st.write(f"**{sim_recipe['dish_name']}**")
|
| 652 |
+
st.write(f"Similarity: {similarity:.0%}")
|
| 653 |
+
st.caption(f"By {sim_recipe.get('your_name', 'Anonymous')}")
|
| 654 |
+
|
| 655 |
+
# --- AI Insights Tab ---
|
| 656 |
+
with tab_ai:
|
| 657 |
+
st.header("🤖 AI Recipe Insights")
|
| 658 |
+
|
| 659 |
+
if not AI_AVAILABLE or not similarity_model:
|
| 660 |
+
st.warning("AI features are not available. Please install required packages to enable AI insights.")
|
| 661 |
+
st.code("pip install sentence-transformers transformers torch scikit-learn")
|
| 662 |
+
st.stop()
|
| 663 |
+
|
| 664 |
+
if not st.session_state.recipes:
|
| 665 |
+
st.info("No recipes available for analysis. Submit some recipes first!")
|
| 666 |
+
st.stop()
|
| 667 |
+
|
| 668 |
+
# AI Analysis Dashboard
|
| 669 |
+
st.subheader("📊 Community Recipe Analysis")
|
| 670 |
+
|
| 671 |
+
# Sentiment distribution
|
| 672 |
+
sentiments = [recipe.get('sentiment_analysis', {}).get('emotion', 'nostalgic')
|
| 673 |
+
for recipe in st.session_state.recipes]
|
| 674 |
+
sentiment_counts = Counter(sentiments)
|
| 675 |
+
|
| 676 |
+
col1, col2 = st.columns(2)
|
| 677 |
|
| 678 |
with col1:
|
| 679 |
+
st.markdown("**Recipe Story Moods:**")
|
| 680 |
+
for emotion, count in sentiment_counts.most_common():
|
| 681 |
+
percentage = (count / len(st.session_state.recipes)) * 100
|
| 682 |
+
st.write(f"😊 {emotion.title()}: {count} recipes ({percentage:.1f}%)")
|
|
|
|
| 683 |
|
| 684 |
with col2:
|
| 685 |
+
# Most common AI tags
|
| 686 |
+
all_tags = []
|
| 687 |
+
for recipe in st.session_state.recipes:
|
| 688 |
+
all_tags.extend(recipe.get('ai_tags', []))
|
| 689 |
+
|
| 690 |
+
if all_tags:
|
| 691 |
+
tag_counts = Counter(all_tags)
|
| 692 |
+
st.markdown("**Most Popular Recipe Types:**")
|
| 693 |
+
for tag, count in tag_counts.most_common(5):
|
| 694 |
+
st.write(f"🏷️ {tag}: {count} recipes")
|
| 695 |
+
|
| 696 |
+
st.markdown("---")
|
| 697 |
+
|
| 698 |
+
# Recipe Recommendation Engine
|
| 699 |
+
st.subheader("🎯 AI Recipe Recommendations")
|
| 700 |
+
|
| 701 |
+
col1, col2 = st.columns(2)
|
| 702 |
+
|
| 703 |
+
with col1:
|
| 704 |
+
preference = st.selectbox(
|
| 705 |
+
"What are you in the mood for?",
|
| 706 |
+
["Spicy dishes", "Sweet treats", "Quick meals", "Traditional recipes",
|
| 707 |
+
"Healthy options", "Festival specials", "Comfort food"]
|
| 708 |
)
|
| 709 |
|
| 710 |
+
with col2:
|
| 711 |
+
dietary_pref = st.selectbox(
|
| 712 |
+
"Dietary preference:",
|
| 713 |
+
["Any", "Vegetarian only", "Non-vegetarian", "Healthy options"]
|
|
|
|
|
|
|
| 714 |
)
|
| 715 |
|
| 716 |
+
if st.button("🤖 Get AI Recommendations"):
|
| 717 |
+
with st.spinner("🤖 AI is finding perfect recipes for you..."):
|
| 718 |
+
# Create recommendation query based on preferences
|
| 719 |
+
query_map = {
|
| 720 |
+
"Spicy dishes": "spicy hot chili pepper karam",
|
| 721 |
+
"Sweet treats": "sweet jaggery sugar dessert halwa",
|
| 722 |
+
"Quick meals": "quick fast instant easy",
|
| 723 |
+
"Traditional recipes": "traditional authentic grandmother ammamma",
|
| 724 |
+
"Healthy options": "healthy nutritious protein vitamin",
|
| 725 |
+
"Festival specials": "festival celebration ugadi sankranti",
|
| 726 |
+
"Comfort food": "comfort home family love"
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
search_query = query_map.get(preference, preference)
|
| 730 |
+
|
| 731 |
+
# Filter by dietary preference
|
| 732 |
+
filtered_recipes = st.session_state.recipes
|
| 733 |
+
if dietary_pref == "Vegetarian only":
|
| 734 |
+
filtered_recipes = [r for r in filtered_recipes if 'Vegetarian' in r.get('ai_tags', [])]
|
| 735 |
+
elif dietary_pref == "Non-vegetarian":
|
| 736 |
+
filtered_recipes = [r for r in filtered_recipes if 'Non-Vegetarian' in r.get('ai_tags', [])]
|
| 737 |
+
elif dietary_pref == "Healthy options":
|
| 738 |
+
filtered_recipes = [r for r in filtered_recipes if any(tag in ['Healthy', 'High Protein', 'Low Fat'] for tag in r.get('ai_tags', []))]
|
| 739 |
+
|
| 740 |
+
if filtered_recipes:
|
| 741 |
+
# Use AI to find best matches
|
| 742 |
+
query_embedding = similarity_model.encode([search_query])
|
| 743 |
+
recipe_texts = []
|
| 744 |
+
|
| 745 |
+
for recipe in filtered_recipes:
|
| 746 |
+
recipe_text = f"{recipe.get('dish_name', '')} {recipe.get('ingredients', '')} {' '.join(recipe.get('ai_tags', []))}"
|
| 747 |
+
recipe_texts.append(recipe_text)
|
| 748 |
+
|
| 749 |
+
if recipe_texts:
|
| 750 |
+
recipe_embeddings = similarity_model.encode(recipe_texts)
|
| 751 |
+
similarities = cosine_similarity(query_embedding, recipe_embeddings)[0]
|
| 752 |
+
|
| 753 |
+
# Get top recommendations
|
| 754 |
+
top_indices = np.argsort(similarities)[::-1][:3]
|
| 755 |
+
|
| 756 |
+
st.success(f"🎯 Here are your personalized recommendations:")
|
| 757 |
+
|
| 758 |
+
for idx in top_indices:
|
| 759 |
+
if similarities[idx] > 0.1: # Minimum relevance
|
| 760 |
+
recipe = filtered_recipes[idx]
|
| 761 |
+
match_score = similarities[idx] * 100
|
| 762 |
+
|
| 763 |
+
with st.container():
|
| 764 |
+
st.markdown(f"### 🍽️ {recipe['dish_name']}")
|
| 765 |
+
col1, col2, col3 = st.columns(3)
|
| 766 |
+
|
| 767 |
+
with col1:
|
| 768 |
+
st.write(f"**Match Score:** {match_score:.0f}%")
|
| 769 |
+
st.write(f"**By:** {recipe.get('your_name', 'Anonymous')}")
|
| 770 |
+
|
| 771 |
+
with col2:
|
| 772 |
+
st.write(f"**Type:** {recipe.get('recipe_type', 'N/A')}")
|
| 773 |
+
st.write(f"**District:** {recipe.get('district', 'N/A')}")
|
| 774 |
+
|
| 775 |
+
with col3:
|
| 776 |
+
if recipe.get('ai_tags'):
|
| 777 |
+
st.write("**Tags:**")
|
| 778 |
+
for tag in recipe['ai_tags'][:2]:
|
| 779 |
+
st.badge(tag)
|
| 780 |
+
|
| 781 |
+
if recipe.get('story'):
|
| 782 |
+
st.write(f"*{recipe['story'][:150]}...*")
|
| 783 |
+
|
| 784 |
+
st.markdown("---")
|
| 785 |
+
else:
|
| 786 |
+
st.info("No recipes found matching your preferences. Try different criteria!")
|
| 787 |
+
|
| 788 |
st.markdown("---")
|
| 789 |
|
| 790 |
+
# Recipe Insights
|
| 791 |
+
st.subheader("🔍 Deep Recipe Analysis")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
|
| 793 |
+
if st.session_state.recipes:
|
| 794 |
+
selected_recipe = st.selectbox(
|
| 795 |
+
"Select a recipe to analyze:",
|
| 796 |
+
options=[f"{r['dish_name']} (by {r.get('your_name', 'Anonymous')})" for r in st.session_state.recipes],
|
| 797 |
+
index=0
|
| 798 |
+
)
|
|
|
|
| 799 |
|
| 800 |
+
if selected_recipe:
|
| 801 |
+
# Find the selected recipe
|
| 802 |
+
recipe_idx = next(i for i, r in enumerate(st.session_state.recipes)
|
| 803 |
+
if f"{r['dish_name']} (by {r.get('your_name', 'Anonymous')})" == selected_recipe)
|
| 804 |
+
recipe = st.session_state.recipes[recipe_idx]
|
| 805 |
+
|
| 806 |
+
col1, col2 = st.columns(2)
|
| 807 |
+
|
| 808 |
+
with col1:
|
| 809 |
+
st.markdown("**🤖 AI Analysis:**")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
|
| 811 |
+
# Sentiment analysis details
|
| 812 |
+
if recipe.get('sentiment_analysis'):
|
| 813 |
+
sentiment = recipe['sentiment_analysis']
|
| 814 |
+
st.metric(
|
| 815 |
+
"Story Emotion",
|
| 816 |
+
sentiment['emotion'].title(),
|
| 817 |
+
f"{sentiment['confidence']:.0%} confidence"
|
| 818 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
|
| 820 |
+
# AI tags
|
| 821 |
+
if recipe.get('ai_tags'):
|
| 822 |
+
st.write("**AI-Detected Categories:**")
|
| 823 |
+
for tag in recipe['ai_tags']:
|
| 824 |
+
st.badge(tag)
|
| 825 |
+
|
| 826 |
+
with col2:
|
| 827 |
+
st.markdown("**💡 Smart Cooking Tips:**")
|
| 828 |
+
if recipe.get('cooking_tips'):
|
| 829 |
+
for tip in recipe['cooking_tips']:
|
| 830 |
+
st.info(tip)
|
| 831 |
+
else:
|
| 832 |
+
# Generate tips on demand
|
| 833 |
+
tips = generate_cooking_tips(recipe)
|
| 834 |
+
for tip in tips:
|
| 835 |
+
st.info(tip)
|
| 836 |
+
|
| 837 |
+
# Similar recipes
|
| 838 |
+
st.markdown("**👥 Similar Recipes:**")
|
| 839 |
+
similar_recipes = find_similar_recipes(recipe, st.session_state.recipes, similarity_model, top_k=5)
|
| 840 |
+
|
| 841 |
+
if similar_recipes:
|
| 842 |
+
for sim_recipe, similarity in similar_recipes:
|
| 843 |
+
with st.expander(f"{sim_recipe['dish_name']} - {similarity:.0%} similar"):
|
| 844 |
+
st.write(f"**By:** {sim_recipe.get('your_name', 'Anonymous')}")
|
| 845 |
+
st.write(f"**Type:** {sim_recipe.get('recipe_type', 'N/A')}")
|
| 846 |
+
if sim_recipe.get('story'):
|
| 847 |
+
st.write(f"**Story:** {sim_recipe['story'][:200]}...")
|
| 848 |
+
else:
|
| 849 |
+
st.info("No similar recipes found in the current database.")
|
| 850 |
|
| 851 |
# --- Analytics Tab ---
|
| 852 |
with tab_analytics:
|
| 853 |
+
st.header("📊 Recipe Analytics Dashboard")
|
| 854 |
|
| 855 |
+
if not st.session_state.recipes:
|
| 856 |
+
st.info("No recipes available for analysis. Submit some recipes first!")
|
| 857 |
+
st.stop()
|
| 858 |
|
| 859 |
+
# Basic Statistics
|
| 860 |
+
total_recipes = len(st.session_state.recipes)
|
| 861 |
+
st.metric("Total Recipes", total_recipes)
|
| 862 |
+
|
| 863 |
+
# Create dataframe for analysis
|
| 864 |
+
df_data = []
|
| 865 |
+
for recipe in st.session_state.recipes:
|
| 866 |
+
df_data.append({
|
| 867 |
+
'dish_name': recipe.get('dish_name', ''),
|
| 868 |
+
'your_name': recipe.get('your_name', 'Anonymous'),
|
| 869 |
+
'district': recipe.get('district', 'Unknown'),
|
| 870 |
+
'recipe_type': recipe.get('recipe_type', 'Other'),
|
| 871 |
+
'submitted_date': recipe.get('submitted_date', ''),
|
| 872 |
+
'ai_tags': recipe.get('ai_tags', []),
|
| 873 |
+
'emotion': recipe.get('sentiment_analysis', {}).get('emotion', 'neutral')
|
| 874 |
+
})
|
| 875 |
+
|
| 876 |
+
df = pd.DataFrame(df_data)
|
| 877 |
+
|
| 878 |
+
# District-wise distribution
|
| 879 |
+
col1, col2 = st.columns(2)
|
| 880 |
+
|
| 881 |
+
with col1:
|
| 882 |
+
st.subheader("📍 Recipes by District")
|
| 883 |
+
district_counts = df['district'].value_counts().head(10)
|
| 884 |
+
if not district_counts.empty:
|
| 885 |
+
st.bar_chart(district_counts)
|
| 886 |
+
else:
|
| 887 |
+
st.info("No district data available")
|
| 888 |
+
|
| 889 |
+
with col2:
|
| 890 |
+
st.subheader("🍽️ Recipe Types Distribution")
|
| 891 |
+
type_counts = df['recipe_type'].value_counts()
|
| 892 |
+
if not type_counts.empty:
|
| 893 |
+
st.bar_chart(type_counts)
|
| 894 |
+
else:
|
| 895 |
+
st.info("No recipe type data available")
|
| 896 |
+
|
| 897 |
+
# Contributors
|
| 898 |
+
st.subheader("👥 Top Contributors")
|
| 899 |
+
contributor_counts = df['your_name'].value_counts().head(5)
|
| 900 |
+
for name, count in contributor_counts.items():
|
| 901 |
+
st.write(f"🏆 {name}: {count} recipe(s)")
|
| 902 |
+
|
| 903 |
+
# Emotional analysis
|
| 904 |
+
if AI_AVAILABLE:
|
| 905 |
+
st.subheader("😊 Recipe Story Emotions")
|
| 906 |
+
emotion_counts = df['emotion'].value_counts()
|
| 907 |
+
|
| 908 |
+
emotion_colors = {
|
| 909 |
+
'joyful': '#FFD700',
|
| 910 |
+
'nostalgic': '#DDA0DD',
|
| 911 |
+
'proud': '#FF6347',
|
| 912 |
+
'loving': '#FF69B4',
|
| 913 |
+
'neutral': '#87CEEB'
|
| 914 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 915 |
|
| 916 |
+
emotion_data = []
|
| 917 |
+
for emotion, count in emotion_counts.items():
|
| 918 |
+
emotion_data.append({
|
| 919 |
+
'Emotion': emotion.title(),
|
| 920 |
+
'Count': count,
|
| 921 |
+
'Percentage': f"{(count/total_recipes)*100:.1f}%"
|
| 922 |
+
})
|
| 923 |
|
| 924 |
+
emotion_df = pd.DataFrame(emotion_data)
|
| 925 |
+
st.dataframe(emotion_df, use_container_width=True)
|
| 926 |
+
|
| 927 |
+
# Recent activity
|
| 928 |
+
st.subheader("📅 Recent Recipe Submissions")
|
| 929 |
+
recent_recipes = st.session_state.recipes[:5] # Show 5 most recent
|
| 930 |
+
|
| 931 |
+
for recipe in recent_recipes:
|
| 932 |
+
with st.container():
|
| 933 |
+
col1, col2, col3 = st.columns([3, 2, 1])
|
| 934 |
+
|
| 935 |
with col1:
|
| 936 |
st.write(f"**{recipe['dish_name']}**")
|
| 937 |
+
st.caption(f"by {recipe.get('your_name', 'Anonymous')}")
|
| 938 |
+
|
| 939 |
with col2:
|
| 940 |
+
st.write(f"{recipe.get('recipe_type', 'N/A')}")
|
| 941 |
+
st.caption(f"{recipe.get('district', 'Unknown')}")
|
| 942 |
+
|
| 943 |
with col3:
|
| 944 |
+
st.write(recipe.get('submitted_date', 'Unknown')[:10]) # Show date only
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
|
| 946 |
+
st.markdown("---")
|
| 947 |
+
|
| 948 |
+
# Export functionality
|
| 949 |
+
st.subheader("📤 Export Data")
|
| 950 |
+
if st.button("Download Recipe Database as CSV"):
|
| 951 |
+
# Flatten the data for CSV export
|
| 952 |
+
export_data = []
|
| 953 |
+
for recipe in st.session_state.recipes:
|
| 954 |
+
export_data.append({
|
| 955 |
+
'ID': recipe.get('id', ''),
|
| 956 |
+
'Dish Name': recipe.get('dish_name', ''),
|
| 957 |
+
'Contributor': recipe.get('your_name', ''),
|
| 958 |
+
'District': recipe.get('district', ''),
|
| 959 |
+
'Recipe Type': recipe.get('recipe_type', ''),
|
| 960 |
+
'Ingredients': recipe.get('ingredients', ''),
|
| 961 |
+
'Instructions': recipe.get('instructions', ''),
|
| 962 |
+
'Story': recipe.get('story', ''),
|
| 963 |
+
'AI Tags': ', '.join(recipe.get('ai_tags', [])),
|
| 964 |
+
'Emotion': recipe.get('sentiment_analysis', {}).get('emotion', ''),
|
| 965 |
+
'Submitted Date': recipe.get('submitted_date', '')
|
| 966 |
+
})
|
| 967 |
+
|
| 968 |
+
export_df = pd.DataFrame(export_data)
|
| 969 |
+
csv_data = export_df.to_csv(index=False)
|
| 970 |
+
|
| 971 |
+
st.download_button(
|
| 972 |
+
label="📥 Download CSV",
|
| 973 |
+
data=csv_data,
|
| 974 |
+
file_name=f"ruchi_chudu_recipes_{datetime.now().strftime('%Y%m%d')}.csv",
|
| 975 |
+
mime="text/csv"
|
| 976 |
+
)
|
| 977 |
+
|
| 978 |
+
# --- Footer ---
|
| 979 |
st.markdown("---")
|
| 980 |
st.markdown("""
|
| 981 |
+
<div style='text-align: center; color: #666; padding: 20px;'>
|
| 982 |
+
<p>🍲 రుచి చూడు (Ruchi Chudu) - Preserving Telugu Cuisine Through Community & AI</p>
|
| 983 |
+
<p>Made with ❤️ for Telugu food lovers everywhere</p>
|
| 984 |
+
<p><small>AI Features powered by Hugging Face Transformers | Data stored locally</small></p>
|
| 985 |
+
</div>
|
| 986 |
+
""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|