Danielos100 commited on
Commit
e1a3b1d
Β·
verified Β·
1 Parent(s): c249c88

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +156 -83
app.py CHANGED
@@ -1,21 +1,30 @@
1
  # app.py
2
- # 🎁 GIfty β€” Smart Gift Recommender (English / USD)
3
- # Dataset: ckandemir/amazon-products (HF)
4
- # Baseline: TF-IDF + cosine. Optional: enable Embeddings + FAISS later.
 
 
 
 
 
 
 
 
 
5
 
6
  import os, re, random
7
- from typing import Dict, List
 
8
  import numpy as np
9
  import pandas as pd
10
- from datasets import load_dataset
11
- from sklearn.feature_extraction.text import TfidfVectorizer
12
- from sklearn.neighbors import NearestNeighbors
13
  import gradio as gr
 
 
 
14
 
15
- # ============= Config =============
16
- USE_EMBEDDINGS = False
17
- MAX_ROWS = int(os.getenv("MAX_ROWS", "5000"))
18
- DEFAULT_OCCASIONS = "birthday, thank_you, housewarming"
19
 
20
  OCCASION_OPTIONS = [
21
  "birthday", "anniversary", "valentines", "graduation",
@@ -33,10 +42,17 @@ AGE_OPTIONS = {
33
  INTEREST_OPTIONS = [
34
  "reading","writing","tech","travel","fitness","cooking","tea","coffee",
35
  "games","movies","plants","music","design","stationery","home","experience",
36
- "digital","aesthetic","premium","eco","practical","minimalist","social","party"
 
37
  ]
38
 
39
- # ============= Data loading & schema =============
 
 
 
 
 
 
40
  def _to_price_usd(x):
41
  s = str(x).strip().replace("$","").replace(",","")
42
  try: return float(s)
@@ -46,9 +62,28 @@ def _infer_age_from_category(cat: str) -> str:
46
  s = (cat or "").lower()
47
  if any(k in s for k in ["baby", "toddler", "infant"]): return "kids"
48
  if "toys & games" in s or "board games" in s or "toy" in s: return "kids"
49
- if any(k in s for k in ["teen", "ya", "young adult"]): return "teens"
50
  return "any"
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  def map_amazon_to_schema(df_raw: pd.DataFrame) -> pd.DataFrame:
53
  cols = {c.lower().strip(): c for c in df_raw.columns}
54
  get = lambda key: df_raw.get(cols.get(key, ""), "")
@@ -59,15 +94,18 @@ def map_amazon_to_schema(df_raw: pd.DataFrame) -> pd.DataFrame:
59
  "price_usd": get("selling price").map(_to_price_usd) if "selling price" in cols else np.nan,
60
  "age_range": "",
61
  "gender_tags": "any",
62
- "occasion_tags": DEFAULT_OCCASIONS,
63
  "persona_fit": get("category"),
64
  "image_url": get("image") if "image" in cols else "",
65
  })
66
- out["name"] = out["name"].astype(str).str.strip().str.slice(0,120)
67
- out["short_desc"] = out["short_desc"].astype(str).str.strip().str.slice(0,400)
 
68
  out["tags"] = out["tags"].astype(str).str.replace("|", ", ").str.lower()
69
  out["persona_fit"] = out["persona_fit"].astype(str).str.lower()
70
- out["age_range"] = out["tags"].map(_infer_age_from_category).fillna("any")
 
 
71
  return out
72
 
73
  def build_doc(row: pd.Series) -> str:
@@ -86,6 +124,7 @@ def load_catalog() -> pd.DataFrame:
86
  ds = load_dataset("ckandemir/amazon-products", split="train")
87
  raw = ds.to_pandas()
88
  except Exception:
 
