aibyml commited on
Commit
863e643
·
verified ·
1 Parent(s): d3302db

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +587 -960
app.py CHANGED
@@ -24,20 +24,14 @@ with open('premium_collections.json', 'r') as f:
24
  with open('clothing.json', 'r') as f:
25
  clothing = json.load(f)
26
 
27
- #with open('products.json', 'r') as f:
28
- # products = json.load(f)
29
-
30
  # Combine both datasets and tag them with their source
31
  for item in premium_collections:
32
  item['source'] = 'premium_collections'
33
  for item in clothing:
34
  item['source'] = 'clothing'
35
 
36
- #for item in products:
37
- # item['source'] = 'products'
38
-
39
- #all_items = products
40
  all_items = premium_collections + clothing
 
41
  # Function to normalize price strings to float
42
  def normalize_price(price_str):
43
  if not price_str:
@@ -60,8 +54,7 @@ for item in all_items:
60
  if item.get('price'):
61
  item['normalized_price'] = normalize_price(item['price'])
62
 
63
- # Define all the dropdown options
64
- AGE_GROUPS = ["Choose an option", "<18", "18-30", "30-40", "40-50", "50-60", ">60"]
65
  GIFT_OCCASIONS = [
66
  "Choose an option",
67
  "Festive Celebration",
@@ -73,25 +66,6 @@ GIFT_OCCASIONS = [
73
  "All The Best!",
74
  "Others"
75
  ]
76
- COLOR_THEMES = [
77
- "Choose an option",
78
- "Black", "White", "Off-White", "Brown", "Red", "Blue", "Gray",
79
- "Gold", "Yellow", "Purple", "Pink", "Green", "Silver",
80
- "Orange", "Multi-color", "Transparent"
81
- ]
82
- JOB_FUNCTIONS = [
83
- "Choose an option",
84
- "C-Suite",
85
- "Sales & Business Development",
86
- "Finance",
87
- "Operations",
88
- "Human Resource",
89
- "Engineering",
90
- "Information Technology",
91
- "Marketing & Communications",
92
- "Others"
93
- ]
94
- GENDERS = ["Choose an option", "Male", "Female", "does not really matter"]
95
 
96
  # Budget options for the new interface
97
  BUDGET_RANGES = [
@@ -141,294 +115,42 @@ class BudgetAgent:
141
 
142
  return fits_budget, total_cost, explanation
143
 
144
- def query_ollama(self, prompt):
145
- """Query the local Ollama model"""
146
- try:
147
- payload = {
148
- "model": self.model,
149
- "prompt": prompt,
150
- "stream": False
151
- }
152
- response = requests.post(OLLAMA_API_URL, json=payload)
153
- if response.status_code == 200:
154
- return response.json().get("response", "Error: No response from Ollama")
155
- else:
156
- return f"Error: {response.status_code} - {response.text}"
157
- except Exception as e:
158
- return f"Error connecting to Ollama: {str(e)}"
159
-
160
- # Update the BudgetAgent's optimize_bundle method to respect the desired number of items
161
- def optimize_bundle(self, min_budget: float, max_budget: float, items: list, criteria: str = None, num_items: str = "Any number") -> list:
162
  """
163
- Optimize item selection to fit within budget range while maximizing value.
164
- The criteria parameter is optional for additional filtering logic.
165
- The num_items parameter allows specifying the desired number of items in the bundle.
166
  """
167
- # Filter out items without prices
168
- valid_items = [item for item in items if item.get('normalized_price') is not None]
169
-
170
- if not valid_items:
171
- return []
172
-
173
- # Sort items by price (highest first) for initial selection
174
- valid_items.sort(key=lambda x: x.get('normalized_price', 0), reverse=True)
175
-
176
- # Calculate current total
177
- current_total = sum(item.get('normalized_price', 0) for item in valid_items)
178
-
179
- # If over budget, remove expensive items
180
- if current_total > max_budget:
181
- while valid_items and current_total > max_budget:
182
- removed_item = valid_items.pop(0) # Remove the most expensive item
183
- current_total -= removed_item.get('normalized_price', 0)
184
-
185
- # If under minimum budget, try to add more items
186
- if current_total < min_budget:
187
- remaining_items = [item for item in self.items if
188
- item.get('normalized_price') is not None and
189
- item not in valid_items]
190
-
191
- # Sort by price (ascending) to add cheaper items first
192
- remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
193
-
194
- for item in remaining_items:
195
- item_price = item.get('normalized_price', 0)
196
- if current_total + item_price <= max_budget:
197
  valid_items.append(item)
198
- current_total += item_price
199
- if current_total >= min_budget:
200
- break
201
-
202
- # Now adjust based on the desired number of items
203
- if num_items != "Any number":
204
- desired_count = 0
205
-
206
- if num_items == "1 item only":
207
- desired_count = 1
208
- elif num_items == "2 items":
209
- desired_count = 2
210
- elif num_items == "3 items":
211
- desired_count = 3
212
- elif num_items == "4 items":
213
- desired_count = 4
214
- elif num_items == "5 or more items":
215
- desired_count = 5 # We'll use 5 as the minimum for this category
216
-
217
- current_count = len(valid_items)
218
-
219
- if num_items == "5 or more items" and current_count < desired_count:
220
- # Add more items to reach the minimum of 5 for this category
221
- remaining_items = [item for item in self.items if
222
- item.get('normalized_price') is not None and
223
- item not in valid_items]
224
-
225
- # Sort by price (ascending) to add cheaper items first
226
- remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
227
-
228
- for item in remaining_items:
229
- item_price = item.get('normalized_price', 0)
230
- if current_total + item_price <= max_budget:
231
- valid_items.append(item)
232
- current_total += item_price
233
- current_count += 1
234
-
235
- # Stop when we reach the desired count
236
- if current_count >= desired_count:
237
- break
238
-
239
- elif current_count > desired_count:
240
- # Too many items, remove the lowest value ones
241
- valid_items.sort(key=lambda x: x.get('normalized_price', 0))
242
-
243
- while len(valid_items) > desired_count:
244
- removed_item = valid_items.pop(0) # Remove the cheapest item
245
- current_total -= removed_item.get('normalized_price', 0)
246
-
247
- elif current_count < desired_count:
248
- # Too few items, add more while staying under budget
249
- remaining_items = [item for item in self.items if
250
- item.get('normalized_price') is not None and
251
- item not in valid_items]
252
-
253
- # Sort by price (ascending) to add cheaper items first
254
- remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
255
-
256
- for item in remaining_items:
257
- item_price = item.get('normalized_price', 0)
258
- if current_total + item_price <= max_budget:
259
- valid_items.append(item)
260
- current_total += item_price
261
- current_count += 1
262
-
263
- # Stop when we reach the desired count
264
- if current_count >= desired_count:
265
- break
266
 
267
- # Return the optimized selection
 
268
  return valid_items
269
 
270
-
271
- def adjust_bundle_to_fit_total_budget(self, bundle_items: list, min_budget: float, max_budget: float, total_budget: float) -> tuple:
272
- """
273
- Adjust the number of bundle packages to fit within the total budget.
274
- Returns: (num_packages, total_cost, explanation)
275
- """
276
- if not bundle_items:
277
- return 0, 0, "No items in the bundle to calculate."
278
-
279
- # Calculate the cost of a single bundle
280
- bundle_cost = sum(item.get('normalized_price', 0) for item in bundle_items)
281
-
282
- if bundle_cost == 0:
283
- return 0, 0, "Bundle has no valid price information."
284
-
285
- # Calculate how many complete bundles fit within the total budget
286
- max_packages = int(total_budget / bundle_cost)
287
-
288
- if max_packages == 0:
289
- return 0, 0, f"The bundle cost (S${bundle_cost:.2f}) exceeds your total budget of S${total_budget:.2f}."
290
-
291
- total_cost = bundle_cost * max_packages
292
-
293
- # Check if the bundle meets minimum budget requirements
294
- bundle_meets_min = bundle_cost >= min_budget
295
-
296
- explanation = f"Each bundle costs S${bundle_cost:.2f}. "
297
-
298
- if not bundle_meets_min:
299
- explanation += f"Note: The bundle is below your minimum item budget of S${min_budget:.2f}. "
300
-
301
- explanation += f"You can purchase {max_packages} complete bundle(s) for a total of S${total_cost:.2f}, "
302
- explanation += f"which is within your total budget of S${total_budget:.2f}."
303
-
304
- if total_budget - total_cost > 0:
305
- explanation += f" You will have S${total_budget - total_cost:.2f} remaining."
306
-
307
- return max_packages, total_cost, explanation
308
-
309
- # In the SelectionAgent class, modify the select_items method to include total_budget parameter
310
-
311
- # In the SelectionAgent class, modify the select_items method to include total_budget parameter
312
-
313
  class SelectionAgent:
314
  def __init__(self, items):
315
  self.items = items
316
  self.client = OpenAI(api_key=OPENAI_API_KEY)
317
- # Define weights for different criteria
318
- self.weights = {
319
- "query": 0.30, # Free text query (30%)
320
- "budget": { # Budget considerations (30%)
321
- "price_range": 0.15, # Budget range per package
322
- "total_budget": 0.10, # Total budget
323
- "item_count": 0.13 # Number of items per package
324
- },
325
- "demographics": { # Demographics (20%)
326
- "age_group": 0.04, # Age group
327
- "gender": 0.04, # Gender
328
- "job_function": 0.04 # Professional role
329
- },
330
- "occasion": 0.10, # Gift occasion (10%)
331
- "other": { # Other factors (10%)
332
- "color_theme": 0.05, # Color preferences
333
- "misc": 0.05
334
- }
335
- }
336
 
337
- def adjust_to_item_count(self, items: List[Dict], target_count: int, min_budget: float, max_budget: float) -> List[Dict]:
 
338
  """
339
- Adjust the selection to match the target item count while staying within budget.
 
340
  """
341
- if not items or target_count <= 0:
342
- return []
343
-
344
- current_count = len(items)
345
-
346
- # If we already have the right number, return as is
347
- if current_count == target_count:
348
- return items
349
-
350
- # Calculate current total cost
351
- current_total = sum(item.get('normalized_price', 0) for item in items if item.get('normalized_price') is not None)
352
-
353
- if current_count < target_count:
354
- # We need to add more items
355
- # Find items not already in our selection
356
- remaining_items = [item for item in self.items if item.get('normalized_price') is not None
357
- and item not in items]
358
-
359
- # Sort by price (ascending) to add cheaper items first
360
- remaining_items.sort(key=lambda x: x.get('normalized_price', 0))
361
-
362
- for item in remaining_items:
363
- item_price = item.get('normalized_price', 0)
364
- if current_total + item_price <= max_budget:
365
- items.append(item)
366
- current_total += item_price
367
- current_count += 1
368
-
369
- if current_count >= target_count:
370
- break
371
- else:
372
- # We need to remove some items
373
- # Sort by price (ascending) so we remove cheaper items first
374
- # This helps maintain value while reducing count
375
- items.sort(key=lambda x: x.get('normalized_price', 0))
376
-
377
- while current_count > target_count:
378
- removed_item = items.pop(0) # Remove the cheapest item
379
- current_total -= removed_item.get('normalized_price', 0)
380
- current_count -= 1
381
-
382
- return items
383
 
384
- def optimize_selection(self, items: List[Dict], min_budget: float, max_budget: float) -> List[Dict]:
385
- """
386
- Optimize the selection to maximize budget utilization while staying within limits.
387
- """
388
- if not items:
389
  return []
390
-
391
- # Calculate current total
392
- current_total = sum(item.get('normalized_price', 0) for item in items if item.get('normalized_price') is not None)
393
-
394
- if current_total > max_budget:
395
- # Over budget, need to remove items
396
- # Sort by price (descending) to remove expensive items first
397
- items.sort(key=lambda x: x.get('normalized_price', 0), reverse=True)
398
-
399
- while items and current_total > max_budget:
400
- removed_item = items.pop(0) # Remove the most expensive item
401
- current_total -= removed_item.get('normalized_price', 0)
402
-
403
- elif current_total < min_budget:
404
- # Under minimum budget, try to add more items
405
- remaining_items = [item for item in self.items if item.get('normalized_price') is not None
406
- and item not in items]
407
-
408
- # Sort by price (descending) to add valuable items first
409
- remaining_items.sort(key=lambda x: x.get('normalized_price', 0), reverse=True)
410
-
411
- for item in remaining_items:
412
- item_price = item.get('normalized_price', 0)
413
- if current_total + item_price <= max_budget:
414
- items.append(item)
415
- current_total += item_price
416
-
417
- if current_total >= min_budget:
418
- break
419
 
420
- return items
421
-
422
- def select_items(self, criteria: str, min_budget: float, max_budget: float, item_count: int = None,
423
- age_group: str = "", gift_occasion: str = "", color_theme: str = "",
424
- job_function: str = "", gender: str = "", quantity: str = "",
425
- total_budget: float = 500.0) -> List[Dict]: # Added total_budget parameter with default
426
- """
427
- Use OpenAI to select items based on the criteria with weighted importance.
428
- Returns a list of items that fit the criteria.
429
- """
430
  # Prepare the data for OpenAI (limit the number of items to avoid token limits)
431
- items_sample = self.items[:50] # Take a sample to avoid token limits
432
 
433
  # Extract discount information for each item
434
  for item in items_sample:
@@ -444,93 +166,41 @@ class SelectionAgent:
444
  "description": item.get('short_description', '')[:100],
445
  "labels": item.get('labels', []),
446
  "has_bulk_discount": item.get('has_bulk_discount', False),
447
- "discount_info": item.get('discount_info', '')
 
 
448
  } for item in items_sample])
