diff --git a/app/actions/index.tsx b/app/actions/index.tsx index 1c8728d..8225074 100644 --- a/app/actions/index.tsx +++ b/app/actions/index.tsx @@ -1,3 +1,3 @@ export * from './experimentActions'; -export * from './jupyterActions'; +export * from './pyodideActions'; export * from './deviceActions'; diff --git a/app/constants/interfaces.js b/app/constants/interfaces.js deleted file mode 100644 index 455823a..0000000 --- a/app/constants/interfaces.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * This file contains all the custom types that we use for Flow type checking - */ - -import { EVENTS } from './constants'; - -// TODO: Write interfaces for device objects (Observables, Classes, etc) - -// ------------------------------------------------------------------ -// lab.js Experiment - -export type ExperimentParameters = { - trialDuration: number, - nbTrials: number, - iti: number, - jitter: number, - sampleType: string, - intro: string, - // Setting this to any prevents ridiculous flow runtime errors - showProgessBar: any, - stimulus1: { dir: string, type: EVENTS, title: string, response: string }, - stimulus2: { dir: string, type: EVENTS, title: string, response: string }, -}; - -export type ExperimentDescription = { - question: string, - hypothesis: string, - methods: string, -}; - -// Array of timeline and trial ids that will be presented in experiment -export type MainTimeline = Array; - -// jsPsych trial presented as part of an experiment -export interface Trial { - id: string; - type: string; - stimulus?: string | StimulusVariable; - trial_duration?: number | (() => number); - post_trial_gap?: number; - on_load?: (string) => void | StimulusVariable; - choices?: Array; -} - -// Timeline of jsPsych trials -export type Timeline = { - id: string, - timeline: Array, - sample?: SampleParameter, - timeline_variables?: Array, -}; - -export interface SampleParameter { - type: string; - size?: number; - fn?: () => Array; -} - -export type StimulusVariable = () => any; - -// -------------------------------------------------------------------- -// Device - -export interface EEGData { - data: Array; - timestamp: number; - marker?: string | number; -} - -export interface DeviceInfo { - name: string; - samplingRate: number; -} - -// -------------------------------------------------------------------- -// General - -export interface ActionType { - +payload: any; - +type: string; -} diff --git a/app/containers/HomeContainer.js b/app/containers/HomeContainer.js deleted file mode 100644 index aa57af8..0000000 --- a/app/containers/HomeContainer.js +++ /dev/null @@ -1,23 +0,0 @@ -// @flow -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import Home from '../components/HomeComponent'; -import * as deviceActions from '../actions/deviceActions'; -import * as pyodideActions from '../actions/pyodideActions'; -import * as experimentActions from '../actions/experimentActions'; - -function mapStateToProps(state) { - return { - availableDevices: state.device.availableDevices, - }; -} - -function mapDispatchToProps(dispatch) { - return { - deviceActions: bindActionCreators(deviceActions, dispatch), - pyodideActions: bindActionCreators(pyodideActions, dispatch), - experimentActions: bindActionCreators(experimentActions, dispatch), - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(Home); diff --git a/app/epics/jupyterEpics.ts b/app/epics/jupyterEpics.ts deleted file mode 100644 index 562505b..0000000 --- a/app/epics/jupyterEpics.ts +++ /dev/null @@ -1,407 +0,0 @@ -import { combineEpics, Epic } from 'redux-observable'; -import { from, of, ObservableInput } from 'rxjs'; -import { - map, - mergeMap, - tap, - pluck, - ignoreElements, - filter, - take, -} from 'rxjs/operators'; -import { find } from 'kernelspecs'; -import { launchSpec } from 'spawnteract'; -import { createMainChannel } from 'enchannel-zmq-backend'; -import { isNil } from 'lodash'; -import { kernelInfoRequest, executeRequest } from '@nteract/messaging'; -import { toast } from 'react-toastify'; -import { isActionOf } from '../utils/redux'; -import { PyodideActions, PyodideActionType } from '../actions'; -import { execute, awaitOkMessage } from '../utils/pyodide/pipes'; -import { RootState } from '../reducers'; -import { getWorkspaceDir } from '../utils/filesystem/storage'; -import { - imports, - utils, - loadCSV, - loadCleanedEpochs, - filterIIR, - epochEvents, - requestEpochsInfo, - requestChannelInfo, - cleanEpochsPlot, - plotPSD, - plotERP, - plotTopoMap, - saveEpochs, -} from '../utils/pyodide/cells'; -import { - EMOTIV_CHANNELS, - EVENTS, - DEVICES, - MUSE_CHANNELS, - PYODIDE_VARIABLE_NAMES, -} from '../constants/constants'; -import { - parseSingleQuoteJSON, - debugParseMessage, -} from '../utils/pyodide/functions'; - -// ------------------------------------------------------------------------- -// Epics - -const launchEpic: Epic = ( - action$ -) => - action$.pipe( - filter(isActionOf(PyodideActions.LaunchKernel)), - mergeMap(() => from(find('brainwaves'))), - tap((kernelInfo) => { - if (isNil(kernelInfo)) { - toast.error( - "Could not find 'brainwaves' jupyter kernel. Have you installed Python?" - ); - } - }), - filter((kernelInfo) => !isNil(kernelInfo)), - mergeMap>((kernelInfo) => - from( - launchSpec(kernelInfo.spec, { - // No STDIN, opt in to STDOUT and STDERR as node streams - stdio: ['ignore', 'pipe', 'pipe'], - }) - ) - ), - tap((kernel) => { - // Route everything that we won't get in messages to our own stdout - kernel.spawn.stdout.on('data', (data) => { - const text = data.toString(); - console.log('KERNEL STDOUT: ', text); - }); - kernel.spawn.stderr.on('data', (data) => { - const text = data.toString(); - console.log('KERNEL STDERR: ', text); - toast.error('Jupyter: ', text); - }); - - kernel.spawn.on('close', () => { - console.log('Kernel closed'); - }); - }), - map(JupyterActions.SetKernel) - ); - -const setUpChannelEpic: Epic< - JupyterActionType, - JupyterActionType, - RootState -> = (action$) => - action$.pipe( - filter(isActionOf(JupyterActions.SetKernel)), - pluck('payload'), - mergeMap((kernel) => from(createMainChannel(kernel.config))), - tap((mainChannel) => mainChannel.next(executeRequest(imports()))), - tap((mainChannel) => mainChannel.next(executeRequest(utils()))), - map(JupyterActions.SetMainChannel) - ); - -const receiveChannelMessageEpic: Epic< - JupyterActionType, - JupyterActionType, - RootState -> = (action$, state$) => - action$.pipe( - filter(isActionOf(JupyterActions.SetMainChannel)), - mergeMap, ObservableInput>(() => - state$.value.jupyter.mainChannel.pipe( - map<{ header: { msg_type: string } }, JupyterActionType>((msg) => { - console.log(debugParseMessage(msg)); - switch (msg.header.msg_type) { - case 'kernel_info_reply': - return JupyterActions.SetKernelInfo(msg); - case 'status': - return JupyterActions.SetKernelStatus(parseKernelStatus(msg)); - case 'stream': - return JupyterActions.ReceiveStream(msg); - case 'execute_reply': - return JupyterActions.ReceiveExecuteReply(msg); - case 'execute_result': - return JupyterActions.ReceiveExecuteResult(msg); - case 'display_data': - default: - return JupyterActions.ReceiveDisplayData(msg); - } - }) - ) - ) - ); - -const requestKernelInfoEpic: Epic< - JupyterActionType, - JupyterActionType, - RootState -> = (action$, state$) => - action$.pipe( - filter(isActionOf(JupyterActions.RequestKernelInfo)), - filter(() => state$.value.jupyter.mainChannel), - map(() => state$.value.jupyter.mainChannel.next(kernelInfoRequest())), - ignoreElements() - ); - -const loadEpochsEpic: Epic = ( - action$, - state$ -) => - // @ts-expect-error - action$.pipe( - filter(isActionOf(JupyterActions.LoadEpochs)), - pluck('payload'), - filter((filePathsArray) => filePathsArray.length >= 1), - map((filePathsArray) => - state$.value.jupyter.mainChannel.next( - executeRequest(loadCSV(filePathsArray)) - ) - ), - awaitOkMessage(action$), - execute(filterIIR(1, 30), state$), - awaitOkMessage(action$), - map(() => { - if (!state$.value.experiment.params?.stimuli) { - return {}; - } - - return epochEvents( - Object.fromEntries( - state$.value.experiment.params?.stimuli.map((stimulus, i) => [ - stimulus.title, - i, - ]) - ), - -0.1, - 0.8 - ); - }), - tap((e) => { - console.log('e', e); - }), - map((epochEventsCommand) => - state$.value.jupyter.mainChannel.next(executeRequest(epochEventsCommand)) - ), - awaitOkMessage(action$), - map(() => JupyterActions.GetEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) - ); - -const loadCleanedEpochsEpic: Epic< - JupyterActionType, - JupyterActionType, - RootState -> = (action$, state$) => - action$.pipe( - filter(isActionOf(JupyterActions.LoadCleanedEpochs)), - pluck('payload'), - filter((filePathsArray) => filePathsArray.length >= 1), - map((filePathsArray) => - state$.value.jupyter.mainChannel.next( - executeRequest(loadCleanedEpochs(filePathsArray)) - ) - ), - awaitOkMessage(action$), - mergeMap(() => - of( - JupyterActions.GetEpochsInfo(JUPYTER_VARIABLE_NAMES.CLEAN_EPOCHS), - JupyterActions.GetChannelInfo(), - JupyterActions.LoadTopo() - ) - ) - ); - -const cleanEpochsEpic: Epic = ( - action$, - state$ -) => - action$.pipe( - filter(isActionOf(JupyterActions.CleanEpochs)), - execute(cleanEpochsPlot(), state$), - mergeMap(() => - action$.ofType(JupyterActions.ReceiveStream.type).pipe( - pluck('payload'), - filter( - (msg) => - msg.channel === 'iopub' && - msg.content.text.includes('Channels marked as bad') - ), - take(1) - ) - ), - map(() => - state$.value.jupyter.mainChannel.next( - executeRequest( - saveEpochs( - getWorkspaceDir(state$.value.experiment.title!), - state$.value.experiment.subject - ) - ) - ) - ), - awaitOkMessage(action$), - map(() => JupyterActions.GetEpochsInfo(JUPYTER_VARIABLE_NAMES.RAW_EPOCHS)) - ); - -const getEpochsInfoEpic: Epic< - JupyterActionType, - JupyterActionType, - RootState -> = (action$, state$) => - action$.pipe( - filter(isActionOf(JupyterActions.GetEpochsInfo)), - pluck('payload'), - map((variableName) => - state$.value.jupyter.mainChannel.next( - executeRequest(requestEpochsInfo(variableName)) - ) - ), - mergeMap(() => - action$.ofType(JupyterActions.ReceiveExecuteReply.type).pipe( - pluck('payload'), - filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), - pluck('content', 'data', 'text/plain'), - filter((msg) => msg.includes('Drop Percentage')), - take(1) - ) - ), - map((epochInfoString) => - parseSingleQuoteJSON(epochInfoString).map((infoObj) => ({ - name: Object.keys(infoObj)[0], - value: infoObj[Object.keys(infoObj)[0]], - })) - ), - map(JupyterActions.SetEpochInfo) - ); - -const getChannelInfoEpic: Epic< - JupyterActionType, - JupyterActionType, - RootState -> = (action$, state$) => - action$.pipe( - filter(isActionOf(JupyterActions.GetChannelInfo)), - execute(requestChannelInfo(), state$), - mergeMap(() => - action$.ofType(JupyterActions.ReceiveExecuteResult.type).pipe( - pluck('payload'), - filter((msg) => msg.channel === 'iopub' && !isNil(msg.content.data)), - pluck('content', 'data', 'text/plain'), // Filter to prevent this from reading requestEpochsInfo returns - filter((msg) => !msg.includes('Drop Percentage')), - take(1) - ) - ), - map((channelInfoString) => - JupyterActions.SetChannelInfo(parseSingleQuoteJSON(channelInfoString)) - ) - ); - -const loadPSDEpic: Epic = ( - action$, - state$ -) => - action$.pipe( - filter(isActionOf(JupyterActions.LoadPSD)), - execute(plotPSD(), state$), - mergeMap(() => - action$.ofType(JupyterActions.ReceiveDisplayData.type).pipe( - pluck('payload'), // PSD graphs should have two axes - filter((msg) => msg.content.data['text/plain'].includes('2 Axes')), - pluck('content', 'data'), - take(1) - ) - ), - map(JupyterActions.SetPSDPlot) - ); - -const loadTopoEpic: Epic = ( - action$, - state$ -) => - action$.pipe( - filter(isActionOf(JupyterActions.LoadTopo)), - execute(plotTopoMap(), state$), - mergeMap(() => - action$ - .ofType(JupyterActions.ReceiveDisplayData.type) - .pipe(pluck('payload'), pluck('content', 'data'), take(1)) - ), - mergeMap((topoPlot) => - of( - JupyterActions.SetTopoPlot(topoPlot), - JupyterActions.LoadERP( - state$.value.device.deviceType === DEVICES.EMOTIV - ? EMOTIV_CHANNELS[0] - : MUSE_CHANNELS[0] - ) - ) - ) - ); - -const loadERPEpic: Epic = ( - action$, - state$ -) => - action$.pipe( - filter(isActionOf(JupyterActions.LoadERP)), - pluck('payload'), - map((channelName) => { - if (MUSE_CHANNELS.includes(channelName)) { - return MUSE_CHANNELS.indexOf(channelName); - } - if (EMOTIV_CHANNELS.includes(channelName)) { - return EMOTIV_CHANNELS.indexOf(channelName); - } - console.warn( - 'channel name supplied to loadERPEpic does not belong to either device' - ); - return EMOTIV_CHANNELS[0]; - }), - map((channelIndex) => - state$.value.jupyter.mainChannel.next( - executeRequest(plotERP(channelIndex)) - ) - ), - mergeMap(() => - action$.ofType(JupyterActions.ReceiveDisplayData.type).pipe( - pluck('payload'), // ERP graphs should have 1 axis according to MNE - filter((msg) => msg.content.data['text/plain'].includes('1 Axes')), - pluck('content', 'data'), - take(1) - ) - ), - map(JupyterActions.SetERPPlot) - ); - -const closeKernelEpic: Epic = ( - action$, - state$ -) => - action$.pipe( - filter(isActionOf(JupyterActions.CloseKernel)), - map(() => { - state$.value.jupyter.kernel?.spawn.kill(); - state$.value.jupyter.mainChannel.complete(); - }), - ignoreElements() - ); - -export default combineEpics( - launchEpic, - setUpChannelEpic, - requestKernelInfoEpic, - receiveChannelMessageEpic, - loadEpochsEpic, - loadCleanedEpochsEpic, - cleanEpochsEpic, - getEpochsInfoEpic, - getChannelInfoEpic, - loadPSDEpic, - loadTopoEpic, - loadERPEpic, - closeKernelEpic -); diff --git a/app/epics/pyodideEpics.js b/app/epics/pyodideEpics.js deleted file mode 100644 index 5d6e6d9..0000000 --- a/app/epics/pyodideEpics.js +++ /dev/null @@ -1,267 +0,0 @@ -import { combineEpics } from 'redux-observable'; -import { of, fromEvent } from 'rxjs'; -import { toast } from 'react-toastify'; -import { map, mergeMap, tap, pluck, filter } from 'rxjs/operators'; -import { getWorkspaceDir } from '../utils/filesystem/storage'; -import { parseSingleQuoteJSON } from '../utils/pyodide/functions'; -import { readFiles } from '../utils/filesystem/read'; -import { - LAUNCH, - LOAD_EPOCHS, - LOAD_CLEANED_EPOCHS, - LOAD_PSD, - LOAD_ERP, - LOAD_TOPO, - CLEAN_EPOCHS, - loadTopo, - loadERP, -} from '../actions/pyodideActions'; -import { - loadPyodide, - loadCSV, - loadCleanedEpochs, - filterIIR, - epochEvents, - requestEpochsInfo, - requestChannelInfo, - cleanEpochsPlot, - plotPSD, - plotERP, - plotTopoMap, - saveEpochs, -} from '../utils/pyodide'; -import { - EMOTIV_CHANNELS, - EVENTS, - DEVICES, - MUSE_CHANNELS, - PYODIDE_VARIABLE_NAMES, - PYODIDE_STATUS, -} from '../constants/constants'; - -export const GET_CHANNEL_INFO = 'GET_CHANNEL_INFO'; -export const GET_EPOCHS_INFO = 'GET_EPOCHS_INFO'; -export const RECEIVE_DISPLAY_DATA = 'RECEIVE_DISPLAY_DATA'; -export const RECEIVE_ERROR = 'RECEIVE_ERROR'; -export const RECEIVE_EXECUTE_REPLY = 'RECEIVE_EXECUTE_REPLY'; -export const RECEIVE_EXECUTE_RESULT = 'RECEIVE_EXECUTE_RESULT'; -export const RECEIVE_MESSAGE = 'RECEIVE_MESSAGE'; -export const RECEIVE_STREAM = 'RECEIVE_STREAM'; -export const SET_CHANNEL_INFO = 'SET_CHANNEL_INFO'; -export const SET_EPOCH_INFO = 'SET_EPOCH_INFO'; -export const SET_ERP_PLOT = 'SET_ERP_PLOT'; -export const SET_MAIN_CHANNEL = 'SET_MAIN_CHANNEL'; -export const SET_PSD_PLOT = 'SET_PSD_PLOT'; -export const SET_PYODIDE_STATUS = 'SET_PYODIDE_STATUS'; -export const SET_PYODIDE_WORKER = 'SET_PYODIDE_WORKER'; -export const SET_TOPO_PLOT = 'SET_TOPO_PLOT'; - -// ------------------------------------------------------------------------- -// Action Creators - -const getEpochsInfo = (payload) => ({ payload, type: GET_EPOCHS_INFO }); - -const getChannelInfo = () => ({ type: GET_CHANNEL_INFO }); - -const setEpochInfo = (payload) => ({ - payload, - type: SET_EPOCH_INFO, -}); - -const setChannelInfo = (payload) => ({ - payload, - type: SET_CHANNEL_INFO, -}); - -const setPSDPlot = (payload) => ({ - payload, - type: SET_PSD_PLOT, -}); - -const setTopoPlot = (payload) => ({ - payload, - type: SET_TOPO_PLOT, -}); - -const setERPPlot = (payload) => ({ - payload, - type: SET_ERP_PLOT, -}); - -const setPyodideStatus = (payload) => ({ - payload, - type: SET_PYODIDE_STATUS, -}); - -const setPyodideWorker = (payload: Worker) => ({ - payload, - type: SET_PYODIDE_WORKER, -}); - -const receivePyodideError = (payload) => ({ - payload, - type: RECEIVE_ERROR, -}); - -const receivePyodideMessage = (payload) => ({ - payload, - type: RECEIVE_MESSAGE, -}); - -// ------------------------------------------------------------------------- -// Epics - -const launchEpic = (action$) => - action$.ofType(LAUNCH).pipe( - tap(() => console.log('launching')), - map(loadPyodide), - map(setPyodideWorker) - ); - -const pyodideError = (action$) => - action$.ofType(SET_PYODIDE_WORKER).pipe( - pluck('payload'), - tap((e) => - toast.error( - `Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}` - ) - ), - map(receivePyodideError) - ); - -const pyodideMessage = (action$) => - action$.ofType(SET_PYODIDE_WORKER).pipe( - pluck('payload'), - tap((e) => { - const { results, error } = e.data; - if (results && !error) { - toast(`Pyodide: `, results); - } else if (error) { - toast.error('Pyodide: ', error); - } - }), - map(receivePyodideMessage) - ); - -const loadEpochsEpic = (action$, state$) => - action$.ofType(LOAD_EPOCHS).pipe( - pluck('payload'), - filter((filePathsArray) => filePathsArray.length >= 1), - tap((files) => console.log('files:', files)), - map((filePathsArray) => readFiles(filePathsArray)), - tap((csvArray) => console.log('csvs:', csvArray)), - mergeMap((csvArray) => loadCSV(csvArray)), - mergeMap(() => filterIIR(1, 30)), - mergeMap(() => - epochEvents( - { - [state$.value.experiment.params.stimulus1.title]: EVENTS.STIMULUS_1, - [state$.value.experiment.params.stimulus2.title]: EVENTS.STIMULUS_2, - [state$.value.experiment.params.stimulus3.title]: EVENTS.STIMULUS_3, - [state$.value.experiment.params.stimulus4.title]: EVENTS.STIMULUS_4, - }, - -0.1, - 0.8 - ) - ), - map(() => getEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) - ); - -const loadCleanedEpochsEpic = (action$) => - action$.ofType(LOAD_CLEANED_EPOCHS).pipe( - pluck('payload'), - filter((filePathsArray) => filePathsArray.length >= 1), - map((filePathsArray) => loadCleanedEpochs(filePathsArray)), - mergeMap(() => - of( - getEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), - getChannelInfo(), - loadTopo() - ) - ) - ); - -const cleanEpochsEpic = (action$, state$) => - action$.ofType(CLEAN_EPOCHS).pipe( - map(cleanEpochsPlot), - map(() => - saveEpochs( - getWorkspaceDir(state$.value.experiment.title), - state$.value.experiment.subject - ) - ), - map(() => getEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) - ); - -const getEpochsInfoEpic = (action$) => - action$.ofType(GET_EPOCHS_INFO).pipe( - pluck('payload'), - tap((payload) => console.log('payload: ', payload)), - mergeMap(requestEpochsInfo), - map((epochInfoArray) => - epochInfoArray.map((infoObj) => ({ - name: Object.keys(infoObj)[0], - value: infoObj[Object.keys(infoObj)[0]], - })) - ), - map(setEpochInfo) - ); - -const getChannelInfoEpic = (action$) => - action$.ofType(GET_CHANNEL_INFO).pipe( - map(requestChannelInfo), - map((channelInfoString) => - setChannelInfo(parseSingleQuoteJSON(channelInfoString)) - ) - ); - -const loadPSDEpic = (action$) => - action$.ofType(LOAD_PSD).pipe(map(plotPSD), map(setPSDPlot)); - -const loadTopoEpic = (action$, state$) => - action$.ofType(LOAD_TOPO).pipe( - map(plotTopoMap), - mergeMap((topoPlot) => - of( - setTopoPlot(topoPlot), - loadERP( - state$.value.device.deviceType === DEVICES.EMOTIV - ? EMOTIV_CHANNELS[0] - : MUSE_CHANNELS[0] - ) - ) - ) - ); - -const loadERPEpic = (action$) => - action$.ofType(LOAD_ERP).pipe( - pluck('payload'), - map((channelName) => { - if (MUSE_CHANNELS.includes(channelName)) { - return MUSE_CHANNELS.indexOf(channelName); - } - if (EMOTIV_CHANNELS.includes(channelName)) { - return EMOTIV_CHANNELS.indexOf(channelName); - } - console.warn( - 'channel name supplied to loadERPEpic does not belong to either device' - ); - return EMOTIV_CHANNELS[0]; - }), - map((channelIndex) => plotERP(channelIndex)), - map(setERPPlot) - ); - -export default combineEpics( - pyodideError, - pyodideMessage, - launchEpic, - loadEpochsEpic, - loadCleanedEpochsEpic, - cleanEpochsEpic, - getEpochsInfoEpic, - getChannelInfoEpic, - loadPSDEpic, - loadTopoEpic, - loadERPEpic -); diff --git a/app/epics/pyodideEpics.ts b/app/epics/pyodideEpics.ts new file mode 100644 index 0000000..23ff7b2 --- /dev/null +++ b/app/epics/pyodideEpics.ts @@ -0,0 +1,245 @@ +import { combineEpics, Epic } from 'redux-observable'; +import { of } from 'rxjs'; +import { map, mergeMap, tap, pluck, filter } from 'rxjs/operators'; +import { toast } from 'react-toastify'; +import { isActionOf } from '../utils/redux'; +import { PyodideActions, PyodideActionType } from '../actions'; +import { RootState } from '../reducers'; +import { getWorkspaceDir } from '../utils/filesystem/storage'; +import { + loadCSV, + loadCleanedEpochs, + filterIIR, + epochEvents, + requestEpochsInfo, + requestChannelInfo, + cleanEpochsPlot, + plotPSD, + plotERP, + plotTopoMap, + saveEpochs, + loadPyodide, +} from '../utils/pyodide'; +import { + EMOTIV_CHANNELS, + DEVICES, + MUSE_CHANNELS, + PYODIDE_VARIABLE_NAMES, +} from '../constants/constants'; +import { parseSingleQuoteJSON } from '../utils/pyodide/functions'; + +import { readFiles } from '../utils/filesystem/read'; + +// ------------------------------------------------------------------------- +// Epics + +const launchEpic: Epic = ( + action$ +) => + action$.pipe( + filter(isActionOf(PyodideActions.Launch)), + tap(() => console.log('launching')), + map(loadPyodide), + map(PyodideActions.SetPyodideWorker) + ); + +const pyodideError: Epic = ( + action$ +) => + action$.pipe( + filter(isActionOf(PyodideActions.SetPyodideWorker)), + pluck('payload'), + tap((e) => + toast.error( + `Error in pyodideWorker at ${e.filename}, Line: ${e.lineno}, ${e.message}` + ) + ), + map(PyodideActions.ReceiveError) + ); + +const receiveChannelMessageEpic: Epic< + PyodideActionType, + PyodideActionType, + RootState +> = (action$, state$) => + action$.pipe( + filter(isActionOf(PyodideActions.SetPyodideWorker)), + tap((e) => { + const { results, error } = e.data; + if (results && !error) { + toast(`Pyodide: `, results); + } else if (error) { + toast.error('Pyodide: ', error); + } + }), + map(PyodideActions.ReceiveMessage) + ); + +const loadEpochsEpic: Epic = ( + action$, + state$ +) => + action$.pipe( + filter(isActionOf(PyodideActions.LoadEpochs)), + pluck('payload'), + filter((filePathsArray: string[]) => filePathsArray.length >= 1), + map((filePathsArray) => readFiles(filePathsArray)), + mergeMap((csvArray) => loadCSV(csvArray)), + mergeMap(() => filterIIR(1, 30)), + map(() => { + if (!state$.value.experiment.params?.stimuli) { + return {}; + } + + return epochEvents( + Object.fromEntries( + state$.value.experiment.params?.stimuli.map((stimulus, i) => [ + stimulus.title, + i, + ]) + ), + -0.1, + 0.8 + ); + }), + tap((e) => { + console.log('epoched events: ', e); + }), + map(() => PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) + ); + +const loadCleanedEpochsEpic: Epic< + PyodideActionType, + PyodideActionType, + RootState +> = (action$) => + action$.pipe( + filter(isActionOf(PyodideActions.LoadCleanedEpochs)), + pluck('payload'), + filter((filePathsArray) => filePathsArray.length >= 1), + map(loadCleanedEpochs), + mergeMap(() => + of( + PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.CLEAN_EPOCHS), + PyodideActions.GetChannelInfo(), + PyodideActions.LoadTopo() + ) + ) + ); + +const cleanEpochsEpic: Epic = ( + action$, + state$ +) => + action$.pipe( + filter(isActionOf(PyodideActions.CleanEpochs)), + mergeMap(cleanEpochsPlot), + map(() => + saveEpochs( + getWorkspaceDir(state$.value.experiment.title), + state$.value.experiment.subject + ) + ), + map(() => PyodideActions.GetEpochsInfo(PYODIDE_VARIABLE_NAMES.RAW_EPOCHS)) + ); + +const getEpochsInfoEpic: Epic< + PyodideActionType, + PyodideActionType, + RootState +> = (action$, state$) => + action$.pipe( + filter(isActionOf(PyodideActions.GetEpochsInfo)), + pluck('payload'), + mergeMap(requestEpochsInfo), + map((epochInfoArray) => + epochInfoArray.map((infoObj) => ({ + name: Object.keys(infoObj)[0], + value: infoObj[Object.keys(infoObj)[0]], + })) + ), + map(PyodideActions.SetEpochInfo) + ); + +const getChannelInfoEpic: Epic< + PyodideActionType, + PyodideActionType, + RootState +> = (action$, state$) => + action$.pipe( + filter(isActionOf(PyodideActions.GetChannelInfo)), + mergeMap(requestChannelInfo), + map((channelInfoString) => + PyodideActions.SetChannelInfo(parseSingleQuoteJSON(channelInfoString)) + ) + ); + +const loadPSDEpic: Epic = ( + action$, + state$ +) => + action$.pipe( + filter(isActionOf(PyodideActions.LoadPSD)), + mergeMap(plotPSD), + map(PyodideActions.SetPSDPlot) + ); + +const loadTopoEpic: Epic = ( + action$, + state$ +) => + action$.pipe( + filter(isActionOf(PyodideActions.LoadTopo)), + mergeMap(plotTopoMap), + tap((e) => console.log('received topo map: ', e)), + mergeMap((topoPlot) => + of( + PyodideActions.SetTopoPlot(topoPlot), + PyodideActions.LoadERP( + state$.value.device.deviceType === DEVICES.EMOTIV + ? EMOTIV_CHANNELS[0] + : MUSE_CHANNELS[0] + ) + ) + ) + ); + +const loadERPEpic: Epic = ( + action$, + state$ +) => + action$.pipe( + filter(isActionOf(PyodideActions.LoadERP)), + pluck('payload'), + map((channelName: string) => { + let index: number | null = null; + if (MUSE_CHANNELS.includes(channelName)) { + index = MUSE_CHANNELS.indexOf(channelName); + } + if (EMOTIV_CHANNELS.includes(channelName)) { + index = EMOTIV_CHANNELS.indexOf(channelName); + } + if (index) { + return index; + } + console.warn( + 'channel name supplied to loadERPEpic does not belong to either device' + ); + return parseInt(EMOTIV_CHANNELS[0], 10); + }), + mergeMap(plotERP), + map(PyodideActions.SetERPPlot) + ); + +export default combineEpics( + launchEpic, + receiveChannelMessageEpic, + loadEpochsEpic, + loadCleanedEpochsEpic, + cleanEpochsEpic, + getEpochsInfoEpic, + getChannelInfoEpic, + loadPSDEpic, + loadTopoEpic, + loadERPEpic +);