89
  raw = pd.DataFrame({
90
  "Product Name": ["Wireless Earbuds", "Coffee Sampler", "Strategy Board Game"],
91
  "Description": [
@@ -105,18 +144,7 @@ def load_catalog() -> pd.DataFrame:
105
 
106
  CATALOG = load_catalog()
107
 
108
- # ============= Retrieval baseline (TF-IDF) =============
109
- _vectorizer = TfidfVectorizer(min_df=1, ngram_range=(1,2))
110
- _X = _vectorizer.fit_transform(CATALOG["doc"].fillna(""))
111
- _nn = NearestNeighbors(n_neighbors=10, metric="cosine").fit(_X)
112
-
113
- def profile_to_query(profile: Dict) -> str:
114
- interests = ", ".join(profile.get("interests", []))
115
- occasion = profile.get("occasion", "")
116
- budget = profile.get("budget_usd", "")
117
- age = profile.get("age_range", "any")
118
- return f"{interests}. occasion: {occasion}. age: {age}. budget: {budget} USD."
119
-
120
  def _contains_ci(series: pd.Series, needle: str) -> pd.Series:
121
  if not needle: return pd.Series(True, index=series.index)
122
  pat = re.escape(needle)
@@ -135,11 +163,57 @@ def filter_business(df: pd.DataFrame, budget_min=None, budget_max=None,
135
  m &= (df["age_range"].fillna("any").isin([age_range, "any"]))
136
  return df[m]
137
 
138
- def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
139
- """Global kNN β†’ filter to the business subset (fixes index mismatch)."""
140
- q = profile_to_query(profile)
141
- q_vec = _vectorizer.transform([q])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  df_f = filter_business(
144
  CATALOG,
145
  budget_min=profile.get("budget_min"),
@@ -147,51 +221,39 @@ def recommend_topk(profile: Dict, k: int=3) -> pd.DataFrame:
147
  occasion=profile.get("occasion"),
148
  age_range=profile.get("age_range","any"),
149
  )
150
- if df_f.empty:
151
- df_f = CATALOG
152
-
153
- # Search on the global index, then keep only rows inside df_f
154
- n_cand = min(max(k*50, k), len(CATALOG))
155
- dists, inds = _nn.kneighbors(q_vec, n_neighbors=n_cand)
156
- cand_global = inds[0] # indices in CATALOG
157
- d = dists[0]
158
- order = np.argsort(d) # ascending distance
159
  seen, picks = set(), []
160
- for gi in cand_global[order]:
161
- if gi not in df_f.index: # keep only filtered subset
162
  continue
163
- nm = CATALOG.loc[gi, "name"]
164
  if nm in seen:
165
  continue
166
  seen.add(nm)
167
- # similarity = 1 - distance
168
- sim = 1 - float(_nn.kneighbors_graph(q_vec, n_neighbors=1, mode="distance")[0, gi]) if False else 1.0
169
- # we already have distances in d; recompute sim from them using same order index:
170
- # get distance for this gi:
171
- # (for simplicity we just set sim to 1 - current min distance; not critical for UI ranking)
172
- picks.append((gi, None))
173
  if len(picks) >= k:
174
  break
175
 
176
  if not picks:
177
- return df_f.head(k).assign(similarity=np.nan)[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
 
 
178
 
179
- sel = [gi for gi,_ in picks]
180
- res = CATALOG.loc[sel].copy()
181
- # compute similarity from the original distances vector for display
182
- gi_to_dist = {int(gi): float(dist) for gi, dist in zip(cand_global, d)}
183
- res["similarity"] = [1.0 - gi_to_dist.get(int(gi), 0.0) for gi in sel]
184
  return res[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
185
 
186
- # ============= Synthetic item + message =============
187
  def generate_item(profile: Dict) -> Dict:
188
  interests = profile.get("interests", [])
189
- occasion = profile.get("occasion","birthday")
190
- budget = profile.get("budget_max", profile.get("budget_usd", 50)) or 50
191
- age = profile.get("age_range","any")
192
- core = (interests[0] if interests else "hobby").strip()
193
  style = random.choice(["personalized","experience","bundle"])
194
- base_name, base_desc = "", ""
195
  if style == "personalized":
196
  base_name = f"Custom {core} accessory with initials"
197
  base_desc = f"Thoughtful personalized {core} accessory tailored to their taste."
@@ -207,7 +269,7 @@ def generate_item(profile: Dict) -> Dict:
207
  base_desc += " Trendy pick that suits young enthusiasts."
208
  elif age == "senior":
209
  base_desc += " Comfortable and easy to use."
210
- price = float(np.clip(float(budget), 10, 250))
211
  return {
212
  "name": f"{base_name} ({occasion})",
213
  "short_desc": base_desc,
@@ -226,16 +288,25 @@ def generate_message(profile: Dict) -> str:
226
  f"Happy {occasion}! Wishing you health, joy, and wonderful memories. "
227
  f"May your goals come true. With {tone}.")
228
 
229
- # ============= Gradio UI (GIfty) =============
230
  EXAMPLES = [
231
- [["reading","travel","aesthetic"], "birthday", [20, 60], "Noa", "adult (18–64)", "warm and friendly"],
232
- [["coffee","home","practical"], "housewarming", [25, 45], "Daniel", "adult (18–64)", "warm"],
233
- [["tech","digital"], "birthday", [30, 120], "Omer", "teen (13–17)", "fun"],
 
234
  ]
235
 
236
- def ui_predict(interests_list: List[str], occasion: str, budget_range, recipient_name: str, age_label: str, tone: str):
 
 
 
 
 
 
 
 
237
  try:
238
- # budget_range is a tuple/list: (min, max)
239
  if isinstance(budget_range, (list, tuple)) and len(budget_range) == 2:
240
  budget_min, budget_max = float(budget_range[0]), float(budget_range[1])
241
  else:
@@ -255,30 +326,32 @@ def ui_predict(interests_list: List[str], occasion: str, budget_range, recipient
255
  "tone": tone or "warm and friendly",
256
  }
257
 
258
- recs = recommend_topk(profile, k=3)
259
  gen = generate_item(profile)
260
  msg = generate_message(profile)
261
 
262
- top3_md = recs[["name","short_desc","price_usd","age_range","similarity"]].to_markdown(index=False)
263
- gen_md = f"**{gen['name']}**\n\n{gen['short_desc']}\n\n~${gen['price_usd']:.0f}"
264
  return top3_md, gen_md, msg
265
  except Exception as e:
266
  return f":warning: Error: {e}", "", ""
267
 
268
  with gr.Blocks() as demo:
269
- gr.Markdown("# 🎁 GIfty β€” Smart Gift Recommender\n*Top-3 similar picks + 1 generated idea + personalized message*")
270
 
271
  with gr.Row():
272
  interests = gr.CheckboxGroup(
273
- label="Interests (select a few)", choices=INTEREST_OPTIONS,
274
- value=["reading","travel","aesthetic"], interactive=True
 
 
275
  )
276
  with gr.Row():
277
  occasion = gr.Dropdown(label="Occasion", choices=OCCASION_OPTIONS, value="birthday")
278
  age = gr.Dropdown(label="Age group", choices=list(AGE_OPTIONS.keys()), value="adult (18–64)")
 
279
 
280
- # Range slider for budget (two handles)
281
- budget = gr.Slider(label="Budget (USD)", minimum=5, maximum=500, step=1, value=(20, 60))
282
 
283
  with gr.Row():
284
  recipient_name = gr.Textbox(label="Recipient name", value="Noa")
@@ -286,18 +359,18 @@ with gr.Blocks() as demo:
286
 
287
  go = gr.Button("Get GIfty 🎯")
288
  out_top3 = gr.Markdown(label="Top-3 recommendations")
289
- out_gen = gr.Markdown(label="Generated item")
290
- out_msg = gr.Markdown(label="Personalized message")
291
 
292
  gr.Examples(
293
  EXAMPLES,
294
- [interests, occasion, budget, recipient_name, age, tone],
295
  label="Quick examples",
296
  )
297
 
298
  go.click(
299
  ui_predict,
300
- [interests, occasion, budget, recipient_name, age, tone],
301
  [out_top3, out_gen, out_msg]
302
  )
303
 
 
1
  # app.py
2
+ # 🎁 GIfty β€” Smart Gift Recommender (Embeddings + FAISS)
3
+ # Dataset: ckandemir/amazon-products (Hugging Face)
4
+ # UI: Gradio (English)
5
+ #
6
+ # Requirements (requirements.txt):
7
+ # gradio>=4.44.0
8
+ # datasets>=3.0.0
9
+ # pandas>=2.2.2
10
+ # numpy>=1.26.4
11
+ # sentence-transformers>=3.0.1
12
+ # faiss-cpu>=1.8.0
13
+ # tabulate>=0.9.0
14
 
15
  import os, re, random
16
+ from typing import Dict, List, Tuple
17
+
18
  import numpy as np
19
  import pandas as pd
 
 
 
20
  import gradio as gr
21
+ from datasets import load_dataset
22
+ from sentence_transformers import SentenceTransformer
23
+ import faiss
24
 
25
+ # ========================= Config =========================
26
+ MAX_ROWS = int(os.getenv("MAX_ROWS", "10000")) # cap for speed
27
+ TITLE = "# 🎁 GIfty β€” Smart Gift Recommender\n*Top-3 similar picks + 1 generated idea + personalized message*"
 
28
 
29
  OCCASION_OPTIONS = [
30
  "birthday", "anniversary", "valentines", "graduation",
 
42
  INTEREST_OPTIONS = [
43
  "reading","writing","tech","travel","fitness","cooking","tea","coffee",
44
  "games","movies","plants","music","design","stationery","home","experience",
45
+ "digital","aesthetic","premium","eco","practical","minimalist","social","party",
46
+ "photography","outdoors","pets","beauty","jewelry"
47
  ]
48
 
49
+ MODEL_CHOICES = {
50
+ "MiniLM (384d)": "sentence-transformers/all-MiniLM-L6-v2",
51
+ "MPNet (768d)": "sentence-transformers/all-mpnet-base-v2",
52
+ "E5-base (768d)": "intfloat/e5-base-v2",
53
+ }
54
+
55
+ # ========================= Data loading & schema =========================
56
  def _to_price_usd(x):
57
  s = str(x).strip().replace("$","").replace(",","")
58
  try: return float(s)
 
62
  s = (cat or "").lower()
63
  if any(k in s for k in ["baby", "toddler", "infant"]): return "kids"
64
  if "toys & games" in s or "board games" in s or "toy" in s: return "kids"
65
+ if any(k in s for k in ["teen", "young adult", "ya"]): return "teens"
66
  return "any"
67
 
68
+ def _infer_occasion_tags(cat: str) -> str:
69
+ s = (cat or "").lower()
70
+ tags = set(["birthday"]) # default
71
+ if any(k in s for k in ["home & kitchen","furniture","home dΓ©cor","home decor","garden","tools","appliance","cookware","kitchen"]):
72
+ tags.update(["housewarming","thank_you"])
73
+ if any(k in s for k in ["beauty","jewelry","watch","fragrance","cosmetic","makeup","skincare"]):
74
+ tags.update(["valentines","anniversary"])
75
+ if any(k in s for k in ["toys","board game","puzzle","kids","lego"]):
76
+ tags.update(["hanukkah","christmas"])
77
+ if any(k in s for k in ["office","stationery","notebook","pen","planner"]):
78
+ tags.update(["graduation","thank_you"])
79
+ if any(k in s for k in ["electronics","camera","audio","headphones","gaming","computer"]):
80
+ tags.update(["birthday","christmas"])
81
+ if any(k in s for k in ["book","novel","literature"]):
82
+ tags.update(["graduation","thank_you"])
83
+ if any(k in s for k in ["sports","fitness","outdoor","camping","hiking","run","yoga"]):
84
+ tags.update(["birthday"])
85
+ return ",".join(sorted(tags))
86
+
87
  def map_amazon_to_schema(df_raw: pd.DataFrame) -> pd.DataFrame:
88
  cols = {c.lower().strip(): c for c in df_raw.columns}
89
  get = lambda key: df_raw.get(cols.get(key, ""), "")
 
94
  "price_usd": get("selling price").map(_to_price_usd) if "selling price" in cols else np.nan,
95
  "age_range": "",
96
  "gender_tags": "any",
97
+ "occasion_tags": "",
98
  "persona_fit": get("category"),
99
  "image_url": get("image") if "image" in cols else "",
100
  })
101
+ # clean
102
+ out["name"] = out["name"].astype(str).str.strip().str.slice(0, 120)
103
+ out["short_desc"] = out["short_desc"].astype(str).str.strip().str.slice(0, 500)
104
  out["tags"] = out["tags"].astype(str).str.replace("|", ", ").str.lower()
105
  out["persona_fit"] = out["persona_fit"].astype(str).str.lower()
106
+ # infer occasion & age
107
+ out["occasion_tags"] = out["tags"].map(_infer_occasion_tags)
108
+ out["age_range"] = out["tags"].map(_infer_age_from_category).fillna("any")
109
  return out
110
 
111
  def build_doc(row: pd.Series) -> str:
 
124
  ds = load_dataset("ckandemir/amazon-products", split="train")
125
  raw = ds.to_pandas()
126
  except Exception:
127
+ # Fallback (keeps the app alive if internet is blocked)
128
  raw = pd.DataFrame({
129
  "Product Name": ["Wireless Earbuds", "Coffee Sampler", "Strategy Board Game"],
130
  "Description": [
 
144
 
145
  CATALOG = load_catalog()
146
 
147
+ # ========================= Business filters =========================
 
 
 
 
 
 
 
 
 
 
 
148
  def _contains_ci(series: pd.Series, needle: str) -> pd.Series:
149
  if not needle: return pd.Series(True, index=series.index)
150
  pat = re.escape(needle)
 
163
  m &= (df["age_range"].fillna("any").isin([age_range, "any"]))
164
  return df[m]
165
 
166
+ # ========================= Embeddings + FAISS =========================
167
+ class EmbeddingStore:
168
+ def __init__(self, docs: List[str]):
169
+ self.docs = docs
170
+ self.model_cache: Dict[str, SentenceTransformer] = {}
171
+ self.index_cache: Dict[str, faiss.Index] = {}
172
+ self.dim_cache: Dict[str, int] = {}
173
+
174
+ def _build(self, model_id: str):
175
+ model = SentenceTransformer(model_id)
176
+ embs = model.encode(self.docs, convert_to_numpy=True, normalize_embeddings=True)
177
+ index = faiss.IndexFlatIP(embs.shape[1]) # cosine if normalized
178
+ index.add(embs)
179
+ self.model_cache[model_id] = model
180
+ self.index_cache[model_id] = index
181
+ self.dim_cache[model_id] = embs.shape[1]
182
+
183
+ def ensure_ready(self, model_id: str):
184
+ if model_id not in self.index_cache:
185
+ self._build(model_id)
186
+
187
+ def search(self, model_id: str, query: str, topn: int) -> Tuple[np.ndarray, np.ndarray]:
188
+ self.ensure_ready(model_id)
189
+ model = self.model_cache[model_id]
190
+ index = self.index_cache[model_id]
191
+ qv = model.encode([query], convert_to_numpy=True, normalize_embeddings=True)
192
+ sims, idxs = index.search(qv, topn)
193
+ return sims[0], idxs[0]
194
+
195
+ EMB_STORE = EmbeddingStore(CATALOG["doc"].tolist())
196
 
197
+ def profile_to_query(profile: Dict) -> str:
198
+ """Weighted, doc-aligned query: focuses on interests/occasion/age used in docs."""
199
+ interests = [t.strip().lower() for t in profile.get("interests", []) if t.strip()]
200
+ interests_expanded = interests + interests + interests # weight *3
201
+ occasion = (profile.get("occasion", "") or "").lower()
202
+ age = profile.get("age_range", "any")
203
+ parts = []
204
+ if interests_expanded: parts.append(", ".join(interests_expanded))
205
+ if occasion: parts.append(occasion)
206
+ if age and age != "any": parts.append(age)
207
+ return " | ".join(parts).strip()
208
+
209
+ def recommend_topk_embeddings(profile: Dict, model_key: str, k: int=3) -> pd.DataFrame:
210
+ model_id = MODEL_CHOICES.get(model_key, list(MODEL_CHOICES.values())[0])
211
+ query = profile_to_query(profile)
212
+
213
+ # global search on whole catalog
214
+ sims, idxs = EMB_STORE.search(model_id, query, topn=min(max(k*50, k), len(CATALOG)))
215
+
216
+ # filter to business subset
217
  df_f = filter_business(
218
  CATALOG,
219
  budget_min=profile.get("budget_min"),
 
221
  occasion=profile.get("occasion"),
222
  age_range=profile.get("age_range","any"),
223
  )
224
+ if df_f.empty: df_f = CATALOG
225
+
226
+ order = np.argsort(-sims) # descending similarity
 
 
 
 
 
 
227
  seen, picks = set(), []
228
+ for gi in idxs[order]:
229
+ if gi not in df_f.index: # keep only allowed subset
230
  continue
231
+ nm = CATALOG.loc[int(gi), "name"]
232
  if nm in seen:
233
  continue
234
  seen.add(nm)
235
+ picks.append(int(gi))
 
 
 
 
 
236
  if len(picks) >= k:
237
  break
238
 
239
  if not picks:
240
+ res = df_f.head(k).copy()
241
+ res["similarity"] = np.nan
242
+ return res[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
243
 
244
+ gi_to_sim = {int(i): float(s) for i, s in zip(idxs, sims)}
245
+ res = CATALOG.loc[picks].copy()
246
+ res["similarity"] = [gi_to_sim.get(int(i), np.nan) for i in picks]
 
 
247
  return res[["name","short_desc","price_usd","occasion_tags","persona_fit","age_range","image_url","similarity"]]
248
 
249
+ # ========================= Synthetic item + message =========================
250
  def generate_item(profile: Dict) -> Dict:
251
  interests = profile.get("interests", [])
252
+ occasion = profile.get("occasion","birthday")
253
+ budget = profile.get("budget_max", profile.get("budget_usd", 50)) or 50
254
+ age = profile.get("age_range","any")
255
+ core = (interests[0] if interests else "hobby").strip() or "hobby"
256
  style = random.choice(["personalized","experience","bundle"])
 
257
  if style == "personalized":
258
  base_name = f"Custom {core} accessory with initials"
259
  base_desc = f"Thoughtful personalized {core} accessory tailored to their taste."
 
269
  base_desc += " Trendy pick that suits young enthusiasts."
270
  elif age == "senior":
271
  base_desc += " Comfortable and easy to use."
272
+ price = float(np.clip(float(budget), 10, 300))
273
  return {
274
  "name": f"{base_name} ({occasion})",
275
  "short_desc": base_desc,
 
288
  f"Happy {occasion}! Wishing you health, joy, and wonderful memories. "
289
  f"May your goals come true. With {tone}.")
290
 
291
+ # ========================= Gradio UI =========================
292
  EXAMPLES = [
293
+ [["tech","music"], "birthday", [20, 60], "Noa", "adult (18–64)", "MiniLM (384d)", "warm and friendly"],
294
+ [["home","cooking","practical"], "housewarming", [25, 45], "Daniel", "adult (18–64)", "MiniLM (384d)", "warm"],
295
+ [["games","photography"], "birthday", [30, 120], "Omer", "teen (13–17)", "MPNet (768d)", "fun"],
296
+ [["reading","design","aesthetic"], "thank_you", [15, 35], "Maya", "any", "E5-base (768d)", "friendly"],
297
  ]
298
 
299
+ def safe_markdown_table(df: pd.DataFrame) -> str:
300
+ try:
301
+ return df.to_markdown(index=False)
302
+ except Exception:
303
+ # fallback if tabulate is missing
304
+ return df.to_string(index=False)
305
+
306
+ def ui_predict(interests_list: List[str], occasion: str, budget_range, recipient_name: str,
307
+ age_label: str, model_key: str, tone: str):
308
  try:
309
+ # Parse budget range [min, max]
310
  if isinstance(budget_range, (list, tuple)) and len(budget_range) == 2:
311
  budget_min, budget_max = float(budget_range[0]), float(budget_range[1])
312
  else:
 
326
  "tone": tone or "warm and friendly",
327
  }
328
 
329
+ recs = recommend_topk_embeddings(profile, model_key, k=3)
330
  gen = generate_item(profile)
331
  msg = generate_message(profile)
332
 
333
+ top3_md = safe_markdown_table(recs[["name","short_desc","price_usd","age_range","similarity"]])
334
+ gen_md = f"**{gen['name']}**\n\n{gen['short_desc']}\n\n~${gen['price_usd']:.0f}"
335
  return top3_md, gen_md, msg
336
  except Exception as e:
337
  return f":warning: Error: {e}", "", ""
338
 
339
  with gr.Blocks() as demo:
340
+ gr.Markdown(TITLE)
341
 
342
  with gr.Row():
343
  interests = gr.CheckboxGroup(
344
+ label="Interests (select a few)",
345
+ choices=INTEREST_OPTIONS,
346
+ value=["tech","music"],
347
+ interactive=True
348
  )
349
  with gr.Row():
350
  occasion = gr.Dropdown(label="Occasion", choices=OCCASION_OPTIONS, value="birthday")
351
  age = gr.Dropdown(label="Age group", choices=list(AGE_OPTIONS.keys()), value="adult (18–64)")
352
+ model = gr.Dropdown(label="Embedding model", choices=list(MODEL_CHOICES.keys()), value="MiniLM (384d)")
353
 
354
+ budget = gr.RangeSlider(label="Budget range (USD)", minimum=5, maximum=500, step=1, value=[20, 60])
 
355
 
356
  with gr.Row():
357
  recipient_name = gr.Textbox(label="Recipient name", value="Noa")
 
359
 
360
  go = gr.Button("Get GIfty 🎯")
361
  out_top3 = gr.Markdown(label="Top-3 recommendations")
362
+ out_gen = gr.Markdown(label="Generated item")
363
+ out_msg = gr.Markdown(label="Personalized message")
364
 
365
  gr.Examples(
366
  EXAMPLES,
367
+ [interests, occasion, budget, recipient_name, age, model, tone],
368
  label="Quick examples",
369
  )
370
 
371
  go.click(
372
  ui_predict,
373
+ [interests, occasion, budget, recipient_name, age, model, tone],
374
  [out_top3, out_gen, out_msg]
375
  )
376