449
 
450
- # Create the system prompt with weighted criteria
451
  system_prompt = """
452
- You are a gift selection expert. Your task is to select items that best match the user's criteria with the following priority weights:
453
-
454
- 1. QUERY (30%): The user's free text description of what they're looking for is the most important factor.
455
- - Pay close attention to specific item types, materials, or features mentioned
456
- - Understand the intent and purpose behind the request
457
-
458
- 2. BUDGET CONSIDERATIONS (30%):
459
- - Budget range per package (15%): Select items that fit within the specified budget range
460
- - Total budget (10%): Consider how many packages can be created within the total budget
461
- - Number of items per package (5%): Aim to include the requested number of items
462
-
463
- 3. DEMOGRAPHICS (20%):
464
- - Age Group (4%): Select age-appropriate items
465
- - Gender (8%): Consider gender preferences if specified
466
- - Job Function/Professional role (8%): Select items appropriate for the recipient's professional context
467
-
468
- 4. OCCASION (10%):
469
- - Match items to the specific occasion or purpose of the gift
470
 
471
- 5. OTHER FACTORS (10%):
472
- - Color theme (5%): Consider color preferences
473
-
474
- Pay special attention to items that offer bulk discounts and prioritize them when appropriate.
475
  Return your selections as a JSON array of item names that meet the criteria.
 
476
  """
477
-
478
- if item_count:
479
- system_prompt += f"\nSelect EXACTLY {item_count} items for the package if possible."
480
- else:
481
- system_prompt += "\nTry to select MORE THAN ONE item."
482
 
483
- budget_text = f"a maximum budget of S${max_budget:.2f}"
484
- if min_budget > 0:
485
- budget_text = f"a budget range of S${min_budget:.2f} to S${max_budget:.2f}"
486
 
487
- item_count_text = ""
488
- if item_count:
489
- item_count_text = f" containing exactly {item_count} items"
490
-
491
- # Build a structured user prompt that clearly separates the different criteria by importance
492
  user_prompt = f"""
493
- I have {budget_text} and I'm looking for items{item_count_text} that match these criteria:
494
 
495
- PRIMARY REQUIREMENT (30% weight):
496
  {criteria}
497
 
498
- BUDGET DETAILS (30% weight):
499
- - Budget per package: {budget_text}
500
- - Total budget available: S${total_budget:.2f}
501
- - Desired items per package: {item_count if item_count else "Multiple items"}
502
 
503
- RECIPIENT DEMOGRAPHICS (20% weight):
504
- """
505
-
506
- # Add demographic information if provided
507
- if age_group and age_group != "Choose an option":
508
- user_prompt += f"- Age group: {age_group}\n "
509
- if gender and gender != "Choose an option" and gender != "does not really matter":
510
- user_prompt += f"- Gender: {gender}\n "
511
- if job_function and job_function != "Choose an option":
512
- user_prompt += f"- Job function: {job_function}\n "
513
-
514
- user_prompt += f"""
515
- OCCASION (10% weight):
516
- {gift_occasion if gift_occasion and gift_occasion != "Choose an option" else "Not specified"}
517
 
518
- OTHER PREFERENCES (10% weight):
519
- """
520
-
521
- # Add other preferences if provided
522
- if color_theme and color_theme != "Choose an option":
523
- user_prompt += f"- Color preference: {color_theme}\n "
524
- if quantity and quantity != "Choose quantity":
525
- user_prompt += f"- Quantity: {quantity}\n "
526
-
527
- user_prompt += f"""
528
- Here are the available items:
529
  {items_data}
530
 
531
- Please select the items that best match my criteria according to the weighted priorities and MAXIMIZE the budget utilization while staying under the maximum budget limit. Try to get as close as possible to the maximum budget.
532
-
533
- If an item has bulk discounts available (has_bulk_discount=true), prioritize these items when appropriate for the budget.
534
 
535
  Return a JSON object with a key called "items" that contains an array of item names, like this:
536
  {{"items": ["Item 1", "Item 2", "Item 3"]}}
@@ -564,7 +234,7 @@ class SelectionAgent:
564
  # Find the corresponding items from our pool
565
  selected_items = []
566
  for name in selected_item_names:
567
- for item in self.items:
568
  if name.lower() in item['name'].lower() or item['name'].lower() in name.lower():
569
  # Add discount info to the item
570
  has_discount, discount_info, formatted_discount = extract_discount_info(item)
@@ -576,131 +246,184 @@ class SelectionAgent:
576
  selected_items.append(item)
577
  break
578
 
579
- # Check if we need to adjust the selection based on the exact item count
580
- if item_count and len(selected_items) != item_count:
581
- selected_items = self.adjust_to_item_count(selected_items, item_count, min_budget, max_budget)
582
- else:
583
- # Optimize selection to maximize budget utilization
584
- selected_items = self.optimize_selection(selected_items, min_budget, max_budget)
585
-
586
  return selected_items
587
  except Exception as e:
588
  print(f"Error calling OpenAI API: {str(e)}")
589
- return []
 
590
 
591
- # Now we need to update the process_query method in GiftBundleChatbot to pass the total_budget parameter
592
  class GiftBundleChatbot:
593
  def __init__(self, items):
594
  self.items = items
595
  self.budget_agent = BudgetAgent(items)
596
  self.selection_agent = SelectionAgent(items)
597
 
598
- def process_query(self, query: str, min_budget: float = 0, max_budget: float = 500,
599
- total_budget: float = 500, item_count: int = None, age_group: str = "",
600
- gift_occasion: str = "", color_theme: str = "", job_function: str = "",
601
- gender: str = "", quantity: str = "") -> Tuple[str, List[Dict]]:
602
  """
603
- Process a user query and return recommendations based on weighted criteria.
 
