annaaaddddd commited on
Commit
caa0df2
·
verified ·
1 Parent(s): 3c03810

Update interactive demo

Browse files

Waiting on update in dataset but this layout should be finalized.

Files changed (1) hide show
  1. src/display/demo_new.html +232 -128
src/display/demo_new.html CHANGED
@@ -7,11 +7,17 @@
7
  <style>
8
  *{margin:0;padding:0;box-sizing:border-box}
9
  body{
10
- font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
11
- background: linear-gradient(135deg,#667eea 0%,#764ba2 100%);
 
12
  min-height:100vh; overflow-x:hidden;
13
  }
14
- .container{max-width:1200px;margin:0 auto;padding:20px}
 
 
 
 
 
15
 
16
  .header{
17
  text-align:center;color:#fff;margin-bottom:18px;padding:22px;
@@ -31,7 +37,7 @@
31
  }
32
  .thumb-card:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(66,133,244,.12);border-color:#82a9ff}
33
  .thumb-card.selected{border-color:#34a853;background:linear-gradient(135deg,#f6fff6 0%,#eaf7ea 100%)}
34
- .thumb-img{width:100%;height:140px;border-radius:6px;object-fit:contain;background:#eef2f7;display:block}
35
  .thumb-caption{margin-top:8px;font-size:.95em;color:#37474f;font-weight:600}
36
  .thumb-small{font-size:.82em;color:#78909c}
37
 
@@ -41,10 +47,21 @@
41
  background:linear-gradient(135deg,#fff3e0 0%,#ffe0b2 100%);color:#7b4a00;font-size:.95em
42
  }
43
  .start-btn{
44
- background:linear-gradient(135deg,#4285f4,#34a853);color:#fff;border:none;border-radius:24px;
45
- padding:12px 22px;font-weight:800;cursor:pointer;box-shadow:0 6px 18px rgba(66,133,244,.3);min-width:180px
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
47
- .start-btn:disabled{background:#c9cdd7;box-shadow:none;cursor:not-allowed}
48
 
49
  .demo-shell{
50
  display:none;margin-top:18px;background:#fff;border-radius:14px;padding:16px;
@@ -72,9 +89,9 @@
72
  .iteration-display{background:#fff;border:2px solid #e8eaed;border-radius:12px;padding:14px}
73
  .iteration-header{
74
  display:flex;align-items:center;justify-content:space-between;background:linear-gradient(135deg,#4285f4,#34a853);
75
- color:#fff;border-radius:10px;padding:12px;margin-bottom:12px
76
  }
77
- .nav-btn{background:#4285f4;color:#fff;border:none;border-radius:12px;padding:6px 10px;margin-left:6px;cursor:pointer}
78
 
79
  .sequence-indicator{text-align:center;margin-bottom:12px;padding:8px;background:linear-gradient(135deg,#ff6b6b,#4ecdc4);border-radius:10px;color:#fff}
80
  .sequence-flow{display:flex;justify-content:center;gap:10px;flex-wrap:wrap}
@@ -103,7 +120,6 @@
103
 
104
  .topline{color:#3b3f47;font-size:.9em}
105
 
106
- /* 绿色提示框整体居中 */
107
  .step-card{ text-align:center }
108
  .success-explain{
109
  display:inline-block;margin:12px auto;padding:12px 16px;
@@ -120,10 +136,10 @@
120
  </head>
121
  <body>
122
  <div class="container">
123
- <div class="header">
124
  <h1>🤖 World Model Closed Loop Framework</h1>
125
  <p>Select by first-frame observation, review the description, then start.</p>
126
- </div>
127
 
128
  <!-- ========= Selection area ========= -->
129
  <div class="selector-shell" id="selectorShell">
@@ -193,21 +209,13 @@ const scenarios = [
193
  frames:2, environment:"Living Room",
194
  base:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/X7HyMhZNoso/E145",
195
  previewImg:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/X7HyMhZNoso/E145/A000/real_obs_bbox.png"
196
- },
197
- // { id:"office", name:"Office Navigation", target:"Green Door (Exit)", prompt:"Navigate to the green emergency exit door", frames:15, environment:"Office",
198
- // base:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014",
199
- // previewImg:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014/A000/real_obs_bbox.png"
200
- // },
201
- // { id:"bedroom", name:"Bedroom Navigation", target:"Yellow Lamp (Bedside)", prompt:"Approach the yellow bedside lamp without disturbing items", frames:10, environment:"Bedroom",
202
- // base:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014",
203
- // previewImg:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014/A000/real_obs_bbox.png"
204
- // }
205
  ];
206
 
207
  let selectedIdx=-1,currentScenario=null;
208
  let frames=[],currentFrameIndex=0,isRunning=false,missionEnded=false,isPlaying=false,playInterval=null;
209
  let realActionData={};
210
- let availableKeys=[]; // ★ 当前场景可用的 A00x 列表
211
  const PLACEHOLDER_CONF={1:50,2:30,3:20};
212
 
213
  /* render selection */
@@ -215,11 +223,11 @@ function renderThumbs(){
215
  const grid=document.getElementById('thumbGrid');
216
  grid.innerHTML=scenarios.map((s,idx)=>`
217
  <div class="thumb-card ${idx===selectedIdx?'selected':''}" data-idx="${idx}">
218
- <img class="thumb-img" src="${s.previewImg}"
219
- onerror="this.src=''; this.style.background='#eef2f7'; this.closest('.thumb-card').querySelector('.thumb-small').textContent='(preview unavailable)';" />
220
  <div class="thumb-caption">${s.name}</div>
221
  <div class="thumb-small">Target: ${s.target}</div>
222
  </div>`).join('');
 
223
  }
224
  function applySelection(idx){
225
  selectedIdx=idx; currentScenario=scenarios[idx];
@@ -235,7 +243,6 @@ document.addEventListener('click',e=>{
235
 
236
  /* data helpers */
237
  async function loadActionData(){
238
- // ★ 根据 frames 动态生成 A001..An
239
  availableKeys = Array.from({length: currentScenario.frames},
240
  (_,i)=>`A${String(i+1).padStart(3,'0')}`);
241
 
@@ -296,23 +303,19 @@ function pct(x){return (Math.round(x*1000)/10).toFixed(1)+'%';}
296
  async function startDemo(){
297
  if (selectedIdx < 0) return;
298
 
299
- // Demo 区一直可见,不回到仅选择页
300
  document.getElementById('demoShell').classList.add('visible');
301
-
302
- // 热切场景:清理上一轮状态(需配合你已有的 resetForNewScenario())
303
  resetForNewScenario();
304
 
305
- // 更新当前场景信息
306
  document.getElementById('currentScenarioBox').innerHTML =
307
  `<div style="font-weight:600;margin-bottom:4px">${currentScenario.name}</div>
308
  <div>🎯 <b>${currentScenario.target}</b></div>
309
  <div>📝 "${currentScenario.prompt}"</div>`;
310
  document.getElementById('bevHeaderTitle').textContent = "🗺️ Bird's Eye View";
311
 
312
- // 加载动作/计划数据(会生成 availableKeys: A001..An)
313
  await loadActionData();
314
 
315
- // 构建帧数据
316
  frames = availableKeys.map((k, i) => {
317
  const d = realActionData[k];
318
  return {
@@ -330,7 +333,6 @@ async function startDemo(){
330
  currentFrameIndex = 0;
331
  isRunning = true;
332
 
333
- // Final Video:取最后一帧,存在才显示
334
  const finalVid = document.getElementById('finalVideo');
335
  const lastKey = availableKeys[availableKeys.length - 1] || 'A001';
336
  const finalUrl = `${currentScenario.base}/${lastKey}/world_model_gen/gen_video.mp4`;
@@ -339,119 +341,161 @@ async function startDemo(){
339
  else { finalVid.removeAttribute('src'); finalVid.style.display = 'none'; }
340
  });
341
 
342
- // 渲染首帧并开始流程
343
  displayFrame(0);
344
  setTimeout(processCurrentFrame, 900);
345
 
346
- // 自动滚动到 overview 顶部对齐
347
  document.querySelector('.overview-panel')
348
  .scrollIntoView({ behavior: 'smooth', block: 'start' });
349
  }
350
 
351
  /* iteration flow */
352
  function processCurrentFrame(){
353
- const f=frames[currentFrameIndex]; if(!f||f.completed||missionEnded) return;
354
- f.status='current'; f.currentStep=1; displayFrame(currentFrameIndex);
355
- setTimeout(()=>{ f.currentStep=2; displayFrame(currentFrameIndex);
356
- setTimeout(()=>{ f.currentStep=3; displayFrame(currentFrameIndex);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  setTimeout(()=>{
 
358
  const {score}=getTopAnswerer(f.actionData);
359
  if(score>=0.95){
360
  document.getElementById('finalVideo').style.display='block';
361
  missionEnded=true;
362
  if(isPlaying) togglePlayback();
363
- }else if(f.actions3?.length){ f.selectedAction=f.actions3[0]; }
364
- f.completed=true; f.status='completed'; f.currentStep=0; displayFrame(currentFrameIndex);
 
 
 
 
 
 
 
365
  },800);
366
  },1100);
367
  },700);
368
  }
369
 
370
- function displayFrame(i){
371
  if(i<0||i>=frames.length) return;
372
  currentFrameIndex=i; const f=frames[i];
373
  const {label,score}=getTopAnswerer(f.actionData); const high=score>=0.95;
374
 
375
- const seq=`
376
- <div class="sequence-indicator">
377
- <div><b>🔄 Processing Sequence</b></div>
378
- <div class="sequence-flow">
379
- <div class="sequence-step ${f.currentStep===1?'active':''}"><span class="flow-num">1</span>Observe</div>
380
- <div class="sequence-step ${f.currentStep===2?'active':''}"><span class="flow-num">2</span>Plan & Simulate</div>
381
- <div class="sequence-step ${f.currentStep===3?'active':''}"><span class="flow-num">3</span>Select</div>
382
- </div>
383
- </div>`;
384
-
385
- const obs=`
386
- <div class="step-card ${f.currentStep===1?'active':''}">
387
- <div class="step-h"><span class="step-num">1</span><span class="step-title">Current Observation</span></div>
388
- <div class="obs-box">
389
- <img src="${currentScenario.base}/${f.frameKey}/real_obs_bbox.png"
390
- class="obs-img" alt="Current Observation"
391
- onerror="this.replaceWith(Object.assign(document.createElement('div'),{innerText:'Image unavailable',style:'color:#999;padding:10px'}))">
392
- </div>
393
- </div>`;
394
-
395
- const planBox=p=>`
396
- <div class="plan-box">
397
- <div style="font-weight:700;font-size:.8em;margin-bottom:4px">${p?.title||'Plan'}</div>
398
- <ul style="padding-left:18px">${(p?.steps||[]).slice(0,4).map(s=>`<li>${s}</li>`).join('')||'<li>—</li>'}</ul>
399
- </div>`;
400
-
401
- const sim=`
402
- <div class="step-card ${f.currentStep===2?'active':''}">
403
- <div class="step-h"><span class="step-num">2</span><span class="step-title">Planning & World Model Simulation</span></div>
404
- <div class="plan-pairs">${planBox(f.plans[0])}${planBox(f.plans[1])}</div>
405
- <div class="sim-grid">
406
- <div><video id="pred1_${f.frameKey}" autoplay loop muted></video></div>
407
- <div><video id="pred2_${f.frameKey}" autoplay loop muted></video></div>
408
- </div>
409
- </div>`;
410
-
411
- const actionCard=(a,idx)=>{
412
- const p=a?.conf ?? (idx===0?50:idx===1?30:20);
413
- const selected=f.selectedAction===a?'style="border:2px solid #34a853;background:rgba(52,168,83,.08);border-radius:8px;padding:8px"':'style="border:2px solid #e8eaed;border-radius:8px;padding:8px"';
414
- const click=high?'':`onclick="frames[${i}].selectedAction = frames[${i}].actions3[${idx}] || null; displayFrame(${i});"`;
415
- return `<div ${selected} ${click}>
416
- <div style="font-weight:700">${['🥇','🥈','🥉'][idx]||''} ${a?.title||''}</div>
417
- <div class="conf-bar"><div class="conf-fill" style="width:${p}%"></div></div>
418
- <div style="display:flex;justify-content:space-between;font-size:.8em;color:#666;margin-top:4px">
419
- <span>${idx===0?'Top choice':''}</span><span>${p}%</span>
420
- </div>
421
- </div>`;
422
- };
 
 
423
 
424
- const decide=high
425
- ? `<div class="success-explain">High confidence (≥95%) on <b>${label}</b> → stop selecting actions and declare success.</div>`
426
- : `<div style="padding:8px;border-radius:6px;background:#fff3cd;border:1px solid #ffe08a;font-size:.9em">Confidence is <b>${pct(score)}</b> &lt; 95% → choose the highest-ranked action.</div>`;
427
-
428
- const action=`
429
- <div class="step-card ${f.currentStep===3?'active':''}">
430
- <div class="step-h"><span class="step-num">3</span><span class="step-title">Action Selection & Confidence</span></div>
431
- <div class="topline"><b>Top object:</b> ${label} &nbsp; <b>Confidence:</b> ${pct(score)}</div>
432
- ${decide}
433
- ${high ? '' : `<div style="display:grid;gap:8px;margin-top:8px">${actionCard(f.actions3?.[0],0)}${actionCard(f.actions3?.[1],1)}${actionCard(f.actions3?.[2],2)}</div>`}
434
- </div>`;
435
-
436
- document.getElementById('iterationDisplay').innerHTML=`
437
- <div class="iteration-header">
438
- <div><b>Frame ${f.frameNumber}</b> — Closed Loop Iteration</div>
439
- <div>
440
- <button class="nav-btn" id="prevBtn" ${currentFrameIndex===0?'disabled':''} onclick="previousFrame()">◀ Previous</button>
441
- <button class="nav-btn" id="nextBtn" ${currentFrameIndex>=frames.length-1?'disabled':''} onclick="nextFrame()">Next ▶</button>
 
442
  </div>
443
- </div>
444
- ${f.status==='current'?seq:''}
445
- <div class="step-grid">${obs}${sim}${action}</div>`; //<button class="nav-btn" id="playBtn" onclick="togglePlayback()">▶️ Play</button>
446
-
447
- // 加载两个预测视频(存在才显示)
448
- (async()=>{
449
- try{
450
- const urls=await findUpToTwoVideos(f.frameKey);
451
- setVideoSrcOrPlaceholder(document.getElementById(`pred1_${f.frameKey}`), urls[0]||null);
452
- setVideoSrcOrPlaceholder(document.getElementById(`pred2_${f.frameKey}`), urls[1]||null);
453
- }catch(e){ console.warn('video init failed',e); }
454
- })();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
455
  }
456
 
457
  /* videos helper */
@@ -510,7 +554,6 @@ function togglePlayback(){
510
  },2600);
511
  }
512
 
513
- // 事件委托,避免重渲染后失效
514
  document.addEventListener('click', (e)=>{
515
  const id = e.target.closest('button')?.id;
516
  if (id === 'prevBtn'){ e.preventDefault(); previousFrame(); }
@@ -518,39 +561,100 @@ document.addEventListener('click', (e)=>{
518
  if (id === 'playBtn'){ e.preventDefault(); togglePlayback(); }
519
  });
520
 
521
- // 暴露给内联 onclick
522
  window.previousFrame = previousFrame;
523
  window.nextFrame = nextFrame;
524
  window.togglePlayback = togglePlayback;
525
 
526
  /* boot */
527
- (function init(){ renderThumbs(); })();
528
 
529
  function resetForNewScenario(){
530
- // 停止播放计时器
531
  if (isPlaying) { clearInterval(playInterval); isPlaying=false; }
532
  playInterval = null;
533
 
534
- // 清理运行态
535
  isRunning = false;
536
  missionEnded = false;
537
 
538
- // 清空帧与索引
539
  frames = [];
540
  currentFrameIndex = 0;
541
 
542
- // 预测视频探测缓存要清空(不同场景也有 A001 等同名)
543
  __videoProbeCache.clear();
544
 
545
- // 隐藏/清空预测视频占位(避免上一场景的视频残留)
546
  const preds = document.querySelectorAll('video[id^="pred1_"], video[id^="pred2_"]');
547
  preds.forEach(v => { v.removeAttribute('src'); v.hidden = true; });
548
 
549
- // Final Video 先隐藏,稍后按新场景探测再显示
550
  const finalVid = document.getElementById('finalVideo');
551
  finalVid.removeAttribute('src');
552
  finalVid.style.display = 'none';
553
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
  </script>
556
  </body>
 
7
  <style>
8
  *{margin:0;padding:0;box-sizing:border-box}
9
  body{
10
+ font-family:'Montserrat',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
11
+ bbackground: transparent;
12
+ /* background: linear-gradient(135deg,#667eea 0%,#764ba2 100%); */
13
  min-height:100vh; overflow-x:hidden;
14
  }
15
+ .container{
16
+ max-width: 95vw;
17
+ margin: 0 auto;
18
+ padding: 10px;
19
+ width: 100%;
20
+ }
21
 
22
  .header{
23
  text-align:center;color:#fff;margin-bottom:18px;padding:22px;
 
37
  }
38
  .thumb-card:hover{transform:translateY(-1px);box-shadow:0 8px 20px rgba(66,133,244,.12);border-color:#82a9ff}
39
  .thumb-card.selected{border-color:#34a853;background:linear-gradient(135deg,#f6fff6 0%,#eaf7ea 100%)}
40
+ .thumb-img{width:100%;height:140px;border-radius:6px;object-fit:contain;background:#eef2f7;display:block; max-height: 210px}
41
  .thumb-caption{margin-top:8px;font-size:.95em;color:#37474f;font-weight:600}
42
  .thumb-small{font-size:.82em;color:#78909c}
43
 
 
47
  background:linear-gradient(135deg,#fff3e0 0%,#ffe0b2 100%);color:#7b4a00;font-size:.95em
48
  }
49
  .start-btn{
50
+ background:linear-gradient(135deg,#4285f4,#34a853) !important;
51
+ color:#fff !important;
52
+ border:none !important;
53
+ border-radius:24px !important;
54
+ padding:12px 22px !important;
55
+ font-weight:800 !important;
56
+ cursor:pointer !important;
57
+ box-shadow:0 6px 18px rgba(66,133,244,.3) !important;
58
+ min-width:180px !important;
59
+ }
60
+ .start-btn:disabled{
61
+ background:#c9cdd7 !important;
62
+ box-shadow:none !important;
63
+ cursor:not-allowed !important;
64
  }
 
65
 
66
  .demo-shell{
67
  display:none;margin-top:18px;background:#fff;border-radius:14px;padding:16px;
 
89
  .iteration-display{background:#fff;border:2px solid #e8eaed;border-radius:12px;padding:14px}
90
  .iteration-header{
91
  display:flex;align-items:center;justify-content:space-between;background:linear-gradient(135deg,#4285f4,#34a853);
92
+ color:#fff !important;border-radius:10px;padding:12px;margin-bottom:12px
93
  }
94
+ .nav-btn{background:#4285f4;color:#fff !important;border:none;border-radius:12px;padding:6px 10px;margin-left:6px;cursor:pointer}
95
 
96
  .sequence-indicator{text-align:center;margin-bottom:12px;padding:8px;background:linear-gradient(135deg,#ff6b6b,#4ecdc4);border-radius:10px;color:#fff}
97
  .sequence-flow{display:flex;justify-content:center;gap:10px;flex-wrap:wrap}
 
120
 
121
  .topline{color:#3b3f47;font-size:.9em}
122
 
 
123
  .step-card{ text-align:center }
124
  .success-explain{
125
  display:inline-block;margin:12px auto;padding:12px 16px;
 
136
  </head>
137
  <body>
138
  <div class="container">
139
+ <!-- <div class="header">
140
  <h1>🤖 World Model Closed Loop Framework</h1>
141
  <p>Select by first-frame observation, review the description, then start.</p>
142
+ </div> -->
143
 
144
  <!-- ========= Selection area ========= -->
145
  <div class="selector-shell" id="selectorShell">
 
209
  frames:2, environment:"Living Room",
210
  base:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/X7HyMhZNoso/E145",
211
  previewImg:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/X7HyMhZNoso/E145/A000/real_obs_bbox.png"
212
+ }
 
 
 
 
 
 
 
 
213
  ];
214
 
215
  let selectedIdx=-1,currentScenario=null;
216
  let frames=[],currentFrameIndex=0,isRunning=false,missionEnded=false,isPlaying=false,playInterval=null;
217
  let realActionData={};
218
+ let availableKeys=[];
219
  const PLACEHOLDER_CONF={1:50,2:30,3:20};
220
 
221
  /* render selection */
 
223
  const grid=document.getElementById('thumbGrid');
224
  grid.innerHTML=scenarios.map((s,idx)=>`
225
  <div class="thumb-card ${idx===selectedIdx?'selected':''}" data-idx="${idx}">
226
+ <img class="thumb-img" src="" data-img-src="${s.previewImg}" />
 
227
  <div class="thumb-caption">${s.name}</div>
228
  <div class="thumb-small">Target: ${s.target}</div>
229
  </div>`).join('');
230
+ fetchAndReplaceThumbImages();
231
  }
232
  function applySelection(idx){
233
  selectedIdx=idx; currentScenario=scenarios[idx];
 
243
 
244
  /* data helpers */
245
  async function loadActionData(){
 
246
  availableKeys = Array.from({length: currentScenario.frames},
247
  (_,i)=>`A${String(i+1).padStart(3,'0')}`);
248
 
 
303
  async function startDemo(){
304
  if (selectedIdx < 0) return;
305
 
306
+ // demo is always visible
307
  document.getElementById('demoShell').classList.add('visible');
 
 
308
  resetForNewScenario();
309
 
310
+ // Update current scenario's info
311
  document.getElementById('currentScenarioBox').innerHTML =
312
  `<div style="font-weight:600;margin-bottom:4px">${currentScenario.name}</div>
313
  <div>🎯 <b>${currentScenario.target}</b></div>
314
  <div>📝 "${currentScenario.prompt}"</div>`;
315
  document.getElementById('bevHeaderTitle').textContent = "🗺️ Bird's Eye View";
316
 
 
317
  await loadActionData();
318
 
 
319
  frames = availableKeys.map((k, i) => {
320
  const d = realActionData[k];
321
  return {
 
333
  currentFrameIndex = 0;
334
  isRunning = true;
335
 
 
336
  const finalVid = document.getElementById('finalVideo');
337
  const lastKey = availableKeys[availableKeys.length - 1] || 'A001';
338
  const finalUrl = `${currentScenario.base}/${lastKey}/world_model_gen/gen_video.mp4`;
 
341
  else { finalVid.removeAttribute('src'); finalVid.style.display = 'none'; }
342
  });
343
 
 
344
  displayFrame(0);
345
  setTimeout(processCurrentFrame, 900);
346
 
 
347
  document.querySelector('.overview-panel')
348
  .scrollIntoView({ behavior: 'smooth', block: 'start' });
349
  }
350
 
351
  /* iteration flow */
352
  function processCurrentFrame(){
353
+ const f=frames[currentFrameIndex];
354
+ if(!f||f.completed||missionEnded) return;
355
+
356
+ // 第1步:设置状态并显示(包含视频初始化)
357
+ f.status='current';
358
+ f.currentStep=1;
359
+ displayFrame(currentFrameIndex); // 第一次调用,会加载视频
360
+
361
+ setTimeout(()=>{
362
+ // 第2步:更新步骤状态(跳过视频初始化)
363
+ f.currentStep=2;
364
+ displayFrame(currentFrameIndex, true); // skipVideoInit=true
365
+
366
+ setTimeout(()=>{
367
+ // 第3步:更新步骤状态(跳过视频初始化)
368
+ f.currentStep=3;
369
+ displayFrame(currentFrameIndex, true); // skipVideoInit=true
370
+
371
  setTimeout(()=>{
372
+ // 完成步骤:检查分数并决定是否结束
373
  const {score}=getTopAnswerer(f.actionData);
374
  if(score>=0.95){
375
  document.getElementById('finalVideo').style.display='block';
376
  missionEnded=true;
377
  if(isPlaying) togglePlayback();
378
+ }else if(f.actions3?.length){
379
+ f.selectedAction=f.actions3[0];
380
+ }
381
+
382
+ // 标记完成并最后一次更新显示(跳过视频初始化)
383
+ f.completed=true;
384
+ f.status='completed';
385
+ f.currentStep=0;
386
+ displayFrame(currentFrameIndex, true); // skipVideoInit=true
387
  },800);
388
  },1100);
389
  },700);
390
  }
391
 
392
+ function displayFrame(i, skipVideoInit = false){
393
  if(i<0||i>=frames.length) return;
394
  currentFrameIndex=i; const f=frames[i];
395
  const {label,score}=getTopAnswerer(f.actionData); const high=score>=0.95;
396
 
397
+ if (!skipVideoInit) {
398
+ // 第一次调用:生成完整HTML并加载视频
399
+ const seq=`
400
+ <div class="sequence-indicator">
401
+ <div><b>🔄 Processing Sequence</b></div>
402
+ <div class="sequence-flow">
403
+ <div class="sequence-step ${f.currentStep===1?'active':''}"><span class="flow-num">1</span>Observe</div>
404
+ <div class="sequence-step ${f.currentStep===2?'active':''}"><span class="flow-num">2</span>Plan & Simulate</div>
405
+ <div class="sequence-step ${f.currentStep===3?'active':''}"><span class="flow-num">3</span>Select</div>
406
+ </div>
407
+ </div>`;
408
+
409
+ const obs=`
410
+ <div class="step-card ${f.currentStep===1?'active':''}">
411
+ <div class="step-h"><span class="step-num">1</span><span class="step-title">Current Observation</span></div>
412
+ <div class="obs-box">
413
+ <img src="${currentScenario.base}/${f.frameKey}/real_obs_bbox.png"
414
+ class="obs-img" alt="Current Observation"
415
+ onerror="this.replaceWith(Object.assign(document.createElement('div'),{innerText:'Image unavailable',style:'color:#999;padding:10px'}))">
416
+ </div>
417
+ </div>`;
418
+
419
+ const planBox=p=>`
420
+ <div class="plan-box">
421
+ <div style="font-weight:700;font-size:.8em;margin-bottom:4px">${p?.title||'Plan'}</div>
422
+ <ul style="padding-left:18px">${(p?.steps||[]).slice(0,4).map(s=>`<li>${s}</li>`).join('')||'<li>—</li>'}</ul>
423
+ </div>`;
424
+
425
+ const sim=`
426
+ <div class="step-card ${f.currentStep===2?'active':''}">
427
+ <div class="step-h"><span class="step-num">2</span><span class="step-title">Planning & World Model Simulation</span></div>
428
+ <div class="plan-pairs">${planBox(f.plans[0])}${planBox(f.plans[1])}</div>
429
+ <div class="sim-grid">
430
+ <div><video id="pred1_${f.frameKey}" autoplay loop muted></video></div>
431
+ <div><video id="pred2_${f.frameKey}" autoplay loop muted></video></div>
432
+ </div>
433
+ </div>`;
434
+
435
+ const actionCard=(a,idx)=>{
436
+ const p=a?.conf ?? (idx===0?50:idx===1?30:20);
437
+ const selected=f.selectedAction===a?'style="border:2px solid #34a853;background:rgba(52,168,83,.08);border-radius:8px;padding:8px"':'style="border:2px solid #e8eaed;border-radius:8px;padding:8px"';
438
+ const click=high?'':`onclick="frames[${i}].selectedAction = frames[${i}].actions3[${idx}] || null; displayFrame(${i});"`;
439
+ return `<div ${selected} ${click}>
440
+ <div style="font-weight:700">${['🥇','🥈','🥉'][idx]||''} ${a?.title||''}</div>
441
+ <div class="conf-bar"><div class="conf-fill" style="width:${p}%"></div></div>
442
+ <div style="display:flex;justify-content:space-between;font-size:.8em;color:#666;margin-top:4px">
443
+ <span>${idx===0?'Top choice':''}</span><span>${p}%</span>
444
+ </div>
445
+ </div>`;
446
+ };
447
 
448
+ const decide=high
449
+ ? `<div class="success-explain">High confidence (≥95%) on <b>${label}</b> → stop selecting actions and declare success.</div>`
450
+ : `<div style="padding:8px;border-radius:6px;background:#fff3cd;border:1px solid #ffe08a;font-size:.9em">Confidence is <b>${pct(score)}</b> &lt; 95% → choose the highest-ranked action.</div>`;
451
+
452
+ const action=`
453
+ <div class="step-card ${f.currentStep===3?'active':''}">
454
+ <div class="step-h"><span class="step-num">3</span><span class="step-title">Action Selection & Confidence</span></div>
455
+ <div class="topline"><b>Top object:</b> ${label} &nbsp; <b>Confidence:</b> ${pct(score)}</div>
456
+ ${decide}
457
+ ${high ? '' : `<div style="display:grid;gap:8px;margin-top:8px">${actionCard(f.actions3?.[0],0)}${actionCard(f.actions3?.[1],1)}${actionCard(f.actions3?.[2],2)}</div>`}
458
+ </div>`;
459
+
460
+ document.getElementById('iterationDisplay').innerHTML=`
461
+ <div class="iteration-header">
462
+ <div><b>Frame ${f.frameNumber}</b> — Closed Loop Iteration</div>
463
+ <div>
464
+ <button class="nav-btn" id="prevBtn" ${currentFrameIndex===0?'disabled':''} onclick="previousFrame()">◀ Previous</button>
465
+ <button class="nav-btn" id="nextBtn" ${currentFrameIndex>=frames.length-1?'disabled':''} onclick="nextFrame()">Next ▶</button>
466
+ </div>
467
  </div>
468
+ ${f.status==='current'?seq:''}
469
+ <div class="step-grid">${obs}${sim}${action}</div>`;
470
+
471
+ // 加载视频
472
+ (async()=>{
473
+ try{
474
+ const urls=await findUpToTwoVideos(f.frameKey);
475
+ setVideoSrcOrPlaceholder(document.getElementById(`pred1_${f.frameKey}`), urls[0]||null);
476
+ setVideoSrcOrPlaceholder(document.getElementById(`pred2_${f.frameKey}`), urls[1]||null);
477
+ }catch(e){ console.warn('video init failed',e); }
478
+ })();
479
+ } else {
480
+ // 后续调用:只更新step状态,保持视频不变
481
+ // 1. 更新sequence steps
482
+ const sequenceSteps = document.querySelectorAll('.sequence-step');
483
+ sequenceSteps.forEach((step, idx) => {
484
+ step.classList.toggle('active', idx + 1 === f.currentStep);
485
+ });
486
+
487
+ // 2. 更新step cards
488
+ const stepCards = document.querySelectorAll('.step-card');
489
+ stepCards.forEach((card, idx) => {
490
+ card.classList.toggle('active', idx + 1 === f.currentStep);
491
+ });
492
+
493
+ // 3. 显示/隐藏sequence indicator
494
+ const sequenceIndicator = document.querySelector('.sequence-indicator');
495
+ if (sequenceIndicator) {
496
+ sequenceIndicator.style.display = f.status === 'current' ? 'block' : 'none';
497
+ }
498
+ }
499
  }
500
 
501
  /* videos helper */
 
554
  },2600);
555
  }
556
 
 
557
  document.addEventListener('click', (e)=>{
558
  const id = e.target.closest('button')?.id;
559
  if (id === 'prevBtn'){ e.preventDefault(); previousFrame(); }
 
561
  if (id === 'playBtn'){ e.preventDefault(); togglePlayback(); }
562
  });
563
 
 
564
  window.previousFrame = previousFrame;
565
  window.nextFrame = nextFrame;
566
  window.togglePlayback = togglePlayback;
567
 
568
  /* boot */
569
+ // (function init(){ renderThumbs(); })();
570
 
571
  function resetForNewScenario(){
572
+ // Cleaning cache and reset fo new scenario
573
  if (isPlaying) { clearInterval(playInterval); isPlaying=false; }
574
  playInterval = null;
575
 
 
576
  isRunning = false;
577
  missionEnded = false;
578
 
 
579
  frames = [];
580
  currentFrameIndex = 0;
581
 
 
582
  __videoProbeCache.clear();
583
 
 
584
  const preds = document.querySelectorAll('video[id^="pred1_"], video[id^="pred2_"]');
585
  preds.forEach(v => { v.removeAttribute('src'); v.hidden = true; });
586
 
 
587
  const finalVid = document.getElementById('finalVideo');
588
  finalVid.removeAttribute('src');
589
  finalVid.style.display = 'none';
590
  }
591
+ async function fetchImageAsBlobURL(url) {
592
+ try {
593
+ const res = await fetch(url, { mode: 'cors' });
594
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
595
+ const blob = await res.blob();
596
+ return URL.createObjectURL(blob);
597
+ } catch (err) {
598
+ console.warn('Image blob fetch failed:', url, err);
599
+ return null;
600
+ }
601
+ }
602
+
603
+ async function fetchAndReplaceThumbImages() {
604
+ const cards = document.querySelectorAll('.thumb-card');
605
+ for (const card of cards) {
606
+ const imgEl = card.querySelector('img.thumb-img');
607
+ const dataIdx = card.dataset.idx;
608
+ const scenario = scenarios[dataIdx];
609
+ const blobUrl = await fetchImageAsBlobURL(scenario.previewImg);
610
+ if (blobUrl) {
611
+ imgEl.src = blobUrl;
612
+ } else {
613
+ imgEl.replaceWith(Object.assign(document.createElement('div'), {
614
+ innerText: 'Image unavailable',
615
+ style: 'color:#999;padding:10px;text-align:center;font-size:.8em;background:#eef2f7;border-radius:6px;height:140px;display:flex;align-items:center;justify-content:center'
616
+ }));
617
+ }
618
+ }
619
+ }
620
+
621
+
622
+ function waitForElement(selector, callback, timeout = 5000) {
623
+ const start = Date.now();
624
+ const check = () => {
625
+ const el = document.querySelector(selector);
626
+ if (el) {
627
+ callback(el);
628
+ } else if (Date.now() - start < timeout) {
629
+ requestAnimationFrame(check);
630
+ } else {
631
+ console.warn(`Element ${selector} not found after timeout`);
632
+ }
633
+ };
634
+ check();
635
+ }
636
+
637
+ // Multiple initialization strategies to handle the initialization of thumbnail images
638
+ function initApp() {
639
+ const thumbGrid = document.getElementById('thumbGrid');
640
+ if (thumbGrid && thumbGrid.children.length === 0) {
641
+ console.log('Initializing thumbnails...');
642
+ renderThumbs();
643
+ }
644
+ }
645
+
646
+ // Try multiple times with different triggers
647
+ document.addEventListener('DOMContentLoaded', initApp);
648
+ window.addEventListener('load', initApp);
649
+ setTimeout(initApp, 500);
650
+ setTimeout(initApp, 1500);
651
+ setTimeout(initApp, 3000);
652
+
653
+ // Also keep your existing code as fallback
654
+ waitForElement('#thumbGrid', () => {
655
+ renderThumbs();
656
+ fetchAndReplaceThumbImages();
657
+ });
658
 
659
  </script>
660
  </body>