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

Update new layout for interactive demo

Browse files
Files changed (1) hide show
  1. src/display/demo_new.html +471 -680
src/display/demo_new.html CHANGED
@@ -1,766 +1,557 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width,initial-scale=1.0" />
6
- <title>World Model Closed Loop Framework</title>
7
- <script>console.log("WMCL: 3-col header (Controls|BEV|Final) + 3-step sequence + merged Plan&Simulate + HEAD+Range probe");</script>
8
- <style>
9
- *{margin:0;padding:0;box-sizing:border-box}
10
- /* 页面背景 + 顶部标题区文字颜色 */
11
- body{
12
- font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
13
- background: transparent;
14
- min-height:100vh;
15
- overflow-x:hidden;
16
- }
17
- .container{
18
- background: linear-gradient(135deg,#667eea 0%,#764ba2 100%); /* 把背景移到这里 */
19
- min-height: auto;
20
- padding: 20px; /* 如果需要的话可以添加一些内边距 */
21
- }
22
- .header{
23
- text-align:center;
24
- color:#fff !important; /* 标题与副标题用白色 */
25
- margin-bottom:30px;
26
- }
27
- .header h1{
28
- font-size:2.5em;
29
- margin-bottom:10px;
30
- text-shadow:2px 2px 4px rgba(0,0,0,.3); /* 轻微阴影让白字更清晰 */
31
- }
32
- .header p{
33
- font-size:1.2em;
34
- opacity:.9;
35
- }
36
 
37
- .demo-container{background:white;border-radius:20px;padding:30px;box-shadow:0 10px 30px rgba(0,0,0,.1);display:flex;flex-direction:column;transition:all .3s ease;margin:20px}
38
- .demo-container.expanded{min-height:auto}
 
 
 
 
39
 
40
- /* ===== 顶部三列并排:控制区 | BEV | Final ===== */
41
- .overview-panel{
42
- display:grid;
43
- grid-template-columns: 1.15fr 1fr 1fr;
44
- gap:20px;
45
- margin-bottom:20px;
46
- flex-shrink:0;
47
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
- /* 控制区:与右侧卡片视觉统一 */
50
- .control-panel{
51
- background:white;
52
- border:3px solid #e8eaed;
53
- border-radius:15px;
54
- padding:20px;
55
- display:flex;flex-direction:column;
56
- }
57
- .control-row{display:flex;gap:20px;align-items:center;margin-bottom:15px}
58
- .control-row:last-child{margin-bottom:0}
59
- .control-label{font-weight:bold;min-width:150px;color:#333}
60
- .control-input{flex:1}
61
- select,input[type="text"]{width:100%;padding:10px;border:2px solid #ddd;border-radius:8px;font-size:14px;transition:border-color .3s}
62
- select:focus,input[type="text"]:focus{outline:none;border-color:#4285f4}
63
- .start-btn{background:linear-gradient(135deg,#4285f4,#34a853)!important;color:#fff!important;border:none;padding:12px 30px!important;border-radius:25px!important;font-size:16px!important;font-weight:bold!important;cursor:pointer;transition:transform .3s!important,box-shadow .3s!important}
64
- .start-btn:hover{transform:translateY(-2px);box-shadow:0 10px 20px rgba(66,133,244,.3)}
65
- .scenario-info{padding:10px;background:#e8f4fd;border-radius:8px;color:#333}
66
-
67
- .birds-eye-section,.results-section{background:white;border:3px solid #e8eaed;border-radius:15px;padding:20px;display:flex;flex-direction:column}
68
- .section-header{background:linear-gradient(135deg,#34a853,#4285f4);color:#fff;padding:12px 20px;border-radius:10px;text-align:center;font-weight:bold;margin-bottom:15px}
69
- .results-section .section-header{background:linear-gradient(135deg,#ea4335,#fbbc04)}
70
- .section-content{flex:1;background:#f8f9fa;border-radius:10px;display:flex;align-items:center;justify-content:center;color:#666;font-style:italic;height:180px;border:2px dashed #ddd}
71
- .results-content{background:#f8f9fa;border-radius:10px;padding:15px;height:180px;display:flex;flex-direction:column;justify-content:flex-start;align-items:center;text-align:center;overflow:hidden}
72
- .final-status-display{padding:8px 12px;border-radius:20px;font-weight:bold;margin-bottom:12px;font-size:1em;flex-shrink:0}
73
- .final-status-display.success{background:#34a853!important;color:#fff!important}
74
- .final-status-display.failure{background:#ea4335!important;color:#fff!important}
75
- .final-status-display.pending{background:#e8eaed!important;color:#666!important}
76
-
77
- /* ===== 时间线 ===== */
78
- .timeline-container{background:#f8f9fa;border-radius:10px;padding:15px;margin-bottom:15px;flex-shrink:0;display: none;}
79
- .timeline-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
80
- .timeline-title{font-weight:bold;color:#333;font-size:1em}
81
- .frame-counter{background:#4285f4;color:#fff;padding:3px 12px;border-radius:15px;font-size:.8em}
82
- .timeline{position:relative;height:40px;background:#e8eaed;border-radius:20px;margin-bottom:10px;overflow:hidden}
83
- .timeline-progress{height:100%;background:linear-gradient(90deg,#4285f4,#34a853);border-radius:20px;transition:width .5s ease;position:relative}
84
- .timeline-markers{position:absolute;top:0;left:0;right:0;height:100%;display:flex;align-items:center;padding:0 15px}
85
- .frame-marker{width:24px;height:24px;border-radius:50%;background:#fff;border:2px solid #e8eaed;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:bold;cursor:pointer;transition:all .3s;position:absolute;color:#666}
86
- .frame-marker:hover{transform:scale(1.1);box-shadow:0 3px 8px rgba(0,0,0,.2)}
87
- .frame-marker.completed{border-color:#34a853;color:#34a853;background:#e8f5e8}
88
- .frame-marker.current{border-color:#4285f4;color:#fff;background:#4285f4;animation:pulse 2s infinite}
89
- @keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.1)}}
90
- .navigation-controls{display:flex;justify-content:center;gap:15px}
91
- .nav-btn{background:#4285f4!important;color:#fff!important;border:none;padding:10px 20px!important;border-radius:20px!important;cursor:pointer!important;font-size:14px!important;transition:background .3s!important}
92
- .nav-btn:hover{background:#3367d6}
93
- .nav-btn:disabled{background:#ccc;cursor:not-allowed}
94
-
95
- /* ===== 主区:三列(Step1 | Step2+3 | Step3[Select]) ===== */
96
- .main-content{flex:1;min-height:0;display:flex;flex-direction:column}
97
- .iteration-display{background:white;border:3px solid #e8eaed;border-radius:15px;padding:20px;flex:1;display:flex;flex-direction:column;transition:all .5s ease;min-height:400px}
98
- .iteration-display.active{border-color:#4285f4;background:linear-gradient(135deg,rgba(66,133,244,.05),rgba(52,168,83,.05))}
99
- .iteration-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding:15px 20px;background:linear-gradient(135deg,#4285f4,#34a853);color:#fff;border-radius:10px;flex-shrink:0}
100
- .iteration-title{font-size:1.4em;font-weight:bold}
101
- .iteration-status{padding:5px 15px;border-radius:20px;background:rgba(255,255,255,.2);font-size:.9em}
102
-
103
- .step-grid{display:grid;grid-template-columns:1fr 1.3fr 1fr;gap:20px;flex:1;margin-bottom:20px}
104
- .step-card{background:white;border:2px solid #e8eaed;border-radius:12px;padding:20px;transition:all .3s ease;display:flex;flex-direction:column;position:relative}
105
- .step-card.active{border-color:#4285f4;transform:translateY(-5px);box-shadow:0 10px 20px rgba(66,133,244,.15)}
106
- .step-card.processing::before{content:'';position:absolute;top:-2px;left:-2px;right:-2px;bottom:-2px;background:linear-gradient(45deg,#4285f4,#34a853,#fbbc04,#ea4335);background-size:300% 300%;border-radius:12px;z-index:-1;animation:rainbow-border 3s linear infinite;opacity:.8}
107
- @keyframes rainbow-border{0%{background-position:0% 50%}50%{background-position:100% 50%}100%{background-position:0% 50%}}
108
- .step-header{display:flex;align-items:center;margin-bottom:15px}
109
- .step-number{width:30px;height:30px;background:#4285f4;color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:bold;margin-right:10px;position:relative}
110
- .step-number.active-number{animation:number-pulse 2s ease-in-out infinite;background:linear-gradient(45deg,#4285f4,#34a853)}
111
- @keyframes number-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.08)}}
112
- .step-title{font-weight:bold;color:#333}
113
- .step-content{background:#f8f9fa;border-radius:8px;padding:12px;flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#666;font-style:italic;min-height:100px;font-size:.9em;text-align:center}
114
-
115
- /* Processing Sequence(圆角胶囊风 + 渐变底) */
116
- .sequence-indicator{
117
- text-align:center;
118
- margin-bottom:20px;
119
- padding:15px;
120
- background:linear-gradient(135deg,#ff6b6b,#4ecdc4); /* 粉红→蓝绿 的渐变底 */
121
- border-radius:15px;
122
- color:#fff;
123
- }
124
- .sequence-title{
125
- font-size:1.2em;
126
- font-weight:bold;
127
- margin-bottom:10px;
128
- }
129
- .sequence-flow{
130
- display:flex;
131
- justify-content:center;
132
- align-items:center;
133
- gap:15px;
134
- flex-wrap:wrap;
135
- }
136
- .sequence-step{
137
- display:flex;
138
- align-items:center;
139
- background:rgba(255,255,255,0.15);
140
- padding:8px 15px;
141
- border-radius:20px;
142
- transition:all .3s;
143
- }
144
- .sequence-step.active{
145
- background:rgba(255,255,255,0.3);
146
- transform:scale(1.05);
147
- box-shadow:0 4px 15px rgba(0,0,0,.2);
148
- }
149
- .sequence-number{
150
- width:25px;height:25px;
151
- background:#fff;color:#333;border-radius:50%;
152
- display:flex;align-items:center;justify-content:center;
153
- font-weight:bold;font-size:14px;margin-right:8px;
154
- }
155
- .sequence-arrow{ color:#fff; font-size:18px; margin:0 5px; }
156
-
157
-
158
- /* Step2+3 合并内部布局 */
159
- .plan-pairs{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:100%}
160
- .plan-box{border:1px dashed #ddd;border-radius:10px;padding:10px;background:#fff}
161
- .sim-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;width:100%}
162
- .sim-grid video{width:100%;height:140px;object-fit:contain;border-radius:4px}
163
- .subsection-title{font-weight:700;color:#333;margin:6px 0 8px 0}
164
-
165
- /* Step3(Select) 右侧 ranking */
166
- .action-list{display:flex;flex-direction:column;gap:12px;width:100%}
167
- .plan-card{background:white;border:2px solid #e8eaed;border-radius:8px;padding:15px;cursor:pointer;transition:all .3s}
168
- .plan-card:hover{border-color:#4285f4}
169
- .plan-card.selected{border-color:#34a853;background:rgba(52,168,83,.08)}
170
- .plan-title{font-weight:bold;margin-bottom:6px;color:#333}
171
- .confidence-bar{width:100%;height:8px;background:#eee;border-radius:5px;overflow:hidden}
172
- .confidence-fill{height:100%;background:linear-gradient(90deg,#ea4335,#fbbc04,#34a853)}
173
- .plan-card.just-selected{animation:pulseSel .9s ease-in-out 2}
174
- @keyframes pulseSel{0%{transform:scale(1);box-shadow:0 0 0 rgba(52,168,83,0)}50%{transform:scale(1.02);box-shadow:0 0 18px rgba(52,168,83,.4)}100%{transform:scale(1);box-shadow:0 0 0 rgba(52,168,83,0)}}
175
-
176
- .success-chip{margin-left:8px;display:inline-flex;align-items:center;gap:6px;background:#e8f5e9;color:#1e4620;border:1px solid #c6e6c9;border-radius:18px;padding:3px 8px;font-weight:600}
177
-
178
- @media (max-width:1100px){
179
- .overview-panel{grid-template-columns:1fr}
180
- .step-grid{grid-template-columns:1fr}
181
- .plan-pairs{grid-template-columns:1fr}
182
- .sim-grid{grid-template-columns:1fr}
183
- }
184
- </style>
185
  </head>
186
  <body>
187
  <div class="container">
188
- <!-- <div class="header">
189
  <h1>🤖 World Model Closed Loop Framework</h1>
190
- <p>Navigate through iterations and explore the decision-making process</p>
191
- </div> -->
192
-
193
- <div class="demo-container">
194
- <!-- 顶部:控制区 | BEV | Final 并排 -->
195
- <div class="overview-panel" id="overviewPanel">
196
- <!-- 左列:控制区 -->
197
- <div class="control-panel">
198
- <div class="control-row">
199
- <div class="control-label">Scenario:</div>
200
- <div class="control-input">
201
- <select id="scenarioSelect" onchange="loadScenario()">
202
- <option value="0">Kitchen Navigation - Find Table</option>
203
- <option value="1">Living Room - Approach Blue Chair</option>
204
- <option value="2">Office Space - Navigate to Green Door</option>
205
- <option value="3">Bedroom - Find Yellow Lamp</option>
206
- </select>
207
- </div>
208
- </div>
209
- <div class="control-row">
210
- <div class="control-label">Target & Prompt:</div>
211
- <div class="control-input">
212
- <div id="scenarioInfo" class="scenario-info">🎯 Target: <strong>Table (Kitchen)</strong> | 📝 Prompt: "Navigate safely to the table in the kitchen"</div>
213
- </div>
214
- </div>
215
- <div class="control-row">
216
- <div class="control-label">Start Demo:</div>
217
- <div class="control-input">
218
- <button class="start-btn" onclick="startDemo()">🚀 Begin Closed Loop Process</button>
219
- </div>
220
  </div>
221
  </div>
222
 
223
- <!-- 中列:BEV -->
224
- <div class="birds-eye-section">
225
- <div class="section-header">🗺️ Bird's Eye View</div>
226
- <div class="section-content" id="birdEyeContent">
227
  <video id="birdEyeVideo"
228
  src="https://huggingface.co/spaces/WiW-collab/wiw-prototype/resolve/main/src/display/bev_video_5ZKStnWn8Zo.mp4"
229
- autoplay loop muted controls
230
- style="width:100%;max-height:200px;object-fit:contain;border-radius:8px;"
231
- onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
232
- Your browser does not support the video tag.
233
- </video>
234
- <div style="display:none;color:#999;text-align:center;padding:20px;font-style:italic;">
235
- 🎥 BEV Video Loading...
236
- </div>
237
  </div>
238
  </div>
239
 
240
- <!-- 右列:Final -->
241
- <div class="results-section">
242
- <div class="section-header">📊 Final Results & Video</div>
243
- <div class="results-content" id="resultsContent">
244
- <div style="display:flex;align-items:center;gap:10px;height:100%;width:100%;">
245
- <div class="final-status-display pending" id="statusDisplay" style="margin-bottom:0;flex-shrink:0;">Processing...</div>
246
- <video id="finalVideo"
247
- src="https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014/A001/world_model_gen/bbox_gen_video_1.mp4"
248
- controls
249
- style="flex:1;height:120px;border-radius:8px;display:none;"
250
- onerror="this.style.display='none'"></video>
251
- </div>
252
  </div>
253
  </div>
254
  </div>
255
 
256
- <!-- 时间线 -->
257
- <div class="timeline-container" id="timelineContainer">
258
- <div class="timeline-header">
259
- <div class="timeline-title">🎬 Navigation Timeline</div>
260
- <div class="frame-counter">Frame <span id="currentFrame">1</span> of <span id="totalFrames">8</span></div>
261
- </div>
262
- <div class="timeline">
263
- <div class="timeline-progress" id="timelineProgress"></div>
264
- <div class="timeline-markers" id="timelineMarkers"></div>
265
- </div>
266
- <div class="navigation-controls">
267
- <button class="nav-btn" id="prevBtn" onclick="previousFrame()" disabled>⏮ Previous</button>
268
- <button class="nav-btn" id="playBtn" onclick="togglePlayback()">▶️ Play</button>
269
- <button class="nav-btn" id="nextBtn" onclick="nextFrame()">Next ⏭</button>
270
- </div>
271
- </div>
272
-
273
- <!-- 主展示 -->
274
- <div class="main-content">
275
- <div class="iteration-display" id="iterationDisplay">
276
- <div class="iteration-header">
277
- <div class="iteration-title">Ready to Start</div>
278
- <div class="iteration-status">Waiting...</div>
279
- </div>
280
- <div style="flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:1.2em;">👆 Click "Begin Closed Loop Process" to start the demonstration</div>
281
- </div>
282
  </div>
283
  </div>
284
  </div>
285
 
286
  <script>
287
- let currentFrameIndex = 0;
288
- let totalFramesCount = 8;
289
- let isPlaying = false;
290
- let playInterval = null;
291
- let frames = [];
292
- let isRunning = false;
293
- let missionEnded = false;
294
- let realActionData = {};
295
-
296
- const DS_BASE = "https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014";
297
-
298
  const scenarios = [
299
- { name: "Kitchen Navigation - Find Table", target: "Table (Kitchen)", prompt: "Navigate safely to the table in the kitchen", frames: 8, environment: "Kitchen" },
300
- { name: "Living Room - Approach Blue Chair", target: "Blue Chair (Living Room)", prompt: "Move carefully to the blue armchair avoiding obstacles", frames: 12, environment: "Living Room" },
301
- { name: "Office Space - Navigate to Green Door", target: "Green Door (Exit)", prompt: "Navigate to the green emergency exit door", frames: 15, environment: "Office" },
302
- { name: "Bedroom - Find Yellow Lamp", target: "Yellow Lamp (Bedside)", prompt: "Approach the yellow bedside lamp without disturbing items", frames: 10, environment: "Bedroom" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  ];
304
- let currentScenario = scenarios[0];
305
-
306
- /* --------- 数据载入 --------- */
307
- async function loadActionData() {
308
- const frameIds = ["A000","A001","A002","A003","A004","A005","A006","A007"];
309
- realActionData = {};
310
- await Promise.all(frameIds.map(async id => {
311
- try {
312
- const resp = await fetch(`${DS_BASE}/${id}/action_plan.json`, { method: 'GET', headers: { 'Accept': 'application/json' }, cache: 'no-store' });
313
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
314
- const data = JSON.parse(await resp.text());
315
- realActionData[id] = data;
316
- } catch (e) {
317
- console.warn(`Load failed for ${id}:`, e);
318
- realActionData[id] = null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  }
320
  }));
321
  }
322
-
323
- /* Step2: 两个 plan(来自 planner_next-4.json) */
324
- function getPlansFromNext4(actionData){
325
- if (!actionData || !actionData.planner_data) return fallbackPlans();
326
- const pd = actionData.planner_data;
327
- let next4 = pd["planner_next-4.json"];
328
- if (next4) {
329
- if (Array.isArray(next4) && next4.length && Array.isArray(next4[0])) {
330
- const seq1 = next4[0] || [];
331
- const seq2 = next4[1] || [];
332
- const p1 = { title: "Plan 1 (next-4)", steps: seq1, description: seq1.join(" → "), ranking: 1 };
333
- const p2 = { title: "Plan 2 (next-4)", steps: seq2, description: seq2.join(" → "), ranking: 2 };
334
- return [p1, p2].filter(p => p.steps && p.steps.length);
335
- }
336
- if (Array.isArray(next4)) {
337
- const p1 = next4[0] ? { title:"Plan 1 (next-4)", steps:[next4[0]], description:String(next4[0]), ranking:1 } : null;
338
- const p2 = next4[1] ? { title:"Plan 2 (next-4)", steps:[next4[1]], description:String(next4[1]), ranking:2 } : null;
339
- return [p1,p2].filter(Boolean);
340
- }
341
  }
342
- if (pd["planner_next-1.json"]) {
343
- const arr = pd["planner_next-1.json"];
344
- if (Array.isArray(arr)) {
345
- const p1 = arr[0] ? { title:"Plan 1", steps:[arr[0]], description:String(arr[0]), ranking:1 } : null;
346
- const p2 = arr[1] ? { title:"Plan 2", steps:[arr[1]], description:String(arr[1]), ranking:2 } : null;
347
- return [p1,p2].filter(Boolean);
 
 
 
 
348
  }
 
 
 
 
 
 
 
349
  }
350
  return fallbackPlans();
351
  }
352
  function fallbackPlans(){
353
  return [
354
- { title:"Plan 1", steps:["go straight for 0.20m","go straight for 0.20m","turn right 22.5 degrees","go straight for 0.20m"], description:"go straight → go straight → turn right 22.5° → go straight", ranking:1 },
355
- { title:"Plan 2", steps:["turn right 22.5 degrees","go straight for 0.20m","go straight for 0.20m","turn left 22.5 degrees"], description:"turn right 22.5° → go straight → go straight → turn left 22.5°", ranking:2 }
356
  ];
357
  }
358
-
359
- /* Step3: 三个 action(planner_next-1.json)+ 占位 confidence 50/30/20 */
360
- const PLACEHOLDER_CONF = {1:50,2:30,3:20};
361
- function niceActionTitle(raw){
362
- const s = String(raw||"").toLowerCase();
363
- if (s.includes("go straight")) return "Go straight";
364
- if (s.includes("turn left")) return "Turn left";
365
- if (s.includes("turn right")) return "Turn right";
366
- return raw || "Action";
367
- }
368
- function getRankedActionsFromNext1(actionData){
369
- if (!actionData || !actionData.planner_data) return [];
370
- const arr = actionData.planner_data["planner_next-1.json"];
371
- if (!Array.isArray(arr) || arr.length===0) return [];
372
- return arr.slice(0,3).map((a,idx)=>({
373
- title: niceActionTitle(a),
374
- ranking: idx+1,
375
- conf: PLACEHOLDER_CONF[idx+1] || 20
376
- }));
377
  }
 
378
 
379
- /* answerer_data top-1 */
380
- function getTopAnswerer(actionData){
381
- if (!actionData || !actionData.answerer_data) return {label:'—', score:0};
382
- const entries = Object.entries(actionData.answerer_data);
383
- if (!entries.length) return {label:'—', score:0};
384
- entries.sort((a,b)=>b[1]-a[1]);
385
- const [label,score] = entries[0];
386
- return {label, score};
387
- }
388
- function pct(x){ return (Math.round(x*1000)/10).toFixed(1) + '%'; }
389
-
390
- /* 仅 obj_centered:HEAD + Range GET 兜底;兼容 base/_1/_2 */
391
- function buildObjCenteredOnly(frameKey) {
392
- const base = `${DS_BASE}/${frameKey}/world_model_gen`;
393
- const names = ["obj_centered_gen_video.mp4","obj_centered_gen_video_1.mp4","obj_centered_gen_video_2.mp4"];
394
- const v = Date.now();
395
- return names.map(n => `${base}/${n}?v=${v}`);
396
- }
397
- async function probe(url){
398
- try{ const h = await fetch(url, { method:"HEAD", cache:"no-store" }); if (h.ok) return true; }catch{}
399
- try{ const g = await fetch(url, { method:"GET", headers:{Range:"bytes=0-31"}, cache:"no-store" }); if (g.ok) return true; }catch{}
400
- return false;
401
- }
402
- async function findUpToTwoVideos(frameKey) {
403
- const candidates = buildObjCenteredOnly(frameKey);
404
- const found = [];
405
- for (const u of candidates) { if (await probe(u)) { found.push(u); if (found.length===2) break; } }
406
- return found;
407
- }
408
- function setVideoSrcOrPlaceholder(videoEl, url) {
409
- if (url) { videoEl.src = url; videoEl.load(); }
410
- else { videoEl.replaceWith(document.createTextNode("Video unavailable")); }
411
- }
412
 
413
- /* --------- 场景控制 --------- */
414
- function loadScenario(){
415
- const select = document.getElementById('scenarioSelect');
416
- currentScenario = scenarios[parseInt(select.value)];
417
- document.getElementById('scenarioInfo').innerHTML =
418
- `🎯 Target: <strong>${currentScenario.target}</strong> | 📝 Prompt: "${currentScenario.prompt}"`;
419
- totalFramesCount = currentScenario.frames;
420
-
421
- if (isRunning){
422
- if (isPlaying) togglePlayback();
423
- document.getElementById('timelineContainer').style.display = 'none';
424
- document.getElementById('overviewPanel').style.display = 'none';
425
- document.querySelector('.demo-container').classList.remove('expanded');
426
- isRunning = false; missionEnded = false;
427
- const display = document.getElementById('iterationDisplay');
428
- display.innerHTML =
429
- `<div class="iteration-header"><div class="iteration-title">Ready to Start</div><div class="iteration-status">Waiting...</div></div>
430
- <div style="flex:1;display:flex;align-items:center;justify-content:center;color:#666;font-size:1.2em;">👆 Click "Begin Closed Loop Process" to start the demonstration</div>`;
431
- display.classList.remove('active');
432
- }
433
- }
434
 
435
- /* --------- 生命周期 --------- */
436
- async function startDemo(){
437
- if (isRunning) return;
438
- await loadActionData();
 
 
 
 
 
439
 
440
- isRunning = true; missionEnded = false;
441
- document.querySelector('.demo-container').classList.add('expanded');
442
- document.getElementById('overviewPanel').style.display = 'grid';
443
- document.getElementById('timelineContainer').style.display = 'block';
444
 
445
- const frameKeys = ["A000","A001","A002","A003","A004","A005","A006","A007"];
446
- frames = Array.from({length: totalFramesCount}, (_, i) => {
447
- const frameKey = frameKeys[Math.min(i, frameKeys.length - 1)];
448
- const actionData = realActionData[frameKey];
449
  return {
450
  frameNumber: i + 1,
451
- frameKey,
452
  status: i === 0 ? 'current' : 'pending',
453
- observation: `Frame ${i + 1} - ${currentScenario.environment} view`,
454
- plans: getPlansFromNext4(actionData), // Step2
455
- actions3: getRankedActionsFromNext1(actionData), // Step3
456
  selectedAction: null,
457
  completed: false,
458
- currentStep: 0, // 1,2,3
459
- actionData
460
  };
461
  });
462
-
463
  currentFrameIndex = 0;
464
- document.getElementById('totalFrames').textContent = totalFramesCount;
465
- createTimeline();
466
- displayFrame(0);
467
- setTimeout(() => { processCurrentFrame(); }, 800);
468
- }
 
 
 
 
 
469
 
470
- /* 核心流程(3 步) */
471
- function processCurrentFrame(){
472
- const frame = frames[currentFrameIndex];
473
- if (frame.completed || missionEnded) return;
474
-
475
- frame.status = 'current';
476
- frame.currentStep = 1; displayFrame(currentFrameIndex); // Observe
477
-
478
- setTimeout(() => {
479
- frame.currentStep = 2; displayFrame(currentFrameIndex); // Plan & Simulate
480
- setTimeout(() => {
481
- frame.currentStep = 3; displayFrame(currentFrameIndex); // Select
482
- setTimeout(() => {
483
- const { score } = getTopAnswerer(frame.actionData);
484
- if (score >= 0.95) {
485
- const statusElement = document.getElementById('statusDisplay');
486
- statusElement.innerHTML = 'Mission<br>Success!';
487
- statusElement.className = 'final-status-display success';
488
- document.getElementById('finalVideo').style.display = 'block';
489
- missionEnded = true;
490
- if (isPlaying) togglePlayback();
491
- } else {
492
- if (frame.actions3 && frame.actions3.length) {
493
- frame.selectedAction = frame.actions3[0];
494
- frame._pulseIdx = 0; // 渲染后触发动画
495
- }
496
- }
497
- frame.completed = true;
498
- frame.status = 'completed';
499
- frame.currentStep = 0;
500
- displayFrame(currentFrameIndex);
501
- }, 900);
502
- }, 1200);
503
- }, 800);
504
- }
505
 
506
- /* 选择动作(点击) */
507
- function selectAction(frameIndex, idx, animate=false){
508
- const frame = frames[frameIndex];
509
- frame.selectedAction = (frame.actions3||[])[idx] || null;
510
- displayFrame(frameIndex);
511
- if (animate) {
512
- requestAnimationFrame(() => {
513
- const cards = document.querySelectorAll('.action-list .plan-card');
514
- const card = cards[idx];
515
- if (card) {
516
- card.classList.remove('just-selected');
517
- void card.offsetWidth;
518
- card.classList.add('just-selected');
519
- }
520
- });
521
- }
522
  }
523
 
524
- /* 时间线/播放 */
525
- function previousFrame(){ if (!missionEnded && currentFrameIndex > 0) displayFrame(currentFrameIndex - 1); }
526
- function nextFrame(){
527
- if (missionEnded) return;
528
- if (currentFrameIndex < totalFramesCount - 1){
529
- const nextIndex = currentFrameIndex + 1;
530
- displayFrame(nextIndex);
531
- if (frames[nextIndex].status === 'pending'){ processCurrentFrame(); }
532
- }
533
- }
534
- function togglePlayback(){
535
- const playBtn = document.getElementById('playBtn');
536
- if (isPlaying){
537
- clearInterval(playInterval);
538
- playBtn.textContent = '▶️ Play';
539
- isPlaying = false;
540
- } else {
541
- if (missionEnded) return;
542
- playBtn.textContent = '⏸️ Pause';
543
- isPlaying = true;
544
- playInterval = setInterval(() => {
545
- if (missionEnded) { togglePlayback(); return; }
546
- if (currentFrameIndex < totalFramesCount - 1) nextFrame();
547
- else togglePlayback();
548
- }, 2800);
549
- }
550
- }
551
- function updateNavigationButtons(){
552
- document.getElementById('prevBtn').disabled = currentFrameIndex === 0;
553
- document.getElementById('nextBtn').disabled = missionEnded || currentFrameIndex === totalFramesCount - 1;
554
- }
555
- function createTimeline(){
556
- const markersContainer = document.getElementById('timelineMarkers');
557
- markersContainer.innerHTML = '';
558
- for (let i = 0; i < totalFramesCount; i++){
559
- const marker = document.createElement('div');
560
- marker.className = 'frame-marker';
561
- marker.textContent = i + 1;
562
- marker.style.left = `${(i / Math.max(1,(totalFramesCount - 1))) * 100}%`;
563
- marker.onclick = () => { if (!missionEnded) jumpToFrame(i); };
564
- if (i === 0) marker.classList.add('current');
565
- markersContainer.appendChild(marker);
566
- }
567
- }
568
- function updateTimeline(){
569
- const progress = ((currentFrameIndex + 1) / totalFramesCount) * 100;
570
- document.getElementById('timelineProgress').style.width = progress + '%';
571
- document.getElementById('currentFrame').textContent = currentFrameIndex + 1;
572
- const markers = document.querySelectorAll('.frame-marker');
573
- markers.forEach((marker, index) => {
574
- marker.className = 'frame-marker';
575
- if (index < currentFrameIndex) marker.classList.add('completed');
576
- else if (index === currentFrameIndex) marker.classList.add('current');
577
- });
578
- }
579
- function jumpToFrame(frameIndex){
580
- if (isPlaying) togglePlayback();
581
- displayFrame(frameIndex);
582
- if (frames[frameIndex].status === 'pending' && frameIndex <= currentFrameIndex + 1){ processCurrentFrame(); }
583
  }
584
 
585
- /* --------- 渲染 --------- */
586
- function displayFrame(frameIndex){
587
- if (frameIndex < 0 || frameIndex >= totalFramesCount) return;
588
- currentFrameIndex = frameIndex;
589
- const frame = frames[frameIndex];
590
- updateTimeline();
591
- updateNavigationButtons();
592
- updateResults();
593
- const display = document.getElementById('iterationDisplay');
594
 
595
- const {label:topLabel, score:topScore} = getTopAnswerer(frame.actionData);
596
- const highConf = topScore >= 0.95;
597
-
598
- const sequenceIndicatorHTML = `
599
  <div class="sequence-indicator">
600
- <div class="sequence-title">🔄 Processing Sequence</div>
601
- <div class="sequence-flow">
602
- <div class="sequence-step ${frame.currentStep === 1 ? 'active' : ''}"><div class="sequence-number">1</div><span>Observe</span></div>
603
- <div class="sequence-arrow">→</div>
604
- <div class="sequence-step ${frame.currentStep === 2 ? 'active' : ''}"><div class="sequence-number">2</div><span>Plan & Simulate</span></div>
605
- <div class="sequence-arrow">→</div>
606
- <div class="sequence-step ${frame.currentStep === 3 ? 'active' : ''}"><div class="sequence-number">3</div><span>Select</span></div>
607
- </div>
608
- </div>`;
609
-
610
- const obsHTML = `
611
- <div class="step-card ${(frame.status === 'current' || frame.completed) && frame.currentStep === 1 ? 'active processing' : (frame.status === 'current' || frame.completed) ? 'active' : ''}">
612
- <div class="step-header">
613
- <div class="step-number ${frame.currentStep === 1 ? 'active-number' : ''}">1</div>
614
- <div class="step-title">Current Observation</div>
615
- </div>
616
- <div class="step-content" style="padding:8px;">
617
- <img src="${DS_BASE}/${frame.frameKey}/real_obs_bbox.png"
618
- alt="Current Observation"
619
- style="width:100%;max-height:200px;object-fit:contain;border-radius:6px;margin-bottom:8px;"
620
- onerror="this.replaceWith(Object.assign(document.createElement('div'),{innerText:'Image unavailable',style:'color:#999;padding:8px'}))">
621
- <small style="color:#666;">First-Person Camera View with Bounding Boxes</small>
622
  </div>
623
  </div>`;
624
 
625
- /* 中列:Step 2 (Plan & Simulate) */
626
- function planBox(p){
627
- const items = (p?.steps||[]).slice(0,4).map(s=>`<li>${s}</li>`).join('') || '<li>—</li>';
628
- return `<div class="plan-box"><div style="font-weight:600;margin-bottom:6px;color:#333">${p?.title||'Plan'}</div><ul style="text-align:left;list-style:disc;margin-left:18px;color:#555">${items}</ul></div>`;
629
- }
630
- const comboHTML = `
631
- <div class="step-card ${(frame.status === 'current' || frame.completed) && frame.currentStep === 2 ? 'active processing' : (frame.status === 'current' || frame.completed) ? 'active' : ''}">
632
- <div class="step-header">
633
- <div class="step-number ${frame.currentStep === 2 ? 'active-number' : ''}">2</div>
634
- <div class="step-title">Planning & World Model Simulation</div>
635
- </div>
636
- <div class="step-content" style="gap:12px;">
637
- <div class="plan-pairs">
638
- ${planBox(frame.plans[0])}
639
- ${planBox(frame.plans[1])}
640
- </div>
641
- <div class="subsection-title" style="margin-top:10px;">Object-centered Predictions</div>
642
- <div class="sim-grid">
643
- <div>
644
- <video id="pred1_${frame.frameKey}" controls></video>
645
- <div style="text-align:center;font-size:.8em;color:#888;margin-top:4px;">Prediction 1</div>
646
- </div>
647
- <div>
648
- <video id="pred2_${frame.frameKey}" controls></video>
649
- <div style="text-align:center;font-size:.8em;color:#888;margin-top:4px;">Prediction 2</div>
650
- </div>
651
- </div>
652
  </div>
653
  </div>`;
654
 
655
- /* 右列:Step 3 (Select) */
656
- function actionCardHTML(action, idx){
657
- const p = action?.conf ?? (idx===0?50:idx===1?30:20);
658
- const selected = frame.selectedAction === action ? 'selected' : '';
659
- const onclick = highConf ? '' : `onclick="selectAction(${frameIndex}, ${idx}, true)"`;
660
- return `
661
- <div class="plan-card ${selected}" ${onclick}>
662
- <div class="plan-title">${action?.title||'Action'}</div>
663
- <div class="confidence-bar"><div class="confidence-fill" style="width:${p}%"></div></div>
664
- <div style="display:flex;justify-content:space-between;margin-top:6px;font-size:.85em;color:#666;">
665
- <span>${idx===0?'🥇 1st Choice':idx===1?'🥈 2nd Choice':'🥉 3rd Choice'}</span>
666
- <span>${p}%</span>
667
- </div>
668
- </div>`;
669
- }
670
 
671
- const decisionExplain =
672
- highConf
673
- ? `<div style="padding:10px;border-radius:8px;background:#e8f5e9;color:#1e4620;border:1px solid #c6e6c9">
674
- High confidence (≥95%) on <b>${topLabel}</b> → <b>Stop</b> selecting actions and declare success.
675
- <span class="success-chip">✅ Success</span>
676
- </div>`
677
- : `<div style="padding:10px;border-radius:8px;background:#fff3cd;color:#7a5a00;border:1px solid #ffe69c">
678
- Confidence is <b>${pct(topScore)}</b> &lt; 95% → choose the <b>highest-ranked</b> action.
679
- </div>`;
680
-
681
- const rightHTML = `
682
- <div class="step-card ${(frame.status === 'current' || frame.completed) && frame.currentStep === 3 ? 'active processing' : (frame.status === 'current' || frame.completed) ? 'active' : ''}">
683
- <div class="step-header">
684
- <div class="step-number ${frame.currentStep === 3 ? 'active-number' : ''}">3</div>
685
- <div class="step-title">Action Selection & Confidence</div>
686
  </div>
687
- <div class="step-content" style="gap:10px;">
688
- <div style="font-style:normal;">
689
- <b>Top object:</b> ${topLabel} &nbsp; <b>Confidence:</b> ${pct(topScore)}
690
- </div>
691
- ${decisionExplain}
692
- ${highConf ? '' : `
693
- <div class="action-list">
694
- ${actionCardHTML(frame.actions3[0] || {title:'Go straight',conf:50}, 0)}
695
- ${actionCardHTML(frame.actions3[1] || {title:'Turn right',conf:30}, 1)}
696
- ${actionCardHTML(frame.actions3[2] || {title:'Turn left',conf:20}, 2)}
697
- </div>
698
- `}
699
  </div>
700
  </div>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
701
 
702
- display.innerHTML = `
703
  <div class="iteration-header">
704
- <div class="iteration-title">Frame ${frame.frameNumber} - Closed Loop Iteration (${frame.frameKey})</div>
705
- <div class="iteration-status">${frame.completed ? 'Completed' : frame.status === 'current' ? 'Processing...' : 'Ready'}</div>
 
 
 
706
  </div>
707
- ${frame.status === 'current' ? sequenceIndicatorHTML : ''}
708
- <div class="step-grid">
709
- ${obsHTML}
710
- ${comboHTML}
711
- ${rightHTML}
712
- </div>`;
713
-
714
- // 初始化两个预测视频
715
- (async () => {
716
- try {
717
- const k = frame.frameKey;
718
- const v1 = document.getElementById(`pred1_${k}`);
719
- const v2 = document.getElementById(`pred2_${k}`);
720
- const urls = await findUpToTwoVideos(k);
721
- if (v1) setVideoSrcOrPlaceholder(v1, urls[0] || null);
722
- if (v2) setVideoSrcOrPlaceholder(v2, urls[1] || null);
723
- } catch (e) { console.warn("init obj_centered videos failed", e); }
724
  })();
 
725
 
726
- if (frame.status === 'current' || frame.completed) display.classList.add('active');
727
- else display.classList.remove('active');
728
-
729
- // 自动选择的动效(未成功时)
730
- if (typeof frame._pulseIdx === 'number') {
731
- const idxToPulse = frame._pulseIdx;
732
- requestAnimationFrame(() => {
733
- requestAnimationFrame(() => {
734
- const cards = document.querySelectorAll('.action-list .plan-card');
735
- const card = cards[idxToPulse];
736
- if (card) {
737
- card.classList.remove('just-selected');
738
- void card.offsetWidth;
739
- card.classList.add('just-selected');
740
- }
741
- delete frame._pulseIdx;
742
- });
743
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
744
  }
745
  }
746
 
747
- /* 结果区状态 */
748
- function updateResults(){
749
- const completedFrames = frames.filter(f => f.completed).length;
750
- const statusElement = document.getElementById('statusDisplay');
751
- const finalVideo = document.getElementById('finalVideo');
752
- if (completedFrames === totalFramesCount || missionEnded){
753
- if (!statusElement.className.includes('success')) {
754
- statusElement.textContent = missionEnded ? statusElement.textContent : 'Mission Completed';
755
- statusElement.className = missionEnded ? statusElement.className : 'final-status-display pending';
756
- }
757
- finalVideo.style.display = 'block';
758
- } else if (completedFrames > 0){
759
- statusElement.textContent = 'In Progress...';
760
- statusElement.className = 'final-status-display pending';
761
- finalVideo.style.display = 'none';
762
  }
763
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  </script>
765
  </body>
766
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />
6
+ <title>World Model Closed Loop Framework</title>
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;
18
+ background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:14px;
19
+ }
20
+ .header h1{font-size:2.1em;margin-bottom:6px;text-shadow:2px 2px 4px rgba(0,0,0,.25)}
21
+ .header p{opacity:.9}
22
 
23
+ .selector-shell{
24
+ background:#fff;border-radius:14px;padding:16px;
25
+ box-shadow:0 10px 28px rgba(0,0,0,.12);
26
+ }
27
+ .selector-title{font-weight:800;color:#263238;margin-bottom:10px}
28
+ .thumb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:12px}
29
+ .thumb-card{
30
+ border:2px solid #e6e9ef;border-radius:10px;padding:10px;cursor:pointer;background:#fff;transition:.2s
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
+
38
+ .desc-row{margin-top:14px;display:flex;gap:12px;flex-wrap:wrap;align-items:flex-start}
39
+ .scenario-info{
40
+ flex:1;min-width:260px;padding:12px;border:2px solid #ff9800;border-radius:10px;
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;
51
+ box-shadow:0 10px 28px rgba(0,0,0,.12);
52
+ }
53
+ .demo-shell.visible{display:block}
54
+
55
+ .overview-panel{
56
+ display:grid;grid-template-columns:0.6fr 1fr 1fr;
57
+ gap:14px;align-items:stretch;margin-bottom:14px;
58
+ }
59
+ .panel-card{
60
+ background:#fff;border:2px solid #e8eaed;border-radius:12px;padding:12px;display:flex;flex-direction:column;min-height:170px
61
+ }
62
+ .section-header{
63
+ background:linear-gradient(135deg,#34a853,#4285f4);color:#fff;padding:8px 12px;border-radius:8px;text-align:center;font-weight:700;margin-bottom:10px;font-size:.9em
64
+ }
65
+ .results-section .section-header{background:linear-gradient(135deg,#ea4335,#fbbc04)}
66
+ .results-content{flex:1;display:flex;align-items:center;justify-content:center;background:#f8f9fa;border-radius:8px;padding:8px}
67
+ #birdEyeVideo,#finalVideo{width:100%;max-height:190px;object-fit:contain;border-radius:6px}
68
+
69
+ .final-status-display{padding:6px 12px;border-radius:14px;font-weight:800;display:inline-block}
70
+ .final-status-display.success{background:#34a853;color:#fff}
71
+
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}
81
+ .sequence-step{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.18);padding:5px 10px;border-radius:16px}
82
+ .sequence-step.active{background:rgba(255,255,255,.32)}
83
+
84
+ .flow-num{width:22px;height:22px;border-radius:50%;background:#fff;color:#1f2937;display:inline-flex;align-items:center;justify-content:center;font-weight:800;font-size:.8em}
85
+ .step-num{width:22px;height:22px;border-radius:50%;background:#e3f2fd;color:#1565c0;display:inline-flex;align-items:center;justify-content:center;font-weight:800;font-size:.8em}
86
+
87
+ .step-grid{display:grid;grid-template-columns:0.8fr 1.5fr 0.9fr;gap:12px}
88
+ .step-card{border:2px solid #e8eaed;border-radius:10px;padding:12px}
89
+ .step-card.active{border-color:#4285f4}
90
+
91
+ .step-h{display:flex;align-items:center;gap:8px;font-weight:800;margin-bottom:6px}
92
+ .step-title{font-weight:800}
93
+
94
+ .obs-box{background:#f4f6f8;border-radius:6px;padding:6px;height:220px;display:flex;align-items:center;justify-content:center}
95
+ .obs-img{max-height:100%;width:auto;object-fit:contain;border-radius:4px;display:block}
96
+
97
+ .plan-pairs{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
98
+ .plan-box{border:1px dashed #cfd8dc;border-radius:6px;padding:8px}
99
+ .plan-box li{font-size:.75em}
100
+
101
+ .sim-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:12px}
102
+ .sim-grid video{width:100%;border-radius:4px;object-fit:contain;max-height:200px}
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;
110
+ background:#e8f5e9;border:1px solid #c6e6c9;border-radius:6px;text-align:center
111
+ }
112
+
113
+ .conf-bar{width:100%;height:6px;background:#eee;border-radius:4px;margin-top:4px;overflow:hidden}
114
+ .conf-fill{height:100%;background:linear-gradient(90deg,#ea4335,#fbbc04,#34a853)}
115
+
116
+ @media (max-width:1100px){
117
+ .overview-panel,.step-grid{grid-template-columns:1fr}
118
+ }
119
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
130
+ <div class="selector-title">👁️ Select by First-Frame Observation</div>
131
+ <div class="thumb-grid" id="thumbGrid"></div>
132
+
133
+ <div class="desc-row">
134
+ <div id="scenarioInfo" class="scenario-info">
135
+ 👉 Pick a scenario above to see its Target & Prompt here.
136
+ </div>
137
+ <div>
138
+ <button id="startBtn" class="start-btn" onclick="startDemo()" disabled>🚀 Begin Demo</button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+
143
+ <!-- ========= Demo area ========= -->
144
+ <div class="demo-shell" id="demoShell">
145
+ <div class="overview-panel">
146
+ <div class="panel-card">
147
+ <div class="section-header">📋 Current Scenario</div>
148
+ <div id="currentScenarioBox" style="padding:10px;background:#f6fff6;border:1px solid #cfe9cf;border-radius:8px"></div>
149
+ <div style="margin-top:12px;">
150
+ <div style="font-weight:700;color:#223; margin-bottom:6px;">🏁 Final Result</div>
151
+ <div id="statusDisplay" class="final-status-display success">Mission Success!</div>
 
 
 
152
  </div>
153
  </div>
154
 
155
+ <div class="panel-card">
156
+ <div class="section-header" id="bevHeaderTitle">🗺️ Bird's Eye View</div>
157
+ <div class="results-content">
 
158
  <video id="birdEyeVideo"
159
  src="https://huggingface.co/spaces/WiW-collab/wiw-prototype/resolve/main/src/display/bev_video_5ZKStnWn8Zo.mp4"
160
+ autoplay loop muted controls></video>
 
 
 
 
 
 
 
161
  </div>
162
  </div>
163
 
164
+ <div class="panel-card results-section">
165
+ <div class="section-header">🎥 Final Video</div>
166
+ <div class="results-content">
167
+ <video id="finalVideo" controls style="display:none"></video>
 
 
 
 
 
 
 
 
168
  </div>
169
  </div>
170
  </div>
171
 
172
+ <div class="iteration-display" id="iterationDisplay">
173
+ <div style="text-align:center;color:#666;padding:16px">Processing will begin shortly…</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </div>
175
  </div>
176
  </div>
177
 
178
  <script>
179
+ /* ========= Scenarios ========= */
 
 
 
 
 
 
 
 
 
 
180
  const scenarios = [
181
+ {
182
+ id:"kitchen", name:"Kitchen Navigation",
183
+ target:"Table (Kitchen)",
184
+ prompt:'Navigate safely to the table in the kitchen',
185
+ frames:7, environment:"Kitchen",
186
+ base:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014",
187
+ previewImg:"https://huggingface.co/datasets/zonszer/demo_source_data/resolve/main/AR/FTwan21_lora/5ZKStnWn8Zo/E014/A000/real_obs_bbox.png"
188
+ },
189
+ {
190
+ id:"living", name:"Living Room Navigation",
191
+ target:"Table (Living Room)",
192
+ prompt:"Move carefully to the table in the living room",
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 */
214
+ 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];
226
+ document.getElementById('startBtn').disabled=false;
227
+ document.getElementById('scenarioInfo').innerHTML=
228
+ `🎯 <b>${currentScenario.target}</b><br>📝 "${currentScenario.prompt}"`;
229
+ document.querySelectorAll('.thumb-card').forEach((c,i)=>c.classList.toggle('selected',i===idx));
230
+ }
231
+ document.addEventListener('click',e=>{
232
+ const card=e.target.closest('.thumb-card'); if(!card) return;
233
+ applySelection(parseInt(card.dataset.idx,10));
234
+ });
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
+
242
+ realActionData={};
243
+ await Promise.all(availableKeys.map(async id=>{
244
+ try{
245
+ const resp=await fetch(`${currentScenario.base}/${id}/action_plan.json`,
246
+ {headers:{'Accept':'application/json'},cache:'no-store'});
247
+ if(!resp.ok) throw new Error(`HTTP ${resp.status}`);
248
+ realActionData[id]=await resp.json();
249
+ }catch(e){
250
+ console.warn('Load failed for',id,e);
251
+ realActionData[id]=null;
252
  }
253
  }));
254
  }
255
+ function getRankedActionsFromNext1(d){
256
+ let arr = d?.planner_data?.["planner_next-1.json"];
257
+ if (typeof arr === 'string') arr = [arr];
258
+ if (Array.isArray(arr) && Array.isArray(arr[0])) {
259
+ arr = arr.map(seq => (Array.isArray(seq) ? seq[0] : seq)).filter(Boolean);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  }
261
+ if (!Array.isArray(arr)) return [];
262
+ return arr.slice(0,3).map((t,i)=>({ title: String(t), ranking:i+1, conf: PLACEHOLDER_CONF[i+1]||20 }));
263
+ }
264
+ function getPlansFromNext4(d){
265
+ if(!d||!d.planner_data) return fallbackPlans();
266
+ const pd=d.planner_data, n4=pd["planner_next-4.json"];
267
+ if(Array.isArray(n4)&&n4.length){
268
+ if(Array.isArray(n4[0])){
269
+ const a=n4[0]||[], b=n4[1]||[];
270
+ return [{title:"Plan 1 (next-4)",steps:a,ranking:1},{title:"Plan 2 (next-4)",steps:b,ranking:2}].filter(p=>p.steps.length);
271
  }
272
+ return [ n4[0] && {title:"Plan 1 (next-4)",steps:[n4[0]],ranking:1},
273
+ n4[1] && {title:"Plan 2 (next-4)",steps:[n4[1]],ranking:2} ].filter(Boolean);
274
+ }
275
+ const n1=pd["planner_next-1.json"];
276
+ if(Array.isArray(n1)){
277
+ return [ n1[0] && {title:"Plan 1",steps:[n1[0]],ranking:1},
278
+ n1[1] && {title:"Plan 2",steps:[n1[1]],ranking:2} ].filter(Boolean);
279
  }
280
  return fallbackPlans();
281
  }
282
  function fallbackPlans(){
283
  return [
284
+ { title:"Plan 1", steps:["go straight for 0.20m","go straight for 0.20m","turn right 22.5 degrees","go straight for 0.20m"], ranking:1 },
285
+ { title:"Plan 2", steps:["turn right 22.5 degrees","go straight for 0.20m","go straight for 0.20m","turn left 22.5 degrees"], ranking:2 }
286
  ];
287
  }
288
+ function getTopAnswerer(d){
289
+ if(!d||!d.answerer_data) return {label:'—',score:0};
290
+ const ent=Object.entries(d.answerer_data); if(!ent.length) return {label:'—',score:0};
291
+ ent.sort((a,b)=>b[1]-a[1]); const [label,score]=ent[0]; return {label,score};
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
+ function pct(x){return (Math.round(x*1000)/10).toFixed(1)+'%';}
294
 
295
+ /* start demo */
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 {
319
  frameNumber: i + 1,
320
+ frameKey: k,
321
  status: i === 0 ? 'current' : 'pending',
322
+ plans: getPlansFromNext4(d),
323
+ actions3: getRankedActionsFromNext1(d),
 
324
  selectedAction: null,
325
  completed: false,
326
+ currentStep: 0,
327
+ actionData: d
328
  };
329
  });
 
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`;
337
+ probe(finalUrl).then(ok => {
338
+ if (ok) { finalVid.src = finalUrl; finalVid.style.display = 'block'; finalVid.load?.(); }
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 */
458
+ function buildObjCenteredOnly(frameKey){
459
+ const base=`${currentScenario.base}/${frameKey}/world_model_gen/obj_centered_gen_video`;
460
+ const v=Date.now();
461
+ return [`${base}.mp4?v=${v}`,`${base}_1.mp4?v=${v}`,`${base}_01.mp4?v=${v}`,`${base}_2.mp4?v=${v}`,`${base}_02.mp4?v=${v}`];
462
+ }
463
+ async function probe(url){
464
+ try{
465
+ const r=await fetch(url,{method:'GET',headers:{Range:'bytes=0-127'},cache:'no-store'});
466
+ if(!r.ok) return false;
467
+ return (r.headers.get('content-type')||'').toLowerCase().startsWith('video/');
468
+ }catch{return false}
469
+ }
470
+ const __videoProbeCache=new Map();
471
+ async function findUpToTwoVideos(k){
472
+ if(__videoProbeCache.has(k)) return __videoProbeCache.get(k);
473
+ const found=[]; for(const u of buildObjCenteredOnly(k)){
474
+ if(found.length===2) break;
475
+ if(await probe(u)) found.push(u);
476
+ }
477
+ __videoProbeCache.set(k,found); return found;
478
+ }
479
+ function setVideoSrcOrPlaceholder(el,url){
480
+ if(!el) return;
481
+ if(url){
482
+ el.hidden=false; el.crossOrigin="anonymous"; el.preload="metadata"; el.muted=true;
483
+ if(el.src!==url) el.src=url;
484
+ el.onerror=()=>{el.removeAttribute('src'); el.hidden=true};
485
+ el.load?.();
486
+ } else {
487
+ el.removeAttribute('src'); el.hidden=true;
488
  }
489
  }
490
 
491
+ /* nav controls */
492
+ function previousFrame(){ if (isPlaying) togglePlayback(); if (currentFrameIndex > 0) displayFrame(currentFrameIndex - 1); }
493
+ function nextFrame(){
494
+ if (isPlaying) togglePlayback();
495
+ if (currentFrameIndex < frames.length - 1){
496
+ displayFrame(currentFrameIndex + 1);
497
+ if (!frames[currentFrameIndex].completed) processCurrentFrame();
 
 
 
 
 
 
 
 
498
  }
499
  }
500
+ function togglePlayback(){
501
+ const btn=document.getElementById('playBtn');
502
+ if (missionEnded){ return; }
503
+ if (isPlaying){ clearInterval(playInterval); btn.textContent='▶️ Play'; isPlaying=false; return; }
504
+ btn.textContent='⏸️ Pause'; isPlaying=true;
505
+ playInterval=setInterval(()=>{
506
+ if (missionEnded || currentFrameIndex>=frames.length-1){
507
+ clearInterval(playInterval); btn.textContent='▶️ Play'; isPlaying=false; return;
508
+ }
509
+ nextFrame();
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(); }
517
+ if (id === 'nextBtn'){ e.preventDefault(); nextFrame(); }
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>
557
  </html>