604
  """
605
- # Use the selection agent to get items based on criteria with weights
606
- selected_items = self.selection_agent.select_items(
607
- criteria=query or "Find me gift items",
608
- min_budget=min_budget,
609
- max_budget=max_budget,
610
- item_count=item_count,
611
- age_group=age_group,
612
- gift_occasion=gift_occasion,
613
- color_theme=color_theme,
614
- job_function=job_function,
615
- gender=gender,
616
- quantity=quantity,
617
- total_budget=total_budget # Pass the total_budget parameter
618
- )
619
 
620
- if not selected_items:
621
- # Fallback to the budget agent for optimization
622
- selected_items = self.budget_agent.optimize_bundle(min_budget, max_budget, self.items, query)
623
-
624
- # Adjust to match the requested item count if specified
625
- if item_count and len(selected_items) != item_count:
626
- # Use the selection agent's method to adjust the count
627
- selected_items = self.selection_agent.adjust_to_item_count(
628
- selected_items, item_count, min_budget, max_budget
629
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
- # Check if the selected items fit within the per-package budget range
632
- fits_budget, total_cost, explanation = self.budget_agent.calculate_bundle(min_budget, max_budget, selected_items)
 
 
 
 
 
 
 
 
 
633
 
634
- # Calculate how many packages fit within the total budget
635
- num_packages, total_packages_cost, total_budget_explanation = self.budget_agent.adjust_bundle_to_fit_total_budget(
636
- selected_items, min_budget, max_budget, total_budget
637
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
 
639
  # Format the response
640
- if not selected_items:
641
- return "I couldn't find any items matching your criteria within your budget range. Please try different criteria or adjust your budget.", []
642
 
643
- budget_text = f"per-package budget of S${min_budget:.2f}-S${max_budget:.2f}"
644
- total_budget_text = f"total budget of S${total_budget:.2f}"
645
- item_count_text = f"with {len(selected_items)} items per package" if item_count else ""
646
-
647
- response = f"Based on your criteria: '{query or 'Gift items'}'"
648
 
649
- # Add filter information to response if selected
650
- filters_used = []
651
- if item_count:
652
- filters_used.append(f"Items per package: {item_count}")
653
- if age_group and age_group != "Choose an option":
654
- filters_used.append(f"Age group: {age_group}")
655
  if gift_occasion and gift_occasion != "Choose an option":
656
- filters_used.append(f"Occasion: {gift_occasion}")
657
- if color_theme and color_theme != "Choose an option":
658
- filters_used.append(f"Color: {color_theme}")
659
- if job_function and job_function != "Choose an option":
660
- filters_used.append(f"Job function: {job_function}")
661
- if gender and gender != "Choose an option" and gender != "does not really matter":
662
- filters_used.append(f"Gender: {gender}")
663
- if quantity and quantity != "Choose quantity":
664
- filters_used.append(f"Quantity: {quantity}")
665
- if filters_used:
666
- response += f"\nFilters: {', '.join(filters_used)}"
667
 
668
- response += f"\nBudget: {budget_text} {item_count_text} with {total_budget_text}\n\nI recommend:\n\n"
669
 
670
- # Check for items with bulk discounts to highlight them
671
- discount_items = []
672
-
673
- for item in selected_items:
674
- price_display = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "Price not available"
675
 
676
- # Check if this item has bulk discount information
677
- if item.get('has_bulk_discount', False):
678
- response += f"- {item['name']} ({price_display}) 💰 BULK DISCOUNT AVAILABLE 💰\n {item.get('short_description', 'No description')[:100]}...\n"
679
- response += f" {item.get('formatted_discount', '')}\n\n"
680
- discount_items.append(item['name'])
681
- else:
682
- response += f"- {item['name']} ({price_display})\n {item.get('short_description', 'No description')[:100]}...\n\n"
683
-
684
- response += f"\n{explanation.replace('$', 'S$')}"
685
-
686
- # Add total budget explanation
687
- response += f"\n\n{total_budget_explanation.replace('$', 'S$')}"
688
-
689
- # Add special note about bulk discounts if any were found
690
- if discount_items:
691
- response += f"\n\n💰 SPECIAL NOTE: {len(discount_items)} item(s) in your selection offer bulk discounts: {', '.join(discount_items)}. Consider ordering in larger quantities to save money!"
692
 
693
- # Add recommendation to adjust budget if needed
694
- if not fits_budget:
695
- if total_cost > max_budget:
696
- response += "\n\nWould you like to increase your maximum per-package budget or see a different selection?"
697
- elif min_budget > 0 and total_cost < min_budget:
698
- response += "\n\nWould you like to decrease your minimum per-package budget or see a selection with additional items?"
699
 
700
- if num_packages == 0:
701
- response += "\n\nThe cost of this bundle exceeds your total budget. Would you like to increase your total budget or see a more affordable selection?"
702
-
703
- return response, selected_items
704
 
705
  def parse_budget_range(budget_range):
706
  """Parse a budget range string into min and max values"""
@@ -772,168 +495,335 @@ def extract_discount_info(item):
772
 
773
  return has_discount, discount_info, formatted_info
774
 
775
- # Set up the Gradio interface
776
- def gift_finder_interface(budget_range, budget_total, package_item_count, color, query):
777
  """
778
- Fixed gift_finder_interface function that incorporates package item count and discount information
779
  """
780
- # Parse the budget range
781
- min_budget, max_budget = parse_budget_range(budget_range)
782
-
783
- # Get the total budget
784
- try:
785
- if budget_total and float(budget_total) > 0:
786
- total_budget = float(budget_total)
787
- else:
788
- total_budget = 100.0
789
- except (ValueError, TypeError):
790
- total_budget = 100.0
791
- # Get the number of items per package
792
- item_count = int(package_item_count) if package_item_count else None
793
 
794
- # Initialize the chatbot
795
- chatbot = GiftBundleChatbot(all_items)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
796
 
797
- # Process the query with all filters
798
- response, selected_items = chatbot.process_query(
799
- query=query,
800
- min_budget=min_budget,
801
- max_budget=max_budget,
802
- total_budget=total_budget,
803
- item_count=item_count,
804
- color_theme=color,
805
- )
806
 
807
- # Create DataFrame for bundle display
808
- if selected_items:
809
- # Calculate bundle cost
810
- package_cost = sum(item['normalized_price'] for item in selected_items if item['normalized_price'] is not None)
 
 
 
 
811
 
812
- # Calculate how many packages fit in total budget
813
- max_packages = int(total_budget / package_cost) if package_cost > 0 else 0
814
- total_cost = package_cost * max_packages if max_packages > 0 else 0
 
 
 
 
 
 
 
 
815
 
816
- # Create dataframe with discount information highlighted
817
- bundle_data = []
818
- for item in selected_items:
819
- price_display = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
820
 
821
- # Add discount note if available
822
- discount_note = ""
 
823
  if item.get('has_bulk_discount', False):
824
- discount_note = "💰 BULK DISCOUNT AVAILABLE"
825
 
826
- # Prepare description with discount info if available
827
- description = item.get('short_description', 'No description')[:100]
828
- if item.get('formatted_discount', ''):
829
- description += f"\n{item.get('formatted_discount', '')}"
830
-
831
- bundle_data.append({
832
- "Name": item['name'],
833
- "Price (S$)": price_display,
834
- "Type": item['type'],
835
- "Bulk Discount": discount_note,
836
- "Description": description
837
- })
838
-
839
- bundle_df = pd.DataFrame(bundle_data)
840
 
841
- budget_utilization = (total_cost / total_budget) * 100 if total_budget > 0 else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
842
 
843
- bundle_summary = f"Package Cost: S${package_cost:.2f}\n" \
844
- f"Items per Package: {len(selected_items)}\n" \
845
- f"Number of Packages Possible: {max_packages}\n" \
846
- f"Total Cost: S${total_cost:.2f}\n" \
847
- f"Total Budget: S${total_budget:.2f}\n" \
848
- f"Budget Utilization: {budget_utilization:.1f}%"
849
 
850
- # Create HTML for displaying images with discount badges
851
- html_content = "<div style='display: flex; flex-wrap: wrap; gap: 20px;'>"
852
- count = 0
853
-
854
- # Debug: Print image information for each item
855
- print("Image debug information:")
856
- for i, item in enumerate(selected_items):
857
- print(f"Item {i+1}: {item['name']}")
858
- print(f" Has 'images' key: {'Yes' if 'images' in item else 'No'}")
859
- if 'images' in item:
860
- print(f" Images value type: {type(item['images'])}")
861
- print(f" Images value: {str(item['images'])[:100]}") # Show first 100 chars
862
-
863
- # Process each item for images
864
- for item in selected_items:
865
- # Check if the item has images key
866
- if 'images' in item and item['images']:
867
- try:
868
- # Get the image URL - handle different possible formats
869
- image_url = None
870
-
871
- if isinstance(item['images'], str):
872
- # Direct URL string
873
- image_url = item['images']
874
- elif isinstance(item['images'], list) and len(item['images']) > 0:
875
- # List of URLs - take the first one
876
- image_url = item['images'][0]
877
- elif isinstance(item['images'], dict) and len(item['images']) > 0:
878
- # Dictionary of URLs - take the first value
879
- image_url = list(item['images'].values())[0]
880
-
881
- # If it's a relative URL, convert to absolute (example)
882
- if image_url and image_url.startswith('/'):
883
- # This is just an example - update with your actual domain
884
- image_url = f"https://yourdomain.com{image_url}"
885
-
886
- # Print the processed URL for debugging
887
- print(f"Processed image URL for {item['name']}: {image_url}")
888
-
889
- if image_url:
890
- # Create HTML for the image with caption
891
- item_price = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
892
-
893
- # Add a discount badge if available
894
- discount_badge = ""
895
- if item.get('has_bulk_discount', False):
896
- discount_badge = """
897
- <div style='position: absolute; top: 5px; right: 5px; background-color: #FF9800; color: white; padding: 5px; border-radius: 4px; font-size: 0.8em;'>
898
- BULK DISCOUNT
899
- </div>
900
- """
901
-
902
- html_content += f"""
903
- <div style='border: 1px solid #ddd; border-radius: 8px; padding: 10px; max-width: 250px; position: relative;'>
904
- {discount_badge}
905
- <img src="{image_url}" alt="{item['name']}" style='width: 100%; max-height: 200px; object-fit: contain;'>
906
- <p style='margin-top: 8px; font-weight: bold;'>{item['name']}</p>
907
- <p>{item_price}</p>
908
- """
909
-
910
- # Add discount info if available
911
- if item.get('formatted_discount', ''):
912
- html_content += f"""<p style='color: #FF9800; font-weight: bold;'>{item.get('formatted_discount', '')}</p>
913
- """
914
-
915
- html_content += "</div>"
916
- count += 1
917
- except Exception as e:
918
- print(f"Error processing image for {item['name']}: {str(e)}")
919
-
920
- # Close the container div
921
- html_content += "</div>"
922
-
923
- # If no images were found
924
- if count == 0:
925
- html_content = """
926
- <div>
927
- <p>No images were found for the selected items.</p>
928
- <p>Check the console logs for debugging information about the image URLs.</p>
929
- </div>
930
- """
931
- else:
932
- bundle_df = pd.DataFrame(columns=["Name", "Price (S$)", "Type", "Bulk Discount", "Description"])
933
- bundle_summary = "No items selected"
934
- html_content = "<p>No items selected.</p>"
935
 
936
- return response, bundle_df, bundle_summary, html_content
 
 
937
 
938
  # Custom CSS to match the Gift Market homepage style
939
  css = """
