# 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 1. [Project Architecture](#1-project-architecture) 2. [Test Specifications & Requirements](#2-test-specifications--requirements) 3. [State Management Strategy](#3-state-management-strategy) 4. [Screen Flow Implementation](#4-screen-flow-implementation) 5. [Image Asset Management](#5-image-asset-management) 6. [Core Logic Implementation](#6-core-logic-implementation) 7. [Data Collection & Recording](#7-data-collection--recording) 8. [Timing & Performance](#8-timing--performance) 9. [Error Handling & Edge Cases](#9-error-handling--edge-cases) 10. [Testing Strategy](#10-testing-strategy) 11. [Future API Integration](#11-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 ```typescript 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 ```typescript 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 ```typescript 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:** ```typescript // 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:** ```typescript 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:** 1. **NO REVERSAL LOGIC** - Even if 3+ consecutive correct 2. Probabilistic feedback still active (25%) 3. Same timing as main test 4. Starting correct stimulus defined in config 5. Timeout handling with score deduction **Practice Complete Transition:** ```typescript // 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:** ```typescript 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):** 1. **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!" 2. **Feedback Display (1 second)** - Stimuli remain visible - Feedback overlays (coins/smiley) - Score updates (adolescents) 3. **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:** ```typescript 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:** 1. **Summary Metrics (Grid Layout)** - Accuracy percentage - Average response time - Total correct responses - Reversals triggered 2. **Error Analysis** - Reversal Errors: X (with explanation) - Perseverative Errors: X (with explanation) - Final Reversal Errors: X (with explanation) 3. **Strategy Flexibility** - Win-Shift Rate: X% (Lower is better) - Lose-Shift Rate: X% (Higher is better) 4. **Performance Summary** - Total rounds: 72 - Rounds answered: X - Rounds missed: X - Final score: X coins (adolescents only) 5. **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:** ```typescript 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 ```typescript // 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 ```typescript const StimulusImage = ({ src, alt, isSelected, onClick, disabled }) => { const [isLoaded, setIsLoaded] = useState(false); const [hasError, setHasError] = useState(false); return (
{!isLoaded && } {alt} setIsLoaded(true)} onError={() => setHasError(true)} onClick={!disabled ? onClick : undefined} style={{ display: isLoaded ? 'block' : 'none' }} /> {hasError &&
Error loading image
}
); }; ``` **Preloading Strategy:** ```typescript // 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 ```typescript // utils/imageLoader.ts export const getImageSource = async ( ageGroup: string, blockType: string, stimulusName: string ): Promise => { // 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:** 1. Count based on ACTUAL correctness, NOT displayed feedback 2. Misleading punishment STILL counts as correct for reversal 3. Only active during main test (disabled in practice) 4. Resets to 0 after trigger or after any incorrect choice ```typescript 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:** ```typescript // 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:** ```typescript 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: ```typescript // 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:** ```typescript 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:** ```typescript 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) ```typescript 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. ```typescript 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) ```typescript const recordTrialData = (trialInfo: Partial) => { 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:** ```typescript 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 ```typescript // 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:** ```typescript 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:** ```typescript const useTimer = (duration: number, onComplete: () => void) => { const [timeRemaining, setTimeRemaining] = useState(duration); const timerRef = useRef(null); const startTimeRef = useRef(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:** ```typescript 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:** ```typescript // 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:** ```typescript // Memoize expensive calculations const blockSummary = useMemo(() => calculateBlockSummary(trialData), [trialData] ); // Prevent unnecessary re-renders const StimulusDisplay = React.memo(({ src, alt, onClick }) => { // ... }); ``` **State Updates:** ```typescript // 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 ```typescript 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 (
⚠️ Image unavailable
); } return {alt}; }; ``` ### 9.2 Browser Tab Visibility **Pause test when tab is hidden:** ```typescript 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:** ```typescript 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 ```typescript 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) ```typescript 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 ```typescript 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 ```typescript // 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 { 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 { // 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 { 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 { 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 ```typescript // 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 ```typescript // 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 ```typescript // 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: 1. **All critical issues from the prototype are addressed** 2. **Exact test specifications are implemented** 3. **Data collection meets research standards** 4. **System is prepared for future API integration** 5. **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