Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .llms/learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,28 @@ The CDN version is derived from `node_modules/pyodide/package.json` — **not**

**Plot result routing pattern** — `worker.postMessage()` is fire-and-forget (returns `undefined`). Plot epics should use `tap()` to fire the worker message and `mergeMap(() => EMPTY)` to emit nothing. Results come back asynchronously on the worker `message` event. Add a `plotKey` field to each worker message; the worker echoes it back; `pyodideMessageEpic` switches on `plotKey` to dispatch `SetTopoPlot`/`SetPSDPlot`/`SetERPPlot` with a `{ 'image/png': base64string }` MIME bundle. `PyodidePlotWidget` renders this via `@nteract/transforms`.

## Lab.js 23.x API: `hooks` replaces `messageHandlers`

Lab.js 23.x renamed the event-handler registration option from `messageHandlers` to `hooks`. The experiment JSON files (e.g. `experiment.ts` in each experiment folder) must use `hooks:` not `messageHandlers:` for before:prepare/run/end callbacks. If `messageHandlers` is used, the handlers are silently ignored — loops won't initialize their `templateParameters`, causing "Empty or invalid parameter set for loop, no content generated".

Affected files: `src/renderer/experiments/*/experiment.ts` (and `custom/experiment.js`). The format is identical — just the key name changed:
```js
// Old (lab.js < 22): messageHandlers: { 'before:prepare': initLoopWithStimuli }
// New (lab.js 23.x): hooks: { 'before:prepare': initLoopWithStimuli }
```

## Lab.js stimulus `filepath` must be a browser URL, not a filesystem path

`balanceStimuliByCondition` (in `src/renderer/utils/labjs/functions.ts`) generates a `filepath` field used by lab.js HTML templates (`<img src="${ this.parameters.filepath }">`). This must be a browser-loadable URL, not a raw filesystem path like `/Users/.../Face1.jpg`.