@@ -1011,29 +901,11 @@ h1.title {
1011
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
1012
  }
1013
 
1014
- /* Specific styling for columns in filter rows */
1015
  .filter-row .gr-column {
1016
  flex: 1;
1017
  min-width: 250px;
1018
  }
1019
 
1020
- .budget-btn {
1021
- background-color: white;
1022
- border: 1px solid var(--border-color);
1023
- color: #333;
1024
- padding: 8px 15px;
1025
- border-radius: 20px;
1026
- margin: 5px;
1027
- cursor: pointer;
1028
- transition: all 0.2s;
1029
- }
1030
-
1031
- .budget-btn:hover, .budget-btn.active {
1032
- background-color: var(--primary-color);
1033
- color: white;
1034
- border-color: var(--primary-color);
1035
- }
1036
-
1037
  .results-container {
1038
  background-color: white;
1039
  border-radius: 5px;
@@ -1049,14 +921,6 @@ h1.title {
1049
  color: #87CEEB;
1050
  }
1051
 
1052
- /* Style for checkboxes */
1053
- .checkbox-container {
1054
- display: flex;
1055
- gap: 15px;
1056
- margin: 10px 0;
1057
- }
1058
-
1059
- /* Style for the search box */
1060
  .search-box {
1061
  display: flex;
1062
  margin: 15px 0;
@@ -1078,30 +942,6 @@ h1.title {
1078
  cursor: pointer;
1079
  }
1080
 
1081
- .gr-dropdown {
1082
- min-height: 35px !important;
1083
- }
1084
-
1085
- .gr-dropdown select {
1086
- height: 35px !important;
1087
- padding: 5px !important;
1088
- }
1089
-
1090
- .gr-box {
1091
- min-height: 35px !important;
1092
- }
1093
-
1094
- .gr-radio-group {
1095
- gap: 5px !important;
1096
- margin: 2px 0 !important;
1097
- }
1098
-
1099
- select {
1100
- height: 35px !important;
1101
- padding: 5px !important;
1102
- }
1103
-
1104
- /* Responsive design for smaller screens */
1105
  @media (max-width: 768px) {
1106
  .filter-row {
1107
  flex-wrap: wrap;
@@ -1114,8 +954,7 @@ select {
1114
 
1115
  # Define the Gradio interface
1116
  with gr.Blocks(css=css, title="Gift Finder") as demo:
1117
- #Header
1118
- # Define styles separately for better organization
1119
  header_styles = {
1120
  "container": """
1121
  display: flex;
@@ -1179,348 +1018,140 @@ with gr.Blocks(css=css, title="Gift Finder") as demo:
1179
  </div>
1180
  """)
1181
 
1182
- # Main title
1183
  # Search bar
1184
  gr.HTML("""
1185
  <div class="section-header">
1186
- <span class="section-number">0</span> Describe what you're looking for*
1187
  </div>
1188
  """)
1189
 
1190
  with gr.Row(elem_classes=["search-box"]):
1191
  query = gr.Textbox(
1192
- placeholder="Example: Find me office supplies or I need premium drinkware items",
1193
- label="Requirements"
 
1194
  )
1195
 
1196
  # Budget section
1197
  gr.HTML("""
1198
  <div class="section-header">
1199
- <span class="section-number">1</span>Budget (S$): per package + total budget + items in bundle*
1200
  </div>
1201
  """)
1202
 
1203
- # Budget buttons and input
1204
  with gr.Row(elem_classes=["filter-row"]):
1205
- with gr.Column(scale=20):
1206
- package_item_count = gr.Slider(
1207
- minimum=1,
1208
- maximum=7,
1209
- value=3,
1210
- step=1,
1211
- label="Number",
1212
- info="Items/gift package"
1213
- )
1214
- with gr.Column(scale=60):
1215
  budget_range = gr.Radio(
1216
  choices=BUDGET_RANGES,
1217
- label="Budget Per Bundle Package",
1218
  value=BUDGET_RANGES[0]
1219
  )
1220
- with gr.Column(scale=20):
1221
- budget_total = gr.Number(
1222
- label="Total Budget (S$)",
1223
- minimum=10,
1224
- value="100",
1225
- info="Net of package cost"
1226
  )
1227
 
1228
- # Combined row for Age Group, Gender, and Colour Theme
1229
- with gr.Row(elem_classes=["filter-row"]):
1230
- with gr.Column(scale=2):
1231
- gr.HTML("""
1232
- <div class="section-header" style="margin-top: 0;">
1233
- <span class="section-number">2</span> Target Age Group*
1234
- </div>
1235
- """)
1236
- age_group = gr.Dropdown(
1237
- choices=["Choose an option", "<18", "18-30", "30-40", "40-50", "50-60", ">60"],
1238
- label="Age",
1239
- value="Choose an option"
1240
- )
1241
-
1242
- with gr.Column(scale=2):
1243
- gr.HTML("""
1244
- <div class="section-header" style="margin-top: 0;">
1245
- <span class="section-number">3</span> Target Gender
1246
- </div>
1247
- """)
1248
- gender = gr.Dropdown(
1249
- choices=["Choose an option", "Male", "Female", "does not really matter"],
1250
- label="Gender",
1251
- value="Choose an option"
1252
- )
1253
-
1254
- with gr.Column(scale=2):
1255
- gr.HTML("""
1256
- <div class="section-header" style="margin-top: 0;">
1257
- <span class="section-number">4</span> Colour Theme
1258
- </div>
1259
- """)
1260
- color_theme = gr.Dropdown(
1261
- choices=[
1262
- "Choose an option",
1263
- "Black", "White", "Off-White", "Brown", "Red", "Blue", "Gray",
1264
- "Gold", "Yellow", "Purple", "Pink", "Green", "Silver",
1265
- "Orange", "Multi-color", "Transparent"
1266
- ],
1267
- label="Color",
1268
- value="Choose an option"
1269
- )
1270
  # Gift Occasion section
1271
  gr.HTML("""
1272
  <div class="section-header">
1273
- <span class="section-number">5</span> Gift Occasion
1274
  </div>
1275
  """)
1276
 
1277
  with gr.Row(elem_classes=["filter-row"]):
1278
  gift_occasion = gr.Dropdown(
1279
- choices=[
1280
- "Choose an option",
1281
- "Festive Celebration",
1282
- "Long Service Award",
1283
- "Corporate Milestones",
1284
- "Onboarding",
1285
- "Christmas/Year-End Celebration",
1286
- "Annual Dinner & Dance",
1287
- "All The Best!",
1288
- "Others"
1289
- ],
1290
  label="Occasion",
1291
- value="Choose an option",
1292
- elem_classes=["gr-dropdown"]
1293
- )
1294
-
1295
- # Job Function section
1296
- gr.HTML("""
1297
- <div class="section-header">
1298
- <span class="section-number">6</span> Recipient's Job Function
1299
- </div>
1300
- """)
1301
-
1302
- with gr.Row(elem_classes=["filter-row"]):
1303
- job_function = gr.Dropdown(
1304
- choices=[
1305
- "Choose an option",
1306
- "C-Suite",
1307
- "Sales & Business Development",
1308
- "Finance",
1309
- "Operations",
1310
- "Human Resource",
1311
- "Engineering",
1312
- "Information Technology",
1313
- "Marketing & Communications",
1314
- "Others"
1315
- ],
1316
- label="Recipient",
1317
- value="Choose an option",
1318
- elem_classes=["gr-dropdown"]
1319
  )
1320
 
1321
- # Results tabs
1322
- with gr.Tabs():
1323
- with gr.TabItem("Recommendations"):
1324
- response = gr.Textbox(label="Recommendation Details", lines=15)
1325
- with gr.TabItem("Bundle Summary"):
1326
- bundle_summary = gr.Textbox(label="Bundle Statistics", lines=3)
1327
- bundle_table = gr.DataFrame(label="Selected Items")
1328
- with gr.TabItem("Bundle Pictures"):
1329
- bundle_images = gr.HTML(label="Product Images")
1330
-
1331
- # Function to determine the final budget range
1332
- def get_final_budget_range(range1, range2):
1333
- return range1 if range1 else range2
1334
 
1335
- def modified_interface(budget_range1, budget_total, package_item_count, age_group, gift_occasion, color_theme, job_function, gender, query):
 
 
 
 
 
 
 
 
 
 
1336
  """
1337
- Updated interface function to handle per-package budget range, total budget,
1338
- specific item count per package, and highlight bulk discounts
1339
  """
1340
- # Get the budget range for individual items in the package
1341
- budget_range = budget_range1 if budget_range1 else "Below S$10" # Default if nothing selected
1342
-
1343
- # Parse the budget range for individual items
1344
- min_budget, max_budget = parse_budget_range(budget_range)
1345
-
1346
- # Get the total budget - ensure it's properly converted to float
1347
- try:
1348
- total_budget = float(budget_total) if budget_total else 100.0
1349
- except (ValueError, TypeError):
1350
- total_budget = 100.0
1351
-
1352
- # Get the number of items per package - ensure it's properly converted to int
1353
  try:
1354
- item_count = int(package_item_count) if package_item_count else None
1355
  except (ValueError, TypeError):
1356
- item_count = None
1357
- # Get the number of items per package
1358
- item_count = int(package_item_count) if package_item_count else None
1359
 
1360
- # Initialize the chatbot
1361
  chatbot = GiftBundleChatbot(all_items)
1362
 
1363
- # Process the query with all filters
1364
- response, selected_items = chatbot.process_query(
1365
- query=query,
1366
  min_budget=min_budget,
1367
  max_budget=max_budget,
1368
- total_budget=total_budget,
1369
- item_count=item_count,
1370
- age_group=age_group,
1371
- gift_occasion=gift_occasion,
1372
- color_theme=color_theme,
1373
- job_function=job_function,
1374
- gender=gender
1375
  )
1376
-
1377
- # Create DataFrame for bundle display
 
 
 
 
 
 
1378
  if selected_items:
1379
- # Calculate the per-package cost
1380
- package_cost = sum(item['normalized_price'] for item in selected_items if item['normalized_price'] is not None)
1381
-
1382
- # Calculate max number of packages that fit within total budget
1383
- max_packages = int(total_budget / package_cost) if package_cost > 0 else 0
1384
- total_cost = package_cost * max_packages if max_packages > 0 else 0
1385
-
1386
- # Create dataframe with discount information highlighted
1387
- bundle_data = []
1388
  for item in selected_items:
1389
- price_display = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
1390
-
1391
- # Add discount note if available
1392
- discount_note = ""
1393
- if item.get('has_bulk_discount', False):
1394
- discount_note = "💰 BULK DISCOUNT AVAILABLE"
1395
 
1396
- # Prepare description with discount info if available
1397
- description = item.get('short_description', 'No description')[:100]
1398
- if item.get('formatted_discount', ''):
1399
- description += f"\n{item.get('formatted_discount', '')}"
1400
-
1401
- bundle_data.append({
1402
  "Name": item['name'],
1403
- "Price (S$)": price_display,
1404
- "Type": item['type'],
1405
- "Bulk Discount": discount_note,
1406
- "Description": description
 
1407
  })
1408
 
1409
- bundle_df = pd.DataFrame(bundle_data)
1410
-
1411
- budget_utilization = (total_cost / total_budget) * 100 if total_budget > 0 else 0
1412
-
1413
- bundle_summary = f"Package Cost: S${package_cost:.2f}\n" \
1414
- f"Items per Package: {len(selected_items)}\n" \
1415
- f"Number of Packages Possible: {max_packages}\n" \
1416
- f"Total Cost: S${total_cost:.2f}\n" \
1417
- f"Total Budget: S${total_budget:.2f}\n" \
1418
- f"Budget Utilization: {budget_utilization:.1f}%"
1419
-
1420
- # Create HTML for displaying images with discount badges
1421
- html_content = "<div style='display: flex; flex-wrap: wrap; gap: 20px;'>"
1422
- count = 0
1423
-
1424
- # Debug: Print image information for each item
1425
- print("Image debug information:")
1426
- for i, item in enumerate(selected_items):
1427
- print(f"Item {i+1}: {item['name']}")
1428
- print(f" Has 'images' key: {'Yes' if 'images' in item else 'No'}")
1429
- if 'images' in item:
1430
- print(f" Images value type: {type(item['images'])}")
1431
- print(f" Images value: {str(item['images'])[:100]}") # Show first 100 chars
1432
-
1433
- for item in selected_items:
1434
- # Check if the item has images key
1435
- if 'images' in item and item['images']:
1436
- try:
1437
- # Get the image URL - handle different possible formats
1438
- image_url = None
1439
-
1440
- if isinstance(item['images'], str):
1441
- # Direct URL string
1442
- image_url = item['images']
1443
- elif isinstance(item['images'], list) and len(item['images']) > 0:
1444
- # List of URLs - take the first one
1445
- image_url = item['images'][0]
1446
- elif isinstance(item['images'], dict) and len(item['images']) > 0:
1447
- # Dictionary of URLs - take the first value
1448
- image_url = list(item['images'].values())[0]
1449
-
1450
- # If it's a relative URL, convert to absolute (example)
1451
- if image_url and image_url.startswith('/'):
1452
- # This is just an example - update with your actual domain
1453
- image_url = f"https://yourdomain.com{image_url}"
1454
-
1455
- # Print the processed URL for debugging
1456
- print(f"Processed image URL for {item['name']}: {image_url}")
1457
-
1458
- if image_url:
1459
- # Create HTML for the image with caption
1460
- item_price = f"S${item['normalized_price']:.2f}" if item['normalized_price'] is not None else "N/A"
1461
-
1462
- # Add a discount badge if available
1463
- discount_badge = ""
1464
- if item.get('has_bulk_discount', False):
1465
- discount_badge = """
1466
- <div style='position: absolute; top: 5px; right: 5px; background-color: #FF9800; color: white; padding: 5px; border-radius: 4px; font-size: 0.8em;'>
1467
- BULK DISCOUNT
1468
- </div>
1469
- """
1470
-
1471
- html_content += f"""
1472
- <div style='border: 1px solid #ddd; border-radius: 8px; padding: 10px; max-width: 250px; position: relative;'>
1473
- {discount_badge}
1474
- <img src="{image_url}" alt="{item['name']}" style='width: 100%; max-height: 200px; object-fit: contain;'>
1475
- <p style='margin-top: 8px; font-weight: bold;'>{item['name']}</p>
1476
- <p>{item_price}</p>
1477
- """
1478
-
1479
- # Add discount info if available
1480
- if item.get('formatted_discount', ''):
1481
- html_content += f"""<p style='color: #FF9800; font-weight: bold;'>{item.get('formatted_discount', '')}</p>
1482
- """
1483
-
1484
- html_content += "</div>"
1485
- count += 1
1486
- except Exception as e:
1487
- print(f"Error processing image for {item['name']}: {str(e)}")
1488
-
1489
- # Close the container div
1490
- html_content += "</div>"
1491
-
1492
- # If no images were found
1493
- if count == 0:
1494
- html_content = """
1495
- <div>
1496
- <p>No images were found for the selected items.</p>
1497
- <p>Check the console logs for debugging information about the image URLs.</p>
1498
- </div>
1499
- """
1500
  else:
1501
- bundle_df = pd.DataFrame(columns=["Name", "Price (S$)", "Type", "Bulk Discount", "Description"])
1502
- bundle_summary = "No items selected"
1503
- html_content = "<p>No items selected.</p>"
1504
 
1505
- return response, bundle_df, bundle_summary, html_content
1506
-
1507
- search_btn = gr.Button("Get Recommendations (Before you press, check your inputs, no automatic input of budget for bundle package)", variant="primary")
1508
- # Search button click handler
1509
-
1510
  search_btn.click(
1511
- fn=modified_interface,
1512
- inputs=[budget_range, budget_total, package_item_count, age_group, gift_occasion, color_theme, job_function, gender, query],
1513
- outputs=[response, bundle_table, bundle_summary, bundle_images]
1514
  )
1515
- # You can also update the examples to include total_budget values
 
1516
  gr.Examples(
1517
  examples=[
1518
- ["S$10 to S$20", 100, "30-40", "Corporate Milestones", "Black", "C-Suite", "Male", "I need some premium wareable items"],
1519
- ["S$35 to S$55", 200, "30-40", "Festive Celebration", "Blue", "Marketing & Communications", "Female", "Find me clothing items suitable for corporate events"],
1520
- ["S$20 to S$35", 150, "18-30", "Long Service Award", "Silver", "Information Technology", "does not really matter", "Recommend tech gadgets"],
1521
- ["S$55 to S$80", 300, "40-50", "All The Best!", "Multi-color", "Operations", "does not really matter", "I need some travel essentials"]
 
1522
  ],
1523
- inputs=[budget_range, budget_total, age_group, gift_occasion, color_theme, job_function, gender, query]
1524
  )
1525
 
1526
  # Footer
@@ -1529,12 +1160,8 @@ with gr.Blocks(css=css, title="Gift Finder") as demo:
1529
  <p>© 2025 Gift Market. All rights reserved.</p>
1530
  </div>
1531
  """)
1532
-
1533
 
1534
  # Launch the app
1535
  if __name__ == "__main__":
1536
- # Uncommment to load real data
1537
- # all_items = load_sample_data()
1538
-
1539
  # Launch Gradio interface
1540
- demo.launch()
 
24
  with open('clothing.json', 'r') as f:
25
  clothing = json.load(f)
26
 
 
 
 
27
  # Combine both datasets and tag them with their source
28
  for item in premium_collections:
29
  item['source'] = 'premium_collections'
30
  for item in clothing:
31
  item['source'] = 'clothing'
32
 
 
 
 
 
33
  all_items = premium_collections + clothing
34
+
35
  # Function to normalize price strings to float
36
  def normalize_price(price_str):
37
  if not price_str:
 
54
  if item.get('price'):
55
  item['normalized_price'] = normalize_price(item['price'])
56
 
57
+ # Define dropdown options (simplified)
 
58
  GIFT_OCCASIONS = [
59
  "Choose an option",
60
  "Festive Celebration",
 
66
  "All The Best!",
67
  "Others"
68
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  # Budget options for the new interface
71
  BUDGET_RANGES = [
 
115
 
116
  return fits_budget, total_cost, explanation
117
 
118
+ def filter_items_by_budget(self, min_budget: float, max_budget: float) -> list:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  """
120
+ Filter all items that fall within the budget range per item.
121
+ Returns: list of items within budget
 
122
  """
123
+ valid_items = []
124
+ for item in self.items:
125
+ if item.get('normalized_price') is not None:
126
+ price = item['normalized_price']
127
+ if min_budget <= price <= max_budget:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  valid_items.append(item)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ # Sort by price (ascending)
131
+ valid_items.sort(key=lambda x: x.get('normalized_price', 0))
132
  return valid_items
133
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  class SelectionAgent:
135
  def __init__(self, items):
136
  self.items = items
137
  self.client = OpenAI(api_key=OPENAI_API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ def select_items(self, criteria: str, min_budget: float, max_budget: float,
140
+ gift_occasion: str = "", quantity: int = 1) -> List[Dict]:
141
  """
142
+ Use OpenAI to select items based on the criteria.
143
+ Returns a list of items that fit the criteria.
144
  """
145
+ # First filter items by budget
146
+ budget_agent = BudgetAgent(self.items)
147
+ budget_filtered_items = budget_agent.filter_items_by_budget(min_budget, max_budget)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ if not budget_filtered_items:
 
 
 
 
150
  return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
 
 
 
 
 
 
 
 
 
 
 
152
  # Prepare the data for OpenAI (limit the number of items to avoid token limits)
153
+ items_sample = budget_filtered_items[:100] # Take more items since we're showing all
154
 
155
  # Extract discount information for each item
156
  for item in items_sample:
 
166
  "description": item.get('short_description', '')[:100],
167
  "labels": item.get('labels', []),
168
  "has_bulk_discount": item.get('has_bulk_discount', False),
169
+ "discount_info": item.get('discount_info', ''),
170
+ "url": item.get('url', ''),
171
+ "images": item.get('images', '')
172
  } for item in items_sample])
173
 
174
+ # Create the system prompt
175
  system_prompt = """
176
+ You are a gift selection expert. Your task is to select items that best match the user's criteria.
177
+ Focus primarily on:
178
+ 1. The user's specific requirements and description (50%)
179
+ 2. Gift occasion appropriateness (30%)
180
+ 3. Value for money and bulk discount opportunities (20%)
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
 
 
 
 
182
  Return your selections as a JSON array of item names that meet the criteria.
183
+ Prioritize items with bulk discounts when quantity is high.
184
  """
 
 
 
 
 
185
 
186
+ budget_text = f"budget range of S${min_budget:.2f} to S${max_budget:.2f} per item"
 
 
187
 
188
+ # Build user prompt
 
 
 
 
189
  user_prompt = f"""
190
+ I need {quantity} items with a {budget_text} that match these criteria:
191
 
192
+ PRIMARY REQUIREMENT:
193
  {criteria}
194
 
195
+ OCCASION:
196
+ {gift_occasion if gift_occasion and gift_occasion != "Choose an option" else "General purpose"}
 
 
197
 
198
+ QUANTITY NEEDED: {quantity}
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
+ Here are the available items within my budget:
 
 
 
 
 
 
 
 
 
 
201
  {items_data}
202
 
203
+ Please select the best items that match my criteria. If quantity is high (>10), prioritize items with bulk discounts.
 
 
204
 
205
  Return a JSON object with a key called "items" that contains an array of item names, like this:
206
  {{"items": ["Item 1", "Item 2", "Item 3"]}}
 
234
  # Find the corresponding items from our pool
235
  selected_items = []
236
  for name in selected_item_names:
237
+ for item in budget_filtered_items:
238
  if name.lower() in item['name'].lower() or item['name'].lower() in name.lower():
239
  # Add discount info to the item
240
  has_discount, discount_info, formatted_discount = extract_discount_info(item)
 
246
  selected_items.append(item)
247
  break
248
 
249
+ # If no specific selection was made, return all budget-filtered items
250
+ if not selected_items:
251
+ selected_items = budget_filtered_items
252
+
 
 
 
253
  return selected_items
254
  except Exception as e:
255
  print(f"Error calling OpenAI API: {str(e)}")
256
+ # Return all budget-filtered items as fallback
257
+ return budget_filtered_items
258
 
 
259
  class GiftBundleChatbot:
260
  def __init__(self, items):
261
  self.items = items
262
  self.budget_agent = BudgetAgent(items)
263
  self.selection_agent = SelectionAgent(items)
264
 
265
+ def create_combination_options(self, items: List[Dict], quantity: int, min_budget: float, max_budget: float) -> List[Dict]:
 
 
 
266
  """
267
+ Create three different combination options from the available items.
268
+ Returns: List of combinations with different themes/strategies
269
  """
270
+ if len(items) < quantity:
271
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
272
 
273
+ combinations = []
274
+
275
+ # Strategy 1: Best Value - Mix of price ranges to maximize value
276
+ best_value_items = []
277
+ sorted_by_value = sorted(items, key=lambda x: x.get('normalized_price', 0))
278
+
279
+ # Take mix of low, mid, and high priced items
280
+ low_third = len(sorted_by_value) // 3
281
+ mid_third = 2 * len(sorted_by_value) // 3
282
+
283
+ low_items = sorted_by_value[:low_third]
284
+ mid_items = sorted_by_value[low_third:mid_third]
285
+ high_items = sorted_by_value[mid_third:]
286
+
287
+ # Create balanced selection
288
+ items_needed = quantity
289
+ while items_needed > 0 and (low_items or mid_items or high_items):
290
+ if items_needed >= 3 and low_items and mid_items and high_items:
291
+ best_value_items.extend([low_items.pop(0), mid_items.pop(0), high_items.pop(0)])
292
+ items_needed -= 3
293
+ elif items_needed >= 2 and low_items and mid_items:
294
+ best_value_items.extend([low_items.pop(0), mid_items.pop(0)])
295
+ items_needed -= 2
296
+ elif low_items:
297
+ best_value_items.append(low_items.pop(0))
298
+ items_needed -= 1
299
+ elif mid_items:
300
+ best_value_items.append(mid_items.pop(0))
301
+ items_needed -= 1
302
+ elif high_items:
303
+ best_value_items.append(high_items.pop(0))
304
+ items_needed -= 1
305
+ else:
306
+ break
307
+
308
+ if len(best_value_items) == quantity:
309
+ total_cost = sum(item['normalized_price'] for item in best_value_items if item['normalized_price'])
310
+ combinations.append({
311
+ "name": "Best Value Mix",
312
+ "description": "Balanced selection across different price ranges for maximum value",
313
+ "items": best_value_items[:quantity],
314
+ "total_cost": total_cost,
315
+ "strategy": "value"
316
+ })
317
 
318
+ # Strategy 2: Premium Selection - Higher-end items
319
+ premium_items = sorted(items, key=lambda x: x.get('normalized_price', 0), reverse=True)[:quantity]
320
+ if len(premium_items) == quantity:
321
+ total_cost = sum(item['normalized_price'] for item in premium_items if item['normalized_price'])
322
+ combinations.append({
323
+ "name": "Premium Selection",
324
+ "description": "Higher-end products for a premium gifting experience",
325
+ "items": premium_items,
326
+ "total_cost": total_cost,
327
+ "strategy": "premium"
328
+ })
329
 
330
+ # Strategy 3: Budget-Friendly or Bulk Discount Focus
331
+ if quantity > 5:
332
+ # For larger quantities, focus on bulk discount items
333
+ bulk_items = [item for item in items if item.get('has_bulk_discount', False)]
334
+ if len(bulk_items) >= quantity:
335
+ bulk_selection = bulk_items[:quantity]
336
+ total_cost = sum(item['normalized_price'] for item in bulk_selection if item['normalized_price'])
337
+ combinations.append({
338
+ "name": "Bulk Discount Special",
339
+ "description": "Items with bulk discounts - perfect for larger quantities",
340
+ "items": bulk_selection,
341
+ "total_cost": total_cost,
342
+ "strategy": "bulk"
343
+ })
344
+ else:
345
+ # Budget-friendly selection
346
+ budget_items = sorted(items, key=lambda x: x.get('normalized_price', 0))[:quantity]
347
+ total_cost = sum(item['normalized_price'] for item in budget_items if item['normalized_price'])
348
+ combinations.append({
349
+ "name": "Budget-Friendly",
350
+ "description": "Most economical selection within your budget range",
351
+ "items": budget_items,
352
+ "total_cost": total_cost,
353
+ "strategy": "budget"
354
+ })
355
+ else:
356
+ # For smaller quantities, create a curated selection
357
+ # Random sampling for variety
358
+ import random
359
+ varied_items = random.sample(items, min(quantity, len(items)))
360
+ total_cost = sum(item['normalized_price'] for item in varied_items if item['normalized_price'])
361
+ combinations.append({
362
+ "name": "Curated Selection",
363
+ "description": "Carefully selected variety for a unique gift combination",
364
+ "items": varied_items,
365
+ "total_cost": total_cost,
366
+ "strategy": "curated"
367
+ })
368
+
369
+ return combinations[:3] # Return maximum 3 combinations
370
+
371
+ def process_query(self, query: str, min_budget: float = 0, max_budget: float = 500,
372
+ quantity: int = 1, gift_occasion: str = "") -> Tuple[str, List[Dict], List[Dict]]:
373
+ """
374
+ Process a user query and return both individual products and combination options.
375
+ Returns: (response_text, all_items, combinations)
376
+ """
377
+ # Get all items within budget range
378
+ all_budget_items = self.budget_agent.filter_items_by_budget(min_budget, max_budget)
379
+
380
+ if not all_budget_items:
381
+ return f"No items found within your budget range of S${min_budget:.2f} to S${max_budget:.2f} per item.", [], []
382
+
383
+ # If there's a specific query or occasion, use AI to filter/rank
384
+ if query and query.strip() and query.lower() not in ["find me gift items", ""]:
385
+ selected_items = self.selection_agent.select_items(
386
+ criteria=query,
387
+ min_budget=min_budget,
388
+ max_budget=max_budget,
389
+ gift_occasion=gift_occasion,
390
+ quantity=quantity
391
+ )
392
+ else:
393
+ # Return all items within budget
394
+ selected_items = all_budget_items
395
+
396
+ # Create combination options
397
+ combinations = self.create_combination_options(selected_items, quantity, min_budget, max_budget)
398
+
399
+ # Calculate totals
400
+ total_items = len(selected_items)
401
+ total_cost_per_item_range = f"S${min_budget:.2f} - S${max_budget:.2f}"
402
 
403
  # Format the response
404
+ response = f"Found {total_items} products within your budget range of {total_cost_per_item_range} per item"
 
405
 
406
+ if query and query.strip():
407
+ response += f" matching: '{query}'"
 
 
 
408
 
 
 
 
 
 
 
409
  if gift_occasion and gift_occasion != "Choose an option":
410
+ response += f" for {gift_occasion}"
 
 
 
 
 
 
 
 
 
 
411
 
412
+ response += f"\nQuantity needed: {quantity}"
413
 
414
+ if combinations:
415
+ response += f"\n\n🎁 THREE CURATED COMBINATIONS:"
416
+ for i, combo in enumerate(combinations, 1):
417
+ response += f"\n{i}. {combo['name']}: S${combo['total_cost']:.2f} total"
 
418
 
419
+ response += f"\n\nView the 'Combination Options' tab to see detailed combinations, or browse all {total_items} individual products below."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
 
421
+ # Check for items with bulk discounts
422
+ discount_items = [item for item in selected_items if item.get('has_bulk_discount', False)]
423
+ if discount_items and quantity > 5:
424
+ response += f"\n\n💰 BULK DISCOUNT OPPORTUNITY: {len(discount_items)} items offer bulk discounts for larger quantities!"
 
 
425
 
426
+ return response, selected_items, combinations
 
 
 
427
 
428
  def parse_budget_range(budget_range):
429
  """Parse a budget range string into min and max values"""
 
495
 
496
  return has_discount, discount_info, formatted_info
497
 
498
+ def create_combination_display_html(combinations):
 
499
  """
500
+ Create HTML display for the three combination options
501
  """
502
+ if not combinations:
503
+ return "<p>No combination options available.</p>"
 
 
 
 
 
 
 
 
 
 
 
504
 
505
+ html_content = """
506
+ <style>
507
+ .combinations-container {
508
+ display: flex;
509
+ flex-direction: column;
510
+ gap: 30px;
511
+ padding: 20px 0;
512
+ }
513
+ .combination-card {
514
+ border: 2px solid #87CEEB;
515
+ border-radius: 12px;
516
+ padding: 20px;
517
+ background: linear-gradient(135deg, #f8fffe 0%, #f0f9ff 100%);
518
+ box-shadow: 0 4px 6px rgba(135, 206, 235, 0.1);
519
+ }
520
+ .combination-header {
521
+ display: flex;
522
+ justify-content: space-between;
523
+ align-items: center;
524
+ margin-bottom: 15px;
525
+ padding-bottom: 10px;
526
+ border-bottom: 1px solid #87CEEB;
527
+ }
528
+ .combination-title {
529
+ font-size: 20px;
530
+ font-weight: bold;
531
+ color: #2c5282;
532
+ }
533
+ .combination-cost {
534
+ font-size: 18px;
535
+ font-weight: bold;
536
+ color: #87CEEB;
537
+ background: white;
538
+ padding: 8px 15px;
539
+ border-radius: 20px;
540
+ border: 1px solid #87CEEB;
541
+ }
542
+ .combination-description {
543
+ color: #4a5568;
544
+ margin-bottom: 20px;
545
+ font-style: italic;
546
+ }
547
+ .combination-items {
548
+ display: grid;
549
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
550
+ gap: 15px;
551
+ }
552
+ .combo-item {
553
+ background: white;
554
+ border: 1px solid #e2e8f0;
555
+ border-radius: 8px;
556
+ padding: 12px;
557
+ text-align: center;
558
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
559
+ }
560
+ .combo-item-image {
561
+ width: 100%;
562
+ height: 120px;
563
+ object-fit: contain;
564
+ margin-bottom: 8px;
565
+ border-radius: 4px;
566
+ }
567
+ .combo-item-name {
568
+ font-weight: bold;
569
+ font-size: 12px;
570
+ color: #2d3748;
571
+ margin-bottom: 5px;
572
+ line-height: 1.2;
573
+ }
574
+ .combo-item-price {
575
+ color: #87CEEB;
576
+ font-weight: bold;
577
+ font-size: 14px;
578
+ }
579
+ .combo-no-image {
580
+ width: 100%;
581
+ height: 120px;
582
+ background-color: #f7fafc;
583
+ display: flex;
584
+ align-items: center;
585
+ justify-content: center;
586
+ color: #a0aec0;
587
+ border-radius: 4px;
588
+ margin-bottom: 8px;
589
+ font-size: 11px;
590
+ }
591
+ .bulk-discount-indicator {
592
+ background-color: #ff9800;
593
+ color: white;
594
+ padding: 2px 6px;
595
+ border-radius: 10px;
596
+ font-size: 10px;
597
+ margin-left: 5px;
598
+ }
599
+ .select-combination-btn {
600
+ background-color: #87CEEB;
601
+ color: white;
602
+ border: none;
603
+ padding: 10px 20px;
604
+ border-radius: 6px;
605
+ cursor: pointer;
606
+ font-weight: bold;
607
+ margin-top: 15px;
608
+ transition: background-color 0.2s;
609
+ }
610
+ .select-combination-btn:hover {
611
+ background-color: #5F9EA0;
612
+ }
613
+ </style>
614
 
615
+ <div class="combinations-container">
616
+ """
 
 
 
 
 
 
 
617
 
618
+ for i, combo in enumerate(combinations, 1):
619
+ strategy_icons = {
620
+ "value": "⚖️",
621
+ "premium": "👑",
622
+ "bulk": "💰",
623
+ "budget": "💵",
624
+ "curated": "🎯"
625
+ }
626
 
627
+ icon = strategy_icons.get(combo['strategy'], "🎁")
628
+
629
+ html_content += f"""
630
+ <div class="combination-card">
631
+ <div class="combination-header">
632
+ <div class="combination-title">{icon} Option {i}: {combo['name']}</div>
633
+ <div class="combination-cost">Total: S${combo['total_cost']:.2f}</div>
634
+ </div>
635
+ <div class="combination-description">{combo['description']}</div>
636
+ <div class="combination-items">
637
+ """
638
 
639
+ for item in combo['items']:
640
+ # Get image
641
+ image_html = ""
642
+ if 'images' in item and item['images']:
643
+ image_url = None
644
+
645
+ if isinstance(item['images'], str):
646
+ image_url = item['images']
647
+ elif isinstance(item['images'], list) and len(item['images']) > 0:
648
+ image_url = item['images'][0]
649
+ elif isinstance(item['images'], dict) and len(item['images']) > 0:
650
+ image_url = list(item['images'].values())[0]
651
+
652
+ if image_url:
653
+ if image_url.startswith('/'):
654
+ image_url = f"https://yourdomain.com{image_url}"
655
+
656
+ image_html = f'<img src="{image_url}" alt="{item["name"]}" class="combo-item-image" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">'
657
+ image_html += '<div class="combo-no-image" style="display:none;">No Image</div>'
658
+ else:
659
+ image_html = '<div class="combo-no-image">No Image</div>'
660
+ else:
661
+ image_html = '<div class="combo-no-image">No Image</div>'
662
 
663
+ # Price and bulk discount indicator
664
+ price_display = f"S${item['normalized_price']:.2f}" if item.get('normalized_price') is not None else "N/A"
665
+ bulk_indicator = ""
666
  if item.get('has_bulk_discount', False):
667
+ bulk_indicator = '<span class="bulk-discount-indicator">BULK</span>'
668
 
669
+ html_content += f"""
670
+ <div class="combo-item">
671
+ {image_html}
672
+ <div class="combo-item-name">{item['name'][:50]}{"..." if len(item['name']) > 50 else ""}</div>
673
+ <div class="combo-item-price">{price_display}{bulk_indicator}</div>
674
+ </div>
675
+ """
 
 
 
 
 
 
 
676
 
677
+ html_content += f"""
678
+ </div>
679
+ </div>
680
+ """
681
+
682
+ html_content += "</div>"
683
+
684
+ return html_content
685
+ """
686
+ Create HTML grid displaying all products with images, titles, and URLs
687
+ """
688
+ if not items:
689
+ return "<p>No products found matching your criteria.</p>"
690
+
691
+ html_content = """
692
+ <style>
693
+ .product-grid {
694
+ display: grid;
695
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
696
+ gap: 20px;
697
+ padding: 20px 0;
698
+ }
699
+ .product-card {
700
+ border: 1px solid #ddd;
701
+ border-radius: 8px;
702
+ padding: 15px;
703
+ text-align: center;
704
+ background: white;
705
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
706
+ transition: transform 0.2s, box-shadow 0.2s;
707
+ }
708
+ .product-card:hover {
709
+ transform: translateY(-2px);
710
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
711
+ }
712
+ .product-image {
713
+ width: 100%;
714
+ height: 200px;
715
+ object-fit: contain;
716
+ margin-bottom: 10px;
717
+ border-radius: 4px;
718
+ }
719
+ .product-title {
720
+ font-weight: bold;
721
+ margin: 10px 0;
722
+ color: #333;
723
+ font-size: 14px;
724
+ line-height: 1.3;
725
+ }
726
+ .product-price {
727
+ color: #87CEEB;
728
+ font-weight: bold;
729
+ font-size: 16px;
730
+ margin: 8px 0;
731
+ }
732
+ .product-url {
733
+ margin-top: 10px;
734
+ }
735
+ .product-url a {
736
+ background-color: #87CEEB;
737
+ color: white;
738
+ padding: 8px 15px;
739
+ text-decoration: none;
740
+ border-radius: 4px;
741
+ font-size: 12px;
742
+ display: inline-block;
743
+ transition: background-color 0.2s;
744
+ }
745
+ .product-url a:hover {
746
+ background-color: #5F9EA0;
747
+ }
748
+ .bulk-discount-badge {
749
+ background-color: #FF9800;
750
+ color: white;
751
+ padding: 4px 8px;
752
+ border-radius: 4px;
753
+ font-size: 10px;
754
+ margin-bottom: 5px;
755
+ display: inline-block;
756
+ }
757
+ .no-image {
758
+ width: 100%;
759
+ height: 200px;
760
+ background-color: #f5f5f5;
761
+ display: flex;
762
+ align-items: center;
763
+ justify-content: center;
764
+ color: #999;
765
+ border-radius: 4px;
766
+ margin-bottom: 10px;
767
+ }
768
+ </style>
769
+
770
+ <div class="product-grid">
771
+ """
772
+
773
+ for item in items:
774
+ # Get image URL
775
+ image_html = ""
776
+ if 'images' in item and item['images']:
777
+ image_url = None
778
+
779
+ if isinstance(item['images'], str):
780
+ image_url = item['images']
781
+ elif isinstance(item['images'], list) and len(item['images']) > 0:
782
+ image_url = item['images'][0]
783
+ elif isinstance(item['images'], dict) and len(item['images']) > 0:
784
+ image_url = list(item['images'].values())[0]
785
+
786
+ if image_url:
787
+ # Handle relative URLs if needed
788
+ if image_url.startswith('/'):
789
+ image_url = f"https://yourdomain.com{image_url}"
790
+
791
+ image_html = f'<img src="{image_url}" alt="{item["name"]}" class="product-image" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">'
792
+ image_html += '<div class="no-image" style="display:none;">No Image Available</div>'
793
+ else:
794
+ image_html = '<div class="no-image">No Image Available</div>'
795
+ else:
796
+ image_html = '<div class="no-image">No Image Available</div>'
797
 
798
+ # Get price
799
+ price_display = f"S${item['normalized_price']:.2f}" if item.get('normalized_price') is not None else "Price on request"
 
 
 
 
800
 
801
+ # Get URL
802
+ product_url = item.get('url', '#')
803
+ if not product_url or product_url == '#':
804
+ url_html = '<span style="color: #999; font-size: 12px;">URL not available</span>'
805
+ else:
806
+ url_html = f'<div class="product-url"><a href="{product_url}" target="_blank">View Product</a></div>'
807
+
808
+ # Check for bulk discount
809
+ bulk_discount_badge = ""
810
+ if item.get('has_bulk_discount', False):
811
+ bulk_discount_badge = '<div class="bulk-discount-badge">💰 BULK DISCOUNT</div>'
812
+
813
+ # Create product card
814
+ html_content += f"""
815
+ <div class="product-card">
816
+ {bulk_discount_badge}
817
+ {image_html}
818
+ <div class="product-title">{item['name']}</div>
819
+ <div class="product-price">{price_display}</div>
820
+ {url_html}
821
+ </div>
822
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
823
 
824
+ html_content += "</div>"
825
+
826
+ return html_content
827
 
828
  # Custom CSS to match the Gift Market homepage style
829
  css = """
 
901
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
902
  }
903
 
 
904
  .filter-row .gr-column {
905
  flex: 1;
906
  min-width: 250px;
907
  }
908
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
909
  .results-container {
910
  background-color: white;
911
  border-radius: 5px;
 
921
  color: #87CEEB;
922
  }
923
 
 
 
 
 
 
 
 
 
924
  .search-box {
925
  display: flex;
926
  margin: 15px 0;
 
942
  cursor: pointer;
943
  }
944
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
945
  @media (max-width: 768px) {
946
  .filter-row {
947
  flex-wrap: wrap;
 
954
 
955
  # Define the Gradio interface
956
  with gr.Blocks(css=css, title="Gift Finder") as demo:
957
+ # Header
 
958
  header_styles = {
959
  "container": """
960
  display: flex;
 
1018
  </div>
1019
  """)
1020
 
 
1021
  # Search bar
1022
  gr.HTML("""
1023
  <div class="section-header">
1024
+ <span class="section-number">1</span> Describe what you're looking for (optional)
1025
  </div>
1026
  """)
1027
 
1028
  with gr.Row(elem_classes=["search-box"]):
1029
  query = gr.Textbox(
1030
+ placeholder="Example: office supplies, premium drinkware, tech gadgets (leave empty to see all products)",
1031
+ label="Requirements",
1032
+ value=""
1033
  )
1034
 
1035
  # Budget section
1036
  gr.HTML("""
1037
  <div class="section-header">
1038
+ <span class="section-number">2</span> Budget per item (S$) + Quantity needed*
1039
  </div>
1040
  """)
1041
 
 
1042
  with gr.Row(elem_classes=["filter-row"]):
1043
+ with gr.Column(scale=3):
 
 
 
 
 
 
 
 
 
1044
  budget_range = gr.Radio(
1045
  choices=BUDGET_RANGES,
1046
+ label="Budget Per Item",
1047
  value=BUDGET_RANGES[0]
1048
  )
1049
+ with gr.Column(scale=1):
1050
+ quantity = gr.Number(
1051
+ label="Quantity",
1052
+ minimum=1,
1053
+ value=1,
1054
+ info="How many items needed"
1055
  )
1056
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1057
  # Gift Occasion section
1058
  gr.HTML("""
1059
  <div class="section-header">
1060
+ <span class="section-number">3</span> Gift Occasion (optional)
1061
  </div>
1062
  """)
1063
 
1064
  with gr.Row(elem_classes=["filter-row"]):
1065
  gift_occasion = gr.Dropdown(
1066
+ choices=GIFT_OCCASIONS,
 
 
 
 
 
 
 
 
 
 
1067
  label="Occasion",
1068
+ value="Choose an option"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1069
  )
1070
 
1071
+ # Search button
1072
+ search_btn = gr.Button("Find Products", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
1073
 
1074
+ # Results section
1075
+ with gr.Tabs():
1076
+ with gr.TabItem("🎁 Combination Options"):
1077
+ combinations_html = gr.HTML(label="Three Curated Combinations")
1078
+ with gr.TabItem("📋 All Products"):
1079
+ response = gr.Textbox(label="Search Summary", lines=3)
1080
+ products_html = gr.HTML(label="Individual Products")
1081
+ with gr.TabItem("📊 Product List"):
1082
+ products_table = gr.DataFrame(label="Product Details")
1083
+
1084
+ def find_products(budget_range_val, quantity_val, gift_occasion_val, query_val):
1085
  """
1086
+ Main function to find and display products with combination options
 
1087
  """
1088
+ # Parse budget range
1089
+ min_budget, max_budget = parse_budget_range(budget_range_val)
1090
+
1091
+ # Get quantity
 
 
 
 
 
 
 
 
 
1092
  try:
1093
+ qty = int(quantity_val) if quantity_val else 1
1094
  except (ValueError, TypeError):
1095
+ qty = 1
 
 
1096
 
1097
+ # Initialize chatbot
1098
  chatbot = GiftBundleChatbot(all_items)
1099
 
1100
+ # Process query (now returns combinations too)
1101
+ response_text, selected_items, combinations = chatbot.process_query(
1102
+ query=query_val,
1103
  min_budget=min_budget,
1104
  max_budget=max_budget,
1105
+ quantity=qty,
1106
+ gift_occasion=gift_occasion_val
 
 
 
 
 
1107
  )
1108
+
1109
+ # Create combination display
1110
+ combinations_display = create_combination_display_html(combinations)
1111
+
1112
+ # Create HTML grid for all products
1113
+ products_grid = create_product_grid_html(selected_items)
1114
+
1115
+ # Create DataFrame for table view
1116
  if selected_items:
1117
+ table_data = []
 
 
 
 
 
 
 
 
1118
  for item in selected_items:
1119
+ price_display = f"S${item['normalized_price']:.2f}" if item.get('normalized_price') is not None else "Price on request"
1120
+ url_display = item.get('url', 'Not available')
1121
+ discount_status = "Yes" if item.get('has_bulk_discount', False) else "No"
 
 
 
1122
 
1123
+ table_data.append({
 
 
 
 
 
1124
  "Name": item['name'],
1125
+ "Price": price_display,
1126
+ "Type": item.get('type', 'N/A'),
1127
+ "Bulk Discount": discount_status,
1128
+ "URL": url_display,
1129
+ "Description": item.get('short_description', 'No description')[:100] + "..."
1130
  })
1131
 
1132
+ products_df = pd.DataFrame(table_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1133
  else:
1134
+ products_df = pd.DataFrame(columns=["Name", "Price", "Type", "Bulk Discount", "URL", "Description"])
 
 
1135
 
1136
+ return combinations_display, response_text, products_grid, products_df
1137
+
1138
+ # Connect the search button
 
 
1139
  search_btn.click(
1140
+ fn=find_products,
1141
+ inputs=[budget_range, quantity, gift_occasion, query],
1142
+ outputs=[combinations_html, response, products_html, products_table]
1143
  )
1144
+
1145
+ # Examples section
1146
  gr.Examples(
1147
  examples=[
1148
+ ["S$10 to S$20", 5, "Corporate Milestones", "office supplies"],
1149
+ ["S$35 to S$55", 10, "Festive Celebration", "premium drinkware"],
1150
+ ["S$20 to S$35", 3, "Long Service Award", "tech gadgets"],
1151
+ ["S$55 to S$80", 1, "All The Best!", "luxury items"],
1152
+ ["Below S$10", 20, "Choose an option", ""] # Show all cheap items
1153
  ],
1154
+ inputs=[budget_range, quantity, gift_occasion, query]
1155
  )
1156
 
1157
  # Footer
 
1160
  <p>© 2025 Gift Market. All rights reserved.</p>
1161
  </div>
1162
  """)
 
1163
 
1164
  # Launch the app
1165
  if __name__ == "__main__":
 
 
 
1166
  # Launch Gradio interface
1167
+ demo.launch()