quaz93's picture
Remove ./traces/ mention from hint text
1174503
Raw
History Blame Contribute Delete
22.1 kB
"""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")),
)