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.

Prototype implementation of Run and Clean Screens

jdpigeon 0aef1539 80fb705b

+433 -150
-134
app/components/CleanComponent.js
··· 1 - // @flow 2 - import React, { Component } from "react"; 3 - import { 4 - Grid, 5 - Button, 6 - Icon, 7 - Segment, 8 - Header, 9 - Dropdown 10 - } from "semantic-ui-react"; 11 - import { isNil } from "lodash"; 12 - import styles from "./styles/common.css"; 13 - import { EXPERIMENTS, DEVICES, KERNEL_STATUS } from "../constants/constants"; 14 - import { Kernel } from "../constants/interfaces"; 15 - import { readWorkspaceRawEEGData } from "../utils/filesystem/storage"; 16 - 17 - interface Props { 18 - type: ?EXPERIMENTS; 19 - title: string; 20 - deviceType: DEVICES; 21 - mainChannel: ?any; 22 - kernel: ?Kernel; 23 - kernelStatus: KERNEL_STATUS; 24 - epochsInfo: ?{ [string]: number }; 25 - jupyterActions: Object; 26 - } 27 - 28 - interface State { 29 - eegFilePaths: Array<?{ 30 - key: string, 31 - text: string, 32 - value: { name: string, dir: string } 33 - }>; 34 - selectedFilePaths: Array<?string>; 35 - } 36 - 37 - export default class Clean extends Component<Props, State> { 38 - props: Props; 39 - state: State; 40 - handleDropdownChange: (Object, Object) => void; 41 - handleLoadData: () => void; 42 - 43 - constructor(props: Props) { 44 - super(props); 45 - this.state = { 46 - eegFilePaths: [{ key: "", text: "", value: "" }], 47 - selectedFilePaths: [] 48 - }; 49 - this.handleDropdownChange = this.handleDropdownChange.bind(this); 50 - this.handleLoadData = this.handleLoadData.bind(this); 51 - } 52 - 53 - async componentDidMount() { 54 - if (this.props.kernelStatus === KERNEL_STATUS.OFFLINE) { 55 - this.props.jupyterActions.launchKernel(); 56 - } 57 - 58 - const workspaceRawData = await readWorkspaceRawEEGData(this.props.title); 59 - this.setState({ 60 - eegFilePaths: workspaceRawData.map(filepath => ({ 61 - key: filepath.name, 62 - text: filepath.name, 63 - value: filepath.path 64 - })) 65 - }); 66 - } 67 - 68 - handleDropdownChange(e: Object, props: Object) { 69 - this.setState({ selectedFilePaths: props.value }); 70 - } 71 - 72 - handleLoadData() { 73 - this.props.jupyterActions.loadEpochs(this.state.selectedFilePaths); 74 - } 75 - 76 - renderEpochLabels() { 77 - if (!isNil(this.props.epochsInfo)) { 78 - const epochsInfo: { [string]: number } = { ...this.props.epochsInfo }; 79 - return ( 80 - <div> 81 - {Object.keys(epochsInfo).map((key, index) => ( 82 - <Segment key={key} basic> 83 - <Icon name={["smile", "home", "x", "book"][index]} /> 84 - {key} 85 - <p>{epochsInfo[key]} Trials</p> 86 - </Segment> 87 - ))} 88 - </div> 89 - ); 90 - } 91 - return <div />; 92 - } 93 - 94 - render() { 95 - return ( 96 - <div className={styles.mainContainer}> 97 - <Grid columns="equal" relaxed padded> 98 - <Grid.Column width={4}> 99 - <Segment raised padded color="red"> 100 - <Header as="h2">Data Sets</Header> 101 - <Segment basic> 102 - <Dropdown 103 - placeholder="Select Data Sets" 104 - fluid 105 - multiple 106 - selection 107 - closeOnChange 108 - options={this.state.eegFilePaths} 109 - onChange={this.handleDropdownChange} 110 - /> 111 - <Button 112 - disabled={this.props.kernelStatus !== KERNEL_STATUS.IDLE} 113 - loading={ 114 - this.props.kernelStatus === KERNEL_STATUS.STARTING || 115 - this.props.kernelStatus === KERNEL_STATUS.BUSY 116 - } 117 - onClick={this.handleLoadData} 118 - > 119 - Load 120 - </Button> 121 - </Segment> 122 - {this.renderEpochLabels()} 123 - </Segment> 124 - </Grid.Column> 125 - <Grid.Column width={12}> 126 - <Button onClick={this.props.jupyterActions.cleanEpochs}> 127 - Clean Epochs 128 - </Button> 129 - </Grid.Column> 130 - </Grid> 131 - </div> 132 - ); 133 - } 134 - }
+173
app/components/CleanComponent/CleanSidebar.js
··· 1 + import React, { Component } from 'react'; 2 + import { Segment, Header, Menu, Icon, Button, Grid } from 'semantic-ui-react'; 3 + import styles from '../styles/collect.css'; 4 + 5 + const HELP_STEP = { 6 + MENU: 0, 7 + SIGNAL_EXPLANATION: 1, 8 + SIGNAL_SALINE: 2, 9 + SIGNAL_CONTACT: 3, 10 + SIGNAL_MOVEMENT: 4, 11 + LEARN_BRAIN: 5, 12 + LEARN_BLINK: 6, 13 + LEARN_THOUGHTS: 7, 14 + LEARN_ALPHA: 8 15 + }; 16 + 17 + interface Props { 18 + handleClose: () => void; 19 + } 20 + 21 + interface State { 22 + helpStep: HELP_STEP; 23 + } 24 + export default class CleanSidebar extends Component<Props, State> { 25 + props: Props; 26 + constructor(props) { 27 + super(props); 28 + this.state = { 29 + helpStep: HELP_STEP.MENU 30 + }; 31 + this.handleStartLearn = this.handleStartLearn.bind(this); 32 + this.handleStartSignal = this.handleStartSignal.bind(this); 33 + this.handleNext = this.handleNext.bind(this); 34 + this.handleBack = this.handleBack.bind(this); 35 + } 36 + 37 + handleStartSignal() { 38 + this.setState({ helpStep: HELP_STEP.SIGNAL_EXPLANATION }); 39 + } 40 + 41 + handleStartLearn() { 42 + this.setState({ helpStep: HELP_STEP.LEARN_BRAIN }); 43 + } 44 + 45 + handleNext() { 46 + if ( 47 + this.state.helpStep === HELP_STEP.SIGNAL_MOVEMENT || 48 + this.state.helpStep === HELP_STEP.LEARN_ALPHA 49 + ) { 50 + this.setState({ helpStep: HELP_STEP.MENU }); 51 + } else { 52 + this.setState({ helpStep: this.state.helpStep + 1 }); 53 + } 54 + } 55 + 56 + handleBack() { 57 + this.setState({ helpStep: this.state.helpStep - 1 }); 58 + } 59 + 60 + renderMenu() { 61 + return ( 62 + <React.Fragment> 63 + <Menu secondary vertical fluid> 64 + <Header className={styles.helpHeader} as="h1"> 65 + What would you like to do? 66 + </Header> 67 + <Menu.Item onClick={this.handleStartSignal}> 68 + <Segment basic className={styles.helpMenuItem}> 69 + <Icon name="star outline" size="large" /> 70 + Improve the signal quality of your sensors 71 + </Segment> 72 + </Menu.Item> 73 + <Menu.Item onClick={this.handleStartLearn}> 74 + <Segment basic className={styles.helpMenuItem}> 75 + <Icon name="exclamation triangle" size="large" /> 76 + Learn about how the subjects movements create noise 77 + </Segment> 78 + </Menu.Item> 79 + </Menu> 80 + </React.Fragment> 81 + ); 82 + } 83 + 84 + renderHelp(header: string, content: string) { 85 + return ( 86 + <React.Fragment> 87 + <Segment basic className={styles.helpContent}> 88 + <Header className={styles.helpHeader} as="h1"> 89 + {header} 90 + </Header> 91 + {content} 92 + </Segment> 93 + <Grid columns="equal"> 94 + <Grid.Column> 95 + <Button fluid secondary onClick={this.handleBack}> 96 + Back 97 + </Button> 98 + </Grid.Column> 99 + <Grid.Column> 100 + <Button fluid primary onClick={this.handleNext}> 101 + Next 102 + </Button> 103 + </Grid.Column> 104 + </Grid> 105 + </React.Fragment> 106 + ); 107 + } 108 + 109 + renderHelpContent() { 110 + switch (this.state.helpStep) { 111 + case HELP_STEP.SIGNAL_EXPLANATION: 112 + return this.renderHelp( 113 + 'Improve the signal quality', 114 + 'In order to collect quality data, you want to make sure that all electrodes have a strong connection' 115 + ); 116 + case HELP_STEP.SIGNAL_SALINE: 117 + return this.renderHelp( 118 + 'Tip #1: Saturate the sensors in saline', 119 + 'Make sure the sensors are thoroughly soaked with saline solution. They should be wet to the touch' 120 + ); 121 + case HELP_STEP.SIGNAL_CONTACT: 122 + return this.renderHelp( 123 + 'Tip #2: Ensure the sensors are making firm contact', 124 + 'Re-seat the headset to make sure that all sensors contact the head with some tension. You may need to sweep hair out of the way to accomplish this' 125 + ); 126 + case HELP_STEP.SIGNAL_MOVEMENT: 127 + return this.renderHelp( 128 + 'Tip #3: Stay still', 129 + 'To reduce noise during your experiment, ensure your subject is relaxed and has both feet on the floor. Sometimes, focusing on relaxing the jaw and the tongue can improve the EEG signal' 130 + ); 131 + case HELP_STEP.LEARN_BRAIN: 132 + return this.renderHelp( 133 + 'Your brain produces electricity', 134 + 'Using the device that you are wearing, we can detect the electrical activity of your brain.' 135 + ); 136 + case HELP_STEP.LEARN_BLINK: 137 + return this.renderHelp( 138 + 'Try blinking your eyes', 139 + 'Does the signal change? Eye movements create noise in the EEG signal' 140 + ); 141 + case HELP_STEP.LEARN_THOUGHTS: 142 + return this.renderHelp( 143 + 'Try thinking of a cat', 144 + "Does the signal change? Although EEG can measure overall brain activity, it's not capable of reading minds" 145 + ); 146 + case HELP_STEP.LEARN_ALPHA: 147 + return this.renderHelp( 148 + 'Try closing your eyes for 10 seconds', 149 + 'You may notice a change in your signal due to an increase in alpha waves' 150 + ); 151 + case HELP_STEP.MENU: 152 + default: 153 + return this.renderMenu(); 154 + } 155 + } 156 + 157 + render() { 158 + return ( 159 + <Segment basic padded vertical className={styles.helpSidebar}> 160 + <Button 161 + basic 162 + circular 163 + size="large" 164 + floated="right" 165 + icon="x" 166 + className={styles.closeButton} 167 + onClick={this.props.handleClose} 168 + /> 169 + {this.renderHelpContent()} 170 + </Segment> 171 + ); 172 + } 173 + }
+226
app/components/CleanComponent/index.js
··· 1 + // @flow 2 + import React, { Component } from 'react'; 3 + import { 4 + Grid, 5 + Button, 6 + Icon, 7 + Segment, 8 + Header, 9 + Dropdown, 10 + Sidebar, 11 + SidebarPusher, 12 + Divider 13 + } from 'semantic-ui-react'; 14 + import { Link } from 'react-router-dom'; 15 + import { isNil } from 'lodash'; 16 + import styles from './../styles/collect.css'; 17 + import { EXPERIMENTS, DEVICES, KERNEL_STATUS } from '../../constants/constants'; 18 + import { Kernel } from '../../constants/interfaces'; 19 + import { readWorkspaceRawEEGData } from '../../utils/filesystem/storage'; 20 + import CleanSidebar from './CleanSidebar'; 21 + 22 + interface Props { 23 + type: ?EXPERIMENTS; 24 + title: string; 25 + deviceType: DEVICES; 26 + mainChannel: ?any; 27 + kernel: ?Kernel; 28 + kernelStatus: KERNEL_STATUS; 29 + epochsInfo: ?{ [string]: number }; 30 + jupyterActions: Object; 31 + subject: string; 32 + session: number; 33 + } 34 + 35 + interface State { 36 + subjects: Array<?string>; 37 + eegFilePaths: Array<?{ 38 + key: string, 39 + text: string, 40 + value: { name: string, dir: string } 41 + }>; 42 + selectedSubject: string; 43 + selectedFilePaths: Array<?string>; 44 + } 45 + 46 + export default class Clean extends Component<Props, State> { 47 + props: Props; 48 + state: State; 49 + handleRecordingChange: (Object, Object) => void; 50 + handleLoadData: () => void; 51 + handleSubjectChange: (Object, Object) => void; 52 + 53 + constructor(props: Props) { 54 + super(props); 55 + this.state = { 56 + subjects: [], 57 + eegFilePaths: [{ key: '', text: '', value: '' }], 58 + selectedFilePaths: [], 59 + selectedSubject: props.subject 60 + }; 61 + this.handleRecordingChange = this.handleRecordingChange.bind(this); 62 + this.handleLoadData = this.handleLoadData.bind(this); 63 + this.handleSidebarToggle = this.handleSidebarToggle.bind(this); 64 + this.handleSubjectChange = this.handleSubjectChange.bind(this); 65 + } 66 + 67 + async componentDidMount() { 68 + if (this.props.kernelStatus === KERNEL_STATUS.OFFLINE) { 69 + this.props.jupyterActions.launchKernel(); 70 + } 71 + 72 + const workspaceRawData = await readWorkspaceRawEEGData(this.props.title); 73 + this.setState({ 74 + subjects: workspaceRawData 75 + .map(filepath => filepath.name.slice(0, filepath.name.length - 10)) 76 + .reduce((acc, curr) => { 77 + if (acc.find(subject => subject.key === curr)) { 78 + return acc; 79 + } 80 + return acc.concat({ 81 + key: curr, 82 + text: curr, 83 + value: curr 84 + }); 85 + }, []) 86 + }); 87 + this.setState({ 88 + eegFilePaths: workspaceRawData.map(filepath => ({ 89 + key: filepath.name, 90 + text: filepath.name, 91 + value: filepath.path 92 + })) 93 + }); 94 + } 95 + 96 + handleRecordingChange(e: Object, data: Object) { 97 + this.setState({ selectedFilePaths: data.value }); 98 + } 99 + 100 + handleSubjectChange(event: Object, data: Object) { 101 + this.setState({ selectedSubject: data.value, selectedFilePaths: [] }); 102 + } 103 + 104 + handleLoadData() { 105 + this.props.jupyterActions.loadEpochs(this.state.selectedFilePaths); 106 + } 107 + 108 + handleSidebarToggle() { 109 + this.setState({ isSidebarVisible: !this.state.isSidebarVisible }); 110 + } 111 + 112 + renderEpochLabels() { 113 + if (!isNil(this.props.epochsInfo)) { 114 + const epochsInfo: { [string]: number } = { ...this.props.epochsInfo }; 115 + return ( 116 + <Segment basic textAlign="left"> 117 + <Header as="h3">Loaded Epochs:</Header> 118 + {Object.keys(epochsInfo).map((key, index) => ( 119 + <Segment key={key} basic> 120 + <Icon name={['smile', 'home', 'x', 'book'][index]} /> 121 + {key} 122 + <p>{epochsInfo[key]} Trials</p> 123 + </Segment> 124 + ))} 125 + <Link to="/analyze"> 126 + <Button primary>Analyze Dataset</Button> 127 + </Link> 128 + </Segment> 129 + ); 130 + } 131 + return <div />; 132 + } 133 + 134 + render() { 135 + return ( 136 + <Sidebar.Pushable basic as={Segment} className={styles.preTestPushable}> 137 + <Sidebar 138 + width="wide" 139 + direction="right" 140 + as={Segment} 141 + visible={this.state.isSidebarVisible} 142 + > 143 + <CleanSidebar handleClose={this.handleSidebarToggle} /> 144 + </Sidebar> 145 + <SidebarPusher> 146 + <Grid 147 + columns="equal" 148 + textAlign="center" 149 + verticalAlign="middle" 150 + className={styles.preTestContainer} 151 + > 152 + <Grid.Row columns="equal"> 153 + <Grid.Column> 154 + <Header as="h1" floated="left"> 155 + Clean 156 + </Header> 157 + </Grid.Column> 158 + </Grid.Row> 159 + <Grid.Row> 160 + <Grid.Column width={6}> 161 + <Segment basic textAlign="left" className={styles.infoSegment}> 162 + <Header as="h1">Select & Clean</Header> 163 + <p> 164 + Ready to clean some data? Select a subject and one or more 165 + EEG recordings, then launch the editor 166 + </p> 167 + <Header as="h3">Select Subject</Header> 168 + <Dropdown 169 + fluid 170 + selection 171 + closeOnChange 172 + value={this.state.selectedSubject} 173 + options={this.state.subjects} 174 + onChange={this.handleSubjectChange} 175 + /> 176 + <Header as="h3">Select Recordings</Header> 177 + <Dropdown 178 + fluid 179 + multiple 180 + selection 181 + closeOnChange 182 + value={this.state.selectedFilePaths} 183 + options={this.state.eegFilePaths.filter( 184 + filepath => 185 + filepath.key.slice(0, filepath.key.length - 10) === 186 + this.state.selectedSubject 187 + )} 188 + onChange={this.handleRecordingChange} 189 + /> 190 + <Divider hidden section /> 191 + <Grid textAlign="center" columns="equal"> 192 + <Grid.Column> 193 + <Button 194 + secondary 195 + disabled={ 196 + this.props.kernelStatus !== KERNEL_STATUS.IDLE 197 + } 198 + loading={ 199 + this.props.kernelStatus === KERNEL_STATUS.STARTING || 200 + this.props.kernelStatus === KERNEL_STATUS.BUSY 201 + } 202 + onClick={this.handleLoadData} 203 + > 204 + Load Dataset 205 + </Button> 206 + </Grid.Column> 207 + <Grid.Column> 208 + <Button 209 + primary 210 + disabled={isNil(this.props.epochsInfo)} 211 + onClick={this.props.jupyterActions.cleanEpochs} 212 + > 213 + Launch Editor 214 + </Button> 215 + </Grid.Column> 216 + </Grid> 217 + </Segment> 218 + </Grid.Column> 219 + <Grid.Column width={4}>{this.renderEpochLabels()}</Grid.Column> 220 + </Grid.Row> 221 + </Grid> 222 + </SidebarPusher> 223 + </Sidebar.Pushable> 224 + ); 225 + } 226 + }
+9
app/components/CollectComponent/PreTestComponent.js
··· 7 7 Header, 8 8 Sidebar 9 9 } from 'semantic-ui-react'; 10 + import Mousetrap from 'mousetrap'; 10 11 import ViewerComponent from '../ViewerComponent'; 11 12 import SignalQualityIndicatorComponent from '../SignalQualityIndicatorComponent'; 12 13 import PreviewExperimentComponent from '../PreviewExperimentComponent'; ··· 57 58 }; 58 59 this.handlePreview = this.handlePreview.bind(this); 59 60 this.handleSidebarToggle = this.handleSidebarToggle.bind(this); 61 + } 62 + 63 + componentDidMount() { 64 + Mousetrap.bind('esc', this.props.experimentActions.stop); 65 + } 66 + 67 + componentWillUnmount() { 68 + Mousetrap.unbind('esc'); 60 69 } 61 70 62 71 handlePreview() {
+15 -9
app/components/CollectComponent/RunComponent.js
··· 112 112 renderCleanButton() { 113 113 if (this.props.session > 1) { 114 114 return ( 115 - <Link to="/clean"> 116 - <Button fluid secondary> 117 - Clean Data 118 - </Button> 119 - </Link> 115 + <Grid.Column> 116 + <Link to="/clean"> 117 + <Button fluid secondary> 118 + Clean Data 119 + </Button> 120 + </Link> 121 + </Grid.Column> 120 122 ); 121 123 } 122 124 } ··· 146 148 Session Number: <b>{this.props.session}</b> 147 149 </Segment> 148 150 <Divider hidden section /> 149 - <Button fluid primary onClick={this.handleStartExperiment}> 150 - Run Experiment 151 - </Button> 152 - {this.renderCleanButton()} 151 + <Grid textAlign="center" columns="equal"> 152 + <Grid.Column> 153 + <Button fluid primary onClick={this.handleStartExperiment}> 154 + Run Experiment 155 + </Button> 156 + </Grid.Column> 157 + {this.renderCleanButton()} 158 + </Grid> 153 159 </Segment> 154 160 </div> 155 161 );
+1
app/components/styles/collect.css
··· 32 32 33 33 .preTestPushable { 34 34 height: 100vh !important; 35 + background: linear-gradient(#f9f9f9, #f0f0ff) !important; 35 36 } 36 37 37 38 .previewColumn {
+7 -5
app/containers/CleanContainer.js
··· 1 1 // @flow 2 - import { bindActionCreators } from "redux"; 3 - import { connect } from "react-redux"; 4 - import CleanComponent from "../components/CleanComponent"; 5 - import * as experimentActions from "../actions/experimentActions"; 6 - import * as jupyterActions from "../actions/jupyterActions"; 2 + import { bindActionCreators } from 'redux'; 3 + import { connect } from 'react-redux'; 4 + import CleanComponent from '../components/CleanComponent'; 5 + import * as experimentActions from '../actions/experimentActions'; 6 + import * as jupyterActions from '../actions/jupyterActions'; 7 7 8 8 function mapStateToProps(state) { 9 9 return { 10 10 type: state.experiment.type, 11 11 title: state.experiment.title, 12 + subject: state.experiment.subject, 13 + session: state.experiment.session, 12 14 deviceType: state.device.deviceType, 13 15 ...state.jupyter 14 16 };
+1
app/epics/experimentEpics.js
··· 128 128 129 129 const experimentStopEpic = (action$, state$) => 130 130 action$.ofType(STOP).pipe( 131 + filter(() => state$.value.experiment.isRunning), 131 132 map(getBehaviouralData), 132 133 map(csv => 133 134 storeBehaviouralData(
-1
app/reducers/experimentReducer.js
··· 80 80 }; 81 81 82 82 case SET_TIMELINE: 83 - console.log(action.payload); 84 83 return { 85 84 ...state, 86 85 ...action.payload
+1 -1
app/utils/jupyter/utils.py
··· 202 202 203 203 204 204 def get_epochs_info(epochs, events, event_id): 205 - return {"totalEpochs": len(epochs.events), "dropPercentage": (1 - len(epochs.events)/len(events)) * 100, **{x: len(epochs[x]) for x in event_id}} 205 + return {"totalEpochs": len(epochs.events), "dropPercentage": round((1 - len(epochs.events)/len(events)) * 100, 2), **{x: len(epochs[x]) for x in event_id}}