From 22f0f8f1de31864992c04518262dbb3014b1a4dc Mon Sep 17 00:00:00 2001 From: jdpigeon Date: Thu, 24 Sep 2026 15:33:54 -0400 Subject: [PATCH 1/5] design: WS4 Prepare step stories with fixture data Adds a pure-props PrepareSteps component and co-located stories for the Prepare design pass (WS4). Stories render inside the real AppShell chrome at location='prepare' using fixtures derived from real experiment content and params files. Stories: - Overview, Background, Protocol (with parameter-driven flow infographic) - PreviewStopped, PreviewRunning, PreviewFinished - DirectCollect, CustomDesign, ImportedConfigure - OliverSacksFallback - ProtocolStroop, ProtocolSearch No runtime wiring; design-only. --- docs/uxr/2026-09-24-ws4-design-brief.md | 59 ++ .../PrepareSteps/PrepareSteps.stories.tsx | 281 +++++++++ .../components/PrepareSteps/PrepareSteps.tsx | 561 ++++++++++++++++++ .../components/PrepareSteps/fixtures.ts | 192 ++++++ 4 files changed, 1093 insertions(+) create mode 100644 docs/uxr/2026-09-24-ws4-design-brief.md create mode 100644 src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx create mode 100644 src/renderer/components/PrepareSteps/PrepareSteps.tsx create mode 100644 src/renderer/components/PrepareSteps/fixtures.ts diff --git a/docs/uxr/2026-09-24-ws4-design-brief.md b/docs/uxr/2026-09-24-ws4-design-brief.md new file mode 100644 index 00000000..16172e36 --- /dev/null +++ b/docs/uxr/2026-09-24-ws4-design-brief.md @@ -0,0 +1,59 @@ +# WS4 design brief — Prepare, local steps, protocol, Preview + +**Gate:** plan §10.1 step 2. Storybook stories with fixture data only, no runtime wiring. Engineering integrates after product approves. + +**Sources:** +- `docs/uxr/playtest_naive_1_design_implementation_plan.md` §1.1, §1.4, §3.2, §3.3, §6, §10.2 ("Experiment Prepare"), §11 WS4. +- Design system: `docs/design/DESIGN.md`, `.design-sync/conventions.md`. +- Shape to match: #272 (`components/HeadsetSetup/`) and #273 (`components/ParticipantScreens/`, `CollectComponent/RunResult.tsx`). Pure-props views, co-located stories, fixtures file. + +## The job + +Prepare is where a student learns what the experiment is before collecting data. Both playtests (09-18 naive; 09-23 product) found that the global workflow bar (Prepare / Collect / Clean / Analyze) and the local step bar (Overview / Background / Protocol / Preview) read as one stacked nav. The student can't tell "where am I in the study" from "where am I in this lesson". Each step also lacks a clear forward action. + +## Current code (read before designing) + +- `src/renderer/components/DesignComponent/index.tsx` — built-in Prepare. `DESIGN_STEPS` OVERVIEW / BACKGROUND / PROTOCOL / PREVIEW, rendered through `SecondaryNavComponent`. +- `src/renderer/components/SecondaryNavComponent/` — the local tab bar (+ existing `SecondaryNavSegment.stories.tsx`). +- `src/renderer/components/AppShell/` — the global bar (WorkflowNav, gold current / quiet `Next →`). Do not change it; design the local steps to be visibly different from it. +- `src/renderer/experiments//content_overview.js`, `content_background.js`, `content_protocol.js` — the real copy and protocol fields (`condition_*`, `*_key`, `pacing`). Take fixtures from these for Faces/Houses and at least one other built-in. +- `src/renderer/components/PreviewExperimentComponent.tsx` + the Design screen's preview state — the existing `PREVIEW · nothing is being recorded` + `Stop preview` + `Run & record` behavior shipped in #270. Keep those semantics. +- Custom (`CustomDesignComponent.tsx`) and imported (`ImportedDesignComponent.tsx`) Prepare surfaces use Design / Configure instead of Learn. Show how the local-step pattern applies to them in at least one story each. Do not redesign their forms. + +## Stories required (plan §10.2 "Experiment Prepare") + +Use a new pure-props component, e.g. `components/PrepareSteps/`, with a fixtures file and stories: + +| Story | Must show | +|---|---| +| Overview | Local stepper clearly subordinate to the global bar (render inside the real `AppShell` chrome, as #273's decorator does). Title, what the study asks, one dominant forward action `Next: Background`. | +| Background | Readable lesson copy. `Back` + `Next: Protocol`. | +| Protocol | The existing condition cards and keycaps (reuse the #273 keycap look), **plus** a compact flow infographic generated from parameters: Faces/Houses = Instructions → 6 practice trials → Main-task reminder → 120 recorded trials → Completion (§6.2). Pacing line from the protocol. Graphics clearly explanatory, not interactive. Forward action `Try the experiment`. | +| PreviewStopped | Preview entry: expected keys, "nothing is recorded", primary `Try the experiment`, and `Run & record` reachable. | +| PreviewRunning | Persistent `PREVIEW` chrome, "nothing is being recorded", expected keys, visible `Stop preview` (§3.3). Placeholder for the experiment area. | +| PreviewFinished | `Run & record` is the dominant next action; `Preview again` secondary. | +| DirectCollect | A student who skips the lessons can still go to Collect. No lock, no completion checkmark, no "you must finish" copy (§1.4, §13). | +| CustomDesign / ImportedConfigure | The same local-step treatment with the local heading Design / Configure; forms shown as placeholders. | +| OliverSacksFallback | Background section with a local illustrated explanation + transcript-length text in place of the YouTube embed (§6.3: no remote player; rights not confirmed). Mark the illustration spot clearly as a placeholder if no art exists. | + +Build the flow infographic from a typed input (e.g. `{ label, count? }[]`) derived from params in fixtures, not hand-drawn per experiment. + +## Constraints + +- Global bar = gold for location, teal `Next →` for recommendation (WS1). The local stepper must not reuse the gold underline in a way that reads as a second global nav. Pick a visibly different treatment and explain it in the PR. +- One filled-teal primary action per surface. Action labels name the consequence (§1.2). +- No persisted lesson/preview completion; no gating of Collect (§1.4, §13). +- Student-facing, friendly, direct tone. Light headings. +- Root font-size is 18px and the window min width is 1180px (`.llms/learnings.md`). Check 1366×768 and 1280×720 inside the real shell chrome. +- Traps in `.llms/learnings.md`: lab.css styles bare `
/
`; global `li { list-style: none }`; global `p { 18px !important }`; `.experiment-design-content` resets already exist for the Design screen. +- Do not edit `DesignComponent/index.tsx`, `SecondaryNavComponent`, the AppShell, or any experiment/runtime file. New files + stories only (plus scoped CSS in `app.global.css` if needed). + +## Out of scope + +Wiring into Redux/routes, the Custom/Imported form contents, real Oliver Sacks media, Collect/Clean/Analyze. + +## Done when + +- Every story renders in Storybook inside the real shell chrome; each is screenshotted at both sizes. +- `npx tsc --noEmit` clean. +- A PR with a review agenda, open copy questions, and any unmet constraint. diff --git a/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx new file mode 100644 index 00000000..0f3d7c27 --- /dev/null +++ b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx @@ -0,0 +1,281 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { fn } from 'storybook/test'; +import AppShell from '../AppShell/AppShell'; +import { Button } from '../ui/button'; +import PrepareSteps, { PrepareStepId } from './PrepareSteps'; +import { + CUSTOM, + FACES_HOUSES, + IMPORTED, + NOOP_HANDLERS, + SEARCH, + STROOP, +} from './fixtures'; + +interface StoryParams { + step?: PrepareStepId; + isPreviewing?: boolean; + hasPreviewed?: boolean; + modality?: 'eeg' | 'behavior'; + fixture?: 'faces' | 'stroop' | 'search' | 'custom' | 'imported'; +} + +const workspace = { + name: 'Faces_Houses_4', + experimentType: 'Faces/Houses', + modality: 'eeg' as const, +}; + +const behaviorWorkspace = { + name: 'Stroop_Task_2', + experimentType: 'Stroop', + modality: 'behavior' as const, +}; + +const customWorkspace = { + name: 'My_Custom_Exp', + experimentType: 'Custom', + modality: 'eeg' as const, +}; + +const importedWorkspace = { + name: 'Imported_Study', + experimentType: 'Imported', + modality: 'eeg' as const, +}; + +function fixtureFor(key: StoryParams['fixture']) { + switch (key) { + case 'stroop': + return STROOP; + case 'search': + return SEARCH; + case 'custom': + return CUSTOM; + case 'imported': + return IMPORTED; + case 'faces': + default: + return FACES_HOUSES; + } +} + +/** + * Wrap the story in the real AppShell chrome at Prepare, with Collect + * recommended. This makes the local stepper visually subordinate to the + * global gold-underline workflow bar. + */ +const withPrepareChrome = (Story: any, { parameters }: any) => { + const ws = parameters.workspace ?? workspace; + return ( + + + + ); +}; + +const meta: Meta = { + title: 'Domain/PrepareSteps', + component: PrepareSteps, + parameters: { layout: 'fullscreen' }, + decorators: [withPrepareChrome], + args: { + heading: FACES_HOUSES.heading, + overview: FACES_HOUSES.overview, + background: FACES_HOUSES.background, + protocol: FACES_HOUSES.protocol, + expectedKeys: FACES_HOUSES.expectedKeys, + flow: FACES_HOUSES.flow, + icon: FACES_HOUSES.icon, + modality: 'eeg', + isPreviewing: false, + hasPreviewed: false, + onStep: fn(), + onCollect: fn(), + onPreviewStart: fn(), + onPreviewStop: fn(), + onPreviewAgain: fn(), + }, +}; +export default meta; +type Story = StoryObj; + +/** Local stepper is a rounded pill sequence, visually distinct from the global gold-underline workflow bar. */ +function InteractiveStep({ + initialStep, + fixtureKey = 'faces', + workspaceOverride, + modality = 'eeg', +}: { + initialStep: PrepareStepId; + fixtureKey?: StoryParams['fixture']; + workspaceOverride?: typeof workspace; + modality?: 'eeg' | 'behavior'; +}) { + const fixture = fixtureFor(fixtureKey); + const [step, setStep] = useState(initialStep); + const [isPreviewing, setIsPreviewing] = useState(false); + const [hasPreviewed, setHasPreviewed] = useState(false); + return ( + {}} + onPreviewStart={() => { + setIsPreviewing(true); + setHasPreviewed(true); + }} + onPreviewStop={() => setIsPreviewing(false)} + onPreviewAgain={() => setIsPreviewing(true)} + /> + ); +} + +/** P01 — Overview. One dominant forward action to Background. */ +export const Overview: Story = { + render: () => , +}; + +/** P02 — Background. Readable lesson copy with Back and Next: Protocol. */ +export const Background: Story = { + render: () => , +}; + +/** P03 — Protocol. Condition cards, keycaps, and a compact flow infographic. */ +export const Protocol: Story = { + render: () => , +}; + +/** P04 — PreviewStopped. Nothing recorded; Try the experiment primary, Run & record reachable. */ +export const PreviewStopped: Story = { + render: () => , +}; + +/** P05 — PreviewRunning. PREVIEW chrome, expected keys, Stop preview. */ +export const PreviewRunning: Story = { + render: () => ( + + ), +}; + +/** P06 — PreviewFinished. Run & record is primary; Preview again secondary. */ +export const PreviewFinished: Story = { + render: () => ( + + ), +}; + +/** P07 — DirectCollect. Skipping lessons is allowed; Collect reachable, no lock or checkmark. */ +export const DirectCollect: Story = { + parameters: { workspace: behaviorWorkspace, device: 'none' }, + render: () => ( + +
+

Ready to run

+

+ This workspace records key presses and reaction times only — no + headset needed. You can review the protocol first, or start now. +

+
+ +
+
+
+ ), +}; + +/** P08 — CustomDesign. Same local-step treatment, with Design heading and form placeholders. */ +export const CustomDesign: Story = { + parameters: { workspace: customWorkspace }, + render: () => , +}; + +/** P09 — ImportedConfigure. Same local-step treatment, with Configure heading and form placeholders. */ +export const ImportedConfigure: Story = { + parameters: { workspace: importedWorkspace }, + render: () => ( + + ), +}; + +/** P10 — OliverSacksFallback. Transcript-length text + illustrated placeholder, no remote player. */ +export const OliverSacksFallback: Story = { + render: () => ( + + ), +}; + +/** P11 — Stroop protocol shows the flow infographic adapts (no practice trials). */ +export const ProtocolStroop: Story = { + render: () => , +}; + +/** P12 — Visual Search protocol shows the flow and pacing wording differences. */ +export const ProtocolSearch: Story = { + render: () => , +}; diff --git a/src/renderer/components/PrepareSteps/PrepareSteps.tsx b/src/renderer/components/PrepareSteps/PrepareSteps.tsx new file mode 100644 index 00000000..fe7d5b6b --- /dev/null +++ b/src/renderer/components/PrepareSteps/PrepareSteps.tsx @@ -0,0 +1,561 @@ +import React from 'react'; +import { Button } from '../ui/button'; + +export type PrepareStepId = 'overview' | 'background' | 'protocol' | 'preview'; + +export interface FlowPhase { + label: string; + count?: number; +} + +export interface PrepareStepsProps { + step: PrepareStepId; + heading: string; + modality: 'eeg' | 'behavior'; + overview: { title: string; overview: string; links: { address: string; name: string }[] }; + background: { + links: { address: string; name: string }[]; + title?: string; + definition_title?: string; + link_meta?: string; + fun_fact_image?: string; + first_column_statement: string; + first_column_question: string; + second_column_statement: string; + second_column_question: string; + }; + protocol: { + title: string; + protocol: string; + condition_first_img: string; + condition_first_title: string; + condition_first: string; + condition_second_img: string; + condition_second_title: string; + condition_second: string; + condition_first_key?: string; + condition_second_key?: string; + pacing?: string; + links?: { address: string; name: string }[]; + }; + expectedKeys: { label: string; key?: string }[]; + flow: FlowPhase[]; + icon?: string; + onStep: (step: PrepareStepId) => void; + onCollect: () => void; + onPreviewStart: () => void; + onPreviewStop: () => void; + onPreviewAgain: () => void; + isPreviewing: boolean; + hasPreviewed: boolean; +} + +function Stepper({ + current, + onStep, +}: { + current: PrepareStepId; + onStep: (step: PrepareStepId) => void; +}) { + const steps: { id: PrepareStepId; label: string }[] = [ + { id: 'overview', label: 'Overview' }, + { id: 'background', label: 'Background' }, + { id: 'protocol', label: 'Protocol' }, + { id: 'preview', label: 'Preview' }, + ]; + const currentIndex = steps.findIndex((s) => s.id === current); + return ( + + ); +} + +function KeyLegend({ keys }: { keys: { label: string; key?: string }[] }) { + return ( +
+ {keys.map(({ label, key }) => ( +
+ {key ? ( + + {key} + + ) : null} + {label} +
+ ))} +
+ ); +} + +function ConditionCard({ + title, + description, + keyLabel, +}: { + title: string; + description: string; + keyLabel?: string; +}) { + return ( +
+

{title}

+

+ {description} + {keyLabel ? ( + <> + {' '} + + {keyLabel} + + + ) : null} +

+
+ ); +} + +function FlowInfographic({ phases }: { phases: FlowPhase[] }) { + return ( +
+

What happens in this task

+
    + {phases.map((phase, i) => ( + +
  1. + {typeof phase.count === 'number' ? ( + + {phase.count} + + ) : null} + {phase.label} +
  2. + {i < phases.length - 1 && ( +
  3. + → +
  4. + )} +
    + ))} +
+
+ ); +} + +function OverviewView({ + overview, + icon, + onNext, +}: { + overview: PrepareStepsProps['overview']; + icon?: string; + onNext: () => void; +}) { + return ( +
+
+ + THE BIG QUESTION + +

{overview.title}

+ {overview.overview.split(/\n\s*\n/).map((paragraph) => ( +

+ {paragraph} +

+ ))} +
+ +
+
+ {icon && ( +
+ {overview.title} +
+ )} +
+ ); +} + +function BackgroundView({ + background, + icon, + onBack, + onNext, +}: { + background: PrepareStepsProps['background']; + icon?: string; + onBack: () => void; + onNext: () => void; +}) { + return ( +
+
+ + BACKGROUND · 2 MIN READ + +

{background.title ?? 'Background'}

+
+

+ {background.first_column_statement} +

+ {background.first_column_question && ( +
+ {icon && ( +
+ +
+ )} +
+

+ {background.definition_title ?? 'What researchers found'} +

+

+ {background.first_column_question} +

+
+
+ )} + {(background.second_column_statement || background.second_column_question) && ( +
+
+ + FUN FACT + +

+ {[background.second_column_statement, background.second_column_question] + .filter(Boolean) + .join(' ')} +

+
+ {background.fun_fact_image && ( + + )} +
+ )} + {background.links.map((link) => ( +
+ + {background.link_meta && ( + {background.link_meta} + )} +
+ ))} +
+ + +
+
+ ); +} + +function ProtocolView({ + protocol, + expectedKeys, + flow, + onBack, + onNext, +}: { + protocol: PrepareStepsProps['protocol']; + expectedKeys: { label: string; key?: string }[]; + flow: FlowPhase[]; + onBack: () => void; + onNext: () => void; +}) { + const conditions = [ + { + title: protocol.condition_first_title, + description: protocol.condition_first, + key: protocol.condition_first_key, + }, + { + title: protocol.condition_second_title, + description: protocol.condition_second, + key: protocol.condition_second_key, + }, + ]; + return ( +
+
+ + PROTOCOL + +

{protocol.title}

+

{protocol.protocol}

+
+
+ {conditions.map( + (condition) => + condition.title && ( + + ) + )} +
+ + {protocol.pacing && ( +
+

+ Pacing: {protocol.pacing} +

+
+ )} + +
+ + +
+
+ ); +} + +function PreviewStoppedView({ + expectedKeys, + onPreviewStart, + onCollect, +}: { + expectedKeys: { label: string; key?: string }[]; + onPreviewStart: () => void; + onCollect: () => void; +}) { + return ( +
+
+ + PREVIEW + +

See it the way they will

+

+ Nothing is recorded during a preview. Try the experiment, then decide + when you are ready to collect real data. +

+
+ +
+ + +
+
+ ); +} + +function PreviewRunningView({ + expectedKeys, + onPreviewStop, +}: { + expectedKeys: { label: string; key?: string }[]; + onPreviewStop: () => void; +}) { + return ( +
+
+ + Preview + + + Nothing is being recorded + +
+
+
+ + Experiment area + + +
+
+ + Participant screen would appear here + +
+
+ +
+ ); +} + +function PreviewFinishedView({ + expectedKeys, + onPreviewAgain, + onCollect, +}: { + expectedKeys: { label: string; key?: string }[]; + onPreviewAgain: () => void; + onCollect: () => void; +}) { + return ( +
+
+ + PREVIEW + +

Ready to record?

+

+ Preview is done. When a participant is ready, run and record real data. +

+
+ +
+ + +
+
+ ); +} + +/** + * Prepare steps: overview → background → protocol → preview, rendered as + * pure props. The local stepper is a rounded pill sequence, visually distinct + * from the global gold-underline workflow bar. + */ +export default function PrepareSteps(props: PrepareStepsProps) { + const { step } = props; + + function transitionTo(nextStep: PrepareStepId) { + props.onStep(nextStep); + } + + return ( +
+
+ +
+
+
+ {step === 'overview' && ( + transitionTo('background')} + /> + )} + {step === 'background' && ( + transitionTo('overview')} + onNext={() => transitionTo('protocol')} + /> + )} + {step === 'protocol' && ( + transitionTo('background')} + onNext={() => transitionTo('preview')} + /> + )} + {step === 'preview' && !props.isPreviewing && !props.hasPreviewed && ( + + )} + {step === 'preview' && props.isPreviewing && ( + + )} + {step === 'preview' && !props.isPreviewing && props.hasPreviewed && ( + + )} +
+
+
+ ); +} diff --git a/src/renderer/components/PrepareSteps/fixtures.ts b/src/renderer/components/PrepareSteps/fixtures.ts new file mode 100644 index 00000000..44c95e1f --- /dev/null +++ b/src/renderer/components/PrepareSteps/fixtures.ts @@ -0,0 +1,192 @@ +import { overview as facesOverview } from '../../experiments/faces_houses/content_overview'; +import { background as facesBackground } from '../../experiments/faces_houses/content_background'; +import { protocol as facesProtocol } from '../../experiments/faces_houses/content_protocol'; +import { params as facesParams } from '../../experiments/faces_houses/params'; + +import { overview as stroopOverview } from '../../experiments/stroop/content_overview'; +import { background as stroopBackground } from '../../experiments/stroop/content_background'; +import { protocol as stroopProtocol } from '../../experiments/stroop/content_protocol'; +import { params as stroopParams } from '../../experiments/stroop/params'; + +import { overview as searchOverview } from '../../experiments/search/content_overview'; +import { background as searchBackground } from '../../experiments/search/content_background'; +import { protocol as searchProtocol } from '../../experiments/search/content_protocol'; +import { params as searchParams } from '../../experiments/search/params'; + +import facesIcon from '../../experiments/faces_houses/icon.png'; +import brad from '../../assets/common/brad.png'; + +import { FlowPhase, PrepareStepsProps } from './PrepareSteps'; + +export interface PrepareFixture { + heading: string; + overview: PrepareStepsProps['overview']; + background: PrepareStepsProps['background']; + protocol: PrepareStepsProps['protocol']; + icon?: string; + expectedKeys: { label: string; key?: string }[]; + flow: FlowPhase[]; +} + +/** + * Build expected keys from the first two stimuli that differ by condition. + * Falls back to a single "press any key" mapping when labels are unavailable. + */ +function keysFromStimuli(stimuli: { condition?: string; response?: string }[]) { + const byCondition = new Map(); + for (const stimulus of stimuli) { + const condition = stimulus.condition; + const response = stimulus.response; + if (condition && response && !byCondition.has(condition)) { + byCondition.set(condition, response); + } + } + const entries = Array.from(byCondition.entries()); + if (entries.length === 0) { + return [{ label: 'Press the key shown on screen' }]; + } + return entries.map(([label, key]) => ({ label, key })); +} + +function buildFlow(params: { + includePractice?: boolean; + nbPracticeTrials?: number; + nbTrials: number; +}): FlowPhase[] { + const phases: FlowPhase[] = [{ label: 'Instructions' }]; + if (params.includePractice && (params.nbPracticeTrials ?? 0) > 0) { + phases.push({ label: 'Practice trials', count: params.nbPracticeTrials }); + } + phases.push({ label: 'Main-task reminder' }); + phases.push({ label: 'Recorded trials', count: params.nbTrials }); + phases.push({ label: 'Completion' }); + return phases; +} + +export const FACES_HOUSES: PrepareFixture = { + heading: 'Faces/Houses', + overview: facesOverview, + background: { ...facesBackground, fun_fact_image: brad }, + protocol: facesProtocol, + icon: facesIcon, + expectedKeys: keysFromStimuli(facesParams.stimuli ?? []), + flow: buildFlow({ + includePractice: facesParams.includePractice, + nbPracticeTrials: facesParams.nbPracticeTrials, + nbTrials: facesParams.nbTrials, + }), +}; + +export const STROOP: PrepareFixture = { + heading: 'Stroop', + overview: stroopOverview, + background: stroopBackground, + protocol: stroopProtocol, + expectedKeys: keysFromStimuli(stroopParams.stimuli ?? []), + flow: buildFlow({ nbTrials: stroopParams.nbTrials }), +}; + +export const SEARCH: PrepareFixture = { + heading: 'Visual Search', + overview: searchOverview, + background: searchBackground, + protocol: searchProtocol, + expectedKeys: keysFromStimuli(searchParams.stimuli ?? []), + flow: buildFlow({ nbTrials: searchParams.nbTrials }), +}; + +export const CUSTOM: PrepareFixture = { + heading: 'Custom', + overview: { + title: 'My Custom Experiment', + overview: + 'In this experiment, students see images you choose and press a key for each one.', + links: [], + }, + background: { + first_column_statement: + 'Custom experiments let you test your own questions using pictures and sounds you provide.', + definition_title: 'Research question', + first_column_question: 'What do you want to find out?', + second_column_statement: + 'You choose the conditions, response keys, and trials.', + second_column_question: '', + links: [], + }, + protocol: { + title: 'What participants are shown', + protocol: + 'Participants see one stimulus at a time and press the key you assigned to its condition.', + condition_first_img: '', + condition_first_title: 'Condition 1', + condition_first: 'Press the assigned key for Condition 1', + condition_second_img: '', + condition_second_title: 'Condition 2', + condition_second: 'Press the assigned key for Condition 2', + pacing: 'Go at a comfortable, steady pace.', + }, + expectedKeys: [ + { label: 'Condition 1', key: '1' }, + { label: 'Condition 2', key: '9' }, + ], + flow: buildFlow({ nbTrials: 0 }), +}; + +export const IMPORTED: PrepareFixture & { file: string } = { + heading: 'Imported', + overview: { + title: 'Imported Study', + overview: + 'This experiment was written outside BrainWaves. BrainWaves runs it and records the responses.', + links: [], + }, + background: { + first_column_statement: + 'Imported studies keep their own instructions and timing.', + definition_title: 'Markers', + first_column_question: + 'Name the conditions in the Markers tab so BrainWaves can write EEG markers.', + second_column_statement: + 'If you do not add conditions, the study runs behavior-only.', + second_column_question: '', + links: [], + }, + protocol: { + title: 'What participants are shown', + protocol: + 'The study file controls the trial order, stimuli, and instructions shown to participants.', + condition_first_img: '', + condition_first_title: 'Condition A', + condition_first: 'Declared condition A', + condition_second_img: '', + condition_second_title: 'Condition B', + condition_second: 'Declared condition B', + pacing: undefined, + }, + expectedKeys: [ + { label: 'Condition A', key: 'f' }, + { label: 'Condition B', key: 'j' }, + ], + flow: [ + { label: 'Study instructions' }, + { label: 'Trials' }, + { label: 'Completion' }, + ], + file: 'my-study.js', +}; + +/** Shared no-op callbacks so stories do not need to supply handlers. */ +export const NOOP_HANDLERS: Pick< + PrepareStepsProps, + | 'onStep' + | 'onCollect' + | 'onPreviewStart' + | 'onPreviewStop' + | 'onPreviewAgain' +> = { + onStep: () => {}, + onCollect: () => {}, + onPreviewStart: () => {}, + onPreviewStop: () => {}, + onPreviewAgain: () => {}, +}; From 228826791c4a99d6808cf411c208c4e8d7691101 Mon Sep 17 00:00:00 2001 From: jdpigeon Date: Thu, 24 Sep 2026 16:06:55 -0400 Subject: [PATCH 2/5] =?UTF-8?q?fix(design):=20review=20fixes=20=E2=80=94?= =?UTF-8?q?=20Sacks=20stand-in=20inside=20Background,=20sticky=20step=20ac?= =?UTF-8?q?tions,=20outline=20Stop=20preview,=20single=20shell=20in=20Dire?= =?UTF-8?q?ctCollect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/app.global.css | 4 +- .../PrepareSteps/PrepareSteps.stories.tsx | 38 +- .../components/PrepareSteps/PrepareSteps.tsx | 391 +++++++++++------- .../components/PrepareSteps/fixtures.ts | 9 + 4 files changed, 253 insertions(+), 189 deletions(-) diff --git a/src/renderer/app.global.css b/src/renderer/app.global.css index 01809b3a..0a09bcb2 100644 --- a/src/renderer/app.global.css +++ b/src/renderer/app.global.css @@ -39,7 +39,7 @@ p { .experiment-design-copy { margin: 0; font-size: 19px; - line-height: 1.8; + line-height: 1.55; letter-spacing: 0.3px; text-wrap: pretty; } @@ -47,7 +47,7 @@ p { .experiment-design-card-copy { margin: 0; font-size: 17px; - line-height: 1.7; + line-height: 1.5; letter-spacing: 0.3px; text-wrap: pretty; } diff --git a/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx index 0f3d7c27..de25f521 100644 --- a/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx +++ b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import type { Meta, StoryObj } from '@storybook/react-vite'; import { fn } from 'storybook/test'; import AppShell from '../AppShell/AppShell'; -import { Button } from '../ui/button'; import PrepareSteps, { PrepareStepId } from './PrepareSteps'; import { CUSTOM, @@ -111,12 +110,10 @@ type Story = StoryObj; function InteractiveStep({ initialStep, fixtureKey = 'faces', - workspaceOverride, modality = 'eeg', }: { initialStep: PrepareStepId; fixtureKey?: StoryParams['fixture']; - workspaceOverride?: typeof workspace; modality?: 'eeg' | 'behavior'; }) { const fixture = fixtureFor(fixtureKey); @@ -208,29 +205,15 @@ export const PreviewFinished: Story = { ), }; -/** P07 — DirectCollect. Skipping lessons is allowed; Collect reachable, no lock or checkmark. */ +/** + * P07 — DirectCollect. A student who skips the lessons: on Overview of a + * behavior-only workspace, Collect is marked Next and clickable in the global + * bar. Nothing is locked or checked off. + */ export const DirectCollect: Story = { parameters: { workspace: behaviorWorkspace, device: 'none' }, render: () => ( - -
-

Ready to run

-

- This workspace records key presses and reaction times only — no - headset needed. You can review the protocol first, or start now. -

-
- -
-
-
+ ), }; @@ -248,20 +231,19 @@ export const ImportedConfigure: Story = { ), }; -/** P10 — OliverSacksFallback. Transcript-length text + illustrated placeholder, no remote player. */ +/** P10 — OliverSacksFallback. Background with the Sacks section: illustration placeholder + explanatory text, no remote player. */ export const OliverSacksFallback: Story = { render: () => ( void; onCollect: () => void; onPreviewStart: () => void; @@ -123,14 +126,14 @@ function KeyLegend({ keys }: { keys: { label: string; key?: string }[] }) { {keys.map(({ label, key }) => (
{key ? ( - + {key} ) : null} - {label} + {label}
))} @@ -147,9 +150,9 @@ function ConditionCard({ keyLabel?: string; }) { return ( -
-

{title}

-

+

+

{title}

+

{description} {keyLabel ? ( <> @@ -166,12 +169,12 @@ function ConditionCard({ function FlowInfographic({ phases }: { phases: FlowPhase[] }) { return ( -

-

What happens in this task

-
    +
    +

    What happens in this task

    +
      {phases.map((phase, i) => ( -
    1. +
    2. {typeof phase.count === 'number' ? ( {phase.count} @@ -194,36 +197,29 @@ function FlowInfographic({ phases }: { phases: FlowPhase[] }) { function OverviewView({ overview, icon, - onNext, }: { overview: PrepareStepsProps['overview']; icon?: string; - onNext: () => void; }) { return ( -
      -
      +
      +
      THE BIG QUESTION -

      {overview.title}

      +

      {overview.title}

      {overview.overview.split(/\n\s*\n/).map((paragraph) => ( -

      +

      {paragraph}

      ))} -
      - -
      {icon && ( -
      +
      {overview.title}
      )} @@ -234,49 +230,45 @@ function OverviewView({ function BackgroundView({ background, icon, - onBack, - onNext, }: { background: PrepareStepsProps['background']; icon?: string; - onBack: () => void; - onNext: () => void; }) { return ( -
      -
      +
      +
      BACKGROUND · 2 MIN READ -

      {background.title ?? 'Background'}

      +

      {background.title ?? 'Background'}

      -

      +

      {background.first_column_statement}

      {background.first_column_question && ( -
      +
      {icon && ( -
      - +
      +
      )} -
      -

      +
      +

      {background.definition_title ?? 'What researchers found'}

      -

      +

      {background.first_column_question}

      )} {(background.second_column_statement || background.second_column_question) && ( -
      -
      +
      +
      FUN FACT -

      +

      {[background.second_column_statement, background.second_column_question] .filter(Boolean) .join(' ')} @@ -286,7 +278,7 @@ function BackgroundView({ )}

      @@ -304,14 +296,6 @@ function BackgroundView({ )}
      ))} -
      - - -
      ); } @@ -320,14 +304,10 @@ function ProtocolView({ protocol, expectedKeys, flow, - onBack, - onNext, }: { protocol: PrepareStepsProps['protocol']; expectedKeys: { label: string; key?: string }[]; flow: FlowPhase[]; - onBack: () => void; - onNext: () => void; }) { const conditions = [ { @@ -342,15 +322,15 @@ function ProtocolView({ }, ]; return ( -
      -
      +
      +
      PROTOCOL -

      {protocol.title}

      -

      {protocol.protocol}

      +

      {protocol.title}

      +

      {protocol.protocol}

      -
      +
      {conditions.map( (condition) => condition.title && ( @@ -365,71 +345,49 @@ function ProtocolView({
      {protocol.pacing && ( -
      -

      +

      +

      Pacing: {protocol.pacing}

      )} -
      - - -
      ); } function PreviewStoppedView({ expectedKeys, - onPreviewStart, - onCollect, }: { expectedKeys: { label: string; key?: string }[]; - onPreviewStart: () => void; - onCollect: () => void; }) { return ( -
      -
      +
      +
      PREVIEW -

      See it the way they will

      -

      +

      See it the way they will

      +

      Nothing is recorded during a preview. Try the experiment, then decide when you are ready to collect real data.

      -
      - - -
      ); } function PreviewRunningView({ expectedKeys, - onPreviewStop, }: { expectedKeys: { label: string; key?: string }[]; - onPreviewStop: () => void; }) { return ( -
      +
      Preview @@ -438,14 +396,11 @@ function PreviewRunningView({ Nothing is being recorded
      -
      +
      Experiment area -
      @@ -460,42 +415,128 @@ function PreviewRunningView({ function PreviewFinishedView({ expectedKeys, - onPreviewAgain, - onCollect, }: { expectedKeys: { label: string; key?: string }[]; - onPreviewAgain: () => void; - onCollect: () => void; }) { return ( -
      -
      +
      +
      PREVIEW -

      Ready to record?

      -

      +

      Ready to record?

      +

      Preview is done. When a participant is ready, run and record real data.

      -
      - - -
      ); } -/** - * Prepare steps: overview → background → protocol → preview, rendered as - * pure props. The local stepper is a rounded pill sequence, visually distinct - * from the global gold-underline workflow bar. - */ +function StepActions({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
      + {children} +
      + ); +} + +function ActionNext({ + children, + onClick, +}: { + children: React.ReactNode; + onClick: () => void; +}) { + return ( + + ); +} + +function ActionBack({ + children, + onClick, +}: { + children: React.ReactNode; + onClick: () => void; +}) { + return ( + + ); +} + +function OliverSacksFallbackView({ + media, +}: { + media: { caption: string; alt: string }; +}) { + return ( +
      +
      + + OLIVER SACKS · FACE BLINDNESS + +

      {media.caption}

      +
      +
      +
      +
      + + 🎬 + + + Illustration placeholder + +
      +
      +

      + Illustration placeholder for: {media.alt} +

      +
      +
      +

      + Some people cannot recognize faces — even faces they have seen thousands + of times. Neurologist Oliver Sacks described this in himself: he might + walk past a close friend, or his own reflection, without knowing who it + was. +

      +

      + Scientists call this condition prosopagnosia, from the Greek for + "face" and "not knowing." It is not poor eyesight; the brain's + face-recognition system doesn't process faces the usual way. +

      +

      + Sacks wrote about what this feels like, and about the idea that the + brain has a dedicated "face area." That idea led researchers + to compare the brain's responses to faces and to other objects. +

      +

      + Source: Oliver Sacks, "Face-Blind" (2010), + The New Yorker / The Mind's Eye. +

      +
      +
      + ); +} export default function PrepareSteps(props: PrepareStepsProps) { const { step } = props; @@ -504,56 +545,88 @@ export default function PrepareSteps(props: PrepareStepsProps) { } return ( -
      +
      -
      - {step === 'overview' && ( - transitionTo('background')} - /> - )} - {step === 'background' && ( - transitionTo('overview')} - onNext={() => transitionTo('protocol')} - /> - )} - {step === 'protocol' && ( - transitionTo('background')} - onNext={() => transitionTo('preview')} - /> - )} - {step === 'preview' && !props.isPreviewing && !props.hasPreviewed && ( - - )} - {step === 'preview' && props.isPreviewing && ( - - )} - {step === 'preview' && !props.isPreviewing && props.hasPreviewed && ( - - )} +
      +
      + {step === 'overview' && ( + + )} + {step === 'background' && ( + <> + + {props.mediaFallback && ( + + )} + + )} + {step === 'protocol' && ( + + )} + {step === 'preview' && !props.isPreviewing && !props.hasPreviewed && ( + + )} + {step === 'preview' && props.isPreviewing && ( + + )} + {step === 'preview' && !props.isPreviewing && props.hasPreviewed && ( + + )} +
      + + {step === 'overview' && ( + transitionTo('background')}>Next: Background → + )} + {step === 'background' && ( + <> + transitionTo('overview')}>← Back + transitionTo('protocol')}>Next: Protocol → + + )} + {step === 'protocol' && ( + <> + transitionTo('background')}>← Back + transitionTo('preview')}>Try the experiment → + + )} + {step === 'preview' && !props.isPreviewing && !props.hasPreviewed && ( + <> + transitionTo('protocol')}>← Back + Try the experiment → + + )} + {step === 'preview' && props.isPreviewing && ( + <> + Stop preview + Preview in progress + + )} + {step === 'preview' && !props.isPreviewing && props.hasPreviewed && ( + <> + Preview again + Run & record → + + )} +
      diff --git a/src/renderer/components/PrepareSteps/fixtures.ts b/src/renderer/components/PrepareSteps/fixtures.ts index 44c95e1f..245aa590 100644 --- a/src/renderer/components/PrepareSteps/fixtures.ts +++ b/src/renderer/components/PrepareSteps/fixtures.ts @@ -26,6 +26,11 @@ export interface PrepareFixture { icon?: string; expectedKeys: { label: string; key?: string }[]; flow: FlowPhase[]; + /** Optional media placeholder shown on the Oliver Sacks fallback step. */ + mediaFallback?: { + caption: string; + alt: string; + }; } /** @@ -75,6 +80,10 @@ export const FACES_HOUSES: PrepareFixture = { nbPracticeTrials: facesParams.nbPracticeTrials, nbTrials: facesParams.nbTrials, }), + mediaFallback: { + caption: 'Oliver Sacks on face blindness', + alt: 'Illustrated portrait placeholder for Oliver Sacks', + }, }; export const STROOP: PrepareFixture = { From 86a5157aa22f1614a6aa6bc1b9c2765dd47d4d08 Mon Sep 17 00:00:00 2001 From: jdpigeon Date: Thu, 24 Sep 2026 16:07:23 -0400 Subject: [PATCH 3/5] fix(design): revert global copy line-height change (leaked into live Design screens) --- src/renderer/app.global.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/app.global.css b/src/renderer/app.global.css index 0a09bcb2..01809b3a 100644 --- a/src/renderer/app.global.css +++ b/src/renderer/app.global.css @@ -39,7 +39,7 @@ p { .experiment-design-copy { margin: 0; font-size: 19px; - line-height: 1.55; + line-height: 1.8; letter-spacing: 0.3px; text-wrap: pretty; } @@ -47,7 +47,7 @@ p { .experiment-design-card-copy { margin: 0; font-size: 17px; - line-height: 1.5; + line-height: 1.7; letter-spacing: 0.3px; text-wrap: pretty; } From 658a2364d9692edc9e08988bfc627b4ec181e54a Mon Sep 17 00:00:00 2001 From: jdpigeon Date: Thu, 24 Sep 2026 16:34:16 -0400 Subject: [PATCH 4/5] =?UTF-8?q?design(ws4):=20round=202=20=E2=80=94=20gold?= =?UTF-8?q?=20step=20pill,=20centered=20column,=20Protocol=20redesign,=20s?= =?UTF-8?q?hared=20PreviewLabel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Local stepper current step is a small gold pill (bg-accent, ink text) in the secondary bar - Overview/Background/Protocol/Preview in one centered 800px column; action row aligned to it - Protocol: static stimulus → key diagram + vertical task timeline from flow; condition cards, pacing callout and key chips removed; Stroop (r/g/b/y) and Search (b/n) fixtures - New PreviewLabel (eye glyph · PREVIEW · not recording) replaces the PreviewRunning card - Sacks stand-in sits in Background's video slot at 16:9 - Drop CustomDesign/ImportedConfigure stories and their fixtures --- .../PrepareSteps/PrepareSteps.stories.tsx | 185 ++--- .../components/PrepareSteps/PrepareSteps.tsx | 643 ++++++++---------- .../components/PrepareSteps/fixtures.ts | 163 ++--- .../components/PreviewLabel.stories.tsx | 32 + src/renderer/components/PreviewLabel.tsx | 36 + 5 files changed, 441 insertions(+), 618 deletions(-) create mode 100644 src/renderer/components/PreviewLabel.stories.tsx create mode 100644 src/renderer/components/PreviewLabel.tsx diff --git a/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx index de25f521..4f4bd9bf 100644 --- a/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx +++ b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx @@ -1,25 +1,17 @@ import React, { useState } from 'react'; -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'; import { fn } from 'storybook/test'; import AppShell from '../AppShell/AppShell'; import PrepareSteps, { PrepareStepId } from './PrepareSteps'; import { - CUSTOM, FACES_HOUSES, - IMPORTED, NOOP_HANDLERS, + PrepareFixture, + SACKS_STAND_IN, SEARCH, STROOP, } from './fixtures'; -interface StoryParams { - step?: PrepareStepId; - isPreviewing?: boolean; - hasPreviewed?: boolean; - modality?: 'eeg' | 'behavior'; - fixture?: 'faces' | 'stroop' | 'search' | 'custom' | 'imported'; -} - const workspace = { name: 'Faces_Houses_4', experimentType: 'Faces/Houses', @@ -32,53 +24,22 @@ const behaviorWorkspace = { modality: 'behavior' as const, }; -const customWorkspace = { - name: 'My_Custom_Exp', - experimentType: 'Custom', - modality: 'eeg' as const, -}; - -const importedWorkspace = { - name: 'Imported_Study', - experimentType: 'Imported', - modality: 'eeg' as const, -}; - -function fixtureFor(key: StoryParams['fixture']) { - switch (key) { - case 'stroop': - return STROOP; - case 'search': - return SEARCH; - case 'custom': - return CUSTOM; - case 'imported': - return IMPORTED; - case 'faces': - default: - return FACES_HOUSES; - } -} - /** * Wrap the story in the real AppShell chrome at Prepare, with Collect * recommended. This makes the local stepper visually subordinate to the * global gold-underline workflow bar. */ -const withPrepareChrome = (Story: any, { parameters }: any) => { - const ws = parameters.workspace ?? workspace; - return ( - - - - ); -}; +const withPrepareChrome: Decorator = (Story, { parameters }) => ( + + + +); const meta: Meta = { title: 'Domain/PrepareSteps', @@ -86,14 +47,7 @@ const meta: Meta = { parameters: { layout: 'fullscreen' }, decorators: [withPrepareChrome], args: { - heading: FACES_HOUSES.heading, - overview: FACES_HOUSES.overview, - background: FACES_HOUSES.background, - protocol: FACES_HOUSES.protocol, - expectedKeys: FACES_HOUSES.expectedKeys, - flow: FACES_HOUSES.flow, - icon: FACES_HOUSES.icon, - modality: 'eeg', + ...FACES_HOUSES, isPreviewing: false, hasPreviewed: false, onStep: fn(), @@ -106,31 +60,21 @@ const meta: Meta = { export default meta; type Story = StoryObj; -/** Local stepper is a rounded pill sequence, visually distinct from the global gold-underline workflow bar. */ +/** Clickable stepper and preview state, so a reviewer can walk the whole lesson from any story. */ function InteractiveStep({ initialStep, - fixtureKey = 'faces', - modality = 'eeg', + fixture = FACES_HOUSES, }: { initialStep: PrepareStepId; - fixtureKey?: StoryParams['fixture']; - modality?: 'eeg' | 'behavior'; + fixture?: PrepareFixture; }) { - const fixture = fixtureFor(fixtureKey); const [step, setStep] = useState(initialStep); const [isPreviewing, setIsPreviewing] = useState(false); const [hasPreviewed, setHasPreviewed] = useState(false); return ( , }; -/** P02 — Background. Readable lesson copy with Back and Next: Protocol. */ +/** P02 — Background. Centered lesson column with Back and Next: Protocol. */ export const Background: Story = { render: () => , }; -/** P03 — Protocol. Condition cards, keycaps, and a compact flow infographic. */ +/** P03 — Protocol. Static stimulus → key diagram beside a vertical task timeline generated from `flow`. */ export const Protocol: Story = { render: () => , }; -/** P04 — PreviewStopped. Nothing recorded; Try the experiment primary, Run & record reachable. */ +/** P04 — Stroop protocol: four ink colors, four keys; no practice phase in its params. */ +export const ProtocolStroop: Story = { + render: () => , +}; + +/** P05 — Visual Search protocol: target present / absent on b / n. */ +export const ProtocolSearch: Story = { + render: () => , +}; + +/** P06 — PreviewStopped. Nothing recorded; Try the experiment is primary. */ export const PreviewStopped: Story = { render: () => , }; -/** P05 — PreviewRunning. PREVIEW chrome, expected keys, Stop preview. */ +/** P07 — PreviewRunning. Shared PreviewLabel beside Stop preview; expected keys under the experiment area. */ export const PreviewRunning: Story = { render: () => ( ), }; -/** P06 — PreviewFinished. Run & record is primary; Preview again secondary. */ +/** P08 — PreviewFinished. Run & record is primary; Preview again secondary. */ export const PreviewFinished: Story = { render: () => ( ), }; /** - * P07 — DirectCollect. A student who skips the lessons: on Overview of a + * P09 — DirectCollect. A student who skips the lessons: on Overview of a * behavior-only workspace, Collect is marked Next and clickable in the global * bar. Nothing is locked or checked off. */ export const DirectCollect: Story = { parameters: { workspace: behaviorWorkspace, device: 'none' }, - render: () => ( - - ), -}; - -/** P08 — CustomDesign. Same local-step treatment, with Design heading and form placeholders. */ -export const CustomDesign: Story = { - parameters: { workspace: customWorkspace }, - render: () => , -}; - -/** P09 — ImportedConfigure. Same local-step treatment, with Configure heading and form placeholders. */ -export const ImportedConfigure: Story = { - parameters: { workspace: importedWorkspace }, - render: () => ( - - ), + render: () => , }; -/** P10 — OliverSacksFallback. Background with the Sacks section: illustration placeholder + explanatory text, no remote player. */ +/** P10 — OliverSacksFallback. Background's video slot holds the local stand-in (16:9 placeholder) and transcript-length text; no remote player. */ export const OliverSacksFallback: Story = { render: () => ( ), }; - -/** P11 — Stroop protocol shows the flow infographic adapts (no practice trials). */ -export const ProtocolStroop: Story = { - render: () => , -}; - -/** P12 — Visual Search protocol shows the flow and pacing wording differences. */ -export const ProtocolSearch: Story = { - render: () => , -}; diff --git a/src/renderer/components/PrepareSteps/PrepareSteps.tsx b/src/renderer/components/PrepareSteps/PrepareSteps.tsx index 91b8eb21..8a3a5fe6 100644 --- a/src/renderer/components/PrepareSteps/PrepareSteps.tsx +++ b/src/renderer/components/PrepareSteps/PrepareSteps.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import PreviewLabel from '../PreviewLabel'; import { Button } from '../ui/button'; import { cn } from '../ui/utils'; @@ -9,10 +10,17 @@ export interface FlowPhase { count?: number; } +/** One stimulus → key pair, drawn in the Protocol diagram and the Preview key legend. */ +export interface ResponseMapping { + key: string; + /** What the participant is looking for, e.g. "Face" or "Red ink". */ + label: string; + /** An example stimulus: an image, or a word in colored ink (Stroop). */ + stimulus: { src: string; alt: string } | { word: string; color: string }; +} + export interface PrepareStepsProps { step: PrepareStepId; - heading: string; - modality: 'eeg' | 'behavior'; overview: { title: string; overview: string; links: { address: string; name: string }[] }; background: { links: { address: string; name: string }[]; @@ -25,24 +33,11 @@ export interface PrepareStepsProps { second_column_statement: string; second_column_question: string; }; - protocol: { - title: string; - protocol: string; - condition_first_img: string; - condition_first_title: string; - condition_first: string; - condition_second_img: string; - condition_second_title: string; - condition_second: string; - condition_first_key?: string; - condition_second_key?: string; - pacing?: string; - links?: { address: string; name: string }[]; - }; - expectedKeys: { label: string; key?: string }[]; + protocol: { title: string; protocol: string }; + responses: ResponseMapping[]; flow: FlowPhase[]; icon?: string; - /** Local stand-in for the Oliver Sacks clip (§6.3: no remote player, rights unconfirmed); shown inside Background. */ + /** Local stand-in for the Oliver Sacks clip (§6.3: no remote player, rights unconfirmed); replaces Background's video link. */ mediaFallback?: { caption: string; alt: string }; onStep: (step: PrepareStepId) => void; onCollect: () => void; @@ -53,6 +48,20 @@ export interface PrepareStepsProps { hasPreviewed: boolean; } +/** The single centered reading column shared by the stepper, step content and action row. */ +const COLUMN = 'mx-auto w-full max-w-[800px] px-6'; + +const EYEBROW = 'text-[13px] font-bold tracking-[0.5px] text-ink-muted'; + +/** Keycap in the #273 participant-screen look (ink outline, heavy bottom edge), sized for lesson pages. */ +const KEYCAP = + 'inline-flex h-[40px] min-w-[40px] flex-none items-center justify-center rounded-[8px] border-2 border-b-[5px] border-ink bg-white px-2 font-sans text-[20px] font-bold uppercase text-ink'; + +/** + * Lesson progress inside Prepare. Pills in the secondary bar, so it never reads + * as a second global nav: the global bar marks location with a 4px gold + * underline, this marks the current step with a small gold-filled pill. + */ function Stepper({ current, onStep, @@ -78,30 +87,24 @@ function Stepper({ type="button" onClick={() => onStep(s.id)} aria-current={isCurrent ? 'step' : undefined} - className={` - flex items-center gap-2 rounded-full px-3 py-1.5 text-[13px] font-bold - tracking-[0.5px] transition-colors focus-visible:outline-none - focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 - ${ - isCurrent - ? 'bg-ink text-white' - : isPast - ? 'bg-brand-light text-brand hover:bg-brand hover:text-white' - : 'bg-white text-ink-muted hover:bg-[#f3f3f8] hover:text-ink' - } - `} + className={cn( + 'flex items-center gap-[6px] rounded-full py-[4px] pl-[5px] pr-[12px] text-[13px] font-bold tracking-[0.5px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2', + isCurrent + ? 'bg-accent text-ink' + : isPast + ? 'bg-brand-light text-brand hover:bg-brand hover:text-white' + : 'bg-white text-ink-muted hover:bg-[#f3f3f8] hover:text-ink' + )} > {isPast ? '✓' : i + 1} @@ -120,77 +123,118 @@ function Stepper({ ); } -function KeyLegend({ keys }: { keys: { label: string; key?: string }[] }) { +function KeyLegend({ responses }: { responses: ResponseMapping[] }) { return ( -
      - {keys.map(({ label, key }) => ( -
      - {key ? ( - - {key} - - ) : null} - {label} -
      +
        + {responses.map(({ label, key }) => ( +
      • + {key} + {label} +
      • ))} -
      + ); } -function ConditionCard({ - title, - description, - keyLabel, -}: { - title: string; - description: string; - keyLabel?: string; -}) { +/** + * Static flowchart: each example stimulus, an arrow, then the key it asks for, + * with "Sees" / "Presses" row labels. A figure, not a control, so it does not + * invite a key press. + */ +function ResponseDiagram({ responses }: { responses: ResponseMapping[] }) { + const rail = 'justify-self-start text-[14px] text-ink-muted'; return ( -
      -

      {title}

      -

      - {description} - {keyLabel ? ( - <> - {' '} - - {keyLabel} +

      +
      HOW PARTICIPANTS RESPOND
      +
        +
      • + Sees + + + Presses +
      • + {responses.map(({ key, label, stimulus }) => ( +
      • + {'src' in stimulus ? ( + {stimulus.alt} + ) : ( + + {stimulus.word} + + )} + {label} + + + + + press + {key} - - ) : null} -

        -
      +
    3. + ))} + + + Just a diagram. You can try it for real in Preview. + + ); } -function FlowInfographic({ phases }: { phases: FlowPhase[] }) { +/** Vertical timeline of the task's phases, generated from `flow`. */ +function FlowTimeline({ phases }: { phases: FlowPhase[] }) { return ( -
      -

      What happens in this task

      -
        +
        +

        WHAT HAPPENS IN THIS TASK

        +
          {phases.map((phase, i) => ( - -
        1. - {typeof phase.count === 'number' ? ( - - {phase.count} - - ) : null} - {phase.label} -
        2. +
        3. {i < phases.length - 1 && ( -
        4. - → -
        5. + )} -
          + + + {phase.count ? ( + <> + {phase.count} {phase.label.toLowerCase()} + + ) : ( + phase.label + )} + + ))}
        -
      + ); } @@ -202,11 +246,9 @@ function OverviewView({ icon?: string; }) { return ( -
      -
      - - THE BIG QUESTION - +
      +
      + THE BIG QUESTION

      {overview.title}

      {overview.overview.split(/\n\s*\n/).map((paragraph) => (

      @@ -215,13 +257,7 @@ function OverviewView({ ))}

      {icon && ( -
      - {overview.title} -
      + )}
      ); @@ -230,23 +266,23 @@ function OverviewView({ function BackgroundView({ background, icon, + media, }: { background: PrepareStepsProps['background']; icon?: string; + media?: PrepareStepsProps['mediaFallback']; }) { return ( -
      -
      - - BACKGROUND · 2 MIN READ - +
      +
      + BACKGROUND · 2 MIN READ

      {background.title ?? 'Background'}

      -

      +

      {background.first_column_statement}

      {background.first_column_question && ( -
      +
      {icon && (
      @@ -263,7 +299,7 @@ function BackgroundView({
      )} {(background.second_column_statement || background.second_column_question) && ( -
      +
      FUN FACT @@ -275,180 +311,135 @@ function BackgroundView({

      {background.fun_fact_image && ( - + )}
      )} - {background.links.map((link) => ( -
      - - {background.link_meta && ( - {background.link_meta} - )} -
      - ))} + {media ? ( + + ) : ( + background.links.map((link) => ( +
      + + {background.link_meta && ( + {background.link_meta} + )} +
      + )) + )} +
      + ); +} + +/** The video slot in Background's column, holding the local Sacks stand-in and its transcript-length text. */ +function OliverSacksFallback({ media }: { media: { caption: string; alt: string } }) { + return ( +
      +

      {media.caption}

      +
      + + 🎬 + + + Illustration placeholder (video slot, 16:9) + + {media.alt} +
      +

      + Some people cannot recognize faces — even faces they have seen thousands + of times. Neurologist Oliver Sacks described this in himself: he might + walk past a close friend, or his own reflection, without knowing who it + was. +

      +

      + Scientists call this condition prosopagnosia, from the Greek for + "face" and "not knowing." It is not poor eyesight; the + brain's face-recognition system doesn't process faces the usual + way. +

      +

      + Sacks wrote about what this feels like, and about the idea that the brain + has a dedicated "face area." That idea led researchers to + compare the brain's responses to faces and to other objects. +

      +

      + Source: Oliver Sacks, "Face-Blind" (2010),{' '} + The New Yorker / The Mind's Eye. +

      ); } function ProtocolView({ protocol, - expectedKeys, + responses, flow, }: { protocol: PrepareStepsProps['protocol']; - expectedKeys: { label: string; key?: string }[]; + responses: ResponseMapping[]; flow: FlowPhase[]; }) { - const conditions = [ - { - title: protocol.condition_first_title, - description: protocol.condition_first, - key: protocol.condition_first_key, - }, - { - title: protocol.condition_second_title, - description: protocol.condition_second, - key: protocol.condition_second_key, - }, - ]; return ( -
      -
      - - PROTOCOL - +
      +
      + PROTOCOL

      {protocol.title}

      {protocol.protocol}

      -
      - {conditions.map( - (condition) => - condition.title && ( - - ) - )} +
      + +
      - - {protocol.pacing && ( -
      -

      - Pacing: {protocol.pacing} -

      -
      - )} -
      ); } -function PreviewStoppedView({ - expectedKeys, -}: { - expectedKeys: { label: string; key?: string }[]; -}) { - return ( -
      -
      - - PREVIEW - -

      See it the way they will

      -

      - Nothing is recorded during a preview. Try the experiment, then decide - when you are ready to collect real data. -

      -
      - -
      - ); -} +/** Preview-step copy before and after a preview run. */ +const PREVIEW_INTRO = { + stopped: { + title: 'See it the way they will', + body: 'Nothing is recorded during a preview. Try the experiment, then decide when you are ready to collect real data.', + }, + finished: { + title: 'Ready to record?', + body: 'Preview is done. When a participant is ready, run and record real data.', + }, +}; -function PreviewRunningView({ - expectedKeys, -}: { - expectedKeys: { label: string; key?: string }[]; -}) { +function PreviewRunningView({ responses }: { responses: ResponseMapping[] }) { return ( -
      -
      - - Preview - - - Nothing is being recorded - -
      +
      -
      - - Experiment area - -
      + EXPERIMENT AREA
      Participant screen would appear here
      - +
      ); } -function PreviewFinishedView({ - expectedKeys, -}: { - expectedKeys: { label: string; key?: string }[]; -}) { - return ( -
      -
      - - PREVIEW - -

      Ready to record?

      -

      - Preview is done. When a participant is ready, run and record real data. -

      -
      - -
      - ); -} - -function StepActions({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { +function StepActions({ children }: { children: React.ReactNode }) { return ( -
      - {children} +
      +
      + {children} +
      ); } @@ -481,146 +472,88 @@ function ActionBack({ ); } -function OliverSacksFallbackView({ - media, -}: { - media: { caption: string; alt: string }; -}) { - return ( -
      -
      - - OLIVER SACKS · FACE BLINDNESS - -

      {media.caption}

      -
      -
      -
      -
      - - 🎬 - - - Illustration placeholder - -
      -
      -

      - Illustration placeholder for: {media.alt} -

      -
      -
      -

      - Some people cannot recognize faces — even faces they have seen thousands - of times. Neurologist Oliver Sacks described this in himself: he might - walk past a close friend, or his own reflection, without knowing who it - was. -

      -

      - Scientists call this condition prosopagnosia, from the Greek for - "face" and "not knowing." It is not poor eyesight; the brain's - face-recognition system doesn't process faces the usual way. -

      -

      - Sacks wrote about what this feels like, and about the idea that the - brain has a dedicated "face area." That idea led researchers - to compare the brain's responses to faces and to other objects. -

      -

      - Source: Oliver Sacks, "Face-Blind" (2010), - The New Yorker / The Mind's Eye. -

      -
      -
      - ); -} +/** Built-in lesson steps of the Prepare area: Overview, Background, Protocol, Preview. Pure props. */ export default function PrepareSteps(props: PrepareStepsProps) { - const { step } = props; - - function transitionTo(nextStep: PrepareStepId) { - props.onStep(nextStep); - } + const { step, onStep, isPreviewing, hasPreviewed } = props; + const previewStopped = step === 'preview' && !isPreviewing && !hasPreviewed; + const previewRunning = step === 'preview' && isPreviewing; + const previewFinished = step === 'preview' && !isPreviewing && hasPreviewed; + const previewIntro = previewStopped + ? PREVIEW_INTRO.stopped + : previewFinished + ? PREVIEW_INTRO.finished + : undefined; return (
      -
      - +
      +
      + +
      -
      +
      {step === 'overview' && ( - + )} {step === 'background' && ( - <> - - {props.mediaFallback && ( - - )} - + )} {step === 'protocol' && ( )} - {step === 'preview' && !props.isPreviewing && !props.hasPreviewed && ( - - )} - {step === 'preview' && props.isPreviewing && ( - - )} - {step === 'preview' && !props.isPreviewing && props.hasPreviewed && ( - + {previewIntro && ( +
      +
      + PREVIEW +

      {previewIntro.title}

      +

      + {previewIntro.body} +

      +
      + +
      )} + {previewRunning && }
      - + {step === 'overview' && ( - transitionTo('background')}>Next: Background → + onStep('background')}>Next: Background → )} {step === 'background' && ( <> - transitionTo('overview')}>← Back - transitionTo('protocol')}>Next: Protocol → + onStep('overview')}>← Back + onStep('protocol')}>Next: Protocol → )} {step === 'protocol' && ( <> - transitionTo('background')}>← Back - transitionTo('preview')}>Try the experiment → + onStep('background')}>← Back + onStep('preview')}>Try the experiment → )} - {step === 'preview' && !props.isPreviewing && !props.hasPreviewed && ( + {previewStopped && ( <> - transitionTo('protocol')}>← Back + onStep('protocol')}>← Back Try the experiment → )} - {step === 'preview' && props.isPreviewing && ( + {previewRunning && ( <> Stop preview - Preview in progress + )} - {step === 'preview' && !props.isPreviewing && props.hasPreviewed && ( + {previewFinished && ( <> Preview again Run & record → diff --git a/src/renderer/components/PrepareSteps/fixtures.ts b/src/renderer/components/PrepareSteps/fixtures.ts index 245aa590..da060fa7 100644 --- a/src/renderer/components/PrepareSteps/fixtures.ts +++ b/src/renderer/components/PrepareSteps/fixtures.ts @@ -14,44 +14,18 @@ import { protocol as searchProtocol } from '../../experiments/search/content_pro import { params as searchParams } from '../../experiments/search/params'; import facesIcon from '../../experiments/faces_houses/icon.png'; +import face from '../../experiments/faces_houses/stimuli/faces/Face1.jpg'; +import house from '../../experiments/faces_houses/stimuli/houses/House1.jpg'; +import orangeT from '../../experiments/search/stimuli/conditionOrangeT.png'; +import noOrangeT from '../../experiments/search/stimuli/conditionNoOrangeT.png'; import brad from '../../assets/common/brad.png'; import { FlowPhase, PrepareStepsProps } from './PrepareSteps'; -export interface PrepareFixture { - heading: string; - overview: PrepareStepsProps['overview']; - background: PrepareStepsProps['background']; - protocol: PrepareStepsProps['protocol']; - icon?: string; - expectedKeys: { label: string; key?: string }[]; - flow: FlowPhase[]; - /** Optional media placeholder shown on the Oliver Sacks fallback step. */ - mediaFallback?: { - caption: string; - alt: string; - }; -} - -/** - * Build expected keys from the first two stimuli that differ by condition. - * Falls back to a single "press any key" mapping when labels are unavailable. - */ -function keysFromStimuli(stimuli: { condition?: string; response?: string }[]) { - const byCondition = new Map(); - for (const stimulus of stimuli) { - const condition = stimulus.condition; - const response = stimulus.response; - if (condition && response && !byCondition.has(condition)) { - byCondition.set(condition, response); - } - } - const entries = Array.from(byCondition.entries()); - if (entries.length === 0) { - return [{ label: 'Press the key shown on screen' }]; - } - return entries.map(([label, key]) => ({ label, key })); -} +export type PrepareFixture = Pick< + PrepareStepsProps, + 'overview' | 'background' | 'protocol' | 'responses' | 'flow' | 'icon' +>; function buildFlow(params: { includePractice?: boolean; @@ -68,120 +42,61 @@ function buildFlow(params: { return phases; } +/** Keys from faces_houses/params.ts stimuli (Face → 1, House → 9). */ export const FACES_HOUSES: PrepareFixture = { - heading: 'Faces/Houses', overview: facesOverview, background: { ...facesBackground, fun_fact_image: brad }, protocol: facesProtocol, icon: facesIcon, - expectedKeys: keysFromStimuli(facesParams.stimuli ?? []), + responses: [ + { key: '1', label: 'Face', stimulus: { src: face, alt: 'A face photo' } }, + { key: '9', label: 'House', stimulus: { src: house, alt: 'A house photo' } }, + ], flow: buildFlow({ includePractice: facesParams.includePractice, nbPracticeTrials: facesParams.nbPracticeTrials, nbTrials: facesParams.nbTrials, }), - mediaFallback: { - caption: 'Oliver Sacks on face blindness', - alt: 'Illustrated portrait placeholder for Oliver Sacks', - }, }; +/** Faces/Houses Background's video slot, filled with the local Oliver Sacks stand-in. */ +export const SACKS_STAND_IN: PrepareStepsProps['mediaFallback'] = { + caption: 'Oliver Sacks on face blindness', + alt: 'Illustrated portrait of Oliver Sacks', +}; + +/** Keys and ink colors from the Stroop screen in stroop/experiment.ts; the word never matches the ink. */ export const STROOP: PrepareFixture = { - heading: 'Stroop', overview: stroopOverview, background: stroopBackground, protocol: stroopProtocol, - expectedKeys: keysFromStimuli(stroopParams.stimuli ?? []), + responses: [ + { key: 'r', label: 'Red ink', stimulus: { word: 'green', color: 'red' } }, + { key: 'g', label: 'Green ink', stimulus: { word: 'blue', color: 'green' } }, + { key: 'b', label: 'Blue ink', stimulus: { word: 'yellow', color: 'blue' } }, + { key: 'y', label: 'Yellow ink', stimulus: { word: 'red', color: '#ffe32a' } }, + ], flow: buildFlow({ nbTrials: stroopParams.nbTrials }), }; +/** Keys from search/experiment.ts (b = orange T present, n = absent). */ export const SEARCH: PrepareFixture = { - heading: 'Visual Search', overview: searchOverview, background: searchBackground, protocol: searchProtocol, - expectedKeys: keysFromStimuli(searchParams.stimuli ?? []), - flow: buildFlow({ nbTrials: searchParams.nbTrials }), -}; - -export const CUSTOM: PrepareFixture = { - heading: 'Custom', - overview: { - title: 'My Custom Experiment', - overview: - 'In this experiment, students see images you choose and press a key for each one.', - links: [], - }, - background: { - first_column_statement: - 'Custom experiments let you test your own questions using pictures and sounds you provide.', - definition_title: 'Research question', - first_column_question: 'What do you want to find out?', - second_column_statement: - 'You choose the conditions, response keys, and trials.', - second_column_question: '', - links: [], - }, - protocol: { - title: 'What participants are shown', - protocol: - 'Participants see one stimulus at a time and press the key you assigned to its condition.', - condition_first_img: '', - condition_first_title: 'Condition 1', - condition_first: 'Press the assigned key for Condition 1', - condition_second_img: '', - condition_second_title: 'Condition 2', - condition_second: 'Press the assigned key for Condition 2', - pacing: 'Go at a comfortable, steady pace.', - }, - expectedKeys: [ - { label: 'Condition 1', key: '1' }, - { label: 'Condition 2', key: '9' }, + responses: [ + { + key: 'b', + label: 'Orange T is there', + stimulus: { src: orangeT, alt: 'Letters with one right-side-up orange T' }, + }, + { + key: 'n', + label: 'No orange T', + stimulus: { src: noOrangeT, alt: 'Letters with no right-side-up orange T' }, + }, ], - flow: buildFlow({ nbTrials: 0 }), -}; - -export const IMPORTED: PrepareFixture & { file: string } = { - heading: 'Imported', - overview: { - title: 'Imported Study', - overview: - 'This experiment was written outside BrainWaves. BrainWaves runs it and records the responses.', - links: [], - }, - background: { - first_column_statement: - 'Imported studies keep their own instructions and timing.', - definition_title: 'Markers', - first_column_question: - 'Name the conditions in the Markers tab so BrainWaves can write EEG markers.', - second_column_statement: - 'If you do not add conditions, the study runs behavior-only.', - second_column_question: '', - links: [], - }, - protocol: { - title: 'What participants are shown', - protocol: - 'The study file controls the trial order, stimuli, and instructions shown to participants.', - condition_first_img: '', - condition_first_title: 'Condition A', - condition_first: 'Declared condition A', - condition_second_img: '', - condition_second_title: 'Condition B', - condition_second: 'Declared condition B', - pacing: undefined, - }, - expectedKeys: [ - { label: 'Condition A', key: 'f' }, - { label: 'Condition B', key: 'j' }, - ], - flow: [ - { label: 'Study instructions' }, - { label: 'Trials' }, - { label: 'Completion' }, - ], - file: 'my-study.js', + flow: buildFlow({ nbTrials: searchParams.nbTrials }), }; /** Shared no-op callbacks so stories do not need to supply handlers. */ diff --git a/src/renderer/components/PreviewLabel.stories.tsx b/src/renderer/components/PreviewLabel.stories.tsx new file mode 100644 index 00000000..6cb8700e --- /dev/null +++ b/src/renderer/components/PreviewLabel.stories.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import PreviewLabel from './PreviewLabel'; +import { Button } from './ui/button'; + +const meta: Meta = { + title: 'Domain/PreviewLabel', + component: PreviewLabel, + decorators: [ + (Story) => ( +
      + +
      + ), + ], +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; + +/** How it sits beside the preview toggle, as in PrepareSteps and (at integration) PreviewButtonComponent. */ +export const BesideStopPreview: Story = { + render: () => ( +
      + + +
      + ), +}; diff --git a/src/renderer/components/PreviewLabel.tsx b/src/renderer/components/PreviewLabel.tsx new file mode 100644 index 00000000..b67d9d6a --- /dev/null +++ b/src/renderer/components/PreviewLabel.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +/** + * The app's one "preview, not recording" status. Same vocabulary as RunBar's + * run status (glyph + bold uppercase label + muted detail), with an outline eye + * glyph distinct from the red EEG dot and the square behavior-only glyph. + */ +export default function PreviewLabel() { + return ( +
      + + + + + + Preview + {' · not recording'} + +
      + ); +} From a3ea44023928bcc8efcb9ebd2b19774384f1c596 Mon Sep 17 00:00:00 2001 From: jdpigeon Date: Thu, 24 Sep 2026 16:37:00 -0400 Subject: [PATCH 5/5] design(ws4): real trial counts for Stroop (8/96) and Search (8/80); matching workspaces in their Protocol stories --- .../components/PrepareSteps/PrepareSteps.stories.tsx | 12 +++++++++++- src/renderer/components/PrepareSteps/fixtures.ts | 8 ++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx index 4f4bd9bf..cb4e2d74 100644 --- a/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx +++ b/src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx @@ -24,6 +24,14 @@ const behaviorWorkspace = { modality: 'behavior' as const, }; +const stroopWorkspace = { ...behaviorWorkspace, modality: 'eeg' as const }; + +const searchWorkspace = { + name: 'Visual_Search_1', + experimentType: 'Visual Search', + modality: 'eeg' as const, +}; + /** * Wrap the story in the real AppShell chrome at Prepare, with Collect * recommended. This makes the local stepper visually subordinate to the @@ -104,13 +112,15 @@ export const Protocol: Story = { render: () => , }; -/** P04 — Stroop protocol: four ink colors, four keys; no practice phase in its params. */ +/** P04 — Stroop protocol: four ink colors, four keys; 8 practice + 96 recorded trials. */ export const ProtocolStroop: Story = { + parameters: { workspace: stroopWorkspace }, render: () => , }; /** P05 — Visual Search protocol: target present / absent on b / n. */ export const ProtocolSearch: Story = { + parameters: { workspace: searchWorkspace }, render: () => , }; diff --git a/src/renderer/components/PrepareSteps/fixtures.ts b/src/renderer/components/PrepareSteps/fixtures.ts index da060fa7..49581cc8 100644 --- a/src/renderer/components/PrepareSteps/fixtures.ts +++ b/src/renderer/components/PrepareSteps/fixtures.ts @@ -6,12 +6,10 @@ import { params as facesParams } from '../../experiments/faces_houses/params'; import { overview as stroopOverview } from '../../experiments/stroop/content_overview'; import { background as stroopBackground } from '../../experiments/stroop/content_background'; import { protocol as stroopProtocol } from '../../experiments/stroop/content_protocol'; -import { params as stroopParams } from '../../experiments/stroop/params'; import { overview as searchOverview } from '../../experiments/search/content_overview'; import { background as searchBackground } from '../../experiments/search/content_background'; import { protocol as searchProtocol } from '../../experiments/search/content_protocol'; -import { params as searchParams } from '../../experiments/search/params'; import facesIcon from '../../experiments/faces_houses/icon.png'; import face from '../../experiments/faces_houses/stimuli/faces/Face1.jpg'; @@ -76,7 +74,8 @@ export const STROOP: PrepareFixture = { { key: 'b', label: 'Blue ink', stimulus: { word: 'yellow', color: 'blue' } }, { key: 'y', label: 'Yellow ink', stimulus: { word: 'red', color: '#ffe32a' } }, ], - flow: buildFlow({ nbTrials: stroopParams.nbTrials }), + // stroop/experiment.ts: 'Practice task' loop has 8 templateParameters (~l.99); 'Stroop task' samples n: '96' (~l.484). params.nbTrials is unused. + flow: buildFlow({ includePractice: true, nbPracticeTrials: 8, nbTrials: 96 }), }; /** Keys from search/experiment.ts (b = orange T present, n = absent). */ @@ -96,7 +95,8 @@ export const SEARCH: PrepareFixture = { stimulus: { src: noOrangeT, alt: 'Letters with no right-side-up orange T' }, }, ], - flow: buildFlow({ nbTrials: searchParams.nbTrials }), + // search/utils.ts: constructTrials makes 8 per block; practice = 1 block (l.81), main = 10 blocks (l.77). params.nbTrials is unused. + flow: buildFlow({ includePractice: true, nbPracticeTrials: 8, nbTrials: 80 }), }; /** Shared no-op callbacks so stories do not need to supply handlers. */