Skip to content

Commit 10ebe7a

Browse files
Merge pull request #20 from hashexplaindata/claude/unify-experiment-code-base
Unify experiment codebase: fix PID generation, timing accuracy, Firebase sync, and XSS vulnerabilities
2 parents 50a2c6e + 5ed09c9 commit 10ebe7a

4 files changed

Lines changed: 235 additions & 112 deletions

File tree

code/experiment.js

Lines changed: 135 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -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 ---
1828
const 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) ---
3150
const 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 ---
295314
const 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 ---
310328
function 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 ---
321349
function 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

355387
function 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

402430
function 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

428460
function 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) ---
454482
async 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

482546
function 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

Comments
 (0)