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 <details> elements, the index
  route pre-resolves intro_open / picker_open from the query and renders
  the <details open> attribute accordingly.
This commit is contained in:
Michael Pilosov 2026-04-22 18:14:29 -06:00
parent ba7eef9df0
commit 3a951b387a
3 changed files with 80 additions and 14 deletions

View File

@ -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 <details> 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,
},
)

View File

@ -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;

View File

@ -35,7 +35,7 @@
</div>
</header>
<details class="dataset-picker intro" id="intro">
<details class="dataset-picker intro" id="intro"{% if intro_open %} open{% endif %}>
<summary>
<span class="picker-meta">
<span class="section-number">§ 0</span>
@ -164,7 +164,7 @@
</div>
</details>
<details class="dataset-picker" id="picker" open>
<details class="dataset-picker" id="picker"{% if picker_open %} open{% endif %}>
<summary>
<span class="picker-meta">
<span class="section-number">§ 1</span>
@ -186,25 +186,23 @@
<div class="picker-controls">
<span class="ctl-label">n samples</span>
<div class="segmented count-5" role="radiogroup" aria-label="number of samples">
<label><input type="radio" name="n" value="100"><span>100</span></label>
<label><input type="radio" name="n" value="500" checked><span>500</span></label>
<label><input type="radio" name="n" value="1000"><span>1,000</span></label>
<label><input type="radio" name="n" value="2500"><span>2,500</span></label>
<label><input type="radio" name="n" value="5000"><span>5,000</span></label>
{% for v, label in [('100','100'),('500','500'),('1000','1,000'),('2500','2,500'),('5000','5,000')] %}
<label><input type="radio" name="n" value="{{ v }}"{% if initial_radios.n == v %} checked{% endif %}><span>{{ label }}</span></label>
{% endfor %}
</div>
<span class="ctl-label">noise σ</span>
<div class="segmented count-3" role="radiogroup" aria-label="noise σ">
<label><input type="radio" name="j" value="0.001"><span>0.001</span></label>
<label><input type="radio" name="j" value="0.005" checked><span>0.005</span></label>
<label><input type="radio" name="j" value="0.01"><span>0.010</span></label>
{% for v, label in [('0.001','0.001'),('0.005','0.005'),('0.01','0.010')] %}
<label><input type="radio" name="j" value="{{ v }}"{% if initial_radios.j == v %} checked{% endif %}><span>{{ label }}</span></label>
{% endfor %}
</div>
<span class="ctl-label">timesteps</span>
<div class="segmented count-3" role="radiogroup" aria-label="number of timesteps">
<label><input type="radio" name="f" value="12"><span>12</span></label>
<label><input type="radio" name="f" value="24" checked><span>24</span></label>
<label><input type="radio" name="f" value="48"><span>48</span></label>
{% for v in ['12','24','48'] %}
<label><input type="radio" name="f" value="{{ v }}"{% if initial_radios.f == v %} checked{% endif %}><span>{{ v }}</span></label>
{% endfor %}
</div>
</div>
@ -499,7 +497,7 @@
</footer>
<script src="/static/theme.js?v=11"></script>
<script type="module" src="/static/dataset-picker.js?v=11"></script>
<script type="module" src="/static/dataset-picker.js?v=12"></script>
<script type="module" src="/static/metrics.js?v=11"></script>
<script src="/static/compare-select.js?v=4"></script>
<script src="/static/runs-filter.js?v=6"></script>