darren.zhang Claude Opus 5 (1M context) commited on
Commit
0f8fa12
·
1 Parent(s): c8f96cb

teleopSample: step annotations + timeline on the older pages, autoplay on ours

Browse files

enhance.js adds to the 14 existing episode pages what the Faber U pick-and-place
pages have: an instruction / object / target / scene header, a segmented step
timeline over the scrubber, and a panel with the current step's annotation and
measured signals (end-effector travel, height change, speed, gripper). Wording
switched from "spans" to "steps" throughout, including the sidebar. The eight
pick-and-place pages now autoplay and loop like the older ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

teleopSample/drink_ep0.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>
teleopSample/drink_ep1.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>
teleopSample/enhance.js ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Brings the older sample pages up to the layout of the Faber U pick-and-place pages:
2
+ an instruction / object / target / scene header, a segmented step timeline above the
3
+ scrubber, and a panel showing the current step's annotation and measured signals.
4
+ Also switches the wording from "spans" to "steps".
5
+ Loaded with <script src="enhance.js"> (or ../enhance.js) — the page itself is untouched. */
6
+ (function () {
7
+ const J = id => { const e = document.getElementById(id); return e ? JSON.parse(e.textContent) : null; };
8
+ const S = J('series'), STEPS = J('spans') || [], META = J('meta');
9
+ if (!S || !STEPS.length) return;
10
+
11
+ const slug = (META && META.slug) || (location.pathname.match(/\/(ep\d+)\//) || [])[1] || '';
12
+ const family = slug.replace(/_?ep\d+$/, '') || 'legacy';
13
+ // object / target / scene, written from each recording's own description
14
+ const TASK = {
15
+ mem: { object: 'DDR memory module', target: 'motherboard slot',
16
+ scene: 'workbench with an open motherboard, dual-arm grippers' },
17
+ drink: { object: 'bottled green beverage', target: 'coaster on the countertop',
18
+ scene: 'countertop with the drink and a coaster set apart' },
19
+ sort: { object: 'packed garment bags (pink / white / beige / green)',
20
+ target: 'left side of the table', scene: 'table with a material bin of packed garment bags' },
21
+ wipe: { object: 'sponge', target: 'stains on the tabletop', scene: 'tabletop with stains' },
22
+ legacy: { object: 'water bottle', target: 'basket',
23
+ scene: 'tabletop with a water bottle and a basket' },
24
+ }[family] || {};
25
+ const instruction = (META && META.high_level_instruction) ||
26
+ (document.querySelector('header h1') || {}).textContent || '';
27
+ const sceneLine = (document.querySelector('.hilevel') || {}).textContent || '';
28
+ const scene = (sceneLine.replace(/^\s*scene\s*/i, '').trim()) || TASK.scene || '';
29
+
30
+ const COLORS = ["#8a8f98", "#4f8ff7", "#41c76b", "#e0a53e", "#b46fe0", "#e0616f", "#e08f5f", "#5fc9e0"];
31
+ const col = i => COLORS[i % COLORS.length];
32
+ const DUR = S.t ? S.t[S.n - 1] : (S.n - 1) / S.fps;
33
+ const END = S.end || S.end_pos; // [n][6] = left xyz, right xyz
34
+ const frameAt = t => Math.max(0, Math.min(S.n - 1, Math.round(t * S.fps)));
35
+
36
+ // ---------- styles
37
+ const css = document.createElement('style');
38
+ css.textContent = `
39
+ .x-instr{margin:10px 22px 0;padding:11px 15px;background:#12151c;border:1px solid #232733;border-radius:8px;font-size:13px;line-height:1.65}
40
+ .x-instr .k{font-size:10.5px;letter-spacing:.04em;text-transform:uppercase;color:#9aa1ad;margin-right:8px}
41
+ .x-instr .obj{color:#7fb2ff}.x-instr .tgt{color:#7fe0a0}
42
+ .x-tlwrap{margin:12px 22px 0;padding:9px 14px;background:#12151c;border:1px solid #232733;border-radius:8px}
43
+ .x-tl{position:relative;display:flex;height:26px;border-radius:5px;overflow:hidden;cursor:pointer}
44
+ .x-tl .seg{position:relative;opacity:.85;transition:opacity .12s}
45
+ .x-tl .seg.on{opacity:1;box-shadow:inset 0 0 0 2px #fff}
46
+ .x-tl .seg span{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:10.5px;color:#0b0d12;font-weight:700}
47
+ .x-tl .cur{position:absolute;top:0;bottom:0;width:2px;background:#fff;pointer-events:none}
48
+ .x-tlnums{display:flex;justify-content:space-between;font-size:10.5px;color:#6b7686;margin-top:4px;font-variant-numeric:tabular-nums}
49
+ .x-annot{margin:12px 22px 0;padding:14px 18px;background:#12151c;border:1px solid #232733;border-radius:8px}
50
+ .x-annot .t{font-size:11px;letter-spacing:.04em;color:#9aa1ad;text-transform:uppercase;margin-bottom:10px}
51
+ .x-annot h3{margin:0 0 2px;font-size:16px}
52
+ .x-annot h3 .tok{font-size:11.5px;color:#6f92c8;font-family:ui-monospace,monospace;margin-left:8px}
53
+ .x-annot .when{font-size:12px;color:#8890a0;font-variant-numeric:tabular-nums;margin-bottom:8px}
54
+ .x-annot p{margin:0 0 6px;line-height:1.6;font-size:13.5px;color:#dfe4ec}
55
+ .x-sgrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:7px 26px;margin-top:10px}
56
+ .x-sg{display:flex;justify-content:space-between;border-bottom:1px dashed #232733;padding-bottom:3px;font-size:12.5px}
57
+ .x-sg .k{color:#9aa1ad}.x-sg .v{font-variant-numeric:tabular-nums}`;
58
+ document.head.appendChild(css);
59
+
60
+ // ---------- instruction header
61
+ const head = document.querySelector('header');
62
+ const instr = document.createElement('div');
63
+ instr.className = 'x-instr';
64
+ instr.innerHTML =
65
+ `<div><span class="k">instruction</span>${instruction}</div>` +
66
+ `<div style="margin-top:6px"><span class="k">object</span><span class="obj">${TASK.object || '—'}</span>` +
67
+ `<span class="k" style="margin-left:14px">target</span><span class="tgt">${TASK.target || '—'}</span>` +
68
+ `<span class="k" style="margin-left:14px">scene</span>${scene}</div>`;
69
+ head.parentNode.insertBefore(instr, head.nextSibling);
70
+
71
+ // ---------- step timeline over the scrubber
72
+ const controls = document.querySelector('.panel.controls');
73
+ const tlw = document.createElement('div');
74
+ tlw.className = 'x-tlwrap';
75
+ tlw.innerHTML = `<div class="x-tl" id="xTl"><div class="cur" id="xCur"></div></div>` +
76
+ `<div class="x-tlnums"><span>0.00s</span><span>${DUR.toFixed(2)}s</span></div>`;
77
+ controls.parentNode.insertBefore(tlw, controls);
78
+ const tl = tlw.querySelector('#xTl'), cur = tlw.querySelector('#xCur');
79
+ const videos = [...document.querySelectorAll('video')];
80
+ const master = videos[0];
81
+ const seek = t => videos.forEach(v => { try { v.currentTime = t; } catch (e) {} });
82
+ const num = (st, i) => (st.id != null ? st.id : i + 1); // keep the page's own numbering
83
+ STEPS.forEach((st, i) => {
84
+ const sg = document.createElement('div');
85
+ sg.className = 'seg';
86
+ sg.style.background = col(i);
87
+ sg.style.width = (100 * (st.end_s - st.start_s) / DUR) + '%';
88
+ sg.title = `${num(st, i)}. ${st.title} ${st.start_s.toFixed(2)}-${st.end_s.toFixed(2)}s`;
89
+ sg.innerHTML = `<span>${num(st, i)}</span>`;
90
+ sg.onclick = e => { e.stopPropagation(); seek(st.start_s + 0.01); };
91
+ tl.insertBefore(sg, cur);
92
+ });
93
+ tl.onclick = e => { const r = tl.getBoundingClientRect(); seek((e.clientX - r.left) / r.width * DUR); };
94
+
95
+ // ---------- current-step annotation
96
+ const an = document.createElement('div');
97
+ an.className = 'x-annot';
98
+ an.innerHTML = `<div class="t">step annotation · current step</div><h3 id="xLbl">–</h3>` +
99
+ `<div class="when" id="xWhen"></div><p id="xDesc"></p><div class="x-sgrid" id="xSig"></div>`;
100
+ controls.parentNode.insertBefore(an, controls.nextSibling);
101
+
102
+ const stat = (a, b) => { // measurements over a frame range
103
+ a = Math.max(0, a); b = Math.min(S.n - 1, b);
104
+ const arm = (o) => {
105
+ let path = 0, dz = 0, vmax = 0;
106
+ for (let i = a + 1; i <= b; i++) {
107
+ const p = END[i], q = END[i - 1];
108
+ const d = Math.hypot(p[o] - q[o], p[o + 1] - q[o + 1], p[o + 2] - q[o + 2]);
109
+ path += d; vmax = Math.max(vmax, d * S.fps);
110
+ }
111
+ dz = END[b][o + 2] - END[a][o + 2];
112
+ const dxy = Math.hypot(END[b][o] - END[a][o], END[b][o + 1] - END[a][o + 1]);
113
+ return { path, dz, dxy, vmax, vmean: path / Math.max(1e-6, (b - a) / S.fps) };
114
+ };
115
+ const L = arm(0), R = arm(3);
116
+ const act = L.path >= R.path ? L : R, side = L.path >= R.path ? 'left' : 'right';
117
+ return { act, side, L, R };
118
+ };
119
+
120
+ let curIdx = -1;
121
+ function render(i, t) {
122
+ const st = STEPS[i];
123
+ if (i !== curIdx) {
124
+ curIdx = i;
125
+ const a = frameAt(st.start_s), b = frameAt(st.end_s), m = stat(a, b);
126
+ document.getElementById('xLbl').innerHTML =
127
+ `${num(st, i)}. ${st.title}` + (st.token ? `<span class="tok">${st.token}</span>` : '');
128
+ document.getElementById('xWhen').textContent =
129
+ `${st.start_s.toFixed(2)}s – ${st.end_s.toFixed(2)}s · ${(st.end_s - st.start_s).toFixed(2)}s · ` +
130
+ `frames ${a}–${b} (${b - a + 1})` +
131
+ (st.success_s != null ? ` · success at ${st.success_s.toFixed(2)}s` : '');
132
+ const seg = META && META.instruction_segments &&
133
+ META.instruction_segments.find(x => Math.abs(x.start_frame_index / S.fps - st.start_s) < 0.2);
134
+ document.getElementById('xDesc').textContent = seg ? seg.instruction :
135
+ `${m.side} arm · ${m.act.path.toFixed(3)} m of end-effector travel in ` +
136
+ `${(st.end_s - st.start_s).toFixed(2)}s (mean ${m.act.vmean.toFixed(2)}, peak ` +
137
+ `${m.act.vmax.toFixed(2)} m/s), height change ${m.act.dz >= 0 ? '+' : ''}${m.act.dz.toFixed(2)} m.`;
138
+ const rows = [
139
+ ['active arm', m.side],
140
+ ['ee path', m.act.path.toFixed(3) + ' m'],
141
+ ['ee Δz', (m.act.dz >= 0 ? '+' : '') + m.act.dz.toFixed(3) + ' m'],
142
+ ['ee Δxy', m.act.dxy.toFixed(3) + ' m'],
143
+ ['speed mean / max', `${m.act.vmean.toFixed(2)} / ${m.act.vmax.toFixed(2)} m/s`],
144
+ ['gripper L', `${S.grip_l[a].toFixed(2)} → ${S.grip_l[b].toFixed(2)}`],
145
+ ['gripper R', `${S.grip_r[a].toFixed(2)} → ${S.grip_r[b].toFixed(2)}`],
146
+ ];
147
+ if (S.intervened) rows.push(['operator-sourced frames',
148
+ `${Math.round(100 * S.intervened.slice(a, b + 1).reduce((p, c) => p + (c ? 1 : 0), 0) / (b - a + 1))}%`]);
149
+ document.getElementById('xSig').innerHTML = rows.map(r =>
150
+ `<div class="x-sg"><span class="k">${r[0]}</span><span class="v">${r[1]}</span></div>`).join('');
151
+ tl.querySelectorAll('.seg').forEach((e, k) => e.classList.toggle('on', k === i));
152
+ }
153
+ cur.style.left = (100 * t / DUR) + '%';
154
+ }
155
+ function at(t) { for (let i = 0; i < STEPS.length; i++) if (t >= STEPS[i].start_s && t < STEPS[i].end_s) return i; return t < STEPS[0].start_s ? 0 : STEPS.length - 1; }
156
+ (function tick() { if (master) render(at(master.currentTime), master.currentTime); requestAnimationFrame(tick); })();
157
+
158
+ // ---------- wording: spans / subtask -> steps
159
+ document.querySelectorAll('.spans-head').forEach(e => {
160
+ e.textContent = e.textContent.replace(/temporal spans/i, 'annotated steps').replace(/spans/gi, 'steps');
161
+ });
162
+ document.querySelectorAll('.ro .k, .rgrid .k').forEach(e => {
163
+ const t = e.textContent.trim().toLowerCase();
164
+ if (t === 'subtask') e.textContent = 'step';
165
+ if (t === 'span progress') e.textContent = 'step progress';
166
+ });
167
+ })();
teleopSample/ep0/viewer.html CHANGED
@@ -74,7 +74,7 @@ canvas{width:100%;display:block;cursor:pointer}
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
- <div class="spans-head">TEMPORAL SPANS (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
@@ -488,4 +488,5 @@ setTimeout(fitHand,2000); setTimeout(fitHand,4000);
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
- </script></body></html>
 
 
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
+ <div class="spans-head">annotated steps (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
 
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
+ </script><script src="../enhance.js"></script>
492
+ </body></html>
teleopSample/ep1/viewer.html CHANGED
@@ -74,7 +74,7 @@ canvas{width:100%;display:block;cursor:pointer}
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
- <div class="spans-head">TEMPORAL SPANS (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
@@ -488,4 +488,5 @@ setTimeout(fitHand,2000); setTimeout(fitHand,4000);
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
- </script></body></html>
 
 
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
+ <div class="spans-head">annotated steps (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
 
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
+ </script><script src="../enhance.js"></script>
492
+ </body></html>
teleopSample/ep2/viewer.html CHANGED
@@ -74,7 +74,7 @@ canvas{width:100%;display:block;cursor:pointer}
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
- <div class="spans-head">TEMPORAL SPANS (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
@@ -488,4 +488,5 @@ setTimeout(fitHand,2000); setTimeout(fitHand,4000);
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
- </script></body></html>
 
 
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
+ <div class="spans-head">annotated steps (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
 
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
+ </script><script src="../enhance.js"></script>
492
+ </body></html>
teleopSample/ep3/viewer.html CHANGED
@@ -74,7 +74,7 @@ canvas{width:100%;display:block;cursor:pointer}
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
- <div class="spans-head">TEMPORAL SPANS (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
@@ -488,4 +488,5 @@ setTimeout(fitHand,2000); setTimeout(fitHand,4000);
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
- </script></body></html>
 
 
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
+ <div class="spans-head">annotated steps (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
 
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
+ </script><script src="../enhance.js"></script>
492
+ </body></html>
teleopSample/ep4/viewer.html CHANGED
@@ -74,7 +74,7 @@ canvas{width:100%;display:block;cursor:pointer}
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
- <div class="spans-head">TEMPORAL SPANS (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
@@ -488,5 +488,6 @@ setTimeout(fitHand,2000); setTimeout(fitHand,4000);
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
- </script></body></html>
 
492
  <!-- rev2 regular-file -->
 
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
+ <div class="spans-head">annotated steps (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
 
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
+ </script><script src="../enhance.js"></script>
492
+ </body></html>
493
  <!-- rev2 regular-file -->
teleopSample/ep5/viewer.html CHANGED
@@ -74,7 +74,7 @@ canvas{width:100%;display:block;cursor:pointer}
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
- <div class="spans-head">TEMPORAL SPANS (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
@@ -488,4 +488,5 @@ setTimeout(fitHand,2000); setTimeout(fitHand,4000);
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
- </script></body></html>
 
 
74
  <div class="panel"><div class="camgrid" id="camgrid"></div></div>
75
  <div class="sidecol">
76
  <div class="panel">
77
+ <div class="spans-head">annotated steps (3) · auto-derived</div>
78
  <div class="chips" id="chips"></div><div id="spanList"></div>
79
  </div>
80
  <div class="panel tacpanel">
 
488
  var probe=grid.querySelector('.cam');
489
  if(probe && window.ResizeObserver) new ResizeObserver(function(){fitHand();}).observe(probe);
490
  })();
491
+ </script><script src="../enhance.js"></script>
492
+ </body></html>
teleopSample/faberU/t2_pet_bottle_into_steel_tray/20260707052527/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/faberU/t2_pet_bottle_into_steel_tray/20260707052609/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/faberU/t3_juice_bottle_onto_plate/20260724050737/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/faberU/t3_juice_bottle_onto_plate/20260724050827/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/faberU/t4_juice_bottle_into_black_tray/20260728075709/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/faberU/t4_juice_bottle_into_black_tray/20260728080339/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/faberU/t6_pick_up_green_tea_bottle/20260730045050/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/faberU/t6_pick_up_green_tea_bottle/20260730045152/viewer.html CHANGED
@@ -192,7 +192,24 @@ function stepAt(t){for(let i=0;i<STEPS.length;i++)if(t>=STEPS[i].start_s&&t<STEP
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
- playBtn.onclick=()=>{playing=!playing;if(playing){videos.forEach(v=>v.play());playBtn.textContent='❚❚';requestAnimationFrame(loop);}else{videos.forEach(v=>v.pause());playBtn.textContent='▶︎';}};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
197
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
198
  master.addEventListener('seeked',resync);
 
192
  const playBtn=document.getElementById('play'),seek=document.getElementById('seek'),tlabel=document.getElementById('tlabel');
193
  let playing=false;
194
  function seekTo(t){videos.forEach(v=>{try{v.currentTime=t;}catch(e){}});}
195
+ function setPlaying(p){
196
+ playing=p; playBtn.textContent=p?'❚❚':'▶︎';
197
+ if(p){videos.forEach(v=>v.play().catch(()=>{}));requestAnimationFrame(loop);}
198
+ else videos.forEach(v=>v.pause());
199
+ }
200
+ playBtn.onclick=()=>setPlaying(!playing);
201
+ // autoplay once every tile can render, then loop — same scheme as the other sample pages
202
+ let started=false, iv=setInterval(tryStart,400);
203
+ function tryStart(){
204
+ if(started||playing||!videos.length||!videos.every(v=>v.readyState>=2))return;
205
+ started=true; clearInterval(iv); videos.forEach(v=>v.muted=true); setPlaying(true);
206
+ }
207
+ videos.forEach(v=>{v.addEventListener('canplay',tryStart);v.addEventListener('loadeddata',tryStart);});
208
+ window.addEventListener('load',tryStart);
209
+ setTimeout(()=>clearInterval(iv),90000);
210
+ setInterval(()=>{
211
+ if(playing&&master.currentTime>=DUR-0.08){seekTo(0);videos.forEach(v=>v.play().catch(()=>{}));}
212
+ },200);
213
  seek.oninput=()=>{const t=seek.value/1000*DUR;seekTo(t);update();};
214
  function resync(){const t=master.currentTime;videos.forEach(v=>{if(v!==master&&Math.abs(v.currentTime-t)>0.2){try{v.currentTime=t;}catch(e){}}});}
215
  master.addEventListener('seeked',resync);
teleopSample/index.html CHANGED
@@ -54,7 +54,7 @@ function build(){
54
  leaves.forEach(m=>{
55
  const el = document.createElement('div'); el.className = 'leaf';
56
  el.innerHTML = `<div class="lt">${esc(m.label)}</div>
57
- <div class="lm"><span class="rig">${esc(rig)}</span>${m.dur.toFixed(0)}s · ${m.spans} spans</div>`;
58
  el.onclick = ()=>go(m.hash);
59
  el._hash = m.hash; el._m = m; el._cat = cat;
60
  body.appendChild(el);
 
54
  leaves.forEach(m=>{
55
  const el = document.createElement('div'); el.className = 'leaf';
56
  el.innerHTML = `<div class="lt">${esc(m.label)}</div>
57
+ <div class="lm"><span class="rig">${esc(rig)}</span>${m.dur.toFixed(0)}s · ${m.spans} steps</div>`;
58
  el.onclick = ()=>go(m.hash);
59
  el._hash = m.hash; el._m = m; el._cat = cat;
60
  body.appendChild(el);
teleopSample/mem_ep0.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (4) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (4) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>
teleopSample/mem_ep1.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (4) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (4) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>
teleopSample/sort_ep0.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (16) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (16) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>
teleopSample/sort_ep1.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (16) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (16) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>
teleopSample/wipe_ep0.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>
teleopSample/wipe_ep1.html CHANGED
@@ -54,7 +54,7 @@ header{padding:16px 22px 8px}header h1{margin:0 0 3px;font-size:21px}
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
- <div class="spans-head">temporal spans (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
@@ -242,4 +242,5 @@ CAMS.forEach(c=>{ const a=document.createElement('a'); a.className='file-chip';
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
 
245
  </body></html>
 
54
  <div class="layout">
55
  <div class="panel camwrap"><div id="camgrid" class="camgrid"></div></div>
56
  <div class="panel sidecol-spans">
57
+ <div class="spans-head">annotated steps (3) · instruction segments</div>
58
  <div class="chips" id="chips"></div><div id="spanList"></div>
59
  </div>
60
  </div>
 
242
  })();
243
  master.addEventListener('loadedmetadata',update); window.addEventListener('load',update);
244
  </script>
245
+ <script src="enhance.js"></script>
246
  </body></html>