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.

flow -> ts

+3423 -5050
-580
app/components/AnalyzeComponent.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { 4 - Grid, 5 - Icon, 6 - Segment, 7 - Header, 8 - Dropdown, 9 - Divider, 10 - Button, 11 - Checkbox, 12 - Sidebar, 13 - } from 'semantic-ui-react'; 14 - import { isNil } from 'lodash'; 15 - import Plot from 'react-plotly.js'; 16 - import styles from './styles/common.css'; 17 - import { 18 - DEVICES, 19 - MUSE_CHANNELS, 20 - EMOTIV_CHANNELS, 21 - KERNEL_STATUS, 22 - EXPERIMENTS, 23 - } from '../constants/constants'; 24 - import { 25 - readWorkspaceCleanedEEGData, 26 - getSubjectNamesFromFiles, 27 - readWorkspaceBehaviorData, 28 - readBehaviorData, 29 - storeAggregatedBehaviorData, 30 - } from '../utils/filesystem/storage'; 31 - import { aggregateDataForPlot, aggregateBehaviorDataToSave } from '../utils/behavior/compute'; 32 - import SecondaryNavComponent from './SecondaryNavComponent'; 33 - import ClickableHeadDiagramSVG from './svgs/ClickableHeadDiagramSVG'; 34 - import JupyterPlotWidget from './JupyterPlotWidget'; 35 - import { HelpButton } from './CollectComponent/HelpSidebar'; 36 - 37 - const ANALYZE_STEPS = { 38 - OVERVIEW: 'OVERVIEW', 39 - ERP: 'ERP', 40 - BEHAVIOR: 'BEHAVIOR', 41 - }; 42 - 43 - const ANALYZE_STEPS_BEHAVIOR = { 44 - BEHAVIOR: 'BEHAVIOR', 45 - }; 46 - 47 - interface Props { 48 - title: string; 49 - type: ?EXPERIMENTS; 50 - deviceType: DEVICES; 51 - isEEGEnabled: boolean; 52 - kernel: ?Kernel; 53 - kernelStatus: KERNEL_STATUS; 54 - mainChannel: ?any; 55 - epochsInfo: ?Array<{ [string]: number | string }>; 56 - channelInfo: ?Array<string>; 57 - psdPlot: ?{ [string]: string }; 58 - topoPlot: ?{ [string]: string }; 59 - erpPlot: ?{ [string]: string }; 60 - jupyterActions: Object; 61 - } 62 - 63 - interface State { 64 - activeStep: string; 65 - selectedChannel: string; 66 - eegFilePaths: Array<?{ 67 - key: string, 68 - text: string, 69 - value: { name: string, dir: string }, 70 - }>; 71 - behaviorFilePaths: Array<?{ 72 - key: string, 73 - text: string, 74 - value: string, 75 - }>; 76 - selectedFilePaths: Array<?string>; 77 - selectedBehaviorFilePaths: Array<?string>; 78 - selectedSubjects: Array<?string>; 79 - selectedDependentVariable: string; 80 - removeOutliers: boolean; 81 - showDataPoints: boolean; 82 - isSidebarVisible: boolean; 83 - displayOutlierVisible: boolean; 84 - displayMode: string; 85 - helpMode: string; 86 - dependentVariables: Array<?{ 87 - key: string, 88 - text: string, 89 - value: string, 90 - }>; 91 - } 92 - // TODO: Add a channel callback from reading epochs so this screen can be aware of which channels are 93 - // available in dataset 94 - export default class Analyze extends Component<Props, State> { 95 - // props: Props; 96 - // state: State; 97 - // handleChannelSelect: (string) => void; 98 - // handleStepClick: (Object, Object) => void; 99 - // handleDatasetChange: (Object, Object) => void; 100 - // handleBehaviorDatasetChange: (Object, Object) => void; 101 - // handleDependentVariableChange: (Object, Object) => void; 102 - // handleRemoveOutliers: (Object, Object) => void; 103 - // handleDisplayModeChange: (string) => void; 104 - // handleDataPoints: (Object, Object) => void; 105 - // saveSelectedDatasets: () => void; 106 - // handleStepClick: (Object, Object) => void; 107 - // toggleDisplayInfoVisibility: () => void; 108 - 109 - constructor(props: Props) { 110 - super(props); 111 - this.state = { 112 - activeStep: 113 - this.props.isEEGEnabled === true ? ANALYZE_STEPS.OVERVIEW : ANALYZE_STEPS.BEHAVIOR, 114 - eegFilePaths: [{ key: '', text: '', value: '' }], 115 - behaviorFilePaths: [{ key: '', text: '', value: '' }], 116 - dependentVariables: [{ key: '', text: '', value: '' }], 117 - dataToPlot: [], 118 - layout: {}, 119 - selectedDependentVariable: '', 120 - removeOutliers: true, 121 - showDataPoints: false, 122 - isSidebarVisible: false, 123 - // displayOutlierVisible: false, 124 - displayMode: 'errorbars', 125 - helpMode: 'errorbars', 126 - selectedFilePaths: [], 127 - selectedBehaviorFilePaths: [], 128 - selectedSubjects: [], 129 - selectedChannel: props.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS[0] : MUSE_CHANNELS[0], 130 - }; 131 - this.handleChannelSelect = this.handleChannelSelect.bind(this); 132 - this.handleDatasetChange = this.handleDatasetChange.bind(this); 133 - this.handleBehaviorDatasetChange = this.handleBehaviorDatasetChange.bind(this); 134 - this.handleDependentVariableChange = this.handleDependentVariableChange.bind(this); 135 - this.handleRemoveOutliers = this.handleRemoveOutliers.bind(this); 136 - this.handleDisplayModeChange = this.handleDisplayModeChange.bind(this); 137 - this.handleDataPoints = this.handleDataPoints.bind(this); 138 - this.saveSelectedDatasets = this.saveSelectedDatasets.bind(this); 139 - this.handleStepClick = this.handleStepClick.bind(this); 140 - this.handleDropdownClick = this.handleDropdownClick.bind(this); 141 - this.toggleDisplayInfoVisibility = this.toggleDisplayInfoVisibility.bind(this); 142 - } 143 - 144 - async componentDidMount() { 145 - const workspaceCleanData = await readWorkspaceCleanedEEGData(this.props.title); 146 - if (this.props.kernelStatus === KERNEL_STATUS.OFFLINE) { 147 - this.props.jupyterActions.launchKernel(); 148 - } 149 - const behavioralData = await readWorkspaceBehaviorData(this.props.title); 150 - this.setState({ 151 - eegFilePaths: workspaceCleanData.map((filepath) => ({ 152 - key: filepath.name, 153 - text: filepath.name, 154 - value: filepath.path, 155 - })), 156 - behaviorFilePaths: behavioralData.map((filepath) => ({ 157 - key: filepath.name, 158 - text: filepath.name, 159 - value: filepath.path, 160 - })), 161 - dependentVariables: ['Response Time', 'Accuracy'].map((dv) => ({ 162 - key: dv, 163 - text: dv, 164 - value: dv, 165 - })), 166 - selectedDependentVariable: 'Response Time', 167 - }); 168 - } 169 - 170 - handleStepClick(step: string) { 171 - this.setState({ activeStep: step }); 172 - } 173 - 174 - handleDatasetChange(event: Object, data: Object) { 175 - this.setState({ 176 - selectedFilePaths: data.value, 177 - selectedSubjects: getSubjectNamesFromFiles(data.value), 178 - }); 179 - this.props.jupyterActions.loadCleanedEpochs(data.value); 180 - } 181 - 182 - handleBehaviorDatasetChange(event: Object, data: Object) { 183 - const { dataToPlot, layout } = aggregateDataForPlot( 184 - readBehaviorData(data.value), 185 - this.state.selectedDependentVariable, 186 - this.state.removeOutliers, 187 - this.state.showDataPoints, 188 - this.state.displayMode 189 - ); 190 - this.setState({ 191 - selectedBehaviorFilePaths: data.value, 192 - selectedSubjects: getSubjectNamesFromFiles(data.value), 193 - dataToPlot, 194 - layout, 195 - }); 196 - } 197 - 198 - async handleDropdownClick() { 199 - const behavioralData = await readWorkspaceBehaviorData(this.props.title); 200 - if (behavioralData.length != this.state.behaviorFilePaths.length) { 201 - this.setState({ 202 - behaviorFilePaths: behavioralData.map((filepath) => ({ 203 - key: filepath.name, 204 - text: filepath.name, 205 - value: filepath.path, 206 - })), 207 - }); 208 - } 209 - } 210 - 211 - handleDependentVariableChange(event: Object, data: Object) { 212 - const { dataToPlot, layout } = aggregateDataForPlot( 213 - readBehaviorData(this.state.selectedBehaviorFilePaths), 214 - data.value, 215 - this.state.removeOutliers, 216 - this.state.showDataPoints, 217 - this.state.displayMode 218 - ); 219 - this.setState({ 220 - selectedDependentVariable: data.value, 221 - dataToPlot, 222 - layout, 223 - }); 224 - } 225 - 226 - handleRemoveOutliers(event: Object, data: Object) { 227 - const { dataToPlot, layout } = aggregateDataForPlot( 228 - readBehaviorData(this.state.selectedBehaviorFilePaths), 229 - this.state.selectedDependentVariable, 230 - !this.state.removeOutliers, 231 - this.state.showDataPoints, 232 - this.state.displayMode 233 - ); 234 - this.setState({ 235 - removeOutliers: !this.state.removeOutliers, 236 - dataToPlot, 237 - layout, 238 - helpMode: 'outliers', 239 - }); 240 - } 241 - 242 - handleDataPoints(event: Object, data: Object) { 243 - const { dataToPlot, layout } = aggregateDataForPlot( 244 - readBehaviorData(this.state.selectedBehaviorFilePaths), 245 - this.state.selectedDependentVariable, 246 - this.state.removeOutliers, 247 - !this.state.showDataPoints, 248 - this.state.displayMode 249 - ); 250 - this.setState({ 251 - showDataPoints: !this.state.showDataPoints, 252 - dataToPlot, 253 - layout, 254 - }); 255 - } 256 - 257 - handleDisplayModeChange(displayMode) { 258 - if (this.state.selectedBehaviorFilePaths && this.state.selectedBehaviorFilePaths.length > 0) { 259 - const { dataToPlot, layout } = aggregateDataForPlot( 260 - readBehaviorData(this.state.selectedBehaviorFilePaths), 261 - this.state.selectedDependentVariable, 262 - this.state.removeOutliers, 263 - this.state.showDataPoints, 264 - displayMode 265 - ); 266 - this.setState({ 267 - dataToPlot, 268 - layout, 269 - displayMode, 270 - helpMode: displayMode, 271 - }); 272 - } 273 - } 274 - 275 - toggleDisplayInfoVisibility() { 276 - this.setState({ 277 - isSidebarVisible: !this.state.isSidebarVisible, 278 - }); 279 - } 280 - 281 - saveSelectedDatasets() { 282 - const data = readBehaviorData(this.state.selectedBehaviorFilePaths); 283 - const aggregatedData = aggregateBehaviorDataToSave(data, this.state.removeOutliers); 284 - storeAggregatedBehaviorData(aggregatedData, this.props.title); 285 - } 286 - 287 - handleChannelSelect(channelName: string) { 288 - this.setState({ selectedChannel: channelName }); 289 - this.props.jupyterActions.loadERP(channelName); 290 - } 291 - 292 - concatSubjectNames = (subjects: Array<?string>) => { 293 - if (subjects.length < 1) { 294 - return ''; 295 - } 296 - return subjects.reduce((acc, curr) => `${acc}-${curr}`); 297 - }; 298 - 299 - renderEpochLabels() { 300 - if (!isNil(this.props.epochsInfo) && this.state.selectedFilePaths.length >= 1) { 301 - const numberConditions = this.props.epochsInfo.filter( 302 - (infoObj) => infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs' 303 - ).length; 304 - let colors; 305 - if (numberConditions === 4) { 306 - colors = ['red', 'yellow', 'green', 'blue']; 307 - } else { 308 - colors = ['red', 'green', 'teal', 'orange']; 309 - } 310 - return ( 311 - <div> 312 - {this.props.epochsInfo 313 - .filter( 314 - (infoObj) => infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs' 315 - ) 316 - .map((infoObj, index) => ( 317 - <> 318 - <Header as='h4'>{infoObj.name}</Header> 319 - <Icon name='circle' color={colors[index]} /> 320 - {infoObj.value} 321 - </> 322 - ))} 323 - </div> 324 - ); 325 - } 326 - return <div />; 327 - } 328 - 329 - renderHelpContent() { 330 - switch (this.state.helpMode) { 331 - case 'datapoints': 332 - return this.renderHelp( 333 - 'Data Points', 334 - `In this graph, each dot refers to one data point, clustered by group (e.g., conditions). 335 - It’s the most “neutral” way of presenting the data, of course, but it may be difficult to see any patterns. 336 - Why is it always a good idea to look at all your datapoints before interpreting any trends in the data?` 337 - ); 338 - case 'errorbars': 339 - return this.renderHelp( 340 - 'Bar Graph', 341 - `Bar graphs are the most common way to summarize data. 342 - It allows you to compare mean values between groups of datapoints (e.g., conditions), 343 - and the error bars give some indication of the variance (here: the standard error of the mean). 344 - Importantly, this way of summarizing data assumes that the mean is in fact representative of the data. 345 - Many researchers have veered away from bar graphs because they can be deceptive, especially without error bars. 346 - Can you think of any such cases?` 347 - ); 348 - case 'whiskers': 349 - return this.renderHelp( 350 - 'Box Plot', 351 - `Box plots summarize the data in a more informative way: 352 - they actually tell you something about the distribution of datapoints within a group, 353 - by taking the median as its reference point instead of the mean 354 - (the median is the “middle” point in a dataset after sorting it from the lowest to the highest value). 355 - The boxes represent so-called “quartiles” which are cut off at the value right between the median and the smallest value or highest value in the dataset. 356 - The lines (“whiskers”) show how much variability there is in the data outside of those quartiles; 357 - any outliers are shown as individual points. Can you go through each plot and describe exactly what you see? 358 - When you toggle between this view and the bar graph view, do the data look very different?` 359 - ); 360 - case 'outliers': 361 - default: 362 - return this.renderHelp( 363 - 'Outliers', 364 - `A datapoint is tagged as an “outlier” if its value exceeds 2 standard deviations below or above the mean of all data in the group. 365 - If a datapoint is unusually high or low (it “deviates”) compared to the rest of the group, 366 - it is likely a special case that doesn’t tell us anything informative about the group as a whole. 367 - Removing such outliers can help unskew the data. What might outliers mean in your dataset? 368 - Can you think of any other cases where identifying outliers can be helpful?` 369 - ); 370 - } 371 - } 372 - 373 - renderHelp(header: string, content: string) { 374 - return ( 375 - <> 376 - <Segment basic className={styles.helpContent}> 377 - <Button 378 - circular 379 - size='large' 380 - floated='right' 381 - icon='x' 382 - className={styles.closeButton} 383 - onClick={this.toggleDisplayInfoVisibility} 384 - /> 385 - <Header className={styles.helpHeader} as='h1'> 386 - {header} 387 - </Header> 388 - {content} 389 - </Segment> 390 - </> 391 - ); 392 - } 393 - 394 - renderSectionContent() { 395 - switch (this.state.activeStep) { 396 - case ANALYZE_STEPS.OVERVIEW: 397 - default: 398 - return ( 399 - <> 400 - <Grid.Column width={4}> 401 - <Segment basic textAlign='left' className={styles.infoSegment}> 402 - <Header as='h1'>Overview</Header> 403 - <p> 404 - Load cleaned datasets from different subjects and view how the EEG differs between 405 - electrodes 406 - </p> 407 - <Header as='h4'>Select Clean Datasets</Header> 408 - <Dropdown 409 - fluid 410 - multiple 411 - selection 412 - closeOnChange 413 - value={this.state.selectedFilePaths} 414 - options={this.state.eegFilePaths} 415 - onChange={this.handleDatasetChange} 416 - /> 417 - {this.renderEpochLabels()} 418 - </Segment> 419 - </Grid.Column> 420 - <Grid.Column width={8}> 421 - <JupyterPlotWidget 422 - title={this.props.title} 423 - imageTitle={`${this.concatSubjectNames(this.state.selectedSubjects)}-Topoplot`} 424 - plotMIMEBundle={this.props.topoPlot} 425 - /> 426 - </Grid.Column> 427 - </> 428 - ); 429 - case ANALYZE_STEPS.ERP: 430 - return ( 431 - <> 432 - <Grid.Column width={4} className={styles.analyzeColumn}> 433 - <Segment basic textAlign='left' className={styles.infoSegment}> 434 - <Header as='h1'>ERP</Header> 435 - <p> 436 - The event-related potential represents EEG activity elicited by a particular 437 - sensory event 438 - </p> 439 - <ClickableHeadDiagramSVG 440 - channelinfo={this.props.channelInfo} 441 - onChannelClick={this.handleChannelSelect} 442 - /> 443 - <Divider hidden /> 444 - {this.renderEpochLabels()} 445 - </Segment> 446 - </Grid.Column> 447 - <Grid.Column width={8}> 448 - <JupyterPlotWidget 449 - title={this.props.title} 450 - imageTitle={`${this.concatSubjectNames(this.state.selectedSubjects)}-${ 451 - this.state.selectedChannel 452 - }-ERP`} 453 - plotMIMEBundle={this.props.erpPlot} 454 - /> 455 - </Grid.Column> 456 - </> 457 - ); 458 - case ANALYZE_STEPS.BEHAVIOR: 459 - return ( 460 - <> 461 - <Grid.Column width={4}> 462 - <Segment basic textAlign='left' className={styles.infoSegment}> 463 - <Header as='h1'>Overview</Header> 464 - <p>Load datasets from different subjects and view behavioral results</p> 465 - 466 - <div> 467 - <span className='ui header'>Datasets</span> 468 - <Button className='export' onClick={this.saveSelectedDatasets}> 469 - <Icon name='download' /> 470 - Export 471 - </Button> 472 - </div> 473 - <p /> 474 - 475 - <Dropdown 476 - fluid 477 - multiple 478 - selection 479 - search 480 - closeOnChange 481 - value={this.state.selectedBehaviorFilePaths} 482 - options={this.state.behaviorFilePaths} 483 - onChange={this.handleBehaviorDatasetChange} 484 - onClick={this.handleDropdownClick} 485 - /> 486 - <p /> 487 - <Divider hidden /> 488 - <span className='ui header'>Dependent Variable</span> 489 - <p /> 490 - <Dropdown 491 - fluid 492 - selection 493 - closeOnChange 494 - value={this.state.selectedDependentVariable} 495 - options={this.state.dependentVariables} 496 - onChange={this.handleDependentVariableChange} 497 - /> 498 - </Segment> 499 - </Grid.Column> 500 - <Grid.Column 501 - width={12} 502 - style={{ 503 - overflow: 'auto', 504 - maxHeight: 650, 505 - display: 'grid', 506 - justifyContent: 'center', 507 - }} 508 - > 509 - <Segment basic textAlign='left' className={styles.plotSegment}> 510 - <Plot data={this.state.dataToPlot} layout={this.state.layout} /> 511 - <p /> 512 - <Checkbox 513 - checked={this.state.removeOutliers} 514 - label='Remove Response Time Outliers' 515 - onChange={this.handleRemoveOutliers} 516 - /> 517 - 518 - <p /> 519 - <Button.Group> 520 - <Button 521 - className='tertiary' 522 - toggle 523 - active={this.state.displayMode === 'datapoints'} 524 - onClick={() => this.handleDisplayModeChange('datapoints')} 525 - > 526 - Data Points 527 - </Button> 528 - <Button 529 - className='tertiary' 530 - toggle 531 - active={this.state.displayMode === 'errorbars'} 532 - onClick={() => this.handleDisplayModeChange('errorbars')} 533 - > 534 - Bar Graph 535 - </Button> 536 - <Button 537 - className='tertiary' 538 - toggle 539 - active={this.state.displayMode === 'whiskers'} 540 - onClick={() => this.handleDisplayModeChange('whiskers')} 541 - > 542 - Box Plot 543 - </Button> 544 - </Button.Group> 545 - 546 - <HelpButton onClick={this.toggleDisplayInfoVisibility} /> 547 - 548 - <Sidebar 549 - width='wide' 550 - direction='right' 551 - as={Segment} 552 - visible={this.state.isSidebarVisible} 553 - > 554 - <Segment basic padded vertical className={styles.helpSidebar}> 555 - {this.renderHelpContent()} 556 - </Segment> 557 - </Sidebar> 558 - </Segment> 559 - </Grid.Column> 560 - </> 561 - ); 562 - } 563 - } 564 - 565 - render() { 566 - return ( 567 - <div className={styles.mainContainer}> 568 - <SecondaryNavComponent 569 - title='Analyze' 570 - steps={this.props.isEEGEnabled === true ? ANALYZE_STEPS : ANALYZE_STEPS_BEHAVIOR} 571 - activeStep={this.state.activeStep} 572 - onStepClick={this.handleStepClick} 573 - /> 574 - <Grid columns='equal' textAlign='center' verticalAlign='top' className={styles.contentGrid}> 575 - {this.renderSectionContent()} 576 - </Grid> 577 - </div> 578 - ); 579 - } 580 - }
+435
app/components/AnalyzeComponent.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Grid, Icon, Segment, Header, Dropdown, Divider, Button, Checkbox, Sidebar } from "semantic-ui-react"; 4 + import { isNil } from "lodash"; 5 + import Plot from "react-plotly.js"; 6 + import styles from "./styles/common.css"; 7 + import { DEVICES, MUSE_CHANNELS, EMOTIV_CHANNELS, KERNEL_STATUS, EXPERIMENTS } from "../constants/constants"; 8 + import { readWorkspaceCleanedEEGData, getSubjectNamesFromFiles, readWorkspaceBehaviorData, readBehaviorData, storeAggregatedBehaviorData } from "../utils/filesystem/storage"; 9 + import { aggregateDataForPlot, aggregateBehaviorDataToSave } from "../utils/behavior/compute"; 10 + import SecondaryNavComponent from "./SecondaryNavComponent"; 11 + import ClickableHeadDiagramSVG from "./svgs/ClickableHeadDiagramSVG"; 12 + import JupyterPlotWidget from "./JupyterPlotWidget"; 13 + import { HelpButton } from "./CollectComponent/HelpSidebar"; 14 + 15 + const ANALYZE_STEPS = { 16 + OVERVIEW: 'OVERVIEW', 17 + ERP: 'ERP', 18 + BEHAVIOR: 'BEHAVIOR' 19 + }; 20 + 21 + const ANALYZE_STEPS_BEHAVIOR = { 22 + BEHAVIOR: 'BEHAVIOR' 23 + }; 24 + 25 + interface Props { 26 + title: string; 27 + type: EXPERIMENTS | null | undefined; 28 + deviceType: DEVICES; 29 + isEEGEnabled: boolean; 30 + kernel: Kernel | null | undefined; 31 + kernelStatus: KERNEL_STATUS; 32 + mainChannel: any | null | undefined; 33 + epochsInfo: Array<{ 34 + [key: string]: number | string; 35 + }> | null | undefined; 36 + channelInfo: Array<string> | null | undefined; 37 + psdPlot: { 38 + [key: string]: string; 39 + } | null | undefined; 40 + topoPlot: { 41 + [key: string]: string; 42 + } | null | undefined; 43 + erpPlot: { 44 + [key: string]: string; 45 + } | null | undefined; 46 + jupyterActions: Object; 47 + } 48 + 49 + interface State { 50 + activeStep: string; 51 + selectedChannel: string; 52 + eegFilePaths: Array<{ 53 + key: string; 54 + text: string; 55 + value: {name: string;dir: string;}; 56 + } | null | undefined>; 57 + behaviorFilePaths: Array<{ 58 + key: string; 59 + text: string; 60 + value: string; 61 + } | null | undefined>; 62 + selectedFilePaths: Array<string | null | undefined>; 63 + selectedBehaviorFilePaths: Array<string | null | undefined>; 64 + selectedSubjects: Array<string | null | undefined>; 65 + selectedDependentVariable: string; 66 + removeOutliers: boolean; 67 + showDataPoints: boolean; 68 + isSidebarVisible: boolean; 69 + displayOutlierVisible: boolean; 70 + displayMode: string; 71 + helpMode: string; 72 + dependentVariables: Array<{ 73 + key: string; 74 + text: string; 75 + value: string; 76 + } | null | undefined>; 77 + } 78 + // TODO: Add a channel callback from reading epochs so this screen can be aware of which channels are 79 + // available in dataset 80 + export default class Analyze extends Component<Props, State> { 81 + // handleDependentVariableChange: (Object, Object) => void; 82 + 83 + // handleRemoveOutliers: (Object, Object) => void; 84 + // handleDisplayModeChange: (string) => void; 85 + // handleDataPoints: (Object, Object) => void; 86 + // saveSelectedDatasets: () => void; 87 + // handleStepClick: (Object, Object) => void; 88 + // toggleDisplayInfoVisibility: () => void; 89 + constructor(props: Props) { 90 + super(props); 91 + this.state = { 92 + activeStep: this.props.isEEGEnabled === true ? ANALYZE_STEPS.OVERVIEW : ANALYZE_STEPS.BEHAVIOR, 93 + eegFilePaths: [{ key: '', text: '', value: '' }], 94 + behaviorFilePaths: [{ key: '', text: '', value: '' }], 95 + dependentVariables: [{ key: '', text: '', value: '' }], 96 + dataToPlot: [], 97 + layout: {}, 98 + selectedDependentVariable: '', 99 + removeOutliers: true, 100 + showDataPoints: false, 101 + isSidebarVisible: false, 102 + // displayOutlierVisible: false, 103 + displayMode: 'errorbars', 104 + helpMode: 'errorbars', 105 + selectedFilePaths: [], 106 + selectedBehaviorFilePaths: [], 107 + selectedSubjects: [], 108 + selectedChannel: props.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS[0] : MUSE_CHANNELS[0] 109 + }; 110 + this.handleChannelSelect = this.handleChannelSelect.bind(this); 111 + this.handleDatasetChange = this.handleDatasetChange.bind(this); 112 + this.handleBehaviorDatasetChange = this.handleBehaviorDatasetChange.bind(this); 113 + this.handleDependentVariableChange = this.handleDependentVariableChange.bind(this); 114 + this.handleRemoveOutliers = this.handleRemoveOutliers.bind(this); 115 + this.handleDisplayModeChange = this.handleDisplayModeChange.bind(this); 116 + this.handleDataPoints = this.handleDataPoints.bind(this); 117 + this.saveSelectedDatasets = this.saveSelectedDatasets.bind(this); 118 + this.handleStepClick = this.handleStepClick.bind(this); 119 + this.handleDropdownClick = this.handleDropdownClick.bind(this); 120 + this.toggleDisplayInfoVisibility = this.toggleDisplayInfoVisibility.bind(this); 121 + } 122 + 123 + async componentDidMount() { 124 + const workspaceCleanData = await readWorkspaceCleanedEEGData(this.props.title); 125 + if (this.props.kernelStatus === KERNEL_STATUS.OFFLINE) { 126 + this.props.jupyterActions.launchKernel(); 127 + } 128 + const behavioralData = await readWorkspaceBehaviorData(this.props.title); 129 + this.setState({ 130 + eegFilePaths: workspaceCleanData.map(filepath => ({ 131 + key: filepath.name, 132 + text: filepath.name, 133 + value: filepath.path 134 + })), 135 + behaviorFilePaths: behavioralData.map(filepath => ({ 136 + key: filepath.name, 137 + text: filepath.name, 138 + value: filepath.path 139 + })), 140 + dependentVariables: ['Response Time', 'Accuracy'].map(dv => ({ 141 + key: dv, 142 + text: dv, 143 + value: dv 144 + })), 145 + selectedDependentVariable: 'Response Time' 146 + }); 147 + } 148 + 149 + handleStepClick(step: string) { 150 + this.setState({ activeStep: step }); 151 + } 152 + 153 + handleDatasetChange(event: Object, data: Object) { 154 + this.setState({ 155 + selectedFilePaths: data.value, 156 + selectedSubjects: getSubjectNamesFromFiles(data.value) 157 + }); 158 + this.props.jupyterActions.loadCleanedEpochs(data.value); 159 + } 160 + 161 + handleBehaviorDatasetChange(event: Object, data: Object) { 162 + const { 163 + dataToPlot, 164 + layout 165 + } = aggregateDataForPlot(readBehaviorData(data.value), this.state.selectedDependentVariable, this.state.removeOutliers, this.state.showDataPoints, this.state.displayMode); 166 + this.setState({ 167 + selectedBehaviorFilePaths: data.value, 168 + selectedSubjects: getSubjectNamesFromFiles(data.value), 169 + dataToPlot, 170 + layout 171 + }); 172 + } 173 + 174 + async handleDropdownClick() { 175 + const behavioralData = await readWorkspaceBehaviorData(this.props.title); 176 + if (behavioralData.length != this.state.behaviorFilePaths.length) { 177 + this.setState({ 178 + behaviorFilePaths: behavioralData.map(filepath => ({ 179 + key: filepath.name, 180 + text: filepath.name, 181 + value: filepath.path 182 + })) 183 + }); 184 + } 185 + } 186 + 187 + handleDependentVariableChange(event: Object, data: Object) { 188 + const { 189 + dataToPlot, 190 + layout 191 + } = aggregateDataForPlot(readBehaviorData(this.state.selectedBehaviorFilePaths), data.value, this.state.removeOutliers, this.state.showDataPoints, this.state.displayMode); 192 + this.setState({ 193 + selectedDependentVariable: data.value, 194 + dataToPlot, 195 + layout 196 + }); 197 + } 198 + 199 + handleRemoveOutliers(event: Object, data: Object) { 200 + const { 201 + dataToPlot, 202 + layout 203 + } = aggregateDataForPlot(readBehaviorData(this.state.selectedBehaviorFilePaths), this.state.selectedDependentVariable, !this.state.removeOutliers, this.state.showDataPoints, this.state.displayMode); 204 + this.setState({ 205 + removeOutliers: !this.state.removeOutliers, 206 + dataToPlot, 207 + layout, 208 + helpMode: 'outliers' 209 + }); 210 + } 211 + 212 + handleDataPoints(event: Object, data: Object) { 213 + const { 214 + dataToPlot, 215 + layout 216 + } = aggregateDataForPlot(readBehaviorData(this.state.selectedBehaviorFilePaths), this.state.selectedDependentVariable, this.state.removeOutliers, !this.state.showDataPoints, this.state.displayMode); 217 + this.setState({ 218 + showDataPoints: !this.state.showDataPoints, 219 + dataToPlot, 220 + layout 221 + }); 222 + } 223 + 224 + handleDisplayModeChange(displayMode) { 225 + if (this.state.selectedBehaviorFilePaths && this.state.selectedBehaviorFilePaths.length > 0) { 226 + const { 227 + dataToPlot, 228 + layout 229 + } = aggregateDataForPlot(readBehaviorData(this.state.selectedBehaviorFilePaths), this.state.selectedDependentVariable, this.state.removeOutliers, this.state.showDataPoints, displayMode); 230 + this.setState({ 231 + dataToPlot, 232 + layout, 233 + displayMode, 234 + helpMode: displayMode 235 + }); 236 + } 237 + } 238 + 239 + toggleDisplayInfoVisibility() { 240 + this.setState({ 241 + isSidebarVisible: !this.state.isSidebarVisible 242 + }); 243 + } 244 + 245 + saveSelectedDatasets() { 246 + const data = readBehaviorData(this.state.selectedBehaviorFilePaths); 247 + const aggregatedData = aggregateBehaviorDataToSave(data, this.state.removeOutliers); 248 + storeAggregatedBehaviorData(aggregatedData, this.props.title); 249 + } 250 + 251 + handleChannelSelect(channelName: string) { 252 + this.setState({ selectedChannel: channelName }); 253 + this.props.jupyterActions.loadERP(channelName); 254 + } 255 + 256 + concatSubjectNames = (subjects: Array<string | null | undefined>) => { 257 + if (subjects.length < 1) { 258 + return ''; 259 + } 260 + return subjects.reduce((acc, curr) => `${acc}-${curr}`); 261 + }; 262 + 263 + renderEpochLabels() { 264 + if (!isNil(this.props.epochsInfo) && this.state.selectedFilePaths.length >= 1) { 265 + const numberConditions = this.props.epochsInfo.filter(infoObj => infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs').length; 266 + let colors; 267 + if (numberConditions === 4) { 268 + colors = ['red', 'yellow', 'green', 'blue']; 269 + } else { 270 + colors = ['red', 'green', 'teal', 'orange']; 271 + } 272 + return <div> 273 + {this.props.epochsInfo.filter(infoObj => infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs').map((infoObj, index) => <> 274 + <Header as='h4'>{infoObj.name}</Header> 275 + <Icon name='circle' color={colors[index]} /> 276 + {infoObj.value} 277 + </>)} 278 + </div>; 279 + } 280 + return <div />; 281 + } 282 + 283 + renderHelpContent() { 284 + switch (this.state.helpMode) { 285 + case 'datapoints': 286 + return this.renderHelp('Data Points', `In this graph, each dot refers to one data point, clustered by group (e.g., conditions). 287 + It’s the most “neutral” way of presenting the data, of course, but it may be difficult to see any patterns. 288 + Why is it always a good idea to look at all your datapoints before interpreting any trends in the data?`); 289 + case 'errorbars': 290 + return this.renderHelp('Bar Graph', `Bar graphs are the most common way to summarize data. 291 + It allows you to compare mean values between groups of datapoints (e.g., conditions), 292 + and the error bars give some indication of the variance (here: the standard error of the mean). 293 + Importantly, this way of summarizing data assumes that the mean is in fact representative of the data. 294 + Many researchers have veered away from bar graphs because they can be deceptive, especially without error bars. 295 + Can you think of any such cases?`); 296 + case 'whiskers': 297 + return this.renderHelp('Box Plot', `Box plots summarize the data in a more informative way: 298 + they actually tell you something about the distribution of datapoints within a group, 299 + by taking the median as its reference point instead of the mean 300 + (the median is the “middle” point in a dataset after sorting it from the lowest to the highest value). 301 + The boxes represent so-called “quartiles” which are cut off at the value right between the median and the smallest value or highest value in the dataset. 302 + The lines (“whiskers”) show how much variability there is in the data outside of those quartiles; 303 + any outliers are shown as individual points. Can you go through each plot and describe exactly what you see? 304 + When you toggle between this view and the bar graph view, do the data look very different?`); 305 + case 'outliers':default: 306 + return this.renderHelp('Outliers', `A datapoint is tagged as an “outlier” if its value exceeds 2 standard deviations below or above the mean of all data in the group. 307 + If a datapoint is unusually high or low (it “deviates”) compared to the rest of the group, 308 + it is likely a special case that doesn’t tell us anything informative about the group as a whole. 309 + Removing such outliers can help unskew the data. What might outliers mean in your dataset? 310 + Can you think of any other cases where identifying outliers can be helpful?`); 311 + 312 + } 313 + } 314 + 315 + renderHelp(header: string, content: string) { 316 + return <> 317 + <Segment basic className={styles.helpContent}> 318 + <Button circular size='large' floated='right' icon='x' className={styles.closeButton} onClick={this.toggleDisplayInfoVisibility} /> 319 + <Header className={styles.helpHeader} as='h1'> 320 + {header} 321 + </Header> 322 + {content} 323 + </Segment> 324 + </>; 325 + } 326 + 327 + renderSectionContent() { 328 + switch (this.state.activeStep) { 329 + case ANALYZE_STEPS.OVERVIEW:default: 330 + return <> 331 + <Grid.Column width={4}> 332 + <Segment basic textAlign='left' className={styles.infoSegment}> 333 + <Header as='h1'>Overview</Header> 334 + <p> 335 + Load cleaned datasets from different subjects and view how the EEG differs between 336 + electrodes 337 + </p> 338 + <Header as='h4'>Select Clean Datasets</Header> 339 + <Dropdown fluid multiple selection closeOnChange value={this.state.selectedFilePaths} options={this.state.eegFilePaths} onChange={this.handleDatasetChange} /> 340 + {this.renderEpochLabels()} 341 + </Segment> 342 + </Grid.Column> 343 + <Grid.Column width={8}> 344 + <JupyterPlotWidget title={this.props.title} imageTitle={`${this.concatSubjectNames(this.state.selectedSubjects)}-Topoplot`} plotMIMEBundle={this.props.topoPlot} /> 345 + </Grid.Column> 346 + </>; 347 + case ANALYZE_STEPS.ERP: 348 + return <> 349 + <Grid.Column width={4} className={styles.analyzeColumn}> 350 + <Segment basic textAlign='left' className={styles.infoSegment}> 351 + <Header as='h1'>ERP</Header> 352 + <p> 353 + The event-related potential represents EEG activity elicited by a particular 354 + sensory event 355 + </p> 356 + <ClickableHeadDiagramSVG channelinfo={this.props.channelInfo} onChannelClick={this.handleChannelSelect} /> 357 + <Divider hidden /> 358 + {this.renderEpochLabels()} 359 + </Segment> 360 + </Grid.Column> 361 + <Grid.Column width={8}> 362 + <JupyterPlotWidget title={this.props.title} imageTitle={`${this.concatSubjectNames(this.state.selectedSubjects)}-${this.state.selectedChannel}-ERP`} plotMIMEBundle={this.props.erpPlot} /> 363 + </Grid.Column> 364 + </>; 365 + case ANALYZE_STEPS.BEHAVIOR: 366 + return <> 367 + <Grid.Column width={4}> 368 + <Segment basic textAlign='left' className={styles.infoSegment}> 369 + <Header as='h1'>Overview</Header> 370 + <p>Load datasets from different subjects and view behavioral results</p> 371 + 372 + <div> 373 + <span className='ui header'>Datasets</span> 374 + <Button className='export' onClick={this.saveSelectedDatasets}> 375 + <Icon name='download' /> 376 + Export 377 + </Button> 378 + </div> 379 + <p /> 380 + 381 + <Dropdown fluid multiple selection search closeOnChange value={this.state.selectedBehaviorFilePaths} options={this.state.behaviorFilePaths} onChange={this.handleBehaviorDatasetChange} onClick={this.handleDropdownClick} /> 382 + <p /> 383 + <Divider hidden /> 384 + <span className='ui header'>Dependent Variable</span> 385 + <p /> 386 + <Dropdown fluid selection closeOnChange value={this.state.selectedDependentVariable} options={this.state.dependentVariables} onChange={this.handleDependentVariableChange} /> 387 + </Segment> 388 + </Grid.Column> 389 + <Grid.Column width={12} style={{ 390 + overflow: 'auto', 391 + maxHeight: 650, 392 + display: 'grid', 393 + justifyContent: 'center' 394 + }}> 395 + <Segment basic textAlign='left' className={styles.plotSegment}> 396 + <Plot data={this.state.dataToPlot} layout={this.state.layout} /> 397 + <p /> 398 + <Checkbox checked={this.state.removeOutliers} label='Remove Response Time Outliers' onChange={this.handleRemoveOutliers} /> 399 + 400 + <p /> 401 + <Button.Group> 402 + <Button className='tertiary' toggle active={this.state.displayMode === 'datapoints'} onClick={() => this.handleDisplayModeChange('datapoints')}> 403 + Data Points 404 + </Button> 405 + <Button className='tertiary' toggle active={this.state.displayMode === 'errorbars'} onClick={() => this.handleDisplayModeChange('errorbars')}> 406 + Bar Graph 407 + </Button> 408 + <Button className='tertiary' toggle active={this.state.displayMode === 'whiskers'} onClick={() => this.handleDisplayModeChange('whiskers')}> 409 + Box Plot 410 + </Button> 411 + </Button.Group> 412 + 413 + <HelpButton onClick={this.toggleDisplayInfoVisibility} /> 414 + 415 + <Sidebar width='wide' direction='right' as={Segment} visible={this.state.isSidebarVisible}> 416 + <Segment basic padded vertical className={styles.helpSidebar}> 417 + {this.renderHelpContent()} 418 + </Segment> 419 + </Sidebar> 420 + </Segment> 421 + </Grid.Column> 422 + </>; 423 + 424 + } 425 + } 426 + 427 + render() { 428 + return <div className={styles.mainContainer}> 429 + <SecondaryNavComponent title='Analyze' steps={this.props.isEEGEnabled === true ? ANALYZE_STEPS : ANALYZE_STEPS_BEHAVIOR} activeStep={this.state.activeStep} onStepClick={this.handleStepClick} /> 430 + <Grid columns='equal' textAlign='center' verticalAlign='top' className={styles.contentGrid}> 431 + {this.renderSectionContent()} 432 + </Grid> 433 + </div>; 434 + } 435 + }
-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 - <> 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 - </> 81 - ); 82 - } 83 - 84 - renderHelp(header: string, content: string) { 85 - return ( 86 - <> 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 - </> 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 - }
+133
app/components/CleanComponent/CleanSidebar.tsx
··· 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 + 26 + props: Props; 27 + constructor(props) { 28 + super(props); 29 + this.state = { 30 + helpStep: HELP_STEP.MENU 31 + }; 32 + this.handleStartLearn = this.handleStartLearn.bind(this); 33 + this.handleStartSignal = this.handleStartSignal.bind(this); 34 + this.handleNext = this.handleNext.bind(this); 35 + this.handleBack = this.handleBack.bind(this); 36 + } 37 + 38 + handleStartSignal() { 39 + this.setState({ helpStep: HELP_STEP.SIGNAL_EXPLANATION }); 40 + } 41 + 42 + handleStartLearn() { 43 + this.setState({ helpStep: HELP_STEP.LEARN_BRAIN }); 44 + } 45 + 46 + handleNext() { 47 + if (this.state.helpStep === HELP_STEP.SIGNAL_MOVEMENT || this.state.helpStep === HELP_STEP.LEARN_ALPHA) { 48 + this.setState({ helpStep: HELP_STEP.MENU }); 49 + } else { 50 + this.setState({ helpStep: this.state.helpStep + 1 }); 51 + } 52 + } 53 + 54 + handleBack() { 55 + this.setState({ helpStep: this.state.helpStep - 1 }); 56 + } 57 + 58 + renderMenu() { 59 + return <> 60 + <Menu secondary vertical fluid> 61 + <Header className={styles.helpHeader} as='h1'> 62 + What would you like to do? 63 + </Header> 64 + <Menu.Item onClick={this.handleStartSignal}> 65 + <Segment basic className={styles.helpMenuItem}> 66 + <Icon name='star outline' size='large' /> 67 + Improve the signal quality of your sensors 68 + </Segment> 69 + </Menu.Item> 70 + <Menu.Item onClick={this.handleStartLearn}> 71 + <Segment basic className={styles.helpMenuItem}> 72 + <Icon name='exclamation triangle' size='large' /> 73 + Learn about how the subjects movements create noise 74 + </Segment> 75 + </Menu.Item> 76 + </Menu> 77 + </>; 78 + } 79 + 80 + renderHelp(header: string, content: string) { 81 + return <> 82 + <Segment basic className={styles.helpContent}> 83 + <Header className={styles.helpHeader} as='h1'> 84 + {header} 85 + </Header> 86 + {content} 87 + </Segment> 88 + <Grid columns='equal'> 89 + <Grid.Column> 90 + <Button fluid secondary onClick={this.handleBack}> 91 + Back 92 + </Button> 93 + </Grid.Column> 94 + <Grid.Column> 95 + <Button fluid primary onClick={this.handleNext}> 96 + Next 97 + </Button> 98 + </Grid.Column> 99 + </Grid> 100 + </>; 101 + } 102 + 103 + renderHelpContent() { 104 + switch (this.state.helpStep) { 105 + case HELP_STEP.SIGNAL_EXPLANATION: 106 + return this.renderHelp('Improve the signal quality', 'In order to collect quality data, you want to make sure that all electrodes have a strong connection'); 107 + case HELP_STEP.SIGNAL_SALINE: 108 + return this.renderHelp('Tip #1: Saturate the sensors in saline', 'Make sure the sensors are thoroughly soaked with saline solution. They should be wet to the touch'); 109 + case HELP_STEP.SIGNAL_CONTACT: 110 + return this.renderHelp('Tip #2: Ensure the sensors are making firm contact', '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'); 111 + case HELP_STEP.SIGNAL_MOVEMENT: 112 + return this.renderHelp('Tip #3: Stay still', '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'); 113 + case HELP_STEP.LEARN_BRAIN: 114 + return this.renderHelp('Your brain produces electricity', 'Using the device that you are wearing, we can detect the electrical activity of your brain.'); 115 + case HELP_STEP.LEARN_BLINK: 116 + return this.renderHelp('Try blinking your eyes', 'Does the signal change? Eye movements create noise in the EEG signal'); 117 + case HELP_STEP.LEARN_THOUGHTS: 118 + return this.renderHelp('Try thinking of a cat', "Does the signal change? Although EEG can measure overall brain activity, it's not capable of reading minds"); 119 + case HELP_STEP.LEARN_ALPHA: 120 + return this.renderHelp('Try closing your eyes for 10 seconds', 'You may notice a change in your signal due to an increase in alpha waves'); 121 + case HELP_STEP.MENU:default: 122 + return this.renderMenu(); 123 + 124 + } 125 + } 126 + 127 + render() { 128 + return <Segment basic padded vertical className={styles.helpSidebar}> 129 + <Button basic circular size='large' floated='right' icon='x' className={styles.closeButton} onClick={this.props.handleClose} /> 130 + {this.renderHelpContent()} 131 + </Segment>; 132 + } 133 + }
-235
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 - import * as path from 'path'; 22 - 23 - interface Props { 24 - type: ?EXPERIMENTS; 25 - title: string; 26 - deviceType: DEVICES; 27 - mainChannel: ?any; 28 - kernel: ?Kernel; 29 - kernelStatus: KERNEL_STATUS; 30 - epochsInfo: ?Array<{ [string]: number | string }>; 31 - jupyterActions: Object; 32 - experimentActions: Object; 33 - subject: string; 34 - session: number; 35 - } 36 - 37 - interface State { 38 - subjects: Array<?string>; 39 - eegFilePaths: Array<?{ 40 - key: string, 41 - text: string, 42 - value: { name: string, dir: string }, 43 - }>; 44 - selectedSubject: string; 45 - selectedFilePaths: Array<?string>; 46 - } 47 - 48 - export default class Clean extends Component<Props, State> { 49 - props: Props; 50 - state: State; 51 - handleRecordingChange: (Object, Object) => void; 52 - handleLoadData: () => void; 53 - handleSubjectChange: (Object, Object) => void; 54 - icons: string[]; 55 - 56 - constructor(props: Props) { 57 - super(props); 58 - this.state = { 59 - subjects: [], 60 - eegFilePaths: [{ key: '', text: '', value: '' }], 61 - selectedFilePaths: [], 62 - selectedSubject: props.subject, 63 - }; 64 - this.handleRecordingChange = this.handleRecordingChange.bind(this); 65 - this.handleLoadData = this.handleLoadData.bind(this); 66 - this.handleSidebarToggle = this.handleSidebarToggle.bind(this); 67 - this.handleSubjectChange = this.handleSubjectChange.bind(this); 68 - this.icons = 69 - props.type === EXPERIMENTS.N170 70 - ? ['smile', 'home', 'x', 'book'] 71 - : ['star', 'star outline', 'x', 'book']; 72 - } 73 - 74 - async componentDidMount() { 75 - const workspaceRawData = await readWorkspaceRawEEGData(this.props.title); 76 - if (this.props.kernelStatus === KERNEL_STATUS.OFFLINE) { 77 - this.props.jupyterActions.launchKernel(); 78 - } 79 - this.setState({ 80 - subjects: workspaceRawData 81 - .map((filepath) => filepath.path.split(path.sep)[filepath.path.split(path.sep).length - 3]) 82 - .reduce((acc, curr) => { 83 - if (acc.find((subject) => subject.key === curr)) { 84 - return acc; 85 - } 86 - return acc.concat({ 87 - key: curr, 88 - text: curr, 89 - value: curr, 90 - }); 91 - }, []), 92 - eegFilePaths: workspaceRawData.map((filepath) => ({ 93 - key: filepath.name, 94 - text: filepath.name, 95 - value: filepath.path, 96 - })), 97 - }); 98 - } 99 - 100 - handleRecordingChange(event: Object, data: Object) { 101 - this.setState({ selectedFilePaths: data.value }); 102 - } 103 - 104 - handleSubjectChange(event: Object, data: Object) { 105 - this.setState({ selectedSubject: data.value, selectedFilePaths: [] }); 106 - } 107 - 108 - handleLoadData() { 109 - this.props.experimentActions.setSubject(this.state.selectedSubject); 110 - this.props.jupyterActions.loadEpochs(this.state.selectedFilePaths); 111 - } 112 - 113 - handleSidebarToggle() { 114 - this.setState({ isSidebarVisible: !this.state.isSidebarVisible }); 115 - } 116 - 117 - renderEpochLabels() { 118 - if (!isNil(this.props.epochsInfo) && this.state.selectedFilePaths.length >= 1) { 119 - return ( 120 - <Segment basic textAlign='left'> 121 - {this.props.epochsInfo.map((infoObj, index) => ( 122 - <Segment key={infoObj.name} basic> 123 - <Icon name={this.icons[index]} /> 124 - {infoObj.name} 125 - <p>{infoObj.value}</p> 126 - </Segment> 127 - ))} 128 - </Segment> 129 - ); 130 - } 131 - return <div />; 132 - } 133 - 134 - renderAnalyzeButton() { 135 - if ( 136 - !isNil(this.props.epochsInfo) && 137 - this.props.epochsInfo.find((infoObj) => infoObj.name === 'Drop Percentage').value >= 2 138 - ) { 139 - return ( 140 - <Link to='/analyze'> 141 - <Button primary>Analyze Dataset</Button> 142 - </Link> 143 - ); 144 - } 145 - } 146 - 147 - render() { 148 - return ( 149 - <Sidebar.Pushable basic as={Segment} className={styles.preTestPushable}> 150 - <Sidebar width='wide' direction='right' as={Segment} visible={this.state.isSidebarVisible}> 151 - <CleanSidebar handleClose={this.handleSidebarToggle} /> 152 - </Sidebar> 153 - <SidebarPusher> 154 - <Grid 155 - columns='equal' 156 - textAlign='center' 157 - verticalAlign='middle' 158 - className={styles.preTestContainer} 159 - > 160 - <Grid.Row columns='equal'> 161 - <Grid.Column> 162 - <Header as='h1' floated='left'> 163 - Clean 164 - </Header> 165 - </Grid.Column> 166 - </Grid.Row> 167 - <Grid.Row> 168 - <Grid.Column width={6}> 169 - <Segment basic textAlign='left' className={styles.infoSegment}> 170 - <Header as='h1'>Select & Clean</Header> 171 - <p> 172 - Ready to clean some data? Select a subject and one or more EEG recordings, then 173 - launch the editor 174 - </p> 175 - <Header as='h4'>Select Subject</Header> 176 - <Dropdown 177 - fluid 178 - selection 179 - closeOnChange 180 - value={this.state.selectedSubject} 181 - options={this.state.subjects} 182 - onChange={this.handleSubjectChange} 183 - /> 184 - <Header as='h4'>Select Recordings</Header> 185 - <Dropdown 186 - fluid 187 - multiple 188 - selection 189 - closeOnChange 190 - value={this.state.selectedFilePaths} 191 - options={this.state.eegFilePaths.filter( 192 - (filepath) => 193 - this.state.selectedSubject === 194 - filepath.value.split(path.sep)[filepath.value.split(path.sep).length - 3] 195 - )} 196 - onChange={this.handleRecordingChange} 197 - /> 198 - <Divider hidden section /> 199 - <Grid textAlign='center' columns='equal'> 200 - <Grid.Column> 201 - <Button 202 - secondary 203 - disabled={this.props.kernelStatus !== KERNEL_STATUS.IDLE} 204 - loading={ 205 - this.props.kernelStatus === KERNEL_STATUS.STARTING || 206 - this.props.kernelStatus === KERNEL_STATUS.BUSY 207 - } 208 - onClick={this.handleLoadData} 209 - > 210 - Load Dataset 211 - </Button> 212 - </Grid.Column> 213 - <Grid.Column> 214 - <Button 215 - primary 216 - disabled={isNil(this.props.epochsInfo)} 217 - onClick={this.props.jupyterActions.cleanEpochs} 218 - > 219 - Clean Data 220 - </Button> 221 - </Grid.Column> 222 - </Grid> 223 - </Segment> 224 - </Grid.Column> 225 - <Grid.Column width={4}> 226 - {this.renderEpochLabels()} 227 - {this.renderAnalyzeButton()} 228 - </Grid.Column> 229 - </Grid.Row> 230 - </Grid> 231 - </SidebarPusher> 232 - </Sidebar.Pushable> 233 - ); 234 - } 235 - }
+176
app/components/CleanComponent/index.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Grid, Button, Icon, Segment, Header, Dropdown, Sidebar, SidebarPusher, Divider } from "semantic-ui-react"; 4 + import { Link } from "react-router-dom"; 5 + import { isNil } from "lodash"; 6 + import styles from "./../styles/collect.css"; 7 + import { EXPERIMENTS, DEVICES, KERNEL_STATUS } from "../../constants/constants"; 8 + import { Kernel } from "../../constants/interfaces"; 9 + import { readWorkspaceRawEEGData } from "../../utils/filesystem/storage"; 10 + import CleanSidebar from "./CleanSidebar"; 11 + import * as path from "path"; 12 + 13 + interface Props { 14 + type: EXPERIMENTS | null | undefined; 15 + title: string; 16 + deviceType: DEVICES; 17 + mainChannel: any | null | undefined; 18 + kernel: Kernel | null | undefined; 19 + kernelStatus: KERNEL_STATUS; 20 + epochsInfo: Array<{ 21 + [key: string]: number | string; 22 + }> | null | undefined; 23 + jupyterActions: Object; 24 + experimentActions: Object; 25 + subject: string; 26 + session: number; 27 + } 28 + 29 + interface State { 30 + subjects: Array<string | null | undefined>; 31 + eegFilePaths: Array<{ 32 + key: string; 33 + text: string; 34 + value: {name: string;dir: string;}; 35 + } | null | undefined>; 36 + selectedSubject: string; 37 + selectedFilePaths: Array<string | null | undefined>; 38 + } 39 + 40 + export default class Clean extends Component<Props, State> { 41 + 42 + props: Props; 43 + state: State; 44 + handleRecordingChange: (arg0: Object, arg1: Object) => void; 45 + handleLoadData: () => void; 46 + handleSubjectChange: (arg0: Object, arg1: Object) => void; 47 + icons: string[]; 48 + 49 + constructor(props: Props) { 50 + super(props); 51 + this.state = { 52 + subjects: [], 53 + eegFilePaths: [{ key: '', text: '', value: '' }], 54 + selectedFilePaths: [], 55 + selectedSubject: props.subject 56 + }; 57 + this.handleRecordingChange = this.handleRecordingChange.bind(this); 58 + this.handleLoadData = this.handleLoadData.bind(this); 59 + this.handleSidebarToggle = this.handleSidebarToggle.bind(this); 60 + this.handleSubjectChange = this.handleSubjectChange.bind(this); 61 + this.icons = props.type === EXPERIMENTS.N170 ? ['smile', 'home', 'x', 'book'] : ['star', 'star outline', 'x', 'book']; 62 + } 63 + 64 + async componentDidMount() { 65 + const workspaceRawData = await readWorkspaceRawEEGData(this.props.title); 66 + if (this.props.kernelStatus === KERNEL_STATUS.OFFLINE) { 67 + this.props.jupyterActions.launchKernel(); 68 + } 69 + this.setState({ 70 + subjects: workspaceRawData.map(filepath => filepath.path.split(path.sep)[filepath.path.split(path.sep).length - 3]).reduce((acc, curr) => { 71 + if (acc.find(subject => subject.key === curr)) { 72 + return acc; 73 + } 74 + return acc.concat({ 75 + key: curr, 76 + text: curr, 77 + value: curr 78 + }); 79 + }, []), 80 + eegFilePaths: workspaceRawData.map(filepath => ({ 81 + key: filepath.name, 82 + text: filepath.name, 83 + value: filepath.path 84 + })) 85 + }); 86 + } 87 + 88 + handleRecordingChange(event: Object, data: Object) { 89 + this.setState({ selectedFilePaths: data.value }); 90 + } 91 + 92 + handleSubjectChange(event: Object, data: Object) { 93 + this.setState({ selectedSubject: data.value, selectedFilePaths: [] }); 94 + } 95 + 96 + handleLoadData() { 97 + this.props.experimentActions.setSubject(this.state.selectedSubject); 98 + this.props.jupyterActions.loadEpochs(this.state.selectedFilePaths); 99 + } 100 + 101 + handleSidebarToggle() { 102 + this.setState({ isSidebarVisible: !this.state.isSidebarVisible }); 103 + } 104 + 105 + renderEpochLabels() { 106 + if (!isNil(this.props.epochsInfo) && this.state.selectedFilePaths.length >= 1) { 107 + return <Segment basic textAlign='left'> 108 + {this.props.epochsInfo.map((infoObj, index) => <Segment key={infoObj.name} basic> 109 + <Icon name={this.icons[index]} /> 110 + {infoObj.name} 111 + <p>{infoObj.value}</p> 112 + </Segment>)} 113 + </Segment>; 114 + } 115 + return <div />; 116 + } 117 + 118 + renderAnalyzeButton() { 119 + if (!isNil(this.props.epochsInfo) && this.props.epochsInfo.find(infoObj => infoObj.name === 'Drop Percentage').value >= 2) { 120 + return <Link to='/analyze'> 121 + <Button primary>Analyze Dataset</Button> 122 + </Link>; 123 + } 124 + } 125 + 126 + render() { 127 + return <Sidebar.Pushable basic as={Segment} className={styles.preTestPushable}> 128 + <Sidebar width='wide' direction='right' as={Segment} visible={this.state.isSidebarVisible}> 129 + <CleanSidebar handleClose={this.handleSidebarToggle} /> 130 + </Sidebar> 131 + <SidebarPusher> 132 + <Grid columns='equal' textAlign='center' verticalAlign='middle' className={styles.preTestContainer}> 133 + <Grid.Row columns='equal'> 134 + <Grid.Column> 135 + <Header as='h1' floated='left'> 136 + Clean 137 + </Header> 138 + </Grid.Column> 139 + </Grid.Row> 140 + <Grid.Row> 141 + <Grid.Column width={6}> 142 + <Segment basic textAlign='left' className={styles.infoSegment}> 143 + <Header as='h1'>Select & Clean</Header> 144 + <p> 145 + Ready to clean some data? Select a subject and one or more EEG recordings, then 146 + launch the editor 147 + </p> 148 + <Header as='h4'>Select Subject</Header> 149 + <Dropdown fluid selection closeOnChange value={this.state.selectedSubject} options={this.state.subjects} onChange={this.handleSubjectChange} /> 150 + <Header as='h4'>Select Recordings</Header> 151 + <Dropdown fluid multiple selection closeOnChange value={this.state.selectedFilePaths} options={this.state.eegFilePaths.filter(filepath => this.state.selectedSubject === filepath.value.split(path.sep)[filepath.value.split(path.sep).length - 3])} onChange={this.handleRecordingChange} /> 152 + <Divider hidden section /> 153 + <Grid textAlign='center' columns='equal'> 154 + <Grid.Column> 155 + <Button secondary disabled={this.props.kernelStatus !== KERNEL_STATUS.IDLE} loading={this.props.kernelStatus === KERNEL_STATUS.STARTING || this.props.kernelStatus === KERNEL_STATUS.BUSY} onClick={this.handleLoadData}> 156 + Load Dataset 157 + </Button> 158 + </Grid.Column> 159 + <Grid.Column> 160 + <Button primary disabled={isNil(this.props.epochsInfo)} onClick={this.props.jupyterActions.cleanEpochs}> 161 + Clean Data 162 + </Button> 163 + </Grid.Column> 164 + </Grid> 165 + </Segment> 166 + </Grid.Column> 167 + <Grid.Column width={4}> 168 + {this.renderEpochLabels()} 169 + {this.renderAnalyzeButton()} 170 + </Grid.Column> 171 + </Grid.Row> 172 + </Grid> 173 + </SidebarPusher> 174 + </Sidebar.Pushable>; 175 + } 176 + }
+39 -117
app/components/CollectComponent/ConnectModal.js app/components/CollectComponent/ConnectModal.tsx
··· 1 - import React, { Component } from 'react'; 2 - import { isNil, debounce } from 'lodash'; 3 - import { Modal, Button, Segment, List, Grid, Divider } from 'semantic-ui-react'; 4 - import { 5 - DEVICES, 6 - DEVICE_AVAILABILITY, 7 - CONNECTION_STATUS, 8 - SCREENS, 9 - } from '../../constants/constants'; 10 - import styles from '../styles/collect.css'; 1 + import React, { Component } from "react"; 2 + import { isNil, debounce } from "lodash"; 3 + import { Modal, Button, Segment, List, Grid, Divider } from "semantic-ui-react"; 4 + import { DEVICES, DEVICE_AVAILABILITY, CONNECTION_STATUS, SCREENS } from "../../constants/constants"; 5 + import styles from "../styles/collect.css"; 11 6 12 7 interface Props { 13 8 history: Object; 14 9 open: boolean; 15 10 onClose: () => void; 16 11 connectedDevice: Object; 17 - signalQualityObservable: ?any; 12 + signalQualityObservable: any | null | undefined; 18 13 deviceType: DEVICES; 19 14 deviceAvailability: DEVICE_AVAILABILITY; 20 15 connectionStatus: CONNECTION_STATUS; ··· 23 18 } 24 19 25 20 interface State { 26 - selectedDevice: ?any; 21 + selectedDevice: any | null | undefined; 27 22 instructionProgress: number; 28 23 } 29 24 30 25 const INSTRUCTION_PROGRESS = { 31 26 SEARCHING: 0, 32 27 TURN_ON: 1, 33 - COMPUTER_CONNECTABILITY: 2, 28 + COMPUTER_CONNECTABILITY: 2 34 29 }; 35 30 36 31 export default class ConnectModal extends Component<Props, State> { 37 - // handleConnect: () => void; 38 32 // handleSearch: () => void; 39 - // handleStartTutorial: () => void; 40 33 34 + // handleStartTutorial: () => void; 41 35 static getDeviceName(device: any) { 42 36 if (!isNil(device)) { 43 37 return isNil(device.name) ? device.id : device.name; ··· 49 43 super(props); 50 44 this.state = { 51 45 selectedDevice: null, 52 - instructionProgress: INSTRUCTION_PROGRESS.SEARCHING, 46 + instructionProgress: INSTRUCTION_PROGRESS.SEARCHING 53 47 }; 54 48 this.handleSearch = debounce(this.handleSearch.bind(this), 300, { 55 49 leading: true, 56 - trailing: false, 50 + trailing: false 57 51 }); 58 52 this.handleConnect = debounce(this.handleConnect.bind(this), 1000, { 59 53 leading: true, 60 - trailing: false, 54 + trailing: false 61 55 }); 62 56 this.handleinstructionProgress = this.handleinstructionProgress.bind(this); 63 57 } 64 58 65 59 componentWillUpdate(nextProps: Props) { 66 - if ( 67 - nextProps.deviceAvailability === DEVICE_AVAILABILITY.NONE && 68 - this.props.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING 69 - ) { 60 + if (nextProps.deviceAvailability === DEVICE_AVAILABILITY.NONE && this.props.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING) { 70 61 this.setState({ instructionProgress: 1 }); 71 62 } 72 - if ( 73 - nextProps.deviceAvailability === DEVICE_AVAILABILITY.AVAILABLE && 74 - this.props.deviceAvailability === DEVICE_AVAILABILITY.NONE 75 - ) { 63 + if (nextProps.deviceAvailability === DEVICE_AVAILABILITY.AVAILABLE && this.props.deviceAvailability === DEVICE_AVAILABILITY.NONE) { 76 64 this.setState({ instructionProgress: 0 }); 77 65 } 78 66 } ··· 93 81 } 94 82 95 83 renderAvailableDeviceList() { 96 - return ( 97 - <Segment basic> 84 + return <Segment basic> 98 85 <List divided relaxed inverted> 99 - {this.props.availableDevices.map((device) => ( 100 - <List.Item className={styles.deviceItem} key={device.id}> 101 - <List.Icon 102 - link 103 - name={ 104 - this.state.selectedDevice === device ? 'check circle outline' : 'circle outline' 105 - } 106 - size='large' 107 - verticalAlign='middle' 108 - onClick={() => this.setState({ selectedDevice: device })} 109 - /> 86 + {this.props.availableDevices.map(device => <List.Item className={styles.deviceItem} key={device.id}> 87 + <List.Icon link name={this.state.selectedDevice === device ? 'check circle outline' : 'circle outline'} size='large' verticalAlign='middle' onClick={() => this.setState({ selectedDevice: device })} /> 110 88 <List.Content> 111 89 <List.Header>{ConnectModal.getDeviceName(device)}</List.Header> 112 90 </List.Content> 113 - </List.Item> 114 - ))} 91 + </List.Item>)} 115 92 </List> 116 - </Segment> 117 - ); 93 + </Segment>; 118 94 } 119 95 120 96 renderContent() { 121 97 if (this.props.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING) { 122 - return ( 123 - <> 98 + return <> 124 99 <Modal.Content className={styles.searchingText}> 125 100 Searching for available headset(s)... 126 101 </Modal.Content> 127 - </> 128 - ); 102 + </>; 129 103 } 130 104 if (this.props.connectionStatus === CONNECTION_STATUS.CONNECTING) { 131 - return ( 132 - <> 105 + return <> 133 106 <Modal.Content className={styles.searchingText}> 134 107 Connecting to {ConnectModal.getDeviceName(this.state.selectedDevice)} 135 108 ... 136 109 </Modal.Content> 137 - </> 138 - ); 110 + </>; 139 111 } 140 112 if (this.state.instructionProgress === INSTRUCTION_PROGRESS.TURN_ON) { 141 - return ( 142 - <> 113 + return <> 143 114 <Modal.Header className={styles.connectHeader}>Turn your headset on</Modal.Header> 144 115 <Modal.Content> 145 116 Make sure your headset is on and fully charged. ··· 150 121 <Modal.Content> 151 122 <Grid textAlign='center' columns='equal'> 152 123 <Grid.Column> 153 - {this.state.step !== 0 && ( 154 - <Button 155 - fluid 156 - className={styles.secondaryButton} 157 - onClick={() => this.handleinstructionProgress(0)} 158 - > 124 + {this.state.step !== 0 && <Button fluid className={styles.secondaryButton} onClick={() => this.handleinstructionProgress(0)}> 159 125 Back 160 - </Button> 161 - )} 126 + </Button>} 162 127 </Grid.Column> 163 128 <Grid.Column> 164 - <Button 165 - fluid 166 - className={styles.primaryButton} 167 - onClick={() => 168 - this.handleinstructionProgress(INSTRUCTION_PROGRESS.COMPUTER_CONNECTABILITY) 169 - } 170 - > 129 + <Button fluid className={styles.primaryButton} onClick={() => this.handleinstructionProgress(INSTRUCTION_PROGRESS.COMPUTER_CONNECTABILITY)}> 171 130 Next 172 131 </Button> 173 132 </Grid.Column> 174 133 </Grid> 175 134 </Modal.Content> 176 - </> 177 - ); 135 + </>; 178 136 } 179 137 if (this.state.instructionProgress === INSTRUCTION_PROGRESS.COMPUTER_CONNECTABILITY) { 180 - return ( 181 - <> 138 + return <> 182 139 <Modal.Header className={styles.connectHeader}>Insert the USB Receiver</Modal.Header> 183 140 <Modal.Content> 184 141 Insert the USB receiver into a USB port on your computer. Ensure that the LED on the ··· 188 145 <Modal.Content> 189 146 <Grid textAlign='center' columns='equal'> 190 147 <Grid.Column> 191 - <Button 192 - fluid 193 - className={styles.secondaryButton} 194 - onClick={() => this.handleinstructionProgress(INSTRUCTION_PROGRESS.TURN_ON)} 195 - > 148 + <Button fluid className={styles.secondaryButton} onClick={() => this.handleinstructionProgress(INSTRUCTION_PROGRESS.TURN_ON)}> 196 149 Back 197 150 </Button> 198 151 </Grid.Column> ··· 203 156 </Grid.Column> 204 157 </Grid> 205 158 </Modal.Content> 206 - </> 207 - ); 159 + </>; 208 160 } 209 161 if (this.props.deviceAvailability === DEVICE_AVAILABILITY.AVAILABLE) { 210 - return ( 211 - <> 162 + return <> 212 163 <Modal.Header className={styles.connectHeader}>Headset(s) found</Modal.Header> 213 164 <Modal.Content>Please select which headset you would like to connect.</Modal.Content> 214 165 <Modal.Content>{this.renderAvailableDeviceList()}</Modal.Content> ··· 216 167 <Modal.Content> 217 168 <Grid textAlign='center' columns='equal'> 218 169 <Grid.Column> 219 - <Button 220 - fluid 221 - className={styles.secondaryButton} 222 - onClick={() => this.handleinstructionProgress(1)} 223 - > 170 + <Button fluid className={styles.secondaryButton} onClick={() => this.handleinstructionProgress(1)}> 224 171 Back 225 172 </Button> 226 173 </Grid.Column> 227 174 <Grid.Column> 228 - <Button 229 - fluid 230 - className={styles.primaryButton} 231 - disabled={isNil(this.state.selectedDevice)} 232 - onClick={this.handleConnect} 233 - > 175 + <Button fluid className={styles.primaryButton} disabled={isNil(this.state.selectedDevice)} onClick={this.handleConnect}> 234 176 Connect 235 177 </Button> 236 178 </Grid.Column> ··· 241 183 Don&#39;t see your device? 242 184 </a> 243 185 </Modal.Content> 244 - </> 245 - ); 186 + </>; 246 187 } 247 188 } 248 189 249 190 render() { 250 - return ( 251 - <Modal 252 - basic 253 - centered 254 - closeOnDimmerClick 255 - closeOnDocumentClick 256 - open={this.props.open} 257 - onOpen={this.handleSearch} 258 - className={styles.connectModal} 259 - > 260 - <Button 261 - circular 262 - basic 263 - bordered='false' 264 - inverted 265 - size='large' 266 - icon='x' 267 - className={styles.modalCloseButton} 268 - onClick={this.props.onClose} 269 - /> 191 + return <Modal basic centered closeOnDimmerClick closeOnDocumentClick open={this.props.open} onOpen={this.handleSearch} className={styles.connectModal}> 192 + <Button circular basic bordered='false' inverted size='large' icon='x' className={styles.modalCloseButton} onClick={this.props.onClose} /> 270 193 {this.renderContent()} 271 - </Modal> 272 - ); 194 + </Modal>; 273 195 } 274 - } 196 + }
-185
app/components/CollectComponent/HelpSidebar.js
··· 1 - /* eslint-disable react/no-multi-comp */ 2 - import React, { Component } from 'react'; 3 - import { Segment, Header, Menu, Icon, Button, Grid } from 'semantic-ui-react'; 4 - import styles from '../styles/common.css'; 5 - 6 - const HELP_STEP = { 7 - MENU: 0, 8 - SIGNAL_EXPLANATION: 1, 9 - SIGNAL_SALINE: 2, 10 - SIGNAL_CONTACT: 3, 11 - SIGNAL_MOVEMENT: 4, 12 - LEARN_BRAIN: 5, 13 - LEARN_BLINK: 6, 14 - LEARN_THOUGHTS: 7, 15 - LEARN_ALPHA: 8, 16 - }; 17 - 18 - interface Props { 19 - handleClose: () => void; 20 - } 21 - 22 - interface State { 23 - helpStep: HELP_STEP; 24 - } 25 - 26 - // TODO: Refactor this into a more reusable Sidebar component that can be used in Collect, Clean, and Analyze screen 27 - export class HelpSidebar extends Component<Props, State> { 28 - // props: Props; 29 - 30 - constructor(props) { 31 - super(props); 32 - this.state = { 33 - helpStep: HELP_STEP.MENU, 34 - }; 35 - this.handleStartLearn = this.handleStartLearn.bind(this); 36 - this.handleStartSignal = this.handleStartSignal.bind(this); 37 - this.handleNext = this.handleNext.bind(this); 38 - this.handleBack = this.handleBack.bind(this); 39 - } 40 - 41 - handleStartSignal() { 42 - this.setState({ helpStep: HELP_STEP.SIGNAL_EXPLANATION }); 43 - } 44 - 45 - handleStartLearn() { 46 - this.setState({ helpStep: HELP_STEP.LEARN_BRAIN }); 47 - } 48 - 49 - handleNext() { 50 - if ( 51 - this.state.helpStep === HELP_STEP.SIGNAL_MOVEMENT || 52 - this.state.helpStep === HELP_STEP.LEARN_ALPHA 53 - ) { 54 - this.setState({ helpStep: HELP_STEP.MENU }); 55 - } else { 56 - this.setState((prevState) => ({ ...prevState, helpStep: prevState.helpStep + 1 })); 57 - } 58 - } 59 - 60 - handleBack() { 61 - this.setState((prevState) => ({ ...prevState, helpStep: prevState.helpStep - 1 })); 62 - } 63 - 64 - renderMenu() { 65 - return ( 66 - <> 67 - <Menu secondary vertical fluid> 68 - <Header className={styles.helpHeader} as='h1'> 69 - What would you like to do? 70 - </Header> 71 - <Menu.Item onClick={this.handleStartSignal}> 72 - <Segment basic className={styles.helpMenuItem}> 73 - <Icon name='star outline' size='large' /> 74 - Improve the signal quality of your sensors 75 - </Segment> 76 - </Menu.Item> 77 - <Menu.Item onClick={this.handleStartLearn}> 78 - <Segment basic className={styles.helpMenuItem}> 79 - <Icon name='exclamation triangle' size='large' /> 80 - Learn about how the subjects movements create noise 81 - </Segment> 82 - </Menu.Item> 83 - </Menu> 84 - </> 85 - ); 86 - } 87 - 88 - renderHelp(header: string, content: string) { 89 - return ( 90 - <> 91 - <Segment basic className={styles.helpContent}> 92 - <Header className={styles.helpHeader} as='h1'> 93 - {header} 94 - </Header> 95 - {content} 96 - </Segment> 97 - <Grid columns='equal'> 98 - <Grid.Column> 99 - <Button fluid secondary onClick={this.handleBack}> 100 - Back 101 - </Button> 102 - </Grid.Column> 103 - <Grid.Column> 104 - <Button fluid primary onClick={this.handleNext}> 105 - Next 106 - </Button> 107 - </Grid.Column> 108 - </Grid> 109 - </> 110 - ); 111 - } 112 - 113 - renderHelpContent() { 114 - switch (this.state.helpStep) { 115 - case HELP_STEP.SIGNAL_EXPLANATION: 116 - return this.renderHelp( 117 - 'Improve the signal quality', 118 - 'In order to collect quality data, you want to make sure that all electrodes have a strong connection' 119 - ); 120 - case HELP_STEP.SIGNAL_SALINE: 121 - return this.renderHelp( 122 - 'Tip #1: Saturate the sensors in saline', 123 - 'Make sure the sensors are thoroughly soaked with saline solution. They should be wet to the touch' 124 - ); 125 - case HELP_STEP.SIGNAL_CONTACT: 126 - return this.renderHelp( 127 - 'Tip #2: Ensure the sensors are making firm contact', 128 - 'Re-seat the headset to make sure that all sensors contact the head with some tension. Take extra care to make sure the reference electrodes (the ones right behind the ears) make proper contact. You may need to sweep hair out of the way to accomplish this' 129 - ); 130 - case HELP_STEP.SIGNAL_MOVEMENT: 131 - return this.renderHelp( 132 - 'Tip #3: Stay still', 133 - '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' 134 - ); 135 - case HELP_STEP.LEARN_BRAIN: 136 - return this.renderHelp( 137 - 'Your brain produces electricity', 138 - 'Using the device that you are wearing, we can detect the electrical activity of your brain.' 139 - ); 140 - case HELP_STEP.LEARN_BLINK: 141 - return this.renderHelp( 142 - 'Try blinking your eyes', 143 - 'Does the signal change? Eye movements create noise in the EEG signal' 144 - ); 145 - case HELP_STEP.LEARN_THOUGHTS: 146 - return this.renderHelp( 147 - 'Try thinking of a cat', 148 - "Does the signal change? Although EEG can measure overall brain activity, it's not capable of reading minds" 149 - ); 150 - case HELP_STEP.LEARN_ALPHA: 151 - return this.renderHelp( 152 - 'Try closing your eyes for 10 seconds', 153 - 'You may notice a change in your signal due to an increase in alpha waves' 154 - ); 155 - case HELP_STEP.MENU: 156 - default: 157 - return this.renderMenu(); 158 - } 159 - } 160 - 161 - render() { 162 - return ( 163 - <Segment basic padded vertical className={styles.helpSidebar}> 164 - <Segment basic className={styles.closeButton}> 165 - <Button circular size='large' icon='x' onClick={this.props.handleClose} /> 166 - </Segment> 167 - {this.renderHelpContent()} 168 - </Segment> 169 - ); 170 - } 171 - } 172 - 173 - export class HelpButton extends Component<{ onClick: () => void }, {}> { 174 - render() { 175 - return ( 176 - <Button 177 - circular 178 - icon='question' 179 - className={styles.helpButton} 180 - floated='right' 181 - onClick={this.props.onClick} 182 - /> 183 - ); 184 - } 185 - }
+145
app/components/CollectComponent/HelpSidebar.tsx
··· 1 + /* eslint-disable react/no-multi-comp */ 2 + import React, { Component } from "react"; 3 + import { Segment, Header, Menu, Icon, Button, Grid } from "semantic-ui-react"; 4 + import styles from "../styles/common.css"; 5 + 6 + const HELP_STEP = { 7 + MENU: 0, 8 + SIGNAL_EXPLANATION: 1, 9 + SIGNAL_SALINE: 2, 10 + SIGNAL_CONTACT: 3, 11 + SIGNAL_MOVEMENT: 4, 12 + LEARN_BRAIN: 5, 13 + LEARN_BLINK: 6, 14 + LEARN_THOUGHTS: 7, 15 + LEARN_ALPHA: 8 16 + }; 17 + 18 + interface Props { 19 + handleClose: () => void; 20 + } 21 + 22 + interface State { 23 + helpStep: HELP_STEP; 24 + } 25 + 26 + // TODO: Refactor this into a more reusable Sidebar component that can be used in Collect, Clean, and Analyze screen 27 + export class HelpSidebar extends Component<Props, State> { 28 + // props: Props; 29 + 30 + constructor(props) { 31 + super(props); 32 + this.state = { 33 + helpStep: HELP_STEP.MENU 34 + }; 35 + this.handleStartLearn = this.handleStartLearn.bind(this); 36 + this.handleStartSignal = this.handleStartSignal.bind(this); 37 + this.handleNext = this.handleNext.bind(this); 38 + this.handleBack = this.handleBack.bind(this); 39 + } 40 + 41 + handleStartSignal() { 42 + this.setState({ helpStep: HELP_STEP.SIGNAL_EXPLANATION }); 43 + } 44 + 45 + handleStartLearn() { 46 + this.setState({ helpStep: HELP_STEP.LEARN_BRAIN }); 47 + } 48 + 49 + handleNext() { 50 + if (this.state.helpStep === HELP_STEP.SIGNAL_MOVEMENT || this.state.helpStep === HELP_STEP.LEARN_ALPHA) { 51 + this.setState({ helpStep: HELP_STEP.MENU }); 52 + } else { 53 + this.setState(prevState => ({ ...prevState, helpStep: prevState.helpStep + 1 })); 54 + } 55 + } 56 + 57 + handleBack() { 58 + this.setState(prevState => ({ ...prevState, helpStep: prevState.helpStep - 1 })); 59 + } 60 + 61 + renderMenu() { 62 + return <> 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 + </>; 81 + } 82 + 83 + renderHelp(header: string, content: string) { 84 + return <> 85 + <Segment basic className={styles.helpContent}> 86 + <Header className={styles.helpHeader} as='h1'> 87 + {header} 88 + </Header> 89 + {content} 90 + </Segment> 91 + <Grid columns='equal'> 92 + <Grid.Column> 93 + <Button fluid secondary onClick={this.handleBack}> 94 + Back 95 + </Button> 96 + </Grid.Column> 97 + <Grid.Column> 98 + <Button fluid primary onClick={this.handleNext}> 99 + Next 100 + </Button> 101 + </Grid.Column> 102 + </Grid> 103 + </>; 104 + } 105 + 106 + renderHelpContent() { 107 + switch (this.state.helpStep) { 108 + case HELP_STEP.SIGNAL_EXPLANATION: 109 + return this.renderHelp('Improve the signal quality', 'In order to collect quality data, you want to make sure that all electrodes have a strong connection'); 110 + case HELP_STEP.SIGNAL_SALINE: 111 + return this.renderHelp('Tip #1: Saturate the sensors in saline', 'Make sure the sensors are thoroughly soaked with saline solution. They should be wet to the touch'); 112 + case HELP_STEP.SIGNAL_CONTACT: 113 + return this.renderHelp('Tip #2: Ensure the sensors are making firm contact', 'Re-seat the headset to make sure that all sensors contact the head with some tension. Take extra care to make sure the reference electrodes (the ones right behind the ears) make proper contact. You may need to sweep hair out of the way to accomplish this'); 114 + case HELP_STEP.SIGNAL_MOVEMENT: 115 + return this.renderHelp('Tip #3: Stay still', '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'); 116 + case HELP_STEP.LEARN_BRAIN: 117 + return this.renderHelp('Your brain produces electricity', 'Using the device that you are wearing, we can detect the electrical activity of your brain.'); 118 + case HELP_STEP.LEARN_BLINK: 119 + return this.renderHelp('Try blinking your eyes', 'Does the signal change? Eye movements create noise in the EEG signal'); 120 + case HELP_STEP.LEARN_THOUGHTS: 121 + return this.renderHelp('Try thinking of a cat', "Does the signal change? Although EEG can measure overall brain activity, it's not capable of reading minds"); 122 + case HELP_STEP.LEARN_ALPHA: 123 + return this.renderHelp('Try closing your eyes for 10 seconds', 'You may notice a change in your signal due to an increase in alpha waves'); 124 + case HELP_STEP.MENU:default: 125 + return this.renderMenu(); 126 + 127 + } 128 + } 129 + 130 + render() { 131 + return <Segment basic padded vertical className={styles.helpSidebar}> 132 + <Segment basic className={styles.closeButton}> 133 + <Button circular size='large' icon='x' onClick={this.props.handleClose} /> 134 + </Segment> 135 + {this.renderHelpContent()} 136 + </Segment>; 137 + } 138 + } 139 + 140 + export class HelpButton extends Component<{onClick: () => void;}, {}> { 141 + 142 + render() { 143 + return <Button circular icon='question' className={styles.helpButton} floated='right' onClick={this.props.onClick} />; 144 + } 145 + }
-186
app/components/CollectComponent/PreTestComponent.js
··· 1 - import React, { Component } from 'react'; 2 - import { Grid, Segment, Button, List, Header, Sidebar } from 'semantic-ui-react'; 3 - import Mousetrap from 'mousetrap'; 4 - import ViewerComponent from '../ViewerComponent'; 5 - import SignalQualityIndicatorComponent from '../SignalQualityIndicatorComponent'; 6 - import PreviewExperimentComponent from '../PreviewExperimentComponent'; 7 - import PreviewButton from '../PreviewButtonComponent'; 8 - import { HelpSidebar, HelpButton } from './HelpSidebar'; 9 - import styles from '../styles/collect.css'; 10 - import { PLOTTING_INTERVAL, CONNECTION_STATUS } from '../../constants/constants'; 11 - import { loadProtocol } from '../../utils/labjs/functions'; 12 - 13 - interface Props { 14 - experimentActions: Object; 15 - connectedDevice: Object; 16 - signalQualityObservable: ?any; 17 - deviceType: DEVICES; 18 - deviceAvailability: DEVICE_AVAILABILITY; 19 - connectionStatus: CONNECTION_STATUS; 20 - deviceActions: Object; 21 - experimentActions: Object; 22 - availableDevices: Array<any>; 23 - type: EXPERIMENTS; 24 - paradigm: EXPERIMENTS; 25 - isRunning: boolean; 26 - params: ExperimentParameters; 27 - mainTimeline: MainTimeline; 28 - trials: { [string]: Trial }; 29 - timelines: {}; 30 - subject: string; 31 - group: string; 32 - session: number; 33 - openRunComponent: () => void; 34 - } 35 - 36 - interface State { 37 - isPreviewing: boolean; 38 - isSidebarVisible: boolean; 39 - } 40 - 41 - export default class PreTestComponent extends Component<Props, State> { 42 - // props: Props; 43 - // state: State; 44 - // handlePreview: () => void; 45 - 46 - constructor(props: Props) { 47 - super(props); 48 - this.state = { 49 - isPreviewing: false, 50 - isSidebarVisible: true, 51 - }; 52 - this.handlePreview = this.handlePreview.bind(this); 53 - this.handleSidebarToggle = this.handleSidebarToggle.bind(this); 54 - this.endPreview = this.endPreview.bind(this); 55 - } 56 - 57 - componentDidMount() { 58 - Mousetrap.bind('esc', this.props.experimentActions.stop); 59 - } 60 - 61 - componentWillUnmount() { 62 - Mousetrap.unbind('esc'); 63 - } 64 - 65 - endPreview() { 66 - this.setState({ isPreviewing: false }); 67 - } 68 - 69 - handlePreview(e) { 70 - e.target.blur(); 71 - this.setState((prevState) => ({ 72 - ...prevState, 73 - isSidebarVisible: false, 74 - isPreviewing: !prevState.isPreviewing, 75 - })); 76 - } 77 - 78 - handleSidebarToggle() { 79 - this.setState((prevState) => ({ 80 - ...prevState, 81 - isSidebarVisible: !prevState.isSidebarVisible, 82 - })); 83 - } 84 - 85 - renderSignalQualityOrPreview() { 86 - if (this.state.isPreviewing) { 87 - return ( 88 - <PreviewExperimentComponent 89 - {...loadProtocol(this.props.paradigm)} 90 - isPreviewing={this.state.isPreviewing} 91 - onEnd={this.endPreview} 92 - type={this.props.type} 93 - paradigm={this.props.paradigm} 94 - previewParams={this.props.params} 95 - title={this.props.title} 96 - /> 97 - ); 98 - } 99 - return ( 100 - <Segment basic> 101 - <SignalQualityIndicatorComponent 102 - signalQualityObservable={this.props.signalQualityObservable} 103 - plottingInterval={PLOTTING_INTERVAL} 104 - /> 105 - <Segment basic> 106 - <List> 107 - <List.Item> 108 - <List.Icon name='circle' className={styles.greatSignal} /> 109 - <List.Content>Strong Signal</List.Content> 110 - </List.Item> 111 - <List.Item> 112 - <List.Icon name='circle' className={styles.okSignal} /> 113 - <List.Content>Mediocre signal</List.Content> 114 - </List.Item> 115 - <List.Item> 116 - <List.Icon name='circle' className={styles.badSignal} /> 117 - <List.Content>Weak Signal</List.Content> 118 - </List.Item> 119 - <List.Item> 120 - <List.Icon name='circle' className={styles.noSignal} /> 121 - <List.Content>No Signal</List.Content> 122 - </List.Item> 123 - </List> 124 - </Segment> 125 - </Segment> 126 - ); 127 - } 128 - 129 - renderHelpButton() { 130 - if (!this.state.isSidebarVisible) { 131 - return <HelpButton onClick={this.handleSidebarToggle} />; 132 - } 133 - } 134 - 135 - render() { 136 - return ( 137 - <Sidebar.Pushable as={Segment} className={styles.preTestPushable} basic> 138 - <Sidebar width='wide' direction='right' as={Segment} visible={this.state.isSidebarVisible}> 139 - <HelpSidebar handleClose={this.handleSidebarToggle} /> 140 - </Sidebar> 141 - <Sidebar.Pusher> 142 - <Grid 143 - className={styles.preTestContainer} 144 - columns='equal' 145 - textAlign='center' 146 - verticalAlign='middle' 147 - > 148 - <Grid.Row columns='equal'> 149 - <Grid.Column> 150 - <Header as='h1' floated='left'> 151 - Collect 152 - </Header> 153 - </Grid.Column> 154 - <Grid.Column floated='right'> 155 - <PreviewButton 156 - isPreviewing={this.state.isPreviewing} 157 - onClick={(e) => this.handlePreview(e)} 158 - /> 159 - <Button 160 - primary 161 - disabled={this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED} 162 - onClick={this.props.openRunComponent} 163 - > 164 - Run & Record Experiment 165 - </Button> 166 - </Grid.Column> 167 - </Grid.Row> 168 - <Grid.Row> 169 - <Grid.Column width={8} className={styles.previewEEGWindow}> 170 - {this.renderSignalQualityOrPreview()} 171 - </Grid.Column> 172 - <Grid.Column width={8}> 173 - <ViewerComponent 174 - signalQualityObservable={this.props.signalQualityObservable} 175 - deviceType={this.props.deviceType} 176 - plottingInterval={PLOTTING_INTERVAL} 177 - /> 178 - {this.renderHelpButton()} 179 - </Grid.Column> 180 - </Grid.Row> 181 - </Grid> 182 - </Sidebar.Pusher> 183 - </Sidebar.Pushable> 184 - ); 185 - } 186 - }
+154
app/components/CollectComponent/PreTestComponent.tsx
··· 1 + import React, { Component } from "react"; 2 + import { Grid, Segment, Button, List, Header, Sidebar } from "semantic-ui-react"; 3 + import Mousetrap from "mousetrap"; 4 + import ViewerComponent from "../ViewerComponent"; 5 + import SignalQualityIndicatorComponent from "../SignalQualityIndicatorComponent"; 6 + import PreviewExperimentComponent from "../PreviewExperimentComponent"; 7 + import PreviewButton from "../PreviewButtonComponent"; 8 + import { HelpSidebar, HelpButton } from "./HelpSidebar"; 9 + import styles from "../styles/collect.css"; 10 + import { PLOTTING_INTERVAL, CONNECTION_STATUS } from "../../constants/constants"; 11 + import { loadProtocol } from "../../utils/labjs/functions"; 12 + 13 + interface Props { 14 + experimentActions: Object; 15 + connectedDevice: Object; 16 + signalQualityObservable: any | null | undefined; 17 + deviceType: DEVICES; 18 + deviceAvailability: DEVICE_AVAILABILITY; 19 + connectionStatus: CONNECTION_STATUS; 20 + deviceActions: Object; 21 + experimentActions: Object; 22 + availableDevices: Array<any>; 23 + type: EXPERIMENTS; 24 + paradigm: EXPERIMENTS; 25 + isRunning: boolean; 26 + params: ExperimentParameters; 27 + mainTimeline: MainTimeline; 28 + trials: { 29 + [key: string]: Trial; 30 + }; 31 + timelines: {}; 32 + subject: string; 33 + group: string; 34 + session: number; 35 + openRunComponent: () => void; 36 + } 37 + 38 + interface State { 39 + isPreviewing: boolean; 40 + isSidebarVisible: boolean; 41 + } 42 + 43 + export default class PreTestComponent extends Component<Props, State> { 44 + // state: State; 45 + 46 + // handlePreview: () => void; 47 + constructor(props: Props) { 48 + super(props); 49 + this.state = { 50 + isPreviewing: false, 51 + isSidebarVisible: true 52 + }; 53 + this.handlePreview = this.handlePreview.bind(this); 54 + this.handleSidebarToggle = this.handleSidebarToggle.bind(this); 55 + this.endPreview = this.endPreview.bind(this); 56 + } 57 + 58 + componentDidMount() { 59 + Mousetrap.bind('esc', this.props.experimentActions.stop); 60 + } 61 + 62 + componentWillUnmount() { 63 + Mousetrap.unbind('esc'); 64 + } 65 + 66 + endPreview() { 67 + this.setState({ isPreviewing: false }); 68 + } 69 + 70 + handlePreview(e) { 71 + e.target.blur(); 72 + this.setState(prevState => ({ 73 + ...prevState, 74 + isSidebarVisible: false, 75 + isPreviewing: !prevState.isPreviewing 76 + })); 77 + } 78 + 79 + handleSidebarToggle() { 80 + this.setState(prevState => ({ 81 + ...prevState, 82 + isSidebarVisible: !prevState.isSidebarVisible 83 + })); 84 + } 85 + 86 + renderSignalQualityOrPreview() { 87 + if (this.state.isPreviewing) { 88 + return <PreviewExperimentComponent {...loadProtocol(this.props.paradigm)} isPreviewing={this.state.isPreviewing} onEnd={this.endPreview} type={this.props.type} paradigm={this.props.paradigm} previewParams={this.props.params} title={this.props.title} />; 89 + } 90 + return <Segment basic> 91 + <SignalQualityIndicatorComponent signalQualityObservable={this.props.signalQualityObservable} plottingInterval={PLOTTING_INTERVAL} /> 92 + <Segment basic> 93 + <List> 94 + <List.Item> 95 + <List.Icon name='circle' className={styles.greatSignal} /> 96 + <List.Content>Strong Signal</List.Content> 97 + </List.Item> 98 + <List.Item> 99 + <List.Icon name='circle' className={styles.okSignal} /> 100 + <List.Content>Mediocre signal</List.Content> 101 + </List.Item> 102 + <List.Item> 103 + <List.Icon name='circle' className={styles.badSignal} /> 104 + <List.Content>Weak Signal</List.Content> 105 + </List.Item> 106 + <List.Item> 107 + <List.Icon name='circle' className={styles.noSignal} /> 108 + <List.Content>No Signal</List.Content> 109 + </List.Item> 110 + </List> 111 + </Segment> 112 + </Segment>; 113 + } 114 + 115 + renderHelpButton() { 116 + if (!this.state.isSidebarVisible) { 117 + return <HelpButton onClick={this.handleSidebarToggle} />; 118 + } 119 + } 120 + 121 + render() { 122 + return <Sidebar.Pushable as={Segment} className={styles.preTestPushable} basic> 123 + <Sidebar width='wide' direction='right' as={Segment} visible={this.state.isSidebarVisible}> 124 + <HelpSidebar handleClose={this.handleSidebarToggle} /> 125 + </Sidebar> 126 + <Sidebar.Pusher> 127 + <Grid className={styles.preTestContainer} columns='equal' textAlign='center' verticalAlign='middle'> 128 + <Grid.Row columns='equal'> 129 + <Grid.Column> 130 + <Header as='h1' floated='left'> 131 + Collect 132 + </Header> 133 + </Grid.Column> 134 + <Grid.Column floated='right'> 135 + <PreviewButton isPreviewing={this.state.isPreviewing} onClick={e => this.handlePreview(e)} /> 136 + <Button primary disabled={this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED} onClick={this.props.openRunComponent}> 137 + Run & Record Experiment 138 + </Button> 139 + </Grid.Column> 140 + </Grid.Row> 141 + <Grid.Row> 142 + <Grid.Column width={8} className={styles.previewEEGWindow}> 143 + {this.renderSignalQualityOrPreview()} 144 + </Grid.Column> 145 + <Grid.Column width={8}> 146 + <ViewerComponent signalQualityObservable={this.props.signalQualityObservable} deviceType={this.props.deviceType} plottingInterval={PLOTTING_INTERVAL} /> 147 + {this.renderHelpButton()} 148 + </Grid.Column> 149 + </Grid.Row> 150 + </Grid> 151 + </Sidebar.Pusher> 152 + </Sidebar.Pushable>; 153 + } 154 + }
-195
app/components/CollectComponent/RunComponent.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Grid, Button, Segment, Header, Divider } from 'semantic-ui-react'; 4 - import { Link } from 'react-router-dom'; 5 - import styles from '../styles/common.css'; 6 - import InputCollect from '../InputCollect'; 7 - import { injectEmotivMarker } from '../../utils/eeg/emotiv'; 8 - import { injectMuseMarker } from '../../utils/eeg/muse'; 9 - import { EXPERIMENTS, DEVICES } from '../../constants/constants'; 10 - import { ExperimentWindow } from '../../utils/labjs'; 11 - import { checkFileExists, getImages } from '../../utils/filesystem/storage'; 12 - import { MainTimeline, Trial, ExperimentParameters } from '../../constants/interfaces'; 13 - 14 - import { remote } from 'electron'; 15 - 16 - const { dialog } = remote; 17 - 18 - interface Props { 19 - type: ?EXPERIMENTS; 20 - title: string; 21 - isRunning: boolean; 22 - params: ExperimentParameters; 23 - mainTimeline: MainTimeline; 24 - trials: { [string]: Trial }; 25 - timelines: {}; 26 - subject: string; 27 - group: string; 28 - session: number; 29 - deviceType: DEVICES; 30 - isEEGEnabled: boolean; 31 - experimentActions: Object; 32 - } 33 - 34 - interface State { 35 - isInputCollectOpen: boolean; 36 - } 37 - 38 - export default class Run extends Component<Props, State> { 39 - // props: Props; 40 - // state: State; 41 - // handleStartExperiment: () => void; 42 - // insertLabJsCallback: () => void; 43 - // handleCloseInputCollect: (string, string, number) => void; 44 - // handleClean: () => void; 45 - 46 - constructor(props: Props) { 47 - super(props); 48 - this.state = { 49 - isInputCollectOpen: props.subject.length === 0, 50 - }; 51 - this.handleStartExperiment = this.handleStartExperiment.bind(this); 52 - this.insertLabJsCallback = this.insertLabJsCallback.bind(this); 53 - this.handleCloseInputCollect = this.handleCloseInputCollect.bind(this); 54 - } 55 - 56 - componentDidMount() { 57 - if (this.props.mainTimeline.length <= 0) { 58 - this.props.experimentActions.loadDefaultTimeline(); 59 - } 60 - } 61 - 62 - handleStartExperiment() { 63 - const filename = `${this.props.subject}-${this.props.group}-${this.props.session}-behavior.csv`; 64 - const { subject, title } = this.props; 65 - const fileExists = checkFileExists(title, subject, filename); 66 - if (fileExists) { 67 - const options = { 68 - buttons: ['No', 'Yes'], 69 - message: 70 - 'You already have a file with the same name. If you continue the experiment, the current file will be deleted. Do you really want to overwrite the data?', 71 - }; 72 - const response = dialog.showMessageBox(options); 73 - if (response === 1) { 74 - this.props.experimentActions.start(); 75 - } 76 - } else { 77 - this.props.experimentActions.start(); 78 - } 79 - } 80 - 81 - handleCloseInputCollect(subject: string, group: string, session: number) { 82 - this.props.experimentActions.setSubject(subject); 83 - this.props.experimentActions.setGroup(group); 84 - // error here 85 - this.props.experimentActions.setSession(parseFloat(session)); 86 - this.setState({ isInputCollectOpen: false }); 87 - } 88 - 89 - insertLabJsCallback() { 90 - let injectionFunction = () => null; 91 - if (this.props.isEEGEnabled) { 92 - injectionFunction = this.props.deviceType === 'MUSE' ? injectMuseMarker : injectEmotivMarker; 93 - } 94 - return injectionFunction; 95 - } 96 - 97 - handleImages() { 98 - return getImages(this.props.params); 99 - } 100 - 101 - renderCleanButton() { 102 - if (this.props.session > 1 && this.props.isEEGEnabled) { 103 - return ( 104 - <Grid.Column> 105 - <Link to='/clean'> 106 - <Button fluid secondary> 107 - Clean Data 108 - </Button> 109 - </Link> 110 - </Grid.Column> 111 - ); 112 - } 113 - } 114 - 115 - renderExperiment() { 116 - if (!this.props.isRunning) { 117 - return ( 118 - <div className={styles.mainContainer}> 119 - <Segment basic textAlign='left' className={styles.descriptionContainer} vertical> 120 - <Header as='h1'>{this.props.title}</Header> 121 - <Button 122 - basic 123 - circular 124 - size='huge' 125 - icon='edit' 126 - className={styles.closeButton} 127 - onClick={() => this.setState({ isInputCollectOpen: true })} 128 - /> 129 - <Segment basic className={styles.infoSegment}> 130 - Subject ID: <b>{this.props.subject}</b> 131 - </Segment> 132 - 133 - <Segment basic className={styles.infoSegment}> 134 - Group Name: <b>{this.props.group}</b> 135 - </Segment> 136 - 137 - <Segment basic className={styles.infoSegment}> 138 - Session Number: <b>{this.props.session}</b> 139 - </Segment> 140 - 141 - <Divider hidden section /> 142 - <Grid textAlign='center' columns='equal'> 143 - <Grid.Column> 144 - <Button 145 - fluid 146 - primary 147 - onClick={this.handleStartExperiment} 148 - disabled={!this.props.subject} 149 - > 150 - Run Experiment 151 - </Button> 152 - </Grid.Column> 153 - </Grid> 154 - </Segment> 155 - </div> 156 - ); 157 - } 158 - return ( 159 - <div className={styles.experimentWindow}> 160 - <ExperimentWindow 161 - settings={{ 162 - title: this.props.title, 163 - script: this.props.paradigm, 164 - params: this.props.params, 165 - eventCallback: this.insertLabJsCallback(), 166 - on_finish: (csv) => { 167 - this.props.experimentActions.stop({ data: csv }); 168 - }, 169 - }} 170 - /> 171 - </div> 172 - ); 173 - } 174 - 175 - render() { 176 - return ( 177 - <div className={styles.mainContainer} data-tid='container'> 178 - <Grid columns={1} divided relaxed className={styles.experimentContainer}> 179 - <Grid.Row centered>{this.renderExperiment()}</Grid.Row> 180 - </Grid> 181 - <InputCollect 182 - open={this.state.isInputCollectOpen} 183 - onClose={this.handleCloseInputCollect} 184 - onExit={() => this.setState({ isInputCollectOpen: false })} 185 - header='Enter Data' 186 - data={{ 187 - subject: this.props.subject, 188 - group: this.props.group, 189 - session: this.props.session, 190 - }} 191 - /> 192 - </div> 193 - ); 194 - } 195 - }
+170
app/components/CollectComponent/RunComponent.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Grid, Button, Segment, Header, Divider } from "semantic-ui-react"; 4 + import { Link } from "react-router-dom"; 5 + import styles from "../styles/common.css"; 6 + import InputCollect from "../InputCollect"; 7 + import { injectEmotivMarker } from "../../utils/eeg/emotiv"; 8 + import { injectMuseMarker } from "../../utils/eeg/muse"; 9 + import { EXPERIMENTS, DEVICES } from "../../constants/constants"; 10 + import { ExperimentWindow } from "../../utils/labjs"; 11 + import { checkFileExists, getImages } from "../../utils/filesystem/storage"; 12 + import { MainTimeline, Trial, ExperimentParameters } from "../../constants/interfaces"; 13 + 14 + import { remote } from "electron"; 15 + 16 + const { 17 + dialog 18 + } = remote; 19 + 20 + interface Props { 21 + type: EXPERIMENTS | null | undefined; 22 + title: string; 23 + isRunning: boolean; 24 + params: ExperimentParameters; 25 + mainTimeline: MainTimeline; 26 + trials: { 27 + [key: string]: Trial; 28 + }; 29 + timelines: {}; 30 + subject: string; 31 + group: string; 32 + session: number; 33 + deviceType: DEVICES; 34 + isEEGEnabled: boolean; 35 + experimentActions: Object; 36 + } 37 + 38 + interface State { 39 + isInputCollectOpen: boolean; 40 + } 41 + 42 + export default class Run extends Component<Props, State> { 43 + 44 + // insertLabJsCallback: () => void; 45 + // handleCloseInputCollect: (string, string, number) => void; 46 + // handleClean: () => void; 47 + constructor(props: Props) { 48 + super(props); 49 + this.state = { 50 + isInputCollectOpen: props.subject.length === 0 51 + }; 52 + this.handleStartExperiment = this.handleStartExperiment.bind(this); 53 + this.insertLabJsCallback = this.insertLabJsCallback.bind(this); 54 + this.handleCloseInputCollect = this.handleCloseInputCollect.bind(this); 55 + } 56 + 57 + componentDidMount() { 58 + if (this.props.mainTimeline.length <= 0) { 59 + this.props.experimentActions.loadDefaultTimeline(); 60 + } 61 + } 62 + 63 + handleStartExperiment() { 64 + const filename = `${this.props.subject}-${this.props.group}-${this.props.session}-behavior.csv`; 65 + const { 66 + subject, 67 + title 68 + } = this.props; 69 + const fileExists = checkFileExists(title, subject, filename); 70 + if (fileExists) { 71 + const options = { 72 + buttons: ['No', 'Yes'], 73 + message: 'You already have a file with the same name. If you continue the experiment, the current file will be deleted. Do you really want to overwrite the data?' 74 + }; 75 + const response = dialog.showMessageBox(options); 76 + if (response === 1) { 77 + this.props.experimentActions.start(); 78 + } 79 + } else { 80 + this.props.experimentActions.start(); 81 + } 82 + } 83 + 84 + handleCloseInputCollect(subject: string, group: string, session: number) { 85 + this.props.experimentActions.setSubject(subject); 86 + this.props.experimentActions.setGroup(group); 87 + // error here 88 + this.props.experimentActions.setSession(parseFloat(session)); 89 + this.setState({ isInputCollectOpen: false }); 90 + } 91 + 92 + insertLabJsCallback() { 93 + let injectionFunction = () => null; 94 + if (this.props.isEEGEnabled) { 95 + injectionFunction = this.props.deviceType === 'MUSE' ? injectMuseMarker : injectEmotivMarker; 96 + } 97 + return injectionFunction; 98 + } 99 + 100 + handleImages() { 101 + return getImages(this.props.params); 102 + } 103 + 104 + renderCleanButton() { 105 + if (this.props.session > 1 && this.props.isEEGEnabled) { 106 + return <Grid.Column> 107 + <Link to='/clean'> 108 + <Button fluid secondary> 109 + Clean Data 110 + </Button> 111 + </Link> 112 + </Grid.Column>; 113 + } 114 + } 115 + 116 + renderExperiment() { 117 + if (!this.props.isRunning) { 118 + return <div className={styles.mainContainer}> 119 + <Segment basic textAlign='left' className={styles.descriptionContainer} vertical> 120 + <Header as='h1'>{this.props.title}</Header> 121 + <Button basic circular size='huge' icon='edit' className={styles.closeButton} onClick={() => this.setState({ isInputCollectOpen: true })} /> 122 + <Segment basic className={styles.infoSegment}> 123 + Subject ID: <b>{this.props.subject}</b> 124 + </Segment> 125 + 126 + <Segment basic className={styles.infoSegment}> 127 + Group Name: <b>{this.props.group}</b> 128 + </Segment> 129 + 130 + <Segment basic className={styles.infoSegment}> 131 + Session Number: <b>{this.props.session}</b> 132 + </Segment> 133 + 134 + <Divider hidden section /> 135 + <Grid textAlign='center' columns='equal'> 136 + <Grid.Column> 137 + <Button fluid primary onClick={this.handleStartExperiment} disabled={!this.props.subject}> 138 + Run Experiment 139 + </Button> 140 + </Grid.Column> 141 + </Grid> 142 + </Segment> 143 + </div>; 144 + } 145 + return <div className={styles.experimentWindow}> 146 + <ExperimentWindow settings={{ 147 + title: this.props.title, 148 + script: this.props.paradigm, 149 + params: this.props.params, 150 + eventCallback: this.insertLabJsCallback(), 151 + on_finish: csv => { 152 + this.props.experimentActions.stop({ data: csv }); 153 + } 154 + }} /> 155 + </div>; 156 + } 157 + 158 + render() { 159 + return <div className={styles.mainContainer} data-tid='container'> 160 + <Grid columns={1} divided relaxed className={styles.experimentContainer}> 161 + <Grid.Row centered>{this.renderExperiment()}</Grid.Row> 162 + </Grid> 163 + <InputCollect open={this.state.isInputCollectOpen} onClose={this.handleCloseInputCollect} onExit={() => this.setState({ isInputCollectOpen: false })} header='Enter Data' data={{ 164 + subject: this.props.subject, 165 + group: this.props.group, 166 + session: this.props.session 167 + }} /> 168 + </div>; 169 + } 170 + }
-140
app/components/CollectComponent/index.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { isNil } from 'lodash'; 4 - import { 5 - EXPERIMENTS, 6 - DEVICES, 7 - CONNECTION_STATUS, 8 - DEVICE_AVAILABILITY, 9 - } from '../../constants/constants'; 10 - import { MainTimeline, Trial, ExperimentParameters } from '../../constants/interfaces'; 11 - import PreTestComponent from './PreTestComponent'; 12 - import ConnectModal from './ConnectModal'; 13 - import RunComponent from './RunComponent'; 14 - 15 - interface Props { 16 - history: Object; 17 - experimentActions: Object; 18 - connectedDevice: Object; 19 - signalQualityObservable: ?any; 20 - deviceType: DEVICES; 21 - deviceAvailability: DEVICE_AVAILABILITY; 22 - connectionStatus: CONNECTION_STATUS; 23 - deviceActions: Object; 24 - availableDevices: Array<any>; 25 - type: ?EXPERIMENTS; 26 - isRunning: boolean; 27 - params: ?ExperimentParameters; 28 - mainTimeline: ?MainTimeline; 29 - trials: ?{ [string]: Trial }; 30 - timelines: ?{}; 31 - subject: string; 32 - group: string; 33 - session: number; 34 - isEEGEnabled: boolean; 35 - } 36 - 37 - interface State { 38 - isConnectModalOpen: boolean; 39 - isRunComponentOpen: boolean; 40 - } 41 - 42 - export default class Collect extends Component<Props, State> { 43 - // props: Props; 44 - // state: State; 45 - // handleStartConnect: () => void; 46 - // handleConnectModalClose: () => void; 47 - // handleRunComponentOpen: () => void; 48 - // handleRunComponentClose: () => void; 49 - 50 - constructor(props: Props) { 51 - super(props); 52 - this.state = { 53 - isConnectModalOpen: false, 54 - isRunComponentOpen: !props.isEEGEnabled, 55 - }; 56 - this.handleStartConnect = this.handleStartConnect.bind(this); 57 - this.handleConnectModalClose = this.handleConnectModalClose.bind(this); 58 - this.handleRunComponentOpen = this.handleRunComponentOpen.bind(this); 59 - this.handleRunComponentClose = this.handleRunComponentClose.bind(this); 60 - if (isNil(props.params)) { 61 - props.experimentActions.loadDefaultTimeline(); 62 - } 63 - } 64 - 65 - componentDidMount() { 66 - if (this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED && this.props.isEEGEnabled) { 67 - this.handleStartConnect(); 68 - } 69 - } 70 - 71 - componentDidUpdate = (prevProps: Props, prevState: State) => { 72 - if ( 73 - this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && 74 - prevState.isConnectModalOpen 75 - ) { 76 - this.setState({ isConnectModalOpen: false }); 77 - } 78 - }; 79 - 80 - handleStartConnect() { 81 - this.setState({ isConnectModalOpen: true }); 82 - this.props.deviceActions.setDeviceAvailability(DEVICE_AVAILABILITY.SEARCHING); 83 - } 84 - 85 - handleConnectModalClose() { 86 - this.setState({ isConnectModalOpen: false }); 87 - } 88 - 89 - handleRunComponentOpen() { 90 - this.setState({ isRunComponentOpen: true }); 91 - } 92 - 93 - handleRunComponentClose() { 94 - this.setState({ isRunComponentOpen: false }); 95 - } 96 - 97 - render() { 98 - if (this.state.isRunComponentOpen) { 99 - return <RunComponent {...this.props} closeRunComponent={this.handleRunComponentClose} />; 100 - } 101 - return ( 102 - <> 103 - <ConnectModal 104 - history={this.props.history} 105 - open={this.state.isConnectModalOpen} 106 - onClose={this.handleConnectModalClose} 107 - connectedDevice={this.props.connectedDevice} 108 - signalQualityObservable={this.props.signalQualityObservable} 109 - deviceType={this.props.deviceType} 110 - deviceAvailability={this.props.deviceAvailability} 111 - connectionStatus={this.props.connectionStatus} 112 - deviceActions={this.props.deviceActions} 113 - availableDevices={this.props.availableDevices} 114 - /> 115 - <PreTestComponent 116 - connectedDevice={this.props.connectedDevice} 117 - signalQualityObservable={this.props.signalQualityObservable} 118 - deviceType={this.props.deviceType} 119 - deviceAvailability={this.props.deviceAvailability} 120 - connectionStatus={this.props.connectionStatus} 121 - deviceActions={this.props.deviceActions} 122 - experimentActions={this.props.experimentActions} 123 - availableDevices={this.props.availableDevices} 124 - type={this.props.type} 125 - paradigm={this.props.paradigm} 126 - isRunning={this.props.isRunning} 127 - params={this.props.params} 128 - mainTimeline={this.props.mainTimeline} 129 - trials={this.props.trials} 130 - timelines={this.props.timelines} 131 - subject={this.props.subject} 132 - group={this.props.group} 133 - session={this.props.session} 134 - openRunComponent={this.handleRunComponentOpen} 135 - title={this.props.title} 136 - /> 137 - </> 138 - ); 139 - } 140 - }
+97
app/components/CollectComponent/index.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { isNil } from "lodash"; 4 + import { EXPERIMENTS, DEVICES, CONNECTION_STATUS, DEVICE_AVAILABILITY } from "../../constants/constants"; 5 + import { MainTimeline, Trial, ExperimentParameters } from "../../constants/interfaces"; 6 + import PreTestComponent from "./PreTestComponent"; 7 + import ConnectModal from "./ConnectModal"; 8 + import RunComponent from "./RunComponent"; 9 + 10 + interface Props { 11 + history: Object; 12 + experimentActions: Object; 13 + connectedDevice: Object; 14 + signalQualityObservable: any | null | undefined; 15 + deviceType: DEVICES; 16 + deviceAvailability: DEVICE_AVAILABILITY; 17 + connectionStatus: CONNECTION_STATUS; 18 + deviceActions: Object; 19 + availableDevices: Array<any>; 20 + type: EXPERIMENTS | null | undefined; 21 + isRunning: boolean; 22 + params: ExperimentParameters | null | undefined; 23 + mainTimeline: MainTimeline | null | undefined; 24 + trials: { 25 + [key: string]: Trial; 26 + } | null | undefined; 27 + timelines: {} | null | undefined; 28 + subject: string; 29 + group: string; 30 + session: number; 31 + isEEGEnabled: boolean; 32 + } 33 + 34 + interface State { 35 + isConnectModalOpen: boolean; 36 + isRunComponentOpen: boolean; 37 + } 38 + 39 + export default class Collect extends Component<Props, State> { 40 + 41 + // handleConnectModalClose: () => void; 42 + // handleRunComponentOpen: () => void; 43 + // handleRunComponentClose: () => void; 44 + constructor(props: Props) { 45 + super(props); 46 + this.state = { 47 + isConnectModalOpen: false, 48 + isRunComponentOpen: !props.isEEGEnabled 49 + }; 50 + this.handleStartConnect = this.handleStartConnect.bind(this); 51 + this.handleConnectModalClose = this.handleConnectModalClose.bind(this); 52 + this.handleRunComponentOpen = this.handleRunComponentOpen.bind(this); 53 + this.handleRunComponentClose = this.handleRunComponentClose.bind(this); 54 + if (isNil(props.params)) { 55 + props.experimentActions.loadDefaultTimeline(); 56 + } 57 + } 58 + 59 + componentDidMount() { 60 + if (this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED && this.props.isEEGEnabled) { 61 + this.handleStartConnect(); 62 + } 63 + } 64 + 65 + componentDidUpdate = (prevProps: Props, prevState: State) => { 66 + if (this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && prevState.isConnectModalOpen) { 67 + this.setState({ isConnectModalOpen: false }); 68 + } 69 + }; 70 + 71 + handleStartConnect() { 72 + this.setState({ isConnectModalOpen: true }); 73 + this.props.deviceActions.setDeviceAvailability(DEVICE_AVAILABILITY.SEARCHING); 74 + } 75 + 76 + handleConnectModalClose() { 77 + this.setState({ isConnectModalOpen: false }); 78 + } 79 + 80 + handleRunComponentOpen() { 81 + this.setState({ isRunComponentOpen: true }); 82 + } 83 + 84 + handleRunComponentClose() { 85 + this.setState({ isRunComponentOpen: false }); 86 + } 87 + 88 + render() { 89 + if (this.state.isRunComponentOpen) { 90 + return <RunComponent {...this.props} closeRunComponent={this.handleRunComponentClose} />; 91 + } 92 + return <> 93 + <ConnectModal history={this.props.history} open={this.state.isConnectModalOpen} onClose={this.handleConnectModalClose} connectedDevice={this.props.connectedDevice} signalQualityObservable={this.props.signalQualityObservable} deviceType={this.props.deviceType} deviceAvailability={this.props.deviceAvailability} connectionStatus={this.props.connectionStatus} deviceActions={this.props.deviceActions} availableDevices={this.props.availableDevices} /> 94 + <PreTestComponent connectedDevice={this.props.connectedDevice} signalQualityObservable={this.props.signalQualityObservable} deviceType={this.props.deviceType} deviceAvailability={this.props.deviceAvailability} connectionStatus={this.props.connectionStatus} deviceActions={this.props.deviceActions} experimentActions={this.props.experimentActions} availableDevices={this.props.availableDevices} type={this.props.type} paradigm={this.props.paradigm} isRunning={this.props.isRunning} params={this.props.params} mainTimeline={this.props.mainTimeline} trials={this.props.trials} timelines={this.props.timelines} subject={this.props.subject} group={this.props.group} session={this.props.session} openRunComponent={this.handleRunComponentOpen} title={this.props.title} /> 95 + </>; 96 + } 97 + }
-636
app/components/DesignComponent/CustomDesignComponent.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Grid, Button, Segment, Header, Form, Checkbox, Image, Table } from 'semantic-ui-react'; 4 - import { isNil } from 'lodash'; 5 - import styles from '../styles/common.css'; 6 - import { EXPERIMENTS, SCREENS } from '../../constants/constants'; 7 - import { 8 - MainTimeline, 9 - Trial, 10 - ExperimentParameters, 11 - ExperimentDescription, 12 - } from '../../constants/interfaces'; 13 - import SecondaryNavComponent from '../SecondaryNavComponent'; 14 - import PreviewExperimentComponent from '../PreviewExperimentComponent'; 15 - import StimuliDesignColumn from './StimuliDesignColumn'; 16 - import ParamSlider from './ParamSlider'; 17 - import PreviewButton from '../PreviewButtonComponent'; 18 - import researchQuestionImage from '../../assets/common/ResearchQuestion2.png'; 19 - import methodsImage from '../../assets/common/Methods2.png'; 20 - import hypothesisImage from '../../assets/common/Hypothesis2.png'; 21 - import { loadProtocol } from '../../utils/labjs/functions'; 22 - import { readImages } from '../../utils/filesystem/storage'; 23 - import StimuliRow from './StimuliRow'; 24 - 25 - const CUSTOM_STEPS = { 26 - OVERVIEW: 'OVERVIEW', 27 - CONDITIONS: 'CONDITIONS', 28 - TRIALS: 'TRIALS', 29 - PARAMETERS: 'PARAMETERS', 30 - INSTRUCTIONS: 'INSTRUCTIONS', 31 - PREVIEW: 'PREVIEW', 32 - }; 33 - 34 - const FIELDS = { 35 - QUESTION: 'Research Question', 36 - HYPOTHESIS: 'Hypothesis', 37 - METHODS: 'Methods', 38 - INTRO: 'Experiment Instructions', 39 - HELP: 'Instructions for the task screen', 40 - }; 41 - 42 - interface Props { 43 - history: Object; 44 - type: ?EXPERIMENTS; 45 - title: string; 46 - params: ExperimentParameters; 47 - mainTimeline: MainTimeline; 48 - trials: { [string]: Trial }; 49 - timelines: {}; 50 - experimentActions: Object; 51 - isEEGEnabled: boolean; 52 - description: ExperimentDescription; 53 - } 54 - 55 - interface State { 56 - activeStep: string; 57 - isPreviewing: boolean; 58 - description: ExperimentDescription; 59 - params: ExperimentParameters; 60 - saved: boolean; 61 - } 62 - 63 - export default class CustomDesign extends Component<Props, State> { 64 - // props: Props; 65 - // state: State; 66 - // handleStepClick: (string) => void; 67 - // handleStartExperiment: (Object) => void; 68 - // handlePreview: () => void; 69 - // handleSaveParams: () => void; 70 - // handleProgressBar: (Object, Object) => void; 71 - 72 - constructor(props: Props) { 73 - super(props); 74 - this.state = { 75 - activeStep: CUSTOM_STEPS.OVERVIEW, 76 - isPreviewing: true, 77 - description: props.description, 78 - params: props.params, 79 - saved: false, 80 - }; 81 - this.handleStepClick = this.handleStepClick.bind(this); 82 - this.handleStartExperiment = this.handleStartExperiment.bind(this); 83 - this.handlePreview = this.handlePreview.bind(this); 84 - this.handleSaveParams = this.handleSaveParams.bind(this); 85 - this.handleProgressBar = this.handleProgressBar.bind(this); 86 - this.handleEEGEnabled = this.handleEEGEnabled.bind(this); 87 - if (isNil(props.params)) { 88 - props.experimentActions.loadDefaultTimeline(); 89 - } 90 - this.endPreview = this.endPreview.bind(this); 91 - } 92 - 93 - endPreview() { 94 - // this.setState({ isPreviewing: false }); 95 - } 96 - 97 - handleStepClick(step: string) { 98 - this.handleSaveParams(); 99 - this.setState({ activeStep: step }); 100 - } 101 - 102 - handleProgressBar(event: Object, data: Object) { 103 - this.setState({ 104 - params: { ...this.state.params, showProgessBar: data.checked }, 105 - }); 106 - } 107 - 108 - handleEEGEnabled(event: Object, data: Object) { 109 - this.props.experimentActions.setEEGEnabled(data.checked); 110 - } 111 - 112 - handleStartExperiment() { 113 - this.props.history.push(SCREENS.COLLECT.route); 114 - } 115 - 116 - handlePreview(e) { 117 - e.target.blur(); 118 - this.setState({ isPreviewing: !this.state.isPreviewing }); 119 - } 120 - 121 - handleSaveParams() { 122 - this.props.experimentActions.setParams(this.state.params); 123 - this.props.experimentActions.setDescription(this.state.description); 124 - this.props.experimentActions.saveWorkspace(); 125 - this.setState({ saved: true }); 126 - } 127 - 128 - renderSectionContent() { 129 - const stimi = [ 130 - { name: 'stimulus1', number: 1 }, 131 - { name: 'stimulus2', number: 2 }, 132 - { name: 'stimulus3', number: 3 }, 133 - { name: 'stimulus4', number: 4 }, 134 - ]; 135 - switch (this.state.activeStep) { 136 - case CUSTOM_STEPS.OVERVIEW: 137 - default: 138 - return ( 139 - <Grid stretched relaxed padded columns='equal' className={styles.contentGrid}> 140 - <Grid.Column stretched verticalAlign='middle'> 141 - <Image 142 - as={Segment} 143 - basic 144 - centered 145 - src={researchQuestionImage} 146 - className={styles.overviewImage} 147 - /> 148 - <Form> 149 - <Form.TextArea 150 - autoHeight 151 - style={{ minHeight: 100, maxHeight: 400 }} 152 - label={FIELDS.QUESTION} 153 - value={this.state.description.question} 154 - placeholder='Explain your research question here.' 155 - onChange={(event, data) => 156 - this.setState({ 157 - description: { 158 - ...this.state.description, 159 - question: data.value, 160 - }, 161 - saved: false, 162 - }) 163 - } 164 - /> 165 - </Form> 166 - </Grid.Column> 167 - <Grid.Column stretched verticalAlign='middle'> 168 - <Image 169 - as={Segment} 170 - basic 171 - centered 172 - src={hypothesisImage} 173 - className={styles.overviewImage} 174 - /> 175 - <Form> 176 - <Form.TextArea 177 - autoHeight 178 - style={{ minHeight: 100, maxHeight: 400 }} 179 - label={FIELDS.HYPOTHESIS} 180 - value={this.state.description.hypothesis} 181 - placeholder='Describe your hypothesis here.' 182 - onChange={(event, data) => 183 - this.setState({ 184 - description: { 185 - ...this.state.description, 186 - hypothesis: data.value, 187 - }, 188 - saved: false, 189 - }) 190 - } 191 - /> 192 - </Form> 193 - </Grid.Column> 194 - <Grid.Column verticalAlign='middle'> 195 - <Image 196 - as={Segment} 197 - basic 198 - centered 199 - src={methodsImage} 200 - className={styles.overviewImage} 201 - /> 202 - <Form> 203 - <Form.TextArea 204 - autoHeight 205 - style={{ minHeight: 100, maxHeight: 400 }} 206 - label={FIELDS.METHODS} 207 - value={this.state.description.methods} 208 - placeholder='Explain how you will design your experiment to answer the question here.' 209 - onChange={(event, data) => 210 - this.setState({ 211 - description: { 212 - ...this.state.description, 213 - methods: data.value, 214 - }, 215 - saved: false, 216 - }) 217 - } 218 - /> 219 - </Form> 220 - </Grid.Column> 221 - </Grid> 222 - ); 223 - 224 - case CUSTOM_STEPS.CONDITIONS: 225 - return ( 226 - <Grid> 227 - <Segment basic> 228 - <Header as='h1'>Conditions</Header> 229 - <p> 230 - Select the folder with images for each condition and choose the correct response. 231 - You can upload image files with the following extensions: ".png", ".jpg", ".jpeg". 232 - Make sure when you preview your experiment that the resolution is high enough. You 233 - can resize or compress your images in an image editing program or on one of the 234 - websites online. 235 - </p> 236 - </Segment> 237 - 238 - <Table basic='very'> 239 - <Table.Header> 240 - <Table.Row className={styles.conditionHeaderRow}> 241 - <Table.HeaderCell className={styles.conditionHeaderRowName}> 242 - Condition 243 - </Table.HeaderCell> 244 - <Table.HeaderCell>Default Key Response</Table.HeaderCell> 245 - <Table.HeaderCell>Condition Folder</Table.HeaderCell> 246 - </Table.Row> 247 - </Table.Header> 248 - 249 - <Table.Body className={styles.experimentTable}> 250 - {stimi.map(({ name, number }) => ( 251 - <StimuliDesignColumn 252 - key={number} 253 - num={number} 254 - {...this.state.params[name]} 255 - numberImages={ 256 - this.state.params.stimuli.filter((trial) => trial.type === number).length 257 - } 258 - onChange={async (key, data, changedName) => { 259 - await this.setState({ 260 - params: { 261 - ...this.state.params, 262 - [changedName]: { ...this.state.params[changedName], [key]: data }, 263 - }, 264 - }); 265 - let newStimuli = []; 266 - await stimi.map((stimul) => { 267 - let dirStimuli = []; 268 - const dir = this.state.params[stimul.name].dir; 269 - if (dir && typeof dir !== 'undefined' && dir !== '') { 270 - dirStimuli = readImages(dir).map((i) => ({ 271 - dir: dir, 272 - filename: i, 273 - name: i, 274 - condition: this.state.params[stimul.name].title, 275 - response: this.state.params[stimul.name].response, 276 - phase: 'main', 277 - type: stimul.number, 278 - })); 279 - } 280 - if (dirStimuli.length) dirStimuli[0].phase = 'practice'; 281 - newStimuli = newStimuli.concat(...dirStimuli); 282 - }); 283 - this.setState({ 284 - params: { 285 - ...this.state.params, 286 - stimuli: [...newStimuli], 287 - nbTrials: newStimuli.filter((t) => t.phase === 'main').length, 288 - nbPracticeTrials: newStimuli.filter((t) => t.phase === 'practice').length, 289 - }, 290 - saved: false, 291 - }); 292 - }} 293 - /> 294 - ))} 295 - </Table.Body> 296 - </Table> 297 - </Grid> 298 - ); 299 - 300 - case CUSTOM_STEPS.TRIALS: 301 - return ( 302 - <Grid> 303 - <div className={styles.trialsHeader}> 304 - <div> 305 - <Header as='h1'>Trials</Header> 306 - <p>Edit the correct key response and type of each trial.</p> 307 - </div> 308 - 309 - <div> 310 - <Form style={{ alignSelf: 'flex-end' }}> 311 - <Form.Group className={styles.trialsTopInfoBar}> 312 - <Form.Select 313 - fluid 314 - selection 315 - label='Order' 316 - value={this.state.params.randomize} 317 - onChange={(event, data) => 318 - this.setState({ 319 - params: { 320 - ...this.state.params, 321 - randomize: data.value, 322 - }, 323 - saved: false, 324 - }) 325 - } 326 - placeholder='Response' 327 - options={[ 328 - { key: 'random', text: 'Random', value: 'random' }, 329 - { key: 'sequential', text: 'Sequential', value: 'sequential' }, 330 - ]} 331 - /> 332 - <Form.Input 333 - label='Total experimental trials' 334 - type='number' 335 - fluid 336 - value={this.state.params.nbTrials} 337 - onChange={(event, data) => 338 - this.setState({ 339 - params: { 340 - ...this.state.params, 341 - nbTrials: parseInt(data.value), 342 - }, 343 - saved: false, 344 - }) 345 - } 346 - /> 347 - <Form.Input 348 - label='Total practice trials' 349 - type='number' 350 - fluid 351 - value={this.state.params.nbPracticeTrials} 352 - onChange={(event, data) => 353 - this.setState({ 354 - params: { 355 - ...this.state.params, 356 - nbPracticeTrials: parseInt(data.value), 357 - }, 358 - saved: false, 359 - }) 360 - } 361 - /> 362 - </Form.Group> 363 - </Form> 364 - </div> 365 - </div> 366 - 367 - <Table basic='very'> 368 - <Table.Header> 369 - <Table.Row className={styles.trialsHeaderRow}> 370 - <Table.HeaderCell className={styles.conditionHeaderRowName}> 371 - Name 372 - </Table.HeaderCell> 373 - <Table.HeaderCell>Condition</Table.HeaderCell> 374 - <Table.HeaderCell>Correct Key Response</Table.HeaderCell> 375 - <Table.HeaderCell>Trial Type</Table.HeaderCell> 376 - </Table.Row> 377 - </Table.Header> 378 - <Table.Body className={styles.trialsTable}> 379 - {this.state.params.stimuli && 380 - this.state.params.stimuli.map((e, num) => ( 381 - <StimuliRow 382 - key={num} 383 - num={num} 384 - conditions={[1, 2, 3, 4].map((n) => this.state.params[`stimulus${n}`].title)} 385 - {...e} 386 - onDelete={(num) => { 387 - const stimuli = this.state.params.stimuli; 388 - stimuli.splice(num, 1); 389 - const nbPracticeTrials = stimuli.filter((s) => s.phase === 'practice') 390 - .length; 391 - const nbTrials = stimuli.filter((s) => s.phase === 'main').length; 392 - this.setState({ 393 - params: { 394 - ...this.state.params, 395 - stimuli: [...stimuli], 396 - nbPracticeTrials, 397 - nbTrials, 398 - }, 399 - saved: false, 400 - }); 401 - }} 402 - onChange={(num, key, data) => { 403 - const stimuli = this.state.params.stimuli; 404 - stimuli[num][key] = data; 405 - let nbPracticeTrials = this.state.params.nbPracticeTrials; 406 - let nbTrials = this.state.params.nbTrials; 407 - if (key === 'phase') { 408 - nbPracticeTrials = stimuli.filter((s) => s.phase === 'practice').length; 409 - nbTrials = stimuli.filter((s) => s.phase === 'main').length; 410 - } 411 - this.setState({ 412 - params: { 413 - ...this.state.params, 414 - stimuli: [...stimuli], 415 - nbPracticeTrials, 416 - nbTrials, 417 - }, 418 - saved: false, 419 - }); 420 - }} 421 - /> 422 - ))} 423 - </Table.Body> 424 - </Table> 425 - </Grid> 426 - ); 427 - 428 - case CUSTOM_STEPS.PARAMETERS: 429 - return ( 430 - <Grid> 431 - <Grid.Column width={8} style={{ display: 'grid', alignContent: 'space-between' }}> 432 - <Segment basic> 433 - <Header as='h1'>Inter-trial interval</Header> 434 - <p> 435 - Select the inter-trial interval duration. This is the amount of time between 436 - trials measured from the end of one trial to the start of the next one. 437 - </p> 438 - </Segment> 439 - <Segment basic style={{ marginTop: '100px' }}> 440 - <ParamSlider 441 - label='ITI Duration (seconds)' 442 - value={this.state.params.iti} 443 - marks={{ 444 - 1: '0.25', 445 - 2: '0.5', 446 - 3: '0.75', 447 - 4: '1', 448 - 5: '1.25', 449 - 6: '1.5', 450 - 7: '1.75', 451 - 8: '2', 452 - }} 453 - ms_conversion='250' 454 - onChange={(value) => 455 - this.setState({ 456 - params: { ...this.state.params, iti: value }, 457 - saved: false, 458 - }) 459 - } 460 - /> 461 - </Segment> 462 - </Grid.Column> 463 - 464 - <Grid.Column width={8} style={{ display: 'grid', alignContent: 'space-between' }}> 465 - <Segment basic> 466 - <Header as='h1'>Image duration</Header> 467 - <p> 468 - Select the time of presentation or make it self-paced - present the image until 469 - participants respond. 470 - </p> 471 - </Segment> 472 - <Segment basic> 473 - <Checkbox 474 - defaultChecked={this.state.params.selfPaced} 475 - label='Self-paced data collection' 476 - onChange={(value) => 477 - this.setState({ 478 - params: { ...this.state.params, selfPaced: !this.state.params.selfPaced }, 479 - saved: false, 480 - }) 481 - } 482 - /> 483 - </Segment> 484 - 485 - {!this.state.params.selfPaced ? ( 486 - <Segment basic> 487 - <ParamSlider 488 - label='Presentation time (seconds)' 489 - value={this.state.params.presentationTime} 490 - marks={{ 491 - 1: '0.25', 492 - 2: '0.5', 493 - 3: '0.75', 494 - 4: '1', 495 - 5: '1.25', 496 - 6: '1.5', 497 - 7: '1.75', 498 - 8: '2', 499 - }} 500 - ms_conversion='250' 501 - onChange={(value) => 502 - this.setState({ 503 - params: { ...this.state.params, presentationTime: value }, 504 - saved: false, 505 - }) 506 - } 507 - /> 508 - </Segment> 509 - ) : ( 510 - <Segment basic style={{ marginBottom: '85px' }} /> 511 - )} 512 - </Grid.Column> 513 - </Grid> 514 - ); 515 - 516 - case CUSTOM_STEPS.INSTRUCTIONS: 517 - return ( 518 - <Grid stretched> 519 - <Grid.Column 520 - width={8} 521 - stretched 522 - style={{ display: 'grid', alignContent: 'space-between' }} 523 - > 524 - <Segment basic> 525 - <Header as='h1'>Experiment Instructions</Header> 526 - <p>Edit the instruction that will be displayed on the first screen.</p> 527 - <Form> 528 - <Form.TextArea 529 - autoHeight 530 - value={this.state.params.intro} 531 - 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." 532 - onChange={(event, data) => 533 - this.setState({ 534 - params: { ...this.state.params, intro: data.value }, 535 - saved: false, 536 - }) 537 - } 538 - /> 539 - </Form> 540 - </Segment> 541 - </Grid.Column> 542 - 543 - <Grid.Column 544 - width={8} 545 - stretched 546 - style={{ display: 'grid', alignContent: 'space-between' }} 547 - > 548 - <Segment basic> 549 - <Header as='h1'>Instructions for the task screen</Header> 550 - <p>Edit the instruction that will be displayed in the footer during the task.</p> 551 - <Form> 552 - <Form.TextArea 553 - autoHeight 554 - value={this.state.params.taskHelp} 555 - placeholder='e.g., Press 1 for a face and 9 for a house' 556 - onChange={(event, data) => 557 - this.setState({ 558 - params: { ...this.state.params, taskHelp: data.value }, 559 - saved: false, 560 - }) 561 - } 562 - /> 563 - </Form> 564 - </Segment> 565 - </Grid.Column> 566 - </Grid> 567 - ); 568 - 569 - case CUSTOM_STEPS.PREVIEW: 570 - return ( 571 - <Grid relaxed padded className={styles.contentGrid}> 572 - <Grid.Column 573 - stretched 574 - width={14} 575 - textAlign='right' 576 - verticalAlign='middle' 577 - className={styles.previewWindow} 578 - > 579 - <PreviewExperimentComponent 580 - {...loadProtocol(this.props.paradigm)} 581 - isPreviewing={this.state.isPreviewing} 582 - onEnd={this.endPreview} 583 - type={this.props.type} 584 - paradigm={this.props.paradigm} 585 - previewParams={this.props.params} 586 - title={this.props.title} 587 - /> 588 - </Grid.Column> 589 - 590 - <Grid.Column width={2} verticalAlign='top'> 591 - <Segment basic> 592 - <PreviewButton 593 - isPreviewing={this.state.isPreviewing} 594 - onClick={(e) => this.handlePreview(e)} 595 - /> 596 - </Segment> 597 - </Grid.Column> 598 - </Grid> 599 - ); 600 - } 601 - } 602 - 603 - render() { 604 - return ( 605 - <div className={styles.mainContainer}> 606 - <SecondaryNavComponent 607 - title='Experiment Design' 608 - steps={CUSTOM_STEPS} 609 - activeStep={this.state.activeStep} 610 - onStepClick={this.handleStepClick} 611 - enableEEGToggle={ 612 - <Checkbox 613 - toggle 614 - defaultChecked={this.props.isEEGEnabled} 615 - onChange={(event, data) => this.handleEEGEnabled(event, data)} 616 - className={styles.EEGToggle} 617 - /> 618 - } 619 - saveButton={ 620 - <Button 621 - compact 622 - size='small' 623 - secondary 624 - onClick={() => { 625 - this.handleSaveParams(); 626 - }} 627 - > 628 - {this.state.saved ? 'Save' : 'Save'} 629 - </Button> 630 - } 631 - /> 632 - {this.renderSectionContent()} 633 - </div> 634 - ); 635 - } 636 - }
+432
app/components/DesignComponent/CustomDesignComponent.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Grid, Button, Segment, Header, Form, Checkbox, Image, Table } from "semantic-ui-react"; 4 + import { isNil } from "lodash"; 5 + import styles from "../styles/common.css"; 6 + import { EXPERIMENTS, SCREENS } from "../../constants/constants"; 7 + import { MainTimeline, Trial, ExperimentParameters, ExperimentDescription } from "../../constants/interfaces"; 8 + import SecondaryNavComponent from "../SecondaryNavComponent"; 9 + import PreviewExperimentComponent from "../PreviewExperimentComponent"; 10 + import StimuliDesignColumn from "./StimuliDesignColumn"; 11 + import ParamSlider from "./ParamSlider"; 12 + import PreviewButton from "../PreviewButtonComponent"; 13 + import researchQuestionImage from "../../assets/common/ResearchQuestion2.png"; 14 + import methodsImage from "../../assets/common/Methods2.png"; 15 + import hypothesisImage from "../../assets/common/Hypothesis2.png"; 16 + import { loadProtocol } from "../../utils/labjs/functions"; 17 + import { readImages } from "../../utils/filesystem/storage"; 18 + import StimuliRow from "./StimuliRow"; 19 + 20 + const CUSTOM_STEPS = { 21 + OVERVIEW: 'OVERVIEW', 22 + CONDITIONS: 'CONDITIONS', 23 + TRIALS: 'TRIALS', 24 + PARAMETERS: 'PARAMETERS', 25 + INSTRUCTIONS: 'INSTRUCTIONS', 26 + PREVIEW: 'PREVIEW' 27 + }; 28 + 29 + const FIELDS = { 30 + QUESTION: 'Research Question', 31 + HYPOTHESIS: 'Hypothesis', 32 + METHODS: 'Methods', 33 + INTRO: 'Experiment Instructions', 34 + HELP: 'Instructions for the task screen' 35 + }; 36 + 37 + interface Props { 38 + history: Object; 39 + type: EXPERIMENTS | null | undefined; 40 + title: string; 41 + params: ExperimentParameters; 42 + mainTimeline: MainTimeline; 43 + trials: { 44 + [key: string]: Trial; 45 + }; 46 + timelines: {}; 47 + experimentActions: Object; 48 + isEEGEnabled: boolean; 49 + description: ExperimentDescription; 50 + } 51 + 52 + interface State { 53 + activeStep: string; 54 + isPreviewing: boolean; 55 + description: ExperimentDescription; 56 + params: ExperimentParameters; 57 + saved: boolean; 58 + } 59 + 60 + export default class CustomDesign extends Component<Props, State> { 61 + // handleStartExperiment: (Object) => void; 62 + 63 + // handlePreview: () => void; 64 + // handleSaveParams: () => void; 65 + // handleProgressBar: (Object, Object) => void; 66 + constructor(props: Props) { 67 + super(props); 68 + this.state = { 69 + activeStep: CUSTOM_STEPS.OVERVIEW, 70 + isPreviewing: true, 71 + description: props.description, 72 + params: props.params, 73 + saved: false 74 + }; 75 + this.handleStepClick = this.handleStepClick.bind(this); 76 + this.handleStartExperiment = this.handleStartExperiment.bind(this); 77 + this.handlePreview = this.handlePreview.bind(this); 78 + this.handleSaveParams = this.handleSaveParams.bind(this); 79 + this.handleProgressBar = this.handleProgressBar.bind(this); 80 + this.handleEEGEnabled = this.handleEEGEnabled.bind(this); 81 + if (isNil(props.params)) { 82 + props.experimentActions.loadDefaultTimeline(); 83 + } 84 + this.endPreview = this.endPreview.bind(this); 85 + } 86 + 87 + endPreview() {// this.setState({ isPreviewing: false }); 88 + } 89 + 90 + handleStepClick(step: string) { 91 + this.handleSaveParams(); 92 + this.setState({ activeStep: step }); 93 + } 94 + 95 + handleProgressBar(event: Object, data: Object) { 96 + this.setState({ 97 + params: { ...this.state.params, showProgessBar: data.checked } 98 + }); 99 + } 100 + 101 + handleEEGEnabled(event: Object, data: Object) { 102 + this.props.experimentActions.setEEGEnabled(data.checked); 103 + } 104 + 105 + handleStartExperiment() { 106 + this.props.history.push(SCREENS.COLLECT.route); 107 + } 108 + 109 + handlePreview(e) { 110 + e.target.blur(); 111 + this.setState({ isPreviewing: !this.state.isPreviewing }); 112 + } 113 + 114 + handleSaveParams() { 115 + this.props.experimentActions.setParams(this.state.params); 116 + this.props.experimentActions.setDescription(this.state.description); 117 + this.props.experimentActions.saveWorkspace(); 118 + this.setState({ saved: true }); 119 + } 120 + 121 + renderSectionContent() { 122 + const stimi = [{ name: 'stimulus1', number: 1 }, { name: 'stimulus2', number: 2 }, { name: 'stimulus3', number: 3 }, { name: 'stimulus4', number: 4 }]; 123 + switch (this.state.activeStep) { 124 + case CUSTOM_STEPS.OVERVIEW:default: 125 + return <Grid stretched relaxed padded columns='equal' className={styles.contentGrid}> 126 + <Grid.Column stretched verticalAlign='middle'> 127 + <Image as={Segment} basic centered src={researchQuestionImage} className={styles.overviewImage} /> 128 + <Form> 129 + <Form.TextArea autoHeight style={{ minHeight: 100, maxHeight: 400 }} label={FIELDS.QUESTION} value={this.state.description.question} placeholder='Explain your research question here.' onChange={(event, data) => this.setState({ 130 + description: { 131 + ...this.state.description, 132 + question: data.value 133 + }, 134 + saved: false 135 + })} /> 136 + </Form> 137 + </Grid.Column> 138 + <Grid.Column stretched verticalAlign='middle'> 139 + <Image as={Segment} basic centered src={hypothesisImage} className={styles.overviewImage} /> 140 + <Form> 141 + <Form.TextArea autoHeight style={{ minHeight: 100, maxHeight: 400 }} label={FIELDS.HYPOTHESIS} value={this.state.description.hypothesis} placeholder='Describe your hypothesis here.' onChange={(event, data) => this.setState({ 142 + description: { 143 + ...this.state.description, 144 + hypothesis: data.value 145 + }, 146 + saved: false 147 + })} /> 148 + </Form> 149 + </Grid.Column> 150 + <Grid.Column verticalAlign='middle'> 151 + <Image as={Segment} basic centered src={methodsImage} className={styles.overviewImage} /> 152 + <Form> 153 + <Form.TextArea autoHeight style={{ minHeight: 100, maxHeight: 400 }} label={FIELDS.METHODS} value={this.state.description.methods} placeholder='Explain how you will design your experiment to answer the question here.' onChange={(event, data) => this.setState({ 154 + description: { 155 + ...this.state.description, 156 + methods: data.value 157 + }, 158 + saved: false 159 + })} /> 160 + </Form> 161 + </Grid.Column> 162 + </Grid>; 163 + 164 + case CUSTOM_STEPS.CONDITIONS: 165 + return <Grid> 166 + <Segment basic> 167 + <Header as='h1'>Conditions</Header> 168 + <p> 169 + Select the folder with images for each condition and choose the correct response. 170 + You can upload image files with the following extensions: ".png", ".jpg", ".jpeg". 171 + Make sure when you preview your experiment that the resolution is high enough. You 172 + can resize or compress your images in an image editing program or on one of the 173 + websites online. 174 + </p> 175 + </Segment> 176 + 177 + <Table basic='very'> 178 + <Table.Header> 179 + <Table.Row className={styles.conditionHeaderRow}> 180 + <Table.HeaderCell className={styles.conditionHeaderRowName}> 181 + Condition 182 + </Table.HeaderCell> 183 + <Table.HeaderCell>Default Key Response</Table.HeaderCell> 184 + <Table.HeaderCell>Condition Folder</Table.HeaderCell> 185 + </Table.Row> 186 + </Table.Header> 187 + 188 + <Table.Body className={styles.experimentTable}> 189 + {stimi.map(({ 190 + name, 191 + number 192 + }) => <StimuliDesignColumn key={number} num={number} {...this.state.params[name]} numberImages={this.state.params.stimuli.filter(trial => trial.type === number).length} onChange={async (key, data, changedName) => { 193 + await this.setState({ 194 + params: { 195 + ...this.state.params, 196 + [changedName]: { ...this.state.params[changedName], [key]: data } 197 + } 198 + }); 199 + let newStimuli = []; 200 + await stimi.map(stimul => { 201 + let dirStimuli = []; 202 + const dir = this.state.params[stimul.name].dir; 203 + if (dir && typeof dir !== 'undefined' && dir !== '') { 204 + dirStimuli = readImages(dir).map(i => ({ 205 + dir: dir, 206 + filename: i, 207 + name: i, 208 + condition: this.state.params[stimul.name].title, 209 + response: this.state.params[stimul.name].response, 210 + phase: 'main', 211 + type: stimul.number 212 + })); 213 + } 214 + if (dirStimuli.length) dirStimuli[0].phase = 'practice'; 215 + newStimuli = newStimuli.concat(...dirStimuli); 216 + }); 217 + this.setState({ 218 + params: { 219 + ...this.state.params, 220 + stimuli: [...newStimuli], 221 + nbTrials: newStimuli.filter(t => t.phase === 'main').length, 222 + nbPracticeTrials: newStimuli.filter(t => t.phase === 'practice').length 223 + }, 224 + saved: false 225 + }); 226 + }} />)} 227 + </Table.Body> 228 + </Table> 229 + </Grid>; 230 + 231 + case CUSTOM_STEPS.TRIALS: 232 + return <Grid> 233 + <div className={styles.trialsHeader}> 234 + <div> 235 + <Header as='h1'>Trials</Header> 236 + <p>Edit the correct key response and type of each trial.</p> 237 + </div> 238 + 239 + <div> 240 + <Form style={{ alignSelf: 'flex-end' }}> 241 + <Form.Group className={styles.trialsTopInfoBar}> 242 + <Form.Select fluid selection label='Order' value={this.state.params.randomize} onChange={(event, data) => this.setState({ 243 + params: { 244 + ...this.state.params, 245 + randomize: data.value 246 + }, 247 + saved: false 248 + })} placeholder='Response' options={[{ key: 'random', text: 'Random', value: 'random' }, { key: 'sequential', text: 'Sequential', value: 'sequential' }]} /> 249 + <Form.Input label='Total experimental trials' type='number' fluid value={this.state.params.nbTrials} onChange={(event, data) => this.setState({ 250 + params: { 251 + ...this.state.params, 252 + nbTrials: parseInt(data.value) 253 + }, 254 + saved: false 255 + })} /> 256 + <Form.Input label='Total practice trials' type='number' fluid value={this.state.params.nbPracticeTrials} onChange={(event, data) => this.setState({ 257 + params: { 258 + ...this.state.params, 259 + nbPracticeTrials: parseInt(data.value) 260 + }, 261 + saved: false 262 + })} /> 263 + </Form.Group> 264 + </Form> 265 + </div> 266 + </div> 267 + 268 + <Table basic='very'> 269 + <Table.Header> 270 + <Table.Row className={styles.trialsHeaderRow}> 271 + <Table.HeaderCell className={styles.conditionHeaderRowName}> 272 + Name 273 + </Table.HeaderCell> 274 + <Table.HeaderCell>Condition</Table.HeaderCell> 275 + <Table.HeaderCell>Correct Key Response</Table.HeaderCell> 276 + <Table.HeaderCell>Trial Type</Table.HeaderCell> 277 + </Table.Row> 278 + </Table.Header> 279 + <Table.Body className={styles.trialsTable}> 280 + {this.state.params.stimuli && this.state.params.stimuli.map((e, num) => <StimuliRow key={num} num={num} conditions={[1, 2, 3, 4].map(n => this.state.params[`stimulus${n}`].title)} {...e} onDelete={num => { 281 + const stimuli = this.state.params.stimuli; 282 + stimuli.splice(num, 1); 283 + const nbPracticeTrials = stimuli.filter(s => s.phase === 'practice').length; 284 + const nbTrials = stimuli.filter(s => s.phase === 'main').length; 285 + this.setState({ 286 + params: { 287 + ...this.state.params, 288 + stimuli: [...stimuli], 289 + nbPracticeTrials, 290 + nbTrials 291 + }, 292 + saved: false 293 + }); 294 + }} onChange={(num, key, data) => { 295 + const stimuli = this.state.params.stimuli; 296 + stimuli[num][key] = data; 297 + let nbPracticeTrials = this.state.params.nbPracticeTrials; 298 + let nbTrials = this.state.params.nbTrials; 299 + if (key === 'phase') { 300 + nbPracticeTrials = stimuli.filter(s => s.phase === 'practice').length; 301 + nbTrials = stimuli.filter(s => s.phase === 'main').length; 302 + } 303 + this.setState({ 304 + params: { 305 + ...this.state.params, 306 + stimuli: [...stimuli], 307 + nbPracticeTrials, 308 + nbTrials 309 + }, 310 + saved: false 311 + }); 312 + }} />)} 313 + </Table.Body> 314 + </Table> 315 + </Grid>; 316 + 317 + case CUSTOM_STEPS.PARAMETERS: 318 + return <Grid> 319 + <Grid.Column width={8} style={{ display: 'grid', alignContent: 'space-between' }}> 320 + <Segment basic> 321 + <Header as='h1'>Inter-trial interval</Header> 322 + <p> 323 + Select the inter-trial interval duration. This is the amount of time between 324 + trials measured from the end of one trial to the start of the next one. 325 + </p> 326 + </Segment> 327 + <Segment basic style={{ marginTop: '100px' }}> 328 + <ParamSlider label='ITI Duration (seconds)' value={this.state.params.iti} marks={{ 329 + 1: '0.25', 330 + 2: '0.5', 331 + 3: '0.75', 332 + 4: '1', 333 + 5: '1.25', 334 + 6: '1.5', 335 + 7: '1.75', 336 + 8: '2' 337 + }} ms_conversion='250' onChange={value => this.setState({ 338 + params: { ...this.state.params, iti: value }, 339 + saved: false 340 + })} /> 341 + </Segment> 342 + </Grid.Column> 343 + 344 + <Grid.Column width={8} style={{ display: 'grid', alignContent: 'space-between' }}> 345 + <Segment basic> 346 + <Header as='h1'>Image duration</Header> 347 + <p> 348 + Select the time of presentation or make it self-paced - present the image until 349 + participants respond. 350 + </p> 351 + </Segment> 352 + <Segment basic> 353 + <Checkbox defaultChecked={this.state.params.selfPaced} label='Self-paced data collection' onChange={value => this.setState({ 354 + params: { ...this.state.params, selfPaced: !this.state.params.selfPaced }, 355 + saved: false 356 + })} /> 357 + </Segment> 358 + 359 + {!this.state.params.selfPaced ? <Segment basic> 360 + <ParamSlider label='Presentation time (seconds)' value={this.state.params.presentationTime} marks={{ 361 + 1: '0.25', 362 + 2: '0.5', 363 + 3: '0.75', 364 + 4: '1', 365 + 5: '1.25', 366 + 6: '1.5', 367 + 7: '1.75', 368 + 8: '2' 369 + }} ms_conversion='250' onChange={value => this.setState({ 370 + params: { ...this.state.params, presentationTime: value }, 371 + saved: false 372 + })} /> 373 + </Segment> : <Segment basic style={{ marginBottom: '85px' }} />} 374 + </Grid.Column> 375 + </Grid>; 376 + 377 + case CUSTOM_STEPS.INSTRUCTIONS: 378 + return <Grid stretched> 379 + <Grid.Column width={8} stretched style={{ display: 'grid', alignContent: 'space-between' }}> 380 + <Segment basic> 381 + <Header as='h1'>Experiment Instructions</Header> 382 + <p>Edit the instruction that will be displayed on the first screen.</p> 383 + <Form> 384 + <Form.TextArea autoHeight value={this.state.params.intro} 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." onChange={(event, data) => this.setState({ 385 + params: { ...this.state.params, intro: data.value }, 386 + saved: false 387 + })} /> 388 + </Form> 389 + </Segment> 390 + </Grid.Column> 391 + 392 + <Grid.Column width={8} stretched style={{ display: 'grid', alignContent: 'space-between' }}> 393 + <Segment basic> 394 + <Header as='h1'>Instructions for the task screen</Header> 395 + <p>Edit the instruction that will be displayed in the footer during the task.</p> 396 + <Form> 397 + <Form.TextArea autoHeight value={this.state.params.taskHelp} placeholder='e.g., Press 1 for a face and 9 for a house' onChange={(event, data) => this.setState({ 398 + params: { ...this.state.params, taskHelp: data.value }, 399 + saved: false 400 + })} /> 401 + </Form> 402 + </Segment> 403 + </Grid.Column> 404 + </Grid>; 405 + 406 + case CUSTOM_STEPS.PREVIEW: 407 + return <Grid relaxed padded className={styles.contentGrid}> 408 + <Grid.Column stretched width={14} textAlign='right' verticalAlign='middle' className={styles.previewWindow}> 409 + <PreviewExperimentComponent {...loadProtocol(this.props.paradigm)} isPreviewing={this.state.isPreviewing} onEnd={this.endPreview} type={this.props.type} paradigm={this.props.paradigm} previewParams={this.props.params} title={this.props.title} /> 410 + </Grid.Column> 411 + 412 + <Grid.Column width={2} verticalAlign='top'> 413 + <Segment basic> 414 + <PreviewButton isPreviewing={this.state.isPreviewing} onClick={e => this.handlePreview(e)} /> 415 + </Segment> 416 + </Grid.Column> 417 + </Grid>; 418 + 419 + } 420 + } 421 + 422 + render() { 423 + return <div className={styles.mainContainer}> 424 + <SecondaryNavComponent title='Experiment Design' steps={CUSTOM_STEPS} activeStep={this.state.activeStep} onStepClick={this.handleStepClick} enableEEGToggle={<Checkbox toggle defaultChecked={this.props.isEEGEnabled} onChange={(event, data) => this.handleEEGEnabled(event, data)} className={styles.EEGToggle} />} saveButton={<Button compact size='small' secondary onClick={() => { 425 + this.handleSaveParams(); 426 + }}> 427 + {this.state.saved ? 'Save' : 'Save'} 428 + </Button>} /> 429 + {this.renderSectionContent()} 430 + </div>; 431 + } 432 + }
-36
app/components/DesignComponent/ParamSlider.js
··· 1 - import { Segment } from 'semantic-ui-react'; 2 - import React, { PureComponent } from 'react'; 3 - import Slider from 'rc-slider'; 4 - import styles from '../styles/common.css'; 5 - 6 - interface Props { 7 - value: number; 8 - label: string; 9 - onChange: (number) => void; 10 - } 11 - 12 - export default class ParamSlider extends PureComponent<Props> { 13 - render() { 14 - const { marks, ms_conversion } = this.props; 15 - return ( 16 - <div> 17 - <p className={styles.label}>{this.props.label}</p> 18 - <Segment basic> 19 - {this.props.label !== 'Practice trials' || Object.keys(marks).length > 1 ? ( 20 - <Slider 21 - dots 22 - marks={this.props.marks} 23 - min={Math.min(...Object.keys(marks))} 24 - max={Math.max(...Object.keys(marks))} 25 - value={this.props.value / parseInt(ms_conversion, 10)} 26 - onChange={(value) => this.props.onChange(value * parseInt(ms_conversion, 10))} 27 - defaultValue={1} 28 - /> 29 - ) : ( 30 - <div>You have not chosen any practice trials.</div> 31 - )} 32 - </Segment> 33 - </div> 34 - ); 35 - } 36 - }
+26
app/components/DesignComponent/ParamSlider.tsx
··· 1 + import { Segment } from "semantic-ui-react"; 2 + import React, { PureComponent } from "react"; 3 + import Slider from "rc-slider"; 4 + import styles from "../styles/common.css"; 5 + 6 + interface Props { 7 + value: number; 8 + label: string; 9 + onChange: (arg0: number) => void; 10 + } 11 + 12 + export default class ParamSlider extends PureComponent<Props> { 13 + 14 + render() { 15 + const { 16 + marks, 17 + ms_conversion 18 + } = this.props; 19 + return <div> 20 + <p className={styles.label}>{this.props.label}</p> 21 + <Segment basic> 22 + {this.props.label !== 'Practice trials' || Object.keys(marks).length > 1 ? <Slider dots marks={this.props.marks} min={Math.min(...Object.keys(marks))} max={Math.max(...Object.keys(marks))} value={this.props.value / parseInt(ms_conversion, 10)} onChange={value => this.props.onChange(value * parseInt(ms_conversion, 10))} defaultValue={1} /> : <div>You have not chosen any practice trials.</div>} 23 + </Segment> 24 + </div>; 25 + } 26 + }
-122
app/components/DesignComponent/StimuliDesignColumn.js
··· 1 - /* Breaking this component on its own is done mainly to increase performance. Text input is slow otherwise */ 2 - 3 - import React, { Component } from 'react'; 4 - import { Form, Button, Table, Icon } from 'semantic-ui-react'; 5 - import { toast } from 'react-toastify'; 6 - import { readImages } from '../../utils/filesystem/storage'; 7 - import { loadFromSystemDialog } from '../../utils/filesystem/select'; 8 - import { FILE_TYPES } from '../../constants/constants'; 9 - import * as path from 'path'; 10 - import styles from '../styles/common.css'; 11 - 12 - interface Props { 13 - num: number; 14 - title: string; 15 - response: string; 16 - dir: string; 17 - onChange: (string, string) => void; 18 - } 19 - 20 - const RESPONSE_OPTIONS = new Array(10).fill(0).map((_, i) => ({ 21 - key: i.toString(), 22 - text: i.toString(), 23 - value: i.toString(), 24 - })); 25 - 26 - export default class StimuliDesignColumn extends Component<Props> { 27 - constructor(props: Props) { 28 - super(props); 29 - this.handleSelectFolder = this.handleSelectFolder.bind(this); 30 - this.handleRemoveFolder = this.handleRemoveFolder.bind(this); 31 - this.state = { 32 - numberImages: undefined, 33 - }; 34 - } 35 - 36 - shouldComponentUpdate(nextProps) { 37 - if ( 38 - nextProps.title !== this.props.title || 39 - nextProps.response !== this.props.response || 40 - nextProps.dir !== this.props.dir 41 - ) { 42 - return true; 43 - } 44 - return false; 45 - } 46 - 47 - async handleSelectFolder() { 48 - const dir = await loadFromSystemDialog(FILE_TYPES.STIMULUS_DIR); 49 - if (dir) { 50 - const images = readImages(dir); 51 - if (images.length < 1) { 52 - toast.error('No images in folder!'); 53 - } 54 - this.setState({ 55 - numberImages: images.length, 56 - }); 57 - this.props.onChange('dir', dir, `stimulus${this.props.num}`); 58 - } 59 - } 60 - 61 - handleRemoveFolder() { 62 - this.setState({ 63 - numberImages: 0, 64 - }); 65 - this.props.onChange('dir', '', `stimulus${this.props.num}`); 66 - } 67 - 68 - render() { 69 - return ( 70 - <Table.Row className={styles.conditionRow}> 71 - <Table.Cell className={styles.conditionsNameRow}> 72 - {this.props.num} 73 - <Form> 74 - <Form.Input 75 - value={this.props.title} 76 - onChange={(event, data) => 77 - this.props.onChange('title', data.value, `stimulus${this.props.num}`) 78 - } 79 - placeholder='Enter condition name' 80 - /> 81 - </Form> 82 - </Table.Cell> 83 - 84 - <Table.Cell className={styles.experimentRowName}> 85 - <Form.Select 86 - fluid 87 - selection 88 - value={this.props.response} 89 - onChange={(event, data) => 90 - this.props.onChange('response', data.value, `stimulus${this.props.num}`) 91 - } 92 - placeholder='Select' 93 - options={RESPONSE_OPTIONS} 94 - /> 95 - </Table.Cell> 96 - 97 - <Table.Cell className={styles.experimentRowName}> 98 - {this.props.dir ? ( 99 - <div className={styles.selectedFolderContainer}> 100 - <div> 101 - Folder{' '} 102 - {this.props.dir && 103 - this.props.dir 104 - .split(path.sep) 105 - .slice(-1) 106 - .join(' / ')} 107 - </div> 108 - <div>( {this.state.numberImages || this.props.numberImages} images ) </div> 109 - <div> 110 - <Icon name='delete' onClick={this.handleRemoveFolder} /> 111 - </div> 112 - </div> 113 - ) : ( 114 - <Button secondary onClick={this.handleSelectFolder}> 115 - Select folder 116 - </Button> 117 - )} 118 - </Table.Cell> 119 - </Table.Row> 120 - ); 121 - } 122 - }
+94
app/components/DesignComponent/StimuliDesignColumn.tsx
··· 1 + /* Breaking this component on its own is done mainly to increase performance. Text input is slow otherwise */ 2 + 3 + import React, { Component } from "react"; 4 + import { Form, Button, Table, Icon } from "semantic-ui-react"; 5 + import { toast } from "react-toastify"; 6 + import { readImages } from "../../utils/filesystem/storage"; 7 + import { loadFromSystemDialog } from "../../utils/filesystem/select"; 8 + import { FILE_TYPES } from "../../constants/constants"; 9 + import * as path from "path"; 10 + import styles from "../styles/common.css"; 11 + 12 + interface Props { 13 + num: number; 14 + title: string; 15 + response: string; 16 + dir: string; 17 + onChange: (arg0: string, arg1: string) => void; 18 + } 19 + 20 + const RESPONSE_OPTIONS = new Array(10).fill(0).map((_, i) => ({ 21 + key: i.toString(), 22 + text: i.toString(), 23 + value: i.toString() 24 + })); 25 + 26 + export default class StimuliDesignColumn extends Component<Props> { 27 + 28 + constructor(props: Props) { 29 + super(props); 30 + this.handleSelectFolder = this.handleSelectFolder.bind(this); 31 + this.handleRemoveFolder = this.handleRemoveFolder.bind(this); 32 + this.state = { 33 + numberImages: undefined 34 + }; 35 + } 36 + 37 + shouldComponentUpdate(nextProps) { 38 + if (nextProps.title !== this.props.title || nextProps.response !== this.props.response || nextProps.dir !== this.props.dir) { 39 + return true; 40 + } 41 + return false; 42 + } 43 + 44 + async handleSelectFolder() { 45 + const dir = await loadFromSystemDialog(FILE_TYPES.STIMULUS_DIR); 46 + if (dir) { 47 + const images = readImages(dir); 48 + if (images.length < 1) { 49 + toast.error('No images in folder!'); 50 + } 51 + this.setState({ 52 + numberImages: images.length 53 + }); 54 + this.props.onChange('dir', dir, `stimulus${this.props.num}`); 55 + } 56 + } 57 + 58 + handleRemoveFolder() { 59 + this.setState({ 60 + numberImages: 0 61 + }); 62 + this.props.onChange('dir', '', `stimulus${this.props.num}`); 63 + } 64 + 65 + render() { 66 + return <Table.Row className={styles.conditionRow}> 67 + <Table.Cell className={styles.conditionsNameRow}> 68 + {this.props.num} 69 + <Form> 70 + <Form.Input value={this.props.title} onChange={(event, data) => this.props.onChange('title', data.value, `stimulus${this.props.num}`)} placeholder='Enter condition name' /> 71 + </Form> 72 + </Table.Cell> 73 + 74 + <Table.Cell className={styles.experimentRowName}> 75 + <Form.Select fluid selection value={this.props.response} onChange={(event, data) => this.props.onChange('response', data.value, `stimulus${this.props.num}`)} placeholder='Select' options={RESPONSE_OPTIONS} /> 76 + </Table.Cell> 77 + 78 + <Table.Cell className={styles.experimentRowName}> 79 + {this.props.dir ? <div className={styles.selectedFolderContainer}> 80 + <div> 81 + Folder{' '} 82 + {this.props.dir && this.props.dir.split(path.sep).slice(-1).join(' / ')} 83 + </div> 84 + <div>( {this.state.numberImages || this.props.numberImages} images ) </div> 85 + <div> 86 + <Icon name='delete' onClick={this.handleRemoveFolder} /> 87 + </div> 88 + </div> : <Button secondary onClick={this.handleSelectFolder}> 89 + Select folder 90 + </Button>} 91 + </Table.Cell> 92 + </Table.Row>; 93 + } 94 + }
+15 -31
app/components/DesignComponent/StimuliRow.js app/components/DesignComponent/StimuliRow.tsx
··· 1 1 /* Breaking this component on its own is done mainly to increase performance. Text input is slow otherwise */ 2 2 3 - import React, { Component } from 'react'; 4 - import { Segment, Form, Button, Table, Dropdown } from 'semantic-ui-react'; 5 - import styles from '../styles/common.css'; 3 + import React, { Component } from "react"; 4 + import { Segment, Form, Button, Table, Dropdown } from "semantic-ui-react"; 5 + import styles from "../styles/common.css"; 6 6 7 7 interface Props { 8 8 num: number; 9 9 response: string; 10 10 dir: string; 11 11 condition: string; 12 - onChange: (string, string) => void; 12 + onChange: (arg0: string, arg1: string) => void; 13 13 } 14 14 15 15 const RESPONSE_OPTIONS = new Array(10).fill(0).map((_, i) => ({ 16 16 key: i.toString(), 17 17 text: i.toString(), 18 - value: i.toString(), 18 + value: i.toString() 19 19 })); 20 20 21 21 export default class StimuliRow extends Component<Props> { 22 + 22 23 render() { 23 - return ( 24 - <Table.Row className={styles.trialsRow}> 24 + return <Table.Row className={styles.trialsRow}> 25 25 <Table.Cell className={styles.conditionsNameRow}> 26 26 <div style={{ alignSelf: 'center' }}>{this.props.num + 1}.</div> 27 27 <div>{this.props.name}</div> ··· 32 32 </Table.Cell> 33 33 34 34 <Table.Cell className={styles.experimentRowName}> 35 - <Form.Select 36 - fluid 37 - selection 38 - value={this.props.response} 39 - onChange={(event, data) => this.props.onChange(this.props.num, 'response', data.value)} 40 - placeholder='Response' 41 - options={RESPONSE_OPTIONS} 42 - /> 35 + <Form.Select fluid selection value={this.props.response} onChange={(event, data) => this.props.onChange(this.props.num, 'response', data.value)} placeholder='Response' options={RESPONSE_OPTIONS} /> 43 36 </Table.Cell> 44 37 45 38 <Table.Cell className={styles.trialsTrialTypeRow}> 46 39 <Segment basic className={styles.trialsTrialTypeSegment}> 47 - <div 48 - className={styles.trialsTrialTypeRowSelector} 49 - style={{ backgroundColor: this.props.phase === 'main' ? '#1AC4EF' : '#EB1B66' }} 50 - > 40 + <div className={styles.trialsTrialTypeRowSelector} style={{ backgroundColor: this.props.phase === 'main' ? '#1AC4EF' : '#EB1B66' }}> 51 41 {this.props.phase === 'main' ? 'Experimental' : 'Practice'} 52 42 </div> 53 43 <Dropdown fluid style={{ display: 'grid', color: '#C4C4C4', justifyContent: 'end' }}> ··· 55 45 <Dropdown.Item onClick={() => this.props.onChange(this.props.num, 'phase', 'main')}> 56 46 <div>Experimental</div> 57 47 </Dropdown.Item> 58 - <Dropdown.Item 59 - onClick={() => this.props.onChange(this.props.num, 'phase', 'practice')} 60 - > 48 + <Dropdown.Item onClick={() => this.props.onChange(this.props.num, 'phase', 'practice')}> 61 49 <div>Practice</div> 62 50 </Dropdown.Item> 63 51 </Dropdown.Menu> 64 52 </Dropdown> 65 53 </Segment> 66 54 67 - <Button 68 - secondary 69 - onClick={() => { 70 - this.props.onDelete(this.props.num); 71 - }} 72 - > 55 + <Button secondary onClick={() => { 56 + this.props.onDelete(this.props.num); 57 + }}> 73 58 Delete 74 59 </Button> 75 60 </Table.Cell> 76 - </Table.Row> 77 - ); 61 + </Table.Row>; 78 62 } 79 63 } 80 64 ··· 90 74 // options={[{key: 'main', text: 'Experimental', value: 'main'}, 91 75 // {key: 'practice', text: 'Practice', value: 'practice'}]} 92 76 // className={styles.trialsTrialTypeRowSelector} 93 - // /> 77 + // />
-390
app/components/DesignComponent/index.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Grid, Button, Segment, Header, Image, Checkbox } from 'semantic-ui-react'; 4 - import { isNil } from 'lodash'; 5 - import styles from '../styles/common.css'; 6 - import { EXPERIMENTS, SCREENS } from '../../constants/constants'; 7 - import { readWorkspaces } from '../../utils/filesystem/storage'; 8 - import { 9 - MainTimeline, 10 - Trial, 11 - ExperimentParameters, 12 - ExperimentDescription, 13 - } from '../../constants/interfaces'; 14 - import SecondaryNavComponent from '../SecondaryNavComponent'; 15 - import PreviewExperimentComponent from '../PreviewExperimentComponent'; 16 - import CustomDesign from './CustomDesignComponent'; 17 - import PreviewButton from '../PreviewButtonComponent'; 18 - 19 - import facesHousesOverview from '../../assets/common/FacesHouses.png'; 20 - import stroopOverview from '../../assets/common/Stroop.png'; 21 - import multitaskingOverview from '../../assets/common/Multitasking.png'; 22 - import searchOverview from '../../assets/common/VisualSearch.png'; 23 - import customOverview from '../../assets/common/Custom.png'; 24 - 25 - // conditions images 26 - import multiConditionShape from '../../assets/multi/multiConditionShape.png'; 27 - import multiConditionDots from '../../assets/multi/multiConditionDots.png'; 28 - import conditionFace from '../../assets/face_house/faces/Face1.jpg'; 29 - import conditionHouse from '../../assets/face_house/houses/House1.jpg'; 30 - import conditionOrangeT from '../../assets/search/conditionOrangeT.png'; 31 - import conditionNoOrangeT from '../../assets/search/conditionNoOrangeT.png'; 32 - import conditionCongruent from '../../assets/stroop/match_g.png'; 33 - import conditionIncongruent from '../../assets/stroop/mismatch6_r.png'; 34 - 35 - import { loadProtocol } from '../../utils/labjs/functions'; 36 - import { toast } from 'react-toastify'; 37 - import InputModal from '../InputModal'; 38 - 39 - import { shell } from 'electron'; 40 - 41 - const DESIGN_STEPS = { 42 - OVERVIEW: 'OVERVIEW', 43 - BACKGROUND: 'BACKGROUND', 44 - PROTOCOL: 'PROTOCOL', 45 - PREVIEW: 'PREVIEW', 46 - }; 47 - 48 - interface Props { 49 - history: Object; 50 - type: EXPERIMENTS; 51 - paradigm: EXPERIMENTS; 52 - title: string; 53 - params: ExperimentParameters; 54 - mainTimeline: MainTimeline; 55 - trials: { [string]: Trial }; 56 - timelines: {}; 57 - experimentActions: Object; 58 - description: ExperimentDescription; 59 - isEEGEnabled: boolean; 60 - } 61 - 62 - interface State { 63 - activeStep: string; 64 - isPreviewing: boolean; 65 - isNewExperimentModalOpen: boolean; 66 - recentWorkspaces: Array<string>; 67 - } 68 - 69 - export default class Design extends Component<Props, State> { 70 - // props: Props; 71 - // state: State; 72 - // handleStepClick: (Object, Object) => void; 73 - // handleStartExperiment: (Object) => void; 74 - // handleCustomizeExperiment: (Object) => void; 75 - // handlePreview: (Object) => void; 76 - // handleLoadCustomExperiment: (string) => void; 77 - 78 - constructor(props: Props) { 79 - super(props); 80 - this.state = { 81 - activeStep: DESIGN_STEPS.OVERVIEW, 82 - isPreviewing: false, 83 - isNewExperimentModalOpen: false, 84 - recentWorkspaces: [], 85 - }; 86 - this.handleStepClick = this.handleStepClick.bind(this); 87 - this.handleStartExperiment = this.handleStartExperiment.bind(this); 88 - this.handleCustomizeExperiment = this.handleCustomizeExperiment.bind(this); 89 - this.handleLoadCustomExperiment = this.handleLoadCustomExperiment.bind(this); 90 - this.handlePreview = this.handlePreview.bind(this); 91 - this.endPreview = this.endPreview.bind(this); 92 - this.handleEEGEnabled = this.handleEEGEnabled.bind(this); 93 - if (isNil(props.params)) { 94 - props.experimentActions.loadDefaultTimeline(); 95 - } 96 - } 97 - 98 - componentDidMount() { 99 - this.setState({ recentWorkspaces: readWorkspaces() }); 100 - } 101 - 102 - handleStepClick(step: string) { 103 - this.setState({ activeStep: step }); 104 - } 105 - 106 - handleStartExperiment() { 107 - this.props.history.push(SCREENS.COLLECT.route); 108 - } 109 - 110 - handleCustomizeExperiment() { 111 - this.setState({ 112 - isNewExperimentModalOpen: true, 113 - }); 114 - } 115 - 116 - handleLoadCustomExperiment(title: string) { 117 - this.setState({ isNewExperimentModalOpen: false }); 118 - // Don't create new workspace if it already exists or title is too short 119 - if (this.state.recentWorkspaces.includes(title)) { 120 - toast.error(`Experiment already exists`); 121 - return; 122 - } 123 - if (title.length <= 3) { 124 - toast.error(`Experiment name is too short`); 125 - return; 126 - } 127 - this.props.experimentActions.createNewWorkspace({ 128 - title, 129 - type: EXPERIMENTS.CUSTOM, 130 - paradigm: 'Custom', 131 - // paradigm: this.props.paradigm 132 - }); 133 - this.props.experimentActions.saveWorkspace(); 134 - } 135 - 136 - handlePreview(e) { 137 - e.target.blur(); 138 - this.setState({ isPreviewing: !this.state.isPreviewing }); 139 - } 140 - 141 - endPreview() { 142 - this.setState({ isPreviewing: false }); 143 - } 144 - 145 - handleEEGEnabled(event: Object, data: Object) { 146 - this.props.experimentActions.setEEGEnabled(data.checked); 147 - this.props.experimentActions.saveWorkspace(); 148 - } 149 - 150 - renderConditionIcon(type) { 151 - switch (type) { 152 - case 'conditionCongruent': 153 - return conditionCongruent; 154 - break; 155 - case 'conditionIncongruent': 156 - return conditionIncongruent; 157 - break; 158 - case 'conditionOrangeT': 159 - return conditionOrangeT; 160 - break; 161 - case 'conditionNoOrangeT': 162 - return conditionNoOrangeT; 163 - break; 164 - case 'conditionFace': 165 - return conditionFace; 166 - break; 167 - case 'conditionHouse': 168 - return conditionHouse; 169 - break; 170 - case 'multiConditionShape': 171 - return multiConditionShape; 172 - break; 173 - case 'multiConditionDots': 174 - default: 175 - return multiConditionDots; 176 - break; 177 - } 178 - } 179 - 180 - renderOverviewIcon(type) { 181 - switch (type) { 182 - case EXPERIMENTS.N170: 183 - return facesHousesOverview; 184 - break; 185 - 186 - case EXPERIMENTS.STROOP: 187 - return stroopOverview; 188 - break; 189 - 190 - case EXPERIMENTS.MULTI: 191 - return multitaskingOverview; 192 - break; 193 - 194 - case EXPERIMENTS.SEARCH: 195 - return searchOverview; 196 - break; 197 - 198 - case EXPERIMENTS.CUSTOM: 199 - default: 200 - return customOverview; 201 - break; 202 - } 203 - } 204 - 205 - renderSectionContent() { 206 - switch (this.state.activeStep) { 207 - case DESIGN_STEPS.OVERVIEW: 208 - default: 209 - return ( 210 - <Grid 211 - stretched 212 - relaxed 213 - padded 214 - className={styles.contentGrid} 215 - style={{ alignItems: 'center' }} 216 - > 217 - <Grid.Row stretched> 218 - <Grid.Column stretched width={5}> 219 - <Segment basic> 220 - <Image src={this.renderOverviewIcon(this.props.type)} /> 221 - </Segment> 222 - </Grid.Column> 223 - 224 - <Grid.Column stretched width={11}> 225 - <Segment basic> 226 - <Header as='h1'>{this.props.overview_title}</Header> 227 - <p>{this.props.overview}</p> 228 - </Segment> 229 - </Grid.Column> 230 - </Grid.Row> 231 - </Grid> 232 - ); 233 - 234 - case DESIGN_STEPS.BACKGROUND: 235 - return ( 236 - <Grid relaxed padded className={styles.contentGrid} style={{ alignItems: 'center' }}> 237 - <Grid.Row> 238 - <Grid.Column stretched width={4}> 239 - <Segment basic> 240 - <Image src={this.renderOverviewIcon(this.props.type)} /> 241 - </Segment> 242 - </Grid.Column> 243 - 244 - <Grid.Column stretched width={5}> 245 - <Segment basic> 246 - <p>{this.props.background_first_column}</p> 247 - <p style={{ fontWeight: 'bold' }}> 248 - {this.props.background_first_column_question} 249 - </p> 250 - </Segment> 251 - </Grid.Column> 252 - 253 - <Grid.Column stretched width={5}> 254 - <Segment basic> 255 - <p>{this.props.background_second_column}</p> 256 - <p style={{ fontWeight: 'bold' }}> 257 - {this.props.background_second_column_question} 258 - </p> 259 - </Segment> 260 - </Grid.Column> 261 - 262 - <Grid.Column width={2}> 263 - <Segment basic> 264 - <div className={styles.externalLinks}> 265 - {this.props.background_links.map((link) => ( 266 - <Button 267 - key={link.address} 268 - secondary 269 - onClick={() => { 270 - shell.openExternal(link.address); 271 - }} 272 - > 273 - {link.name} 274 - </Button> 275 - ))} 276 - </div> 277 - </Segment> 278 - </Grid.Column> 279 - </Grid.Row> 280 - </Grid> 281 - ); 282 - 283 - case DESIGN_STEPS.PROTOCOL: 284 - return ( 285 - <Grid relaxed padded className={styles.contentGrid} style={{ alignItems: 'center' }}> 286 - <Grid.Row stretched> 287 - <Grid.Column stretched width={7} textAlign='left'> 288 - <Segment basic> 289 - <Header as='h2'>{this.props.protocol_title}</Header> 290 - <p>{this.props.protocol}</p> 291 - </Segment> 292 - </Grid.Column> 293 - 294 - <Grid.Column width={9}> 295 - <Grid> 296 - <Grid.Row> 297 - <Grid.Column width={5}> 298 - <Image 299 - src={this.renderConditionIcon(this.props.protocol_condition_first_img)} 300 - /> 301 - </Grid.Column> 302 - <Grid.Column width={10}> 303 - <Segment basic> 304 - <Header as='h3'>{this.props.protocol_condition_first_title}</Header> 305 - <p>{this.props.protocol_condition_first}</p> 306 - </Segment> 307 - </Grid.Column> 308 - </Grid.Row> 309 - 310 - <Grid.Row> 311 - <Grid.Column width={5}> 312 - <Image 313 - src={this.renderConditionIcon(this.props.protocol_condition_second_img)} 314 - /> 315 - </Grid.Column> 316 - <Grid.Column width={10}> 317 - <Segment basic> 318 - <Header as='h3'>{this.props.protocol_condition_second_title}</Header> 319 - <p>{this.props.protocol_condition_second}</p> 320 - </Segment> 321 - </Grid.Column> 322 - </Grid.Row> 323 - </Grid> 324 - </Grid.Column> 325 - </Grid.Row> 326 - </Grid> 327 - ); 328 - 329 - case DESIGN_STEPS.PREVIEW: 330 - return ( 331 - <Grid relaxed padded className={styles.contentGrid}> 332 - <Grid.Column 333 - stretched 334 - width={12} 335 - textAlign='right' 336 - verticalAlign='middle' 337 - className={styles.previewWindow} 338 - > 339 - <PreviewExperimentComponent 340 - {...loadProtocol(this.props.paradigm)} 341 - isPreviewing={this.state.isPreviewing} 342 - onEnd={this.endPreview} 343 - type={this.props.type} 344 - paradigm={this.props.paradigm} 345 - /> 346 - </Grid.Column> 347 - <Grid.Column width={4} verticalAlign='middle'> 348 - <PreviewButton 349 - isPreviewing={this.state.isPreviewing} 350 - onClick={(e) => this.handlePreview(e)} 351 - /> 352 - </Grid.Column> 353 - </Grid> 354 - ); 355 - } 356 - } 357 - 358 - render() { 359 - if (this.props.type === EXPERIMENTS.CUSTOM) { 360 - return <CustomDesign {...this.props} />; 361 - } 362 - return ( 363 - <div className={styles.mainContainer}> 364 - <SecondaryNavComponent 365 - title='Experiment Design' 366 - steps={DESIGN_STEPS} 367 - activeStep={this.state.activeStep} 368 - onStepClick={this.handleStepClick} 369 - onEditClick={this.handleCustomizeExperiment} 370 - enableEEGToggle={ 371 - <Checkbox 372 - toggle 373 - defaultChecked={this.props.isEEGEnabled} 374 - onChange={(event, data) => this.handleEEGEnabled(event, data)} 375 - className={styles.EEGToggle} 376 - /> 377 - } 378 - canEditExperiment={this.props.paradigm === 'Faces and Houses'} 379 - /> 380 - {this.renderSectionContent()} 381 - <InputModal 382 - open={this.state.isNewExperimentModalOpen} 383 - onClose={this.handleLoadCustomExperiment} 384 - onExit={() => this.setState({ isNewExperimentModalOpen: false })} 385 - header='Enter a title for this experiment' 386 - /> 387 - </div> 388 - ); 389 - } 390 - }
+323
app/components/DesignComponent/index.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Grid, Button, Segment, Header, Image, Checkbox } from "semantic-ui-react"; 4 + import { isNil } from "lodash"; 5 + import styles from "../styles/common.css"; 6 + import { EXPERIMENTS, SCREENS } from "../../constants/constants"; 7 + import { readWorkspaces } from "../../utils/filesystem/storage"; 8 + import { MainTimeline, Trial, ExperimentParameters, ExperimentDescription } from "../../constants/interfaces"; 9 + import SecondaryNavComponent from "../SecondaryNavComponent"; 10 + import PreviewExperimentComponent from "../PreviewExperimentComponent"; 11 + import CustomDesign from "./CustomDesignComponent"; 12 + import PreviewButton from "../PreviewButtonComponent"; 13 + 14 + import facesHousesOverview from "../../assets/common/FacesHouses.png"; 15 + import stroopOverview from "../../assets/common/Stroop.png"; 16 + import multitaskingOverview from "../../assets/common/Multitasking.png"; 17 + import searchOverview from "../../assets/common/VisualSearch.png"; 18 + import customOverview from "../../assets/common/Custom.png"; 19 + 20 + // conditions images 21 + import multiConditionShape from "../../assets/multi/multiConditionShape.png"; 22 + import multiConditionDots from "../../assets/multi/multiConditionDots.png"; 23 + import conditionFace from "../../assets/face_house/faces/Face1.jpg"; 24 + import conditionHouse from "../../assets/face_house/houses/House1.jpg"; 25 + import conditionOrangeT from "../../assets/search/conditionOrangeT.png"; 26 + import conditionNoOrangeT from "../../assets/search/conditionNoOrangeT.png"; 27 + import conditionCongruent from "../../assets/stroop/match_g.png"; 28 + import conditionIncongruent from "../../assets/stroop/mismatch6_r.png"; 29 + 30 + import { loadProtocol } from "../../utils/labjs/functions"; 31 + import { toast } from "react-toastify"; 32 + import InputModal from "../InputModal"; 33 + 34 + import { shell } from "electron"; 35 + 36 + const DESIGN_STEPS = { 37 + OVERVIEW: 'OVERVIEW', 38 + BACKGROUND: 'BACKGROUND', 39 + PROTOCOL: 'PROTOCOL', 40 + PREVIEW: 'PREVIEW' 41 + }; 42 + 43 + interface Props { 44 + history: Object; 45 + type: EXPERIMENTS; 46 + paradigm: EXPERIMENTS; 47 + title: string; 48 + params: ExperimentParameters; 49 + mainTimeline: MainTimeline; 50 + trials: { 51 + [key: string]: Trial; 52 + }; 53 + timelines: {}; 54 + experimentActions: Object; 55 + description: ExperimentDescription; 56 + isEEGEnabled: boolean; 57 + } 58 + 59 + interface State { 60 + activeStep: string; 61 + isPreviewing: boolean; 62 + isNewExperimentModalOpen: boolean; 63 + recentWorkspaces: Array<string>; 64 + } 65 + 66 + export default class Design extends Component<Props, State> { 67 + // handleStartExperiment: (Object) => void; 68 + 69 + // handleCustomizeExperiment: (Object) => void; 70 + // handlePreview: (Object) => void; 71 + // handleLoadCustomExperiment: (string) => void; 72 + constructor(props: Props) { 73 + super(props); 74 + this.state = { 75 + activeStep: DESIGN_STEPS.OVERVIEW, 76 + isPreviewing: false, 77 + isNewExperimentModalOpen: false, 78 + recentWorkspaces: [] 79 + }; 80 + this.handleStepClick = this.handleStepClick.bind(this); 81 + this.handleStartExperiment = this.handleStartExperiment.bind(this); 82 + this.handleCustomizeExperiment = this.handleCustomizeExperiment.bind(this); 83 + this.handleLoadCustomExperiment = this.handleLoadCustomExperiment.bind(this); 84 + this.handlePreview = this.handlePreview.bind(this); 85 + this.endPreview = this.endPreview.bind(this); 86 + this.handleEEGEnabled = this.handleEEGEnabled.bind(this); 87 + if (isNil(props.params)) { 88 + props.experimentActions.loadDefaultTimeline(); 89 + } 90 + } 91 + 92 + componentDidMount() { 93 + this.setState({ recentWorkspaces: readWorkspaces() }); 94 + } 95 + 96 + handleStepClick(step: string) { 97 + this.setState({ activeStep: step }); 98 + } 99 + 100 + handleStartExperiment() { 101 + this.props.history.push(SCREENS.COLLECT.route); 102 + } 103 + 104 + handleCustomizeExperiment() { 105 + this.setState({ 106 + isNewExperimentModalOpen: true 107 + }); 108 + } 109 + 110 + handleLoadCustomExperiment(title: string) { 111 + this.setState({ isNewExperimentModalOpen: false }); 112 + // Don't create new workspace if it already exists or title is too short 113 + if (this.state.recentWorkspaces.includes(title)) { 114 + toast.error(`Experiment already exists`); 115 + return; 116 + } 117 + if (title.length <= 3) { 118 + toast.error(`Experiment name is too short`); 119 + return; 120 + } 121 + this.props.experimentActions.createNewWorkspace({ 122 + title, 123 + type: EXPERIMENTS.CUSTOM, 124 + paradigm: 'Custom' 125 + // paradigm: this.props.paradigm 126 + }); 127 + this.props.experimentActions.saveWorkspace(); 128 + } 129 + 130 + handlePreview(e) { 131 + e.target.blur(); 132 + this.setState({ isPreviewing: !this.state.isPreviewing }); 133 + } 134 + 135 + endPreview() { 136 + this.setState({ isPreviewing: false }); 137 + } 138 + 139 + handleEEGEnabled(event: Object, data: Object) { 140 + this.props.experimentActions.setEEGEnabled(data.checked); 141 + this.props.experimentActions.saveWorkspace(); 142 + } 143 + 144 + renderConditionIcon(type) { 145 + switch (type) { 146 + case 'conditionCongruent': 147 + return conditionCongruent; 148 + break; 149 + case 'conditionIncongruent': 150 + return conditionIncongruent; 151 + break; 152 + case 'conditionOrangeT': 153 + return conditionOrangeT; 154 + break; 155 + case 'conditionNoOrangeT': 156 + return conditionNoOrangeT; 157 + break; 158 + case 'conditionFace': 159 + return conditionFace; 160 + break; 161 + case 'conditionHouse': 162 + return conditionHouse; 163 + break; 164 + case 'multiConditionShape': 165 + return multiConditionShape; 166 + break; 167 + case 'multiConditionDots':default: 168 + return multiConditionDots; 169 + break; 170 + 171 + } 172 + } 173 + 174 + renderOverviewIcon(type) { 175 + switch (type) { 176 + case EXPERIMENTS.N170: 177 + return facesHousesOverview; 178 + break; 179 + 180 + case EXPERIMENTS.STROOP: 181 + return stroopOverview; 182 + break; 183 + 184 + case EXPERIMENTS.MULTI: 185 + return multitaskingOverview; 186 + break; 187 + 188 + case EXPERIMENTS.SEARCH: 189 + return searchOverview; 190 + break; 191 + 192 + case EXPERIMENTS.CUSTOM:default: 193 + return customOverview; 194 + break; 195 + 196 + } 197 + } 198 + 199 + renderSectionContent() { 200 + switch (this.state.activeStep) { 201 + case DESIGN_STEPS.OVERVIEW:default: 202 + return <Grid stretched relaxed padded className={styles.contentGrid} style={{ alignItems: 'center' }}> 203 + <Grid.Row stretched> 204 + <Grid.Column stretched width={5}> 205 + <Segment basic> 206 + <Image src={this.renderOverviewIcon(this.props.type)} /> 207 + </Segment> 208 + </Grid.Column> 209 + 210 + <Grid.Column stretched width={11}> 211 + <Segment basic> 212 + <Header as='h1'>{this.props.overview_title}</Header> 213 + <p>{this.props.overview}</p> 214 + </Segment> 215 + </Grid.Column> 216 + </Grid.Row> 217 + </Grid>; 218 + 219 + case DESIGN_STEPS.BACKGROUND: 220 + return <Grid relaxed padded className={styles.contentGrid} style={{ alignItems: 'center' }}> 221 + <Grid.Row> 222 + <Grid.Column stretched width={4}> 223 + <Segment basic> 224 + <Image src={this.renderOverviewIcon(this.props.type)} /> 225 + </Segment> 226 + </Grid.Column> 227 + 228 + <Grid.Column stretched width={5}> 229 + <Segment basic> 230 + <p>{this.props.background_first_column}</p> 231 + <p style={{ fontWeight: 'bold' }}> 232 + {this.props.background_first_column_question} 233 + </p> 234 + </Segment> 235 + </Grid.Column> 236 + 237 + <Grid.Column stretched width={5}> 238 + <Segment basic> 239 + <p>{this.props.background_second_column}</p> 240 + <p style={{ fontWeight: 'bold' }}> 241 + {this.props.background_second_column_question} 242 + </p> 243 + </Segment> 244 + </Grid.Column> 245 + 246 + <Grid.Column width={2}> 247 + <Segment basic> 248 + <div className={styles.externalLinks}> 249 + {this.props.background_links.map(link => <Button key={link.address} secondary onClick={() => { 250 + shell.openExternal(link.address); 251 + }}> 252 + {link.name} 253 + </Button>)} 254 + </div> 255 + </Segment> 256 + </Grid.Column> 257 + </Grid.Row> 258 + </Grid>; 259 + 260 + case DESIGN_STEPS.PROTOCOL: 261 + return <Grid relaxed padded className={styles.contentGrid} style={{ alignItems: 'center' }}> 262 + <Grid.Row stretched> 263 + <Grid.Column stretched width={7} textAlign='left'> 264 + <Segment basic> 265 + <Header as='h2'>{this.props.protocol_title}</Header> 266 + <p>{this.props.protocol}</p> 267 + </Segment> 268 + </Grid.Column> 269 + 270 + <Grid.Column width={9}> 271 + <Grid> 272 + <Grid.Row> 273 + <Grid.Column width={5}> 274 + <Image src={this.renderConditionIcon(this.props.protocol_condition_first_img)} /> 275 + </Grid.Column> 276 + <Grid.Column width={10}> 277 + <Segment basic> 278 + <Header as='h3'>{this.props.protocol_condition_first_title}</Header> 279 + <p>{this.props.protocol_condition_first}</p> 280 + </Segment> 281 + </Grid.Column> 282 + </Grid.Row> 283 + 284 + <Grid.Row> 285 + <Grid.Column width={5}> 286 + <Image src={this.renderConditionIcon(this.props.protocol_condition_second_img)} /> 287 + </Grid.Column> 288 + <Grid.Column width={10}> 289 + <Segment basic> 290 + <Header as='h3'>{this.props.protocol_condition_second_title}</Header> 291 + <p>{this.props.protocol_condition_second}</p> 292 + </Segment> 293 + </Grid.Column> 294 + </Grid.Row> 295 + </Grid> 296 + </Grid.Column> 297 + </Grid.Row> 298 + </Grid>; 299 + 300 + case DESIGN_STEPS.PREVIEW: 301 + return <Grid relaxed padded className={styles.contentGrid}> 302 + <Grid.Column stretched width={12} textAlign='right' verticalAlign='middle' className={styles.previewWindow}> 303 + <PreviewExperimentComponent {...loadProtocol(this.props.paradigm)} isPreviewing={this.state.isPreviewing} onEnd={this.endPreview} type={this.props.type} paradigm={this.props.paradigm} /> 304 + </Grid.Column> 305 + <Grid.Column width={4} verticalAlign='middle'> 306 + <PreviewButton isPreviewing={this.state.isPreviewing} onClick={e => this.handlePreview(e)} /> 307 + </Grid.Column> 308 + </Grid>; 309 + 310 + } 311 + } 312 + 313 + render() { 314 + if (this.props.type === EXPERIMENTS.CUSTOM) { 315 + return <CustomDesign {...this.props} />; 316 + } 317 + return <div className={styles.mainContainer}> 318 + <SecondaryNavComponent title='Experiment Design' steps={DESIGN_STEPS} activeStep={this.state.activeStep} onStepClick={this.handleStepClick} onEditClick={this.handleCustomizeExperiment} enableEEGToggle={<Checkbox toggle defaultChecked={this.props.isEEGEnabled} onChange={(event, data) => this.handleEEGEnabled(event, data)} className={styles.EEGToggle} />} canEditExperiment={this.props.paradigm === 'Faces and Houses'} /> 319 + {this.renderSectionContent()} 320 + <InputModal open={this.state.isNewExperimentModalOpen} onClose={this.handleLoadCustomExperiment} onExit={() => this.setState({ isNewExperimentModalOpen: false })} header='Enter a title for this experiment' /> 321 + </div>; 322 + } 323 + }
-133
app/components/EEGExplorationComponent.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Grid, Button, Header, Segment, Image, Divider } from 'semantic-ui-react'; 4 - import { PLOTTING_INTERVAL, CONNECTION_STATUS, DEVICE_AVAILABILITY } from '../constants/constants'; 5 - import eegImage from '../assets/common/EEG.png'; 6 - import SignalQualityIndicatorComponent from './SignalQualityIndicatorComponent'; 7 - import ViewerComponent from './ViewerComponent'; 8 - import ConnectModal from './CollectComponent/ConnectModal'; 9 - import styles from './styles/common.css'; 10 - 11 - interface Props { 12 - history: Object; 13 - connectedDevice: Object; 14 - signalQualityObservable: ?any; 15 - deviceType: DEVICES; 16 - deviceAvailability: DEVICE_AVAILABILITY; 17 - connectionStatus: CONNECTION_STATUS; 18 - deviceActions: Object; 19 - availableDevices: Array<any>; 20 - } 21 - 22 - interface State { 23 - isConnectModalOpen: boolean; 24 - } 25 - 26 - export default class Home extends Component<Props, State> { 27 - // props: Props; 28 - // state: State; 29 - // handleConnectModalClose: () => void; 30 - // handleStartConnect: () => void; 31 - 32 - constructor(props: Props) { 33 - super(props); 34 - this.state = { 35 - isConnectModalOpen: false, 36 - }; 37 - this.handleConnectModalClose = this.handleConnectModalClose.bind(this); 38 - this.handleStartConnect = this.handleStartConnect.bind(this); 39 - this.handleStopConnect = this.handleStopConnect.bind(this); 40 - } 41 - 42 - componentDidUpdate = (prevProps: Props, prevState: State) => { 43 - if ( 44 - this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && 45 - prevState.isConnectModalOpen 46 - ) { 47 - this.setState({ isConnectModalOpen: false }); 48 - } 49 - }; 50 - 51 - handleStartConnect() { 52 - this.setState({ isConnectModalOpen: true }); 53 - this.props.deviceActions.setDeviceAvailability(DEVICE_AVAILABILITY.SEARCHING); 54 - } 55 - 56 - handleStopConnect() { 57 - this.props.deviceActions.disconnectFromDevice(this.props.connectedDevice); 58 - this.setState({ isConnectModalOpen: false }); 59 - this.props.deviceActions.setDeviceAvailability(DEVICE_AVAILABILITY.NONE); 60 - } 61 - 62 - handleConnectModalClose() { 63 - this.setState({ isConnectModalOpen: false }); 64 - } 65 - 66 - render() { 67 - return ( 68 - <Grid 69 - stretched 70 - relaxed 71 - padded 72 - className={styles.contentGrid} 73 - style={{ alignItems: 'center' }} 74 - > 75 - {this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && ( 76 - <Grid.Row> 77 - <Grid.Column stretched width={6}> 78 - <SignalQualityIndicatorComponent 79 - signalQualityObservable={this.props.signalQualityObservable} 80 - plottingInterval={PLOTTING_INTERVAL} 81 - /> 82 - </Grid.Column> 83 - <Grid.Column stretched width={10}> 84 - <div className={styles.disconnectButtonContainer}> 85 - <Button secondary onClick={this.handleStopConnect}> 86 - Disconnect EEG Device 87 - </Button> 88 - </div> 89 - <ViewerComponent 90 - signalQualityObservable={this.props.signalQualityObservable} 91 - deviceType={this.props.deviceType} 92 - plottingInterval={PLOTTING_INTERVAL} 93 - /> 94 - </Grid.Column> 95 - </Grid.Row> 96 - )} 97 - {this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED && ( 98 - <Grid.Row stretched> 99 - <Grid.Column stretched width={5}> 100 - <Segment basic> 101 - <Image src={eegImage} /> 102 - </Segment> 103 - </Grid.Column> 104 - 105 - <Grid.Column stretched width={11}> 106 - <Segment basic> 107 - <Header as='h1'>Explore Raw EEG</Header> 108 - <Divider /> 109 - <p>Connect directly to an EEG device and view raw streaming data</p> 110 - <Button primary onClick={this.handleStartConnect}> 111 - Connect 112 - </Button> 113 - </Segment> 114 - </Grid.Column> 115 - <ConnectModal 116 - history={this.props.history} 117 - open={this.state.isConnectModalOpen} 118 - onClose={this.handleConnectModalClose} 119 - connectedDevice={this.props.connectedDevice} 120 - signalQualityObservable={this.props.signalQualityObservable} 121 - deviceType={this.props.deviceType} 122 - deviceAvailability={this.props.deviceAvailability} 123 - connectionStatus={this.props.connectionStatus} 124 - deviceActions={this.props.deviceActions} 125 - availableDevices={this.props.availableDevices} 126 - style={{ marginTop: '100px' }} 127 - /> 128 - </Grid.Row> 129 - )} 130 - </Grid> 131 - ); 132 - } 133 - }
+97
app/components/EEGExplorationComponent.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Grid, Button, Header, Segment, Image, Divider } from "semantic-ui-react"; 4 + import { PLOTTING_INTERVAL, CONNECTION_STATUS, DEVICE_AVAILABILITY } from "../constants/constants"; 5 + import eegImage from "../assets/common/EEG.png"; 6 + import SignalQualityIndicatorComponent from "./SignalQualityIndicatorComponent"; 7 + import ViewerComponent from "./ViewerComponent"; 8 + import ConnectModal from "./CollectComponent/ConnectModal"; 9 + import styles from "./styles/common.css"; 10 + 11 + interface Props { 12 + history: Object; 13 + connectedDevice: Object; 14 + signalQualityObservable: any | null | undefined; 15 + deviceType: DEVICES; 16 + deviceAvailability: DEVICE_AVAILABILITY; 17 + connectionStatus: CONNECTION_STATUS; 18 + deviceActions: Object; 19 + availableDevices: Array<any>; 20 + } 21 + 22 + interface State { 23 + isConnectModalOpen: boolean; 24 + } 25 + 26 + export default class Home extends Component<Props, State> { 27 + 28 + // handleConnectModalClose: () => void; 29 + // handleStartConnect: () => void; 30 + constructor(props: Props) { 31 + super(props); 32 + this.state = { 33 + isConnectModalOpen: false 34 + }; 35 + this.handleConnectModalClose = this.handleConnectModalClose.bind(this); 36 + this.handleStartConnect = this.handleStartConnect.bind(this); 37 + this.handleStopConnect = this.handleStopConnect.bind(this); 38 + } 39 + 40 + componentDidUpdate = (prevProps: Props, prevState: State) => { 41 + if (this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && prevState.isConnectModalOpen) { 42 + this.setState({ isConnectModalOpen: false }); 43 + } 44 + }; 45 + 46 + handleStartConnect() { 47 + this.setState({ isConnectModalOpen: true }); 48 + this.props.deviceActions.setDeviceAvailability(DEVICE_AVAILABILITY.SEARCHING); 49 + } 50 + 51 + handleStopConnect() { 52 + this.props.deviceActions.disconnectFromDevice(this.props.connectedDevice); 53 + this.setState({ isConnectModalOpen: false }); 54 + this.props.deviceActions.setDeviceAvailability(DEVICE_AVAILABILITY.NONE); 55 + } 56 + 57 + handleConnectModalClose() { 58 + this.setState({ isConnectModalOpen: false }); 59 + } 60 + 61 + render() { 62 + return <Grid stretched relaxed padded className={styles.contentGrid} style={{ alignItems: 'center' }}> 63 + {this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && <Grid.Row> 64 + <Grid.Column stretched width={6}> 65 + <SignalQualityIndicatorComponent signalQualityObservable={this.props.signalQualityObservable} plottingInterval={PLOTTING_INTERVAL} /> 66 + </Grid.Column> 67 + <Grid.Column stretched width={10}> 68 + <div className={styles.disconnectButtonContainer}> 69 + <Button secondary onClick={this.handleStopConnect}> 70 + Disconnect EEG Device 71 + </Button> 72 + </div> 73 + <ViewerComponent signalQualityObservable={this.props.signalQualityObservable} deviceType={this.props.deviceType} plottingInterval={PLOTTING_INTERVAL} /> 74 + </Grid.Column> 75 + </Grid.Row>} 76 + {this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED && <Grid.Row stretched> 77 + <Grid.Column stretched width={5}> 78 + <Segment basic> 79 + <Image src={eegImage} /> 80 + </Segment> 81 + </Grid.Column> 82 + 83 + <Grid.Column stretched width={11}> 84 + <Segment basic> 85 + <Header as='h1'>Explore Raw EEG</Header> 86 + <Divider /> 87 + <p>Connect directly to an EEG device and view raw streaming data</p> 88 + <Button primary onClick={this.handleStartConnect}> 89 + Connect 90 + </Button> 91 + </Segment> 92 + </Grid.Column> 93 + <ConnectModal history={this.props.history} open={this.state.isConnectModalOpen} onClose={this.handleConnectModalClose} connectedDevice={this.props.connectedDevice} signalQualityObservable={this.props.signalQualityObservable} deviceType={this.props.deviceType} deviceAvailability={this.props.deviceAvailability} connectionStatus={this.props.connectionStatus} deviceActions={this.props.deviceActions} availableDevices={this.props.availableDevices} style={{ marginTop: '100px' }} /> 94 + </Grid.Row>} 95 + </Grid>; 96 + } 97 + }
-141
app/components/HomeComponent/OverviewComponent.js
··· 1 - import React, { Component } from 'react'; 2 - import { Grid, Header, Button, Segment } from 'semantic-ui-react'; 3 - import { isNil } from 'lodash'; 4 - import styles from '../styles/common.css'; 5 - import { EXPERIMENTS } from '../../constants/constants'; 6 - import SecondaryNavComponent from '../SecondaryNavComponent'; 7 - import PreviewExperimentComponent from '../PreviewExperimentComponent'; 8 - import PreviewButton from '../PreviewButtonComponent'; 9 - import { loadProtocol } from '../../utils/labjs/functions'; 10 - 11 - const OVERVIEW_STEPS = { 12 - OVERVIEW: 'OVERVIEW', 13 - BACKGROUND: 'BACKGROUND', 14 - PROTOCOL: 'PROTOCOL', 15 - }; 16 - 17 - interface Props { 18 - type: EXPERIMENTS; 19 - onStartExperiment: (EXPERIMENTS) => void; 20 - onCloseOverview: () => void; 21 - } 22 - 23 - interface State { 24 - activeStep: OVERVIEW_STEPS; 25 - isPreviewing: boolean; 26 - } 27 - 28 - export default class OverviewComponent extends Component<Props, State> { 29 - constructor(props) { 30 - super(props); 31 - this.state = { 32 - activeStep: OVERVIEW_STEPS.OVERVIEW, 33 - isPreviewing: false, 34 - }; 35 - this.handleStepClick = this.handleStepClick.bind(this); 36 - this.handlePreview = this.handlePreview.bind(this); 37 - this.endPreview = this.endPreview.bind(this); 38 - } 39 - 40 - handleStepClick(step: string) { 41 - this.setState({ activeStep: step }); 42 - } 43 - 44 - handlePreview(e) { 45 - e.target.blur(); 46 - this.setState((prevState) => ({ ...prevState, isPreviewing: !prevState.isPreviewing })); 47 - } 48 - 49 - endPreview() { 50 - this.setState({ isPreviewing: false }); 51 - } 52 - 53 - renderSectionContent() { 54 - switch (this.state.activeStep) { 55 - case OVERVIEW_STEPS.PROTOCOL: 56 - return ( 57 - <Grid relaxed padded className={styles.contentGrid}> 58 - <Grid.Column 59 - stretched 60 - width={12} 61 - textAlign='right' 62 - verticalAlign='middle' 63 - className={styles.previewWindow} 64 - > 65 - <PreviewExperimentComponent 66 - {...loadProtocol(this.props.paradigm)} 67 - isPreviewing={this.state.isPreviewing} 68 - onEnd={this.endPreview} 69 - type={this.props.type} 70 - paradigm={this.props.paradigm} 71 - /> 72 - </Grid.Column> 73 - <Grid.Column stretched width={4} verticalAlign='middle'> 74 - <Segment as='p' basic> 75 - {this.props.protocol} 76 - </Segment> 77 - <PreviewButton 78 - isPreviewing={this.state.isPreviewing} 79 - onClick={(e) => this.handlePreview(e)} 80 - /> 81 - </Grid.Column> 82 - </Grid> 83 - ); 84 - case OVERVIEW_STEPS.BACKGROUND: 85 - return ( 86 - <Grid stretched relaxed padded className={styles.contentGrid}> 87 - <Grid.Column stretched width={6} textAlign='right' verticalAlign='middle'> 88 - <Header as='h1'>{this.props.background_title}</Header> 89 - </Grid.Column> 90 - <Grid.Column stretched width={6} verticalAlign='middle'> 91 - <Segment as='p' basic> 92 - {this.props.background} 93 - </Segment> 94 - </Grid.Column> 95 - </Grid> 96 - ); 97 - case OVERVIEW_STEPS.OVERVIEW: 98 - default: 99 - return ( 100 - <Grid stretched relaxed padded className={styles.contentGrid}> 101 - <Grid.Column stretched width={6} textAlign='right' verticalAlign='middle'> 102 - <Header as='h1'>{this.props.type}</Header> 103 - </Grid.Column> 104 - <Grid.Column stretched width={6} verticalAlign='middle'> 105 - <Segment as='p' basic> 106 - {this.props.overview} 107 - </Segment> 108 - </Grid.Column> 109 - </Grid> 110 - ); 111 - } 112 - } 113 - 114 - render() { 115 - return ( 116 - <> 117 - <Button 118 - basic 119 - circular 120 - size='huge' 121 - floated='right' 122 - icon='x' 123 - className={styles.closeButton} 124 - onClick={this.props.onCloseOverview} 125 - /> 126 - <SecondaryNavComponent 127 - title={this.props.type} 128 - steps={OVERVIEW_STEPS} 129 - activeStep={this.state.activeStep} 130 - onStepClick={this.handleStepClick} 131 - button={ 132 - <Button primary onClick={() => this.props.onStartExperiment(this.props.type)}> 133 - Start Experiment 134 - </Button> 135 - } 136 - /> 137 - <div className={styles.homeContentContainer}>{this.renderSectionContent()}</div> 138 - </> 139 - ); 140 - } 141 - }
+103
app/components/HomeComponent/OverviewComponent.tsx
··· 1 + import React, { Component } from "react"; 2 + import { Grid, Header, Button, Segment } from "semantic-ui-react"; 3 + import { isNil } from "lodash"; 4 + import styles from "../styles/common.css"; 5 + import { EXPERIMENTS } from "../../constants/constants"; 6 + import SecondaryNavComponent from "../SecondaryNavComponent"; 7 + import PreviewExperimentComponent from "../PreviewExperimentComponent"; 8 + import PreviewButton from "../PreviewButtonComponent"; 9 + import { loadProtocol } from "../../utils/labjs/functions"; 10 + 11 + const OVERVIEW_STEPS = { 12 + OVERVIEW: 'OVERVIEW', 13 + BACKGROUND: 'BACKGROUND', 14 + PROTOCOL: 'PROTOCOL' 15 + }; 16 + 17 + interface Props { 18 + type: EXPERIMENTS; 19 + onStartExperiment: (arg0: EXPERIMENTS) => void; 20 + onCloseOverview: () => void; 21 + } 22 + 23 + interface State { 24 + activeStep: OVERVIEW_STEPS; 25 + isPreviewing: boolean; 26 + } 27 + 28 + export default class OverviewComponent extends Component<Props, State> { 29 + 30 + constructor(props) { 31 + super(props); 32 + this.state = { 33 + activeStep: OVERVIEW_STEPS.OVERVIEW, 34 + isPreviewing: false 35 + }; 36 + this.handleStepClick = this.handleStepClick.bind(this); 37 + this.handlePreview = this.handlePreview.bind(this); 38 + this.endPreview = this.endPreview.bind(this); 39 + } 40 + 41 + handleStepClick(step: string) { 42 + this.setState({ activeStep: step }); 43 + } 44 + 45 + handlePreview(e) { 46 + e.target.blur(); 47 + this.setState(prevState => ({ ...prevState, isPreviewing: !prevState.isPreviewing })); 48 + } 49 + 50 + endPreview() { 51 + this.setState({ isPreviewing: false }); 52 + } 53 + 54 + renderSectionContent() { 55 + switch (this.state.activeStep) { 56 + case OVERVIEW_STEPS.PROTOCOL: 57 + return <Grid relaxed padded className={styles.contentGrid}> 58 + <Grid.Column stretched width={12} textAlign='right' verticalAlign='middle' className={styles.previewWindow}> 59 + <PreviewExperimentComponent {...loadProtocol(this.props.paradigm)} isPreviewing={this.state.isPreviewing} onEnd={this.endPreview} type={this.props.type} paradigm={this.props.paradigm} /> 60 + </Grid.Column> 61 + <Grid.Column stretched width={4} verticalAlign='middle'> 62 + <Segment as='p' basic> 63 + {this.props.protocol} 64 + </Segment> 65 + <PreviewButton isPreviewing={this.state.isPreviewing} onClick={e => this.handlePreview(e)} /> 66 + </Grid.Column> 67 + </Grid>; 68 + case OVERVIEW_STEPS.BACKGROUND: 69 + return <Grid stretched relaxed padded className={styles.contentGrid}> 70 + <Grid.Column stretched width={6} textAlign='right' verticalAlign='middle'> 71 + <Header as='h1'>{this.props.background_title}</Header> 72 + </Grid.Column> 73 + <Grid.Column stretched width={6} verticalAlign='middle'> 74 + <Segment as='p' basic> 75 + {this.props.background} 76 + </Segment> 77 + </Grid.Column> 78 + </Grid>; 79 + case OVERVIEW_STEPS.OVERVIEW:default: 80 + return <Grid stretched relaxed padded className={styles.contentGrid}> 81 + <Grid.Column stretched width={6} textAlign='right' verticalAlign='middle'> 82 + <Header as='h1'>{this.props.type}</Header> 83 + </Grid.Column> 84 + <Grid.Column stretched width={6} verticalAlign='middle'> 85 + <Segment as='p' basic> 86 + {this.props.overview} 87 + </Segment> 88 + </Grid.Column> 89 + </Grid>; 90 + 91 + } 92 + } 93 + 94 + render() { 95 + return <> 96 + <Button basic circular size='huge' floated='right' icon='x' className={styles.closeButton} onClick={this.props.onCloseOverview} /> 97 + <SecondaryNavComponent title={this.props.type} steps={OVERVIEW_STEPS} activeStep={this.state.activeStep} onStepClick={this.handleStepClick} button={<Button primary onClick={() => this.props.onStartExperiment(this.props.type)}> 98 + Start Experiment 99 + </Button>} /> 100 + <div className={styles.homeContentContainer}>{this.renderSectionContent()}</div> 101 + </>; 102 + } 103 + }
+70 -145
app/components/HomeComponent/index.js app/components/HomeComponent/index.tsx
··· 1 - import React, { Component } from 'react'; 2 - import { isNil } from 'lodash'; 3 - import { Grid, Button, Header, Segment, Image, Table } from 'semantic-ui-react'; 4 - import { toast } from 'react-toastify'; 5 - import * as moment from 'moment'; 1 + import React, { Component } from "react"; 2 + import { isNil } from "lodash"; 3 + import { Grid, Button, Header, Segment, Image, Table } from "semantic-ui-react"; 4 + import { toast } from "react-toastify"; 5 + import * as moment from "moment"; 6 6 7 - import styles from '../styles/common.css'; 8 - import { 9 - EXPERIMENTS, 10 - SCREENS, 11 - KERNEL_STATUS, 12 - CONNECTION_STATUS, 13 - DEVICE_AVAILABILITY, 14 - } from '../../constants/constants'; 15 - import faceHouseIcon from '../../assets/common/FacesHouses.png'; 16 - import stroopIcon from '../../assets/common/Stroop.png'; 17 - import multitaskingIcon from '../../assets/common/Multitasking.png'; 18 - import searchIcon from '../../assets/common/VisualSearch.png'; 19 - import customIcon from '../../assets/common/Custom.png'; 20 - import appLogo from '../../assets/common/app_logo.png'; 21 - import divingMan from '../../assets/common/divingMan.svg'; 22 - import { 23 - readWorkspaces, 24 - readAndParseState, 25 - openWorkspaceDir, 26 - deleteWorkspaceDir, 27 - } from '../../utils/filesystem/storage'; 7 + import styles from "../styles/common.css"; 8 + import { EXPERIMENTS, SCREENS, KERNEL_STATUS, CONNECTION_STATUS, DEVICE_AVAILABILITY } from "../../constants/constants"; 9 + import faceHouseIcon from "../../assets/common/FacesHouses.png"; 10 + import stroopIcon from "../../assets/common/Stroop.png"; 11 + import multitaskingIcon from "../../assets/common/Multitasking.png"; 12 + import searchIcon from "../../assets/common/VisualSearch.png"; 13 + import customIcon from "../../assets/common/Custom.png"; 14 + import appLogo from "../../assets/common/app_logo.png"; 15 + import divingMan from "../../assets/common/divingMan.svg"; 16 + import { readWorkspaces, readAndParseState, openWorkspaceDir, deleteWorkspaceDir } from "../../utils/filesystem/storage"; 28 17 29 - import InputModal from '../InputModal'; 30 - import SecondaryNavComponent from '../SecondaryNavComponent'; 31 - import OverviewComponent from './OverviewComponent'; 32 - import { loadProtocol } from '../../utils/labjs/functions'; 33 - import EEGExplorationComponent from '../EEGExplorationComponent'; 18 + import InputModal from "../InputModal"; 19 + import SecondaryNavComponent from "../SecondaryNavComponent"; 20 + import OverviewComponent from "./OverviewComponent"; 21 + import { loadProtocol } from "../../utils/labjs/functions"; 22 + import EEGExplorationComponent from "../EEGExplorationComponent"; 34 23 35 - import { remote } from 'electron'; 24 + import { remote } from "electron"; 36 25 37 - const { dialog } = remote; 26 + const { 27 + dialog 28 + } = remote; 38 29 39 30 const HOME_STEPS = { 40 31 // TODO: maybe change the recent and new labels, but not necessary right now 41 32 RECENT: 'MY EXPERIMENTS', 42 33 NEW: 'EXPERIMENT BANK', 43 - EXPLORE: 'EXPLORE EEG DATA', 34 + EXPLORE: 'EXPLORE EEG DATA' 44 35 }; 45 36 46 37 interface Props { ··· 48 39 history: Object; 49 40 jupyterActions: Object; 50 41 connectedDevice: Object; 51 - signalQualityObservable: ?any; 42 + signalQualityObservable: any | null | undefined; 52 43 deviceType: DEVICES; 53 44 deviceAvailability: DEVICE_AVAILABILITY; 54 45 connectionStatus: CONNECTION_STATUS; ··· 66 57 } 67 58 68 59 export default class Home extends Component<Props, State> { 69 - // props: Props; 70 - // state: State; 71 - // handleNewExperiment: (EXPERIMENTS) => void; 72 - // handleStepClick: (string) => void; 60 + 73 61 // handleLoadCustomExperiment: (string) => void; 74 62 // handleOpenOverview: (EXPERIMENTS) => void; 75 63 // handleCloseOverview: () => void; 76 64 // handleDeleteWorkspace: (string) => void; 77 - 78 65 constructor(props: Props) { 79 66 super(props); 80 67 this.state = { ··· 82 69 recentWorkspaces: [], 83 70 isNewExperimentModalOpen: false, 84 71 isOverviewComponentOpen: false, 85 - overviewExperimentType: EXPERIMENTS.NONE, 72 + overviewExperimentType: EXPERIMENTS.NONE 86 73 }; 87 74 this.handleStepClick = this.handleStepClick.bind(this); 88 75 this.handleNewExperiment = this.handleNewExperiment.bind(this); ··· 103 90 handleNewExperiment(experimentType: EXPERIMENTS) { 104 91 if (experimentType === EXPERIMENTS.CUSTOM) { 105 92 this.setState({ 106 - isNewExperimentModalOpen: true, 93 + isNewExperimentModalOpen: true 107 94 }); 108 95 // If pre-designed experiment, load existing workspace 109 96 } else if (this.state.recentWorkspaces.includes(experimentType)) { ··· 113 100 this.props.experimentActions.createNewWorkspace({ 114 101 title: experimentType, 115 102 type: experimentType, 116 - paradigm: experimentType, 103 + paradigm: experimentType 117 104 }); 118 105 this.props.history.push(SCREENS.DESIGN.route); 119 106 } ··· 133 120 this.props.experimentActions.createNewWorkspace({ 134 121 title, 135 122 type: EXPERIMENTS.CUSTOM, 136 - paradigm: EXPERIMENTS.CUSTOM, 123 + paradigm: EXPERIMENTS.CUSTOM 137 124 }); 138 125 this.props.history.push(SCREENS.DESIGN.route); 139 126 } ··· 149 136 handleOpenOverview(type: EXPERIMENTS) { 150 137 this.setState({ 151 138 overviewExperimentType: type, 152 - isOverviewComponentOpen: true, 139 + isOverviewComponentOpen: true 153 140 }); 154 141 } 155 142 156 143 handleCloseOverview() { 157 144 this.setState({ 158 - isOverviewComponentOpen: false, 145 + isOverviewComponentOpen: false 159 146 }); 160 147 } 161 148 162 149 handleDeleteWorkspace(dir) { 163 150 const options = { 164 151 buttons: ['No', 'Yes'], 165 - message: 'Do you really want to delete the experiment?', 152 + message: 'Do you really want to delete the experiment?' 166 153 }; 167 154 const response = dialog.showMessageBox(options); 168 155 if (response === 1) { ··· 175 162 renderSectionContent() { 176 163 switch (this.state.activeStep) { 177 164 case HOME_STEPS.RECENT: 178 - return ( 179 - <Grid stackable padded columns='equal' className={styles.myExperimentsPage}> 180 - {this.state.recentWorkspaces.length > 0 ? ( 181 - <Table basic='very'> 165 + return <Grid stackable padded columns='equal' className={styles.myExperimentsPage}> 166 + {this.state.recentWorkspaces.length > 0 ? <Table basic='very'> 182 167 <Table.Header> 183 168 <Table.Row className={styles.experimentHeaderRow}> 184 169 <Table.HeaderCell className={styles.experimentHeaderName}> ··· 191 176 </Table.Row> 192 177 </Table.Header> 193 178 <Table.Body className={styles.experimentTable}> 194 - {this.state.recentWorkspaces 195 - .sort((a, b) => { 196 - const aTime = readAndParseState(a).params.dateModified || 0; 197 - const bTime = readAndParseState(b).params.dateModified || 0; 198 - return bTime - aTime; 199 - }) 200 - .map((dir) => { 201 - const { 202 - params: { dateModified }, 203 - } = readAndParseState(dir); 204 - return ( 205 - <Table.Row key={dir} className={styles.experimentRow}> 179 + {this.state.recentWorkspaces.sort((a, b) => { 180 + const aTime = readAndParseState(a).params.dateModified || 0; 181 + const bTime = readAndParseState(b).params.dateModified || 0; 182 + return bTime - aTime; 183 + }).map(dir => { 184 + const { 185 + params: { 186 + dateModified 187 + } 188 + } = readAndParseState(dir); 189 + return <Table.Row key={dir} className={styles.experimentRow}> 206 190 <Table.Cell className={styles.experimentRowName}>{dir}</Table.Cell> 207 191 <Table.Cell className={styles.experimentRowName}> 208 192 {dateModified && moment.default(dateModified).fromNow()} ··· 218 202 Open Experiment 219 203 </Button> 220 204 </Table.Cell> 221 - </Table.Row> 222 - ); 223 - })} 205 + </Table.Row>; 206 + })} 224 207 </Table.Body> 225 - </Table> 226 - ) : ( 227 - <Grid.Column textAlign='center'> 208 + </Table> : <Grid.Column textAlign='center'> 228 209 <Image src={divingMan} centered className={styles.noExperimentsImage} /> 229 210 <Header className={styles.noExperimentsTitle}> 230 211 You don&apos;t have any experiments yet ··· 235 216 <Button primary onClick={() => this.handleStepClick('EXPERIMENT BANK')}> 236 217 View Experiments 237 218 </Button> 238 - </Grid.Column> 239 - )} 240 - </Grid> 241 - ); 242 - case HOME_STEPS.NEW: 243 - default: 244 - return ( 245 - <Grid columns='two' relaxed padded> 219 + </Grid.Column>} 220 + </Grid>; 221 + case HOME_STEPS.NEW:default: 222 + return <Grid columns='two' relaxed padded> 246 223 <Grid.Row> 247 224 <Grid.Column> 248 225 <Segment> 249 - <Grid 250 - columns='two' 251 - className={styles.experimentCard} 252 - onClick={() => this.handleNewExperiment(EXPERIMENTS.N170)} 253 - > 226 + <Grid columns='two' className={styles.experimentCard} onClick={() => this.handleNewExperiment(EXPERIMENTS.N170)}> 254 227 <Grid.Row> 255 228 <Grid.Column width={4} className={styles.experimentCardImage}> 256 229 <Image src={faceHouseIcon} /> ··· 273 246 274 247 <Grid.Column> 275 248 <Segment> 276 - <Grid 277 - columns='two' 278 - className={styles.experimentCard} 279 - onClick={() => this.handleNewExperiment(EXPERIMENTS.STROOP)} 280 - > 249 + <Grid columns='two' className={styles.experimentCard} onClick={() => this.handleNewExperiment(EXPERIMENTS.STROOP)}> 281 250 <Grid.Row> 282 251 <Grid.Column width={4} className={styles.experimentCardImage}> 283 252 <Image src={stroopIcon} /> ··· 302 271 <Grid.Row> 303 272 <Grid.Column> 304 273 <Segment> 305 - <Grid 306 - columns='two' 307 - className={styles.experimentCard} 308 - onClick={() => this.handleNewExperiment(EXPERIMENTS.MULTI)} 309 - > 274 + <Grid columns='two' className={styles.experimentCard} onClick={() => this.handleNewExperiment(EXPERIMENTS.MULTI)}> 310 275 <Grid.Row> 311 276 <Grid.Column width={4} className={styles.experimentCardImage}> 312 277 <Image src={multitaskingIcon} /> ··· 329 294 330 295 <Grid.Column> 331 296 <Segment> 332 - <Grid 333 - columns='two' 334 - className={styles.experimentCard} 335 - onClick={() => this.handleNewExperiment(EXPERIMENTS.SEARCH)} 336 - > 297 + <Grid columns='two' className={styles.experimentCard} onClick={() => this.handleNewExperiment(EXPERIMENTS.SEARCH)}> 337 298 <Grid.Row> 338 299 <Grid.Column width={4} className={styles.experimentCardImage}> 339 300 <Image src={searchIcon} /> ··· 355 316 <Grid.Row> 356 317 <Grid.Column> 357 318 <Segment> 358 - <Grid 359 - columns='two' 360 - className={styles.experimentCard} 361 - onClick={() => this.handleNewExperiment(EXPERIMENTS.CUSTOM)} 362 - > 319 + <Grid columns='two' className={styles.experimentCard} onClick={() => this.handleNewExperiment(EXPERIMENTS.CUSTOM)}> 363 320 <Grid.Row> 364 321 <Grid.Column width={4} className={styles.experimentCardImage}> 365 322 <Image src={customIcon} /> ··· 379 336 380 337 <Grid.Column /> 381 338 </Grid.Row> 382 - </Grid> 383 - ); 339 + </Grid>; 384 340 case HOME_STEPS.EXPLORE: 385 - return ( 386 - <EEGExplorationComponent 387 - history={this.props.history} 388 - connectedDevice={this.props.connectedDevice} 389 - signalQualityObservable={this.props.signalQualityObservable} 390 - deviceType={this.props.deviceType} 391 - deviceAvailability={this.props.deviceAvailability} 392 - connectionStatus={this.props.connectionStatus} 393 - availableDevices={this.props.availableDevices} 394 - deviceActions={this.props.deviceActions} 395 - /> 396 - ); 341 + return <EEGExplorationComponent history={this.props.history} connectedDevice={this.props.connectedDevice} signalQualityObservable={this.props.signalQualityObservable} deviceType={this.props.deviceType} deviceAvailability={this.props.deviceAvailability} connectionStatus={this.props.connectionStatus} availableDevices={this.props.availableDevices} deviceActions={this.props.deviceActions} />; 342 + 397 343 } 398 344 } 399 345 400 346 renderOverviewOrHome() { 401 347 if (this.state.isOverviewComponentOpen) { 402 - return ( 403 - <OverviewComponent 404 - {...loadProtocol(this.state.overviewExperimentType)} 405 - type={this.state.overviewExperimentType} 406 - onStartExperiment={this.handleNewExperiment} 407 - onCloseOverview={this.handleCloseOverview} 408 - /> 409 - ); 348 + return <OverviewComponent {...loadProtocol(this.state.overviewExperimentType)} type={this.state.overviewExperimentType} onStartExperiment={this.handleNewExperiment} onCloseOverview={this.handleCloseOverview} />; 410 349 } 411 - return ( 412 - <> 413 - <SecondaryNavComponent 414 - title={<Image src={appLogo} />} 415 - steps={HOME_STEPS} 416 - activeStep={this.state.activeStep} 417 - onStepClick={this.handleStepClick} 418 - /> 350 + return <> 351 + <SecondaryNavComponent title={<Image src={appLogo} />} steps={HOME_STEPS} activeStep={this.state.activeStep} onStepClick={this.handleStepClick} /> 419 352 <div className={styles.homeContentContainer}>{this.renderSectionContent()}</div> 420 - </> 421 - ); 353 + </>; 422 354 } 423 355 424 356 render() { 425 - return ( 426 - <div className={styles.mainContainer} data-tid='container'> 357 + return <div className={styles.mainContainer} data-tid='container'> 427 358 {this.renderOverviewOrHome()} 428 - <InputModal 429 - open={this.state.isNewExperimentModalOpen} 430 - onClose={this.handleLoadCustomExperiment} 431 - onExit={() => this.setState({ isNewExperimentModalOpen: false })} 432 - header='Enter a title for this experiment' 433 - /> 434 - </div> 435 - ); 359 + <InputModal open={this.state.isNewExperimentModalOpen} onClose={this.handleLoadCustomExperiment} onExit={() => this.setState({ isNewExperimentModalOpen: false })} header='Enter a title for this experiment' /> 360 + </div>; 436 361 } 437 - } 362 + }
-132
app/components/InputCollect.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Input, Modal, Button } from 'semantic-ui-react'; 4 - import { debounce } from 'lodash'; 5 - import styles from './styles/common.css'; 6 - 7 - interface Props { 8 - open: boolean; 9 - onClose: (string) => void; 10 - onExit: (string) => void; 11 - header: string; 12 - } 13 - 14 - interface State { 15 - subject: string; 16 - group: string; 17 - session: number; 18 - isError: boolean; 19 - isSubjectError: boolean; 20 - isSessionError: boolean; 21 - } 22 - 23 - export default class InputCollect extends Component<Props, State> { 24 - // props: Props; 25 - // state: State; 26 - // handleTextEntry: (Object, Object, string) => void; 27 - // handleClose: () => void; 28 - // handleExit: () => void; 29 - // handleEnterSubmit: (Object) => void; 30 - 31 - constructor(props: Props) { 32 - super(props); 33 - this.state = { 34 - subject: this.props.data && this.props.data.subject, 35 - group: this.props.data && this.props.data.group, 36 - session: this.props.data && this.props.data.session, 37 - isError: false, 38 - isSubjectError: false, 39 - isSessionError: false, 40 - }; 41 - this.handleTextEntry = this.handleTextEntry.bind(this); 42 - this.handleClose = this.handleClose.bind(this); 43 - this.handleEnterSubmit = this.handleEnterSubmit.bind(this); 44 - this.handleExit = this.handleExit.bind(this); 45 - } 46 - 47 - sanitizeTextInput(text: string) { 48 - return text.replace(/[|&;$%@"<>()+,./]/g, ''); 49 - } 50 - 51 - handleTextEntry(event, data, field) { 52 - const val = field === 'session' ? parseInt(data.value) : data.value; 53 - this.setState({ [field]: val }); 54 - } 55 - 56 - handleClose() { 57 - if (this.state.subject.length >= 1 && this.state.session) { 58 - this.props.onClose( 59 - this.sanitizeTextInput(this.state.subject), 60 - this.sanitizeTextInput(this.state.group), 61 - this.state.session 62 - ); 63 - } else { 64 - if(this.state.subject.length < 1) { 65 - this.setState({ isSubjectError: true }); 66 - } 67 - if(!this.state.session) { 68 - this.setState({ isSessionError: true }); 69 - } 70 - } 71 - } 72 - 73 - handleExit() { 74 - this.props.onExit(); 75 - } 76 - 77 - handleEnterSubmit(event: Object) { 78 - if (event.key === 'Enter') { 79 - this.handleClose(); 80 - } 81 - } 82 - 83 - render() { 84 - return ( 85 - <Modal 86 - dimmer='inverted' 87 - centered 88 - className={styles.inputModal} 89 - open={this.props.open} 90 - onClose={this.handleExit} 91 - > 92 - <Modal.Content> 93 - Enter Subject ID 94 - <Input 95 - focus 96 - fluid 97 - error={this.state.isSubjectError} 98 - onChange={(object, data) => this.handleTextEntry(object, data, 'subject')} 99 - onKeyDown={this.handleEnterSubmit} 100 - value={this.state.subject} 101 - autoFocus 102 - /> 103 - </Modal.Content> 104 - <Modal.Content> 105 - Enter group name (optional) 106 - <Input 107 - focus 108 - fluid 109 - onChange={(object, data) => this.handleTextEntry(object, data, 'group')} 110 - onKeyDown={this.handleEnterSubmit} 111 - value={this.state.group} 112 - /> 113 - </Modal.Content> 114 - <Modal.Content> 115 - Enter session number 116 - <Input 117 - focus 118 - fluid 119 - error={this.state.isSessionError} 120 - onChange={(object, data) => this.handleTextEntry(object, data, 'session')} 121 - onKeyDown={this.handleEnterSubmit} 122 - value={this.state.session} 123 - type="number" 124 - /> 125 - </Modal.Content> 126 - <Modal.Actions> 127 - <Button color='blue' content='OK' onClick={this.handleClose} /> 128 - </Modal.Actions> 129 - </Modal> 130 - ); 131 - } 132 - }
+95
app/components/InputCollect.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Input, Modal, Button } from "semantic-ui-react"; 4 + import { debounce } from "lodash"; 5 + import styles from "./styles/common.css"; 6 + 7 + interface Props { 8 + open: boolean; 9 + onClose: (arg0: string) => void; 10 + onExit: (arg0: string) => void; 11 + header: string; 12 + } 13 + 14 + interface State { 15 + subject: string; 16 + group: string; 17 + session: number; 18 + isError: boolean; 19 + isSubjectError: boolean; 20 + isSessionError: boolean; 21 + } 22 + 23 + export default class InputCollect extends Component<Props, State> { 24 + 25 + // handleClose: () => void; 26 + // handleExit: () => void; 27 + // handleEnterSubmit: (Object) => void; 28 + constructor(props: Props) { 29 + super(props); 30 + this.state = { 31 + subject: this.props.data && this.props.data.subject, 32 + group: this.props.data && this.props.data.group, 33 + session: this.props.data && this.props.data.session, 34 + isError: false, 35 + isSubjectError: false, 36 + isSessionError: false 37 + }; 38 + this.handleTextEntry = this.handleTextEntry.bind(this); 39 + this.handleClose = this.handleClose.bind(this); 40 + this.handleEnterSubmit = this.handleEnterSubmit.bind(this); 41 + this.handleExit = this.handleExit.bind(this); 42 + } 43 + 44 + sanitizeTextInput(text: string) { 45 + return text.replace(/[|&;$%@"<>()+,./]/g, ''); 46 + } 47 + 48 + handleTextEntry(event, data, field) { 49 + const val = field === 'session' ? parseInt(data.value) : data.value; 50 + this.setState({ [field]: val }); 51 + } 52 + 53 + handleClose() { 54 + if (this.state.subject.length >= 1 && this.state.session) { 55 + this.props.onClose(this.sanitizeTextInput(this.state.subject), this.sanitizeTextInput(this.state.group), this.state.session); 56 + } else { 57 + if (this.state.subject.length < 1) { 58 + this.setState({ isSubjectError: true }); 59 + } 60 + if (!this.state.session) { 61 + this.setState({ isSessionError: true }); 62 + } 63 + } 64 + } 65 + 66 + handleExit() { 67 + this.props.onExit(); 68 + } 69 + 70 + handleEnterSubmit(event: Object) { 71 + if (event.key === 'Enter') { 72 + this.handleClose(); 73 + } 74 + } 75 + 76 + render() { 77 + return <Modal dimmer='inverted' centered className={styles.inputModal} open={this.props.open} onClose={this.handleExit}> 78 + <Modal.Content> 79 + Enter Subject ID 80 + <Input focus fluid error={this.state.isSubjectError} onChange={(object, data) => this.handleTextEntry(object, data, 'subject')} onKeyDown={this.handleEnterSubmit} value={this.state.subject} autoFocus /> 81 + </Modal.Content> 82 + <Modal.Content> 83 + Enter group name (optional) 84 + <Input focus fluid onChange={(object, data) => this.handleTextEntry(object, data, 'group')} onKeyDown={this.handleEnterSubmit} value={this.state.group} /> 85 + </Modal.Content> 86 + <Modal.Content> 87 + Enter session number 88 + <Input focus fluid error={this.state.isSessionError} onChange={(object, data) => this.handleTextEntry(object, data, 'session')} onKeyDown={this.handleEnterSubmit} value={this.state.session} type="number" /> 89 + </Modal.Content> 90 + <Modal.Actions> 91 + <Button color='blue' content='OK' onClick={this.handleClose} /> 92 + </Modal.Actions> 93 + </Modal>; 94 + } 95 + }
+13 -31
app/components/InputModal.js app/components/InputModal.tsx
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Input, Modal, Button } from 'semantic-ui-react'; 4 - import { debounce } from 'lodash'; 5 - import styles from './styles/common.css'; 1 + 2 + import React, { Component } from "react"; 3 + import { Input, Modal, Button } from "semantic-ui-react"; 4 + import { debounce } from "lodash"; 5 + import styles from "./styles/common.css"; 6 6 7 7 interface Props { 8 8 open: boolean; 9 - onClose: (string) => void; 10 - onExit: (string) => void; 9 + onClose: (arg0: string) => void; 10 + onExit: (arg0: string) => void; 11 11 header: string; 12 12 } 13 13 ··· 17 17 } 18 18 19 19 export default class InputModal extends Component<Props, State> { 20 - // props: Props; 21 - // state: State; 22 - // handleTextEntry: (Object, Object) => void; 20 + 23 21 // handleClose: () => void; 24 22 // handleExit: () => void; 25 23 // handleEnterSubmit: (Object) => void; 26 - 27 24 constructor(props: Props) { 28 25 super(props); 29 26 this.state = { 30 27 enteredText: '', 31 - isError: false, 28 + isError: false 32 29 }; 33 30 this.handleTextEntry = debounce(this.handleTextEntry, 100).bind(this); 34 31 this.handleClose = this.handleClose.bind(this); ··· 62 59 } 63 60 64 61 render() { 65 - return ( 66 - <Modal 67 - dimmer='inverted' 68 - centered 69 - className={styles.inputModal} 70 - open={this.props.open} 71 - onClose={this.handleExit} 72 - > 62 + return <Modal dimmer='inverted' centered className={styles.inputModal} open={this.props.open} onClose={this.handleExit}> 73 63 <Modal.Content>{this.props.header}</Modal.Content> 74 64 <Modal.Content> 75 - <Input 76 - focus 77 - fluid 78 - error={this.state.isError} 79 - onChange={this.handleTextEntry} 80 - onKeyDown={this.handleEnterSubmit} 81 - autoFocus 82 - /> 65 + <Input focus fluid error={this.state.isError} onChange={this.handleTextEntry} onKeyDown={this.handleEnterSubmit} autoFocus /> 83 66 </Modal.Content> 84 67 <Modal.Actions> 85 68 <Button color='blue' content='OK' onClick={this.handleClose} /> 86 69 </Modal.Actions> 87 - </Modal> 88 - ); 70 + </Modal>; 89 71 } 90 - } 72 + }
+20 -23
app/components/JupyterPlotWidget.js app/components/JupyterPlotWidget.tsx
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Segment, Button } from 'semantic-ui-react'; 4 - import { richestMimetype, standardDisplayOrder, standardTransforms } from '@nteract/transforms'; 5 - import { isNil } from 'lodash'; 6 - import { storeJupyterImage } from '../utils/filesystem/storage'; 1 + 2 + import React, { Component } from "react"; 3 + import { Segment, Button } from "semantic-ui-react"; 4 + import { richestMimetype, standardDisplayOrder, standardTransforms } from "@nteract/transforms"; 5 + import { isNil } from "lodash"; 6 + import { storeJupyterImage } from "../utils/filesystem/storage"; 7 7 8 8 interface Props { 9 9 title: string; 10 10 imageTitle: string; 11 - plotMIMEBundle: ?{ [string]: string }; 11 + plotMIMEBundle: { 12 + [key: string]: string; 13 + } | null | undefined; 12 14 } 13 15 14 16 interface State { ··· 17 19 } 18 20 19 21 export default class JupyterPlotWidget extends Component<Props, State> { 20 - // props: Props; 22 + 21 23 // state: State; 22 24 constructor(props: Props) { 23 25 super(props); 24 26 this.state = { 25 27 rawData: '', 26 - mimeType: '', 28 + mimeType: '' 27 29 }; 28 30 this.handleSave = this.handleSave.bind(this); 29 31 } 30 32 31 33 componentDidUpdate(prevProps: Props) { 32 - if ( 33 - this.props.plotMIMEBundle !== prevProps.plotMIMEBundle && 34 - !isNil(this.props.plotMIMEBundle) 35 - ) { 36 - const bundle: { [string]: string } = this.props.plotMIMEBundle; 34 + if (this.props.plotMIMEBundle !== prevProps.plotMIMEBundle && !isNil(this.props.plotMIMEBundle)) { 35 + const bundle: { 36 + [key: string]: string; 37 + } = this.props.plotMIMEBundle; 37 38 const mimeType = richestMimetype(bundle, standardDisplayOrder, standardTransforms); 38 39 if (mimeType) { 39 40 this.setState({ rawData: bundle[mimeType], mimeType }); ··· 55 56 56 57 renderSaveButton() { 57 58 if (this.state.rawData) { 58 - return ( 59 - <Button primary size='tiny' onClick={this.handleSave}> 59 + return <Button primary size='tiny' onClick={this.handleSave}> 60 60 Save Image 61 - </Button> 62 - ); 61 + </Button>; 63 62 } 64 63 } 65 64 66 65 render() { 67 - return ( 68 - <Segment basic> 66 + return <Segment basic> 69 67 {this.renderResults()} 70 68 {this.renderSaveButton()} 71 - </Segment> 72 - ); 69 + </Segment>; 73 70 } 74 - } 71 + }
-24
app/components/PreviewButtonComponent.js
··· 1 - import React, { PureComponent } from 'react'; 2 - import { Button } from 'semantic-ui-react'; 3 - 4 - interface Props { 5 - isPreviewing: boolean; 6 - onClick: () => void; 7 - } 8 - 9 - export default class PreviewButton extends PureComponent<Props> { 10 - render() { 11 - if (!this.props.isPreviewing) { 12 - return ( 13 - <Button secondary onClick={this.props.onClick}> 14 - Preview Experiment 15 - </Button> 16 - ); 17 - } 18 - return ( 19 - <Button negative onClick={this.props.onClick}> 20 - Stop Preview 21 - </Button> 22 - ); 23 - } 24 - }
+21
app/components/PreviewButtonComponent.tsx
··· 1 + import React, { PureComponent } from "react"; 2 + import { Button } from "semantic-ui-react"; 3 + 4 + interface Props { 5 + isPreviewing: boolean; 6 + onClick: () => void; 7 + } 8 + 9 + export default class PreviewButton extends PureComponent<Props> { 10 + 11 + render() { 12 + if (!this.props.isPreviewing) { 13 + return <Button secondary onClick={this.props.onClick}> 14 + Preview Experiment 15 + </Button>; 16 + } 17 + return <Button negative onClick={this.props.onClick}> 18 + Stop Preview 19 + </Button>; 20 + } 21 + }
-57
app/components/PreviewExperimentComponent.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Segment } from 'semantic-ui-react'; 4 - import { ExperimentWindow } from '../utils/labjs'; 5 - import styles from './styles/collect.css'; 6 - 7 - import { getImages } from '../utils/filesystem/storage'; 8 - import { MainTimeline, Trial, ExperimentParameters } from '../constants/interfaces'; 9 - 10 - interface Props { 11 - params: ExperimentParameters; 12 - isPreviewing: boolean; 13 - mainTimeline: MainTimeline; 14 - trials: { [string]: Trial }; 15 - timelines: {}; 16 - } 17 - 18 - export default class PreviewExperimentComponent extends Component<Props> { 19 - // props: Props; 20 - 21 - constructor(props: Props) { 22 - super(props); 23 - } 24 - 25 - handleImages() { 26 - return getImages(this.props.params); 27 - } 28 - 29 - insertPreviewLabJsCallback(e) { 30 - console.log('EEG marker', e); 31 - } 32 - 33 - render() { 34 - if (!this.props.isPreviewing) { 35 - return ( 36 - <div className={styles.previewPlaceholder}> 37 - <Segment basic> The experiment will be shown in the window </Segment> 38 - </div> 39 - ) 40 - } 41 - return ( 42 - <div className={styles.previewExpComponent}> 43 - <ExperimentWindow 44 - settings={{ 45 - title: this.props.title, 46 - script: this.props.paradigm, 47 - params: this.props.previewParams || this.props.params, 48 - eventCallback: this.insertPreviewLabJsCallback, 49 - on_finish: (csv) => { 50 - this.props.onEnd(); 51 - }, 52 - }} 53 - /> 54 - </div> 55 - ); 56 - } 57 - }
+53
app/components/PreviewExperimentComponent.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { Segment } from "semantic-ui-react"; 4 + import { ExperimentWindow } from "../utils/labjs"; 5 + import styles from "./styles/collect.css"; 6 + 7 + import { getImages } from "../utils/filesystem/storage"; 8 + import { MainTimeline, Trial, ExperimentParameters } from "../constants/interfaces"; 9 + 10 + interface Props { 11 + params: ExperimentParameters; 12 + isPreviewing: boolean; 13 + mainTimeline: MainTimeline; 14 + trials: { 15 + [key: string]: Trial; 16 + }; 17 + timelines: {}; 18 + } 19 + 20 + export default class PreviewExperimentComponent extends Component<Props> { 21 + // props: Props; 22 + 23 + constructor(props: Props) { 24 + super(props); 25 + } 26 + 27 + handleImages() { 28 + return getImages(this.props.params); 29 + } 30 + 31 + insertPreviewLabJsCallback(e) { 32 + console.log('EEG marker', e); 33 + } 34 + 35 + render() { 36 + if (!this.props.isPreviewing) { 37 + return <div className={styles.previewPlaceholder}> 38 + <Segment basic> The experiment will be shown in the window </Segment> 39 + </div>; 40 + } 41 + return <div className={styles.previewExpComponent}> 42 + <ExperimentWindow settings={{ 43 + title: this.props.title, 44 + script: this.props.paradigm, 45 + params: this.props.previewParams || this.props.params, 46 + eventCallback: this.insertPreviewLabJsCallback, 47 + on_finish: csv => { 48 + this.props.onEnd(); 49 + } 50 + }} /> 51 + </div>; 52 + } 53 + }
-27
app/components/SecondaryNavComponent/SecondaryNavSegment.js
··· 1 - import React, { Component } from 'react'; 2 - import { Grid } from 'semantic-ui-react'; 3 - import styles from '../styles/secondarynav.css'; 4 - 5 - interface Props { 6 - title: string; 7 - style: string; 8 - onClick: () => void; 9 - } 10 - 11 - export default class SecondaryNavSegment extends Component<Props> { 12 - // props: Props; 13 - 14 - render() { 15 - return ( 16 - <Grid.Column 17 - as='a' 18 - onClick={this.props.onClick} 19 - textAlign='center' 20 - verticalAlign='bottom' 21 - className={[this.props.style, styles.secondaryNavSegment].join(' ')} 22 - > 23 - {this.props.title} 24 - </Grid.Column> 25 - ); 26 - } 27 - }
+19
app/components/SecondaryNavComponent/SecondaryNavSegment.tsx
··· 1 + import React, { Component } from "react"; 2 + import { Grid } from "semantic-ui-react"; 3 + import styles from "../styles/secondarynav.css"; 4 + 5 + interface Props { 6 + title: string; 7 + style: string; 8 + onClick: () => void; 9 + } 10 + 11 + export default class SecondaryNavSegment extends Component<Props> { 12 + // props: Props; 13 + 14 + render() { 15 + return <Grid.Column as='a' onClick={this.props.onClick} textAlign='center' verticalAlign='bottom' className={[this.props.style, styles.secondaryNavSegment].join(' ')}> 16 + {this.props.title} 17 + </Grid.Column>; 18 + } 19 + }
-82
app/components/SecondaryNavComponent/index.js
··· 1 - import React, { Component } from 'react'; 2 - import { Grid, Header, Dropdown } from 'semantic-ui-react'; 3 - import styles from '../styles/secondarynav.css'; 4 - import SecondaryNavSegment from './SecondaryNavSegment'; 5 - import { NavLink } from 'react-router-dom'; 6 - import { SCREENS } from '../../constants/constants'; 7 - 8 - interface Props { 9 - title: string | React.ReactNode; 10 - steps: { [string]: string }; 11 - activeStep: string; 12 - onStepClick: (string) => void; 13 - button?: React.ReactNode; 14 - } 15 - 16 - export default class SecondaryNavComponent extends Component<Props> { 17 - shouldComponentUpdate(nextProps) { 18 - return nextProps.activeStep !== this.props.activeStep; 19 - } 20 - 21 - renderTitle() { 22 - if (typeof this.props.title === 'string') { 23 - return <Header className={styles.secondaryNavContainerExpName}>{this.props.title}</Header>; 24 - } 25 - return this.props.title; 26 - } 27 - 28 - renderSteps() { 29 - return ( 30 - <> 31 - {Object.values(this.props.steps).map((stepTitle) => ( 32 - <SecondaryNavSegment 33 - key={stepTitle} 34 - title={stepTitle} 35 - style={ 36 - this.props.activeStep === stepTitle 37 - ? styles.activeSecondaryNavSegment 38 - : styles.inactiveSecondaryNavSegment 39 - } 40 - onClick={() => this.props.onStepClick(stepTitle)} 41 - /> 42 - ))} 43 - </> 44 - ); 45 - } 46 - 47 - render() { 48 - return ( 49 - <Grid verticalAlign='middle' className={styles.secondaryNavContainer}> 50 - <Grid.Column width={3} verticalAlign='bottom'> 51 - {this.renderTitle()} 52 - </Grid.Column> 53 - 54 - {this.renderSteps()} 55 - 56 - {this.props.enableEEGToggle && ( 57 - <Grid.Column width={2} floated='right'> 58 - <div className={styles.settingsButtons}> 59 - <Dropdown icon='setting' direction='left' fluid className={styles.dropdownSettings}> 60 - <Dropdown.Menu className={styles.dropdownMenu}> 61 - <Dropdown.Item 62 - className={styles.dropdownItem} 63 - onClick={(e) => e.stopPropagation()} 64 - > 65 - <div>Enable EEG</div> 66 - {this.props.enableEEGToggle} 67 - </Dropdown.Item> 68 - <Dropdown.Item> 69 - <NavLink to={SCREENS.HOME.route}> 70 - <p>Exit Experiment</p> 71 - </NavLink> 72 - </Dropdown.Item> 73 - </Dropdown.Menu> 74 - </Dropdown> 75 - {this.props.saveButton} 76 - </div> 77 - </Grid.Column> 78 - )} 79 - </Grid> 80 - ); 81 - } 82 - }
+65
app/components/SecondaryNavComponent/index.tsx
··· 1 + import React, { Component } from "react"; 2 + import { Grid, Header, Dropdown } from "semantic-ui-react"; 3 + import styles from "../styles/secondarynav.css"; 4 + import SecondaryNavSegment from "./SecondaryNavSegment"; 5 + import { NavLink } from "react-router-dom"; 6 + import { SCREENS } from "../../constants/constants"; 7 + 8 + interface Props { 9 + title: string | React.ReactNode; 10 + steps: { 11 + [key: string]: string; 12 + }; 13 + activeStep: string; 14 + onStepClick: (arg0: string) => void; 15 + button?: React.ReactNode; 16 + } 17 + 18 + export default class SecondaryNavComponent extends Component<Props> { 19 + 20 + shouldComponentUpdate(nextProps) { 21 + return nextProps.activeStep !== this.props.activeStep; 22 + } 23 + 24 + renderTitle() { 25 + if (typeof this.props.title === 'string') { 26 + return <Header className={styles.secondaryNavContainerExpName}>{this.props.title}</Header>; 27 + } 28 + return this.props.title; 29 + } 30 + 31 + renderSteps() { 32 + return <> 33 + {Object.values(this.props.steps).map(stepTitle => <SecondaryNavSegment key={stepTitle} title={stepTitle} style={this.props.activeStep === stepTitle ? styles.activeSecondaryNavSegment : styles.inactiveSecondaryNavSegment} onClick={() => this.props.onStepClick(stepTitle)} />)} 34 + </>; 35 + } 36 + 37 + render() { 38 + return <Grid verticalAlign='middle' className={styles.secondaryNavContainer}> 39 + <Grid.Column width={3} verticalAlign='bottom'> 40 + {this.renderTitle()} 41 + </Grid.Column> 42 + 43 + {this.renderSteps()} 44 + 45 + {this.props.enableEEGToggle && <Grid.Column width={2} floated='right'> 46 + <div className={styles.settingsButtons}> 47 + <Dropdown icon='setting' direction='left' fluid className={styles.dropdownSettings}> 48 + <Dropdown.Menu className={styles.dropdownMenu}> 49 + <Dropdown.Item className={styles.dropdownItem} onClick={e => e.stopPropagation()}> 50 + <div>Enable EEG</div> 51 + {this.props.enableEEGToggle} 52 + </Dropdown.Item> 53 + <Dropdown.Item> 54 + <NavLink to={SCREENS.HOME.route}> 55 + <p>Exit Experiment</p> 56 + </NavLink> 57 + </Dropdown.Item> 58 + </Dropdown.Menu> 59 + </Dropdown> 60 + {this.props.saveButton} 61 + </div> 62 + </Grid.Column>} 63 + </Grid>; 64 + } 65 + }
-70
app/components/SignalQualityIndicatorComponent.js
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { isNil } from 'lodash'; 4 - import { Segment } from 'semantic-ui-react'; 5 - import * as d3 from 'd3'; 6 - import { Observable } from 'rxjs'; 7 - import SignalQualityIndicatorSVG from './svgs/SignalQualityIndicatorSVG'; 8 - 9 - interface Props { 10 - signalQualityObservable: ?Observable; 11 - plottingInterval: ?number; 12 - } 13 - 14 - class SignalQualityIndicatorComponent extends Component<Props> { 15 - // signalQualitySubscription: Subscription; 16 - 17 - constructor(props: Props) { 18 - super(props); 19 - this.signalQualitySubscription = null; 20 - } 21 - 22 - componentDidMount() { 23 - if (!isNil(this.props.signalQualityObservable)) { 24 - this.subscribeToObservable(this.props.signalQualityObservable); 25 - } 26 - } 27 - 28 - componentDidUpdate(prevProps: Props) { 29 - if (this.props.signalQualityObservable !== prevProps.signalQualityObservable) { 30 - this.subscribeToObservable(this.props.signalQualityObservable); 31 - } 32 - } 33 - 34 - componentWillUnmount() { 35 - if (!isNil(this.signalQualitySubscription)) { 36 - this.signalQualitySubscription.unsubscribe(); 37 - } 38 - } 39 - 40 - subscribeToObservable(observable: any) { 41 - if (!isNil(this.signalQualitySubscription)) { 42 - this.signalQualitySubscription.unsubscribe(); 43 - } 44 - 45 - this.signalQualitySubscription = observable.subscribe( 46 - (epoch) => { 47 - Object.keys(epoch.signalQuality).forEach((key) => { 48 - d3.select(`#${key}`) 49 - .attr('visibility', 'show') 50 - .attr('stroke', '#000') 51 - .transition() 52 - .duration(this.props.plottingInterval) 53 - .ease(d3.easeLinear) 54 - .attr('fill', epoch.signalQuality[key]); 55 - }); 56 - }, 57 - (error) => new Error(`Error in signalQualitySubscription ${error}`) 58 - ); 59 - } 60 - 61 - render() { 62 - return ( 63 - <Segment basic size='massive'> 64 - <SignalQualityIndicatorSVG /> 65 - </Segment> 66 - ); 67 - } 68 - } 69 - 70 - export default SignalQualityIndicatorComponent;
+59
app/components/SignalQualityIndicatorComponent.tsx
··· 1 + 2 + import React, { Component } from "react"; 3 + import { isNil } from "lodash"; 4 + import { Segment } from "semantic-ui-react"; 5 + import * as d3 from "d3"; 6 + import { Observable } from "rxjs"; 7 + import SignalQualityIndicatorSVG from "./svgs/SignalQualityIndicatorSVG"; 8 + 9 + interface Props { 10 + signalQualityObservable: Observable | null | undefined; 11 + plottingInterval: number | null | undefined; 12 + } 13 + 14 + class SignalQualityIndicatorComponent extends Component<Props> { 15 + // signalQualitySubscription: Subscription; 16 + 17 + constructor(props: Props) { 18 + super(props); 19 + this.signalQualitySubscription = null; 20 + } 21 + 22 + componentDidMount() { 23 + if (!isNil(this.props.signalQualityObservable)) { 24 + this.subscribeToObservable(this.props.signalQualityObservable); 25 + } 26 + } 27 + 28 + componentDidUpdate(prevProps: Props) { 29 + if (this.props.signalQualityObservable !== prevProps.signalQualityObservable) { 30 + this.subscribeToObservable(this.props.signalQualityObservable); 31 + } 32 + } 33 + 34 + componentWillUnmount() { 35 + if (!isNil(this.signalQualitySubscription)) { 36 + this.signalQualitySubscription.unsubscribe(); 37 + } 38 + } 39 + 40 + subscribeToObservable(observable: any) { 41 + if (!isNil(this.signalQualitySubscription)) { 42 + this.signalQualitySubscription.unsubscribe(); 43 + } 44 + 45 + this.signalQualitySubscription = observable.subscribe(epoch => { 46 + Object.keys(epoch.signalQuality).forEach(key => { 47 + d3.select(`#${key}`).attr('visibility', 'show').attr('stroke', '#000').transition().duration(this.props.plottingInterval).ease(d3.easeLinear).attr('fill', epoch.signalQuality[key]); 48 + }); 49 + }, error => new Error(`Error in signalQualitySubscription ${error}`)); 50 + } 51 + 52 + render() { 53 + return <Segment basic size='massive'> 54 + <SignalQualityIndicatorSVG /> 55 + </Segment>; 56 + } 57 + } 58 + 59 + export default SignalQualityIndicatorComponent;
-24
app/components/TopNavComponent/PrimaryNavSegment.js
··· 1 - import React, { Component } from 'react'; 2 - import { Grid } from 'semantic-ui-react'; 3 - import { NavLink } from 'react-router-dom'; 4 - import styles from '../styles/topnavbar.css'; 5 - 6 - interface Props { 7 - style: string; 8 - route: string; 9 - title: string; 10 - order: number; 11 - } 12 - 13 - export default class PrimaryNavSegment extends Component<Props> { 14 - render() { 15 - return ( 16 - <Grid.Column width={2} className={[this.props.style, styles.navColumn].join(' ')}> 17 - <NavLink to={this.props.route}> 18 - <div className={styles.numberBubble}>{this.props.order}</div> 19 - {this.props.title} 20 - </NavLink> 21 - </Grid.Column> 22 - ); 23 - } 24 - }
+23
app/components/TopNavComponent/PrimaryNavSegment.tsx
··· 1 + import React, { Component } from "react"; 2 + import { Grid } from "semantic-ui-react"; 3 + import { NavLink } from "react-router-dom"; 4 + import styles from "../styles/topnavbar.css"; 5 + 6 + interface Props { 7 + style: string; 8 + route: string; 9 + title: string; 10 + order: number; 11 + } 12 + 13 + export default class PrimaryNavSegment extends Component<Props> { 14 + 15 + render() { 16 + return <Grid.Column width={2} className={[this.props.style, styles.navColumn].join(' ')}> 17 + <NavLink to={this.props.route}> 18 + <div className={styles.numberBubble}>{this.props.order}</div> 19 + {this.props.title} 20 + </NavLink> 21 + </Grid.Column>; 22 + } 23 + }
-117
app/components/TopNavComponent/index.js
··· 1 - import React, { Component } from 'react'; 2 - import { Grid, Button, Segment, Image, Dropdown } from 'semantic-ui-react'; 3 - import { NavLink } from 'react-router-dom'; 4 - import { EXPERIMENTS, SCREENS } from '../../constants/constants'; 5 - import styles from '../styles/topnavbar.css'; 6 - import PrimaryNavSegment from './PrimaryNavSegment'; 7 - import { readAndParseState, readWorkspaces } from '../../utils/filesystem/storage'; 8 - import BrainwavesIcon from '../../assets/common/Brainwaves_Icon_big.png'; 9 - import { isNil } from 'lodash'; 10 - 11 - interface Props { 12 - title: ?string; 13 - location: { pathname: string, search: string, hash: string }; 14 - isRunning: boolean; 15 - experimentActions: Object; 16 - type: EXPERIMENTS; 17 - } 18 - 19 - interface State { 20 - recentWorkspaces: Array<string>; 21 - } 22 - 23 - export default class TopNavComponent extends Component<Props, State> { 24 - // props: Props; 25 - 26 - state = { 27 - recentWorkspaces: [], 28 - }; 29 - 30 - getStyleForScreen(navSegmentScreen: SCREENS) { 31 - if (navSegmentScreen.route === this.props.location.pathname) { 32 - return styles.activeNavColumn; 33 - } 34 - 35 - const routeOrder = Object.values(SCREENS).find( 36 - (screen) => screen.route === navSegmentScreen.route 37 - ).order; 38 - const currentOrder = Object.values(SCREENS).find( 39 - (screen) => screen.route === this.props.location.pathname 40 - ).order; 41 - if (currentOrder > routeOrder) { 42 - return styles.visitedNavColumn; 43 - } 44 - return styles.initialNavColumn; 45 - } 46 - 47 - handleLoadRecentWorkspace(dir: string) { 48 - const recentWorkspaceState = readAndParseState(dir); 49 - if (!isNil(recentWorkspaceState)) { 50 - this.props.experimentActions.setState(recentWorkspaceState); 51 - } 52 - } 53 - 54 - updateWorkspaces = () => { 55 - this.setState({ recentWorkspaces: readWorkspaces() }); 56 - }; 57 - 58 - render() { 59 - if ( 60 - this.props.location.pathname === SCREENS.HOME.route || 61 - this.props.location.pathname === SCREENS.BANK.route || 62 - this.props.location.pathname === '/' || 63 - this.props.isRunning 64 - ) { 65 - return null; 66 - } 67 - return ( 68 - <Grid className={styles.navContainer} verticalAlign='middle'> 69 - <Grid.Column className={styles.experimentTitleGridColumn}> 70 - <Segment basic className={styles.homeButton}> 71 - <NavLink to={SCREENS.HOME.route}> 72 - <Image centered className={styles.exitWorkspaceBtn} src={BrainwavesIcon} /> 73 - Home 74 - </NavLink> 75 - </Segment> 76 - </Grid.Column> 77 - 78 - <Grid.Column width={3} className={styles.experimentTitleGridColumn}> 79 - <Segment basic> 80 - <Dropdown 81 - text={this.props.title ? this.props.title : 'Untitled'} 82 - direction='right' 83 - onClick={() => { 84 - this.updateWorkspaces(); 85 - }} 86 - > 87 - <Dropdown.Menu> 88 - {this.state.recentWorkspaces.map((workspace) => ( 89 - <Dropdown.Item 90 - key={workspace} 91 - onClick={() => this.handleLoadRecentWorkspace(workspace)} 92 - > 93 - <p>{workspace}</p> 94 - </Dropdown.Item> 95 - ))} 96 - </Dropdown.Menu> 97 - </Dropdown> 98 - </Segment> 99 - </Grid.Column> 100 - 101 - <PrimaryNavSegment {...SCREENS.DESIGN} style={this.getStyleForScreen(SCREENS.DESIGN)} /> 102 - <PrimaryNavSegment {...SCREENS.COLLECT} style={this.getStyleForScreen(SCREENS.COLLECT)} /> 103 - {this.props.isEEGEnabled ? ( 104 - <PrimaryNavSegment {...SCREENS.CLEAN} style={this.getStyleForScreen(SCREENS.CLEAN)} /> 105 - ) : null} 106 - {this.props.isEEGEnabled ? ( 107 - <PrimaryNavSegment {...SCREENS.ANALYZE} style={this.getStyleForScreen(SCREENS.ANALYZE)} /> 108 - ) : ( 109 - <PrimaryNavSegment 110 - {...SCREENS.ANALYZEBEHAVIOR} 111 - style={this.getStyleForScreen(SCREENS.ANALYZE)} 112 - /> 113 - )} 114 - </Grid> 115 - ); 116 - } 117 - }
+88
app/components/TopNavComponent/index.tsx
··· 1 + import React, { Component } from "react"; 2 + import { Grid, Button, Segment, Image, Dropdown } from "semantic-ui-react"; 3 + import { NavLink } from "react-router-dom"; 4 + import { EXPERIMENTS, SCREENS } from "../../constants/constants"; 5 + import styles from "../styles/topnavbar.css"; 6 + import PrimaryNavSegment from "./PrimaryNavSegment"; 7 + import { readAndParseState, readWorkspaces } from "../../utils/filesystem/storage"; 8 + import BrainwavesIcon from "../../assets/common/Brainwaves_Icon_big.png"; 9 + import { isNil } from "lodash"; 10 + 11 + interface Props { 12 + title: string | null | undefined; 13 + location: {pathname: string;search: string;hash: string;}; 14 + isRunning: boolean; 15 + experimentActions: Object; 16 + type: EXPERIMENTS; 17 + } 18 + 19 + interface State { 20 + recentWorkspaces: Array<string>; 21 + } 22 + 23 + export default class TopNavComponent extends Component<Props, State> { 24 + // props: Props; 25 + 26 + state = { 27 + recentWorkspaces: [] 28 + }; 29 + 30 + getStyleForScreen(navSegmentScreen: SCREENS) { 31 + if (navSegmentScreen.route === this.props.location.pathname) { 32 + return styles.activeNavColumn; 33 + } 34 + 35 + const routeOrder = Object.values(SCREENS).find(screen => screen.route === navSegmentScreen.route).order; 36 + const currentOrder = Object.values(SCREENS).find(screen => screen.route === this.props.location.pathname).order; 37 + if (currentOrder > routeOrder) { 38 + return styles.visitedNavColumn; 39 + } 40 + return styles.initialNavColumn; 41 + } 42 + 43 + handleLoadRecentWorkspace(dir: string) { 44 + const recentWorkspaceState = readAndParseState(dir); 45 + if (!isNil(recentWorkspaceState)) { 46 + this.props.experimentActions.setState(recentWorkspaceState); 47 + } 48 + } 49 + 50 + updateWorkspaces = () => { 51 + this.setState({ recentWorkspaces: readWorkspaces() }); 52 + }; 53 + 54 + render() { 55 + if (this.props.location.pathname === SCREENS.HOME.route || this.props.location.pathname === SCREENS.BANK.route || this.props.location.pathname === '/' || this.props.isRunning) { 56 + return null; 57 + } 58 + return <Grid className={styles.navContainer} verticalAlign='middle'> 59 + <Grid.Column className={styles.experimentTitleGridColumn}> 60 + <Segment basic className={styles.homeButton}> 61 + <NavLink to={SCREENS.HOME.route}> 62 + <Image centered className={styles.exitWorkspaceBtn} src={BrainwavesIcon} /> 63 + Home 64 + </NavLink> 65 + </Segment> 66 + </Grid.Column> 67 + 68 + <Grid.Column width={3} className={styles.experimentTitleGridColumn}> 69 + <Segment basic> 70 + <Dropdown text={this.props.title ? this.props.title : 'Untitled'} direction='right' onClick={() => { 71 + this.updateWorkspaces(); 72 + }}> 73 + <Dropdown.Menu> 74 + {this.state.recentWorkspaces.map(workspace => <Dropdown.Item key={workspace} onClick={() => this.handleLoadRecentWorkspace(workspace)}> 75 + <p>{workspace}</p> 76 + </Dropdown.Item>)} 77 + </Dropdown.Menu> 78 + </Dropdown> 79 + </Segment> 80 + </Grid.Column> 81 + 82 + <PrimaryNavSegment {...SCREENS.DESIGN} style={this.getStyleForScreen(SCREENS.DESIGN)} /> 83 + <PrimaryNavSegment {...SCREENS.COLLECT} style={this.getStyleForScreen(SCREENS.COLLECT)} /> 84 + {this.props.isEEGEnabled ? <PrimaryNavSegment {...SCREENS.CLEAN} style={this.getStyleForScreen(SCREENS.CLEAN)} /> : null} 85 + {this.props.isEEGEnabled ? <PrimaryNavSegment {...SCREENS.ANALYZE} style={this.getStyleForScreen(SCREENS.ANALYZE)} /> : <PrimaryNavSegment {...SCREENS.ANALYZEBEHAVIOR} style={this.getStyleForScreen(SCREENS.ANALYZE)} />} 86 + </Grid>; 87 + } 88 + }
+15 -28
app/components/ViewerComponent.js app/components/ViewerComponent.tsx
··· 1 - // @flow 2 - import React, { Component } from 'react'; 3 - import { Subscription, Observable } from 'rxjs'; 4 - import { isNil } from 'lodash'; 5 - import { MUSE_CHANNELS, EMOTIV_CHANNELS, DEVICES, VIEWER_DEFAULTS } from '../constants/constants'; 1 + 2 + import React, { Component } from "react"; 3 + import { Subscription, Observable } from "rxjs"; 4 + import { isNil } from "lodash"; 5 + import { MUSE_CHANNELS, EMOTIV_CHANNELS, DEVICES, VIEWER_DEFAULTS } from "../constants/constants"; 6 6 7 7 const Mousetrap = require('mousetrap'); 8 8 9 9 interface Props { 10 - signalQualityObservable: ?Observable; 10 + signalQualityObservable: Observable | null | undefined; 11 11 deviceType: DEVICES; 12 12 plottingInterval: number; 13 13 } ··· 19 19 } 20 20 21 21 class ViewerComponent extends Component<Props, State> { 22 - // props: Props; 23 - // state: State; 22 + 24 23 // graphView: ?HTMLElement; 25 24 // signalQualitySubscription: Subscription; 26 - 27 25 constructor(props: Props) { 28 26 super(props); 29 27 this.state = { 30 28 ...VIEWER_DEFAULTS, 31 - channels: props.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS : MUSE_CHANNELS, 29 + channels: props.deviceType === DEVICES.EMOTIV ? EMOTIV_CHANNELS : MUSE_CHANNELS 32 30 }; 33 31 this.graphView = null; 34 32 this.signalQualitySubscription = null; ··· 41 39 plottingInterval: this.props.plottingInterval, 42 40 channels: this.state.channels, 43 41 domain: this.state.domain, 44 - channelColours: this.state.channels.map(() => '#66B0A9'), 42 + channelColours: this.state.channels.map(() => '#66B0A9') 45 43 }); 46 44 this.setKeyListeners(); 47 45 if (!isNil(this.props.signalQualityObservable)) { ··· 56 54 } 57 55 if (this.props.deviceType !== prevProps.deviceType) { 58 56 this.setState({ 59 - channels: this.props.deviceType === DEVICES.MUSE ? MUSE_CHANNELS : EMOTIV_CHANNELS, 57 + channels: this.props.deviceType === DEVICES.MUSE ? MUSE_CHANNELS : EMOTIV_CHANNELS 60 58 }); 61 59 } 62 60 if (this.state.channels !== prevState.channels) { ··· 90 88 if (!isNil(this.signalQualitySubscription)) { 91 89 this.signalQualitySubscription.unsubscribe(); 92 90 } 93 - this.signalQualitySubscription = observable.subscribe( 94 - (chunk) => { 95 - this.graphView.send('newData', chunk); 96 - }, 97 - (error) => new Error(`Error in epochSubscription ${error}`) 98 - ); 91 + this.signalQualitySubscription = observable.subscribe(chunk => { 92 + this.graphView.send('newData', chunk); 93 + }, error => new Error(`Error in epochSubscription ${error}`)); 99 94 } 100 95 101 96 render() { 102 - return ( 103 - <webview 104 - id='eegView' 105 - src={`file://${__dirname}/viewer.html`} 106 - autosize='true' 107 - nodeintegration='true' 108 - plugins='true' 109 - /> 110 - ); 97 + return <webview id='eegView' src={`file://${__dirname}/viewer.html`} autosize='true' nodeintegration='true' plugins='true' />; 111 98 } 112 99 } 113 100 114 - export default ViewerComponent; 101 + export default ViewerComponent;
-248
app/components/d3Classes/EEGViewer.js
··· 1 - /* eslint prefer-template: 0 */ 2 - const d3 = require('d3'); 3 - const throttle = require('lodash/throttle'); 4 - const simplify = require('simplify-js'); 5 - 6 - class EEGViewer { 7 - constructor(svg, parameters) { 8 - this.channels = parameters.channels; 9 - this.plottingInterval = parameters.plottingInterval; // NOTE: Plotting interval in Ms 10 - this.domain = parameters.domain + this.plottingInterval; 11 - this.channelColours = parameters.channelColours; 12 - this.downsampling = 2; 13 - this.lineWidth = 1.75; 14 - this.zoom = 1; 15 - this.zoomScalar = 1.5; 16 - this.canvas = d3.select(svg); 17 - this.margin = { top: 20, right: 10, bottom: 0, left: 20 }; 18 - this.channelMaxs = new Array(this.channels.length).fill(100); 19 - this.channelMins = new Array(this.channels.length).fill(-100); 20 - this.lastTimestamp = new Date().getTime(); 21 - this.firstTimestamp = this.lastTimestamp - this.domain; 22 - 23 - this.resetData(); 24 - this.init(); 25 - const resize = this.resize.bind(this); 26 - d3.select(window).on('resize.updatesvg', throttle(resize, 200)); 27 - } 28 - 29 - updateData(epoch) { 30 - const { 31 - info: { samplingRate, startTime }, 32 - } = epoch; 33 - this.lastTimestamp = startTime + epoch.data[0].length / (samplingRate / 1000); 34 - this.firstTimestamp = this.lastTimestamp - this.domain; 35 - for (let i = 0; i < this.channels.length; i++) { 36 - this.data[i] = this.data[i] 37 - .concat( 38 - simplify( 39 - epoch.data[i].map((dataPoint, index) => ({ 40 - x: startTime + index / (samplingRate / 1000), 41 - y: dataPoint, 42 - })), 43 - this.downsampling 44 - ) 45 - ) 46 - .filter((sample) => sample.x >= this.firstTimestamp); 47 - } 48 - 49 - this.channelColours = this.channels.map((channelName) => epoch.signalQuality[channelName]); 50 - this.redraw(); 51 - } 52 - 53 - updateChannels(channels) { 54 - this.channels = channels; 55 - this.resetData(); 56 - this.init(); 57 - } 58 - 59 - updateDomain(domain) { 60 - this.domain = domain; 61 - this.resetData(); 62 - this.init(); 63 - } 64 - 65 - zoomIn() { 66 - this.zoom /= this.zoomScalar; 67 - this.redraw(); 68 - } 69 - 70 - zoomOut() { 71 - this.zoom *= this.zoomScalar; 72 - this.redraw(); 73 - } 74 - 75 - autoScale() { 76 - this.channelMaxs = this.data.map((channelData) => 77 - EEGViewer.findExtreme(channelData, (a, b) => a > b) 78 - ); 79 - this.channelMins = this.data.map((channelData) => 80 - EEGViewer.findExtreme(channelData, (a, b) => a < b) 81 - ); 82 - this.zoom = 1; 83 - this.redraw(); 84 - } 85 - 86 - resetData() { 87 - this.data = new Array(this.channels.length).fill([{ x: this.lastTimestamp, y: 0 }]); 88 - this.channelMaxs = new Array(this.channels.length).fill(100); 89 - this.channelMins = new Array(this.channels.length).fill(-100); 90 - } 91 - 92 - init() { 93 - try { 94 - d3.selectAll('svg > *').remove(); 95 - this.width = 96 - parseInt(d3.select('#graph').style('width'), 10) - (this.margin.left + this.margin.right); 97 - this.height = 98 - parseInt(d3.select('#graph').style('height'), 10) - (this.margin.top + this.margin.bottom); 99 - this.graph = this.canvas 100 - .attr('width', this.width) 101 - .attr('height', this.height) 102 - .append('g') 103 - .attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); 104 - this.addScales(); 105 - this.addAxes(); 106 - this.addLines(); 107 - } catch (e) { 108 - console.error(e); 109 - } 110 - } 111 - 112 - addScales() { 113 - this.xScale = d3 114 - .scaleTime() 115 - .domain([this.lastTimestamp, this.firstTimestamp + this.plottingInterval]) 116 - .range([this.width, 0]); 117 - 118 - this.yScaleLines = d3.scaleLinear(); 119 - 120 - this.yScaleLabels = d3 121 - .scaleLinear() 122 - .domain([this.channels.length - 1, 0]) 123 - .range([ 124 - (this.channels.length - 1) * (this.height / this.channels.length) + 125 - this.height / this.channels.length / 2, 126 - this.height / this.channels.length / 2, 127 - ]); 128 - } 129 - 130 - addAxes() { 131 - this.yAxis = d3 132 - .axisLeft() 133 - .scale(this.yScaleLabels) 134 - .tickSize(2) 135 - .tickFormat((d, i) => this.channels[i].replace(/\s/g, '')) 136 - .tickValues(d3.range(this.channels.length)); 137 - 138 - this.axisY = this.graph 139 - .append('g') 140 - .attr('class', 'axis') 141 - .call(this.yAxis); 142 - } 143 - 144 - addLines() { 145 - this.graph.selectAll('.legend').remove(); 146 - this.graph.select('#lines').remove(); 147 - this.graph.selectAll('#clip').remove(); 148 - this.paths = []; 149 - this.graph 150 - .append('clipPath') 151 - .attr('id', 'clip') 152 - .append('rect') 153 - .attr('height', this.height + this.height / this.channels.length) 154 - .attr('width', this.width) 155 - .attr('transform', 'translate(0,' + -this.height / this.channels.length + ')'); 156 - this.lines = this.graph 157 - .append('g') 158 - .attr('id', 'lines') 159 - .attr('clip-path', 'url(#clip)'); 160 - this.line = d3 161 - .line() 162 - .x((d) => this.xScale(d.x)) 163 - .y((d) => this.yScaleLines(d.y)) 164 - .curve(d3.curveLinear) 165 - .defined((d) => d.y); 166 - 167 - for (let i = 0; i < this.channels.length; i++) { 168 - const channelData = this.data[i]; 169 - 170 - this.yScaleLines 171 - .domain([this.channelMins[i] / this.zoom, this.channelMaxs[i] / this.zoom]) 172 - .range(EEGViewer.getLineRange(i, this.channels.length, this.height)); 173 - 174 - this.paths[i] = this.lines 175 - .append('path') 176 - .data([channelData]) 177 - .attr('class', 'line') 178 - .attr('id', 'line' + i + 1) 179 - .attr('d', this.line) 180 - .attr('stroke', this.channelColours[i]) 181 - .attr('stroke-width', this.lineWidth); 182 - } 183 - } 184 - 185 - redraw() { 186 - if (this.paths != null) { 187 - for (let i = 0; i < this.channels.length; i++) { 188 - const channelData = this.data[i]; 189 - this.yScaleLines 190 - .domain([this.channelMins[i] / this.zoom, this.channelMaxs[i] / this.zoom]) 191 - .range(EEGViewer.getLineRange(i, this.channels.length, this.height)); 192 - 193 - this.paths[i] 194 - .interrupt() 195 - .transition() 196 - .duration(this.plottingInterval) 197 - .ease(d3.easeLinear) 198 - .attr( 199 - 'transform', 200 - 'translate(' + 201 - -(this.xScale(channelData[channelData.length - 1].x) - this.width) + 202 - ', 0)' 203 - ) 204 - .attr('stroke', this.channelColours[i]); 205 - 206 - this.paths[i] 207 - .data([channelData]) 208 - .attr('d', this.line) 209 - .attr('transform', 'translate(0,0)'); 210 - } 211 - 212 - this.xScale 213 - .domain([this.lastTimestamp, this.firstTimestamp + this.plottingInterval]) 214 - .range([this.width, 0]); 215 - } 216 - } 217 - 218 - resize() { 219 - if (this.paths != null) { 220 - this.width = 221 - parseInt(d3.select('#graph').style('width'), 10) - (this.margin.left + this.margin.right); 222 - this.height = 223 - parseInt(d3.select('#graph').style('height'), 10) - (this.margin.top + this.margin.bottom); 224 - this.lines.attr('height', this.height).attr('width', this.width); 225 - this.xScale.range([this.width, 0]); 226 - this.yScaleLines.range([this.height, 0]); 227 - this.yScaleLabels.range([ 228 - (this.channels.length - 1) * (this.height / this.channels.length) + 229 - this.height / this.channels.length / 2, 230 - this.height / this.channels.length / 2, 231 - ]); 232 - this.axisY.call(this.yAxis); 233 - this.addLines(); 234 - } 235 - } 236 - 237 - static findExtreme(data, comparison) { 238 - return data 239 - .slice(data.slice(data.length / 2)) 240 - .reduce((acc, curr) => (comparison(curr.y, acc) ? curr.y : acc), data[0].y); 241 - } 242 - 243 - static getLineRange(index, nbChannels, height) { 244 - return [(index + 1) * (height / nbChannels), index * (height / nbChannels)]; 245 - } 246 - } 247 - 248 - module.exports = EEGViewer;
+168
app/components/d3Classes/EEGViewer.ts
··· 1 + /* eslint prefer-template: 0 */ 2 + const d3 = require('d3'); 3 + const throttle = require('lodash/throttle'); 4 + const simplify = require('simplify-js'); 5 + 6 + class EEGViewer { 7 + 8 + constructor(svg, parameters) { 9 + this.channels = parameters.channels; 10 + this.plottingInterval = parameters.plottingInterval; // NOTE: Plotting interval in Ms 11 + this.domain = parameters.domain + this.plottingInterval; 12 + this.channelColours = parameters.channelColours; 13 + this.downsampling = 2; 14 + this.lineWidth = 1.75; 15 + this.zoom = 1; 16 + this.zoomScalar = 1.5; 17 + this.canvas = d3.select(svg); 18 + this.margin = { top: 20, right: 10, bottom: 0, left: 20 }; 19 + this.channelMaxs = new Array(this.channels.length).fill(100); 20 + this.channelMins = new Array(this.channels.length).fill(-100); 21 + this.lastTimestamp = new Date().getTime(); 22 + this.firstTimestamp = this.lastTimestamp - this.domain; 23 + 24 + this.resetData(); 25 + this.init(); 26 + const resize = this.resize.bind(this); 27 + d3.select(window).on('resize.updatesvg', throttle(resize, 200)); 28 + } 29 + 30 + updateData(epoch) { 31 + const { 32 + info: { 33 + samplingRate, 34 + startTime 35 + } 36 + } = epoch; 37 + this.lastTimestamp = startTime + epoch.data[0].length / (samplingRate / 1000); 38 + this.firstTimestamp = this.lastTimestamp - this.domain; 39 + for (let i = 0; i < this.channels.length; i++) { 40 + this.data[i] = this.data[i].concat(simplify(epoch.data[i].map((dataPoint, index) => ({ 41 + x: startTime + index / (samplingRate / 1000), 42 + y: dataPoint 43 + })), this.downsampling)).filter(sample => sample.x >= this.firstTimestamp); 44 + } 45 + 46 + this.channelColours = this.channels.map(channelName => epoch.signalQuality[channelName]); 47 + this.redraw(); 48 + } 49 + 50 + updateChannels(channels) { 51 + this.channels = channels; 52 + this.resetData(); 53 + this.init(); 54 + } 55 + 56 + updateDomain(domain) { 57 + this.domain = domain; 58 + this.resetData(); 59 + this.init(); 60 + } 61 + 62 + zoomIn() { 63 + this.zoom /= this.zoomScalar; 64 + this.redraw(); 65 + } 66 + 67 + zoomOut() { 68 + this.zoom *= this.zoomScalar; 69 + this.redraw(); 70 + } 71 + 72 + autoScale() { 73 + this.channelMaxs = this.data.map(channelData => EEGViewer.findExtreme(channelData, (a, b) => a > b)); 74 + this.channelMins = this.data.map(channelData => EEGViewer.findExtreme(channelData, (a, b) => a < b)); 75 + this.zoom = 1; 76 + this.redraw(); 77 + } 78 + 79 + resetData() { 80 + this.data = new Array(this.channels.length).fill([{ x: this.lastTimestamp, y: 0 }]); 81 + this.channelMaxs = new Array(this.channels.length).fill(100); 82 + this.channelMins = new Array(this.channels.length).fill(-100); 83 + } 84 + 85 + init() { 86 + try { 87 + d3.selectAll('svg > *').remove(); 88 + this.width = parseInt(d3.select('#graph').style('width'), 10) - (this.margin.left + this.margin.right); 89 + this.height = parseInt(d3.select('#graph').style('height'), 10) - (this.margin.top + this.margin.bottom); 90 + this.graph = this.canvas.attr('width', this.width).attr('height', this.height).append('g').attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')'); 91 + this.addScales(); 92 + this.addAxes(); 93 + this.addLines(); 94 + } catch (e) { 95 + console.error(e); 96 + } 97 + } 98 + 99 + addScales() { 100 + this.xScale = d3.scaleTime().domain([this.lastTimestamp, this.firstTimestamp + this.plottingInterval]).range([this.width, 0]); 101 + 102 + this.yScaleLines = d3.scaleLinear(); 103 + 104 + this.yScaleLabels = d3.scaleLinear().domain([this.channels.length - 1, 0]).range([(this.channels.length - 1) * (this.height / this.channels.length) + this.height / this.channels.length / 2, this.height / this.channels.length / 2]); 105 + } 106 + 107 + addAxes() { 108 + this.yAxis = d3.axisLeft().scale(this.yScaleLabels).tickSize(2).tickFormat((d, i) => this.channels[i].replace(/\s/g, '')).tickValues(d3.range(this.channels.length)); 109 + 110 + this.axisY = this.graph.append('g').attr('class', 'axis').call(this.yAxis); 111 + } 112 + 113 + addLines() { 114 + this.graph.selectAll('.legend').remove(); 115 + this.graph.select('#lines').remove(); 116 + this.graph.selectAll('#clip').remove(); 117 + this.paths = []; 118 + this.graph.append('clipPath').attr('id', 'clip').append('rect').attr('height', this.height + this.height / this.channels.length).attr('width', this.width).attr('transform', 'translate(0,' + -this.height / this.channels.length + ')'); 119 + this.lines = this.graph.append('g').attr('id', 'lines').attr('clip-path', 'url(#clip)'); 120 + this.line = d3.line().x(d => this.xScale(d.x)).y(d => this.yScaleLines(d.y)).curve(d3.curveLinear).defined(d => d.y); 121 + 122 + for (let i = 0; i < this.channels.length; i++) { 123 + const channelData = this.data[i]; 124 + 125 + this.yScaleLines.domain([this.channelMins[i] / this.zoom, this.channelMaxs[i] / this.zoom]).range(EEGViewer.getLineRange(i, this.channels.length, this.height)); 126 + 127 + this.paths[i] = this.lines.append('path').data([channelData]).attr('class', 'line').attr('id', 'line' + i + 1).attr('d', this.line).attr('stroke', this.channelColours[i]).attr('stroke-width', this.lineWidth); 128 + } 129 + } 130 + 131 + redraw() { 132 + if (this.paths != null) { 133 + for (let i = 0; i < this.channels.length; i++) { 134 + const channelData = this.data[i]; 135 + this.yScaleLines.domain([this.channelMins[i] / this.zoom, this.channelMaxs[i] / this.zoom]).range(EEGViewer.getLineRange(i, this.channels.length, this.height)); 136 + 137 + this.paths[i].interrupt().transition().duration(this.plottingInterval).ease(d3.easeLinear).attr('transform', 'translate(' + -(this.xScale(channelData[channelData.length - 1].x) - this.width) + ', 0)').attr('stroke', this.channelColours[i]); 138 + 139 + this.paths[i].data([channelData]).attr('d', this.line).attr('transform', 'translate(0,0)'); 140 + } 141 + 142 + this.xScale.domain([this.lastTimestamp, this.firstTimestamp + this.plottingInterval]).range([this.width, 0]); 143 + } 144 + } 145 + 146 + resize() { 147 + if (this.paths != null) { 148 + this.width = parseInt(d3.select('#graph').style('width'), 10) - (this.margin.left + this.margin.right); 149 + this.height = parseInt(d3.select('#graph').style('height'), 10) - (this.margin.top + this.margin.bottom); 150 + this.lines.attr('height', this.height).attr('width', this.width); 151 + this.xScale.range([this.width, 0]); 152 + this.yScaleLines.range([this.height, 0]); 153 + this.yScaleLabels.range([(this.channels.length - 1) * (this.height / this.channels.length) + this.height / this.channels.length / 2, this.height / this.channels.length / 2]); 154 + this.axisY.call(this.yAxis); 155 + this.addLines(); 156 + } 157 + } 158 + 159 + static findExtreme(data, comparison) { 160 + return data.slice(data.slice(data.length / 2)).reduce((acc, curr) => comparison(curr.y, acc) ? curr.y : acc, data[0].y); 161 + } 162 + 163 + static getLineRange(index, nbChannels, height) { 164 + return [(index + 1) * (height / nbChannels), index * (height / nbChannels)]; 165 + } 166 + } 167 + 168 + module.exports = EEGViewer;
-421
app/components/svgs/ClickableHeadDiagramSVG.js
··· 1 - import React from 'react'; 2 - 3 - interface Props { 4 - channelinfo: Array<string>; 5 - onChannelClick: (string) => void; 6 - } 7 - 8 - const SvgComponent = (props: Props) => ( 9 - <svg 10 - id='SignalQualityIndicator' 11 - data-name='SignalQualityIndicator' 12 - style={{ minHeight: 250, minWidth: 250 }} 13 - viewBox='0 0 674.44 610.29' 14 - {...props} 15 - > 16 - <title>Signal Quality Indicator</title> 17 - <g id='Head_Plot' data-name='Head Plot'> 18 - <circle 19 - cx={336.54} 20 - cy={334.96} 21 - r={212.53} 22 - fillOpacity='0.0' 23 - stroke='#000' 24 - strokeLinecap='round' 25 - strokeMiterlimit={10} 26 - strokeWidth={2} 27 - strokeDasharray='16.064828872680664,22.089139938354492' 28 - /> 29 - <path 30 - stroke='#000' 31 - strokeLinecap='round' 32 - strokeMiterlimit={10} 33 - strokeWidth={2} 34 - d='M68.73 334h8' 35 - /> 36 - <path 37 - stroke='#000' 38 - strokeLinecap='round' 39 - strokeMiterlimit={10} 40 - strokeWidth={2} 41 - strokeDasharray='16.026018142700195,22.035776138305664' 42 - d='M98.76 334h483.79' 43 - /> 44 - <path 45 - stroke='#000' 46 - strokeLinecap='round' 47 - strokeMiterlimit={10} 48 - strokeWidth={2} 49 - d='M593.57 334h8M335.68 600.42v-8' 50 - /> 51 - <path 52 - fillOpacity='0.0' 53 - stroke='#000' 54 - strokeLinecap='round' 55 - strokeMiterlimit={10} 56 - strokeWidth={2} 57 - strokeDasharray='16.026018142700195,22.035776138305664' 58 - d='M335.68 570.39V86.6' 59 - /> 60 - <path 61 - fillOpacity='0.0' 62 - stroke='#000' 63 - strokeLinecap='round' 64 - strokeMiterlimit={10} 65 - strokeWidth={2} 66 - d='M335.68 75.58v-8' 67 - /> 68 - <path 69 - d='M282.85 72.63s24.95-42.38 51.95-42.38 51.95 42.38 51.95 42.38' 70 - fillOpacity='0.0' 71 - stroke='#000' 72 - strokeMiterlimit={10} 73 - strokeWidth={2} 74 - /> 75 - <circle 76 - cx={335.15} 77 - cy={334} 78 - r={266.42} 79 - fillOpacity='0.0' 80 - stroke='#000' 81 - strokeMiterlimit={10} 82 - strokeWidth={2} 83 - /> 84 - <path 85 - d='M73.11 288.57s-30 1.44-30 42.92S72.1 373 72.1 373M598 288.57s30 1.44 30 42.92S599 373 599 373' 86 - fillOpacity='0.0' 87 - stroke='#000' 88 - strokeMiterlimit={10} 89 - strokeWidth={2} 90 - /> 91 - </g> 92 - <g 93 - className='channelCircle' 94 - id='T7' 95 - visibility={props.channelinfo.includes('T7') ? 'show' : 'hidden'} 96 - onClick={() => props.onChannelClick('T7')} 97 - > 98 - <circle cx={124.37} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 99 - <text 100 - transform='translate(111.44 341.22)' 101 - fontSize={22} 102 - fontFamily='Lato-Bold,Lato' 103 - fontWeight={700} 104 - fill='#000' 105 - > 106 - T7 107 - </text> 108 - </g> 109 - <g 110 - className='channelCircle' 111 - id='FC5' 112 - onClick={() => props.onChannelClick('FC5')} 113 - visibility={props.channelinfo.includes('FC5') ? 'show' : 'hidden'} 114 - > 115 - <circle cx={178.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 116 - <text 117 - transform='translate(158.65 266.78)' 118 - fontSize={22} 119 - fontFamily='Lato-Bold,Lato' 120 - fontWeight={700} 121 - fill='#000' 122 - > 123 - FC5 124 - </text> 125 - </g> 126 - <g 127 - className='channelCircle' 128 - id='FC6' 129 - onClick={() => props.onChannelClick('FC6')} 130 - visibility={props.channelinfo.includes('FC6') ? 'show' : 'hidden'} 131 - > 132 - <circle cx={495.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 133 - <text 134 - transform='translate(475.65 266.78)' 135 - fontSize={22} 136 - fontFamily='Lato-Bold,Lato' 137 - fontWeight={700} 138 - fill='#000' 139 - > 140 - FC6 141 - </text> 142 - </g> 143 - <g 144 - className='channelCircle' 145 - id='F3' 146 - onClick={() => props.onChannelClick('F3')} 147 - visibility={props.channelinfo.includes('F3') ? 'show' : 'hidden'} 148 - > 149 - <circle cx={240.35} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 150 - <text 151 - transform='translate(227.8 248.5)' 152 - fontSize={22} 153 - fontFamily='Lato-Bold,Lato' 154 - fontWeight={700} 155 - fill='#000' 156 - > 157 - F3 158 - </text> 159 - </g> 160 - <g 161 - className='channelCircle' 162 - id='F4' 163 - onClick={() => props.onChannelClick('F4')} 164 - visibility={props.channelinfo.includes('F4') ? 'show' : 'hidden'} 165 - > 166 - <circle cx={434.23} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 167 - <text 168 - transform='translate(421.68 248.5)' 169 - fontSize={22} 170 - fontFamily='Lato-Bold,Lato' 171 - fontWeight={700} 172 - fill='#000' 173 - > 174 - F4 175 - </text> 176 - </g> 177 - <g 178 - className='channelCircle' 179 - id='AF3' 180 - onClick={() => props.onChannelClick('AF3')} 181 - visibility={props.channelinfo.includes('AF3') ? 'show' : 'hidden'} 182 - > 183 - <circle cx={269.35} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 184 - <text 185 - transform='translate(248.99 193.18)' 186 - fontSize={22} 187 - fontFamily='Lato-Bold,Lato' 188 - fontWeight={700} 189 - fill='#000' 190 - > 191 - AF3 192 - </text> 193 - </g> 194 - <g 195 - className='channelCircle' 196 - id='AF4' 197 - onClick={() => props.onChannelClick('AF4')} 198 - visibility={props.channelinfo.includes('AF4') ? 'show' : 'hidden'} 199 - > 200 - <circle cx={406.36} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 201 - <text 202 - transform='translate(385.99 193.18)' 203 - fontSize={22} 204 - fontFamily='Lato-Bold,Lato' 205 - fontWeight={700} 206 - fill='#000' 207 - > 208 - AF4 209 - </text> 210 - </g> 211 - <g 212 - className='channelCircle' 213 - id='M1' 214 - onClick={() => props.onChannelClick('M1')} 215 - visibility={props.channelinfo.includes('M1') ? 'show' : 'hidden'} 216 - > 217 - <circle cx={78.53} cy={401.34} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 218 - <text 219 - transform='translate(61.92 408.83)' 220 - fontSize={22} 221 - fontFamily='Lato-Bold,Lato' 222 - fontWeight={700} 223 - fill='#000' 224 - > 225 - M1 226 - </text> 227 - </g> 228 - <g 229 - className='channelCircle' 230 - id='P7' 231 - onClick={() => props.onChannelClick('P7')} 232 - visibility={props.channelinfo.includes('P7') ? 'show' : 'hidden'} 233 - > 234 - <circle cx={178.58} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 235 - <text 236 - transform='translate(165.33 483.4)' 237 - fontSize={22} 238 - fontFamily='Lato-Bold,Lato' 239 - fontWeight={700} 240 - fill='#000' 241 - > 242 - P7 243 - </text> 244 - </g> 245 - <g 246 - className='channelCircle' 247 - id='O1' 248 - onClick={() => props.onChannelClick('O1')} 249 - visibility={props.channelinfo.includes('O1') ? 'show' : 'hidden'} 250 - > 251 - <circle cx={255.35} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 252 - <text 253 - transform='translate(240.18 538.79)' 254 - fontSize={22} 255 - fontFamily='Lato-Bold,Lato' 256 - fontWeight={700} 257 - fill='#000' 258 - > 259 - O1 260 - </text> 261 - </g> 262 - <g 263 - className='channelCircle' 264 - id='O2' 265 - onClick={() => props.onChannelClick('O2')} 266 - visibility={props.channelinfo.includes('O2') ? 'show' : 'hidden'} 267 - > 268 - <circle cx={420.11} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 269 - <text 270 - transform='translate(404.94 538.79)' 271 - fontSize={22} 272 - fontFamily='Lato-Bold,Lato' 273 - fontWeight={700} 274 - fill='#000' 275 - > 276 - O2 277 - </text> 278 - </g> 279 - <g 280 - className='channelCircle' 281 - id='P8' 282 - onClick={() => props.onChannelClick('P8')} 283 - visibility={props.channelinfo.includes('P8') ? 'show' : 'hidden'} 284 - > 285 - <circle cx={494.77} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 286 - <text 287 - transform='translate(481.51 483.4)' 288 - fontSize={22} 289 - fontFamily='Lato-Bold,Lato' 290 - fontWeight={700} 291 - fill='#000' 292 - > 293 - P8 294 - </text> 295 - </g> 296 - <g 297 - className='channelCircle' 298 - id='T8' 299 - onClick={() => props.onChannelClick('T8')} 300 - visibility={props.channelinfo.includes('T8') ? 'show' : 'hidden'} 301 - > 302 - <circle cx={548.92} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 303 - <text 304 - transform='translate(535.99 341.22)' 305 - fontSize={22} 306 - fontFamily='Lato-Bold,Lato' 307 - fontWeight={700} 308 - fill='#000' 309 - > 310 - T8 311 - </text> 312 - </g> 313 - <g 314 - className='channelCircle' 315 - id='M2' 316 - onClick={() => props.onChannelClick('M2')} 317 - visibility={props.channelinfo.includes('M2') ? 'show' : 'hidden'} 318 - > 319 - <circle cx={592.53} cy={400.89} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 320 - <text 321 - transform='translate(575.92 408.38)' 322 - fontSize={22} 323 - fontFamily='Lato-Bold,Lato' 324 - fontWeight={700} 325 - fill='#000' 326 - > 327 - M2 328 - </text> 329 - </g> 330 - <g 331 - className='channelCircle' 332 - id='TP10' 333 - onClick={() => props.onChannelClick('TP10')} 334 - visibility={props.channelinfo.includes('TP10') ? 'show' : 'hidden'} 335 - > 336 - <circle cx={571.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 337 - <text 338 - transform='translate(550.45 463.3)' 339 - fontSize={18} 340 - fontFamily='Lato-Bold,Lato' 341 - fontWeight={700} 342 - fill='#000' 343 - > 344 - TP10 345 - </text> 346 - </g> 347 - <g 348 - className='channelCircle' 349 - id='Fpz' 350 - onClick={() => props.onChannelClick('Fpz')} 351 - visibility={props.channelinfo.includes('Fpz') ? 'show' : 'hidden'} 352 - > 353 - <circle cx={335.79} cy={121.75} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 354 - <text 355 - transform='translate(318.56 129.24)' 356 - fontSize={22} 357 - fontFamily='Lato-Bold,Lato' 358 - fontWeight={700} 359 - fill='#000' 360 - > 361 - <tspan letterSpacing='-.03em'>F</tspan> 362 - <tspan x={11.69} y={0}> 363 - pz 364 - </tspan> 365 - </text> 366 - </g> 367 - <g 368 - className='channelCircle' 369 - id='TP9' 370 - onClick={() => props.onChannelClick('TP9')} 371 - visibility={props.channelinfo.includes('TP9') ? 'show' : 'hidden'} 372 - > 373 - <circle cx={98.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 374 - <text 375 - transform='translate(79.07 463.3)' 376 - fontSize={22} 377 - fontFamily='Lato-Bold,Lato' 378 - fontWeight={700} 379 - fill='#000' 380 - > 381 - TP9 382 - </text> 383 - </g> 384 - <g 385 - className='channelCircle' 386 - id='AF7' 387 - onClick={() => props.onChannelClick('AF7')} 388 - visibility={props.channelinfo.includes('AF7') ? 'show' : 'hidden'} 389 - > 390 - <circle cx={208.33} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 391 - <text 392 - transform='translate(187.97 173.57)' 393 - fontSize={22} 394 - fontFamily='Lato-Bold,Lato' 395 - fontWeight={700} 396 - fill='#000' 397 - > 398 - AF7 399 - </text> 400 - </g> 401 - <g 402 - className='channelCircle' 403 - id='AF8' 404 - onClick={() => props.onChannelClick('AF8')} 405 - visibility={props.channelinfo.includes('AF8') ? 'show' : 'hidden'} 406 - > 407 - <circle cx={467.66} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 408 - <text 409 - transform='translate(447.3 173.57)' 410 - fontSize={22} 411 - fontFamily='Lato-Bold,Lato' 412 - fontWeight={700} 413 - fill='#000' 414 - > 415 - AF8 416 - </text> 417 - </g> 418 - </svg> 419 - ); 420 - 421 - export default SvgComponent;
+140
app/components/svgs/ClickableHeadDiagramSVG.tsx
··· 1 + import React from "react"; 2 + 3 + interface Props { 4 + channelinfo: Array<string>; 5 + onChannelClick: (arg0: string) => void; 6 + } 7 + 8 + const SvgComponent = (props: Props) => <svg id='SignalQualityIndicator' data-name='SignalQualityIndicator' style={{ minHeight: 250, minWidth: 250 }} viewBox='0 0 674.44 610.29' {...props}> 9 + <title>Signal Quality Indicator</title> 10 + <g id='Head_Plot' data-name='Head Plot'> 11 + <circle cx={336.54} cy={334.96} r={212.53} fillOpacity='0.0' stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} strokeDasharray='16.064828872680664,22.089139938354492' /> 12 + <path stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} d='M68.73 334h8' /> 13 + <path stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} strokeDasharray='16.026018142700195,22.035776138305664' d='M98.76 334h483.79' /> 14 + <path stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} d='M593.57 334h8M335.68 600.42v-8' /> 15 + <path fillOpacity='0.0' stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} strokeDasharray='16.026018142700195,22.035776138305664' d='M335.68 570.39V86.6' /> 16 + <path fillOpacity='0.0' stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} d='M335.68 75.58v-8' /> 17 + <path d='M282.85 72.63s24.95-42.38 51.95-42.38 51.95 42.38 51.95 42.38' fillOpacity='0.0' stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 18 + <circle cx={335.15} cy={334} r={266.42} fillOpacity='0.0' stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 19 + <path d='M73.11 288.57s-30 1.44-30 42.92S72.1 373 72.1 373M598 288.57s30 1.44 30 42.92S599 373 599 373' fillOpacity='0.0' stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 20 + </g> 21 + <g className='channelCircle' id='T7' visibility={props.channelinfo.includes('T7') ? 'show' : 'hidden'} onClick={() => props.onChannelClick('T7')}> 22 + <circle cx={124.37} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 23 + <text transform='translate(111.44 341.22)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 24 + T7 25 + </text> 26 + </g> 27 + <g className='channelCircle' id='FC5' onClick={() => props.onChannelClick('FC5')} visibility={props.channelinfo.includes('FC5') ? 'show' : 'hidden'}> 28 + <circle cx={178.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 29 + <text transform='translate(158.65 266.78)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 30 + FC5 31 + </text> 32 + </g> 33 + <g className='channelCircle' id='FC6' onClick={() => props.onChannelClick('FC6')} visibility={props.channelinfo.includes('FC6') ? 'show' : 'hidden'}> 34 + <circle cx={495.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 35 + <text transform='translate(475.65 266.78)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 36 + FC6 37 + </text> 38 + </g> 39 + <g className='channelCircle' id='F3' onClick={() => props.onChannelClick('F3')} visibility={props.channelinfo.includes('F3') ? 'show' : 'hidden'}> 40 + <circle cx={240.35} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 41 + <text transform='translate(227.8 248.5)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 42 + F3 43 + </text> 44 + </g> 45 + <g className='channelCircle' id='F4' onClick={() => props.onChannelClick('F4')} visibility={props.channelinfo.includes('F4') ? 'show' : 'hidden'}> 46 + <circle cx={434.23} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 47 + <text transform='translate(421.68 248.5)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 48 + F4 49 + </text> 50 + </g> 51 + <g className='channelCircle' id='AF3' onClick={() => props.onChannelClick('AF3')} visibility={props.channelinfo.includes('AF3') ? 'show' : 'hidden'}> 52 + <circle cx={269.35} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 53 + <text transform='translate(248.99 193.18)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 54 + AF3 55 + </text> 56 + </g> 57 + <g className='channelCircle' id='AF4' onClick={() => props.onChannelClick('AF4')} visibility={props.channelinfo.includes('AF4') ? 'show' : 'hidden'}> 58 + <circle cx={406.36} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 59 + <text transform='translate(385.99 193.18)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 60 + AF4 61 + </text> 62 + </g> 63 + <g className='channelCircle' id='M1' onClick={() => props.onChannelClick('M1')} visibility={props.channelinfo.includes('M1') ? 'show' : 'hidden'}> 64 + <circle cx={78.53} cy={401.34} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 65 + <text transform='translate(61.92 408.83)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 66 + M1 67 + </text> 68 + </g> 69 + <g className='channelCircle' id='P7' onClick={() => props.onChannelClick('P7')} visibility={props.channelinfo.includes('P7') ? 'show' : 'hidden'}> 70 + <circle cx={178.58} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 71 + <text transform='translate(165.33 483.4)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 72 + P7 73 + </text> 74 + </g> 75 + <g className='channelCircle' id='O1' onClick={() => props.onChannelClick('O1')} visibility={props.channelinfo.includes('O1') ? 'show' : 'hidden'}> 76 + <circle cx={255.35} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 77 + <text transform='translate(240.18 538.79)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 78 + O1 79 + </text> 80 + </g> 81 + <g className='channelCircle' id='O2' onClick={() => props.onChannelClick('O2')} visibility={props.channelinfo.includes('O2') ? 'show' : 'hidden'}> 82 + <circle cx={420.11} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 83 + <text transform='translate(404.94 538.79)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 84 + O2 85 + </text> 86 + </g> 87 + <g className='channelCircle' id='P8' onClick={() => props.onChannelClick('P8')} visibility={props.channelinfo.includes('P8') ? 'show' : 'hidden'}> 88 + <circle cx={494.77} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 89 + <text transform='translate(481.51 483.4)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 90 + P8 91 + </text> 92 + </g> 93 + <g className='channelCircle' id='T8' onClick={() => props.onChannelClick('T8')} visibility={props.channelinfo.includes('T8') ? 'show' : 'hidden'}> 94 + <circle cx={548.92} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 95 + <text transform='translate(535.99 341.22)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 96 + T8 97 + </text> 98 + </g> 99 + <g className='channelCircle' id='M2' onClick={() => props.onChannelClick('M2')} visibility={props.channelinfo.includes('M2') ? 'show' : 'hidden'}> 100 + <circle cx={592.53} cy={400.89} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 101 + <text transform='translate(575.92 408.38)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 102 + M2 103 + </text> 104 + </g> 105 + <g className='channelCircle' id='TP10' onClick={() => props.onChannelClick('TP10')} visibility={props.channelinfo.includes('TP10') ? 'show' : 'hidden'}> 106 + <circle cx={571.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 107 + <text transform='translate(550.45 463.3)' fontSize={18} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 108 + TP10 109 + </text> 110 + </g> 111 + <g className='channelCircle' id='Fpz' onClick={() => props.onChannelClick('Fpz')} visibility={props.channelinfo.includes('Fpz') ? 'show' : 'hidden'}> 112 + <circle cx={335.79} cy={121.75} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 113 + <text transform='translate(318.56 129.24)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 114 + <tspan letterSpacing='-.03em'>F</tspan> 115 + <tspan x={11.69} y={0}> 116 + pz 117 + </tspan> 118 + </text> 119 + </g> 120 + <g className='channelCircle' id='TP9' onClick={() => props.onChannelClick('TP9')} visibility={props.channelinfo.includes('TP9') ? 'show' : 'hidden'}> 121 + <circle cx={98.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 122 + <text transform='translate(79.07 463.3)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 123 + TP9 124 + </text> 125 + </g> 126 + <g className='channelCircle' id='AF7' onClick={() => props.onChannelClick('AF7')} visibility={props.channelinfo.includes('AF7') ? 'show' : 'hidden'}> 127 + <circle cx={208.33} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 128 + <text transform='translate(187.97 173.57)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 129 + AF7 130 + </text> 131 + </g> 132 + <g className='channelCircle' id='AF8' onClick={() => props.onChannelClick('AF8')} visibility={props.channelinfo.includes('AF8') ? 'show' : 'hidden'}> 133 + <circle cx={467.66} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 134 + <text transform='translate(447.3 173.57)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 135 + AF8 136 + </text> 137 + </g> 138 + </svg>; 139 + 140 + export default SvgComponent;
-321
app/components/svgs/SignalQualityIndicatorSVG.js
··· 1 - import React from 'react'; 2 - 3 - const SvgComponent = (props) => ( 4 - <svg 5 - id='SignalQualityIndicator' 6 - data-name='SignalQualityIndicator' 7 - style={{ minHeight: 250, minWidth: 250 }} 8 - viewBox='0 0 674.44 610.29' 9 - {...props} 10 - > 11 - <title>Signal Quality Indicator</title> 12 - <g id='Head_Plot' data-name='Head Plot'> 13 - <circle 14 - cx={336.54} 15 - cy={334.96} 16 - r={212.53} 17 - fillOpacity='0.0' 18 - stroke='#000' 19 - strokeLinecap='round' 20 - strokeMiterlimit={10} 21 - strokeWidth={2} 22 - strokeDasharray='16.064828872680664,22.089139938354492' 23 - /> 24 - <path 25 - stroke='#000' 26 - strokeLinecap='round' 27 - strokeMiterlimit={10} 28 - strokeWidth={2} 29 - d='M68.73 334h8' 30 - /> 31 - <path 32 - stroke='#000' 33 - strokeLinecap='round' 34 - strokeMiterlimit={10} 35 - strokeWidth={2} 36 - strokeDasharray='16.026018142700195,22.035776138305664' 37 - d='M98.76 334h483.79' 38 - /> 39 - <path 40 - stroke='#000' 41 - strokeLinecap='round' 42 - strokeMiterlimit={10} 43 - strokeWidth={2} 44 - d='M593.57 334h8M335.68 600.42v-8' 45 - /> 46 - <path 47 - fillOpacity='0.0' 48 - stroke='#000' 49 - strokeLinecap='round' 50 - strokeMiterlimit={10} 51 - strokeWidth={2} 52 - strokeDasharray='16.026018142700195,22.035776138305664' 53 - d='M335.68 570.39V86.6' 54 - /> 55 - <path 56 - fillOpacity='0.0' 57 - stroke='#000' 58 - strokeLinecap='round' 59 - strokeMiterlimit={10} 60 - strokeWidth={2} 61 - d='M335.68 75.58v-8' 62 - /> 63 - <path 64 - d='M282.85 72.63s24.95-42.38 51.95-42.38 51.95 42.38 51.95 42.38' 65 - fillOpacity='0.0' 66 - stroke='#000' 67 - strokeMiterlimit={10} 68 - strokeWidth={2} 69 - /> 70 - <circle 71 - cx={335.15} 72 - cy={334} 73 - r={266.42} 74 - fillOpacity='0.0' 75 - stroke='#000' 76 - strokeMiterlimit={10} 77 - strokeWidth={2} 78 - /> 79 - <path 80 - d='M73.11 288.57s-30 1.44-30 42.92S72.1 373 72.1 373M598 288.57s30 1.44 30 42.92S599 373 599 373' 81 - fillOpacity='0.0' 82 - stroke='#000' 83 - strokeMiterlimit={10} 84 - strokeWidth={2} 85 - /> 86 - </g> 87 - <g id='T7' visibility='hidden'> 88 - <circle cx={124.37} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 89 - <text 90 - transform='translate(111.44 341.22)' 91 - fontSize={22} 92 - fontFamily='Lato-Bold,Lato' 93 - fontWeight={700} 94 - fill='#000' 95 - > 96 - T7 97 - </text> 98 - </g> 99 - <g id='FC5' visibility='hidden'> 100 - <circle cx={178.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 101 - <text 102 - transform='translate(158.65 266.78)' 103 - fontSize={22} 104 - fontFamily='Lato-Bold,Lato' 105 - fontWeight={700} 106 - fill='#000' 107 - > 108 - FC5 109 - </text> 110 - </g> 111 - <g id='FC6' visibility='hidden'> 112 - <circle cx={495.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 113 - <text 114 - transform='translate(475.65 266.78)' 115 - fontSize={22} 116 - fontFamily='Lato-Bold,Lato' 117 - fontWeight={700} 118 - fill='#000' 119 - > 120 - FC6 121 - </text> 122 - </g> 123 - <g id='F3' visibility='hidden'> 124 - <circle cx={240.35} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 125 - <text 126 - transform='translate(227.8 248.5)' 127 - fontSize={22} 128 - fontFamily='Lato-Bold,Lato' 129 - fontWeight={700} 130 - fill='#000' 131 - > 132 - F3 133 - </text> 134 - </g> 135 - <g id='F4' visibility='hidden'> 136 - <circle cx={434.23} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 137 - <text 138 - transform='translate(421.68 248.5)' 139 - fontSize={22} 140 - fontFamily='Lato-Bold,Lato' 141 - fontWeight={700} 142 - fill='#000' 143 - > 144 - F4 145 - </text> 146 - </g> 147 - <g id='AF3' visibility='hidden'> 148 - <circle cx={269.35} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 149 - <text 150 - transform='translate(248.99 193.18)' 151 - fontSize={22} 152 - fontFamily='Lato-Bold,Lato' 153 - fontWeight={700} 154 - fill='#000' 155 - > 156 - AF3 157 - </text> 158 - </g> 159 - <g id='AF4' visibility='hidden'> 160 - <circle cx={406.36} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 161 - <text 162 - transform='translate(385.99 193.18)' 163 - fontSize={22} 164 - fontFamily='Lato-Bold,Lato' 165 - fontWeight={700} 166 - fill='#000' 167 - > 168 - AF4 169 - </text> 170 - </g> 171 - <g id='M1' visibility='hidden'> 172 - <circle cx={78.53} cy={401.34} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 173 - <text 174 - transform='translate(61.92 408.83)' 175 - fontSize={22} 176 - fontFamily='Lato-Bold,Lato' 177 - fontWeight={700} 178 - fill='#000' 179 - > 180 - M1 181 - </text> 182 - </g> 183 - <g id='P7' visibility='hidden'> 184 - <circle cx={178.58} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 185 - <text 186 - transform='translate(165.33 483.4)' 187 - fontSize={22} 188 - fontFamily='Lato-Bold,Lato' 189 - fontWeight={700} 190 - fill='#000' 191 - > 192 - P7 193 - </text> 194 - </g> 195 - <g id='O1' visibility='hidden'> 196 - <circle cx={255.35} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 197 - <text 198 - transform='translate(240.18 538.79)' 199 - fontSize={22} 200 - fontFamily='Lato-Bold,Lato' 201 - fontWeight={700} 202 - fill='#000' 203 - > 204 - O1 205 - </text> 206 - </g> 207 - <g id='O2' visibility='hidden'> 208 - <circle cx={420.11} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 209 - <text 210 - transform='translate(404.94 538.79)' 211 - fontSize={22} 212 - fontFamily='Lato-Bold,Lato' 213 - fontWeight={700} 214 - fill='#000' 215 - > 216 - O2 217 - </text> 218 - </g> 219 - <g id='P8' visibility='hidden'> 220 - <circle cx={494.77} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 221 - <text 222 - transform='translate(481.51 483.4)' 223 - fontSize={22} 224 - fontFamily='Lato-Bold,Lato' 225 - fontWeight={700} 226 - fill='#000' 227 - > 228 - P8 229 - </text> 230 - </g> 231 - <g id='T8' visibility='hidden'> 232 - <circle cx={548.92} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 233 - <text 234 - transform='translate(535.99 341.22)' 235 - fontSize={22} 236 - fontFamily='Lato-Bold,Lato' 237 - fontWeight={700} 238 - fill='#000' 239 - > 240 - T8 241 - </text> 242 - </g> 243 - <g id='M2' visibility='hidden'> 244 - <circle cx={592.53} cy={400.89} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 245 - <text 246 - transform='translate(575.92 408.38)' 247 - fontSize={22} 248 - fontFamily='Lato-Bold,Lato' 249 - fontWeight={700} 250 - fill='#000' 251 - > 252 - M2 253 - </text> 254 - </g> 255 - <g id='TP10' visibility='hidden'> 256 - <circle cx={571.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 257 - <text 258 - transform='translate(550.45 463.3)' 259 - fontSize={18} 260 - fontFamily='Lato-Bold,Lato' 261 - fontWeight={700} 262 - fill='#000' 263 - > 264 - TP10 265 - </text> 266 - </g> 267 - <g id='Fpz' visibility='hidden'> 268 - <circle cx={335.79} cy={121.75} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 269 - <text 270 - transform='translate(318.56 129.24)' 271 - fontSize={22} 272 - fontFamily='Lato-Bold,Lato' 273 - fontWeight={700} 274 - fill='#000' 275 - > 276 - <tspan letterSpacing='-.03em'>F</tspan> 277 - <tspan x={11.69} y={0}> 278 - pz 279 - </tspan> 280 - </text> 281 - </g> 282 - <g id='TP9' visibility='hidden'> 283 - <circle cx={98.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 284 - <text 285 - transform='translate(79.07 463.3)' 286 - fontSize={22} 287 - fontFamily='Lato-Bold,Lato' 288 - fontWeight={700} 289 - fill='#000' 290 - > 291 - TP9 292 - </text> 293 - </g> 294 - <g id='AF7' visibility='hidden'> 295 - <circle cx={208.33} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 296 - <text 297 - transform='translate(187.97 173.57)' 298 - fontSize={22} 299 - fontFamily='Lato-Bold,Lato' 300 - fontWeight={700} 301 - fill='#000' 302 - > 303 - AF7 304 - </text> 305 - </g> 306 - <g id='AF8' visibility='hidden'> 307 - <circle cx={467.66} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 308 - <text 309 - transform='translate(447.3 173.57)' 310 - fontSize={22} 311 - fontFamily='Lato-Bold,Lato' 312 - fontWeight={700} 313 - fill='#000' 314 - > 315 - AF8 316 - </text> 317 - </g> 318 - </svg> 319 - ); 320 - 321 - export default SvgComponent;
+135
app/components/svgs/SignalQualityIndicatorSVG.tsx
··· 1 + import React from "react"; 2 + 3 + const SvgComponent = props => <svg id='SignalQualityIndicator' data-name='SignalQualityIndicator' style={{ minHeight: 250, minWidth: 250 }} viewBox='0 0 674.44 610.29' {...props}> 4 + <title>Signal Quality Indicator</title> 5 + <g id='Head_Plot' data-name='Head Plot'> 6 + <circle cx={336.54} cy={334.96} r={212.53} fillOpacity='0.0' stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} strokeDasharray='16.064828872680664,22.089139938354492' /> 7 + <path stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} d='M68.73 334h8' /> 8 + <path stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} strokeDasharray='16.026018142700195,22.035776138305664' d='M98.76 334h483.79' /> 9 + <path stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} d='M593.57 334h8M335.68 600.42v-8' /> 10 + <path fillOpacity='0.0' stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} strokeDasharray='16.026018142700195,22.035776138305664' d='M335.68 570.39V86.6' /> 11 + <path fillOpacity='0.0' stroke='#000' strokeLinecap='round' strokeMiterlimit={10} strokeWidth={2} d='M335.68 75.58v-8' /> 12 + <path d='M282.85 72.63s24.95-42.38 51.95-42.38 51.95 42.38 51.95 42.38' fillOpacity='0.0' stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 13 + <circle cx={335.15} cy={334} r={266.42} fillOpacity='0.0' stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 14 + <path d='M73.11 288.57s-30 1.44-30 42.92S72.1 373 72.1 373M598 288.57s30 1.44 30 42.92S599 373 599 373' fillOpacity='0.0' stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 15 + </g> 16 + <g id='T7' visibility='hidden'> 17 + <circle cx={124.37} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 18 + <text transform='translate(111.44 341.22)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 19 + T7 20 + </text> 21 + </g> 22 + <g id='FC5' visibility='hidden'> 23 + <circle cx={178.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 24 + <text transform='translate(158.65 266.78)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 25 + FC5 26 + </text> 27 + </g> 28 + <g id='FC6' visibility='hidden'> 29 + <circle cx={495.58} cy={259.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 30 + <text transform='translate(475.65 266.78)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 31 + FC6 32 + </text> 33 + </g> 34 + <g id='F3' visibility='hidden'> 35 + <circle cx={240.35} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 36 + <text transform='translate(227.8 248.5)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 37 + F3 38 + </text> 39 + </g> 40 + <g id='F4' visibility='hidden'> 41 + <circle cx={434.23} cy={241.01} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 42 + <text transform='translate(421.68 248.5)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 43 + F4 44 + </text> 45 + </g> 46 + <g id='AF3' visibility='hidden'> 47 + <circle cx={269.35} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 48 + <text transform='translate(248.99 193.18)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 49 + AF3 50 + </text> 51 + </g> 52 + <g id='AF4' visibility='hidden'> 53 + <circle cx={406.36} cy={185.69} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 54 + <text transform='translate(385.99 193.18)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 55 + AF4 56 + </text> 57 + </g> 58 + <g id='M1' visibility='hidden'> 59 + <circle cx={78.53} cy={401.34} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 60 + <text transform='translate(61.92 408.83)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 61 + M1 62 + </text> 63 + </g> 64 + <g id='P7' visibility='hidden'> 65 + <circle cx={178.58} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 66 + <text transform='translate(165.33 483.4)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 67 + P7 68 + </text> 69 + </g> 70 + <g id='O1' visibility='hidden'> 71 + <circle cx={255.35} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 72 + <text transform='translate(240.18 538.79)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 73 + O1 74 + </text> 75 + </g> 76 + <g id='O2' visibility='hidden'> 77 + <circle cx={420.11} cy={531.29} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 78 + <text transform='translate(404.94 538.79)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 79 + O2 80 + </text> 81 + </g> 82 + <g id='P8' visibility='hidden'> 83 + <circle cx={494.77} cy={475.91} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 84 + <text transform='translate(481.51 483.4)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 85 + P8 86 + </text> 87 + </g> 88 + <g id='T8' visibility='hidden'> 89 + <circle cx={548.92} cy={333.72} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 90 + <text transform='translate(535.99 341.22)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 91 + T8 92 + </text> 93 + </g> 94 + <g id='M2' visibility='hidden'> 95 + <circle cx={592.53} cy={400.89} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 96 + <text transform='translate(575.92 408.38)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 97 + M2 98 + </text> 99 + </g> 100 + <g id='TP10' visibility='hidden'> 101 + <circle cx={571.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 102 + <text transform='translate(550.45 463.3)' fontSize={18} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 103 + TP10 104 + </text> 105 + </g> 106 + <g id='Fpz' visibility='hidden'> 107 + <circle cx={335.79} cy={121.75} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 108 + <text transform='translate(318.56 129.24)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 109 + <tspan letterSpacing='-.03em'>F</tspan> 110 + <tspan x={11.69} y={0}> 111 + pz 112 + </tspan> 113 + </text> 114 + </g> 115 + <g id='TP9' visibility='hidden'> 116 + <circle cx={98.87} cy={455.81} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 117 + <text transform='translate(79.07 463.3)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 118 + TP9 119 + </text> 120 + </g> 121 + <g id='AF7' visibility='hidden'> 122 + <circle cx={208.33} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 123 + <text transform='translate(187.97 173.57)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 124 + AF7 125 + </text> 126 + </g> 127 + <g id='AF8' visibility='hidden'> 128 + <circle cx={467.66} cy={166.08} r={30} stroke='#000' strokeMiterlimit={10} strokeWidth={2} /> 129 + <text transform='translate(447.3 173.57)' fontSize={22} fontFamily='Lato-Bold,Lato' fontWeight={700} fill='#000'> 130 + AF8 131 + </text> 132 + </g> 133 + </svg>; 134 + 135 + export default SvgComponent;