An easy-to-use platform for EEG experimentation in the classroom
at main 353 lines 17 kB view raw view rendered
1# BrainWaves Modernization — Implementation Progress 2 3Tracking 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 12Key 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:** 441. 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 502. Create `vitest.config.ts` (project root) 513. Create `src/test-setup.ts` (imports `@testing-library/jest-dom`) 524. Create `src/renderer/App.test.tsx` (basic sanity render test) 535. 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:** 631. Edit `.github/workflows/test.yml`: add `npm test` step before or after existing steps 642. 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 113import React from 'react'; 114import { useNavigate } from 'react-router-dom'; 115 116function 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 128function 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 152Changes: 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 167The `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:** 1731. Replace `sns.color_palette(...)` with a hardcoded palette (consistent with `plot_topo`) 1742. Replace `sns.tsplot(...)` with manual bootstrap CI using numpy + `ax.plot()` + `ax.fill_between()` 1753. 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 179cond_data = X[y.isin(cond), ch_ind] 180mean = np.nanmean(cond_data, axis=0) 181n_samples = cond_data.shape[0] 182boot_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]) 186alpha = (100 - ci) / 2 187low = np.percentile(boot_means, alpha, axis=0) 188high = np.percentile(boot_means, 100 - alpha, axis=0) 189ax.plot(times, mean, color=color) 190ax.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)