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.

Add retryable calibration failure state

- Show a failure overlay with the last calibration error
- Let users retry calibration explicitly instead of auto-restarting
- Track the final calibration result and support the new session state

+61 -10
+8 -2
index.css
··· 194 194 195 195 #test-iframe, 196 196 #calibration-screen, 197 - #start-overlay { 197 + #start-overlay, 198 + #calibration-failure-overlay { 198 199 position: absolute; 199 200 inset: 0; 200 201 } ··· 256 257 box-shadow: 0 18px 36px rgba(126, 40, 23, 0.35); 257 258 } 258 259 259 - #start-overlay { 260 + #start-overlay, 261 + #calibration-failure-overlay { 260 262 z-index: 3; 261 263 display: grid; 262 264 place-items: center; 263 265 background: rgba(14, 12, 10, 0.2); 264 266 padding: 24px; 267 + } 268 + 269 + .failure-card { 270 + border: 1px solid rgba(126, 40, 23, 0.18); 265 271 } 266 272 267 273 .start-card {
+10
index.html
··· 89 89 <button id="start-test-btn">Start Test</button> 90 90 </div> 91 91 </section> 92 + 93 + <section id="calibration-failure-overlay" class="hidden"> 94 + <div class="start-card failure-card"> 95 + <p class="start-label">Calibration result</p> 96 + <h2>Calibration didn't pass</h2> 97 + <p id="calibration-failure-summary">Average error summary will appear here.</p> 98 + <p>Try keeping your head still and looking directly at each point.</p> 99 + <button id="retry-calibration-btn" type="button">Retry Calibration</button> 100 + </div> 101 + </section> 92 102 </section> 93 103 94 104 <section id="debrief-shell" class="hidden">
+3 -3
js/calibration.js
··· 33 33 this.pointResults = []; 34 34 this.currentPointStart = 0; 35 35 this.onStateChange = null; 36 - this.onPass = null; 36 + this.onComplete = null; 37 37 } 38 38 39 39 start() { ··· 106 106 107 107 if (this.currentIndex >= this.sequence.length) { 108 108 const result = this.finalize(); 109 - if (this.onPass) { 110 - this.onPass(result); 109 + if (this.onComplete) { 110 + this.onComplete(result); 111 111 } 112 112 return; 113 113 }
+39 -5
js/main.js
··· 18 18 this.calibrationPassed = false; 19 19 this.debugOverride = false; 20 20 this.gazeInitialized = false; 21 + this.lastCalibrationResult = null; 21 22 22 23 this.elements = { 23 24 setupShell: document.getElementById('setup-shell'), ··· 52 53 53 54 startOverlay: document.getElementById('start-overlay'), 54 55 startOverlayTask: document.getElementById('start-overlay-task'), 56 + calibrationFailureOverlay: document.getElementById('calibration-failure-overlay'), 57 + calibrationFailureSummary: document.getElementById('calibration-failure-summary'), 58 + retryCalibrationBtn: document.getElementById('retry-calibration-btn'), 55 59 56 60 debriefScreen: document.getElementById('debrief-screen'), 57 61 debriefTime: document.getElementById('debrief-time'), ··· 116 120 this.elements.resetBtn.addEventListener('click', () => this.resetSession()); 117 121 this.elements.exportBtn.addEventListener('click', () => this.exportData()); 118 122 this.elements.startTestBtn.addEventListener('click', () => this.beginTesting()); 123 + this.elements.retryCalibrationBtn.addEventListener('click', () => this.retryCalibration()); 119 124 this.elements.debugToggleBtn.addEventListener('click', () => { 120 125 this.elements.debugPanel.classList.toggle('hidden'); 121 126 }); ··· 157 162 this.failSession(error.message || 'Failed to attach to iframe.'); 158 163 }; 159 164 160 - this.calibration.onPass = (result) => { 165 + this.calibration.onComplete = (result) => { 166 + this.lastCalibrationResult = result; 161 167 if (result.passed) { 162 168 this.calibrationPassed = true; 163 169 this.debugOverride = false; 164 170 this.session.setState('ready_to_start'); 165 171 } else { 166 - this.elements.sessionMessage.textContent = 'Calibration average was too noisy. Restarting calibration.'; 167 - window.setTimeout(() => this.calibration.start(), 700); 172 + this.calibrationPassed = false; 173 + this.session.setState('calibration_failed'); 168 174 } 169 175 }; 170 176 } ··· 226 232 const metrics = this.bridge.getMetricsSnapshot(); 227 233 this.gazeTracker.updateMetrics(metrics); 228 234 this.gazeTracker.setMode('calibration'); 235 + this.lastCalibrationResult = null; 229 236 this.calibration.start(); 230 237 this.captureScreen(metrics?.key); 231 238 } ··· 239 246 this.calibrationPassed = false; 240 247 this.elements.calibrationFeedback.textContent = 'Calibration bypassed via debug override'; 241 248 this.elements.calibrationQuality.textContent = 'Debug override'; 249 + this.lastCalibrationResult = null; 242 250 this.session.setState('ready_to_start'); 243 251 } 244 252 253 + retryCalibration() { 254 + if (this.session.state !== 'calibration_failed') { 255 + return; 256 + } 257 + 258 + this.lastCalibrationResult = null; 259 + this.calibrationPassed = false; 260 + this.debugOverride = false; 261 + this.elements.calibrationFailureSummary.textContent = 'Average error summary will appear here.'; 262 + this.gazeTracker.setMode('calibration'); 263 + this.session.setState('calibrating'); 264 + this.calibration.start(); 265 + } 266 + 245 267 beginTesting() { 246 268 if (!this.selectedApp) { 247 269 return; ··· 371 393 this.elements.galleryLabel.textContent = 'Heatmaps will appear here after a test completes.'; 372 394 this.elements.calibrationScreen.classList.add('hidden'); 373 395 this.elements.startOverlay.classList.add('hidden'); 396 + this.elements.calibrationFailureOverlay.classList.add('hidden'); 374 397 this.elements.debriefShell.classList.add('hidden'); 375 398 this.elements.sessionStage.classList.add('hidden'); 399 + this.lastCalibrationResult = null; 376 400 377 401 if (this.gazeInitialized) { 378 402 await this.gazeTracker.end(); ··· 422 446 } 423 447 424 448 updateUiForState(state, details = {}) { 425 - const sessionStates = new Set(['calibrating', 'ready_to_start', 'recording', 'finishing']); 449 + const sessionStates = new Set(['calibrating', 'calibration_failed', 'ready_to_start', 'recording', 'finishing']); 426 450 const sessionActive = sessionStates.has(state); 427 451 428 452 this.elements.sessionStatus.textContent = state.replace(/_/g, ' '); 429 453 this.elements.exportBtn.disabled = state !== 'complete'; 430 - this.elements.setupShell.classList.toggle('hidden', state === 'calibrating' || state === 'ready_to_start' || state === 'recording' || state === 'finishing'); 454 + this.elements.setupShell.classList.toggle('hidden', state === 'calibrating' || state === 'calibration_failed' || state === 'ready_to_start' || state === 'recording' || state === 'finishing'); 431 455 this.elements.sessionStage.classList.toggle('hidden', !sessionActive); 432 456 this.elements.debriefShell.classList.toggle('hidden', state !== 'complete'); 433 457 this.elements.calibrationScreen.classList.toggle('hidden', state !== 'calibrating'); 434 458 this.elements.startOverlay.classList.toggle('hidden', state !== 'ready_to_start'); 459 + this.elements.calibrationFailureOverlay.classList.toggle('hidden', state !== 'calibration_failed'); 435 460 document.body.classList.toggle('session-active', sessionActive); 436 461 437 462 switch (state) { ··· 448 473 this.elements.iframe.classList.remove('hidden'); 449 474 this.calibration.updateUi(); 450 475 break; 476 + case 'calibration_failed': { 477 + const averageError = this.lastCalibrationResult?.averageError; 478 + const summary = Number.isFinite(averageError) 479 + ? `Average error: ${averageError}px.` 480 + : 'Calibration could not be scored accurately.'; 481 + this.elements.calibrationFailureSummary.textContent = summary; 482 + this.elements.sessionMessage.textContent = 'Calibration failed. Review the result and retry explicitly.'; 483 + break; 484 + } 451 485 case 'ready_to_start': 452 486 this.elements.sessionMessage.textContent = this.debugOverride 453 487 ? 'Calibration skipped in debug mode. Recording can start.'
+1
js/session.js
··· 6 6 'idle', 7 7 'loading_app', 8 8 'calibrating', 9 + 'calibration_failed', 9 10 'ready_to_start', 10 11 'recording', 11 12 'finishing',