Procházet zdrojové kódy

TG-23 TG-24 TG-26: Implement Premium CSS Frontend Aesthetics, Build Numerical Macro Filters, and Build the My Plate Combinations Engine.

lanfr144 před 3 týdny
rodič
revize
63f56b503e
1 změnil soubory, kde provedl 146 přidání a 7 odebrání
  1. 146 7
      app.py

+ 146 - 7
app.py

@@ -75,6 +75,15 @@ def verify_login(username, password):
             return bcrypt.checkpw(password.encode('utf-8'), result['password_hash'].encode('utf-8'))
     return False
 
+def get_user_id(username):
+    conn = get_db_connection('app_auth')
+    if not conn: return None
+    with conn.cursor() as cursor:
+        cursor.execute("SELECT id FROM users WHERE username = %s LIMIT 1", (username,))
+        result = cursor.fetchone()
+        conn.close()
+        return result['id'] if result else None
+
 def register_user(username, password):
     conn = get_db_connection('app_auth')
     if not conn: return False
@@ -94,6 +103,60 @@ def register_user(username, password):
 # -------------------------------------------------------------------
 st.set_page_config(page_title="Food AI Explorer", page_icon="🍔", layout="wide")
 
+# Scientific Medical Theming (CSS Injection)
+st.markdown("""
+<style>
+    @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');
+    
+    html, body, [class*="css"]  {
+        font-family: 'Inter', sans-serif;
+        background-color: #0b192c;
+        color: #e2e8f0;
+    }
+    
+    h1, h2, h3 {
+        color: #38bdf8 !important;
+        font-weight: 600;
+        letter-spacing: 0.5px;
+    }
+    
+    div[data-testid="stSidebar"] {
+        background: rgba(11, 25, 44, 0.95) !important;
+        backdrop-filter: blur(10px);
+        border-right: 1px solid #1e293b;
+    }
+    
+    .stButton>button {
+        background: linear-gradient(135deg, #0ea5e9, #0284c7);
+        color: white;
+        border: none;
+        border-radius: 6px;
+        box-shadow: 0 4px 10px rgba(2, 132, 199, 0.3);
+        transition: transform 0.2s, box-shadow 0.2s;
+    }
+    
+    .stButton>button:hover {
+        transform: scale(1.02);
+        box-shadow: 0 6px 15px rgba(2, 132, 199, 0.5);
+    }
+    
+    .stTextInput>div>div>input, .stNumberInput>div>div>input {
+        background-color: #0f172a;
+        color: #f8fafc;
+        border: 1px solid #38bdf8;
+        border-radius: 6px;
+    }
+    
+    .stTabs [data-baseweb="tab"] {
+        color: #94a3b8;
+    }
+    .stTabs [aria-selected="true"] {
+        color: #38bdf8 !important;
+        border-bottom-color: #38bdf8 !important;
+    }
+</style>
+""", unsafe_allow_html=True)
+
 if "authenticated_user" not in st.session_state:
     st.session_state["authenticated_user"] = None
 
@@ -146,7 +209,7 @@ if conn_reader:
         total_products = cursor.fetchone()['total']
         st.sidebar.info(f"Database Scope: {total_products} products.")
 
-tab_chat, tab_explore = st.tabs(["💬 AI Chat", "🔍 Food Search & Details"])
+tab_chat, tab_explore, tab_plate = st.tabs(["💬 AI Chat", "🔬 Scientific Nutrients Search", "🍽️ My Plate Combinations"])
 
 with tab_chat:
     st.subheader("Chat with the Context")
@@ -209,28 +272,104 @@ with tab_explore:
     st.subheader("Raw Data Search")
     search_query = st.text_input("Search Product Name or Ingredient (e.g. 'Nutella' or 'Sugar')")
     
+    st.markdown("### 🧬 Filter by Macronutrients")
+    cols = st.columns(4)
+    min_pro = cols[0].number_input("Min Protein (g)", 0, 1000, 0)
+    min_fat = cols[1].number_input("Min Fat (g)", 0, 1000, 0)
+    min_carb = cols[2].number_input("Min Carbs (g)", 0, 1000, 0)
+    max_sug = cols[3].number_input("Max Sugar (g)", 0, 1000, 1000)
+    
     if st.button("Search Database") and search_query and conn_reader:
         with st.spinner("Querying MySQL..."):
             try:
                 with conn_reader.cursor() as cursor:
