An easy-to-use platform for EEG experimentation in the classroom
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)