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:
parent
ba7eef9df0
commit
3a951b387a
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user