Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,13 @@ def test_map_matches_simulate_at_a_grid_point():


def test_static_assets_served():
for path in ("/static/app.js", "/static/style.css", "/static/replay.js", "/static/vessel3d.js",
for path in ("/static/app.js", "/static/workspaces.js", "/static/style.css", "/static/replay.js", "/static/vessel3d.js",
"/static/vendor/three.module.min.js", "/static/vendor/OrbitControls.js"):
assert client.get(path).status_code == 200, path
assert "fusionlab:tab" in client.get("/static/app.js").text
assert client.get("/static/vendor/plotly.min.js").status_code == 200
html = client.get("/").text
assert 'id="tab-replay"' in html and 'id="tab-db"' in html and 'id="tab-sandbox"' in html
assert 'id="tab-replay"' in html and 'id="tab-compare"' in html and 'id="tab-explore"' in html
# the demo never needs the network: no CDN in the page or in any script of ours (vendored libraries aside)
ours = [html] + [p.read_text() for p in sorted(WEB.glob("*.js")) + sorted(WEB.glob("*.json"))]
for text in ours:
Expand Down
4 changes: 3 additions & 1 deletion tests/test_web_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ def test_failed_shot_switch_reverts_the_select():
def test_fire_and_forget_panels_render_their_errors():
"""JS-07: the /db and /surrogate fetches carry the figures.js one-line .catch."""
replay = _served("replay.js")
assert ".catch(() => { $('tab-db').innerHTML" in replay
# /db failure: only the catalog-clouds panel blanks — the comparison bar and the search live in the
# same Compare workspace and must survive a /db outage.
assert ".catch(() => { const el = $('rp-clouds'); if (el) el.innerHTML" in replay
assert ".catch(() => { const el = $('rp-sur'); if (el) el.innerHTML" in replay


Expand Down
2 changes: 1 addition & 1 deletion web/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@
const j = await r.json().catch(() => null);
if (!r.ok) { status.textContent = (j && j.detail) || `accept failed (${r.status})`; status.className = 'small agent-line-no'; btn.disabled = false; return; }
host.innerHTML = '';
status.textContent = `Accepted — saved to data/protocols/${j.protocol_id || pid}.json. An accepted protocol fills the sandbox form step by step, each step still confirmed.`;
status.textContent = `Accepted — saved to data/protocols/${j.protocol_id || pid}.json. An accepted protocol fills the Explore form step by step, each step still confirmed.`;
status.className = 'small agent-line-ok';
} catch (e) {
status.textContent = 'the accept request failed: ' + (e && e.message || e);
Expand Down
21 changes: 11 additions & 10 deletions web/app.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// FusionLab shell (tabs) + Sandbox tab: what-if sliders on the reduced 0D model.
// Static file, no bundler. The Replay tab lives in replay.js and listens for 'fusionlab:tab'.
// FusionLab shell + Explore workspace (the reduced 0D model): what-if sliders on device presets.
// Static file, no bundler. Navigation lives in workspaces.js (the router, loaded first); the Replay
// workspace lives in replay.js. Both listen for 'fusionlab:tab'.
(() => {
'use strict';
const $ = (id) => document.getElementById(id);
Expand All @@ -10,15 +11,15 @@
const GAUGE_SPAN = 1.25; // gauge full scale (the 1.0 tick sits at 80%, see style.css)
let devices = {}, simSeq = 0, mapSeq = 0, mapReady = false;

// ---------------------------------------------------------------- tabs
// ---------------------------------------------------------------- workspaces
// The router (workspaces.js) owns which section is visible and the URL grammar. showTab keeps its
// name and event contract for guide.js / agent.js (they call it and listen for 'fusionlab:tab') —
// it is now a thin alias that writes the hash, the one navigation act.
function showTab(name) {
document.querySelectorAll('.tabs button').forEach((b) => b.classList.toggle('active', b.dataset.tab === name));
document.querySelectorAll('section[id^="tab-"]').forEach((s) => { s.hidden = s.id !== 'tab-' + name; });
document.dispatchEvent(new CustomEvent('fusionlab:tab', { detail: name }));
window.FusionLabRouter.navigate(name);
}
document.querySelectorAll('.tabs button').forEach((b) => b.addEventListener('click', () => showTab(b.dataset.tab)));
document.addEventListener('fusionlab:tab', (e) => {
if (e.detail === 'sandbox' && mapReady && window.Plotly) Plotly.Plots.resize($('map'));
if (e.detail === 'explore' && mapReady && window.Plotly) Plotly.Plots.resize($('map'));
});

// ---------------------------------------------------------------- helpers
Expand Down Expand Up @@ -167,6 +168,6 @@
},
});

// Replay (real shots) is the landing tab; /#sandbox opens the what-if sliders directly.
if (location.hash !== '#sandbox') showTab('replay');
// Boot navigation is the router's job (workspaces.js): it applies the deep link or the
// landing workspace (last visited, else the welcome-modal pick, else Replay).
})();
2 changes: 1 addition & 1 deletion web/figures.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@
}