-                    # Leverage the FULLTEXT INDEX built in init.sql
+                    # Leverage the FULLTEXT INDEX and dynamically parsed pandas schema
                     query = """
-                        SELECT code, product_name, generic_name, brands, ingredients_text 
+                        SELECT code, product_name, generic_name, brands, 
+                               proteins_100g, fat_100g, carbohydrates_100g, sugars_100g, energy_kcal_100g
                         FROM products 
                         WHERE MATCH(product_name, ingredients_text) AGAINST(%s IN NATURAL LANGUAGE MODE)
+                        AND (proteins_100g >= %s OR proteins_100g IS NULL)
+                        AND (fat_100g >= %s OR fat_100g IS NULL)
+                        AND (carbohydrates_100g >= %s OR carbohydrates_100g IS NULL)
+                        AND (sugars_100g <= %s OR sugars_100g IS NULL)
                         LIMIT 50
                     """
-                    cursor.execute(query, (search_query,))
+                    cursor.execute(query, (search_query, min_pro, min_fat, min_carb, max_sug))
                     results = cursor.fetchall()
             except Exception as e:
-                st.error(f"SQL Error: {e}")
+                st.error(f"SQL Error: {e} (Has the background ingestion script created the new full schema yet?)")
                 results = []
                 
         if results:
-            st.success(f"Found {len(results)} matching records!")
+            st.success(f"Found {len(results)} matching records! (Use product 'code' to add to your Plate)")
             st.dataframe(results, use_container_width=True)
         else:
-            st.warning("No products found matching those terms.")
+            st.warning("No products found matching those strict terms.")
+
+with tab_plate:
+    st.subheader("🍽️ My Plate Builder")
+    st.markdown("Create a mapped collection of foods to calculate compounding total nutritional values.")
+    
+    uid = get_user_id(st.session_state["authenticated_user"])
+    if not uid:
+        st.warning("Authentication link failed.")
+    else:
+        conn = get_db_connection('app_auth')
+        if conn:
+            with conn.cursor() as cursor:
+                # Get the user's active plates
+                cursor.execute("SELECT id, plate_name FROM plates WHERE user_id = %s", (uid,))
+                plates = cursor.fetchall()
+                
+                with st.expander("➕ Create a New Plate"):
+                    new_plate_name = st.text_input("Plate Name (e.g., 'Bulking Meal')")
+                    if st.button("Create Plate"):
+                        cursor.execute("INSERT INTO plates (user_id, plate_name) VALUES (%s, %s)", (uid, new_plate_name))
+                        conn.commit()
+                        st.success("New plate established in the database!")
+                        st.rerun()
+
+                if plates:
+                    selected_plate = st.selectbox("Select Active Plate", [p['plate_name'] for p in plates])
+                    active_p_id = next(p['id'] for p in plates if p['plate_name'] == selected_plate)
+                    
+                    st.markdown(f"### Current Items in `{selected_plate}`")
+                    
+                    try:
+                        cursor.execute("""
+                            SELECT i.id, i.product_code, i.quantity_grams, p.product_name, p.proteins_100g, p.fat_100g, p.carbohydrates_100g 
+                            FROM plate_items i
+                            LEFT JOIN products p ON i.product_code = p.code
+                            WHERE i.plate_id = %s
+                        """, (active_p_id,))
+                        items = cursor.fetchall()
+                        
+                        if items:
+                            st.dataframe(items, use_container_width=True)
+                            
+                            # Aggregate total logic mapping grams relative to 100g baseline
+                            total_pro = sum((float(i['proteins_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
+                            total_fat = sum((float(i['fat_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
+                            total_carb = sum((float(i['carbohydrates_100g'] or 0) * (float(i['quantity_grams'])/100.0)) for i in items)
+                            
+                            st.markdown("### 📊 Combined Nutritional Value")
+                            st.info(f"**Total Protein:** {total_pro:.1f}g | **Total Fat:** {total_fat:.1f}g | **Total Carbs:** {total_carb:.1f}g")
+                        else:
+                            st.write("Plate is empty. Switch to the Search tab, find a tracking 'code', and add it below!")
+                    except Exception as e:
+                        st.error(f"Cannot render plate items until dynamic product schema exists. {e}")
+                        
+                    st.markdown("---")
+                    st.markdown("### Add Food to Plate")
+                    add_code = st.text_input("Enter exact Product `code` (Find this in the Search tab)")
+                    add_grams = st.number_input("Portion Quantity (Grams)", min_value=1.0, value=100.0)
+                    if st.button("Add Item to Plate"):
+                        cursor.execute("INSERT INTO plate_items (plate_id, product_code, quantity_grams) VALUES (%s, %s, %s)", 
+                                      (active_p_id, add_code, add_grams))
+                        conn.commit()
+                        st.success("Item logically attached to plate!")
+                        st.rerun()
 
 if conn_reader:
     conn_reader.close()