Spaces:
Running on Zero
Running on Zero
| """FLIGHTDECK — live flights on a transparent 3D globe, with an LLM flight agent. | |
| Data: FlightRadar24 API (https://fr24api.flightradar24.com/docs/getting-started) | |
| Globe: Globe.gl / Three.js (3D, transparent, neon glow) | |
| LLM: openbmb/MiniCPM5-1B via transformers (default safetensors model) | |
| Set FR24_API_TOKEN in your environment (see .env.example), then `python app.py`. | |
| """ | |
| from __future__ import annotations | |
| import datetime as dt | |
| import os | |
| import gradio as gr | |
| try: | |
| from dotenv import load_dotenv | |
| # Load .env sitting next to this file, regardless of the launch cwd. | |
| load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")) | |
| except Exception: | |
| pass | |
| import agent | |
| import fr24 | |
| import geo | |
| import globe as globe_mod | |
| import liquid | |
| import sidebar as sidebar_mod | |
| import trace_sync | |
| import transit_agent | |
| import transit_map | |
| # Mirror ./traces/ to a HF dataset repo (no-op without HF_TOKEN); the container | |
| # disk on Spaces is ephemeral, so this is what makes traces survive restarts. | |
| print(f"[flightdeck] {trace_sync.start()}") | |
| # ---- Preset regions (north, south, west, east) ----------------------------- | |
| REGIONS = { | |
| "Europe": (60.0, 36.0, -11.0, 30.0), | |
| "North America": (50.0, 25.0, -125.0, -67.0), | |
| "USA — East Coast": (45.0, 30.0, -82.0, -68.0), | |
| "UK & Ireland": (59.0, 49.0, -11.0, 2.0), | |
| "Middle East": (33.0, 22.0, 34.0, 60.0), | |
| "East Asia": (46.0, 22.0, 100.0, 146.0), | |
| "Australia": (-10.0, -44.0, 112.0, 154.0), | |
| "World (sampled)": (70.0, -60.0, -170.0, 170.0), | |
| } | |
| MAX_FLIGHTS = 220 # keep the globe/LLM responsive | |
| def normalize_flight(raw: dict) -> dict | None: | |
| """Map an FR24 record to the fields the rest of the app expects.""" | |
| lat = raw.get("lat") | |
| lon = raw.get("lon") | |
| if lat is None or lon is None: | |
| return None | |
| eta_raw = raw.get("eta") | |
| eta_iso, eta_secs = fr24.parse_eta(eta_raw) | |
| dest_code = raw.get("dest_icao") or raw.get("dest_iata") | |
| orig_code = raw.get("orig_icao") or raw.get("orig_iata") | |
| dest_ll = fr24.airport_coords(dest_code) if dest_code else None | |
| # The estimated-path arc targets the REAL destination airport. If we can't | |
| # resolve the destination's coordinates, we draw no arc (rather than a | |
| # misleading dead-reckoned line to a random point). | |
| if dest_ll: | |
| est_path = (dest_ll[0], dest_ll[1]) | |
| dist_km = round(geo.haversine_km(lat, lon, dest_ll[0], dest_ll[1]), 1) | |
| else: | |
| est_path = None | |
| dist_km = None | |
| return { | |
| "fr24_id": raw.get("fr24_id"), | |
| "callsign": raw.get("callsign") or raw.get("flight"), | |
| "flight": raw.get("flight"), | |
| "lat": float(lat), "lon": float(lon), | |
| "alt": raw.get("alt"), | |
| "gspeed": raw.get("gspeed"), | |
| "track": raw.get("track"), | |
| "orig": orig_code, "dest": dest_code, | |
| "type": raw.get("type"), | |
| "reg": raw.get("reg"), | |
| "eta_iso": eta_iso, | |
| "eta_secs": eta_secs, | |
| "eta_human": fr24.human_duration(eta_secs) if eta_secs is not None else "—", | |
| "est_dist_km": dist_km, | |
| "est_to_dest": est_path is not None, | |
| "est_path": est_path, | |
| } | |
| def _focus_point(flights): | |
| if not flights: | |
| return None | |
| avg_lat = sum(f["lat"] for f in flights) / len(flights) | |
| avg_lon = sum(f["lon"] for f in flights) / len(flights) | |
| return {"lat": avg_lat, "lng": avg_lon} | |
| def _table_rows(flights): | |
| rows = [] | |
| for f in flights: | |
| rows.append([ | |
| f.get("callsign") or "—", | |
| f.get("type") or "—", | |
| f"{f.get('orig') or '?'} → {f.get('dest') or '?'}", | |
| f.get("alt") if f.get("alt") is not None else "—", | |
| f.get("gspeed") if f.get("gspeed") is not None else "—", | |
| f.get("eta_human") or "—", | |
| (f"{f.get('est_dist_km')} km →dest" if f.get("est_to_dest") else "—"), | |
| ]) | |
| return rows | |
| # ---- Gradio callbacks ------------------------------------------------------ | |
| def scan(region_name, custom_bounds, state): | |
| region = REGIONS.get(region_name) | |
| if custom_bounds and custom_bounds.strip(): | |
| try: | |
| n, s, w, e = [float(x) for x in custom_bounds.split(",")] | |
| region = (n, s, w, e) | |
| region_label = f"custom[{custom_bounds.strip()}]" | |
| except Exception: | |
| return ( | |
| "<div style='color:#ff5277'>Bounds must be: north,south,west,east</div>", | |
| [], "Bad bounds.", "", state, | |
| ) | |
| else: | |
| region_label = region_name | |
| if region is None: | |
| return ("<div style='color:#ff5277'>Pick a region.</div>", [], | |
| "No region.", "", state) | |
| n, s, w, e = region | |
| try: | |
| raw = fr24.live_positions(n, s, w, e) | |
| except fr24.FR24Error as ex: | |
| return (f"<div style='color:#ff5277'>FR24: {ex}</div>", [], str(ex), "", state) | |
| except Exception as ex: # noqa: BLE001 | |
| return (f"<div style='color:#ff5277'>Error: {ex}</div>", [], str(ex), "", state) | |
| flights = [] | |
| for r in raw[:MAX_FLIGHTS]: | |
| nf = normalize_flight(r) | |
| if nf: | |
| flights.append(nf) | |
| stamp = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ") | |
| html = globe_mod.build_globe_html(flights, stamp=stamp, focus=_focus_point(flights)) | |
| rows = _table_rows(flights) | |
| side = sidebar_mod.build_sidebar_html(flights) | |
| state = {"flights": flights, "region": region_label} | |
| status = f">> {len(flights)} contacts in {region_label} @ {stamp}" | |
| return html, rows, status, side, state | |
| import json as _json | |
| # An idle, rotating globe shown at startup and whenever a query yields no points. | |
| STANDBY_GLOBE = globe_mod.build_globe_html([], stamp="STANDBY // DRAG TO SPIN") | |
| def _read_trace(path): | |
| """Load a saved trace file for the trace-viewer panel.""" | |
| try: | |
| with open(path, encoding="utf-8") as fh: | |
| return _json.load(fh) | |
| except Exception as e: # noqa: BLE001 | |
| return {"trace_error": str(e), "path": path} | |
| def _render(flights, label): | |
| """Build globe + table + sidebar outputs for a set of normalized flights.""" | |
| stamp = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ") | |
| html = globe_mod.build_globe_html(flights, stamp=stamp, focus=_focus_point(flights)) | |
| rows = _table_rows(flights) | |
| side = sidebar_mod.build_sidebar_html(flights) | |
| return html, rows, side, stamp | |
| def assistant(query, state): | |
| """Primary entry point: the LLM agent decides + runs the FR24 call.""" | |
| if not query or not query.strip(): | |
| return (STANDBY_GLOBE, [], "Ask e.g. 'flights from London to Dubai'.", | |
| "<div id='fd-sidebar'></div>", state, | |
| "Type a flight query above.", {}) | |
| result = agent.run(query.strip()) | |
| flights = [nf for nf in (normalize_flight(r) for r in result["flights"]) if nf] | |
| if flights: | |
| # Globe magnifies/flies in to the result location (focus set below). | |
| html, rows, side, stamp = _render(flights, query) | |
| else: | |
| html, rows, side = STANDBY_GLOBE, [], "<div id='fd-sidebar'></div>" | |
| state = {"flights": flights, "region": query.strip()} | |
| trace_file = os.path.basename(result["trace_path"]) | |
| status = (f">> agent[{result['mode']}] tools={result['tool_calls'] or '—'} " | |
| f"→ {len(flights)} flights · trace {result['trace_id']}") | |
| answer_md = ( | |
| f"{result['answer']}\n\n" | |
| f"<small>`mode={result['mode']}` · `tools={result['tool_calls'] or 'none'}` " | |
| f"· `flights={len(flights)}` · trace saved → " | |
| f"`traces/{trace_file}`</small>") | |
| return html, rows, status, side, state, answer_md, _read_trace(result["trace_path"]) | |
| TRANSIT_EXAMPLES = [ | |
| "fastest way from Berkeley to SFO", | |
| "next train from Embarcadero", | |
| "traffic on the Bay Bridge", | |
| "fastest way from Oakland to San Jose", | |
| ] | |
| _EMPTY_MAP = ("<div style='height:560px;display:flex;align-items:center;" | |
| "justify-content:center;color:#FF9A1E;text-shadow:0 0 10px #FF7A00;" | |
| "border:1px solid rgba(255,154,30,0.35);border-radius:10px'>" | |
| "◌ AWAITING TRANSIT QUERY ◌</div>") | |
| def transit_assistant(query, t_state): | |
| """Bay Area transit agent: picks a 511 tool, runs it, recommends.""" | |
| if not query or not query.strip(): | |
| return (_EMPTY_MAP, "Ask e.g. 'fastest way from Berkeley to SFO'.", | |
| "Type a Bay Area transit query above.", t_state, {}) | |
| r = transit_agent.run(query.strip()) | |
| stamp = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%SZ") | |
| map_html = transit_map.build_map_html(r["markers"], stamp) if r["markers"] else _EMPTY_MAP | |
| trace_file = os.path.basename(r["trace_path"]) | |
| status = (f">> agent[{r['mode']}] tools={r['tool_calls'] or '—'} · " | |
| f"{len(r['markers'])} pins · trace {r['trace_id']}") | |
| answer_md = ( | |
| f"{r['answer']}\n\n" | |
| f"<small>`mode={r['mode']}` · `tools={r['tool_calls'] or 'none'}` · " | |
| f"trace saved → `traces/{trace_file}`</small>") | |
| t_state = {"transit_last": query.strip()} | |
| return map_html, status, answer_md, t_state, _read_trace(r["trace_path"]) | |
| # ---- Theme / CSS (glow-in-the-dark, 80's hacker) --------------------------- | |
| NEON_CSS = """ | |
| /* Palette is variable-driven so the transit tab can repaint by swapping vars. */ | |
| :root, .gradio-container { | |
| --neon: #39FF14; --neon-rgb: 57,255,20; | |
| --neon2: #00E5FF; --neon2-rgb: 0,229,255; | |
| --bg: #02060a; --bg2: #061a12; | |
| --panel-rgb: 2,18,12; --in-bg: #02100a; | |
| --btn-a: #0a3a22; --btn-b: #04150d; | |
| } | |
| /* Transit mode: glow-in-the-dark ORANGE on BLACK — just redefine the vars. */ | |
| .gradio-container.transit-mode { | |
| --neon: #FF9A1E; --neon-rgb: 255,154,30; | |
| --neon2: #FFC65C; --neon2-rgb: 255,198,92; | |
| --bg: #000000; --bg2: #1a0c00; | |
| --panel-rgb: 20,10,0; --in-bg: #160a00; | |
| --btn-a: #3a2200; --btn-b: #150b00; | |
| } | |
| .gradio-container, body, .gradio-container * { | |
| font-family: 'Courier New', Consolas, monospace !important; | |
| } | |
| .gradio-container { | |
| background: | |
| radial-gradient(circle at 20% 0%, var(--bg2) 0%, var(--bg) 55%), | |
| repeating-linear-gradient(0deg, rgba(var(--neon-rgb),0.035) 0px, | |
| rgba(var(--neon-rgb),0.035) 1px, transparent 1px, transparent 3px) !important; | |
| color: var(--neon) !important; | |
| } | |
| h1, h2, h3, label, .prose, span, p, li, strong, em { | |
| color: var(--neon) !important; | |
| text-shadow: 0 0 8px rgba(var(--neon-rgb),0.55) !important; | |
| } | |
| #title { font-size: 30px; letter-spacing: 4px; text-align:center; | |
| text-shadow: 0 0 12px var(--neon), 0 0 24px var(--neon) !important; } | |
| #subtitle { text-align:center; color: var(--neon2) !important; | |
| text-shadow: 0 0 8px var(--neon2) !important; letter-spacing:2px; } | |
| .block, .form, .gr-box, .gr-panel, .panel { | |
| background: rgba(var(--panel-rgb),0.66) !important; | |
| border: 1px solid rgba(var(--neon-rgb),0.35) !important; | |
| box-shadow: 0 0 14px rgba(var(--neon-rgb),0.18), inset 0 0 12px rgba(0,0,0,0.6) !important; | |
| border-radius: 10px !important; | |
| } | |
| input, textarea, select, .gr-input { | |
| background: var(--in-bg) !important; color: var(--neon) !important; | |
| border: 1px solid rgba(var(--neon-rgb),0.4) !important; | |
| text-shadow: 0 0 6px rgba(var(--neon-rgb),0.5) !important; | |
| } | |
| button { | |
| background: linear-gradient(180deg, var(--btn-a), var(--btn-b)) !important; | |
| color: var(--neon) !important; | |
| border: 1px solid var(--neon) !important; | |
| text-shadow: 0 0 8px var(--neon) !important; | |
| box-shadow: 0 0 12px rgba(var(--neon-rgb),0.4) !important; | |
| text-transform: uppercase; letter-spacing: 2px; | |
| } | |
| button:hover { box-shadow: 0 0 22px var(--neon), 0 0 8px var(--neon2) !important; } | |
| .gr-dataframe table { color: var(--neon) !important; } | |
| .gr-dataframe th { color: var(--neon2) !important; | |
| text-shadow: 0 0 6px var(--neon2) !important; } | |
| footer { display:none !important; } | |
| /* ---- sidebar (MOST TRACKED / AIRPORT DISRUPTIONS) ---- */ | |
| #fd-sidebar { display:flex; flex-direction:column; gap:10px; } | |
| .fd-panel { | |
| background: rgba(2,16,11,0.78); | |
| border: 1px solid rgba(57,255,20,0.35); | |
| border-radius: 10px; padding: 6px 8px 8px; | |
| box-shadow: 0 0 14px rgba(57,255,20,0.18), inset 0 0 12px rgba(0,0,0,0.6); | |
| } | |
| .fd-panel > summary { | |
| list-style:none; cursor:pointer; user-select:none; | |
| display:flex; align-items:center; gap:8px; | |
| font-size:12px; letter-spacing:2px; color:var(--neon); | |
| text-shadow:0 0 8px var(--neon); padding:4px 2px; | |
| } | |
| .fd-panel > summary::-webkit-details-marker { display:none; } | |
| .fd-chev { margin-left:auto; transition:transform .2s; } | |
| .fd-panel[open] > summary .fd-chev { transform:rotate(180deg); } | |
| .fd-live { | |
| font-size:9px; font-weight:bold; color:#02060a; | |
| background:var(--neon); padding:1px 5px; border-radius:3px; | |
| box-shadow:0 0 8px var(--neon); animation:fd-pulse 1.6s infinite; | |
| } | |
| @keyframes fd-pulse { 0%,100%{opacity:1} 50%{opacity:0.5} } | |
| .fd-note { font-size:9px; color:#5fe0a0; opacity:0.7; margin:2px 2px 6px; | |
| letter-spacing:0.5px; text-shadow:none; } | |
| .fd-list { display:flex; flex-direction:column; gap:6px; } | |
| .fd-item { | |
| background: rgba(0,0,0,0.35); | |
| border:1px solid rgba(57,255,20,0.18); | |
| border-left:3px solid var(--neon); | |
| border-radius:6px; padding:6px 8px; | |
| } | |
| .fd-item:hover { border-left-color:var(--neon2); | |
| box-shadow:0 0 10px rgba(0,229,255,0.25); } | |
| .fd-row { display:flex; align-items:center; gap:6px; } | |
| .fd-rank { color:var(--neon2); font-weight:bold; text-shadow:0 0 6px var(--neon2); } | |
| .fd-cs { font-weight:bold; color:var(--neon); text-shadow:0 0 6px var(--neon); } | |
| .fd-badge { | |
| font-size:9px; color:var(--neon2); border:1px solid rgba(0,229,255,0.5); | |
| border-radius:3px; padding:0 4px; text-shadow:0 0 5px var(--neon2); | |
| } | |
| .fd-metric { margin-left:auto; color:#FFD23F; text-shadow:0 0 6px #FFD23F; | |
| font-weight:bold; } | |
| .fd-metric small { font-size:8px; opacity:0.8; } | |
| .fd-route { font-size:11px; color:#9fffd0; opacity:0.85; margin-top:2px; | |
| text-shadow:none; } | |
| .fd-arrow { color:var(--neon2); } | |
| .fd-wx { display:flex; justify-content:space-between; font-size:11px; | |
| color:#9fffd0; margin-top:3px; text-shadow:none; } | |
| .fd-wind { color:var(--neon2); } | |
| .fd-scores { display:flex; justify-content:space-between; font-size:11px; | |
| margin-top:3px; color:var(--neon); } | |
| .fd-dot { font-weight:bold; } | |
| .fd-empty { font-size:11px; color:#5fe0a0; opacity:0.7; padding:6px; | |
| text-align:center; } | |
| .fd-hint { font-size:11px; color:#5fe0a0; opacity:0.8; margin:2px 0 8px; | |
| letter-spacing:0.5px; } | |
| .fd-hint code { color:var(--neon2); } | |
| /* Transit mode is driven entirely by the variable swap above; just a couple | |
| of orange accents that reference the (now-orange) vars. */ | |
| #transit_hint { font-size:11px; color:var(--neon2); opacity:0.9; margin:2px 0 8px; } | |
| #transit_hint code { color:var(--neon); } | |
| """ | |
| theme = gr.themes.Base( | |
| primary_hue=gr.themes.colors.green, | |
| secondary_hue=gr.themes.colors.cyan, | |
| neutral_hue=gr.themes.colors.gray, | |
| ).set( | |
| body_background_fill="#02060a", | |
| body_text_color="#39FF14", | |
| block_background_fill="rgba(2,18,12,0.66)", | |
| ) | |
| # Force the UI into dark mode on load (FLIGHTDECK is dark-only). | |
| FORCE_DARK_JS = """ | |
| function() { | |
| const url = new URL(window.location); | |
| if (url.searchParams.get('__theme') !== 'dark') { | |
| url.searchParams.set('__theme', 'dark'); | |
| window.location.replace(url.href); | |
| } | |
| } | |
| """ | |
| EXAMPLE_QUERIES = [ | |
| "flights from London to Dubai", | |
| "arrivals into JFK", | |
| "departures from LAX", | |
| "flights from Tokyo to Singapore", | |
| ] | |
| _JS_FLIGHT = ("() => document.querySelector('.gradio-container')" | |
| ".classList.remove('transit-mode')") | |
| _JS_TRANSIT = ("() => document.querySelector('.gradio-container')" | |
| ".classList.add('transit-mode')") | |
| with gr.Blocks(title="FLIGHTDECK", theme=theme, css=NEON_CSS, js=FORCE_DARK_JS) as demo: | |
| state = gr.State({}) | |
| transit_state = gr.State({}) | |
| with gr.Tabs(): | |
| # ================= TAB 1: FLIGHTDECK (green) ================= | |
| with gr.Tab("✈ FLIGHTDECK", elem_id="flight_tab") as flight_tab: | |
| gr.HTML("<div id='title'>▚ F L I G H T D E C K ▞</div>" | |
| "<div id='subtitle'>ai flight agent // 3D globe // live FR24 uplink</div>") | |
| with gr.Row(): | |
| question = gr.Textbox( | |
| label="ASK THE FLIGHT AGENT", scale=5, autofocus=True, | |
| placeholder="flights from London to Dubai · arrivals into JFK · " | |
| "departures from LAX") | |
| ask_btn = gr.Button("▶ DISPATCH", scale=1, variant="primary") | |
| with gr.Row(): | |
| example_btns = [gr.Button(q, size="sm") for q in EXAMPLE_QUERIES] | |
| gr.HTML("<div class='fd-hint'>scope: live flights TO / FROM an airport or " | |
| "on a route only · drag to spin the globe · all traces are saved " | |
| "to a dataset with almost the same name as this Space — look for it " | |
| "in the Hackathon Datasets</div>") | |
| answer = gr.Markdown(f"**UPLINK:** {liquid.status()} — ask a flight " | |
| "question above to dispatch the agent.") | |
| with gr.Row(): | |
| with gr.Column(scale=1, min_width=240): | |
| sidebar_out = gr.HTML( | |
| "<div id='fd-sidebar'><div class='fd-empty'>◌ awaiting agent ◌</div></div>") | |
| status = gr.Textbox(label="AGENT STATUS", interactive=False) | |
| with gr.Accordion("⛭ MANUAL OVERRIDE (region scan)", open=False): | |
| region = gr.Dropdown( | |
| choices=list(REGIONS.keys()), value="Europe", label="SECTOR") | |
| custom = gr.Textbox( | |
| label="CUSTOM BOUNDS (north,south,west,east)", | |
| placeholder="e.g. 51.7,51.2,-0.6,0.3") | |
| scan_btn = gr.Button("◈ SCAN AIRSPACE") | |
| with gr.Column(scale=3): | |
| globe_out = gr.HTML(STANDBY_GLOBE) | |
| table = gr.Dataframe( | |
| headers=["CALLSIGN", "TYPE", "ROUTE", "ALT ft", "GS kt", "ETA", "EST. PATH"], | |
| label="CONTACTS", interactive=False, wrap=True) | |
| with gr.Accordion("🧾 AGENT TRACE (reasoning · tool call · API URL)", | |
| open=False): | |
| trace_view = gr.JSON(label="last run — saved to ./traces/") | |
| # ================= TAB 2: BAY TRANSIT (orange) ================= | |
| with gr.Tab("🚉 BAY TRANSIT", elem_id="transit_tab") as transit_tab: | |
| gr.HTML("<div id='title'>◢ B A Y L I N E ◣</div>" | |
| "<div id='subtitle'>bay area transit agent // 511 live data // " | |
| "fastest-route reasoning</div>") | |
| with gr.Row(): | |
| t_question = gr.Textbox( | |
| label="ASK THE TRANSIT AGENT", scale=5, | |
| placeholder="fastest way from Berkeley to SFO · next train from " | |
| "Embarcadero · traffic on the Bay Bridge") | |
| t_ask = gr.Button("▶ DISPATCH", scale=1, variant="primary") | |
| with gr.Row(): | |
| t_example_btns = [gr.Button(q, size="sm") for q in TRANSIT_EXAMPLES] | |
| gr.HTML("<div id='transit_hint'>scope: Bay Area transit & traffic only · " | |
| "powered by <code>511.org</code> live data · the agent reasons " | |
| "the fastest option · all traces are saved to a dataset with almost " | |
| "the same name as this Space — look for it in the Hackathon Datasets</div>") | |
| t_answer = gr.Markdown("**511 UPLINK ready** — ask a Bay Area transit " | |
| "question above.") | |
| t_status = gr.Textbox(label="AGENT STATUS", interactive=False) | |
| t_map = gr.HTML(_EMPTY_MAP) | |
| with gr.Accordion("🧾 AGENT TRACE (reasoning · tool call · API URL)", | |
| open=False): | |
| t_trace_view = gr.JSON(label="last run — saved to ./traces/") | |
| # ---- theme switch on tab select ---- | |
| flight_tab.select(None, None, None, js=_JS_FLIGHT) | |
| transit_tab.select(None, None, None, js=_JS_TRANSIT) | |
| # ---- flight tab wiring ---- | |
| agent_outputs = [globe_out, table, status, sidebar_out, state, answer, trace_view] | |
| ask_btn.click(assistant, [question, state], agent_outputs) | |
| question.submit(assistant, [question, state], agent_outputs) | |
| for btn, q in zip(example_btns, EXAMPLE_QUERIES): | |
| btn.click(lambda q=q: q, None, question).then( | |
| assistant, [question, state], agent_outputs) | |
| scan_btn.click(scan, [region, custom, state], | |
| [globe_out, table, status, sidebar_out, state]) | |
| # ---- transit tab wiring ---- | |
| transit_outputs = [t_map, t_status, t_answer, transit_state, t_trace_view] | |
| t_ask.click(transit_assistant, [t_question, transit_state], transit_outputs) | |
| t_question.submit(transit_assistant, [t_question, transit_state], transit_outputs) | |
| for btn, q in zip(t_example_btns, TRANSIT_EXAMPLES): | |
| btn.click(lambda q=q: q, None, t_question).then( | |
| transit_assistant, [t_question, transit_state], transit_outputs) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| # 0.0.0.0 so HuggingFace Spaces' proxy can reach the app (127.0.0.1 | |
| # only binds localhost inside the container and the Space won't load). | |
| server_name=os.environ.get("HOST", "0.0.0.0"), | |
| server_port=int(os.environ.get("PORT", "7860")), | |
| ) | |