diff --git a/src/JsPsych/timeline.js b/src/JsPsych/timeline.js index 13afe99c3..670e759d3 100644 --- a/src/JsPsych/timeline.js +++ b/src/JsPsych/timeline.js @@ -1,9 +1,10 @@ // TODO 204: @ import for language -import { language } from "./language"; +// import { language } from "./language"; // TODO 204: @ import for trials -import { Preamble, createCountdownTrial, EndExperiment } from "./trials/examples"; -import { createHoneycombBlock } from "./trials/honeycombBlock"; +import { Preamble, EndExperiment } from "./trials/examples"; +// import { Preamble, createCountdownTrial, EndExperiment } from "./trials/examples"; +// import { createHoneycombBlock } from "./trials/honeycombBlock"; /** * Create your custom JsPsych options here. These settings will applied experiment wide. @@ -25,44 +26,38 @@ export const JSPSYCH_OPTIONS = { * @returns array of trials */ // TODO 207: Eslint warning is causing build to fail -// eslint-disable-next-line -export function buildTimeline(jsPsych) { - // Get slider text from the language file and create the trials - // const sliderLanguage = language.quiz.direction.slider; - // const sliderLeft = createSliderTrial(sliderLanguage.left); - // const sliderRight = createSliderTrial(sliderLanguage.right); - +export function buildTimeline() { // Get countdown txt from the language fil and create the trials+ - const countdownLanguage = language.countdown; - const firstBlockCountdown = createCountdownTrial({ message: countdownLanguage.first }); - const secondBlockCountdown = createCountdownTrial({ message: countdownLanguage.second }); + // const countdownLanguage = language.countdown; + // const firstBlockCountdown = createCountdownTrial({ message: countdownLanguage.first }); + // const secondBlockCountdown = createCountdownTrial({ message: countdownLanguage.second }); - // Create a tutorial block of Honeycomb's custom task - const honeycombTutorialBlock = createHoneycombBlock({ - isTutorial: true, - photodiodeActive: false, - }); + // // Create a tutorial block of Honeycomb's custom task + // const honeycombTutorialBlock = createHoneycombBlock({ + // isTutorial: true, + // photodiodeActive: false, + // }); - // Create a practice block of Honeycomb's custom task - const honeycombPracticeBlock = createHoneycombBlock({ - conditions: ["m", "n"], - repeatsPerCondition: 1, - isPractice: true, - }); + // // Create a practice block of Honeycomb's custom task + // const honeycombPracticeBlock = createHoneycombBlock({ + // conditions: ["m", "n"], + // repeatsPerCondition: 1, + // isPractice: true, + // }); - // Create an experiment block - const honeycombBlock1 = createHoneycombBlock({ - repeatsPerCondition: 2, - }); + // // Create an experiment block + // const honeycombBlock1 = createHoneycombBlock({ + // repeatsPerCondition: 2, + // }); // Build the timeline const timeline = [ Preamble, - honeycombTutorialBlock, - firstBlockCountdown, - honeycombPracticeBlock, - secondBlockCountdown, - honeycombBlock1, + // honeycombTutorialBlock, + // firstBlockCountdown, + // honeycombPracticeBlock, + // secondBlockCountdown, + // honeycombBlock1, EndExperiment, // Task complete message ]; return timeline; diff --git a/src/components/App.jsx b/src/components/App.jsx index fc490d97c..7d05bc154 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -4,7 +4,7 @@ import "bootstrap/dist/css/bootstrap.css"; import { addToFirebase, validateParticipant } from "../firebase"; -import JsPsychExperiment from "./JsPsychExperiment"; +import Experiment from "./Experiment"; import Login from "./Login"; import Error from "./Error"; @@ -13,6 +13,9 @@ import { getQueryVariable } from "../utils"; import { TASK_VERSION } from "../JsPsych/constants"; +// Additional options passed by the user +import { JSPSYCH_OPTIONS } from "../JsPsych/timeline"; + /** Top-level React component for Honeycomb. * * This component stores the state of the app. @@ -189,11 +192,10 @@ function App() { setLoggedIn(true); }, []); - if (isError) { - return ; - } else { + if (isError) return ; + else { return loggedIn ? ( - ) : ( // Not logged in - display login screen diff --git a/src/components/Experiment.jsx b/src/components/Experiment.jsx new file mode 100644 index 000000000..12a767082 --- /dev/null +++ b/src/components/Experiment.jsx @@ -0,0 +1,55 @@ +import { initJsPsych } from "jspsych"; +import React, { useEffect } from "react"; + +import { initParticipant } from "../firebase"; + +// These will be passed in as props to the package once it's split +import { buildTimeline } from "../JsPsych/timeline"; + +const EXPERIMENT_ID = "jspsych-experiment-window"; + +// Based on https://react.dev/reference/react/useEffect#controlling-a-non-react-widget +export default function Experiment({ + config, + studyID, + participantID, + taskVersion, + dataUpdateFunction, + dataFinishFunction, + jsPsychOptions, +}) { + // Initialize and run JsPsych on first render + useEffect(() => { + // Start date of the experiment - used as the UID + // TODO 169: JsPsych has a built in timestamp function + const startDate = new Date().toISOString(); + + // Write the initial record to Firestore + if (config.USE_FIREBASE) initParticipant(studyID, participantID, startDate); + + // Initialize experiment, user passed options are merged with defaults + const jsPsych = initJsPsych({ + display_element: EXPERIMENT_ID, + on_data_update: (data) => dataUpdateFunction(data), + on_finish: (data) => dataFinishFunction(data), + ...jsPsychOptions, + }); + // Add experiment data to the trials + jsPsych.data.addProperties({ + study_id: studyID, + participant_id: participantID, + start_date: startDate, + task_version: taskVersion, + }); + + // Run the experiment + jsPsych.run(buildTimeline()).then(() => { + // TODO: Return to home page after experiment completes + // TODO: Need to update the electron code to save data here, not + console.log("FINISHED EXPERIMENT"); + window.location.reload(); + }); + }, []); + + return
; +} diff --git a/src/components/JsPsychExperiment.jsx b/src/components/JsPsychExperiment.jsx deleted file mode 100644 index eec1a425b..000000000 --- a/src/components/JsPsychExperiment.jsx +++ /dev/null @@ -1,94 +0,0 @@ -import { initJsPsych } from "jspsych"; -import React, { useEffect, useMemo, useRef } from "react"; - -import { initParticipant } from "../firebase"; - -// These will be passed in as props to the package once it's split -import { buildTimeline, JSPSYCH_OPTIONS } from "../JsPsych/timeline"; - -/** JsPsych Experiment - * - * This component initializes the JsPsych instance for the experiment. - * It provides a window for JsPsych to run inside of (experimentDiv) - * It also handles the passing of keyboard/mouse events into JsPsych - */ -function JsPsychExperiment({ - config, - studyID, - participantID, - taskVersion, - dataUpdateFunction, - dataFinishFunction, -}) { - // This will be the div in the dom that holds the experiment. - // We reference it explicitly here so we can do some plumbing with react, jspsych, and events. - const experimentDivID = "experimentWindow"; - const experimentDivRef = useRef(null); - - // Create the instance of jsPsych that we'll reuse within the scope of this JsPsychExperiment component. - // As of jspsych 7, we create our own jspsych instance(s) where needed instead of importing one global instance. - const jsPsych = useMemo(() => { - // Start date of the experiment - used as the UID - // TODO 169: JsPsych has a built in timestamp function - const startDate = new Date().toISOString(); - - // Write the initial record to Firestore - if (config.USE_FIREBASE) initParticipant(studyID, participantID, startDate); - - // Initialize experiment with needed data. Combines custom options with necessary Honeycomb options. - const jsPsych = initJsPsych({ - ...JSPSYCH_OPTIONS, // This will be passed to the package - display_element: experimentDivID, - on_data_update: (data) => dataUpdateFunction(data), - on_finish: (data) => dataFinishFunction(data), - }); - jsPsych.data.addProperties({ - study_id: studyID, - participant_id: participantID, - start_date: startDate, - task_version: taskVersion, - }); - return jsPsych; - }, [studyID, participantID, taskVersion]); - - // Build our jspsych experiment timeline (in this case a Honeycomb demo, you could substitute your own here). - // ? I wonder if it makes the most sense to just have the user create and pass the jsPsych object? - const timeline = buildTimeline(jsPsych); - // TODO 238: I think the user needs to confirm if they're going to enable audio? - - // Set up event and lifecycle callbacks to start and stop jspsych. - // Inspiration from jspsych-react: https://github.com/makebrainwaves/jspsych-react/blob/master/src/index.js - // These useEffect callbacks are similar to componentDidMount / componentWillUnmount. - function handleKeyEvent(e) { - if (e.redispatched) return; - - const newEvent = new e.constructor(e.type, e); - newEvent.redispatched = true; - experimentDivRef.current.dispatchEvent(newEvent); - } - - useEffect(() => { - window.addEventListener("keyup", handleKeyEvent, true); - window.addEventListener("keydown", handleKeyEvent, true); - jsPsych.run(timeline); - - return () => { - window.removeEventListener("keyup", handleKeyEvent, true); - window.removeEventListener("keydown", handleKeyEvent, true); - try { - jsPsych.endExperiment("Ended Experiment"); - } catch (e) { - console.error("Experiment closed before unmount"); - } - }; - }); - - // TODO 184: Root is not taking up 100vh here? The isn't? Are the trials causing that? - return ( -
-
-
- ); -} - -export default JsPsychExperiment; diff --git a/src/firebase/firebase.js b/src/firebase/firebase.js index f5284a20d..65a3d11f4 100644 --- a/src/firebase/firebase.js +++ b/src/firebase/firebase.js @@ -56,9 +56,9 @@ export async function initParticipant(studyID, participantID, startDate) { await experiment.set({ start_time: startDate, // TODO 173: app_version and app_platform are deprecated + // TODO: Pass taskVersion into this function from app_version: window.navigator.appVersion, app_platform: window.navigator.platform, - // TODO 175: Store participantID and studyID here, not on each trial }); console.log("Initialized experiment:", studyID, participantID, startDate); return true;