Spaces:
Build error
Build error
| import streamlit as st | |
| import pandas as pd | |
| import json | |
| import os | |
| import uuid | |
| from datetime import datetime | |
| from collections import Counter | |
| import base64 | |
| import glob | |
| # --- Page Configuration --- | |
| st.set_page_config( | |
| page_title="రుచి చూడు (Ruchi Chudu)", | |
| page_icon="🍲", | |
| layout="wide" | |
| ) | |
| # --- Data Storage Functions --- | |
| RECIPES_FOLDER = "recipes" | |
| IMAGES_FOLDER = "recipe_images" | |
| def ensure_folders_exist(): | |
| """Create necessary folders if they don't exist""" | |
| os.makedirs(RECIPES_FOLDER, exist_ok=True) | |
| os.makedirs(IMAGES_FOLDER, exist_ok=True) | |
| def generate_recipe_id(): | |
| """Generate a unique ID for each recipe""" | |
| return str(uuid.uuid4())[:8] | |
| def save_image(image_file, recipe_id): | |
| """Save uploaded image to images folder""" | |
| if image_file is not None: | |
| try: | |
| ensure_folders_exist() | |
| # Get file extension | |
| file_extension = image_file.name.split('.')[-1].lower() | |
| image_filename = f"{recipe_id}.{file_extension}" | |
| image_path = os.path.join(IMAGES_FOLDER, image_filename) | |
| # Save image file | |
| with open(image_path, "wb") as f: | |
| f.write(image_file.getvalue()) | |
| return image_filename | |
| except Exception as e: | |
| st.error(f"Error saving image: {e}") | |
| return None | |
| return None | |
| def load_image(image_filename): | |
| """Load image from images folder""" | |
| if image_filename: | |
| try: | |
| image_path = os.path.join(IMAGES_FOLDER, image_filename) | |
| if os.path.exists(image_path): | |
| with open(image_path, "rb") as f: | |
| return f.read() | |
| except Exception as e: | |
| st.error(f"Error loading image: {e}") | |
| return None | |
| def save_recipe(recipe_data): | |
| """Save individual recipe to its own JSON file""" | |
| try: | |
| ensure_folders_exist() | |
| recipe_id = recipe_data['id'] | |
| filename = f"recipe_{recipe_id}.json" | |
| filepath = os.path.join(RECIPES_FOLDER, filename) | |
| with open(filepath, 'w', encoding='utf-8') as f: | |
| json.dump(recipe_data, f, ensure_ascii=False, indent=2) | |
| return True | |
| except Exception as e: | |
| st.error(f"Error saving recipe: {e}") | |
| return False | |
| def load_all_recipes(): | |
| """Load all recipes from individual JSON files""" | |
| recipes = [] | |
| try: | |
| ensure_folders_exist() | |
| # Get all recipe JSON files | |
| recipe_files = glob.glob(os.path.join(RECIPES_FOLDER, "recipe_*.json")) | |
| for filepath in recipe_files: | |
| try: | |
| with open(filepath, 'r', encoding='utf-8') as f: | |
| recipe = json.load(f) | |
| recipes.append(recipe) | |
| except Exception as e: | |
| st.error(f"Error loading recipe from {filepath}: {e}") | |
| continue | |
| # Sort by submission date (newest first) | |
| recipes.sort(key=lambda x: x.get('submitted_date', ''), reverse=True) | |
| except Exception as e: | |
| st.error(f"Error loading recipes: {e}") | |
| return recipes | |
| def delete_recipe(recipe_id): | |
| """Delete a recipe and its associated image""" | |
| try: | |
| # Delete recipe JSON file | |
| recipe_filename = f"recipe_{recipe_id}.json" | |
| recipe_filepath = os.path.join(RECIPES_FOLDER, recipe_filename) | |
| if os.path.exists(recipe_filepath): | |
| os.remove(recipe_filepath) | |
| # Delete associated image files (check all common extensions) | |
| for ext in ['jpg', 'jpeg', 'png', 'gif']: | |
| image_filename = f"{recipe_id}.{ext}" | |
| image_filepath = os.path.join(IMAGES_FOLDER, image_filename) | |
| if os.path.exists(image_filepath): | |
| os.remove(image_filepath) | |
| break | |
| return True | |
| except Exception as e: | |
| st.error(f"Error deleting recipe: {e}") | |
| return False | |
| def get_recipe_stats(): | |
| """Get statistics about stored recipes""" | |
| try: | |
| recipe_files = glob.glob(os.path.join(RECIPES_FOLDER, "recipe_*.json")) | |
| image_files = glob.glob(os.path.join(IMAGES_FOLDER, "*.*")) | |
| return { | |
| 'total_recipes': len(recipe_files), | |
| 'total_images': len(image_files), | |
| 'storage_size': sum(os.path.getsize(f) for f in recipe_files + image_files if os.path.exists(f)) | |
| } | |
| except: | |
| return {'total_recipes': 0, 'total_images': 0, 'storage_size': 0} | |
| # --- Language and Text --- | |
| TEXT = { | |
| "en": { | |
| "title": "Ruchi Chudu", | |
| "subtitle": "A Community Cookbook to Preserve Telugu Cuisine", | |
| "language_select": "Choose Language", | |
| "sidebar_header": "Contribute Your Recipe!", | |
| "tab_submit": "📝 Submit Your Recipe", | |
| "tab_gallery": "🍲 Community Gallery", | |
| "tab_analytics": "📊 Recipe Analytics", | |
| "tab_manage": "⚙️ Manage Recipes", | |
| "form_header": "Tell us about your dish", | |
| "dish_name": "Dish Name", | |
| "your_name": "Your Name", | |
| "district": "Your District", | |
| "select_district": "--- Select a District ---", | |
| "recipe_type": "Recipe Type (e.g., Curry, Pickle, Snack)", | |
| "ingredients": "Ingredients (one per line)", | |
| "instructions": "Instructions", | |
| "story": "The Story Behind Your Dish (your memories, family traditions, etc.)", | |
| "image_upload": "Upload a photo of the dish (optional)", | |
| "submit_button": "Submit Recipe", | |
| "success_message": "Thank you! Your recipe has been submitted successfully.", | |
| "gallery_header": "Explore Recipes from the Community", | |
| "gallery_empty": "No recipes submitted yet. Be the first to share!", | |
| "submitted_by": "Submitted by", | |
| "from_district": "from", | |
| "submitted_on": "Submitted on", | |
| "recipe_id": "Recipe ID", | |
| "story_title": "📖 The Story", | |
| "ingredients_title": "🌶️ Ingredients", | |
| "instructions_title": "✍️ Instructions", | |
| "tags_title": "AI-Generated Tags", | |
| "error_dish_name": "Please enter a dish name before submitting.", | |
| "search_placeholder": "Search recipes...", | |
| "filter_by_district": "Filter by District", | |
| "filter_by_type": "Filter by Recipe Type", | |
| "all_districts": "All Districts", | |
| "all_types": "All Recipe Types", | |
| "total_recipes": "Total Recipes", | |
| "total_images": "Total Images", | |
| "storage_size": "Storage Used", | |
| "top_districts": "Top Contributing Districts", | |
| "popular_types": "Popular Recipe Types", | |
| "recent_recipes": "Recent Submissions", | |
| "clear_filters": "Clear All Filters", | |
| "export_recipes": "Export All Recipes", | |
| "manage_header": "Recipe Management", | |
| "delete_recipe": "Delete Recipe", | |
| "confirm_delete": "Are you sure you want to delete this recipe?", | |
| "recipe_deleted": "Recipe deleted successfully!", | |
| "backup_data": "Backup All Data", | |
| "backup_created": "Backup created successfully!" | |
| }, | |
| "te": { | |
| "title": "రుచి చూడు", | |
| "subtitle": "మన తెలుగు వంటల సంస్కృతిని కాపాడుదాం", | |
| "language_select": "భాషను ఎంచుకోండి", | |
| "sidebar_header": "మీ వంటకాన్ని పంపండి!", | |
| "tab_submit": "📝 మీ వంటకాన్ని పంపండి", | |
| "tab_gallery": "🍲 కమ్యూనిటీ గ్యాలరీ", | |
| "tab_analytics": "📊 వంటకాల విశ్లేషణ", | |
| "tab_manage": "⚙️ వంటకాలను నిర్వహించండి", | |
| "form_header": "మీ వంటకం గురించి చెప్పండి", | |
| "dish_name": "వంటకం పేరు", | |
| "your_name": "మీ పేరు", | |
| "district": "మీ జిల్లా", | |
| "select_district": "--- జిల్లాను ఎంచుకోండి ---", | |
| "recipe_type": "వంటకం రకం (ఉదా. కూర, పచ్చడి, స్నాక్)", | |
| "ingredients": "కావలసినవి (ఒకదానికి ఒకటి)", | |
| "instructions": "తయారీ విధానం", | |
| "story": "ఈ వంటతో మీ కథ (మీ జ్ఞాపకాలు, కుటుంబ సంప్రదాయాలు మొదలైనవి)", | |
| "image_upload": "వంటకం ఫోటోను అప్లోడ్ చేయండి (ఐచ్ఛికం)", | |
| "submit_button": "వంటకాన్ని పంపండి", | |
| "success_message": "ధన్యవాదాలు! మీ వంటకం విజయవంతంగా సమర్పించబడింది.", | |
| "gallery_header": "కమ్యూనిటీ నుండి వంటకాలను అన్వేషించండి", | |
| "gallery_empty": "ఇంకా వంటకాలు సమర్పించబడలేదు. పంచుకున్న మొదటి వ్యక్తి మీరే అవ్వండి!", | |
| "submitted_by": "సమర్పించిన వారు", | |
| "from_district": "నుండి", | |
| "submitted_on": "సమర్పించిన తేదీ", | |
| "recipe_id": "వంటకం ID", | |
| "story_title": "📖 కథ", | |
| "ingredients_title": "🌶️ కావలసినవి", | |
| "instructions_title": "✍️ తయారీ విధానం", | |
| "tags_title": "AI ద్వారా రూపొందించబడిన ట్యాగ్లు", | |
| "error_dish_name": "సమర్పించే ముందు దయచేసి వంటకం పేరును నమోదు చేయండి.", | |
| "search_placeholder": "వంటకాలను వెతకండి...", | |
| "filter_by_district": "జిల్లా ద్వారా ఫిల్టర్ చేయండి", | |
| "filter_by_type": "వంటకం రకం ద్వారా ఫిల్టర్ చేయండి", | |
| "all_districts": "అన్ని జిల్లాలు", | |
| "all_types": "అన్ని వంటకాల రకాలు", | |
| "total_recipes": "మొత్తం వంటకాలు", | |
| "total_images": "మొత్తం చిత్రాలు", | |
| "storage_size": "వాడిన స్టోరేజ్", | |
| "top_districts": "అధిక సహకారం అందించిన జిల్లాలు", | |
| "popular_types": "ప్రాచుర్యం పొందిన వంటకాల రకాలు", | |
| "recent_recipes": "ఇటీవలి సమర్పణలు", | |
| "clear_filters": "అన్ని ఫిల్టర్లను క్లియర్ చేయండి", | |
| "export_recipes": "అన్ని వంటకాలను ఎగుమతి చేయండి", | |
| "manage_header": "వంటకాల నిర్వహణ", | |
| "delete_recipe": "వంటకాన్ని తొలగించండి", | |
| "confirm_delete": "మీరు ఖచ్చితంగా ఈ వంటకాన్ని తొలగించాలనుకుంటున్నారా?", | |
| "recipe_deleted": "వంటకం విజయవంతంగా తొలగించబడింది!", | |
| "backup_data": "అన్ని డేటాను బ్యాకప్ చేయండి", | |
| "backup_created": "బ్యాకప్ విజయవంతంగా సృష్టించబడింది!" | |
| } | |
| } | |
| # --- Data for Dropdowns --- | |
| DISTRICTS = [ | |
| "Adilabad", "Bhadradri Kothagudem", "Hanumakonda", "Hyderabad", "Jagtial", "Jangaon", | |
| "Jayashankar Bhupalpally", "Jogulamba Gadwal", "Kamareddy", "Karimnagar", "Khammam", | |
| "Komaram Bheem", "Mahabubabad", "Mahbubnagar", "Mancherial", "Medak", "Medchal-Malkajgiri", | |
| "Mulugu", "Nagarkurnool", "Nalgonda", "Narayanpet", "Nirmal", "Nizamabad", "Peddapalli", | |
| "Rajanna Sircilla", "Ranga Reddy", "Sangareddy", "Siddipet", "Suryapet", "Vikarabad", | |
| "Wanaparthy", "Warangal", "Yadadri Bhuvanagiri", "Alluri Sitharama Raju", "Anakapalli", | |
| "Anantapur", "Annamayya", "Bapatla", "Chittoor", "East Godavari", "Eluru", "Guntur", | |
| "Kakinada", "Konaseema", "Krishna", "Kurnool", "Nandyal", "NTR", "Palnadu", "Parvathipuram Manyam", | |
| "Prakasam", "Sri Potti Sriramulu Nellore", "Sri Sathya Sai", "Srikakulam", "Tirupati", | |
| "Visakhapatnam", "Vizianagaram", "West Godavari", "YSR Kadapa" | |
| ] | |
| DISTRICTS.sort() | |
| RECIPE_TYPES = ["Curry (కూర)", "Fry (వేపుడు)", "Pickle (పచ్చడి)", "Chutney (చట్నీ)", "Pulusu (పులుసు)", "Snack (చిరుతిండి)", "Sweet (తీపి)", "Breakfast (అల్పాహారం)", "Rice Dish (అన్నం రకం)", "Other (ఇతర)"] | |
| # --- Session State Initialization --- | |
| if 'language' not in st.session_state: | |
| st.session_state['language'] = 'en' | |
| if 'recipes' not in st.session_state: | |
| st.session_state['recipes'] = load_all_recipes() | |
| if 'refresh_recipes' not in st.session_state: | |
| st.session_state['refresh_recipes'] = False | |
| # Refresh recipes if needed | |
| if st.session_state.get('refresh_recipes', False): | |
| st.session_state['recipes'] = load_all_recipes() | |
| st.session_state['refresh_recipes'] = False | |
| # --- Helper Functions --- | |
| def get_text(key): | |
| """Fetches text from the dictionary based on the selected language.""" | |
| return TEXT[st.session_state['language']][key] | |
| def simulate_ai_tagging(story, ingredients): | |
| """Simulate AI tagging based on content analysis""" | |
| tags = set() | |
| text_to_scan = (story + " " + ingredients).lower() | |
| keyword_map = { | |
| "festival": ["sankranti", "ugadi", "dasara", "diwali", "vinayaka chaviti"], | |
| "spicy": ["chilli", "karam", "mirapa", "pepper", "guntur"], | |
| "healthy": ["millet", "ragi", "jowar", "vegetable", "leafy", "greens"], | |
| "quick": ["15 min", "20 min", "quick", "easy", "tvaraga"], | |
| "traditional": ["grandma", "ammamma", "nanamma", "traditional", "sampradayam"], | |
| "summer": ["mango", "summer", "cool", "curd", "perugu"], | |
| "winter": ["winter", "warm", "hot", "sheeta"], | |
| "vegetarian": ["vegetable", "veg", "sabzi", "kura"], | |
| "non-vegetarian": ["chicken", "mutton", "fish", "egg", "kodi", "meka"] | |
| } | |
| for tag, keywords in keyword_map.items(): | |
| if any(keyword in text_to_scan for keyword in keywords): | |
| tags.add(tag.capitalize()) | |
| if not tags: | |
| tags.add("Community Recipe") | |
| return list(tags) | |
| def filter_recipes(recipes, search_term="", district_filter="", type_filter=""): | |
| """Filter recipes based on search and filter criteria""" | |
| filtered = recipes | |
| if search_term: | |
| filtered = [r for r in filtered if | |
| search_term.lower() in r['dish_name'].lower() or | |
| search_term.lower() in r.get('your_name', '').lower() or | |
| search_term.lower() in r.get('ingredients', '').lower()] | |
| if district_filter and district_filter != get_text('all_districts'): | |
| filtered = [r for r in filtered if r.get('district') == district_filter] | |
| if type_filter and type_filter != get_text('all_types'): | |
| filtered = [r for r in filtered if r.get('recipe_type') == type_filter] | |
| return filtered | |
| def export_recipes_to_csv(recipes): | |
| """Export recipes to CSV format""" | |
| if not recipes: | |
| return None | |
| export_data = [] | |
| for recipe in recipes: | |
| export_data.append({ | |
| 'Recipe ID': recipe.get('id', ''), | |
| 'Dish Name': recipe['dish_name'], | |
| 'Submitted By': recipe.get('your_name', ''), | |
| 'District': recipe.get('district', ''), | |
| 'Recipe Type': recipe.get('recipe_type', ''), | |
| 'Ingredients': recipe.get('ingredients', ''), | |
| 'Instructions': recipe.get('instructions', ''), | |
| 'Story': recipe.get('story', ''), | |
| 'Tags': ', '.join(recipe.get('tags', [])), | |
| 'Submitted Date': recipe.get('submitted_date', ''), | |
| 'Image File': recipe.get('image_filename', '') | |
| }) | |
| return pd.DataFrame(export_data) | |
| def format_storage_size(size_bytes): | |
| """Convert bytes to human readable format""" | |
| for unit in ['B', 'KB', 'MB', 'GB']: | |
| if size_bytes < 1024.0: | |
| return f"{size_bytes:.1f} {unit}" | |
| size_bytes /= 1024.0 | |
| return f"{size_bytes:.1f} TB" | |
| # --- Sidebar --- | |
| with st.sidebar: | |
| st.title(f"🍲 {get_text('title')}") | |
| st.markdown(get_text('subtitle')) | |
| st.markdown("---") | |
| # Language selector | |
| lang_choice = st.radio( | |
| get_text('language_select'), | |
| ('English', 'తెలుగు'), | |
| horizontal=True, | |
| key='lang_radio' | |
| ) | |
| st.session_state['language'] = 'te' if lang_choice == 'తెలుగు' else 'en' | |
| st.info(get_text('sidebar_header')) | |
| # Storage stats | |
| stats = get_recipe_stats() | |
| st.metric(get_text('total_recipes'), stats['total_recipes']) | |
| st.metric(get_text('total_images'), stats['total_images']) | |
| st.metric(get_text('storage_size'), format_storage_size(stats['storage_size'])) | |
| # --- Main App Layout --- | |
| st.title(get_text('title')) | |
| tab_submit, tab_gallery, tab_analytics, tab_manage = st.tabs([ | |
| get_text('tab_submit'), | |
| get_text('tab_gallery'), | |
| get_text('tab_analytics'), | |
| get_text('tab_manage') | |
| ]) | |
| # --- Submission Form Tab --- | |
| with tab_submit: | |
| st.header(get_text('form_header')) | |
| with st.form(key="recipe_form"): | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| dish_name = st.text_input(label=get_text('dish_name')) | |
| your_name = st.text_input(label=get_text('your_name')) | |
| image_file = st.file_uploader(get_text('image_upload'), type=['jpg', 'jpeg', 'png']) | |
| with col2: | |
| recipe_type = st.selectbox(label=get_text('recipe_type'), options=RECIPE_TYPES) | |
| district = st.selectbox( | |
| label=get_text('district'), | |
| options=[get_text('select_district')] + DISTRICTS | |
| ) | |
| ingredients = st.text_area(label=get_text('ingredients'), height=150) | |
| instructions = st.text_area(label=get_text('instructions'), height=200) | |
| story = st.text_area(label=get_text('story'), height=150) | |
| submitted = st.form_submit_button(get_text('submit_button')) | |
| if submitted: | |
| if not dish_name: | |
| st.error(get_text('error_dish_name')) | |
| else: | |
| # Generate unique ID for this recipe | |
| recipe_id = generate_recipe_id() | |
| # Save image if provided | |
| image_filename = save_image(image_file, recipe_id) | |
| # Generate AI tags | |
| ai_tags = simulate_ai_tagging(story, ingredients) | |
| # Create new recipe with unique ID | |
| new_recipe = { | |
| "id": recipe_id, | |
| "dish_name": dish_name, | |
| "your_name": your_name, | |
| "district": district if district != get_text('select_district') else '', | |
| "recipe_type": recipe_type, | |
| "ingredients": ingredients, | |
| "instructions": instructions, | |
| "story": story, | |
| "image_filename": image_filename, | |
| "tags": ai_tags, | |
| "submitted_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| } | |
| # Save recipe to individual JSON file | |
| if save_recipe(new_recipe): | |
| # Add to session state for immediate display | |
| st.session_state.recipes.insert(0, new_recipe) | |
| st.success(f"{get_text('success_message')} (ID: {recipe_id})") | |
| st.balloons() | |
| # Show recipe details | |
| with st.expander("Recipe Details", expanded=True): | |
| st.write(f"**{get_text('recipe_id')}:** `{recipe_id}`") | |
| st.write(f"**File Location:** `{RECIPES_FOLDER}/recipe_{recipe_id}.json`") | |
| if image_filename: | |
| st.write(f"**Image Location:** `{IMAGES_FOLDER}/{image_filename}`") | |
| else: | |
| st.error("Failed to save recipe. Please try again.") | |
| # --- Community Gallery Tab --- | |
| with tab_gallery: | |
| st.header(get_text('gallery_header')) | |
| # Refresh button | |
| if st.button("🔄 Refresh Recipes"): | |
| st.session_state['refresh_recipes'] = True | |
| st.rerun() | |
| # Filters and search | |
| col1, col2, col3, col4 = st.columns([2, 1, 1, 1]) | |
| with col1: | |
| search_term = st.text_input( | |
| label="", | |
| placeholder=get_text('search_placeholder'), | |
| key="search_recipes" | |
| ) | |
| with col2: | |
| district_options = [get_text('all_districts')] + DISTRICTS | |
| district_filter = st.selectbox( | |
| get_text('filter_by_district'), | |
| district_options, | |
| key="district_filter" | |
| ) | |
| with col3: | |
| type_options = [get_text('all_types')] + RECIPE_TYPES | |
| type_filter = st.selectbox( | |
| get_text('filter_by_type'), | |
| type_options, | |
| key="type_filter" | |
| ) | |
| with col4: | |
| if st.button(get_text('clear_filters')): | |
| st.session_state.search_recipes = "" | |
| st.session_state.district_filter = get_text('all_districts') | |
| st.session_state.type_filter = get_text('all_types') | |
| st.rerun() | |
| st.markdown("---") | |
| # Filter recipes | |
| filtered_recipes = filter_recipes( | |
| st.session_state.recipes, | |
| search_term, | |
| district_filter, | |
| type_filter | |
| ) | |
| if not filtered_recipes: | |
| if not st.session_state.recipes: | |
| st.info(get_text('gallery_empty')) | |
| else: | |
| st.info("No recipes match your search criteria. Try adjusting your filters.") | |
| else: | |
| st.write(f"**{len(filtered_recipes)}** recipes found") | |
| # Display recipes | |
| for recipe in filtered_recipes: | |
| with st.container(): | |
| st.markdown(""" | |
| <style> | |
| .recipe-card { | |
| background-color: #f8f9fa; | |
| border-radius: 15px; | |
| padding: 25px; | |
| margin-bottom: 25px; | |
| border-left: 5px solid #ff6b6b; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| with st.container(): | |
| st.markdown('<div class="recipe-card">', unsafe_allow_html=True) | |
| # Header with recipe ID | |
| col_header1, col_header2, col_header3 = st.columns([2, 1, 1]) | |
| with col_header1: | |
| st.subheader(f"🍽️ {recipe['dish_name']}") | |
| st.code(f"ID: {recipe.get('id', 'N/A')}", language=None) | |
| with col_header2: | |
| if recipe.get('recipe_type'): | |
| st.info(recipe['recipe_type']) | |
| with col_header3: | |
| if recipe.get('submitted_date'): | |
| st.caption(f"{get_text('submitted_on')}: {recipe['submitted_date'][:10]}") | |
| # Metadata | |
| metadata_parts = [] | |
| if recipe.get('your_name'): | |
| metadata_parts.append(f"**{recipe['your_name']}**") | |
| if recipe.get('district'): | |
| metadata_parts.append(f"{get_text('from_district')} **{recipe['district']}**") | |
| if metadata_parts: | |
| st.caption(f"{get_text('submitted_by')} " + " • ".join(metadata_parts)) | |
| # Tags | |
| if recipe.get('tags'): | |
| tag_display = " ".join([f"`{tag}`" for tag in recipe['tags']]) | |
| st.markdown(tag_display) | |
| # Content | |
| left_col, right_col = st.columns([1, 1.2]) | |
| with left_col: | |
| if recipe.get('image_filename'): | |
| try: | |
| image_data = load_image(recipe['image_filename']) | |
| if image_data: | |
| st.image(image_data, caption=recipe['dish_name'], use_column_width=True) | |
| else: | |
| st.image("https://placehold.co/400x300/E8F4FD/31343C?text=Image+Not+Found") | |
| except: | |
| st.image("https://placehold.co/400x300/E8F4FD/31343C?text=Error+Loading+Image") | |
| else: | |
| st.image("https://placehold.co/400x300/E8F4FD/31343C?text=No+Image+Available") | |
| with right_col: | |
| if recipe.get('story'): | |
| with st.expander(get_text('story_title')): | |
| st.write(recipe['story']) | |
| with st.expander(get_text('ingredients_title')): | |
| if recipe.get('ingredients'): | |
| ingredients_list = recipe['ingredients'].split('\n') | |
| for ingredient in ingredients_list: | |
| if ingredient.strip(): | |
| st.write(f"• {ingredient.strip()}") | |
| else: | |
| st.write("No ingredients listed.") | |
| with st.expander(get_text('instructions_title'), expanded=True): | |
| st.write(recipe.get('instructions', 'No instructions provided.')) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown("---") | |
| # --- Analytics Tab --- | |
| with tab_analytics: | |
| st.header("📊 Recipe Analytics") | |
| if not st.session_state.recipes: | |
| st.info("No data available yet. Submit some recipes to see analytics!") | |
| else: | |
| # Overview metrics | |
| col1, col2, col3, col4 = st.columns(4) | |
| stats = get_recipe_stats() | |
| with col1: | |
| st.metric(get_text('total_recipes'), len(st.session_state.recipes)) | |
| with col2: | |
| unique_contributors = len(set(r.get('your_name', 'Anonymous') for r in st.session_state.recipes if r.get('your_name'))) | |
| st.metric("Contributors", unique_contributors) | |
| with col3: | |
| unique_districts = len(set(r.get('district', '') for r in st.session_state.recipes if r.get('district'))) | |
| st.metric("Districts Represented", unique_districts) | |
| with col4: | |
| recipes_with_images = len([r for r in st.session_state.recipes if r.get('image_filename')]) | |
| st.metric("Recipes with Photos", recipes_with_images) | |
| st.markdown("---") | |
| # Storage information | |
| st.subheader("📁 Storage Information") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| st.info(f"**Recipe Files:** {stats['total_recipes']}") | |
| st.caption(f"Location: `{RECIPES_FOLDER}/`") | |
| with col2: | |
| st.info(f"**Image Files:** {stats['total_images']}") | |
| st.caption(f"Location: `{IMAGES_FOLDER}/`") | |
| with col3: | |
| st.info(f"**Total Storage:** {format_storage_size(stats['storage_size'])}") | |
| st.markdown("---") | |
| # Charts | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader(get_text('top_districts')) | |
| district_counts = Counter(r.get('district', 'Unknown') for r in st.session_state.recipes if r.get('district')) | |
| if district_counts: | |
| district_df = pd.DataFrame(list(district_counts.items()), columns=['District', 'Count']) | |
| district_df = district_df.sort_values('Count', ascending=True).tail(10) | |
| st.bar_chart(district_df.set_index('District')) | |
| else: | |
| st.info("No district data available") | |
| with col2: | |
| st.subheader(get_text('popular_types')) | |
| type_counts = Counter(r.get('recipe_type', 'Unknown') for r in st.session_state.recipes) | |
| if type_counts: | |
| type_df = pd.DataFrame(list(type_counts.items()), columns=['Type', 'Count']) | |
| st.bar_chart(type_df.set_index('Type')) | |
| # Recent activity | |
| st.subheader(get_text('recent_recipes')) | |
| recent_recipes = sorted(st.session_state.recipes, | |
| key=lambda x: x.get('submitted_date', ''), | |
| reverse=True)[:10] | |
| for recipe in recent_recipes: | |
| col1, col2, col3, col4 = st.columns([2, 1, 1, 1]) | |
| with col1: | |
| st.write(f"**{recipe['dish_name']}**") | |
| with col2: | |
| st.write(recipe.get('your_name', 'Anonymous')) | |
| with col3: | |
| st.write(recipe.get('submitted_date', 'Unknown')[:10] if recipe.get('submitted_date') else 'Unknown') | |
| with col4: | |
| st.code(recipe.get('id', 'N/A'), language=None) | |
| # Export functionality | |
| st.markdown("---") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button(get_text('export_recipes')): | |
| csv_data = export_recipes_to_csv(st.session_state.recipes) | |
| if csv_data is not None: | |
| csv_string = csv_data.to_csv(index=False) | |
| st.download_button( | |
| label="Download CSV", | |
| data=csv_string, | |
| file_name=f"ruchi_chudu_recipes_{datetime.now().strftime('%Y%m%d')}.csv", | |
| mime="text/csv" | |
| ) | |
| else: | |
| st.error("No recipes to export") | |
| with col2: | |
| if st.button(get_text('backup_data')): | |
| try: | |
| import zipfile | |
| from io import BytesIO | |
| # Create a zip file containing all recipe files and images | |
| zip_buffer = BytesIO() | |
| with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: | |
| # Add all recipe JSON files | |
| recipe_files = glob.glob(os.path.join(RECIPES_FOLDER, "recipe_*.json")) | |
| for recipe_file in recipe_files: | |
| zip_file.write(recipe_file, os.path.basename(recipe_file)) | |
| # Add all image files | |
| image_files = glob.glob(os.path.join(IMAGES_FOLDER, "*.*")) | |
| for image_file in image_files: | |
| zip_file.write(image_file, f"images/{os.path.basename(image_file)}") | |
| zip_buffer.seek(0) | |
| st.download_button( | |
| label="Download Backup ZIP", | |
| data=zip_buffer.getvalue(), | |
| file_name=f"ruchi_chudu_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip", | |
| mime="application/zip" | |
| ) | |
| st.success(get_text('backup_created')) | |
| except Exception as e: | |
| st.error(f"Error creating backup: {e}") | |
| # --- Management Tab --- | |
| with tab_manage: | |
| st.header(get_text('manage_header')) | |
| if not st.session_state.recipes: | |
| st.info("No recipes to manage yet.") | |
| else: | |
| st.write(f"Managing **{len(st.session_state.recipes)}** recipes") | |
| # Search for specific recipe to manage | |
| search_manage = st.text_input("Search for recipe to manage", placeholder="Enter recipe name or ID...") | |
| # Filter recipes for management | |
| manage_recipes = st.session_state.recipes | |
| if search_manage: | |
| manage_recipes = [r for r in st.session_state.recipes if | |
| search_manage.lower() in r['dish_name'].lower() or | |
| search_manage.lower() in r.get('id', '').lower()] | |
| if not manage_recipes and search_manage: | |
| st.warning("No recipes found matching your search.") | |
| # Display recipes for management | |
| for recipe in manage_recipes[:20]: # Limit to 20 for performance | |
| with st.expander(f"🍽️ {recipe['dish_name']} (ID: {recipe.get('id', 'N/A')})"): | |
| col1, col2, col3 = st.columns([2, 1, 1]) | |
| with col1: | |
| st.write(f"**Submitted by:** {recipe.get('your_name', 'Anonymous')}") | |
| st.write(f"**District:** {recipe.get('district', 'Not specified')}") | |
| st.write(f"**Type:** {recipe.get('recipe_type', 'Not specified')}") | |
| st.write(f"**Submitted:** {recipe.get('submitted_date', 'Unknown')}") | |
| with col2: | |
| st.write(f"**Recipe File:**") | |
| st.code(f"recipe_{recipe.get('id', 'unknown')}.json") | |
| if recipe.get('image_filename'): | |
| st.write(f"**Image File:**") | |
| st.code(recipe['image_filename']) | |
| with col3: | |
| # Delete button with confirmation | |
| if st.button(f"🗑️ {get_text('delete_recipe')}", key=f"delete_{recipe.get('id', 'unknown')}"): | |
| if st.session_state.get(f"confirm_delete_{recipe.get('id')}", False): | |
| if delete_recipe(recipe.get('id')): | |
| st.success(get_text('recipe_deleted')) | |
| st.session_state['refresh_recipes'] = True | |
| st.rerun() | |
| else: | |
| st.error("Failed to delete recipe.") | |
| else: | |
| st.session_state[f"confirm_delete_{recipe.get('id')}"] = True | |
| st.warning(get_text('confirm_delete')) | |
| st.button(f"✅ Confirm Delete", key=f"confirm_{recipe.get('id')}") | |
| if len(manage_recipes) > 20: | |
| st.info(f"Showing first 20 recipes. Use search to find specific recipes. Total: {len(manage_recipes)}") | |
| # Bulk operations | |
| st.markdown("---") | |
| st.subheader("🔧 Bulk Operations") | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| if st.button("📊 Recount Storage Stats"): | |
| stats = get_recipe_stats() | |
| st.success("Storage stats updated!") | |
| with col2: | |
| if st.button("🔄 Refresh All Data"): | |
| st.session_state['refresh_recipes'] = True | |
| st.success("Data will be refreshed!") | |
| with col3: | |
| if st.button("🧹 Clean Orphaned Files"): | |
| try: | |
| # Find orphaned image files (images without corresponding recipes) | |
| recipe_ids = set(r.get('id') for r in st.session_state.recipes if r.get('id')) | |
| image_files = glob.glob(os.path.join(IMAGES_FOLDER, "*.*")) | |
| orphaned_count = 0 | |
| for image_file in image_files: | |
| image_name = os.path.basename(image_file) | |
| image_id = image_name.split('.')[0] # Get ID from filename | |
| if image_id not in recipe_ids: | |
| os.remove(image_file) | |
| orphaned_count += 1 | |
| st.success(f"Cleaned {orphaned_count} orphaned files!") | |
| except Exception as e: | |
| st.error(f"Error cleaning files: {e}") | |
| # --- File Structure Display --- | |
| st.sidebar.markdown("---") | |
| st.sidebar.subheader("📁 File Structure") | |
| if st.sidebar.button("Show File Structure"): | |
| with st.sidebar.expander("Current Structure", expanded=True): | |
| try: | |
| recipe_files = glob.glob(os.path.join(RECIPES_FOLDER, "*.json")) | |
| image_files = glob.glob(os.path.join(IMAGES_FOLDER, "*.*")) | |
| st.write("📂 recipes/") | |
| for recipe_file in sorted(recipe_files)[:5]: # Show first 5 | |
| st.write(f" 📄 {os.path.basename(recipe_file)}") | |
| if len(recipe_files) > 5: | |
| st.write(f" ... and {len(recipe_files) - 5} more") | |
| st.write("📂 recipe_images/") | |
| for image_file in sorted(image_files)[:5]: # Show first 5 | |
| st.write(f" 🖼️ {os.path.basename(image_file)}") | |
| if len(image_files) > 5: | |
| st.write(f" ... and {len(image_files) - 5} more") | |
| except: | |
| st.write("Error reading file structure") |