File size: 19,354 Bytes
a6927da
 
 
 
 
 
646c6af
a6927da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7556728
 
 
 
 
 
 
 
 
a6927da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646c6af
a6927da
 
 
 
 
 
 
 
 
646c6af
 
a6927da
646c6af
 
 
 
 
a6927da
 
 
 
 
 
 
 
 
 
 
 
7556728
ebbf558
 
 
7556728
81974f6
 
7556728
 
a6927da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646c6af
 
a6927da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646c6af
 
 
 
 
 
 
 
 
 
 
a6927da
 
 
 
 
 
646c6af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a6927da
 
 
 
 
 
 
 
 
 
 
646c6af
a6927da
 
 
 
 
 
 
 
 
 
 
646c6af
 
 
 
a6927da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646c6af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a6927da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7556728
 
a6927da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ToolTree — Interactive Case Walkthrough</title>
<meta name="description" content="Step through real ToolTree search traces on three GTA tasks: candidate drafts, pre/post evaluation, bidirectional pruning, real tool outputs, and Q/N backpropagation.">
<link rel="icon" href="data:,">
<style>
  :root {
    --ink: #1c2430;
    --muted: #5b6a7c;
    --line: #dde4ec;
    --bg: #f6f8fa;
    --card: #ffffff;
    --accent: #1f5fbf;
    --ok: #1e8a4c;
    --bad: #b3402f;
    --warn: #8a6d1e;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    background: var(--bg);
    color: var(--ink);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
    font-size: 15px;
    line-height: 1.5;
  }
  a { color: var(--accent); text-decoration: none; }
  a:hover { text-decoration: underline; }
  code { background: #eef2f7; border-radius: 4px; padding: 0 4px; font-size: .86em; }
  .wrap { max-width: 980px; margin: 0 auto; padding: 20px 18px 60px; }
  header h1 { font-size: 1.45rem; margin: 8px 0 4px; }
  header .sub { color: var(--muted); font-size: .92rem; }
  .navlinks { font-size: .9rem; margin-top: 6px; }
  .navlinks a + a { margin-left: 14px; }
  .banner {
    margin: 14px 0 18px;
    padding: 10px 14px;
    background: #fff7e8;
    border: 1px solid #ecd9ac;
    border-radius: 8px;
    font-size: .88rem;
    color: #6a5320;
  }
  .card {
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 16px 18px;
    margin-bottom: 16px;
  }
  /* case tabs */
  .tabs { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
  .tabs button {
    font: inherit;
    padding: 8px 14px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: #fff;
    color: var(--ink);
    cursor: pointer;
    text-align: left;
  }
  .tabs button:hover { border-color: var(--accent); color: var(--accent); }
  .tabs button.active { border-color: var(--accent); background: #eaf1fb; font-weight: 600; }
  .query {
    border-left: 3px solid var(--accent);
    background: #f2f6fc;
    padding: 8px 12px;
    margin: 0 0 4px;
    font-size: .92rem;
    border-radius: 0 6px 6px 0;
  }
  .query .who { font-weight: 700; font-size: .78rem; color: var(--accent); display: block; }
  /* controls */
  .controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
  .controls button {
    font: inherit;
    padding: 5px 14px;
    border: 1px solid var(--line);
    border-radius: 7px;
    background: #fff;
    color: var(--ink);
    cursor: pointer;
  }
  .controls button:hover { border-color: var(--accent); color: var(--accent); }
  .controls input[type=range] { flex: 1; min-width: 180px; accent-color: var(--accent); }
  .controls .steplabel { font-variant-numeric: tabular-nums; font-weight: 600; min-width: 96px; }
  .hint { color: var(--muted); font-size: .8rem; margin-top: 6px; }
  /* step panel */
  .stephead { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
  .stephead .sid { font-weight: 700; font-size: 1.05rem; }
  .badge {
    display: inline-block;
    padding: 1px 10px;
    border-radius: 6px;
    font-size: .76rem;
    font-weight: 700;
    letter-spacing: .03em;
    white-space: nowrap;
  }
  .badge.ok   { background: #e2f3e8; color: var(--ok);  border: 1px solid #bcdfc9; }
  .badge.bad  { background: #f7e8e5; color: var(--bad); border: 1px solid #e6c5be; }
  .badge.gray { background: #eef1f5; color: var(--muted); border: 1px solid #d8dfe7; }
  .secline { margin: 8px 0; font-size: .92rem; }
  .seclabel { font-weight: 700; font-size: .78rem; color: var(--accent); letter-spacing: .04em; display: block; margin: 12px 0 4px; }
  table.cands { width: 100%; border-collapse: collapse; font-size: .88rem; }
  table.cands th, table.cands td {
    text-align: left;
    padding: 5px 8px;
    border-bottom: 1px solid var(--line);
    vertical-align: top;
  }
  table.cands th { color: var(--muted); font-weight: 600; font-size: .8rem; }
  table.cands td.num { font-variant-numeric: tabular-nums; white-space: nowrap; }
  pre.out {
    background: #0f1720;
    color: #cfd8e3;
    border-radius: 8px;
    padding: 12px 14px;
    overflow-x: auto;
    font-size: .8rem;
    line-height: 1.45;
    white-space: pre-wrap;
    word-break: break-word;
    margin: 6px 0;
  }
  pre.out.err { color: #f0a89c; }
  .trunc { color: var(--warn); font-size: .78rem; }
  ul.plain { margin: 4px 0; padding-left: 20px; font-size: .9rem; }
  .answerbox {
    border-left: 3px solid var(--ok);
    background: #f0f9f3;
    padding: 10px 14px;
    border-radius: 0 6px 6px 0;
    margin: 8px 0;
    font-size: .95rem;
  }
  .stats { display: flex; gap: 22px; flex-wrap: wrap; font-size: .9rem; margin: 6px 0 2px; }
  .stats .k { color: var(--muted); }
  .stats .v { font-weight: 600; font-variant-numeric: tabular-nums; }
  figure.treefig {
    margin: 0 0 16px;
    background: var(--card);
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 14px 14px 10px;
  }
  figure.treefig img { display: block; width: 100%; height: auto; }
  figure.treefig figcaption { color: var(--muted); font-size: .8rem; margin-top: 8px; }
  #error {
    display: none;
    background: #f7e8e5;
    border: 1px solid #e6c5be;
    color: var(--bad);
    padding: 14px 16px;
    border-radius: 8px;
    margin-top: 14px;
  }
  footer { color: var(--muted); font-size: .8rem; margin-top: 26px; }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <h1>ToolTree — Interactive Case Walkthrough</h1>
    <div class="sub">Real execution traces from this repository (real tool-execution mode) on three GTA tasks:
      step through each rollout — candidate drafts, pre-evaluation, execution with real tool outputs,
      post-evaluation, pruning, and Q/N backpropagation.</div>
    <div class="navlinks">
      <a href="index.html">&larr; Project page</a>
      <a href="https://github.com/SYang2000/ICLR_2026_ToolTree" target="_blank" rel="noopener">GitHub repo</a>
      <a href="https://openreview.net/forum?id=Ef5O9gNNLE" target="_blank" rel="noopener">Paper (ICLR 2026)</a>
    </div>
    <div class="banner">
      <b>Provenance:</b> every candidate draft, score (r_pre / r_post), Q/N value, tool output, and final
      answer below is taken verbatim from two logged runs of this repository — 5 GTA tasks (Cases 1&ndash;2)
      and 4 GTA tasks with 3&ndash;4-step reference plans (Case 3) — both with
      <code>tool_execution: real</code> (seed 42, &tau;<sub>pre</sub> 0.3, &tau;<sub>post</sub> 0.4,
      c 1.4, max 40 rollouts). Long tool outputs are truncated to 300 characters and flagged. Failed tool
      calls (e.g. a tool without a real backend, or a malformed argument draft) are shown verbatim —
      post-evaluation prunes those branches; one error message that echoed a local filesystem path has
      that path masked and is flagged. Task images are referenced by their dataset-relative paths and are
      not embedded here.
    </div>
  </header>

  <div id="error">
    Walkthrough data failed to load. This page needs <code>static/demo_data.js</code> next to it
    (shipped in the repo under <code>docs/static/</code>). If you are viewing a copy of this file alone,
    grab the full <code>docs/</code> directory from the
    <a href="https://github.com/SYang2000/ICLR_2026_ToolTree" target="_blank" rel="noopener">GitHub repo</a>.
  </div>

  <noscript><p style="max-width:720px;margin:24px auto;padding:12px 16px;background:#fef3c7;border:1px solid #d97706;border-radius:8px;font-size:14px">This interactive walkthrough requires JavaScript. The search mechanism it illustrates is documented on the <a href="index.html">project page</a> and in the <a href="https://github.com/SYang2000/ICLR_2026_ToolTree">repository README</a>.</p></noscript>

  <figure class="treefig" id="case3-tree">
    <a href="static/paper/case3_tree.svg" target="_blank" rel="noopener">
      <img src="static/paper/case3_tree.svg" alt="Full search tree of Case 3: from the user query, ImageDescription, GoogleSearch, and Calculator branches carry pre- and post-evaluation scores; dashed boxes mark pruned and duplicate (skipped) candidate drafts, and a bold green path through ImageDescription, GoogleSearch, and Calculator leads to the final answer of 112.5 miles." width="1680" height="1500">
    </a>
    <figcaption>Search tree of this case (reconstructed from the logged run): solid boxes are executed tool
      calls with their pre-/post-evaluation scores, dashed boxes are pruned or duplicate (skipped)
      candidate drafts, and the bold green path is the final highest-reward plan.</figcaption>
  </figure>

  <div id="app" style="display:none">
    <div class="tabs" id="tabs"></div>

    <div class="card">
      <div class="query"><span class="who">USER QUERY</span><span id="query"></span></div>
    </div>

    <div class="card">
      <div class="controls">
        <button id="btnPrev" title="previous step (&larr;)">&#9664; Prev</button>
        <button id="btnNext" title="next step (&rarr;)">Next &#9654;</button>
        <input type="range" id="slider" min="0" max="1" value="0" step="1">
        <span class="steplabel" id="stepLabel"></span>
      </div>
      <div class="hint">Keyboard: &larr; / &rarr; step through the search.</div>
    </div>

    <div class="card" id="stepPanel"></div>

    <footer>
      Data source: <code>static/demo_data.js</code>, generated from the audit logs of two real-execution
      validation runs of this codebase (per-rollout AUDIT events, LLM traffic, and final results).
      &middot; <a href="index.html">Project page</a>
      &middot; <a href="https://github.com/SYang2000/ICLR_2026_ToolTree" target="_blank" rel="noopener">GitHub repo</a>
    </footer>
  </div>
</div>

<script src="static/demo_data.js"></script>
<script>
(function () {
  "use strict";

  var D = window.DEMO_DATA;
  if (!D || !D.cases || !D.cases.length) {
    document.getElementById("error").style.display = "block";
    return;
  }
  document.getElementById("app").style.display = "block";

  var caseIdx = 0;
  var stepIdx = 0;
  var slider = document.getElementById("slider");

  function esc(s) {
    return String(s == null ? "" : s)
      .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  }
  function num(x) { return esc(String(x)); }
  function args(a) { return "<code>" + esc(JSON.stringify(a)) + "</code>"; }

  // ---------- tabs ----------
  var tabsEl = document.getElementById("tabs");
  D.cases.forEach(function (c, i) {
    var b = document.createElement("button");
    b.textContent = c.title;
    b.addEventListener("click", function () { caseIdx = i; stepIdx = 0; render(); });
    tabsEl.appendChild(b);
  });

  // ---------- step rendering ----------
  function statusBadge(st) {
    if (st === "kept") return "<span class='badge ok'>kept</span>";
    if (st === "pre_pruned") return "<span class='badge bad'>pre-pruned</span>";
    return "<span class='badge gray'>dedup &middot; duplicate action</span>";
  }

  function candTable(cands) {
    var h = "<table class='cands'><tr><th>tool</th><th>argument draft</th><th>r_pre</th><th>outcome</th></tr>";
    cands.forEach(function (c) {
      h += "<tr><td><code>" + esc(c.tool) + "</code></td><td>" +
        (c.args ? args(c.args) : "&mdash;") + "</td><td class='num'>" +
        (c.r_pre == null ? "&mdash;" : num(c.r_pre)) + "</td><td>" +
        statusBadge(c.status) + "</td></tr>";
    });
    return h + "</table>";
  }

  function renderRollout(s, total) {
    var h = "<div class='stephead'><span class='sid'>Rollout " + s.rollout + " of " +
      total + "</span>";
    if (s.post_pruned) h += "<span class='badge bad'>branch post-pruned</span>";
    h += "</div>";

    if (s.discovery) {
      s.discovery.forEach(function (d) {
        h += "<span class='seclabel'>DISCOVERY PASS &mdash; CONSUMES NO ROLLOUT BUDGET</span>";
        h += "<div class='secline'>Selection first landed on leaf <code>" + esc(d.selected.action) +
          "</code> (Q=" + num(d.selected.Q) + ", N=" + d.selected.N + ", r_pre=" +
          num(d.selected.r_pre) + "), but its expansion produced no usable child:</div>";
        h += candTable(d.candidates);
        if (d.exhausted) {
          h += "<div class='secline'><code>" + esc(d.exhausted.node) +
            "</code> is marked exhausted (depth " + d.exhausted.depth +
            ") and selection re-runs.</div>";
        }
      });
    }

    var sel = s.selected;
    h += "<div class='secline'><b>Selection:</b> leaf <code>" + esc(sel.action) + "</code> " +
      "(Q=" + num(sel.Q) + ", N=" + sel.N + ", r_pre=" + num(sel.r_pre) + ")";
    if (s.realize) {
      h += " &mdash; unvisited candidate; its stored argument draft is executed (<code>realize_leaf</code>)";
    }
    h += "</div>";

    if (s.candidates.length) {
      h += "<span class='seclabel'>EXPANSION &mdash; CANDIDATES &amp; PRE-EVALUATION (&tau;<sub>pre</sub> = " +
        num(D.meta.tau_pre) + ")</span>";
      h += candTable(s.candidates);
    }

    if (s.executed) {
      var ex = s.executed;
      h += "<span class='seclabel'>EXECUTION &mdash; REAL TOOL OUTPUT</span>";
      h += "<div class='secline'><b>" + esc(ex.tool) + "</b> " + args(ex.args) + "</div>";
      h += "<pre class='out" + (ex.error ? " err" : "") + "'>" + esc(ex.output) + "</pre>";
      if (ex.truncated) {
        h += "<div class='trunc'>output truncated to " + D.meta.trunc_chars +
          " characters for display; the full text is in the run artifacts</div>";
      }
      if (ex.redacted) {
        h += "<div class='trunc'>a local filesystem path in this message is masked as " +
          "&lt;local dataset dir&gt;; everything else is verbatim</div>";
      }
      h += "<span class='seclabel'>POST-EVALUATION (&tau;<sub>post</sub> = " + num(s.tau_post) + ")</span>";
      h += "<div class='secline'>r_post = <b>" + num(s.r_post) + "</b> &rarr; " +
        (s.post_pruned ? "<span class='badge bad'>post-pruned</span>" : "<span class='badge ok'>kept</span>") +
        "</div>";
    }

    if (s.backprop.length) {
      h += "<span class='seclabel'>BACKPROPAGATION (reward " + num(s.backprop[0].reward) + ")</span>";
      h += "<ul class='plain'>";
      s.backprop.forEach(function (b) {
        h += "<li><code>" + esc(b.node) + "</code> &rarr; Q=" + num(b.Q) + ", N=" + b.N + "</li>";
      });
      h += "</ul>";
      h += "<div class='secline'><span class='badge gray'>best Q after rollout: " + num(s.best_q) + "</span></div>";
    }
    return h;
  }

  function renderTerminal(s) {
    if (s.event === "early_stop") {
      var eh = "<div class='stephead'><span class='sid'>Search terminated</span>" +
        "<span class='badge gray'>early_stop &middot; rollouts_used=" + s.rollouts_used + "</span></div>";
      eh += "<div class='secline'>The best root Q has plateaued: baseline " + num(s.baseline) +
        " vs best recent " + num(s.best_recent) + " &mdash; improvement " + num(s.improvement) +
        " &lt; &delta; = " + num(s.delta) + " over the last " + s.patience + " rollouts.</div>";
      if (s.exhausted.length) {
        eh += "<div class='secline'>Nodes marked exhausted during the search:</div><ul class='plain'>";
        s.exhausted.forEach(function (e) {
          eh += "<li><code>" + esc(e.node) + "</code> (depth " + e.depth + ")</li>";
        });
        eh += "</ul>";
      }
      eh += "<div class='secline'>The early-stopping rule halts the search at rollout " +
        s.rollouts_used + " &mdash; well under the budget of " + D.meta.max_rollouts +
        " rollouts.</div>";
      return eh;
    }
    var h = "<div class='stephead'><span class='sid'>Search terminated</span>" +
      "<span class='badge gray'>search_exhausted &middot; rollouts_used=" + s.rollouts_used + "</span></div>";
    if (s.dedups.length) {
      h += "<div class='secline'>Further expansion only produced duplicates of actions already on the path:</div><ul class='plain'>";
      s.dedups.forEach(function (d) {
        h += "<li><code>" + esc(d.tool) + "</code>" + (d.args ? " " + args(d.args) : "") +
          " <span class='badge gray'>dedup</span></li>";
      });
      h += "</ul>";
    } else {
      h += "<div class='secline'>Every remaining candidate was pre-pruned, post-pruned, or a duplicate of an action already on the path.</div>";
    }
    h += "<div class='secline'>Nodes marked exhausted:</div><ul class='plain'>";
    s.exhausted.forEach(function (e) {
      h += "<li><code>" + esc(e.node) + "</code> (depth " + e.depth + ")</li>";
    });
    h += "</ul><div class='secline'>With no expandable leaf left, the search stops early &mdash; well under the budget of " +
      D.meta.max_rollouts + " rollouts.</div>";
    return h;
  }

  function renderFinal(s) {
    var h = "<div class='stephead'><span class='sid'>Final trajectory &amp; answer</span>" +
      "<span class='badge ok'>highest-reward path</span></div>";
    h += "<span class='seclabel'>FINAL PLAN</span><ul class='plain'>";
    s.plan.forEach(function (p, i) {
      h += "<li>" + (i + 1) + ". <code>" + esc(p.tool) + "</code> " + args(p.args) + "</li>";
    });
    h += "</ul>";
    h += "<span class='seclabel'>FINAL ANSWER</span>";
    h += "<div class='answerbox'>" + esc(s.answer) + "</div>";
    h += "<div class='stats'>" +
      "<span><span class='k'>final reward&nbsp;</span><span class='v'>" + num(s.reward) + "</span></span>" +
      "<span><span class='k'>rollouts used&nbsp;</span><span class='v'>" + s.rollouts_used + "</span></span>" +
      "</div>";
    return h;
  }

  function render() {
    var c = D.cases[caseIdx];
    var steps = c.steps;
    stepIdx = Math.max(0, Math.min(steps.length - 1, stepIdx));

    Array.prototype.forEach.call(tabsEl.children, function (b, i) {
      b.className = i === caseIdx ? "active" : "";
    });
    var treeFig = document.getElementById("case3-tree");
    if (treeFig) treeFig.style.display = c.id === "case3" ? "" : "none";
    document.getElementById("query").textContent = c.query;

    slider.max = steps.length - 1;
    slider.value = stepIdx;
    document.getElementById("stepLabel").textContent = "step " + (stepIdx + 1) + " / " + steps.length;

    var s = steps[stepIdx];
    var totalRollouts = steps.filter(function (x) { return x.type === "rollout"; }).length;
    var html;
    if (s.type === "rollout") html = renderRollout(s, totalRollouts);
    else if (s.type === "terminal") html = renderTerminal(s);
    else html = renderFinal(s);
    document.getElementById("stepPanel").innerHTML = html;
  }

  function goto(i) { stepIdx = i; render(); }

  document.getElementById("btnPrev").addEventListener("click", function () { goto(stepIdx - 1); });
  document.getElementById("btnNext").addEventListener("click", function () { goto(stepIdx + 1); });
  slider.addEventListener("input", function () { goto(parseInt(slider.value, 10)); });
  document.addEventListener("keydown", function (e) {
    if (e.key === "ArrowLeft") { goto(stepIdx - 1); e.preventDefault(); }
    else if (e.key === "ArrowRight") { goto(stepIdx + 1); e.preventDefault(); }
  });

  render();
})();
</script>
</body>
</html>