// ---------------------------------------------------------------- n · T · tau_E
const PRESET = { // ITER: this app's sandbox baseline (physics.simulate). MAST: shot #30166 at t = 0.27 s (n̄e, Te0, W/P_loss).
const PRESET = { // ITER: this app's Explore baseline (physics.simulate). MAST: shot #30166 at t = 0.27 s (n̄e, Te0, W/P_loss).
iter: { n: 1.015, T: 7.6, tau: 3.81, label: 'ITER: this app’s model, volume-average T' },
mast: { n: 0.386, T: 1.22, tau: 0.049, label: 'MAST #30166 at 0.27 s: measured, core electron T' },
};
Expand Down
2 changes: 1 addition & 1 deletion web/glossary.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_about": "One sentence per term, shared by the guided study (hover on a dotted word) and the lab (hover on a panel label, help line under a slider). 'on' lists data-guide names that get this definition as their tooltip; 'help' is the line under a sandbox slider. Physics statements follow docs/PHYSICS.md.",
"_about": "One sentence per term, shared by the guided study (hover on a dotted word) and the lab (hover on a panel label, help line under a slider). 'on' lists data-guide names that get this definition as their tooltip; 'help' is the line under an Explore slider. Physics statements follow docs/PHYSICS.md.",
"terms": {
"plasma": { "name": "Plasma", "def": "A gas hot enough that electrons have left their atoms, so it conducts electricity and can be pushed and held by magnetic fields." },
"kev": { "name": "keV", "def": "The temperature unit used in fusion: 1 keV is about 11.6 million °C. A burning D-T plasma needs roughly 10 keV." },
Expand Down
16 changes: 14 additions & 2 deletions web/guide.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
['mouseover', 'focusin'].forEach((ev) => document.addEventListener(ev, (e) => { const t = e.target.closest && e.target.closest('.term'); if (t) showTip(t); }));
['mouseout', 'focusout'].forEach((ev) => document.addEventListener(ev, (e) => { if (e.target.closest && e.target.closest('.term')) tip.hidden = true; }));

// Lab-mode help, from the same glossary: tooltips on panel labels, one line under each sandbox slider, "?" back to a lesson.
// Lab-mode help, from the same glossary: tooltips on panel labels, one line under each Explore slider, "?" back to a lesson.
function labHelp() {
Object.values(G).forEach((g) => (g.on || []).forEach((name) => { const el = target(name); if (el && !el.title) el.title = `${g.name}: ${g.def}`; }));
const KNOB = { Ip: 'ip', B: 'bt', n: 'density', P_aux: 'heating', H: 'h-factor', Zeff: 'zeff' };
Expand Down Expand Up @@ -107,7 +107,7 @@
const keep = new Set(); // the lit elements and every ancestor up to the tab
inTab.forEach((el) => { for (let n = el; n && n !== section.parentNode; n = n.parentNode) keep.add(n); });
const dock = parseFloat(getComputedStyle(document.body).getPropertyValue('--dock')) || 0, r = inTab[0].getBoundingClientRect();
if (r.top < 0 || r.top > innerHeight - dock - 80) inTab[0].scrollIntoView({ block: 'start', behavior: 'smooth' }); // its top is off screen (pages that scroll)
if (r.top < 0 || r.top > innerHeight - dock - 80) inTab[0].scrollIntoView({ block: 'start', behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' }); // its top is off screen (pages that scroll)
keep.forEach((node) => {
if (inTab.includes(node)) return; // a lit element keeps all of its children
[...node.children].forEach((c) => {
Expand Down Expand Up @@ -246,6 +246,18 @@
modeSeg.addEventListener('click', (e) => { const b = e.target.closest('button'); if (b) setMode(b.dataset.v); });
welcome.addEventListener('click', (e) => { const b = e.target.closest('[data-mode]'); if (b) setMode(b.dataset.mode); });

// ---------------------------------------------------------------- the command palette's hook
// The palette (web/palette.js) starts or resumes the study through this — the same jump the per-panel
// "?" buttons take (find the lesson, setMode). Additive: existing FusionLab keys untouched.
window.FusionLab = Object.assign(window.FusionLab || {}, { guided: {
start(lessonId) {
if (!L) return false; // lessons not loaded (boot failed): the palette reports it, nothing breaks
if (lessonId != null) { const i = L.steps.findIndex((s) => s.id === lessonId); if (i >= 0) idx = i; }
setMode('guided');
return true;
},
} });

async function boot() {
const [lessons, glossary] = await Promise.all(['lessons', 'glossary'].map((f) => fetch(`/static/${f}.json`).then((r) => r.json())));
L = lessons; G = glossary.terms;
Expand Down
24 changes: 15 additions & 9 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
<script type="importmap">{"imports":{"three":"/static/vendor/three.module.min.js"}}</script>
<script src="/static/vendor/plotly.min.js" defer></script>
<script type="module" src="/static/vessel3d.js"></script>
<script src="/static/workspaces.js" defer></script>
<script src="/static/app.js" defer></script>
<script src="/static/shotload.js" defer></script>
<script src="/static/replay.js" defer></script>
<script src="/static/figures.js" defer></script>
<script src="/static/guide.js" defer></script>
<script src="/static/palette.js" defer></script>
</head>
<body>
<header>
Expand All @@ -26,17 +29,18 @@ <h1>FUSIONLAB<span class="dim"> · open-data tokamak twin</span></h1>
</div>
<span class="seg" id="mode" role="group" aria-label="Mode" title="Guided: a 15-minute study that teaches you to read this screen. Lab: the full app.">
<button type="button" data-v="guided">Guided</button><button type="button" data-v="lab" class="on">Lab</button></span>
<nav class="tabs">
<button type="button" data-tab="replay">Replay a real shot</button>
<button type="button" data-tab="db">Shot database</button>
<button type="button" data-tab="sandbox" class="active">What-if sandbox</button>
<button type="button" id="cmdk" class="tool" disabled title="Command palette (Ctrl/Cmd+K)" aria-haspopup="dialog" aria-expanded="false">⌘K</button>
<nav class="tabs" role="tablist" aria-label="Workspaces">
<button type="button" role="tab" id="tabbtn-replay" data-tab="replay" aria-selected="true" aria-controls="tab-replay">Replay</button>
<button type="button" role="tab" id="tabbtn-compare" data-tab="compare" aria-selected="false" aria-controls="tab-compare">Compare</button>
<button type="button" role="tab" id="tabbtn-explore" data-tab="explore" aria-selected="false" aria-controls="tab-explore">Explore</button>
</nav>
</header>

<section id="tab-replay" hidden></section>
<section id="tab-db" hidden></section>
<section id="tab-replay" role="tabpanel" aria-labelledby="tabbtn-replay" tabindex="0" hidden></section>
<section id="tab-compare" role="tabpanel" aria-labelledby="tabbtn-compare" tabindex="0" hidden></section>

<section id="tab-sandbox">
<section id="tab-explore" role="tabpanel" aria-labelledby="tabbtn-explore" tabindex="0" hidden>
<div class="grid">
<div class="panel">
<h2>Device</h2>
Expand All @@ -55,6 +59,7 @@ <h2>Controls</h2>
<input type="range" id="H" min="0.5" max="2" step="0.01" value="1"></div>
<div class="control" data-guide="knob-Zeff"><label for="Zeff"><span>Impurity content Z<sub>eff</sub></span> <output id="Zeff-out"></output></label>
<input type="range" id="Zeff" min="1" max="4" step="0.05" value="1.7"></div>
<p class="entry-action"><a href="#/replay/30166?edit=1" title="Open shot #30166 in the Replay workspace and edit its real programme, anchored on the measurement">Edit a real shot instead →</a></p>
</div>

<div class="panel wide stack">
Expand Down Expand Up @@ -91,9 +96,10 @@ <h2>Operating map: Q over density × heating</h2>
<h1>FUSIONLAB<span class="dim"> · open-data tokamak twin</span></h1>
<p class="muted">Real shots of the MAST tokamak, beside the physics models, with learned corrections and their errors stated. Where would you like to start?</p>
<div class="welcome-pick">
<button type="button" data-mode="guided"><b>New to fusion</b><span>A guided study, about 15 minutes. What a tokamak is trying to do, what each knob means, and how to read a real shot. It runs on this very screen, so you finish knowing the app.</span><i>Start the guided study →</i></button>
<button type="button" data-mode="lab"><b>I know tokamaks</b><span>Straight to the lab: replay real shots, the 6,353-shot database, the what-if sandbox. Every panel has a <b>?</b> that opens its lesson if you want one.</span><i>Open the lab →</i></button>
<button type="button" data-mode="guided" data-landing="replay"><b>New to fusion</b><span>A guided study, about 15 minutes. What a tokamak is trying to do, what each knob means, and how to read a real shot. It runs on this very screen, so you finish knowing the app.</span><i>Start the guided study →</i></button>
<button type="button" data-mode="lab" data-landing="replay"><b>I know tokamaks</b><span>Straight to the lab: replay real shots, compare shots in the catalog, and edit a shot's programme. Every panel has a <b>?</b> that opens its lesson if you want one.</span><i>Open the lab →</i></button>
</div>
<p class="welcome-alt small"><button type="button" data-mode="lab" data-landing="explore">Explore the model</button> — the what-if sliders on their own: pick a machine, turn the knobs, and watch the operating limits respond.</p>
<p class="small dim">You can switch at any time with Guided | Lab in the header.</p>
</div>
</div>
Expand Down
Loading
Loading