47 KiB
Cognitive Flexibility Test - Frontend Implementation Plan
Document Overview
This document provides a comprehensive plan for implementing the Cognitive Flexibility Test (Probabilistic Reversal Learning Task) frontend application. It addresses all critical issues from the prototype and ensures strict adherence to the test specifications outlined in the research documentation.
Table of Contents
- Project Architecture
- Test Specifications & Requirements
- State Management Strategy
- Screen Flow Implementation
- Image Asset Management
- Core Logic Implementation
- Data Collection & Recording
- Timing & Performance
- Error Handling & Edge Cases
- Testing Strategy
- Future API Integration
1. Project Architecture
1.1 Technology Stack
Core Framework:
- React 18+ with TypeScript
- React Hooks for state management
- CSS-in-JS or Tailwind CSS for styling
Key Libraries:
- None required for core functionality (keep dependencies minimal)
- Optional: date-fns for timestamp formatting
- Optional: react-confetti for celebration effects
1.2 Folder Structure
src/
├── components/
│ ├── screens/
│ │ ├── IntroScreen.tsx
│ │ ├── PracticeScreen.tsx
│ │ ├── MainTestScreen.tsx
│ │ └── ResultsScreen.tsx
│ ├── shared/
│ │ ├── StimulusDisplay.tsx
│ │ ├── FeedbackDisplay.tsx
│ │ ├── FixationCross.tsx
│ │ ├── Timer.tsx
│ │ └── ProgressBar.tsx
│ └── CognitiveFlexibilityTest.tsx (main orchestrator)
├── hooks/
│ ├── useTestState.ts
│ ├── useTimer.ts
│ ├── useDataRecorder.ts
│ └── useReversalLogic.ts
├── types/
│ ├── test.types.ts
│ └── data.types.ts
├── constants/
│ ├── testConfig.ts
│ └── stimuliConfig.ts
├── utils/
│ ├── dataExport.ts
│ ├── calculations.ts
│ └── validation.ts
└── public/
└── stimuli/
├── adolescent/
│ ├── practice/
│ │ ├── purple-pen.png
│ │ └── pink-pen.png
│ ├── blocks-1-2/
│ │ ├── golden-treasure-box.png
│ │ └── silver-treasure-box.png
│ ├── blocks-3-4/
│ │ ├── purple-pen.png
│ │ └── pink-pen.png
│ └── blocks-5-6/
│ ├── yellow-key.png
│ └── green-key.png
└── adult/
├── practice/
│ ├── star-oval-diamond.png
│ └── diamond-rectangle.png
├── blocks-1-2/
│ ├── blue-cube.png
│ └── yellow-square.png
├── blocks-3-4/
│ ├── star-purple-oval.png
│ └── heart-diamond-rectangle.png
└── blocks-5-6/
├── horizontal-lines.png
└── vertical-lines.png
2. Test Specifications & Requirements
2.1 Test Structure (CORRECTED)
Practice Phase:
- 12 rounds
- Uses practice stimuli
- 25% probabilistic feedback
- NO reversals allowed
- Same timing as main test
Main Test Phase:
- 6 blocks × 12 rounds = 72 total rounds
- Different stimulus pair for each block pair
- Reversals triggered after 3 consecutive correct responses
- Block instruction screen before each block
2.2 Age Group Configurations
Adolescents (14-18 years)
Stimuli:
| Block | Left Stimulus | Right Stimulus | Initial Correct |
|---|---|---|---|
| Practice | Purple Pen | Pink Pen | Purple Pen |
| Block 1-2 | Golden Treasure Box | Silver Treasure Box | Golden Box |
| Block 3-4 | Purple Pen | Pink Pen | Purple Pen |
| Block 5-6 | Yellow Key | Green Key | Yellow Key |
Feedback:
- Correct: +110 gold coins with coin icon
- Incorrect: -40 gold coins with broken coin icon
- Starting score: 3,000 coins
- Score always visible
Adults (18-22 years)
Stimuli:
| Block | Left Stimulus | Right Stimulus | Initial Correct |
|---|---|---|---|
| Practice | Star+Oval+Diamond | Diamond+Rectangle | Star+Oval+Diamond |
| Block 1-2 | Blue Cube | Yellow Square | Blue Cube |
| Block 3-4 | Yellow Star+Purple Oval | Red Heart+Blue Diamond+Green Rectangle | Star+Oval |
| Block 5-6 | Horizontal Lines | Vertical Lines | Horizontal Lines |
Feedback:
- Correct: Green smiley face 😊
- Incorrect: Red sad face 😢
- No scoring system (only emotional feedback)
2.3 Timing Specifications
const TIMING_CONFIG = {
RESPONSE_WINDOW: 4000, // 4 seconds exact
FEEDBACK_DURATION: 1000, // 1 second
FIXATION_DURATION: 300, // 0.3 seconds
TOTAL_TRIAL: 5300 // 5.3 seconds per trial
};
2.4 Feedback Distribution
75% Contingent (Normal):
- Correct choice → Reward
- Incorrect choice → Punishment
- 9 out of 12 trials per block
25% Non-Contingent (Misleading):
- Correct choice → Punishment (misleading)
- Incorrect choice → Reward (misleading)
- 3 out of 12 trials per block
Random Distribution Patterns:
- All 3 misleading punishments, OR
- 2 misleading punishments + 1 misleading reward, OR
- 2 misleading rewards + 1 misleading punishment
3. State Management Strategy
3.1 Core State Structure
interface TestState {
// Navigation
currentScreen: 'intro' | 'practice' | 'practiceComplete' | 'main' | 'blockInstruction' | 'results';
ageGroup: 'adolescent' | 'adult';
// Progress tracking
currentBlock: number; // 0-5 (6 blocks total)
currentRound: number; // 0-11 (12 rounds per block)
// Trial state
leftStimulusPath: string;
rightStimulusPath: string;
currentCorrectSide: 'left' | 'right';
selectedSide: 'left' | 'right' | null;
// Timing
trialStartTime: number;
responseTime: number | null;
timeRemaining: number;
// Feedback
showFeedback: boolean;
feedbackType: 'correct' | 'incorrect';
isProbabilisticFeedback: boolean;
showFixation: boolean;
showTimeout: boolean;
// Scoring (adolescents only)
score: number;
// Reversal tracking
consecutiveCorrectCount: number;
reversalCount: number;
lastReversalTrial: number | null;
errorsSinceReversal: number;
// Data collection
trialData: TrialRecord[];
// Error counts
totalReversalErrors: number;
totalPerseverativeErrors: number;
totalFinalReversalErrors: number;
// Shift tracking
lastChosenStimulus: string | null;
lastOutcome: 'win' | 'loss' | null;
winShifts: number;
loseShifts: number;
totalWins: number;
totalLosses: number;
}
3.2 Trial Data Recording Structure
interface TrialRecord {
// Identifiers
trialNumber: number; // 1-84 (12 practice + 72 main)
blockNumber: number | 'Practice'; // 'Practice' or 1-6
roundInBlock: number; // 1-12
// Stimulus information
stimulusSet: string; // e.g., "Golden Box vs Silver Box"
leftStimulus: string; // Image filename
rightStimulus: string; // Image filename
// Task rule
taskRule: string; // e.g., "Golden Box is rewarded"
currentCorrectStimulus: string; // e.g., "Golden Box"
switchIndicator: boolean; // True if reversal occurred this trial
// Response
participantChoice: string | 'timeout'; // Chosen stimulus name or 'timeout'
chosenSide: 'left' | 'right' | null;
correctResponse: string; // Expected correct stimulus
responseAccuracy: 0 | 1; // 0 = incorrect, 1 = correct
responseTime: number; // Milliseconds
// Feedback
isProbabilistic: boolean; // Was this misleading feedback?
feedbackGiven: string; // e.g., "+110 coins" or "Green Smiley"
feedbackType: 'reward' | 'punishment';
// Error classification
errorType: 'none' | 'reversal' | 'perseverative' | 'final_reversal' | 'random';
// Reversal tracking
reversalTriggered: boolean; // Did this trial trigger a reversal?
consecutiveCorrectBeforeTrial: number;
// Score (adolescents only)
scoreChange: number; // +110, -40, or 0
totalScore: number;
// Timestamp
timestamp: number;
}
3.3 State Separation Strategy
Use Custom Hooks for Modularity:
// Main orchestrator
const useTestState = () => {
// Core navigation and progress state
};
// Separate concerns
const useTimer = (duration: number, onComplete: () => void);
const useReversalLogic = (consecutiveCorrect: number, isPractice: boolean);
const useDataRecorder = (trialData: TrialRecord[]);
const useShiftTracking = (lastChoice: string, lastOutcome: string);
4. Screen Flow Implementation
4.1 Screen: Intro (Age Selection + Instructions)
Purpose: Welcome screen, age group selection, and initial instructions.
UI Components:
- Welcome message with brain icon
- Age group selector (buttons or dropdown)
- "Adolescent (14-18 years)"
- "Adult (18-22 years)"
- Instructions tailored to selected age group
- Warning about rule changes
- "Let's Practice!" button
Key Logic:
const IntroScreen = ({ onStart, onAgeGroupSelect }) => {
const [selectedAge, setSelectedAge] = useState<'adolescent' | 'adult' | null>(null);
const instructions = {
adolescent: "Welcome to the Game! Two pictures will appear—tap the one you think hides the gold coins...",
adult: "Welcome! You will see two shapes or designs side by side on the screen..."
};
return (
// Display age selection first
// Then show instructions based on selection
// Enable "Let's Practice!" only when age selected
);
};
Critical Implementation Notes:
- Age group MUST be selected before proceeding
- Instructions must match PDF text exactly
- Store age group in state for entire test session
- Different stimuli and feedback will be loaded based on selection
4.2 Screen: Practice (12 Rounds)
Purpose: Familiarize user with mechanics, NO reversals.
Header Display:
- "Practice Round - Round X/12"
- Score display (adolescents only)
- Progress bar
Core Components:
- Timer countdown (4 seconds)
- Two stimulus images (left/right randomized)
- Feedback display
- Fixation cross (+)
Critical Rules:
- NO REVERSAL LOGIC - Even if 3+ consecutive correct
- Probabilistic feedback still active (25%)
- Same timing as main test
- Starting correct stimulus defined in config
- Timeout handling with score deduction
Practice Complete Transition:
// After 12 rounds complete
const handlePracticeComplete = () => {
// Show intermediate screen
setCurrentScreen('practiceComplete');
// Display: "Great job! Now ready for real challenge"
// Reset relevant counters but keep age group
};
4.3 Screen: Block Instructions (Between Main Test Blocks)
Purpose: Brief instruction before each of 6 blocks.
Display Requirements:
- Block number (1-6)
- New stimulus set preview (optional)
- Brief instruction text
- "Continue" or "Let's Go!" button
Example Instructions (FROM PDF):
- Block 1: "Welcome to the Game! [full instructions]"
- Block 2 (Adolescent): "Purple Pen will provide reward for this block."
- Block 3 (Adolescent): "Green Key will provide reward for this block."
- Block 2 (Adult): "[Stimulus] will provide reward for this block."
- (Pattern continues as specified in PDF Section 5 Main Task Flow Table)
Implementation:
const BlockInstructionScreen = ({ blockNumber, stimulusName, onContinue }) => {
const instructions = {
1: "Welcome to the Game! [full text]",
2: `${stimulusName} will provide the reward from this block.`,
// ... for all 6 blocks
};
return (
// Show block instruction
// "Let's Go!" button calls onContinue()
);
};
Timing: No time limit, user proceeds when ready.
4.4 Screen: Main Test (72 Rounds across 6 Blocks)
Purpose: Core cognitive flexibility assessment with reversals.
Header Display:
- "Block X - Round Y/12"
- Score (adolescents) or blank (adults)
- Progress bar (per block or overall)
Trial Flow (5.3 seconds total):
-
Stimulus Display + Response (0-4 seconds)
- Show two images
- Timer counts down
- User taps/clicks one image
- Selected image gets blue outline immediately
- OR timeout shows "Time is up!"
-
Feedback Display (1 second)
- Stimuli remain visible
- Feedback overlays (coins/smiley)
- Score updates (adolescents)
-
Fixation Cross (0.3 seconds)
- Clear screen
- Show "+" symbol
- Prepare for next trial
Block Transitions:
- After round 12 of each block → Block Instruction Screen
- Continue until 6 blocks complete
Critical Implementation Points:
const MainTestScreen = () => {
// Check if reversal should be disabled
const isPractice = currentScreen === 'practice';
const canTriggerReversal = !isPractice;
// Handle choice
const handleChoice = (side: 'left' | 'right') => {
const responseTime = Date.now() - trialStartTime;
const chosenStimulus = side === 'left' ? leftStimulus : rightStimulus;
const correctStimulus = currentCorrectSide === 'left' ? leftStimulus : rightStimulus;
// Determine TRUE correctness (internal)
const isActuallyCorrect = chosenStimulus === correctStimulus;
// Determine DISPLAYED feedback (may be misleading)
const isProbabilistic = shouldApplyProbabilisticFeedback();
const displayAsCorrect = isProbabilistic ? !isActuallyCorrect : isActuallyCorrect;
// Track ACTUAL correctness for reversal logic
if (isActuallyCorrect && canTriggerReversal) {
incrementConsecutiveCorrect();
if (consecutiveCorrect >= 3) {
triggerReversal();
}
} else {
resetConsecutiveCorrect();
}
// Track errors BASED ON ACTUAL correctness
if (!isActuallyCorrect) {
classifyError();
}
// Update score BASED ON DISPLAYED feedback
updateScore(displayAsCorrect);
// Track shifts BASED ON CURRENT outcome
trackShifts(chosenStimulus, displayAsCorrect);
// Record trial data
recordTrial({...});
// Show feedback
showFeedback(displayAsCorrect);
};
};
4.5 Screen: Results
Purpose: Display comprehensive performance analysis.
Display Sections:
-
Summary Metrics (Grid Layout)
- Accuracy percentage
- Average response time
- Total correct responses
- Reversals triggered
-
Error Analysis
- Reversal Errors: X (with explanation)
- Perseverative Errors: X (with explanation)
- Final Reversal Errors: X (with explanation)
-
Strategy Flexibility
- Win-Shift Rate: X% (Lower is better)
- Lose-Shift Rate: X% (Higher is better)
-
Performance Summary
- Total rounds: 72
- Rounds answered: X
- Rounds missed: X
- Final score: X coins (adolescents only)
-
Interpretation Guide
- What high accuracy means
- What few reversal errors indicate
- Cognitive flexibility indicators
Actions:
- "Restart Test" button
- "Export Data" button (CSV/JSON)
- "View Detailed Report" (optional)
Calculations:
const calculateResults = (trialData: TrialRecord[]) => {
const mainTestData = trialData.filter(t => t.blockNumber !== 'Practice');
const correctCount = mainTestData.filter(t => t.responseAccuracy === 1 && t.participantChoice !== 'timeout').length;
const totalResponded = mainTestData.filter(t => t.participantChoice !== 'timeout').length;
const accuracy = (correctCount / totalResponded) * 100;
const avgRT = mainTestData
.filter(t => t.participantChoice !== 'timeout')
.reduce((sum, t) => sum + t.responseTime, 0) / totalResponded;
const winShiftRate = totalWins > 0 ? (winShifts / totalWins) * 100 : 0;
const loseShiftRate = totalLosses > 0 ? (loseShifts / totalLosses) * 100 : 0;
return {
accuracy: accuracy.toFixed(1),
avgRT: avgRT.toFixed(0),
correctCount,
totalResponded,
totalUnanswered: mainTestData.filter(t => t.participantChoice === 'timeout').length,
reversalCount,
reversalErrors,
perseverativeErrors,
finalReversalErrors,
winShiftRate: winShiftRate.toFixed(1),
loseShiftRate: loseShiftRate.toFixed(1)
};
};
5. Image Asset Management
5.1 Image Requirements
Format: PNG with transparent background (recommended)
Size: 400x400px (adjust based on display needs)
Quality: High resolution for clarity
Naming Convention: kebab-case (e.g., golden-treasure-box.png)
5.2 Local Storage Structure
public/stimuli/
├── adolescent/
│ ├── practice/
│ │ ├── purple-pen.png
│ │ └── pink-pen.png
│ ├── golden-treasure-box.png
│ ├── silver-treasure-box.png
│ ├── purple-pen.png (duplicate, different context)
│ ├── pink-pen.png (duplicate, different context)
│ ├── yellow-key.png
│ └── green-key.png
└── adult/
├── practice/
│ ├── star-oval-diamond.png
│ └── diamond-rectangle.png
├── blue-cube.png
├── yellow-square.png
├── star-purple-oval.png
├── heart-diamond-rectangle.png
├── horizontal-lines.png
└── vertical-lines.png
5.3 Configuration File
// constants/stimuliConfig.ts
export const STIMULI_CONFIG = {
adolescent: {
practice: {
left: '/stimuli/adolescent/practice/purple-pen.png',
right: '/stimuli/adolescent/practice/pink-pen.png',
correct: 'left',
names: { left: 'Purple Pen', right: 'Pink Pen' }
},
blocks: [
{ // Block 1-2
left: '/stimuli/adolescent/golden-treasure-box.png',
right: '/stimuli/adolescent/silver-treasure-box.png',
correct: 'left',
names: { left: 'Golden Treasure Box', right: 'Silver Treasure Box' }
},
{ // Block 3-4
left: '/stimuli/adolescent/purple-pen.png',
right: '/stimuli/adolescent/pink-pen.png',
correct: 'left',
names: { left: 'Purple Pen', right: 'Pink Pen' }
},
{ // Block 5-6
left: '/stimuli/adolescent/yellow-key.png',
right: '/stimuli/adolescent/green-key.png',
correct: 'left',
names: { left: 'Yellow Key', right: 'Green Key' }
}
]
},
adult: {
// Similar structure for adult stimuli
}
};
5.4 Image Loading Strategy
const StimulusImage = ({ src, alt, isSelected, onClick, disabled }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [hasError, setHasError] = useState(false);
return (
<div className={`stimulus-container ${isSelected ? 'selected' : ''}`}>
{!isLoaded && <LoadingSpinner />}
<img
src={src}
alt={alt}
onLoad={() => setIsLoaded(true)}
onError={() => setHasError(true)}
onClick={!disabled ? onClick : undefined}
style={{ display: isLoaded ? 'block' : 'none' }}
/>
{hasError && <div>Error loading image</div>}
</div>
);
};
Preloading Strategy:
// Preload all images for current block and next block
useEffect(() => {
const imagesToPreload = [
...getCurrentBlockImages(),
...getNextBlockImages()
];
imagesToPreload.forEach(src => {
const img = new Image();
img.src = src;
});
}, [currentBlock]);
5.5 Future API Integration Preparation
// utils/imageLoader.ts
export const getImageSource = async (
ageGroup: string,
blockType: string,
stimulusName: string
): Promise<string> => {
// Current: Return local path
if (USE_LOCAL_IMAGES) {
return `/stimuli/${ageGroup}/${blockType}/${stimulusName}.png`;
}
// Future: Fetch from API
try {
const response = await fetch(`/api/stimuli/${ageGroup}/${blockType}/${stimulusName}`);
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (error) {
console.error('Failed to fetch image:', error);
return '/stimuli/fallback.png';
}
};
6. Core Logic Implementation
6.1 Reversal Logic (CRITICAL)
Trigger Condition: After 3 consecutive ACTUAL correct responses
Key Points:
- Count based on ACTUAL correctness, NOT displayed feedback
- Misleading punishment STILL counts as correct for reversal
- Only active during main test (disabled in practice)
- Resets to 0 after trigger or after any incorrect choice
const handleReversalLogic = (isActuallyCorrect: boolean, isPractice: boolean) => {
if (isPractice) {
// NO reversals during practice
return { shouldReverse: false };
}
if (isActuallyCorrect) {
const newCount = consecutiveCorrectCount + 1;
setConsecutiveCorrectCount(newCount);
if (newCount >= 3) {
// TRIGGER REVERSAL
const previousCorrectSide = currentCorrectSide;
const newCorrectSide = previousCorrectSide === 'left' ? 'right' : 'left';
setCurrentCorrectSide(newCorrectSide);
setConsecutiveCorrectCount(0);
setReversalCount(prev => prev + 1);
setLastReversalTrial(currentTrialNumber);
setErrorsSinceReversal(0);
return {
shouldReverse: true,
previousSide: previousCorrectSide,
newSide: newCorrectSide
};
}
} else {
// Incorrect choice resets counter
setConsecutiveCorrectCount(0);
}
return { shouldReverse: false };
};
6.2 Probabilistic Feedback Logic
Distribution: 25% of trials (3 out of 12 per block)
Implementation Strategy:
// Generate probabilistic pattern at block start
const generateProbabilisticPattern = (totalRounds: number = 12) => {
const probabilisticCount = 3;
const rounds = Array.from({ length: totalRounds }, (_, i) => i);
// Shuffle and select 3 random rounds
const shuffled = rounds.sort(() => Math.random() - 0.5);
const probabilisticRounds = shuffled.slice(0, probabilisticCount);
// Randomly determine type for each
const pattern = probabilisticRounds.map(round => ({
round,
type: Math.random() > 0.5 ? 'misleading_punishment' : 'misleading_reward'
}));
return pattern;
};
// Check if current round should have probabilistic feedback
const shouldApplyProbabilisticFeedback = (currentRound: number, pattern: any[]) => {
return pattern.find(p => p.round === currentRound);
};
// Apply probabilistic feedback
const determineFeedback = (isActuallyCorrect: boolean, probabilisticInfo: any) => {
if (!probabilisticInfo) {
// Normal contingent feedback
return isActuallyCorrect ? 'reward' : 'punishment';
}
// Non-contingent (misleading) feedback
if (probabilisticInfo.type === 'misleading_punishment') {
return isActuallyCorrect ? 'punishment' : 'reward';
} else {
return isActuallyCorrect ? 'reward' : 'punishment';
}
};
Critical Rules:
- Generate pattern ONCE per block (not per trial)
- Can be any combination: 3P, 2P+1R, 2R+1P (P=punishment, R=reward)
- Participant should NOT be able to predict pattern
6.3 Error Classification Logic
Three Error Types to Track:
const classifyError = (
isActuallyCorrect: boolean,
lastReversalTrial: number | null,
currentTrialNumber: number,
errorsSinceReversal: number,
nextTrialWillBeCorrect: boolean // Lookahead for final reversal error
) => {
if (isActuallyCorrect) {
return 'none';
}
// Only classify errors after a reversal has occurred
if (lastReversalTrial === null) {
return 'random'; // Error before any reversal
}
const trialsSinceReversal = currentTrialNumber - lastReversalTrial;
// REVERSAL ERROR: Immediately after reversal (first error)
if (trialsSinceReversal === 1 && errorsSinceReversal === 0) {
setTotalReversalErrors(prev => prev + 1);
return 'reversal';
}
// PERSEVERATIVE ERROR: Continuing with old pattern
if (trialsSinceReversal > 1 && errorsSinceReversal > 0) {
setTotalPerseverativeErrors(prev => prev + 1);
// FINAL REVERSAL ERROR: Last error before participant adapts
// This requires lookahead - mark retroactively if next trial is correct
if (shouldMarkAsFinalReversalError) {
setTotalFinalReversalErrors(prev => prev + 1);
return 'final_reversal';
}
return 'perseverative';
}
return 'random';
};
Final Reversal Error Detection: Since we can't predict the future, use this approach:
// Store last perseverative error index
let lastPerseverativeErrorIndex: number | null = null;
// On error
if (errorType === 'perseverative') {
lastPerseverativeErrorIndex = currentTrialNumber;
}
// On correct response after reversal
if (isActuallyCorrect && lastReversalTrial && lastPerseverativeErrorIndex) {
// Mark last perseverative error as "final reversal error"
updateTrialData(lastPerseverativeErrorIndex, {
errorType: 'final_reversal'
});
setTotalFinalReversalErrors(prev => prev + 1);
setTotalPerseverativeErrors(prev => prev - 1); // Adjust count
lastPerseverativeErrorIndex = null;
}
6.4 Shift Tracking Logic (CORRECTED)
Purpose: Measure behavioral flexibility
Definitions:
- Win-Shift: Change choice after receiving reward
- Lose-Shift: Change choice after receiving punishment
Corrected Implementation:
const trackShifts = (
currentChosenStimulus: string,
currentDisplayedOutcome: 'reward' | 'punishment',
lastChosenStimulus: string | null,
lastDisplayedOutcome: 'reward' | 'punishment' | null
) => {
// First trial has no previous to compare
if (!lastChosenStimulus || !lastDisplayedOutcome) {
// Store current for next trial
setLastChosenStimulus(currentChosenStimulus);
setLastDisplayedOutcome(currentDisplayedOutcome);
// Count current outcome
if (currentDisplayedOutcome === 'reward') {
setTotalWins(prev => prev + 1);
} else {
setTotalLosses(prev => prev + 1);
}
return;
}
// Check if participant shifted choice
const didShift = currentChosenStimulus !== lastChosenStimulus;
// Classify based on PREVIOUS trial's outcome
if (lastDisplayedOutcome === 'reward') {
if (didShift) {
setWinShifts(prev => prev + 1);
}
// Implicit: if !didShift, it's a "win-stay"
} else if (lastDisplayedOutcome === 'punishment') {
if (didShift) {
setLoseShifts(prev => prev + 1);
}
// Implicit: if !didShift, it's a "lose-stay"
}
// Count current outcome for next trial
if (currentDisplayedOutcome === 'reward') {
setTotalWins(prev => prev + 1);
} else {
setTotalLosses(prev => prev + 1);
}
// Store current for next trial
setLastChosenStimulus(currentChosenStimulus);
setLastDisplayedOutcome(currentDisplayedOutcome);
};
Calculation Formula:
const winShiftRate = totalWins > 0 ? (winShifts / totalWins) * 100 : 0;
const loseShiftRate = totalLosses > 0 ? (loseShifts / totalLosses) * 100 : 0;
Interpretation:
- Low Win-Shift Rate (good): Participant sticks with winning choices
- High Lose-Shift Rate (good): Participant adapts after mistakes
- High Win-Shift Rate (poor): Random/impulsive behavior
- Low Lose-Shift Rate (poor): Cognitive rigidity
6.5 Timeout Handling (CORRECTED)
const handleTimeout = () => {
// Clear timer
clearInterval(timerRef.current);
// Show timeout message
setShowTimeout(true);
// CRITICAL: Deduct score (was missing in prototype)
if (ageGroup === 'adolescent') {
setScore(prev => prev - 40);
}
// Record as incorrect response
const trialData: TrialRecord = {
participantChoice: 'timeout',
responseAccuracy: 0,
responseTime: 4000,
errorType: 'random',
scoreChange: -40,
feedbackGiven: 'Time is up! -40 coins',
// ... other fields
};
recordTrial(trialData);
// Reset consecutive correct counter
setConsecutiveCorrectCount(0);
// Show timeout for 1 second, then proceed
setTimeout(() => {
showFixationAndProceed();
}, 1000);
};
6.6 Forced Reversal Between Blocks (CONDITIONAL)
Rule from PDF: If participant achieves first reversal but not second reversal before block ends, next block can start with forced reversal.
const handleBlockCompletion = (currentBlock: number, lastReversalTrial: number | null) => {
const blockStartTrial = currentBlock * 12;
const blockEndTrial = blockStartTrial + 11;
// Check if reversal occurred in this block
const reversalOccurredInBlock = lastReversalTrial &&
lastReversalTrial >= blockStartTrial &&
lastReversalTrial <= blockEndTrial;
// Check if participant adapted to reversal (got 3 correct on new rule)
const adaptedToReversal = consecutiveCorrectCount >= 3;
// Scenario 3: Reversal occurred but not adapted
if (reversalOccurredInBlock && !adaptedToReversal) {
// Next block should start with FORCED reversal
// Meaning: keep current correct side (don't flip)
return { shouldForceReversal: false }; // Don't flip side
}
// Default: Start next block with natural initial correct side
// Flip to opposite of current
return { shouldForceReversal: true }; // Flip side
};
// At block transition
const startNewBlock = (nextBlockNumber: number) => {
const { shouldForceReversal } = handleBlockCompletion(currentBlock, lastReversalTrial);
if (shouldForceReversal) {
setCurrentCorrectSide(prev => prev === 'left' ? 'right' : 'left');
}
// else: keep current side
setCurrentBlock(nextBlockNumber);
setCurrentRound(0);
setConsecutiveCorrectCount(0);
// ... other resets
};
7. Data Collection & Recording
7.1 Trial-Level Data Recording
When to Record: After EVERY trial (including timeouts)
const recordTrialData = (trialInfo: Partial<TrialRecord>) => {
const completeRecord: TrialRecord = {
// Auto-generated
trialNumber: testData.length + 1,
blockNumber: currentScreen === 'practice' ? 'Practice' : currentBlock + 1,
roundInBlock: currentRound + 1,
timestamp: Date.now(),
// From trial state
stimulusSet: getCurrentStimulusSetName(),
leftStimulus: leftStimulusPath,
rightStimulus: rightStimulusPath,
taskRule: `${getCurrentCorrectStimulusName()} is rewarded`,
currentCorrectStimulus: getCurrentCorrectStimulusName(),
// From user action
...trialInfo
};
setTestData(prev => [...prev, completeRecord]);
};
7.2 Block-Level Summary
After Each Block, Calculate:
interface BlockSummary {
blockNumber: number;
accuracy: number;
avgResponseTime: number;
reversalsTriggered: number;
reversalErrors: number;
perseverativeErrors: number;
timeouts: number;
}
const generateBlockSummary = (blockNumber: number, trialData: TrialRecord[]) => {
const blockTrials = trialData.filter(t => t.blockNumber === blockNumber);
return {
blockNumber,
accuracy: (blockTrials.filter(t => t.responseAccuracy === 1).length / 12) * 100,
avgResponseTime: blockTrials.reduce((sum, t) => sum + t.responseTime, 0) / 12,
reversalsTriggered: blockTrials.filter(t => t.reversalTriggered).length,
// ... other metrics
};
};
7.3 Data Export Functionality
Export Formats: CSV and JSON
// utils/dataExport.ts
export const exportToCSV = (trialData: TrialRecord[], filename: string) => {
const headers = Object.keys(trialData[0]).join(',');
const rows = trialData.map(trial =>
Object.values(trial).map(v =>
typeof v === 'string' ? `"${v}"` : v
).join(',')
);
const csv = [headers, ...rows].join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
};
export const exportToJSON = (trialData: TrialRecord[], summary: any, filename: string) => {
const exportData = {
metadata: {
testName: 'Cognitive Flexibility Test',
version: '1.0',
exportDate: new Date().toISOString(),
totalTrials: trialData.length
},
summary,
trialData
};
const json = JSON.stringify(exportData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
};
7.4 Local Storage Backup
Auto-save Progress:
useEffect(() => {
// Save state every trial
const stateBackup = {
currentScreen,
currentBlock,
currentRound,
score,
trialData,
timestamp: Date.now()
};
localStorage.setItem('cognitive-test-backup', JSON.stringify(stateBackup));
}, [trialData]);
// On mount, check for backup
useEffect(() => {
const backup = localStorage.getItem('cognitive-test-backup');
if (backup) {
const parsed = JSON.parse(backup);
// Check if backup is recent (within 1 hour)
if (Date.now() - parsed.timestamp < 3600000) {
// Offer to resume
setShowResumePrompt(true);
}
}
}, []);
8. Timing & Performance
8.1 Timer Implementation
Use useRef and setInterval for Accuracy:
const useTimer = (duration: number, onComplete: () => void) => {
const [timeRemaining, setTimeRemaining] = useState(duration);
const timerRef = useRef<NodeJS.Timeout | null>(null);
const startTimeRef = useRef<number | null>(null);
const start = () => {
startTimeRef.current = Date.now();
let elapsed = 0;
timerRef.current = setInterval(() => {
elapsed = Math.floor((Date.now() - startTimeRef.current!) / 1000);
const remaining = duration - elapsed;
setTimeRemaining(remaining);
if (remaining <= 0) {
stop();
onComplete();
}
}, 100); // Update every 100ms for smooth display
};
const stop = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
};
const getElapsedTime = () => {
if (!startTimeRef.current) return 0;
return Date.now() - startTimeRef.current;
};
useEffect(() => {
return () => stop(); // Cleanup
}, []);
return { timeRemaining, start, stop, getElapsedTime };
};
8.2 Response Time Measurement
Use High-Precision Timestamps:
const handleTrialStart = () => {
const trialStartTime = performance.now(); // High precision
setTrialStartTime(trialStartTime);
};
const handleChoice = (side: 'left' | 'right') => {
const responseTime = performance.now() - trialStartTime;
// Record in milliseconds with decimal precision
recordTrial({ responseTime: Math.round(responseTime) });
};
8.3 Preventing Timer Drift
Issue: JavaScript timers can drift over many trials
Solution:
// Calculate total expected time vs actual time
const totalExpectedTime = trialNumber * TIMING_CONFIG.TOTAL_TRIAL;
const actualElapsedTime = Date.now() - testStartTime;
const drift = actualElapsedTime - totalExpectedTime;
// Log drift for debugging
if (drift > 1000) { // More than 1 second drift
console.warn(`Timer drift detected: ${drift}ms`);
}
8.4 Performance Optimization
Image Optimization:
- Preload next block images during current block
- Use WebP format with PNG fallback
- Lazy load images for results screen
Render Optimization:
// Memoize expensive calculations
const blockSummary = useMemo(() =>
calculateBlockSummary(trialData),
[trialData]
);
// Prevent unnecessary re-renders
const StimulusDisplay = React.memo(({ src, alt, onClick }) => {
// ...
});
State Updates:
// Batch state updates when possible
setGameState(prev => ({
...prev,
currentRound: prev.currentRound + 1,
showFeedback: false,
showFixation: false
}));
9. Error Handling & Edge Cases
9.1 Image Loading Failures
const StimulusImage = ({ src, alt, onError }) => {
const [error, setError] = useState(false);
const handleImageError = () => {
setError(true);
console.error(`Failed to load image: ${src}`);
onError?.(src);
};
if (error) {
return (
<div className="image-error">
<span>⚠️ Image unavailable</span>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return <img src={src} alt={alt} onError={handleImageError} />;
};
9.2 Browser Tab Visibility
Pause test when tab is hidden:
useEffect(() => {
const handleVisibilityChange = () => {
if (document.hidden) {
// Pause timers
pauseTest();
showWarning('Test paused. Please return to this tab.');
} else {
// Resume timers
resumeTest();
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, []);
9.3 Rapid Double-Clicks
Prevent multiple responses:
const handleChoice = (side: 'left' | 'right') => {
// Check if already responded
if (selectedSide !== null || showFeedback) {
return; // Ignore duplicate clicks
}
// Disable further clicks
setSelectedSide(side);
// Process response
// ...
};
9.4 Browser Refresh/Close Warning
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (currentScreen === 'practice' || currentScreen === 'main') {
e.preventDefault();
e.returnValue = 'Your progress will be lost. Are you sure?';
return e.returnValue;
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [currentScreen]);
9.5 Network Interruption (Future API)
const fetchStimulusImage = async (url: string) => {
try {
const response = await fetch(url, {
timeout: 5000,
retry: 3
});
return await response.blob();
} catch (error) {
// Fallback to local image
console.error('API fetch failed, using local fallback');
return loadLocalImage(url);
}
};
9.6 Invalid State Recovery
const validateState = (state: TestState) => {
const issues: string[] = [];
if (state.currentBlock < 0 || state.currentBlock > 5) {
issues.push('Invalid block number');
}
if (state.currentRound < 0 || state.currentRound > 11) {
issues.push('Invalid round number');
}
if (state.consecutiveCorrectCount < 0) {
issues.push('Invalid consecutive correct count');
}
if (issues.length > 0) {
console.error('State validation failed:', issues);
// Reset to safe state
return getDefaultState();
}
return state;
};
Practice Phase:
- 12 rounds complete (not 8)
- No reversals occur even after 3+ consecutive correct
- Probabilistic feedback appears ~3 times
- Timeout handled correctly with score deduction
- "Practice Complete" screen shows
Main Test:
- 6 blocks × 12 rounds = 72 total
- Block instructions appear between blocks
- Reversals trigger after 3 consecutive correct
- Images load correctly for each block
- Score updates correctly (adolescents)
- Blue outline appears on selection
- Timer counts down from 4 seconds
Results Screen:
- All metrics calculated correctly
- Win-shift and lose-shift rates accurate
- Error counts match manual count
- Export data works (CSV/JSON)
Edge Cases:
- Rapid clicking doesn't cause issues
- Tab switching pauses test
- Browser refresh warns user
- Image load failures handled gracefully
11. Future API Integration
11.1 API Endpoints (Planned)
Authentication:
POST /api/auth/login
POST /api/auth/register
Test Management:
GET /api/tests // List available tests
GET /api/tests/{testId} // Get test configuration
POST /api/tests/{testId}/start // Initialize test session
Stimulus Fetching:
GET /api/stimuli/{ageGroup}/{blockType}/{stimulusName}
Data Submission:
POST /api/tests/{testId}/sessions/{sessionId}/trials // Submit trial data
POST /api/tests/{testId}/sessions/{sessionId}/complete // Submit complete results
Results Retrieval:
GET /api/tests/{testId}/sessions/{sessionId}/results
GET /api/users/{userId}/test-history
11.2 API Service Layer
// services/api.service.ts
export class CognitiveTestAPI {
private baseURL: string;
private sessionId: string | null = null;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
async startSession(testId: string, userId: string): Promise<string> {
const response = await fetch(`${this.baseURL}/tests/${testId}/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId })
});
const data = await response.json();
this.sessionId = data.sessionId;
return this.sessionId;
}
async fetchStimulus(path: string): Promise<string> {
// Try API first, fallback to local
try {
const response = await fetch(`${this.baseURL}/stimuli${path}`);
const blob = await response.blob();
return URL.createObjectURL(blob);
} catch (error) {
console.warn('API fetch failed, using local image');
return path; // Return local path
}
}
async submitTrial(trialData: TrialRecord): Promise<void> {
await fetch(`${this.baseURL}/tests/${testId}/sessions/${this.sessionId}/trials`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(trialData)
});
}
async submitResults(results: any): Promise<void> {
await fetch(`${this.baseURL}/tests/${testId}/sessions/${this.sessionId}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(results)
});
}
}
11.3 Environment Configuration
// config/environment.ts
export const config = {
API_URL: process.env.REACT_APP_API_URL || 'http://localhost:3000/api',
USE_LOCAL_IMAGES: process.env.REACT_APP_USE_LOCAL_IMAGES === 'true',
ENABLE_API: process.env.REACT_APP_ENABLE_API === 'true',
AUTO_SAVE_INTERVAL: 30000, // 30 seconds
};
11.4 Offline Support
// utils/offlineQueue.ts
class OfflineQueue {
private queue: any[] = [];
add(request: any) {
this.queue.push(request);
this.saveToStorage();
}
async flush() {
while (this.queue.length > 0) {
const request = this.queue[0];
try {
await this.sendRequest(request);
this.queue.shift();
} catch (error) {
console.error('Failed to flush request:', error);
break; // Stop if still offline
}
}
this.saveToStorage();
}
private saveToStorage() {
localStorage.setItem('offline-queue', JSON.stringify(this.queue));
}
restore() {
const saved = localStorage.getItem('offline-queue');
if (saved) {
this.queue = JSON.parse(saved);
}
}
}
11.5 Real-time Progress Sync
// For future implementation with WebSocket
class ProgressSync {
private ws: WebSocket | null = null;
connect(sessionId: string) {
this.ws = new WebSocket(`ws://api.example.com/sessions/${sessionId}`);
this.ws.onmessage = (event) => {
const message = JSON.parse(event.data);
// Handle real-time updates (e.g., supervisor monitoring)
};
}
sendProgress(trial: TrialRecord) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(trial));
}
}
}
12. Implementation Checklist
12.1 Phase 1: Core Implementation (Week 1-2)
- Set up project structure with TypeScript
- Implement state management architecture
- Create all screen components (Intro, Practice, Main, Results)
- Implement core game loop with correct timing
- Add reversal logic (disabled for practice)
- Implement probabilistic feedback system
- Create stimulus image display components
- Add timeout handling with score deduction
12.2 Phase 2: Data & Logic (Week 2-3)
- Implement trial data recording
- Add error classification (reversal, perseverative, final)
- Implement shift tracking (corrected logic)
- Add data export (CSV/JSON)
- Implement block transition logic
- Add conditional forced reversal between blocks
- Create results calculation functions
12.3 Phase 3: Assets & UI (Week 3)
- Design/source all stimulus images
- Organize images in public folder structure
- Implement age-specific UI (adolescent vs adult)
- Add animations and transitions
- Create feedback displays (coins vs smileys)
- Implement responsive design (345px - large screens)
- Add loading states and error handling
12.4 Phase 4: Testing & Polish (Week 4)
- Write unit tests for core logic
- Write integration tests for game flow
- Perform manual testing with checklist
- Test on multiple devices/browsers
- Optimize performance
- Add accessibility features (ARIA labels)
- Create user documentation
12.5 Phase 5: Future Preparation (Ongoing)
- Design API integration layer
- Implement offline queue system
- Add environment configuration
- Create API service abstraction
- Document API requirements for backend team
- Plan authentication flow
| Issue | Prototype | Corrected Implementation |
|---|---|---|
| Test Length | 24 rounds (3×8) | 72 rounds (6×12) |
| Practice Rounds | 8 rounds | 12 rounds |
| Practice Reversals | Enabled | DISABLED |
| Stimuli | Generic emojis | Age-specific images |
| Shift Tracking | Off by one trial | Correct timing |
| Timeout Score | No deduction | -40 coins deducted |
| Final Reversal Error | Not tracked | Properly tracked |
| Block Instructions | Missing | Added between blocks |
| Forced Reversal | Always | Conditional (Scenario 3) |
| Data Fields | Basic | Complete per PDF spec |
14. Conclusion
This implementation plan provides a comprehensive roadmap for building the Cognitive Flexibility Test frontend application that strictly adheres to the research specifications. By following this documentation:
- All critical issues from the prototype are addressed
- Exact test specifications are implemented
- Data collection meets research standards
- System is prepared for future API integration
- Code is maintainable and testable
The modular architecture ensures that each component can be developed, tested, and debugged independently while maintaining the integrity of the overall system.
State Management Summary
- Navigation States: intro → practice → practiceComplete → main → blockInstruction → results
- Core Counters: currentBlock (0-5), currentRound (0-11)
- Reversal Tracking: consecutiveCorrectCount, reversalCount, lastReversalTrial
- Error Tracking: reversalErrors, perseverativeErrors, finalReversalErrors
- Shift Tracking: winShifts, loseShifts, totalWins, totalLosses
Document Version: 1.0
Last Updated: October 30, 2025
Author: AI Assistant
Status: Ready for Implementation