aibyml's picture
Upload app.py
acdfab9 verified
import json
import os
import gradio as gr
import pandas as pd
import re
from openai import OpenAI
import requests
import sys
from typing import List, Dict, Any, Tuple
import base64
# Add this function at the top of your file
def get_image_base64(image_path):
with open(image_path, "rb") as img_file:
return base64.b64encode(img_file.read()).decode()
# Get the base64 string for your logo
logo_base64 = get_image_base64("Logo.png")
# Load the JSON data
with open('premium_collections.json', 'r') as f:
premium_collections = json.load(f)
with open('clothing.json', 'r') as f:
clothing = json.load(f)
# Combine both datasets and tag them with their source
for item in premium_collections:
item['source'] = 'premium_collections'
for item in clothing:
item['source'] = 'clothing'
all_items = premium_collections + clothing
# Function to normalize price strings to float
def normalize_price(price_str):
if not price_str:
return None
# Handle ranges like "$8.50 – $28.00"
if '–' in price_str or '-' in price_str:
parts = re.split(r'–|-', price_str)
# Take the lower price for calculation
price_str = parts[0].strip()
# Extract the numeric value
match = re.search(r'(\d+\.\d+|\d+)', price_str)
if match:
return float(match.group(1))
return None
# Process items to have normalized prices
for item in all_items:
if item.get('price'):
item['normalized_price'] = normalize_price(item['price'])
# Define dropdown options (simplified)
GIFT_OCCASIONS = [
"Choose an option",
"Festive Celebration",
"Long Service Award",
"Corporate Milestones",
"Onboarding",
"Christmas/Year-End Celebration",
"Annual Dinner & Dance",
"All The Best!",
"Others"
]
# Budget options for the new interface
BUDGET_RANGES = [
"Below S$10",
"S$10 to S$20",
"S$20 to S$35",
"S$35 to S$55",
"S$55 to S$80"
]
# Configure API keys
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OLLAMA_API_URL = "http://localhost:11434/api/generate" # Default Ollama URL
class BudgetAgent:
def __init__(self, items, model="deepseek-r1:32b"):
self.items = items
self.model = model
def calculate_bundle(self, min_budget: float, max_budget: float, selected_items: list) -> tuple:
"""
Calculate if the selected items fit within the budget range.
Returns: (fits_budget, total_cost, explanation)
"""
# Filter out items without valid prices
valid_items = [item for item in selected_items if item.get('normalized_price') is not None]
if not valid_items:
return False, 0, "No items with valid prices were selected."
total_cost = sum(item['normalized_price'] for item in valid_items)
# Check if total fits within budget range
fits_budget = min_budget <= total_cost <= max_budget
# Create explanation
item_details = [f"{item['name']} (S${item['normalized_price']:.2f})" for item in valid_items]
explanation = f"Total cost: S${total_cost:.2f} for items: {', '.join(item_details)}. "
if fits_budget:
explanation += f"This bundle is within your budget range of S${min_budget:.2f} to S${max_budget:.2f}."
else:
if total_cost < min_budget:
explanation += f"This bundle is below your minimum budget of S${min_budget:.2f} by S${min_budget - total_cost:.2f}."
else:
explanation += f"This bundle exceeds your maximum budget of S${max_budget:.2f} by S${total_cost - max_budget:.2f}."
return fits_budget, total_cost, explanation
def filter_items_by_budget(self, min_budget: float, max_budget: float) -> list:
"""
Filter all items that fall within the budget range per item.
Returns: list of items within budget
"""
valid_items = []
for item in self.items:
if item.get('normalized_price') is not None:
price = item['normalized_price']
if min_budget <= price <= max_budget:
valid_items.append(item)
# Sort by price (ascending)
valid_items.sort(key=lambda x: x.get('normalized_price', 0))
return valid_items
class SelectionAgent:
def __init__(self, items):
self.items = items
self.client = OpenAI(api_key=OPENAI_API_KEY)
def select_items(self, criteria: str, min_budget: float, max_budget: float,
gift_occasion: str = "", quantity: int = 1) -> List[Dict]:
"""
Use OpenAI to select items based on the criteria.
Returns a list of items that fit the criteria.
"""
# First filter items by budget
budget_agent = BudgetAgent(self.items)
budget_filtered_items = budget_agent.filter_items_by_budget(min_budget, max_budget)
if not budget_filtered_items:
return []
# Prepare the data for OpenAI (limit the number of items to avoid token limits)
items_sample = budget_filtered_items[:100] # Take more items since we're showing all
# Extract discount information for each item
for item in items_sample:
has_discount, discount_info, _ = extract_discount_info(item)
if has_discount:
item['has_bulk_discount'] = True
item['discount_info'] = discount_info
items_data = json.dumps([{
"name": item['name'],
"type": item['type'],
"price": item.get('normalized_price'),
"description": item.get('short_description', '')[:100],
"labels": item.get('labels', []),
"has_bulk_discount": item.get('has_bulk_discount', False),
"discount_info": item.get('discount_info', ''),
"url": item.get('url', ''),
"images": item.get('images', '')
} for item in items_sample])
# Create the system prompt
system_prompt = """
You are a gift selection expert. Your task is to select items that best match the user's criteria.
Focus primarily on:
1. The user's specific requirements and description (50%)
2. Gift occasion appropriateness (30%)
3. Value for money and bulk discount opportunities (20%)
Return your selections as a JSON array of item names that meet the criteria.
Prioritize items with bulk discounts when quantity is high.
"""
budget_text = f"budget range of S${min_budget:.2f} to S${max_budget:.2f} per item"
# Build user prompt
user_prompt = f"""
I need {quantity} items with a {budget_text} that match these criteria:
PRIMARY REQUIREMENT:
{criteria}
OCCASION:
{gift_occasion if gift_occasion and gift_occasion != "Choose an option" else "General purpose"}
QUANTITY NEEDED: {quantity}
Here are the available items within my budget:
{items_data}
Please select the best items that match my criteria. If quantity is high (>10), prioritize items with bulk discounts.
Return a JSON object with a key called "items" that contains an array of item names, like this:
{{"items": ["Item 1", "Item 2", "Item 3"]}}
"""
try:
response = self.client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
response_format={"type": "json_object"},
temperature=0.1
)
# Extract the selected item names
result = json.loads(response.choices[0].message.content)
selected_item_names = result.get("items", [])
if not isinstance(selected_item_names, list):
# Try to handle different response formats
if isinstance(result, list):
selected_item_names = result
else:
# Look for any list in the response
for value in result.values():
if isinstance(value, list):
selected_item_names = value
break
# Find the corresponding items from our pool
selected_items = []
for name in selected_item_names:
for item in budget_filtered_items:
if name.lower() in item['name'].lower() or item['name'].lower() in name.lower():
# Add discount info to the item
has_discount, discount_info, formatted_discount = extract_discount_info(item)
if has_discount:
item['has_bulk_discount'] = True
item['discount_info'] = discount_info
item['formatted_discount'] = formatted_discount
selected_items.append(item)
break
# If no specific selection was made, return all budget-filtered items
if not selected_items:
selected_items = budget_filtered_items
return selected_items
except Exception as e:
print(f"Error calling OpenAI API: {str(e)}")
# Return all budget-filtered items as fallback
return budget_filtered_items
class GiftBundleChatbot:
def __init__(self, items):
self.items = items
self.budget_agent = BudgetAgent(items)
self.selection_agent = SelectionAgent(items)
def create_combination_options(self, items: List[Dict], quantity: int, min_budget: float, max_budget: float) -> List[Dict]:
"""
Create three different combination options from the available items.
Returns: List of combinations with different themes/strategies
"""
if len(items) < quantity:
return []
combinations = []
# Strategy 1: Best Value - Mix of price ranges to maximize value
best_value_items = []
sorted_by_value = sorted(items, key=lambda x: x.get('normalized_price', 0))
# Take mix of low, mid, and high priced items
low_third = len(sorted_by_value) // 3
mid_third = 2 * len(sorted_by_value) // 3
low_items = sorted_by_value[:low_third]
mid_items = sorted_by_value[low_third:mid_third]
high_items = sorted_by_value[mid_third:]
# Create balanced selection
items_needed = quantity
while items_needed > 0 and (low_items or mid_items or high_items):
if items_needed >= 3 and low_items and mid_items and high_items:
best_value_items.extend([low_items.pop(0), mid_items.pop(0), high_items.pop(0)])
items_needed -= 3
elif items_needed >= 2 and low_items and mid_items:
best_value_items.extend([low_items.pop(0), mid_items.pop(0)])
items_needed -= 2
elif low_items:
best_value_items.append(low_items.pop(0))
items_needed -= 1
elif mid_items:
best_value_items.append(mid_items.pop(0))
items_needed -= 1
elif high_items:
best_value_items.append(high_items.pop(0))
items_needed -= 1
else:
break
if len(best_value_items) == quantity:
total_cost = sum(item['normalized_price'] for item in best_value_items if item['normalized_price'])
combinations.append({
"name": "Best Value Mix",
"description": "Balanced selection across different price ranges for maximum value",
"items": best_value_items[:quantity],
"total_cost": total_cost,
"strategy": "value"
})
# Strategy 2: Premium Selection - Higher-end items
premium_items = sorted(items, key=lambda x: x.get('normalized_price', 0), reverse=True)[:quantity]
if len(premium_items) == quantity:
total_cost = sum(item['normalized_price'] for item in premium_items if item['normalized_price'])
combinations.append({
"name": "Premium Selection",
"description": "Higher-end products for a premium gifting experience",
"items": premium_items,
"total_cost": total_cost,
"strategy": "premium"
})
# Strategy 3: Budget-Friendly or Bulk Discount Focus
if quantity > 5:
# For larger quantities, focus on bulk discount items
bulk_items = [item for item in items if item.get('has_bulk_discount', False)]
if len(bulk_items) >= quantity:
bulk_selection = bulk_items[:quantity]
total_cost = sum(item['normalized_price'] for item in bulk_selection if item['normalized_price'])
combinations.append({
"name": "Bulk Discount Special",
"description": "Items with bulk discounts - perfect for larger quantities",
"items": bulk_selection,
"total_cost": total_cost,
"strategy": "bulk"
})
else:
# Budget-friendly selection
budget_items = sorted(items, key=lambda x: x.get('normalized_price', 0))[:quantity]
total_cost = sum(item['normalized_price'] for item in budget_items if item['normalized_price'])
combinations.append({
"name": "Budget-Friendly",
"description": "Most economical selection within your budget range",
"items": budget_items,
"total_cost": total_cost,
"strategy": "budget"
})
else:
# For smaller quantities, create a curated selection
# Random sampling for variety
import random
varied_items = random.sample(items, min(quantity, len(items)))
total_cost = sum(item['normalized_price'] for item in varied_items if item['normalized_price'])
combinations.append({
"name": "Curated Selection",
"description": "Carefully selected variety for a unique gift combination",
"items": varied_items,
"total_cost": total_cost,
"strategy": "curated"
})
return combinations[:3] # Return maximum 3 combinations
def process_query(self, query: str, min_budget: float = 0, max_budget: float = 500,
quantity: int = 1, gift_occasion: str = "") -> Tuple[str, List[Dict], List[Dict]]:
"""
Process a user query and return both individual products and combination options.
Returns: (response_text, all_items, combinations)
"""
# Get all items within budget range
all_budget_items = self.budget_agent.filter_items_by_budget(min_budget, max_budget)
if not all_budget_items:
return f"No items found within your budget range of S${min_budget:.2f} to S${max_budget:.2f} per item.", [], []
# If there's a specific query or occasion, use AI to filter/rank
if query and query.strip() and query.lower() not in ["find me gift items", ""]:
selected_items = self.selection_agent.select_items(
criteria=query,
min_budget=min_budget,
max_budget=max_budget,
gift_occasion=gift_occasion,
quantity=quantity
)
else:
# Return all items within budget
selected_items = all_budget_items
# Create combination options
combinations = self.create_combination_options(selected_items, quantity, min_budget, max_budget)
# Calculate totals
total_items = len(selected_items)
total_cost_per_item_range = f"S${min_budget:.2f} - S${max_budget:.2f}"
# Format the response
response = f"Found {total_items} products within your budget range of {total_cost_per_item_range} per item"
if query and query.strip():
response += f" matching: '{query}'"
if gift_occasion and gift_occasion != "Choose an option":
response += f" for {gift_occasion}"
response += f"\nQuantity needed: {quantity}"
if combinations:
response += f"\n\n🎁 THREE CURATED COMBINATIONS:"
for i, combo in enumerate(combinations, 1):
response += f"\n{i}. {combo['name']}: S${combo['total_cost']:.2f} total"
response += f"\n\nView the 'Combination Options' tab to see detailed combinations, or browse all {total_items} individual products below."
# Check for items with bulk discounts
discount_items = [item for item in selected_items if item.get('has_bulk_discount', False)]
if discount_items and quantity > 5:
response += f"\n\nπŸ’° BULK DISCOUNT OPPORTUNITY: {len(discount_items)} items offer bulk discounts for larger quantities!"
return response, selected_items, combinations
def parse_budget_range(budget_range):
"""Parse a budget range string into min and max values"""
if budget_range == "Below S$10":
return 0, 10
elif budget_range == "S$10 to S$20":
return 10, 20
elif budget_range == "S$20 to S$35":
return 20, 35
elif budget_range == "S$35 to S$55":
return 35, 55
elif budget_range == "S$55 to S$80":
return 55, 80
else:
# Default range if no match
return 0, 500
def extract_discount_info(item):
"""
Extract bulk discount information from item description.
Returns: (has_discount, discount_info, formatted_info)
"""
has_discount = False
discount_info = None
formatted_info = ""
# Check if the item has a description
description = item.get('short_description', '') or item.get('description', '')
if not description:
return has_discount, discount_info, formatted_info
# Keywords that might indicate a bulk discount
discount_keywords = [
'bulk discount', 'volume discount', 'quantity discount',
'bulk pricing', 'buy more save more', 'discount for quantities',
'bulk purchase', 'special pricing', 'wholesale price',
'bulk orders', 'quantity pricing', 'discount for bulk'
]
description_lower = description.lower()
# Check for discount keywords
for keyword in discount_keywords:
if keyword in description_lower:
has_discount = True
break
if has_discount:
# Try to extract sentences containing discount information
sentences = description.split('.')
discount_sentences = []
for sentence in sentences:
sentence = sentence.strip()
sentence_lower = sentence.lower()
for keyword in discount_keywords:
if keyword in sentence_lower and sentence:
discount_sentences.append(sentence)
break
if discount_sentences:
discount_info = '. '.join(discount_sentences) + '.'
formatted_info = f"<strong>Bulk Discount:</strong> {discount_info}"
else:
# If we can't extract specific sentences, use entire description
discount_info = description
formatted_info = f"<strong>Bulk Discount Available</strong> (see description for details)"
return has_discount, discount_info, formatted_info
def create_combination_display_html(combinations):
"""
Create HTML display for the three combination options
"""
if not combinations:
return "<p>No combination options available.</p>"
html_content = """
<style>
.combinations-container {
display: flex;
flex-direction: column;
gap: 30px;
padding: 20px 0;
}
.combination-card {
border: 2px solid #87CEEB;
border-radius: 12px;
padding: 20px;
background: linear-gradient(135deg, #f8fffe 0%, #f0f9ff 100%);
box-shadow: 0 4px 6px rgba(135, 206, 235, 0.1);
}
.combination-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #87CEEB;
}
.combination-title {
font-size: 20px;
font-weight: bold;
color: #2c5282;
}
.combination-cost {
font-size: 18px;
font-weight: bold;
color: #87CEEB;
background: white;
padding: 8px 15px;
border-radius: 20px;
border: 1px solid #87CEEB;
}
.combination-description {
color: #4a5568;
margin-bottom: 20px;
font-style: italic;
}
.combination-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.combo-item {
background: white;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.combo-item-image {
width: 100%;
height: 120px;
object-fit: contain;
margin-bottom: 8px;
border-radius: 4px;
}
.combo-item-name {
font-weight: bold;
font-size: 12px;
color: #2d3748;
margin-bottom: 5px;
line-height: 1.2;
}
.combo-item-price {
color: #87CEEB;
font-weight: bold;
font-size: 14px;
}
.combo-no-image {
width: 100%;
height: 120px;
background-color: #f7fafc;
display: flex;
align-items: center;
justify-content: center;
color: #a0aec0;
border-radius: 4px;
margin-bottom: 8px;
font-size: 11px;
}
.bulk-discount-indicator {
background-color: #ff9800;
color: white;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
margin-left: 5px;
}
.select-combination-btn {
background-color: #87CEEB;
color: white;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
margin-top: 15px;
transition: background-color 0.2s;
}
.select-combination-btn:hover {
background-color: #5F9EA0;
}
</style>
<div class="combinations-container">
"""
for i, combo in enumerate(combinations, 1):
strategy_icons = {
"value": "βš–οΈ",
"premium": "πŸ‘‘",
"bulk": "πŸ’°",
"budget": "πŸ’΅",
"curated": "🎯"
}
icon = strategy_icons.get(combo['strategy'], "🎁")
html_content += f"""
<div class="combination-card">
<div class="combination-header">
<div class="combination-title">{icon} Option {i}: {combo['name']}</div>
<div class="combination-cost">Total: S${combo['total_cost']:.2f}</div>
</div>
<div class="combination-description">{combo['description']}</div>
<div class="combination-items">
"""
for item in combo['items']:
# Get image
image_html = ""
if 'images' in item and item['images']:
image_url = None
if isinstance(item['images'], str):
image_url = item['images']
elif isinstance(item['images'], list) and len(item['images']) > 0:
image_url = item['images'][0]
elif isinstance(item['images'], dict) and len(item['images']) > 0:
image_url = list(item['images'].values())[0]
if image_url:
if image_url.startswith('/'):
image_url = f"https://yourdomain.com{image_url}"
image_html = f'<img src="{image_url}" alt="{item["name"]}" class="combo-item-image" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">'
image_html += '<div class="combo-no-image" style="display:none;">No Image</div>'
else:
image_html = '<div class="combo-no-image">No Image</div>'
else:
image_html = '<div class="combo-no-image">No Image</div>'
# Price and bulk discount indicator
price_display = f"S${item['normalized_price']:.2f}" if item.get('normalized_price') is not None else "N/A"
bulk_indicator = ""
if item.get('has_bulk_discount', False):
bulk_indicator = '<span class="bulk-discount-indicator">BULK</span>'
html_content += f"""
<div class="combo-item">
{image_html}
<div class="combo-item-name">{item['name'][:50]}{"..." if len(item['name']) > 50 else ""}</div>
<div class="combo-item-price">{price_display}{bulk_indicator}</div>
</div>
"""
html_content += f"""
</div>
</div>
"""
html_content += "</div>"
return html_content
def create_product_grid_html(items):
"""
Create HTML grid displaying all products with images, titles, and URLs
"""
if not items:
return "<p>No products found matching your criteria.</p>"
html_content = """
<style>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px 0;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
text-align: center;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.product-image {
width: 100%;
height: 200px;
object-fit: contain;
margin-bottom: 10px;
border-radius: 4px;
}
.product-title {
font-weight: bold;
margin: 10px 0;
color: #333;
font-size: 14px;
line-height: 1.3;
}
.product-price {
color: #87CEEB;
font-weight: bold;
font-size: 16px;
margin: 8px 0;
}
.product-url {
margin-top: 10px;
}
.product-url a {
background-color: #87CEEB;
color: white;
padding: 8px 15px;
text-decoration: none;
border-radius: 4px;
font-size: 12px;
display: inline-block;
transition: background-color 0.2s;
}
.product-url a:hover {
background-color: #5F9EA0;
}
.bulk-discount-badge {
background-color: #FF9800;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
margin-bottom: 5px;
display: inline-block;
}
.no-image {
width: 100%;
height: 200px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #999;
border-radius: 4px;
margin-bottom: 10px;
}
</style>
<div class="product-grid">
"""
for item in items:
# Get image URL
image_html = ""
if 'images' in item and item['images']:
image_url = None
if isinstance(item['images'], str):
image_url = item['images']
elif isinstance(item['images'], list) and len(item['images']) > 0:
image_url = item['images'][0]
elif isinstance(item['images'], dict) and len(item['images']) > 0:
image_url = list(item['images'].values())[0]
if image_url:
# Handle relative URLs if needed
if image_url.startswith('/'):
image_url = f"https://yourdomain.com{image_url}"
image_html = f'<img src="{image_url}" alt="{item["name"]}" class="product-image" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">'
image_html += '<div class="no-image" style="display:none;">No Image Available</div>'
else:
image_html = '<div class="no-image">No Image Available</div>'
else:
image_html = '<div class="no-image">No Image Available</div>'
# Get price
price_display = f"S${item['normalized_price']:.2f}" if item.get('normalized_price') is not None else "Price on request"
# Get URL
product_url = item.get('url', '#')
if not product_url or product_url == '#':
url_html = '<span style="color: #999; font-size: 12px;">URL not available</span>'
else:
url_html = f'<div class="product-url"><a href="{product_url}" target="_blank">View Product</a></div>'
# Check for bulk discount
bulk_discount_badge = ""
if item.get('has_bulk_discount', False):
bulk_discount_badge = '<div class="bulk-discount-badge">πŸ’° BULK DISCOUNT AVAILABLE</div>'
# Create product card
html_content += f"""
<div class="product-card">
{bulk_discount_badge}
{image_html}
<div class="product-title">{item['name']}</div>
<div class="product-price">{price_display}</div>
{url_html}
</div>
"""
html_content += "</div>"
return html_content
"""
Create HTML grid displaying all products with images, titles, and URLs
"""
if not items:
return "<p>No products found matching your criteria.</p>"
html_content = """
<style>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px 0;
}
.product-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
text-align: center;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.product-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.product-image {
width: 100%;
height: 200px;
object-fit: contain;
margin-bottom: 10px;
border-radius: 4px;
}
.product-title {
font-weight: bold;
margin: 10px 0;
color: #333;
font-size: 14px;
line-height: 1.3;
}
.product-price {
color: #87CEEB;
font-weight: bold;
font-size: 16px;
margin: 8px 0;
}
.product-url {
margin-top: 10px;
}
.product-url a {
background-color: #87CEEB;
color: white;
padding: 8px 15px;
text-decoration: none;
border-radius: 4px;
font-size: 12px;
display: inline-block;
transition: background-color 0.2s;
}
.product-url a:hover {
background-color: #5F9EA0;
}
.bulk-discount-badge {
background-color: #FF9800;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
margin-bottom: 5px;
display: inline-block;
}
.no-image {
width: 100%;
height: 200px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
color: #999;
border-radius: 4px;
margin-bottom: 10px;
}
</style>
<div class="product-grid">
"""
for item in items:
# Get image URL
image_html = ""
if 'images' in item and item['images']:
image_url = None
if isinstance(item['images'], str):
image_url = item['images']
elif isinstance(item['images'], list) and len(item['images']) > 0:
image_url = item['images'][0]
elif isinstance(item['images'], dict) and len(item['images']) > 0:
image_url = list(item['images'].values())[0]
if image_url:
# Handle relative URLs if needed
if image_url.startswith('/'):
image_url = f"https://yourdomain.com{image_url}"
image_html = f'<img src="{image_url}" alt="{item["name"]}" class="product-image" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">'
image_html += '<div class="no-image" style="display:none;">No Image Available</div>'
else:
image_html = '<div class="no-image">No Image Available</div>'
else:
image_html = '<div class="no-image">No Image Available</div>'
# Get price
price_display = f"S${item['normalized_price']:.2f}" if item.get('normalized_price') is not None else "Price on request"
# Get URL
product_url = item.get('url', '#')
if not product_url or product_url == '#':
url_html = '<span style="color: #999; font-size: 12px;">URL not available</span>'
else:
url_html = f'<div class="product-url"><a href="{product_url}" target="_blank">View Product</a></div>'
# Check for bulk discount
bulk_discount_badge = ""
if item.get('has_bulk_discount', False):
bulk_discount_badge = '<div class="bulk-discount-badge">πŸ’° BULK DISCOUNT</div>'
# Create product card
html_content += f"""
<div class="product-card">
{bulk_discount_badge}
{image_html}
<div class="product-title">{item['name']}</div>
<div class="product-price">{price_display}</div>
{url_html}
</div>
"""
html_content += "</div>"
return html_content
# Custom CSS to match the Gift Market homepage style
css = """
:root {
--primary-color: #87CEEB;
--secondary-color: #3C3B6E;
--background-color: #f0f2f5;
--border-color: #ddd;
}
body {
font-family: 'Arial', sans-serif;
background-color: var(--background-color);
}
.main-container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background-color: white;
padding: 10px 0;
border-bottom: 1px solid var(--border-color);
}
h1.title {
color: var(--primary-color);
font-weight: bold;
font-size: 2.5em;
margin: 0;
padding: 10px 0;
}
.section-header {
background-color: var(--background-color);
padding: 8px;
margin-top: 10px;
border-radius: 5px;
font-size: 1.2em;
color: #333;
font-weight: bold;
}
.section-number {
display: inline-block;
width: 24px;
height: 24px;
background-color: var(--secondary-color);
color: white;
border-radius: 50%;
text-align: center;
margin-right: 10px;
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
}
.btn-primary:hover {
background-color: #8f1c2a;
border-color: #8f1c2a;
}
.filter-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 10px;
margin-bottom: 8px;
padding: 6px;
background-color: white;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.filter-row .gr-column {
flex: 1;
min-width: 250px;
}
.results-container {
background-color: white;
border-radius: 5px;
padding: 15px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.footer {
text-align: center;
padding: 20px 0;
margin-top: 30px;
font-size: 0.9em;
color: #87CEEB;
}
.search-box {
display: flex;
margin: 15px 0;
}
.search-box input {
flex-grow: 1;
padding: 8px 15px;
border: 1px solid var(--border-color);
border-radius: 4px 0 0 4px;
}
.search-box button {
background-color: var(--secondary-color);
color: white;
border: none;
padding: 8px 15px;
border-radius: 0 4px 4px 0;
cursor: pointer;
}
@media (max-width: 768px) {
.filter-row {
flex-wrap: wrap;
}
.filter-row .gr-column {
min-width: 100%;
}
}
"""
# Define the Gradio interface
with gr.Blocks(css=css, title="Gift Finder") as demo:
# Header
header_styles = {
"container": """
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
width: 100%;
flex-wrap: wrap;
gap: 20px;
""",
"logo_section": """
display: flex;
align-items: center;
gap: 15px;
""",
"logo": """
height: 50px;
width: auto;
object-fit: contain;
""",
"title": """
color: #87CEEB;
font-weight: bold;
margin: 0;
font-size: clamp(1.5rem, 2vw, 2rem);
""",
"nav": """
display: flex;
gap: 20px;
align-items: center;
""",
"nav_item": """
display: flex;
align-items: center;
cursor: pointer;
"""
}
with gr.Row(elem_classes=["header"]):
gr.HTML(f"""
<div style="{header_styles['container']}">
<div style="{header_styles['logo_section']}">
<img src="data:image/png;base64,{logo_base64}"
alt="PrintNGift Logo"
style="{header_styles['logo']}">
<div>
<h1 style="{header_styles['title']}">
Your Gift Finder
</h1>
</div>
</div>
<nav style="{header_styles['nav']}">
<div style="{header_styles['nav_item']}">
<span style="font-weight: bold; margin-right: 10px;">Shop</span>
<span style="font-size: 0.8rem;">β–Ό</span>
</div>
<div style="{header_styles['nav_item']}">
<span style="font-weight: bold;">My Enquiry (0)</span>
</div>
</nav>
</div>
""")
# Search bar
gr.HTML("""
<div class="section-header">
<span class="section-number">1</span> Describe what you're looking for (optional)
</div>
""")
with gr.Row(elem_classes=["search-box"]):
query = gr.Textbox(
placeholder="Example: office supplies, premium drinkware, tech gadgets (leave empty to see all products)",
label="Requirements",
value=""
)
# Budget section
gr.HTML("""
<div class="section-header">
<span class="section-number">2</span> Budget per item (S$) + Quantity needed*
</div>
""")
with gr.Row(elem_classes=["filter-row"]):
with gr.Column(scale=3):
budget_range = gr.Radio(
choices=BUDGET_RANGES,
label="Budget Per Item",
value=BUDGET_RANGES[0]
)
with gr.Column(scale=1):
quantity = gr.Number(
label="Quantity",
minimum=1,
value=1,
info="How many items needed"
)
# Gift Occasion section
gr.HTML("""
<div class="section-header">
<span class="section-number">3</span> Gift Occasion (optional)
</div>
""")
with gr.Row(elem_classes=["filter-row"]):
gift_occasion = gr.Dropdown(
choices=GIFT_OCCASIONS,
label="Occasion",
value="Choose an option"
)
# Search button
search_btn = gr.Button("Find Products", variant="primary")
# Results section
with gr.Tabs():
with gr.TabItem("🎁 Combination Options"):
combinations_html = gr.HTML(label="Three Curated Combinations")
with gr.TabItem("πŸ“‹ All Products"):
response = gr.Textbox(label="Search Summary", lines=3)
products_html = gr.HTML(label="Individual Products")
with gr.TabItem("πŸ“Š Product List"):
products_table = gr.DataFrame(label="Product Details")
def find_products(budget_range_val, quantity_val, gift_occasion_val, query_val):
"""
Main function to find and display products with combination options
"""
# Parse budget range
min_budget, max_budget = parse_budget_range(budget_range_val)
# Get quantity
try:
qty = int(quantity_val) if quantity_val else 1
except (ValueError, TypeError):
qty = 1
# Initialize chatbot
chatbot = GiftBundleChatbot(all_items)
# Process query (now returns combinations too)
response_text, selected_items, combinations = chatbot.process_query(
query=query_val,
min_budget=min_budget,
max_budget=max_budget,
quantity=qty,
gift_occasion=gift_occasion_val
)
# Create combination display
combinations_display = create_combination_display_html(combinations)
# Create HTML grid for all products
products_grid = create_product_grid_html(selected_items)
# Create DataFrame for table view
if selected_items:
table_data = []
for item in selected_items:
price_display = f"S${item['normalized_price']:.2f}" if item.get('normalized_price') is not None else "Price on request"
url_display = item.get('url', 'Not available')
discount_status = "Yes" if item.get('has_bulk_discount', False) else "No"
table_data.append({
"Name": item['name'],
"Price": price_display,
"Type": item.get('type', 'N/A'),
"Bulk Discount": discount_status,
"URL": url_display,
"Description": item.get('short_description', 'No description')[:100] + "..."
})
products_df = pd.DataFrame(table_data)
else:
products_df = pd.DataFrame(columns=["Name", "Price", "Type", "Bulk Discount", "URL", "Description"])
return combinations_display, response_text, products_grid, products_df
# Connect the search button
search_btn.click(
fn=find_products,
inputs=[budget_range, quantity, gift_occasion, query],
outputs=[combinations_html, response, products_html, products_table]
)
# Examples section
gr.Examples(
examples=[
["S$10 to S$20", 5, "Corporate Milestones", "office supplies"],
["S$35 to S$55", 10, "Festive Celebration", "premium drinkware"],
["S$20 to S$35", 3, "Long Service Award", "tech gadgets"],
["S$55 to S$80", 1, "All The Best!", "luxury items"],
["Below S$10", 20, "Choose an option", ""] # Show all cheap items
],
inputs=[budget_range, quantity, gift_occasion, query]
)
# Footer
gr.HTML("""
<div class="footer">
<p>Β© 2025 Gift Market. All rights reserved.</p>
</div>
""")
# Launch the app
if __name__ == "__main__":
# Launch Gradio interface
demo.launch()