ravim254 commited on
Commit
ced71b8
·
verified ·
1 Parent(s): a7c31a2

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +168 -133
app.py CHANGED
@@ -1,160 +1,195 @@
1
- # app.py
 
 
2
  import pandas as pd
3
  import gradio as gr
4
- from html import escape as esc
5
- import re, json
6
-
7
- # ---------- Load & normalize ----------
8
- DF = pd.read_csv("offers_with_prices.csv")
9
- DF.columns = [c.strip().lower() for c in DF.columns]
10
-
11
- # Ensure required columns exist
12
- for col in [
13
- "country","dealer","brand","model","trim_or_variant",
14
- "starting_price","heading","offer_description",
15
- "vehicle_offer_url","vehcileoffer_url","images"
16
- ]:
17
- if col not in DF.columns:
18
- DF[col] = ""
19
-
20
- def _first_image(val) -> str:
21
- if not val:
22
- return ""
23
- s = str(val).strip().replace("&", "&")
24
- # try json list/dict
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  try:
26
  if s[:1] in "[{" or (s[:1] == '"' and s[-1:] == '"'):
27
  j = json.loads(s)
28
  if isinstance(j, list):
29
  for item in j:
30
- m = re.search(r'https?://[^\s<>()\'"]+', str(item))
31
  if m: return m.group(0)
32
  elif isinstance(j, dict):
33
  for k in ("image","image_url","url","src"):
34
  if k in j:
35
- m = re.search(r'https?://[^\s<>()\'"]+', str(j[k]))
36
  if m: return m.group(0)
37
  except Exception:
38
  pass
39
- # plain string
40
- m = re.search(r'https?://[^\s<>()\'"]+', s)
41
  if m: return m.group(0)
42
- # splitters
43
  for sep in ("|","\n"," "):
44
  if sep in s:
45
  for part in s.split(sep):
46
- m = re.search(r'https?://[^\s<>()\'"]+', part)
47
  if m: return m.group(0)
48
  return ""
49
 
50
- def _card_html(row: pd.Series) -> str:
51
- img = _first_image(row.get("images",""))
52
- heading = esc(str(row.get("heading","")) or f"{row.get('brand','')} {row.get('model','')}")
53
- offer = esc(str(row.get("offer_description","")))
54
- price = esc(str(row.get("starting_price","")))
55
- url = str(row.get("vehicle_offer_url","") or row.get("vehcileoffer_url","") or "#")
56
-
57
- country = esc(str(row.get("country","")))
58
- brand = esc(str(row.get("brand","")))
59
- dealer = esc(str(row.get("dealer","")))
60
- model = esc(str(row.get("model","")))
61
- trim = esc(str(row.get("trim_or_variant","")))
62
-
63
- chips = " ".join(
64
- f"<span class='chip'>{v}</span>"
65
- for v in [country, brand, dealer, model, trim] if v
66
- )
67
- return f"""
68
- <div class="card" onclick="window.open('{url}','_blank')">
69
- <div class="thumb"><img src="{img}" alt="{heading}" onerror="this.style.opacity=0.2"></div>
70
- <div class="body">
71
- <div class="sp">Starting Price<strong>{price or "—"}</strong></div>
72
- <div class="heading">{heading}</div>
73
- <div class="offer">{offer}</div>
74
- </div>
75
- <div class="meta">{chips}</div>
76
- </div>
77
- """
78
 
