Skip to content
Merged
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
24 changes: 24 additions & 0 deletions .llms/learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,3 +431,27 @@ deadlock the flip (the previous screen's `lock` frame is cancelled), so wait
~700 ms before each automated response. Native `showMessageBox` dialogs can be
auto-answered by re-registering `dialog:showMessage` from that inspector
(`process.getBuiltinModule('module').createRequire(...)('electron')`).

## Prepare: `PrepareSteps` owns the built-in step chrome

`DesignComponent` renders `PrepareSteps` for the four built-in studies;
Custom and Imported keep `SecondaryNavComponent` and their authoring steps.
The switch is `Experiment.prepare`: an experiment with a `prepare.ts` gets the
lesson, one without authors its own. Per-experiment data lives in each
experiment's `prepare.ts` (`responses` + `flow`, plus Faces/Houses'
`mediaFallback`), re-exported by `PrepareSteps/fixtures.ts`, so Storybook shows
what runs. Trial counts come from `flowFromStructure` and the experiment's real
loops — never `params.nbTrials`, which is stale for Stroop (real 8/96) and
Search (real 8/80). `experiments/__tests__/prepareContent.test.ts` runs each
loop's `before:prepare` hook to check the counts, and checks the diagram's keys
against every key the study accepts. `PreviewLabel` is the single preview
status, used by `PreviewButtonComponent` (Custom, Imported, Collect) and
`PrepareSteps`. Multitasking puts the same key under two rules, so response
rows are keyed by `label`, not `key`.

