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'{html.escape(text)}' 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'
{" ".join(badges)}
' 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'{name}' if image_url else f'
{emoji}
' ) card_class = "featured-card" if featured else "product-card" return f"""
{image_html}
{name}
${price} โ€ข โญ {rating}
Seller: {seller}
{badges}
{reason}
Risk: {risk_label}

View Product
""" 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 """

No products match your filters

Try lowering the minimum rating or switching the category filter to All.

""" 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 = [ '
', f'
Found {total_found} products. We filtered out irrelevant options and highlighted the safest, best-value picks.
', build_card(featured, featured=True) ] if others: html_out.append('
') html_out.extend(build_card(p, featured=False) for p in others) html_out.append('
') html_out.append("""
How this project works: 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.

Stack: Python, Gradio, SerpApi Google Shopping, and LLM-assisted recommendation logic.
""") html_out.append('
') 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 """

Enter a search

Try smartphone under 800, headphones noise cancelling, or best cucumber face mask.

""" 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'

Something went wrong

{html.escape(str(e))}

' with gr.Blocks(css=CUSTOM_CSS, theme=gr.themes.Soft()) as demo: gr.HTML("""
๐Ÿ›’ ShopSmart
Smarter, safer purchase decisions
1. Search live products
Pulls live shopping results across multiple sellers using Google Shopping data.
2. Rank for value + relevance
Scores products using price, ratings, reviews, and query relevance.
3. Flag risk + recommend
Highlights the safest, best-value pick and surfaces risk signals clearly.
""") 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()