An easy-to-use platform for EEG experimentation in the classroom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactored ExperimentWindow

+166 -194
+3 -17
app/components/CollectComponent/PreTestComponent.tsx
··· 23 23 PLOTTING_INTERVAL, 24 24 CONNECTION_STATUS, 25 25 } from '../../constants/constants'; 26 - import { 27 - ExperimentParameters, 28 - MainTimeline, 29 - Trial, 30 - } from '../../constants/interfaces'; 26 + import { ExperimentParameters, Trial } from '../../constants/interfaces'; 31 27 32 28 interface Props { 33 29 ExperimentActions: typeof ExperimentActions; 34 - connectedDevice: object; 30 + connectedDevice: Record<string, any>; 35 31 signalQualityObservable: any | null | undefined; 36 32 deviceType: DEVICES; 37 33 deviceAvailability: DEVICE_AVAILABILITY; ··· 39 35 DeviceActions: typeof DeviceActions; 40 36 availableDevices: Array<any>; 41 37 type: EXPERIMENTS; 42 - paradigm: EXPERIMENTS; 43 38 isRunning: boolean; 44 39 params: ExperimentParameters; 45 - mainTimeline: MainTimeline; 46 - trials: { 47 - [key: string]: Trial; 48 - }; 49 - timelines: {}; 50 40 subject: string; 51 41 group: string; 52 42 session: number; ··· 60 50 } 61 51 62 52 export default class PreTestComponent extends Component<Props, State> { 63 - // state: State; 64 - 65 - // handlePreview: () => void; 66 53 constructor(props: Props) { 67 54 super(props); 68 55 this.state = { ··· 106 93 if (this.state.isPreviewing) { 107 94 return ( 108 95 <PreviewExperimentComponent 109 - {...loadProtocol(this.props.paradigm)} 96 + {...loadProtocol(this.props.type)} 110 97 isPreviewing={this.state.isPreviewing} 111 98 onEnd={this.endPreview} 112 99 type={this.props.type} 113 - paradigm={this.props.paradigm} 114 100 previewParams={this.props.params} 115 101 title={this.props.title} 116 102 />
+16 -20
app/components/CollectComponent/RunComponent.tsx
··· 7 7 import { injectEmotivMarker } from '../../utils/eeg/emotiv'; 8 8 import { injectMuseMarker } from '../../utils/eeg/muse'; 9 9 import { EXPERIMENTS, DEVICES } from '../../constants/constants'; 10 - import { ExperimentWindow } from '../../utils/labjs'; 10 + import { ExperimentWindow } from '../ExperimentWindow'; 11 11 import { checkFileExists, getImages } from '../../utils/filesystem/storage'; 12 12 import { Trial, ExperimentParameters } from '../../constants/interfaces'; 13 13 import { ExperimentActions } from '../../actions'; ··· 15 15 const { dialog } = remote; 16 16 17 17 interface Props { 18 - type: EXPERIMENTS | null | undefined; 18 + type: EXPERIMENTS; 19 19 title: string; 20 20 isRunning: boolean; 21 21 params: ExperimentParameters; 22 - trials: { 23 - [key: string]: Trial; 24 - }; 25 - timelines: {}; 26 22 subject: string; 23 + studyObject: any; 27 24 group: string; 28 25 session: number; 29 26 deviceType: DEVICES; 30 - paradigm: EXPERIMENTS; 31 27 isEEGEnabled: boolean; 32 28 ExperimentActions: typeof ExperimentActions; 33 29 } ··· 37 33 } 38 34 39 35 export default class Run extends Component<Props, State> { 40 - // insertLabJsCallback: () => void; 41 - // handleCloseInputCollect: (string, string, number) => void; 42 - // handleClean: () => void; 43 36 constructor(props: Props) { 44 37 super(props); 45 38 this.state = { ··· 55 48 } 56 49 57 50 async handleStartExperiment() { 58 - const filename = `${this.props.subject}-${this.props.group}-${this.props.session}-behavior.csv`; 59 - const { subject, title } = this.props; 51 + const { subject, title, session, group } = this.props; 52 + const filename = `${subject}-${group}-${session}-behavior.csv`; 60 53 const fileExists = checkFileExists(title, subject, filename); 61 54 if (fileExists) { 62 55 const options = { ··· 106 99 </Grid.Column> 107 100 ); 108 101 } 102 + return <></>; 109 103 } 110 104 111 105 renderExperiment() { ··· 156 150 </div> 157 151 ); 158 152 } 153 + 154 + const { title, type, studyObject, params } = this.props; 155 + 159 156 return ( 160 157 <div className={styles.experimentWindow}> 161 158 <ExperimentWindow 162 - settings={{ 163 - title: this.props.title, 164 - script: this.props.paradigm, 165 - params: this.props.params, 166 - eventCallback: this.insertLabJsCallback(), 167 - on_finish: (csv) => { 168 - this.props.ExperimentActions.Stop({ data: csv }); 169 - }, 159 + title={title} 160 + type={type} 161 + studyObject={studyObject} 162 + params={params} 163 + eventCallback={this.insertLabJsCallback()} 164 + onFinish={(csv) => { 165 + ExperimentActions.Stop({ data: csv }); 170 166 }} 171 167 /> 172 168 </div>
+4 -12
app/components/CollectComponent/index.tsx
··· 20 20 export interface Props { 21 21 history: HashHistory; 22 22 ExperimentActions: typeof ExperimentActions; 23 - connectedDevice: object; 23 + connectedDevice: Record<string, any>; 24 24 deviceType: DEVICES; 25 25 deviceAvailability: DEVICE_AVAILABILITY; 26 26 connectionStatus: CONNECTION_STATUS; 27 27 DeviceActions: typeof DeviceActions; 28 28 availableDevices: Array<any>; 29 29 type: EXPERIMENTS; 30 + studyObject: any; 30 31 signalQualityObservable: Observable<SignalQualityData>; 31 32 isRunning: boolean; 32 33 params: ExperimentParameters; 33 - paradigm: EXPERIMENTS; 34 - trials: { 35 - [key: string]: Trial; 36 - }; 37 - timelines: {}; 38 34 subject: string; 39 35 group: string; 40 36 session: number; ··· 48 44 } 49 45 50 46 export default class Collect extends Component<Props, State> { 51 - // handleConnectModalClose: () => void; 52 - // handleRunComponentOpen: () => void; 53 - // handleRunComponentClose: () => void; 54 47 constructor(props: Props) { 55 48 super(props); 56 49 this.state = { ··· 62 55 this.handleRunComponentOpen = this.handleRunComponentOpen.bind(this); 63 56 this.handleRunComponentClose = this.handleRunComponentClose.bind(this); 64 57 if (isNil(props.params)) { 58 + // TODO: Timeline -> Parameters name change? 65 59 props.ExperimentActions.LoadDefaultTimeline(); 66 60 } 67 61 } ··· 131 125 ExperimentActions={this.props.ExperimentActions} 132 126 availableDevices={this.props.availableDevices} 133 127 type={this.props.type} 134 - paradigm={this.props.paradigm} 135 128 isRunning={this.props.isRunning} 136 129 params={this.props.params} 137 - trials={this.props.trials} 138 - timelines={this.props.timelines} 130 + title={this.props.title} 139 131 subject={this.props.subject} 140 132 group={this.props.group} 141 133 session={this.props.session}
+119
app/components/ExperimentWindow.tsx
··· 1 + import React, { useEffect } from 'react'; 2 + import path from 'path'; 3 + import clonedeep from 'lodash.clonedeep'; 4 + import * as lab from 'lab.js/dist/lab.dev'; 5 + import { ExperimentParameters } from '../constants/interfaces'; 6 + 7 + // TODO: Switch to using .json files to load lab.js studies 8 + import visualsearch from '../utils/labjs/scripts/visualsearch'; 9 + import stroop from '../utils/labjs/scripts/stroop'; 10 + import multitasking from '../utils/labjs/scripts/multitasking'; 11 + import faceshouses from '../utils/labjs/scripts/faceshouses'; 12 + import custom from '../utils/labjs/scripts/custom'; 13 + import { EXPERIMENTS } from '../constants/constants'; 14 + 15 + export interface ExperimentWindowProps { 16 + title: string; 17 + type: EXPERIMENTS; 18 + studyObject: any; 19 + params: ExperimentParameters; 20 + eventCallback: (value: any, time: number) => void; 21 + onFinish: (csv: any) => void; 22 + } 23 + 24 + export const ExperimentWindow: React.FC<ExperimentWindowProps> = ({ 25 + title, 26 + type, 27 + studyObject, 28 + params, 29 + eventCallback, 30 + onFinish, 31 + }) => { 32 + useEffect(() => { 33 + console.log('Experiment Window effect'); 34 + // TODO: move this study mutation into Redux 35 + let studyWithParams = studyObject; 36 + 37 + // TODO: Remove eventually when study updating has been brought into redux 38 + switch (type) { 39 + case 'Multi-tasking': 40 + multitasking.parameters = params; 41 + studyWithParams = lab.util.fromObject(clonedeep(multitasking), lab); 42 + break; 43 + case 'Visual Search': 44 + visualsearch.parameters = params; 45 + studyWithParams = lab.util.fromObject(clonedeep(visualsearch), lab); 46 + break; 47 + case 'Stroop Task': 48 + stroop.parameters = params; 49 + studyWithParams = lab.util.fromObject(clonedeep(stroop), lab); 50 + break; 51 + case 'Faces and Houses': 52 + faceshouses.parameters = params; 53 + studyWithParams = lab.util.fromObject(clonedeep(faceshouses), lab); 54 + break; 55 + case 'Custom': 56 + default: 57 + custom.parameters = params; 58 + studyWithParams = lab.util.fromObject(clonedeep(custom), lab); 59 + break; 60 + } 61 + 62 + if (type === 'Custom' || type === 'Faces and Houses') { 63 + studyWithParams.parameters.title = title; 64 + studyWithParams.files = params.stimuli 65 + .map((image) => ({ 66 + [path.join(image.dir, image.filename)]: path.join( 67 + image.dir, 68 + image.filename 69 + ), 70 + })) 71 + .reduce((obj, item) => { 72 + // return { ...obj, item }; ?? 73 + obj[Object.keys(item)[0]] = Object.values(item)[0]; 74 + return obj; 75 + }, {}); 76 + } 77 + 78 + studyWithParams.on('end', () => { 79 + const csv = studyWithParams.options.datastore.exportCsv(); 80 + studyWithParams = undefined; 81 + onFinish(csv); 82 + }); 83 + studyWithParams.parameters.callbackForEEG = (e) => { 84 + eventCallback(e, new Date().getTime()); 85 + }; 86 + studyWithParams.options.events.keydown = async (e) => { 87 + if (e.code === 'Escape') { 88 + if (studyWithParams) { 89 + await studyWithParams.internals.controller.audioContext.close(); 90 + studyWithParams.end(); 91 + } 92 + } 93 + }; 94 + 95 + studyWithParams.run(); 96 + 97 + return () => { 98 + try { 99 + if (studyWithParams) { 100 + studyWithParams.internals.controller.audioContext.close(); 101 + studyWithParams.end(); 102 + } 103 + } catch (e) { 104 + console.log('Experiment closed before unmount'); 105 + } 106 + }; 107 + }, []); 108 + 109 + return ( 110 + <div className="container fullscreen" data-labjs-section="main"> 111 + <main className="content-vertical-center content-horizontal-center"> 112 + <div> 113 + <h2>Loading Experiment</h2> 114 + <p>The experiment is loading and should start in a few seconds</p> 115 + </div> 116 + </main> 117 + </div> 118 + ); 119 + };
+11 -15
app/components/PreviewExperimentComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { Segment } from 'semantic-ui-react'; 3 - import { ExperimentWindow } from '../utils/labjs'; 3 + import { ExperimentWindow } from './ExperimentWindow'; 4 4 import styles from './styles/collect.css'; 5 5 6 6 import { getImages } from '../utils/filesystem/storage'; 7 7 import { Trial, ExperimentParameters } from '../constants/interfaces'; 8 + import { EXPERIMENTS } from '../constants/constants'; 8 9 9 10 interface Props { 10 11 title: string; 11 - paradigm: string; 12 + type: EXPERIMENTS; 13 + studyObject: any; 12 14 params: ExperimentParameters; 13 15 previewParams?: ExperimentParameters; 14 16 isPreviewing: boolean; 15 - trials: { 16 - [key: string]: Trial; 17 - }; 18 - timelines: {}; 19 17 onEnd: () => void; 20 18 } 21 19 ··· 39 37 return ( 40 38 <div className={styles.previewExpComponent}> 41 39 <ExperimentWindow 42 - settings={{ 43 - title: this.props.title, 44 - script: this.props.paradigm, 45 - params: this.props.previewParams || this.props.params, 46 - eventCallback: 47 - PreviewExperimentComponent.insertPreviewLabJsCallback, 48 - on_finish: (csv) => { 49 - this.props.onEnd(); 50 - }, 40 + title={this.props.title} 41 + type={this.props.type} 42 + studyObject={this.props.studyObject} 43 + params={this.props.previewParams || this.props.params} 44 + eventCallback={PreviewExperimentComponent.insertPreviewLabJsCallback} 45 + onFinish={(csv) => { 46 + this.props.onEnd(); 51 47 }} 52 48 /> 53 49 </div>
+1 -1
app/constants/constants.ts
··· 5 5 SSVEP = 'Steady-state Visual Evoked Potential', 6 6 STROOP = 'Stroop Task', 7 7 MULTI = 'Multi-tasking', 8 - SEARCH = 'Visual search', 8 + SEARCH = 'Visual Search', 9 9 CUSTOM = 'Custom', 10 10 } 11 11
+12 -14
app/reducers/experimentReducer.ts
··· 2 2 import { ExperimentActions } from '../actions'; 3 3 import { EXPERIMENTS } from '../constants/constants'; 4 4 import { 5 - MainTimeline, 6 - Trial, 7 5 ExperimentDescription, 8 6 ExperimentParameters, 9 7 } from '../constants/interfaces'; 10 8 11 9 export interface ExperimentStateType { 12 - readonly type: EXPERIMENTS | null | undefined; 13 - readonly title: string | null | undefined; 14 - readonly params: ExperimentParameters | null | undefined; 15 - readonly mainTimeline: MainTimeline; 16 - readonly trials: { 17 - [key: string]: Trial; 18 - }; 19 - readonly timelines: {}; 20 - readonly plugins: object; 10 + readonly type: EXPERIMENTS; 11 + readonly title: string; 12 + // Aspects of a study that can be tweaked within the BrainWaves app 13 + readonly params: ExperimentParameters | null; 14 + // lab.js study object that is executed by lab.js to rendder the study 15 + readonly studyObject: any; 16 + readonly plugins: Record<string, any>; 17 + // Subject/student name (e.g. Brian) 21 18 readonly subject: string; 19 + // Classroom group name 20 + // TODO: Should this be optional? 22 21 readonly group: string; 22 + // Session num. Each complete run through of the experiment is one session 23 23 readonly session: number; 24 24 readonly isRunning: boolean; 25 25 readonly isEEGEnabled: boolean; ··· 30 30 type: EXPERIMENTS.NONE, 31 31 title: '', 32 32 params: null, 33 - mainTimeline: [], 34 - trials: {}, 35 - timelines: {}, 33 + studyObject: {}, 36 34 plugins: {}, 37 35 subject: '', 38 36 group: '',
-115
app/utils/labjs/index.tsx
··· 1 - import React, { Component } from 'react'; 2 - import path from 'path'; 3 - import clonedeep from 'lodash.clonedeep'; 4 - import * as lab from 'lab.js/dist/lab.dev'; 5 - import { ExperimentSettings } from '../../constants/interfaces'; 6 - 7 - // TODO: Switch to using .json files to load lab.js studies 8 - import visualsearch from './scripts/visualsearch'; 9 - import stroop from './scripts/stroop'; 10 - import multitasking from './scripts/multitasking'; 11 - import faceshouses from './scripts/faceshouses'; 12 - import custom from './scripts/custom'; 13 - 14 - class ExperimentWindow extends Component<{ settings: ExperimentSettings }> { 15 - // TO BE TYPED Lab.js Study 16 - study: any = {}; 17 - 18 - componentDidMount() { 19 - const { props } = this; 20 - switch (props.settings.script) { 21 - case 'Multi-tasking': 22 - multitasking.parameters = props.settings.params; 23 - this.study = lab.util.fromObject(clonedeep(multitasking), lab); 24 - break; 25 - case 'Visual search': 26 - visualsearch.parameters = props.settings.params; 27 - this.study = lab.util.fromObject(clonedeep(visualsearch), lab); 28 - break; 29 - case 'Stroop Task': 30 - stroop.parameters = props.settings.params; 31 - this.study = lab.util.fromObject(clonedeep(stroop), lab); 32 - break; 33 - case 'Faces and Houses': 34 - faceshouses.parameters = props.settings.params; 35 - faceshouses.parameters.title = props.settings.title; 36 - faceshouses.files = props.settings.params.stimuli 37 - .map((image) => ({ 38 - [path.join(image.dir, image.filename)]: path.join( 39 - image.dir, 40 - image.filename 41 - ), 42 - })) 43 - .reduce((obj, item) => { 44 - // return { ...obj, item }; ?? 45 - obj[Object.keys(item)[0]] = Object.values(item)[0]; 46 - return obj; 47 - }, {}); 48 - this.study = lab.util.fromObject(clonedeep(faceshouses), lab); 49 - break; 50 - case 'Custom': 51 - default: 52 - custom.parameters = props.settings.params; 53 - custom.parameters.title = props.settings.title; 54 - custom.files = props.settings.params.stimuli 55 - .map((image) => ({ 56 - [path.join(image.dir, image.filename)]: path.join( 57 - image.dir, 58 - image.filename 59 - ), 60 - })) 61 - .reduce((obj, item) => { 62 - // return { ...obj, item }; ?? 63 - obj[Object.keys(item)[0]] = Object.values(item)[0]; 64 - return obj; 65 - }, {}); 66 - this.study = lab.util.fromObject(clonedeep(custom), lab); 67 - break; 68 - } 69 - 70 - this.study.on('end', () => { 71 - const csv = this.study.options.datastore.exportCsv(); 72 - this.study = undefined; 73 - props.settings.on_finish(csv); 74 - }); 75 - this.study.parameters.callbackForEEG = (e) => { 76 - props.settings.eventCallback(e, new Date().getTime()); 77 - }; 78 - this.study.options.events.keydown = async (e) => { 79 - if (e.code === 'Escape') { 80 - if (this.study) { 81 - await this.study.internals.controller.audioContext.close(); 82 - this.study.end(); 83 - } 84 - } 85 - }; 86 - 87 - this.study.run(); 88 - } 89 - 90 - componentWillUnmount() { 91 - try { 92 - if (this.study) { 93 - this.study.internals.controller.audioContext.close(); 94 - this.study.end(); 95 - } 96 - } catch (e) { 97 - console.log('Experiment closed before unmount'); 98 - } 99 - } 100 - 101 - render() { 102 - return ( 103 - <div className="container fullscreen" data-labjs-section="main"> 104 - <main className="content-vertical-center content-horizontal-center"> 105 - <div> 106 - <h2>Loading Experiment</h2> 107 - <p>The experiment is loading and should start in a few seconds</p> 108 - </div> 109 - </main> 110 - </div> 111 - ); 112 - } 113 - } 114 - 115 - export { ExperimentWindow };