The running preview draws the participant screen at `zoom: 0.55` inside an
`absolute inset-0` stage. Both halves matter: the lab.js mount sizes itself with
`height: 100%`, which only resolves against a definite height (inside the
`min-h-full` scroll column it fell back to content height, so the box grew and
the page scrolled), and at the 800px column width an unzoomed screen is
550–750px tall.
7 changes: 5 additions & 2 deletions TODOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,20 @@ Deferred and in-flight work. Keep this current — when something ships, delete
- Clean vs Analyze labels are ambiguous — clarify the step purpose.
- Clean: allow choosing a different file without going back/undoing.
- Add "What does clean your data mean?" student explainer (aligns with Epoch reviewer onboarding work below).
- Nav-state distinction: the workflow bar (Prepare/Collect/Clean/Analyze) and the local steps bar (Overview/Background/Protocol/Preview) read as one stacked nav on Prepare — Workstream 4, plan §3.2. (playtest 09-23)
- ~~Nav-state distinction: the workflow bar (Prepare/Collect/Clean/Analyze) and the local steps bar (Overview/Background/Protocol/Preview) read as one stacked nav on Prepare — Workstream 4, plan §3.2. (playtest 09-23)~~ — shipped 2026-09-25 (WS4 Prepare integration, PR #281). Built-ins' Design screen is `PrepareSteps`: gold step pills under the gold-underline workflow bar, one forward action per step.
- ~~Blocked areas~~ — shipped 2026-09-23 (`WorkspaceAreaGate` on /clean and /analyze, PR #269).
- ~~Device chip is display-only~~ — shipped 2026-09-23 (WS2, PR #274). The header chip opens headset setup; the RunBar chip stays status-only.
- ~~Early exit and incomplete runs (WS5)~~ — shipped 2026-09-24 (branch `feat/ws5-early-exit`). `End experiment early` and a held Escape both end the run with no confirm; what was recorded is kept as `*.incomplete.csv` and left out of Clean, Analyze and the badges; the result screen says `Experiment ended early`.
- QA (WS5): the agent playtest could not drive native dialogs or file pickers. Still to check by hand: a complete Faces/Houses run (keeps `-behavior.csv`/`-raw.csv`, shows `Clean this recording →`), the #270 "session 2" prompt after an ended-early session 1, ending an imported jsPsych study early, and a behavior-only run (complete → `Analyze results →`; ended early → only `-behavior.incomplete.csv`).
- ~~BrainWaves-owned participant screens (§7.2)~~ — shipped 2026-09-25 (WS5b, PR #279). Built-ins and Custom show the #273 instruction / practice→main / end screens; the stillness line follows the run's EEG setting.
- ~~Q never skips practice (lab.js 23)~~ — fixed 2026-09-25 (WS5b, PR #279). A `skip` template on the practice block was parsed before the instruction screen's response was committed, so it was always false. Instruction screens now have an `end` hook, `skipPracticeOnRequest`, that sets `skip` on the block that follows.
- Accepted (WS5b): `isEEGEnabled` is now a column in every lab.js behavior CSV, imported lab.js studies included, the same way `title` is.
- Design preview box (`h-[330px]`) clips the participant screens, so the keycaps need scrolling. Fix during the WS4 Prepare integration.
- ~~Design preview box (`h-[330px]`) clips the participant screens, so the keycaps need scrolling.~~ — fixed 2026-09-25 (WS4 Prepare integration, PR #281). The running preview fills the free height (at least 420px) and draws the participant screen at 0.55 zoom; all four built-ins' first screens show whole at 1366×768 and 1280×720.

## Next (V1.5: Visual Polish and Juice)
- [ ] **Multitasking's Prepare protocol diagram** describes two rules on the same keys (`Top: diamond → B`, `Bottom: 2 dots → B`, …); verify with a teacher whether it reads clearly.
- [ ] **Expanded preview** — the Design preview is fit-to-box at zoom 0.55 (small print ~10px). Add an expanded/full-size preview with the same Preview label and Stop action.
- [ ] **Collect without a workspace** — Collect actions (Preview, Run & record) should be unavailable when there is no workspace (`params` is null after `ExperimentCleanup`, e.g. cancelling headset setup while connecting). `LabjsExperimentWindow` now waits instead of crashing (PR #281), but the buttons still look usable.
- [ ] **Cleaning UXR playtest** (added 2026-09-25, after the WS6 Clean design #280) — a naive user, with no facilitator help, cleans one real recording in the redesigned Clean screen. Can they find the job (leave out noisy trials, flag a bad sensor, review auto-flag suggestions, watch the Live ERP change, save), and do they understand why cleaning comes before Analyze? Run after WS6 integration, before WS8's full second playtest.
- [ ] **Participant screens for imported jsPsych/lab.js studies** (deferred 2026-09-25): wrap author timelines with BrainWaves instruction/transition/end screens.
- [ ] **Import stimuli into the workspace?** — today custom experiments load images/sounds straight from wherever the student keeps them (Documents/Downloads) via the `bwfile://` allowlist; moving/renaming that folder silently breaks the study, and a workspace can't be zipped up and shared as a self-contained bundle. Alternative: copy stimuli into `BrainWaves_Workspaces/<title>/stimuli/<condition>/` at selection time (single pre-authorized root, portable study bundles; costs disk duplication + stale copies if the source folder is edited later). **Contingent on user testing** — students may actually prefer managing their own folders in Documents/Downloads, since workspace folders are semi-private territory full of mysterious things like `appState.json`. Decide after watching a class use the current flow.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import Design, { DesignProps } from '../index';
import { EXPERIMENTS } from '../../../constants/constants';

vi.mock('lab.js', () => ({}));
vi.mock('../../../utils/filesystem/storage', () => ({
readWorkspaces: () => Promise.resolve([]),
}));
vi.mock('../CustomDesignComponent', () => ({
default: () => <div data-testid="custom-design" />,
}));
vi.mock('../ImportedDesignComponent', () => ({
default: () => <div data-testid="imported-design" />,
}));
vi.mock('../../PreviewExperimentComponent', () => ({
default: () => <div data-testid="preview-experiment" />,
}));

const baseProps = {
navigate: vi.fn(),
type: EXPERIMENTS.N170,
title: 'Faces_Houses_1',
params: {},
experimentObject: {},
ExperimentActions: {
SetEEGEnabled: vi.fn(),
SaveWorkspace: vi.fn(),
CreateNewWorkspace: vi.fn(),
},
isEEGEnabled: true,
} as unknown as DesignProps;

describe('Design — built-in Prepare', () => {
it('renders the Prepare steps with the experiment keys and the real trial counts', () => {
render(<Design {...baseProps} />);
expect(
screen.getByRole('navigation', { name: 'Prepare steps' })
).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /Next: Background/ })
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Protocol/ }));
expect(screen.getByText('Face')).toBeInTheDocument();
expect(screen.getByText('House')).toBeInTheDocument();
expect(screen.getByText('6').parentElement).toHaveTextContent(
'6 practice trials'
);
expect(screen.getByText('120').parentElement).toHaveTextContent(
'120 recorded trials'
);
});

it('walks Overview → Background → Protocol → Preview and offers Try the experiment', () => {
render(<Design {...baseProps} />);
fireEvent.click(screen.getByRole('button', { name: /Next: Background/ }));
expect(screen.getByText(/BACKGROUND/)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Next: Protocol/ }));
expect(screen.getByText('WHAT HAPPENS IN THIS TASK')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Try the experiment/ }));
expect(screen.queryByTestId('preview-experiment')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Try the experiment/ }));
expect(screen.getByTestId('preview-experiment')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Stop preview' }));
fireEvent.click(screen.getByRole('button', { name: /Run & record/ }));
expect(baseProps.navigate).toHaveBeenCalledWith('/collect');
});

it('keeps the EEG toggle, saving the workspace choice', () => {
render(<Design {...baseProps} />);
fireEvent.click(screen.getByRole('checkbox', { name: 'EEG recording' }));
expect(baseProps.ExperimentActions.SetEEGEnabled).toHaveBeenCalledWith(
false
);
expect(baseProps.ExperimentActions.SaveWorkspace).toHaveBeenCalled();
});

it('keeps Custom on its authoring flow', () => {
render(<Design {...baseProps} type={EXPERIMENTS.CUSTOM} />);
expect(screen.getByTestId('custom-design')).toBeInTheDocument();
expect(
screen.queryByRole('navigation', { name: 'Prepare steps' })
).not.toBeInTheDocument();
});

it('keeps Imported on its authoring flow', () => {
render(<Design {...baseProps} type={EXPERIMENTS.IMPORTED} />);
expect(screen.getByTestId('imported-design')).toBeInTheDocument();
expect(
screen.queryByRole('navigation', { name: 'Prepare steps' })
).not.toBeInTheDocument();
});
});
Loading
Loading