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.

fixed bugs

authored by

Yury Shevchenko and committed by
Teon L Brooks
46f06c50 246f4507

+507 -286
+11 -11
app/components/AnalyzeComponent.js
··· 301 301 return ( 302 302 <div> 303 303 {this.props.epochsInfo 304 + .filter( infoObj => (infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs')) 304 305 .map((infoObj, index) => ( 305 306 <React.Fragment key={infoObj.name}> 306 - <Header as='h4'>{infoObj.name}</Header> 307 - <Icon name='circle' color={['red', 'green'][index]} /> 307 + <Header as="h4">{infoObj.name}</Header> 308 + <Icon name="circle" color={['red', 'orange', 'green', 'blue'][index]} /> 308 309 {infoObj.value} 309 310 </React.Fragment> 310 - )) 311 - .slice(0, 2)} 311 + ))} 312 312 </div> 313 313 ); 314 314 } ··· 459 459 Export 460 460 </Button> 461 461 </div> 462 - <p></p> 462 + <p /> 463 463 464 464 <Dropdown 465 465 fluid ··· 472 472 onChange={this.handleBehaviorDatasetChange} 473 473 onClick={this.handleDropdownClick} 474 474 /> 475 - <p></p> 475 + <p /> 476 476 <Divider hidden /> 477 - <span className='ui header'>Dependent Variable</span> 478 - <p></p> 477 + <span className="ui header">Dependent Variable</span> 478 + <p /> 479 479 <Dropdown 480 480 fluid 481 481 selection ··· 489 489 <Grid.Column width={8} style={{ overflow: 'auto', maxHeight: 650 }}> 490 490 <Segment basic textAlign='left' className={styles.plotSegment}> 491 491 <Plot data={this.state.dataToPlot} layout={this.state.layout} /> 492 - <p></p> 492 + <p /> 493 493 <Checkbox 494 494 checked={this.state.removeOutliers} 495 - label='Remove outliers' 495 + label="Remove Response Time Outliers" 496 496 onChange={this.handleRemoveOutliers} 497 497 /> 498 498 499 - <p></p> 499 + <p /> 500 500 <Button.Group> 501 501 <Button 502 502 className='tertiary'
+8 -8
app/components/CollectComponent/HelpSidebar.js
··· 160 160 render() { 161 161 return ( 162 162 <Segment basic padded vertical className={styles.helpSidebar}> 163 - <Button 164 - circular 165 - size='large' 166 - floated='right' 167 - icon='x' 168 - className={styles.closeButton} 169 - onClick={this.props.handleClose} 170 - /> 163 + <Segment basic className={styles.closeButton}> 164 + <Button 165 + circular 166 + size="big" 167 + icon="x" 168 + onClick={this.props.handleClose} 169 + /> 170 + </Segment> 171 171 {this.renderHelpContent()} 172 172 </Segment> 173 173 );
+7 -3
app/components/CollectComponent/PreTestComponent.js
··· 68 68 69 69 handlePreview(e) { 70 70 e.target.blur(); 71 - this.setState({ isPreviewing: !this.state.isPreviewing }); 71 + this.setState({ 72 + isSidebarVisible: false, 73 + isPreviewing: !this.state.isPreviewing 74 + }); 72 75 } 73 76 74 77 handleSidebarToggle() { ··· 85 88 type={this.props.type} 86 89 paradigm={this.props.paradigm} 87 90 previewParams={this.props.params} 91 + title={this.props.title} 88 92 /> 89 93 ); 90 94 } ··· 158 162 </Grid.Column> 159 163 </Grid.Row> 160 164 <Grid.Row> 161 - <Grid.Column width={6} className={styles.previewEEGWindow}> 165 + <Grid.Column width={8} className={styles.previewEEGWindow}> 162 166 {this.renderSignalQualityOrPreview()} 163 167 </Grid.Column> 164 - <Grid.Column width={10}> 168 + <Grid.Column width={8}> 165 169 <ViewerComponent 166 170 signalQualityObservable={this.props.signalQualityObservable} 167 171 deviceType={this.props.deviceType}
+8 -3
app/components/CollectComponent/RunComponent.js
··· 117 117 if (!this.props.isRunning) { 118 118 return ( 119 119 <div className={styles.mainContainer}> 120 - <Segment basic textAlign='left' className={styles.descriptionContainer} vertical> 121 - <Header as='h1'>{this.props.type}</Header> 120 + <Segment 121 + basic 122 + textAlign="left" 123 + className={styles.descriptionContainer} 124 + vertical 125 + > 126 + <Header as="h1">{this.props.title}</Header> 122 127 <Segment basic className={styles.infoSegment}> 123 128 Subject Name: <b>{this.props.subject}</b> 124 129 <Button ··· 153 158 Run Experiment 154 159 </Button> 155 160 </Grid.Column> 156 - {this.renderCleanButton()} 157 161 </Grid> 158 162 </Segment> 159 163 </div> ··· 163 167 <div className={styles.experimentWindow}> 164 168 <ExperimentWindow 165 169 settings={{ 170 + title: this.props.title, 166 171 script: this.props.paradigm, 167 172 params: this.props.params, 168 173 eventCallback: this.insertLabJsCallback(),
+1
app/components/CollectComponent/index.js
··· 132 132 group={this.props.group} 133 133 session={this.props.session} 134 134 openRunComponent={this.handleRunComponentOpen} 135 + title={this.props.title} 135 136 /> 136 137 </React.Fragment> 137 138 );
+278 -230
app/components/DesignComponent/CustomDesignComponent.js
··· 27 27 CONDITIONS: 'CONDITIONS', 28 28 TRIALS: 'TRIALS', 29 29 PARAMETERS: 'PARAMETERS', 30 - PREVIEW: 'PREVIEW', 30 + INSTRUCTIONS: 'INSTRUCTIONS', 31 + PREVIEW: 'PREVIEW' 31 32 }; 32 33 33 34 const FIELDS = { ··· 72 73 super(props); 73 74 this.state = { 74 75 activeStep: CUSTOM_STEPS.OVERVIEW, 75 - isPreviewing: false, 76 + isPreviewing: true, 76 77 description: props.description, 77 78 params: props.params, 78 79 saved: false, ··· 90 91 } 91 92 92 93 endPreview() { 93 - this.setState({ isPreviewing: false }); 94 + // this.setState({ isPreviewing: false }); 94 95 } 95 96 96 97 handleStepClick(step: string) { ··· 132 133 { name: 'stimulus4', number: 4 }, 133 134 ]; 134 135 switch (this.state.activeStep) { 136 + 137 + case CUSTOM_STEPS.OVERVIEW: 138 + default: 139 + return ( 140 + <Grid 141 + stretched 142 + relaxed 143 + padded 144 + columns="equal" 145 + className={styles.contentGrid} 146 + > 147 + <Grid.Column stretched verticalAlign="middle"> 148 + <Image 149 + as={Segment} 150 + basic 151 + centered 152 + src={researchQuestionImage} 153 + className={styles.overviewImage} 154 + /> 155 + <Form> 156 + <Form.TextArea 157 + autoHeight 158 + style={{ minHeight: 100, maxHeight: 400 }} 159 + label={FIELDS.QUESTION} 160 + value={this.state.description.question} 161 + placeholder="Explain your research question here." 162 + onChange={(event, data) => 163 + this.setState({ 164 + description: { 165 + ...this.state.description, 166 + question: data.value 167 + }, 168 + saved: false, 169 + }) 170 + } 171 + /> 172 + </Form> 173 + </Grid.Column> 174 + <Grid.Column stretched verticalAlign="middle"> 175 + <Image 176 + as={Segment} 177 + basic 178 + centered 179 + src={hypothesisImage} 180 + className={styles.overviewImage} 181 + /> 182 + <Form> 183 + <Form.TextArea 184 + autoHeight 185 + style={{ minHeight: 100, maxHeight: 400 }} 186 + label={FIELDS.HYPOTHESIS} 187 + value={this.state.description.hypothesis} 188 + placeholder="Describe your hypothesis here." 189 + onChange={(event, data) => 190 + this.setState({ 191 + description: { 192 + ...this.state.description, 193 + hypothesis: data.value 194 + }, 195 + saved: false, 196 + }) 197 + } 198 + /> 199 + </Form> 200 + </Grid.Column> 201 + <Grid.Column verticalAlign="middle"> 202 + <Image 203 + as={Segment} 204 + basic 205 + centered 206 + src={methodsImage} 207 + className={styles.overviewImage} 208 + /> 209 + <Form> 210 + <Form.TextArea 211 + autoHeight 212 + style={{ minHeight: 100, maxHeight: 400 }} 213 + label={FIELDS.METHODS} 214 + value={this.state.description.methods} 215 + placeholder="Explain how you will design your experiment to answer the question here." 216 + onChange={(event, data) => 217 + this.setState({ 218 + description: { 219 + ...this.state.description, 220 + methods: data.value 221 + }, 222 + saved: false, 223 + }) 224 + } 225 + /> 226 + </Form> 227 + </Grid.Column> 228 + </Grid> 229 + ); 230 + 135 231 case CUSTOM_STEPS.CONDITIONS: 136 232 return ( 137 233 <Grid> ··· 139 235 <Header as='h1'>Conditions</Header> 140 236 <p> 141 237 Select the folder with images for each condition and choose the correct response. 238 + You can upload image files with the following extensions: ".png", ".jpg", ".jpeg". 239 + Make sure when you preview your experiment that the resolution is high enough. 142 240 </p> 143 241 </Segment> 144 242 ··· 203 301 </Table> 204 302 </Grid> 205 303 ); 206 - case CUSTOM_STEPS.TRIALS: 207 - return ( 208 - <Grid> 209 - <div className={styles.trialsHeader}> 210 - <div> 211 - <Header as='h1'>Trials</Header> 212 - <p>Edit the name, condition and correct key response of each trial.</p> 304 + 305 + case CUSTOM_STEPS.TRIALS: 306 + return ( 307 + <Grid> 308 + <div className={styles.trialsHeader}> 309 + <div> 310 + <Header as="h1">Trials</Header> 311 + <p> 312 + Edit the name, condition and correct key response of each trial. 313 + </p> 314 + </div> 315 + 316 + <div> 317 + <Form style={{ alignSelf: 'flex-end' }}> 318 + <Form.Group className={styles.trialsTopInfoBar}> 319 + <Form.Select 320 + fluid 321 + selection 322 + label="Order" 323 + value={this.state.params.randomize} 324 + onChange={(event, data) => 325 + this.setState({ 326 + params: { 327 + ...this.state.params, 328 + randomize: data.value 329 + }, 330 + saved: false, 331 + }) 332 + } 333 + placeholder="Response" 334 + options={[{key: 'random', text: 'Random', value: 'random'}, 335 + {key: 'sequential', text: 'Sequential', value: 'sequential'}]} 336 + /> 337 + <Form.Input 338 + label="Total experimental trials" 339 + type="number" 340 + fluid 341 + value={this.state.params.nbTrials} 342 + onChange={(event, data) => 343 + this.setState({ 344 + params: { 345 + ...this.state.params, 346 + nbTrials: parseInt(data.value) 347 + }, 348 + saved: false, 349 + }) 350 + } 351 + /> 352 + <Form.Input 353 + label="Total practice trials" 354 + type="number" 355 + fluid 356 + value={this.state.params.nbPracticeTrials} 357 + onChange={(event, data) => 358 + this.setState({ 359 + params: { 360 + ...this.state.params, 361 + nbPracticeTrials: parseInt(data.value) 362 + }, 363 + saved: false, 364 + }) 365 + } 366 + /> 367 + </Form.Group> 368 + </Form> 369 + </div> 213 370 </div> 214 371 215 372 <div> ··· 400 557 /> 401 558 </Segment> 402 559 403 - {!this.state.params.selfPaced ? ( 560 + </Grid> 561 + ); 562 + 563 + case CUSTOM_STEPS.PARAMETERS: 564 + return ( 565 + <Grid> 566 + <Grid.Column width={8} style={{display: 'grid', 'alignContent': 'space-between'}}> 404 567 <Segment basic> 568 + <Header as="h1">Inter-trial interval</Header> 569 + <p> 570 + Select the inter-trial interval duration. This is the amount 571 + of time between trials measured from the end of one trial to 572 + the start of the next one. 573 + </p> 574 + </Segment> 575 + <Segment basic style={{'marginTop': '100px'}}> 405 576 <ParamSlider 406 577 label='Presentation time (seconds)' 407 578 value={this.state.params.presentationTime} ··· 418 589 ms_conversion='250' 419 590 onChange={(value) => 420 591 this.setState({ 421 - params: { 422 - ...this.state.params, 423 - presentationTime: value, 424 - }, 592 + params: { ...this.state.params, iti: value }, 593 + saved: false, 594 + }) 595 + } 596 + /> 597 + </Segment> 598 + </Grid.Column> 599 + 600 + <Grid.Column width={8} style={{display: 'grid', 'alignContent': 'space-between'}}> 601 + <Segment basic> 602 + <Header as="h1">Image duration</Header> 603 + <p> 604 + Select the time of presentation or make it self-paced - present the image until participants respond. 605 + </p> 606 + </Segment> 607 + <Segment basic> 608 + <Checkbox 609 + defaultChecked={this.state.params.selfPaced} 610 + label="Self-paced data collection" 611 + onChange={value => 612 + this.setState({ 613 + params: { ...this.state.params, selfPaced: !this.state.params.selfPaced }, 425 614 saved: false, 426 615 }) 427 616 } 428 617 /> 429 618 </Segment> 430 - ) : ( 431 - <Segment basic style={{ marginBottom: '85px' }} /> 432 - )} 433 - </Grid.Column> 434 - </Grid> 435 - ); 436 - case CUSTOM_STEPS.PREVIEW: 619 + 620 + 621 + {!this.state.params.selfPaced ? <Segment basic><ParamSlider 622 + label="Presentation time (seconds)" 623 + value={this.state.params.presentationTime} 624 + marks={{ 625 + 1: '0.25', 626 + 2: '0.5', 627 + 3: '0.75', 628 + 4: '1', 629 + 5: '1.25', 630 + 6: '1.5', 631 + 7: '1.75', 632 + 8: '2' 633 + }} 634 + ms_conversion='250' 635 + onChange={value => 636 + this.setState({ 637 + params: { ...this.state.params, presentationTime: value }, 638 + saved: false, 639 + }) 640 + } 641 + /> 642 + </Segment> : <Segment basic style={{'margin-bottom': '85px'}} />} 643 + 644 + </Grid.Column> 645 + 646 + </Grid> 647 + ); 648 + 649 + case CUSTOM_STEPS.INSTRUCTIONS: 437 650 return ( 438 - <Grid relaxed padded className={styles.contentGrid}> 439 - <Grid.Column 440 - stretched 441 - width={11} 442 - textAlign='right' 443 - verticalAlign='middle' 444 - className={styles.previewWindow} 445 - > 446 - <PreviewExperimentComponent 447 - {...loadProtocol(this.props.paradigm)} 448 - isPreviewing={this.state.isPreviewing} 449 - onEnd={this.endPreview} 450 - type={this.props.type} 451 - paradigm={this.props.paradigm} 452 - previewParams={this.props.params} 453 - /> 454 - </Grid.Column> 455 - <Grid.Column width={5} verticalAlign='middle'> 651 + <Grid stretched> 652 + <Grid.Column width={8} stretched style={{display: 'grid', 'alignContent': 'space-between'}}> 456 653 <Segment basic> 654 + <Header as="h1">Experiment Instructions</Header> 655 + <p> 656 + Edit the instruction that will be displayed on the first screen. 657 + </p> 457 658 <Form> 458 659 <Form.TextArea 459 660 autoHeight 460 - style={{ minHeight: 100 }} 461 - label={FIELDS.INTRO} 462 661 value={this.state.params.intro} 463 - placeholder='You will view a series of images...' 662 + placeholder="e.g., You will view a series of faces and houses. Press 1 when a face appears and 9 for a house. Press the the space bar on your keyboard to start doing the practice trials. If you want to skip the practice trials and go directly to the task, press the 'q' button on your keyboard." 464 663 onChange={(event, data) => 465 664 this.setState({ 466 665 params: { ...this.state.params, intro: data.value }, ··· 470 669 /> 471 670 </Form> 472 671 </Segment> 672 + </Grid.Column> 473 673 674 + <Grid.Column width={8} stretched style={{display: 'grid', 'alignContent': 'space-between'}}> 474 675 <Segment basic> 676 + <Header as="h1">Instructions for the task screen</Header> 677 + <p> 678 + Edit the instruction that will be displayed in the footer during the task. 679 + </p> 475 680 <Form> 476 681 <Form.TextArea 477 682 autoHeight 478 - style={{ minHeight: 50 }} 479 - label={FIELDS.HELP} 480 683 value={this.state.params.taskHelp} 481 - placeholder='Press 1 for ...' 684 + placeholder="e.g., Press 1 for a face and 9 for a house" 482 685 onChange={(event, data) => 483 - this.setState({ 484 - params: { ...this.state.params, taskHelp: data.value }, 485 - saved: false, 486 - }) 487 - } 686 + this.setState({ 687 + params: { ...this.state.params, taskHelp: data.value }, 688 + saved: false, 689 + }) 690 + } 488 691 /> 489 692 </Form> 490 693 </Segment> 491 - 492 - <PreviewButton 493 - isPreviewing={this.state.isPreviewing} 494 - onClick={(e) => this.handlePreview(e)} 495 - /> 496 694 </Grid.Column> 497 695 </Grid> 498 696 ); 499 697 500 - case CUSTOM_STEPS.OVERVIEW: 501 - default: 698 + case CUSTOM_STEPS.PREVIEW: 502 699 return ( 503 - <Grid stretched relaxed padded columns='equal' className={styles.contentGrid}> 504 - <Grid.Column stretched verticalAlign='middle'> 505 - <Image 506 - as={Segment} 507 - basic 508 - centered 509 - src={researchQuestionImage} 510 - className={styles.overviewImage} 700 + <Grid relaxed padded className={styles.contentGrid}> 701 + <Grid.Column 702 + stretched 703 + width={14} 704 + textAlign="right" 705 + verticalAlign="middle" 706 + className={styles.previewWindow} 707 + > 708 + <PreviewExperimentComponent 709 + {...loadProtocol(this.props.paradigm)} 710 + isPreviewing={this.state.isPreviewing} 711 + onEnd={this.endPreview} 712 + type={this.props.type} 713 + paradigm={this.props.paradigm} 714 + previewParams={this.props.params} 715 + title={this.props.title} 511 716 /> 512 - <Form> 513 - <Form.TextArea 514 - autoHeight 515 - style={{ minHeight: 100, maxHeight: 400 }} 516 - label={FIELDS.QUESTION} 517 - value={this.state.description.question} 518 - placeholder='Explain your research question here.' 519 - onChange={(event, data) => 520 - this.setState({ 521 - description: { 522 - ...this.state.description, 523 - question: data.value, 524 - }, 525 - saved: false, 526 - }) 527 - } 528 - /> 529 - </Form> 530 717 </Grid.Column> 531 - <Grid.Column stretched verticalAlign='middle'> 532 - <Image 533 - as={Segment} 534 - basic 535 - centered 536 - src={hypothesisImage} 537 - className={styles.overviewImage} 538 - /> 539 - <Form> 540 - <Form.TextArea 541 - autoHeight 542 - style={{ minHeight: 100, maxHeight: 400 }} 543 - label={FIELDS.HYPOTHESIS} 544 - value={this.state.description.hypothesis} 545 - placeholder='Describe your hypothesis here.' 546 - onChange={(event, data) => 547 - this.setState({ 548 - description: { 549 - ...this.state.description, 550 - hypothesis: data.value, 551 - }, 552 - saved: false, 553 - }) 554 - } 555 - /> 556 - </Form> 557 - </Grid.Column> 558 - <Grid.Column verticalAlign='middle'> 559 - <Image 560 - as={Segment} 561 - basic 562 - centered 563 - src={methodsImage} 564 - className={styles.overviewImage} 565 - /> 566 - <Form> 567 - <Form.TextArea 568 - autoHeight 569 - style={{ minHeight: 100, maxHeight: 400 }} 570 - label={FIELDS.METHODS} 571 - value={this.state.description.methods} 572 - placeholder='Explain how you will design your experiment to answer the question here.' 573 - onChange={(event, data) => 574 - this.setState({ 575 - description: { 576 - ...this.state.description, 577 - methods: data.value, 578 - }, 579 - saved: false, 580 - }) 581 - } 718 + 719 + <Grid.Column width={2} verticalAlign="top"> 720 + <Segment basic> 721 + <PreviewButton 722 + isPreviewing={this.state.isPreviewing} 723 + onClick={e => this.handlePreview(e)} 582 724 /> 583 - </Form> 725 + </Segment> 584 726 </Grid.Column> 585 727 </Grid> 586 728 ); ··· 621 763 ); 622 764 } 623 765 } 624 - 625 - // saveButton={ 626 - // <Button 627 - // compact 628 - // size="small" 629 - // secondary 630 - // onClick={() => { 631 - // this.handleSaveParams(); 632 - // // this.props.experimentActions.saveWorkspace() 633 - // }} 634 - // > 635 - // {this.state.saved ? 'Saved' : 'Save'} 636 - // </Button> 637 - // } 638 - 639 - // 640 - // button={ 641 - // <Button 642 - // compact 643 - // size="small" 644 - // primary 645 - // onClick={() => { 646 - // this.handleSaveParams(); 647 - // this.handleStartExperiment(); 648 - // }} 649 - // > 650 - // Start 651 - // </Button> 652 - // } 653 - // 654 - // <Button 655 - // compact 656 - // size="small" 657 - // secondary 658 - // onClick={() => { 659 - // this.handleSaveParams(); 660 - // }} 661 - // > 662 - // {this.state.saved ? 'Saved' : 'Save'} 663 - // </Button> 664 - // enableEEGToggle={ 665 - // <Checkbox 666 - // defaultChecked={this.props.isEEGEnabled} 667 - // label="Enable EEG" 668 - // onChange={(event, data) => this.handleEEGEnabled(event, data)} 669 - // /> 670 - // } 671 - // 672 - // <Segment basic style={{ overflow: 'auto', maxHeight: 400 }}> 673 - // <Form> 674 - // {this.state.params.stimuli && this.state.params.stimuli.map((e,num) => ( 675 - // <StimuliRow 676 - // key={num} 677 - // num={num} 678 - // conditions={[1,2,3,4].map(n => this.state.params[`stimulus${n}`].title)} 679 - // {...e} 680 - // onDelete={(num) => { 681 - // const stimuli = this.state.params.stimuli; 682 - // stimuli.splice(num, 1); 683 - // const nbPracticeTrials = stimuli.filter(s => (s.phase === 'practice')).length; 684 - // const nbTrials = stimuli.filter(s => (s.phase === 'main')).length; 685 - // this.setState({ 686 - // params: { 687 - // ...this.state.params, 688 - // stimuli: [ ... stimuli ], 689 - // nbPracticeTrials, 690 - // nbTrials, 691 - // }, 692 - // saved: false, 693 - // }) 694 - // }} 695 - // onChange={(num, key, data) => { 696 - // const stimuli = this.state.params.stimuli; 697 - // stimuli[num][key] = data; 698 - // let nbPracticeTrials = this.state.params.nbPracticeTrials; 699 - // let nbTrials = this.state.params.nbTrials; 700 - // if(key === 'phase'){ 701 - // nbPracticeTrials = stimuli.filter(s => (s.phase === 'practice')).length; 702 - // nbTrials = stimuli.filter(s => (s.phase === 'main')).length; 703 - // } 704 - // this.setState({ 705 - // params: { 706 - // ...this.state.params, 707 - // stimuli: [ ... stimuli ], 708 - // nbPracticeTrials, 709 - // nbTrials, 710 - // }, 711 - // saved: false, 712 - // }) 713 - // }} 714 - // /> 715 - // ))} 716 - // </Form> 717 - // </Segment>
+1 -8
app/components/DesignComponent/StimuliRow.js
··· 67 67 > 68 68 {this.props.phase === 'main' ? 'Experimental' : 'Practice'} 69 69 </div> 70 - <Dropdown 71 - fluid 72 - style={{ 73 - display: 'grid', 74 - color: '#C4C4C4', 75 - justifyContent: 'end', 76 - }} 77 - > 70 + <Dropdown fluid style={{ display: 'grid', color: '#C4C4C4', justifyContent: 'end' }}> 78 71 <Dropdown.Menu> 79 72 <Dropdown.Item onClick={() => this.props.onChange(this.props.num, 'phase', 'main')}> 80 73 <div>Experimental</div>
+2 -1
app/components/DesignComponent/index.js
··· 128 128 this.props.experimentActions.createNewWorkspace({ 129 129 title, 130 130 type: EXPERIMENTS.CUSTOM, 131 - paradigm: this.props.paradigm, 131 + paradigm: 'Custom', 132 + // paradigm: this.props.paradigm 132 133 }); 133 134 this.props.experimentActions.saveWorkspace(); 134 135 }
+1
app/components/InputModal.js
··· 78 78 error={this.state.isError} 79 79 onChange={this.handleTextEntry} 80 80 onKeyDown={this.handleEnterSubmit} 81 + autoFocus 81 82 /> 82 83 </Modal.Content> 83 84 <Modal.Actions>
+6 -1
app/components/PreviewExperimentComponent.js
··· 32 32 33 33 render() { 34 34 if (!this.props.isPreviewing) { 35 - return <Segment basic />; 35 + return ( 36 + <div className={styles.previewPlaceholder}> 37 + <Segment basic> The experiment will be shown in the window </Segment> 38 + </div> 39 + ) 36 40 } 37 41 return ( 38 42 <div className={styles.previewExpComponent}> 39 43 <ExperimentWindow 40 44 settings={{ 45 + title: this.props.title, 41 46 script: this.props.paradigm, 42 47 params: this.props.previewParams || this.props.params, 43 48 eventCallback: this.insertPreviewLabJsCallback,
+1 -1
app/components/SecondaryNavComponent/index.js
··· 67 67 )} 68 68 <Dropdown.Item> 69 69 <NavLink to={SCREENS.HOME.route}> 70 - <p>Exit Workspace</p> 70 + <p>Exit Experiment</p> 71 71 </NavLink> 72 72 </Dropdown.Item> 73 73 </Dropdown.Menu>
+4 -1
app/components/TopNavComponent/index.js
··· 47 47 <Grid.Column className={styles.experimentTitleGridColumn}> 48 48 <Segment basic className={styles.experimentTitleSegment}> 49 49 <NavLink to={SCREENS.HOME.route}> 50 - <Image centered className={styles.exitWorkspaceBtn} src={BrainwavesIcon} /> 50 + <div className={styles.homeTopNavButton}> 51 + <Image centered className={styles.exitWorkspaceBtn} src={BrainwavesIcon} /> 52 + Home 53 + </div> 51 54 </NavLink> 52 55 {this.props.title ? this.props.title : 'Untitled'} 53 56 </Segment>
+11 -1
app/components/styles/collect.css
··· 23 23 left: -40px; 24 24 } 25 25 26 + .closeButton { 27 + display: grid; 28 + justify-content: end; 29 + } 30 + 26 31 .searchingText { 27 32 text-align: center !important; 28 33 } ··· 47 52 margin-bottom: 20px; 48 53 } 49 54 55 + .previewPlaceholder { 56 + display: grid; 57 + align-items: center; 58 + justify-content: center; 59 + } 60 + 50 61 .previewExpComponent { 51 62 height: 100%; 52 63 width: 100%; 53 - align-items: center; 54 64 } 55 65 56 66 .primaryButton {
+8 -3
app/components/styles/common.css
··· 23 23 } 24 24 25 25 .contentGrid { 26 - height: 95%; 26 + height: 90%; 27 27 } 28 28 29 29 .overviewImage { ··· 357 357 align-content: baseline; 358 358 grid-template-columns: 1fr; 359 359 overflow-y: scroll; 360 - min-height: 500px; 361 - max-height: 500px; 360 + max-height: 50vh; 362 361 } 363 362 364 363 .EEGToggle { ··· 375 374 display: flex; 376 375 justify-content: flex-end; 377 376 } 377 + 378 + .trialsTopInfoBar { 379 + display: grid; 380 + grid-template-columns: 1fr 1fr 1fr; 381 + grid-column-gap: 10px; 382 + }
+8
app/components/styles/topnavbar.css
··· 113 113 min-width: 35px; 114 114 margin-left: 10px !important; 115 115 } 116 + 117 + .homeTopNavButton{ 118 + display:grid; 119 + grid-template-columns: 1fr 1fr; 120 + align-items: center; 121 + grid-column-gap: 5px; 122 + margin-right: 20px; 123 + }
+2
app/constants/constants.js
··· 61 61 export const EVENTS = { 62 62 STIMULUS_1: 1, 63 63 STIMULUS_2: 2, 64 + STIMULUS_3: 3, 65 + STIMULUS_4: 4, 64 66 TARGET: 2, 65 67 NONTARGET: 1, 66 68 };
+3 -1
app/epics/experimentEpics.js
··· 31 31 import { 32 32 getWorkspaceDir, 33 33 storeExperimentState, 34 + restoreExperimentState, 34 35 createWorkspaceDir, 35 36 storeBehaviouralData, 36 37 readWorkspaceBehaviorData, ··· 178 179 ignoreElements() 179 180 ); 180 181 181 - const navigationCleanupEpic = (action$) => 182 + const navigationCleanupEpic = (action$, state$) => 182 183 action$.ofType('@@router/LOCATION_CHANGE').pipe( 183 184 pluck('payload', 'pathname'), 184 185 filter((pathname) => pathname === '/'), 186 + tap(() => restoreExperimentState(state$.value.experiment)), 185 187 map(cleanup) 186 188 ); 187 189
+3
app/epics/jupyterEpics.js
··· 230 230 awaitOkMessage(action$), 231 231 execute(filterIIR(1, 30), state$), 232 232 awaitOkMessage(action$), 233 + tap(() => console.log('state', state$.value.experiment.params)), 233 234 map(() => 234 235 epochEvents( 235 236 { 236 237 [state$.value.experiment.params.stimulus1.title]: EVENTS.STIMULUS_1, 237 238 [state$.value.experiment.params.stimulus2.title]: EVENTS.STIMULUS_2, 239 + [state$.value.experiment.params.stimulus3.title]: EVENTS.STIMULUS_3, 240 + [state$.value.experiment.params.stimulus4.title]: EVENTS.STIMULUS_4, 238 241 }, 239 242 -0.1, 240 243 0.8
+15
app/utils/filesystem/storage.js
··· 41 41 ); 42 42 }; 43 43 44 + export const restoreExperimentState = (state: ExperimentStateType) => { 45 + if(state.type !== 'NONE'){ 46 + const timestampedState = { 47 + ...state, 48 + subject: '', 49 + group: '', 50 + session: 1 51 + }; 52 + fs.writeFileSync( 53 + path.join(getWorkspaceDir(timestampedState.title), 'appState.json'), 54 + JSON.stringify(timestampedState) 55 + ); 56 + } 57 + } 58 + 44 59 export const storeBehaviouralData = ( 45 60 csv: string, 46 61 title: string,
+6 -1
app/utils/labjs/functions.js
··· 7 7 import { buildStroopTimeline } from './protocols/stroop'; 8 8 import { buildMultiTimeline } from './protocols/multi'; 9 9 import { buildSearchTimeline } from './protocols/search'; 10 + import { buildCustomTimeline } from './protocols/custom'; 10 11 11 12 import { MainTimeline, Trial, ExperimentParameters } from '../../constants/interfaces'; 12 13 ··· 27 28 break; 28 29 29 30 case EXPERIMENTS.N170: 30 - default: 31 31 protocol = buildN170Timeline(); 32 + break; 33 + 34 + case EXPERIMENTS.CUSTOM: 35 + default: 36 + protocol = buildCustomTimeline(); 32 37 break; 33 38 } 34 39 return protocol;
+4 -4
app/utils/labjs/index.js
··· 30 30 case 'Faces and Houses': 31 31 default: 32 32 faceshouses.parameters = props.settings.params; 33 - // inject files with their addresses from parameters values 33 + faceshouses.parameters.title = props.settings.title; 34 34 faceshouses.files = props.settings.params.stimuli 35 - .map((image) => { 36 - return { [image.filename]: `${image.dir}/${image.filename}` }; 37 - }) 35 + .map((image) => ({ 36 + [`${image.dir}/${image.filename}`]: `${image.dir}/${image.filename}`, 37 + })) 38 38 .reduce((obj, item) => { 39 39 obj[Object.keys(item)[0]] = Object.values(item)[0]; 40 40 return obj;
+110
app/utils/labjs/protocols/custom.js
··· 1 + import * as path from 'path'; 2 + import { EVENTS } from '../../../constants/constants'; 3 + 4 + // Default directories containing stimuli 5 + const rootFolder = __dirname; // Note: there's a weird issue where the fs readdir function reads from BrainWaves dir 6 + 7 + const facesDir = path.join(rootFolder, 'assets', 'face_house', 'faces'); 8 + const housesDir = path.join(rootFolder, 'assets', 'face_house', 'houses'); 9 + const fixation = path.join(rootFolder, 'assets', 'common', 'fixationcross.png'); 10 + 11 + export const buildCustomTimeline = () => ({ 12 + overview_title: `Custom Experiment`, 13 + overview: `When you scroll through your social media feed, you may find that you are more likely to pause when a picture 14 + contains a face, than, for example, a tree. 15 + In this experiment, you will explore whether our brains process faces differently than other objects (in this case, houses).`, 16 + background_first_column: `Did you know that we spend more time looking at faces than any other type of stimuli? 17 + Faces contain a lot of information that is relevant to our day-to-day lives. 18 + For example, by looking at someone’s face we can assess their emotional state. 19 + This has led researchers speculate that faces may be processed differently than other stimuli.`, 20 + background_first_column_question: `In fact, there is a special area in your brain, the Fusiform Face Area, that has been shown to be selective for faces. 21 + People who have damage in this area may have a hard time recognizing faces, a condition called face blindness, or prosopagnosia.`, 22 + background_second_column: `In the video (Link 1) the famous neurologist Oliver Sacks explains what it is like to have face blindness to the extent 23 + that he sometimes didn’t even recognize his own face (!)`, 24 + background_second_column_question: `Fun fact: Brad Pitt claims he has face blindness, but he has not been tested. 25 + Do you know anyone who has face blindness?`, 26 + protocol_title: `What participants are shown`, 27 + protocol: `In the Faces/Houses experiment, you will see pictures of different faces and houses.`, 28 + protocol_condition_first_img: `conditionFace`, 29 + protocol_condition_first_title: `Faces`, 30 + protocol_condition_first: `When you see a face, press the key “1”.`, 31 + protocol_condition_second_img: `conditionHouse`, 32 + protocol_condition_second_title: `Houses`, 33 + protocol_condition_second: `If you see a house, press “9”.`, 34 + overview_links: [], 35 + background_links: [{name: 'Link 1', address: 'https://www.cnn.com/videos/health/2011/01/04/sacks.face.blindness.cnn'}], 36 + protocal_links: [], 37 + params: { 38 + randomize: 'random', 39 + includePractice: true, 40 + trialDuration: 1000, 41 + nbTrials: 8, 42 + nbPracticeTrials: 2, 43 + iti: 500, 44 + presentationTime: 1000, 45 + selfPaced: true, 46 + jitter: 200, 47 + sampleType: 'with-replacement', 48 + pluginName: 'callback-image-display', 49 + intro: '', 50 + taskHelp: '', 51 + showProgressBar: false, 52 + stimulus1: { 53 + dir: '', 54 + title: 'Condition 1', 55 + type: EVENTS.STIMULUS_1, 56 + response: '' 57 + }, 58 + stimulus2: { 59 + dir: '', 60 + title: 'Condition 2', 61 + type: EVENTS.STIMULUS_2, 62 + response: '' 63 + }, 64 + stimulus3: { 65 + dir: '', 66 + title: '', 67 + type: 3, 68 + response: '' 69 + }, 70 + stimulus4: { 71 + dir: '', 72 + title: '', 73 + type: 4, 74 + response: '' 75 + }, 76 + stimuli: [], 77 + }, 78 + mainTimeline: ['intro', 'faceHouseTimeline', 'end'], // array of trial and timeline ids 79 + trials: { 80 + intro: { 81 + type: 'callback-html-display', 82 + id: 'intro', 83 + post_trial_gap: 1000 84 + }, 85 + end: { 86 + id: 'end', 87 + type: 'callback-html-display', 88 + stimulus: 'Thanks for participating. Press any key to continue', 89 + response_ends_trial: true, 90 + post_trial_gap: 500 91 + } 92 + }, 93 + timelines: { 94 + faceHouseTimeline: { 95 + id: 'faceHouseTimeline', 96 + timeline: [ 97 + { 98 + id: 'interTrial', 99 + type: 'callback-image-display', 100 + stimulus: fixation, 101 + response_ends_trial: false 102 + }, 103 + { 104 + id: 'trial', 105 + response_ends_trial: false 106 + } 107 + ] 108 + } 109 + } 110 + });
+4 -4
app/utils/labjs/scripts/faceshouses.js
··· 33 33 messageHandlers: {}, 34 34 title: 'Instruction', 35 35 content: 36 - '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EThe face-house task\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E', 36 + '\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003E${this.parameters.title || "The face-house task"}\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E', 37 37 }, 38 38 { 39 39 type: 'lab.canvas.Frame', ··· 92 92 93 93 const trialConstructor = (file) => ({ 94 94 condition: file.condition, 95 - image: file.filename, 95 + image: `${file.dir}/${file.filename}`, 96 96 correctResponse: file.response, 97 97 phase: 'practice', 98 98 name: file.name, ··· 370 370 parameters: {}, 371 371 responses: {}, 372 372 messageHandlers: { 373 - end: function anonymous() { 373 + run: function anonymous() { 374 374 this.data.correct_response = false; 375 375 }, 376 376 }, ··· 465 465 466 466 const trialConstructor = (file) => ({ 467 467 condition: file.condition, 468 - image: file.filename, 468 + image: `${file.dir}/${file.filename}`, 469 469 correctResponse: file.response, 470 470 phase: 'task', 471 471 name: file.name,
+5 -5
keys.js
··· 1 1 // These values must be filled with the appropriate Emotiv credentials to be able to use the Cortex SDK 2 2 // We have our credentials stored in environement variables 3 3 4 - var USERNAME = process.env.EMOTIV_USERNAME; 5 - var PASSWORD = process.env.EMOTIV_PASSWORD; 6 - var CLIENT_ID = process.env.EMOTIV_CLIENT_ID; // Created through Cortex Apps page on Emotiv.com 7 - var CLIENT_SECRET = process.env.EMOTIV_CLIENT_SECRET; // Created through Cortex Apps page on Emotiv.com 8 - var LICENSE_ID = process.env.EMOTIV_LICENSE_ID; // Visible on My Account page of Emotiv.com 4 + const USERNAME = process.env.EMOTIV_USERNAME; 5 + const PASSWORD = process.env.EMOTIV_PASSWORD; 6 + const CLIENT_ID = process.env.EMOTIV_CLIENT_ID; // Created through Cortex Apps page on Emotiv.com 7 + const CLIENT_SECRET = process.env.EMOTIV_CLIENT_SECRET; // Created through Cortex Apps page on Emotiv.com 8 + const LICENSE_ID = process.env.EMOTIV_LICENSE_ID; // Visible on My Account page of Emotiv.com 9 9 10 10 module.exports = { USERNAME, PASSWORD, CLIENT_ID, CLIENT_SECRET, LICENSE_ID };