Download app.py from nandutt/House: direct link, hf CLI and curl.
- Browser
- Download file 26.8 kB
-
https://huggingface.co/spaces/nandutt/House/resolve/main/app.py
- Command line
-
hf download hf://spaces/nandutt/House/app.py
-
curl -L -o app.py https://huggingface.co/spaces/nandutt/House/resolve/main/app.py
26.8 kB
| import os | |
| import json | |
| import re | |
| import pandas as pd | |
| import gradio as gr | |
| from inference_core import run_full_inference | |
| from fe_avm_bdo26 import location_map_dict | |
| from map_builder import generate_area_map_safe as generate_area_map, generate_full_bandung_map_safe as generate_full_bandung_map | |
| from chatbot_core import chat, EMPTY_STATE | |
| # ============================================================================= | |
| # GEMINI CLIENT | |
| # ============================================================================= | |
| client = None | |
| gemini_available = False | |
| try: | |
| from google import genai | |
| api_key = os.getenv("GEMINI_API_KEY") | |
| if api_key: | |
| client = genai.Client(api_key=api_key) | |
| gemini_available = True | |
| print("Gemini client initialized.") | |
| else: | |
| print("No GEMINI_API_KEY found. Using fallback parser only.") | |
| except Exception as e: | |
| print("Gemini init error:", e) | |
| client = None | |
| gemini_available = False | |
| print("GEMINI AVAILABLE:", gemini_available) | |
| print("CLIENT IS NONE:", client is None) | |
| print("ENV KEY FOUND:", os.getenv("GEMINI_API_KEY") is not None) | |
| # ============================================================================= | |
| # SAFE PARSERS | |
| # ============================================================================= | |
| def safe_float(x): | |
| if x is None or str(x).strip() == "": | |
| return None | |
| return float(x) | |
| def safe_int(x): | |
| if x is None or str(x).strip() == "": | |
| return None | |
| return int(float(x)) | |
| # ============================================================================= | |
| # FALLBACK PARSER | |
| # ============================================================================= | |
| def fallback_parse_notes(user_notes): | |
| if not user_notes: | |
| return {} | |
| text = user_notes.lower() | |
| parsed = {} | |
| # location parsing | |
| known_locations = sorted( | |
| location_map_dict.keys(), | |
| key=len, | |
| reverse=True | |
| ) | |
| for loc in known_locations: | |
| if loc.lower() in text: | |
| parsed["location"] = loc | |
| break | |
| m = re.search(r"(\d+)\s*bed", text) | |
| if m: | |
| parsed["bedroom_total"] = int(m.group(1)) | |
| m = re.search(r"(\d+)\s*bath", text) | |
| if m: | |
| parsed["bathroom_total"] = int(m.group(1)) | |
| m = re.search(r"(\d+)\s*car", text) | |
| if m: | |
| parsed["carport_total"] = int(m.group(1)) | |
| # budget parsing (more robust) | |
| budget_patterns = [ | |
| r"(?:budget|harga|max|under|below|dibawah|di bawah|sekitar|around)\s*(?:rp\s*)?(\d+(?:\.\d+)?)\s*(m|miliar|billion|jt|juta)?", | |
| r"(?:rp\s*)?(\d+(?:\.\d+)?)\s*(m|miliar|billion|jt|juta)\b" | |
| ] | |
| budget_val = None | |
| unit = "" | |
| for pattern in budget_patterns: | |
| m = re.search(pattern, text) | |
| if m: | |
| budget_val = float(m.group(1)) | |
| unit = (m.group(2) or "").lower() | |
| break | |
| if budget_val is not None: | |
| if unit in ["m", "miliar", "billion"]: | |
| budget_val *= 1000 | |
| elif unit in ["jt", "juta"]: | |
| budget_val = budget_val | |
| elif budget_val < 10: | |
| budget_val *= 1000 | |
| parsed["budget_million_idr"] = budget_val | |
| return parsed | |
| # ============================================================================= | |
| # GEMINI PARSER | |
| # ============================================================================= | |
| def parse_preferences_with_gemini(user_notes, active_client): | |
| if not user_notes: | |
| return {} | |
| fallback = fallback_parse_notes(user_notes) | |
| parsed = {} | |
| if active_client is None: | |
| return fallback | |
| try: | |
| prompt = f""" | |
| Extract housing preferences from this text. | |
| Return ONLY valid JSON. DO NOT explain anything. Keep concise. Exact location from Bandung where possible. Correct OBVIOUS typos and use right caps. e.g: sayap dago into Sayap Dago, daog Pakar into Dago Pakar. | |
| Budget rules: | |
| - if budget number is below 10, assume it is in billions | |
| - convert to million IDR | |
| - example: 1 = 1000, 1.5 = 1500, 9.9 = 9900 | |
| - words like under, below, max mean budget | |
| Use keys: | |
| location | |
| building_type | |
| bedroom_total | |
| bathroom_total | |
| carport_total | |
| budget_million_idr | |
| building_area_m2 | |
| land_area_m2 | |
| Text: | |
| {user_notes} | |
| """ | |
| model_candidates = [ | |
| "gemini-2.5-flash-lite" | |
| ] | |
| for model_name in model_candidates: | |
| try: | |
| response = active_client.models.generate_content( | |
| model=model_name, | |
| contents=prompt, | |
| config={ | |
| "max_output_tokens": 80, | |
| "temperature": 0.1, | |
| "top_p": 0.9 | |
| } | |
| ) | |
| if hasattr(response, "text") and response.text: | |
| clean_text = response.text.strip() | |
| else: | |
| clean_text = str(response).strip() | |
| clean_text = ( | |
| clean_text | |
| .replace("```json", "") | |
| .replace("```", "") | |
| .strip() | |
| ) | |
| gemini_parsed = json.loads(clean_text) | |
| if isinstance(gemini_parsed, dict): | |
| # Gemini first | |
| parsed.update(gemini_parsed) | |
| # fallback fills only missing values | |
| for k, v in fallback.items(): | |
| if k not in parsed or parsed[k] in [None, ""]: | |
| parsed[k] = v | |
| print(f"GEMINI PARSER SUCCESS: {model_name}") | |
| break | |
| except Exception as e: | |
| print(f"GEMINI PARSER FAILED {model_name}: {e}") | |
| return parsed if parsed else fallback | |
| except Exception as e: | |
| print("Gemini parse error:", e) | |
| return parsed | |
| # ============================================================================= | |
| # HTML FORMATTERS | |
| # ============================================================================= | |
| def make_clickable_links(df): | |
| if df is None or df.empty: | |
| return """ | |
| <div class="listing-empty"> | |
| No listings found. | |
| </div> | |
| """ | |
| df = df.copy() | |
| pretty_cols = { | |
| "building_type": "Type", | |
| "location": "Location", | |
| "price_million_idr": "Price (IDR Mn)", | |
| "listing_summary": "Listing Summary" | |
| } | |
| df = df.rename(columns=pretty_cols) | |
| if "url" in df.columns: | |
| def make_short_clickable_link(x): | |
| if pd.isna(x) or str(x).strip() == "": | |
| return "" | |
| full_url = str(x).strip() | |
| # ensure full valid URL | |
| if not full_url.startswith("http"): | |
| full_url = "https://" + full_url | |
| # pretty display text | |
| display_text = full_url[:25] + "..." if len(full_url) > 25 else full_url | |
| return f'<a href="{full_url}" target="_blank">{display_text}</a>' | |
| df["Link"] = df["url"].apply(make_short_clickable_link) | |
| df = df.drop(columns=["url"]) | |
| df["Listing Summary"] = df["Listing Summary"].apply( | |
| lambda x: str(x)[:90] + "..." if pd.notna(x) and len(str(x)) > 90 else x | |
| ) | |
| return df.to_html( | |
| escape=False, | |
| index=False, | |
| classes="listing-table", | |
| justify="left" | |
| ) | |
| def wrap_params_card(df): | |
| if df is None or df.empty: | |
| return "<div class='info-card'>No parameters available.</div>" | |
| row = df.iloc[0].to_dict() | |
| html_parts = ["<div class='info-card'>"] | |
| html_parts.append("<div class='info-card-title'>Parameters Used</div>") | |
| html_parts.append("<div class='param-grid'>") | |
| for key, val in row.items(): | |
| show_val = "—" if pd.isna(val) else val | |
| html_parts.append( | |
| f""" | |
| <div class="param-item"> | |
| <div class="param-label">{key}</div> | |
| <div class="param-value">{show_val}</div> | |
| </div> | |
| """ | |
| ) | |
| html_parts.append("</div></div>") | |
| return "".join(html_parts) | |
| # ============================================================================= | |
| # MAIN SMART VALUATION FUNCTION | |
| # ============================================================================= | |
| def predict_housing( | |
| user_notes, | |
| location, | |
| building_type, | |
| bedroom_total, | |
| bathroom_total, | |
| carport_total, | |
| building_area_m2, | |
| land_area_m2, | |
| budget_million_idr | |
| ): | |
| user_info = { | |
| "location": location, | |
| "building_type": building_type, | |
| "bedroom_total": safe_int(bedroom_total), | |
| "bathroom_total": safe_int(bathroom_total), | |
| "carport_total": safe_int(carport_total), | |
| "building_area_m2": safe_float(building_area_m2), | |
| "land_area_m2": safe_float(land_area_m2), | |
| "budget_million_idr": safe_float(budget_million_idr), | |
| } | |
| # treat untouched UI zeros as missing | |
| for key in ["bedroom_total", "bathroom_total", "carport_total"]: | |
| if user_info.get(key) == 0: | |
| user_info[key] = None | |
| parsed_notes = parse_preferences_with_gemini( | |
| user_notes, | |
| client | |
| ) | |
| print("PARSED NOTES:", parsed_notes) | |
| if user_notes and user_notes.strip(): | |
| for key, val in parsed_notes.items(): | |
| if user_info.get(key) in [None, "", 0]: | |
| if val not in [None, ""]: | |
| user_info[key] = val | |
| if user_notes and user_notes.strip(): | |
| user_info["notes"] = user_notes | |
| print("FINAL USER INFO:", user_info) | |
| result = run_full_inference( | |
| user_info, | |
| client=client | |
| ) | |
| params_df = pd.DataFrame([result["params_used"]]) | |
| params_card_html = wrap_params_card(params_df) | |
| pred = result["prediction"] | |
| estimate_md = f""" | |
| <div class="estimate-card"> | |
| <div class="estimate-title">House Price Estimate</div> | |
| <div class="estimate-main">IDR {pred['Predicted Price (IDR Mn)']:.0f} Mn</div> | |
| <div class="estimate-subgrid"> | |
| <div class="estimate-subitem"> | |
| <div class="estimate-sub-label">Lower Bound</div> | |
| <div class="estimate-sub-value">IDR {pred['Lower Range']:.0f} Mn</div> | |
| </div> | |
| <div class="estimate-subitem"> | |
| <div class="estimate-sub-label">Upper Bound</div> | |
| <div class="estimate-sub-value">IDR {pred['Upper Range']:.0f} Mn</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| map_html = generate_area_map( | |
| location=user_info.get("location"), | |
| price_mid=round(pred["Predicted Price (IDR Mn)"], -1), | |
| price_lo=round(pred["Lower Range"], -1), | |
| price_hi=round(pred["Upper Range"], -1) | |
| ) | |
| comparison_intro_text = ( | |
| f"### If you like **{user_info.get('location')}**, " | |
| f"you may also want to consider:" | |
| ) | |
| comparison_df = result["comparison_table"].copy() | |
| comparison_df = comparison_df.rename( | |
| columns={"Desirability Score": "Desirability Score"} | |
| ) | |
| target_title = ( | |
| f"### Example listings in **{user_info.get('location')}**" | |
| ) | |
| alt_title = "### Example listings in **alternative areas**" | |
| target_html = make_clickable_links(result["target_listings"]) | |
| rec_html = make_clickable_links(result["recommended_listings"]) | |
| target_block = f"{target_title}\n{target_html}" | |
| alt_block = f"{alt_title}\n{rec_html}" | |
| summary_md = f""" | |
| <div class="summary-card"> | |
| {result["summary"]} | |
| </div> | |
| """ | |
| return ( | |
| params_card_html, | |
| estimate_md, | |
| map_html, | |
| result["market_summary"], | |
| comparison_intro_text, | |
| comparison_df, | |
| target_block, | |
| alt_block, | |
| summary_md | |
| ) | |
| # ============================================================================= | |
| # ABOUT TAB HELPERS | |
| # ============================================================================= | |
| def get_booklet_path(): | |
| candidates = [ | |
| "booklet_rmt52_boni.pdf", | |
| "RMT52_product_booklet.pdf", | |
| "RMT52 Booklet.pdf", | |
| "RMT52_booklet.pdf", | |
| "company_booklet.pdf", | |
| "product_booklet.pdf" | |
| ] | |
| for path in candidates: | |
| if os.path.exists(path): | |
| return path | |
| return None | |
| BOOKLET_PATH = get_booklet_path() | |
| # ============================================================================= | |
| # UI | |
| # ============================================================================= | |
| custom_css = """ | |
| :root { | |
| --rmt-main: #330066; | |
| --rmt-text: #142B4D; | |
| --rmt-secondary: #4D6A92; | |
| --rmt-accent: #660033; | |
| --rmt-bg: #FFFFFF; | |
| --rmt-soft: #FAF8F4; | |
| --rmt-line: #E7EDF3; | |
| } | |
| .gradio-container { | |
| max-width: 1380px !important; | |
| margin: auto; | |
| background: var(--rmt-bg); | |
| } | |
| .block-title { | |
| background: var(--rmt-main); | |
| color: white !important; | |
| padding: 14px 18px; | |
| border-radius: 14px; | |
| font-weight: 700; | |
| margin-bottom: 14px; | |
| font-size: 18px; | |
| } | |
| .section-title { | |
| color: var(--rmt-text); | |
| font-weight: 700; | |
| margin-top: 8px; | |
| margin-bottom: 6px; | |
| } | |
| .soft-note { | |
| color: var(--rmt-secondary); | |
| font-size: 14px; | |
| margin-bottom: 10px; | |
| } | |
| .info-card { | |
| background: white; | |
| border: 1px solid var(--rmt-line); | |
| border-radius: 16px; | |
| padding: 16px; | |
| box-shadow: 0 2px 8px rgba(20,43,77,0.04); | |
| } | |
| .info-card-title { | |
| color: var(--rmt-text); | |
| font-size: 17px; | |
| font-weight: 700; | |
| margin-bottom: 12px; | |
| } | |
| .param-grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 10px; | |
| } | |
| .param-item { | |
| background: var(--rmt-soft); | |
| border: 1px solid var(--rmt-line); | |
| border-radius: 12px; | |
| padding: 10px 12px; | |
| } | |
| .param-label { | |
| color: var(--rmt-secondary); | |
| font-size: 12px; | |
| margin-bottom: 4px; | |
| } | |
| .param-value { | |
| color: var(--rmt-text); | |
| font-weight: 600; | |
| font-size: 14px; | |
| } | |
| .estimate-card { | |
| background: white; | |
| border: 1px solid var(--rmt-line); | |
| border-top: 6px solid var(--rmt-main); | |
| border-radius: 18px; | |
| padding: 18px; | |
| box-shadow: 0 3px 10px rgba(20,43,77,0.06); | |
| } | |
| .estimate-title { | |
| color: var(--rmt-text); | |
| font-size: 16px; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| } | |
| .estimate-main { | |
| color: var(--rmt-main); | |
| font-size: 30px; | |
| font-weight: 800; | |
| margin-bottom: 14px; | |
| } | |
| .estimate-subgrid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| } | |
| .estimate-subitem { | |
| background: var(--rmt-soft); | |
| border-radius: 12px; | |
| padding: 12px; | |
| border: 1px solid var(--rmt-line); | |
| } | |
| .estimate-sub-label { | |
| color: var(--rmt-secondary); | |
| font-size: 12px; | |
| margin-bottom: 4px; | |
| } | |
| .estimate-sub-value { | |
| color: var(--rmt-text); | |
| font-weight: 700; | |
| font-size: 15px; | |
| } | |
| .leaflet-control-container { | |
| z-index: 9999 !important; | |
| } | |
| .leaflet-top, | |
| .leaflet-right { | |
| z-index: 9999 !important; | |
| } | |
| .leaflet-control-layers { | |
| z-index: 9999 !important; | |
| background: white !important; | |
| border-radius: 12px !important; | |
| } | |
| .summary-card { | |
| background: #ffffff; | |
| border-left: 6px solid var(--rmt-accent); | |
| border-radius: 12px; | |
| border-top: 1px solid var(--rmt-line); | |
| border-right: 1px solid var(--rmt-line); | |
| border-bottom: 1px solid var(--rmt-line); | |
| padding: 16px; | |
| color: var(--rmt-text); | |
| line-height: 1.6; | |
| } | |
| .listing-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| } | |
| .listing-table th { | |
| background: var(--rmt-secondary); | |
| color: white; | |
| padding: 10px; | |
| text-align: left; | |
| white-space: normal !important; | |
| } | |
| .listing-table td { | |
| padding: 10px; | |
| border-bottom: 1px solid var(--rmt-line); | |
| white-space: normal !important; | |
| word-wrap: break-word !important; | |
| overflow-wrap: anywhere !important; | |
| color: var(--rmt-text); | |
| background: white; | |
| } | |
| .listing-empty { | |
| padding: 12px; | |
| border: 1px dashed var(--rmt-line); | |
| border-radius: 12px; | |
| color: var(--rmt-secondary); | |
| background: var(--rmt-soft); | |
| } | |
| footer { | |
| text-align: center; | |
| color: var(--rmt-secondary); | |
| font-size: 12px; | |
| margin-top: 18px; | |
| } | |
| button.primary, | |
| button[variant="primary"] { | |
| background: var(--rmt-accent) !important; | |
| border: none !important; | |
| } | |
| .gr-tab-nav button.selected { | |
| color: var(--rmt-main) !important; | |
| border-color: var(--rmt-main) !important; | |
| } | |
| h1, h2, h3, h4 { | |
| color: var(--rmt-text); | |
| } | |
| /* FULL MAP FIX */ | |
| #full-map iframe, | |
| #area-map iframe { | |
| height: 780px !important; | |
| min-height: 780px !important; | |
| width: 100% !important; | |
| border-radius: 18px; | |
| } | |
| #full-map, | |
| #area-map { | |
| min-height: 780px !important; | |
| overflow: visible !important; | |
| } | |
| .leaflet-control-container { | |
| z-index: 99999 !important; | |
| overflow: visible !important; | |
| } | |
| .leaflet-top, | |
| .leaflet-right, | |
| .leaflet-control { | |
| z-index: 99999 !important; | |
| overflow: visible !important; | |
| } | |
| .leaflet-control-layers { | |
| z-index: 99999 !important; | |
| overflow: visible !important; | |
| max-height: none !important; | |
| } | |
| iframe { | |
| overflow: visible !important; | |
| } | |
| """ | |
| with gr.Blocks( | |
| title="RMT52", | |
| theme=gr.themes.Soft(), | |
| css=custom_css | |
| ) as demo: | |
| gr.Markdown(""" | |
| # RMT52 | |
| ### Bandung Housing Decision Support Tool · 2026 | |
| """) | |
| with gr.Tabs(): | |
| # ========================================================================= | |
| # TAB 1 - SMART VALUATION | |
| # ========================================================================= | |
| with gr.Tab("🏡 Smart Valuation"): | |
| with gr.Row(): | |
| # ------------------------------------------------- | |
| # LEFT COLUMN | |
| # ------------------------------------------------- | |
| with gr.Column(scale=1): | |
| gr.HTML( | |
| "<div class='section-title' style='font-size:24px;'>What are you looking for today?</div>" | |
| ) | |
| user_notes = gr.Textbox( | |
| lines=4, | |
| label="", | |
| placeholder=( | |
| "I’m looking for a 4-bedroom house in Riau " | |
| "with a budget around 1.5B, preferably near schools." | |
| ) | |
| ) | |
| chat_parse_btn = gr.Button( | |
| "Find With Chat", | |
| variant="primary" | |
| ) | |
| gr.Markdown( | |
| "<div class='soft-note'><b>OR</b> fill in the form below</div>" | |
| ) | |
| gr.HTML("<div class='block-title'>House Price Estimate</div>") | |
| location = gr.Textbox( | |
| label="Preferred Location", | |
| placeholder="e.g. Riau" | |
| ) | |
| building_type = gr.Dropdown( | |
| choices=["Rumah", "Apartemen", "Ruko"], | |
| label="Building Type", | |
| value="Rumah" | |
| ) | |
| with gr.Row(): | |
| land_area_m2 = gr.Textbox( | |
| label="Land Area (m²)", | |
| placeholder="Optional" | |
| ) | |
| building_area_m2 = gr.Textbox( | |
| label="Building Area (m²)", | |
| placeholder="Optional" | |
| ) | |
| bedroom_total = gr.Number( | |
| label="Bedrooms", | |
| precision=0 | |
| ) | |
| bathroom_total = gr.Number( | |
| label="Bathrooms", | |
| precision=0 | |
| ) | |
| carport_total = gr.Number( | |
| label="Carport", | |
| precision=0 | |
| ) | |
| budget_million_idr = gr.Textbox( | |
| label="Budget (IDR Mn)", | |
| placeholder="Optional" | |
| ) | |
| run_btn = gr.Button( | |
| "Estimate Price", | |
| variant="primary" | |
| ) | |
| params_used_output = gr.HTML() | |
| # ------------------------------------------------- | |
| # RIGHT COLUMN | |
| # ------------------------------------------------- | |
| with gr.Column(scale=2): | |
| estimate_output = gr.HTML() | |
| map_output = gr.HTML(elem_id="area-map") | |
| gr.Markdown("# Outputs") | |
| gr.Markdown("## Market Context") | |
| market_summary_output = gr.Markdown() | |
| gr.Markdown("## Area Comparison") | |
| comparison_intro = gr.Markdown() | |
| comparison_output = gr.Dataframe(wrap=True, label="") | |
| target_listing_output = gr.HTML() | |
| rec_listing_output = gr.HTML() | |
| gr.Markdown("## Summary") | |
| summary_output = gr.HTML() | |
| run_btn.click( | |
| fn=predict_housing, | |
| inputs=[ | |
| user_notes, | |
| location, | |
| building_type, | |
| bedroom_total, | |
| bathroom_total, | |
| carport_total, | |
| building_area_m2, | |
| land_area_m2, | |
| budget_million_idr | |
| ], | |
| outputs=[ | |
| params_used_output, | |
| estimate_output, | |
| map_output, | |
| market_summary_output, | |
| comparison_intro, | |
| comparison_output, | |
| target_listing_output, | |
| rec_listing_output, | |
| summary_output | |
| ] | |
| ) | |
| chat_parse_btn.click( | |
| fn=predict_housing, | |
| inputs=[ | |
| user_notes, | |
| location, | |
| building_type, | |
| bedroom_total, | |
| bathroom_total, | |
| carport_total, | |
| building_area_m2, | |
| land_area_m2, | |
| budget_million_idr | |
| ], | |
| outputs=[ | |
| params_used_output, | |
| estimate_output, | |
| map_output, | |
| market_summary_output, | |
| comparison_intro, | |
| comparison_output, | |
| target_listing_output, | |
| rec_listing_output, | |
| summary_output | |
| ] | |
| ) | |
| # ========================================================================= | |
| # TAB 2 - CHATBOT | |
| # ========================================================================= | |
| with gr.Tab("💬 HomieRMT Chatbot"): | |
| gr.Markdown(""" | |
| ### 💬 HomieRMT Assistant | |
| Ask naturally about: | |
| - property price estimates | |
| - similar areas | |
| - Bandung market context | |
| """) | |
| chat_history_box = gr.Markdown( | |
| value="**HomieRMT:** Hi! Ask me anything about Bandung housing.\n" | |
| ) | |
| chat_input = gr.Textbox( | |
| label="Message", | |
| placeholder="Ask about housing in Bandung...", | |
| lines=2 | |
| ) | |
| send_btn = gr.Button("Send", variant="primary") | |
| history_state = gr.State([]) | |
| chat_state = gr.State(EMPTY_STATE) | |
| def conversational_chat(msg, transcript, history, state): | |
| updated_history, new_state = chat(msg, history, state) | |
| latest_reply = updated_history[-1]["content"] | |
| transcript += f"\n\n**You:** {msg}\n\n**HomieRMT:** {latest_reply}" | |
| return transcript, updated_history, new_state | |
| send_btn.click( | |
| fn=conversational_chat, | |
| inputs=[chat_input, chat_history_box, history_state, chat_state], | |
| outputs=[chat_history_box, history_state, chat_state] | |
| ) | |
| chat_input.submit( | |
| fn=conversational_chat, | |
| inputs=[chat_input, chat_history_box, history_state, chat_state], | |
| outputs=[chat_history_box, history_state, chat_state] | |
| ) | |
| # ========================================================================= | |
| # TAB 3 - FULL BANDUNG MAP | |
| # ========================================================================= | |
| with gr.Tab("🗺️ Bandung Market Map"): | |
| gr.Markdown(""" | |
| ### Bandung Market Map | |
| Explore city-wide housing patterns, facilities, and accessibility layers. | |
| Toggle map layers from the control panel on the map. | |
| """) | |
| load_full_map_btn = gr.Button( | |
| "Load Bandung Market Map", | |
| variant="primary" | |
| ) | |
| full_map_display = gr.HTML( | |
| value="Click the button to load map.", | |
| elem_id="full-map" | |
| ) | |
| load_full_map_btn.click( | |
| fn=generate_full_bandung_map, | |
| outputs=full_map_display | |
| ) | |
| # ========================================================================= | |
| # TAB 4 - ABOUT | |
| # ========================================================================= | |
| with gr.Tab("ℹ️ About RMT52"): | |
| gr.Markdown(""" | |
| ## RMT52 | |
| RMT52 combines smart valuation, neighborhood intelligence, and interactive market mapping to support better housing decisions in Bandung. | |
| ### What it does | |
| - smart property valuation | |
| - comparable area recommendations | |
| - market context and listing examples | |
| - interactive Bandung market map | |
| - conversational advisor support | |
| ### Methodology | |
| RMT52 is powered by a calibrated valuation and recommendation stack using location-aware feature engineering and structured market signals from Bandung housing listings. | |
| ### Product Booklet | |
| Download the product booklet for more information, use cases, and methodology. | |
| """) | |
| if BOOKLET_PATH is not None: | |
| gr.File( | |
| value=BOOKLET_PATH, | |
| label="📘 Download the RMT52 Product Booklet" | |
| ) | |
| else: | |
| gr.Markdown( | |
| "_Booklet file not found yet. Add your PDF to the app folder and name it one of the expected booklet filenames._" | |
| ) | |
| gr.Markdown( | |
| "<footer>RMT52, LLC. 2026.</footer>" | |
| ) | |
| if __name__ == "__main__": | |
| demo.queue() | |
| demo.launch(server_name="0.0.0.0") |