From fef23c045d80fdfe72d006b98599c77033f3c310 Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Thu, 26 Feb 2026 11:24:14 -0600 Subject: [PATCH 01/15] INT-83 Fix border placement on `Character` --- src/view/components/Character/index.tsx | 1 + src/view/screens/SelectAvatar/index.tsx | 19 ++++++++----------- types/props.d.ts | 3 +++ 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/view/components/Character/index.tsx b/src/view/components/Character/index.tsx index 6a7250f..9259a0d 100644 --- a/src/view/components/Character/index.tsx +++ b/src/view/components/Character/index.tsx @@ -41,6 +41,7 @@ const Character: FC = ( = ( > {avatars.map((avatar, i) => { return ( - + = ( setSelectedAvatarName(avatarName); setSelectedAvatarIndex(i); }} + border={ + Configuration.manipulations.useButtonInput === true && + selectedAvatarIndex === i && { + color: "selectedElement", + size: "large", + } + } /> ); diff --git a/types/props.d.ts b/types/props.d.ts index 4c28db7..e1ad65f 100644 --- a/types/props.d.ts +++ b/types/props.d.ts @@ -2,6 +2,8 @@ * @file Prop declarations for React components and screens. * @author Henry Burgess */ +// Grommet imports for styling components +import { BorderType } from "grommet/utils"; // Props for 'Components' declare namespace Components { @@ -60,6 +62,7 @@ declare namespace Components { name: string; state: string; setState: (avatar: string) => void; + border?: BorderType; }; // Status componet From aec9ebe78ccb6ce5ffc3b9fc62c1f5747baedf4f Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Thu, 26 Feb 2026 14:44:28 -0600 Subject: [PATCH 02/15] INT-83 Unify types into one file * Use `index.d.ts` instead of multiple files --- types/data.d.ts | 88 --------- types/index.d.ts | 462 +++++++++++++++++++++++++++++++++++++++++++-- types/jsPsych.d.ts | 122 ------------ types/props.d.ts | 220 --------------------- types/window.d.ts | 18 -- 5 files changed, 450 insertions(+), 460 deletions(-) delete mode 100644 types/data.d.ts delete mode 100644 types/jsPsych.d.ts delete mode 100644 types/props.d.ts delete mode 100644 types/window.d.ts diff --git a/types/data.d.ts b/types/data.d.ts deleted file mode 100644 index e38b06e..0000000 --- a/types/data.d.ts +++ /dev/null @@ -1,88 +0,0 @@ -// Declare CSV module type -declare module "*.csv"; - -// CSV data row types -declare type Row = { - randomise_blocks: string; - randomise_trials: number; - display: Display; - ANSWER: Options; - Option1_PPT: number; - Option1_Partner: number; - Option2_PPT: number; - Option2_Partner: number; - ShowProgressBar: number; - Type1: Partner; - Type2: Partner; - Difference1: number; - Difference2: number; -}; - -// Data type used to enforce trial data storage format -declare type Dataframe = { - // Trial identification - trial_number: number; - display: Display; - participantID: string; - - // Points - playerPoints_option1: number; - partnerPoints_option1: number; - playerPoints_option2: number; - partnerPoints_option2: number; - playerPoints_selected: number; - partnerPoints_selected: number; - - // Trial state - selectedOption_player: NaN | 1 | 2; // uses 1 and 2 rather than strings - realAnswer: Options; - trialDuration: number; - correctGuess: NaN | 0 | 1; // 0 incorrect; 1 correct - - // Model parameters - server_alpha_ppt: number; - server_beta_ppt: number; - server_alpha_par: number; - server_beta_par: number; - setupDuration: number; - operationDuration: number; - - // Social status - participantDefaultStatus: number; - partnerCyberballLowStatus: number; - partnerCyberballHighStatus: number; - partnerOneLowStatus: number; - partnerOneHighStatus: number; - partnerTwoLowStatus: number; - partnerTwoHighStatus: number; - partnerThreeLowStatus: number; - partnerThreeHighStatus: number; - - // Signal timestamps - signalTimestamps: number[]; - - // Cyberball data - cyberballTossCount: number; - cyberballParticipantTossCount: number; - cyberballParticipantCatchCount: number; - - // Questionnaire responses - questionnaireResponseInferenceSelfish: number; - questionnaireResponseInferenceHarm: number; - questionnaireResponseAgency: number; - questionnaireResponseClassification: string; - questionnaireResponseCloseFriends: number; - questionnaireResponsePartyInvitations: number; - questionnaireResponseMeanPeople: number; - questionnaireResponseSocialMediaFollowers: number; - questionnaireResponseSocialMediaFollowing: number; - questionnaireResponsesDASS: number[]; - questionnaireResponsesScreentime: number[]; - questionnaireResponsesDemographicsAge: number; - questionnaireResponsesDemographicsGender: string; - questionnaireResponsesDemographicsEthnicity: string; - questionnaireResponsesDemographicsHouseholdIncome: string; - questionnaireResponsesDemographicsEducation: string; - questionnaireResponsesDemographicsSocialMediaDaily: boolean; - questionnaireResponsesDemographicsSocialMediaPlatforms: string; -}; diff --git a/types/index.d.ts b/types/index.d.ts index ad9bb8d..326f70f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -4,8 +4,141 @@ * @author Henry Burgess */ +// Grommet imports for styling components +import { BorderType } from "grommet/utils"; + +// 'Experiment' jsPsych wrapper library +import type { Experiment } from "neurocog"; + +// Compute class +import type Compute from "src/classes/Compute"; + // Module declaration -declare module "intentions-game" {} +export module "intentions-game" {} + +// Add 'Experiment' to the global Window interface +declare global { + interface Window { + Experiment: Experiment; + Compute: Compute; + } +} + +export type GenericNode = { + type?: string; + trial_number?: number; +}; + +export type FullscreenNode = { + // Fullscreen plugin + message?: string; + fullscreen_mode?: boolean; +}; + +export type InstructionsNode = { + // Instructions plugin + pages?: string[] | string[]; + allow_keys?: boolean; + key_forward?: string; + key_backward?: string; + show_page_number?: boolean; + show_clickable_nav?: boolean; +}; + +export type ComprehensionNode = { + // Attention-check plugin + prompt?: string; + style?: "default" | "radio"; + responses?: string[]; + correct?: number; + feedback?: { correct: string; incorrect: string }; + input_timeout?: number; + input_schema?: { + select: string | null; + next: string | null; + previous: string | null; + }; + confirm_continue?: boolean; +}; + +export type LoopNode = { + // Loop nodes + timeline?: any[]; + conditional_function?: () => boolean; +}; + +export type InputNode = { + // HTML input plugin + preamble?: string; + html?: string; +}; + +export type IntentionsNode = { + // Basic trial identification + trial_number: number; // Trial number/identifier + display: Display; // Screen type to display (e.g., "playerChoice", "loading", etc.) + + // Game option parameters (used by Trial screen) + optionOneParticipant: number; // Points for participant in Option 1 + optionOnePartner: number; // Points for partner in Option 1 + optionTwoParticipant: number; // Points for participant in Option 2 + optionTwoPartner: number; // Points for partner in Option 2 + + // Partner type information + typeOne: string; // Partner type for Option 1 + typeTwo: string; // Partner type for Option 2 + + // Avatar selection (used by SelectAvatar screen) + avatar: 0; // Avatar index (currently hardcoded to 0) + + // Game logic + answer: Options; // Correct answer for the trial ("Option 1" or "Option 2") + isPractice: boolean; // Whether this is a practice trial + + // Loading screen configuration (used by Loading screen) + state?: "matchingIntentions" | "matchingCyberball" | "social" | "default"; // Type of loading: "matchingIntentions" (partner matching), "matchingCyberball" (cyberball partners), "social" (status generation), or "default" (generic loading) + runComputeSetup?: boolean; // Whether to run WebR setup + runComputeOperation?: boolean; // Whether to fetch data from server (only used when state is "matchingIntentions") + + // Status preview screen configuration (used by StatusPreview screen) + nextPhase?: "cyberball" | "phaseOne" | "phaseTwo" | "phaseThree"; + isPreviewPartnerHighStatus?: boolean; // Used for the `StatusPreview` screen + + // Waiting screen configuration (used by Waiting screen) + mode: "facilitator" | "mri"; + + // Spotlight configuration (used by Trial screen) + spotlight?: { + enabled: boolean; + target: "status" | "options" | "none"; + message: string; + }; + + // Cyberball screen + isInclusive?: boolean; + isCyberballPartnerHighStatus?: boolean; + probabilities?: { + inclusion: number; + exclusion: { + partnerA: number; + partnerB: number; + }; + }; +}; + +// Timeline collection type +export type Timeline = TimelineNode[]; + +// Timeline node type, representing different timeline +// element parameter types +export type TimelineNode = GenericNode & ( + | FullscreenNode + | InstructionsNode + | ComprehensionNode + | LoopNode + | InputNode + | IntentionsNode + ); // 'Factory' interface interface Factory { @@ -13,13 +146,13 @@ interface Factory { } // Type for the 'ScreenPropFactory' return -declare type ScreenProps = { +export type ScreenProps = { props: Screens; callback: (...args) => void; duration: number; }; -declare type Display = +export type Display = | "playerChoice" | "playerChoicePractice" | "playerChoice2" @@ -45,10 +178,10 @@ declare type Display = | "resources"; // The three partner types -declare type Partner = "Test" | "Prosocial" | "Individualist" | "Competitive"; +export type Partner = "Test" | "Prosocial" | "Individualist" | "Competitive"; // Avatar styles -declare type AvatarStyles = +export type AvatarStyles = | "beam" | "marble" | "pixel" @@ -57,10 +190,10 @@ declare type AvatarStyles = | "bauhaus"; // Selection options -declare type Options = "Option 1" | "Option 2"; +export type Options = "Option 1" | "Option 2" | ""; // Type to represent the active state of a trial -declare type TrialState = { +export type TrialState = { hasSelected: boolean; highlightedOptionIndex: number; selectedOption: Options; @@ -68,7 +201,7 @@ declare type TrialState = { }; // Points storage -declare type Points = { +export type Points = { one: { participant: number; partner: number; @@ -80,7 +213,7 @@ declare type Points = { }; // Response data from the model -declare type ModelResponse = { +export type ModelResponse = { participantParameters: number[]; partnerParameters: number[]; partnerChoices: { @@ -93,7 +226,7 @@ declare type ModelResponse = { }; // Cyberball game state -declare type CyberballGameState = { +export type CyberballGameState = { ballOwner: "participant" | "partnerA" | "partnerB"; canToss: boolean; tossCount: number; @@ -105,14 +238,319 @@ declare type CyberballGameState = { // Recursive partial type, allows tests using the // 'jspsych-wrapper' Experiment class to be run -declare type RecursivePartial = { +export type RecursivePartial = { [P in keyof T]?: RecursivePartial; }; +// Props for 'Components' +export namespace Components { + // Wrapper component + type Wrapper = { + display: Display; + props: + | Screens.Loaded + | Screens.Loading + | Screens.Trial + | Screens.SelectAvatar + | Screens.Inference + | Screens.Agency + | Screens.Classification + | Screens.Status + | Screens.DASS + | Screens.Screentime + | Screens.Demographics + | Screens.Summary + | Screens.Waiting + | Screens.Cyberball + | Screens.Resources; + }; + + // Option component + type Option = { + optionKey: string; + optionName: string; + pointsParticipant: number; + pointsPartner: number; + }; + + // Card component + type Card = { + gridArea: string; + name: string; + points: string; + avatar: string; + }; + + // Slider component + type Slider = { + min: number; + max: number; + value: number; + setValue: (value: number) => void; + leftLabel: string; + rightLabel: string; + onChange?: () => any; + isFocused: boolean; + }; + + // Character component + type Character = { + size: number; + name: string; + state: string; + setState: (avatar: string) => void; + border?: BorderType; + }; + + // Status componet + type Status = { + participantStatus: number; + partnerStatus: number; + isPractice?: boolean; + hidePartner?: boolean; + }; +} + +// Props for 'Screens' +export namespace Screens { + type GenericScreenProps = { + trial_number: number; + display: Display; + }; + + // Loaded screen + type Loaded = GenericScreenProps & { + state: "matchingIntentions" | "matchingCyberball" | "social"; + handler: () => void; + }; + + // Loading screen + type Loading = GenericScreenProps & { + state: "matchingIntentions" | "matchingCyberball" | "social" | "default"; + runComputeSetup?: boolean; + runComputeOperation?: boolean; + handler?: ( + storeParameters: boolean, + participantParameters: number[], + partnerParameters: number[], + setupDuration: number, + operationDuration: number + ) => void; + }; + + // Trial screen + type Trial = GenericScreenProps & { + isPractice: boolean; + participantPoints: number; + partnerPoints: number; + options: Points; + answer: Options; + spotlight?: { + enabled: boolean; + target: "status" | "options" | "none"; + message: string; + }; + handler: ( + selection: Options, + points: { options: Points }, + answer: Options + ) => void; + }; + + // SelectAvatar screen + type SelectAvatar = GenericScreenProps & { + handler: (selectedIndex: number) => void; + }; + + // Inference screen + type Inference = GenericScreenProps & { + handler: (firstValue: number, secondValue: number) => void; + }; + + // Agency screen + type Agency = GenericScreenProps & { + handler: (firstValue: number) => void; + }; + + // Classification screen + type Classification = GenericScreenProps & { + handler: (classification: string) => void; + }; + + // Status screen + type Status = GenericScreenProps & { + handler: ( + closeFriends: number, + partyInvitations: number, + meanPeople: number, + socialMediaFollowers: number, + socialMediaFollowing: number + ) => void; + }; + + // Status preview screen + type StatusPreview = GenericScreenProps & { + nextPhase: "cyberball" | "phaseOne" | "phaseTwo" | "phaseThree"; + isPreviewPartnerHighStatus: boolean; + handler: () => void; + }; + + // DASS screen + type DASS = GenericScreenProps & { + version: "adult" | "adolescent"; + handler: (responses: number[]) => void; + }; + + // Screentime screen + type Screentime = GenericScreenProps & { + handler: (weekdayTime: number, weekendTime: number) => void; + }; + + // Demographics screen + type Demographics = GenericScreenProps & { + version: "adult" | "adolescent"; + handler: ( + age: number, + genderIdentity: string, + ethnicity: string, + householdIncome: string, + education: string, + socialMediaDaily: boolean, + socialMediaPlatforms: string, + ) => void; + }; + + // Summary screen + type Summary = GenericScreenProps & { + postPhase: Display; + handler: () => void; + }; + + // Waiting screen + type Waiting = GenericScreenProps & { + mode: "facilitator" | "mri"; + handler: () => void; + }; + + // Resources screen + type Resources = GenericScreenProps & { + handler: () => void; + }; + + // Cyberball screen + type Cyberball = GenericScreenProps & { + isInclusive: boolean; // Operate exclusively as inclusive or exclusive + isCyberballPartnerHighStatus: boolean; // Partner A is high status or low status + probabilities: { + inclusion: number; // Probability of partners passing to participant + exclusion: { + partnerA: number; // Probability of partner A passing to participant + partnerB: number; // Probability of partner B passing to participant + }; + }; + handler: ( + tossCount: number, + participantTossCount: number, + participantCatchCount: number + ) => void; + }; +} + + // Backup storage object -declare type BackupStorage = { +export type BackupStorage = { experimentID: string; timestamp: number; completed: boolean; data: any[]; }; + +// Export CSV module type +export module "*.csv"; + +// CSV data row types +export type Row = { + randomise_blocks: string; + randomise_trials: number; + display: Display; + ANSWER: Options; + Option1_PPT: number; + Option1_Partner: number; + Option2_PPT: number; + Option2_Partner: number; + ShowProgressBar: number; + Type1: Partner; + Type2: Partner; + Difference1: number; + Difference2: number; +}; + +// Data type used to enforce trial data storage format +export type Dataframe = { + // Trial identification + trial_number: number; + display: Display; + participantID: string; + + // Points + playerPoints_option1: number; + partnerPoints_option1: number; + playerPoints_option2: number; + partnerPoints_option2: number; + playerPoints_selected: number; + partnerPoints_selected: number; + + // Trial state + selectedOption_player: NaN | 1 | 2; // uses 1 and 2 rather than strings + realAnswer: Options; + trialDuration: number; + correctGuess: NaN | 0 | 1; // 0 incorrect; 1 correct + + // Model parameters + server_alpha_ppt: number; + server_beta_ppt: number; + server_alpha_par: number; + server_beta_par: number; + setupDuration: number; + operationDuration: number; + + // Social status + participantDefaultStatus: number; + partnerCyberballLowStatus: number; + partnerCyberballHighStatus: number; + partnerOneLowStatus: number; + partnerOneHighStatus: number; + partnerTwoLowStatus: number; + partnerTwoHighStatus: number; + partnerThreeLowStatus: number; + partnerThreeHighStatus: number; + + // Signal timestamps + signalTimestamps: number[]; + + // Cyberball data + cyberballTossCount: number; + cyberballParticipantTossCount: number; + cyberballParticipantCatchCount: number; + + // Questionnaire responses + questionnaireResponseInferenceSelfish: number; + questionnaireResponseInferenceHarm: number; + questionnaireResponseAgency: number; + questionnaireResponseClassification: string; + questionnaireResponseCloseFriends: number; + questionnaireResponsePartyInvitations: number; + questionnaireResponseMeanPeople: number; + questionnaireResponseSocialMediaFollowers: number; + questionnaireResponseSocialMediaFollowing: number; + questionnaireResponsesDASS: number[]; + questionnaireResponsesScreentime: number[]; + questionnaireResponsesDemographicsAge: number; + questionnaireResponsesDemographicsGender: string; + questionnaireResponsesDemographicsEthnicity: string; + questionnaireResponsesDemographicsHouseholdIncome: string; + questionnaireResponsesDemographicsEducation: string; + questionnaireResponsesDemographicsSocialMediaDaily: boolean; + questionnaireResponsesDemographicsSocialMediaPlatforms: string; +}; + diff --git a/types/jsPsych.d.ts b/types/jsPsych.d.ts deleted file mode 100644 index 29818cc..0000000 --- a/types/jsPsych.d.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @file jsPsych-related declarations. - * @author Henry Burgess - */ - -// Declare jsPsych -declare const jsPsych; - -declare type GenericNode = { - type?: string; -}; - -declare type FullscreenNode = { - // Fullscreen plugin - message?: string; - fullscreen_mode?: boolean; -}; - -declare type InstructionsNode = { - // Instructions plugin - pages?: string[] | string[]; - allow_keys?: boolean; - key_forward?: string; - key_backward?: string; - show_page_number?: boolean; - show_clickable_nav?: boolean; -}; - -declare type ComprehensionNode = { - // Attention-check plugin - prompt?: string; - style?: "default" | "radio"; - responses?: string[]; - correct?: number; - feedback?: { correct: string; incorrect: string }; - input_timeout?: number; - input_schema?: { - select: string | null; - next: string | null; - previous: string | null; - }; - confirm_continue?: boolean; -}; - -declare type LoopNode = { - // Loop nodes - timeline?: any[]; - conditional_function?: () => boolean; -}; - -declare type InputNode = { - // HTML input plugin - preamble?: string; - html?: string; -}; - -declare type IntentionsNode = { - // Basic trial identification - trial_number: number; // Trial number/identifier - display: Display; // Screen type to display (e.g., "playerChoice", "loading", etc.) - - // Game option parameters (used by Trial screen) - optionOneParticipant: number; // Points for participant in Option 1 - optionOnePartner: number; // Points for partner in Option 1 - optionTwoParticipant: number; // Points for participant in Option 2 - optionTwoPartner: number; // Points for partner in Option 2 - - // Partner type information - typeOne: string; // Partner type for Option 1 - typeTwo: string; // Partner type for Option 2 - - // Avatar selection (used by SelectAvatar screen) - avatar: 0; // Avatar index (currently hardcoded to 0) - - // Game logic - answer: Options; // Correct answer for the trial ("Option 1" or "Option 2") - isPractice: boolean; // Whether this is a practice trial - - // Loading screen configuration (used by Loading screen) - state?: "matchingIntentions" | "matchingCyberball" | "social" | "default"; // Type of loading: "matchingIntentions" (partner matching), "matchingCyberball" (cyberball partners), "social" (status generation), or "default" (generic loading) - runComputeSetup?: boolean; // Whether to run WebR setup - runComputeOperation?: boolean; // Whether to fetch data from server (only used when state is "matchingIntentions") - - // Status preview screen configuration (used by StatusPreview screen) - nextPhase?: "cyberball" | "phaseOne" | "phaseTwo" | "phaseThree"; - isPreviewPartnerHighStatus?: boolean; // Used for the `StatusPreview` screen - - // Waiting screen configuration (used by Waiting screen) - mode: "facilitator" | "mri"; - - // Spotlight configuration (used by Trial screen) - spotlight?: { - enabled: boolean; - target: "status" | "options" | "none"; - message: string; - }; - - // Cyberball screen - isInclusive?: boolean; - isCyberballPartnerHighStatus?: boolean; - probabilities?: { - inclusion: number; - exclusion: { - partnerA: number; - partnerB: number; - }; - }; -}; - -// Timeline collection type -declare type Timeline = TimelineNode[]; - -// Timeline node type, representing different timeline -// element parameter types -declare type TimelineNode = GenericNode & ( - | FullscreenNode - | InstructionsNode - | ComprehensionNode - | LoopNode - | InputNode - | IntentionsNode -); diff --git a/types/props.d.ts b/types/props.d.ts deleted file mode 100644 index e1ad65f..0000000 --- a/types/props.d.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * @file Prop declarations for React components and screens. - * @author Henry Burgess - */ -// Grommet imports for styling components -import { BorderType } from "grommet/utils"; - -// Props for 'Components' -declare namespace Components { - // Wrapper component - type Wrapper = { - display: Display; - props: - | Screens.Loaded - | Screens.Loading - | Screens.Trial - | Screens.SelectAvatar - | Screens.Inference - | Screens.Agency - | Screens.Classification - | Screens.Status - | Screens.DASS - | Screens.Screentime - | Screens.Demographics - | Screens.Summary - | Screens.Waiting - | Screens.Cyberball - | Screens.Resources; - }; - - // Option component - type Option = { - optionKey: string; - optionName: string; - pointsParticipant: number; - pointsPartner: number; - }; - - // Card component - type Card = { - gridArea: string; - name: string; - points: string; - avatar: string; - }; - - // Slider component - type Slider = { - min: number; - max: number; - value: number; - setValue: (value: number) => void; - leftLabel: string; - rightLabel: string; - onChange?: () => any; - isFocused: boolean; - }; - - // Character component - type Character = { - size: number; - name: string; - state: string; - setState: (avatar: string) => void; - border?: BorderType; - }; - - // Status componet - type Status = { - participantStatus: number; - partnerStatus: number; - isPractice?: boolean; - hidePartner?: boolean; - }; -} - -// Props for 'Screens' -declare namespace Screens { - type GenericScreenProps = { - trial_number: number; - display: Display; - }; - - // Loaded screen - type Loaded = GenericScreenProps & { - state: "matchingIntentions" | "matchingCyberball" | "social"; - handler: () => void; - }; - - // Loading screen - type Loading = GenericScreenProps & { - state: "matchingIntentions" | "matchingCyberball" | "social" | "default"; - runComputeSetup?: boolean; - runComputeOperation?: boolean; - handler?: ( - storeParameters: boolean, - participantParameters: number[], - partnerParameters: number[], - setupDuration: number, - operationDuration: number - ) => void; - }; - - // Trial screen - type Trial = GenericScreenProps & { - isPractice: boolean; - participantPoints: number; - partnerPoints: number; - options: Points; - answer: Options; - spotlight?: { - enabled: boolean; - target: "status" | "options" | "none"; - message: string; - }; - handler: ( - selection: Options, - points: { options: Points }, - answer: Options - ) => void; - }; - - // SelectAvatar screen - type SelectAvatar = GenericScreenProps & { - handler: (selectedIndex: number) => void; - }; - - // Inference screen - type Inference = GenericScreenProps & { - handler: (firstValue: number, secondValue: number) => void; - }; - - // Agency screen - type Agency = GenericScreenProps & { - handler: (firstValue: number) => void; - }; - - // Classification screen - type Classification = GenericScreenProps & { - handler: (classification: string) => void; - }; - - // Status screen - type Status = GenericScreenProps & { - handler: ( - closeFriends: number, - partyInvitations: number, - meanPeople: number, - socialMediaFollowers: number, - socialMediaFollowing: number - ) => void; - }; - - // Status preview screen - type StatusPreview = GenericScreenProps & { - nextPhase: "cyberball" | "phaseOne" | "phaseTwo" | "phaseThree"; - isPreviewPartnerHighStatus: boolean; - handler: () => void; - }; - - // DASS screen - type DASS = GenericScreenProps & { - version: "adult" | "adolescent"; - handler: (responses: number[]) => void; - }; - - // Screentime screen - type Screentime = GenericScreenProps & { - handler: (weekdayTime: number, weekendTime: number) => void; - }; - - // Demographics screen - type Demographics = GenericScreenProps & { - version: "adult" | "adolescent"; - handler: ( - age: number, - genderIdentity: string, - ethnicity: string, - householdIncome: string, - education: string, - socialMediaDaily: boolean, - socialMediaPlatforms: string, - ) => void; - }; - - // Summary screen - type Summary = GenericScreenProps & { - postPhase: Display; - handler: () => void; - }; - - // Waiting screen - type Waiting = GenericScreenProps & { - mode: "facilitator" | "mri"; - handler: () => void; - }; - - // Resources screen - type Resources = GenericScreenProps & { - handler: () => void; - }; - - // Cyberball screen - type Cyberball = GenericScreenProps & { - isInclusive: boolean; // Operate exclusively as inclusive or exclusive - isCyberballPartnerHighStatus: boolean; // Partner A is high status or low status - probabilities: { - inclusion: number; // Probability of partners passing to participant - exclusion: { - partnerA: number; // Probability of partner A passing to participant - partnerB: number; // Probability of partner B passing to participant - }; - }; - handler: ( - tossCount: number, - participantTossCount: number, - participantCatchCount: number - ) => void; - }; -} diff --git a/types/window.d.ts b/types/window.d.ts deleted file mode 100644 index 37a4889..0000000 --- a/types/window.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * @file Extension of the 'Window' interface. - * @author Henry Burgess - */ - -// 'Experiment' jsPsych wrapper library -import { Experiment } from "neurocog"; - -// Compute class -import Compute from "src/classes/Compute"; - -// Add 'Experiment' to the global Window interface -declare global { - interface Window { - Experiment: Experiment; - Compute: Compute; - } -} From e840834d68166869af6efcfee07dabd9ef050e6d Mon Sep 17 00:00:00 2001 From: Henry Burgess Date: Thu, 26 Feb 2026 15:26:03 -0600 Subject: [PATCH 03/15] INT-83 Update imports in all files --- src/classes/Compute.ts | 4 +- src/classes/Handler.ts | 67 ++++++--- src/classes/factories/ScreenPropFactory.ts | 5 + src/flags.ts | 6 +- src/index.tsx | 133 ++++++++---------- src/plugin.ts | 19 ++- src/util.ts | 7 +- src/view/components/Card/index.tsx | 4 +- src/view/components/Character/index.tsx | 4 +- src/view/components/Option/index.tsx | 4 +- src/view/components/Slider/index.tsx | 4 +- src/view/components/Status/index.tsx | 4 +- src/view/components/Wrapper/index.tsx | 36 ++--- src/view/index.tsx | 8 +- src/view/screens/Cyberball/index.tsx | 4 +- src/view/screens/End/index.tsx | 1 - src/view/screens/Loaded/index.tsx | 4 +- src/view/screens/Loading/index.tsx | 7 +- .../screens/Questionnaires/Agency/index.tsx | 41 +++--- .../Questionnaires/Classification/index.tsx | 58 ++++---- .../screens/Questionnaires/DASS/index.tsx | 4 +- .../Questionnaires/Demographics/index.tsx | 4 +- .../Questionnaires/Inference/index.tsx | 37 +++-- .../Questionnaires/Screentime/index.tsx | 4 +- .../screens/Questionnaires/Status/index.tsx | 4 +- src/view/screens/Resources/index.tsx | 4 +- src/view/screens/SelectAvatar/index.tsx | 12 +- src/view/screens/StatusPreview/index.tsx | 4 +- src/view/screens/Summary/index.tsx | 10 +- src/view/screens/Trial/index.tsx | 43 +++--- src/view/screens/Waiting/index.tsx | 11 +- webpack.common.js | 2 +- 32 files changed, 318 insertions(+), 241 deletions(-) diff --git a/src/classes/Compute.ts b/src/classes/Compute.ts index 7931bd4..9c1df7d 100644 --- a/src/classes/Compute.ts +++ b/src/classes/Compute.ts @@ -12,6 +12,8 @@ * * @author Henry Burgess */ +// Custom types +import type { ModelResponse } from "types"; // Logging library import consola from "consola"; @@ -1047,7 +1049,7 @@ class Compute { // Get the participant parameters const participantParameters = data[0].values; - // Get the parenter parameters, convert string to two floats + // Get the partner parameters, convert string to two floats const partnerParameters = [ ...data[1].values[0].split(" ").map((value: string) => parseFloat(value)), ]; diff --git a/src/classes/Handler.ts b/src/classes/Handler.ts index 9b69af1..8d186cd 100644 --- a/src/classes/Handler.ts +++ b/src/classes/Handler.ts @@ -14,6 +14,8 @@ * * @author Henry Burgess */ +// Custom types +import type { Dataframe, Options, Points } from "types"; // Logging library import consola from "consola"; @@ -143,10 +145,11 @@ class Handler { /** * Handler called after status questions completed - * @param {number} followers number of followers - * @param {number} averageLikes average number of likes - * @param {number} friends number of friends - * @param {number} socialCloseness social closeness rating + * @param {number} closeFriends number of close friends + * @param {number} partyInvitations number of party invitations + * @param {number} meanPeople number of mean people + * @param {number} socialMediaFollowers number of social media followers + * @param {number} socialMediaFollowing number of social media following */ public status( closeFriends: number, @@ -219,15 +222,33 @@ class Handler { // Store social status const experiment = window.Experiment; - this.dataframe.participantDefaultStatus = experiment.getState().get("participantDefaultStatus"); - this.dataframe.partnerCyberballLowStatus = experiment.getState().get("partnerCyberballLowStatus"); - this.dataframe.partnerCyberballHighStatus = experiment.getState().get("partnerCyberballHighStatus"); - this.dataframe.partnerOneLowStatus = experiment.getState().get("partnerOneLowStatus"); - this.dataframe.partnerOneHighStatus = experiment.getState().get("partnerOneHighStatus"); - this.dataframe.partnerTwoLowStatus = experiment.getState().get("partnerTwoLowStatus"); - this.dataframe.partnerTwoHighStatus = experiment.getState().get("partnerTwoHighStatus"); - this.dataframe.partnerThreeLowStatus = experiment.getState().get("partnerThreeLowStatus"); - this.dataframe.partnerThreeHighStatus = experiment.getState().get("partnerThreeHighStatus"); + this.dataframe.participantDefaultStatus = experiment + .getState() + .get("participantDefaultStatus"); + this.dataframe.partnerCyberballLowStatus = experiment + .getState() + .get("partnerCyberballLowStatus"); + this.dataframe.partnerCyberballHighStatus = experiment + .getState() + .get("partnerCyberballHighStatus"); + this.dataframe.partnerOneLowStatus = experiment + .getState() + .get("partnerOneLowStatus"); + this.dataframe.partnerOneHighStatus = experiment + .getState() + .get("partnerOneHighStatus"); + this.dataframe.partnerTwoLowStatus = experiment + .getState() + .get("partnerTwoLowStatus"); + this.dataframe.partnerTwoHighStatus = experiment + .getState() + .get("partnerTwoHighStatus"); + this.dataframe.partnerThreeLowStatus = experiment + .getState() + .get("partnerThreeLowStatus"); + this.dataframe.partnerThreeHighStatus = experiment + .getState() + .get("partnerThreeHighStatus"); // Finish trial this.callback(); @@ -276,14 +297,26 @@ class Handler { socialMediaDaily: boolean, socialMediaPlatforms: string ): void { - consola.debug("Demographics responses:", age, genderIdentity, ethnicity, householdIncome, education, socialMediaDaily, socialMediaPlatforms); + consola.debug( + "Demographics responses:", + age, + genderIdentity, + ethnicity, + householdIncome, + education, + socialMediaDaily, + socialMediaPlatforms + ); this.dataframe.questionnaireResponsesDemographicsAge = age; this.dataframe.questionnaireResponsesDemographicsGender = genderIdentity; this.dataframe.questionnaireResponsesDemographicsEthnicity = ethnicity; - this.dataframe.questionnaireResponsesDemographicsHouseholdIncome = householdIncome; + this.dataframe.questionnaireResponsesDemographicsHouseholdIncome = + householdIncome; this.dataframe.questionnaireResponsesDemographicsEducation = education; - this.dataframe.questionnaireResponsesDemographicsSocialMediaDaily = socialMediaDaily; - this.dataframe.questionnaireResponsesDemographicsSocialMediaPlatforms = socialMediaPlatforms; + this.dataframe.questionnaireResponsesDemographicsSocialMediaDaily = + socialMediaDaily; + this.dataframe.questionnaireResponsesDemographicsSocialMediaPlatforms = + socialMediaPlatforms; this.callback(); } diff --git a/src/classes/factories/ScreenPropFactory.ts b/src/classes/factories/ScreenPropFactory.ts index 93194b2..f359d91 100644 --- a/src/classes/factories/ScreenPropFactory.ts +++ b/src/classes/factories/ScreenPropFactory.ts @@ -10,6 +10,11 @@ * * @author Henry Burgess */ +// Custom types +import type { Display, Factory, IntentionsNode, ScreenProps } from "types"; + +// Declare jsPsych +declare const jsPsych: never; // Logging library import consola from "consola"; diff --git a/src/flags.ts b/src/flags.ts index ac3c555..4acb947 100644 --- a/src/flags.ts +++ b/src/flags.ts @@ -7,12 +7,14 @@ * * @author Henry Burgess */ +// Import React +import React from "react"; // Logging library import consola from "consola"; // Configuration -import { Configuration } from "./configuration"; +import { Configuration } from "src/configuration"; /** * Feature flag utility class for type-safe feature checking @@ -24,7 +26,7 @@ export class Flags { * @returns boolean indicating if the feature is enabled */ static isEnabled(featureName: keyof typeof Configuration.features): boolean { - return Configuration.features[featureName] === true; + return Configuration.features[featureName]; } /** diff --git a/src/index.tsx b/src/index.tsx index a611592..69cd1ec 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,35 +13,40 @@ * - React-to-HTML conversion for instruction screens * * The experiment uses jsPsych for trial management and the `neurocog` - * crossplatform API for data handling. Each phase includes practice + * cross-platform API for data handling. Each phase includes practice * trials, attention checks, and detailed instructions. The timeline * supports both standard and alternate input modes. * * @author Henry Burgess */ - // React import React from "react"; // Grommet UI components import { Box, Grommet, Heading, Paragraph } from "grommet"; +// Custom types +import type { Row, Timeline } from "types"; + +// Declare jsPsych +declare const jsPsych: never; + // Feature flags -import { Flags } from "./flags"; +import { Flags } from "src/flags"; // Configuration -import { Configuration } from "./configuration"; +import { Configuration } from "src/configuration"; // Import data spreadsheets -import Default from "../data/default.csv"; -import Test from "../data/test.csv"; +import Default from "data/default.csv"; +import Test from "data/test.csv"; // Utility functions -import { generateStatuses, initializeLocalStorage, react2html } from "./util"; +import { generateStatuses, initializeLocalStorage, react2html } from "src/util"; import { shuffle } from "d3-array"; // Custom input bindings -import { BINDINGS } from "./bindings"; +import { BINDINGS } from "src/bindings"; // Logging library import consola from "consola"; @@ -49,11 +54,11 @@ import consola from "consola"; // Generate unique identifiers import { v4 as uuidv4 } from "uuid"; -// Import crossplatform API +// Import cross-platform API import { Experiment } from "neurocog"; // Import Compute class -import Compute from "./classes/Compute"; +import Compute from "src/classes/Compute"; // Import jsPsych plugins import "jspsych/plugins/jspsych-fullscreen"; @@ -62,7 +67,7 @@ import "jspsych/plugins/jspsych-survey-html-form"; import "jspsych-attention-check"; // Import the custom plugin before adding it to the timeline -import "./plugin"; +import "src/plugin"; // Create a new Experiment instance const experiment = new Experiment(Configuration); @@ -86,7 +91,7 @@ const handleSignal = () => { .set("signalTimestamps", [...collectedSignals, Date.now()]); }; -// Setup the signal listener +// Set up the signal listener document.addEventListener("keydown", event => { if (event.repeat) return; if (event.key === BINDINGS.SIGNAL) { @@ -123,7 +128,7 @@ experiment.getState().set("partnerTwoHighStatus", partnerTwoHighStatus); experiment.getState().set("partnerThreeLowStatus", partnerThreeLowStatus); experiment.getState().set("partnerThreeHighStatus", partnerThreeHighStatus); -// Setup the Compute instance +// Set up the Compute instance window.Compute = new Compute(); // Timeline setup @@ -135,7 +140,7 @@ for (let i = 0; i < Configuration.avatars.names.partner.length; i++) { Configuration.avatars.names.partner[i] = `${partner} ${performance.now()}`; } -if (Configuration.manipulations.requireID === true) { +if (Configuration.manipulations.requireID) { timeline.push({ type: "survey-html-form", preamble: `

Please enter the 8 digit participant LUID.

`, @@ -162,7 +167,7 @@ timeline.push({ }); // Add controls instructions first if using alternate input scheme -if (Configuration.manipulations.useButtonInput === true) { +if (Configuration.manipulations.useButtonInput) { timeline.push({ type: "instructions", pages: [ @@ -221,7 +226,7 @@ if (Configuration.manipulations.useButtonInput === true) { } // Add the first set of instructions -if (Configuration.manipulations.enableCyberball === true) { +if (Configuration.manipulations.enableCyberball) { // Cyberball enabled, first Cyberball then Intentions Game timeline.push({ type: "instructions", @@ -350,10 +355,10 @@ timeline.push({ // If the status display is enabled, check if the flags need to be updated to match the manipulations if ( - Configuration.manipulations.enableStatusPhaseOne === true || - Configuration.manipulations.enableStatusPhaseTwo === true || - Configuration.manipulations.enableStatusPhaseThree === true || - Configuration.manipulations.enableCyberball === true + Configuration.manipulations.enableStatusPhaseOne || + Configuration.manipulations.enableStatusPhaseTwo || + Configuration.manipulations.enableStatusPhaseThree || + Configuration.manipulations.enableCyberball ) { // Enable the status display and the questionnaire globally Flags.setValue("enableStatusDisplay", true); @@ -383,7 +388,7 @@ Flags.setValue( ); // Insert the status questionnaire if enabled -if (Flags.isEnabled("enableQuestionnaireStatus") === true) { +if (Flags.isEnabled("enableQuestionnaireStatus")) { timeline.push({ type: "instructions", pages: [ @@ -463,7 +468,7 @@ if (Flags.isEnabled("enableQuestionnaireStatus") === true) { } // Introduce the status display if enabled -if (Flags.isEnabled("enableStatusDisplay") === true) { +if (Flags.isEnabled("enableStatusDisplay")) { timeline.push({ type: Configuration.studyName, optionOneParticipant: 5, @@ -485,7 +490,7 @@ if (Flags.isEnabled("enableStatusDisplay") === true) { } // Add Cyberball screen if enabled -if (Configuration.manipulations.enableCyberball === true) { +if (Configuration.manipulations.enableCyberball) { timeline.push({ type: Configuration.studyName, display: "loading", @@ -613,7 +618,7 @@ const intentionsInstructions = [ At the end of all the stages you will be shown a summary of how many points you and your partner accumulated during that phase. - {Configuration.manipulations.enableEndingQuestionnaires === true && ( + {Configuration.manipulations.enableEndingQuestionnaires && ( There will be some extra questionnaires after completing all the stages. @@ -775,15 +780,15 @@ timeline.push({ }, input_schema: { select: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.SELECT : null, next: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.NEXT : null, previous: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.PREVIOUS : null, }, @@ -805,11 +810,7 @@ timeline.push({ conditional_function: () => { // Check if the response from the previous trial was correct const data = jsPsych.data.get().last(1).values()[0]; - if (data.attentionCorrect === true) { - return false; - } else { - return true; - } + return !data.attentionCorrect; }, }); @@ -828,15 +829,15 @@ timeline.push({ }, input_schema: { select: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.SELECT : null, next: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.NEXT : null, previous: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.PREVIOUS : null, }, @@ -858,11 +859,7 @@ timeline.push({ conditional_function: () => { // Check if the response from the previous trial was correct const data = jsPsych.data.get().last(1).values()[0]; - if (data.attentionCorrect === true) { - return false; - } else { - return true; - } + return !data.attentionCorrect; }, }); @@ -912,7 +909,7 @@ timeline.push({ }); // Insert `statusPreview` screen if the participant will be shown their status -if (Configuration.manipulations.enableStatusPhaseOne === true) { +if (Configuration.manipulations.enableStatusPhaseOne) { timeline.push({ type: Configuration.studyName, display: "statusPreview", @@ -965,7 +962,7 @@ for (let i = 0; i < dataCollection.length; i++) { } // Insert Waiting screens before Phase 01 - if (Configuration.manipulations.useButtonInput === true) { + if (Configuration.manipulations.useButtonInput) { // Facilitator waiting timeline.push({ type: Configuration.studyName, @@ -1148,15 +1145,15 @@ for (let i = 0; i < dataCollection.length; i++) { }, input_schema: { select: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.SELECT : null, next: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.NEXT : null, previous: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.PREVIOUS : null, }, @@ -1178,11 +1175,7 @@ for (let i = 0; i < dataCollection.length; i++) { conditional_function: () => { // Check if the response from the previous trial was correct const data = jsPsych.data.get().last(1).values()[0]; - if (data.attentionCorrect === true) { - return false; - } else { - return true; - } + return !data.attentionCorrect; }, }); @@ -1204,15 +1197,15 @@ for (let i = 0; i < dataCollection.length; i++) { }, input_schema: { select: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.SELECT : null, next: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.NEXT : null, previous: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.PREVIOUS : null, }, @@ -1234,11 +1227,7 @@ for (let i = 0; i < dataCollection.length; i++) { conditional_function: () => { // Check if the response from the previous trial was correct const data = jsPsych.data.get().last(1).values()[0]; - if (data.attentionCorrect === true) { - return false; - } else { - return true; - } + return !data.attentionCorrect; }, }); @@ -1288,7 +1277,7 @@ for (let i = 0; i < dataCollection.length; i++) { }); // Insert `statusPreview` screen if the participant will be shown their status - if (Configuration.manipulations.enableStatusPhaseTwo === true) { + if (Configuration.manipulations.enableStatusPhaseTwo) { timeline.push({ type: Configuration.studyName, display: "statusPreview", @@ -1309,7 +1298,7 @@ for (let i = 0; i < dataCollection.length; i++) { } // Insert Waiting screens before Phase 02 - if (Configuration.manipulations.useButtonInput === true) { + if (Configuration.manipulations.useButtonInput) { // Facilitator waiting timeline.push({ type: Configuration.studyName, @@ -1397,15 +1386,15 @@ for (let i = 0; i < dataCollection.length; i++) { }, input_schema: { select: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.SELECT : null, next: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.NEXT : null, previous: - Configuration.manipulations.useButtonInput === true + Configuration.manipulations.useButtonInput ? BINDINGS.PREVIOUS : null, }, @@ -1427,11 +1416,7 @@ for (let i = 0; i < dataCollection.length; i++) { conditional_function: () => { // Check if the response from the previous trial was correct const data = jsPsych.data.get().last(1).values()[0]; - if (data.attentionCorrect === true) { - return false; - } else { - return true; - } + return !data.attentionCorrect; }, }); @@ -1481,7 +1466,7 @@ for (let i = 0; i < dataCollection.length; i++) { }); // Insert `statusPreview` screen if the participant will be shown their status - if (Configuration.manipulations.enableStatusPhaseThree === true) { + if (Configuration.manipulations.enableStatusPhaseThree) { timeline.push({ type: Configuration.studyName, display: "statusPreview", @@ -1565,7 +1550,7 @@ for (const trial of stageThreeTrials) { } // Insert Waiting screens before Phase 03 -if (Configuration.manipulations.useButtonInput === true) { +if (Configuration.manipulations.useButtonInput) { // Facilitator waiting timeline.push({ type: Configuration.studyName, @@ -1596,9 +1581,9 @@ timeline.push({ }); // Extra questionnaires -if (Configuration.manipulations.enableEndingQuestionnaires === true) { +if (Configuration.manipulations.enableEndingQuestionnaires) { // Screen time questionnaire - if (Flags.isEnabled("enableQuestionnaireScreentime") === true) { + if (Flags.isEnabled("enableQuestionnaireScreentime")) { timeline.push({ type: Configuration.studyName, display: "screentime", @@ -1606,7 +1591,7 @@ if (Configuration.manipulations.enableEndingQuestionnaires === true) { } // DASS screen - if (Flags.isEnabled("enableQuestionnaireDASS") === true) { + if (Flags.isEnabled("enableQuestionnaireDASS")) { timeline.push({ type: Configuration.studyName, display: "dass", @@ -1614,7 +1599,7 @@ if (Configuration.manipulations.enableEndingQuestionnaires === true) { } // Demographics screen - if (Flags.isEnabled("enableQuestionnaireDemographics") === true) { + if (Flags.isEnabled("enableQuestionnaireDemographics")) { timeline.push({ type: Configuration.studyName, display: "demographics", diff --git a/src/plugin.ts b/src/plugin.ts index 40c28ca..a7cc8ca 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -9,19 +9,24 @@ * - Data collection and storage * - Trial completion handling * - * The plugin integrates with the `neurocog` crossplatform API and manages + * The plugin integrates with the `neurocog` cross-platform API and manages * experiment state, including signal timestamps and completion flags. * It supports both standard and alternate input modes, and handles * practice trials differently from experimental trials. * * @author Henry Burgess */ +// Declare jsPsych +declare const jsPsych: never; + +// Custom types +import type { Dataframe, IntentionsNode } from "types"; // Logging library import consola from "consola"; // Configuration -import { Configuration } from "./configuration"; +import { Configuration } from "src/configuration"; // Core modules import ScreenPropFactory from "src/classes/factories/ScreenPropFactory"; @@ -29,7 +34,7 @@ import View from "src/view"; import Handler from "src/classes/Handler"; // Utility functions -import { saveToLocalStorage, setCompleted } from "./util"; +import { saveToLocalStorage, setCompleted } from "src/util"; jsPsych.plugins[Configuration.studyName] = (() => { const plugin = { @@ -138,10 +143,10 @@ jsPsych.plugins[Configuration.studyName] = (() => { // Trial state selectedOption_player: NaN, // option selected by participant realAnswer: trial.answer, - correctGuess: NaN, // whether or not the participant guessed correctly + correctGuess: NaN, // whether the participant guessed correctly trialDuration: NaN, // duration of the trial in ms setupDuration: NaN, // duration of the setup operation in ms - operationDuration: NaN, // duration of the operation operation in ms + operationDuration: NaN, // duration of the operation in ms // Model parameters server_alpha_ppt: NaN, // parameters generated by server @@ -183,7 +188,7 @@ jsPsych.plugins[Configuration.studyName] = (() => { return false; }); - if (Configuration.manipulations.requireID === true) { + if (Configuration.manipulations.requireID) { // Determine the ID of the participant const inputData = jsPsych.data .get() @@ -194,7 +199,7 @@ jsPsych.plugins[Configuration.studyName] = (() => { const validInputID = inputData.length === 8 && /[0-9]{8}/.test(inputData); - if (validInputID === true) { + if (validInputID) { // Check first if a participant ID has been specified const givenID = jsPsych.data .get() diff --git a/src/util.ts b/src/util.ts index 8d74d52..26e3483 100644 --- a/src/util.ts +++ b/src/util.ts @@ -15,6 +15,9 @@ import { ReactElement } from "react"; import { renderToString } from "react-dom/server"; +// Custom types +import type { BackupStorage, Display } from "types"; + // File saving import FileSaver from "file-saver"; @@ -25,10 +28,10 @@ import consola from "consola"; import { randomUniform } from "d3-random"; // Experiment configuration -import { Configuration } from "./configuration"; +import { Configuration } from "src/configuration"; // Feature flags -import { Flags } from "./flags"; +import { Flags } from "src/flags"; /** * Calculate the points gained from all prior trials of a specific display type diff --git a/src/view/components/Card/index.tsx b/src/view/components/Card/index.tsx index 6af854c..8153df3 100644 --- a/src/view/components/Card/index.tsx +++ b/src/view/components/Card/index.tsx @@ -10,13 +10,15 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement } from "react"; // Grommet UI components import { Box, Heading } from "grommet"; +// Custom types +import type { AvatarStyles, Components } from "types"; + // Other imports import TextTransition, { presets } from "react-text-transition"; import Avatar from "boring-neutral-avatars"; diff --git a/src/view/components/Character/index.tsx b/src/view/components/Character/index.tsx index 9259a0d..34a001c 100644 --- a/src/view/components/Character/index.tsx +++ b/src/view/components/Character/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement } from "react"; @@ -18,6 +17,9 @@ import React, { FC, ReactElement } from "react"; import Avatar from "boring-neutral-avatars"; import { Box } from "grommet"; +// Custom types +import { AvatarStyles, Components } from "types"; + // Configuration import { Configuration } from "src/configuration"; diff --git a/src/view/components/Option/index.tsx b/src/view/components/Option/index.tsx index 8904c81..a576f4c 100644 --- a/src/view/components/Option/index.tsx +++ b/src/view/components/Option/index.tsx @@ -10,13 +10,15 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement } from "react"; // Grommet UI components import { Grid, Heading } from "grommet"; +// Custom types +import type { Components } from "types"; + /** * @summary Generate an 'Option' component that displays a grid of points to split between participant and partner * @param {Components.Option} props component props containing: diff --git a/src/view/components/Slider/index.tsx b/src/view/components/Slider/index.tsx index 758c2b9..47ea2d9 100644 --- a/src/view/components/Slider/index.tsx +++ b/src/view/components/Slider/index.tsx @@ -10,13 +10,15 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useEffect, useState } from "react"; // Grommet UI components import { Box, Heading, RangeInput } from "grommet"; +// Custom types +import type { Components } from "types"; + /** * @summary Generate a 'Slider' component that displays a horizontal range input with labels * @param {Components.Slider} props component props containing: diff --git a/src/view/components/Status/index.tsx b/src/view/components/Status/index.tsx index 3ee4c3e..6a46434 100644 --- a/src/view/components/Status/index.tsx +++ b/src/view/components/Status/index.tsx @@ -6,7 +6,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -16,6 +15,9 @@ import { Box, Text } from "grommet"; // Avatar component import Avatar from "boring-neutral-avatars"; +// Custom types +import type { AvatarStyles, Components } from "types"; + // Configuration import { Configuration } from "src/configuration"; diff --git a/src/view/components/Wrapper/index.tsx b/src/view/components/Wrapper/index.tsx index 1f36c6c..a7f70e1 100644 --- a/src/view/components/Wrapper/index.tsx +++ b/src/view/components/Wrapper/index.tsx @@ -10,13 +10,15 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useEffect, useState } from "react"; // Grommet component import { Grommet, ThemeContext } from "grommet"; +// Custom types +import type { Components, Screens } from "types"; + // Import styling import "src/scss/styles.scss"; @@ -24,23 +26,23 @@ import "src/scss/styles.scss"; import { Theme } from "src/theme"; // Screens used throughout the task -import Agency from "../../screens/Questionnaires/Agency"; -import Trial from "../../screens/Trial"; -import Inference from "../../screens/Questionnaires/Inference"; -import Classification from "../../screens/Questionnaires/Classification"; -import SelectAvatar from "../../screens/SelectAvatar"; -import Status from "../../screens/Questionnaires/Status"; -import DASS from "../../screens/Questionnaires/DASS"; -import Screentime from "../../screens/Questionnaires/Screentime"; -import Demographics from "../../screens/Questionnaires/Demographics"; -import Loading from "../../screens/Loading"; -import Loaded from "../../screens/Loaded"; +import Agency from "src/view/screens/Questionnaires/Agency"; +import Trial from "src/view/screens/Trial"; +import Inference from "src/view/screens/Questionnaires/Inference"; +import Classification from "src/view/screens/Questionnaires/Classification"; +import SelectAvatar from "src/view/screens/SelectAvatar"; +import Status from "src/view/screens/Questionnaires/Status"; +import DASS from "src/view/screens/Questionnaires/DASS"; +import Screentime from "src/view/screens/Questionnaires/Screentime"; +import Demographics from "src/view/screens/Questionnaires/Demographics"; +import Loading from "src/view/screens/Loading"; +import Loaded from "src/view/screens/Loaded"; import StatusPreview from "src/view/screens/StatusPreview"; -import Waiting from "../../screens/Waiting"; -import End from "../../screens/End"; -import Resources from "../../screens/Resources"; -import Summary from "../../screens/Summary"; -import Cyberball from "../../screens/Cyberball"; +import Waiting from "src/view/screens/Waiting"; +import End from "src/view/screens/End"; +import Resources from "src/view/screens/Resources"; +import Summary from "src/view/screens/Summary"; +import Cyberball from "src/view/screens/Cyberball"; /** * @summary Generate a 'Wrapper' component that acts as a container for all React screens and components, diff --git a/src/view/index.tsx b/src/view/index.tsx index 1076324..33b0c04 100644 --- a/src/view/index.tsx +++ b/src/view/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React from "react"; import { createRoot, Root } from "react-dom/client"; @@ -18,8 +17,11 @@ import { createRoot, Root } from "react-dom/client"; // Foundational 'Wrapper' component import Wrapper from "src/view/components/Wrapper"; +// Custom types +import type { Display, ScreenProps } from "types"; + /** - * @summary 'View' class to abstract the display and clean-up of React-based screens + * @summary 'View' class to abstract the display and clean up of React-based screens * used in the game */ class View { @@ -55,7 +57,7 @@ class View { // Render the 'Wrapper' component this.root.render(); - // Setup a timeout to execute the callback + // Set up a timeout to execute the callback if (props.duration > 0) { setTimeout(() => { props.callback(); diff --git a/src/view/screens/Cyberball/index.tsx b/src/view/screens/Cyberball/index.tsx index bb8f74d..3c2d775 100644 --- a/src/view/screens/Cyberball/index.tsx +++ b/src/view/screens/Cyberball/index.tsx @@ -11,7 +11,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useRef, useReducer, useState } from "react"; @@ -19,6 +18,9 @@ import React, { FC, ReactElement, useRef, useReducer, useState } from "react"; import { Box, Text } from "grommet"; import Avatar from "boring-neutral-avatars"; +// Custom types +import type { AvatarStyles, CyberballGameState, Screens } from "types"; + // Configuration import { Configuration } from "src/configuration"; diff --git a/src/view/screens/End/index.tsx b/src/view/screens/End/index.tsx index 664bc76..6210ce3 100644 --- a/src/view/screens/End/index.tsx +++ b/src/view/screens/End/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement } from "react"; diff --git a/src/view/screens/Loaded/index.tsx b/src/view/screens/Loaded/index.tsx index 53f307a..7289b4f 100644 --- a/src/view/screens/Loaded/index.tsx +++ b/src/view/screens/Loaded/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement } from "react"; @@ -20,6 +19,9 @@ import { LinkNext } from "grommet-icons"; import Status from "src/view/components/Status"; import Avatar from "boring-neutral-avatars"; +// Custom types +import type { Screens } from "types"; + // Logging library import consola from "consola"; diff --git a/src/view/screens/Loading/index.tsx b/src/view/screens/Loading/index.tsx index c225b21..96f8c96 100644 --- a/src/view/screens/Loading/index.tsx +++ b/src/view/screens/Loading/index.tsx @@ -11,10 +11,15 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useEffect, useState } from "react"; +// Custom types +import type { Dataframe, Screens } from "types"; + +// Declare jsPsych +declare const jsPsych: never; + // Logging library import consola from "consola"; diff --git a/src/view/screens/Questionnaires/Agency/index.tsx b/src/view/screens/Questionnaires/Agency/index.tsx index 1fa5a3b..e69e883 100644 --- a/src/view/screens/Questionnaires/Agency/index.tsx +++ b/src/view/screens/Questionnaires/Agency/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -21,6 +20,9 @@ import { LinkNext } from "grommet-icons"; // Custom components import Slider from "src/view/components/Slider"; +// Custom types +import type { Screens } from "types"; + // Experiment configuration import { Configuration } from "src/configuration"; @@ -57,26 +59,24 @@ const Agency: FC = ( */ const inputHandler = (event: React.KeyboardEvent) => { // Disable keyboard input if not enabled in configuration - if (Configuration.manipulations.useButtonInput === false) return; + if (!Configuration.manipulations.useButtonInput) return; // Avoid holding the key down if no element focused - if (elementFocused === false && event.repeat) return; + if (!elementFocused && event.repeat) return; event.preventDefault(); if ( event.key.toString() === BINDINGS.NEXT || event.key.toString() === BINDINGS.PREVIOUS ) { - if (elementFocused === true) { - if (selectedElementIndex === 0) { - // First slider, increase and decrease value within bounds when keys pressed - if (event.key.toString() === BINDINGS.NEXT) { - setSliderValue(sliderValue + 1 <= 100 ? sliderValue + 1 : 100); - } else if (event.key.toString() === BINDINGS.PREVIOUS) { - setSliderValue(sliderValue - 1 >= 0 ? sliderValue - 1 : 0); - } - setSliderMoved(true); + if (elementFocused && selectedElementIndex === 0) { + // First slider, increase and decrease value within bounds when keys pressed + if (event.key.toString() === BINDINGS.NEXT) { + setSliderValue(sliderValue + 1 <= 100 ? sliderValue + 1 : 100); + } else if (event.key.toString() === BINDINGS.PREVIOUS) { + setSliderValue(sliderValue - 1 >= 0 ? sliderValue - 1 : 0); } + setSliderMoved(true); } else { if (event.key.toString() === BINDINGS.NEXT) { setSelectedElementIndex( @@ -92,11 +92,9 @@ const Agency: FC = ( if (selectedElementIndex !== 1) { // Focus or unfocus sliders setElementFocused(!elementFocused); - } else { + } else if (sliderMoved) { // Select the `Continue` button if permitted - if (sliderMoved === true) { - props.handler(sliderValue); - } + props.handler(sliderValue); } } }; @@ -121,7 +119,7 @@ const Agency: FC = ( pad={"xsmall"} border={{ color: - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 0 && !elementFocused ? "selectedElement" @@ -149,14 +147,14 @@ const Agency: FC = ( pad={"none"} border={{ color: - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 1 ? "selectedElement" : "transparent", size: "large", }} style={ - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 1 ? { borderRadius: "36px " } : {} @@ -167,10 +165,7 @@ const Agency: FC = ( primary color="button" label="Continue" - disabled={ - // Disabled until slider has been interacted with - sliderMoved === false - } + disabled={!sliderMoved} // Disabled until slider has been interacted with icon={} reverse onClick={() => { diff --git a/src/view/screens/Questionnaires/Classification/index.tsx b/src/view/screens/Questionnaires/Classification/index.tsx index 53e2767..900da1d 100644 --- a/src/view/screens/Questionnaires/Classification/index.tsx +++ b/src/view/screens/Questionnaires/Classification/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -18,6 +17,9 @@ import React, { FC, ReactElement, useState } from "react"; import { Box, Button, Paragraph, RadioButtonGroup, Keyboard } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + // Experiment configuration import { Configuration } from "src/configuration"; @@ -56,7 +58,7 @@ const Classification: FC = ( */ const inputHandler = (event: React.KeyboardEvent) => { // Disable keyboard input if not enabled in configuration - if (Configuration.manipulations.useButtonInput === false) return; + if (!Configuration.manipulations.useButtonInput) return; // Avoid holding the key down if no element focused if (event.repeat) return; @@ -66,29 +68,27 @@ const Classification: FC = ( event.key.toString() === BINDINGS.NEXT || event.key.toString() === BINDINGS.PREVIOUS ) { - if (elementFocused === true) { - if (selectedElementIndex === 0) { - let updatedIndex = classificationIndex; - // Radio button group - if (classification === "") { - updatedIndex = 0; - } else if (event.key.toString() === BINDINGS.PREVIOUS) { - updatedIndex = - classificationIndex - 1 < 0 ? 0 : classificationIndex - 1; - } else if (event.key.toString() === BINDINGS.NEXT) { - updatedIndex = - classificationIndex + 1 > partners.length - 1 - ? classificationIndex - : classificationIndex + 1; - } + if (elementFocused && selectedElementIndex === 0) { + let updatedIndex = classificationIndex; + // Radio button group + if (classification === "") { + updatedIndex = 0; + } else if (event.key.toString() === BINDINGS.PREVIOUS) { + updatedIndex = + classificationIndex - 1 < 0 ? 0 : classificationIndex - 1; + } else if (event.key.toString() === BINDINGS.NEXT) { + updatedIndex = + classificationIndex + 1 > partners.length - 1 + ? classificationIndex + : classificationIndex + 1; + } - // Enable the continue button - setContinueDisabled(false); + // Enable the continue button + setContinueDisabled(false); - // Update state - setClassification(partners[updatedIndex]); - setClassificationIndex(updatedIndex); - } + // Update state + setClassification(partners[updatedIndex]); + setClassificationIndex(updatedIndex); } else { if (event.key.toString() === BINDINGS.NEXT) { setSelectedElementIndex( @@ -104,11 +104,9 @@ const Classification: FC = ( if (selectedElementIndex !== 1) { // Focus or unfocus sliders setElementFocused(!elementFocused); - } else { + } else if (!continueDisabled) { // Select the `Continue` button if permitted - if (!continueDisabled) { - props.handler(classification); - } + props.handler(classification); } } }; @@ -136,7 +134,7 @@ const Classification: FC = ( margin={"xsmall"} border={{ color: - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 0 && !elementFocused ? "selectedElement" @@ -166,14 +164,14 @@ const Classification: FC = ( pad={"none"} border={{ color: - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 1 ? "selectedElement" : "transparent", size: "large", }} style={ - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 1 ? { borderRadius: "36px " } : {} diff --git a/src/view/screens/Questionnaires/DASS/index.tsx b/src/view/screens/Questionnaires/DASS/index.tsx index 32f460f..0c5d37a 100644 --- a/src/view/screens/Questionnaires/DASS/index.tsx +++ b/src/view/screens/Questionnaires/DASS/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -26,6 +25,9 @@ import { } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + // Question sets const ADULT_QUESTIONS = [ "I found it hard to wind down", diff --git a/src/view/screens/Questionnaires/Demographics/index.tsx b/src/view/screens/Questionnaires/Demographics/index.tsx index 40fb78a..b2f6309 100644 --- a/src/view/screens/Questionnaires/Demographics/index.tsx +++ b/src/view/screens/Questionnaires/Demographics/index.tsx @@ -14,7 +14,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -31,6 +30,9 @@ import { } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + /** * @summary Generate a 'Demographics' screen component with multiple pages for collecting demographic information * @param {Screens.Demographics} props Component props containing: diff --git a/src/view/screens/Questionnaires/Inference/index.tsx b/src/view/screens/Questionnaires/Inference/index.tsx index 3d0c6e9..6a1baf1 100644 --- a/src/view/screens/Questionnaires/Inference/index.tsx +++ b/src/view/screens/Questionnaires/Inference/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -18,6 +17,9 @@ import React, { FC, ReactElement, useState } from "react"; import { Box, Button, Keyboard, Paragraph } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + // Custom components import Slider from "src/view/components/Slider"; @@ -64,17 +66,17 @@ const Inference: FC = ( */ const inputHandler = (event: React.KeyboardEvent) => { // Disable keyboard input if not enabled in configuration - if (Configuration.manipulations.useButtonInput === false) return; + if (!Configuration.manipulations.useButtonInput) return; // Avoid holding the key down if no element focused - if (elementFocused === false && event.repeat) return; + if (!elementFocused && event.repeat) return; event.preventDefault(); if ( event.key.toString() === BINDINGS.NEXT || event.key.toString() === BINDINGS.PREVIOUS ) { - if (elementFocused === true) { + if (elementFocused) { if (selectedElementIndex === 0) { // First slider, increase and decrease value within bounds when keys pressed if (event.key.toString() === BINDINGS.NEXT) { @@ -107,11 +109,9 @@ const Inference: FC = ( if (selectedElementIndex !== 2) { // Focus or unfocus sliders setElementFocused(!elementFocused); - } else { + } else if (firstMoved && secondMoved) { // Select the `Continue` button if permitted - if (firstMoved === true && secondMoved === true) { - props.handler(firstValue, secondValue); - } + props.handler(firstValue, secondValue); } } }; @@ -130,13 +130,13 @@ const Inference: FC = ( {/* First question */} Please use the slider below to indicate the extent to which you - believe your partner's decisions are driven by their desire to earn - points in this game. + believe your partner's decisions are driven by their desire to + earn points in this game. = ( {/* Second question */} Please use the slider below to indicate the extent to which you - believe your partner's decisions are driven by their desire to reduce - your bonus in this game. + believe your partner's decisions are driven by their desire to + reduce your bonus in this game. = ( pad={"none"} border={{ color: - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 2 ? "selectedElement" : "transparent", size: "large", }} style={ - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedElementIndex === 2 ? { borderRadius: "36px " } : {} @@ -217,10 +217,7 @@ const Inference: FC = ( primary color="button" label="Continue" - disabled={ - // Disabled until both sliders have been interacted with - firstMoved === false || secondMoved === false - } + disabled={!firstMoved || !secondMoved} // Disabled until both sliders have been interacted with icon={} reverse onClick={() => { diff --git a/src/view/screens/Questionnaires/Screentime/index.tsx b/src/view/screens/Questionnaires/Screentime/index.tsx index 5ce25c4..a514891 100644 --- a/src/view/screens/Questionnaires/Screentime/index.tsx +++ b/src/view/screens/Questionnaires/Screentime/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -18,6 +17,9 @@ import React, { FC, ReactElement, useState } from "react"; import { Box, Button, RadioButtonGroup, Heading, Text } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + // Time scale options const TIME_OPTIONS = [ { label: "1", value: 1 }, diff --git a/src/view/screens/Questionnaires/Status/index.tsx b/src/view/screens/Questionnaires/Status/index.tsx index 7a16d31..37033f7 100644 --- a/src/view/screens/Questionnaires/Status/index.tsx +++ b/src/view/screens/Questionnaires/Status/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -25,6 +24,9 @@ import { } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + /** * @summary Generate a 'Status' screen component with two pages for collecting social status information * @param {Screens.Status} props Component props containing: diff --git a/src/view/screens/Resources/index.tsx b/src/view/screens/Resources/index.tsx index c8c0b97..f61e3dc 100644 --- a/src/view/screens/Resources/index.tsx +++ b/src/view/screens/Resources/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -18,6 +17,9 @@ import React, { FC, ReactElement, useState } from "react"; import { Box, Heading, Button, Text } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + /** * @summary Generate a 'Resources' screen that displays mental health resources * @param {Screens.Resources} props Props containing callback function to proceed to next screen diff --git a/src/view/screens/SelectAvatar/index.tsx b/src/view/screens/SelectAvatar/index.tsx index cdf3bdf..d53514d 100644 --- a/src/view/screens/SelectAvatar/index.tsx +++ b/src/view/screens/SelectAvatar/index.tsx @@ -10,7 +10,6 @@ * * @author Henry Burgess */ - // React import import React, { FC, ReactElement, useState } from "react"; @@ -18,6 +17,9 @@ import React, { FC, ReactElement, useState } from "react"; import { Box, Button, Heading, Keyboard } from "grommet"; import { LinkNext } from "grommet-icons"; +// Custom types +import type { Screens } from "types"; + // Configuration import { Configuration } from "src/configuration"; @@ -54,7 +56,7 @@ const SelectAvatar: FC = ( */ const inputHandler = (event: React.KeyboardEvent) => { // Disable keyboard input if not enabled in configuration - if (Configuration.manipulations.useButtonInput === false) return; + if (!Configuration.manipulations.useButtonInput) return; // Avoid holding the key down if (event.repeat) return; @@ -113,12 +115,12 @@ const SelectAvatar: FC = ( name={avatar} size={128} // Size is fixed at 128 state={selectedAvatarName} - setState={avatarName => { + setState={(avatarName: string) => { setSelectedAvatarName(avatarName); setSelectedAvatarIndex(i); }} border={ - Configuration.manipulations.useButtonInput === true && + Configuration.manipulations.useButtonInput && selectedAvatarIndex === i && { color: "selectedElement", size: "large", @@ -131,7 +133,7 @@ const SelectAvatar: FC = ( {/* Continue button */} - {Configuration.manipulations.useButtonInput !== true && ( + {!Configuration.manipulations.useButtonInput && (