Jessieg777's picture
Upload folder using huggingface_hub
6267028 verified
Raw
History Blame Contribute Delete
20.1 kB
import html
import gradio as gr
from agent import ShopSmartAgent
agent = ShopSmartAgent()
CUSTOM_CSS = """
body, .gradio-container {
background: #f8fafc;
color: #111827;
}
.page-wrap {
max-width: 1200px;
margin: 0 auto;
}
.hero {
margin-bottom: 14px;
}
.hero-title {
font-size: 2.2rem;
font-weight: 800;
color: #111827;
margin-bottom: 2px;
}
.hero-subtitle {
color: #6b7280;
font-size: 1.02rem;
}
.how-it-works {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
margin: 14px 0 18px;
}
.how-card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.04);
}
.how-card-title {
font-weight: 700;
color: #111827;
margin-bottom: 6px;
}
.how-card-text {
color: #4b5563;
font-size: 0.94rem;
line-height: 1.45;
}
.filters-wrap {
margin: 6px 0 10px;
}
.results-wrap {
margin-top: 12px;
}
.featured-card, .product-card {
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 16px;
background: white;
box-shadow: 0 4px 14px rgba(0,0,0,0.06);
}
.featured-card {
border: 3px solid gold;
background: linear-gradient(135deg, #fff9e6 0%, #ffffff 100%);
margin-bottom: 16px;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 14px;
}
.product-card:hover, .featured-card:hover {
transform: translateY(-2px);
transition: all 0.18s ease;
box-shadow: 0 8px 22px rgba(0,0,0,0.08);
}
.product-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.product-image, .emoji-fallback {
width: 72px;
height: 72px;
border-radius: 10px;
object-fit: cover;
background: #f3f4f6;
}
.emoji-fallback {
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
}
.product-name {
font-size: 1.08rem;
font-weight: 700;
color: #111827;
line-height: 1.25;
}
.product-meta {
color: #4b5563;
font-size: 0.95rem;
margin-top: 2px;
}
.product-seller {
color: #6b7280;
font-size: 0.9rem;
margin-top: 4px;
}
.product-reason {
color: #374151;
margin: 10px 0;
min-height: 20px;
}
.badges-row {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.badge {
display: inline-block;
padding: 5px 10px;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 700;
white-space: nowrap;
}
.badge-blue {
background: #dbeafe;
color: #1d4ed8;
}
.badge-purple {
background: #ede9fe;
color: #6d28d9;
}
.badge-green {
background: #dcfce7;
color: #166534;
}
.badge-amber {
background: #fef3c7;
color: #92400e;
}
.risk-pill {
display: inline-block;
padding: 5px 10px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 600;
margin-bottom: 10px;
background: #e8f5e8;
color: #2d5f2d;
}
.risk-pill.medium {
background: #fff3cd;
color: #856404;
}
.risk-pill.high {
background: #f8d7da;
color: #721c24;
}
.product-link {
display: inline-block;
margin-top: 4px;
text-decoration: none;
background: #4f46e5;
color: white !important;
padding: 10px 14px;
border-radius: 10px;
font-weight: 600;
}
.summary-box {
border: 1px solid #dbeafe;
background: #eff6ff;
color: #1e3a8a;
padding: 10px 12px;
border-radius: 12px;
margin: 10px 0 14px;
}
.empty-state {
border: 1px dashed #d1d5db;
border-radius: 14px;
padding: 24px;
text-align: center;
color: #6b7280;
background: #fafafa;
}
.portfolio-note {
margin-top: 18px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 14px;
color: #374151;
line-height: 1.5;
font-size: 0.95rem;
}
.portfolio-note b,
.portfolio-note strong {
color: #111827;
}
/* --- Form field visibility fixes --- */
.gradio-container .gr-block,
.gradio-container .gr-box,
.gradio-container .gr-group,
.gradio-container .gr-panel {
background: transparent;
}
.gradio-container textarea,
.gradio-container input,
.gradio-container select {
color: #111827 !important;
background: #ffffff !important;
border: 1px solid #cbd5e1 !important;
border-radius: 12px !important;
box-shadow: none !important;
}
.gradio-container input::placeholder,
.gradio-container textarea::placeholder {
color: #6b7280 !important;
opacity: 1 !important;
}
.gradio-container .gradio-textbox,
.gradio-container .gradio-number,
.gradio-container .gradio-dropdown,
.gradio-container .gradio-slider {
background: transparent !important;
}
.gradio-container .gradio-textbox label,
.gradio-container .gradio-number label,
.gradio-container .gradio-dropdown label,
.gradio-container .gradio-slider label,
.gradio-container label {
color: #111827 !important;
font-weight: 600;
}
.gradio-container [class*="input"],
.gradio-container [class*="textarea"],
.gradio-container [class*="select"],
.gradio-container [class*="wrap"] {
background-color: transparent;
}
.gradio-container button {
border-radius: 12px !important;
}
/* ============================================
MOBILE + TABLET LAYOUT (max-width: 1024px)
============================================ */
@media (max-width: 1024px) {
.gradio-container {
padding: 10px !important;
}
.hero-title {
font-size: 1.8rem;
}
.hero-subtitle {
font-size: 0.95rem;
}
.how-it-works {
grid-template-columns: 1fr;
gap: 10px;
}
.how-card {
padding: 12px;
}
.cards-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.featured-card, .product-card {
padding: 14px;
}
.product-header {
align-items: flex-start;
}
.product-image, .emoji-fallback {
width: 64px;
height: 64px;
flex-shrink: 0;
}
.product-name {
font-size: 1rem;
}
.product-meta, .product-seller, .product-reason {
font-size: 0.92rem;
}
.product-link {
width: 100%;
text-align: center;
box-sizing: border-box;
}
}
/* ============================================
MOBILE + TABLET DARK MODE (max-width: 1024px)
============================================ */
@media (max-width: 1024px) {
body, .gradio-container {
background: #0f172a !important;
color: #f8fafc !important;
}
.how-card,
.product-card,
.portfolio-note,
.summary-box {
background: #1e293b !important;
border: 1px solid #334155 !important;
}
/* Featured card gets gold treatment on mobile/tablet */
.featured-card {
background: linear-gradient(135deg, #2d2500 0%, #1e293b 100%) !important;
border: 3px solid gold !important;
}
.hero-title,
.product-name {
color: #f8fafc !important;
-webkit-text-fill-color: #f8fafc !important;
}
.hero-subtitle,
.how-card-text,
.product-meta,
.product-seller,
.product-reason,
.portfolio-note {
color: #94a3b8 !important;
-webkit-text-fill-color: #94a3b8 !important;
}
.how-card-title {
color: #f1f5f9 !important;
-webkit-text-fill-color: #f1f5f9 !important;
}
.summary-box {
color: #93c5fd !important;
-webkit-text-fill-color: #93c5fd !important;
}
.portfolio-note b,
.portfolio-note strong {
color: #f1f5f9 !important;
-webkit-text-fill-color: #f1f5f9 !important;
}
/* Empty state fix */
.empty-state {
background: #1e293b !important;
border: 1px dashed #475569 !important;
color: #94a3b8 !important;
-webkit-text-fill-color: #94a3b8 !important;
}
.empty-state h3 {
color: #f1f5f9 !important;
-webkit-text-fill-color: #f1f5f9 !important;
}
.empty-state p,
.empty-state b {
color: #94a3b8 !important;
-webkit-text-fill-color: #94a3b8 !important;
}
/* Badge fixes */
.badge {
-webkit-text-fill-color: unset !important;
}
.badge-blue {
background: #1e40af !important;
color: #e0e7ff !important;
-webkit-text-fill-color: #e0e7ff !important;
}
.badge-purple {
background: #6d28d9 !important;
color: #ede9fe !important;
-webkit-text-fill-color: #ede9fe !important;
}
.badge-green {
background: #166534 !important;
color: #dcfce7 !important;
-webkit-text-fill-color: #dcfce7 !important;
}
.badge-amber {
background: #92400e !important;
color: #fef3c7 !important;
-webkit-text-fill-color: #fef3c7 !important;
}
/* Risk pill fixes */
.risk-pill {
background: #14532d !important;
color: #dcfce7 !important;
-webkit-text-fill-color: #dcfce7 !important;
}
.risk-pill.medium {
background: #78350f !important;
color: #fde68a !important;
-webkit-text-fill-color: #fde68a !important;
}
.risk-pill.high {
background: #7f1d1d !important;
color: #fecaca !important;
-webkit-text-fill-color: #fecaca !important;
}
/* Input fixes */
.gradio-container input,
.gradio-container textarea,
.gradio-container select {
background: #0f172a !important;
color: #f8fafc !important;
-webkit-text-fill-color: #f8fafc !important;
border: 1px solid #475569 !important;
}
.gradio-container input::placeholder,
.gradio-container textarea::placeholder {
color: #64748b !important;
-webkit-text-fill-color: #64748b !important;
}
/* Example buttons — indigo */
.gradio-container button,
.product-link {
background: linear-gradient(135deg, #6366f1, #4f46e5) !important;
color: white !important;
-webkit-text-fill-color: white !important;
}
/* Search button — green so it stands out */
.gradio-container button.primary,
.gradio-container button[variant="primary"] {
background: linear-gradient(135deg, #059669, #047857) !important;
color: white !important;
-webkit-text-fill-color: white !important;
}
}
"""
def _emoji_for_name(name: str) -> str:
s = (name or "").lower()
if any(k in s for k in ["iphone", "phone", "pixel", "galaxy s", "oneplus", "smartphone"]):
return "📱"
if any(k in s for k in ["headphone", "earbud", "airpods", "bose", "sony wh"]):
return "🎧"
if any(k in s for k in ["watch", "garmin", "fitbit"]):
return "⌚"
if any(k in s for k in ["laptop", "macbook", "notebook", "chromebook"]):
return "💻"
if any(k in s for k in ["tablet", "ipad"]):
return "📱"
if any(k in s for k in ["mask", "serum", "cleanser", "moisturizer", "tonic"]):
return "🧴"
return "🛍️"
def _safe(value, default=""):
return default if value is None else value
def _short_reason(product: dict) -> str:
reason = product.get("recommendation") or product.get("short_reason") or "Strong match for your search"
reason = str(reason).strip().split(".")[0].strip()
return html.escape(reason + ("." if reason and not reason.endswith(".") else ""))
def _risk_info(product: dict):
risk = product.get("risk", {}) or {}
level = str(risk.get("level", "Low")).strip().lower()
if level not in {"low", "medium", "high"}:
level = "low"
label = level.title()
return level, label
def _infer_category(name: str) -> str:
s = (name or "").lower()
if any(k in s for k in ["iphone", "phone", "pixel", "galaxy s", "oneplus", "smartphone"]):
return "Smartphones"
if any(k in s for k in ["headphone", "earbud", "airpods", "bose", "sony wh"]):
return "Audio"
if any(k in s for k in ["watch", "garmin", "fitbit"]):
return "Wearables"
if any(k in s for k in ["laptop", "macbook", "notebook", "chromebook"]):
return "Laptops"
if any(k in s for k in ["mask", "serum", "cleanser", "moisturizer", "tonic", "gel masque"]):
return "Beauty"
return "All"
def _badge_html(text: str, style_class: str) -> str:
return f'<span class="badge {style_class}">{html.escape(text)}</span>'
def _extra_badges(product: dict, featured: bool = False) -> str:
badges = []
price = product.get("price")
rating = product.get("rating", product.get("product_rating", 0))
reviews_count = product.get("reviews_count", 0)
if featured:
badges.append(_badge_html("Best Overall", "badge-blue"))
if isinstance(rating, (int, float)) and rating >= 4.6:
badges.append(_badge_html("Top Rated", "badge-purple"))
if isinstance(price, (int, float)) and price < 300:
badges.append(_badge_html("Best Budget", "badge-green"))
if isinstance(reviews_count, int) and reviews_count >= 1000:
badges.append(_badge_html("Popular Pick", "badge-amber"))
if not badges:
return ""
return f'<div class="badges-row">{" ".join(badges)}</div>'
def _filter_products(products: list, category: str, min_rating: float):
filtered = []
for product in products:
product_category = _infer_category(product.get("name", ""))
rating = product.get("rating", product.get("product_rating", 0)) or 0
if category and category != "All" and product_category != category:
continue
if isinstance(rating, (int, float)) and rating < min_rating:
continue
filtered.append(product)
return filtered
def build_card(product: dict, featured: bool = False) -> str:
name = html.escape(str(_safe(product.get("name"), "Unknown Product")))
price = _safe(product.get("price"), "N/A")
if isinstance(price, (int, float)):
price = f"{price:,.2f}"
rating = _safe(product.get("rating", product.get("product_rating")), "N/A")
if isinstance(rating, (int, float)):
rating = f"{rating:.1f}"
seller = html.escape(str(_safe(product.get("seller", "Unknown Seller"))))
url = html.escape(str(_safe(product.get("url"), "#")))
image_url = str(_safe(product.get("image_url"), "")).strip()
emoji = _emoji_for_name(name)
risk_class, risk_label = _risk_info(product)
reason = _short_reason(product)
badges = _extra_badges(product, featured=featured)
image_html = (
f'<img src="{html.escape(image_url)}" class="product-image" alt="{name}">'
if image_url
else f'<div class="emoji-fallback">{emoji}</div>'
)
card_class = "featured-card" if featured else "product-card"
return f"""
<div class="{card_class}">
<div class="product-header">
{image_html}
<div>
<div class="product-name">{name}</div>
<div class="product-meta">${price} • ⭐ {rating}</div>
<div class="product-seller">Seller: {seller}</div>
</div>
</div>
{badges}
<div class="product-reason">{reason}</div>
<div class="risk-pill {risk_class}">Risk: {risk_label}</div><br/>
<a href="{url}" class="product-link" target="_blank">View Product</a>
</div>
"""
def build_results(results: dict, category: str, min_rating: float) -> str:
products = results.get("products", []) or []
products = _filter_products(products, category, min_rating)
total_found = len(products)
if not products:
return """
<div class="empty-state">
<h3>No products match your filters</h3>
<p>Try lowering the minimum rating or switching the category filter to <b>All</b>.</p>
</div>
"""
best_products = [p for p in products if p.get("is_best", False)]
featured = best_products[0] if best_products else products[0]
others = [p for p in products if p is not featured][:4]
html_out = [
'<div class="results-wrap">',
f'<div class="summary-box">Found {total_found} products. We filtered out irrelevant options and highlighted the safest, best-value picks.</div>',
build_card(featured, featured=True)
]
if others:
html_out.append('<div class="cards-grid">')
html_out.extend(build_card(p, featured=False) for p in others)
html_out.append('</div>')
html_out.append("""
<div class="portfolio-note">
<b>How this project works:</b> ShopSmart searches live product results across multiple sellers, ranks listings using
price, ratings, review volume, and risk signals, and highlights the best overall option in a clean, card-based interface.
<br/><br/>
<b>Stack:</b> Python, Gradio, SerpApi Google Shopping, and LLM-assisted recommendation logic.
</div>
""")
html_out.append('</div>')
return ''.join(html_out)
def process_request(query: str, max_price: float, category: str, min_rating: float):
if not query or not query.strip():
return """
<div class="empty-state">
<h3>Enter a search</h3>
<p>Try <b>smartphone under 800</b>, <b>headphones noise cancelling</b>, or <b>best cucumber face mask</b>.</p>
</div>
"""
try:
results = agent.process_query(query.strip(), max_price)
for product in results.get("products", []):
if not product.get("recommendation") and hasattr(agent, "generate_recommendation"):
try:
product["recommendation"] = agent.generate_recommendation(
product,
product.get("is_best", False)
)
except Exception:
pass
return build_results(results, category, min_rating)
except Exception as e:
return f'<div class="empty-state"><h3>Something went wrong</h3><p>{html.escape(str(e))}</p></div>'
with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft()) as demo:
gr.HTML("""
<div class="page-wrap">
<div class="hero">
<div class="hero-title">🛒 ShopSmart</div>
<div class="hero-subtitle">Smarter, safer purchase decisions</div>
</div>
<div class="how-it-works">
<div class="how-card">
<div class="how-card-title">1. Search live products</div>
<div class="how-card-text">Pulls live shopping results across multiple sellers using Google Shopping data.</div>
</div>
<div class="how-card">
<div class="how-card-title">2. Rank for value + relevance</div>
<div class="how-card-text">Scores products using price, ratings, reviews, and query relevance.</div>
</div>
<div class="how-card">
<div class="how-card-title">3. Flag risk + recommend</div>
<div class="how-card-text">Highlights the safest, best-value pick and surfaces risk signals clearly.</div>
</div>
</div>
</div>
""")
with gr.Row():
with gr.Column(scale=3):
query_box = gr.Textbox(
label="What are you shopping for?",
placeholder="e.g. smartphone under $800, gaming laptop, wireless headphones",
lines=2,
)
with gr.Column(scale=1):
price_box = gr.Number(label="Max Price ($)", value=1000, minimum=0)
with gr.Row(elem_classes="filters-wrap"):
category_box = gr.Dropdown(
label="Category",
choices=["All", "Smartphones", "Audio", "Wearables", "Laptops", "Beauty"],
value="All"
)
rating_box = gr.Slider(
label="Minimum Rating",
minimum=0,
maximum=5,
step=0.1,
value=0
)
with gr.Row():
search_btn = gr.Button("🔍 Search Products", variant="primary")
ex1 = gr.Button("Smartphone under 800")
ex2 = gr.Button("Best power drill")
ex3 = gr.Button("Best cucumber face mask")
results_html = gr.HTML()
search_btn.click(
fn=process_request,
inputs=[query_box, price_box, category_box, rating_box],
outputs=results_html
)
query_box.submit(
fn=process_request,
inputs=[query_box, price_box, category_box, rating_box],
outputs=results_html
)
ex1.click(lambda: ["smartphone under 800", 800, "Smartphones", 4.0], outputs=[query_box, price_box, category_box, rating_box])
ex2.click(lambda: ["best power drill", 200, "All", 4.0], outputs=[query_box, price_box, category_box, rating_box])
ex3.click(lambda: ["best cucumber face mask", 100, "Beauty", 4.0], outputs=[query_box, price_box, category_box, rating_box])
if __name__ == "__main__":
demo.launch()