Capstone project. I'm ngl it's vibe-coded and it's only here so I can mess around with it
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor session flow for fullscreen calibration and test start

- Split setup, session, and debrief shells
- Move calibration into a fullscreen overlay with fixed target placement
- Replace the debug end-test button with a keyboard shortcut and refresh metrics on resize

+310 -196
+138 -100
index.css
··· 7 7 --line: rgba(27, 26, 23, 0.12); 8 8 --accent: #a33b24; 9 9 --accent-strong: #7e2817; 10 - --ok: #276749; 11 - --warn: #b45309; 12 10 --shadow: 0 24px 60px rgba(41, 31, 22, 0.12); 13 11 } 14 12 ··· 28 26 line-height: 1.45; 29 27 } 30 28 29 + body.session-active { 30 + overflow: hidden; 31 + } 32 + 31 33 button, 32 34 select, 33 35 input, ··· 35 37 font: inherit; 36 38 } 37 39 40 + button, 41 + select { 42 + border-radius: 14px; 43 + border: 1px solid var(--line); 44 + padding: 12px 16px; 45 + background: var(--panel-solid); 46 + color: var(--ink); 47 + } 48 + 49 + button { 50 + cursor: pointer; 51 + transition: transform 0.16s ease, background 0.16s ease, border-color 0.16s ease; 52 + } 53 + 54 + button:hover:not(:disabled) { 55 + transform: translateY(-1px); 56 + border-color: rgba(163, 59, 36, 0.35); 57 + } 58 + 59 + button:disabled { 60 + opacity: 0.45; 61 + cursor: not-allowed; 62 + } 63 + 64 + #load-app-btn, 65 + #export-btn, 66 + #start-test-btn { 67 + background: linear-gradient(135deg, var(--accent), var(--accent-strong)); 68 + color: #fff8f3; 69 + border: none; 70 + } 71 + 38 72 #app-container { 39 73 min-height: 100vh; 74 + } 75 + 76 + #setup-shell, 77 + #debrief-shell { 78 + min-height: 100vh; 40 79 padding: 24px; 80 + } 81 + 82 + #setup-shell { 41 83 display: grid; 42 84 grid-template-rows: auto auto 1fr; 43 85 gap: 16px; ··· 59 101 } 60 102 61 103 .brand-block h1, 62 - .panel-header h2 { 104 + .panel-header h2, 105 + .start-card h2 { 63 106 margin: 0; 64 107 font-size: clamp(1.8rem, 2vw, 2.5rem); 65 108 letter-spacing: -0.03em; ··· 67 110 68 111 .brand-block p, 69 112 .panel-header p, 70 - #screen-gallery-label { 113 + #screen-gallery-label, 114 + .start-card p, 115 + .debug-copy { 71 116 margin: 6px 0 0; 72 117 color: var(--muted); 73 118 } ··· 88 133 } 89 134 90 135 .field span, 91 - .label { 136 + .label, 137 + .start-label { 92 138 font-size: 0.78rem; 93 139 text-transform: uppercase; 94 140 letter-spacing: 0.08em; 95 141 color: var(--muted); 96 142 } 97 143 98 - select, 99 - button { 100 - border-radius: 14px; 101 - border: 1px solid var(--line); 102 - padding: 12px 16px; 103 - background: var(--panel-solid); 104 - color: var(--ink); 105 - } 106 - 107 - button { 108 - cursor: pointer; 109 - transition: transform 0.16s ease, background 0.16s ease, border-color 0.16s ease; 110 - } 111 - 112 - button:hover:not(:disabled) { 113 - transform: translateY(-1px); 114 - border-color: rgba(163, 59, 36, 0.35); 115 - } 116 - 117 - button:disabled { 118 - opacity: 0.45; 119 - cursor: not-allowed; 120 - } 121 - 122 - #load-app-btn, 123 - #start-test-btn, 124 - #export-btn { 125 - background: linear-gradient(135deg, var(--accent), var(--accent-strong)); 126 - color: #fff8f3; 127 - border: none; 128 - } 129 - 130 144 .primary-actions, 131 - .debug-shell { 145 + .debug-shell, 146 + #debug-panel { 132 147 display: flex; 133 148 align-items: center; 134 149 gap: 10px; ··· 139 154 background: transparent; 140 155 } 141 156 142 - #debug-panel { 143 - display: flex; 144 - gap: 10px; 145 - flex-wrap: wrap; 146 - } 147 - 148 157 #status-bar { 149 158 border-radius: 18px; 150 159 padding: 14px 18px; ··· 153 162 gap: 12px; 154 163 } 155 164 156 - #status-bar > div { 165 + #status-bar > div, 166 + #stats-container li { 157 167 display: grid; 158 168 gap: 4px; 159 169 } 160 170 161 - #main-content { 162 - position: relative; 171 + #shell-main { 163 172 border-radius: 24px; 164 - overflow: hidden; 165 173 border: 1px solid var(--line); 166 174 background: rgba(255, 249, 240, 0.72); 167 - min-height: 65vh; 168 - } 169 - 170 - #iframe-placeholder, 171 - #test-iframe, 172 - #calibration-screen, 173 - #debrief-screen { 174 - position: absolute; 175 - inset: 0; 175 + min-height: 48vh; 176 + display: grid; 176 177 } 177 178 178 179 #iframe-placeholder { ··· 181 182 color: var(--muted); 182 183 font-size: 1.1rem; 183 184 padding: 32px; 185 + text-align: center; 184 186 } 185 187 186 - #test-iframe { 187 - width: 100%; 188 - height: 100%; 189 - border: none; 190 - background: white; 188 + #session-stage { 189 + position: fixed; 190 + inset: 0; 191 + z-index: 100; 192 + background: #000; 191 193 } 192 194 195 + #test-iframe, 193 196 #calibration-screen, 194 - #debrief-screen { 195 - z-index: 2; 197 + #start-overlay { 198 + position: absolute; 199 + inset: 0; 196 200 } 197 201 198 - #calibration-screen { 199 - background: rgba(20, 17, 14, 0.16); 202 + #test-iframe { 203 + width: 100vw; 204 + height: 100vh; 205 + border: none; 206 + background: #fff; 200 207 } 201 208 202 - .overlay-card { 203 - border-radius: 22px; 209 + #calibration-screen { 210 + z-index: 2; 204 211 } 205 212 206 - .calibration-panel, 207 - .debrief-panel { 213 + #calibration-veil { 208 214 position: absolute; 209 - top: 20px; 210 - left: 20px; 211 - right: 20px; 212 - padding: 20px; 213 - z-index: 3; 215 + inset: 0; 216 + background: rgba(14, 12, 10, 0.16); 214 217 } 215 218 216 - .calibration-stats { 217 - display: grid; 218 - grid-template-columns: repeat(4, minmax(0, 1fr)); 219 - gap: 12px; 220 - margin-top: 18px; 221 - } 222 - 223 - .calibration-stats > div, 224 - #stats-container li { 225 - display: grid; 226 - gap: 4px; 219 + #calibration-hud { 220 + position: absolute; 221 + top: 14px; 222 + left: 14px; 223 + right: 14px; 224 + display: flex; 225 + flex-wrap: wrap; 226 + gap: 10px 16px; 227 + align-items: center; 228 + min-height: 56px; 229 + padding: 12px 14px; 230 + border-radius: 16px; 231 + background: rgba(255, 250, 243, 0.9); 232 + border: 1px solid rgba(27, 26, 23, 0.12); 233 + box-shadow: 0 16px 36px rgba(17, 12, 10, 0.14); 234 + backdrop-filter: blur(10px); 235 + pointer-events: none; 227 236 } 228 237 229 238 #calibration-stage { ··· 247 256 box-shadow: 0 18px 36px rgba(126, 40, 23, 0.35); 248 257 } 249 258 259 + #start-overlay { 260 + z-index: 3; 261 + display: grid; 262 + place-items: center; 263 + background: rgba(14, 12, 10, 0.2); 264 + padding: 24px; 265 + } 266 + 267 + .start-card { 268 + width: min(420px, calc(100vw - 32px)); 269 + padding: 22px; 270 + border-radius: 20px; 271 + background: rgba(255, 250, 243, 0.96); 272 + border: 1px solid rgba(27, 26, 23, 0.12); 273 + box-shadow: var(--shadow); 274 + } 275 + 276 + #debrief-shell { 277 + display: block; 278 + } 279 + 250 280 #debrief-screen { 251 - overflow: auto; 281 + background: rgba(247, 242, 234, 0.95); 282 + border-radius: 22px; 283 + padding: 20px; 284 + min-height: calc(100vh - 48px); 285 + border: 1px solid var(--line); 286 + } 287 + 288 + .overlay-card { 289 + border-radius: 22px; 290 + } 291 + 292 + .debrief-panel { 252 293 padding: 20px; 253 - background: rgba(247, 242, 234, 0.95); 254 294 } 255 295 256 296 #stats-container { ··· 274 314 } 275 315 276 316 #screen-gallery { 277 - margin-top: 196px; 317 + margin-top: 20px; 278 318 display: grid; 279 319 gap: 18px; 280 320 } ··· 337 377 } 338 378 339 379 @media (max-width: 900px) { 340 - #app-container { 380 + #setup-shell, 381 + #debrief-shell { 341 382 padding: 16px; 342 383 } 343 384 344 385 #status-bar, 345 - .calibration-stats, 346 386 #stats-container { 347 387 grid-template-columns: 1fr; 348 388 } 349 389 350 - .calibration-panel, 351 - .debrief-panel { 352 - left: 12px; 353 - right: 12px; 354 - top: 12px; 355 - } 356 - 357 - #screen-gallery { 358 - margin-top: 290px; 390 + #calibration-hud { 391 + top: 10px; 392 + left: 10px; 393 + right: 10px; 394 + gap: 8px 12px; 395 + padding: 10px 12px; 396 + min-height: 64px; 359 397 } 360 398 361 399 .screen-card-header {
+65 -56
index.html
··· 12 12 13 13 <body> 14 14 <div id="app-container"> 15 - <header id="controls"> 16 - <div class="brand-block"> 17 - <h1>UXET</h1> 18 - <p>Calibrate, record, and review real page-level gaze coverage.</p> 19 - </div> 15 + <section id="setup-shell"> 16 + <header id="controls"> 17 + <div class="brand-block"> 18 + <h1>UXET</h1> 19 + <p>Operator shell for selecting apps, exporting sessions, and reviewing page-level gaze coverage.</p> 20 + </div> 20 21 21 - <div class="control-row"> 22 - <label class="field"> 23 - <span>App Under Test</span> 24 - <select id="app-select"> 25 - <option value="">Select an app...</option> 26 - <option value="testable-apps/shop-app/index.html" data-task="Find and purchase a blue t-shirt" 27 - data-win="selector:.checkout-success.active">ShopEasy Store</option> 28 - <option value="testable-apps/example-app/index.html" 29 - data-task="Fill out the contact form with your details" data-win="text:Form submitted successfully"> 30 - Example Form App 31 - </option> 32 - <option value="testable-apps/long-page-app/index.html" 33 - data-task="Review the comparison sections and subscribe at the bottom of the page" 34 - data-win="selector:#success-banner">Long Page Demo</option> 35 - </select> 36 - </label> 22 + <div class="control-row"> 23 + <label class="field"> 24 + <span>App Under Test</span> 25 + <select id="app-select"> 26 + <option value="">Select an app...</option> 27 + <option value="testable-apps/shop-app/index.html" data-task="Find and purchase a blue t-shirt" 28 + data-win="selector:.checkout-success.active">ShopEasy Store</option> 29 + <option value="testable-apps/example-app/index.html" 30 + data-task="Fill out the contact form with your details" data-win="text:Form submitted successfully"> 31 + Example Form App 32 + </option> 33 + <option value="testable-apps/long-page-app/index.html" 34 + data-task="Review the comparison sections and subscribe at the bottom of the page" 35 + data-win="selector:#success-banner">Long Page Demo</option> 36 + </select> 37 + </label> 37 38 38 - <div class="primary-actions"> 39 - <button id="load-app-btn">Load App</button> 40 - <button id="start-test-btn" class="hidden">Start Test</button> 41 - <button id="export-btn" disabled>Export Data</button> 42 - <button id="reset-btn">Reset</button> 39 + <div class="primary-actions"> 40 + <button id="load-app-btn">Load App</button> 41 + <button id="export-btn" disabled>Export Data</button> 42 + <button id="reset-btn">Reset</button> 43 + </div> 43 44 </div> 44 - </div> 45 45 46 - <div class="debug-shell"> 47 - <button id="debug-toggle-btn" class="debug-toggle" type="button">Debug Controls</button> 48 - <div id="debug-panel" class="hidden"> 49 - <button id="debug-skip-calibration" type="button">Skip Calibration</button> 50 - <button id="debug-end-test" type="button" disabled>End Test</button> 46 + <div class="debug-shell"> 47 + <button id="debug-toggle-btn" class="debug-toggle" type="button">Debug Controls</button> 48 + <div id="debug-panel" class="hidden"> 49 + <button id="debug-skip-calibration" type="button">Skip Calibration</button> 50 + <p class="debug-copy">Debug end test shortcut: <code>Shift+Escape</code></p> 51 + </div> 51 52 </div> 52 - </div> 53 - </header> 53 + </header> 54 54 55 - <section id="status-bar"> 56 - <div><span class="label">State</span><strong id="session-status">idle</strong></div> 57 - <div><span class="label">Time</span><strong id="session-timer">00:00:00</strong></div> 58 - <div><span class="label">Task</span><strong id="current-task-text">None</strong></div> 59 - <div><span class="label">Message</span><strong id="session-message">Select an app to begin.</strong></div> 55 + <section id="status-bar"> 56 + <div><span class="label">State</span><strong id="session-status">idle</strong></div> 57 + <div><span class="label">Time</span><strong id="session-timer">00:00:00</strong></div> 58 + <div><span class="label">Task</span><strong id="current-task-text">None</strong></div> 59 + <div><span class="label">Message</span><strong id="session-message">Select an app to begin.</strong></div> 60 + </section> 61 + 62 + <main id="shell-main"> 63 + <div id="iframe-placeholder">Select an app and load it to begin calibration.</div> 64 + </main> 60 65 </section> 61 66 62 - <main id="main-content"> 63 - <div id="iframe-placeholder">Select an app and load it to begin calibration.</div> 67 + <section id="session-stage" class="hidden" aria-live="polite"> 64 68 <iframe id="test-iframe" class="hidden" title="Test Application"></iframe> 65 69 66 70 <section id="calibration-screen" class="hidden"> 67 - <div class="overlay-card calibration-panel"> 68 - <div class="panel-header"> 69 - <h2>Guided Calibration</h2> 70 - <p id="calibration-instruction">Look directly at the target and click it three times.</p> 71 - </div> 72 - 73 - <div class="calibration-stats"> 74 - <div><span class="label">Point</span><strong id="calibration-progress">1 / 9</strong></div> 75 - <div><span class="label">Clicks</span><strong id="calibration-clicks">0 / 3</strong></div> 76 - <div><span class="label">Quality</span><strong id="calibration-quality">Pending</strong></div> 77 - <div><span class="label">Status</span><strong id="calibration-feedback">Waiting for first point</strong></div> 78 - </div> 71 + <div id="calibration-veil"></div> 72 + <div id="calibration-hud"> 73 + <span><strong id="calibration-progress">1 / 9</strong> points</span> 74 + <span><strong id="calibration-clicks">0 / 3</strong> clicks</span> 75 + <span id="calibration-quality">Pending</span> 76 + <span id="calibration-feedback">Waiting for first point</span> 77 + <span id="calibration-instruction">Look at the target and click it three times.</span> 79 78 </div> 80 - 81 79 <div id="calibration-stage"> 82 80 <button id="calibration-target" type="button">1</button> 83 81 </div> 84 82 </section> 85 83 86 - <section id="debrief-screen" class="hidden"> 84 + <section id="start-overlay" class="hidden"> 85 + <div class="start-card"> 86 + <p class="start-label">Calibration complete</p> 87 + <h2>Ready to start the task</h2> 88 + <p id="start-overlay-task">Task will appear here.</p> 89 + <button id="start-test-btn">Start Test</button> 90 + </div> 91 + </section> 92 + </section> 93 + 94 + <section id="debrief-shell" class="hidden"> 95 + <section id="debrief-screen"> 87 96 <div class="overlay-card debrief-panel"> 88 97 <div class="panel-header"> 89 98 <h2>Test Debrief</h2> ··· 109 118 110 119 <div id="screen-gallery"></div> 111 120 </section> 112 - </main> 121 + </section> 113 122 </div> 114 123 115 124 <script type="module" src="js/main.js"></script>
+23 -18
js/calibration.js
··· 1 1 const CALIBRATION_SEQUENCE = [ 2 - { id: 'center', x: 50, y: 50 }, 3 - { id: 'top-left', x: 12, y: 12 }, 4 - { id: 'top-right', x: 88, y: 12 }, 5 - { id: 'bottom-left', x: 12, y: 88 }, 6 - { id: 'bottom-right', x: 88, y: 88 }, 7 - { id: 'top', x: 50, y: 12 }, 8 - { id: 'left', x: 12, y: 50 }, 9 - { id: 'right', x: 88, y: 50 }, 10 - { id: 'bottom', x: 50, y: 88 } 2 + { id: 'center', x: 0.5, y: 0.5 }, 3 + { id: 'top-left', x: 0, y: 0 }, 4 + { id: 'top-right', x: 1, y: 0 }, 5 + { id: 'bottom-left', x: 0, y: 1 }, 6 + { id: 'bottom-right', x: 1, y: 1 }, 7 + { id: 'top', x: 0.5, y: 0 }, 8 + { id: 'left', x: 0, y: 0.5 }, 9 + { id: 'right', x: 1, y: 0.5 }, 10 + { id: 'bottom', x: 0.5, y: 1 } 11 11 ]; 12 12 13 13 /** ··· 18 18 */ 19 19 20 20 export class CalibrationController { 21 - constructor({ gazeTracker, elements }) { 21 + constructor({ gazeTracker, elements, getViewportRect, getSafeAreaInsets }) { 22 22 this.gazeTracker = gazeTracker; 23 23 this.elements = elements; 24 + this.getViewportRect = getViewportRect; 25 + this.getSafeAreaInsets = getSafeAreaInsets; 24 26 this.sequence = CALIBRATION_SEQUENCE; 25 27 this.clicksPerPoint = 3; 26 28 this.sampleWindowMs = 600; ··· 30 32 this.currentClicks = 0; 31 33 this.pointResults = []; 32 34 this.currentPointStart = 0; 33 - this.onReady = null; 34 35 this.onStateChange = null; 35 36 this.onPass = null; 36 37 } ··· 66 67 67 68 const pointResult = this.pointResults[this.currentIndex]; 68 69 pointResult.attempts += 1; 69 - 70 70 this.elements.calibrationFeedback.textContent = 'Scoring point...'; 71 71 72 72 await new Promise((resolve) => window.setTimeout(resolve, 220)); ··· 138 138 } 139 139 140 140 getTargetPixelPosition(point) { 141 - const rect = this.elements.calibrationStage.getBoundingClientRect(); 141 + const viewport = this.getViewportRect(); 142 + const insets = this.getSafeAreaInsets(); 143 + const width = Math.max(0, viewport.width - insets.left - insets.right); 144 + const height = Math.max(0, viewport.height - insets.top - insets.bottom); 145 + 142 146 return { 143 - x: rect.left + (rect.width * point.x / 100), 144 - y: rect.top + (rect.height * point.y / 100) 147 + x: viewport.left + insets.left + (width * point.x), 148 + y: viewport.top + insets.top + (height * point.y) 145 149 }; 146 150 } 147 151 ··· 153 157 this.elements.calibrationTarget.style.display = point ? 'flex' : 'none'; 154 158 155 159 if (point) { 156 - this.elements.calibrationTarget.style.left = `${point.x}%`; 157 - this.elements.calibrationTarget.style.top = `${point.y}%`; 160 + const target = this.getTargetPixelPosition(point); 161 + this.elements.calibrationTarget.style.left = `${target.x}px`; 162 + this.elements.calibrationTarget.style.top = `${target.y}px`; 158 163 this.elements.calibrationTarget.textContent = String(currentNumber); 159 - this.elements.calibrationInstruction.textContent = `Look directly at point ${currentNumber} and click it ${this.clicksPerPoint} times.`; 164 + this.elements.calibrationInstruction.textContent = `Look at point ${currentNumber} and click it ${this.clicksPerPoint} times.`; 160 165 } 161 166 } 162 167
+84 -22
js/main.js
··· 20 20 this.gazeInitialized = false; 21 21 22 22 this.elements = { 23 + setupShell: document.getElementById('setup-shell'), 24 + debriefShell: document.getElementById('debrief-shell'), 25 + sessionStage: document.getElementById('session-stage'), 26 + 23 27 appSelect: document.getElementById('app-select'), 24 28 loadAppBtn: document.getElementById('load-app-btn'), 25 29 resetBtn: document.getElementById('reset-btn'), ··· 28 32 debugToggleBtn: document.getElementById('debug-toggle-btn'), 29 33 debugPanel: document.getElementById('debug-panel'), 30 34 debugSkipBtn: document.getElementById('debug-skip-calibration'), 31 - debugEndBtn: document.getElementById('debug-end-test'), 32 35 33 36 sessionStatus: document.getElementById('session-status'), 34 37 sessionTimer: document.getElementById('session-timer'), ··· 47 50 calibrationQuality: document.getElementById('calibration-quality'), 48 51 calibrationFeedback: document.getElementById('calibration-feedback'), 49 52 53 + startOverlay: document.getElementById('start-overlay'), 54 + startOverlayTask: document.getElementById('start-overlay-task'), 55 + 50 56 debriefScreen: document.getElementById('debrief-screen'), 51 57 debriefTime: document.getElementById('debrief-time'), 52 58 debriefClicks: document.getElementById('debrief-clicks'), ··· 75 81 calibrationClicks: this.elements.calibrationClicks, 76 82 calibrationQuality: this.elements.calibrationQuality, 77 83 calibrationFeedback: this.elements.calibrationFeedback 84 + }, 85 + getViewportRect: () => ({ 86 + left: 0, 87 + top: 0, 88 + width: window.innerWidth, 89 + height: window.innerHeight 90 + }), 91 + getSafeAreaInsets: () => { 92 + const hudHeight = this.elements.calibrationScreen.classList.contains('hidden') 93 + ? 0 94 + : Math.ceil(this.elements.calibrationScreen.querySelector('#calibration-hud')?.getBoundingClientRect().height || 0); 95 + return { 96 + left: Math.max(48, Math.round(window.innerWidth * 0.06)), 97 + right: Math.max(48, Math.round(window.innerWidth * 0.06)), 98 + top: Math.max(72, Math.round(window.innerHeight * 0.1), hudHeight + 20), 99 + bottom: Math.max(48, Math.round(window.innerHeight * 0.06)) 100 + }; 78 101 } 79 102 }); 80 103 ··· 97 120 this.elements.debugPanel.classList.toggle('hidden'); 98 121 }); 99 122 this.elements.debugSkipBtn.addEventListener('click', () => this.skipCalibration()); 100 - this.elements.debugEndBtn.addEventListener('click', () => this.finishTest({ strategy: 'debug-manual' })); 101 123 this.elements.iframe.addEventListener('load', () => this.onIframeLoad()); 102 124 this.elements.calibrationTarget.addEventListener('click', () => this.calibration.handleTargetClick()); 125 + 103 126 window.addEventListener('resize', () => { 104 - const metrics = this.bridge.refreshMetrics(false); 105 - if (metrics) { 106 - this.gazeTracker.updateMetrics(metrics); 127 + this.calibration.updateUi(); 128 + this.refreshMetricsIfActive(); 129 + }); 130 + 131 + window.addEventListener('keydown', (event) => { 132 + if (event.shiftKey && event.key === 'Escape' && this.session.state === 'recording') { 133 + event.preventDefault(); 134 + this.finishTest({ strategy: 'debug-shortcut' }); 107 135 } 108 136 }); 109 137 } ··· 147 175 this.selectedApp = null; 148 176 return; 149 177 } 178 + 150 179 this.selectedApp = { 151 180 value: option.value, 152 181 task: option.dataset.task, ··· 168 197 this.session.setAppName(this.selectedApp.name); 169 198 this.session.setTask(this.selectedApp.task); 170 199 this.elements.currentTaskText.textContent = this.selectedApp.task; 200 + this.elements.startOverlayTask.textContent = this.selectedApp.task; 171 201 this.elements.iframePlaceholder.classList.add('hidden'); 172 202 this.elements.iframe.classList.remove('hidden'); 173 203 this.elements.iframe.src = this.selectedApp.value; ··· 191 221 } 192 222 193 223 this.gazeInitialized = true; 224 + this.session.setState('calibrating'); 225 + await this.forceMetricsRefresh(); 194 226 const metrics = this.bridge.getMetricsSnapshot(); 195 227 this.gazeTracker.updateMetrics(metrics); 196 228 this.gazeTracker.setMode('calibration'); 197 - this.session.setState('calibrating'); 198 229 this.calibration.start(); 199 - this.captureScreen(metrics.key); 230 + this.captureScreen(metrics?.key); 200 231 } 201 232 202 233 skipCalibration() { ··· 246 277 this.tracker.stop(); 247 278 this.gazeTracker.finalizeFixation(); 248 279 280 + await this.forceMetricsRefresh(); 249 281 const currentMetrics = this.bridge.getMetricsSnapshot(); 250 282 if (currentMetrics) { 251 283 this.gazeTracker.updateMetrics(currentMetrics); ··· 299 331 this.elements.debriefFixationDuration.textContent = gazeStats.avgFixationDuration.toLocaleString(); 300 332 this.elements.sessionMessage.textContent = `Test finished via ${details?.strategy || 'manual'} completion.`; 301 333 302 - this.elements.debriefScreen.classList.remove('hidden'); 303 334 await this.debriefRenderer.render(screens); 304 335 this.elements.exportBtn.disabled = false; 305 336 } ··· 338 369 this.elements.exportBtn.disabled = true; 339 370 this.elements.gallery.innerHTML = ''; 340 371 this.elements.galleryLabel.textContent = 'Heatmaps will appear here after a test completes.'; 341 - this.elements.debriefScreen.classList.add('hidden'); 342 372 this.elements.calibrationScreen.classList.add('hidden'); 343 - this.elements.startTestBtn.classList.add('hidden'); 373 + this.elements.startOverlay.classList.add('hidden'); 374 + this.elements.debriefShell.classList.add('hidden'); 375 + this.elements.sessionStage.classList.add('hidden'); 344 376 345 377 if (this.gazeInitialized) { 346 378 await this.gazeTracker.end(); ··· 361 393 this.elements.iframePlaceholder.classList.remove('hidden'); 362 394 this.elements.sessionMessage.textContent = 'Select an app to begin.'; 363 395 this.elements.debugPanel.classList.add('hidden'); 396 + document.body.classList.remove('session-active'); 364 397 } 365 398 366 399 failSession(message) { 367 400 this.session.setState('error', { errorMessage: message }); 368 401 this.elements.sessionMessage.textContent = message; 402 + document.body.classList.remove('session-active'); 403 + } 404 + 405 + async forceMetricsRefresh() { 406 + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); 407 + const metrics = this.bridge.refreshMetrics(false); 408 + if (metrics) { 409 + this.gazeTracker.updateMetrics(metrics); 410 + } 411 + return metrics; 412 + } 413 + 414 + refreshMetricsIfActive() { 415 + if (!this.bridge.isAttached) { 416 + return; 417 + } 418 + const metrics = this.bridge.refreshMetrics(false); 419 + if (metrics) { 420 + this.gazeTracker.updateMetrics(metrics); 421 + } 369 422 } 370 423 371 424 updateUiForState(state, details = {}) { 425 + const sessionStates = new Set(['calibrating', 'ready_to_start', 'recording', 'finishing']); 426 + const sessionActive = sessionStates.has(state); 427 + 372 428 this.elements.sessionStatus.textContent = state.replace(/_/g, ' '); 373 429 this.elements.exportBtn.disabled = state !== 'complete'; 374 - 375 - const showCalibration = state === 'calibrating' || state === 'ready_to_start'; 376 - this.elements.calibrationScreen.classList.toggle('hidden', !showCalibration); 377 - this.elements.startTestBtn.classList.toggle('hidden', state !== 'ready_to_start'); 378 - this.elements.debugEndBtn.disabled = state !== 'recording'; 430 + this.elements.setupShell.classList.toggle('hidden', state === 'calibrating' || state === 'ready_to_start' || state === 'recording' || state === 'finishing'); 431 + this.elements.sessionStage.classList.toggle('hidden', !sessionActive); 432 + this.elements.debriefShell.classList.toggle('hidden', state !== 'complete'); 433 + this.elements.calibrationScreen.classList.toggle('hidden', state !== 'calibrating'); 434 + this.elements.startOverlay.classList.toggle('hidden', state !== 'ready_to_start'); 435 + document.body.classList.toggle('session-active', sessionActive); 379 436 380 437 switch (state) { 381 438 case 'idle': 382 439 this.elements.sessionMessage.textContent = 'Select an app to begin.'; 440 + this.elements.iframe.classList.add('hidden'); 383 441 break; 384 442 case 'loading_app': 385 443 this.elements.sessionMessage.textContent = 'Loading app and attaching instrumentation bridge...'; 444 + this.elements.iframe.classList.remove('hidden'); 386 445 break; 387 446 case 'calibrating': 388 - this.elements.sessionMessage.textContent = 'Complete guided calibration before recording starts.'; 447 + this.elements.sessionMessage.textContent = 'Complete fullscreen calibration before recording starts.'; 448 + this.elements.iframe.classList.remove('hidden'); 449 + this.calibration.updateUi(); 389 450 break; 390 451 case 'ready_to_start': 391 452 this.elements.sessionMessage.textContent = this.debugOverride 392 453 ? 'Calibration skipped in debug mode. Recording can start.' 393 - : 'Calibration passed. Start the test when the participant is ready.'; 454 + : 'Calibration passed. Start the fullscreen test when ready.'; 455 + this.elements.startOverlayTask.textContent = this.selectedApp?.task || 'Complete the assigned task.'; 394 456 break; 395 457 case 'recording': 396 - this.elements.calibrationScreen.classList.add('hidden'); 397 - this.elements.sessionMessage.textContent = 'Recording interactions, gaze, and page state.'; 458 + this.elements.sessionMessage.textContent = 'Recording fullscreen session.'; 398 459 break; 399 460 case 'finishing': 400 461 this.elements.sessionMessage.textContent = 'Finalizing screenshots and rendering debrief...'; 401 462 break; 402 463 case 'complete': 403 - if (!this.elements.sessionMessage.textContent) { 404 - this.elements.sessionMessage.textContent = 'Debrief ready.'; 405 - } 464 + this.elements.sessionMessage.textContent = this.elements.sessionMessage.textContent || 'Debrief ready.'; 465 + document.body.classList.remove('session-active'); 406 466 break; 407 467 case 'error': 408 468 this.elements.sessionMessage.textContent = details.errorMessage || 'The session entered an error state.'; 469 + this.elements.sessionStage.classList.add('hidden'); 470 + document.body.classList.remove('session-active'); 409 471 break; 410 472 } 411 473 }