79
- def _grid_html(df: pd.DataFrame) -> str:
80
- cards = "\n".join(_card_html(r) for _, r in df.iterrows())
81
  return f"""
82
- <html>
83
- <head>
84
- <meta charset="utf-8" />
85
- <style>
86
- :root{{ --bg:#f6f7fb; --card:#ffffff; --ink:#111827; --muted:#6b7280; --line:#e5e7eb; --chip:#eef2f7; }}
87
- body{{background:var(--bg);margin:0;font-family:Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif;}}
88
- .cards{{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:18px;padding:6px;}}
89
- .card{{background:var(--card);border:1px solid var(--line);border-radius:16px;overflow:hidden;
90
- box-shadow:0 4px 12px rgba(0,0,0,.06);cursor:pointer;
91
- transition:transform .08s ease,box-shadow .2s ease;}}
92
- .card:hover{{transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,.12);}}
93
- .thumb{{height:160px;overflow:hidden;background:#000;}}
94
- .thumb img{{width:100%;height:100%;object-fit:cover;}}
95
- .body{{padding:12px;display:flex;flex-direction:column;gap:6px;}}
96
- .sp{{font-size:13px;color:var(--muted);}}
97
- .sp strong{{display:block;font-size:20px;color:var(--ink);margin-top:2px;}}
98
- .heading{{font-weight:700;color:var(--ink);font-size:15px;line-height:1.25;}}
99
- .offer{{color:var(--ink);font-size:13px;line-height:1.45;opacity:.9;
100
- display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;}}
101
- .meta{{display:flex;gap:6px;flex-wrap:wrap;padding:0 12px 12px;}}
102
- .chip{{font-size:11px;color:#374151;background:var(--chip);border-radius:999px;padding:4px 8px;}}
103
- </style>
104
- </head>
105
- <body>
106
- <div class="cards">{cards}</div>
107
- </body>
108
- </html>
109
- """
110
-
111
- # ---------- Filtering logic ----------
112
- def filter_offers(country: str, brand: str, dealer: str, limit: int):
113
- df_f = DF.copy()
114
- # normalize to str before comparing
115
- if country and country != "All":
116
- df_f = df_f[df_f["country"].astype(str) == str(country)]
117
- if brand and brand != "All":
118
- df_f = df_f[df_f["brand"].astype(str) == str(brand)]
119
- if dealer and dealer != "All":
120
- df_f = df_f[df_f["dealer"].astype(str) == str(dealer)]
121
- df_show = df_f.head(int(limit))
122
- return _grid_html(df_show)
123
-
124
- # ---------- Build Gradio UI ----------
125
- def app():
126
- countries = ["All"] + sorted(DF["country"].dropna().astype(str).unique().tolist())
127
- brands = ["All"] + sorted(DF["brand"].dropna().astype(str).unique().tolist())
128
- dealers = ["All"] + sorted(DF["dealer"].dropna().astype(str).unique().tolist())
129
-
130
- # One scrolling region (like an iframe) for the HTML
131
- custom_css = """
132
- body, .gradio-container, .gr-block, .gr-button, .gr-input, .gr-dropdown,
133
- .gradio-container * { font-family: Inter, Segoe UI, Roboto, Helvetica, Arial, sans-serif !important; }
134
- #cards-pane { height: 820px; overflow: auto; border-radius: 10px; }
135
  """
136
 
137
- with gr.Blocks(title="Vehicle Offers", css=custom_css) as demo:
138
- gr.Markdown("## 🚗 Vehicle Offers")
139
-
140
- with gr.Row():
141
- dd_country = gr.Dropdown(choices=countries, label="Country", value="All")
142
- dd_brand = gr.Dropdown(choices=brands, label="Brand", value="All")
143
- dd_dealer = gr.Dropdown(choices=dealers, label="Dealer", value="All")
144
- s_limit = gr.Slider(6, 200, value=50, step=6, label="Max Offers")
145
-
146
- out_html = gr.HTML(elem_id="cards-pane")
147
-
148
- # Render once on page load using current input values
149
- demo.load(filter_offers, [dd_country, dd_brand, dd_dealer, s_limit], out_html)
150
-
151
- # Auto-update whenever a filter changes (NO button needed)
152
- dd_country.change(filter_offers, [dd_country, dd_brand, dd_dealer, s_limit], out_html)
153
- dd_brand.change( filter_offers, [dd_country, dd_brand, dd_dealer, s_limit], out_html)
154
- dd_dealer.change( filter_offers, [dd_country, dd_brand, dd_dealer, s_limit], out_html)
155
- s_limit.change( filter_offers, [dd_country, dd_brand, dd_dealer, s_limit], out_html)
156
-
157
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  if __name__ == "__main__":
160
- app().launch()
 
1
+ from __future__ import annotations
2
+ import re, json
3
+ from pathlib import Path
4
  import pandas as pd
5
  import gradio as gr
