Spaces:
Sleeping
Sleeping
| # app.py | |
| import pandas as pd | |
| import gradio as gr | |
| from html import escape as esc | |
| import re, json | |
| # ---------- Load & normalize ---------- | |
| DF = pd.read_csv("offers_with_prices.csv") | |
| DF.columns = [c.strip().lower() for c in DF.columns] | |
| # Ensure required columns exist | |
| for col in [ | |
| "country","dealer","brand","model","trim_or_variant", | |
| "starting_price","heading","offer_description", | |
| "vehicle_offer_url","vehcileoffer_url","images" | |
| ]: | |
| if col not in DF.columns: | |
| DF[col] = "" | |
| def _first_image(val) -> str: | |
| if not val: | |
| return "" | |
| s = str(val).strip().replace("&", "&") | |
| try: | |
| if s[:1] in "[{" or (s[:1] == '"' and s[-1:] == '"'): | |
| j = json.loads(s) | |
| if isinstance(j, list): | |
| for item in j: | |
| m = re.search(r'https?://[^\s<>()\'"]+', str(item)) | |
| if m: return m.group(0) | |
| elif isinstance(j, dict): | |
| for k in ("image","image_url","url","src"): | |
| if k in j: | |
| m = re.search(r'https?://[^\s<>()\'"]+', str(j[k])) | |
| if m: return m.group(0) | |
| except Exception: | |
| pass | |
| m = re.search(r'https?://[^\s<>()\'"]+', s) | |
| if m: return m.group(0) | |
| for sep in ("|","\n"," "): | |
| if sep in s: | |
| for part in s.split(sep): | |
| m = re.search(r'https?://[^\s<>()\'"]+', part) | |
| if m: return m.group(0) | |
| return "" | |
| def _card_html(row: pd.Series) -> str: | |
| img = _first_image(row.get("images","")) | |
| heading = esc(str(row.get("heading","")) or f"{row.get('brand','')} {row.get('model','')}") | |
| offer = esc(str(row.get("offer_description",""))) | |
| price = esc(str(row.get("starting_price",""))) | |
| url = str(row.get("vehicle_offer_url","") or row.get("vehcileoffer_url","") or "#") | |
| country = esc(str(row.get("country",""))) | |
| brand = esc(str(row.get("brand",""))) | |
| dealer = esc(str(row.get("dealer",""))) | |
| model = esc(str(row.get("model",""))) | |
| trim = esc(str(row.get("trim_or_variant",""))) | |
| chips = " ".join( | |
| f"<span class='chip'>{v}</span>" | |
| for v in [country, brand, dealer, model, trim] if v | |
| ) | |
| return f""" | |
| <div class="card" onclick="window.open('{url}','_blank')"> | |
| <div class="thumb"><img src="{img}" alt="{heading}" onerror="this.style.opacity=0.2"></div> | |
| <div class="body"> | |
| <div class="sp">Starting Price<strong>{price or "—"}</strong></div> | |
| <div class="heading">{heading}</div> | |
| <div class="offer">{offer}</div> | |
| </div> | |
| <div class="meta">{chips}</div> | |
| </div> | |
| """ | |
| def _grid_html(df: pd.DataFrame) -> str: | |
| cards = "\n".join(_card_html(r) for _, r in df.iterrows()) | |
| return f""" | |
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <style> | |
| :root{{ --bg:#f6f7fb; --card:#ffffff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb; --chip:#eef2f7; }} | |
| body{{background:var(--bg);margin:0;font-family:Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif;}} | |
| .cards{{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:18px;padding:6px;}} | |
| .card{{background:var(--card);border:1px solid var(--line);border-radius:16px;overflow:hidden; | |
| box-shadow:0 4px 12px rgba(0,0,0,.06);cursor:pointer; | |
| transition:transform .08s ease,box-shadow .2s ease;}} | |
| .card:hover{{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.12);}} | |
| .thumb{{height:160px;overflow:hidden;background:#000;}} | |
| .thumb img{{width:100%;height:100%;object-fit:cover;}} | |
| .body{{padding:12px;display:flex;flex-direction:column;gap:6px;}} | |
| .sp{{font-size:13px;color:var(--muted);}} | |
| .sp strong{{display:block;font-size:20px;color:var(--ink);margin-top:2px;}} | |
| .heading{{font-weight:700;color:var(--ink);font-size:15px;line-height:1.25;}} | |
| .offer{{color:var(--ink);font-size:13px;line-height:1.45;opacity:.9; | |
| display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;}} | |
| .meta{{display:flex;gap:6px;flex-wrap:wrap;padding:0 12px 12px;}} | |
| .chip{{font-size:11px;color:#374151;background:var(--chip);border-radius:999px;padding:4px 8px;}} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="cards">{cards}</div> | |
| </body> | |
| </html> | |
| """ | |
| # ---------- Options helpers ---------- | |
| def _opts_with_all(series: pd.Series): | |
| vals = [x for x in series.dropna().astype(str).unique() if str(x).strip()] | |
| return ["All"] + sorted(vals) | |
| def _brand_and_dealer_options_for_country(country: str): | |
| df_c = DF if country == "All" else DF[DF["country"].astype(str) == str(country)] | |
| return _opts_with_all(df_c["brand"]), _opts_with_all(df_c["dealer"]) | |
| def _dealer_options_for_country_brand(country: str, brand: str): | |
| df_c = DF if country == "All" else DF[DF["country"].astype(str) == str(country)] | |
| df_cb = df_c if brand == "All" else df_c[df_c["brand"].astype(str) == str(brand)] | |
| return _opts_with_all(df_cb["dealer"]) | |
| def _brand_options_for_country_dealer(country: str, dealer: str): | |
| df_c = DF if country == "All" else DF[DF["country"].astype(str) == str(country)] | |
| df_cd = df_c if dealer == "All" else df_c[df_c["dealer"].astype(str) == str(dealer)] | |
| return _opts_with_all(df_cd["brand"]) | |
| # ---------- Filtering ---------- | |
| def filter_offers(country: str, brand: str, dealer: str, limit: int): | |
| df_f = DF.copy() | |
| if country and country != "All": | |
| df_f = df_f[df_f["country"].astype(str) == str(country)] | |
| if brand and brand != "All": | |
| df_f = df_f[df_f["brand"].astype(str) == str(brand)] | |
| if dealer and dealer != "All": | |
| df_f = df_f[df_f["dealer"].astype(str) == str(dealer)] | |
| df_show = df_f.head(int(limit)) | |
| return _grid_html(df_show) | |
| # ---------- Gradio event handlers ---------- | |
| def init_on_load(country, brand, dealer, limit): | |
| brand_opts, dealer_opts = _brand_and_dealer_options_for_country(country) | |
| brand_val = brand if brand in brand_opts else "All" | |
| dealer_val = dealer if dealer in dealer_opts else "All" | |
| html = filter_offers(country, brand_val, dealer_val, limit) | |
| return ( | |
| gr.update(choices=brand_opts, value=brand_val), | |
| gr.update(choices=dealer_opts, value=dealer_val), | |
| html | |
| ) | |
| def on_country_change(country, brand, dealer, limit): | |
| brand_opts, dealer_opts = _brand_and_dealer_options_for_country(country) | |
| brand_val = brand if brand in brand_opts else "All" | |
| dealer_val = dealer if dealer in dealer_opts else "All" | |
| html = filter_offers(country, brand_val, dealer_val, limit) | |
| return ( | |
| gr.update(choices=brand_opts, value=brand_val), | |
| gr.update(choices=dealer_opts, value=dealer_val), | |
| html | |
| ) | |
| def on_brand_change(country, brand, dealer, limit): | |
| # Country + Brand -> restrict Dealer | |
| dealer_opts = _dealer_options_for_country_brand(country, brand) | |
| dealer_val = dealer if dealer in dealer_opts else "All" | |
| html = filter_offers(country, brand, dealer_val, limit) | |
| return ( | |
| gr.update(choices=dealer_opts, value=dealer_val), | |
| html | |
| ) | |
| def on_dealer_change(country, brand, dealer, limit): | |
| # Country + Dealer -> restrict Brand (fix for your Saudi/Altawkilat => only GMC) | |
| brand_opts = _brand_options_for_country_dealer(country, dealer) | |
| brand_val = brand if brand in brand_opts else "All" | |
| html = filter_offers(country, brand_val, dealer, limit) | |
| return ( | |
| gr.update(choices=brand_opts, value=brand_val), | |
| html | |
| ) | |
| def on_limit_change(country, brand, dealer, limit): | |
| return filter_offers(country, brand, dealer, limit) | |
| # ---------- Build Gradio UI ---------- | |
| def app(): | |
| countries = _opts_with_all(DF["country"]) | |
| brands, dealers = _brand_and_dealer_options_for_country("All") | |
| custom_css = """ | |
| body, .gradio-container, .gr-block, .gr-button, .gr-input, .gr-dropdown, | |
| .gradio-container * { font-family: Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif !important; } | |
| #cards-pane { height: 820px; overflow: auto; border-radius: 10px; } | |
| """ | |
| with gr.Blocks(title="Vehicle Offers", css=custom_css) as demo: | |
| gr.Markdown("## 🚗 Vehicle Offers") | |
| with gr.Row(): | |
| dd_country = gr.Dropdown(choices=countries, label="Country", value="All") | |
| dd_brand = gr.Dropdown(choices=brands, label="Brand", value="All") | |
| dd_dealer = gr.Dropdown(choices=dealers, label="Dealer", value="All") | |
| s_limit = gr.Slider(6, 200, value=50, step=6, label="Max Offers") | |
| out_html = gr.HTML(elem_id="cards-pane") | |
| # Initialize on load | |
| demo.load( | |
| init_on_load, | |
| [dd_country, dd_brand, dd_dealer, s_limit], | |
| [dd_brand, dd_dealer, out_html] | |
| ) | |
| # Country -> update Brand + Dealer options + grid | |
| dd_country.change( | |
| on_country_change, | |
| [dd_country, dd_brand, dd_dealer, s_limit], | |
| [dd_brand, dd_dealer, out_html] | |
| ) | |
| # Brand -> update Dealer options + grid | |
| dd_brand.change( | |
| on_brand_change, | |
| [dd_country, dd_brand, dd_dealer, s_limit], | |
| [dd_dealer, out_html] | |
| ) | |
| # Dealer -> update Brand options + grid (new) | |
| dd_dealer.change( | |
| on_dealer_change, | |
| [dd_country, dd_brand, dd_dealer, s_limit], | |
| [dd_brand, out_html] | |
| ) | |
| # Limit -> update grid | |
| s_limit.change( | |
| on_limit_change, | |
| [dd_country, dd_brand, dd_dealer, s_limit], | |
| out_html | |
| ) | |
| return demo | |
| if __name__ == "__main__": | |
| app().launch() | |