Spaces:
Sleeping
Sleeping
| # ================================================================ | |
| # ProSync AI β The Event Producer's Command Center | |
| # Gradio application for Hugging Face Spaces | |
| # | |
| # Data source : HF Dataset repo eliel2003/events (vendors file) | |
| # Embed model : sentence-transformers/all-MiniLM-L6-v2 | |
| # Scoring : 60% semantic similarity + 40% composite quality | |
| # ================================================================ | |
| import spaces # required by HF GPU Space infrastructure β do not remove | |
| import os | |
| import io | |
| import json | |
| import warnings | |
| import numpy as np | |
| import pandas as pd | |
| import torch | |
| import gradio as gr | |
| from sentence_transformers import SentenceTransformer, util as st_util | |
| warnings.filterwarnings("ignore") | |
| os.environ["CUDA_VISIBLE_DEVICES"] = "" | |
| os.environ["TOKENIZERS_PARALLELISM"] = "false" | |
| # Required by HF GPU Space infrastructure β satisfies the | |
| # "@spaces.GPU function detected" startup check. | |
| def _gpu_stub(): | |
| pass | |
| # ββ Configuration βββββββββββββββββββββββββββββββββββββββββββββ | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "") | |
| HF_DATASET = "eliel2003/events" | |
| EMBED_MODEL_ID = "sentence-transformers/all-MiniLM-L6-v2" | |
| # ββ Domain constants (match notebook exactly) βββββββββββββββββ | |
| ALLOC_RATIOS = { | |
| "Catering": 0.304, | |
| "Venue": 0.228, | |
| "AV_Technology": 0.175, | |
| "Entertainment": 0.104, | |
| "Photography_Video": 0.076, | |
| "Logistics": 0.057, | |
| "Security": 0.057, | |
| } | |
| VENDOR_CATEGORIES = sorted(ALLOC_RATIOS.keys()) | |
| CATEGORY_EMOJI = { | |
| "Catering": "π½οΈ", | |
| "AV_Technology": "π¬", | |
| "Venue": "ποΈ", | |
| "Security": "π‘οΈ", | |
| "Photography_Video": "π·", | |
| "Entertainment": "π", | |
| "Logistics": "π", | |
| } | |
| CITIES = [ | |
| "Beer Sheva", "Haifa", "Herzliya", "Jerusalem", | |
| "Netanya", "Petah Tikva", "Ramat Gan", "Tel Aviv", | |
| ] | |
| SEASONS = ["Winter", "Spring", "Summer", "Fall"] | |
| EVENT_TYPES = [ | |
| "Annual Conference", "Award Ceremony", "Bar/Bat Mitzvah", | |
| "Brand Activation", "Corporate Gala", "Family Reunion", | |
| "Investor Day", "Private Birthday", "Product Launch", | |
| "Team Building", "Tech Summit", "Trade Show", | |
| "Wedding", "Workshop Series", | |
| ] | |
| QUICK_STARTERS = [ | |
| { | |
| "label": "ποΈ Tech Summit Β· Tel Aviv", | |
| "brief": "Large-scale tech summit β advanced AV, LED walls, live streaming, " | |
| "kosher catering for 400 guests, VIP executive security.", | |
| "city": "Tel Aviv", "season": "Summer", "budget": 250_000, | |
| "type": "Tech Summit", "guests": 400, "date": "2026-10-15", | |
| "notes": "Kosher catering required. VIP lounge for 30 executives.", | |
| }, | |
| { | |
| "label": "π½οΈ Corporate Gala Β· Jerusalem", | |
| "brief": "Elegant annual corporate gala β plated fine dining, live band, " | |
| "professional photography and videography for 200 guests.", | |
| "city": "Jerusalem", "season": "Winter", "budget": 140_000, | |
| "type": "Corporate Gala", "guests": 200, "date": "2026-12-05", | |
| "notes": "Black-tie dress code. Award presentation segment.", | |
| }, | |
| { | |
| "label": "πΏ Team Building Β· Haifa", | |
| "brief": "Outdoor team building day β interactive entertainment, DJ, " | |
| "logistics, casual catering for 150 employees.", | |
| "city": "Haifa", "season": "Spring", "budget": 65_000, | |
| "type": "Team Building", "guests": 150, "date": "2026-04-22", | |
| "notes": "Outdoor venue preferred. Vegetarian options required.", | |
| }, | |
| { | |
| "label": "π Boutique Wedding Β· Netanya", | |
| "brief": "Intimate outdoor wedding β elegant catering, DJ, floral design, " | |
| "photography, and logistics for 250 guests.", | |
| "city": "Netanya", "season": "Spring", "budget": 120_000, | |
| "type": "Wedding", "guests": 250, "date": "2027-05-14", | |
| "notes": "Chuppah at sunset. Vegan and gluten-free menu options.", | |
| }, | |
| ] | |
| # ================================================================ | |
| # DATA LOADING β from HF Dataset repo (not local file) | |
| # ================================================================ | |
| def _safe_to_list(val) -> list: | |
| """Parse a column value to list regardless of storage type.""" | |
| if isinstance(val, list): return val | |
| if isinstance(val, str): | |
| try: | |
| r = json.loads(val) | |
| return r if isinstance(r, list) else [] | |
| except Exception: return [] | |
| return [] | |
| def _load_vendors() -> pd.DataFrame: | |
| """ | |
| Load the vendor dataset from HF Dataset repo eliel2003/events. | |
| Tries three approaches in order: | |
| 1. datasets.load_dataset (handles private repos via HF_TOKEN) | |
| 2. hf_hub_download (direct file download) | |
| 3. pd.read_csv via URL (public repo fallback) | |
| """ | |
| token = HF_TOKEN or None | |
| # ββ Approach 1: datasets library βββββββββββββββββββββββββ | |
| try: | |
| from datasets import load_dataset | |
| print("β³ Trying datasets.load_dataset β¦") | |
| ds = load_dataset(HF_DATASET, token=token) | |
| # Find the vendors split β try common names | |
| vendor_split = None | |
| for name in ["vendors", "dataset_b_vendors", "vendor", "train"]: | |
| if name in ds: | |
| vendor_split = name | |
| break | |
| if vendor_split is None: | |
| vendor_split = list(ds.keys())[0] | |
| df = ds[vendor_split].to_pandas() | |
| # If the dataset has both events and vendors in one split, | |
| # filter to vendor rows using the vendor_id column pattern | |
| if "vendor_id" not in df.columns and "event_id" in df.columns: | |
| raise ValueError("Split contains events, not vendors.") | |
| print(f"β Loaded {len(df):,} vendors from '{vendor_split}' split.") | |
| return df | |
| except Exception as e1: | |
| print(f"β οΈ datasets.load_dataset failed: {e1}") | |
| # ββ Approach 2: hf_hub_download βββββββββββββββββββββββββββ | |
| try: | |
| from huggingface_hub import hf_hub_download | |
| print("β³ Trying hf_hub_download β¦") | |
| for fname in ["dataset_b_vendors.csv", "vendors.csv", | |
| "data/dataset_b_vendors.csv"]: | |
| try: | |
| path = hf_hub_download( | |
| repo_id=HF_DATASET, filename=fname, | |
| repo_type="dataset", token=token, | |
| ) | |
| df = pd.read_csv(path) | |
| print(f"β Loaded {len(df):,} vendors from '{fname}'.") | |
| return df | |
| except Exception: | |
| continue | |
| except Exception as e2: | |
| print(f"β οΈ hf_hub_download failed: {e2}") | |
| # ββ Approach 3: direct URL ββββββββββββββββββββββββββββββββ | |
| print("β³ Trying direct CSV URL β¦") | |
| base = f"https://huggingface.co/datasets/{HF_DATASET}/resolve/main" | |
| for fname in ["dataset_b_vendors.csv", "vendors.csv"]: | |
| try: | |
| headers = {} | |
| if token: | |
| headers["Authorization"] = f"Bearer {token}" | |
| import urllib.request | |
| req = urllib.request.Request(f"{base}/{fname}", headers=headers) | |
| with urllib.request.urlopen(req, timeout=30) as r: | |
| df = pd.read_csv(io.BytesIO(r.read())) | |
| print(f"β Loaded {len(df):,} vendors via URL '{fname}'.") | |
| return df | |
| except Exception: | |
| continue | |
| raise RuntimeError( | |
| f"Could not load vendor data from '{HF_DATASET}'. " | |
| "Make sure the repository is public or set HF_TOKEN as a Space Secret." | |
| ) | |
| def _engineer_features(df: pd.DataFrame) -> pd.DataFrame: | |
| """Apply the exact same feature engineering as EDA Cell 3.""" | |
| JSON_COLS = ["coverage_cities", "seasonal_availability", | |
| "specializations", "certifications"] | |
| # Parse JSON list columns β exclude them from the str.strip() loop | |
| for col in JSON_COLS: | |
| df[col] = df[col].apply(_safe_to_list) | |
| # Strip whitespace from plain string columns only (not JSON lists) | |
| for col in df.select_dtypes(include="object").columns: | |
| if col not in JSON_COLS and col != "vendor_profile_text": | |
| df[col] = df[col].str.strip() | |
| # Strip LLM artifact prefix from profile text | |
| artifact = "**Vendor Profile:**" | |
| df["vendor_profile_text"] = ( | |
| df["vendor_profile_text"].astype(str).str.strip() | |
| .str.removeprefix(artifact).str.strip() | |
| ) | |
| # Numeric features | |
| df["day_rate_mid"] = (df["day_rate_min_usd"] + df["day_rate_max_usd"]) / 2 | |
| # Composite vendor quality score (mirrors EDA Cell 3 exactly) | |
| r_min, r_max = df["avg_rating"].min(), df["avg_rating"].max() | |
| df["rating_norm"] = (df["avg_rating"] - r_min) / (r_max - r_min + 1e-9) | |
| df["value_score"] = 1 - (df["price_tier"] - 1) / 4 | |
| df["composite_score"] = ( | |
| 0.4 * df["rating_norm"] | |
| + 0.4 * df["sla_compliance_rate"] | |
| + 0.2 * df["value_score"] | |
| ) | |
| return df | |
| # ββ Load and prepare data βββββββββββββββββββββββββββββββββββββ | |
| print("β³ Loading vendor data from HF Dataset repo β¦") | |
| try: | |
| _df = _load_vendors() | |
| _df = _engineer_features(_df) | |
| # Pre-extract arrays for vectorized filtering (Section 13 pattern) | |
| _VCITIES = [_safe_to_list(v) for v in _df["coverage_cities"]] | |
| _VSEASONS = [_safe_to_list(v) for v in _df["seasonal_availability"]] | |
| _VCATS = _df["category"].values | |
| _VRATES = _df["day_rate_mid"].values | |
| _VCOMP = _df["composite_score"].values | |
| _VIDX = np.arange(len(_df)) | |
| print(f"β {len(_df):,} vendors ready.") | |
| except Exception as e: | |
| print(f"β Vendor data load failed: {e}") | |
| _df = None | |
| # ================================================================ | |
| # EMBEDDING MODEL β loaded from HF model repo | |
| # ================================================================ | |
| print(f"β³ Loading embedding model ({EMBED_MODEL_ID}) β¦") | |
| _embed = SentenceTransformer(EMBED_MODEL_ID, device="cpu") | |
| if _df is not None: | |
| print("β³ Encoding vendor profiles β¦") | |
| _vemb = _embed.encode( | |
| _df["vendor_profile_text"].tolist(), | |
| batch_size=128, show_progress_bar=True, | |
| normalize_embeddings=True, convert_to_tensor=True, | |
| device="cpu", | |
| ) | |
| print(f"β Embeddings ready: {_vemb.shape}") | |
| else: | |
| _vemb = None | |
| # ================================================================ | |
| # RECOMMENDATION ENGINE | |
| # Scoring: 60% semantic similarity + 40% composite quality score | |
| # (mirrors the design choice documented in Section 13 notebook) | |
| # ================================================================ | |
| def recommend_vendors( | |
| event_brief: str, | |
| event_city: str, | |
| event_season: str, | |
| total_budget_usd: float, | |
| top_n: int = 3, | |
| ) -> dict: | |
| """ | |
| Stage 1 β Vectorized hard filters: | |
| β’ City : vendor must cover event_city | |
| β’ Season : vendor must be available in event_season | |
| β’ Budget : vendor day_rate_mid β€ category-specific allocation | |
| Stage 2 β Semantic ranking (60/40 blend): | |
| final_score = 0.6 Γ cosine_similarity + 0.4 Γ composite_score | |
| Returns {category: [vendor_dicts]} or {"error": str}. | |
| """ | |
| if _df is None or _vemb is None: | |
| return {"error": "Vendor data not loaded. Check Space logs."} | |
| if not event_brief.strip(): | |
| return {"error": "Please enter an event description."} | |
| # Stage 1: hard filters (vectorized β no apply()) | |
| city_ok = np.array([event_city in c for c in _VCITIES], dtype=bool) | |
| season_ok = np.array([event_season in s for s in _VSEASONS], dtype=bool) | |
| alloc_vec = np.array( | |
| [total_budget_usd * ALLOC_RATIOS.get(cat, 0.10) for cat in _VCATS], | |
| dtype=float, | |
| ) | |
| budget_ok = _VRATES <= alloc_vec | |
| combined = city_ok & season_ok & budget_ok | |
| pool_idx = _VIDX[combined].tolist() | |
| if not pool_idx: | |
| n_c, n_s, n_b = int(city_ok.sum()), int(season_ok.sum()), int(budget_ok.sum()) | |
| return {"error": ( | |
| f"No vendors matched all three filters.\n" | |
| f" City '{event_city}': {n_c} vendors\n" | |
| f" Season '{event_season}': {n_s} vendors\n" | |
| f" Budget ${total_budget_usd:,.0f}: {n_b} vendors\n" | |
| f" Combined: 0 vendors\n\n" | |
| f"Try increasing the budget or selecting a different city." | |
| )} | |
| # Stage 2: semantic similarity | |
| q_vec = _embed.encode( | |
| event_brief, convert_to_tensor=True, | |
| normalize_embeddings=True, device="cpu", | |
| ) | |
| pool_embeds = _vemb[pool_idx] | |
| sims = st_util.cos_sim(q_vec, pool_embeds)[0].cpu().numpy() | |
| pool = _df.iloc[pool_idx].copy().reset_index(drop=True) | |
| pool["similarity"] = sims | |
| pool["final_score"] = 0.6 * sims + 0.4 * _VCOMP[pool_idx] | |
| results = {} | |
| for cat in VENDOR_CATEGORIES: | |
| sub = pool[pool["category"] == cat].nlargest(top_n, "final_score") | |
| if len(sub): | |
| results[cat] = sub[[ | |
| "vendor_name", "category", "price_tier", | |
| "avg_rating", "sla_compliance_rate", "day_rate_mid", | |
| "specializations", "similarity", "composite_score", "final_score", | |
| ]].to_dict("records") | |
| return results | |
| # ================================================================ | |
| # OUTPUT FORMATTER | |
| # ================================================================ | |
| def _stars(r: float) -> str: | |
| n = min(5, max(0, int(round(float(r))))) | |
| return "β " * n + "β" * (5 - n) | |
| def _fmt_vendors(recs: dict, budget: float) -> str: | |
| if "error" in recs: | |
| return f"### β οΈ No Results\n\n```\n{recs['error']}\n```" | |
| lines = [] | |
| for cat in VENDOR_CATEGORIES: | |
| if cat not in recs: continue | |
| alloc = budget * ALLOC_RATIOS[cat] | |
| cat_name = cat.replace("_", " ") | |
| lines.append( | |
| f"### {CATEGORY_EMOJI[cat]} {cat_name} " | |
| f"Β· Budget ceiling: ${alloc:,.0f}\n" | |
| ) | |
| for i, v in enumerate(recs[cat], 1): | |
| sp = v.get("specializations", []) | |
| if isinstance(sp, str): | |
| try: sp = json.loads(sp) | |
| except: sp = [] | |
| sc = v.get("final_score", 0) | |
| lines.append( | |
| f"**#{i} {v['vendor_name']}** \n" | |
| f"{_stars(v.get('avg_rating', 0))} Β· " | |
| f"{v.get('sla_compliance_rate', 0):.0%} SLA Β· " | |
| f"${v.get('day_rate_mid', 0):,.0f}/day Β· " | |
| f"Score `{sc:.3f}`\n\n" | |
| f"*{', '.join(sp[:2]) if sp else 'β'}*\n" | |
| ) | |
| lines.append("---\n") | |
| return "\n".join(lines) or "_No results._" | |
| # ================================================================ | |
| # GRADIO HANDLER | |
| # ================================================================ | |
| def handle_submit(brief, city, season, budget, ev_type, | |
| date_from, date_to, guests, notes): | |
| recs = recommend_vendors(brief, city, season, float(budget)) | |
| return _fmt_vendors(recs, float(budget)) | |
| def _date_html(df="2026-10-15", dt="2026-10-15"): | |
| """Generate HTML calendar date range picker styled to match the palette.""" | |
| label_css = ( | |
| "font-size:.88rem;font-weight:500;color:#5C3D1E;" | |
| "text-transform:uppercase;letter-spacing:.4px;" | |
| "margin-bottom:6px;display:block;" | |
| ) | |
| input_css = ( | |
| "width:100%;padding:9px 12px;border:1.5px solid #DDD0BE;" | |
| "border-radius:10px;background:#fff;color:#2C1810;" | |
| "font-family:Inter,sans-serif;font-size:.95rem;" | |
| "box-sizing:border-box;cursor:pointer;" | |
| ) | |
| sync_js = lambda eid: ( | |
| f"(function(v){{" | |
| f"var el=document.querySelector('#{eid}');" | |
| f"if(!el)return;" | |
| f"var t=el.querySelector('textarea')||el.querySelector('input');" | |
| f"if(t){{t.value=v;t.dispatchEvent(new Event('input',{{bubbles:true}}))}}" | |
| f"}})(this.value)" | |
| ) | |
| return f""" | |
| <div style="display:flex;gap:16px;margin:4px 0 12px;"> | |
| <div style="flex:1;"> | |
| <span style="{label_css}">Event Start Date</span> | |
| <input type="date" id="ps_df" value="{df}" | |
| style="{input_css}" oninput="{sync_js('ps_df_hid')}"> | |
| </div> | |
| <div style="flex:1;"> | |
| <span style="{label_css}">Event End Date</span> | |
| <input type="date" id="ps_dt" value="{dt}" | |
| style="{input_css}" oninput="{sync_js('ps_dt_hid')}"> | |
| </div> | |
| </div> | |
| """ | |
| def _qs(idx): | |
| q = QUICK_STARTERS[idx] | |
| b, c, s, bu = q["brief"], q["city"], q["season"], q["budget"] | |
| et, dt = q["type"], q["date"] | |
| gs, nt = q["guests"], q["notes"] | |
| vm = handle_submit(b, c, s, bu, et, dt, dt, gs, nt) | |
| return b, c, s, bu, et, dt, dt, gs, nt, _date_html(dt, dt), vm | |
| def _qs0(): return _qs(0) | |
| def _qs1(): return _qs(1) | |
| def _qs2(): return _qs(2) | |
| def _qs3(): return _qs(3) | |
| # ================================================================ | |
| # CSS β WARM BROWN / CREAM / BEIGE PALETTE | |
| # ================================================================ | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=Inter:wght@300;400;500;600&display=swap'); | |
| body, .gradio-container { | |
| background-color: #FAF7F2 !important; | |
| font-family: 'Inter', sans-serif !important; | |
| color: #2C1810 !important; | |
| } | |
| .ps-header { | |
| background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 60%, #B8895A 100%); | |
| border-radius: 16px; padding: 36px 40px; margin-bottom: 24px; | |
| box-shadow: 0 8px 32px rgba(61,35,20,.25); text-align: center; | |
| } | |
| .ps-header h1 { | |
| font-family: 'Playfair Display', serif; font-size: 2.4rem; | |
| font-weight: 700; color: #FAF7F2; margin: 0 0 6px; letter-spacing: .5px; | |
| } | |
| .ps-header p { color: #DDD0BE; font-size: 1.05rem; margin: 0; } | |
| label span, .label-wrap span { | |
| font-weight: 500 !important; font-size: .88rem !important; | |
| color: #5C3D1E !important; text-transform: uppercase !important; | |
| letter-spacing: .4px !important; | |
| } | |
| textarea, input[type="text"], input[type="number"] { | |
| background: #FFFFFF !important; border: 1.5px solid #DDD0BE !important; | |
| border-radius: 10px !important; color: #2C1810 !important; | |
| font-family: 'Inter', sans-serif !important; font-size: .95rem !important; | |
| } | |
| textarea:focus, input:focus { | |
| border-color: #B8895A !important; | |
| box-shadow: 0 0 0 3px rgba(184,137,90,.12) !important; | |
| } | |
| input[type="range"] { accent-color: #B8895A !important; } | |
| .wrap-inner, .svelte-select { | |
| background: #FFFFFF !important; border: 1.5px solid #DDD0BE !important; | |
| border-radius: 10px !important; color: #2C1810 !important; | |
| } | |
| .qs-btn { | |
| background: #F5EFE6 !important; border: 1.5px solid #D4B896 !important; | |
| color: #5C3D1E !important; font-family: 'Inter', sans-serif !important; | |
| font-weight: 500 !important; border-radius: 10px !important; | |
| padding: 10px 16px !important; transition: all .2s !important; | |
| } | |
| .qs-btn:hover { | |
| background: #EDE0CE !important; border-color: #B8895A !important; | |
| transform: translateY(-1px) !important; | |
| } | |
| .submit-btn { | |
| background: linear-gradient(135deg, #5C3D1E 0%, #8B6239 100%) !important; | |
| color: #FAF7F2 !important; font-family: 'Inter', sans-serif !important; | |
| font-size: 1.05rem !important; font-weight: 600 !important; | |
| border: none !important; border-radius: 12px !important; | |
| padding: 14px 28px !important; width: 100% !important; | |
| margin-top: 8px !important; | |
| box-shadow: 0 4px 16px rgba(61,35,20,.25) !important; | |
| } | |
| .submit-btn:hover { | |
| background: linear-gradient(135deg, #3D2314 0%, #7A4E2D 100%) !important; | |
| transform: translateY(-1px) !important; | |
| } | |
| .prose, .markdown-body { | |
| font-family: 'Inter', sans-serif !important; | |
| color: #2C1810 !important; line-height: 1.7 !important; | |
| } | |
| .prose h3 { | |
| font-family: 'Playfair Display', serif !important; | |
| color: #5C3D1E !important; | |
| border-bottom: 1px solid #DDD0BE; padding-bottom: 4px; | |
| } | |
| .prose hr { border-color: #EDE0CE !important; } | |
| .prose code { | |
| background: #F5EFE6 !important; color: #7A4E2D !important; | |
| border-radius: 4px !important; padding: 1px 5px !important; | |
| } | |
| .ps-footer { | |
| text-align: center; color: #A68B6A; font-size: .78rem; | |
| margin-top: 28px; border-top: 1px solid #EDE0CE; padding-top: 14px; | |
| } | |
| """ | |
| # ================================================================ | |
| # UI | |
| # ================================================================ | |
| with gr.Blocks(css=CSS, theme=gr.themes.Base(), title="ProSync AI") as demo: | |
| gr.HTML(""" | |
| <div class="ps-header"> | |
| <h1>ProSync AI</h1> | |
| <p>The Event Producer's Command Center β intelligent vendor matching</p> | |
| </div> | |
| """) | |
| # ββ Quick Starters ββββββββββββββββββββββββββββββββββββββββ | |
| gr.Markdown("#### β‘ Quick Starters β click to auto-fill and search") | |
| with gr.Row(): | |
| qs0 = gr.Button(QUICK_STARTERS[0]["label"], elem_classes=["qs-btn"]) | |
| qs1 = gr.Button(QUICK_STARTERS[1]["label"], elem_classes=["qs-btn"]) | |
| with gr.Row(): | |
| qs2 = gr.Button(QUICK_STARTERS[2]["label"], elem_classes=["qs-btn"]) | |
| qs3 = gr.Button(QUICK_STARTERS[3]["label"], elem_classes=["qs-btn"]) | |
| gr.Markdown("---") | |
| # ββ Event inputs βββββββββββββββββββββββββββββββββββββββββ | |
| brief = gr.Textbox( | |
| label="Describe your event", lines=4, | |
| placeholder=( | |
| "e.g. Tech summit for 400 guests β advanced AV, live streaming, " | |
| "kosher catering, VIP securityβ¦" | |
| ), | |
| ) | |
| with gr.Row(): | |
| city = gr.Dropdown( | |
| label="City", choices=CITIES, value="Tel Aviv", | |
| allow_custom_value=False, | |
| ) | |
| season = gr.Dropdown( | |
| label="Season", choices=SEASONS, value="Summer", | |
| allow_custom_value=False, | |
| ) | |
| budget = gr.Number( | |
| label="Total Budget (USD)", value=200_000, | |
| minimum=5_000, maximum=2_000_000, | |
| ) | |
| gr.Markdown("---") | |
| # ββ Document settings βββββββββββββββββββββββββββββββββββββ | |
| with gr.Row(): | |
| ev_type = gr.Dropdown( | |
| label="Event Type", choices=EVENT_TYPES, value="Tech Summit", | |
| allow_custom_value=False, | |
| ) | |
| guests = gr.Number( | |
| label="Guest Count", value=300, minimum=10, maximum=5000, | |
| ) | |
| # Calendar date range picker (real <input type="date"> elements) | |
| date_picker = gr.HTML(value=_date_html()) | |
| date_from = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_df_hid") | |
| date_to = gr.Textbox(value="2026-10-15", visible=False, elem_id="ps_dt_hid") | |
| notes = gr.Textbox( | |
| label="Special Requirements", | |
| placeholder="e.g. Kosher catering, black-tie dress code, outdoor settingβ¦", | |
| lines=2, | |
| ) | |
| submit = gr.Button( | |
| "π Find Matching Vendors", | |
| elem_classes=["submit-btn"], | |
| ) | |
| gr.Markdown("---") | |
| # ββ Results βββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.Markdown("### πͺ Vendor Matches") | |
| vendor_out = gr.Markdown( | |
| value="_Complete the form above and click **Find Matching Vendors**._", | |
| elem_classes=["prose"], | |
| ) | |
| gr.HTML( | |
| '<div class="ps-footer">' | |
| 'ProSync AI Β· Gradio + HuggingFace Β· ' | |
| 'Dataset: eliel2003/events Β· ' | |
| 'Embedding: all-MiniLM-L6-v2 Β· ' | |
| 'Scoring: 60% semantic + 40% quality' | |
| '</div>' | |
| ) | |
| # ββ Wiring βββββββββββββββββββββββββββββββββββββββββββββββ | |
| _in = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes] | |
| _out = [vendor_out] | |
| _form = [brief, city, season, budget, ev_type, date_from, date_to, guests, notes] | |
| _qs_out = _form + [date_picker, vendor_out] | |
| submit.click(fn=handle_submit, inputs=_in, outputs=_out) | |
| qs0.click(fn=_qs0, outputs=_qs_out) | |
| qs1.click(fn=_qs1, outputs=_qs_out) | |
| qs2.click(fn=_qs2, outputs=_qs_out) | |
| qs3.click(fn=_qs3, outputs=_qs_out) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860) |