6
+ from html import escape as _esc
7
+
8
+ DATA_PATH = Path("data/offers_with_prices.csv")
9
+
10
+ # ---------- Utilities ----------
11
+ def h(x: str) -> str:
12
+ return _esc((x or ""))
13
+
14
+ def load_csv() -> pd.DataFrame:
15
+ if not DATA_PATH.exists():
16
+ cols = ["country","dealer","brand","model","trim_or_variant","starting_price",
17
+ "heading","offer_description","images","currency","vehicle_offer_url"]
18
+ return pd.DataFrame(columns=cols)
19
+
20
+ df = pd.read_csv(DATA_PATH)
21
+ df.columns = [c.strip().lower() for c in df.columns]
22
+ alias = {
23
+ "brand_name": "brand", "make": "brand",
24
+ "model_name": "model",
25
+ "img": "images", "image_url": "images", "image": "images", "imageurl": "images",
26
+ "desc": "offer_description", "offer_text": "offer_description",
27
+ "price": "starting_price",
28
+ "offer heading": "heading", "title": "heading",
29
+ "vehcileoffer_url": "vehicle_offer_url",
30
+ }
31
+ for a,b in alias.items():
32
+ if a in df.columns and b not in df.columns:
33
+ df[b] = df[a]
34
+ for c in ["country","dealer","brand","model","trim_or_variant","starting_price",
35
+ "heading","offer_description","images","currency","vehicle_offer_url"]:
36
+ if c not in df.columns: df[c] = ""
37
+ return df
38
+
39
+ _IMG_URL_RE = re.compile(r'https?://[^\s<>()\'"]+')
40
+ def first_image(url_field) -> str:
41
+ if not url_field: return ""
42
+ s = str(url_field).strip().replace("&amp;", "&")
43
  try:
44
  if s[:1] in "[{" or (s[:1] == '"' and s[-1:] == '"'):
45
  j = json.loads(s)
46
  if isinstance(j, list):
47
  for item in j:
48
+ m = _IMG_URL_RE.search(str(item))
49
  if m: return m.group(0)
50
  elif isinstance(j, dict):
51
  for k in ("image","image_url","url","src"):
52
  if k in j:
53
+ m = _IMG_URL_RE.search(str(j[k]))
54
  if m: return m.group(0)
55
  except Exception:
56
  pass
57
+ m = _IMG_URL_RE.search(s)
 
58
  if m: return m.group(0)
 
59
  for sep in ("|","\n"," "):
60
  if sep in s:
61
  for part in s.split(sep):
62
+ m = _IMG_URL_RE.search(part)
63
  if m: return m.group(0)
64
  return ""
65
 
66
+ def price_number(s: str):
67
+ if not isinstance(s, str): return None
68
+ s = s.replace(",", "")
69
+ m = re.search(r"([\d]+(?:\.\d+)?)", s)
70
+ return float(m.group(1)) if m else None
71
+
72
+ # ---------- Card Renderer ----------
73
+ def card_html(row: pd.Series) -> str:
74
+ img = first_image(row.get("images",""))
75
+ brand, dealer, country = map(lambda x: h(str(x).strip()),
76
+ [row.get("brand",""), row.get("dealer",""), row.get("country","")])
77
+ model, trim = h(str(row.get("model","")).strip()), h(str(row.get("trim_or_variant","")).strip())
78
+ heading = h(str(row.get("heading","")).strip() or f"{brand} {model}".strip())
79
+ price = h(str(row.get("starting_price","")).strip())
80
+ offer = h(str(row.get("offer_description","")).strip()).replace("\n","<br>")
81
+ url = str(row.get("vehicle_offer_url","")).strip()
82
+ has_url = url and url.lower() != "nan"
83
+ onclick = f"onclick=\"window.open('{url}','_blank');\"" if has_url else ""
84
+ chips = "".join(f"<div class='chip'>{x}</div>" for x in [country, brand, dealer, model, trim] if x)
 
 
 
 
 
 
 
 
 
85
 
 
 
86
  return f"""
87
+ <div class="card" {onclick}>
88
+ <div class="thumb"><img src="{img}" alt="{brand} {model}" onerror="this.style.opacity=0.15"></div>
89
+ <div class="body">
90
+ <div class="sp">Starting Price<strong>{price or "—"}</strong></div>
91
+ <div class="heading">{heading}</div>
92
+ <div class="offer">{offer}</div>
93
+ </div>
94
+ <div class="meta">{chips}</div>
95
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  """
97
 
