Spaces:
Running
Running
File size: 9,745 Bytes
a1f52bf c5672b8 a1f52bf 9fae83d 69bd160 9fae83d a1f52bf 9fae83d a1f52bf 9fae83d a1f52bf 69bd160 9fae83d a1f52bf c5672b8 b5e3689 c5672b8 a1f52bf 9fae83d a1f52bf c5672b8 a1f52bf 9fae83d 69bd160 c5672b8 a1f52bf 8c80df3 a1f52bf 9fae83d 69bd160 9fae83d a1f52bf 9fae83d 8c80df3 a1f52bf 9fae83d 8c80df3 69bd160 9fae83d c5672b8 69bd160 a1f52bf 8c80df3 91f98a7 9fae83d a1f52bf 69bd160 9fae83d a1f52bf 69bd160 9fae83d a1f52bf 9fae83d 69bd160 a1f52bf 91f98a7 a1f52bf 91f98a7 a1f52bf 91f98a7 a1f52bf 9fae83d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | """wave~reader v2 β ONE page: map lens β spot story β intel rail + agent.
No tabs. One shared ``selected`` spot drives everything: picking it in
the search box (or the load default) re-tells the whole page β badges,
week strip, world model, climatology β then auto-fetches
the staged forecast, and the agent reads the same context. Exactly three
session states: the shared pick, the scored payload, the agent store.
Filters are one-way only.
All component-output ordering comes from :mod:`ui.contracts` β handlers
yield via ``fill(KEYS, β¦)`` and the wiring assembles outputs the same
way, so panels and wiring can't drift apart. On load, a background
thread warms caches for featured breaks (the default spot fetches live).
"""
from __future__ import annotations
import threading
import gradio as gr
from ui import _compat as C
from ui.contracts import AGENT_KEYS, FETCH_KEYS, SELECT_KEYS
from ui.panels import agent as agent_panel
from ui.panels import intel as intel_panel
from ui.panels import lens as lens_panel
from ui.panels import story as story_panel
_DEFAULT_SPOT = "Bells Beach"
_FEATURED = ["Snapper Rocks", "Bondi Beach", "Shipstern Bluff", "Kirra"]
_MODE_FORECAST = "π forecast"
_MODE_AGENT = "π€ agentic mode"
def _warm_caches(records: list[dict]) -> None:
"""Pre-fetch forecast + GEBCO grids for featured breaks (daemon thread)."""
def work():
for name in _FEATURED:
b = C.find_break(records, name)
if b is None:
continue
try:
C.get_scored_week(b, skill="intermediate", days=7)
except Exception: # noqa: BLE001 β warming must never break the UI
pass
try:
C.get_seafloor(b)
except Exception: # noqa: BLE001
pass
threading.Thread(target=work, daemon=True).start()
def build() -> gr.Blocks:
"""Assemble the demo (no launch β the Space entrypoint launches)."""
records = C.list_breaks()
vocab = C.index_breaks(records)
with gr.Blocks(title="wave~reader β one page to your next surf") as demo:
# Hero band: title + mode toggle on one line, engine chips (with
# the MCP note folded in) as a single compact strip underneath.
with gr.Column(elem_classes=["hero-band"]):
with gr.Row(elem_classes=["hero-row"]):
with gr.Column(scale=5, min_width=220):
gr.Markdown("# wave~reader", elem_classes=["hero-title"])
gr.Markdown("smart forecasting for australia's coast β 238 breaks scored "
"hour by hour (0β10) from live marine data, real seafloor data and "
"each spot's ideal conditions.", elem_classes=["hero-sub"])
with gr.Column(scale=2, min_width=280, elem_classes=["hero-right"]):
mode_toggle = gr.Radio(
[_MODE_FORECAST, _MODE_AGENT], value=_MODE_FORECAST,
show_label=False, elem_classes=["mode-toggle"],
container=False)
gr.Markdown(
C.engine_strip(
"mcp ready β <code>/gradio_api/mcp/</code> Β· "
"<code>score_week</code> / <code>rank_region_week</code> / "
"<code>explain_score</code>"),
elem_classes=["engine-md"])
selected = gr.State(None)
scored = gr.State(None)
agent_store = gr.State(agent_panel.fresh_store())
# Two full views share the page: the forecast dashboard and the
# agent interface. The header toggle swaps them wholesale; both
# stay wired so switching is instant (no refetch).
with gr.Column(visible=True, elem_classes=["main-col"]) as main_col:
with gr.Row():
b = lens_panel.build_lens(records, vocab, selected)
s = story_panel.build_story(selected, scored)
with gr.Column(scale=3):
i = intel_panel.build_intel()
a = agent_panel.build_agent(selected, agent_store, records, vocab, visible=False)
# Footer: the machinery line β feed/scorer/world-model latencies β
# lives below the fold, in both modes (outside the swapped columns).
with gr.Column(elem_classes=["page-foot"]):
f = story_panel.build_footer()
def _switch_mode(mode: str):
agentic = mode == _MODE_AGENT
return gr.update(visible=not agentic), gr.update(visible=agentic)
mode_toggle.change(_switch_mode, inputs=mode_toggle,
outputs=[main_col, a["agent_col"]], api_name=False)
# Internal listeners stay off the API/MCP surface β only the three
# typed ``gr.api`` tools (score_week / rank_region_week /
# explain_score) are exposed.
comp = {**b, **s, **i, **a, **f,
"selected": selected, "scored": scored, "agent_store": agent_store}
select_outputs = [comp[k] for k in SELECT_KEYS]
fetch_outputs = [comp[k] for k in FETCH_KEYS]
agent_outputs = [comp[k] for k in AGENT_KEYS]
def _on_load():
"""First paint already tells a story: warm caches + default spot."""
_warm_caches(records)
record = C.find_break(records, _DEFAULT_SPOT)
return b["select_spot"](record, None, fly=False)
demo.load(_on_load, inputs=None, outputs=select_outputs, api_name=False).then(
s["fetch_forecast"], inputs=[selected, b["skill_dd"]], outputs=fetch_outputs,
show_progress="minimal", api_name=False,
)
# -- lens: one-way filters reframe the map + search choices --
b["state_dd"].change(
b["state_regions"], inputs=b["state_dd"], outputs=b["region_dd"],
api_name=False,
).then(
b["filter_changed"], inputs=[b["state_dd"], b["region_dd"], b["skill_dd"]],
outputs=[b["map_plot"], b["search_dd"]], api_name=False,
)
b["region_dd"].change(
b["filter_changed"], inputs=[b["state_dd"], b["region_dd"], b["skill_dd"]],
outputs=[b["map_plot"], b["search_dd"]], api_name=False,
)
# the Skill filter doubles as the scoring tier: refilter the map,
# then rescore the picked spot's week at that level
b["skill_dd"].change(
b["filter_changed"], inputs=[b["state_dd"], b["region_dd"], b["skill_dd"]],
outputs=[b["map_plot"], b["search_dd"]], api_name=False,
).then(
s["fetch_forecast"], inputs=[selected, b["skill_dd"]], outputs=fetch_outputs,
show_progress="minimal", api_name=False,
)
# -- picking a spot re-tells the page, then auto-fetches its week --
b["search_dd"].change(
b["pick_by_name"],
inputs=[b["search_dd"], b["state_dd"], b["region_dd"], b["skill_dd"]],
outputs=select_outputs, api_name=False,
).then(
s["fetch_forecast"], inputs=[selected, b["skill_dd"]], outputs=fetch_outputs,
show_progress="minimal", api_name=False,
)
# -- intel rail: pill tabs swap the three feature columns --
i["tab_picker"].change(i["pick"], inputs=i["tab_picker"],
outputs=[i["report_col"], i["world_col"], i["climate_col"]],
api_name=False)
# the report button lives in the intel rail; its handler stays story-side
i["narrate_btn"].click(s["narrate"], inputs=scored, outputs=i["report_md"],
show_progress="minimal", api_name=False)
# -- agent: chat streams trace cards + meter + verdict + charts --
chat_inputs = [a["msg"], a["chatbot"], b["skill_dd"], a["token_box"],
selected, agent_store, a["steps_slider"], a["state_dd"],
a["region_dd"]]
a["send_btn"].click(
a["chat"], inputs=chat_inputs, outputs=agent_outputs,
show_progress="minimal", api_name=False,
)
a["msg"].submit(
a["chat"], inputs=chat_inputs, outputs=agent_outputs,
show_progress="minimal", api_name=False,
)
a["spot_dd"].change(
a["show_spot"], inputs=[a["spot_dd"], agent_store],
outputs=[a["agent_strip"]], api_name=False,
)
a["clear_btn"].click(a["clear"], inputs=None, outputs=agent_outputs,
api_name=False)
# Chips fill AND send in one click (set msg, then run the chat chain).
for chip, prompt in zip(a["chips"], agent_panel.CHIP_PROMPTS):
chip.click(a["chip_text"](prompt), inputs=None, outputs=[a["msg"]],
api_name=False).then(
a["chat"], inputs=chat_inputs, outputs=agent_outputs,
show_progress="minimal", api_name=False,
)
# -- agent filters: page-one pattern, State rewrites Region choices --
a["state_dd"].change(
a["state_regions"], inputs=a["state_dd"], outputs=a["region_dd"],
api_name=False,
)
# -- agent context line: what the bar inherits from the page --
ctx_outputs = [a["ctx_md"]]
ctx_inputs = [b["skill_dd"], selected, a["steps_slider"],
a["state_dd"], a["region_dd"]]
for trigger in (selected, b["skill_dd"], a["steps_slider"],
a["state_dd"], a["region_dd"]):
trigger.change(
a["ctx"], inputs=ctx_inputs,
outputs=ctx_outputs, api_name=False,
)
return demo
|