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.

Phase 1-4 complete

+2138 -2167
+3
.github/workflows/test.yml
··· 24 24 run: npm install 25 25 26 26 - name: Test 27 + run: npm test 28 + 29 + - name: Lint and typecheck 27 30 env: 28 31 GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 32 USERNAME: ${{ secrets.USERNAME }}
+104
docs/BrainWaves Technical Implementation Plan (2026 Modernization).md
··· 1 + # BrainWaves: Technical Implementation Plan (2026 Modernization) 2 + 3 + **Author:** Manus AI 4 + **Date:** March 2026 5 + 6 + This document outlines a step-by-step technical implementation plan for AI agents working on the BrainWaves codebase. The goal is to modernize the stack, resolve deployment blockers, and replace the UI library, while establishing a robust testing harness. 7 + 8 + *Note: The migration to Lab Streaming Layer (LSL) is explicitly excluded from this phase and will be addressed once the application is successfully building and deploying.* 9 + 10 + --- 11 + 12 + ## Phase 1: Test Harness & Build Environment Setup 13 + 14 + Before modifying the application logic, we must establish a reliable testing harness using Vitest and ensure the Electron build pipeline is functional. The current codebase uses a legacy Jest configuration (`"test": "cross-env jest --passWithNoTests"`) which must be replaced. 15 + 16 + ### Step 1.1: Install and Configure Vitest 17 + * **Action:** Remove Jest dependencies and install Vitest, `@testing-library/react`, `@testing-library/jest-dom`, and `jsdom`. 18 + * **Action:** Create a `vitest.config.ts` file configured for a React/DOM environment. 19 + * **Action:** Update `package.json` scripts to use Vitest (`"test": "vitest run"`, `"test:watch": "vitest"`). 20 + * **Acceptance Criteria:** 21 + * A basic sanity test (`src/renderer/App.test.tsx`) rendering a simple component passes using `npm test`. 22 + * The CI workflow (`.github/workflows/test.yml`) is updated to run `npm test` using Vitest. 23 + 24 + ### Step 1.2: Verify Electron Build Pipeline 25 + * **Action:** Run the existing `npm run build` and `npm run package-ci` commands to identify any immediate build failures caused by Node 22 or Vite 7. 26 + * **Action:** Fix any immediate compilation errors in `src/main/index.ts` or `vite.config.ts`. 27 + * **Acceptance Criteria:** 28 + * `npm run build` completes without errors. 29 + * An integration test (`tests/build.test.ts`) is added that programmatically verifies the existence of the `out/main/index.js` and `out/renderer/index.html` files after a build. 30 + 31 + --- 32 + 33 + ## Phase 2: Routing and State Synchronization Modernization 34 + 35 + The codebase currently relies on `react-router-dom v5` and the abandoned `connected-react-router` (which syncs router state to Redux). This causes significant issues with modern React 18 and Redux Toolkit. 36 + 37 + ### Step 2.1: Remove `connected-react-router` 38 + * **Action:** Uninstall `connected-react-router`. 39 + * **Action:** Remove `routerMiddleware` and `connectRouter` from `src/renderer/store.ts` and `src/renderer/reducers/index.ts`. 40 + * **Action:** Remove `ConnectedRouter` from `src/renderer/containers/Root.tsx` and replace it with a standard `HashRouter` from `react-router-dom`. 41 + * **Acceptance Criteria:** 42 + * The Redux store initializes successfully without the router reducer. 43 + * Unit tests verify that the Redux store can be created with initial state (`tests/store.test.ts`). 44 + 45 + ### Step 2.2: Upgrade to React Router v6/v7 46 + * **Action:** Upgrade `react-router-dom` to the latest stable version. 47 + * **Action:** Refactor `src/renderer/routes.tsx` to use the new `<Routes>` and `<Route element={<Component />}>` syntax instead of the legacy `<Switch>` and `component={}` props. 48 + * **Action:** Refactor class components (e.g., `HomeComponent`, `DesignComponent`) that rely on `this.props.history.push`. Convert them to functional components using the `useNavigate` hook, or create a Higher-Order Component (HOC) `withRouter` wrapper if functional conversion is too extensive. 49 + * **Action:** Update `src/renderer/epics/experimentEpics.ts` which currently listens to `@@router/LOCATION_CHANGE`. Refactor the logic to trigger based on specific Redux actions rather than URL changes. 50 + * **Acceptance Criteria:** 51 + * Unit tests (`tests/routing.test.tsx`) verify that navigation between the Home, Design, and Collect screens renders the correct components. 52 + 53 + --- 54 + 55 + ## Phase 3: Pyodide and Python Dependency Modernization 56 + 57 + The application uses Pyodide `v0.21.0` (hardcoded in `internals/scripts/InstallPyodide.js`) and relies on deprecated Python libraries for data visualization. 58 + 59 + ### Step 3.1: Upgrade Pyodide 60 + * **Action:** Update `internals/scripts/InstallPyodide.js` to download a modern stable release of Pyodide (e.g., `v0.27.0`). 61 + * **Action:** Ensure the `vite.config.ts` `publicDir` setting still correctly serves the updated Pyodide WASM and JS files. 62 + * **Acceptance Criteria:** 63 + * An integration test (`tests/pyodide.test.ts`) successfully instantiates the Pyodide web worker (`src/renderer/utils/pyodide/webworker.js`) and executes a simple `1 + 1` Python command. 64 + 65 + ### Step 3.2: Refactor Python Plotting Logic 66 + * **Action:** In `src/renderer/utils/pyodide/utils.py`, locate the usage of `sns.tsplot` (which was removed in Seaborn v0.10.0). 67 + * **Action:** Rewrite the `plot_conditions` function to use `sns.lineplot` or standard `matplotlib.pyplot.plot` with `fill_between` for confidence intervals. 68 + * **Acceptance Criteria:** 69 + * A unit test (`tests/python_utils.test.ts`) loads `utils.py` into the Pyodide worker, passes mock EEG data, and verifies that the plotting function executes without throwing a Python `AttributeError`. 70 + 71 + --- 72 + 73 + ## Phase 4: UI Library Replacement (Semantic UI to Shadcn/ui) 74 + 75 + `semantic-ui-react` is abandoned and throws deprecation warnings in React 18. We will replace it with Tailwind CSS and Shadcn/ui components. 76 + 77 + ### Step 4.1: Install Tailwind CSS and Shadcn/ui 78 + * **Action:** Install Tailwind CSS, PostCSS, and Autoprefixer. Configure `tailwind.config.js` and `postcss.config.js`. 79 + * **Action:** Initialize Shadcn/ui (`npx shadcn-ui@latest init`) and configure it to output components to `src/renderer/components/ui`. 80 + * **Acceptance Criteria:** 81 + * A unit test verifies that a basic Shadcn/ui `Button` component renders correctly with Tailwind utility classes applied. 82 + 83 + ### Step 4.2: Component-by-Component Replacement 84 + * **Action:** Identify the ~26 files importing `semantic-ui-react`. The most heavily used components are `Segment`, `Button`, `Grid`, and `Header`. 85 + * **Action:** Replace `semantic-ui-react` components with their Shadcn/ui equivalents: 86 + * `Button` -> Shadcn `Button` 87 + * `Segment` -> Shadcn `Card` or a simple `div` with Tailwind borders/padding. 88 + * `Grid` -> Tailwind CSS Grid (`grid grid-cols-12 gap-4`). 89 + * `Header` -> Standard HTML `h1`-`h6` tags with Tailwind typography classes. 90 + * `Modal` -> Shadcn `Dialog`. 91 + * **Action:** Remove `semantic-ui-css` from `src/renderer/index.tsx` and uninstall `semantic-ui-react`. 92 + * **Acceptance Criteria:** 93 + * `grep -r "semantic-ui-react" src/` returns zero results. 94 + * Visual regression or DOM snapshot tests (`tests/ui_migration.test.tsx`) for key screens (Home, Design, Collect) pass, ensuring the new components render without crashing. 95 + 96 + --- 97 + 98 + ## Phase 5: Final Build and Verification 99 + 100 + ### Step 5.1: End-to-End Build Verification 101 + * **Action:** Run the full build pipeline (`npm run package-all`). 102 + * **Acceptance Criteria:** 103 + * The Electron application compiles and packages successfully for the target OS. 104 + * All Vitest suites (Routing, Store, Pyodide, UI) pass with 100% success rate.
+353
docs/progress.md
··· 1 + # BrainWaves Modernization — Implementation Progress 2 + 3 + Tracking file for executing the [Technical Implementation Plan](./BrainWaves_%20Technical%20Implementation%20Plan%20(2026%20Modernization).md) across sessions. 4 + 5 + **Last updated:** 2026-03-07 6 + **Overall status:** PHASES 1–4 COMPLETE (Phase 5 pending: npm install + build verification) 7 + 8 + --- 9 + 10 + ## Codebase Reconnaissance (completed) 11 + 12 + Key files read and understood before starting implementation: 13 + 14 + - `package.json` — current deps, jest config, scripts 15 + - `src/renderer/store.ts` — uses `connected-react-router` (`routerMiddleware`, `createHashHistory`) 16 + - `src/renderer/reducers/index.ts` — uses `connectRouter` from `connected-react-router` 17 + - `src/renderer/containers/Root.tsx` — uses `ConnectedRouter`, receives `history` prop 18 + - `src/renderer/index.tsx` — imports and passes `history` from store to Root 19 + - `src/renderer/routes.tsx` — React Router v5 `<Switch>` / `<Route component={}>` / custom `PropsRoute` 20 + - `src/renderer/epics/experimentEpics.ts` — `autoSaveEpic` and `navigationCleanupEpic` listen to `@@router/LOCATION_CHANGE` 21 + - `src/renderer/containers/TopNavBarContainer.ts` — maps `state.router.location` to props 22 + - `src/renderer/components/TopNavComponent/index.tsx` — class component, uses `this.props.location.pathname` 23 + - `src/renderer/components/HomeComponent/index.tsx` — class component, calls `this.props.history.push()` 24 + - `src/renderer/components/CollectComponent/index.tsx` — passes `history` prop to `ConnectModal` (unused there) 25 + - `src/renderer/components/CollectComponent/ConnectModal.tsx` — has `history` in Props but does NOT use it 26 + - `src/renderer/components/EEGExplorationComponent.tsx` — passes `history` to `ConnectModal` (unused) 27 + - `src/renderer/actions/experimentActions.ts` — RTK `createAction`, `typesafe-actions` 28 + - `src/renderer/utils/pyodide/utils.py` — uses `sns.tsplot` (removed in seaborn v0.10), seaborn import commented out 29 + - `src/renderer/utils/pyodide/webworker.js` — `importScripts('/pyodide/pyodide.js')`, loads matplotlib/mne/pandas 30 + - `internals/scripts/InstallPyodide.js` — downloads pyodide v0.21.0 tarball 31 + - `vite.config.ts` — `publicDir` serves `src/renderer/utils/pyodide/src/` as static assets 32 + - `.github/workflows/test.yml` — runs `npm run package-ci`, lint, tsc (no unit tests) 33 + - 26 files import `semantic-ui-react` (see list below) 34 + 35 + --- 36 + 37 + ## Phase 1: Test Harness & Vitest 38 + 39 + **Status: COMPLETE** 40 + 41 + ### Step 1.1 — Install and configure Vitest 42 + 43 + **What to do:** 44 + 1. Edit `package.json`: 45 + - Remove from `devDependencies`: `jest`, `@types/jest`, `identity-obj-proxy`, `react-test-renderer` 46 + - Add to `devDependencies`: `vitest`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom` 47 + - Change `"test": "cross-env jest --passWithNoTests"` → `"test": "vitest run"` 48 + - Add `"test:watch": "vitest"` 49 + - Remove the `"jest": { ... }` config block from package.json 50 + 2. Create `vitest.config.ts` (project root) 51 + 3. Create `src/test-setup.ts` (imports `@testing-library/jest-dom`) 52 + 4. Create `src/renderer/App.test.tsx` (basic sanity render test) 53 + 5. Run `npm install` to update node_modules 54 + 55 + **Notes:** 56 + - Vitest needs `jsdom` environment for React component tests 57 + - The vite.config.ts babel plugins (decorators, class-properties) must also be present in vitest.config.ts 58 + - CSS modules: vitest handles them natively with `css.modules` config; no `identity-obj-proxy` needed 59 + 60 + ### Step 1.2 — Update CI workflow 61 + 62 + **What to do:** 63 + 1. Edit `.github/workflows/test.yml`: add `npm test` step before or after existing steps 64 + 2. Create `tests/build.test.ts`: verifies `out/main/index.js` and `out/renderer/index.html` exist after build 65 + 66 + --- 67 + 68 + ## Phase 2: Routing Modernization 69 + 70 + **Status: COMPLETE** 71 + 72 + ### Step 2.1 — Remove `connected-react-router` 73 + 74 + **Package changes:** 75 + - Remove from `dependencies`: `connected-react-router`, `history` 76 + - Remove from `devDependencies`: `@types/history`, `@types/react-router`, `@types/react-router-dom` 77 + - Remove `overrides["connected-react-router"]` block 78 + 79 + **File changes:** 80 + 81 + | File | Change | 82 + |------|--------| 83 + | `src/renderer/store.ts` | Remove `createHashHistory`, `history` export, `routerMiddleware`, remove `router` from middleware | 84 + | `src/renderer/reducers/index.ts` | Remove `connectRouter`, `History` import, remove `router` from combineReducers, remove `router: any` from RootState | 85 + | `src/renderer/containers/Root.tsx` | Remove `ConnectedRouter`; use `HashRouter` from `react-router-dom`; remove `history` prop | 86 + | `src/renderer/index.tsx` | Remove `history` import; pass only `store` to `<Root>` | 87 + 88 + ### Step 2.2 — Upgrade to React Router v6 89 + 90 + **Package changes:** 91 + - Change `react-router` and `react-router-dom`: `"^5.2.0"` → `"^6.x"` (v6 merges the two packages; keep both entries or consolidate) 92 + 93 + **New file:** 94 + - `src/renderer/actions/routerActions.ts` — defines `RouterActions.RouteChanged(pathname: string)` action 95 + 96 + **File changes:** 97 + 98 + | File | Change | 99 + |------|--------| 100 + | `src/renderer/routes.tsx` | Replace `<Switch>/<Route component={}>` with `<Routes>/<Route element={<C />}>`. Remove `PropsRoute`. Pass `activeStep` directly as JSX prop on `<HomeContainer>`. | 101 + | `src/renderer/containers/App.tsx` | Add `NavigationTracker` functional component (uses `useLocation` + `useDispatch` to dispatch `RouterActions.RouteChanged` on location change) | 102 + | `src/renderer/epics/experimentEpics.ts` | Replace both `ofType('@@router/LOCATION_CHANGE')` epics to use `filter(isActionOf(RouterActions.RouteChanged))`. Access `action.payload` as pathname string directly. Remove `pluck('payload', 'pathname')` etc. | 103 + | `src/renderer/components/TopNavComponent/index.tsx` | Convert class → functional component. Replace `this.props.location.pathname` with `useLocation().pathname`. State becomes `useState`. Methods become callbacks. | 104 + | `src/renderer/containers/TopNavBarContainer.ts` | Remove `location: state.router.location` from mapStateToProps | 105 + | `src/renderer/components/HomeComponent/index.tsx` | Change `history: History` prop to `navigate: (path: string) => void`. Replace all `this.props.history.push(X)` with `this.props.navigate(X)`. Remove `import { History } from 'history'`. | 106 + | `src/renderer/containers/HomeContainer.ts` | Wrap exported component with `withNavigate` HOC that injects `useNavigate()` as `navigate` prop | 107 + | `src/renderer/components/CollectComponent/index.tsx` | Remove `history: History` from Props; remove passing `history` to `ConnectModal` | 108 + | `src/renderer/components/EEGExplorationComponent.tsx` | Remove `history: History` from Props; remove passing `history` to `ConnectModal` | 109 + | `src/renderer/components/CollectComponent/ConnectModal.tsx` | Remove `history: History` from Props interface | 110 + 111 + **withNavigate HOC pattern (for HomeContainer.ts):** 112 + ```tsx 113 + import React from 'react'; 114 + import { useNavigate } from 'react-router-dom'; 115 + 116 + function withNavigate<P extends { navigate: (path: string) => void }>( 117 + Component: React.ComponentType<P> 118 + ) { 119 + return function WithNavigate(props: Omit<P, 'navigate'>) { 120 + const navigate = useNavigate(); 121 + return <Component {...(props as P)} navigate={navigate} />; 122 + }; 123 + } 124 + ``` 125 + 126 + **NavigationTracker pattern (for App.tsx):** 127 + ```tsx 128 + function NavigationTracker() { 129 + const location = useLocation(); 130 + const dispatch = useDispatch(); 131 + useEffect(() => { 132 + dispatch(RouterActions.RouteChanged(location.pathname)); 133 + }, [location.pathname, dispatch]); 134 + return null; 135 + } 136 + ``` 137 + 138 + **Tests to add:** 139 + - `tests/store.test.ts` — verifies store initializes without router reducer 140 + - `tests/routing.test.tsx` — verifies navigation between Home/Design/Collect renders correct components 141 + 142 + --- 143 + 144 + ## Phase 3: Pyodide Modernization 145 + 146 + **Status: COMPLETE** 147 + 148 + ### Step 3.1 — Upgrade Pyodide version 149 + 150 + **File:** `internals/scripts/InstallPyodide.js` 151 + 152 + Changes: 153 + - `PYODIDE_VERSION`: `'0.21.0'` → `'0.27.0'` 154 + - `TAR_NAME`: `pyodide-build-${PYODIDE_VERSION}.tar.bz2` → `pyodide-${PYODIDE_VERSION}.tar.bz2` 155 + - `TAR_URL`: update to match new naming 156 + 157 + **Note:** The tarball for 0.27.0 extracts to a `pyodide/` subdirectory, which is correct for the webworker's `importScripts('/pyodide/pyodide.js')` call. The old 0.21.0 tar was also `pyodide-build-*` but extracted to a `pyodide/` dir. 158 + 159 + **Caution:** `mne` package availability in pyodide 0.27.0 needs verification. If not in the default package list, `webworker.js` `loadPackage(['matplotlib', 'mne', 'pandas'])` will fail. May need to load `mne` via `micropip.install('mne')` instead. 160 + 161 + **Test to add:** `tests/pyodide.test.ts` 162 + 163 + ### Step 3.2 — Fix Python plotting (`utils.py`) 164 + 165 + **File:** `src/renderer/utils/pyodide/utils.py` 166 + 167 + The `plot_conditions` function currently: 168 + - Calls `sns.color_palette(...)` — but `import seaborn as sns` is commented out 169 + - Calls `sns.tsplot(...)` — removed from seaborn in v0.10.0 170 + - Calls `sns.despine()` — still exists but seaborn not imported 171 + 172 + **Fix — replace `plot_conditions` body:** 173 + 1. Replace `sns.color_palette(...)` with a hardcoded palette (consistent with `plot_topo`) 174 + 2. Replace `sns.tsplot(...)` with manual bootstrap CI using numpy + `ax.plot()` + `ax.fill_between()` 175 + 3. Replace `sns.despine()` with `ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False)` 176 + 177 + **Bootstrap CI replacement for `sns.tsplot(X[...], time=times, color=color, n_boot=n_boot, ci=ci)`:** 178 + ```python 179 + cond_data = X[y.isin(cond), ch_ind] 180 + mean = np.nanmean(cond_data, axis=0) 181 + n_samples = cond_data.shape[0] 182 + boot_means = np.array([ 183 + np.nanmean(cond_data[np.random.randint(0, n_samples, n_samples)], axis=0) 184 + for _ in range(n_boot) 185 + ]) 186 + alpha = (100 - ci) / 2 187 + low = np.percentile(boot_means, alpha, axis=0) 188 + high = np.percentile(boot_means, 100 - alpha, axis=0) 189 + ax.plot(times, mean, color=color) 190 + ax.fill_between(times, low, high, color=color, alpha=0.3) 191 + ``` 192 + 193 + **Test to add:** `tests/python_utils.test.ts` 194 + 195 + --- 196 + 197 + ## Phase 4: UI Library Replacement (Semantic UI → Tailwind + Shadcn/ui) 198 + 199 + **Status: COMPLETE** 200 + 201 + ### Step 4.1 — Install Tailwind CSS and Shadcn/ui 202 + 203 + **Package additions (devDependencies):** 204 + - `tailwindcss`, `postcss`, `autoprefixer` 205 + 206 + **Package additions (dependencies):** 207 + - `@radix-ui/react-dialog` 208 + - `@radix-ui/react-dropdown-menu` 209 + - `@radix-ui/react-slot` 210 + - `class-variance-authority` 211 + - `clsx` 212 + - `tailwind-merge` 213 + 214 + **Package removals (dependencies):** 215 + - `semantic-ui-react` 216 + - `semantic-ui-css` 217 + 218 + **New config files:** 219 + - `tailwind.config.js` 220 + - `postcss.config.js` 221 + 222 + **New UI component files** (in `src/renderer/components/ui/`): 223 + - `utils.ts` — `cn()` helper (`clsx` + `tailwind-merge`) 224 + - `button.tsx` — Shadcn Button (CVA variants) 225 + - `card.tsx` — Shadcn Card (replaces `Segment`) 226 + - `dialog.tsx` — Shadcn Dialog (replaces `Modal`) 227 + - `dropdown-menu.tsx` — Shadcn DropdownMenu (replaces `Dropdown`) 228 + - `table.tsx` — Shadcn Table (replaces `Table`) 229 + 230 + **Update `src/renderer/app.global.css`:** add Tailwind directives (`@tailwind base/components/utilities`) 231 + 232 + **Remove from `src/renderer/index.tsx`:** `import 'semantic-ui-css/semantic.min.css'` 233 + 234 + ### Step 4.2 — Component-by-component replacement 235 + 236 + **26 files to update** (grep confirmed): 237 + 238 + | File | Semantic UI components used | Status | 239 + |------|-----------------------------|--------| 240 + | `components/AnalyzeComponent.tsx` | Grid, Icon, Segment, Header, Dropdown, Divider, Button, Checkbox, Sidebar, DropdownProps | NOT STARTED | 241 + | `components/CleanComponent/CleanSidebar.tsx` | (need to read) | NOT STARTED | 242 + | `components/CleanComponent/index.tsx` | Grid, Button, Icon, Segment, Header, Dropdown, Sidebar, SidebarPusher, Divider, DropdownProps | NOT STARTED | 243 + | `components/CollectComponent/ConnectModal.tsx` | Modal, Button, Segment, List, Grid, Divider | NOT STARTED | 244 + | `components/CollectComponent/HelpSidebar.tsx` | (need to read) | NOT STARTED | 245 + | `components/CollectComponent/PreTestComponent.tsx` | (need to read) | NOT STARTED | 246 + | `components/CollectComponent/RunComponent.tsx` | (need to read) | NOT STARTED | 247 + | `components/CollectComponent/index.tsx` | (passes through, may be minimal) | NOT STARTED | 248 + | `components/DesignComponent/CustomDesignComponent.tsx` | (need to read) | NOT STARTED | 249 + | `components/DesignComponent/ParamSlider.tsx` | (need to read) | NOT STARTED | 250 + | `components/DesignComponent/StimuliDesignColumn.tsx` | (need to read) | NOT STARTED | 251 + | `components/DesignComponent/StimuliRow.tsx` | (need to read) | NOT STARTED | 252 + | `components/DesignComponent/index.tsx` | (need to read) | NOT STARTED | 253 + | `components/EEGExplorationComponent.tsx` | Grid, Button, Header, Segment, Image, Divider | NOT STARTED | 254 + | `components/HomeComponent/ExperimentCard.tsx` | (need to read) | NOT STARTED | 255 + | `components/HomeComponent/OverviewComponent.tsx` | (need to read) | NOT STARTED | 256 + | `components/HomeComponent/index.tsx` | Grid, Button, Header, Image, Table | NOT STARTED | 257 + | `components/InputCollect.tsx` | (need to read) | NOT STARTED | 258 + | `components/InputModal.tsx` | (need to read) | NOT STARTED | 259 + | `components/PreviewButtonComponent.tsx` | (need to read) | NOT STARTED | 260 + | `components/PreviewExperimentComponent.tsx` | (need to read) | NOT STARTED | 261 + | `components/PyodidePlotWidget.tsx` | (need to read) | NOT STARTED | 262 + | `components/SecondaryNavComponent/SecondaryNavSegment.tsx` | (need to read) | NOT STARTED | 263 + | `components/SecondaryNavComponent/index.tsx` | (need to read) | NOT STARTED | 264 + | `components/SignalQualityIndicatorComponent.tsx` | (need to read) | NOT STARTED | 265 + | `components/TopNavComponent/PrimaryNavSegment.tsx` | (need to read) | NOT STARTED | 266 + | `components/TopNavComponent/index.tsx` | Grid, Segment, Image, Dropdown | NOT STARTED (also being changed in Phase 2) | 267 + 268 + **Replacement mapping:** 269 + | Semantic UI | Replacement | 270 + |-------------|-------------| 271 + | `<Grid>` | `<div className="grid ...">` (Tailwind) | 272 + | `<Grid.Row>` | `<div className="flex ...">` or grid row | 273 + | `<Grid.Column width={N}>` | Tailwind `col-span-N` | 274 + | `<Segment>` | `<div className="p-4 ...">` or `<Card>` | 275 + | `<Button>` | `<Button>` from `./ui/button` | 276 + | `<Button.Group>` | `<div className="flex gap-1">` | 277 + | `<Header as="h1">` | `<h1 className="...">` | 278 + | `<Image src={x}>` | `<img src={x} />` | 279 + | `<Dropdown>` | `<DropdownMenu>` from `./ui/dropdown-menu` | 280 + | `<Modal>` | `<Dialog>` from `./ui/dialog` | 281 + | `<List>/<List.Item>` | `<ul>/<li>` | 282 + | `<Divider>` | `<hr />` or `<div className="my-4">` | 283 + | `<Checkbox>` | `<input type="checkbox" />` | 284 + | `<Icon name="x">` | inline SVG or FontAwesome (already installed) | 285 + | `<Sidebar>/<Sidebar.Pushable>` | `<div>` with conditional className | 286 + | `<Table>` etc. | `<Table>` from `./ui/table` or HTML table | 287 + | `DropdownProps`, `SemanticICONS` | Remove type imports; use plain React event types | 288 + 289 + --- 290 + 291 + ## Phase 5: Final Build Verification 292 + 293 + **Status: PENDING — run `npm install` then `npm run build`** 294 + 295 + - Run `npm run build` — should complete without errors 296 + - Run `npm test` — all Vitest suites pass 297 + - Run `npm run package` — Electron app packages successfully 298 + 299 + --- 300 + 301 + ## Dependency Change Summary 302 + 303 + ### To remove 304 + | Package | Location | 305 + |---------|----------| 306 + | `connected-react-router` | dependencies | 307 + | `history` | dependencies | 308 + | `react-router` | dependencies (merged into react-router-dom v6) | 309 + | `semantic-ui-react` | dependencies | 310 + | `semantic-ui-css` | dependencies | 311 + | `jest` | devDependencies | 312 + | `@types/jest` | devDependencies | 313 + | `@types/history` | devDependencies | 314 + | `@types/react-router` | devDependencies | 315 + | `@types/react-router-dom` | devDependencies | 316 + | `identity-obj-proxy` | devDependencies | 317 + | `react-test-renderer` | devDependencies | 318 + 319 + ### To add 320 + | Package | Location | Purpose | 321 + |---------|----------|---------| 322 + | `vitest` | devDependencies | test runner | 323 + | `@testing-library/react` | devDependencies | React test utilities | 324 + | `@testing-library/jest-dom` | devDependencies | DOM matchers | 325 + | `jsdom` | devDependencies | DOM env for tests | 326 + | `react-router-dom` (upgrade to v6) | dependencies | routing | 327 + | `tailwindcss` | devDependencies | CSS framework | 328 + | `postcss` | devDependencies | CSS processing | 329 + | `autoprefixer` | devDependencies | CSS vendor prefixes | 330 + | `@radix-ui/react-dialog` | dependencies | Dialog primitive | 331 + | `@radix-ui/react-dropdown-menu` | dependencies | Dropdown primitive | 332 + | `@radix-ui/react-slot` | dependencies | Slot primitive (shadcn) | 333 + | `class-variance-authority` | dependencies | CVA for Button variants | 334 + | `clsx` | dependencies | className utility | 335 + | `tailwind-merge` | dependencies | Tailwind class merging | 336 + 337 + --- 338 + 339 + ## Session Notes 340 + 341 + ### Session 1 (2026-03-07) 342 + - Read all relevant source files 343 + - Created this progress tracker 344 + - No code changes made yet 345 + - Ready to begin Phase 1 (Vitest) in next session 346 + 347 + ### Session 2 (2026-03-07) 348 + - Completed Phases 1–4 in full 349 + - Phase 1: Vitest config, test-setup.ts, App.test.tsx, CI workflow update 350 + - Phase 2: Removed connected-react-router, upgraded to React Router v6, NavigationTracker pattern, withNavigate HOC for HomeContainer + ExperimentDesignContainer, routerActions.ts 351 + - Phase 3: Pyodide version 0.21→0.27, fixed sns.tsplot with manual bootstrap CI in utils.py 352 + - Phase 4: Created Tailwind config, postcss config, shadcn/ui components (button, card, dialog, dropdown-menu, table, utils). Replaced all 26 semantic-ui-react component files. Removed semantic-ui import from app.global.css, added Tailwind directives. 353 + - Next step: Run `npm install` then `npm run build` to verify (Phase 5)
+3 -3
internals/scripts/InstallPyodide.js
··· 6 6 import url from 'url'; 7 7 import bz2 from 'unbzip2-stream'; 8 8 9 - const PYODIDE_VERSION = '0.21.0'; 10 - const TAR_NAME = `pyodide-build-${PYODIDE_VERSION}.tar.bz2`; 11 - const TAR_URL = `https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/pyodide-build-${PYODIDE_VERSION}.tar.bz2`; 9 + const PYODIDE_VERSION = '0.27.0'; 10 + const TAR_NAME = `pyodide-${PYODIDE_VERSION}.tar.bz2`; 11 + const TAR_URL = `https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/pyodide-${PYODIDE_VERSION}.tar.bz2`; 12 12 const PYODIDE_DIR = 'src/renderer/utils/pyodide/src/'; 13 13 14 14 const writeAndUnzipFile = (response) => {
+18 -37
package.json
··· 22 22 "lint-styles-fix": "npm run lint-styles -- --fix; exit 0", 23 23 "postlint-fix": "prettier --single-quote --write '**/*.{ts,tsx,js,jsx,json,html,css,less,scss,yml}'", 24 24 "postlint-styles-fix": "prettier --single-quote --write '**/*.{css,scss}'", 25 - "test": "cross-env jest --passWithNoTests", 25 + "test": "vitest run", 26 + "test:watch": "vitest", 26 27 "test-all": "npm run lint && npm run lint-styles && npm run typecheck && npm run build && npm test", 27 28 "tsc": "tsc --noEmit", 28 29 "typecheck": "tsc --noEmit" ··· 135 136 "lab.js" 136 137 ], 137 138 "homepage": "https://github.com/makebrainwaves/BrainWaves/", 138 - "jest": { 139 - "testEnvironment": "node", 140 - "moduleNameMapper": { 141 - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js", 142 - "\\.(css|less|sass|scss)$": "identity-obj-proxy" 143 - }, 144 - "moduleFileExtensions": [ 145 - "js", 146 - "jsx", 147 - "ts", 148 - "tsx", 149 - "json" 150 - ], 151 - "moduleDirectories": [ 152 - "node_modules" 153 - ] 154 - }, 155 139 "devDependencies": { 156 140 "@babel/plugin-proposal-class-properties": "^7.10.4", 157 141 "@babel/plugin-proposal-decorators": "^7.10.5", 158 - "@types/history": "^4.7.6", 159 - "@types/jest": "^30.0.0", 142 + "@testing-library/jest-dom": "^6.4.0", 143 + "@testing-library/react": "^16.0.0", 160 144 "@types/node": "^22.12.0", 161 145 "@types/react": "^18.x", 162 146 "@types/react-dom": "^18.x", 163 147 "@types/react-plotly.js": "^2.6.4", 164 - "@types/react-router": "^5.1.8", 165 - "@types/react-router-dom": "^5.1.5", 166 148 "@types/redux-logger": "^3.0.8", 167 149 "@typescript-eslint/eslint-plugin": "^8.56.0", 168 150 "@typescript-eslint/parser": "^8.56.0", 169 151 "@vitejs/plugin-react": "^4.0.0", 152 + "autoprefixer": "^10.4.0", 170 153 "chalk": "^4.1.0", 171 154 "cross-env": "^7.0.0", 172 155 "electron": "^39.6.1", ··· 180 163 "eslint-plugin-react": "^7.37.5", 181 164 "eslint-plugin-react-hooks": "^5.2.0", 182 165 "husky": "^8.0.0", 183 - "identity-obj-proxy": "^3.0.0", 184 - "jest": "^30.2.0", 166 + "jsdom": "^25.0.0", 185 167 "lint-staged": "^16.2.7", 168 + "postcss": "^8.4.0", 186 169 "prettier": "^3.8.1", 187 - "react-test-renderer": "^18.x", 188 170 "redux-logger": "^3.0.6", 189 171 "rimraf": "^6.1.3", 190 172 "sass": "^1.50.0", 191 173 "stylelint": "^17.3.0", 192 174 "stylelint-config-standard": "^40.0.0", 175 + "tailwindcss": "^3.4.0", 193 176 "tar-fs": "^2.1.4", 194 177 "typescript": "^5.0.0", 195 178 "unbzip2-stream": "1.4.2", 196 - "vite": "^7.3.1" 179 + "vite": "^7.3.1", 180 + "vitest": "^3.0.0" 197 181 }, 198 182 "dependencies": { 199 183 "@electron-toolkit/preload": "^3.0.2", ··· 201 185 "@fortawesome/fontawesome-free": "^5.13.0", 202 186 "@neurosity/pipes": "^5.2.1", 203 187 "@nteract/transforms": "^3.2.0", 188 + "@radix-ui/react-dialog": "^1.1.0", 189 + "@radix-ui/react-dropdown-menu": "^2.1.0", 190 + "@radix-ui/react-slot": "^1.1.0", 204 191 "@reduxjs/toolkit": "^2.11.2", 205 192 "ajv": "^8.18.0", 206 193 "caniuse-lite": "^1.0.30001241", 207 - "connected-react-router": "^6.6.1", 194 + "class-variance-authority": "^0.7.0", 195 + "clsx": "^2.1.0", 208 196 "d3": "^7.9.0", 209 197 "dayjs": "^1.11.19", 210 198 "electron-log": "^5.4.3", 211 199 "electron-updater": "^6.8.3", 212 200 "events": "^3.3.0", 213 - "history": "^4.7.2", 214 201 "lab.js": "23.0.0-alpha4", 215 202 "lodash": "^4.17.15", 216 203 "mkdirp": "^1.0.4", ··· 224 211 "react-dom": "^18.x", 225 212 "react-plotly.js": "^2.6.0", 226 213 "react-redux": "^9.2.0", 227 - "react-router": "^5.2.0", 228 - "react-router-dom": "^5.2.0", 214 + "react-router-dom": "^6.26.0", 229 215 "react-toastify": "^11.x", 230 216 "redux": "^5.x", 231 217 "redux-observable": "^2.0.0-rc.2", 232 218 "redux-thunk": "^2.3.0", 233 219 "rxjs": "^7.8.2", 234 - "semantic-ui-css": "^2.4.1", 235 - "semantic-ui-react": "^2.1.5", 236 220 "simple-statistics": "^7.1.0", 237 221 "simplify-js": "^1.2.4", 222 + "tailwind-merge": "^2.3.0", 238 223 "typesafe-actions": "^5.1.0", 239 224 "ws": "^8.19.0" 240 225 }, 241 226 "overrides": { 242 227 "react": "^18.x", 243 - "react-dom": "^18.x", 244 - "connected-react-router": { 245 - "react-redux": "^9.2.0", 246 - "redux": "^5.x" 247 - } 228 + "react-dom": "^18.x" 248 229 }, 249 230 "engines": { 250 231 "node": "^20.19.0 || >=22.12.0",
+6
postcss.config.js
··· 1 + module.exports = { 2 + plugins: { 3 + tailwindcss: {}, 4 + autoprefixer: {}, 5 + }, 6 + };
+14
src/renderer/App.test.tsx
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { render, screen } from '@testing-library/react'; 3 + import React from 'react'; 4 + 5 + function HelloWorld() { 6 + return <div data-testid="hello">Hello, BrainWaves!</div>; 7 + } 8 + 9 + describe('sanity check', () => { 10 + it('renders a basic component', () => { 11 + render(<HelloWorld />); 12 + expect(screen.getByTestId('hello')).toBeInTheDocument(); 13 + }); 14 + });
+5
src/renderer/actions/routerActions.ts
··· 1 + import { createAction } from '@reduxjs/toolkit'; 2 + 3 + export const RouterActions = { 4 + RouteChanged: createAction<string, 'ROUTE_CHANGED'>('ROUTE_CHANGED'), 5 + } as const;
+3 -143
src/renderer/app.global.css
··· 1 - @import 'semantic-ui-css/semantic.min.css'; 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities; 2 4 @import 'rc-slider/assets/index.css'; 3 5 @import 'react-toastify/dist/ReactToastify.css'; 4 6 @import 'lab.js/dist/css/lab.css'; ··· 66 68 width: 100% !important; 67 69 } 68 70 69 - /* Semantic UI OVerrides */ 70 - 71 - .ui.form .field > label { 72 - font-size: 14px; 73 - } 74 - 75 - .ui.card { 76 - width: auto; 77 - } 78 - 79 - .ui.avatar.image { 80 - width: auto; 81 - } 82 - 83 - .ui.modal > .image.content { 84 - display: block; 85 - } 86 - 87 71 .info { 88 72 font-size: 16px !important; 89 73 letter-spacing: 0.64px !important; 90 - } 91 - 92 - .ui.button.export { 93 - float: right; 94 - font-size: 18px; 95 - border-radius: 4px; 96 - background-color: rgba(255, 255, 255, 0); 97 - margin: 0 !important; 98 - padding: 0 !important; 99 - } 100 - 101 - .ui.button.tertiary { 102 - border-left: 2px solid #ebe8e5 !important; 103 - font-size: 18px; 104 - font-weight: normal !important; 105 - color: #007c70 !important; 106 - border-radius: 4px; 107 - border: 2px solid #ebe8e5; 108 - background-color: rgba(255, 255, 255, 0); 109 - } 110 - 111 - .ui.button.tertiary.toggle.active { 112 - font-size: 18px; 113 - font-weight: bold !important; 114 - color: #007c70 !important; 115 - border-radius: 4px; 116 - border: 2px solid #ebe8e5; 117 - background-color: white !important; 118 - } 119 - 120 - .ui.secondary.button { 121 - font-size: 18px; 122 - font-weight: normal !important; 123 - color: #007c70 !important; 124 - border-radius: 4px; 125 - border: 2px solid #007c70; 126 - background-color: rgba(255, 255, 255, 0); 127 - } 128 - 129 - .ui.secondary.button:hover { 130 - background-color: #e0fff5; 131 - } 132 - 133 - .ui.secondary.button:focus { 134 - background-color: #e0fff5; 135 - } 136 - 137 - .ui.secondary.button:active { 138 - background-color: #e0fff5; 139 - } 140 - 141 - .ui.primary.button { 142 - font-size: 18px; 143 - font-weight: normal !important; 144 - color: #fff; 145 - border: 2px solid #007c70; 146 - background-color: #007c70; 147 - border-radius: 4px; 148 - } 149 - 150 - .ui.primary.button:hover { 151 - background-color: #0b4d37; 152 - } 153 - 154 - .ui.primary.button:focus { 155 - background-color: #0b4d37; 156 - } 157 - 158 - .ui.primary.button:active { 159 - background-color: #0b4d37; 160 - } 161 - 162 - .ui.negative.button { 163 - font-size: 18px; 164 - font-weight: normal !important; 165 - border: 2px solid #ca1010; 166 - border-radius: 4px; 167 - } 168 - 169 - .ui.dimmer { 170 - /* top: 60px !important; */ 171 - background-image: linear-gradient(#43968d, #007c70); 172 - } 173 - 174 - .ui.dimmer.inverted { 175 - background-image: none; 176 - } 177 - 178 - .ui.modal > .header { 179 - border-style: none !important; 180 - } 181 - 182 - .ui.modal > .actions { 183 - border-style: none !important; 184 - } 185 - 186 - .ui.sidebar { 187 - box-shadow: none !important; 188 - border: 2px solid #e6e6e6 !important; 189 - transition: 190 - width 0.5s ease, 191 - transform 0.5s ease, 192 - -webkit-transform 0.5s ease !important; 193 - } 194 - 195 - .pusher { 196 - width: 100%; 197 - transition: 198 - width 0.5s ease, 199 - transform 0.5s ease, 200 - -webkit-transform 0.5s ease !important; 201 - float: right; 202 - } 203 - 204 - .ui.sidebar.visible ~ .pusher { 205 - width: calc(100% - 350px); 206 - } 207 - 208 - .ui.selection.dropdown { 209 - min-width: 50px !important; 210 - } 211 - 212 - .ui.multiple.dropdown > .label { 213 - font-size: 0.8em !important; 214 74 } 215 75 216 76 /* Webview-related styles */
+127 -207
src/renderer/components/AnalyzeComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { 3 - Grid, 4 - Icon, 5 - Segment, 6 - Header, 7 - Dropdown, 8 - Divider, 9 - Button, 10 - Checkbox, 11 - Sidebar, 12 - DropdownProps, 13 - } from 'semantic-ui-react'; 14 - import { isNil, isArray, isString } from 'lodash'; 2 + import { Button } from './ui/button'; 3 + import { isNil } from 'lodash'; 15 4 import Plot from 'react-plotly.js'; 16 5 import type { Data as PlotlyData } from 'plotly.js'; 17 6 import styles from './styles/common.module.css'; ··· 184 173 return subjects.reduce((acc, curr) => `${acc}-${curr}`); 185 174 }; 186 175 187 - handleDatasetChange(event: Record<string, any>, data: DropdownProps) { 188 - if (isStringArray(data.value)) { 189 - this.setState({ 190 - selectedFilePaths: data.value, 191 - selectedSubjects: getSubjectNamesFromFiles(data.value), 192 - }); 193 - this.props.PyodideActions.LoadCleanedEpochs(data.value); 194 - } 176 + handleDatasetChange(e: React.ChangeEvent<HTMLSelectElement>) { 177 + const values = Array.from(e.target.selectedOptions, (o) => o.value); 178 + this.setState({ 179 + selectedFilePaths: values, 180 + selectedSubjects: getSubjectNamesFromFiles(values), 181 + }); 182 + this.props.PyodideActions.LoadCleanedEpochs(values); 195 183 } 196 184 197 - handleBehaviorDatasetChange( 198 - event: Record<string, any>, 199 - data: Record<string, any> 200 - ) { 185 + handleBehaviorDatasetChange(e: React.ChangeEvent<HTMLSelectElement>) { 186 + const values = Array.from(e.target.selectedOptions, (o) => o.value); 201 187 const aggregatedData = aggregateDataForPlot( 202 - readBehaviorData(data.value), 188 + readBehaviorData(values), 203 189 this.state.selectedDependentVariable, 204 190 this.state.removeOutliers, 205 191 this.state.showDataPoints, 206 192 this.state.displayMode 207 193 ); 208 - if (!aggregatedData) { 209 - return; 210 - } 194 + if (!aggregatedData) return; 211 195 const { dataToPlot, layout } = aggregatedData; 212 196 this.setState({ 213 - selectedBehaviorFilePaths: data.value, 214 - selectedSubjects: getSubjectNamesFromFiles(data.value), 197 + selectedBehaviorFilePaths: values, 198 + selectedSubjects: getSubjectNamesFromFiles(values), 215 199 dataToPlot, 216 200 layout, 217 201 }); ··· 230 214 } 231 215 } 232 216 233 - handleDependentVariableChange( 234 - event: Record<string, any>, 235 - data: Record<string, any> 236 - ) { 217 + handleDependentVariableChange(e: React.ChangeEvent<HTMLSelectElement>) { 218 + const value = e.target.value; 237 219 const aggregatedData = aggregateDataForPlot( 238 220 readBehaviorData(this.state.selectedBehaviorFilePaths), 239 - data.value, 221 + value, 240 222 this.state.removeOutliers, 241 223 this.state.showDataPoints, 242 224 this.state.displayMode 243 225 ); 244 - if (!aggregatedData) { 245 - return; 246 - } 226 + if (!aggregatedData) return; 247 227 const { dataToPlot, layout } = aggregatedData; 248 - this.setState({ 249 - selectedDependentVariable: data.value, 250 - dataToPlot, 251 - layout, 252 - }); 228 + this.setState({ selectedDependentVariable: value, dataToPlot, layout }); 253 229 } 254 230 255 - handleRemoveOutliers(event: Record<string, any>, data: Record<string, any>) { 231 + handleRemoveOutliers() { 256 232 const aggregatedData = aggregateDataForPlot( 257 233 readBehaviorData(this.state.selectedBehaviorFilePaths), 258 234 this.state.selectedDependentVariable, ··· 260 236 this.state.showDataPoints, 261 237 this.state.displayMode 262 238 ); 263 - if (!aggregatedData) { 264 - return; 265 - } 239 + if (!aggregatedData) return; 266 240 const { dataToPlot, layout } = aggregatedData; 267 - this.setState({ 268 - removeOutliers: !this.state.removeOutliers, 269 - dataToPlot, 270 - layout, 271 - helpMode: 'outliers', 272 - }); 241 + this.setState({ removeOutliers: !this.state.removeOutliers, dataToPlot, layout, helpMode: 'outliers' }); 273 242 } 274 243 275 - handleDataPoints(event: Record<string, any>, data: Record<string, any>) { 244 + handleDataPoints() { 276 245 const aggregatedData = aggregateDataForPlot( 277 246 readBehaviorData(this.state.selectedBehaviorFilePaths), 278 247 this.state.selectedDependentVariable, ··· 280 249 !this.state.showDataPoints, 281 250 this.state.displayMode 282 251 ); 283 - if (!aggregatedData) { 284 - return; 285 - } 252 + if (!aggregatedData) return; 286 253 const { dataToPlot, layout } = aggregatedData; 287 - this.setState({ 288 - showDataPoints: !this.state.showDataPoints, 289 - dataToPlot, 290 - layout, 291 - }); 254 + this.setState({ showDataPoints: !this.state.showDataPoints, dataToPlot, layout }); 292 255 } 293 256 294 257 handleDisplayModeChange(displayMode) { ··· 349 312 (infoObj) => 350 313 infoObj.name !== 'Drop Percentage' && infoObj.name !== 'Total Epochs' 351 314 ).length; 352 - let colors; 353 - if (numberConditions === 4) { 354 - colors = ['red', 'yellow', 'green', 'blue']; 355 - } else { 356 - colors = ['red', 'green', 'teal', 'orange']; 357 - } 315 + const colors = numberConditions === 4 316 + ? ['red', 'yellow', 'green', 'blue'] 317 + : ['red', 'green', 'teal', 'orange']; 358 318 return ( 359 319 <div> 360 320 {this.props.epochsInfo ··· 364 324 infoObj.name !== 'Total Epochs' 365 325 ) 366 326 .map((infoObj, index) => ( 367 - <> 368 - <Header as="h4">{infoObj.name}</Header> 369 - <Icon name="circle" color={colors[index]} /> 370 - {infoObj.value} 371 - </> 327 + <div key={String(infoObj.name)}> 328 + <h4>{infoObj.name}</h4> 329 + <span style={{ color: colors[index] }}>●</span> 330 + {' '}{infoObj.value} 331 + </div> 372 332 ))} 373 333 </div> 374 334 ); ··· 422 382 423 383 renderHelp(header: string, content: string) { 424 384 return ( 425 - <> 426 - <Segment basic className={styles.helpContent}> 427 - <Button 428 - circular 429 - size="large" 430 - floated="right" 431 - icon="x" 432 - className={styles.closeButton} 433 - onClick={this.toggleDisplayInfoVisibility} 434 - /> 435 - <Header className={styles.helpHeader} as="h1"> 436 - {header} 437 - </Header> 438 - {content} 439 - </Segment> 440 - </> 385 + <div className={styles.helpContent}> 386 + <button 387 + className={styles.closeButton} 388 + onClick={this.toggleDisplayInfoVisibility} 389 + aria-label="Close" 390 + >✕</button> 391 + <h1 className={styles.helpHeader}>{header}</h1> 392 + {content} 393 + </div> 441 394 ); 442 395 } 443 396 ··· 447 400 default: 448 401 return ( 449 402 <> 450 - <Grid.Column width={4}> 451 - <Segment basic textAlign="left" className={styles.infoSegment}> 452 - <Header as="h1">Overview</Header> 403 + <div className="w-1/3 p-2 text-left"> 404 + <div className={styles.infoSegment}> 405 + <h1>Overview</h1> 453 406 <p> 454 407 Load cleaned datasets from different subjects and view how the 455 408 EEG differs between electrodes 456 409 </p> 457 - <Header as="h4">Select Clean Datasets</Header> 458 - <Dropdown 459 - fluid 410 + <h4>Select Clean Datasets</h4> 411 + <select 460 412 multiple 461 - selection 462 - closeOnChange 413 + className="w-full border border-gray-300 rounded p-1" 463 414 value={this.state.selectedFilePaths} 464 - options={this.state.eegFilePaths.map( 465 - (eegFilePath) => eegFilePath.value 466 - )} 467 415 onChange={this.handleDatasetChange} 468 - /> 416 + > 417 + {this.state.eegFilePaths.map((eegFilePath) => ( 418 + <option key={eegFilePath.key} value={String(eegFilePath.value)}> 419 + {eegFilePath.text} 420 + </option> 421 + ))} 422 + </select> 469 423 {this.renderEpochLabels()} 470 - </Segment> 471 - </Grid.Column> 472 - <Grid.Column width={8}> 424 + </div> 425 + </div> 426 + <div className="w-2/3 p-2"> 473 427 <PyodidePlotWidget 474 428 title={this.props.title} 475 429 imageTitle={`${this.concatSubjectNames( ··· 477 431 )}-Topoplot`} 478 432 plotMIMEBundle={this.props.topoPlot} 479 433 /> 480 - </Grid.Column> 434 + </div> 481 435 </> 482 436 ); 483 437 case ANALYZE_STEPS.ERP: 484 438 return ( 485 439 <> 486 - <Grid.Column width={4} className={styles.analyzeColumn}> 487 - <Segment basic textAlign="left" className={styles.infoSegment}> 488 - <Header as="h1">ERP</Header> 440 + <div className={['w-1/3 p-2 text-left', styles.analyzeColumn].join(' ')}> 441 + <div className={styles.infoSegment}> 442 + <h1>ERP</h1> 489 443 <p> 490 444 The event-related potential represents EEG activity elicited 491 445 by a particular sensory event ··· 494 448 channelinfo={this.props.channelInfo} 495 449 onChannelClick={this.handleChannelSelect} 496 450 /> 497 - <Divider hidden /> 451 + <div className="my-2" /> 498 452 {this.renderEpochLabels()} 499 - </Segment> 500 - </Grid.Column> 501 - <Grid.Column width={8}> 453 + </div> 454 + </div> 455 + <div className="w-2/3 p-2"> 502 456 <PyodidePlotWidget 503 457 title={this.props.title} 504 458 imageTitle={`${this.concatSubjectNames( ··· 506 460 )}-${this.state.selectedChannel}-ERP`} 507 461 plotMIMEBundle={this.props.erpPlot} 508 462 /> 509 - </Grid.Column> 463 + </div> 510 464 </> 511 465 ); 512 466 case ANALYZE_STEPS.BEHAVIOR: 513 467 return ( 514 468 <> 515 - <Grid.Column width={4}> 516 - <Segment basic textAlign="left" className={styles.infoSegment}> 517 - <Header as="h1">Overview</Header> 469 + <div className="w-1/3 p-2 text-left"> 470 + <div className={styles.infoSegment}> 471 + <h1>Overview</h1> 518 472 <p> 519 - Load datasets from different subjects and view behavioral 520 - results 473 + Load datasets from different subjects and view behavioral results 521 474 </p> 522 - 523 - <div> 524 - <span className="ui header">Datasets</span> 525 - <Button 526 - className="export" 527 - onClick={this.saveSelectedDatasets} 528 - > 529 - <Icon name="download" /> 530 - Export 475 + <div className="flex items-center justify-between mb-2"> 476 + <span className="font-semibold">Datasets</span> 477 + <Button variant="outline" size="sm" onClick={this.saveSelectedDatasets}> 478 + ↓ Export 531 479 </Button> 532 480 </div> 533 - <p /> 534 - 535 - <Dropdown 536 - fluid 481 + <select 537 482 multiple 538 - selection 539 - search 540 - closeOnChange 483 + className="w-full border border-gray-300 rounded p-1" 541 484 value={this.state.selectedBehaviorFilePaths} 542 - options={this.state.behaviorFilePaths} 543 485 onChange={this.handleBehaviorDatasetChange} 544 486 onClick={this.handleDropdownClick} 545 - /> 546 - <p /> 547 - <Divider hidden /> 548 - <span className="ui header">Dependent Variable</span> 549 - <p /> 550 - <Dropdown 551 - fluid 552 - selection 553 - closeOnChange 487 + > 488 + {this.state.behaviorFilePaths.map((fp) => ( 489 + <option key={fp.key} value={fp.value}>{fp.text}</option> 490 + ))} 491 + </select> 492 + <div className="my-2" /> 493 + <p className="font-semibold">Dependent Variable</p> 494 + <select 495 + className="w-full border border-gray-300 rounded p-1" 554 496 value={this.state.selectedDependentVariable} 555 - options={this.state.dependentVariables} 556 497 onChange={this.handleDependentVariableChange} 557 - /> 558 - </Segment> 559 - </Grid.Column> 560 - <Grid.Column 561 - width={12} 562 - style={{ 563 - overflow: 'auto', 564 - maxHeight: 650, 565 - display: 'grid', 566 - justifyContent: 'center', 567 - }} 498 + > 499 + {this.state.dependentVariables.map((dv) => ( 500 + <option key={dv.key} value={dv.value}>{dv.text}</option> 501 + ))} 502 + </select> 503 + </div> 504 + </div> 505 + <div 506 + className="w-2/3 p-2" 507 + style={{ overflow: 'auto', maxHeight: 650 }} 568 508 > 569 - <Segment basic textAlign="left" className={styles.plotSegment}> 509 + <div className={['text-left', styles.plotSegment].join(' ')}> 570 510 <Plot data={this.state.dataToPlot} layout={this.state.layout} /> 571 - <p /> 572 - <Checkbox 573 - checked={this.state.removeOutliers} 574 - label="Remove Response Time Outliers" 575 - onChange={this.handleRemoveOutliers} 576 - /> 577 - 578 - <p /> 579 - <Button.Group> 580 - <Button 581 - className="tertiary" 582 - toggle 583 - active={this.state.displayMode === 'datapoints'} 511 + <div className="my-2" /> 512 + <label className="flex items-center gap-2"> 513 + <input 514 + type="checkbox" 515 + checked={this.state.removeOutliers} 516 + onChange={this.handleRemoveOutliers} 517 + /> 518 + Remove Response Time Outliers 519 + </label> 520 + <div className="my-2" /> 521 + <div className="flex gap-1"> 522 + <button 523 + className={['px-3 py-1 border rounded', this.state.displayMode === 'datapoints' ? 'bg-gray-200' : ''].join(' ')} 584 524 onClick={() => this.handleDisplayModeChange('datapoints')} 585 525 > 586 526 Data Points 587 - </Button> 588 - <Button 589 - className="tertiary" 590 - toggle 591 - active={this.state.displayMode === 'errorbars'} 527 + </button> 528 + <button 529 + className={['px-3 py-1 border rounded', this.state.displayMode === 'errorbars' ? 'bg-gray-200' : ''].join(' ')} 592 530 onClick={() => this.handleDisplayModeChange('errorbars')} 593 531 > 594 532 Bar Graph 595 - </Button> 596 - <Button 597 - className="tertiary" 598 - toggle 599 - active={this.state.displayMode === 'whiskers'} 533 + </button> 534 + <button 535 + className={['px-3 py-1 border rounded', this.state.displayMode === 'whiskers' ? 'bg-gray-200' : ''].join(' ')} 600 536 onClick={() => this.handleDisplayModeChange('whiskers')} 601 537 > 602 538 Box Plot 603 - </Button> 604 - </Button.Group> 605 - 539 + </button> 540 + </div> 606 541 <HelpButton onClick={this.toggleDisplayInfoVisibility} /> 607 - 608 - <Sidebar 609 - width="wide" 610 - direction="right" 611 - as={Segment} 612 - visible={this.state.isSidebarVisible} 613 - > 614 - <Segment basic padded vertical className={styles.helpSidebar}> 542 + {this.state.isSidebarVisible && ( 543 + <div className={styles.helpSidebar}> 615 544 {this.renderHelpContent()} 616 - </Segment> 617 - </Sidebar> 618 - </Segment> 619 - </Grid.Column> 545 + </div> 546 + )} 547 + </div> 548 + </div> 620 549 </> 621 550 ); 622 551 } ··· 635 564 activeStep={this.state.activeStep} 636 565 onStepClick={this.handleStepClick} 637 566 /> 638 - <Grid 639 - columns="equal" 640 - textAlign="center" 641 - verticalAlign="top" 642 - className={styles.contentGrid} 643 - > 567 + <div className={['flex items-start', styles.contentGrid].join(' ')}> 644 568 {this.renderSectionContent()} 645 - </Grid> 569 + </div> 646 570 </div> 647 571 ); 648 572 } 649 573 } 650 - 651 - function isStringArray(data: any): data is string[] { 652 - return isArray(data.value) && data.value.every(isString); 653 - }
+26 -46
src/renderer/components/CleanComponent/CleanSidebar.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { Segment, Header, Menu, Icon, Button, Grid } from 'semantic-ui-react'; 2 + import { Button } from '../ui/button'; 3 3 import styles from '../styles/common.module.css'; 4 4 5 5 enum HELP_STEP { ··· 58 58 59 59 renderMenu() { 60 60 return ( 61 - <> 62 - <Menu secondary vertical fluid> 63 - <Header className={styles.helpHeader} as="h1"> 64 - What would you like to do? 65 - </Header> 66 - <Menu.Item onClick={this.handleStartSignal}> 67 - <Segment basic className={styles.helpMenuItem}> 68 - <Icon name="star outline" size="large" /> 69 - Improve the signal quality of your sensors 70 - </Segment> 71 - </Menu.Item> 72 - <Menu.Item onClick={this.handleStartLearn}> 73 - <Segment basic className={styles.helpMenuItem}> 74 - <Icon name="exclamation triangle" size="large" /> 75 - Learn about how the subjects movements create noise 76 - </Segment> 77 - </Menu.Item> 78 - </Menu> 79 - </> 61 + <div className="flex flex-col"> 62 + <h1 className={styles.helpHeader}>What would you like to do?</h1> 63 + <div className={styles.helpMenuItem} onClick={this.handleStartSignal}> 64 + ★ Improve the signal quality of your sensors 65 + </div> 66 + <div className={styles.helpMenuItem} onClick={this.handleStartLearn}> 67 + ⚠ Learn about how the subjects movements create noise 68 + </div> 69 + </div> 80 70 ); 81 71 } 82 72 83 73 renderHelp(header: string, content: string) { 84 74 return ( 85 75 <> 86 - <Segment basic className={styles.helpContent}> 87 - <Header className={styles.helpHeader} as="h1"> 88 - {header} 89 - </Header> 76 + <div className={styles.helpContent}> 77 + <h1 className={styles.helpHeader}>{header}</h1> 90 78 {content} 91 - </Segment> 92 - <Grid columns="equal"> 93 - <Grid.Column> 94 - <Button fluid secondary onClick={this.handleBack}> 95 - Back 96 - </Button> 97 - </Grid.Column> 98 - <Grid.Column> 99 - <Button fluid primary onClick={this.handleNext}> 100 - Next 101 - </Button> 102 - </Grid.Column> 103 - </Grid> 79 + </div> 80 + <div className="flex gap-2 mt-4"> 81 + <Button variant="secondary" className="w-full" onClick={this.handleBack}> 82 + Back 83 + </Button> 84 + <Button variant="default" className="w-full" onClick={this.handleNext}> 85 + Next 86 + </Button> 87 + </div> 104 88 </> 105 89 ); 106 90 } ··· 155 139 156 140 render() { 157 141 return ( 158 - <Segment basic padded vertical className={styles.helpSidebar}> 159 - <Button 160 - basic 161 - circular 162 - size="large" 163 - floated="right" 164 - icon="x" 142 + <div className={styles.helpSidebar}> 143 + <button 165 144 className={styles.closeButton} 166 145 onClick={this.props.handleClose} 167 - /> 146 + aria-label="Close" 147 + >✕</button> 168 148 {this.renderHelpContent()} 169 - </Segment> 149 + </div> 170 150 ); 171 151 } 172 152 }
+92 -130
src/renderer/components/CleanComponent/index.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { 3 - Grid, 4 - Button, 5 - Icon, 6 - Segment, 7 - Header, 8 - Dropdown, 9 - Sidebar, 10 - SidebarPusher, 11 - Divider, 12 - DropdownProps, 13 - DropdownItemProps, 14 - SemanticICONS, 15 - } from 'semantic-ui-react'; 16 2 import path from 'pathe'; 17 3 import { Link } from 'react-router-dom'; 18 - import { isNil, isArray, isString } from 'lodash'; 4 + import { isNil, isString } from 'lodash'; 5 + import { Button } from '../ui/button'; 19 6 import styles from '../styles/collect.module.css'; 20 7 import commonStyles from '../styles/common.module.css'; 21 8 import { EXPERIMENTS, DEVICES } from '../../constants/constants'; ··· 36 23 session: number; 37 24 } 38 25 26 + interface DropdownOption { 27 + key: string; 28 + text: string; 29 + value: string; 30 + } 31 + 39 32 interface State { 40 - subjects: Array<DropdownItemProps>; 41 - eegFilePaths: Array<DropdownItemProps>; 33 + subjects: Array<DropdownOption>; 34 + eegFilePaths: Array<DropdownOption>; 42 35 selectedSubject: string; 43 36 selectedFilePaths: Array<string>; 44 37 isSidebarVisible: boolean; 45 38 } 46 39 47 40 export default class Clean extends Component<Props, State> { 48 - icons: SemanticICONS[]; 41 + icons: string[]; 49 42 50 43 constructor(props: Props) { 51 44 super(props); ··· 62 55 this.handleSubjectChange = this.handleSubjectChange.bind(this); 63 56 this.icons = 64 57 props.type === EXPERIMENTS.N170 65 - ? ['smile', 'home', 'x', 'book'] 66 - : ['star', 'star outline', 'x', 'book']; 58 + ? ['😊', '🏠', '✕', '📖'] 59 + : ['★', '☆', '✕', '📖']; 67 60 } 68 61 69 62 async componentDidMount() { ··· 94 87 }); 95 88 } 96 89 97 - handleRecordingChange(event: Record<string, any>, data: DropdownProps) { 98 - const { value } = data; 99 - if (isArray(value)) { 100 - const filePaths = (value as (string | number | boolean)[]).filter(isString) as string[]; 101 - this.setState({ selectedFilePaths: filePaths }); 102 - } 90 + handleRecordingChange(e: React.ChangeEvent<HTMLSelectElement>) { 91 + const filePaths = Array.from(e.target.selectedOptions, (o) => o.value); 92 + this.setState({ selectedFilePaths: filePaths }); 103 93 } 104 94 105 - handleSubjectChange(event: Record<string, any>, data: DropdownProps) { 106 - const { value } = data; 107 - if (!isNil(data) && isString(value)) { 108 - this.setState({ selectedSubject: value as string, selectedFilePaths: [] }); 95 + handleSubjectChange(e: React.ChangeEvent<HTMLSelectElement>) { 96 + const value = e.target.value; 97 + if (!isNil(value) && isString(value)) { 98 + this.setState({ selectedSubject: value, selectedFilePaths: [] }); 109 99 } 110 100 } 111 101 ··· 124 114 this.state.selectedFilePaths.length >= 1 125 115 ) { 126 116 return ( 127 - <Segment basic textAlign="left"> 117 + <div className="text-left"> 128 118 {this.props.epochsInfo.map((infoObj, index) => ( 129 - <Segment key={infoObj.name} basic> 130 - <Icon name={this.icons[index]} /> 131 - {infoObj.name} 119 + <div key={String(infoObj.name)} className="mb-2"> 120 + <span>{this.icons[index]}</span> 121 + {' '}{infoObj.name} 132 122 <p>{infoObj.value}</p> 133 - </Segment> 123 + </div> 134 124 ))} 135 - </Segment> 125 + </div> 136 126 ); 137 127 } 138 128 return <div />; ··· 148 138 if (drop && typeof drop === 'number' && drop >= 2) { 149 139 return ( 150 140 <Link to="/analyze"> 151 - <Button primary>Analyze Dataset</Button> 141 + <Button variant="default">Analyze Dataset</Button> 152 142 </Link> 153 143 ); 154 144 } 155 145 } 156 - return <></>; 146 + return null; 157 147 } 158 148 159 149 render() { 150 + const filteredFilePaths = this.state.eegFilePaths.filter((filepath) => { 151 + const strVal = filepath.value; 152 + const subjectFromFilepath = strVal.split(path.sep)[strVal.split(path.sep).length - 3]; 153 + return this.state.selectedSubject === subjectFromFilepath; 154 + }); 155 + 160 156 return ( 161 - <Sidebar.Pushable basic as={Segment} className={styles.preTestPushable}> 162 - <Sidebar 163 - width="wide" 164 - direction="right" 165 - as={Segment} 166 - visible={this.state.isSidebarVisible} 167 - > 168 - <CleanSidebar handleClose={this.handleSidebarToggle} /> 169 - </Sidebar> 170 - <SidebarPusher> 171 - <Grid 172 - columns="equal" 173 - textAlign="center" 174 - verticalAlign="middle" 175 - className={styles.preTestContainer} 176 - > 177 - <Grid.Row columns="equal"> 178 - <Grid.Column> 179 - <Header as="h1" floated="left"> 180 - Clean 181 - </Header> 182 - </Grid.Column> 183 - </Grid.Row> 184 - <Grid.Row> 185 - <Grid.Column width={6}> 186 - <Segment 187 - basic 188 - textAlign="left" 189 - className={commonStyles.infoSegment} 157 + <div className={['relative flex', styles.preTestPushable].join(' ')}> 158 + {this.state.isSidebarVisible && ( 159 + <div className="absolute right-0 top-0 h-full w-64 z-10"> 160 + <CleanSidebar handleClose={this.handleSidebarToggle} /> 161 + </div> 162 + )} 163 + <div className={['flex-1', styles.preTestContainer].join(' ')}> 164 + <div className="flex items-center mb-4"> 165 + <h1>Clean</h1> 166 + </div> 167 + <div className="flex gap-4"> 168 + <div className={['w-6/12 text-left', commonStyles.infoSegment].join(' ')}> 169 + <h1>Select & Clean</h1> 170 + <p> 171 + Ready to clean some data? Select a subject and one or more 172 + EEG recordings, then launch the editor 173 + </p> 174 + <h4>Select Subject</h4> 175 + <select 176 + className="w-full border border-gray-300 rounded p-1 mb-2" 177 + value={this.state.selectedSubject} 178 + onChange={this.handleSubjectChange} 179 + > 180 + {this.state.subjects.map((s) => ( 181 + <option key={s.key} value={s.value}>{s.text}</option> 182 + ))} 183 + </select> 184 + <h4>Select Recordings</h4> 185 + <select 186 + multiple 187 + className="w-full border border-gray-300 rounded p-1" 188 + value={this.state.selectedFilePaths} 189 + onChange={this.handleRecordingChange} 190 + > 191 + {filteredFilePaths.map((fp) => ( 192 + <option key={fp.key} value={fp.value}>{fp.text}</option> 193 + ))} 194 + </select> 195 + <div className="flex gap-2 mt-4"> 196 + <Button variant="secondary" className="w-full" onClick={this.handleLoadData}> 197 + Load Dataset 198 + </Button> 199 + <Button 200 + variant="default" 201 + className="w-full" 202 + disabled={isNil(this.props.epochsInfo)} 203 + onClick={() => this.props.PyodideActions.CleanEpochs()} 190 204 > 191 - <Header as="h1">Select & Clean</Header> 192 - <p> 193 - Ready to clean some data? Select a subject and one or more 194 - EEG recordings, then launch the editor 195 - </p> 196 - <Header as="h4">Select Subject</Header> 197 - <Dropdown 198 - fluid 199 - selection 200 - closeOnChange 201 - value={this.state.selectedSubject} 202 - options={this.state.subjects} 203 - onChange={this.handleSubjectChange} 204 - /> 205 - <Header as="h4">Select Recordings</Header> 206 - <Dropdown 207 - fluid 208 - multiple 209 - selection 210 - closeOnChange 211 - value={this.state.selectedFilePaths} 212 - options={this.state.eegFilePaths.filter((filepath) => { 213 - const val = filepath.value; 214 - if (isString(val)) { 215 - const strVal = val as string; 216 - const subjectFromFilepath = strVal.split( 217 - path.sep 218 - )[strVal.split(path.sep).length - 3]; 219 - return ( 220 - this.state.selectedSubject === subjectFromFilepath 221 - ); 222 - } 223 - return false; 224 - })} 225 - onChange={this.handleRecordingChange} 226 - /> 227 - <Divider hidden section /> 228 - <Grid textAlign="center" columns="equal"> 229 - <Grid.Column> 230 - <Button secondary onClick={this.handleLoadData}> 231 - Load Dataset 232 - </Button> 233 - </Grid.Column> 234 - <Grid.Column> 235 - <Button 236 - primary 237 - disabled={isNil(this.props.epochsInfo)} 238 - onClick={() => this.props.PyodideActions.CleanEpochs()} 239 - > 240 - Clean Data 241 - </Button> 242 - </Grid.Column> 243 - </Grid> 244 - </Segment> 245 - </Grid.Column> 246 - <Grid.Column width={4}> 247 - {this.renderEpochLabels()} 248 - {this.renderAnalyzeButton()} 249 - </Grid.Column> 250 - </Grid.Row> 251 - </Grid> 252 - </SidebarPusher> 253 - </Sidebar.Pushable> 205 + Clean Data 206 + </Button> 207 + </div> 208 + </div> 209 + <div className="w-4/12"> 210 + {this.renderEpochLabels()} 211 + {this.renderAnalyzeButton()} 212 + </div> 213 + </div> 214 + </div> 215 + </div> 254 216 ); 255 217 } 256 218 }
+100 -160
src/renderer/components/CollectComponent/ConnectModal.tsx
··· 1 1 import { Observable } from 'rxjs'; 2 2 import React, { Component } from 'react'; 3 3 import { isNil, debounce } from 'lodash'; 4 - import { History } from 'history'; 5 - import { Modal, Button, Segment, List, Grid, Divider } from 'semantic-ui-react'; 4 + import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog'; 5 + import { Button } from '../ui/button'; 6 6 import { 7 7 DEVICES, 8 8 DEVICE_AVAILABILITY, ··· 14 14 import { DeviceActions } from '../../actions'; 15 15 16 16 interface Props { 17 - history: History; 18 17 open: boolean; 19 18 onClose: () => void; 20 19 connectedDevice: Record<string, any>; ··· 99 98 100 99 renderAvailableDeviceList() { 101 100 return ( 102 - <Segment basic> 103 - <List divided relaxed inverted> 104 - {this.props.availableDevices.map((device) => ( 105 - <List.Item className={styles.deviceItem} key={device.id}> 106 - <List.Icon 107 - link 108 - name={ 109 - this.state.selectedDevice === device 110 - ? 'check circle outline' 111 - : 'circle outline' 112 - } 113 - size="large" 114 - verticalAlign="middle" 115 - onClick={() => this.setState({ selectedDevice: device })} 116 - /> 117 - <List.Content> 118 - <List.Header>{ConnectModal.getDeviceName(device)}</List.Header> 119 - </List.Content> 120 - </List.Item> 121 - ))} 122 - </List> 123 - </Segment> 101 + <ul className="divide-y divide-gray-200"> 102 + {this.props.availableDevices.map((device) => ( 103 + <li 104 + key={device.id} 105 + className={[styles.deviceItem, 'flex items-center gap-2 py-2 cursor-pointer'].join(' ')} 106 + onClick={() => this.setState({ selectedDevice: device })} 107 + > 108 + <span>{this.state.selectedDevice === device ? '✓' : '○'}</span> 109 + <span>{ConnectModal.getDeviceName(device)}</span> 110 + </li> 111 + ))} 112 + </ul> 124 113 ); 125 114 } 126 115 127 116 renderContent() { 128 117 if (this.props.deviceAvailability === DEVICE_AVAILABILITY.SEARCHING) { 129 118 return ( 130 - <> 131 - <Modal.Content className={styles.searchingText}> 132 - Searching for available headset(s)... 133 - </Modal.Content> 134 - </> 119 + <p className={styles.searchingText}> 120 + Searching for available headset(s)... 121 + </p> 135 122 ); 136 123 } 137 124 if (this.props.connectionStatus === CONNECTION_STATUS.CONNECTING) { 138 125 return ( 139 - <> 140 - <Modal.Content className={styles.searchingText}> 141 - Connecting to{' '} 142 - {ConnectModal.getDeviceName(this.state.selectedDevice)} 143 - ... 144 - </Modal.Content> 145 - </> 126 + <p className={styles.searchingText}> 127 + Connecting to {ConnectModal.getDeviceName(this.state.selectedDevice)}... 128 + </p> 146 129 ); 147 130 } 148 131 if (this.state.instructionProgress === INSTRUCTION_PROGRESS.TURN_ON) { 149 132 return ( 150 133 <> 151 - <Modal.Header className={styles.connectHeader}> 152 - Turn your headset on 153 - </Modal.Header> 154 - <Modal.Content> 134 + <h2 className={styles.connectHeader}>Turn your headset on</h2> 135 + <p> 155 136 Make sure your headset is on and fully charged. 156 - <p /> 137 + </p> 138 + <p> 157 139 If the headset needs charging, set the power switch to off and plug 158 140 in the headset. <b>Do not charge the headset while wearing it</b> 159 - </Modal.Content> 160 - <Modal.Content> 161 - <Grid textAlign="center" columns="equal"> 162 - <Grid.Column> 163 - {(this.state.instructionProgress as number) !== 0 && ( 164 - <Button 165 - fluid 166 - className={styles.secondaryButton} 167 - onClick={() => this.handleinstructionProgress(0)} 168 - > 169 - Back 170 - </Button> 171 - )} 172 - </Grid.Column> 173 - <Grid.Column> 174 - <Button 175 - fluid 176 - className={styles.primaryButton} 177 - onClick={() => 178 - this.handleinstructionProgress( 179 - INSTRUCTION_PROGRESS.COMPUTER_CONNECTABILITY 180 - ) 181 - } 182 - > 183 - Next 184 - </Button> 185 - </Grid.Column> 186 - </Grid> 187 - </Modal.Content> 141 + </p> 142 + <div className="flex gap-2 mt-4"> 143 + {(this.state.instructionProgress as number) !== 0 && ( 144 + <Button 145 + variant="secondary" 146 + className={['w-full', styles.secondaryButton].join(' ')} 147 + onClick={() => this.handleinstructionProgress(0)} 148 + > 149 + Back 150 + </Button> 151 + )} 152 + <Button 153 + variant="default" 154 + className={['w-full', styles.primaryButton].join(' ')} 155 + onClick={() => 156 + this.handleinstructionProgress( 157 + INSTRUCTION_PROGRESS.COMPUTER_CONNECTABILITY 158 + ) 159 + } 160 + > 161 + Next 162 + </Button> 163 + </div> 188 164 </> 189 165 ); 190 166 } ··· 194 170 ) { 195 171 return ( 196 172 <> 197 - <Modal.Header className={styles.connectHeader}> 198 - Insert the USB Receiver 199 - </Modal.Header> 200 - <Modal.Content> 173 + <h2 className={styles.connectHeader}>Insert the USB Receiver</h2> 174 + <p> 201 175 Insert the USB receiver into a USB port on your computer. Ensure 202 176 that the LED on the receiver is continously lit or flickering 203 177 rapidly. If it is blinking slowly or not illuminated, remove and 204 178 reinsert the receiver 205 - </Modal.Content> 206 - <Modal.Content> 207 - <Grid textAlign="center" columns="equal"> 208 - <Grid.Column> 209 - <Button 210 - fluid 211 - className={styles.secondaryButton} 212 - onClick={() => 213 - this.handleinstructionProgress(INSTRUCTION_PROGRESS.TURN_ON) 214 - } 215 - > 216 - Back 217 - </Button> 218 - </Grid.Column> 219 - <Grid.Column> 220 - <Button 221 - fluid 222 - className={styles.primaryButton} 223 - onClick={this.handleSearch} 224 - > 225 - Next 226 - </Button> 227 - </Grid.Column> 228 - </Grid> 229 - </Modal.Content> 179 + </p> 180 + <div className="flex gap-2 mt-4"> 181 + <Button 182 + variant="secondary" 183 + className={['w-full', styles.secondaryButton].join(' ')} 184 + onClick={() => 185 + this.handleinstructionProgress(INSTRUCTION_PROGRESS.TURN_ON) 186 + } 187 + > 188 + Back 189 + </Button> 190 + <Button 191 + variant="default" 192 + className={['w-full', styles.primaryButton].join(' ')} 193 + onClick={this.handleSearch} 194 + > 195 + Next 196 + </Button> 197 + </div> 230 198 </> 231 199 ); 232 200 } 233 201 if (this.props.deviceAvailability === DEVICE_AVAILABILITY.AVAILABLE) { 234 202 return ( 235 203 <> 236 - <Modal.Header className={styles.connectHeader}> 237 - Headset(s) found 238 - </Modal.Header> 239 - <Modal.Content> 240 - Please select which headset you would like to connect. 241 - </Modal.Content> 242 - <Modal.Content>{this.renderAvailableDeviceList()}</Modal.Content> 243 - <Divider section hidden /> 244 - <Modal.Content> 245 - <Grid textAlign="center" columns="equal"> 246 - <Grid.Column> 247 - <Button 248 - fluid 249 - className={styles.secondaryButton} 250 - onClick={() => this.handleinstructionProgress(1)} 251 - > 252 - Back 253 - </Button> 254 - </Grid.Column> 255 - <Grid.Column> 256 - <Button 257 - fluid 258 - className={styles.primaryButton} 259 - disabled={isNil(this.state.selectedDevice)} 260 - onClick={this.handleConnect} 261 - > 262 - Connect 263 - </Button> 264 - </Grid.Column> 265 - </Grid> 266 - </Modal.Content> 267 - <Modal.Content> 268 - <a 269 - role="link" 270 - tabIndex={0} 204 + <h2 className={styles.connectHeader}>Headset(s) found</h2> 205 + <p>Please select which headset you would like to connect.</p> 206 + {this.renderAvailableDeviceList()} 207 + <div className="flex gap-2 mt-4"> 208 + <Button 209 + variant="secondary" 210 + className={['w-full', styles.secondaryButton].join(' ')} 271 211 onClick={() => this.handleinstructionProgress(1)} 272 212 > 273 - Don&#39;t see your device? 274 - </a> 275 - </Modal.Content> 213 + Back 214 + </Button> 215 + <Button 216 + variant="default" 217 + className={['w-full', styles.primaryButton].join(' ')} 218 + disabled={isNil(this.state.selectedDevice)} 219 + onClick={this.handleConnect} 220 + > 221 + Connect 222 + </Button> 223 + </div> 224 + <a 225 + role="link" 226 + tabIndex={0} 227 + className="block mt-2 text-sm cursor-pointer" 228 + onClick={() => this.handleinstructionProgress(1)} 229 + > 230 + Don&#39;t see your device? 231 + </a> 276 232 </> 277 233 ); 278 234 } 279 - return <></>; 235 + return null; 280 236 } 281 237 282 238 render() { 283 239 return ( 284 - <Modal 285 - basic 286 - centered 287 - closeOnDimmerClick 288 - closeOnDocumentClick 289 - open={this.props.open} 290 - onOpen={this.handleSearch} 291 - className={styles.connectModal} 292 - > 293 - <Button 294 - circular 295 - basic 296 - bordered="false" 297 - inverted 298 - size="large" 299 - icon="x" 300 - className={styles.modalCloseButton} 301 - onClick={this.props.onClose} 302 - /> 303 - {this.renderContent()} 304 - </Modal> 240 + <Dialog open={this.props.open} onOpenChange={(open) => { if (!open) this.props.onClose(); }}> 241 + <DialogContent className={styles.connectModal}> 242 + {this.renderContent()} 243 + </DialogContent> 244 + </Dialog> 305 245 ); 306 246 } 307 247 }
+27 -54
src/renderer/components/CollectComponent/HelpSidebar.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { Segment, Header, Menu, Icon, Button, Grid } from 'semantic-ui-react'; 2 + import { Button } from '../ui/button'; 3 3 import styles from '../styles/common.module.css'; 4 4 5 5 enum HELP_STEP { ··· 68 68 69 69 renderMenu() { 70 70 return ( 71 - <> 72 - <Menu secondary vertical fluid> 73 - <Header className={styles.helpHeader} as="h1"> 74 - What would you like to do? 75 - </Header> 76 - <Menu.Item onClick={this.handleStartSignal}> 77 - <Segment basic className={styles.helpMenuItem}> 78 - <Icon name="star outline" size="large" /> 79 - Improve the signal quality of your sensors 80 - </Segment> 81 - </Menu.Item> 82 - <Menu.Item onClick={this.handleStartLearn}> 83 - <Segment basic className={styles.helpMenuItem}> 84 - <Icon name="exclamation triangle" size="large" /> 85 - Learn about how the subjects movements create noise 86 - </Segment> 87 - </Menu.Item> 88 - </Menu> 89 - </> 71 + <div className="flex flex-col"> 72 + <h1 className={styles.helpHeader}>What would you like to do?</h1> 73 + <div className={styles.helpMenuItem} onClick={this.handleStartSignal}> 74 + ★ Improve the signal quality of your sensors 75 + </div> 76 + <div className={styles.helpMenuItem} onClick={this.handleStartLearn}> 77 + ⚠ Learn about how the subjects movements create noise 78 + </div> 79 + </div> 90 80 ); 91 81 } 92 82 93 83 renderHelp(header: string, content: string) { 94 84 return ( 95 85 <> 96 - <Segment basic className={styles.helpContent}> 97 - <Header className={styles.helpHeader} as="h1"> 98 - {header} 99 - </Header> 86 + <div className={styles.helpContent}> 87 + <h1 className={styles.helpHeader}>{header}</h1> 100 88 {content} 101 - </Segment> 102 - <Grid columns="equal"> 103 - <Grid.Column> 104 - <Button fluid secondary onClick={this.handleBack}> 105 - Back 106 - </Button> 107 - </Grid.Column> 108 - <Grid.Column> 109 - <Button fluid primary onClick={this.handleNext}> 110 - Next 111 - </Button> 112 - </Grid.Column> 113 - </Grid> 89 + </div> 90 + <div className="flex gap-2 mt-4"> 91 + <Button variant="secondary" className="w-full" onClick={this.handleBack}> 92 + Back 93 + </Button> 94 + <Button variant="default" className="w-full" onClick={this.handleNext}> 95 + Next 96 + </Button> 97 + </div> 114 98 </> 115 99 ); 116 100 } ··· 165 149 166 150 render() { 167 151 return ( 168 - <Segment basic padded vertical className={styles.helpSidebar}> 169 - <Segment basic className={styles.closeButton}> 170 - <Button 171 - circular 172 - size="large" 173 - icon="x" 174 - onClick={this.props.handleClose} 175 - /> 176 - </Segment> 152 + <div className={styles.helpSidebar}> 153 + <div className={styles.closeButton}> 154 + <button onClick={this.props.handleClose} aria-label="Close">✕</button> 155 + </div> 177 156 {this.renderHelpContent()} 178 - </Segment> 157 + </div> 179 158 ); 180 159 } 181 160 } 182 161 183 162 export const HelpButton: React.FC<{ onClick: () => void }> = ({ onClick }) => { 184 163 return ( 185 - <Button 186 - circular 187 - icon="question" 188 - className={styles.helpButton} 189 - floated="right" 190 - onClick={onClick} 191 - /> 164 + <button className={styles.helpButton} onClick={onClick} aria-label="Help">?</button> 192 165 ); 193 166 };
+49 -84
src/renderer/components/CollectComponent/PreTestComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { 3 - Grid, 4 - Segment, 5 - Button, 6 - List, 7 - Header, 8 - Sidebar, 9 - } from 'semantic-ui-react'; 2 + import { Button } from '../ui/button'; 10 3 import Mousetrap from 'mousetrap'; 11 4 import ViewerComponent from '../ViewerComponent'; 12 5 import SignalQualityIndicatorComponent from '../SignalQualityIndicatorComponent'; ··· 103 96 ); 104 97 } 105 98 return ( 106 - <Segment basic> 99 + <div className="p-2"> 107 100 <SignalQualityIndicatorComponent 108 101 signalQualityObservable={this.props.signalQualityObservable} 109 102 plottingInterval={PLOTTING_INTERVAL} 110 103 /> 111 - <Segment basic> 112 - <List> 113 - <List.Item> 114 - <List.Icon name="circle" className={styles.greatSignal} /> 115 - <List.Content>Strong Signal</List.Content> 116 - </List.Item> 117 - <List.Item> 118 - <List.Icon name="circle" className={styles.okSignal} /> 119 - <List.Content>Mediocre signal</List.Content> 120 - </List.Item> 121 - <List.Item> 122 - <List.Icon name="circle" className={styles.badSignal} /> 123 - <List.Content>Weak Signal</List.Content> 124 - </List.Item> 125 - <List.Item> 126 - <List.Icon name="circle" className={styles.noSignal} /> 127 - <List.Content>No Signal</List.Content> 128 - </List.Item> 129 - </List> 130 - </Segment> 131 - </Segment> 104 + <ul className="mt-2 space-y-1"> 105 + <li><span className={styles.greatSignal}>●</span> Strong Signal</li> 106 + <li><span className={styles.okSignal}>●</span> Mediocre signal</li> 107 + <li><span className={styles.badSignal}>●</span> Weak Signal</li> 108 + <li><span className={styles.noSignal}>●</span> No Signal</li> 109 + </ul> 110 + </div> 132 111 ); 133 112 } 134 113 ··· 140 119 141 120 render() { 142 121 return ( 143 - <Sidebar.Pushable as={Segment} className={styles.preTestPushable} basic> 144 - <Sidebar 145 - width="wide" 146 - direction="right" 147 - as={Segment} 148 - visible={this.state.isSidebarVisible} 149 - > 150 - <HelpSidebar handleClose={this.handleSidebarToggle} /> 151 - </Sidebar> 152 - <Sidebar.Pusher> 153 - <Grid 154 - className={styles.preTestContainer} 155 - columns="equal" 156 - textAlign="center" 157 - verticalAlign="middle" 158 - > 159 - <Grid.Row columns="equal"> 160 - <Grid.Column> 161 - <Header as="h1" floated="left"> 162 - Collect 163 - </Header> 164 - </Grid.Column> 165 - <Grid.Column floated="right"> 166 - <PreviewButton 167 - isPreviewing={this.state.isPreviewing} 168 - onClick={(e) => this.handlePreview(e)} 169 - /> 170 - <Button 171 - primary 172 - disabled={ 173 - this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED 174 - } 175 - onClick={this.props.openRunComponent} 176 - > 177 - Run & Record Experiment 178 - </Button> 179 - </Grid.Column> 180 - </Grid.Row> 181 - <Grid.Row> 182 - <Grid.Column width={8} className={styles.previewEEGWindow}> 183 - {this.renderSignalQualityOrPreview()} 184 - </Grid.Column> 185 - <Grid.Column width={8}> 186 - <ViewerComponent 187 - signalQualityObservable={this.props.signalQualityObservable} 188 - deviceType={this.props.deviceType} 189 - plottingInterval={PLOTTING_INTERVAL} 190 - /> 191 - {this.renderHelpButton()} 192 - </Grid.Column> 193 - </Grid.Row> 194 - </Grid> 195 - </Sidebar.Pusher> 196 - </Sidebar.Pushable> 122 + <div className={['relative flex', styles.preTestPushable].join(' ')}> 123 + {this.state.isSidebarVisible && ( 124 + <div className="absolute right-0 top-0 h-full w-64 z-10"> 125 + <HelpSidebar handleClose={this.handleSidebarToggle} /> 126 + </div> 127 + )} 128 + <div className={['flex-1', styles.preTestContainer].join(' ')}> 129 + <div className="flex items-center justify-between mb-4"> 130 + <h1>Collect</h1> 131 + <div className="flex gap-2"> 132 + <PreviewButton 133 + isPreviewing={this.state.isPreviewing} 134 + onClick={(e) => this.handlePreview(e)} 135 + /> 136 + <Button 137 + variant="default" 138 + disabled={ 139 + this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED 140 + } 141 + onClick={this.props.openRunComponent} 142 + > 143 + Run & Record Experiment 144 + </Button> 145 + </div> 146 + </div> 147 + <div className="flex gap-4"> 148 + <div className={['w-1/2', styles.previewEEGWindow].join(' ')}> 149 + {this.renderSignalQualityOrPreview()} 150 + </div> 151 + <div className="w-1/2"> 152 + <ViewerComponent 153 + signalQualityObservable={this.props.signalQualityObservable} 154 + deviceType={this.props.deviceType} 155 + plottingInterval={PLOTTING_INTERVAL} 156 + /> 157 + {this.renderHelpButton()} 158 + </div> 159 + </div> 160 + </div> 161 + </div> 197 162 ); 198 163 } 199 164 }
+43 -59
src/renderer/components/CollectComponent/RunComponent.tsx
··· 1 1 import React, { useCallback, useState } from 'react'; 2 - import { Grid, Button, Segment, Header, Divider } from 'semantic-ui-react'; 2 + import { Button } from '../ui/button'; 3 3 import { Link } from 'react-router-dom'; 4 4 import styles from '../styles/common.module.css'; 5 5 import InputCollect from '../InputCollect'; ··· 96 96 97 97 return ( 98 98 <div className={styles.mainContainer} data-tid="container"> 99 - <Grid columns={1} divided relaxed className={styles.experimentContainer}> 100 - <Grid.Row centered> 101 - {!isRunning && ( 102 - <div className={styles.mainContainer}> 103 - <Segment 104 - basic 105 - textAlign="left" 106 - className={styles.descriptionContainer} 107 - vertical 108 - > 109 - <Header as="h1">{title}</Header> 99 + <div className={styles.experimentContainer}> 100 + {!isRunning && ( 101 + <div className={styles.mainContainer}> 102 + <div className={['text-left', styles.descriptionContainer].join(' ')}> 103 + <h1>{title}</h1> 104 + <button 105 + className={styles.closeButton} 106 + onClick={() => setIsInputCollectOpen(true)} 107 + aria-label="Edit" 108 + >✏</button> 109 + <div className={styles.infoSegment}> 110 + Subject ID: <b>{subject}</b> 111 + </div> 112 + <div className={styles.infoSegment}> 113 + Group Name: <b>{group}</b> 114 + </div> 115 + <div className={styles.infoSegment}> 116 + Session Number: <b>{session}</b> 117 + </div> 118 + <div className="mt-6"> 110 119 <Button 111 - basic 112 - circular 113 - size="huge" 114 - icon="edit" 115 - className={styles.closeButton} 116 - onClick={() => setIsInputCollectOpen(true)} 117 - /> 118 - <Segment basic className={styles.infoSegment}> 119 - Subject ID: <b>{subject}</b> 120 - </Segment> 121 - 122 - <Segment basic className={styles.infoSegment}> 123 - Group Name: <b>{group}</b> 124 - </Segment> 125 - 126 - <Segment basic className={styles.infoSegment}> 127 - Session Number: <b>{session}</b> 128 - </Segment> 129 - 130 - <Divider hidden section /> 131 - <Grid textAlign="center" columns="equal"> 132 - <Grid.Column> 133 - <Button 134 - fluid 135 - primary 136 - onClick={handleStartExperiment} 137 - disabled={!subject} 138 - > 139 - Run Experiment 140 - </Button> 141 - </Grid.Column> 142 - </Grid> 143 - </Segment> 120 + variant="default" 121 + className="w-full" 122 + onClick={handleStartExperiment} 123 + disabled={!subject} 124 + > 125 + Run Experiment 126 + </Button> 127 + </div> 144 128 </div> 145 - )} 129 + </div> 130 + )} 146 131 147 - {isRunning && ( 148 - <div className={styles.experimentWindow}> 149 - <ExperimentWindow 150 - title={title} 151 - experimentObject={experimentObject} 152 - params={params} 153 - eventCallback={eventCallback} 154 - onFinish={onFinish} 155 - /> 156 - </div> 157 - )} 158 - </Grid.Row> 159 - </Grid> 132 + {isRunning && ( 133 + <div className={styles.experimentWindow}> 134 + <ExperimentWindow 135 + title={title} 136 + experimentObject={experimentObject} 137 + params={params} 138 + eventCallback={eventCallback} 139 + onFinish={onFinish} 140 + /> 141 + </div> 142 + )} 143 + </div> 160 144 <InputCollect 161 145 open={isInputCollectOpen} 162 146 onClose={handleCloseInputCollect}
-3
src/renderer/components/CollectComponent/index.tsx
··· 1 1 import { Observable } from 'rxjs'; 2 2 import React, { Component } from 'react'; 3 - import { History } from 'history'; 4 3 import { 5 4 EXPERIMENTS, 6 5 DEVICES, ··· 17 16 import { ExperimentActions, DeviceActions } from '../../actions'; 18 17 19 18 export interface Props { 20 - history: History; 21 19 ExperimentActions: typeof ExperimentActions; 22 20 connectedDevice: Record<string, any>; 23 21 deviceType: DEVICES; ··· 99 97 return ( 100 98 <> 101 99 <ConnectModal 102 - history={this.props.history} 103 100 open={this.state.isConnectModalOpen} 104 101 onClose={this.handleConnectModalClose} 105 102 connectedDevice={this.props.connectedDevice}
+193 -470
src/renderer/components/DesignComponent/CustomDesignComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { 3 - Grid, 4 - Button, 5 - Segment, 6 - Header, 7 - Form, 8 - Checkbox, 9 - Image, 10 - Table, 11 - CheckboxProps, 12 - } from 'semantic-ui-react'; 2 + import { Button } from '../ui/button'; 3 + import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../ui/table'; 13 4 import { isString } from 'lodash'; 14 5 15 6 import styles from '../styles/common.module.css'; ··· 75 66 this.setState({ activeStep: step }); 76 67 } 77 68 78 - handleProgressBar(_, data: CheckboxProps) { 79 - const { checked } = data; 80 - if (checked === undefined) return; 69 + handleProgressBar(e: React.ChangeEvent<HTMLInputElement>) { 70 + const checked = e.target.checked; 81 71 this.setState((prevState) => ({ 82 72 params: { ...prevState.params, showProgessBar: checked }, 83 73 })); 84 74 } 85 75 86 - handleEEGEnabled(_, data: CheckboxProps) { 87 - if (data.checked === undefined) return; 88 - this.props.ExperimentActions.SetEEGEnabled(data.checked); 76 + handleEEGEnabled(e: React.ChangeEvent<HTMLInputElement>) { 77 + this.props.ExperimentActions.SetEEGEnabled(e.target.checked); 89 78 } 90 79 91 80 handleStartExperiment() { 92 - this.props.history.push(SCREENS.COLLECT.route); 81 + this.props.navigate(SCREENS.COLLECT.route); 93 82 } 94 83 95 84 handlePreview(e) { ··· 125 114 case CUSTOM_STEPS.OVERVIEW: 126 115 default: 127 116 return ( 128 - <Grid 129 - stretched 130 - relaxed 131 - padded 132 - columns="equal" 133 - className={styles.contentGrid} 134 - > 135 - <Grid.Column stretched verticalAlign="middle"> 136 - <Image 137 - as={Segment} 138 - basic 139 - centered 140 - src={researchQuestionImage} 141 - className={styles.overviewImage} 117 + <div className={['flex gap-4 p-4', styles.contentGrid].join(' ')}> 118 + <div className="flex-1 flex flex-col items-center"> 119 + <img src={researchQuestionImage} className={styles.overviewImage} alt="Research Question" /> 120 + <label className="block text-sm font-medium mb-1">{FIELDS.QUESTION}</label> 121 + <textarea 122 + style={{ minHeight: 100, maxHeight: 400 }} 123 + className="w-full border border-gray-300 rounded p-2" 124 + value={this.state.params.description?.question} 125 + placeholder="Explain your research question here." 126 + onChange={(event) => this.handleSetText(event.target.value, 'question')} 142 127 /> 143 - <Form> 144 - <Form.TextArea 145 - autoHeight 146 - style={{ minHeight: 100, maxHeight: 400 }} 147 - label={FIELDS.QUESTION} 148 - value={this.state.params.description?.question} 149 - placeholder="Explain your research question here." 150 - onChange={(event, data) => { 151 - const val = data.value; 152 - if (!isString(val)) { 153 - return; 154 - } 155 - this.handleSetText(val as string, 'question'); 156 - }} 157 - /> 158 - </Form> 159 - </Grid.Column> 160 - <Grid.Column stretched verticalAlign="middle"> 161 - <Image 162 - as={Segment} 163 - basic 164 - centered 165 - src={hypothesisImage} 166 - className={styles.overviewImage} 128 + </div> 129 + <div className="flex-1 flex flex-col items-center"> 130 + <img src={hypothesisImage} className={styles.overviewImage} alt="Hypothesis" /> 131 + <label className="block text-sm font-medium mb-1">{FIELDS.HYPOTHESIS}</label> 132 + <textarea 133 + style={{ minHeight: 100, maxHeight: 400 }} 134 + className="w-full border border-gray-300 rounded p-2" 135 + value={this.state.params.description?.hypothesis} 136 + placeholder="Describe your hypothesis here." 137 + onChange={(event) => this.handleSetText(event.target.value, 'hypothesis')} 167 138 /> 168 - <Form> 169 - <Form.TextArea 170 - autoHeight 171 - style={{ minHeight: 100, maxHeight: 400 }} 172 - label={FIELDS.HYPOTHESIS} 173 - value={this.state.params.description?.hypothesis} 174 - placeholder="Describe your hypothesis here." 175 - onChange={(event, data) => { 176 - const val = data.value; 177 - if (!isString(val)) { 178 - return; 179 - } 180 - this.handleSetText(val as string, 'hypothesis'); 181 - }} 182 - /> 183 - </Form> 184 - </Grid.Column> 185 - <Grid.Column verticalAlign="middle"> 186 - <Image 187 - as={Segment} 188 - basic 189 - centered 190 - src={methodsImage} 191 - className={styles.overviewImage} 139 + </div> 140 + <div className="flex-1 flex flex-col items-center"> 141 + <img src={methodsImage} className={styles.overviewImage} alt="Methods" /> 142 + <label className="block text-sm font-medium mb-1">{FIELDS.METHODS}</label> 143 + <textarea 144 + style={{ minHeight: 100, maxHeight: 400 }} 145 + className="w-full border border-gray-300 rounded p-2" 146 + value={this.state.params.description?.methods} 147 + placeholder="Explain how you will design your experiment to answer the question here." 148 + onChange={(event) => this.handleSetText(event.target.value, 'methods')} 192 149 /> 193 - <Form> 194 - <Form.TextArea 195 - autoHeight 196 - style={{ minHeight: 100, maxHeight: 400 }} 197 - label={FIELDS.METHODS} 198 - value={this.state.params.description?.methods} 199 - placeholder="Explain how you will design your experiment to answer the question here." 200 - onChange={(event, data) => { 201 - const val = data.value; 202 - if (!isString(val)) { 203 - return; 204 - } 205 - this.handleSetText(val as string, 'methods'); 206 - }} 207 - /> 208 - </Form> 209 - </Grid.Column> 210 - </Grid> 150 + </div> 151 + </div> 211 152 ); 212 153 213 154 case CUSTOM_STEPS.CONDITIONS: 214 155 return ( 215 - <Grid> 216 - <Segment basic> 217 - <Header as="h1">Conditions</Header> 156 + <div className="p-4"> 157 + <div className="mb-4"> 158 + <h1>Conditions</h1> 218 159 <p> 219 160 {`Select the folder with images for each condition and choose 220 161 the correct response. You can upload image files with the ··· 223 164 You can resize or compress your images in an image editing 224 165 program or on one of the websites online.`} 225 166 </p> 226 - </Segment> 167 + </div> 227 168 228 - <Table basic="very"> 229 - <Table.Header> 230 - <Table.Row className={styles.conditionHeaderRow}> 231 - <Table.HeaderCell className={styles.conditionHeaderRowName}> 169 + <Table> 170 + <TableHeader> 171 + <TableRow className={styles.conditionHeaderRow}> 172 + <TableHead className={styles.conditionHeaderRowName}> 232 173 Condition 233 - </Table.HeaderCell> 234 - <Table.HeaderCell>Default Key Response</Table.HeaderCell> 235 - <Table.HeaderCell>Condition Folder</Table.HeaderCell> 236 - </Table.Row> 237 - </Table.Header> 174 + </TableHead> 175 + <TableHead>Default Key Response</TableHead> 176 + <TableHead>Condition Folder</TableHead> 177 + </TableRow> 178 + </TableHeader> 238 179 239 - <Table.Body className={styles.experimentTable}> 240 - <div>Stimulus customization is currently unavailable</div> 180 + <TableBody className={styles.experimentTable}> 181 + <TableRow> 182 + <TableCell colSpan={3}>Stimulus customization is currently unavailable</TableCell> 183 + </TableRow> 241 184 {stimi.map(({ name, number }) => ( 242 - <div key={name}> 243 - {`Stimulus name: ${name}, number: ${number}`} 244 - </div> 245 - // key={number} 246 - // num={number} 247 - // {...this.state.params[name]} 248 - // numberImages={this.state.params.stimuli.length} 249 - // onChange={async (key, data, changedName) => { 250 - // await this.setState({ 251 - // params: { 252 - // ...this.state.params, 253 - // [changedName]: { 254 - // ...this.state.params[changedName], 255 - // [key]: data, 256 - // }, 257 - // }, 258 - // }); 259 - // let newStimuli: StimuliDesc[] = []; 260 - // await stimi.forEach((stimul) => { 261 - // let dirStimuli: StimuliDesc[] = []; 262 - // const { dir } = this.state.params[stimul.name]; 263 - // if (dir && typeof dir !== 'undefined' && dir !== '') { 264 - // dirStimuli = readImages(dir).map((i) => ({ 265 - // dir, 266 - // filename: i, 267 - // name: i, 268 - // condition: this.state.params[stimul.name].title, 269 - // response: this.state.params[stimul.name].response, 270 - // phase: 'main', 271 - // type: stimul.number, 272 - // })); 273 - // } 274 - // if (dirStimuli.length) dirStimuli[0].phase = 'practice'; 275 - // newStimuli = newStimuli.concat(...dirStimuli); 276 - // }); 277 - // this.setState({ 278 - // params: { 279 - // ...this.state.params, 280 - // stimuli: [...newStimuli], 281 - // nbTrials: newStimuli.filter((t) => t.phase === 'main') 282 - // .length, 283 - // nbPracticeTrials: newStimuli.filter( 284 - // (t) => t.phase === 'practice' 285 - // ).length, 286 - // }, 287 - // saved: false, 288 - // }); 289 - // }} 290 - // /> 185 + <TableRow key={name}> 186 + <TableCell colSpan={3}>{`Stimulus name: ${name}, number: ${number}`}</TableCell> 187 + </TableRow> 291 188 ))} 292 - </Table.Body> 189 + </TableBody> 293 190 </Table> 294 - </Grid> 191 + </div> 295 192 ); 296 193 297 194 case CUSTOM_STEPS.TRIALS: 298 195 return ( 299 - <Grid> 196 + <div className="p-4"> 300 197 <div className={styles.trialsHeader}> 301 198 <div> 302 - <Header as="h1">Trials</Header> 199 + <h1>Trials</h1> 303 200 <p>Edit the correct key response and type of each trial.</p> 304 201 </div> 305 - 306 - <div> 307 - <Form style={{ alignSelf: 'flex-end' }}> 308 - <Form.Group className={styles.trialsTopInfoBar}> 309 - <Form.Select 310 - fluid 311 - selection 312 - label="Order" 313 - value={this.state.params.randomize} 314 - onChange={(event, data) => { 315 - if ( 316 - data.value === 'sequential' || 317 - data.value === 'random' 318 - ) { 319 - this.setState({ 320 - params: { 321 - ...this.state.params, 322 - randomize: data.value, 323 - }, 324 - saved: false, 325 - }); 326 - } 327 - }} 328 - placeholder="Response" 329 - options={[ 330 - { key: 'random', text: 'Random', value: 'random' }, 331 - { 332 - key: 'sequential', 333 - text: 'Sequential', 334 - value: 'sequential', 335 - }, 336 - ]} 337 - /> 338 - <Form.Input 339 - label="Total experimental trials" 340 - type="number" 341 - fluid 342 - value={this.state.params.nbTrials} 343 - onChange={(event, data) => 344 - this.setState({ 345 - params: { 346 - ...this.state.params, 347 - nbTrials: parseInt(data.value, 10), 348 - }, 349 - saved: false, 350 - }) 202 + <div className={styles.trialsTopInfoBar} style={{ alignSelf: 'flex-end' }}> 203 + <div> 204 + <label className="block text-sm mb-1">Order</label> 205 + <select 206 + className="border border-gray-300 rounded px-2 py-1" 207 + value={this.state.params.randomize} 208 + onChange={(event) => { 209 + const val = event.target.value; 210 + if (val === 'sequential' || val === 'random') { 211 + this.setState({ params: { ...this.state.params, randomize: val }, saved: false }); 351 212 } 352 - /> 353 - <Form.Input 354 - label="Total practice trials" 355 - type="number" 356 - fluid 357 - value={this.state.params.nbPracticeTrials} 358 - onChange={(event, data) => 359 - this.setState({ 360 - params: { 361 - ...this.state.params, 362 - nbPracticeTrials: parseInt(data.value, 10), 363 - }, 364 - saved: false, 365 - }) 366 - } 367 - /> 368 - </Form.Group> 369 - </Form> 213 + }} 214 + > 215 + <option value="random">Random</option> 216 + <option value="sequential">Sequential</option> 217 + </select> 218 + </div> 219 + <div> 220 + <label className="block text-sm mb-1">Total experimental trials</label> 221 + <input 222 + type="number" 223 + className="border border-gray-300 rounded px-2 py-1" 224 + value={this.state.params.nbTrials} 225 + onChange={(event) => this.setState({ params: { ...this.state.params, nbTrials: parseInt(event.target.value, 10) }, saved: false })} 226 + /> 227 + </div> 228 + <div> 229 + <label className="block text-sm mb-1">Total practice trials</label> 230 + <input 231 + type="number" 232 + className="border border-gray-300 rounded px-2 py-1" 233 + value={this.state.params.nbPracticeTrials} 234 + onChange={(event) => this.setState({ params: { ...this.state.params, nbPracticeTrials: parseInt(event.target.value, 10) }, saved: false })} 235 + /> 236 + </div> 370 237 </div> 371 238 </div> 372 239 373 - <Table basic="very"> 374 - <Table.Header> 375 - <Table.Row className={styles.trialsHeaderRow}> 376 - <Table.HeaderCell className={styles.conditionHeaderRowName}> 377 - Name 378 - </Table.HeaderCell> 379 - <Table.HeaderCell>Condition</Table.HeaderCell> 380 - <Table.HeaderCell>Correct Key Response</Table.HeaderCell> 381 - <Table.HeaderCell>Trial Type</Table.HeaderCell> 382 - </Table.Row> 383 - </Table.Header> 384 - <Table.Body className={styles.trialsTable}> 385 - <div>Stimulus customization is currently unavailable</div> 386 - 387 - {/* {this.state.params.stimuli && 388 - this.state.params.stimuli.map((e, num) => ( 389 - <StimuliRow 390 - key={`stim_row_${num}`} 391 - num={num} 392 - condition={[1, 2, 3, 4].map( 393 - (n) => this.state.params[`stimulus${n}`].title 394 - )} 395 - {...e} 396 - onDelete={(deletedNum) => { 397 - const { stimuli } = this.state.params; 398 - stimuli.splice(deletedNum, 1); 399 - const nbPracticeTrials = stimuli.filter( 400 - (s) => s.phase === 'practice' 401 - ).length; 402 - const nbTrials = stimuli.filter( 403 - (s) => s.phase === 'main' 404 - ).length; 405 - this.setState({ 406 - params: { 407 - ...this.state.params, 408 - stimuli: [...stimuli], 409 - nbPracticeTrials, 410 - nbTrials, 411 - }, 412 - saved: false, 413 - }); 414 - }} 415 - onChange={(changedNum, key, data) => { 416 - const { stimuli } = this.state.params; 417 - stimuli[changedNum][key] = data; 418 - let { nbPracticeTrials } = this.state.params; 419 - let { nbTrials } = this.state.params; 420 - if (key === 'phase') { 421 - nbPracticeTrials = stimuli.filter( 422 - (s) => s.phase === 'practice' 423 - ).length; 424 - nbTrials = stimuli.filter((s) => s.phase === 'main') 425 - .length; 426 - } 427 - this.setState({ 428 - params: { 429 - ...this.state.params, 430 - stimuli: [...stimuli], 431 - nbPracticeTrials, 432 - nbTrials, 433 - }, 434 - saved: false, 435 - }); 436 - }} 437 - /> 438 - ))} */} 439 - </Table.Body> 240 + <Table> 241 + <TableHeader> 242 + <TableRow className={styles.trialsHeaderRow}> 243 + <TableHead className={styles.conditionHeaderRowName}>Name</TableHead> 244 + <TableHead>Condition</TableHead> 245 + <TableHead>Correct Key Response</TableHead> 246 + <TableHead>Trial Type</TableHead> 247 + </TableRow> 248 + </TableHeader> 249 + <TableBody className={styles.trialsTable}> 250 + <TableRow> 251 + <TableCell colSpan={4}>Stimulus customization is currently unavailable</TableCell> 252 + </TableRow> 253 + </TableBody> 440 254 </Table> 441 - </Grid> 255 + </div> 442 256 ); 443 257 444 258 case CUSTOM_STEPS.PARAMETERS: 445 259 return ( 446 - <Grid> 447 - <Grid.Column 448 - width={8} 449 - style={{ display: 'grid', alignContent: 'space-between' }} 450 - > 451 - <Segment basic> 452 - <Header as="h1">Inter-trial interval</Header> 260 + <div className="flex gap-4 p-4"> 261 + <div className="w-1/2 flex flex-col justify-between"> 262 + <div> 263 + <h1>Inter-trial interval</h1> 453 264 <p> 454 265 Select the inter-trial interval duration. This is the amount 455 266 of time between trials measured from the end of one trial to 456 267 the start of the next one. 457 268 </p> 458 - </Segment> 459 - <Segment basic style={{ marginTop: '100px' }}> 269 + </div> 270 + <div style={{ marginTop: '100px' }}> 460 271 <ParamSlider 461 272 label="ITI Duration (seconds)" 462 273 value={this.state.params.iti} 463 - marks={{ 464 - 1: '0.25', 465 - 2: '0.5', 466 - 3: '0.75', 467 - 4: '1', 468 - 5: '1.25', 469 - 6: '1.5', 470 - 7: '1.75', 471 - 8: '2', 472 - }} 274 + marks={{ 1: '0.25', 2: '0.5', 3: '0.75', 4: '1', 5: '1.25', 6: '1.5', 7: '1.75', 8: '2' }} 473 275 msConversion="250" 474 - onChange={(value) => 475 - this.setState({ 476 - params: { ...this.state.params, iti: value }, 477 - saved: false, 478 - }) 479 - } 276 + onChange={(value) => this.setState({ params: { ...this.state.params, iti: value }, saved: false })} 480 277 /> 481 - </Segment> 482 - </Grid.Column> 278 + </div> 279 + </div> 483 280 484 - <Grid.Column 485 - width={8} 486 - style={{ display: 'grid', alignContent: 'space-between' }} 487 - > 488 - <Segment basic> 489 - <Header as="h1">Image duration</Header> 281 + <div className="w-1/2 flex flex-col justify-between"> 282 + <div> 283 + <h1>Image duration</h1> 490 284 <p> 491 285 Select the time of presentation or make it self-paced - 492 286 present the image until participants respond. 493 287 </p> 494 - </Segment> 495 - <Segment basic> 496 - <Checkbox 497 - defaultChecked={this.state.params.selfPaced} 498 - label="Self-paced data collection" 499 - onChange={(value) => 500 - this.setState({ 501 - params: { 502 - ...this.state.params, 503 - selfPaced: !this.state.params.selfPaced, 504 - }, 505 - saved: false, 506 - }) 507 - } 508 - /> 509 - </Segment> 510 - 288 + </div> 289 + <div> 290 + <label className="flex items-center gap-2"> 291 + <input 292 + type="checkbox" 293 + defaultChecked={this.state.params.selfPaced} 294 + onChange={() => this.setState({ params: { ...this.state.params, selfPaced: !this.state.params.selfPaced }, saved: false })} 295 + /> 296 + Self-paced data collection 297 + </label> 298 + </div> 511 299 {!this.state.params.selfPaced ? ( 512 - <Segment basic> 300 + <div> 513 301 <ParamSlider 514 302 label="Presentation time (seconds)" 515 - value={ 516 - this.state.params.presentationTime 517 - ? this.state.params.presentationTime 518 - : 0 519 - } 520 - marks={{ 521 - 1: '0.25', 522 - 2: '0.5', 523 - 3: '0.75', 524 - 4: '1', 525 - 5: '1.25', 526 - 6: '1.5', 527 - 7: '1.75', 528 - 8: '2', 529 - }} 303 + value={this.state.params.presentationTime ? this.state.params.presentationTime : 0} 304 + marks={{ 1: '0.25', 2: '0.5', 3: '0.75', 4: '1', 5: '1.25', 6: '1.5', 7: '1.75', 8: '2' }} 530 305 msConversion="250" 531 - onChange={(value) => 532 - this.setState({ 533 - params: { 534 - ...this.state.params, 535 - presentationTime: value, 536 - }, 537 - saved: false, 538 - }) 539 - } 306 + onChange={(value) => this.setState({ params: { ...this.state.params, presentationTime: value }, saved: false })} 540 307 /> 541 - </Segment> 308 + </div> 542 309 ) : ( 543 - <Segment basic style={{ marginBottom: '85px' }} /> 310 + <div style={{ marginBottom: '85px' }} /> 544 311 )} 545 - </Grid.Column> 546 - </Grid> 312 + </div> 313 + </div> 547 314 ); 548 315 549 316 case CUSTOM_STEPS.INSTRUCTIONS: 550 317 return ( 551 - <Grid stretched> 552 - <Grid.Column 553 - width={8} 554 - stretched 555 - style={{ display: 'grid', alignContent: 'space-between' }} 556 - > 557 - <Segment basic> 558 - <Header as="h1">Experiment Instructions</Header> 559 - <p> 560 - Edit the instruction that will be displayed on the first 561 - screen. 562 - </p> 563 - <Form> 564 - <Form.TextArea 565 - autoHeight 566 - value={this.state.params.intro} 567 - 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." 568 - onChange={(event, data) => { 569 - const val = data.value; 570 - if (!isString(val)) { 571 - return; 572 - } 573 - this.setState({ 574 - params: { ...this.state.params, intro: val as string }, 575 - saved: false, 576 - }); 577 - }} 578 - /> 579 - </Form> 580 - </Segment> 581 - </Grid.Column> 318 + <div className="flex gap-4 p-4"> 319 + <div className="w-1/2"> 320 + <h1>Experiment Instructions</h1> 321 + <p>Edit the instruction that will be displayed on the first screen.</p> 322 + <textarea 323 + className="w-full border border-gray-300 rounded p-2" 324 + style={{ minHeight: 150 }} 325 + value={this.state.params.intro} 326 + placeholder="e.g., You will view a series of faces and houses. Press 1 when a face appears and 9 for a house." 327 + onChange={(event) => { 328 + const val = event.target.value; 329 + if (!isString(val)) return; 330 + this.setState({ params: { ...this.state.params, intro: val }, saved: false }); 331 + }} 332 + /> 333 + </div> 582 334 583 - <Grid.Column 584 - width={8} 585 - stretched 586 - style={{ display: 'grid', alignContent: 'space-between' }} 587 - > 588 - <Segment basic> 589 - <Header as="h1">Instructions for the task screen</Header> 590 - <p> 591 - Edit the instruction that will be displayed in the footer 592 - during the task. 593 - </p> 594 - <Form> 595 - <Form.TextArea 596 - autoHeight 597 - value={this.state.params.taskHelp} 598 - placeholder="e.g., Press 1 for a face and 9 for a house" 599 - onChange={(event, data) => { 600 - const val = data.value; 601 - if (!isString(val)) { 602 - return; 603 - } 604 - this.setState({ 605 - params: { ...this.state.params, taskHelp: val as string }, 606 - saved: false, 607 - }); 608 - }} 609 - /> 610 - </Form> 611 - </Segment> 612 - </Grid.Column> 613 - </Grid> 335 + <div className="w-1/2"> 336 + <h1>Instructions for the task screen</h1> 337 + <p>Edit the instruction that will be displayed in the footer during the task.</p> 338 + <textarea 339 + className="w-full border border-gray-300 rounded p-2" 340 + style={{ minHeight: 150 }} 341 + value={this.state.params.taskHelp} 342 + placeholder="e.g., Press 1 for a face and 9 for a house" 343 + onChange={(event) => { 344 + const val = event.target.value; 345 + if (!isString(val)) return; 346 + this.setState({ params: { ...this.state.params, taskHelp: val }, saved: false }); 347 + }} 348 + /> 349 + </div> 350 + </div> 614 351 ); 615 352 616 353 case CUSTOM_STEPS.PREVIEW: 617 354 return ( 618 - <Grid relaxed padded className={styles.contentGrid}> 619 - <Grid.Column 620 - stretched 621 - width={14} 622 - textAlign="right" 623 - verticalAlign="middle" 624 - className={styles.previewWindow} 625 - > 355 + <div className={['flex items-start p-4', styles.contentGrid].join(' ')}> 356 + <div className={['flex-1', styles.previewWindow].join(' ')}> 626 357 {this.props.type && ( 627 358 <PreviewExperimentComponent 628 359 isPreviewing={this.state.isPreviewing} 629 360 onEnd={this.endPreview} 630 361 type={this.props.type} 631 362 experimentObject={this.props.experimentObject} 632 - // TODO: I believe this lets the user preview the parameter changes 633 - // before saving them 634 363 params={this.state.params} 635 364 title={this.props.title} 636 365 /> 637 366 )} 638 - </Grid.Column> 639 - 640 - <Grid.Column width={2} verticalAlign="top"> 641 - <Segment basic> 642 - <PreviewButton 643 - isPreviewing={this.state.isPreviewing} 644 - onClick={(e) => this.handlePreview(e)} 645 - /> 646 - </Segment> 647 - </Grid.Column> 648 - </Grid> 367 + </div> 368 + <div className="flex-shrink-0 p-2"> 369 + <PreviewButton 370 + isPreviewing={this.state.isPreviewing} 371 + onClick={(e) => this.handlePreview(e)} 372 + /> 373 + </div> 374 + </div> 649 375 ); 650 376 } 651 377 } ··· 659 385 activeStep={this.state.activeStep} 660 386 onStepClick={this.handleStepClick} 661 387 enableEEGToggle={ 662 - <Checkbox 663 - toggle 388 + <input 389 + type="checkbox" 664 390 defaultChecked={this.props.isEEGEnabled} 665 - onChange={(event, data) => this.handleEEGEnabled(event, data)} 391 + onChange={(event) => this.handleEEGEnabled(event)} 666 392 className={styles.EEGToggle} 667 393 /> 668 394 } 669 395 saveButton={ 670 396 <Button 671 - compact 672 - size="small" 673 - secondary 674 - onClick={() => { 675 - this.handleSaveParams(); 676 - }} 397 + variant="secondary" 398 + size="sm" 399 + onClick={() => this.handleSaveParams()} 677 400 > 678 - {this.state.saved ? 'Save' : 'Save'} 401 + Save 679 402 </Button> 680 403 } 681 404 />
+2 -3
src/renderer/components/DesignComponent/ParamSlider.tsx
··· 1 - import { Segment } from 'semantic-ui-react'; 2 1 import React from 'react'; 3 2 import Slider from 'rc-slider'; 4 3 import styles from '../styles/common.module.css'; ··· 21 20 return ( 22 21 <div> 23 22 <p className={styles.label}>{label}</p> 24 - <Segment basic> 23 + <div className="py-2"> 25 24 {label !== 'Practice trials' || Object.keys(marks).length > 1 ? ( 26 25 <Slider 27 26 dots ··· 35 34 ) : ( 36 35 <div>You have not chosen any practice trials.</div> 37 36 )} 38 - </Segment> 37 + </div> 39 38 </div> 40 39 ); 41 40 };
+33 -45
src/renderer/components/DesignComponent/StimuliDesignColumn.tsx
··· 1 1 /* Breaking this component on its own is done mainly to increase performance. Text input is slow otherwise */ 2 2 3 3 import React, { Component } from 'react'; 4 - import { Form, Button, Table, Icon } from 'semantic-ui-react'; 4 + import { Button } from '../ui/button'; 5 + import { TableRow, TableCell } from '../ui/table'; 5 6 import { toast } from 'react-toastify'; 6 7 import path from 'pathe'; 7 8 import { isString } from 'lodash'; ··· 73 74 74 75 render() { 75 76 return ( 76 - <Table.Row className={styles.conditionRow}> 77 - <Table.Cell className={styles.conditionsNameRow}> 77 + <TableRow className={styles.conditionRow}> 78 + <TableCell className={styles.conditionsNameRow}> 78 79 {this.props.num} 79 - <Form> 80 - <Form.Input 81 - value={this.props.title} 82 - onChange={(event, data) => 83 - this.props.onChange( 84 - 'title', 85 - data.value, 86 - `stimulus${this.props.num}` 87 - ) 88 - } 89 - placeholder="Enter condition name" 90 - /> 91 - </Form> 92 - </Table.Cell> 80 + <input 81 + className="border border-gray-300 rounded px-2 py-1 w-full mt-1" 82 + value={this.props.title} 83 + onChange={(event) => 84 + this.props.onChange('title', event.target.value, `stimulus${this.props.num}`) 85 + } 86 + placeholder="Enter condition name" 87 + /> 88 + </TableCell> 93 89 94 - <Table.Cell className={styles.experimentRowName}> 95 - <Form.Select 96 - fluid 97 - selection 90 + <TableCell className={styles.experimentRowName}> 91 + <select 92 + className="w-full border border-gray-300 rounded px-2 py-1" 98 93 value={this.props.response} 99 - onChange={(event, data) => { 100 - const val = data.value; 94 + onChange={(event) => { 95 + const val = event.target.value; 101 96 if (val && isString(val)) { 102 - this.props.onChange( 103 - 'response', 104 - val as string, 105 - `stimulus${this.props.num}` 106 - ); 97 + this.props.onChange('response', val, `stimulus${this.props.num}`); 107 98 } 108 99 }} 109 - placeholder="Select" 110 - options={RESPONSE_OPTIONS} 111 - /> 112 - </Table.Cell> 100 + > 101 + <option value="">Select</option> 102 + {RESPONSE_OPTIONS.map((o) => ( 103 + <option key={o.key} value={o.value}>{o.text}</option> 104 + ))} 105 + </select> 106 + </TableCell> 113 107 114 - <Table.Cell className={styles.experimentRowName}> 108 + <TableCell className={styles.experimentRowName}> 115 109 {this.props.dir ? ( 116 110 <div className={styles.selectedFolderContainer}> 117 111 <div> 118 112 Folder{' '} 119 - {this.props.dir && 120 - this.props.dir.split(path.sep).slice(-1).join(' / ')} 121 - </div> 122 - <div> 123 - ( {this.state.numberImages || this.props.numberImages} images 124 - ){' '} 125 - </div> 126 - <div> 127 - <Icon name="delete" onClick={this.handleRemoveFolder} /> 113 + {this.props.dir && this.props.dir.split(path.sep).slice(-1).join(' / ')} 128 114 </div> 115 + <div>( {this.state.numberImages || this.props.numberImages} images )</div> 116 + <button onClick={this.handleRemoveFolder} aria-label="Remove">✕</button> 129 117 </div> 130 118 ) : ( 131 - <Button secondary onClick={this.handleSelectFolder}> 119 + <Button variant="secondary" onClick={this.handleSelectFolder}> 132 120 Select folder 133 121 </Button> 134 122 )} 135 - </Table.Cell> 136 - </Table.Row> 123 + </TableCell> 124 + </TableRow> 137 125 ); 138 126 } 139 127 }
+44 -50
src/renderer/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 from 'react'; 4 - import { Segment, Form, Button, Table, Dropdown } from 'semantic-ui-react'; 3 + import React, { useState } from 'react'; 5 4 import { isString } from 'lodash'; 5 + import { Button } from '../ui/button'; 6 + import { TableRow, TableCell } from '../ui/table'; 6 7 import styles from '../styles/common.module.css'; 7 8 8 9 interface Props { ··· 32 33 onChange, 33 34 onDelete, 34 35 }) => { 36 + const [phaseMenuOpen, setPhaseMenuOpen] = useState(false); 37 + 35 38 return ( 36 - <Table.Row className={styles.trialsRow}> 37 - <Table.Cell className={styles.conditionsNameRow}> 39 + <TableRow className={styles.trialsRow}> 40 + <TableCell className={styles.conditionsNameRow}> 38 41 <div style={{ alignSelf: 'center' }}>{num + 1}.</div> 39 42 <div>{name}</div> 40 - </Table.Cell> 43 + </TableCell> 41 44 42 - <Table.Cell className={styles.experimentRowName}> 45 + <TableCell className={styles.experimentRowName}> 43 46 <div>{condition}</div> 44 - </Table.Cell> 47 + </TableCell> 45 48 46 - <Table.Cell className={styles.experimentRowName}> 47 - <Form.Select 48 - fluid 49 - selection 49 + <TableCell className={styles.experimentRowName}> 50 + <select 51 + className="w-full border border-gray-300 rounded px-2 py-1" 50 52 value={response} 51 - onChange={(event, data) => { 52 - const val = data.value; 53 - onChange(num, 'response', isString(val) ? (val as string) : ''); 53 + onChange={(event) => { 54 + const val = event.target.value; 55 + onChange(num, 'response', isString(val) ? val : ''); 54 56 }} 55 - placeholder="Response" 56 - options={RESPONSE_OPTIONS} 57 - /> 58 - </Table.Cell> 57 + > 58 + <option value="">Response</option> 59 + {RESPONSE_OPTIONS.map((o) => ( 60 + <option key={o.key} value={o.value}>{o.text}</option> 61 + ))} 62 + </select> 63 + </TableCell> 59 64 60 - <Table.Cell className={styles.trialsTrialTypeRow}> 61 - <Segment basic className={styles.trialsTrialTypeSegment}> 65 + <TableCell className={styles.trialsTrialTypeRow}> 66 + <div className={styles.trialsTrialTypeSegment}> 62 67 <div 63 68 className={styles.trialsTrialTypeRowSelector} 64 - style={{ 65 - backgroundColor: phase === 'main' ? '#1AC4EF' : '#EB1B66', 66 - }} 69 + style={{ backgroundColor: phase === 'main' ? '#1AC4EF' : '#EB1B66' }} 67 70 > 68 71 {phase === 'main' ? 'Experimental' : 'Practice'} 69 72 </div> 70 - <Dropdown 71 - fluid 72 - style={{ 73 - display: 'grid', 74 - color: '#C4C4C4', 75 - justifyContent: 'end', 76 - }} 77 - > 78 - <Dropdown.Menu> 79 - <Dropdown.Item onClick={() => onChange(num, 'phase', 'main')}> 80 - <div>Experimental</div> 81 - </Dropdown.Item> 82 - <Dropdown.Item onClick={() => onChange(num, 'phase', 'practice')}> 83 - <div>Practice</div> 84 - </Dropdown.Item> 85 - </Dropdown.Menu> 86 - </Dropdown> 87 - </Segment> 88 - 89 - <Button 90 - secondary 91 - onClick={() => { 92 - onDelete(num); 93 - }} 94 - > 73 + <div style={{ position: 'relative', display: 'inline-block' }}> 74 + <button 75 + style={{ color: '#C4C4C4' }} 76 + onClick={() => setPhaseMenuOpen((o) => !o)} 77 + > 78 + 79 + </button> 80 + {phaseMenuOpen && ( 81 + <div style={{ position: 'absolute', right: 0, zIndex: 10, background: 'white', border: '1px solid #eee', borderRadius: 4 }}> 82 + <div className="px-3 py-1 cursor-pointer hover:bg-gray-100" onClick={() => { onChange(num, 'phase', 'main'); setPhaseMenuOpen(false); }}>Experimental</div> 83 + <div className="px-3 py-1 cursor-pointer hover:bg-gray-100" onClick={() => { onChange(num, 'phase', 'practice'); setPhaseMenuOpen(false); }}>Practice</div> 84 + </div> 85 + )} 86 + </div> 87 + </div> 88 + <Button variant="secondary" onClick={() => onDelete(num)}> 95 89 Delete 96 90 </Button> 97 - </Table.Cell> 98 - </Table.Row> 91 + </TableCell> 92 + </TableRow> 99 93 ); 100 94 };
+78 -156
src/renderer/components/DesignComponent/index.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { History } from 'history'; 3 - import { 4 - Grid, 5 - Button, 6 - Segment, 7 - Header, 8 - Image, 9 - Checkbox, 10 - CheckboxProps, 11 - } from 'semantic-ui-react'; 2 + import { Button } from '../ui/button'; 12 3 import { isNil } from 'lodash'; 13 4 import { toast } from 'react-toastify'; 14 5 import styles from '../styles/common.module.css'; ··· 51 42 }; 52 43 53 44 export interface DesignProps { 54 - history: History; 45 + navigate: (path: string) => void; 55 46 type: EXPERIMENTS; 56 47 title: string; 57 48 params: ExperimentParameters; ··· 95 86 } 96 87 97 88 handleStartExperiment() { 98 - this.props.history.push(SCREENS.COLLECT.route); 89 + this.props.navigate(SCREENS.COLLECT.route); 99 90 } 100 91 101 92 handleCustomizeExperiment() { ··· 133 124 this.setState({ isPreviewing: false }); 134 125 } 135 126 136 - handleEEGEnabled(_, data: CheckboxProps) { 137 - if (data.checked === undefined) return; 138 - this.props.ExperimentActions.SetEEGEnabled(data.checked); 127 + handleEEGEnabled(e: React.ChangeEvent<HTMLInputElement>) { 128 + this.props.ExperimentActions.SetEEGEnabled(e.target.checked); 139 129 this.props.ExperimentActions.SaveWorkspace(); 140 130 } 141 131 ··· 186 176 case DESIGN_STEPS.OVERVIEW: 187 177 default: 188 178 return ( 189 - <Grid 190 - stretched 191 - relaxed 192 - padded 193 - className={styles.contentGrid} 194 - style={{ alignItems: 'center' }} 195 - > 196 - <Grid.Row stretched> 197 - <Grid.Column stretched width={5}> 198 - <Segment basic> 199 - <Image src={Design.renderOverviewIcon(this.props.type)} /> 200 - </Segment> 201 - </Grid.Column> 202 - 203 - <Grid.Column stretched width={11}> 204 - <Segment basic> 205 - <Header as="h1">{overview.title}</Header> 206 - <p>{overview.overview}</p> 207 - </Segment> 208 - </Grid.Column> 209 - </Grid.Row> 210 - </Grid> 179 + <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 180 + <div className="w-5/12 p-2"> 181 + <img src={Design.renderOverviewIcon(this.props.type)} alt={overview.title} /> 182 + </div> 183 + <div className="w-7/12 p-2"> 184 + <h1>{overview.title}</h1> 185 + <p>{overview.overview}</p> 186 + </div> 187 + </div> 211 188 ); 212 189 213 190 case DESIGN_STEPS.BACKGROUND: 214 191 return ( 215 - <Grid 216 - relaxed 217 - padded 218 - className={styles.contentGrid} 219 - style={{ alignItems: 'center' }} 220 - > 221 - <Grid.Row> 222 - <Grid.Column stretched width={4}> 223 - <Segment basic> 224 - <Image src={Design.renderOverviewIcon(this.props.type)} /> 225 - </Segment> 226 - </Grid.Column> 227 - 228 - <Grid.Column stretched width={5}> 229 - <Segment basic> 230 - <p>{background?.first_column_statement}</p> 231 - <p style={{ fontWeight: 'bold' }}> 232 - {background?.first_column_question} 233 - </p> 234 - </Segment> 235 - </Grid.Column> 236 - 237 - <Grid.Column stretched width={5}> 238 - <Segment basic> 239 - <p>{background?.second_column_statement}</p> 240 - <p style={{ fontWeight: 'bold' }}> 241 - {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 - {background?.links.map((link) => ( 250 - <Button 251 - key={link.address} 252 - secondary 253 - onClick={() => { 254 - window.open(link.address, '_blank'); 255 - }} 256 - > 257 - {link.name} 258 - </Button> 259 - ))} 260 - </div> 261 - </Segment> 262 - </Grid.Column> 263 - </Grid.Row> 264 - </Grid> 192 + <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 193 + <div className="w-1/4 p-2"> 194 + <img src={Design.renderOverviewIcon(this.props.type)} alt="overview" /> 195 + </div> 196 + <div className="w-5/12 p-2"> 197 + <p>{background?.first_column_statement}</p> 198 + <p style={{ fontWeight: 'bold' }}>{background?.first_column_question}</p> 199 + </div> 200 + <div className="w-5/12 p-2"> 201 + <p>{background?.second_column_statement}</p> 202 + <p style={{ fontWeight: 'bold' }}>{background?.second_column_question}</p> 203 + </div> 204 + <div className="p-2"> 205 + <div className={styles.externalLinks}> 206 + {background?.links.map((link) => ( 207 + <Button 208 + key={link.address} 209 + variant="secondary" 210 + onClick={() => { window.open(link.address, '_blank'); }} 211 + > 212 + {link.name} 213 + </Button> 214 + ))} 215 + </div> 216 + </div> 217 + </div> 265 218 ); 266 219 267 220 case DESIGN_STEPS.PROTOCOL: 268 221 return ( 269 - <Grid 270 - relaxed 271 - padded 272 - className={styles.contentGrid} 273 - style={{ alignItems: 'center' }} 274 - > 275 - <Grid.Row stretched> 276 - <Grid.Column stretched width={7} textAlign="left"> 277 - <Segment basic> 278 - <Header as="h2">{protocol?.title}</Header> 279 - <p>{protocol?.protocol}</p> 280 - </Segment> 281 - </Grid.Column> 282 - 283 - <Grid.Column width={9}> 284 - <Grid> 285 - <Grid.Row> 286 - <Grid.Column width={5}> 287 - <Image 288 - src={Design.renderConditionIcon( 289 - protocol?.condition_first_img 290 - )} 291 - /> 292 - </Grid.Column> 293 - <Grid.Column width={10}> 294 - <Segment basic> 295 - <Header as="h3"> 296 - {protocol?.condition_first_title} 297 - </Header> 298 - <p>{protocol?.condition_first}</p> 299 - </Segment> 300 - </Grid.Column> 301 - </Grid.Row> 302 - 303 - <Grid.Row> 304 - <Grid.Column width={5}> 305 - <Image 306 - src={Design.renderConditionIcon( 307 - protocol?.condition_second_img 308 - )} 309 - /> 310 - </Grid.Column> 311 - <Grid.Column width={10}> 312 - <Segment basic> 313 - <Header as="h3"> 314 - {protocol?.condition_second_title} 315 - </Header> 316 - <p>{protocol?.condition_second}</p> 317 - </Segment> 318 - </Grid.Column> 319 - </Grid.Row> 320 - </Grid> 321 - </Grid.Column> 322 - </Grid.Row> 323 - </Grid> 222 + <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 223 + <div className="w-7/12 p-2 text-left"> 224 + <h2>{protocol?.title}</h2> 225 + <p>{protocol?.protocol}</p> 226 + </div> 227 + <div className="w-5/12 p-2 space-y-4"> 228 + <div className="flex gap-2 items-center"> 229 + <img 230 + className="w-1/3" 231 + src={Design.renderConditionIcon(protocol?.condition_first_img)} 232 + alt={protocol?.condition_first_title} 233 + /> 234 + <div className="w-2/3"> 235 + <h3>{protocol?.condition_first_title}</h3> 236 + <p>{protocol?.condition_first}</p> 237 + </div> 238 + </div> 239 + <div className="flex gap-2 items-center"> 240 + <img 241 + className="w-1/3" 242 + src={Design.renderConditionIcon(protocol?.condition_second_img)} 243 + alt={protocol?.condition_second_title} 244 + /> 245 + <div className="w-2/3"> 246 + <h3>{protocol?.condition_second_title}</h3> 247 + <p>{protocol?.condition_second}</p> 248 + </div> 249 + </div> 250 + </div> 251 + </div> 324 252 ); 325 253 326 254 case DESIGN_STEPS.PREVIEW: 327 255 return ( 328 - <Grid relaxed padded className={styles.contentGrid}> 329 - <Grid.Column 330 - stretched 331 - width={12} 332 - textAlign="right" 333 - verticalAlign="middle" 334 - className={styles.previewWindow} 335 - > 256 + <div className={['flex items-center p-4', styles.contentGrid].join(' ')}> 257 + <div className={['w-3/4', styles.previewWindow].join(' ')}> 336 258 <PreviewExperimentComponent 337 259 title={this.props.title} 338 260 params={this.props.params} ··· 341 263 onEnd={this.endPreview} 342 264 type={this.props.type} 343 265 /> 344 - </Grid.Column> 345 - <Grid.Column width={4} verticalAlign="middle"> 266 + </div> 267 + <div className="w-1/4 flex justify-center"> 346 268 <PreviewButton 347 269 isPreviewing={this.state.isPreviewing} 348 270 onClick={this.handlePreview} 349 271 /> 350 - </Grid.Column> 351 - </Grid> 272 + </div> 273 + </div> 352 274 ); 353 275 } 354 276 } ··· 365 287 activeStep={this.state.activeStep} 366 288 onStepClick={this.handleStepClick} 367 289 enableEEGToggle={ 368 - <Checkbox 369 - toggle 290 + <input 291 + type="checkbox" 370 292 defaultChecked={this.props.isEEGEnabled} 371 293 onChange={this.handleEEGEnabled} 372 294 className={styles.EEGToggle}
+25 -45
src/renderer/components/EEGExplorationComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { 3 - Grid, 4 - Button, 5 - Header, 6 - Segment, 7 - Image, 8 - Divider, 9 - } from 'semantic-ui-react'; 2 + import { Button } from './ui/button'; 10 3 import { Observable } from 'rxjs'; 11 - import { History } from 'history'; 12 4 import { 13 5 PLOTTING_INTERVAL, 14 6 CONNECTION_STATUS, ··· 24 16 import { SignalQualityData } from '../constants/interfaces'; 25 17 26 18 interface Props { 27 - history: History; 28 19 connectedDevice: Record<string, any>; 29 20 signalQualityObservable?: Observable<SignalQualityData>; 30 21 deviceType: DEVICES; ··· 77 68 78 69 render() { 79 70 return ( 80 - <Grid 81 - stretched 82 - relaxed 83 - padded 84 - className={styles.contentGrid} 85 - style={{ alignItems: 'center' }} 86 - > 71 + <div className={['flex items-center', styles.contentGrid].join(' ')}> 87 72 {this.props.connectionStatus === CONNECTION_STATUS.CONNECTED && 88 73 this.props.signalQualityObservable && ( 89 - <Grid.Row> 90 - <Grid.Column stretched width={6}> 74 + <div className="flex w-full"> 75 + <div className="w-2/5"> 91 76 <SignalQualityIndicatorComponent 92 77 signalQualityObservable={this.props.signalQualityObservable} 93 78 plottingInterval={PLOTTING_INTERVAL} 94 79 /> 95 - </Grid.Column> 96 - <Grid.Column stretched width={10}> 80 + </div> 81 + <div className="w-3/5"> 97 82 <div className={styles.disconnectButtonContainer}> 98 - <Button secondary onClick={this.handleStopConnect}> 83 + <Button variant="secondary" onClick={this.handleStopConnect}> 99 84 Disconnect EEG Device 100 85 </Button> 101 86 </div> ··· 104 89 deviceType={this.props.deviceType} 105 90 plottingInterval={PLOTTING_INTERVAL} 106 91 /> 107 - </Grid.Column> 108 - </Grid.Row> 92 + </div> 93 + </div> 109 94 )} 110 95 {this.props.connectionStatus !== CONNECTION_STATUS.CONNECTED && ( 111 - <Grid.Row stretched> 112 - <Grid.Column stretched width={5}> 113 - <Segment basic> 114 - <Image src={eegImage} /> 115 - </Segment> 116 - </Grid.Column> 96 + <div className="flex w-full"> 97 + <div className="w-5/12 p-2"> 98 + <img src={eegImage} alt="EEG device" /> 99 + </div> 117 100 118 - <Grid.Column stretched width={11}> 119 - <Segment basic> 120 - <Header as="h1">Explore Raw EEG</Header> 121 - <Divider /> 122 - <p> 123 - Connect directly to an EEG device and view raw streaming data 124 - </p> 125 - <Button primary onClick={this.handleStartConnect}> 126 - Connect 127 - </Button> 128 - </Segment> 129 - </Grid.Column> 101 + <div className="w-7/12 p-2"> 102 + <h1>Explore Raw EEG</h1> 103 + <hr className="my-2" /> 104 + <p> 105 + Connect directly to an EEG device and view raw streaming data 106 + </p> 107 + <Button variant="default" onClick={this.handleStartConnect}> 108 + Connect 109 + </Button> 110 + </div> 130 111 <ConnectModal 131 - history={this.props.history} 132 112 open={this.state.isConnectModalOpen} 133 113 onClose={this.handleConnectModalClose} 134 114 connectedDevice={this.props.connectedDevice} ··· 139 119 DeviceActions={this.props.DeviceActions} 140 120 availableDevices={this.props.availableDevices} 141 121 /> 142 - </Grid.Row> 122 + </div> 143 123 )} 144 - </Grid> 124 + </div> 145 125 ); 146 126 } 147 127 }
+12 -15
src/renderer/components/HomeComponent/ExperimentCard.tsx
··· 1 1 import React, { ReactElement } from 'react'; 2 - import { Segment, Grid, Header, Image } from 'semantic-ui-react'; 3 2 import styles from '../styles/common.module.css'; 4 3 5 4 interface ExperimentCardProps { ··· 16 15 description, 17 16 }: ExperimentCardProps): ReactElement { 18 17 return ( 19 - <Segment> 20 - <Grid columns="two" className={styles.experimentCard} onClick={onClick}> 21 - <Grid.Row> 22 - <Grid.Column width={4} className={styles.experimentCardImage}> 23 - <Image src={icon} /> 24 - </Grid.Column> 25 - <Grid.Column width={12} className={styles.descriptionContainer}> 26 - <Header as="h1" className={styles.experimentCardHeader}> 27 - {title} 28 - </Header> 18 + <div className="border border-gray-200 rounded-lg p-4 shadow-sm"> 19 + <div className={styles.experimentCard} onClick={onClick}> 20 + <div className="flex"> 21 + <div className={[styles.experimentCardImage, 'w-1/4'].join(' ')}> 22 + <img src={icon} alt={title} /> 23 + </div> 24 + <div className={[styles.descriptionContainer, 'w-3/4'].join(' ')}> 25 + <h1 className={styles.experimentCardHeader}>{title}</h1> 29 26 <div className={styles.experimentCardDescription}> 30 27 <p>{description}</p> 31 28 </div> 32 - </Grid.Column> 33 - </Grid.Row> 34 - </Grid> 35 - </Segment> 29 + </div> 30 + </div> 31 + </div> 32 + </div> 36 33 ); 37 34 }
+13 -24
src/renderer/components/HomeComponent/OverviewComponent.tsx
··· 1 1 import React, { Component, useMemo, useState } from 'react'; 2 - import { Grid, Header, Button, Segment } from 'semantic-ui-react'; 2 + import { Button } from '../ui/button'; 3 3 import styles from '../styles/common.module.css'; 4 4 import { EXPERIMENTS } from '../../constants/constants'; 5 5 import SecondaryNavComponent from '../SecondaryNavComponent'; ··· 40 40 case OVERVIEW_STEPS.OVERVIEW: 41 41 default: 42 42 return ( 43 - <Grid stretched relaxed padded className={styles.contentGrid}> 44 - <Grid.Column 45 - stretched 46 - width={6} 47 - textAlign="right" 48 - verticalAlign="middle" 49 - > 50 - <Header as="h1">{experiment?.text.overview.title}</Header> 51 - </Grid.Column> 52 - <Grid.Column stretched width={6} verticalAlign="middle"> 53 - <Segment as="p" basic> 54 - {experiment?.text.overview.overview} 55 - </Segment> 56 - </Grid.Column> 57 - </Grid> 43 + <div className={['flex items-center gap-8', styles.contentGrid].join(' ')}> 44 + <div className="flex-1 text-right"> 45 + <h1>{experiment?.text.overview.title}</h1> 46 + </div> 47 + <div className="flex-1"> 48 + <p>{experiment?.text.overview.overview}</p> 49 + </div> 50 + </div> 58 51 ); 59 52 } 60 53 }; 61 54 62 55 return ( 63 56 <> 64 - <Button 65 - basic 66 - circular 67 - size="huge" 68 - floated="right" 69 - icon="x" 57 + <button 70 58 className={styles.closeButton} 71 59 onClick={onCloseOverview} 72 - /> 60 + aria-label="Close" 61 + >✕</button> 73 62 <SecondaryNavComponent 74 63 title={type} 75 64 steps={OVERVIEW_STEPS} 76 65 activeStep={activeStep} 77 66 onStepClick={handleStepClick} 78 67 saveButton={ 79 - <Button primary onClick={() => onStartExperiment(type)}> 68 + <Button variant="default" onClick={() => onStartExperiment(type)}> 80 69 Start Experiment 81 70 </Button> 82 71 }
+84 -121
src/renderer/components/HomeComponent/index.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { isNil } from 'lodash'; 3 - import { Grid, Button, Header, Image, Table } from 'semantic-ui-react'; 3 + import { Button } from '../ui/button'; 4 + import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '../ui/table'; 4 5 import { toast } from 'react-toastify'; 5 6 import dayjs from 'dayjs'; 6 7 import relativeTime from 'dayjs/plugin/relativeTime'; 7 - import { History } from 'history'; 8 - 9 8 dayjs.extend(relativeTime); 10 9 import { Observable } from 'rxjs'; 11 10 import styles from '../styles/common.module.css'; ··· 61 60 deviceAvailability: DEVICE_AVAILABILITY; 62 61 deviceType: DEVICES; 63 62 ExperimentActions: typeof ExperimentActions; 64 - history: History; 63 + navigate: (path: string) => void; 65 64 PyodideActions: typeof PyodideActions; 66 65 signalQualityObservable?: Observable<SignalQualityData>; 67 66 topoPlot: { ··· 132 131 title: experimentType, 133 132 type: experimentType, 134 133 }); 135 - this.props.history.push(SCREENS.DESIGN.route); 134 + this.props.navigate(SCREENS.DESIGN.route); 136 135 } 137 136 } 138 137 ··· 151 150 title, 152 151 type: EXPERIMENTS.CUSTOM, 153 152 }); 154 - this.props.history.push(SCREENS.DESIGN.route); 153 + this.props.navigate(SCREENS.DESIGN.route); 155 154 } 156 155 157 156 // Load recent workspace by copying saved 'experiment' redux state into current redux state ··· 172 171 .experimentObject, 173 172 }; 174 173 this.props.ExperimentActions.SetState(deserializedWorkspaceState as any); 175 - this.props.history.push(SCREENS.DESIGN.route); 174 + this.props.navigate(SCREENS.DESIGN.route); 176 175 } 177 176 178 177 handleOpenOverview(type: EXPERIMENTS) { ··· 207 206 switch (this.state.activeStep) { 208 207 case HOME_STEPS.RECENT: 209 208 return ( 210 - <Grid 211 - stackable 212 - padded 213 - columns="equal" 214 - className={styles.myExperimentsPage} 215 - > 209 + <div className={styles.myExperimentsPage}> 216 210 {this.state.recentWorkspaces.length > 0 ? ( 217 - <Table basic="very"> 218 - <Table.Header> 219 - <Table.Row className={styles.experimentHeaderRow}> 220 - <Table.HeaderCell className={styles.experimentHeaderName}> 211 + <Table> 212 + <TableHeader> 213 + <TableRow className={styles.experimentHeaderRow}> 214 + <TableHead className={styles.experimentHeaderName}> 221 215 Experiment name 222 - </Table.HeaderCell> 223 - <Table.HeaderCell>Date Last Opened</Table.HeaderCell> 224 - <Table.HeaderCell 225 - className={styles.experimentHeaderActionsName} 226 - > 216 + </TableHead> 217 + <TableHead>Date Last Opened</TableHead> 218 + <TableHead className={styles.experimentHeaderActionsName}> 227 219 Actions 228 - </Table.HeaderCell> 229 - </Table.Row> 230 - </Table.Header> 220 + </TableHead> 221 + </TableRow> 222 + </TableHeader> 231 223 232 - <Table.Body className={styles.experimentTable}> 224 + <TableBody className={styles.experimentTable}> 233 225 {this.state.recentWorkspaces 234 226 .sort((a, b) => { 235 227 const aState = this.state.workspaceStates[a]; ··· 247 239 } 248 240 const { dateModified } = workspaceState; 249 241 return ( 250 - <Table.Row key={dir} className={styles.experimentRow}> 251 - <Table.Cell className={styles.experimentRowName}> 242 + <TableRow key={dir} className={styles.experimentRow}> 243 + <TableCell className={styles.experimentRowName}> 252 244 {dir} 253 - </Table.Cell> 254 - <Table.Cell className={styles.experimentRowName}> 245 + </TableCell> 246 + <TableCell className={styles.experimentRowName}> 255 247 {dateModified && dayjs(dateModified).fromNow()} 256 - </Table.Cell> 257 - <Table.Cell className={styles.experimentRowName}> 248 + </TableCell> 249 + <TableCell className={styles.experimentRowName}> 258 250 <Button 259 - secondary 251 + variant="secondary" 260 252 onClick={() => this.handleDeleteWorkspace(dir)} 261 253 > 262 254 Delete 263 255 </Button> 264 256 <Button 265 - secondary 257 + variant="secondary" 266 258 onClick={() => openWorkspaceDir(dir)} 267 259 > 268 260 Go to Folder 269 261 </Button> 270 262 <Button 271 - primary 263 + variant="default" 272 264 onClick={() => 273 265 this.handleLoadRecentWorkspace(dir) 274 266 } 275 267 > 276 268 Open Experiment 277 269 </Button> 278 - </Table.Cell> 279 - </Table.Row> 270 + </TableCell> 271 + </TableRow> 280 272 ); 281 273 })} 282 - </Table.Body> 274 + </TableBody> 283 275 </Table> 284 276 ) : ( 285 - <Grid.Column textAlign="center"> 286 - <Image 277 + <div className="text-center"> 278 + <img 287 279 src={divingMan} 288 - centered 289 280 className={styles.noExperimentsImage} 281 + alt="No experiments" 290 282 /> 291 - <Header className={styles.noExperimentsTitle}> 283 + <h2 className={styles.noExperimentsTitle}> 292 284 You don&apos;t have any experiments yet 293 - </Header> 285 + </h2> 294 286 <p className={styles.noExperimentsText}> 295 287 Head over to the &quot;Experiment Bank&quot; section to start 296 288 an experiment. 297 289 </p> 298 290 <Button 299 - primary 291 + variant="default" 300 292 onClick={() => this.handleStepClick('EXPERIMENT BANK')} 301 293 > 302 294 View Experiments 303 295 </Button> 304 - </Grid.Column> 296 + </div> 305 297 )} 306 - </Grid> 298 + </div> 307 299 ); 308 300 case HOME_STEPS.NEW: 309 301 default: 310 302 return ( 311 - <Grid columns="two" relaxed padded> 312 - <Grid.Row> 313 - <Grid.Column> 314 - <ExperimentCard 315 - onClick={() => this.handleNewExperiment(EXPERIMENTS.N170)} 316 - icon={faceHouseIcon} 317 - title="Faces/Houses" 318 - description={`Explore how people react to different kinds of 319 - images, like faces vs. houses.`} 320 - /> 321 - </Grid.Column> 322 - 323 - <Grid.Column> 324 - <ExperimentCard 325 - onClick={() => this.handleNewExperiment(EXPERIMENTS.STROOP)} 326 - icon={stroopIcon} 327 - title="Stroop" 328 - description={`Investigate why it is hard to deal with 329 - contradictory information (like the word "RED" 330 - printed in blue).`} 331 - /> 332 - </Grid.Column> 333 - </Grid.Row> 334 - <Grid.Row> 335 - <Grid.Column> 336 - <ExperimentCard 337 - onClick={() => this.handleNewExperiment(EXPERIMENTS.MULTI)} 338 - icon={multitaskingIcon} 339 - title="Multi-tasking" 340 - description={`Explore why it is challenging to carry out multiple 341 - tasks at the same time.`} 342 - /> 343 - </Grid.Column> 344 - 345 - <Grid.Column> 346 - <ExperimentCard 347 - onClick={() => this.handleNewExperiment(EXPERIMENTS.SEARCH)} 348 - icon={searchIcon} 349 - title="Visual Search" 350 - description={`Examine why it is difficult to find your keys in a 351 - messy room.`} 352 - /> 353 - </Grid.Column> 354 - </Grid.Row> 355 - {/* <Grid.Row> 356 - <Grid.Column> 357 - <ExperimentCard 358 - onClick={() => this.handleNewExperiment(EXPERIMENTS.CUSTOM)} 359 - icon={customIcon} 360 - title="Custom" 361 - description="Design your own experiment!" 362 - /> 363 - </Grid.Column> 364 - 365 - <Grid.Column /> 366 - </Grid.Row> */} 367 - </Grid> 303 + <div className="grid grid-cols-2 gap-4 p-4"> 304 + <ExperimentCard 305 + onClick={() => this.handleNewExperiment(EXPERIMENTS.N170)} 306 + icon={faceHouseIcon} 307 + title="Faces/Houses" 308 + description={`Explore how people react to different kinds of 309 + images, like faces vs. houses.`} 310 + /> 311 + <ExperimentCard 312 + onClick={() => this.handleNewExperiment(EXPERIMENTS.STROOP)} 313 + icon={stroopIcon} 314 + title="Stroop" 315 + description={`Investigate why it is hard to deal with 316 + contradictory information (like the word "RED" 317 + printed in blue).`} 318 + /> 319 + <ExperimentCard 320 + onClick={() => this.handleNewExperiment(EXPERIMENTS.MULTI)} 321 + icon={multitaskingIcon} 322 + title="Multi-tasking" 323 + description={`Explore why it is challenging to carry out multiple 324 + tasks at the same time.`} 325 + /> 326 + <ExperimentCard 327 + onClick={() => this.handleNewExperiment(EXPERIMENTS.SEARCH)} 328 + icon={searchIcon} 329 + title="Visual Search" 330 + description={`Examine why it is difficult to find your keys in a 331 + messy room.`} 332 + /> 333 + </div> 368 334 ); 369 335 case HOME_STEPS.EXPLORE: 370 336 return ( 371 337 <EEGExplorationComponent 372 - history={this.props.history} 373 338 connectedDevice={this.props.connectedDevice} 374 339 signalQualityObservable={this.props.signalQualityObservable} 375 340 deviceType={this.props.deviceType} ··· 381 346 ); 382 347 case HOME_STEPS.PYODIDE_TEST: 383 348 return ( 384 - <Grid columns="two" relaxed padded> 385 - <Grid.Row> 386 - <Grid.Column> 387 - <Button onClick={() => this.props.PyodideActions.LoadTopo()}> 388 - Generate Plot 389 - </Button> 390 - </Grid.Column> 391 - <Grid.Column> 392 - <PyodidePlotWidget 393 - title={'Test Plot'} 394 - imageTitle={`Test-Topoplot`} 395 - plotMIMEBundle={this.props.topoPlot} 396 - /> 397 - </Grid.Column> 398 - </Grid.Row> 399 - </Grid> 349 + <div className="grid grid-cols-2 gap-4 p-4"> 350 + <div> 351 + <Button variant="default" onClick={() => this.props.PyodideActions.LoadTopo()}> 352 + Generate Plot 353 + </Button> 354 + </div> 355 + <div> 356 + <PyodidePlotWidget 357 + title={'Test Plot'} 358 + imageTitle={`Test-Topoplot`} 359 + plotMIMEBundle={this.props.topoPlot} 360 + /> 361 + </div> 362 + </div> 400 363 ); 401 364 } 402 365 } ··· 414 377 return ( 415 378 <> 416 379 <SecondaryNavComponent 417 - title={<Image src={appLogo} />} 380 + title={<img src={appLogo} alt="BrainWaves" />} 418 381 steps={HOME_STEPS} 419 382 activeStep={this.state.activeStep} 420 383 onStepClick={this.handleStepClick}
+48 -50
src/renderer/components/InputCollect.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { Input, Modal, Button, InputOnChangeData } from 'semantic-ui-react'; 3 2 import { sanitizeTextInput } from '../utils/ui'; 4 3 import styles from './styles/common.module.css'; 4 + import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; 5 + import { Button } from './ui/button'; 5 6 6 7 interface InputData { 7 8 subject: string; ··· 40 41 this.handleExit = this.handleExit.bind(this); 41 42 } 42 43 43 - handleTextEntry(data: InputOnChangeData, field: keyof InputData) { 44 + handleTextEntry(event: React.ChangeEvent<HTMLInputElement>, field: keyof InputData) { 45 + const value = event.target.value; 44 46 switch (field) { 45 47 case 'session': 46 - this.setState({ [field]: parseInt(data.value, 10) }); 48 + this.setState({ [field]: parseInt(value, 10) }); 47 49 break; 48 50 case 'group': 49 - this.setState({ [field]: data.value }); 51 + this.setState({ [field]: value }); 50 52 break; 51 53 case 'subject': 52 54 default: 53 - this.setState({ subject: data.value }); 55 + this.setState({ subject: value }); 54 56 } 55 57 } 56 58 ··· 83 85 84 86 render() { 85 87 return ( 86 - <Modal 87 - dimmer="inverted" 88 - centered 89 - className={styles.inputModal} 90 - open={this.props.open} 91 - onClose={this.handleExit} 92 - > 93 - <Modal.Content> 94 - Enter Subject ID 95 - <Input 96 - focus 97 - fluid 98 - error={this.state.isSubjectError} 99 - onChange={(object, data) => this.handleTextEntry(data, 'subject')} 100 - onKeyDown={this.handleEnterSubmit} 101 - value={this.state.subject} 102 - autoFocus 103 - /> 104 - </Modal.Content> 105 - <Modal.Content> 106 - Enter group name (optional) 107 - <Input 108 - focus 109 - fluid 110 - onChange={(object, data) => this.handleTextEntry(data, 'group')} 111 - onKeyDown={this.handleEnterSubmit} 112 - value={this.state.group} 113 - /> 114 - </Modal.Content> 115 - <Modal.Content> 116 - Enter session number 117 - <Input 118 - focus 119 - fluid 120 - error={this.state.isSessionError} 121 - onChange={(object, data) => this.handleTextEntry(data, 'session')} 122 - onKeyDown={this.handleEnterSubmit} 123 - value={this.state.session} 124 - type="number" 125 - /> 126 - </Modal.Content> 127 - <Modal.Actions> 128 - <Button color="blue" content="OK" onClick={this.handleClose} /> 129 - </Modal.Actions> 130 - </Modal> 88 + <Dialog open={this.props.open} onOpenChange={(open) => { if (!open) this.handleExit(); }}> 89 + <DialogContent className={styles.inputModal}> 90 + <DialogHeader> 91 + <DialogTitle>{this.props.header}</DialogTitle> 92 + </DialogHeader> 93 + <div className="space-y-4"> 94 + <div> 95 + <label className="block text-sm mb-1">Enter Subject ID</label> 96 + <input 97 + className={['w-full border rounded px-3 py-2', this.state.isSubjectError ? 'border-red-500' : 'border-gray-300'].join(' ')} 98 + onChange={(e) => this.handleTextEntry(e, 'subject')} 99 + onKeyDown={this.handleEnterSubmit} 100 + value={this.state.subject} 101 + autoFocus 102 + /> 103 + </div> 104 + <div> 105 + <label className="block text-sm mb-1">Enter group name (optional)</label> 106 + <input 107 + className="w-full border border-gray-300 rounded px-3 py-2" 108 + onChange={(e) => this.handleTextEntry(e, 'group')} 109 + onKeyDown={this.handleEnterSubmit} 110 + value={this.state.group} 111 + /> 112 + </div> 113 + <div> 114 + <label className="block text-sm mb-1">Enter session number</label> 115 + <input 116 + className={['w-full border rounded px-3 py-2', this.state.isSessionError ? 'border-red-500' : 'border-gray-300'].join(' ')} 117 + type="number" 118 + onChange={(e) => this.handleTextEntry(e, 'session')} 119 + onKeyDown={this.handleEnterSubmit} 120 + value={this.state.session} 121 + /> 122 + </div> 123 + </div> 124 + <div className="flex justify-end mt-4"> 125 + <Button variant="default" onClick={this.handleClose}>OK</Button> 126 + </div> 127 + </DialogContent> 128 + </Dialog> 131 129 ); 132 130 } 133 131 }
+16 -21
src/renderer/components/InputModal.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { Input, Modal, Button } from 'semantic-ui-react'; 3 2 import { debounce } from 'lodash'; 4 3 import { sanitizeTextInput } from '../utils/ui'; 5 4 import styles from './styles/common.module.css'; 5 + import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; 6 + import { Button } from './ui/button'; 6 7 7 8 interface Props { 8 9 open: boolean; ··· 29 30 this.handleExit = this.handleExit.bind(this); 30 31 } 31 32 32 - handleTextEntry(event, data) { 33 - this.setState({ enteredText: data.value }); 33 + handleTextEntry(event: React.ChangeEvent<HTMLInputElement>) { 34 + this.setState({ enteredText: event.target.value }); 34 35 } 35 36 36 37 handleClose() { ··· 53 54 54 55 render() { 55 56 return ( 56 - <Modal 57 - dimmer="inverted" 58 - centered 59 - className={styles.inputModal} 60 - open={this.props.open} 61 - onClose={this.handleExit} 62 - > 63 - <Modal.Content>{this.props.header}</Modal.Content> 64 - <Modal.Content> 65 - <Input 66 - focus 67 - fluid 68 - error={this.state.isError} 57 + <Dialog open={this.props.open} onOpenChange={(open) => { if (!open) this.handleExit(); }}> 58 + <DialogContent className={styles.inputModal}> 59 + <DialogHeader> 60 + <DialogTitle>{this.props.header}</DialogTitle> 61 + </DialogHeader> 62 + <input 63 + className={['w-full border rounded px-3 py-2', this.state.isError ? 'border-red-500' : 'border-gray-300'].join(' ')} 69 64 onChange={this.handleTextEntry} 70 65 onKeyDown={this.handleEnterSubmit} 71 66 autoFocus 72 67 /> 73 - </Modal.Content> 74 - <Modal.Actions> 75 - <Button color="blue" content="OK" onClick={this.handleClose} /> 76 - </Modal.Actions> 77 - </Modal> 68 + <div className="flex justify-end mt-4"> 69 + <Button variant="default" onClick={this.handleClose}>OK</Button> 70 + </div> 71 + </DialogContent> 72 + </Dialog> 78 73 ); 79 74 } 80 75 }
+4 -7
src/renderer/components/PreviewButtonComponent.tsx
··· 1 1 import React, { PureComponent } from 'react'; 2 - import { Button, ButtonProps } from 'semantic-ui-react'; 2 + import { Button } from './ui/button'; 3 3 4 4 interface Props { 5 5 isPreviewing: boolean; 6 - onClick: ( 7 - event: React.MouseEvent<HTMLButtonElement, MouseEvent>, 8 - data: ButtonProps 9 - ) => void; 6 + onClick: (event: React.MouseEvent<HTMLButtonElement>) => void; 10 7 } 11 8 12 9 export default class PreviewButton extends PureComponent<Props> { 13 10 render() { 14 11 if (!this.props.isPreviewing) { 15 12 return ( 16 - <Button secondary onClick={this.props.onClick}> 13 + <Button variant="secondary" onClick={this.props.onClick}> 17 14 Preview Experiment 18 15 </Button> 19 16 ); 20 17 } 21 18 return ( 22 - <Button negative onClick={this.props.onClick}> 19 + <Button variant="destructive" onClick={this.props.onClick}> 23 20 Stop Preview 24 21 </Button> 25 22 );
+1 -2
src/renderer/components/PreviewExperimentComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { Segment } from 'semantic-ui-react'; 3 2 import { ExperimentWindow } from './ExperimentWindow'; 4 3 import styles from './styles/collect.module.css'; 5 4 ··· 32 31 if (!this.props.isPreviewing) { 33 32 return ( 34 33 <div className={styles.previewPlaceholder}> 35 - <Segment basic> The experiment will be shown in the window </Segment> 34 + <div className="p-2"> The experiment will be shown in the window </div> 36 35 </div> 37 36 ); 38 37 }
+4 -4
src/renderer/components/PyodidePlotWidget.tsx
··· 1 1 import React, { Component } from 'react'; 2 - import { Segment, Button } from 'semantic-ui-react'; 2 + import { Button } from './ui/button'; 3 3 import { 4 4 richestMimetype, 5 5 standardDisplayOrder, ··· 67 67 renderSaveButton() { 68 68 if (this.state.rawData) { 69 69 return ( 70 - <Button primary size="tiny" onClick={this.handleSave}> 70 + <Button variant="default" size="sm" onClick={this.handleSave}> 71 71 Save Image 72 72 </Button> 73 73 ); ··· 76 76 77 77 render() { 78 78 return ( 79 - <Segment basic> 79 + <div className="p-2"> 80 80 {this.renderResults()} 81 81 {this.renderSaveButton()} 82 - </Segment> 82 + </div> 83 83 ); 84 84 } 85 85 }
+3 -7
src/renderer/components/SecondaryNavComponent/SecondaryNavSegment.tsx
··· 1 1 import React from 'react'; 2 - import { Grid } from 'semantic-ui-react'; 3 2 import styles from '../styles/secondarynav.module.css'; 4 3 5 4 interface Props { ··· 10 9 11 10 export default function SecondaryNavSegment(props: Props) { 12 11 return ( 13 - <Grid.Column 14 - as="a" 12 + <a 15 13 onClick={props.onClick} 16 - textAlign="center" 17 - verticalAlign="bottom" 18 - className={[props.style, styles.secondaryNavSegment].join(' ')} 14 + className={[props.style, styles.secondaryNavSegment, 'text-center flex items-end justify-center'].join(' ')} 19 15 > 20 16 {props.title} 21 - </Grid.Column> 17 + </a> 22 18 ); 23 19 }
+51 -32
src/renderer/components/SecondaryNavComponent/index.tsx
··· 1 - import React, { Component } from 'react'; 2 - import { Grid, Header, Dropdown } from 'semantic-ui-react'; 1 + import React, { Component, useState } from 'react'; 3 2 import { NavLink } from 'react-router-dom'; 4 3 import styles from '../styles/secondarynav.module.css'; 5 4 import SecondaryNavSegment from './SecondaryNavSegment'; 6 5 import { SCREENS } from '../../constants/constants'; 7 6 7 + interface SettingsDropdownProps { 8 + enableEEGToggle: JSX.Element; 9 + saveButton?: JSX.Element; 10 + dropdownSettings: string; 11 + dropdownMenu: string; 12 + dropdownItem: string; 13 + homeRoute: string; 14 + } 15 + 16 + function SettingsDropdown({ enableEEGToggle, saveButton, dropdownSettings, dropdownMenu, dropdownItem, homeRoute }: SettingsDropdownProps) { 17 + const [open, setOpen] = useState(false); 18 + return ( 19 + <div style={{ position: 'relative', display: 'inline-block' }}> 20 + <button className={dropdownSettings} onClick={() => setOpen((o) => !o)} aria-label="Settings"> 21 + 22 + </button> 23 + {open && ( 24 + <div className={dropdownMenu} style={{ position: 'absolute', right: 0, zIndex: 50 }}> 25 + <div className={dropdownItem} onClick={(e) => e.stopPropagation()}> 26 + <div>Enable EEG</div> 27 + {enableEEGToggle} 28 + </div> 29 + <div className={dropdownItem}> 30 + <NavLink to={homeRoute} onClick={() => setOpen(false)}> 31 + <p>Exit Experiment</p> 32 + </NavLink> 33 + </div> 34 + </div> 35 + )} 36 + {saveButton} 37 + </div> 38 + ); 39 + } 40 + 8 41 interface Props { 9 42 title: string | React.ReactNode; 10 43 steps: { ··· 24 57 renderTitle() { 25 58 if (typeof this.props.title === 'string') { 26 59 return ( 27 - <Header className={styles.secondaryNavContainerExpName}> 60 + <span className={styles.secondaryNavContainerExpName}> 28 61 {this.props.title} 29 - </Header> 62 + </span> 30 63 ); 31 64 } 32 65 return this.props.title; ··· 53 86 54 87 render() { 55 88 return ( 56 - <Grid verticalAlign="middle" className={styles.secondaryNavContainer}> 57 - <Grid.Column width={3} verticalAlign="bottom"> 89 + <div className={['flex items-center', styles.secondaryNavContainer].join(' ')}> 90 + <div className="w-1/4 flex items-end"> 58 91 {this.renderTitle()} 59 - </Grid.Column> 92 + </div> 60 93 61 94 {this.renderSteps()} 62 95 63 96 {this.props.enableEEGToggle && ( 64 - <Grid.Column width={2} floated="right"> 97 + <div className="ml-auto"> 65 98 <div className={styles.settingsButtons}> 66 - <Dropdown 67 - icon="setting" 68 - direction="left" 69 - fluid 70 - className={styles.dropdownSettings} 71 - > 72 - <Dropdown.Menu className={styles.dropdownMenu}> 73 - <Dropdown.Item 74 - className={styles.dropdownItem} 75 - onClick={(e) => e.stopPropagation()} 76 - > 77 - <div>Enable EEG</div> 78 - {this.props.enableEEGToggle} 79 - </Dropdown.Item> 80 - <Dropdown.Item> 81 - <NavLink to={SCREENS.HOME.route}> 82 - <p>Exit Experiment</p> 83 - </NavLink> 84 - </Dropdown.Item> 85 - </Dropdown.Menu> 86 - </Dropdown> 87 - {this.props.saveButton} 99 + <SettingsDropdown 100 + enableEEGToggle={this.props.enableEEGToggle} 101 + saveButton={this.props.saveButton} 102 + dropdownSettings={styles.dropdownSettings} 103 + dropdownMenu={styles.dropdownMenu} 104 + dropdownItem={styles.dropdownItem} 105 + homeRoute={SCREENS.HOME.route} 106 + /> 88 107 </div> 89 - </Grid.Column> 108 + </div> 90 109 )} 91 - </Grid> 110 + </div> 92 111 ); 93 112 } 94 113 }
+3 -4
src/renderer/components/SignalQualityIndicatorComponent.tsx
··· 1 1 import React, { Component } from 'react'; 2 2 import { isNil } from 'lodash'; 3 - import { Segment } from 'semantic-ui-react'; 4 3 import * as d3 from 'd3'; 5 4 import { Observable, Subscription } from 'rxjs'; 6 5 import SignalQualityIndicatorSVG from './svgs/SignalQualityIndicatorSVG'; 7 - import { PipesEpoch, SignalQualityData } from '../constants/interfaces'; 6 + import { SignalQualityData } from '../constants/interfaces'; 8 7 9 8 interface Props { 10 9 signalQualityObservable: Observable<SignalQualityData>; ··· 58 57 59 58 render() { 60 59 return ( 61 - <Segment basic size="massive"> 60 + <div> 62 61 <SignalQualityIndicatorSVG /> 63 - </Segment> 62 + </div> 64 63 ); 65 64 } 66 65 }
+3 -7
src/renderer/components/TopNavComponent/PrimaryNavSegment.tsx
··· 1 1 import React from 'react'; 2 - import { Grid } from 'semantic-ui-react'; 3 2 import { NavLink } from 'react-router-dom'; 4 3 import styles from '../styles/topnavbar.module.css'; 5 4 ··· 10 9 order: number; 11 10 } 12 11 13 - const PrimaryNavSegment = (props) => { 12 + const PrimaryNavSegment = (props: Props) => { 14 13 return ( 15 - <Grid.Column 16 - width={2} 17 - className={[props.style, styles.navColumn].join(' ')} 18 - > 14 + <div className={[props.style, styles.navColumn].join(' ')}> 19 15 <NavLink to={props.route}> 20 16 <div className={styles.numberBubble}>{props.order}</div> 21 17 {props.title} 22 18 </NavLink> 23 - </Grid.Column> 19 + </div> 24 20 ); 25 21 }; 26 22
+84 -95
src/renderer/components/TopNavComponent/index.tsx
··· 1 - import React, { Component } from 'react'; 2 - import { Grid, Segment, Image, Dropdown } from 'semantic-ui-react'; 3 - import { NavLink } from 'react-router-dom'; 1 + import React, { useState } from 'react'; 2 + import { NavLink, useLocation } from 'react-router-dom'; 4 3 import { isNil } from 'lodash'; 5 4 import { EXPERIMENTS, SCREENS } from '../../constants/constants'; 6 5 import styles from '../styles/topnavbar.module.css'; ··· 14 13 15 14 export interface Props { 16 15 title: string | null | undefined; 17 - location: { pathname: string; search: string; hash: string }; 18 16 isRunning: boolean; 19 17 ExperimentActions: typeof ExperimentActions; 20 18 isEEGEnabled: boolean; 21 19 type: EXPERIMENTS; 22 20 } 23 21 24 - interface State { 25 - recentWorkspaces: Array<string>; 26 - } 22 + export default function TopNavComponent(props: Props) { 23 + const location = useLocation(); 24 + const [recentWorkspaces, setRecentWorkspaces] = useState<string[]>([]); 27 25 28 - export default class TopNavComponent extends Component<Props, State> { 29 - constructor(props: Props) { 30 - super(props); 31 - this.state = { 32 - recentWorkspaces: [], 33 - }; 34 - this.getStyleForScreen = this.getStyleForScreen.bind(this); 35 - this.updateWorkspaces = this.updateWorkspaces.bind(this); 36 - this.handleLoadRecentWorkspace = this.handleLoadRecentWorkspace.bind(this); 37 - } 38 - 39 - getStyleForScreen(navSegmentScreen: (typeof SCREENS)[keyof typeof SCREENS]) { 40 - if (navSegmentScreen.route === this.props.location.pathname) { 26 + const getStyleForScreen = ( 27 + navSegmentScreen: (typeof SCREENS)[keyof typeof SCREENS] 28 + ) => { 29 + if (navSegmentScreen.route === location.pathname) { 41 30 return styles.activeNavColumn; 42 31 } 43 - 44 32 const routeOrder = Object.values(SCREENS).find( 45 33 (screen) => screen.route === navSegmentScreen.route 46 34 )?.order; 47 35 const currentOrder = Object.values(SCREENS).find( 48 - (screen) => screen.route === this.props.location.pathname 36 + (screen) => screen.route === location.pathname 49 37 )?.order; 50 38 if (routeOrder && currentOrder && currentOrder > routeOrder) { 51 39 return styles.visitedNavColumn; 52 40 } 53 41 return styles.initialNavColumn; 54 - } 42 + }; 55 43 56 - updateWorkspaces = async () => { 57 - this.setState({ recentWorkspaces: await readWorkspaces() }); 44 + const updateWorkspaces = async () => { 45 + setRecentWorkspaces(await readWorkspaces()); 58 46 }; 59 47 60 - async handleLoadRecentWorkspace(dir: string) { 48 + const handleLoadRecentWorkspace = async (dir: string) => { 61 49 const recentWorkspaceState = await readAndParseState(dir); 62 50 if (recentWorkspaceState != null) { 63 - this.props.ExperimentActions.SetState(recentWorkspaceState); 51 + props.ExperimentActions.SetState(recentWorkspaceState); 64 52 } 53 + }; 54 + 55 + if ( 56 + location.pathname === SCREENS.HOME.route || 57 + location.pathname === SCREENS.BANK.route || 58 + location.pathname === '/' || 59 + props.isRunning 60 + ) { 61 + return null; 65 62 } 66 63 67 - render() { 68 - if ( 69 - this.props.location.pathname === SCREENS.HOME.route || 70 - this.props.location.pathname === SCREENS.BANK.route || 71 - this.props.location.pathname === '/' || 72 - this.props.isRunning 73 - ) { 74 - return null; 75 - } 76 - return ( 77 - <Grid className={styles.navContainer} verticalAlign="middle"> 78 - <Grid.Column className={styles.experimentTitleGridColumn}> 79 - <Segment basic className={styles.homeButton}> 80 - <NavLink to={SCREENS.HOME.route}> 81 - <Image 82 - centered 83 - className={styles.exitWorkspaceBtn} 84 - src={BrainwavesIcon} 85 - /> 86 - Home 87 - </NavLink> 88 - </Segment> 89 - </Grid.Column> 64 + return ( 65 + <div className={styles.navContainer}> 66 + <div className={styles.experimentTitleGridColumn}> 67 + <div className={styles.homeButton}> 68 + <NavLink to={SCREENS.HOME.route}> 69 + <img 70 + className={styles.exitWorkspaceBtn} 71 + src={BrainwavesIcon} 72 + alt="Home" 73 + /> 74 + Home 75 + </NavLink> 76 + </div> 77 + </div> 90 78 91 - <Grid.Column width={3} className={styles.experimentTitleGridColumn}> 92 - <Segment basic> 93 - <Dropdown 94 - text={this.props.title ? this.props.title : 'Untitled'} 95 - direction="right" 96 - onClick={() => { 97 - this.updateWorkspaces(); 98 - }} 99 - > 100 - <Dropdown.Menu> 101 - {this.state.recentWorkspaces.map((workspace) => ( 102 - <Dropdown.Item 103 - key={workspace} 104 - onClick={() => this.handleLoadRecentWorkspace(workspace)} 79 + <div className={styles.experimentTitleGridColumn}> 80 + <div className="relative"> 81 + <button 82 + onClick={updateWorkspaces} 83 + className={styles.workspaceDropdownTrigger} 84 + > 85 + {props.title ? props.title : 'Untitled'} 86 + </button> 87 + {recentWorkspaces.length > 0 && ( 88 + <ul className={styles.workspaceDropdownMenu}> 89 + {recentWorkspaces.map((workspace) => ( 90 + <li key={workspace}> 91 + <button 92 + onClick={() => handleLoadRecentWorkspace(workspace)} 93 + className={styles.workspaceDropdownItem} 105 94 > 106 - <p>{workspace}</p> 107 - </Dropdown.Item> 108 - ))} 109 - </Dropdown.Menu> 110 - </Dropdown> 111 - </Segment> 112 - </Grid.Column> 95 + {workspace} 96 + </button> 97 + </li> 98 + ))} 99 + </ul> 100 + )} 101 + </div> 102 + </div> 113 103 104 + <PrimaryNavSegment 105 + {...SCREENS.DESIGN} 106 + style={getStyleForScreen(SCREENS.DESIGN)} 107 + /> 108 + <PrimaryNavSegment 109 + {...SCREENS.COLLECT} 110 + style={getStyleForScreen(SCREENS.COLLECT)} 111 + /> 112 + {props.isEEGEnabled ? ( 114 113 <PrimaryNavSegment 115 - {...SCREENS.DESIGN} 116 - style={this.getStyleForScreen(SCREENS.DESIGN)} 114 + {...SCREENS.CLEAN} 115 + style={getStyleForScreen(SCREENS.CLEAN)} 117 116 /> 117 + ) : null} 118 + {props.isEEGEnabled ? ( 118 119 <PrimaryNavSegment 119 - {...SCREENS.COLLECT} 120 - style={this.getStyleForScreen(SCREENS.COLLECT)} 120 + {...SCREENS.ANALYZE} 121 + style={getStyleForScreen(SCREENS.ANALYZE)} 121 122 /> 122 - {this.props.isEEGEnabled ? ( 123 - <PrimaryNavSegment 124 - {...SCREENS.CLEAN} 125 - style={this.getStyleForScreen(SCREENS.CLEAN)} 126 - /> 127 - ) : null} 128 - {this.props.isEEGEnabled ? ( 129 - <PrimaryNavSegment 130 - {...SCREENS.ANALYZE} 131 - style={this.getStyleForScreen(SCREENS.ANALYZE)} 132 - /> 133 - ) : ( 134 - <PrimaryNavSegment 135 - {...SCREENS.ANALYZEBEHAVIOR} 136 - style={this.getStyleForScreen(SCREENS.ANALYZE)} 137 - /> 138 - )} 139 - </Grid> 140 - ); 141 - } 123 + ) : ( 124 + <PrimaryNavSegment 125 + {...SCREENS.ANALYZEBEHAVIOR} 126 + style={getStyleForScreen(SCREENS.ANALYZE)} 127 + /> 128 + )} 129 + </div> 130 + ); 142 131 }
+53
src/renderer/components/ui/button.tsx
··· 1 + import * as React from 'react'; 2 + import { Slot } from '@radix-ui/react-slot'; 3 + import { cva, type VariantProps } from 'class-variance-authority'; 4 + import { cn } from './utils'; 5 + 6 + const buttonVariants = cva( 7 + 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 8 + { 9 + variants: { 10 + variant: { 11 + default: 'bg-blue-600 text-white hover:bg-blue-700', 12 + secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300', 13 + outline: 14 + 'border border-gray-300 bg-transparent hover:bg-gray-100 text-gray-900', 15 + ghost: 'hover:bg-gray-100 text-gray-900', 16 + destructive: 'bg-red-600 text-white hover:bg-red-700', 17 + link: 'text-blue-600 underline-offset-4 hover:underline', 18 + }, 19 + size: { 20 + default: 'h-9 px-4 py-2', 21 + sm: 'h-8 px-3 text-xs', 22 + lg: 'h-10 px-6', 23 + icon: 'h-9 w-9', 24 + }, 25 + }, 26 + defaultVariants: { 27 + variant: 'default', 28 + size: 'default', 29 + }, 30 + } 31 + ); 32 + 33 + export interface ButtonProps 34 + extends React.ButtonHTMLAttributes<HTMLButtonElement>, 35 + VariantProps<typeof buttonVariants> { 36 + asChild?: boolean; 37 + } 38 + 39 + const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( 40 + ({ className, variant, size, asChild = false, ...props }, ref) => { 41 + const Comp = asChild ? Slot : 'button'; 42 + return ( 43 + <Comp 44 + className={cn(buttonVariants({ variant, size, className }))} 45 + ref={ref} 46 + {...props} 47 + /> 48 + ); 49 + } 50 + ); 51 + Button.displayName = 'Button'; 52 + 53 + export { Button, buttonVariants };
+32
src/renderer/components/ui/card.tsx
··· 1 + import * as React from 'react'; 2 + import { cn } from './utils'; 3 + 4 + const Card = React.forwardRef< 5 + HTMLDivElement, 6 + React.HTMLAttributes<HTMLDivElement> 7 + >(({ className, ...props }, ref) => ( 8 + <div 9 + ref={ref} 10 + className={cn('rounded-lg border border-gray-200 bg-white shadow-sm', className)} 11 + {...props} 12 + /> 13 + )); 14 + Card.displayName = 'Card'; 15 + 16 + const CardHeader = React.forwardRef< 17 + HTMLDivElement, 18 + React.HTMLAttributes<HTMLDivElement> 19 + >(({ className, ...props }, ref) => ( 20 + <div ref={ref} className={cn('flex flex-col space-y-1.5 p-4', className)} {...props} /> 21 + )); 22 + CardHeader.displayName = 'CardHeader'; 23 + 24 + const CardContent = React.forwardRef< 25 + HTMLDivElement, 26 + React.HTMLAttributes<HTMLDivElement> 27 + >(({ className, ...props }, ref) => ( 28 + <div ref={ref} className={cn('p-4 pt-0', className)} {...props} /> 29 + )); 30 + CardContent.displayName = 'CardContent'; 31 + 32 + export { Card, CardHeader, CardContent };
+88
src/renderer/components/ui/dialog.tsx
··· 1 + import * as React from 'react'; 2 + import * as DialogPrimitive from '@radix-ui/react-dialog'; 3 + import { cn } from './utils'; 4 + 5 + const Dialog = DialogPrimitive.Root; 6 + const DialogTrigger = DialogPrimitive.Trigger; 7 + const DialogPortal = DialogPrimitive.Portal; 8 + const DialogClose = DialogPrimitive.Close; 9 + 10 + const DialogOverlay = React.forwardRef< 11 + React.ElementRef<typeof DialogPrimitive.Overlay>, 12 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> 13 + >(({ className, ...props }, ref) => ( 14 + <DialogPrimitive.Overlay 15 + ref={ref} 16 + className={cn( 17 + 'fixed inset-0 z-50 bg-black/60 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', 18 + className 19 + )} 20 + {...props} 21 + /> 22 + )); 23 + DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 24 + 25 + const DialogContent = React.forwardRef< 26 + React.ElementRef<typeof DialogPrimitive.Content>, 27 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> 28 + >(({ className, children, ...props }, ref) => ( 29 + <DialogPortal> 30 + <DialogOverlay /> 31 + <DialogPrimitive.Content 32 + ref={ref} 33 + className={cn( 34 + 'fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg bg-white p-6 shadow-lg', 35 + className 36 + )} 37 + {...props} 38 + > 39 + {children} 40 + <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100"> 41 + <span className="sr-only">Close</span> 42 + 43 + </DialogPrimitive.Close> 44 + </DialogPrimitive.Content> 45 + </DialogPortal> 46 + )); 47 + DialogContent.displayName = DialogPrimitive.Content.displayName; 48 + 49 + const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( 50 + <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} /> 51 + ); 52 + DialogHeader.displayName = 'DialogHeader'; 53 + 54 + const DialogTitle = React.forwardRef< 55 + React.ElementRef<typeof DialogPrimitive.Title>, 56 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> 57 + >(({ className, ...props }, ref) => ( 58 + <DialogPrimitive.Title 59 + ref={ref} 60 + className={cn('text-lg font-semibold leading-none tracking-tight', className)} 61 + {...props} 62 + /> 63 + )); 64 + DialogTitle.displayName = DialogPrimitive.Title.displayName; 65 + 66 + const DialogDescription = React.forwardRef< 67 + React.ElementRef<typeof DialogPrimitive.Description>, 68 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> 69 + >(({ className, ...props }, ref) => ( 70 + <DialogPrimitive.Description 71 + ref={ref} 72 + className={cn('text-sm text-gray-500', className)} 73 + {...props} 74 + /> 75 + )); 76 + DialogDescription.displayName = DialogPrimitive.Description.displayName; 77 + 78 + export { 79 + Dialog, 80 + DialogPortal, 81 + DialogOverlay, 82 + DialogClose, 83 + DialogTrigger, 84 + DialogContent, 85 + DialogHeader, 86 + DialogTitle, 87 + DialogDescription, 88 + };
+80
src/renderer/components/ui/dropdown-menu.tsx
··· 1 + import * as React from 'react'; 2 + import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; 3 + import { cn } from './utils'; 4 + 5 + const DropdownMenu = DropdownMenuPrimitive.Root; 6 + const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 7 + const DropdownMenuGroup = DropdownMenuPrimitive.Group; 8 + const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 9 + const DropdownMenuSub = DropdownMenuPrimitive.Sub; 10 + const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 11 + 12 + const DropdownMenuContent = React.forwardRef< 13 + React.ElementRef<typeof DropdownMenuPrimitive.Content>, 14 + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> 15 + >(({ className, sideOffset = 4, ...props }, ref) => ( 16 + <DropdownMenuPrimitive.Portal> 17 + <DropdownMenuPrimitive.Content 18 + ref={ref} 19 + sideOffset={sideOffset} 20 + className={cn( 21 + 'z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 shadow-md', 22 + className 23 + )} 24 + {...props} 25 + /> 26 + </DropdownMenuPrimitive.Portal> 27 + )); 28 + DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 29 + 30 + const DropdownMenuItem = React.forwardRef< 31 + React.ElementRef<typeof DropdownMenuPrimitive.Item>, 32 + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> 33 + >(({ className, ...props }, ref) => ( 34 + <DropdownMenuPrimitive.Item 35 + ref={ref} 36 + className={cn( 37 + 'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-gray-100 focus:bg-gray-100', 38 + className 39 + )} 40 + {...props} 41 + /> 42 + )); 43 + DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 44 + 45 + const DropdownMenuLabel = React.forwardRef< 46 + React.ElementRef<typeof DropdownMenuPrimitive.Label>, 47 + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> 48 + >(({ className, ...props }, ref) => ( 49 + <DropdownMenuPrimitive.Label 50 + ref={ref} 51 + className={cn('px-2 py-1.5 text-sm font-semibold', className)} 52 + {...props} 53 + /> 54 + )); 55 + DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 56 + 57 + const DropdownMenuSeparator = React.forwardRef< 58 + React.ElementRef<typeof DropdownMenuPrimitive.Separator>, 59 + React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> 60 + >(({ className, ...props }, ref) => ( 61 + <DropdownMenuPrimitive.Separator 62 + ref={ref} 63 + className={cn('-mx-1 my-1 h-px bg-gray-100', className)} 64 + {...props} 65 + /> 66 + )); 67 + DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 68 + 69 + export { 70 + DropdownMenu, 71 + DropdownMenuTrigger, 72 + DropdownMenuContent, 73 + DropdownMenuGroup, 74 + DropdownMenuPortal, 75 + DropdownMenuSub, 76 + DropdownMenuRadioGroup, 77 + DropdownMenuItem, 78 + DropdownMenuLabel, 79 + DropdownMenuSeparator, 80 + };
+73
src/renderer/components/ui/table.tsx
··· 1 + import * as React from 'react'; 2 + import { cn } from './utils'; 3 + 4 + const Table = React.forwardRef< 5 + HTMLTableElement, 6 + React.HTMLAttributes<HTMLTableElement> 7 + >(({ className, ...props }, ref) => ( 8 + <div className="relative w-full overflow-auto"> 9 + <table 10 + ref={ref} 11 + className={cn('w-full caption-bottom text-sm', className)} 12 + {...props} 13 + /> 14 + </div> 15 + )); 16 + Table.displayName = 'Table'; 17 + 18 + const TableHeader = React.forwardRef< 19 + HTMLTableSectionElement, 20 + React.HTMLAttributes<HTMLTableSectionElement> 21 + >(({ className, ...props }, ref) => ( 22 + <thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} /> 23 + )); 24 + TableHeader.displayName = 'TableHeader'; 25 + 26 + const TableBody = React.forwardRef< 27 + HTMLTableSectionElement, 28 + React.HTMLAttributes<HTMLTableSectionElement> 29 + >(({ className, ...props }, ref) => ( 30 + <tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} /> 31 + )); 32 + TableBody.displayName = 'TableBody'; 33 + 34 + const TableRow = React.forwardRef< 35 + HTMLTableRowElement, 36 + React.HTMLAttributes<HTMLTableRowElement> 37 + >(({ className, ...props }, ref) => ( 38 + <tr 39 + ref={ref} 40 + className={cn('border-b transition-colors hover:bg-gray-50', className)} 41 + {...props} 42 + /> 43 + )); 44 + TableRow.displayName = 'TableRow'; 45 + 46 + const TableHead = React.forwardRef< 47 + HTMLTableCellElement, 48 + React.ThHTMLAttributes<HTMLTableCellElement> 49 + >(({ className, ...props }, ref) => ( 50 + <th 51 + ref={ref} 52 + className={cn( 53 + 'h-10 px-4 text-left align-middle font-medium text-gray-500', 54 + className 55 + )} 56 + {...props} 57 + /> 58 + )); 59 + TableHead.displayName = 'TableHead'; 60 + 61 + const TableCell = React.forwardRef< 62 + HTMLTableCellElement, 63 + React.TdHTMLAttributes<HTMLTableCellElement> 64 + >(({ className, ...props }, ref) => ( 65 + <td 66 + ref={ref} 67 + className={cn('p-4 align-middle', className)} 68 + {...props} 69 + /> 70 + )); 71 + TableCell.displayName = 'TableCell'; 72 + 73 + export { Table, TableHeader, TableBody, TableRow, TableHead, TableCell };
+6
src/renderer/components/ui/utils.ts
··· 1 + import { clsx, type ClassValue } from 'clsx'; 2 + import { twMerge } from 'tailwind-merge'; 3 + 4 + export function cn(...inputs: ClassValue[]) { 5 + return twMerge(clsx(inputs)); 6 + }
+14
src/renderer/containers/App.tsx
··· 1 1 import * as React from 'react'; 2 + import { useEffect } from 'react'; 3 + import { useLocation } from 'react-router-dom'; 4 + import { useDispatch } from 'react-redux'; 2 5 import { ToastContainer } from 'react-toastify'; 3 6 import TopNav from './TopNavBarContainer'; 7 + import { RouterActions } from '../actions/routerActions'; 8 + 9 + function NavigationTracker() { 10 + const location = useLocation(); 11 + const dispatch = useDispatch(); 12 + useEffect(() => { 13 + dispatch(RouterActions.RouteChanged(location.pathname)); 14 + }, [location.pathname, dispatch]); 15 + return null; 16 + } 4 17 5 18 type Props = { 6 19 children: React.ReactNode; ··· 9 22 export function App(props: Props) { 10 23 return ( 11 24 <div> 25 + <NavigationTracker /> 12 26 <TopNav /> 13 27 {props.children} 14 28 <ToastContainer />
+10 -1
src/renderer/containers/ExperimentDesignContainer.ts
··· 1 + import React from 'react'; 1 2 import { bindActionCreators } from 'redux'; 2 3 import { connect } from 'react-redux'; 4 + import { useNavigate } from 'react-router-dom'; 3 5 import Design from '../components/DesignComponent'; 4 6 import { ExperimentActions } from '../actions'; 5 7 ··· 15 17 }; 16 18 } 17 19 18 - export default connect(mapStateToProps, mapDispatchToProps)(Design); 20 + const ConnectedDesign = connect(mapStateToProps, mapDispatchToProps)(Design); 21 + 22 + function ExperimentDesignContainer(props: any) { 23 + const navigate = useNavigate(); 24 + return React.createElement(ConnectedDesign, { ...props, navigate }); 25 + } 26 + 27 + export default ExperimentDesignContainer;
+10 -1
src/renderer/containers/HomeContainer.ts
··· 1 + import React from 'react'; 1 2 import { connect } from 'react-redux'; 2 3 import { bindActionCreators } from 'redux'; 4 + import { useNavigate } from 'react-router-dom'; 3 5 import Home from '../components/HomeComponent'; 4 6 import { DeviceActions, ExperimentActions, PyodideActions } from '../actions'; 5 7 ··· 18 20 }; 19 21 } 20 22 21 - export default connect(mapStateToProps, mapDispatchToProps)(Home); 23 + const ConnectedHome = connect(mapStateToProps, mapDispatchToProps)(Home); 24 + 25 + function HomeContainer(props: any) { 26 + const navigate = useNavigate(); 27 + return React.createElement(ConnectedHome, { ...props, navigate }); 28 + } 29 + 30 + export default HomeContainer;
+4 -11
src/renderer/containers/Root.tsx
··· 1 1 import React from 'react'; 2 2 import { Provider } from 'react-redux'; 3 - import { ConnectedRouter } from 'connected-react-router'; 4 - import { History } from 'history'; 5 - 6 - const Router = ConnectedRouter as React.ComponentType<{ 7 - history: History; 8 - children?: React.ReactNode; 9 - }>; 3 + import { HashRouter } from 'react-router-dom'; 10 4 import { Store } from '../reducers/types'; 11 5 import Routes from '../routes'; 12 6 import { RootState } from '../reducers'; 13 7 14 8 interface Props { 15 9 store: Store<RootState>; 16 - history: History; 17 10 } 18 11 19 - const Root = ({ store, history }: Props) => ( 12 + const Root = ({ store }: Props) => ( 20 13 <Provider store={store}> 21 - <Router history={history}> 14 + <HashRouter> 22 15 <Routes /> 23 - </Router> 16 + </HashRouter> 24 17 </Provider> 25 18 ); 26 19
-1
src/renderer/containers/TopNavBarContainer.ts
··· 6 6 function mapStateToProps(state) { 7 7 return { 8 8 title: state.experiment.title, 9 - location: state.router.location, 10 9 isRunning: state.experiment.isRunning, 11 10 type: state.experiment.type, 12 11 isEEGEnabled: state.experiment.isEEGEnabled,
+7 -9
src/renderer/epics/experimentEpics.ts
··· 3 3 import { 4 4 map, 5 5 mergeMap, 6 - pluck, 7 6 filter, 8 7 takeUntil, 9 8 throttleTime, ··· 11 10 } from 'rxjs/operators'; 12 11 import { isActionOf } from '../utils/redux'; 13 12 import { ExperimentActions, ExperimentActionType } from '../actions'; 13 + import { RouterActions } from '../actions/routerActions'; 14 14 import { 15 15 DEVICES, 16 16 MUSE_CHANNELS, ··· 165 165 166 166 const autoSaveEpic: Epic<any, ExperimentActionType, RootState> = (action$) => 167 167 action$.pipe( 168 - ofType('@@router/LOCATION_CHANGE'), 169 - pluck('payload', 'pathname'), 168 + filter(isActionOf(RouterActions.RouteChanged)), 169 + map((action) => action.payload as string), 170 170 filter((pathname) => pathname !== '/' && pathname !== '/home'), 171 171 map(ExperimentActions.SaveWorkspace) 172 172 ); ··· 196 196 ); 197 197 198 198 const navigationCleanupEpic: Epic<any, ExperimentActionType, RootState> = ( 199 - action$, 200 - state$ 199 + action$ 201 200 ) => 202 201 action$.pipe( 203 - ofType('@@router/LOCATION_CHANGE'), 204 - tap((pathname) => console.log('navigation', pathname)), 205 - pluck('payload', 'location', 'pathname'), 206 - tap((pathname) => console.log('navigation', pathname)), 202 + filter(isActionOf(RouterActions.RouteChanged)), 203 + tap((action) => console.log('navigation', action.payload)), 204 + map((action) => action.payload as string), 207 205 filter((pathname) => pathname === '/' || pathname === '/home'), 208 206 map(ExperimentActions.ExperimentCleanup) 209 207 );
+2 -2
src/renderer/index.tsx
··· 1 1 import React from 'react'; 2 2 import { createRoot } from 'react-dom/client'; 3 3 import Root from './containers/Root'; 4 - import { configuredStore, history } from './store'; 4 + import { configuredStore } from './store'; 5 5 import './app.global.css'; 6 6 7 7 const store = configuredStore(); 8 8 9 9 createRoot(document.getElementById('root') as HTMLElement).render( 10 - <Root store={store} history={history} /> 10 + <Root store={store} /> 11 11 );
+5 -11
src/renderer/reducers/index.ts
··· 1 1 import { combineReducers } from 'redux'; 2 - import { connectRouter } from 'connected-react-router'; 3 - import { History } from 'history'; 4 2 import pyodide, { PyodideStateType } from './pyodideReducer'; 5 3 import device, { DeviceStateType } from './deviceReducer'; 6 4 import experiment, { ExperimentStateType } from './experimentReducer'; ··· 9 7 pyodide: PyodideStateType; 10 8 device: DeviceStateType; 11 9 experiment: ExperimentStateType; 12 - router: any; 13 10 } 14 11 15 - export default function createRootReducer(history: History) { 16 - return combineReducers({ 17 - router: connectRouter(history), 18 - pyodide, 19 - device, 20 - experiment, 21 - }); 22 - } 12 + export default combineReducers({ 13 + pyodide, 14 + device, 15 + experiment, 16 + });
+12 -28
src/renderer/routes.tsx
··· 1 1 import React from 'react'; 2 - import { Switch, Route } from 'react-router'; 2 + import { Routes, Route } from 'react-router-dom'; 3 3 import { App } from './containers/App'; 4 4 import HomeContainer from './containers/HomeContainer'; 5 5 import ExperimentDesignContainer from './containers/ExperimentDesignContainer'; ··· 8 8 import AnalyzeContainer from './containers/AnalyzeContainer'; 9 9 import { SCREENS } from './constants/constants'; 10 10 11 - const renderMergedProps = (component, ...rest) => { 12 - const finalProps = Object.assign({}, ...rest); 13 - return React.createElement(component, finalProps); 14 - }; 15 - 16 - // Wraps the normal Route class with the ability to inject a particular prop 17 - // Allows us to manipulate props directly thruogh route transitions 18 - const PropsRoute = ({ component, ...rest }) => ( 19 - <Route 20 - {...rest} 21 - render={(routeProps) => renderMergedProps(component, routeProps, rest)} 22 - /> 23 - ); 24 - 25 - export default function Routes() { 11 + export default function AppRoutes() { 26 12 return ( 27 13 <App> 28 - <Switch> 29 - <Route path={SCREENS.ANALYZE.route} component={AnalyzeContainer} /> 30 - <Route path={SCREENS.CLEAN.route} component={CleanContainer} /> 31 - <Route path={SCREENS.COLLECT.route} component={CollectContainer} /> 14 + <Routes> 15 + <Route path={SCREENS.ANALYZE.route} element={<AnalyzeContainer />} /> 16 + <Route path={SCREENS.CLEAN.route} element={<CleanContainer />} /> 17 + <Route path={SCREENS.COLLECT.route} element={<CollectContainer />} /> 32 18 <Route 33 19 path={SCREENS.DESIGN.route} 34 - component={ExperimentDesignContainer} 20 + element={<ExperimentDesignContainer />} 35 21 /> 36 - <PropsRoute 22 + <Route 37 23 path="/home" 38 - component={HomeContainer} 39 - activeStep="EXPERIMENT BANK" 24 + element={<HomeContainer activeStep="EXPERIMENT BANK" />} 40 25 /> 41 - <PropsRoute 26 + <Route 42 27 path="/" 43 - component={HomeContainer} 44 - activeStep="MY EXPERIMENTS" 28 + element={<HomeContainer activeStep="MY EXPERIMENTS" />} 45 29 /> 46 - </Switch> 30 + </Routes> 47 31 </App> 48 32 ); 49 33 }
+1 -10
src/renderer/store.ts
··· 1 1 import { configureStore, Action } from '@reduxjs/toolkit'; 2 - import { createHashHistory } from 'history'; 3 - import { routerMiddleware } from 'connected-react-router'; 4 2 import { createEpicMiddleware } from 'redux-observable'; 5 3 import { createLogger } from 'redux-logger'; 6 4 import { ThunkAction } from 'redux-thunk'; 7 - import createRootReducer from './reducers'; 5 + import rootReducer from './reducers'; 8 6 import rootEpic from './epics'; 9 7 10 - export const history = createHashHistory(); 11 - const rootReducer = createRootReducer(history); 12 - 13 8 export type RootState = ReturnType<typeof rootReducer>; 14 9 15 - const router = routerMiddleware(history); 16 - 17 - // Redux Observable (Epic) Middleware 18 10 const epicMiddleware = createEpicMiddleware(); 19 11 20 12 const excludeLoggerEnvs = ['test', 'production']; ··· 27 19 reducer: rootReducer, 28 20 middleware: (getDefaultMiddleware) => { 29 21 const base = getDefaultMiddleware({ serializableCheck: false }).concat( 30 - router, 31 22 epicMiddleware 32 23 ); 33 24 if (shouldIncludeLogger) {
+22 -9
src/renderer/utils/pyodide/utils.py
··· 155 155 conditions = OrderedDict(conditions) 156 156 157 157 if palette is None: 158 - palette = sns.color_palette("hls", len(conditions) + 1) 158 + palette = [ 159 + (0.86, 0.37, 0.34), 160 + (0.34, 0.86, 0.37), 161 + (0.37, 0.34, 0.86), 162 + (0.86, 0.72, 0.34), 163 + ] 159 164 160 165 X = epochs.get_data() 161 166 times = epochs.times ··· 163 168 fig, ax = plt.subplots() 164 169 165 170 for cond, color in zip(conditions.values(), palette): 166 - sns.tsplot(X[y.isin(cond), ch_ind], time=times, color=color, 167 - n_boot=n_boot, ci=ci) 171 + cond_data = X[y.isin(cond), ch_ind] 172 + mean = np.nanmean(cond_data, axis=0) 173 + n_samples = cond_data.shape[0] 174 + boot_means = np.array([ 175 + np.nanmean( 176 + cond_data[np.random.randint(0, n_samples, n_samples)], axis=0 177 + ) 178 + for _ in range(n_boot) 179 + ]) 180 + alpha = (100 - ci) / 2 181 + low = np.percentile(boot_means, alpha, axis=0) 182 + high = np.percentile(boot_means, 100 - alpha, axis=0) 183 + ax.plot(times, mean, color=color) 184 + ax.fill_between(times, low, high, color=color, alpha=0.3) 168 185 169 186 if diff_waveform: 170 187 diff = (np.nanmean(X[y == diff_waveform[1], ch_ind], axis=0) - ··· 176 193 177 194 ax.set_xlabel('Time (s)') 178 195 ax.set_ylabel('Amplitude (uV)') 179 - ax.set_xlabel('Time (s)') 180 - ax.set_ylabel('Amplitude (uV)') 181 - 182 - # Round y axis tick labels to 2 decimal places 183 - # ax.yaxis.set_major_formatter(FormatStrFormatter('%.2f')) 184 196 185 197 if diff_waveform: 186 198 legend = (['{} - {}'.format(diff_waveform[1], diff_waveform[0])] + ··· 188 200 else: 189 201 legend = conditions.keys() 190 202 ax.legend(legend) 191 - sns.despine() 203 + ax.spines['top'].set_visible(False) 204 + ax.spines['right'].set_visible(False) 192 205 plt.tight_layout() 193 206 194 207 if title:
+1
src/test-setup.ts
··· 1 + import '@testing-library/jest-dom';
+15
tailwind.config.js
··· 1 + /** @type {import('tailwindcss').Config} */ 2 + module.exports = { 3 + darkMode: ['class'], 4 + content: ['./src/renderer/**/*.{ts,tsx,js,jsx}'], 5 + theme: { 6 + extend: { 7 + borderRadius: { 8 + lg: 'var(--radius)', 9 + md: 'calc(var(--radius) - 2px)', 10 + sm: 'calc(var(--radius) - 4px)', 11 + }, 12 + }, 13 + }, 14 + plugins: [], 15 + };
+29
vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + import react from '@vitejs/plugin-react'; 3 + import path from 'path'; 4 + 5 + export default defineConfig({ 6 + plugins: [ 7 + react({ 8 + babel: { 9 + plugins: [ 10 + ['@babel/plugin-proposal-decorators', { legacy: true }], 11 + ['@babel/plugin-proposal-class-properties', { loose: true }], 12 + ], 13 + }, 14 + }), 15 + ], 16 + test: { 17 + environment: 'jsdom', 18 + globals: true, 19 + setupFiles: ['./src/test-setup.ts'], 20 + css: true, 21 + }, 22 + resolve: { 23 + alias: { 24 + '@renderer': path.resolve(__dirname, 'src/renderer'), 25 + path: 'pathe', 26 + events: 'events', 27 + }, 28 + }, 29 + });