From 3a951b387a7d9ba03f02a120bce7478d968b1089 Mon Sep 17 00:00:00 2001 From: Michael Pilosov Date: Wed, 22 Apr 2026 18:14:29 -0600 Subject: [PATCH] homepage: persist intro/picker open state + dataset/N/T/J in URL query - dataset-picker.js writes a compact query string (?ds=&n=&f=&j= plus intro=1/picker=0 when non-default) on every change and reads it on init. Refresh restores the page; the URL also works as a shareable deep-link. - To avoid a first-paint flicker of the
elements, the index route pre-resolves intro_open / picker_open from the query and renders the
attribute accordingly. --- app/web/main.py | 15 +++++++++ app/web/static/dataset-picker.js | 53 ++++++++++++++++++++++++++++++++ app/web/templates/index.html | 26 ++++++++-------- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/app/web/main.py b/app/web/main.py index bdb62c9..705a9ff 100644 --- a/app/web/main.py +++ b/app/web/main.py @@ -747,6 +747,18 @@ async def index(request: Request) -> HTMLResponse: dep_id = await PREFECT.deployment_id(client) views = [_run_view(r) for r in runs] _mark_stale_views(views) + # Pre-resolve the two
sections' open state from the URL so + # first paint matches (no flash). Intro defaults closed, picker open. + q = request.query_params + intro_open = q.get("intro") == "1" + picker_open = q.get("picker") != "0" + # Also pre-resolve the radio-group selections so n/f/j render with the + # correct `checked` attribute on first paint. + initial_radios = { + "n": q.get("n") or "500", + "f": q.get("f") or "24", + "j": q.get("j") or "0.005", + } return templates.TemplateResponse( request, "index.html", @@ -757,6 +769,9 @@ async def index(request: Request) -> HTMLResponse: "runs": views, "deployment_id": dep_id, "prefect_api": PREFECT_API, + "intro_open": intro_open, + "picker_open": picker_open, + "initial_radios": initial_radios, }, ) diff --git a/app/web/static/dataset-picker.js b/app/web/static/dataset-picker.js index 22a6661..6c6b00f 100644 --- a/app/web/static/dataset-picker.js +++ b/app/web/static/dataset-picker.js @@ -234,6 +234,7 @@ async function main() { selectedPath.textContent = ds.path; hidden.datasetId.value = id; updateContinue(); + updateUrlState(); } document.addEventListener('themechange', () => { @@ -292,6 +293,58 @@ async function main() { }); applyF(parseInt(document.querySelector('input[name="f"]:checked').value, 10)); + // ---- URL state persistence ------------------------------------------- + // Intro/picker open state, selected dataset, and n/f/j radios sync into + // the query string so a refresh restores the page. Intro defaults to + // closed (no param); picker defaults to open (no param). + function updateUrlState() { + const p = new URLSearchParams(); + const introEl = document.getElementById('intro'); + const pickerEl = document.getElementById('picker'); + if (introEl && introEl.open) p.set('intro', '1'); + if (pickerEl && !pickerEl.open) p.set('picker', '0'); + if (selectedId) p.set('ds', selectedId); + for (const name of ['n', 'f', 'j']) { + const r = document.querySelector(`input[name="${name}"]:checked`); + if (r) p.set(name, r.value); + } + const qs = p.toString(); + history.replaceState(null, '', qs ? `?${qs}` : window.location.pathname); + } + + (function applyUrlState() { + const u = new URLSearchParams(window.location.search); + const introEl = document.getElementById('intro'); + if (introEl && u.has('intro')) introEl.open = u.get('intro') === '1'; + const pickerEl = document.getElementById('picker'); + if (pickerEl && u.has('picker')) pickerEl.open = u.get('picker') !== '0'; + const appliers = { n: applyN, f: applyF, j: applyJ }; + for (const name of ['n', 'f', 'j']) { + const val = u.get(name); + if (val == null) continue; + const r = document.querySelector(`input[name="${name}"][value="${val}"]`); + if (!r) continue; + r.checked = true; + appliers[name](name === 'j' ? parseFloat(val) : parseInt(val, 10)); + } + const dsId = u.get('ds'); + if (dsId && data[dsId]) { + const idx = order.findIndex(([id]) => id === dsId); + const card = idx >= 0 ? gallery.children[idx] : null; + if (card) selectCard(dsId, card, data[dsId]); + } + })(); + + for (const id of ['intro', 'picker']) { + const el = document.getElementById(id); + if (el) el.addEventListener('toggle', updateUrlState); + } + for (const name of ['n', 'f', 'j']) { + document.querySelectorAll(`input[name="${name}"]`).forEach((input) => + input.addEventListener('change', updateUrlState), + ); + } + function selectByIndex(idx, { scroll = true } = {}) { const entry = order[idx]; if (!entry) return; diff --git a/app/web/templates/index.html b/app/web/templates/index.html index 5e2e0fb..51294ff 100644 --- a/app/web/templates/index.html +++ b/app/web/templates/index.html @@ -35,7 +35,7 @@ -
+
§ 0 @@ -164,7 +164,7 @@
-
+
§ 1 @@ -186,25 +186,23 @@
n samples
- - - - - + {% for v, label in [('100','100'),('500','500'),('1000','1,000'),('2500','2,500'),('5000','5,000')] %} + + {% endfor %}
noise σ
- - - + {% for v, label in [('0.001','0.001'),('0.005','0.005'),('0.01','0.010')] %} + + {% endfor %}
timesteps
- - - + {% for v in ['12','24','48'] %} + + {% endfor %}
@@ -499,7 +497,7 @@ - +