""" Integrated Makerspace Inventory Management System Smart inventory management powered by AI """ import google.generativeai as genai import chromadb from sentence_transformers import SentenceTransformer import gradio as gr from PIL import Image import json import re import os import pandas as pd from pdf2image import convert_from_path import pytesseract from rapidfuzz import process from transformers import AutoTokenizer, AutoModelForSequenceClassification import torch import datetime import itertools import math import numpy as np import matplotlib.pyplot as plt import io import shutil from typing import List, Dict, Tuple, Optional print("=" * 80) print("🎨 Makerspace Inventory Management System") print("=" * 80) print("Modules: Check Out | Add Items | Inventory Analysis") print("=" * 80 + "\n") # Gemini API Configuration GEMINI_API_KEY = "AIzaSyA5_Cx0rriZWtTr1KyEkWCJ6fVyXpUKuJw" genai.configure(api_key=GEMINI_API_KEY) gemini_model = genai.GenerativeModel('models/gemini-2.5-flash') print("βœ… Gemini API configured (gemini-2.5-flash)") # Initialize shared embedding model for ChromaDB embedding_model = SentenceTransformer('all-MiniLM-L6-v2') print("βœ… Embedding model loaded") # File paths ITEMS_CSV = "items.csv" LOCATIONS_CSV = "locations.csv" CHECKOUTS_CSV = "checkouts.csv" UPDATE_LOG_CSV = "update_log.csv" # ============================================================================= # CUSTOM CSS # ============================================================================= CUSTOM_CSS = """ /* Global styling - Fixed desktop layout */ .gradio-container { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important; max-width: 1400px !important; margin: auto !important; } /* Fix all blocks to have consistent width */ .gradio-container .block { width: 100% !important; max-width: 100% !important; } /* Ensure all rows stay full width */ .gradio-container .row { width: 100% !important; } /* Main menu - gradient background */ #main-menu-container { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 50px 40px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.12); margin-bottom: 30px; width: 100%; } #main-menu-title { color: white !important; text-align: center; font-size: 2.8em !important; font-weight: 700 !important; margin-bottom: 8px !important; letter-spacing: -0.5px; } #main-menu-subtitle { color: rgba(255,255,255,0.95) !important; text-align: center; font-size: 1.1em !important; margin-bottom: 40px !important; font-weight: 300; } /* Module buttons in main menu */ .module-button { background: white !important; color: #667eea !important; border: none !important; padding: 32px 24px !important; font-size: 1.25em !important; font-weight: 600 !important; border-radius: 12px !important; box-shadow: 0 4px 16px rgba(0,0,0,0.1) !important; transition: all 0.3s ease !important; min-height: 120px !important; display: flex !important; align-items: center !important; justify-content: center !important; } .module-button:hover { transform: translateY(-3px) !important; box-shadow: 0 6px 24px rgba(0,0,0,0.15) !important; } /* Clean module pages - consistent sizing */ .module-page { background: white; padding: 40px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); min-height: 600px; width: 100%; } /* Section headers */ .section-header { color: #1a202c; font-size: 2.2em; font-weight: 700; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 3px solid #667eea; } /* Subsection headers */ .subsection-header { color: #2d3748; font-size: 1.4em; font-weight: 600; margin-top: 30px; margin-bottom: 15px; } /* Instructions box */ .instructions-box { background: #f7fafc; border-left: 4px solid #667eea; padding: 20px 25px; border-radius: 8px; margin: 20px 0; } .instructions-box p { margin: 10px 0; line-height: 1.7; } /* Buttons - consistent sizing */ button { min-height: 48px !important; font-size: 1.05em !important; font-weight: 600 !important; border-radius: 8px !important; padding: 12px 28px !important; transition: all 0.3s ease !important; } .primary-button { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; color: white !important; border: none !important; } .primary-button:hover { transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important; } /* Input fields - consistent sizing */ input, textarea, select { min-height: 44px !important; font-size: 1em !important; } /* Image upload areas - better styling */ .image-upload-container { min-height: 400px !important; } /* Upload/Webcam tab buttons - make them visible and styled */ /* Use more aggressive selectors for Gradio 5.50 */ .tabs button, .tab-nav button, button[id*="component"], button[id*="upload"], button[id*="webcam"] { background: white !important; border: 2px solid #cbd5e0 !important; border-radius: 10px !important; padding: 14px 28px !important; font-size: 1em !important; font-weight: 600 !important; color: #2d3748 !important; transition: all 0.3s ease !important; min-width: 130px !important; margin: 4px !important; } .tabs button:hover, .tab-nav button:hover { background: #667eea !important; border-color: #667eea !important; color: white !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important; } .tabs button.selected, .tab-nav button.selected, button[aria-selected="true"] { background: #667eea !important; border-color: #667eea !important; color: white !important; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4) !important; } /* Force ALL SVG icons to be visible with color */ .tabs button svg, .tab-nav button svg, button[id*="component"] svg, button svg { width: 20px !important; height: 20px !important; fill: currentColor !important; stroke: currentColor !important; opacity: 1 !important; visibility: visible !important; } .tabs button svg *, .tab-nav button svg *, button svg * { fill: currentColor !important; stroke: currentColor !important; opacity: 1 !important; } /* Specific for image component tabs */ div[id*="image"] button, .image-container button { background: white !important; color: #2d3748 !important; border: 2px solid #cbd5e0 !important; } div[id*="image"] button:hover, .image-container button:hover { background: #667eea !important; color: white !important; } div[id*="image"] button svg, .image-container button svg { fill: currentColor !important; stroke: currentColor !important; } [data-testid="image"] [role="tablist"], .image-container [role="tablist"], button[role="tab"] { background: transparent !important; } [data-testid="image"] [role="tab"], .image-container [role="tab"], button[role="tab"] { background: white !important; border: 2px solid #cbd5e0 !important; border-radius: 10px !important; padding: 14px 28px !important; font-size: 1em !important; font-weight: 600 !important; color: #2d3748 !important; transition: all 0.3s ease !important; min-width: 130px !important; margin: 4px !important; } [data-testid="image"] [role="tab"]:hover, .image-container [role="tab"]:hover, button[role="tab"]:hover { background: #667eea !important; border-color: #667eea !important; color: white !important; transform: translateY(-2px) !important; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important; } [data-testid="image"] [role="tab"][aria-selected="true"], .image-container [role="tab"][aria-selected="true"], button[role="tab"][aria-selected="true"] { background: #667eea !important; border-color: #667eea !important; color: white !important; box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4) !important; } /* Icon colors in tabs - force visibility */ [data-testid="image"] [role="tab"] svg, .image-container [role="tab"] svg, button[role="tab"] svg { width: 20px !important; height: 20px !important; fill: currentColor !important; stroke: currentColor !important; } [data-testid="image"] [role="tab"] svg path, [data-testid="image"] [role="tab"] svg line, [data-testid="image"] [role="tab"] svg circle, [data-testid="image"] [role="tab"] svg rect, .image-container [role="tab"] svg path, button[role="tab"] svg path { stroke: currentColor !important; fill: currentColor !important; stroke-width: 2 !important; } /* Ensure tab text is visible */ [data-testid="image"] [role="tab"] span, .image-container [role="tab"] span, button[role="tab"] span { color: inherit !important; } /* Upload area */ [data-testid="image"] .upload-container { background: white !important; border: 2px dashed #cbd5e0 !important; border-radius: 12px !important; padding: 40px !important; min-height: 350px !important; } [data-testid="image"] .upload-container:hover { border-color: #667eea !important; background: #f7fafc !important; } /* File upload button styling */ .file-upload button { background: white !important; border: 2px solid #e2e8f0 !important; color: #2d3748 !important; } .file-upload button:hover { background: #f7fafc !important; border-color: #667eea !important; } /* Dataframe tables */ .dataframe-container { min-height: 200px !important; max-height: 500px !important; } /* Markdown containers */ .markdown-container { line-height: 1.7 !important; } /* Accordions */ .gradio-accordion { border: 1px solid #e2e8f0 !important; border-radius: 8px !important; margin: 15px 0 !important; } /* Tabs */ .tabs { border-radius: 8px !important; margin-top: 20px !important; } /* Status messages styling */ .status-loading { background: #fff3cd; border-left: 4px solid #ffc107; color: #856404; padding: 15px; border-radius: 6px; margin: 15px 0; } .status-success { background: #d4edda; border-left: 4px solid #28a745; color: #155724; padding: 15px; border-radius: 6px; margin: 15px 0; } .status-error { background: #f8d7da; border-left: 4px solid #dc3545; color: #721c24; padding: 15px; border-radius: 6px; margin: 15px 0; } /* Remove mobile responsiveness - keep desktop width */ @media (max-width: 768px) { .gradio-container { max-width: 1400px !important; } } /* Ensure consistent spacing */ .gap { gap: 20px !important; } /* Column consistency */ .column { padding: 10px !important; } """ # Example files for grading/testing EXAMPLE_CHECKOUT_IMAGE = "screwdriver.png" if os.path.exists("screwdriver.png") else None EXAMPLE_RECEIPT_PDF = "mcmaster_receipt.pdf" if os.path.exists("mcmaster_receipt.pdf") else None if EXAMPLE_CHECKOUT_IMAGE: print(f"βœ… Example checkout image found: {EXAMPLE_CHECKOUT_IMAGE}") if EXAMPLE_RECEIPT_PDF: print(f"βœ… Example receipt PDF found: {EXAMPLE_RECEIPT_PDF}") # ============================================================================= # DATA MANAGEMENT FUNCTIONS # ============================================================================= def load_items(): """Load items from CSV""" return pd.read_csv(ITEMS_CSV) def save_items(df): """Save items to CSV""" df.to_csv(ITEMS_CSV, index=False) def load_locations(): """Load locations from CSV""" return pd.read_csv(LOCATIONS_CSV) def load_checkouts(): """Load checkouts from CSV""" return pd.read_csv(CHECKOUTS_CSV) def append_checkout(timestamp, user_id, session_id, item_id): """Append a new checkout record""" df = load_checkouts() new_row = pd.DataFrame([[timestamp, user_id, session_id, item_id]], columns=["timestamp", "user_id", "session_id", "item_id"]) df = pd.concat([df, new_row], ignore_index=True) df.to_csv(CHECKOUTS_CSV, index=False) def get_categories(): """Get list of all categories""" df = load_items() return sorted(df['category'].unique().tolist()) def rebuild_chromadb(): """Rebuild ChromaDB from current inventory""" global chroma_client, collection df = load_items() chroma_client = chromadb.Client() try: chroma_client.delete_collection(name="makerspace_inventory") except: pass collection = chroma_client.create_collection( name="makerspace_inventory", metadata={"description": "Makerspace tool inventory"} ) documents = [] metadatas = [] ids = [] for i, row in df.iterrows(): doc_text = f"{row['item_name']} {row['category']} {row['description']}" documents.append(doc_text) metadatas.append({ "item_id": row['item_id'], "item_name": row['item_name'], "category": row['category'], "quantity": str(row['quantity']), "unit": row['unit'], "description": row['description'], "location_id": row['location_id'] }) ids.append(f"item_{i}") collection.add( documents=documents, metadatas=metadatas, ids=ids ) print(f"βœ… ChromaDB rebuilt with {len(df)} items") # Initialize ChromaDB rebuild_chromadb() # ============================================================================= # CHECK OUT MODULE FUNCTIONS # ============================================================================= def retrieve_top_candidates(query_text, top_k=5): """Retrieve top matching items from vector database""" results = collection.query( query_texts=[query_text], n_results=top_k ) candidates = [] if results['metadatas'] and len(results['metadatas'][0]) > 0: for metadata in results['metadatas'][0]: candidates.append({ 'Item ID': metadata['item_id'], 'Item Name': metadata['item_name'], 'Category': metadata['category'], 'Quantity': int(metadata['quantity']), 'Unit': metadata['unit'], 'Description': metadata['description'], 'Location': metadata['location_id'] }) return candidates def detect_all_items(image): """Detect ALL items in image using Gemini""" prompt = """Analyze this image and list EVERY distinct tool or item you see. IMPORTANT: List each item on ONE line only. Do not include additional details like "Type:", "Brand:", etc. Format your response as: 1. [One-line description including brand and key features] 2. [One-line description including brand and key features] Example: 1. Digital caliper with LCD display 2. Arduino microcontroller board If you see only one item, list just that one. If you see no tools/items, respond with "No items detected." Focus on items in the FOREGROUND. Ignore background objects unless they are clearly the subject.""" try: if isinstance(image, str): image = Image.open(image) response = gemini_model.generate_content([prompt, image]) response_text = response.text.strip() if "no items" in response_text.lower(): return "No items detected" items = [] lines = response_text.split('\n') for line in lines: line = line.strip() if re.match(r'^\d+[\.\)]\s+', line): item_desc = re.sub(r'^\d+[\.\)]\s+', '', line) if item_desc: items.append(item_desc.strip()) if not items and response_text: items = [response_text] return items if items else "Error: Could not parse items from response" except Exception as e: return f"Error: {str(e)}" def match_single_item_to_inventory(description): """Match a single item description to inventory using ChromaDB""" try: candidates = retrieve_top_candidates(description, top_k=3) if not candidates: return None for candidate in candidates: if candidate['Quantity'] > 0: return candidate return None except Exception as e: print(f"Error matching item: {e}") return None def scan_items_checkout(image, manual_text): """Process image or manual text and return matched items""" detected_items = [] if image is not None: detected = detect_all_items(image) if isinstance(detected, list): detected_items.extend(detected) elif isinstance(detected, str) and "error" not in detected.lower(): detected_items.append(detected) if manual_text and manual_text.strip(): manual_items = [item.strip() for item in manual_text.split(',') if item.strip()] detected_items.extend(manual_items) if not detected_items: return None, "⚠️ No items detected. Please upload an image or enter item names manually." matched_items = [] for desc in detected_items: match = match_single_item_to_inventory(desc) if match: matched_items.append({ 'description': desc, 'item': match, 'quantity': 1 }) if not matched_items: return None, "⚠️ Could not match any detected items to inventory." return matched_items, "" def create_checkout_item_display(item_data, index): """Create display text for a matched item""" item = item_data['item'] desc = item_data['description'] display = f"""
πŸ” DETECTED ITEM
{desc}
Matched Item:{item['Item Name']} Category:{item['Category']} Location:{item['Location']} Available:{item['Quantity']} {item['Unit']}
""" return display def confirm_checkout_preview(matched_items): """Generate checkout confirmation preview""" if not matched_items: return "No items to check out." preview = "# πŸ“‹ Checkout Summary\n\n" preview += "Please review your items before completing checkout:\n\n" for i, item_data in enumerate(matched_items, 1): item = item_data['item'] qty = item_data['quantity'] preview += f"{i}. **{item['Item Name']}** Γ— {qty} (Location: {item['Location']})\n" preview += f"\n**Total Items:** {len(matched_items)}" return preview def process_checkout(matched_items, user_id_input): """Process the checkout and update inventory""" if not matched_items: return "No items to check out." if not user_id_input or not user_id_input.strip(): user_id = f"U{np.random.randint(1, 9999):04d}" else: user_id = user_id_input.strip() df_checkouts = load_checkouts() if len(df_checkouts) > 0: last_session = df_checkouts['session_id'].max() session_num = int(last_session[1:]) + 1 else: session_num = 1 session_id = f"S{session_num:05d}" df_items = load_items() timestamp_base = datetime.datetime.now() checked_out = [] errors = [] for i, item_data in enumerate(matched_items): item_id = item_data['item']['Item ID'] qty = item_data['quantity'] item_name = item_data['item']['Item Name'] item_idx = df_items[df_items['item_id'] == item_id].index if len(item_idx) == 0: errors.append(f"Item {item_name} not found in inventory") continue item_idx = item_idx[0] current_qty = df_items.loc[item_idx, 'quantity'] if current_qty < qty: errors.append(f"Not enough {item_name} available (requested: {qty}, available: {current_qty})") continue df_items.loc[item_idx, 'quantity'] = current_qty - qty checkout_time = timestamp_base + datetime.timedelta(seconds=i*10) append_checkout( checkout_time.strftime("%Y-%m-%dT%H:%M:%SZ"), user_id, session_id, item_id ) checked_out.append(f"βœ… {item_name} Γ— {qty}") save_items(df_items) rebuild_chromadb() summary = f"# βœ… Checkout Complete!\n\n" summary += f"**Session ID:** `{session_id}`\n" summary += f"**User ID:** `{user_id}`\n" summary += f"**Timestamp:** {timestamp_base.strftime('%Y-%m-%d %H:%M:%S')}\n\n" if checked_out: summary += "## Items Checked Out:\n" for item in checked_out: summary += f"- {item}\n" if errors: summary += "\n## ⚠️ Errors:\n" for error in errors: summary += f"- {error}\n" return summary # ============================================================================= # ADD ITEMS MODULE FUNCTIONS # ============================================================================= def extract_text_from_receipt(file_path): """Extract text from PDF or image receipt""" if file_path is None: return None, [] try: # Try to find tesseract automatically import shutil tesseract_path = shutil.which('tesseract') if tesseract_path: pytesseract.pytesseract.tesseract_cmd = tesseract_path else: # Try common paths for path in ['/usr/bin/tesseract', '/usr/local/bin/tesseract', '/bin/tesseract']: if os.path.exists(path): pytesseract.pytesseract.tesseract_cmd = path break text = "" if file_path.name.lower().endswith('.pdf'): # Try multiple possible poppler paths for Hugging Face poppler_paths = [None, '/usr/bin', '/usr/local/bin', '/bin'] images = None last_error = None for poppler_path in poppler_paths: try: if poppler_path: images = convert_from_path(file_path.name, poppler_path=poppler_path) else: images = convert_from_path(file_path.name) break # Success! Exit loop except Exception as e: last_error = str(e) continue if images is None: # If all paths failed, return helpful error return [[True, f"PDF processing unavailable. Please upload images (PNG/JPG) instead. Error: {last_error}", "", ""]], [] for img in images: text += pytesseract.image_to_string(img) + "\n" else: img = Image.open(file_path.name) text = pytesseract.image_to_string(img) if not text.strip(): return [["No text extracted", "", "", True]], [] proposals = parse_receipt_text(text) if not proposals: return [["No items recognized", "", "", True]], [] table_data = [] for prop in proposals: table_data.append([ True, prop['item_name'], str(prop['quantity']), prop['match_type'] ]) return table_data, proposals except Exception as e: return [[True, f"Error: {str(e)}", "", ""]], [] def parse_receipt_text(text): """Parse receipt text to extract items and quantities""" df_items = load_items() item_names = df_items['item_name'].tolist() proposals = [] lines = text.split('\n') for line in lines: line = line.strip() if not line or len(line) < 3: continue qty_match = re.search(r'(\d+)\s*x?\s*(.+)', line, re.IGNORECASE) if qty_match: qty = int(qty_match.group(1)) item_text = qty_match.group(2).strip() else: qty = 1 item_text = line match = process.extractOne(item_text, item_names, score_cutoff=60) if match: matched_name = match[0] confidence = match[1] item_row = df_items[df_items['item_name'] == matched_name].iloc[0] proposals.append({ 'item_id': item_row['item_id'], 'item_name': matched_name, 'quantity': qty, 'match_type': f"Fuzzy ({confidence}%)", 'original_text': item_text }) return proposals def apply_updates_from_table(table_data): """Apply inventory updates from edited table""" # Convert to list if it's a DataFrame if isinstance(table_data, pd.DataFrame): if table_data.empty: return "No updates to apply.", None table_data = table_data.values.tolist() if not table_data or len(table_data) == 0: return "No updates to apply.", None df_items = load_items() updated = [] errors = [] for row in table_data: if not row[0]: continue item_name = row[1] try: qty = int(row[2]) except: errors.append(f"Invalid quantity for {item_name}") continue item_names = df_items['item_name'].tolist() match = process.extractOne(item_name, item_names, score_cutoff=60) if not match: errors.append(f"Could not find item: {item_name}") continue matched_name = match[0] item_idx = df_items[df_items['item_name'] == matched_name].index[0] current_qty = df_items.loc[item_idx, 'quantity'] new_qty = current_qty + qty df_items.loc[item_idx, 'quantity'] = new_qty updated.append(f"βœ… {matched_name}: {current_qty} β†’ {new_qty} (+{qty})") if not updated: return "No items were updated. " + ("\n".join(errors) if errors else ""), None save_items(df_items) rebuild_chromadb() timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_entry = f"{timestamp},Receipt Upload,{len(updated)} items\n" with open(UPDATE_LOG_CSV, 'a') as f: f.write(log_entry) summary = "# βœ… Updates Applied!\n\n" for item in updated: summary += f"- {item}\n" if errors: summary += "\n## ⚠️ Warnings:\n" for error in errors: summary += f"- {error}\n" return summary, df_items[['item_id', 'item_name', 'category', 'quantity', 'unit', 'location_id']] def manual_update(item_name, quantity): """Manually update an item's quantity""" if not item_name or not quantity: return "⚠️ Please provide item name and quantity.", None try: qty = int(quantity) except: return "⚠️ Invalid quantity.", None df_items = load_items() item_names = df_items['item_name'].tolist() match = process.extractOne(item_name, item_names, score_cutoff=60) if not match: return f"❌ Could not find item: {item_name}", None matched_name = match[0] item_idx = df_items[df_items['item_name'] == matched_name].index[0] current_qty = df_items.loc[item_idx, 'quantity'] new_qty = current_qty + qty df_items.loc[item_idx, 'quantity'] = new_qty save_items(df_items) rebuild_chromadb() timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_entry = f"{timestamp},Manual Update,{matched_name}: {current_qty} β†’ {new_qty}\n" with open(UPDATE_LOG_CSV, 'a') as f: f.write(log_entry) summary = f"βœ… Updated **{matched_name}**: {current_qty} β†’ {new_qty} (+{qty})" return summary, df_items[['item_id', 'item_name', 'category', 'quantity', 'unit', 'location_id']] def view_inventory_table(category_filter="All"): """View current inventory with optional category filter""" df_items = load_items() if category_filter != "All": df_items = df_items[df_items['category'] == category_filter] return df_items[['item_id', 'item_name', 'category', 'quantity', 'unit', 'location_id']] def view_update_history(): """View update history""" if not os.path.exists(UPDATE_LOG_CSV) or os.path.getsize(UPDATE_LOG_CSV) == 0: return "No update history available." with open(UPDATE_LOG_CSV, 'r') as f: history = f.read() return history if history.strip() else "No update history available." # ============================================================================= # INVENTORY ANALYSIS MODULE FUNCTIONS # ============================================================================= def ensure_sessions(df_chk, gap_minutes=30): """Ensure sessions exist in checkout data""" df = df_chk.copy() if "session_id" in df.columns and df['session_id'].notna().all(): return df if "user_id" not in df.columns or "timestamp" not in df.columns: df['session_id'] = [f"S{i:05d}" for i in range(1, len(df) + 1)] return df df["timestamp"] = pd.to_datetime(df["timestamp"]) df.sort_values(["user_id", "timestamp"], inplace=True) session_ids = [] last_user = None last_time = None session_counter = 0 gap = pd.Timedelta(minutes=gap_minutes) for row in df.itertuples(): if (row.user_id != last_user) or (last_time is None) or ((row.timestamp - last_time) > gap): session_counter += 1 current_session_id = f"AUTO_S{session_counter:05d}" session_ids.append(current_session_id) last_user = row.user_id last_time = row.timestamp df["session_id"] = session_ids return df def baskets_from_sessions(df_chk): """Create baskets from checkout sessions""" return df_chk.groupby("session_id")["item_id"].apply(lambda x: set(x)).tolist() def pair_metrics(baskets, min_support=0.02): """Compute support, confidence, lift for item pairs""" n = len(baskets) item_counts = {} pair_counts = {} for b in baskets: for i in b: item_counts[i] = item_counts.get(i, 0) + 1 for a, b_item in itertools.combinations(sorted(b), 2): pair_counts[(a, b_item)] = pair_counts.get((a, b_item), 0) + 1 rows = [] for (a, b_item), c_ab in pair_counts.items(): supp = c_ab / n if supp < min_support: continue pa = item_counts[a] / n pb = item_counts[b_item] / n conf_a_b = supp / pa conf_b_a = supp / pb lift = supp / (pa * pb) rows.append((a, b_item, supp, conf_a_b, conf_b_a, lift)) df = pd.DataFrame(rows, columns=["item_a", "item_b", "support", "conf_a_b", "conf_b_a", "lift"]) df.sort_values(["lift", "support"], ascending=[False, False], inplace=True) return df, item_counts def build_distance_matrix(df_loc): """Precompute Euclidean distances between all locations""" loc_ids = df_loc["location_id"].tolist() coords = {r.location_id: (float(r.x), float(r.y)) for r in df_loc.itertuples()} dist = {} for a in loc_ids: xa, ya = coords[a] for b in loc_ids: xb, yb = coords[b] dist[(a, b)] = math.dist((xa, ya), (xb, yb)) return loc_ids, coords, dist def total_weighted_distance(item2loc, W, dist): """Calculate total weighted distance""" cost = 0.0 for (a, b), w in W.items(): la = item2loc.get(a) lb = item2loc.get(b) if la is None or lb is None: continue cost += w * dist[(la, lb)] return cost def delta_cost_for_move(item, from_loc, to_loc, item2loc, W, dist): """Compute change in cost if item moves from from_loc to to_loc""" delta = 0.0 for (a, b), w in W.items(): if a == item or b == item: other = b if a == item else a other_loc = item2loc.get(other) if other_loc is None: continue old_d = dist[(from_loc, other_loc)] new_d = dist[(to_loc, other_loc)] delta += w * (new_d - old_d) return delta def greedy_relocate(df_items, df_loc, W, dist, top_k=30, max_moves=15, min_gain=0.05): """Greedy relocation algorithm""" original_item2loc = dict(zip(df_items["item_id"], df_items["location_id"])) item2loc = original_item2loc.copy() loc2item = {loc: item for item, loc in item2loc.items()} all_locs = df_loc["location_id"].tolist() moved_items = set() used_empty_targets = set() recs = [] sorted_pairs = sorted(W.items(), key=lambda x: x[1], reverse=True)[:top_k] for (a, b), w in sorted_pairs: for item in (a, b): if item in moved_items: continue from_loc = item2loc[item] best_cand = None best_delta = 0.0 best_occ = None for cand in all_locs: if cand == from_loc: continue occ = loc2item.get(cand) if occ is None and cand in used_empty_targets: continue if occ is not None and occ in moved_items: continue delta = delta_cost_for_move(item, from_loc, cand, item2loc, W, dist) if delta < best_delta: best_delta = delta best_cand = cand best_occ = occ gain = -best_delta if best_cand is not None and gain >= min_gain: cand = best_cand occ = best_occ recs.append({ "move_item": item, "from": from_loc, "to": cand, "swap_with": occ if occ else "Empty", "gain": gain }) if occ is not None: item2loc[occ] = from_loc loc2item[from_loc] = occ moved_items.add(occ) else: del loc2item[from_loc] used_empty_targets.add(cand) item2loc[item] = cand loc2item[cand] = item moved_items.add(item) if len(recs) >= max_moves: break if len(recs) >= max_moves: break df_recs = pd.DataFrame(recs) if recs else None return df_recs, item2loc def run_analysis(min_support, top_k_pairs, max_moves, min_gain, progress=gr.Progress()): """Run complete inventory analysis with progress tracking""" progress(0, desc="Loading data...") df_items = load_items() df_loc = load_locations() df_chk = load_checkouts() id_to_name = dict(zip(df_items['item_id'], df_items['item_name'])) progress(0.2, desc="Processing checkout sessions...") df_chk = ensure_sessions(df_chk) baskets = baskets_from_sessions(df_chk) progress(0.4, desc="Mining frequent item pairs...") df_pairs, item_counts = pair_metrics(baskets, min_support=min_support) if df_pairs.empty: return None, None, "⚠️ No frequent pairs found. Try lowering the minimum support threshold.", None df_pairs['item_a_name'] = df_pairs['item_a'].map(id_to_name) df_pairs['item_b_name'] = df_pairs['item_b'].map(id_to_name) df_pairs_display = df_pairs[['item_a_name', 'item_b_name', 'support', 'lift', 'conf_a_b', 'conf_b_a']] df_pairs_display.columns = ['Item A', 'Item B', 'Support', 'Lift', 'Confidence Aβ†’B', 'Confidence Bβ†’A'] progress(0.6, desc="Building distance matrix...") loc_ids, coords, dist = build_distance_matrix(df_loc) W = {} for _, row in df_pairs.iterrows(): W[(row['item_a'], row['item_b'])] = row['lift'] * row['support'] progress(0.7, desc="Calculating current layout cost...") item2loc_orig = dict(zip(df_items["item_id"], df_items["location_id"])) cost_before = total_weighted_distance(item2loc_orig, W, dist) progress(0.8, desc="Optimizing item placement...") df_recs, item2loc_new = greedy_relocate( df_items, df_loc, W, dist, top_k=int(top_k_pairs), max_moves=int(max_moves), min_gain=min_gain ) cost_after = total_weighted_distance(item2loc_new, W, dist) improvement = cost_before - cost_after improvement_pct = (improvement / cost_before * 100) if cost_before > 0 else 0 if df_recs is not None and not df_recs.empty: df_recs['item_name'] = df_recs['move_item'].map(id_to_name) df_recs_display = df_recs[['item_name', 'from', 'to', 'swap_with', 'gain']] df_recs_display.columns = ['Item', 'From Location', 'To Location', 'Swap With', 'Distance Saved'] df_recs_display['Distance Saved'] = df_recs_display['Distance Saved'].round(2) else: df_recs_display = pd.DataFrame(columns=['Item', 'From Location', 'To Location', 'Swap With', 'Distance Saved']) progress(0.9, desc="Generating visualization...") summary = f"""# πŸ“Š Analysis Results ## Pattern Mining Results We analyzed **{len(baskets)} checkout sessions** and found **{len(df_pairs)} frequent item pairs**. ### Top Discovered Pattern: - **{df_pairs.iloc[0]['item_a_name']}** ↔ **{df_pairs.iloc[0]['item_b_name']}** - Lift: **{df_pairs.iloc[0]['lift']:.2f}** (these items are {df_pairs.iloc[0]['lift']:.1f}Γ— more likely to be checked out together) - Support: **{df_pairs.iloc[0]['support']:.1%}** (appears in {df_pairs.iloc[0]['support']:.1%} of checkouts) ## Layout Optimization Results ### Distance Costs: - **Before optimization:** {cost_before:.2f} units - **After optimization:** {cost_after:.2f} units - **Improvement:** {improvement:.2f} units ({improvement_pct:.1f}% reduction) ### Recommendations: - **{len(df_recs) if df_recs is not None else 0} moves** suggested - Total distance saved: **{improvement:.2f} units** --- πŸ’‘ **How to interpret these results:** - **Lift > 1**: Items are frequently checked out together - **Higher support**: Pattern occurs more often - **Distance savings**: How much walking you'll save by reorganizing """ img = visualize_reorganization(df_items, df_loc, df_pairs, df_recs, coords, id_to_name) progress(1.0, desc="Complete!") return df_pairs_display, df_recs_display, summary, img def visualize_reorganization(df_items, df_loc, df_pairs, df_recs, coords, id_to_name): """Create visualization of current layout and suggested moves""" fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 8)) ax1.set_title("Current Layout + Frequent Item Pairs", fontsize=16, fontweight='bold', pad=20) for loc_id, (x, y) in coords.items(): ax1.scatter(x, y, color='#E8E8E8', s=150, alpha=0.7, zorder=1, edgecolors='#999', linewidths=1) ax1.text(x, y+0.18, loc_id, fontsize=8, ha='center', va='bottom', color='#666') if df_pairs is not None and not df_pairs.empty: top_pairs = df_pairs.head(15) item2loc = dict(zip(df_items['item_id'], df_items['location_id'])) max_lift = top_pairs['lift'].max() for idx, (_, row) in enumerate(top_pairs.iterrows()): item_a, item_b = row['item_a'], row['item_b'] if item_a in item2loc and item_b in item2loc: loc_a = item2loc[item_a] loc_b = item2loc[item_b] if loc_a in coords and loc_b in coords: xa, ya = coords[loc_a] xb, yb = coords[loc_b] alpha = 0.3 + (row['lift'] / max_lift) * 0.4 linewidth = 1 + (row['lift'] / max_lift) * 2 ax1.plot([xa, xb], [ya, yb], color='#667eea', alpha=alpha, linewidth=linewidth, zorder=0) ax1.set_xlabel("X Coordinate", fontsize=12, fontweight='bold') ax1.set_ylabel("Y Coordinate", fontsize=12, fontweight='bold') ax1.grid(True, alpha=0.2, linestyle='--') ax1.set_facecolor('#F8F9FA') ax2.set_title("Suggested Reorganization", fontsize=16, fontweight='bold', pad=20) for loc_id, (x, y) in coords.items(): ax2.scatter(x, y, color='#E8E8E8', s=150, alpha=0.7, zorder=1, edgecolors='#999', linewidths=1) ax2.text(x, y+0.18, loc_id, fontsize=8, ha='center', va='bottom', color='#666') if df_recs is not None and not df_recs.empty: for idx, (_, rec) in enumerate(df_recs.iterrows()): from_loc = rec['from'] to_loc = rec['to'] if from_loc in coords and to_loc in coords: x1, y1 = coords[from_loc] x2, y2 = coords[to_loc] color = plt.cm.Reds(0.5 + idx * 0.05) ax2.annotate('', xy=(x2, y2), xytext=(x1, y1), arrowprops=dict(arrowstyle='->', color=color, lw=2.5, alpha=0.8), zorder=2) item_name = id_to_name.get(rec['move_item'], rec['move_item']) short_name = ' '.join(item_name.split()[:2]) mid_x, mid_y = (x1 + x2) / 2, (y1 + y2) / 2 ax2.text(mid_x, mid_y, short_name, fontsize=9, ha='center', fontweight='bold', bbox=dict(boxstyle='round,pad=0.4', facecolor='#FFE5E5', edgecolor=color, alpha=0.9, linewidth=2), zorder=3) ax2.set_xlabel("X Coordinate", fontsize=12, fontweight='bold') ax2.set_ylabel("Y Coordinate", fontsize=12, fontweight='bold') ax2.grid(True, alpha=0.2, linestyle='--') ax2.set_facecolor('#F8F9FA') plt.tight_layout() buf = io.BytesIO() fig.savefig(buf, format='png', dpi=120, bbox_inches='tight', facecolor='white') buf.seek(0) img = np.array(Image.open(buf)) plt.close(fig) return img # ============================================================================= # GRADIO INTERFACE # ============================================================================= with gr.Blocks(title="Makerspace Inventory System", css=CUSTOM_CSS) as demo: # State variables matched_items_state = gr.State(None) proposals_state = gr.State([]) # Main menu with gr.Group(visible=True, elem_id="main-menu-container") as main_menu: gr.Markdown("# πŸ”§ Makerspace Inventory System", elem_id="main-menu-title") gr.Markdown("*Smart inventory management powered by AI*", elem_id="main-menu-subtitle") with gr.Row(): checkout_btn = gr.Button("πŸ›’ Check Out Items", size="lg", elem_classes=["module-button"], scale=1) add_items_btn = gr.Button("πŸ“¦ Add Items", size="lg", elem_classes=["module-button"], scale=1) analysis_btn = gr.Button("πŸ“Š Inventory Analysis", size="lg", elem_classes=["module-button"], scale=1) # CHECK OUT MODULE with gr.Group(visible=False, elem_classes=["module-page"]) as checkout_module: gr.Markdown("# πŸ›’ Check Out Items", elem_classes=["section-header"]) with gr.Accordion("πŸ“– How to Use", open=False): gr.Markdown(""" **Step 1:** Upload a photo of items or type item names manually (comma-separated) **Step 2:** Review detected items and adjust quantities **Step 3:** Confirm checkout with optional user ID πŸ’‘ **Tip:** The AI automatically matches items to inventory using smart search! """) with gr.Group(visible=True) as checkout_screen1: gr.Markdown("### πŸ“Έ Scan Items", elem_classes=["subsection-header"]) with gr.Row(): with gr.Column(scale=2): checkout_image = gr.Image( type="pil", label="Upload Image of Items", sources=["upload", "webcam"], height=400 ) load_example_checkout_btn = gr.Button("πŸ“Έ Load Example", size="sm", variant="secondary", scale=0, visible=EXAMPLE_CHECKOUT_IMAGE is not None) with gr.Column(scale=1): gr.Markdown(""" ### Quick Guide **Upload:** Click the upload icon to select an image from your device **Webcam:** Click the camera icon to take a photo with your webcam πŸ’‘ Make sure items are clearly visible and well-lit """) checkout_manual = gr.Textbox( label="Or Enter Item Names Manually", placeholder="e.g., drill, safety glasses, Arduino", info="Separate multiple items with commas" ) checkout_status = gr.Markdown("") checkout_scan_btn = gr.Button("πŸ” Scan & Match Items", variant="primary", size="lg") with gr.Group(visible=False) as checkout_screen2: gr.Markdown("### Review Selected Items", elem_classes=["subsection-header"]) gr.Markdown("*Verify items, adjust quantities, or remove unwanted items before proceeding*") checkout_item_controls = [] for i in range(10): with gr.Row(visible=False) as item_row: with gr.Column(scale=3): item_info = gr.Markdown("") with gr.Column(scale=1): item_qty = gr.Number(value=1, minimum=1, label="Qty") with gr.Column(scale=1): item_remove = gr.Button("πŸ—‘οΈ Remove", size="sm", variant="stop") checkout_item_controls.append({ 'row': item_row, 'info': item_info, 'qty': item_qty, 'remove': item_remove }) with gr.Row(): checkout_rescan_btn = gr.Button("↩️ Rescan", variant="secondary") checkout_confirm_btn = gr.Button("βœ… Proceed to Checkout", variant="primary", size="lg") with gr.Group(visible=False) as checkout_screen3: gr.Markdown("### 🎫 Confirm Checkout", elem_classes=["subsection-header"]) checkout_preview = gr.Markdown("") checkout_user_id = gr.Textbox( label="User ID (Optional)", placeholder="Enter your ID or leave blank", info="Leave blank for auto-generated ID" ) checkout_processing = gr.Markdown("") with gr.Row(): checkout_cancel_btn = gr.Button("❌ Cancel", variant="secondary") checkout_final_btn = gr.Button("βœ… Complete Checkout", variant="primary", size="lg") checkout_result = gr.Markdown("") with gr.Row(): checkout_return_btn = gr.Button("🏠 Return to Main Menu", variant="secondary") checkout_another_btn = gr.Button("πŸ›’ Check Out More Items", variant="primary", visible=False) # ADD ITEMS MODULE with gr.Group(visible=False, elem_classes=["module-page"]) as add_items_module: gr.Markdown("# πŸ“¦ Add Items to Inventory", elem_classes=["section-header"]) with gr.Accordion("πŸ“– How to Use", open=False): gr.Markdown(""" **Receipt Upload:** 1. Upload receipt image or PDF β†’ AI extracts items 2. Review and edit detected items (check/uncheck, edit quantities) 3. Click "Apply Updates" to add to inventory **Manual Entry:** - Enter item name and quantity for quick updates - System uses fuzzy matching to find items **View & History:** - Browse inventory with category filters - Track all changes with timestamps """) with gr.Tab("πŸ“„ Receipt Upload"): gr.Markdown("### Upload Receipt", elem_classes=["subsection-header"]) receipt_file = gr.File(label="Upload Receipt (PDF or Image)", file_types=[".pdf", ".png", ".jpg", ".jpeg"]) load_example_receipt_btn = gr.Button("πŸ“„ Load Example", size="sm", variant="secondary", scale=0, visible=EXAMPLE_RECEIPT_PDF is not None) receipt_status = gr.Markdown("") gr.Markdown("### Review Detected Items", elem_classes=["subsection-header"]) gr.Markdown("*Check items to include, edit quantities, then apply*") add_items_table = gr.Dataframe( headers=["Include", "Item Name", "Quantity", "Match Confidence"], label="Detected Items", datatype=["bool", "str", "number", "str"], interactive=True, col_count=(4, "fixed") ) add_items_status = gr.Markdown("") with gr.Row(): add_items_reject_btn = gr.Button("❌ Clear All", variant="secondary") add_items_confirm_btn = gr.Button("βœ… Apply Updates", variant="primary", interactive=False) with gr.Tab("✍️ Manual Entry"): gr.Markdown("### Manually Add Items", elem_classes=["subsection-header"]) with gr.Row(): manual_item = gr.Textbox( label="Item Name", placeholder="e.g., Arduino Uno", info="Fuzzy matching will find similar items" ) manual_qty = gr.Number( label="Quantity to Add", value=1, minimum=1 ) manual_apply_btn = gr.Button("βž• Add to Inventory", variant="primary") manual_status = gr.Markdown("") manual_inventory_display = gr.Dataframe(label="Updated Inventory", visible=False) with gr.Tab("πŸ“Š View & History"): gr.Markdown("### Current Inventory", elem_classes=["subsection-header"]) with gr.Row(): inventory_category_filter = gr.Dropdown( choices=["All"] + get_categories(), value="All", label="Filter by Category" ) view_inventory_btn = gr.Button("πŸ”„ Refresh Inventory") inventory_display = gr.Dataframe(label="Current Inventory", visible=False) gr.Markdown("### Update History", elem_classes=["subsection-header"]) view_history_btn = gr.Button("πŸ“œ View Update Log") history_display = gr.Textbox(label="Update History", lines=10, visible=False) add_items_return_btn = gr.Button("🏠 Return to Main Menu", variant="secondary") # INVENTORY ANALYSIS MODULE with gr.Group(visible=False, elem_classes=["module-page"]) as analysis_module: gr.Markdown("# πŸ“Š Inventory Layout Analysis", elem_classes=["section-header"]) with gr.Accordion("πŸ“– Understanding the Analysis", open=True): gr.Markdown(""" This module analyzes checkout patterns to optimize item placement and minimize walking distance. ### πŸ“ˆ Key Metrics: **Support** - Frequency of co-occurrence (e.g., 0.10 = 10% of checkouts) **Lift** - Correlation strength (Lift > 1 = items checked out together more than random) **Confidence** - Conditional probability (e.g., 0.80 = 80% chance of B when checking A) **Distance Saved** - Walking distance reduction in grid units ### 🎯 Goal: Items frequently checked together should be placed closer to reduce travel time! """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("### βš™οΈ Parameters", elem_classes=["subsection-header"]) min_support = gr.Slider( minimum=0.01, maximum=0.2, value=0.02, step=0.01, label="Minimum Support", info="Lower = more patterns (less significant)" ) top_k_pairs = gr.Slider( minimum=5, maximum=50, value=25, step=5, label="Top K Pairs", info="Number of frequent pairs to optimize" ) max_moves = gr.Slider( minimum=5, maximum=30, value=10, step=1, label="Maximum Moves", info="Limit on relocations" ) min_gain = gr.Slider( minimum=0.0, maximum=1.0, value=0.05, step=0.01, label="Minimum Distance Gain", info="Threshold for suggestions" ) run_analysis_btn = gr.Button("πŸ” Run Analysis", variant="primary", size="lg") with gr.Column(scale=2): gr.Markdown("### πŸ“Š Summary", elem_classes=["subsection-header"]) analysis_summary = gr.Textbox(label="", lines=14, show_label=False) gr.Markdown("### πŸ—ΊοΈ Visual Layout Comparison", elem_classes=["subsection-header"]) analysis_viz = gr.Image(label="", type="numpy", show_label=False) with gr.Row(): with gr.Column(): gr.Markdown("### πŸ”— Frequent Pairs", elem_classes=["subsection-header"]) pairs_table = gr.Dataframe(label="", show_label=False) with gr.Column(): gr.Markdown("### πŸ“ Recommendations", elem_classes=["subsection-header"]) recs_table = gr.Dataframe(label="", show_label=False) analysis_return_btn = gr.Button("🏠 Return to Main Menu", variant="secondary") # EVENT HANDLERS - Navigation def show_checkout(): return [ gr.update(visible=False), # main_menu gr.update(visible=True), # checkout_module gr.update(visible=False), # add_items_module gr.update(visible=False) # analysis_module ] def show_add_items(): return [ gr.update(visible=False), # main_menu gr.update(visible=False), # checkout_module gr.update(visible=True), # add_items_module gr.update(visible=False) # analysis_module ] def show_analysis(): return [ gr.update(visible=False), # main_menu gr.update(visible=False), # checkout_module gr.update(visible=False), # add_items_module gr.update(visible=True) # analysis_module ] def return_to_menu(): return [ gr.update(visible=True), # main_menu gr.update(visible=False), # checkout_module gr.update(visible=False), # add_items_module gr.update(visible=False) # analysis_module ] checkout_btn.click(fn=show_checkout, outputs=[main_menu, checkout_module, add_items_module, analysis_module]) add_items_btn.click(fn=show_add_items, outputs=[main_menu, checkout_module, add_items_module, analysis_module]) analysis_btn.click(fn=show_analysis, outputs=[main_menu, checkout_module, add_items_module, analysis_module]) checkout_return_btn.click(fn=return_to_menu, outputs=[main_menu, checkout_module, add_items_module, analysis_module]) add_items_return_btn.click(fn=return_to_menu, outputs=[main_menu, checkout_module, add_items_module, analysis_module]) analysis_return_btn.click(fn=return_to_menu, outputs=[main_menu, checkout_module, add_items_module, analysis_module]) # EVENT HANDLERS - Checkout def update_checkout_screen2(matched_items): updates = [] if not matched_items: for i in range(10): updates.extend([gr.update(visible=False), gr.update(value=""), gr.update(value=1)]) return updates for i in range(10): if i < len(matched_items): item_data = matched_items[i] updates.extend([ gr.update(visible=True), gr.update(value=create_checkout_item_display(item_data, i)), gr.update(value=item_data['quantity'], maximum=item_data['item']['Quantity']), ]) else: updates.extend([gr.update(visible=False), gr.update(value=""), gr.update(value=1)]) return updates def remove_checkout_item(matched_items, item_idx): if matched_items and 0 <= item_idx < len(matched_items): matched_items.pop(item_idx) return matched_items def set_checkout_quantity(matched_items, item_idx, new_qty): if matched_items and 0 <= item_idx < len(matched_items): max_qty = matched_items[item_idx]['item']['Quantity'] matched_items[item_idx]['quantity'] = max(1, min(int(new_qty), max_qty)) return matched_items def reset_checkout(): return ( gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), None, "", None, "", "", gr.update(visible=False) ) def show_checkout_complete_options(): return gr.update(visible=False), gr.update(visible=True) checkout_scan_btn.click( fn=lambda: (gr.update(value="⏳ Scanning...", interactive=False), "πŸ” **Processing...** AI is analyzing your image..."), outputs=[checkout_scan_btn, checkout_status] ).then( fn=scan_items_checkout, inputs=[checkout_image, checkout_manual], outputs=[matched_items_state, checkout_status] ).then( fn=lambda: gr.update(value="πŸ” Scan & Match Items", interactive=True), outputs=[checkout_scan_btn] ).then( fn=lambda items: ( gr.update(visible=False) if items else gr.update(), gr.update(visible=True) if items else gr.update(), gr.update(visible=False) ), inputs=[matched_items_state], outputs=[checkout_screen1, checkout_screen2, checkout_screen3] ).then( fn=update_checkout_screen2, inputs=[matched_items_state], outputs=[checkout_item_controls[i][key] for i in range(10) for key in ['row', 'info', 'qty']] ) for i in range(10): checkout_item_controls[i]['qty'].change( fn=lambda items, new_qty, idx=i: set_checkout_quantity(items, idx, new_qty), inputs=[matched_items_state, checkout_item_controls[i]['qty']], outputs=[matched_items_state] ) checkout_item_controls[i]['remove'].click( fn=lambda items, idx=i: remove_checkout_item(items, idx), inputs=[matched_items_state], outputs=[matched_items_state] ).then( fn=update_checkout_screen2, inputs=[matched_items_state], outputs=[checkout_item_controls[j][key] for j in range(10) for key in ['row', 'info', 'qty']] ) checkout_confirm_btn.click( fn=confirm_checkout_preview, inputs=[matched_items_state], outputs=[checkout_preview] ).then( fn=lambda: (gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)), outputs=[checkout_screen1, checkout_screen2, checkout_screen3] ) checkout_rescan_btn.click( fn=reset_checkout, outputs=[checkout_screen1, checkout_screen2, checkout_screen3, matched_items_state, checkout_status, checkout_image, checkout_manual, checkout_result, checkout_another_btn] ) checkout_cancel_btn.click( fn=reset_checkout, outputs=[checkout_screen1, checkout_screen2, checkout_screen3, matched_items_state, checkout_status, checkout_image, checkout_manual, checkout_result, checkout_another_btn] ) checkout_final_btn.click( fn=lambda: (gr.update(value="⏳ Processing...", interactive=False), "⏳ **Processing checkout...** Updating inventory..."), outputs=[checkout_final_btn, checkout_processing] ).then( fn=process_checkout, inputs=[matched_items_state, checkout_user_id], outputs=[checkout_result] ).then( fn=lambda: (gr.update(value="βœ… Complete Checkout", interactive=True), ""), outputs=[checkout_final_btn, checkout_processing] ).then( fn=lambda: (gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)), outputs=[checkout_screen1, checkout_screen2, checkout_screen3] ).then( fn=show_checkout_complete_options, outputs=[checkout_return_btn, checkout_another_btn] ) checkout_another_btn.click( fn=reset_checkout, outputs=[checkout_screen1, checkout_screen2, checkout_screen3, matched_items_state, checkout_status, checkout_image, checkout_manual, checkout_result, checkout_another_btn] ).then( fn=lambda: (gr.update(visible=True), gr.update(visible=False)), outputs=[checkout_return_btn, checkout_another_btn] ) # EVENT HANDLERS - Add Items receipt_file.change( fn=lambda: "πŸ“„ **Processing receipt...** Extracting text with OCR...", outputs=[receipt_status] ).then( fn=extract_text_from_receipt, inputs=[receipt_file], outputs=[add_items_table, proposals_state] ).then( fn=lambda proposals: (gr.update(interactive=len(proposals) > 0), "βœ… Items detected! Review and edit the table below."), inputs=[proposals_state], outputs=[add_items_confirm_btn, receipt_status] ) add_items_confirm_btn.click( fn=lambda: (gr.update(value="⏳ Applying...", interactive=False), "⏳ **Updating inventory...**"), outputs=[add_items_confirm_btn, add_items_status] ).then( fn=apply_updates_from_table, inputs=[add_items_table], outputs=[add_items_status, inventory_display] ).then( fn=lambda: (gr.update(value="βœ… Apply Updates", interactive=False), gr.update(visible=True)), outputs=[add_items_confirm_btn, inventory_display] ) add_items_reject_btn.click( fn=lambda: ([["No items", "", "", ""]], "❌ Cleared all items.", []), outputs=[add_items_table, add_items_status, proposals_state] ).then( fn=lambda: gr.update(interactive=False), outputs=[add_items_confirm_btn] ) manual_apply_btn.click( fn=lambda: gr.update(value="⏳ Adding...", interactive=False), outputs=[manual_apply_btn] ).then( fn=manual_update, inputs=[manual_item, manual_qty], outputs=[manual_status, manual_inventory_display] ).then( fn=lambda: (gr.update(value="βž• Add to Inventory", interactive=True), gr.update(visible=True)), outputs=[manual_apply_btn, manual_inventory_display] ) view_inventory_btn.click( fn=view_inventory_table, inputs=[inventory_category_filter], outputs=[inventory_display] ).then( fn=lambda: gr.update(visible=True), outputs=[inventory_display] ) inventory_category_filter.change( fn=view_inventory_table, inputs=[inventory_category_filter], outputs=[inventory_display] ) view_history_btn.click( fn=view_update_history, outputs=[history_display] ).then( fn=lambda: gr.update(visible=True), outputs=[history_display] ) # EVENT HANDLERS - Analysis run_analysis_btn.click( fn=run_analysis, inputs=[min_support, top_k_pairs, max_moves, min_gain], outputs=[pairs_table, recs_table, analysis_summary, analysis_viz] ) # EXAMPLE INPUTS def load_checkout_example(): try: if EXAMPLE_CHECKOUT_IMAGE: return Image.open(EXAMPLE_CHECKOUT_IMAGE) except: pass return None load_example_checkout_btn.click( fn=load_checkout_example, outputs=[checkout_image] ) def load_receipt_example(): try: if EXAMPLE_RECEIPT_PDF: return EXAMPLE_RECEIPT_PDF except: pass return None load_example_receipt_btn.click( fn=load_receipt_example, outputs=[receipt_file] ) if __name__ == "__main__": demo.queue() demo.launch()