···11import React from 'react';
22import { Provider } from 'react-redux';
33import { ConnectedRouter } from 'connected-react-router';
44-import { hot } from 'react-hot-loader/root';
54import { History } from 'history';
65import { Store } from '../reducers/types';
76import Routes from '../routes';
···2019 </Provider>
2120);
22212323-export default hot(Root);
2222+export default Root;
···33 * pyodide to be used in a web worker within this
44 */
5566-// self.languagePluginUrl = './src';
77-importScripts('./src/pyodide/pyodide.js');
66+// pyodide is served as a static asset at /pyodide/ (via Vite publicDir).
77+// An absolute path is required so importScripts resolves correctly regardless
88+// of where the worker script itself is served from.
99+importScripts('/pyodide/pyodide.js');
810911async function loadPyodideAndPackages() {
1010- self.pyodide = await loadPyodide({ indexURL: './src/pyodide/' });
1212+ self.pyodide = await loadPyodide({ indexURL: '/pyodide/' });
1113 await self.pyodide.loadPackage(['matplotlib', 'mne', 'pandas']);
1214}
1315let pyodideReadyPromise = loadPyodideAndPackages();
···11+/**
22+ * Post-build script for the EEG viewer assets.
33+ *
44+ * electron-vite's preload and renderer builds only support a single entry
55+ * point each. This script uses esbuild directly to compile the viewer
66+ * preload and the viewer renderer page after the main electron-vite build.
77+ *
88+ * Run automatically via the "postbuild" npm lifecycle hook.
99+ */
1010+import { build } from 'esbuild';
1111+import { readFileSync, writeFileSync, mkdirSync } from 'fs';
1212+import { resolve, join } from 'path';
1313+import { fileURLToPath } from 'url';
1414+1515+const __dirname = fileURLToPath(new URL('.', import.meta.url));
1616+const root = resolve(__dirname, '../..');
1717+1818+// ----------------------------------------------------------------
1919+// 1. Viewer preload: src/preload/viewer.ts → out/preload/viewer.js
2020+// ----------------------------------------------------------------
2121+await build({
2222+ entryPoints: [resolve(root, 'src/preload/viewer.ts')],
2323+ bundle: true,
2424+ platform: 'node',
2525+ external: ['electron'],
2626+ outfile: resolve(root, 'out/preload/viewer.js'),
2727+ format: 'cjs',
2828+});
2929+3030+// ----------------------------------------------------------------
3131+// 2. Viewer renderer: src/renderer/viewer.ts → out/renderer/viewer.js
3232+// ----------------------------------------------------------------
3333+const rendererOut = resolve(root, 'out/renderer');
3434+mkdirSync(rendererOut, { recursive: true });
3535+3636+await build({
3737+ entryPoints: [resolve(root, 'src/renderer/viewer.ts')],
3838+ bundle: true,
3939+ platform: 'browser',
4040+ outfile: join(rendererOut, 'viewer.js'),
4141+ format: 'esm',
4242+});
4343+4444+// ----------------------------------------------------------------
4545+// 3. viewer.html: copy src → out, update script src .ts → .js
4646+// ----------------------------------------------------------------
4747+let html = readFileSync(resolve(root, 'src/renderer/viewer.html'), 'utf-8');
4848+html = html.replace('src="./viewer.ts"', 'src="./viewer.js"');
4949+writeFileSync(join(rendererOut, 'viewer.html'), html);
5050+5151+console.log('Viewer builds complete: out/preload/viewer.js, out/renderer/viewer.{js,html}');
-5
internals/scripts/CheckYarn.js
···11-if (!/yarn\.js$/.test(process.env.npm_execpath || '')) {
22- console.warn(
33- "\u001b[33mYou don't seem to be using yarn. This could produce unexpected results.\u001b[39m"
44- );
55-}
···11+import React from 'react';
22+import { render } from 'react-dom';
33+import Root from './containers/Root';
44+import { configuredStore, history } from './store';
55+import './app.global.css';
66+77+const store = configuredStore();
88+99+render(
1010+ <Root store={store} history={history} />,
1111+ document.getElementById('root')
1212+);
+9
src/renderer/utils/filesystem/dialog.ts
···11+/**
22+ * Dialog helpers — proxied to the main process via window.electronAPI.
33+ * The actual electron.dialog calls live in src/main/index.ts IPC handlers.
44+ */
55+66+export const showOpenDialog = (
77+ options: Record<string, unknown>
88+): Promise<{ canceled: boolean; filePaths: string[] }> =>
99+ window.electronAPI.showOpenDialog(options);
···11+/**
22+ * Functions for writing EEG data to disk.
33+ * The actual write stream lives in the main process; the renderer
44+ * communicates via the IPC bridge in window.electronAPI.
55+ */
66+77+import { EEGData } from '../../constants/interfaces';
88+99+// Creates an EEG write stream in the main process and returns a stream ID
1010+export const createEEGWriteStream = (
1111+ title: string,
1212+ subject: string,
1313+ group: string,
1414+ session: number
1515+): Promise<string> =>
1616+ window.electronAPI.createEEGWriteStream(title, subject, group, session);
1717+1818+// Writes the CSV header row to the stream identified by streamId
1919+export const writeHeader = (streamId: string, channels: string[]): void =>
2020+ window.electronAPI.writeEEGHeader(streamId, channels);
2121+2222+// Writes a single EEG data row (fire-and-forget for performance)
2323+export const writeEEGData = (streamId: string, eegData: EEGData): void =>
2424+ window.electronAPI.writeEEGData(streamId, eegData);
2525+2626+// Closes the write stream
2727+export const closeEEGStream = (streamId: string): Promise<void> =>
2828+ window.electronAPI.closeEEGStream(streamId);
+47
src/renderer/viewer.ts
···11+/**
22+ * EEG Viewer renderer — uses the viewerAPI exposed by src/preload/viewer.ts
33+ * to receive graph data from the main process via IPC.
44+ */
55+import EEGGraph from './components/d3Classes/EEGViewer';
66+77+// eslint-disable-next-line @typescript-eslint/no-explicit-any
88+let graph: any = {};
99+1010+declare global {
1111+ interface Window {
1212+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1313+ viewerAPI: any;
1414+ }
1515+}
1616+1717+window.viewerAPI.onInitGraph((message: unknown) => {
1818+ graph = new EEGGraph(document.getElementById('graph'), message);
1919+});
2020+2121+window.viewerAPI.onNewData((message: unknown) => {
2222+ graph.updateData(message);
2323+});
2424+2525+window.viewerAPI.onZoomIn(() => {
2626+ graph.zoomOut();
2727+});
2828+2929+window.viewerAPI.onZoomOut(() => {
3030+ graph.zoomIn();
3131+});
3232+3333+window.viewerAPI.onUpdateChannels((message: unknown) => {
3434+ graph.updateChannels(message);
3535+});
3636+3737+window.viewerAPI.onUpdateDomain((message: unknown) => {
3838+ graph.updateDomain(message);
3939+});
4040+4141+window.viewerAPI.onUpdateDownsampling((message: unknown) => {
4242+ graph.updateDownsampling(message);
4343+});
4444+4545+window.viewerAPI.onAutoScale(() => {
4646+ graph.autoScale();
4747+});