Hamdy005 commited on
Commit
ecad91b
Β·
verified Β·
1 Parent(s): 664e314

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +399 -66
src/streamlit_app.py CHANGED
@@ -1,91 +1,424 @@
1
  import streamlit as st
2
  import requests
 
3
 
4
- API_URL_TEXT = "https://hamdy005-raij-ai.hf.space/search/text"
5
- API_URL_IMAGE = "https://hamdy005-raij-ai.hf.space/search/image"
6
- API_URL_AUDIO = "https://hamdy005-raij-ai.hf.space/search/audio"
 
 
 
 
7
 
8
- st.title("πŸ”Ž AI Semantic Search Client")
9
- st.write("Search by text, image, or audio")
 
 
 
 
 
10
 
11
- with st.sidebar:
12
- st.header("Number of products to show")
13
- top_k = st.sidebar.number_input("Top-K Results", value=10, min_value=1, max_value=100)
 
 
 
14
 
15
- # ---------------------- TEXT SEARCH ----------------------
16
- st.header("πŸ“ Text Search")
17
- query = st.text_input("Enter search query:")
18
 
19
- if st.button("Search Text"):
 
 
 
 
 
 
 
 
20
 
21
- if not query.strip():
22
- st.error("Query cannot be empty!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- else:
25
- response = requests.post(
26
- API_URL_TEXT,
27
- params={"query": query, "top_k": top_k},
28
- allow_redirects=True
29
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
 
 
 
 
 
31
  if response.status_code == 200:
32
- st.success("βœ… Response received")
33
- st.json(response.json())
 
 
 
34
 
35
- else:
36
- st.error(f"❌ API Error: {response.status_code}")
37
- st.write(response.text)
38
 
39
- # ---------------------- IMAGE SEARCH ----------------------
40
- st.header("πŸ–ΌοΈ Image Search")
41
- image_file = st.file_uploader("Upload an image", type=["png", "jpg", "jpeg"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
- if st.button("Search Image"):
 
 
 
 
 
 
44
 
45
- if image_file is None:
46
- st.error("Please upload an image first!")
 
47
 
48
- else:
 
 
 
 
 
49
 
50
- files = {"image": (image_file.name, image_file, image_file.type)}
51
- response = requests.post(
52
- API_URL_IMAGE,
53
- files=files,
54
- params={"top_k": top_k},
55
- allow_redirects=True
 
56
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- if response.status_code == 200:
59
- st.success("βœ… Response received")
60
- st.json(response.json())
 
 
 
61
 
62
- else:
63
- st.error(f"❌ API Error: {response.status_code}")
64
- st.write(response.text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
- # ---------------------- AUDIO SEARCH ----------------------
67
- st.header("🎀 Audio Search")
68
- audio_file = st.file_uploader("Upload audio file", type=["wav", "mp3", "m4a"])
69
- language = st.selectbox("Choose a language", ['en', 'ar'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
- if st.button("Search Audio"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- if audio_file is None:
74
- st.error("Please upload an audio file first!")
 
 
 
75
 
76
- else:
77
- files = {"audio": (audio_file.name, audio_file, audio_file.type)}
78
- response = requests.post(
79
- API_URL_AUDIO,
80
- files=files,
81
- params={"top_k": top_k, "language": language},
82
- allow_redirects=True
83
- )
84
 
85
- if response.status_code == 200:
86
- st.success("βœ… Response received")
87
- st.json(response.json())
88
-
89
- else:
90
- st.error(f"❌ API Error: {response.status_code}")
91
- st.write(response.text)
 
 
 
 
 
1
  import streamlit as st
2
  import requests
3
+ from pathlib import Path
4
 
5
+ # ---------------------- CONFIG ----------------------
6
+ API_BASE_URL = "https://hamdy005-raij-ai.hf.space"
7
+ API_URL_TEXT = f"{API_BASE_URL}/search/text"
8
+ API_URL_IMAGE = f"{API_BASE_URL}/search/image"
9
+ API_URL_AUDIO = f"{API_BASE_URL}/search/audio"
10
+ API_URL_PRODUCT = f"{API_BASE_URL}/product"
11
+ API_URL_RANDOM = f"{API_BASE_URL}/products/random"
12
 
13
+ # ---------------------- PAGE CONFIG ----------------------
14
+ st.set_page_config(
15
+ page_title="AI Smart Search",
16
+ page_icon="πŸ”Ž",
17
+ layout="wide",
18
+ initial_sidebar_state="collapsed"
19
+ )
20
 
21
+ # ---------------------- LOAD CSS ----------------------
22
+ def load_css():
23
+ css_file = Path(__file__).parent / "styles.css"
24
+ if css_file.exists():
25
+ with open(css_file) as f:
26
+ st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
27
 
28
+ load_css()
 
 
29
 
30
+ # ---------------------- SESSION STATE ----------------------
31
+ if "audio_key" not in st.session_state:
32
+ st.session_state.audio_key = 0
33
+ if "search_results" not in st.session_state:
34
+ st.session_state.search_results = None
35
+ if "prediction_info" not in st.session_state:
36
+ st.session_state.prediction_info = None
37
+ if "selected_product" not in st.session_state:
38
+ st.session_state.selected_product = None
39
 
40
+ # ---------------------- HELPER FUNCTIONS ----------------------
41
+ def build_products_list(data):
42
+ """Build products list from API response"""
43
+ products = []
44
+ image_urls = data.get("image_urls", [])
45
+ prices = data.get("prices", [])
46
+ for idx, (pid, title) in enumerate(zip(
47
+ data.get("product_ids", []),
48
+ data.get("titles", []),
49
+ )):
50
+ products.append({
51
+ "id": pid,
52
+ "title": title,
53
+ "image_url": image_urls[idx] if idx < len(image_urls) else None,
54
+ "price": prices[idx] if idx < len(prices) else None
55
+ })
56
+ return products
57
 
58
+ def display_products(products, prediction_info=None):
59
+ """Display products in a grid layout"""
60
+
61
+ # Show prediction info as a compact banner
62
+ if prediction_info and prediction_info.get("type") != "text":
63
+ if prediction_info.get("type") == "image":
64
+ st.info(f"πŸ–ΌοΈ **Detected Category:** {prediction_info.get('category', 'N/A')} ({prediction_info.get('confidence', 0):.1%} confidence)")
65
+ elif prediction_info.get("type") == "audio":
66
+ st.info(f"🎀 **Transcription:** \"{prediction_info.get('transcription', 'N/A')}\"")
67
+
68
+ # Products header
69
+ st.markdown(f"### πŸ›οΈ {len(products)} Products Found")
70
+
71
+ # Create grid layout - 4 columns for more compact display
72
+ cols_per_row = 4
73
+ for i in range(0, len(products), cols_per_row):
74
+ cols = st.columns(cols_per_row)
75
+ for j, col in enumerate(cols):
76
+ if i + j < len(products):
77
+ product = products[i + j]
78
+ with col:
79
+ with st.container(border=True):
80
+ # Show image first if available
81
+ if product.get('image_url'):
82
+ try:
83
+ st.image(product['image_url'], width="stretch")
84
+ except:
85
+ st.markdown("πŸ–ΌοΈ *No image*")
86
+
87
+ # Product title (compact)
88
+ title = product.get('title', 'N/A')
89
+ display_title = title[:40] + '...' if len(title) > 40 else title
90
+ st.markdown(f"**{display_title}**")
91
+
92
+ # Price
93
+ price = product.get('price')
94
+ if price:
95
+ st.markdown(f"<span style='color: #2ecc71; font-weight: bold;'>${price:.2f}</span>", unsafe_allow_html=True)
96
+
97
+ # View Details button
98
+ if st.button("View Details", key=f"view_{product.get('id')}", type="secondary"):
99
+ st.session_state.selected_product = product.get('id')
100
+ st.rerun()
101
+
102
+
103
+ def display_product_details(product_id):
104
+ """Display detailed product information"""
105
+
106
+ # Back button
107
+ if st.button("← Back to Results", type="secondary"):
108
+ st.session_state.selected_product = None
109
+ st.rerun()
110
+
111
+ # Fetch product details from API
112
+ with st.spinner("Loading product details..."):
113
+ try:
114
+ response = requests.get(f"{API_URL_PRODUCT}/{product_id}", timeout=30)
115
+ if response.status_code == 200:
116
+ product = response.json()
117
+
118
+ if product.get("error"):
119
+ st.error("Product not found")
120
+ return
121
+
122
+ # Layout: Image on left, details on right
123
+ col1, col2 = st.columns([1, 2])
124
+
125
+ with col1:
126
+ # Product images
127
+ images = product.get("images", [])
128
+ if images:
129
+ st.image(images[0], width="stretch")
130
+ # Show thumbnails if multiple images
131
+ if len(images) > 1:
132
+ thumb_cols = st.columns(min(4, len(images)))
133
+ for idx, img_url in enumerate(images[:4]):
134
+ with thumb_cols[idx]:
135
+ st.image(img_url, width=80)
136
+ else:
137
+ st.markdown("πŸ–ΌοΈ *No image available*")
138
+
139
+ with col2:
140
+ # Product title
141
+ st.markdown(f"## {product.get('title', 'N/A')}")
142
+
143
+ # SKU
144
+ sku = product.get('sku')
145
+ if sku:
146
+ st.caption(f"SKU: {sku}")
147
+
148
+ st.markdown("---")
149
+
150
+ # Price section
151
+ price = product.get('price')
152
+ old_price = product.get('old_price')
153
+
154
+ if old_price and old_price > price:
155
+ discount = int((1 - price / old_price) * 100)
156
+ st.markdown(f"""
157
+ <div style='margin: 10px 0;'>
158
+ <span style='text-decoration: line-through; color: #888; font-size: 1.2rem;'>${old_price:.2f}</span>
159
+ <span style='color: #e74c3c; font-size: 1.8rem; font-weight: bold; margin-left: 10px;'>${price:.2f}</span>
160
+ <span style='background: #e74c3c; color: white; padding: 3px 8px; border-radius: 4px; margin-left: 10px; font-size: 0.9rem;'>-{discount}%</span>
161
+ </div>
162
+ """, unsafe_allow_html=True)
163
+ elif price:
164
+ st.markdown(f"<span style='color: #2ecc71; font-size: 1.8rem; font-weight: bold;'>${price:.2f}</span>", unsafe_allow_html=True)
165
+ else:
166
+ st.markdown("*Price not available*")
167
+
168
+ st.markdown("---")
169
+
170
+ # Stock status
171
+ stock = product.get('stock', 0)
172
+ if stock > 0:
173
+ st.success(f"βœ… In Stock ({stock} available)")
174
+ else:
175
+ st.error("❌ Out of Stock")
176
+
177
+ st.markdown("---")
178
+
179
+ # Description
180
+ st.markdown("### πŸ“ Description")
181
+ description = product.get('description', 'No description available.')
182
+ st.write(description)
183
+
184
+ # Tags
185
+ tags = product.get('tags', [])
186
+ if tags:
187
+ st.markdown("### 🏷️ Tags")
188
+ st.write(" β€’ ".join(tags))
189
+
190
+ else:
191
+ st.error(f"Error loading product: {response.status_code}")
192
+ except Exception as e:
193
+ st.error(f"Error: {str(e)}")
194
+
195
+
196
+ def reset_search():
197
+ """Reset search state"""
198
+ st.session_state.search_results = None
199
+ st.session_state.prediction_info = None
200
+ st.session_state.selected_product = None
201
 
202
+
203
+ def fetch_random_products(limit=10):
204
+ """Fetch products from API"""
205
+ try:
206
+ response = requests.get(API_URL_RANDOM, params={"limit": limit}, timeout=30)
207
  if response.status_code == 200:
208
+ data = response.json()
209
+ return data.get("products", [])
210
+ except Exception as e:
211
+ print(f"Error fetching products: {e}")
212
+ return []
213
 
 
 
 
214
 
215
+ def display_random_products():
216
+ """Display products in a grid layout"""
217
+ st.markdown("### πŸ›οΈ Discover Products")
218
+
219
+ # Cache products in session state to avoid fetching on every rerun
220
+ if "random_products" not in st.session_state:
221
+ st.session_state.random_products = fetch_random_products(10)
222
+
223
+ products = st.session_state.random_products
224
+
225
+ if not products:
226
+ st.info("No products available. Try searching instead!")
227
+ return
228
+
229
+ # Refresh button
230
+ if st.button("πŸ”„ Refresh Products", type="secondary"):
231
+ st.session_state.random_products = fetch_random_products(10)
232
+ st.rerun()
233
+
234
+ # Create grid layout - 4 columns
235
+ cols_per_row = 4
236
+ for i in range(0, len(products), cols_per_row):
237
+ cols = st.columns(cols_per_row)
238
+ for j, col in enumerate(cols):
239
+ if i + j < len(products):
240
+ product = products[i + j]
241
+ with col:
242
+ with st.container(border=True):
243
+ # Show image first if available
244
+ if product.get('image_url'):
245
+ try:
246
+ st.image(product['image_url'], width="stretch")
247
+ except:
248
+ st.markdown("πŸ–ΌοΈ *No image*")
249
+
250
+ # Product title (compact)
251
+ title = product.get('title', 'N/A')
252
+ display_title = title[:40] + '...' if len(title) > 40 else title
253
+ st.markdown(f"**{display_title}**")
254
+
255
+ # Price
256
+ price = product.get('price')
257
+ if price:
258
+ st.markdown(f"<span style='color: #2ecc71; font-weight: bold;'>${price:.2f}</span>", unsafe_allow_html=True)
259
+
260
+ # View Details button
261
+ if st.button("View Details", key=f"random_{product.get('id')}", type="secondary"):
262
+ st.session_state.selected_product = product.get('id')
263
+ st.rerun()
264
 
265
+ # ---------------------- HEADER ----------------------
266
+ st.markdown("""
267
+ <div class="main-header">
268
+ <h1>πŸ”Ž AI Smart Search</h1>
269
+ <p>Search by Text, Image, or Voice</p>
270
+ </div>
271
+ """, unsafe_allow_html=True)
272
 
273
+ # ---------------------- COMPACT SEARCH BAR ----------------------
274
+ # All search controls in one row
275
+ search_col1, search_col2, search_col3 = st.columns([1, 4, 1])
276
 
277
+ with search_col1:
278
+ search_type = st.selectbox(
279
+ "Type",
280
+ ["πŸ“ Text", "πŸ–ΌοΈ Image", "🎀 Audio"],
281
+ label_visibility="collapsed"
282
+ )
283
 
284
+ with search_col2:
285
+ # Dynamic input based on search type
286
+ if search_type == "πŸ“ Text":
287
+ query = st.text_input(
288
+ "Search",
289
+ placeholder="Search for products...",
290
+ label_visibility="collapsed"
291
  )
292
+ elif search_type == "πŸ–ΌοΈ Image":
293
+ image_file = st.file_uploader(
294
+ "Upload image",
295
+ type=["png", "jpg", "jpeg"],
296
+ label_visibility="collapsed"
297
+ )
298
+ else: # Audio
299
+ audio_col1, audio_col2 = st.columns([3, 1])
300
+ with audio_col1:
301
+ audio_method = st.radio(
302
+ "Method",
303
+ ["πŸŽ™οΈ Record", "πŸ“ Upload"],
304
+ horizontal=True,
305
+ label_visibility="collapsed"
306
+ )
307
+ with audio_col2:
308
+ language = st.selectbox(
309
+ "Lang",
310
+ ["en", "ar"],
311
+ format_func=lambda x: "EN" if x == "en" else "AR",
312
+ label_visibility="collapsed"
313
+ )
314
 
315
+ with search_col3:
316
+ top_k = st.selectbox(
317
+ "Results",
318
+ [10, 20, 30, 50],
319
+ label_visibility="collapsed"
320
+ )
321
 
322
+ # ---------------------- SEARCH INPUT ROW 2 (for Image/Audio) ----------------------
323
+ if search_type == "πŸ–ΌοΈ Image" and image_file:
324
+ preview_col, btn_col = st.columns([1, 3])
325
+ with preview_col:
326
+ st.image(image_file, width=100)
327
+ with btn_col:
328
+ search_btn = st.button("πŸ” Search", type="primary")
329
+ if search_btn:
330
+ with st.spinner("Analyzing..."):
331
+ try:
332
+ image_file.seek(0)
333
+ files = {"image": (image_file.name, image_file, image_file.type)}
334
+ response = requests.post(API_URL_IMAGE, files=files, params={"top_k": top_k}, timeout=60)
335
+ if response.status_code == 200:
336
+ data = response.json()
337
+ st.session_state.search_results = build_products_list(data)
338
+ st.session_state.prediction_info = {
339
+ "type": "image",
340
+ "category": data.get("predicted_category", "Unknown"),
341
+ "confidence": data.get("confidence_score", 0)
342
+ }
343
+ st.rerun()
344
+ else:
345
+ st.error(f"Error: {response.status_code}")
346
+ except Exception as e:
347
+ st.error(str(e))
348
 
349
+ elif search_type == "🎀 Audio":
350
+ audio_data = None
351
+ audio_filename = "recording.wav"
352
+
353
+ if audio_method == "πŸŽ™οΈ Record":
354
+ recorded = st.audio_input("Record", key=f"rec_{st.session_state.audio_key}", label_visibility="collapsed")
355
+ if recorded:
356
+ audio_data = recorded
357
+ else:
358
+ uploaded = st.file_uploader("Upload", type=["wav", "mp3", "m4a"], key=f"up_{st.session_state.audio_key}", label_visibility="collapsed")
359
+ if uploaded:
360
+ audio_data = uploaded
361
+ audio_filename = uploaded.name
362
+
363
+ if audio_data:
364
+ col1, col2 = st.columns([1, 2])
365
+ with col1:
366
+ st.audio(audio_data)
367
+ with col2:
368
+ if st.button("πŸ” Search", type="primary"):
369
+ with st.spinner("Transcribing..."):
370
+ try:
371
+ audio_data.seek(0)
372
+ files = {"audio": (audio_filename, audio_data, "audio/wav")}
373
+ response = requests.post(API_URL_AUDIO, files=files, params={"top_k": top_k, "language": language}, timeout=60)
374
+ if response.status_code == 200:
375
+ data = response.json()
376
+ st.session_state.search_results = build_products_list(data)
377
+ st.session_state.prediction_info = {
378
+ "type": "audio",
379
+ "transcription": data.get("caption", "")
380
+ }
381
+ st.session_state.audio_key += 1
382
+ st.rerun()
383
+ else:
384
+ st.error(f"Error: {response.status_code}")
385
+ except Exception as e:
386
+ st.error(str(e))
387
 
388
+ elif search_type == "πŸ“ Text":
389
+ if st.button("πŸ” Search", type="primary") or (query and st.session_state.get("last_query") != query):
390
+ if query and query.strip():
391
+ with st.spinner("Searching..."):
392
+ try:
393
+ response = requests.post(API_URL_TEXT, params={"query": query, "top_k": top_k}, timeout=30)
394
+ if response.status_code == 200:
395
+ data = response.json()
396
+ st.session_state.search_results = build_products_list(data)
397
+ st.session_state.prediction_info = {"type": "text", "query": query}
398
+ st.session_state.last_query = query
399
+ st.rerun()
400
+ else:
401
+ st.error(f"Error: {response.status_code}")
402
+ except Exception as e:
403
+ st.error(str(e))
404
 
405
+ # ---------------------- CLEAR BUTTON ----------------------
406
+ if st.session_state.search_results and not st.session_state.selected_product:
407
+ if st.button("πŸ—‘οΈ Clear", type="secondary"):
408
+ reset_search()
409
+ st.rerun()
410
 
411
+ # ---------------------- DISPLAY RESULTS ----------------------
412
+ st.markdown("---")
 
 
 
 
 
 
413
 
414
+ # Show product details if a product is selected
415
+ if st.session_state.selected_product:
416
+ display_product_details(st.session_state.selected_product)
417
+ elif st.session_state.search_results:
418
+ display_products(
419
+ st.session_state.search_results,
420
+ st.session_state.prediction_info
421
+ )
422
+ else:
423
+ # Show random products before any search
424
+ display_random_products()