In dev: use `/@fs<absPath>` (Vite's `/@fs/` serving). In prod: use `file://<absPath>`. The helper `absPathToUrl` in `functions.ts` handles this. Same pattern as `ExperimentWindow.tsx` for `options.media.images`.

## Lab.js: `this.id` vs `this.options.id` for loop-cloned components

`prepareNested` (in `flow/util/nested.js`) sets IDs on cloned loop components via `c.id = [parent.id, i].join('_')` — this sets the **component's own property**, NOT `c.options.id`. The options proxy reads through `rawOptions`, which never has an `id` for template-cloned components (the JSON template has no explicit `id` field).

Any hook function (e.g. `initResponseHandlers` in `src/renderer/utils/labjs/functions.ts`) that needs the component ID must use `this.id`, not `this.options.id`. Using `this.options.id` will always be `undefined` for loop-cloned components, causing silent early returns and broken behavior (e.g. keydown handlers never installed).

## Pre-existing TypeScript errors (do not treat as regressions)

- `src/renderer/epics/experimentEpics.ts` (lines 170, 205) — RxJS operator type mismatch
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@
"productName": "BrainWaves",
"appId": "com.electron.brainwaves",
"asar": true,
"protocols": [
{
"name": "BrainWaves",
"schemes": ["brainwaves"]
}
],
"files": [
"out/**/*",
"node_modules/**/*",
Expand Down
47 changes: 46 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* All Node.js / filesystem / shell operations the renderer needs
* are handled here via ipcMain handlers and exposed via the preload.
*/
import { app, BrowserWindow, ipcMain, dialog, shell, session, protocol, net } from 'electron';

Check warning on line 8 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Replace `·app,·BrowserWindow,·ipcMain,·dialog,·shell,·session,·protocol,·net·` with `⏎··app,⏎··BrowserWindow,⏎··ipcMain,⏎··dialog,⏎··shell,⏎··session,⏎··protocol,⏎··net,⏎`

Check warning on line 8 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Replace `·app,·BrowserWindow,·ipcMain,·dialog,·shell,·session,·protocol,·net·` with `⏎··app,⏎··BrowserWindow,⏎··ipcMain,⏎··dialog,⏎··shell,⏎··session,⏎··protocol,⏎··net,⏎`

Check warning on line 8 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Replace `·app,·BrowserWindow,·ipcMain,·dialog,·shell,·session,·protocol,·net·` with `⏎··app,⏎··BrowserWindow,⏎··ipcMain,⏎··dialog,⏎··shell,⏎··session,⏎··protocol,⏎··net,⏎`
import path from 'path';
import fs from 'fs';
import { pathToFileURL } from 'url';
Expand All @@ -24,14 +24,53 @@
'true'
);

// Enforce a single app instance — required so the second-instance event fires
// on Windows/Linux when the OS re-launches the app to deliver an OAuth callback.
if (!app.requestSingleInstanceLock()) {
app.quit();
}

// Register brainwaves:// as the OS-level deep-link scheme.
// Redirect URI for OAuth: brainwaves://oauth/callback
app.setAsDefaultProtocolClient('brainwaves');

// Buffer an OAuth callback URL that arrives before the window is ready
// (e.g. the app is cold-launched by the OS to handle the redirect).
let pendingOAuthUrl: string | null = null;

const handleOAuthCallback = (url: string) => {
if (mainWindow) {
mainWindow.webContents.send('oauth:callback', url);
} else {
pendingOAuthUrl = url;
}
};

// macOS: OS fires open-url when a brainwaves:// link is opened.
app.on('open-url', (event, url) => {
event.preventDefault();
handleOAuthCallback(url);
});

// Windows / Linux: OS relaunches the app with the URL as a CLI argument.
// requestSingleInstanceLock() above ensures the existing instance gets this event.
app.on('second-instance', (_event, argv) => {
const url = argv.find((arg) => arg.startsWith('brainwaves://'));
if (url) handleOAuthCallback(url);
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});

// Register pyodide:// as a privileged custom scheme so web workers can
// fetch() package .whl files from it. Must be called before app.whenReady().
protocol.registerSchemesAsPrivileged([
{
scheme: 'pyodide',
privileges: {
standard: true, // treat like http for URL parsing / resolution

Check warning on line 72 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Delete `···`

Check warning on line 72 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Delete `···`

Check warning on line 72 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Delete `···`
secure: true, // counts as a secure origin (needed for WASM, SAB)

Check warning on line 73 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Delete `·····`

Check warning on line 73 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Delete `·····`

Check warning on line 73 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Delete `·····`
supportFetchAPI: true, // allow fetch() from renderer and worker contexts
corsEnabled: true, // no CORS errors when Pyodide fetches its own assets
},
Expand Down Expand Up @@ -113,7 +152,7 @@
try {
return fs
.readdirSync(workspaces)
.filter((workspace) => workspace !== '.DS_Store' && workspace !== 'Test_Plot');

Check warning on line 155 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Replace `(workspace)·=>·workspace·!==·'.DS_Store'·&&·workspace·!==·'Test_Plot'` with `⏎········(workspace)·=>·workspace·!==·'.DS_Store'·&&·workspace·!==·'Test_Plot'⏎······`

Check warning on line 155 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Replace `(workspace)·=>·workspace·!==·'.DS_Store'·&&·workspace·!==·'Test_Plot'` with `⏎········(workspace)·=>·workspace·!==·'.DS_Store'·&&·workspace·!==·'Test_Plot'⏎······`

Check warning on line 155 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Replace `(workspace)·=>·workspace·!==·'.DS_Store'·&&·workspace·!==·'Test_Plot'` with `⏎········(workspace)·=>·workspace·!==·'.DS_Store'·&&·workspace·!==·'Test_Plot'⏎······`
} catch (e: unknown) {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
mkdirPathSync(workspaces);
Expand Down Expand Up @@ -142,8 +181,10 @@
ipcMain.handle(
'fs:storeExperimentState',
(_event, state: Record<string, unknown>) => {
const dir = getWorkspaceDir(state.title as string);
if (!fs.existsSync(dir)) return;
fs.writeFileSync(

Check warning on line 186 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Replace `⏎······path.join(dir,·'appState.json'),⏎······JSON.stringify(state)⏎····` with `path.join(dir,·'appState.json'),·JSON.stringify(state)`

Check warning on line 186 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Replace `⏎······path.join(dir,·'appState.json'),⏎······JSON.stringify(state)⏎····` with `path.join(dir,·'appState.json'),·JSON.stringify(state)`

Check warning on line 186 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Replace `⏎······path.join(dir,·'appState.json'),⏎······JSON.stringify(state)⏎····` with `path.join(dir,·'appState.json'),·JSON.stringify(state)`
path.join(getWorkspaceDir(state.title as string), 'appState.json'),
path.join(dir, 'appState.json'),
JSON.stringify(state)
);
}
Expand Down Expand Up @@ -235,10 +276,10 @@
const dir = path.join(getWorkspaceDir(title), 'Results', 'Images');
mkdirPathSync(dir);
return new Promise<void>((resolve, reject) => {
fs.writeFile(path.join(dir, `${imageTitle}.svg`), svgContent, 'utf8', (err) => {

Check warning on line 279 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Replace `path.join(dir,·`${imageTitle}.svg`),·svgContent,·'utf8',` with `⏎········path.join(dir,·`${imageTitle}.svg`),⏎········svgContent,⏎········'utf8',⏎·······`

Check warning on line 279 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Replace `path.join(dir,·`${imageTitle}.svg`),·svgContent,·'utf8',` with `⏎········path.join(dir,·`${imageTitle}.svg`),⏎········svgContent,⏎········'utf8',⏎·······`

Check warning on line 279 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Replace `path.join(dir,·`${imageTitle}.svg`),·svgContent,·'utf8',` with `⏎········path.join(dir,·`${imageTitle}.svg`),⏎········svgContent,⏎········'utf8',⏎·······`
if (err) reject(err);

Check warning on line 280 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Insert `··`

Check warning on line 280 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Insert `··`

Check warning on line 280 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Insert `··`
else resolve();

Check warning on line 281 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Insert `··`

Check warning on line 281 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Insert `··`

Check warning on line 281 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Insert `··`
});

Check warning on line 282 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Replace `}` with `··}⏎······`

Check warning on line 282 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Replace `}` with `··}⏎······`

Check warning on line 282 in src/main/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Replace `}` with `··}⏎······`
});
}
);
Expand Down Expand Up @@ -458,6 +499,10 @@
if (is.dev || process.env.DEBUG_PROD === 'true') {
mainWindow.webContents.openDevTools();
}
if (pendingOAuthUrl) {
mainWindow.webContents.send('oauth:callback', pendingOAuthUrl);
pendingOAuthUrl = null;
}
});

mainWindow.on('closed', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
imageTitle: string,
svgContent: string
): Promise<void> =>
ipcRenderer.invoke('fs:storePyodideImageSvg', title, imageTitle, svgContent),

Check warning on line 96 in src/preload/index.ts

View workflow job for this annotation

GitHub Actions / ci (macos-latest)

Replace `'fs:storePyodideImageSvg',·title,·imageTitle,·svgContent` with `⏎······'fs:storePyodideImageSvg',⏎······title,⏎······imageTitle,⏎······svgContent⏎····`

Check warning on line 96 in src/preload/index.ts

View workflow job for this annotation

GitHub Actions / ci (ubuntu-latest)

Replace `'fs:storePyodideImageSvg',·title,·imageTitle,·svgContent` with `⏎······'fs:storePyodideImageSvg',⏎······title,⏎······imageTitle,⏎······svgContent⏎····`

Check warning on line 96 in src/preload/index.ts

View workflow job for this annotation

GitHub Actions / ci (windows-latest)

Replace `'fs:storePyodideImageSvg',·title,·imageTitle,·svgContent` with `⏎······'fs:storePyodideImageSvg',⏎······title,⏎······imageTitle,⏎······svgContent⏎····`

storePyodideImagePng: (
title: string,
Expand Down Expand Up @@ -153,4 +153,14 @@
getResourcePath: (): Promise<string> => ipcRenderer.invoke('getResourcePath'),

getViewerUrl: (): Promise<string> => ipcRenderer.invoke('getViewerUrl'),

// ------------------------------------------------------------------
// OAuth deep-link callback
// ------------------------------------------------------------------
onOAuthCallback: (callback: (url: string) => void) => {
const handler = (_event: Electron.IpcRendererEvent, url: string) =>
callback(url);
ipcRenderer.on('oauth:callback', handler);
return () => ipcRenderer.removeListener('oauth:callback', handler);
},
});
14 changes: 13 additions & 1 deletion src/renderer/components/ExperimentWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export interface ExperimentWindowProps {
onFinish: (csv: any) => void; // lab.js finish event data — shape is opaque third-party type
}

// Converts an absolute filesystem path to a URL the renderer can load.
// In Vite dev mode, /@fs/<path> serves files outside publicDir.
// In production the renderer has a file:// origin so file:// URLs work directly.
function absPathToUrl(absPath: string): string {
return import.meta.env.DEV ? `/@fs${absPath}` : `file://${absPath}`;
}

export const ExperimentWindow: React.FC<ExperimentWindowProps> = ({
title,
experimentObject,
Expand All @@ -27,6 +34,11 @@ export const ExperimentWindow: React.FC<ExperimentWindowProps> = ({
onFinish,
}) => {
useEffect(() => {
// experimentObject starts as {} in Redux initial state — bail out until a
// real experiment is loaded, otherwise lab.core.deserialize crashes on
// the missing `type` field.
if (!experimentObject?.type) return;

// TODO: move this study mutation into Redux?
const experimentClone = clonedeep(experimentObject);
const paramsClone = clonedeep(params);
Expand All @@ -38,7 +50,7 @@ export const ExperimentWindow: React.FC<ExperimentWindowProps> = ({
experimentToRun.options.media.images = params.stimuli?.reduce<string[]>(
(images, stimulus) => {
if (stimulus.dir && stimulus.filename) {
return [...images, path.join(stimulus.dir, stimulus.filename)];
return [...images, absPathToUrl(path.join(stimulus.dir, stimulus.filename))];
}
return images;
},
Expand Down
1 change: 1 addition & 0 deletions src/renderer/components/HomeComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export default class Home extends Component<Props, State> {
}

handleLoadCustomExperiment(title: string) {
title = title.replace(/ /g, '_');
this.setState({ isNewExperimentModalOpen: false });
if (this.state.recentWorkspaces.includes(title)) {
toast.error(`Experiment already exists`);
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export enum EXPERIMENTS {
NONE = 'NONE',
N170 = 'Faces and Houses',
STROOP = 'Stroop Task',
N170 = 'Faces_and_Houses',
STROOP = 'Stroop_Task',
MULTI = 'Multi-tasking',
SEARCH = 'Visual Search',
SEARCH = 'Visual_Search',
CUSTOM = 'Custom',
// P300 = 'Visual Oddball',
// SSVEP = 'Steady-state Visual Evoked Potential',
Expand Down
15 changes: 7 additions & 8 deletions src/renderer/epics/experimentEpics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ const createNewWorkspaceEpic: Epic<
action$.pipe(
filter(isActionOf(ExperimentActions.CreateNewWorkspace)),
map((action) => action.payload as WorkSpaceInfo),
mergeMap(async (workspaceInfo) => {
await createWorkspaceDir(workspaceInfo.title);
return workspaceInfo;
}),
mergeMap((workspaceInfo) => {
const experiment = getExperimentFromType(workspaceInfo.type);
return of(
Expand All @@ -66,6 +62,7 @@ const startEpic = (action$, state$) =>
filter(isActionOf(ExperimentActions.Start)),
filter(() => !state$.value.experiment.isRunning),
mergeMap(async () => {
await createWorkspaceDir(state$.value.experiment.title);
if (
state$.value.device.connectionStatus === CONNECTION_STATUS.CONNECTED
) {
Expand Down Expand Up @@ -193,10 +190,12 @@ const saveWorkspaceEpic: Epic<
),
mergeMap(async () => {
const now = Date.now();
await storeExperimentState({
...state$.value.experiment,
dateModified: now,
});
// experimentObject contains function references (hooks) that cannot be
// serialized via IPC structured clone. It is always re-derived from
// `type` on load (see handleLoadRecentWorkspace), so omit it here.
const { experimentObject: _omit, ...serializableState } =
state$.value.experiment;
await storeExperimentState({ ...serializableState, dateModified: now });
return now;
}),
map(ExperimentActions.SetDateModified)
Expand Down
32 changes: 16 additions & 16 deletions src/renderer/experiments/custom/experiment.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const facesHousesExperiment = {
files: {},
parameters: {},
responses: {},
messageHandlers: {},
hooks: {},
title: 'The face-house task',
content: [
{
Expand All @@ -28,7 +28,7 @@ export const facesHousesExperiment = {
'keypress(Space)': 'continue',
'keypress(q)': 'skipPractice',
},
messageHandlers: {},
hooks: {},
title: 'Instruction',
content:
'\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EThe face-house task\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n\n \u003Cp\u003E\n ${this.parameters.intro}\n \u003C\u002Fp\u003E\n \n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E',
Expand All @@ -43,7 +43,7 @@ export const facesHousesExperiment = {
n: '',
},
responses: {},
messageHandlers: {
hooks: {
'before:prepare': function anonymous() {
let initParameters = [...this.parameters.stimuli] || [];

Expand Down Expand Up @@ -134,7 +134,7 @@ export const facesHousesExperiment = {
files: {},
parameters: {},
responses: {},
messageHandlers: {},
hooks: {},
title: 'Trial',
content: [
{
Expand Down Expand Up @@ -166,7 +166,7 @@ export const facesHousesExperiment = {
files: {},
parameters: {},
responses: {},
messageHandlers: {},
hooks: {},
viewport: [800, 600],
title: 'Fixation cross',
timeout: '${parameters.iti}',
Expand All @@ -176,7 +176,7 @@ export const facesHousesExperiment = {
files: {},
responses: {},
parameters: {},
messageHandlers: {
hooks: {
'before:prepare': function anonymous() {
console.log('before:prepare screen 1');

Expand Down Expand Up @@ -227,7 +227,7 @@ export const facesHousesExperiment = {
timeout:
"${parameters.selfPaced ? '3600000' : parameters.presentationTime}",
content:
'\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E',
'<main class="content-horizontal-center content-vertical-center">\n <div>\n <img src="${ this.files[this.parameters.image] }" style="max-height: ${ this.parameters.imageHeight }; max-width: 100%; object-fit: contain;" />\n </div>\n</main>\n\n<footer class="content-vertical-center content-horizontal-center">\n <p>\n ${this.parameters.taskHelp} \n </p>\n</footer>',
},
{
type: 'lab.canvas.Screen',
Expand All @@ -254,7 +254,7 @@ export const facesHousesExperiment = {
files: {},
parameters: {},
responses: {},
messageHandlers: {
hooks: {
end: function anonymous() {
this.data.correct_response = false;
},
Expand All @@ -275,7 +275,7 @@ export const facesHousesExperiment = {
responses: {
'keypress(Space)': 'continue',
},
messageHandlers: {},
hooks: {},
title: 'Main task',
content:
'\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \u003Ch1\u003EReady for the real data collection?\u003C\u002Fh1\u003E\n\u003C\u002Fheader\u003E\n\u003Cmain\u003E\n\n \u003Cp\u003E\n Press the the space bar to start the main task.\n \u003C\u002Fp\u003E\n\n\u003C\u002Fmain\u003E\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Ffooter\u003E',
Expand All @@ -290,7 +290,7 @@ export const facesHousesExperiment = {
n: '',
},
responses: {},
messageHandlers: {
hooks: {
'before:prepare': function anonymous() {
let initialParameters = [...this.parameters.stimuli] || [];
console.log('before:prepare initial params 2', initialParameters);
Expand Down Expand Up @@ -384,7 +384,7 @@ export const facesHousesExperiment = {
files: {},
parameters: {},
responses: {},
messageHandlers: {},
hooks: {},
title: 'Trial',
content: [
{
Expand Down Expand Up @@ -416,7 +416,7 @@ export const facesHousesExperiment = {
files: {},
parameters: {},
responses: {},
messageHandlers: {},
hooks: {},
viewport: [800, 600],
title: 'Fixation cross',
timeout: '${parameters.iti}',
Expand All @@ -426,7 +426,7 @@ export const facesHousesExperiment = {
files: {},
responses: {},
parameters: {},
messageHandlers: {
hooks: {
'before:prepare': function anonymous() {
// This code registers an event listener for this screen.
// We have a timeout for this screen, but we also want to record responses.
Expand Down Expand Up @@ -477,7 +477,7 @@ export const facesHousesExperiment = {
timeout:
"${parameters.selfPaced ? '3600000' : parameters.presentationTime}",
content:
'\u003Cmain class="content-horizontal-center content-vertical-center"\u003E\n \u003Cdiv\u003E\n \u003Cimg src=${ this.files[this.parameters.image] } height=${ this.parameters.imageHeight } \u002F\u003E\n \u003C\u002Fdiv\u003E\n\u003C\u002Fmain\u003E\n\n\u003Cfooter class="content-vertical-center content-horizontal-center"\u003E\n \u003Cp\u003E\n ${this.parameters.taskHelp} \n \u003C\u002Fp\u003E\n\u003C\u002Ffooter\u003E',
'<main class="content-horizontal-center content-vertical-center">\n <div>\n <img src="${ this.files[this.parameters.image] }" style="max-height: ${ this.parameters.imageHeight }; max-width: 100%; object-fit: contain;" />\n </div>\n</main>\n\n<footer class="content-vertical-center content-horizontal-center">\n <p>\n ${this.parameters.taskHelp} \n </p>\n</footer>',
},
{
type: 'lab.canvas.Screen',
Expand All @@ -504,7 +504,7 @@ export const facesHousesExperiment = {
files: {},
parameters: {},
responses: {},
messageHandlers: {},
hooks: {},
viewport: [800, 600],
title: 'Feedback',
tardy: true,
Expand All @@ -521,7 +521,7 @@ export const facesHousesExperiment = {
responses: {
'keypress(Space)': 'end',
},
messageHandlers: {},
hooks: {},
title: 'End',
content:
'\u003Cheader class="content-vertical-center content-horizontal-center"\u003E\n \n\u003C\u002Fheader\u003E\n\n\u003Cmain\u003E\n \u003Ch1\u003E\n Thank you!\n \u003C\u002Fh1\u003E\n \u003Ch1\u003E\n Press the space bar to finish the task.\n \u003C\u002Fh1\u003E\n\u003C\u002Fmain\u003E\n\n',
Expand Down
Loading
Loading