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.

Added Peri-experiment screen to runcomponent; fixed bug where appState was getting saved in workspaces root

jdpigeon 688bc7ea e5e63b7a

+145 -53
+4 -2
app/components/CollectComponent/HelpSidebar.js
··· 45 45 handleNext() { 46 46 if ( 47 47 this.state.helpStep === HELP_STEP.SIGNAL_MOVEMENT || 48 - this.state.helpStep === HELP_STEP.LEARN_THOUGHTS 48 + this.state.helpStep === HELP_STEP.LEARN_ALPHA 49 49 ) { 50 50 this.setState({ helpStep: HELP_STEP.MENU }); 51 51 } else { ··· 158 158 return ( 159 159 <Segment basic padded vertical className={styles.helpSidebar}> 160 160 <Button 161 - inverted 161 + basic 162 162 circular 163 + size="large" 163 164 floated="right" 164 165 icon="x" 166 + className={styles.closeButton} 165 167 onClick={this.props.handleClose} 166 168 /> 167 169 {this.renderHelpContent()}
+1 -1
app/components/CollectComponent/PreTestComponent.js
··· 125 125 126 126 render() { 127 127 return ( 128 - <Sidebar.Pushable as={Segment} basic> 128 + <Sidebar.Pushable as={Segment} className={styles.preTestPushable} basic> 129 129 <Sidebar 130 130 width="wide" 131 131 direction="right"
+75 -31
app/components/CollectComponent/RunComponent.js
··· 1 1 // @flow 2 2 import React, { Component } from 'react'; 3 - import { Grid, Button, Segment, Input } from 'semantic-ui-react'; 3 + import { Grid, Button, Segment, Header, Divider } from 'semantic-ui-react'; 4 4 import { Experiment } from 'jspsych-react'; 5 5 import { debounce } from 'lodash'; 6 6 import { Link } from 'react-router-dom'; 7 7 import styles from '../styles/common.css'; 8 + import InputModal from '../InputModal'; 8 9 import { injectEmotivMarker } from '../../utils/eeg/emotiv'; 9 10 import { injectMuseMarker } from '../../utils/eeg/muse'; 10 11 import callbackHTMLDisplay from '../../utils/jspsych/plugins/callback-html-display'; 11 12 import callbackImageDisplay from '../../utils/jspsych/plugins/callback-image-display'; 12 - import { EXPERIMENTS, DEVICES, SCREENS } from '../../constants/constants'; 13 + import { EXPERIMENTS, DEVICES } from '../../constants/constants'; 13 14 import { 14 15 parseTimeline, 15 16 instantiateTimeline, ··· 35 36 experimentActions: Object; 36 37 } 37 38 38 - export default class Run extends Component<Props> { 39 + interface State { 40 + isInputModalOpen: boolean; 41 + } 42 + 43 + export default class Run extends Component<Props, State> { 39 44 props: Props; 45 + state: State; 40 46 handleSubjectEntry: (Object, Object) => void; 41 47 handleSessionEntry: (Object, Object) => void; 42 48 handleStartExperiment: () => void; 43 49 handleTimeline: () => void; 50 + handleCloseInputModal: () => void; 51 + handleClean: () => void; 44 52 45 53 constructor(props: Props) { 46 54 super(props); 55 + this.state = { 56 + isInputModalOpen: props.subject.length === 0 57 + }; 47 58 this.handleSubjectEntry = debounce(this.handleSubjectEntry, 500).bind(this); 48 59 this.handleSessionEntry = debounce(this.handleSessionEntry, 500).bind(this); 49 60 this.handleStartExperiment = this.handleStartExperiment.bind(this); 50 61 this.handleTimeline = this.handleTimeline.bind(this); 62 + this.handleCloseInputModal = this.handleCloseInputModal.bind(this); 63 + // this.handleClean = this.handleClean.bind(this); 51 64 } 52 65 53 66 componentDidMount() { ··· 68 81 this.props.experimentActions.start(); 69 82 } 70 83 84 + handleCloseInputModal(name: string) { 85 + this.props.experimentActions.setSubject(name); 86 + this.setState({ isInputModalOpen: false }); 87 + } 88 + 71 89 handleTimeline() { 72 90 const injectionFunction = 73 91 this.props.deviceType === 'MUSE' ? injectMuseMarker : injectEmotivMarker; ··· 90 108 return getImages(this.props.params); 91 109 } 92 110 111 + renderCleanButton() { 112 + if (this.props.session > 1) { 113 + return ( 114 + <Link to="/clean"> 115 + <Button fluid secondary> 116 + Clean Data 117 + </Button> 118 + </Link> 119 + ); 120 + } 121 + } 122 + 93 123 renderExperiment() { 94 124 if (!this.props.isRunning) { 95 125 return ( 96 - <div> 97 - <Segment raised padded color="red"> 98 - {this.props.title} 99 - <div className={styles.inputDiv}> 100 - <Input 101 - focus 102 - label={{ basic: true, content: 'Subject Name' }} 103 - onChange={this.handleSubjectEntry} 104 - placeholder="Name" 105 - /> 106 - </div> 107 - <div className={styles.inputDiv}> 108 - <Input 109 - focus 110 - label={{ basic: true, content: 'Session Number' }} 111 - onChange={this.handleSessionEntry} 112 - placeholder={this.props.session} 126 + <div className={styles.mainContainer}> 127 + <Segment 128 + basic 129 + textAlign="left" 130 + className={styles.descriptionContainer} 131 + > 132 + <Header as="h1">{this.props.type}</Header> 133 + <Segment basic className={styles.infoSegment}> 134 + Subject Name: <b>{this.props.subject}</b> 135 + <Button 136 + basic 137 + circular 138 + size="huge" 139 + icon="edit" 140 + className={styles.closeButton} 141 + onClick={() => this.setState({ isInputModalOpen: true })} 113 142 /> 114 - </div> 115 - <Button onClick={this.handleStartExperiment}> 143 + </Segment> 144 + <Segment basic className={styles.infoSegment}> 145 + Session Number: <b>{this.props.session}</b> 146 + </Segment> 147 + {/* <Grid.Column> 148 + <Button 149 + fluid 150 + className={styles.secondaryButton} 151 + onClick={() => this.handleinstructionProgress(1)} 152 + > 153 + Back 154 + </Button> 155 + </Grid.Column> */} 156 + <Divider hidden section /> 157 + 158 + <Button fluid primary onClick={this.handleStartExperiment}> 116 159 Start Experiment 117 160 </Button> 161 + {this.renderCleanButton} 118 162 </Segment> 119 - <Link to={SCREENS.CLEAN.route}> 120 - <Button>Clean Data</Button> 121 - </Link> 122 163 </div> 123 164 ); 124 165 } ··· 141 182 142 183 render() { 143 184 return ( 144 - <div> 145 - <div className={styles.mainContainer} data-tid="container"> 146 - <Grid columns={1} divided relaxed> 147 - <Grid.Row centered>{this.renderExperiment()}</Grid.Row> 148 - </Grid> 149 - </div> 185 + <div className={styles.mainContainer} data-tid="container"> 186 + <Grid columns={1} divided relaxed> 187 + <Grid.Row centered>{this.renderExperiment()}</Grid.Row> 188 + </Grid> 189 + <InputModal 190 + open={this.state.isInputModalOpen} 191 + onClose={this.handleCloseInputModal} 192 + header="Enter Subject Name" 193 + /> 150 194 </div> 151 195 ); 152 196 }
+2 -2
app/components/CollectComponent/index.js
··· 108 108 ); 109 109 } 110 110 return ( 111 - <div> 111 + <React.Fragment> 112 112 <ConnectModal 113 113 history={this.props.history} 114 114 open={this.state.isConnectModalOpen} ··· 140 140 session={this.props.session} 141 141 openRunComponent={this.handleRunComponentOpen} 142 142 /> 143 - </div> 143 + </React.Fragment> 144 144 ); 145 145 } 146 146 }
+18 -7
app/components/InputModal.js
··· 12 12 13 13 interface State { 14 14 enteredText: string; 15 + isError: boolean; 15 16 } 16 17 17 18 export default class InputModal extends Component<Props, State> { 18 19 props: Props; 19 20 state: State; 20 21 handleTextEntry: (Object, Object) => void; 22 + handleClose: () => void; 21 23 22 24 constructor(props: Props) { 23 25 super(props); 24 26 this.state = { 25 - enteredText: '' 27 + enteredText: '', 28 + isError: false 26 29 }; 27 30 this.handleTextEntry = debounce(this.handleTextEntry, 500).bind(this); 28 31 } ··· 31 34 this.setState({ enteredText: data.value }); 32 35 } 33 36 37 + handleClose() { 38 + if (this.state.enteredText.length > 1) { 39 + this.props.onClose(this.state.enteredText); 40 + } 41 + this.setState({ isError: true }); 42 + } 43 + 34 44 render() { 35 45 return ( 36 46 <Modal ··· 43 53 > 44 54 <Modal.Content>{this.props.header}</Modal.Content> 45 55 <Modal.Content> 46 - <Input focus fluid onChange={this.handleTextEntry} /> 56 + <Input 57 + focus 58 + fluid 59 + error={this.state.isError} 60 + onChange={this.handleTextEntry} 61 + /> 47 62 </Modal.Content> 48 63 <Modal.Actions> 49 - <Button 50 - color="blue" 51 - content="OK" 52 - onClick={() => this.props.onClose(this.state.enteredText)} 53 - /> 64 + <Button color="blue" content="OK" onClick={this.handleClose} /> 54 65 </Modal.Actions> 55 66 </Modal> 56 67 );
+5
app/components/styles/common.css
··· 58 58 margin-right: 20px !important; 59 59 } 60 60 61 + .infoSegment { 62 + padding: 0% !important; 63 + font-size: 18px !important; 64 + } 65 + 61 66 .closeButton { 62 67 border: none !important; 63 68 box-shadow: none !important;
+40 -10
app/epics/experimentEpics.js
··· 1 1 import { combineEpics } from 'redux-observable'; 2 2 import { executeRequest } from '@nteract/messaging'; 3 - import { of } from 'rxjs'; 3 + import { from, of } from 'rxjs'; 4 4 import { 5 5 map, 6 6 mapTo, ··· 21 21 START, 22 22 STOP, 23 23 SAVE_WORKSPACE, 24 - CREATE_NEW_WORKSPACE 24 + CREATE_NEW_WORKSPACE, 25 + SET_SUBJECT 25 26 } from '../actions/experimentActions'; 26 27 import { 27 28 DEVICES, ··· 39 40 getWorkspaceDir, 40 41 storeExperimentState, 41 42 createWorkspaceDir, 42 - storeBehaviouralData 43 + storeBehaviouralData, 44 + readWorkspaceRawEEGData 43 45 } from '../utils/filesystem/storage'; 44 46 import { saveEpochs } from '../utils/jupyter/cells'; 45 47 46 48 export const SET_TIMELINE = 'LOAD_TIMELINE'; 47 49 export const SET_IS_RUNNING = 'SET_IS_RUNNING'; 50 + export const UPDATE_SESSION = 'UPDATE_SESSION'; 48 51 export const SET_SESSION = 'SET_SESSION'; 49 52 export const EXPERIMENT_CLEANUP = 'EXPERIMENT_CLEANUP'; 50 53 ··· 60 63 payload, 61 64 type: SET_IS_RUNNING 62 65 }); 66 + 67 + const updateSession = () => ({ type: UPDATE_SESSION }); 63 68 64 69 const setSession = payload => ({ 65 70 payload, ··· 133 138 state$.value.experiment.session 134 139 ) 135 140 ), 136 - mapTo(false), 137 - map(setIsRunning) 141 + mergeMap(() => of(setIsRunning(false), updateSession())) 138 142 ); 139 143 140 - const sessionCountEpic = (action$, state$) => 141 - action$.ofType(STOP).pipe( 142 - filter(() => state$.value.experiment.isRunning), 143 - map(() => setSession(state$.value.experiment.session + 1)) 144 + const setSubjectEpic = action$ => 145 + action$.ofType(SET_SUBJECT).pipe(map(updateSession)); 146 + 147 + // TODO: Refactor this to use redux-observable state stream 148 + const updateSessionEpic = (action$, state$) => 149 + action$.ofType(UPDATE_SESSION).pipe( 150 + mapTo(state$.value.experiment.subject), 151 + mergeMap(subject => 152 + from(readWorkspaceRawEEGData(state$.value.experiment.title)).pipe( 153 + map(rawFiles => { 154 + if (rawFiles.length > 0) { 155 + console.log(rawFiles[0].slice(0, rawFiles[0].length - 8)); 156 + const subjectFiles = rawFiles.filter(filepath => 157 + filepath.path.includes(subject) 158 + ); 159 + return subjectFiles.length; 160 + } 161 + return 1; 162 + }) 163 + ) 164 + ), 165 + map(setSession) 144 166 ); 145 167 168 + // const sessionCountEpic = (action$, state$) => 169 + // action$.ofType(STOP).pipe( 170 + // filter(() => state$.value.experiment.isRunning), 171 + // map(() => setSession(state$.value.experiment.session + 1)) 172 + // ); 173 + 146 174 const autoSaveEpic = action$ => 147 175 action$.ofType(SET_TIMELINE).pipe(map(saveWorkspace)); 148 176 149 177 const saveWorkspaceEpic = (action$, state$) => 150 178 action$.ofType(SAVE_WORKSPACE).pipe( 151 179 throttleTime(1000), 180 + filter(() => state$.value.experiment.title.length > 1), 152 181 map(() => getWorkspaceDir(state$.value.experiment.title)), 153 182 tap(() => storeExperimentState(state$.value.experiment)), 154 183 tap(dir => { ··· 183 212 createNewWorkspaceEpic, 184 213 startEpic, 185 214 experimentStopEpic, 186 - sessionCountEpic, 215 + setSubjectEpic, 216 + updateSessionEpic, 187 217 autoSaveEpic, 188 218 saveWorkspaceEpic, 189 219 navigationCleanupEpic