CP_AUTOMATION/CognitivePrism/my-project/cognitive-docs/Doc/FRONTEND_IMPLEMENTATION_PLAN.md
2025-12-12 19:54:54 +05:30

47 KiB
Raw Blame History

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
  2. Test Specifications & Requirements
  3. State Management Strategy
  4. Screen Flow Implementation
  5. Image Asset Management
  6. Core Logic Implementation
  7. Data Collection & Recording
  8. Timing & Performance
  9. Error Handling & Edge Cases
  10. Testing Strategy
  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

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:

  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:

// 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):

  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:

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:

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:

  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
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:

  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