98
+ # ---------- CSS ----------
99
+ CARD_CSS = """
100
+ <style>
101
+ body, * {
102
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial !important;
103
+ }
104
+ .offers-container {
105
+ height: 850px;
106
+ overflow: auto;
107
+ background: #f6f7fb;
108
+ padding: 8px;
109
+ border-radius: 8px;
110
+ }
111
+ .cards {
112
+ display: grid;
113
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
114
+ gap: 18px;
115
+ }
116
+ .card {
117
+ background:#fff;
118
+ border:1px solid #e5e7eb;
119
+ border-radius:16px;
120
+ overflow:hidden;
121
+ box-shadow:0 4px 18px rgba(0,0,0,.05);
122
+ display:flex;
123
+ flex-direction:column;
124
+ transition: transform .08s ease, box-shadow .2s ease;
125
+ cursor:pointer;
126
+ height:100%;
127
+ }
128
+ .card:hover { box-shadow:0 10px 28px rgba(2,6,23,.12); transform:translateY(-1px); }
129
+ .thumb { background:#0b0b0b; height:180px; overflow:hidden; }
130
+ .thumb img { width:100%; height:100%; object-fit:cover; }
131
+ .body { padding:14px; display:flex; flex-direction:column; gap:6px; flex:1 1 auto; }
132
+ .sp { font-size:15px; color:#070708; letter-spacing:.02em; }
133
+ .sp strong { display:block; font-size:22px; color:#e5c100; margin-top:2px; }
134
+ .heading { font-weight:700; color:#111827; font-size:16px; margin:4px 0; line-height:1.25; }
135
+ .offer { color:#111827; font-size:14px; line-height:1.45; opacity:.9;
136
+ display:-webkit-box; -webkit-line-clamp:5; -webkit-box-orient:vertical; overflow:hidden; }
137
+ .meta { display:flex; gap:8px; flex-wrap:wrap; padding:0 14px 14px; margin-top:auto; }
138
+ .chip { font-size:13px; color:#f3eac0; background:#1e2640; border-radius:999px;
139
+ border:1px solid #e5e7eb; padding:4px 8px; }
140
+ </style>
141
+ """
142
+
143
+ # ---------- HTML Renderer ----------
144
+ def render_html(df: pd.DataFrame) -> str:
145
+ if df.empty:
146
+ return f"{CARD_CSS}<div class='offers-container'><p style='padding:10px;color:#64748b;'>No offers found.</p></div>"
147
+ cards = "".join(card_html(r) for _, r in df.iterrows())
148
+ return f"{CARD_CSS}<div class='offers-container'><div class='cards'>{cards}</div></div>"
149
+
150
+ # ---------- Filtering ----------
151
+ DF = load_csv()
152
+
153
+ def get_filters():
154
+ countries = ["All"] + sorted([x for x in DF["country"].dropna().astype(str).unique() if x])
155
+ brands = ["All"] + sorted([x for x in DF["brand"].dropna().astype(str).unique() if x])
156
+ dealers = ["All"] + sorted([x for x in DF["dealer"].dropna().astype(str).unique() if x])
157
+ return countries, brands, dealers
158
+
159
+ def update_cards(country, brand, dealer, sort_choice, limit):
160
+ df = DF.copy()
161
+ if country != "All": df = df[df["country"].astype(str) == country]
162
+ if brand != "All": df = df[df["brand"].astype(str) == brand]
163
+ if dealer != "All": df = df[df["dealer"].astype(str) == dealer]
164
+
165
+ if sort_choice != "Default":
166
+ tmp = df.apply(lambda r: price_number(r["starting_price"]), axis=1)
167
+ df = df.assign(__p=tmp)
168
+ df = df.sort_values("__p", ascending=(sort_choice=="Price (Low → High)"), na_position="last").drop(columns="__p", errors="ignore")
169
+
170
+ df = df.head(int(limit))
171
+ return render_html(df)
172
+
173
+ # ---------- Gradio UI ----------
174
+ countries, brands, dealers = get_filters()
175
+
176
+ with gr.Blocks(fill_height=True) as demo:
177
+ gr.Markdown("## 🚗 Vehicle Offers")
178
+ with gr.Row():
179
+ c = gr.Dropdown(countries, value="All", label="Country")
180
+ b = gr.Dropdown(brands, value="All", label="Brand")
181
+ d = gr.Dropdown(dealers, value="All", label="Dealer")
182
+ s = gr.Dropdown(["Default","Price (Low → High)","Price (High → Low)"], value="Default", label="Sort by")
183
+ l = gr.Slider(6, 200, 50, step=6, label="Max Cards")
184
+ out = gr.HTML()
185
+
186
+ # Auto-update and initial load
187
+ demo.load(update_cards, [c,b,d,s,l], [out])
188
+ c.change(update_cards, [c,b,d,s,l], [out])
189
+ b.change(update_cards, [c,b,d,s,l], [out])
190
+ d.change(update_cards, [c,b,d,s,l], [out])
191
+ s.change(update_cards, [c,b,d,s,l], [out])
192
+ l.change(update_cards, [c,b,d,s,l], [out])
193
 
194
  if __name__ == "__main__":
195
+ demo.launch()