@@ -14,18 +14,37 @@ const CFG = Object.freeze({
1414 COLLECTION : 'conformity_telemetry'
1515} ) ;
1616
17+ // --- Participant ID Generation ---
18+ function generatePID ( ) {
19+ try {
20+ return self . crypto . randomUUID ( ) ;
21+ } catch ( e ) {
22+ return Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) +
23+ Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ;
24+ }
25+ }
26+
1727// --- State Machine ---
1828const STATE = {
19- pid : Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) + Math . random ( ) . toString ( 36 ) . substring ( 2 , 15 ) ,
29+ pid : localStorage . getItem ( 'experiment_pid' ) || generatePID ( ) ,
2030 condition : CFG . CONDITION ,
2131 covariate : 0 ,
2232 currentTrial : 0 ,
23- results : [ ] , // Tidy Data Long Format
33+ results : [ ] ,
2434 trialStartTime : 0 ,
2535 isTrialActive : false ,
26- justification : ""
36+ justification : "" ,
37+ activeScreen : null ,
38+ pendingTransitionTimer : null
2739} ;
2840
41+ // Persist PID to localStorage
42+ try {
43+ localStorage . setItem ( 'experiment_pid' , STATE . pid ) ;
44+ } catch ( e ) {
45+ // Silent fallback
46+ }
47+
2948// --- Trial Definitions (Pixel-Perfect Components) ---
3049// --- High-Fidelity, Zero-Latency UI Trials (Iqra University Context) ---
3150const TRIALS = [
@@ -76,7 +95,7 @@ const TRIALS = [
7695 </div>
7796 </div>
7897 </div>` ,
79- target : 'B' // Hypothesized preference for modern Bento grid layouts
98+ target : 'B'
8099 } ,
81100 {
82101 domain : 'Data Visualization (HEC Attendance Warning)' ,
@@ -122,7 +141,7 @@ const TRIALS = [
122141 <strong>Alert:</strong> You can only miss 2 more classes in RM-2 before facing HEC examination block.
123142 </div>
124143 </div>` ,
125- target : 'A'
144+ target : 'A'
126145 } ,
127146 {
128147 domain : 'Financial Overview (Fee Voucher)' ,
@@ -156,7 +175,7 @@ const TRIALS = [
156175 </div>
157176 <h3 style="font-size:1.2rem; margin:0 0 5px 0;">Spring 2026 Invoice</h3>
158177 <p style="font-size:0.85rem; color:var(--text-secondary); margin:0 0 25px 0;">Challan: IU-9938-26</p>
159-
178+
160179 <div style="background:var(--bg-surface); padding:20px; border-radius:16px; margin-bottom:20px;">
161180 <span style="display:block; font-size:0.85rem; color:#ff453a; font-weight:600; text-transform:uppercase; letter-spacing:1px; margin-bottom:8px;">Due March 10</span>
162181 <span style="display:block; font-size:2rem; font-weight:800; letter-spacing:-1px;">Rs. 95,500</span>
@@ -242,7 +261,7 @@ const TRIALS = [
242261 <h3 style="font-size:1.1rem; margin:0 0 6px 0;">RM-2 Instructor Evaluation</h3>
243262 <p style="font-size:0.85rem; color:var(--text-secondary); margin:0; line-height:1.4;">"The instructor provided clear grading rubrics and feedback."</p>
244263 </div>
245-
264+
246265 <div style="padding: 20px 10px;">
247266 <div style="position:relative; width:100%; height:6px; background:var(--bg-surface); border-radius:3px;">
248267 <div style="position:absolute; top:0; left:0; height:100%; width:75%; background:var(--accent-blue); border-radius:3px;"></div>
@@ -293,7 +312,6 @@ const TRIALS = [
293312
294313// --- DOM Elements ---
295314const DOM = {
296- screens : document . querySelectorAll ( '.screen' ) ,
297315 btnConsent : document . getElementById ( 'btn-consent' ) ,
298316 btnsFamiliarity : document . querySelectorAll ( '.btn-familiarity' ) ,
299317 trialGrid : document . getElementById ( 'trial-grid' ) ,
@@ -308,34 +326,49 @@ const DOM = {
308326
309327// --- Navigation Logic ---
310328function showScreen ( id ) {
311- DOM . screens . forEach ( s => {
312- s . classList . remove ( 'active' ) ;
313- s . style . display = 'none' ;
314- } ) ;
315- const target = document . getElementById ( `screen-${ id } ` ) ;
329+ const target = document . getElementById ( 'screen-' + id ) ;
330+ if ( ! target || target === STATE . activeScreen ) return ;
331+
332+ const outgoing = STATE . activeScreen ;
333+ STATE . activeScreen = target ;
334+
335+ if ( outgoing ) {
336+ outgoing . classList . remove ( 'active' ) ;
337+ setTimeout ( ( ) => { outgoing . style . display = 'none' ; } , 400 ) ;
338+ }
339+
316340 target . style . display = 'flex' ;
317- setTimeout ( ( ) => target . classList . add ( 'active' ) , 50 ) ;
341+ if ( STATE . pendingTransitionTimer ) clearTimeout ( STATE . pendingTransitionTimer ) ;
342+ STATE . pendingTransitionTimer = setTimeout ( ( ) => {
343+ target . classList . add ( 'active' ) ;
344+ STATE . pendingTransitionTimer = null ;
345+ } , 50 ) ;
318346}
319347
320348// --- Experiment Logic ---
321349function init ( ) {
350+ STATE . activeScreen = document . querySelector ( '.screen.active' ) ;
351+
322352 // Navigation Lock
323- window . history . pushState ( null , "" , window . location . href ) ;
324- window . onpopstate = ( ) => window . history . pushState ( null , "" , window . location . href ) ;
353+ window . history . replaceState ( null , document . title , window . location . href ) ;
354+ window . history . pushState ( null , document . title , window . location . href ) ;
355+ window . addEventListener ( 'popstate' , ( ) => {
356+ window . history . go ( 1 ) ;
357+ } ) ;
325358
326359 // Screen 1 Event
327360 DOM . btnConsent . addEventListener ( 'click' , ( ) => showScreen ( 2 ) ) ;
328361
329362 // Screen 2 Event
330- let covariateSelected = false ;
331- DOM . btnsFamiliarity . forEach ( btn => {
332- btn . addEventListener ( 'click' , ( ) => {
333- if ( covariateSelected ) return ; // Prevent double-tap
334- covariateSelected = true ;
335- STATE . covariate = parseInt ( btn . dataset . val ) ;
336- showScreen ( 'trial' ) ;
337- loadNextTrial ( ) ;
338- } ) ;
363+ let covariateSelected = false ;
364+ DOM . btnsFamiliarity . forEach ( btn => {
365+ btn . addEventListener ( 'click' , ( ) => {
366+ if ( covariateSelected ) return ;
367+ covariateSelected = true ;
368+ STATE . covariate = parseInt ( btn . dataset . val ) ;
369+ showScreen ( 'trial' ) ;
370+ loadNextTrial ( ) ;
371+ } ) ;
339372 } ) ;
340373
341374 // Screen 9 Events
@@ -344,12 +377,11 @@ function init() {
344377 } ) ;
345378
346379 DOM . btnFinalize . addEventListener ( 'click' , ( ) => {
380+ DOM . btnFinalize . disabled = true ;
347381 STATE . justification = DOM . textareaJustification . value . trim ( ) ;
348382 showScreen ( 10 ) ;
349383 executeBatchPayload ( ) ;
350384 } ) ;
351-
352- console . log ( `Diagnostic Engine Initialized. PID: ${ STATE . pid } | Condition: ${ STATE . condition } ` ) ;
353385}
354386
355387function loadNextTrial ( ) {
@@ -359,52 +391,47 @@ function loadNextTrial() {
359391 }
360392
361393 const trial = TRIALS [ STATE . currentTrial ] ;
362- DOM . trialCounter . innerText = `Diagnostic ${ STATE . currentTrial + 1 } /${ CFG . NUM_TRIALS } ` ;
394+ DOM . trialCounter . textContent = `Diagnostic ${ STATE . currentTrial + 1 } /${ CFG . NUM_TRIALS } ` ;
363395 DOM . progressFill . style . width = `${ ( STATE . currentTrial / CFG . NUM_TRIALS ) * 100 } %` ;
364396
365- // Randomize L/R positioning to prevent motor habituation
366397 const leftIsA = Math . random ( ) > 0.5 ;
367-
368- // Build the Bento Choice Cards
398+
369399 DOM . trialGrid . innerHTML = '' ;
370-
400+
371401 const cardL = createChoiceCard ( leftIsA ? 'A' : 'B' , trial ) ;
372402 const cardR = createChoiceCard ( leftIsA ? 'B' : 'A' , trial ) ;
373-
403+
374404 DOM . trialGrid . appendChild ( cardL ) ;
375405 DOM . trialGrid . appendChild ( cardR ) ;
376406
377- // Inject AI Badge for experimental condition
378407 if ( STATE . condition === 'ai_labeled' ) {
379- // Find which card is the "target" layout (A or B)
380408 const targetType = trial . target ;
381-
382- // Find the DOM element for that specific layout type
383- const targetCard = ( leftIsA && targetType === 'A' ) || ( ! leftIsA && targetType === 'B' )
384- ? cardL
409+ const targetCard = ( leftIsA && targetType === 'A' ) || ( ! leftIsA && targetType === 'B' )
410+ ? cardL
385411 : cardR ;
386-
412+
387413 const badge = document . createElement ( 'div' ) ;
388414 badge . className = 'ai-recommendation-badge' ;
389- badge . innerHTML = '<span>✨</span> AI Recommended' ;
415+ const badgeSpan = document . createElement ( 'span' ) ;
416+ badgeSpan . textContent = '✨' ;
417+ badge . appendChild ( badgeSpan ) ;
418+ badge . appendChild ( document . createTextNode ( ' AI Recommended' ) ) ;
390419 targetCard . appendChild ( badge ) ;
391420 }
392421
393- // Start millisecond-accurate timer
394- requestAnimationFrame ( ( ) => {
395- requestAnimationFrame ( ( ) => {
396- STATE . trialStartTime = performance . now ( ) ;
397- STATE . isTrialActive = true ;
398- } ) ;
422+ requestAnimationFrame ( ( ) => {
423+ setTimeout ( ( ) => {
424+ STATE . trialStartTime = performance . now ( ) ;
425+ STATE . isTrialActive = true ;
426+ } , 0 ) ;
399427 } ) ;
400428}
401429
402430function createChoiceCard ( type , trial ) {
403431 const card = document . createElement ( 'div' ) ;
404432 card . className = 'bento-choice-card' ;
405433 card . innerHTML = type === 'A' ? trial . renderA ( ) : trial . renderB ( ) ;
406-
407- // Add mouse move listener for the radial glow effect
434+
408435 card . addEventListener ( 'mousemove' , ( e ) => {
409436 const rect = card . getBoundingClientRect ( ) ;
410437 const x = ( ( e . clientX - rect . left ) / rect . width ) * 100 ;
@@ -413,23 +440,26 @@ function createChoiceCard(type, trial) {
413440 card . style . setProperty ( '--mouse-y' , `${ y } %` ) ;
414441 } ) ;
415442
416- card . addEventListener ( 'pointerdown' , ( ) => {
443+ const handlePointerDown = ( e ) => {
417444 if ( ! STATE . isTrialActive ) return ;
418-
419- // Visual feedback
445+
446+ e . preventDefault ( ) ;
447+ STATE . isTrialActive = false ;
448+
449+ card . removeEventListener ( 'pointerdown' , handlePointerDown ) ;
420450 card . classList . add ( 'selected' ) ;
421-
451+
422452 handleUserSelection ( type , trial ) ;
423- } ) ;
424-
453+ } ;
454+
455+ card . addEventListener ( 'pointerdown' , handlePointerDown ) ;
456+
425457 return card ;
426458}
427459
428460function handleUserSelection ( selection , trial ) {
429- const rt = performance . now ( ) - STATE . trialStartTime ;
430- STATE . isTrialActive = false ;
461+ const latency = performance . now ( ) - STATE . trialStartTime ;
431462
432- // Log Tidy Data Row
433463 STATE . results . push ( {
434464 participant_id : STATE . pid ,
435465 experimental_condition : STATE . condition ,
@@ -439,26 +469,37 @@ function handleUserSelection(selection, trial) {
439469 ai_badge_position : STATE . condition === 'ai_labeled' ? `Layout ${ trial . target } ` : 'none' ,
440470 user_selection : `Layout ${ selection } ` ,
441471 chose_target_layout : selection === trial . target ,
442- reaction_time_ms : parseFloat ( rt . toFixed ( 2 ) ) ,
443- semantic_justification : null , // Placeholder
472+ response_latency_ms : parseFloat ( latency . toFixed ( 2 ) ) ,
444473 timestamp : Date . now ( )
445474 } ) ;
446475
447476 STATE . currentTrial ++ ;
448-
449- // Debounce transition for visual feedback
477+
450478 setTimeout ( loadNextTrial , 200 ) ;
451479}
452480
453481// --- Firebase Integration (Batch Write) ---
454482async function executeBatchPayload ( ) {
455- // Append justification to all rows
456483 STATE . results . forEach ( row => row . semantic_justification = STATE . justification ) ;
457484
485+ let localBackupSucceeded = false ;
458486 try {
459- // Check for Firebase (initialized in index.html via firebase-config.js)
460- if ( typeof firebase !== 'undefined' && firebase . apps . length > 0 ) {
487+ localStorage . setItem ( 'telemetry_backup_' + STATE . pid , JSON . stringify ( STATE . results ) ) ;
488+ localBackupSucceeded = true ;
489+ } catch ( storageError ) {
490+ // Silent fallback
491+ }
492+
493+ try {
494+ if ( typeof firebase !== 'undefined' && firebase . apps && firebase . apps . length > 0 ) {
461495 const db = firebase . firestore ( ) ;
496+
497+ try {
498+ await db . enablePersistence ( { synchronizeTabs : true } ) ;
499+ } catch ( persistErr ) {
500+ // Silent fallback
501+ }
502+
462503 const batch = db . batch ( ) ;
463504
464505 STATE . results . forEach ( data => {
@@ -467,22 +508,45 @@ async function executeBatchPayload() {
467508 } ) ;
468509
469510 await batch . commit ( ) ;
511+ await db . waitForPendingWrites ( ) ;
512+
513+ try { localStorage . removeItem ( 'telemetry_backup_' + STATE . pid ) ; } catch ( e ) { }
514+ try { localStorage . removeItem ( 'experiment_pid' ) ; } catch ( e ) { }
515+
470516 onSyncSuccess ( ) ;
471517 } else {
472- console . warn ( "Firebase not detected. Payload logged to console:" , STATE . results ) ;
473- setTimeout ( onSyncSuccess , 1500 ) ; // Simulate sync delay
518+ if ( ! localBackupSucceeded ) {
519+ try {
520+ localStorage . setItem ( 'telemetry_backup_' + STATE . pid , JSON . stringify ( STATE . results ) ) ;
521+ } catch ( storageError ) {
522+ // Silent fallback
523+ }
524+ }
525+ setTimeout ( onSyncSuccess , 0 ) ;
474526 }
475527 } catch ( error ) {
476- console . error ( "Critical Sync Failure:" , error ) ;
477- DOM . syncStatus . innerHTML = `<span style="color:#ff453a">⚠️ Sync Failed. Error: ${ error . code || 'Network' } </span>` ;
478- // Potential fallback: Save to localStorage for later recovery
528+ if ( ! localBackupSucceeded ) {
529+ try {
530+ localStorage . setItem ( 'telemetry_backup_' + STATE . pid , JSON . stringify ( STATE . results ) ) ;
531+ localBackupSucceeded = true ;
532+ } catch ( storageError ) {
533+ // Silent fallback
534+ }
535+ }
536+
537+ DOM . syncStatus . textContent = localBackupSucceeded
538+ ? '⚠️ Network Timeout — your responses are saved locally'
539+ : '⚠️ Network Timeout' ;
540+ DOM . syncStatus . style . color = '#ff453a' ;
541+ DOM . finalActions . style . display = 'block' ;
542+ DOM . displayPid . textContent = STATE . pid ;
479543 }
480544}
481545
482546function onSyncSuccess ( ) {
483547 DOM . syncStatus . style . display = 'none' ;
484548 DOM . finalActions . style . display = 'block' ;
485- DOM . displayPid . innerText = STATE . pid ;
549+ DOM . displayPid . textContent = STATE . pid ;
486550}
487551
488552// Initialize on Load
0 commit comments