SebastianAndreu commited on
Commit
50e985a
Β·
verified Β·
1 Parent(s): a351f4b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1768 -0
app.py ADDED
@@ -0,0 +1,1768 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Integrated Makerspace Inventory Management System
3
+ Smart inventory management powered by AI
4
+ """
5
+
6
+ import google.generativeai as genai
7
+ import chromadb
8
+ from sentence_transformers import SentenceTransformer
9
+ import gradio as gr
10
+ from PIL import Image
11
+ import json
12
+ import re
13
+ import os
14
+ import pandas as pd
15
+ from pdf2image import convert_from_path
16
+ import pytesseract
17
+ from rapidfuzz import process
18
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification
19
+ import torch
20
+ import datetime
21
+ import itertools
22
+ import math
23
+ import numpy as np
24
+ import matplotlib.pyplot as plt
25
+ import io
26
+ import shutil
27
+ from typing import List, Dict, Tuple, Optional
28
+
29
+ print("=" * 80)
30
+ print("🎨 Makerspace Inventory Management System")
31
+ print("=" * 80)
32
+ print("Modules: Check Out | Add Items | Inventory Analysis")
33
+ print("=" * 80 + "\n")
34
+
35
+ # Gemini API Configuration
36
+ GEMINI_API_KEY = "AIzaSyA5_Cx0rriZWtTr1KyEkWCJ6fVyXpUKuJw"
37
+ genai.configure(api_key=GEMINI_API_KEY)
38
+ gemini_model = genai.GenerativeModel('models/gemini-2.5-flash')
39
+ print("βœ… Gemini API configured (gemini-2.5-flash)")
40
+
41
+ # Initialize shared embedding model for ChromaDB
42
+ embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
43
+ print("βœ… Embedding model loaded")
44
+
45
+ # File paths
46
+ ITEMS_CSV = "items.csv"
47
+ LOCATIONS_CSV = "locations.csv"
48
+ CHECKOUTS_CSV = "checkouts.csv"
49
+ UPDATE_LOG_CSV = "update_log.csv"
50
+
51
+ # =============================================================================
52
+ # CUSTOM CSS
53
+ # =============================================================================
54
+
55
+ CUSTOM_CSS = """
56
+ /* Global styling - Fixed desktop layout */
57
+ .gradio-container {
58
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif !important;
59
+ max-width: 1400px !important;
60
+ margin: auto !important;
61
+ }
62
+
63
+ /* Fix all blocks to have consistent width */
64
+ .gradio-container .block {
65
+ width: 100% !important;
66
+ max-width: 100% !important;
67
+ }
68
+
69
+ /* Ensure all rows stay full width */
70
+ .gradio-container .row {
71
+ width: 100% !important;
72
+ }
73
+
74
+ /* Main menu - gradient background */
75
+ #main-menu-container {
76
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
77
+ padding: 50px 40px;
78
+ border-radius: 16px;
79
+ box-shadow: 0 8px 32px rgba(0,0,0,0.12);
80
+ margin-bottom: 30px;
81
+ width: 100%;
82
+ }
83
+
84
+ #main-menu-title {
85
+ color: white !important;
86
+ text-align: center;
87
+ font-size: 2.8em !important;
88
+ font-weight: 700 !important;
89
+ margin-bottom: 8px !important;
90
+ letter-spacing: -0.5px;
91
+ }
92
+
93
+ #main-menu-subtitle {
94
+ color: rgba(255,255,255,0.95) !important;
95
+ text-align: center;
96
+ font-size: 1.1em !important;
97
+ margin-bottom: 40px !important;
98
+ font-weight: 300;
99
+ }
100
+
101
+ /* Module buttons in main menu */
102
+ .module-button {
103
+ background: white !important;
104
+ color: #667eea !important;
105
+ border: none !important;
106
+ padding: 32px 24px !important;
107
+ font-size: 1.25em !important;
108
+ font-weight: 600 !important;
109
+ border-radius: 12px !important;
110
+ box-shadow: 0 4px 16px rgba(0,0,0,0.1) !important;
111
+ transition: all 0.3s ease !important;
112
+ min-height: 120px !important;
113
+ display: flex !important;
114
+ align-items: center !important;
115
+ justify-content: center !important;
116
+ }
117
+
118
+ .module-button:hover {
119
+ transform: translateY(-3px) !important;
120
+ box-shadow: 0 6px 24px rgba(0,0,0,0.15) !important;
121
+ }
122
+
123
+ /* Clean module pages - consistent sizing */
124
+ .module-page {
125
+ background: white;
126
+ padding: 40px;
127
+ border-radius: 12px;
128
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
129
+ min-height: 600px;
130
+ width: 100%;
131
+ }
132
+
133
+ /* Section headers */
134
+ .section-header {
135
+ color: #1a202c;
136
+ font-size: 2.2em;
137
+ font-weight: 700;
138
+ margin-bottom: 15px;
139
+ padding-bottom: 15px;
140
+ border-bottom: 3px solid #667eea;
141
+ }
142
+
143
+ /* Subsection headers */
144
+ .subsection-header {
145
+ color: #2d3748;
146
+ font-size: 1.4em;
147
+ font-weight: 600;
148
+ margin-top: 30px;
149
+ margin-bottom: 15px;
150
+ }
151
+
152
+ /* Instructions box */
153
+ .instructions-box {
154
+ background: #f7fafc;
155
+ border-left: 4px solid #667eea;
156
+ padding: 20px 25px;
157
+ border-radius: 8px;
158
+ margin: 20px 0;
159
+ }
160
+
161
+ .instructions-box p {
162
+ margin: 10px 0;
163
+ line-height: 1.7;
164
+ }
165
+
166
+ /* Buttons - consistent sizing */
167
+ button {
168
+ min-height: 48px !important;
169
+ font-size: 1.05em !important;
170
+ font-weight: 600 !important;
171
+ border-radius: 8px !important;
172
+ padding: 12px 28px !important;
173
+ transition: all 0.3s ease !important;
174
+ }
175
+
176
+ .primary-button {
177
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
178
+ color: white !important;
179
+ border: none !important;
180
+ }
181
+
182
+ .primary-button:hover {
183
+ transform: translateY(-2px) !important;
184
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4) !important;
185
+ }
186
+
187
+ /* Input fields - consistent sizing */
188
+ input, textarea, select {
189
+ min-height: 44px !important;
190
+ font-size: 1em !important;
191
+ }
192
+
193
+ /* Image upload areas - better styling */
194
+ .image-upload-container {
195
+ min-height: 400px !important;
196
+ }
197
+
198
+ /* Upload/Webcam tab buttons - make them visible and styled */
199
+ [data-testid="image"] [role="tablist"] {
200
+ background: #f8f9fa !important;
201
+ padding: 12px !important;
202
+ border-radius: 12px !important;
203
+ gap: 12px !important;
204
+ margin-bottom: 20px !important;
205
+ display: flex !important;
206
+ justify-content: center !important;
207
+ }
208
+
209
+ [data-testid="image"] [role="tab"] {
210
+ background: white !important;
211
+ border: 2px solid #cbd5e0 !important;
212
+ border-radius: 10px !important;
213
+ padding: 16px 32px !important;
214
+ font-size: 1.1em !important;
215
+ font-weight: 600 !important;
216
+ color: #2d3748 !important;
217
+ transition: all 0.3s ease !important;
218
+ min-width: 150px !important;
219
+ display: flex !important;
220
+ align-items: center !important;
221
+ justify-content: center !important;
222
+ gap: 8px !important;
223
+ }
224
+
225
+ [data-testid="image"] [role="tab"]:hover {
226
+ background: #667eea !important;
227
+ border-color: #667eea !important;
228
+ color: white !important;
229
+ transform: translateY(-2px) !important;
230
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3) !important;
231
+ }
232
+
233
+ [data-testid="image"] [role="tab"][aria-selected="true"] {
234
+ background: #667eea !important;
235
+ border-color: #667eea !important;
236
+ color: white !important;
237
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4) !important;
238
+ }
239
+
240
+ /* Icon colors in tabs */
241
+ [data-testid="image"] [role="tab"] svg {
242
+ width: 22px !important;
243
+ height: 22px !important;
244
+ }
245
+
246
+ [data-testid="image"] [role="tab"] svg path,
247
+ [data-testid="image"] [role="tab"] svg line,
248
+ [data-testid="image"] [role="tab"] svg circle,
249
+ [data-testid="image"] [role="tab"] svg rect {
250
+ stroke: currentColor !important;
251
+ fill: none !important;
252
+ }
253
+
254
+ /* Upload area */
255
+ [data-testid="image"] .upload-container {
256
+ background: white !important;
257
+ border: 2px dashed #cbd5e0 !important;
258
+ border-radius: 12px !important;
259
+ padding: 40px !important;
260
+ min-height: 350px !important;
261
+ }
262
+
263
+ [data-testid="image"] .upload-container:hover {
264
+ border-color: #667eea !important;
265
+ background: #f7fafc !important;
266
+ }
267
+
268
+ /* File upload button styling */
269
+ .file-upload button {
270
+ background: white !important;
271
+ border: 2px solid #e2e8f0 !important;
272
+ color: #2d3748 !important;
273
+ }
274
+
275
+ .file-upload button:hover {
276
+ background: #f7fafc !important;
277
+ border-color: #667eea !important;
278
+ }
279
+
280
+ /* Dataframe tables */
281
+ .dataframe-container {
282
+ min-height: 200px !important;
283
+ max-height: 500px !important;
284
+ }
285
+
286
+ /* Markdown containers */
287
+ .markdown-container {
288
+ line-height: 1.7 !important;
289
+ }
290
+
291
+ /* Accordions */
292
+ .gradio-accordion {
293
+ border: 1px solid #e2e8f0 !important;
294
+ border-radius: 8px !important;
295
+ margin: 15px 0 !important;
296
+ }
297
+
298
+ /* Tabs */
299
+ .tabs {
300
+ border-radius: 8px !important;
301
+ margin-top: 20px !important;
302
+ }
303
+
304
+ /* Status messages styling */
305
+ .status-loading {
306
+ background: #fff3cd;
307
+ border-left: 4px solid #ffc107;
308
+ color: #856404;
309
+ padding: 15px;
310
+ border-radius: 6px;
311
+ margin: 15px 0;
312
+ }
313
+
314
+ .status-success {
315
+ background: #d4edda;
316
+ border-left: 4px solid #28a745;
317
+ color: #155724;
318
+ padding: 15px;
319
+ border-radius: 6px;
320
+ margin: 15px 0;
321
+ }
322
+
323
+ .status-error {
324
+ background: #f8d7da;
325
+ border-left: 4px solid #dc3545;
326
+ color: #721c24;
327
+ padding: 15px;
328
+ border-radius: 6px;
329
+ margin: 15px 0;
330
+ }
331
+
332
+ /* Remove mobile responsiveness - keep desktop width */
333
+ @media (max-width: 768px) {
334
+ .gradio-container {
335
+ max-width: 1400px !important;
336
+ }
337
+ }
338
+
339
+ /* Ensure consistent spacing */
340
+ .gap {
341
+ gap: 20px !important;
342
+ }
343
+
344
+ /* Column consistency */
345
+ .column {
346
+ padding: 10px !important;
347
+ }
348
+ """
349
+
350
+ # Example files for grading/testing
351
+ EXAMPLE_CHECKOUT_IMAGE = "screwdriver.png" if os.path.exists("screwdriver.png") else None
352
+ EXAMPLE_RECEIPT_PDF = "mcmaster_receipt.pdf" if os.path.exists("mcmaster_receipt.pdf") else None
353
+
354
+ if EXAMPLE_CHECKOUT_IMAGE:
355
+ print(f"βœ… Example checkout image found: {EXAMPLE_CHECKOUT_IMAGE}")
356
+ if EXAMPLE_RECEIPT_PDF:
357
+ print(f"βœ… Example receipt PDF found: {EXAMPLE_RECEIPT_PDF}")
358
+
359
+ # =============================================================================
360
+ # DATA MANAGEMENT FUNCTIONS
361
+ # =============================================================================
362
+
363
+ def load_items():
364
+ """Load items from CSV"""
365
+ return pd.read_csv(ITEMS_CSV)
366
+
367
+ def save_items(df):
368
+ """Save items to CSV"""
369
+ df.to_csv(ITEMS_CSV, index=False)
370
+
371
+ def load_locations():
372
+ """Load locations from CSV"""
373
+ return pd.read_csv(LOCATIONS_CSV)
374
+
375
+ def load_checkouts():
376
+ """Load checkouts from CSV"""
377
+ return pd.read_csv(CHECKOUTS_CSV)
378
+
379
+ def append_checkout(timestamp, user_id, session_id, item_id):
380
+ """Append a new checkout record"""
381
+ df = load_checkouts()
382
+ new_row = pd.DataFrame([[timestamp, user_id, session_id, item_id]],
383
+ columns=["timestamp", "user_id", "session_id", "item_id"])
384
+ df = pd.concat([df, new_row], ignore_index=True)
385
+ df.to_csv(CHECKOUTS_CSV, index=False)
386
+
387
+ def get_categories():
388
+ """Get list of all categories"""
389
+ df = load_items()
390
+ return sorted(df['category'].unique().tolist())
391
+
392
+ def rebuild_chromadb():
393
+ """Rebuild ChromaDB from current inventory"""
394
+ global chroma_client, collection
395
+
396
+ df = load_items()
397
+ chroma_client = chromadb.Client()
398
+
399
+ try:
400
+ chroma_client.delete_collection(name="makerspace_inventory")
401
+ except:
402
+ pass
403
+
404
+ collection = chroma_client.create_collection(
405
+ name="makerspace_inventory",
406
+ metadata={"description": "Makerspace tool inventory"}
407
+ )
408
+
409
+ documents = []
410
+ metadatas = []
411
+ ids = []
412
+
413
+ for i, row in df.iterrows():
414
+ doc_text = f"{row['item_name']} {row['category']} {row['description']}"
415
+ documents.append(doc_text)
416
+
417
+ metadatas.append({
418
+ "item_id": row['item_id'],
419
+ "item_name": row['item_name'],
420
+ "category": row['category'],
421
+ "quantity": str(row['quantity']),
422
+ "unit": row['unit'],
423
+ "description": row['description'],
424
+ "location_id": row['location_id']
425
+ })
426
+
427
+ ids.append(f"item_{i}")
428
+
429
+ collection.add(
430
+ documents=documents,
431
+ metadatas=metadatas,
432
+ ids=ids
433
+ )
434
+
435
+ print(f"βœ… ChromaDB rebuilt with {len(df)} items")
436
+
437
+ # Initialize ChromaDB
438
+ rebuild_chromadb()
439
+
440
+ # =============================================================================
441
+ # CHECK OUT MODULE FUNCTIONS
442
+ # =============================================================================
443
+
444
+ def retrieve_top_candidates(query_text, top_k=5):
445
+ """Retrieve top matching items from vector database"""
446
+ results = collection.query(
447
+ query_texts=[query_text],
448
+ n_results=top_k
449
+ )
450
+
451
+ candidates = []
452
+ if results['metadatas'] and len(results['metadatas'][0]) > 0:
453
+ for metadata in results['metadatas'][0]:
454
+ candidates.append({
455
+ 'Item ID': metadata['item_id'],
456
+ 'Item Name': metadata['item_name'],
457
+ 'Category': metadata['category'],
458
+ 'Quantity': int(metadata['quantity']),
459
+ 'Unit': metadata['unit'],
460
+ 'Description': metadata['description'],
461
+ 'Location': metadata['location_id']
462
+ })
463
+
464
+ return candidates
465
+
466
+ def detect_all_items(image):
467
+ """Detect ALL items in image using Gemini"""
468
+ prompt = """Analyze this image and list EVERY distinct tool or item you see.
469
+
470
+ IMPORTANT: List each item on ONE line only. Do not include additional details like "Type:", "Brand:", etc.
471
+
472
+ Format your response as:
473
+ 1. [One-line description including brand and key features]
474
+ 2. [One-line description including brand and key features]
475
+
476
+ Example:
477
+ 1. Digital caliper with LCD display
478
+ 2. Arduino microcontroller board
479
+
480
+ If you see only one item, list just that one.
481
+ If you see no tools/items, respond with "No items detected."
482
+
483
+ Focus on items in the FOREGROUND. Ignore background objects unless they are clearly the subject."""
484
+
485
+ try:
486
+ if isinstance(image, str):
487
+ image = Image.open(image)
488
+
489
+ response = gemini_model.generate_content([prompt, image])
490
+ response_text = response.text.strip()
491
+
492
+ if "no items" in response_text.lower():
493
+ return "No items detected"
494
+
495
+ items = []
496
+ lines = response_text.split('\n')
497
+
498
+ for line in lines:
499
+ line = line.strip()
500
+ if re.match(r'^\d+[\.\)]\s+', line):
501
+ item_desc = re.sub(r'^\d+[\.\)]\s+', '', line)
502
+ if item_desc:
503
+ items.append(item_desc.strip())
504
+
505
+ if not items and response_text:
506
+ items = [response_text]
507
+
508
+ return items if items else "Error: Could not parse items from response"
509
+
510
+ except Exception as e:
511
+ return f"Error: {str(e)}"
512
+
513
+ def match_single_item_to_inventory(description):
514
+ """Match a single item description to inventory using ChromaDB"""
515
+ try:
516
+ candidates = retrieve_top_candidates(description, top_k=3)
517
+
518
+ if not candidates:
519
+ return None
520
+
521
+ for candidate in candidates:
522
+ if candidate['Quantity'] > 0:
523
+ return candidate
524
+
525
+ return None
526
+
527
+ except Exception as e:
528
+ print(f"Error matching item: {e}")
529
+ return None
530
+
531
+ def scan_items_checkout(image, manual_text):
532
+ """Process image or manual text and return matched items"""
533
+ detected_items = []
534
+
535
+ if image is not None:
536
+ detected = detect_all_items(image)
537
+ if isinstance(detected, list):
538
+ detected_items.extend(detected)
539
+ elif isinstance(detected, str) and "error" not in detected.lower():
540
+ detected_items.append(detected)
541
+
542
+ if manual_text and manual_text.strip():
543
+ manual_items = [item.strip() for item in manual_text.split(',') if item.strip()]
544
+ detected_items.extend(manual_items)
545
+
546
+ if not detected_items:
547
+ return None, "⚠️ No items detected. Please upload an image or enter item names manually."
548
+
549
+ matched_items = []
550
+ for desc in detected_items:
551
+ match = match_single_item_to_inventory(desc)
552
+ if match:
553
+ matched_items.append({
554
+ 'description': desc,
555
+ 'item': match,
556
+ 'quantity': 1
557
+ })
558
+
559
+ if not matched_items:
560
+ return None, "⚠️ Could not match any detected items to inventory."
561
+
562
+ return matched_items, ""
563
+
564
+ def create_checkout_item_display(item_data, index):
565
+ """Create display text for a matched item"""
566
+ item = item_data['item']
567
+ desc = item_data['description']
568
+
569
+ display = f"""**Detected:** {desc}
570
+ **Matched:** {item['Item Name']}
571
+ **Category:** {item['Category']}
572
+ **Location:** {item['Location']}
573
+ **Available:** {item['Quantity']} {item['Unit']}"""
574
+
575
+ return display
576
+
577
+ def confirm_checkout_preview(matched_items):
578
+ """Generate checkout confirmation preview"""
579
+ if not matched_items:
580
+ return "No items to check out."
581
+
582
+ preview = "# πŸ“‹ Checkout Summary\n\n"
583
+ preview += "Please review your items before completing checkout:\n\n"
584
+ for i, item_data in enumerate(matched_items, 1):
585
+ item = item_data['item']
586
+ qty = item_data['quantity']
587
+ preview += f"{i}. **{item['Item Name']}** Γ— {qty} (Location: {item['Location']})\n"
588
+
589
+ preview += f"\n**Total Items:** {len(matched_items)}"
590
+
591
+ return preview
592
+
593
+ def process_checkout(matched_items, user_id_input):
594
+ """Process the checkout and update inventory"""
595
+ if not matched_items:
596
+ return "No items to check out."
597
+
598
+ if not user_id_input or not user_id_input.strip():
599
+ user_id = f"U{np.random.randint(1, 9999):04d}"
600
+ else:
601
+ user_id = user_id_input.strip()
602
+
603
+ df_checkouts = load_checkouts()
604
+ if len(df_checkouts) > 0:
605
+ last_session = df_checkouts['session_id'].max()
606
+ session_num = int(last_session[1:]) + 1
607
+ else:
608
+ session_num = 1
609
+ session_id = f"S{session_num:05d}"
610
+
611
+ df_items = load_items()
612
+
613
+ timestamp_base = datetime.datetime.now()
614
+ checked_out = []
615
+ errors = []
616
+
617
+ for i, item_data in enumerate(matched_items):
618
+ item_id = item_data['item']['Item ID']
619
+ qty = item_data['quantity']
620
+ item_name = item_data['item']['Item Name']
621
+
622
+ item_idx = df_items[df_items['item_id'] == item_id].index
623
+ if len(item_idx) == 0:
624
+ errors.append(f"Item {item_name} not found in inventory")
625
+ continue
626
+
627
+ item_idx = item_idx[0]
628
+ current_qty = df_items.loc[item_idx, 'quantity']
629
+
630
+ if current_qty < qty:
631
+ errors.append(f"Not enough {item_name} available (requested: {qty}, available: {current_qty})")
632
+ continue
633
+
634
+ df_items.loc[item_idx, 'quantity'] = current_qty - qty
635
+
636
+ checkout_time = timestamp_base + datetime.timedelta(seconds=i*10)
637
+ append_checkout(
638
+ checkout_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
639
+ user_id,
640
+ session_id,
641
+ item_id
642
+ )
643
+
644
+ checked_out.append(f"βœ… {item_name} Γ— {qty}")
645
+
646
+ save_items(df_items)
647
+ rebuild_chromadb()
648
+
649
+ summary = f"# βœ… Checkout Complete!\n\n"
650
+ summary += f"**Session ID:** `{session_id}`\n"
651
+ summary += f"**User ID:** `{user_id}`\n"
652
+ summary += f"**Timestamp:** {timestamp_base.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
653
+
654
+ if checked_out:
655
+ summary += "## Items Checked Out:\n"
656
+ for item in checked_out:
657
+ summary += f"- {item}\n"
658
+
659
+ if errors:
660
+ summary += "\n## ⚠️ Errors:\n"
661
+ for error in errors:
662
+ summary += f"- {error}\n"
663
+
664
+ return summary
665
+
666
+ # =============================================================================
667
+ # ADD ITEMS MODULE FUNCTIONS
668
+ # =============================================================================
669
+
670
+ def extract_text_from_receipt(file_path):
671
+ """Extract text from PDF or image receipt"""
672
+ if file_path is None:
673
+ return None, []
674
+
675
+ try:
676
+ text = ""
677
+
678
+ if file_path.name.lower().endswith('.pdf'):
679
+ images = convert_from_path(file_path.name)
680
+ for img in images:
681
+ text += pytesseract.image_to_string(img) + "\n"
682
+ else:
683
+ img = Image.open(file_path.name)
684
+ text = pytesseract.image_to_string(img)
685
+
686
+ if not text.strip():
687
+ return [["No text extracted", "", "", True]], []
688
+
689
+ proposals = parse_receipt_text(text)
690
+
691
+ if not proposals:
692
+ return [["No items recognized", "", "", True]], []
693
+
694
+ table_data = []
695
+ for prop in proposals:
696
+ table_data.append([
697
+ True,
698
+ prop['item_name'],
699
+ str(prop['quantity']),
700
+ prop['match_type']
701
+ ])
702
+
703
+ return table_data, proposals
704
+
705
+ except Exception as e:
706
+ return [[True, f"Error: {str(e)}", "", ""]], []
707
+
708
+ def parse_receipt_text(text):
709
+ """Parse receipt text to extract items and quantities"""
710
+ df_items = load_items()
711
+ item_names = df_items['item_name'].tolist()
712
+
713
+ proposals = []
714
+ lines = text.split('\n')
715
+
716
+ for line in lines:
717
+ line = line.strip()
718
+ if not line or len(line) < 3:
719
+ continue
720
+
721
+ qty_match = re.search(r'(\d+)\s*x?\s*(.+)', line, re.IGNORECASE)
722
+ if qty_match:
723
+ qty = int(qty_match.group(1))
724
+ item_text = qty_match.group(2).strip()
725
+ else:
726
+ qty = 1
727
+ item_text = line
728
+
729
+ match = process.extractOne(item_text, item_names, score_cutoff=60)
730
+
731
+ if match:
732
+ matched_name = match[0]
733
+ confidence = match[1]
734
+
735
+ item_row = df_items[df_items['item_name'] == matched_name].iloc[0]
736
+
737
+ proposals.append({
738
+ 'item_id': item_row['item_id'],
739
+ 'item_name': matched_name,
740
+ 'quantity': qty,
741
+ 'match_type': f"Fuzzy ({confidence}%)",
742
+ 'original_text': item_text
743
+ })
744
+
745
+ return proposals
746
+
747
+ def apply_updates_from_table(table_data):
748
+ """Apply inventory updates from edited table"""
749
+ # Convert to list if it's a DataFrame
750
+ if isinstance(table_data, pd.DataFrame):
751
+ if table_data.empty:
752
+ return "No updates to apply.", None
753
+ table_data = table_data.values.tolist()
754
+
755
+ if not table_data or len(table_data) == 0:
756
+ return "No updates to apply.", None
757
+
758
+ df_items = load_items()
759
+
760
+ updated = []
761
+ errors = []
762
+
763
+ for row in table_data:
764
+ if not row[0]:
765
+ continue
766
+
767
+ item_name = row[1]
768
+ try:
769
+ qty = int(row[2])
770
+ except:
771
+ errors.append(f"Invalid quantity for {item_name}")
772
+ continue
773
+
774
+ item_names = df_items['item_name'].tolist()
775
+ match = process.extractOne(item_name, item_names, score_cutoff=60)
776
+
777
+ if not match:
778
+ errors.append(f"Could not find item: {item_name}")
779
+ continue
780
+
781
+ matched_name = match[0]
782
+ item_idx = df_items[df_items['item_name'] == matched_name].index[0]
783
+
784
+ current_qty = df_items.loc[item_idx, 'quantity']
785
+ new_qty = current_qty + qty
786
+
787
+ df_items.loc[item_idx, 'quantity'] = new_qty
788
+ updated.append(f"βœ… {matched_name}: {current_qty} β†’ {new_qty} (+{qty})")
789
+
790
+ if not updated:
791
+ return "No items were updated. " + ("\n".join(errors) if errors else ""), None
792
+
793
+ save_items(df_items)
794
+ rebuild_chromadb()
795
+
796
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
797
+ log_entry = f"{timestamp},Receipt Upload,{len(updated)} items\n"
798
+ with open(UPDATE_LOG_CSV, 'a') as f:
799
+ f.write(log_entry)
800
+
801
+ summary = "# βœ… Updates Applied!\n\n"
802
+ for item in updated:
803
+ summary += f"- {item}\n"
804
+
805
+ if errors:
806
+ summary += "\n## ⚠️ Warnings:\n"
807
+ for error in errors:
808
+ summary += f"- {error}\n"
809
+
810
+ return summary, df_items[['item_id', 'item_name', 'category', 'quantity', 'unit', 'location_id']]
811
+
812
+ def manual_update(item_name, quantity):
813
+ """Manually update an item's quantity"""
814
+ if not item_name or not quantity:
815
+ return "⚠️ Please provide item name and quantity.", None
816
+
817
+ try:
818
+ qty = int(quantity)
819
+ except:
820
+ return "⚠️ Invalid quantity.", None
821
+
822
+ df_items = load_items()
823
+
824
+ item_names = df_items['item_name'].tolist()
825
+ match = process.extractOne(item_name, item_names, score_cutoff=60)
826
+
827
+ if not match:
828
+ return f"❌ Could not find item: {item_name}", None
829
+
830
+ matched_name = match[0]
831
+ item_idx = df_items[df_items['item_name'] == matched_name].index[0]
832
+
833
+ current_qty = df_items.loc[item_idx, 'quantity']
834
+ new_qty = current_qty + qty
835
+
836
+ df_items.loc[item_idx, 'quantity'] = new_qty
837
+
838
+ save_items(df_items)
839
+ rebuild_chromadb()
840
+
841
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
842
+ log_entry = f"{timestamp},Manual Update,{matched_name}: {current_qty} β†’ {new_qty}\n"
843
+ with open(UPDATE_LOG_CSV, 'a') as f:
844
+ f.write(log_entry)
845
+
846
+ summary = f"βœ… Updated **{matched_name}**: {current_qty} β†’ {new_qty} (+{qty})"
847
+
848
+ return summary, df_items[['item_id', 'item_name', 'category', 'quantity', 'unit', 'location_id']]
849
+
850
+ def view_inventory_table(category_filter="All"):
851
+ """View current inventory with optional category filter"""
852
+ df_items = load_items()
853
+
854
+ if category_filter != "All":
855
+ df_items = df_items[df_items['category'] == category_filter]
856
+
857
+ return df_items[['item_id', 'item_name', 'category', 'quantity', 'unit', 'location_id']]
858
+
859
+ def view_update_history():
860
+ """View update history"""
861
+ if not os.path.exists(UPDATE_LOG_CSV) or os.path.getsize(UPDATE_LOG_CSV) == 0:
862
+ return "No update history available."
863
+
864
+ with open(UPDATE_LOG_CSV, 'r') as f:
865
+ history = f.read()
866
+
867
+ return history if history.strip() else "No update history available."
868
+
869
+ # =============================================================================
870
+ # INVENTORY ANALYSIS MODULE FUNCTIONS
871
+ # =============================================================================
872
+
873
+ def ensure_sessions(df_chk, gap_minutes=30):
874
+ """Ensure sessions exist in checkout data"""
875
+ df = df_chk.copy()
876
+
877
+ if "session_id" in df.columns and df['session_id'].notna().all():
878
+ return df
879
+
880
+ if "user_id" not in df.columns or "timestamp" not in df.columns:
881
+ df['session_id'] = [f"S{i:05d}" for i in range(1, len(df) + 1)]
882
+ return df
883
+
884
+ df["timestamp"] = pd.to_datetime(df["timestamp"])
885
+ df.sort_values(["user_id", "timestamp"], inplace=True)
886
+
887
+ session_ids = []
888
+ last_user = None
889
+ last_time = None
890
+ session_counter = 0
891
+ gap = pd.Timedelta(minutes=gap_minutes)
892
+
893
+ for row in df.itertuples():
894
+ if (row.user_id != last_user) or (last_time is None) or ((row.timestamp - last_time) > gap):
895
+ session_counter += 1
896
+ current_session_id = f"AUTO_S{session_counter:05d}"
897
+ session_ids.append(current_session_id)
898
+ last_user = row.user_id
899
+ last_time = row.timestamp
900
+
901
+ df["session_id"] = session_ids
902
+ return df
903
+
904
+ def baskets_from_sessions(df_chk):
905
+ """Create baskets from checkout sessions"""
906
+ return df_chk.groupby("session_id")["item_id"].apply(lambda x: set(x)).tolist()
907
+
908
+ def pair_metrics(baskets, min_support=0.02):
909
+ """Compute support, confidence, lift for item pairs"""
910
+ n = len(baskets)
911
+ item_counts = {}
912
+ pair_counts = {}
913
+
914
+ for b in baskets:
915
+ for i in b:
916
+ item_counts[i] = item_counts.get(i, 0) + 1
917
+ for a, b_item in itertools.combinations(sorted(b), 2):
918
+ pair_counts[(a, b_item)] = pair_counts.get((a, b_item), 0) + 1
919
+
920
+ rows = []
921
+ for (a, b_item), c_ab in pair_counts.items():
922
+ supp = c_ab / n
923
+ if supp < min_support:
924
+ continue
925
+ pa = item_counts[a] / n
926
+ pb = item_counts[b_item] / n
927
+ conf_a_b = supp / pa
928
+ conf_b_a = supp / pb
929
+ lift = supp / (pa * pb)
930
+ rows.append((a, b_item, supp, conf_a_b, conf_b_a, lift))
931
+
932
+ df = pd.DataFrame(rows, columns=["item_a", "item_b", "support", "conf_a_b", "conf_b_a", "lift"])
933
+ df.sort_values(["lift", "support"], ascending=[False, False], inplace=True)
934
+ return df, item_counts
935
+
936
+ def build_distance_matrix(df_loc):
937
+ """Precompute Euclidean distances between all locations"""
938
+ loc_ids = df_loc["location_id"].tolist()
939
+ coords = {r.location_id: (float(r.x), float(r.y)) for r in df_loc.itertuples()}
940
+
941
+ dist = {}
942
+ for a in loc_ids:
943
+ xa, ya = coords[a]
944
+ for b in loc_ids:
945
+ xb, yb = coords[b]
946
+ dist[(a, b)] = math.dist((xa, ya), (xb, yb))
947
+ return loc_ids, coords, dist
948
+
949
+ def total_weighted_distance(item2loc, W, dist):
950
+ """Calculate total weighted distance"""
951
+ cost = 0.0
952
+ for (a, b), w in W.items():
953
+ la = item2loc.get(a)
954
+ lb = item2loc.get(b)
955
+ if la is None or lb is None:
956
+ continue
957
+ cost += w * dist[(la, lb)]
958
+ return cost
959
+
960
+ def delta_cost_for_move(item, from_loc, to_loc, item2loc, W, dist):
961
+ """Compute change in cost if item moves from from_loc to to_loc"""
962
+ delta = 0.0
963
+ for (a, b), w in W.items():
964
+ if a == item or b == item:
965
+ other = b if a == item else a
966
+ other_loc = item2loc.get(other)
967
+ if other_loc is None:
968
+ continue
969
+ old_d = dist[(from_loc, other_loc)]
970
+ new_d = dist[(to_loc, other_loc)]
971
+ delta += w * (new_d - old_d)
972
+ return delta
973
+
974
+ def greedy_relocate(df_items, df_loc, W, dist, top_k=30, max_moves=15, min_gain=0.05):
975
+ """Greedy relocation algorithm"""
976
+ original_item2loc = dict(zip(df_items["item_id"], df_items["location_id"]))
977
+ item2loc = original_item2loc.copy()
978
+ loc2item = {loc: item for item, loc in item2loc.items()}
979
+
980
+ all_locs = df_loc["location_id"].tolist()
981
+ moved_items = set()
982
+ used_empty_targets = set()
983
+ recs = []
984
+
985
+ sorted_pairs = sorted(W.items(), key=lambda x: x[1], reverse=True)[:top_k]
986
+
987
+ for (a, b), w in sorted_pairs:
988
+ for item in (a, b):
989
+ if item in moved_items:
990
+ continue
991
+
992
+ from_loc = item2loc[item]
993
+ best_cand = None
994
+ best_delta = 0.0
995
+ best_occ = None
996
+
997
+ for cand in all_locs:
998
+ if cand == from_loc:
999
+ continue
1000
+
1001
+ occ = loc2item.get(cand)
1002
+
1003
+ if occ is None and cand in used_empty_targets:
1004
+ continue
1005
+
1006
+ if occ is not None and occ in moved_items:
1007
+ continue
1008
+
1009
+ delta = delta_cost_for_move(item, from_loc, cand, item2loc, W, dist)
1010
+ if delta < best_delta:
1011
+ best_delta = delta
1012
+ best_cand = cand
1013
+ best_occ = occ
1014
+
1015
+ gain = -best_delta
1016
+ if best_cand is not None and gain >= min_gain:
1017
+ cand = best_cand
1018
+ occ = best_occ
1019
+
1020
+ recs.append({
1021
+ "move_item": item,
1022
+ "from": from_loc,
1023
+ "to": cand,
1024
+ "swap_with": occ if occ else "Empty",
1025
+ "gain": gain
1026
+ })
1027
+
1028
+ if occ is not None:
1029
+ item2loc[occ] = from_loc
1030
+ loc2item[from_loc] = occ
1031
+ moved_items.add(occ)
1032
+ else:
1033
+ del loc2item[from_loc]
1034
+ used_empty_targets.add(cand)
1035
+
1036
+ item2loc[item] = cand
1037
+ loc2item[cand] = item
1038
+ moved_items.add(item)
1039
+
1040
+ if len(recs) >= max_moves:
1041
+ break
1042
+
1043
+ if len(recs) >= max_moves:
1044
+ break
1045
+
1046
+ df_recs = pd.DataFrame(recs) if recs else None
1047
+ return df_recs, item2loc
1048
+
1049
+ def run_analysis(min_support, top_k_pairs, max_moves, min_gain, progress=gr.Progress()):
1050
+ """Run complete inventory analysis with progress tracking"""
1051
+ progress(0, desc="Loading data...")
1052
+
1053
+ df_items = load_items()
1054
+ df_loc = load_locations()
1055
+ df_chk = load_checkouts()
1056
+
1057
+ id_to_name = dict(zip(df_items['item_id'], df_items['item_name']))
1058
+
1059
+ progress(0.2, desc="Processing checkout sessions...")
1060
+ df_chk = ensure_sessions(df_chk)
1061
+ baskets = baskets_from_sessions(df_chk)
1062
+
1063
+ progress(0.4, desc="Mining frequent item pairs...")
1064
+ df_pairs, item_counts = pair_metrics(baskets, min_support=min_support)
1065
+
1066
+ if df_pairs.empty:
1067
+ return None, None, "⚠️ No frequent pairs found. Try lowering the minimum support threshold.", None
1068
+
1069
+ df_pairs['item_a_name'] = df_pairs['item_a'].map(id_to_name)
1070
+ df_pairs['item_b_name'] = df_pairs['item_b'].map(id_to_name)
1071
+
1072
+ df_pairs_display = df_pairs[['item_a_name', 'item_b_name', 'support', 'lift', 'conf_a_b', 'conf_b_a']]
1073
+ df_pairs_display.columns = ['Item A', 'Item B', 'Support', 'Lift', 'Confidence A→B', 'Confidence B→A']
1074
+
1075
+ progress(0.6, desc="Building distance matrix...")
1076
+ loc_ids, coords, dist = build_distance_matrix(df_loc)
1077
+
1078
+ W = {}
1079
+ for _, row in df_pairs.iterrows():
1080
+ W[(row['item_a'], row['item_b'])] = row['lift'] * row['support']
1081
+
1082
+ progress(0.7, desc="Calculating current layout cost...")
1083
+ item2loc_orig = dict(zip(df_items["item_id"], df_items["location_id"]))
1084
+ cost_before = total_weighted_distance(item2loc_orig, W, dist)
1085
+
1086
+ progress(0.8, desc="Optimizing item placement...")
1087
+ df_recs, item2loc_new = greedy_relocate(
1088
+ df_items, df_loc, W, dist,
1089
+ top_k=int(top_k_pairs),
1090
+ max_moves=int(max_moves),
1091
+ min_gain=min_gain
1092
+ )
1093
+
1094
+ cost_after = total_weighted_distance(item2loc_new, W, dist)
1095
+ improvement = cost_before - cost_after
1096
+ improvement_pct = (improvement / cost_before * 100) if cost_before > 0 else 0
1097
+
1098
+ if df_recs is not None and not df_recs.empty:
1099
+ df_recs['item_name'] = df_recs['move_item'].map(id_to_name)
1100
+ df_recs_display = df_recs[['item_name', 'from', 'to', 'swap_with', 'gain']]
1101
+ df_recs_display.columns = ['Item', 'From Location', 'To Location', 'Swap With', 'Distance Saved']
1102
+ df_recs_display['Distance Saved'] = df_recs_display['Distance Saved'].round(2)
1103
+ else:
1104
+ df_recs_display = pd.DataFrame(columns=['Item', 'From Location', 'To Location', 'Swap With', 'Distance Saved'])
1105
+
1106
+ progress(0.9, desc="Generating visualization...")
1107
+
1108
+ summary = f"""# πŸ“Š Analysis Results
1109
+
1110
+ ## Pattern Mining Results
1111
+ We analyzed **{len(baskets)} checkout sessions** and found **{len(df_pairs)} frequent item pairs**.
1112
+
1113
+ ### Top Discovered Pattern:
1114
+ - **{df_pairs.iloc[0]['item_a_name']}** ↔ **{df_pairs.iloc[0]['item_b_name']}**
1115
+ - Lift: **{df_pairs.iloc[0]['lift']:.2f}** (these items are {df_pairs.iloc[0]['lift']:.1f}Γ— more likely to be checked out together)
1116
+ - Support: **{df_pairs.iloc[0]['support']:.1%}** (appears in {df_pairs.iloc[0]['support']:.1%} of checkouts)
1117
+
1118
+ ## Layout Optimization Results
1119
+
1120
+ ### Distance Costs:
1121
+ - **Before optimization:** {cost_before:.2f} units
1122
+ - **After optimization:** {cost_after:.2f} units
1123
+ - **Improvement:** {improvement:.2f} units ({improvement_pct:.1f}% reduction)
1124
+
1125
+ ### Recommendations:
1126
+ - **{len(df_recs) if df_recs is not None else 0} moves** suggested
1127
+ - Total distance saved: **{improvement:.2f} units**
1128
+
1129
+ ---
1130
+
1131
+ πŸ’‘ **How to interpret these results:**
1132
+ - **Lift > 1**: Items are frequently checked out together
1133
+ - **Higher support**: Pattern occurs more often
1134
+ - **Distance savings**: How much walking you'll save by reorganizing
1135
+ """
1136
+
1137
+ img = visualize_reorganization(df_items, df_loc, df_pairs, df_recs, coords, id_to_name)
1138
+
1139
+ progress(1.0, desc="Complete!")
1140
+
1141
+ return df_pairs_display, df_recs_display, summary, img
1142
+
1143
+ def visualize_reorganization(df_items, df_loc, df_pairs, df_recs, coords, id_to_name):
1144
+ """Create visualization of current layout and suggested moves"""
1145
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(18, 8))
1146
+
1147
+ ax1.set_title("Current Layout + Frequent Item Pairs", fontsize=16, fontweight='bold', pad=20)
1148
+
1149
+ for loc_id, (x, y) in coords.items():
1150
+ ax1.scatter(x, y, color='#E8E8E8', s=150, alpha=0.7, zorder=1, edgecolors='#999', linewidths=1)
1151
+ ax1.text(x, y+0.18, loc_id, fontsize=8, ha='center', va='bottom', color='#666')
1152
+
1153
+ if df_pairs is not None and not df_pairs.empty:
1154
+ top_pairs = df_pairs.head(15)
1155
+ item2loc = dict(zip(df_items['item_id'], df_items['location_id']))
1156
+
1157
+ max_lift = top_pairs['lift'].max()
1158
+
1159
+ for idx, (_, row) in enumerate(top_pairs.iterrows()):
1160
+ item_a, item_b = row['item_a'], row['item_b']
1161
+ if item_a in item2loc and item_b in item2loc:
1162
+ loc_a = item2loc[item_a]
1163
+ loc_b = item2loc[item_b]
1164
+ if loc_a in coords and loc_b in coords:
1165
+ xa, ya = coords[loc_a]
1166
+ xb, yb = coords[loc_b]
1167
+
1168
+ alpha = 0.3 + (row['lift'] / max_lift) * 0.4
1169
+ linewidth = 1 + (row['lift'] / max_lift) * 2
1170
+
1171
+ ax1.plot([xa, xb], [ya, yb], color='#667eea', alpha=alpha,
1172
+ linewidth=linewidth, zorder=0)
1173
+
1174
+ ax1.set_xlabel("X Coordinate", fontsize=12, fontweight='bold')
1175
+ ax1.set_ylabel("Y Coordinate", fontsize=12, fontweight='bold')
1176
+ ax1.grid(True, alpha=0.2, linestyle='--')
1177
+ ax1.set_facecolor('#F8F9FA')
1178
+
1179
+ ax2.set_title("Suggested Reorganization", fontsize=16, fontweight='bold', pad=20)
1180
+
1181
+ for loc_id, (x, y) in coords.items():
1182
+ ax2.scatter(x, y, color='#E8E8E8', s=150, alpha=0.7, zorder=1, edgecolors='#999', linewidths=1)
1183
+ ax2.text(x, y+0.18, loc_id, fontsize=8, ha='center', va='bottom', color='#666')
1184
+
1185
+ if df_recs is not None and not df_recs.empty:
1186
+ for idx, (_, rec) in enumerate(df_recs.iterrows()):
1187
+ from_loc = rec['from']
1188
+ to_loc = rec['to']
1189
+ if from_loc in coords and to_loc in coords:
1190
+ x1, y1 = coords[from_loc]
1191
+ x2, y2 = coords[to_loc]
1192
+
1193
+ color = plt.cm.Reds(0.5 + idx * 0.05)
1194
+
1195
+ ax2.annotate('', xy=(x2, y2), xytext=(x1, y1),
1196
+ arrowprops=dict(arrowstyle='->', color=color, lw=2.5, alpha=0.8),
1197
+ zorder=2)
1198
+
1199
+ item_name = id_to_name.get(rec['move_item'], rec['move_item'])
1200
+ short_name = ' '.join(item_name.split()[:2])
1201
+ mid_x, mid_y = (x1 + x2) / 2, (y1 + y2) / 2
1202
+
1203
+ ax2.text(mid_x, mid_y, short_name, fontsize=9, ha='center',
1204
+ fontweight='bold',
1205
+ bbox=dict(boxstyle='round,pad=0.4', facecolor='#FFE5E5',
1206
+ edgecolor=color, alpha=0.9, linewidth=2),
1207
+ zorder=3)
1208
+
1209
+ ax2.set_xlabel("X Coordinate", fontsize=12, fontweight='bold')
1210
+ ax2.set_ylabel("Y Coordinate", fontsize=12, fontweight='bold')
1211
+ ax2.grid(True, alpha=0.2, linestyle='--')
1212
+ ax2.set_facecolor('#F8F9FA')
1213
+
1214
+ plt.tight_layout()
1215
+
1216
+ buf = io.BytesIO()
1217
+ fig.savefig(buf, format='png', dpi=120, bbox_inches='tight', facecolor='white')
1218
+ buf.seek(0)
1219
+ img = np.array(Image.open(buf))
1220
+ plt.close(fig)
1221
+
1222
+ return img
1223
+
1224
+ # =============================================================================
1225
+ # GRADIO INTERFACE
1226
+ # =============================================================================
1227
+
1228
+ with gr.Blocks(title="Makerspace Inventory System", css=CUSTOM_CSS) as demo:
1229
+
1230
+ # State variables
1231
+ matched_items_state = gr.State(None)
1232
+ proposals_state = gr.State([])
1233
+
1234
+ # Main menu
1235
+ with gr.Group(visible=True, elem_id="main-menu-container") as main_menu:
1236
+ gr.Markdown("# πŸ”§ Makerspace Inventory System", elem_id="main-menu-title")
1237
+ gr.Markdown("*Smart inventory management powered by AI*", elem_id="main-menu-subtitle")
1238
+
1239
+ with gr.Row():
1240
+ checkout_btn = gr.Button("πŸ›’ Check Out Items", size="lg", elem_classes=["module-button"], scale=1)
1241
+ add_items_btn = gr.Button("πŸ“¦ Add Items", size="lg", elem_classes=["module-button"], scale=1)
1242
+ analysis_btn = gr.Button("πŸ“Š Inventory Analysis", size="lg", elem_classes=["module-button"], scale=1)
1243
+
1244
+ # CHECK OUT MODULE
1245
+ with gr.Group(visible=False, elem_classes=["module-page"]) as checkout_module:
1246
+ gr.Markdown("# πŸ›’ Check Out Items", elem_classes=["section-header"])
1247
+
1248
+ with gr.Accordion("πŸ“– How to Use", open=False):
1249
+ gr.Markdown("""
1250
+ **Step 1:** Upload a photo of items or type item names manually (comma-separated)
1251
+
1252
+ **Step 2:** Review detected items and adjust quantities
1253
+
1254
+ **Step 3:** Confirm checkout with optional user ID
1255
+
1256
+ πŸ’‘ **Tip:** The AI automatically matches items to inventory using smart search!
1257
+ """)
1258
+
1259
+ with gr.Group(visible=True) as checkout_screen1:
1260
+ gr.Markdown("### πŸ“Έ Scan Items", elem_classes=["subsection-header"])
1261
+
1262
+ with gr.Row():
1263
+ with gr.Column(scale=2):
1264
+ checkout_image = gr.Image(
1265
+ type="pil",
1266
+ label="Upload Image of Items",
1267
+ sources=["upload", "webcam"],
1268
+ height=400
1269
+ )
1270
+
1271
+ if EXAMPLE_CHECKOUT_IMAGE:
1272
+ load_example_checkout_btn = gr.Button("πŸ“Έ Load Example", size="sm", variant="secondary", scale=0)
1273
+
1274
+ with gr.Column(scale=1):
1275
+ gr.Markdown("""
1276
+ ### Quick Guide
1277
+
1278
+ **Upload:** Click the upload icon to select an image from your device
1279
+
1280
+ **Webcam:** Click the camera icon to take a photo with your webcam
1281
+
1282
+ πŸ’‘ Make sure items are clearly visible and well-lit
1283
+ """)
1284
+
1285
+ checkout_manual = gr.Textbox(
1286
+ label="Or Enter Item Names Manually",
1287
+ placeholder="e.g., drill, safety glasses, Arduino",
1288
+ info="Separate multiple items with commas"
1289
+ )
1290
+
1291
+ checkout_status = gr.Markdown("")
1292
+ checkout_scan_btn = gr.Button("πŸ” Scan & Match Items", variant="primary", size="lg")
1293
+
1294
+ with gr.Group(visible=False) as checkout_screen2:
1295
+ gr.Markdown("### βœ… Review Items", elem_classes=["subsection-header"])
1296
+ gr.Markdown("*Adjust quantities or remove items before checkout*")
1297
+
1298
+ checkout_item_controls = []
1299
+ for i in range(10):
1300
+ with gr.Row(visible=False) as item_row:
1301
+ with gr.Column(scale=3):
1302
+ item_info = gr.Markdown("")
1303
+ with gr.Column(scale=1):
1304
+ item_qty = gr.Number(value=1, minimum=1, label="Qty")
1305
+ with gr.Column(scale=1):
1306
+ item_remove = gr.Button("πŸ—‘οΈ Remove", size="sm", variant="stop")
1307
+
1308
+ checkout_item_controls.append({
1309
+ 'row': item_row,
1310
+ 'info': item_info,
1311
+ 'qty': item_qty,
1312
+ 'remove': item_remove
1313
+ })
1314
+
1315
+ with gr.Row():
1316
+ checkout_rescan_btn = gr.Button("↩️ Rescan", variant="secondary")
1317
+ checkout_confirm_btn = gr.Button("βœ… Proceed to Checkout", variant="primary", size="lg")
1318
+
1319
+ with gr.Group(visible=False) as checkout_screen3:
1320
+ gr.Markdown("### 🎫 Confirm Checkout", elem_classes=["subsection-header"])
1321
+
1322
+ checkout_preview = gr.Markdown("")
1323
+
1324
+ checkout_user_id = gr.Textbox(
1325
+ label="User ID (Optional)",
1326
+ placeholder="Enter your ID or leave blank",
1327
+ info="Leave blank for auto-generated ID"
1328
+ )
1329
+
1330
+ checkout_processing = gr.Markdown("")
1331
+
1332
+ with gr.Row():
1333
+ checkout_cancel_btn = gr.Button("❌ Cancel", variant="secondary")
1334
+ checkout_final_btn = gr.Button("βœ… Complete Checkout", variant="primary", size="lg")
1335
+
1336
+ checkout_result = gr.Markdown("")
1337
+
1338
+ with gr.Row():
1339
+ checkout_return_btn = gr.Button("🏠 Return to Main Menu", variant="secondary")
1340
+ checkout_another_btn = gr.Button("πŸ›’ Check Out More Items", variant="primary", visible=False)
1341
+
1342
+ # ADD ITEMS MODULE
1343
+ with gr.Group(visible=False, elem_classes=["module-page"]) as add_items_module:
1344
+ gr.Markdown("# πŸ“¦ Add Items to Inventory", elem_classes=["section-header"])
1345
+
1346
+ with gr.Accordion("πŸ“– How to Use", open=False):
1347
+ gr.Markdown("""
1348
+ **Receipt Upload:**
1349
+ 1. Upload receipt image or PDF β†’ AI extracts items
1350
+ 2. Review and edit detected items (check/uncheck, edit quantities)
1351
+ 3. Click "Apply Updates" to add to inventory
1352
+
1353
+ **Manual Entry:**
1354
+ - Enter item name and quantity for quick updates
1355
+ - System uses fuzzy matching to find items
1356
+
1357
+ **View & History:**
1358
+ - Browse inventory with category filters
1359
+ - Track all changes with timestamps
1360
+ """)
1361
+
1362
+ with gr.Tab("πŸ“„ Receipt Upload"):
1363
+ gr.Markdown("### Upload Receipt", elem_classes=["subsection-header"])
1364
+
1365
+ receipt_file = gr.File(label="Upload Receipt (PDF or Image)", file_types=[".pdf", ".png", ".jpg", ".jpeg"])
1366
+
1367
+ if EXAMPLE_RECEIPT_PDF:
1368
+ load_example_receipt_btn = gr.Button("πŸ“„ Load Example", size="sm", variant="secondary", scale=0)
1369
+
1370
+ receipt_status = gr.Markdown("")
1371
+
1372
+ gr.Markdown("### Review Detected Items", elem_classes=["subsection-header"])
1373
+ gr.Markdown("*Check items to include, edit quantities, then apply*")
1374
+
1375
+ add_items_table = gr.Dataframe(
1376
+ headers=["Include", "Item Name", "Quantity", "Match Confidence"],
1377
+ label="Detected Items",
1378
+ datatype=["bool", "str", "number", "str"],
1379
+ interactive=True,
1380
+ col_count=(4, "fixed")
1381
+ )
1382
+
1383
+ add_items_status = gr.Markdown("")
1384
+
1385
+ with gr.Row():
1386
+ add_items_reject_btn = gr.Button("❌ Clear All", variant="secondary")
1387
+ add_items_confirm_btn = gr.Button("βœ… Apply Updates", variant="primary", interactive=False)
1388
+
1389
+ with gr.Tab("✍️ Manual Entry"):
1390
+ gr.Markdown("### Manually Add Items", elem_classes=["subsection-header"])
1391
+
1392
+ with gr.Row():
1393
+ manual_item = gr.Textbox(
1394
+ label="Item Name",
1395
+ placeholder="e.g., Arduino Uno",
1396
+ info="Fuzzy matching will find similar items"
1397
+ )
1398
+ manual_qty = gr.Number(
1399
+ label="Quantity to Add",
1400
+ value=1,
1401
+ minimum=1
1402
+ )
1403
+
1404
+ manual_apply_btn = gr.Button("βž• Add to Inventory", variant="primary")
1405
+ manual_status = gr.Markdown("")
1406
+ manual_inventory_display = gr.Dataframe(label="Updated Inventory", visible=False)
1407
+
1408
+ with gr.Tab("πŸ“Š View & History"):
1409
+ gr.Markdown("### Current Inventory", elem_classes=["subsection-header"])
1410
+
1411
+ with gr.Row():
1412
+ inventory_category_filter = gr.Dropdown(
1413
+ choices=["All"] + get_categories(),
1414
+ value="All",
1415
+ label="Filter by Category"
1416
+ )
1417
+ view_inventory_btn = gr.Button("πŸ”„ Refresh Inventory")
1418
+
1419
+ inventory_display = gr.Dataframe(label="Current Inventory", visible=False)
1420
+
1421
+ gr.Markdown("### Update History", elem_classes=["subsection-header"])
1422
+
1423
+ view_history_btn = gr.Button("πŸ“œ View Update Log")
1424
+ history_display = gr.Textbox(label="Update History", lines=10, visible=False)
1425
+
1426
+ add_items_return_btn = gr.Button("🏠 Return to Main Menu", variant="secondary")
1427
+
1428
+ # INVENTORY ANALYSIS MODULE
1429
+ with gr.Group(visible=False, elem_classes=["module-page"]) as analysis_module:
1430
+ gr.Markdown("# πŸ“Š Inventory Layout Analysis", elem_classes=["section-header"])
1431
+
1432
+ with gr.Accordion("πŸ“– Understanding the Analysis", open=True):
1433
+ gr.Markdown("""
1434
+ This module analyzes checkout patterns to optimize item placement and minimize walking distance.
1435
+
1436
+ ### πŸ“ˆ Key Metrics:
1437
+
1438
+ **Support** - Frequency of co-occurrence (e.g., 0.10 = 10% of checkouts)
1439
+
1440
+ **Lift** - Correlation strength (Lift > 1 = items checked out together more than random)
1441
+
1442
+ **Confidence** - Conditional probability (e.g., 0.80 = 80% chance of B when checking A)
1443
+
1444
+ **Distance Saved** - Walking distance reduction in grid units
1445
+
1446
+ ### 🎯 Goal:
1447
+ Items frequently checked together should be placed closer to reduce travel time!
1448
+ """)
1449
+
1450
+ with gr.Row():
1451
+ with gr.Column(scale=1):
1452
+ gr.Markdown("### βš™οΈ Parameters", elem_classes=["subsection-header"])
1453
+
1454
+ min_support = gr.Slider(
1455
+ minimum=0.01,
1456
+ maximum=0.2,
1457
+ value=0.02,
1458
+ step=0.01,
1459
+ label="Minimum Support",
1460
+ info="Lower = more patterns (less significant)"
1461
+ )
1462
+
1463
+ top_k_pairs = gr.Slider(
1464
+ minimum=5,
1465
+ maximum=50,
1466
+ value=25,
1467
+ step=5,
1468
+ label="Top K Pairs",
1469
+ info="Number of frequent pairs to optimize"
1470
+ )
1471
+
1472
+ max_moves = gr.Slider(
1473
+ minimum=5,
1474
+ maximum=30,
1475
+ value=10,
1476
+ step=1,
1477
+ label="Maximum Moves",
1478
+ info="Limit on relocations"
1479
+ )
1480
+
1481
+ min_gain = gr.Slider(
1482
+ minimum=0.0,
1483
+ maximum=1.0,
1484
+ value=0.05,
1485
+ step=0.01,
1486
+ label="Minimum Distance Gain",
1487
+ info="Threshold for suggestions"
1488
+ )
1489
+
1490
+ run_analysis_btn = gr.Button("πŸ” Run Analysis", variant="primary", size="lg")
1491
+
1492
+ with gr.Column(scale=2):
1493
+ gr.Markdown("### πŸ“Š Summary", elem_classes=["subsection-header"])
1494
+ analysis_summary = gr.Textbox(label="", lines=14, show_label=False)
1495
+
1496
+ gr.Markdown("### πŸ—ΊοΈ Visual Layout Comparison", elem_classes=["subsection-header"])
1497
+ analysis_viz = gr.Image(label="", type="numpy", show_label=False)
1498
+
1499
+ with gr.Row():
1500
+ with gr.Column():
1501
+ gr.Markdown("### πŸ”— Frequent Pairs", elem_classes=["subsection-header"])
1502
+ pairs_table = gr.Dataframe(label="", show_label=False)
1503
+
1504
+ with gr.Column():
1505
+ gr.Markdown("### πŸ“ Recommendations", elem_classes=["subsection-header"])
1506
+ recs_table = gr.Dataframe(label="", show_label=False)
1507
+
1508
+ analysis_return_btn = gr.Button("🏠 Return to Main Menu", variant="secondary")
1509
+
1510
+ # EVENT HANDLERS - Navigation
1511
+ def show_checkout():
1512
+ return (gr.update(visible=False), gr.update(visible=True),
1513
+ gr.update(visible=False), gr.update(visible=False))
1514
+
1515
+ def show_add_items():
1516
+ return (gr.update(visible=False), gr.update(visible=False),
1517
+ gr.update(visible=True), gr.update(visible=False))
1518
+
1519
+ def show_analysis():
1520
+ return (gr.update(visible=False), gr.update(visible=False),
1521
+ gr.update(visible=False), gr.update(visible=True))
1522
+
1523
+ def return_to_menu():
1524
+ return (gr.update(visible=True), gr.update(visible=False),
1525
+ gr.update(visible=False), gr.update(visible=False))
1526
+
1527
+ checkout_btn.click(fn=show_checkout, outputs=[main_menu, checkout_module, add_items_module, analysis_module])
1528
+ add_items_btn.click(fn=show_add_items, outputs=[main_menu, checkout_module, add_items_module, analysis_module])
1529
+ analysis_btn.click(fn=show_analysis, outputs=[main_menu, checkout_module, add_items_module, analysis_module])
1530
+
1531
+ checkout_return_btn.click(fn=return_to_menu, outputs=[main_menu, checkout_module, add_items_module, analysis_module])
1532
+ add_items_return_btn.click(fn=return_to_menu, outputs=[main_menu, checkout_module, add_items_module, analysis_module])
1533
+ analysis_return_btn.click(fn=return_to_menu, outputs=[main_menu, checkout_module, add_items_module, analysis_module])
1534
+
1535
+ # EVENT HANDLERS - Checkout
1536
+ def update_checkout_screen2(matched_items):
1537
+ updates = []
1538
+
1539
+ if not matched_items:
1540
+ for i in range(10):
1541
+ updates.extend([gr.update(visible=False), gr.update(value=""), gr.update(value=1)])
1542
+ return updates
1543
+
1544
+ for i in range(10):
1545
+ if i < len(matched_items):
1546
+ item_data = matched_items[i]
1547
+ updates.extend([
1548
+ gr.update(visible=True),
1549
+ gr.update(value=create_checkout_item_display(item_data, i)),
1550
+ gr.update(value=item_data['quantity'], maximum=item_data['item']['Quantity']),
1551
+ ])
1552
+ else:
1553
+ updates.extend([gr.update(visible=False), gr.update(value=""), gr.update(value=1)])
1554
+
1555
+ return updates
1556
+
1557
+ def remove_checkout_item(matched_items, item_idx):
1558
+ if matched_items and 0 <= item_idx < len(matched_items):
1559
+ matched_items.pop(item_idx)
1560
+ return matched_items
1561
+
1562
+ def set_checkout_quantity(matched_items, item_idx, new_qty):
1563
+ if matched_items and 0 <= item_idx < len(matched_items):
1564
+ max_qty = matched_items[item_idx]['item']['Quantity']
1565
+ matched_items[item_idx]['quantity'] = max(1, min(int(new_qty), max_qty))
1566
+ return matched_items
1567
+
1568
+ def reset_checkout():
1569
+ return (
1570
+ gr.update(visible=True), gr.update(visible=False), gr.update(visible=False),
1571
+ None, "", None, "", "", gr.update(visible=False)
1572
+ )
1573
+
1574
+ def show_checkout_complete_options():
1575
+ return gr.update(visible=False), gr.update(visible=True)
1576
+
1577
+ checkout_scan_btn.click(
1578
+ fn=lambda: (gr.update(value="⏳ Scanning...", interactive=False), "πŸ” **Processing...** AI is analyzing your image..."),
1579
+ outputs=[checkout_scan_btn, checkout_status]
1580
+ ).then(
1581
+ fn=scan_items_checkout,
1582
+ inputs=[checkout_image, checkout_manual],
1583
+ outputs=[matched_items_state, checkout_status]
1584
+ ).then(
1585
+ fn=lambda: gr.update(value="πŸ” Scan & Match Items", interactive=True),
1586
+ outputs=[checkout_scan_btn]
1587
+ ).then(
1588
+ fn=lambda items: (
1589
+ gr.update(visible=False) if items else gr.update(),
1590
+ gr.update(visible=True) if items else gr.update(),
1591
+ gr.update(visible=False)
1592
+ ),
1593
+ inputs=[matched_items_state],
1594
+ outputs=[checkout_screen1, checkout_screen2, checkout_screen3]
1595
+ ).then(
1596
+ fn=update_checkout_screen2,
1597
+ inputs=[matched_items_state],
1598
+ outputs=[checkout_item_controls[i][key] for i in range(10) for key in ['row', 'info', 'qty']]
1599
+ )
1600
+
1601
+ for i in range(10):
1602
+ checkout_item_controls[i]['qty'].change(
1603
+ fn=lambda items, new_qty, idx=i: set_checkout_quantity(items, idx, new_qty),
1604
+ inputs=[matched_items_state, checkout_item_controls[i]['qty']],
1605
+ outputs=[matched_items_state]
1606
+ )
1607
+
1608
+ checkout_item_controls[i]['remove'].click(
1609
+ fn=lambda items, idx=i: remove_checkout_item(items, idx),
1610
+ inputs=[matched_items_state],
1611
+ outputs=[matched_items_state]
1612
+ ).then(
1613
+ fn=update_checkout_screen2,
1614
+ inputs=[matched_items_state],
1615
+ outputs=[checkout_item_controls[j][key] for j in range(10) for key in ['row', 'info', 'qty']]
1616
+ )
1617
+
1618
+ checkout_confirm_btn.click(
1619
+ fn=confirm_checkout_preview,
1620
+ inputs=[matched_items_state],
1621
+ outputs=[checkout_preview]
1622
+ ).then(
1623
+ fn=lambda: (gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)),
1624
+ outputs=[checkout_screen1, checkout_screen2, checkout_screen3]
1625
+ )
1626
+
1627
+ checkout_rescan_btn.click(
1628
+ fn=reset_checkout,
1629
+ outputs=[checkout_screen1, checkout_screen2, checkout_screen3, matched_items_state,
1630
+ checkout_status, checkout_image, checkout_manual, checkout_result, checkout_another_btn]
1631
+ )
1632
+
1633
+ checkout_cancel_btn.click(
1634
+ fn=reset_checkout,
1635
+ outputs=[checkout_screen1, checkout_screen2, checkout_screen3, matched_items_state,
1636
+ checkout_status, checkout_image, checkout_manual, checkout_result, checkout_another_btn]
1637
+ )
1638
+
1639
+ checkout_final_btn.click(
1640
+ fn=lambda: (gr.update(value="⏳ Processing...", interactive=False), "⏳ **Processing checkout...** Updating inventory..."),
1641
+ outputs=[checkout_final_btn, checkout_processing]
1642
+ ).then(
1643
+ fn=process_checkout,
1644
+ inputs=[matched_items_state, checkout_user_id],
1645
+ outputs=[checkout_result]
1646
+ ).then(
1647
+ fn=lambda: (gr.update(value="βœ… Complete Checkout", interactive=True), ""),
1648
+ outputs=[checkout_final_btn, checkout_processing]
1649
+ ).then(
1650
+ fn=lambda: (gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)),
1651
+ outputs=[checkout_screen1, checkout_screen2, checkout_screen3]
1652
+ ).then(
1653
+ fn=show_checkout_complete_options,
1654
+ outputs=[checkout_return_btn, checkout_another_btn]
1655
+ )
1656
+
1657
+ checkout_another_btn.click(
1658
+ fn=reset_checkout,
1659
+ outputs=[checkout_screen1, checkout_screen2, checkout_screen3, matched_items_state,
1660
+ checkout_status, checkout_image, checkout_manual, checkout_result, checkout_another_btn]
1661
+ ).then(
1662
+ fn=lambda: (gr.update(visible=True), gr.update(visible=False)),
1663
+ outputs=[checkout_return_btn, checkout_another_btn]
1664
+ )
1665
+
1666
+ # EVENT HANDLERS - Add Items
1667
+ receipt_file.change(
1668
+ fn=lambda: "πŸ“„ **Processing receipt...** Extracting text with OCR...",
1669
+ outputs=[receipt_status]
1670
+ ).then(
1671
+ fn=extract_text_from_receipt,
1672
+ inputs=[receipt_file],
1673
+ outputs=[add_items_table, proposals_state]
1674
+ ).then(
1675
+ fn=lambda proposals: (gr.update(interactive=len(proposals) > 0), "βœ… Items detected! Review and edit the table below."),
1676
+ inputs=[proposals_state],
1677
+ outputs=[add_items_confirm_btn, receipt_status]
1678
+ )
1679
+
1680
+ add_items_confirm_btn.click(
1681
+ fn=lambda: (gr.update(value="⏳ Applying...", interactive=False), "⏳ **Updating inventory...**"),
1682
+ outputs=[add_items_confirm_btn, add_items_status]
1683
+ ).then(
1684
+ fn=apply_updates_from_table,
1685
+ inputs=[add_items_table],
1686
+ outputs=[add_items_status, inventory_display]
1687
+ ).then(
1688
+ fn=lambda: (gr.update(value="βœ… Apply Updates", interactive=False), gr.update(visible=True)),
1689
+ outputs=[add_items_confirm_btn, inventory_display]
1690
+ )
1691
+
1692
+ add_items_reject_btn.click(
1693
+ fn=lambda: ([["No items", "", "", ""]], "❌ Cleared all items.", []),
1694
+ outputs=[add_items_table, add_items_status, proposals_state]
1695
+ ).then(
1696
+ fn=lambda: gr.update(interactive=False),
1697
+ outputs=[add_items_confirm_btn]
1698
+ )
1699
+
1700
+ manual_apply_btn.click(
1701
+ fn=lambda: gr.update(value="⏳ Adding...", interactive=False),
1702
+ outputs=[manual_apply_btn]
1703
+ ).then(
1704
+ fn=manual_update,
1705
+ inputs=[manual_item, manual_qty],
1706
+ outputs=[manual_status, manual_inventory_display]
1707
+ ).then(
1708
+ fn=lambda: (gr.update(value="βž• Add to Inventory", interactive=True), gr.update(visible=True)),
1709
+ outputs=[manual_apply_btn, manual_inventory_display]
1710
+ )
1711
+
1712
+ view_inventory_btn.click(
1713
+ fn=view_inventory_table,
1714
+ inputs=[inventory_category_filter],
1715
+ outputs=[inventory_display]
1716
+ ).then(
1717
+ fn=lambda: gr.update(visible=True),
1718
+ outputs=[inventory_display]
1719
+ )
1720
+
1721
+ inventory_category_filter.change(
1722
+ fn=view_inventory_table,
1723
+ inputs=[inventory_category_filter],
1724
+ outputs=[inventory_display]
1725
+ )
1726
+
1727
+ view_history_btn.click(
1728
+ fn=view_update_history,
1729
+ outputs=[history_display]
1730
+ ).then(
1731
+ fn=lambda: gr.update(visible=True),
1732
+ outputs=[history_display]
1733
+ )
1734
+
1735
+ # EVENT HANDLERS - Analysis
1736
+ run_analysis_btn.click(
1737
+ fn=run_analysis,
1738
+ inputs=[min_support, top_k_pairs, max_moves, min_gain],
1739
+ outputs=[pairs_table, recs_table, analysis_summary, analysis_viz]
1740
+ )
1741
+
1742
+ # EXAMPLE INPUTS
1743
+ if EXAMPLE_CHECKOUT_IMAGE:
1744
+ def load_checkout_example():
1745
+ try:
1746
+ return Image.open(EXAMPLE_CHECKOUT_IMAGE)
1747
+ except:
1748
+ return None
1749
+
1750
+ load_example_checkout_btn.click(
1751
+ fn=load_checkout_example,
1752
+ outputs=[checkout_image]
1753
+ )
1754
+
1755
+ if EXAMPLE_RECEIPT_PDF:
1756
+ def load_receipt_example():
1757
+ try:
1758
+ return EXAMPLE_RECEIPT_PDF
1759
+ except:
1760
+ return None
1761
+
1762
+ load_example_receipt_btn.click(
1763
+ fn=load_receipt_example,
1764
+ outputs=[receipt_file]
1765
+ )
1766
+
1767
+ if __name__ == "__main__":
1768
+ demo.launch()