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'
Try lowering the minimum rating or switching the category filter to All.
Try smartphone under 800, headphones noise cancelling, or best cucumber face mask.
{html.escape(str(e))}