From 249ffcea436261003727f138e7398fe6992ebe19 Mon Sep 17 00:00:00 2001 From: Will Engelhardt Date: Fri, 30 Aug 2024 13:52:51 -0500 Subject: [PATCH 1/3] General update to work with current dependencies and BCI2000. Improved theme (light/dark mode). Added optional capability to create paradigms or modify system states (status, participant name, amplifier) --- START.bat | 3 +- components/ConnectionState.tsx | 54 ----- .../ConnectionStatus/ConnectionState.tsx | 114 +++++++++++ .../ConnectionStatus/ParticipantButton.tsx | 57 ++++++ components/ConnectionStatus/SourceButton.tsx | 113 +++++++++++ components/ExperimentConfiguration.tsx | 44 +++++ components/NewParadigm/NewBlock.tsx | 38 ++++ components/NewParadigm/NewTask.tsx | 187 ++++++++++++++++++ components/NewParadigm/Parameters.tsx | 46 +++++ components/Notes.tsx | 2 +- components/Paradigms.tsx | 102 +++++++++- components/Tasks.tsx | 27 ++- components/Toolbox.tsx | 5 +- components/store.ts | 14 +- index.scss | 95 ++++++++- package.json | 6 +- pages/_app.js | 4 +- pages/api/paradigms/newParadigm.ts | 35 ++++ pages/api/saveLocalConfig.ts | 18 ++ pages/index.tsx | 69 +++---- server/config/amplifiers.json | 11 +- server/config/localconfig.json | 10 +- server/index.ts | 24 +-- 23 files changed, 911 insertions(+), 167 deletions(-) delete mode 100644 components/ConnectionState.tsx create mode 100644 components/ConnectionStatus/ConnectionState.tsx create mode 100644 components/ConnectionStatus/ParticipantButton.tsx create mode 100644 components/ConnectionStatus/SourceButton.tsx create mode 100644 components/ExperimentConfiguration.tsx create mode 100644 components/NewParadigm/NewBlock.tsx create mode 100644 components/NewParadigm/NewTask.tsx create mode 100644 components/NewParadigm/Parameters.tsx create mode 100644 pages/api/paradigms/newParadigm.ts create mode 100644 pages/api/saveLocalConfig.ts diff --git a/START.bat b/START.bat index a90b644..741e3b5 100644 --- a/START.bat +++ b/START.bat @@ -1,2 +1 @@ -npm run server -pause \ No newline at end of file +node -r esbuild-register server/index.ts \ No newline at end of file diff --git a/components/ConnectionState.tsx b/components/ConnectionState.tsx deleted file mode 100644 index 8cf7919..0000000 --- a/components/ConnectionState.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { ListGroupItem, ListGroup, Card } from "react-bootstrap"; -import { BCI2K_OperatorConnection } from "bci2k"; -import React, { useState, useEffect } from "react"; -import localConfig from "../server/config/localconfig.json"; -import { useStore } from "./store"; -const ConnectionState = () => { - const [connectionStatus, setConnectionStatus] = useState("Not Connected"); - - useEffect(() => { - (async () => { - let bci = useStore.getState().bci; - await bci.connect(`ws://localhost:3000`); - - bci.execute("Reset System"); - bci.stateListen(); - bci.onStateChange = (e) => setConnectionStatus(e); - })(); - }, []); - - return ( -
- - - -

System

-
-
- - - - {connectionStatus} - - - {useStore.getState().config.subject} - - - {useStore.getState().config.source} - - -
-
- ); -}; - -export default ConnectionState; diff --git a/components/ConnectionStatus/ConnectionState.tsx b/components/ConnectionStatus/ConnectionState.tsx new file mode 100644 index 0000000..541516e --- /dev/null +++ b/components/ConnectionStatus/ConnectionState.tsx @@ -0,0 +1,114 @@ +import { ListGroupItem, ListGroup, Card, Button, Row, Col } from "react-bootstrap"; +import React, { useState, useEffect } from "react"; +import localConfig from "../../server/config/localconfig.json"; +import { useStore } from "../store"; +import SourceButton from "./SourceButton"; +import ParticipantButton from "./ParticipantButton"; +const ConnectionState = () => { + const [connectionStatus, setConnectionStatus] = useState("Not Connected"); + const [subj, setSubj] = useState(useStore.getState().config.subject); + useStore.subscribe((state) => state.config.subject, (st) => setSubj(st)); + + const [amp, setAmp] = useState(useStore.getState().config.source); + useStore.subscribe((state) => state.config.source, (st) => setAmp(st)); + + //present buttons to use based on localconfig setting + const showButtons = useStore.getState().config.editable ? "flex" : "none"; + //const [vis, setVis] = useState(useStore.getState().config.editable); + + useEffect(() => { + (async () => { + let bci = useStore.getState().bci; + await bci.connect(`ws://localhost:3000`); + + //bci.execute("Reset System"); + console.log("Connected!") + bci.stateListen(); + bci.onStateChange = (e) => setConnectionStatus(e); + })(); + }, []); + + return ( +
+ + + +

System

+
+
+ + + + + Status: + + {connectionStatus} + + + + + + + + + Participant: + + {subj} + + + + + + + + + Amplifier: + + {amp} + + + + + + + +
+
+ ); +}; +const updateLocalConfig = async () => { + try { + const paradigmsReq = await fetch(`/api/saveLocalConfig`, {method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(useStore.getState().config)}); + //let pReq = await fetch("/api/paradigms/newGo"); + console.log("created json") + console.log(paradigmsReq); + const result = await paradigmsReq.json(); + console.log("success:", result); + } catch (error) { + console.error(`error: ${error}`); + } +} +const UpdateLocalConfig = () => { + return ( +
+ +
+ ); +} + +export { ConnectionState, updateLocalConfig }; diff --git a/components/ConnectionStatus/ParticipantButton.tsx b/components/ConnectionStatus/ParticipantButton.tsx new file mode 100644 index 0000000..913270a --- /dev/null +++ b/components/ConnectionStatus/ParticipantButton.tsx @@ -0,0 +1,57 @@ +import { Container, Card, ListGroupItem, Button, Modal, Form, FloatingLabel, Row, Col, ToggleButton, ToggleButtonGroup } from "react-bootstrap"; +import React, { useState, useEffect, useContext } from "react"; +import { useStore } from "../store"; +import { updateLocalConfig } from "./ConnectionState"; + +const SourceButton = (settings) => { + const [show, setShow] = useState(false); + const [subject, setParticipant] = useState(useStore.getState().config.subject); + + const handleClose = () => { + setShow(false); + } + const handleShow = async () => { + console.log(useStore.getState().config.subject); + setParticipant(useStore.getState().config.subject) + setShow(true); + } + const handleChange = (e) => { + let newSourceName = e.target.value; + setParticipant(newSourceName) + } + const handleSubmit = () => { + let newConfig = useStore.getState().config; + newConfig.subject = subject; + useStore.setState({config : newConfig}) + updateLocalConfig(); + setShow(false); + } + + return ( + <> + + + + Change your participant! + + +
+ + New participant name: + + +
+
+ + + + +
+ + ); +}; +export default SourceButton; diff --git a/components/ConnectionStatus/SourceButton.tsx b/components/ConnectionStatus/SourceButton.tsx new file mode 100644 index 0000000..ea1babe --- /dev/null +++ b/components/ConnectionStatus/SourceButton.tsx @@ -0,0 +1,113 @@ +import { Container, Card, ListGroupItem, Button, Modal, Form, FloatingLabel, Row, Col, ToggleButton, ToggleButtonGroup } from "react-bootstrap"; +import React, { useState, useEffect, useContext } from "react"; +import { useStore } from "../store"; +import { updateLocalConfig } from "./ConnectionState"; +//import fs from "fs"; +const ParameterDescription = (input) => { + const prm = input.prm; + const checkPrm = (prm) => { + //recursively print object + if (prm.constructor === ({}).constructor) { + return (<>) + } + else { + return (<>{prm}); + } + } + return ( + + ); +} + +const SourceButton = (settings) => { + //let setDispTxt = settings.dispSt; + const [show, setShow] = useState(false); + const [amps, setAmps] = useState({}); + const [source, setLocalSource] = useState(useStore.getState().config.source); + + const handleClose = () => { + setShow(false); + } + const handleShow = async () => { + let ampsReq = await fetch("/api/amplifiers"); + let ampsRes = await ampsReq.json(); + setAmps(ampsRes); + console.log(useStore.getState().config.source); + setLocalSource(useStore.getState().config.source) + setShow(true); + } + const handleClick = (e) => { + let newSourceName = e.target.innerHTML; + setLocalSource(newSourceName) + } + // const updateLocalConfig = async () => { + // try { + // const paradigmsReq = await fetch(`/api/saveLocalConfig`, {method: "POST", + // headers: { + // "Content-Type": "application/json", + // }, + // body: JSON.stringify(useStore.getState().config)}); + // //let pReq = await fetch("/api/paradigms/newGo"); + // console.log("created json") + // console.log(paradigmsReq); + // const result = await paradigmsReq.json(); + // console.log("success:", result); + // } catch (error) { + // console.error(`error: ${error}`); + // } + // } + const handleSubmit = () => { + let newConfig = useStore.getState().config; + newConfig.source = source; + useStore.setState({config : newConfig}) + updateLocalConfig(); + setShow(false); + } + + return ( + <> + + + + Choose from available amplifiers + + + + + + + {Array(Object.keys(amps).length) + .fill(0) + .map((x, idx) => ( + + {Object.keys(amps)[idx]} + + ))} + + + + + + + + + + + + + + + ); +}; +export default SourceButton; diff --git a/components/ExperimentConfiguration.tsx b/components/ExperimentConfiguration.tsx new file mode 100644 index 0000000..1d1dcd2 --- /dev/null +++ b/components/ExperimentConfiguration.tsx @@ -0,0 +1,44 @@ +import { useState, useContext, useEffect } from "react"; +import { Row, Col, Navbar, Container, Form, Button } from "react-bootstrap"; +import { ConnectionState } from "./ConnectionStatus/ConnectionState"; +import Toolbox from "./Toolbox"; +import { + ReplayParadigms, + ReplayTasks, +} from "./ToolboxComps/Replay"; +import Paradigms from "./Paradigms"; +import Tasks from "./Tasks"; +import { useStore } from "./store"; +import Notes from "./Notes"; + +export default function ExperimentConfiguration() { + + return ( + + + + + + + + {useStore.getState().replayMode ? ( + + ) : ( + + )} + + + {useStore.getState().replayMode && + useStore.getState().replayTask != null ? ( + + ) : ( + + )} + + + + + + + ); +} diff --git a/components/NewParadigm/NewBlock.tsx b/components/NewParadigm/NewBlock.tsx new file mode 100644 index 0000000..f791cbb --- /dev/null +++ b/components/NewParadigm/NewBlock.tsx @@ -0,0 +1,38 @@ +import { ListGroup, Card, ListGroupItem, Button, Modal, Form, FloatingLabel, Row, Col, Accordion } from "react-bootstrap"; +import React, { useState, useEffect, useContext } from "react"; +import Parameter from "./Parameters" + +const NewBlock = (data) => { + let task = data.pState[0] + let setNewTask = data.pState[1] + const [blockName, setBlockName] = useState(""); + const [prms, setPrms] = useState([]); + + const handlePrms = (e) => { + console.log(e.target.value) + prms.push(e.target.value); + setPrms(prms) + } + + const setParadigmState = () => { + //organize for task.json + let thisP = { + title: blockName, + loadParameters: prms} + task["Block_" + data.n] = thisP + setNewTask(task) + } + + return ( + + Block {data.n} + + + setBlockName(e.target.value)}/> + + + + + ); +}; +export default NewBlock; diff --git a/components/NewParadigm/NewTask.tsx b/components/NewParadigm/NewTask.tsx new file mode 100644 index 0000000..8981912 --- /dev/null +++ b/components/NewParadigm/NewTask.tsx @@ -0,0 +1,187 @@ +import { ListGroup, Card, InputGroup, Button, Modal, Form, FloatingLabel, Row, Col, Accordion } from "react-bootstrap"; +import React, { useState, useEffect, useContext } from "react"; +import NewBlock from "./NewBlock"; +import Parameter from "./Parameters"; +import { useStore } from "../store"; + +const NewTask = (data) => { + let pName = data.pName + let paradigm = data.pState[0] + let setNewParadigm = data.pState[1] + const [procEnabled, setProcMod] = useState(false); + const [procValid, setProcValid] = useState(true); + const [appEnabled, setAppMod] = useState(false); + const [appValid, setAppValid] = useState(true); + const [prms, setPrms] = useState([]); + //task control + const exeProc = "procID"; + const exeApp = "appID"; + let defaultTask = { + name: "", + description: "", + [exeProc]: "", + [exeApp]: "", + nBlocks: 1 + } + const [task, setTask] = useState(defaultTask); + //task name control + const handleTaskNameChange = async (e) => { + let t = e.target.value; + if (t.length === 0) + setTask({ + ...task, + name: "" + }); + else + setTask({ + ...task, + name: "(" + t + ")" + }); + + //setParadigmState(); + } + + const setParadigmState = () => { + //organize for task.json + let thisP = { + title: task.name.replace(new RegExp("\\(|\\)", "g"), ""), + description: task.description, + executables: { + processing: task[exeProc], + application: task[exeApp] + }, + setParameters: {}, + loadParameters: prms, + Blocks: newTask + } + paradigm["Task_" + data.n] = thisP + setNewParadigm(paradigm) + } + const handleFileInput = (e) => { + if (e.target.files.length != 0) { + let newFile = e.target.files[0].name + useStore.getState().bci.execute(`Categorize Executable ${newFile}`).then((value) => { + console.log(value); + let enabled = false; + switch (e.target.id) { + case exeProc: + if (value === 'SignalProcessing') { + enabled = true; + setProcValid(true); + console.log(newFile) + if (newFile === 'DummySignalProcessing.exe') { + setProcMod(false); + } + } + else { + setProcValid(false); + } + break; + case exeApp: + if (value === 'Application') { + enabled = true; + setAppValid(true); + if (newFile === 'DummyApplication.exe') { + setAppMod(false); + } + } + else { + setAppValid(false); + } + //setAppMod(enabled); + break; + } + if (enabled === true) { + setTask({...task, [e.target.id]: newFile}) + } + }); + } + else { + switch (e.target.id) { + case exeProc: + setProcMod(false); + break; + case exeApp: + setAppMod(false); + break; + } + } + } + + const handleDefaultModuleClick= (e) => { + console.log(e) + } + + const [newTask, setNewTask] = useState({}); + + return ( + + Task {data.n} {task.name} + +
+ + handleTaskNameChange(e)}/> + + + + setTask({...task, description: e.target.value})} /> + + + + Signal Processing Module + + setProcMod(!procEnabled)} + //defaultChecked + checked={!procEnabled} + /> + + + + + Please choose a Signal Processing module. + + + + + + Application Module + + setAppMod(!appEnabled)} + onChange={(e) => setAppMod(!appEnabled)} + //defaultChecked + checked={!appEnabled} + /> + + + + + Please choose an Application module. + + + + + + {Array(task.nBlocks) + .fill(0) + .map((x, idx) => ( + + ))} + + + +
+
+ ); +}; +export default NewTask; diff --git a/components/NewParadigm/Parameters.tsx b/components/NewParadigm/Parameters.tsx new file mode 100644 index 0000000..4ae7460 --- /dev/null +++ b/components/NewParadigm/Parameters.tsx @@ -0,0 +1,46 @@ +import { ListGroup, Card, ListGroupItem, Button, Modal, Form, InputGroup, Tooltip, OverlayTrigger} from "react-bootstrap"; +import React, { useState, useEffect, useContext } from "react"; + +const Parameter = (data) => { + let pName = data.pName; + let prms = data.prmState[0] + let setPrms = data.prmState[1] + let pTitle = "Choose parameter files (relative from prog folder or full path)" + if (data.title != null) + pTitle = data.title; + const [paramNum, setParamNum] = useState(1); + + const handlePrms = (e) => { + prms[e.target.id] = e.target.value; + setPrms(prms) + } + //button tooltip + const renderTooltip = (props) => ( + + Click to add another parameter file + + ); + return ( + <> + {pTitle} + {Array(paramNum) + .fill(0) + .map((x, idx) => ( + + + + + + + + ))} + + ); +}; +export default Parameter; diff --git a/components/Notes.tsx b/components/Notes.tsx index a830258..277b6a9 100644 --- a/components/Notes.tsx +++ b/components/Notes.tsx @@ -56,7 +56,7 @@ const Notes = () => { /> - diff --git a/components/Paradigms.tsx b/components/Paradigms.tsx index aa2c66f..14c3ed4 100644 --- a/components/Paradigms.tsx +++ b/components/Paradigms.tsx @@ -1,20 +1,66 @@ -import { ListGroup, Card, ListGroupItem } from "react-bootstrap"; +import { ListGroup, Card, ListGroupItem, Button, Modal, Form, FloatingLabel, Row, Col, Accordion } from "react-bootstrap"; import React, { useState, useEffect, useContext } from "react"; -import amplifiers from "../server/config/amplifiers.json"; import { useStore } from "./store"; +import NewTask from "./NewParadigm/NewTask"; + const Paradigms = () => { const [paradigms, setParadigms] = useState([]); - // const [ampConfig, setAmpConfig] = useState(); - useEffect(() => { + const [taskNum, setTaskNum] = useState(1); + const [pName, setPName] = useState(""); + const [newParadigm, setNewParadigm] = useState({}); + const handleAnotherTask = () => { + setTaskNum(taskNum + 1) + } + + //modal control + const [createParadigm, setShow] = useState(false); + const handleClose = () => { + setShow(false); + setTaskNum(0); + } + const handleShow = () => { + setTaskNum(1); + setNewParadigm({}); + setShow(true); + } + const handleCancel = () => { + if (Object.keys(newParadigm).length === 0 ) { + handleClose(); + } + else if (confirm("Close and lose all progress?") == true) { + handleClose(); + } + } + async function postJSON(data) { + console.log("creating json") + console.log(newParadigm) + try { + const paradigmsReq = await fetch(`/api/paradigms/newParadigm?name=${pName}`, {method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newParadigm)}); + console.log("created json") + console.log(paradigmsReq); + const result = await paradigmsReq.json(); + console.log("success:", result); + } catch (error) { + console.error(`error: ${error}`); + } + } + const handleSubmit = () => { + postJSON(newParadigm); + handleClose(); + } + + //child states for json population + +useEffect(() => { (async () => { let paradigmsReq = await fetch("/api/paradigms"); let paradigmsRes = await paradigmsReq.json(); - // if (useStore.getState().config) { - // setAmpConfig(amplifiers[useStore.getState().config.source]); - // } - //use pubsub model to set the ListGroupItem tomorrow - // useStore.setState({ tasks: paradigms }); + setParadigms(paradigmsRes); useStore; })(); @@ -27,8 +73,9 @@ const Paradigms = () => { }; return ( + <> - Paradigms +

Paradigms

{paradigms.map((x) => { @@ -39,8 +86,43 @@ const Paradigms = () => { ); })} + + +
+ + + + Create a new Paradigm! + + + + setPName(e.target.value)}/> + + + {Array(taskNum) + .fill(0) + .map((x, idx) => ( + + ))} + + + + + + + + + ); }; export default Paradigms; diff --git a/components/Tasks.tsx b/components/Tasks.tsx index 6ad2999..123c134 100644 --- a/components/Tasks.tsx +++ b/components/Tasks.tsx @@ -15,6 +15,12 @@ const Tasks = () => { (state) => state.task, (ss) => setTask(ss) ); + const valid = (attribute) => { + if (attribute != null && attribute.length >= 1) + return true; + else + return false; + } const blockSelect = async (task, currentBlock) => { useStore.setState({ @@ -26,15 +32,16 @@ const Tasks = () => { script += `Startup System localhost; `; //Add task states/events - if (task.addEvents.length >= 1) + if (valid(task.addEvents)) task.addEvents.forEach((e) => (script += `Add Event ${e}; `)); - if (task.addStates.length >= 1) + if (valid(task.addStates)) task.addStates.forEach((state) => (script += `Add State ${state}; `)); - task.addParameters.map((tskPrm) => (script += `Add parameter ${tskPrm}; `)); + if (valid(task.addParameters)) + task.addParameters.map((tskPrm) => (script += `Add parameter ${tskPrm}; `)); //Ask user if they want to log keyboard/mouse/etc and write that data to the source module //Start source module - if (task.userPrompt.length > 0) { + if (valid(task.userPrompt)) { if (confirm(`${Object.keys(task.userPrompt[0])}`) == true) { script += `Start executable ${config.source} --local ${Object.values( task.userPrompt[0] @@ -46,22 +53,22 @@ const Tasks = () => { script += `Start executable ${config.source} --local; `; } //Start processing module - if (task.executables.processing != null) { + if (task.executables.processing != null && task.executables.processing != "") { script += `Start executable ${task.executables.processing} --local; `; } else { script += `Start executable DummySignalProcessing --local; `; } //Start application module - if (task.executables.application != null) { + if (task.executables.application != null && task.executables.application != "") { script += `Start executable ${task.executables.application} --local; `; } else { script += `Start executable DummyApplication --local; `; } + script += "Wait for Connected; "; console.log(`${config.subject}`); script += `Set parameter SubjectName ${config.subject}; `; - script += "Wait for Connected; "; script += `Set parameter SubjectSession ${currentBlock.block}; `; script += `Set parameter DataFile ${config.subject}/${task.title.replace( @@ -92,9 +99,11 @@ const Tasks = () => { (script += `Set parameter ${tskPrm} ${task.setParameters[tskPrm]}; `) ); - script += `Set parameter WSSourceServer *:20100; `; - script += `Set parameter WSSpectralOutputServer *:20203; `; + //script += `Set parameter WSSourceServer *:20100; `; + //script += `Set parameter WSSpectralOutputServer *:20203; `; useStore.setState({ bciConfig: script }); + useStore.getState().bci.execute(useStore.getState().bciConfig); + //useStore.getState().bci.execute("start websocket localhost:1879"); }; return ( diff --git a/components/Toolbox.tsx b/components/Toolbox.tsx index ee6f0e5..413c949 100644 --- a/components/Toolbox.tsx +++ b/components/Toolbox.tsx @@ -32,10 +32,7 @@ const Toolbox = () => {

Toolbox

- - + Replay diff --git a/components/store.ts b/components/store.ts index eade0c1..2ee5718 100644 --- a/components/store.ts +++ b/components/store.ts @@ -1,7 +1,5 @@ import create from "zustand"; import { subscribeWithSelector } from "zustand/middleware"; -import { persist } from "zustand/middleware"; -// import { mountStoreDevtool } from "simple-zustand-devtools"; import { BCI2K_OperatorConnection } from "bci2k"; export const useStore = create( @@ -10,7 +8,7 @@ export const useStore = create( task: { title: "", description: "", - Blocks: "", + Blocks: "" }, tasks: [], bci: new BCI2K_OperatorConnection(), @@ -22,6 +20,8 @@ export const useStore = create( operatorPath: "", HostIP: "", webPort: 80, + editable: false, + darkMode: true }, block: { title: "", @@ -36,12 +36,6 @@ export const useStore = create( researcher: "", badChannels: "", comments: "", - })) - // , - // { - // name: 'test', - // } - // ) + })) ); -// mountStoreDevtool("Store", useStore); diff --git a/index.scss b/index.scss index 8662527..7368980 100644 --- a/index.scss +++ b/index.scss @@ -2,16 +2,99 @@ html { position: relative; - min-height: 100%; } + +$bg-light: #eff3f7; +$bg-dark: #161e25; +//$navbar-bg: #fff780; + + body { margin-bottom: 60px; - background-color: #fff5fa; - padding-top: 75px; - padding-bottom: 20px; + //background-color: #eff3f7; + background-color: #{$bg-light}; + //padding-top: 74px; + //padding-bottom: 20px + padding-left: 0px; + padding-right: 0px; + + border-radius: 10px; + border-top: #070027; + //border-color: #070027; +} +.wrapWords { + word-wrap: break-word; +} + +.myContainer { + padding-top: 85px; +} + +.primary { + border-width: 1px; + border-radius: 10px; +} +$bg-light: #eff3f7; +$bg-dark: #161e25; + +[data-bs-theme=light] { + background-color: #{$bg-light}; + --navbar-bg: #fff780; + + .btn-primary { + --bs-btn-bg: #585fb9; + --bs-btn-border-color: #505050; + --bs-btn-hover-bg: #0c1cf1; + } + //$primary:#585fb9; +} +$primary-dark: #000442; +$secondary-dark: #f7f096; +[data-bs-theme=dark] { + background-color: #{$bg-dark}; + --navbar-bg: #f7f096; + // $primary:#000442; + + // .btn { + // background-color: #000442; + // border-color: #3b3b3b; + // } + .btn-primary { + --bs-btn-bg: #{$primary-dark}; + --bs-btn-border-color: #3b3b3b; + --bs-btn-hover-bg: #010211; + } + .btn-secondary { + --bs-btn-bg: #{$secondary-dark}; + --bs-btn-border-color: #3b3b3b; + --bs-btn-hover-bg: #000dc5; + --bs-btn-color: #1d1d1d; + --bs-btn-hover-bg: #d1c300; + --bs-btn-hover-color: #1d1d1d; + + } +} +// [data-bs-theme="dark"] { +// --bs-primary: #{$primary-dark}; +// --bs-primary-bg-subtle: #{$primary-dark}; +// --bs-primary-bg-subtle-dark: #{$primary-dark}; + +// } +// .card { +// color:#000327; +// } +.systemButtons { + padding-top: 1px; + padding-bottom: 1px; + width: 80%; } .navbar-light { - background-color: #ff809d; + color:#070027; + font-weight: bold; + background-color: var(--navbar-bg); + font-size:"4px"; + padding-left: 30px; + padding-right: 10px; } .footer { @@ -19,7 +102,7 @@ body { bottom: 0; width: 100%; height: 60px; - background-color: #ffe9ed; + //background-color: #f0f0f0; .container { margin-top: 20px; } diff --git a/package.json b/package.json index 3e931a9..ab189b0 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "lint": "next lint" }, "dependencies": { - "bci2k": "^3.0.0", + "bci2k": "^3.1.3", "bootstrap": "^5.1.3", "death": "^1.1.0", + "d3": "^7.8.5", "esbuild": "^0.14.20", "esbuild-register": "^3.3.2", "express": "^4.17.2", @@ -21,9 +22,10 @@ "react": "17.0.2", "react-bootstrap": "^2.1.2", "react-dom": "17.0.2", + "react-popup": "^0.11.2", "react-use-websocket": "^3.0.0", + "reactjs-popup": "^2.0.6", "sass": "^1.49.7", - "simple-zustand-devtools": "^1.0.1", "telnet-client": "^2.0.1", "ui": "*", "ws": "^8.5.0", diff --git a/pages/_app.js b/pages/_app.js index 64a82d5..b67fccb 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -6,8 +6,8 @@ import SSRProvider from 'react-bootstrap/SSRProvider'; export default function MyApp({ Component, pageProps }) { return ( - - + + ); diff --git a/pages/api/paradigms/newParadigm.ts b/pages/api/paradigms/newParadigm.ts new file mode 100644 index 0000000..d360b12 --- /dev/null +++ b/pages/api/paradigms/newParadigm.ts @@ -0,0 +1,35 @@ +//? sends the task.json +import fs from "fs"; + +export default function handler(req, res) { + //first create directory + const path = `${process.cwd()}/server/paradigms/${req.query.name}`; + fs.access(path, (error) => { + // To check if the given directory + // already exists or not + if (error) { + // If current directory does not exist + // then create it + fs.mkdir(path, (error) => { + if (error) { + console.log(error); + } else { + console.log("New Directory created successfully !!"); + } + }); + } else { + console.log("Given Directory already exists !!"); + } + //then create json + fs.writeFile(path + '/task.json', JSON.stringify(req.body, null, 2), err => { + if (err) { + console.error(err); + } else { + // file written successfully + console.log("successful") + }}) + }); + //const task = req.data; + //const { task } = req.query; + //res = task; +} \ No newline at end of file diff --git a/pages/api/saveLocalConfig.ts b/pages/api/saveLocalConfig.ts new file mode 100644 index 0000000..088c732 --- /dev/null +++ b/pages/api/saveLocalConfig.ts @@ -0,0 +1,18 @@ +//? updates the localconfig.json +import fs from "fs"; + +export default function handler(req, res) { + //first create directory + const path = `${process.cwd()}/server/Config/localconfig.json`; + fs.access(path, (error) => { + //then create json + console.log(req.query) + fs.writeFile(path, JSON.stringify(req.body, null, 2), err => { + if (err) { + console.error(err); + } else { + // file written successfully + console.log("local config updated") + }}) + }); +} \ No newline at end of file diff --git a/pages/index.tsx b/pages/index.tsx index 1212699..2655d96 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,67 +1,50 @@ import { useState, useContext, useEffect } from "react"; -import { Row, Col, Navbar, Container, Form, Button } from "react-bootstrap"; -import ConnectionState from "../components/ConnectionState"; -import Toolbox from "../components/Toolbox"; -import { - ReplayParadigms, - ReplayTasks, -} from "../components/ToolboxComps/Replay"; -import Paradigms from "../components/Paradigms"; -import Tasks from "../components/Tasks"; +import { Row, Col, Navbar, Container, Form, Button, Tabs, Tab } from "react-bootstrap"; import localconfig from "../server/config/localconfig.json"; import { useStore } from "../components/store"; -import Notes from "../components/Notes"; +import ExperimentConfiguration from "../components/ExperimentConfiguration"; import Head from "next/head"; export default function BCI2000Web() { useEffect(() => { useStore.setState({ config: localconfig }); + document.documentElement.setAttribute('data-bs-theme', theme ? "dark" : "light"); }, []); + const [theme, setTheme] = useState(useStore.getState().config.darkMode); return ( -
+
BCI2000Web - - - + + BCI2000 + - - - - - - - - {useStore.getState().replayMode ? ( - - ) : ( - - )} - - - {useStore.getState().replayMode && - useStore.getState().replayTask != null ? ( - - ) : ( - - )} - - - - - - + + + + + + +

diff --git a/server/config/amplifiers.json b/server/config/amplifiers.json index 6eed3c2..d15eeac 100644 --- a/server/config/amplifiers.json +++ b/server/config/amplifiers.json @@ -2,20 +2,23 @@ "SignalGenerator": { "setParameters": { "SamplingRate": "1000" - } + }, + "loadParameters": ["../parms/fragments/amplifiers/SignalGenerator.prm"] }, - "NihonKohden": { + "NihonKohdenSource": { "setParameters": { "DeviceAddress": "", "SampleBlockSize": "100" - } + }, + "loadParameters": ["../parms/fragments/amplifiers/NihonKohden.prm"] }, "Blackrock": { "setParameters": { "NSPInstances": "1", "SamplingRate": "1000", "SampleBlockSize": "60" - } + }, + "loadParameters": ["../parms/fragments/amplifiers/Blackrock.prm"] }, "Grapevine": { "setParameters": { diff --git a/server/config/localconfig.json b/server/config/localconfig.json index f58ad4a..b9a50cd 100644 --- a/server/config/localconfig.json +++ b/server/config/localconfig.json @@ -6,7 +6,9 @@ "VisualizeSource": "0", "VisualizeTiming": "0" }, - "operatorPath": "C:/Users/Chris/Downloads/Dickens_BCI2000/prog/BCI2000Shell.exe", - "webPort": 80, - "HostIP": "192.168.1.7" -} + "operatorPath": "C:\\bci2000.x64\\prog\\BCI2000Shell.exe", + "webPort": 3000, + "HostIP": "localhost", + "editable": true, + "darkMode": true +} \ No newline at end of file diff --git a/server/index.ts b/server/index.ts index c00a914..ac7d6d2 100644 --- a/server/index.ts +++ b/server/index.ts @@ -33,22 +33,14 @@ app.prepare().then(async () => { }); wss.on("connection", async (ws) => { ws.onmessage = (e) => { - let msg = JSON.parse(e.data.toString()); - if (msg.opcode == "E") { - operator.stdin.write(`${msg.contents}; \n`); - operator.stdout.on("data", (data) => { - ws.send( - JSON.stringify({ - opcode: "O", - id: msg.id, - response: data - .toString() - .replaceAll("BCI2000Shell> ", "") - .trim(), - }) - ); - }); - } + operator.stdin.write(`${e.data.toString()}; \n`); + operator.stdout.on("data", (data) => { + ws.send(data + .toString() + .replaceAll("BCI2000Shell> ", "") + .trim(), + ); + }); }; }); } From e539c6313c39b2dbf12338a21db7047007974ced Mon Sep 17 00:00:00 2001 From: Will Engelhardt Date: Fri, 30 Aug 2024 14:05:14 -0500 Subject: [PATCH 2/3] Fix error of tasks having same ID --- server/paradigms/My new paradigm/task.json | 22 ++++++++ server/paradigms/My task!/task.json | 51 +++++++++++++++++++ .../Task made thru BCI200 Web/task.json | 44 ++++++++++++++++ server/paradigms/Template/task.json | 2 +- 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 server/paradigms/My new paradigm/task.json create mode 100644 server/paradigms/My task!/task.json create mode 100644 server/paradigms/Task made thru BCI200 Web/task.json diff --git a/server/paradigms/My new paradigm/task.json b/server/paradigms/My new paradigm/task.json new file mode 100644 index 0000000..814bce7 --- /dev/null +++ b/server/paradigms/My new paradigm/task.json @@ -0,0 +1,22 @@ +{ + "Task_1": { + "title": "First task", + "description": "1", + "executables": { + "processing": "", + "application": "" + }, + "setParameters": {}, + "loadParameters": [ + "../parms/examples/StimulusPresentation_SignalGenerator.prm" + ], + "Blocks": { + "Block_1": { + "title": "Block 1", + "loadParameters": [ + "C:\\bci2000.x64\\parms\\examples\\StimulusPresentation_Images.prm" + ] + } + } + } +} \ No newline at end of file diff --git a/server/paradigms/My task!/task.json b/server/paradigms/My task!/task.json new file mode 100644 index 0000000..f37f710 --- /dev/null +++ b/server/paradigms/My task!/task.json @@ -0,0 +1,51 @@ +{ + "Task_1": { + "title": "Baseline", + "description": "No task or processing", + "executables": { + "processing": null, + "application": null + }, + "addParameters": [], + "setParameters": {}, + "loadParameters": [], + "addEvents": [], + "addStates": [], + "userPrompt": [], + "Blocks": { + "Block_1": { + "title": "Run baseline recording", + "loadParameters": [] + } + } + }, + "Task_2": { + "title": "Run your task", + "description": "Let's try Stimulus Presentation", + "executables": { + "processing": "P3SignalProcessing", + "application": "StimulusPresentation" + }, + "addParameters": [], + "setParameters": { + "AudioSwitch": "0", + "WindowLeft": "600" + }, + "loadParameters": ["../parms/examples/StimulusPresentation_SignalGenerator.prm"], + "addEvents": [], + "addStates": [], + "userPrompt": [], + "Blocks": { + "Block_1": { + "title": "Run default task", + "loadParameters": [] + }, + "Block_2": { + "title": "Run with fun images", + "loadParameters": [ + "../parms/examples/StimulusPresentation_Images.prm" + ] + } + } + } +} diff --git a/server/paradigms/Task made thru BCI200 Web/task.json b/server/paradigms/Task made thru BCI200 Web/task.json new file mode 100644 index 0000000..c1e9739 --- /dev/null +++ b/server/paradigms/Task made thru BCI200 Web/task.json @@ -0,0 +1,44 @@ +{ + "Task_1": { + "title": "First task", + "description": "lets do baseline", + "executables": { + "processing": "", + "application": "" + }, + "setParameters": {}, + "loadParameters": [], + "Blocks": { + "Block_1": { + "title": "Baselin", + "loadParameters": [] + } + } + }, + "Task_2": { + "title": "Default Stimulus Presentation", + "description": "oldest one in the book", + "executables": { + "processing": "Spectral_WS", + "application": "StimulusPresentation.exe" + }, + "setParameters": { + "WSSpectralOutputServer": "localhost:1879" + }, + "loadParameters": [ + "C:\\bci2000.x64\\parms\\examples\\StimulusPresentation_SignalGenerator.prm" + ], + "Blocks": { + "Block_1": { + "title": "Default parameter", + "loadParameters": [] + }, + "Block_2": { + "title": "Free Spelling task", + "loadParameters": [ + "C:\\bci2000.x64\\parms\\examples\\StimulusPresentation_FreeSpelling.prm" + ] + } + } + } +} \ No newline at end of file diff --git a/server/paradigms/Template/task.json b/server/paradigms/Template/task.json index 1bf437c..dce0b80 100644 --- a/server/paradigms/Template/task.json +++ b/server/paradigms/Template/task.json @@ -31,7 +31,7 @@ } }, "Task_2": { - "title": "New Task Format", + "title": "New Task Format 2", "description": "Structure your tasks as follows.", "executables": { "processing": "Spectral_WS", From ece791653027543e283dcd2bfd94528c94b43e70 Mon Sep 17 00:00:00 2001 From: Will Engelhardt Date: Fri, 30 Aug 2024 14:13:44 -0500 Subject: [PATCH 3/3] Remove accidental addition of task files --- server/paradigms/My new paradigm/task.json | 22 -------- server/paradigms/My task!/task.json | 51 ------------------- .../Task made thru BCI200 Web/task.json | 44 ---------------- 3 files changed, 117 deletions(-) delete mode 100644 server/paradigms/My new paradigm/task.json delete mode 100644 server/paradigms/My task!/task.json delete mode 100644 server/paradigms/Task made thru BCI200 Web/task.json diff --git a/server/paradigms/My new paradigm/task.json b/server/paradigms/My new paradigm/task.json deleted file mode 100644 index 814bce7..0000000 --- a/server/paradigms/My new paradigm/task.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "Task_1": { - "title": "First task", - "description": "1", - "executables": { - "processing": "", - "application": "" - }, - "setParameters": {}, - "loadParameters": [ - "../parms/examples/StimulusPresentation_SignalGenerator.prm" - ], - "Blocks": { - "Block_1": { - "title": "Block 1", - "loadParameters": [ - "C:\\bci2000.x64\\parms\\examples\\StimulusPresentation_Images.prm" - ] - } - } - } -} \ No newline at end of file diff --git a/server/paradigms/My task!/task.json b/server/paradigms/My task!/task.json deleted file mode 100644 index f37f710..0000000 --- a/server/paradigms/My task!/task.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "Task_1": { - "title": "Baseline", - "description": "No task or processing", - "executables": { - "processing": null, - "application": null - }, - "addParameters": [], - "setParameters": {}, - "loadParameters": [], - "addEvents": [], - "addStates": [], - "userPrompt": [], - "Blocks": { - "Block_1": { - "title": "Run baseline recording", - "loadParameters": [] - } - } - }, - "Task_2": { - "title": "Run your task", - "description": "Let's try Stimulus Presentation", - "executables": { - "processing": "P3SignalProcessing", - "application": "StimulusPresentation" - }, - "addParameters": [], - "setParameters": { - "AudioSwitch": "0", - "WindowLeft": "600" - }, - "loadParameters": ["../parms/examples/StimulusPresentation_SignalGenerator.prm"], - "addEvents": [], - "addStates": [], - "userPrompt": [], - "Blocks": { - "Block_1": { - "title": "Run default task", - "loadParameters": [] - }, - "Block_2": { - "title": "Run with fun images", - "loadParameters": [ - "../parms/examples/StimulusPresentation_Images.prm" - ] - } - } - } -} diff --git a/server/paradigms/Task made thru BCI200 Web/task.json b/server/paradigms/Task made thru BCI200 Web/task.json deleted file mode 100644 index c1e9739..0000000 --- a/server/paradigms/Task made thru BCI200 Web/task.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "Task_1": { - "title": "First task", - "description": "lets do baseline", - "executables": { - "processing": "", - "application": "" - }, - "setParameters": {}, - "loadParameters": [], - "Blocks": { - "Block_1": { - "title": "Baselin", - "loadParameters": [] - } - } - }, - "Task_2": { - "title": "Default Stimulus Presentation", - "description": "oldest one in the book", - "executables": { - "processing": "Spectral_WS", - "application": "StimulusPresentation.exe" - }, - "setParameters": { - "WSSpectralOutputServer": "localhost:1879" - }, - "loadParameters": [ - "C:\\bci2000.x64\\parms\\examples\\StimulusPresentation_SignalGenerator.prm" - ], - "Blocks": { - "Block_1": { - "title": "Default parameter", - "loadParameters": [] - }, - "Block_2": { - "title": "Free Spelling task", - "loadParameters": [ - "C:\\bci2000.x64\\parms\\examples\\StimulusPresentation_FreeSpelling.prm" - ] - } - } - } -} \ No newline at end of file