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
59 changes: 59 additions & 0 deletions docs/uxr/2026-09-24-ws4-design-brief.md
Original file line number Diff line number Diff line change
@@ -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/<name>/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 `<main>/<header>`; 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.
180 changes: 180 additions & 0 deletions src/renderer/components/PrepareSteps/PrepareSteps.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import React, { useState } from 'react';
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 {
FACES_HOUSES,
NOOP_HANDLERS,
PrepareFixture,
SACKS_STAND_IN,
SEARCH,
STROOP,
} from './fixtures';

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 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
* global gold-underline workflow bar.
*/
const withPrepareChrome: Decorator = (Story, { parameters }) => (
<AppShell
location="prepare"
workspace={parameters.workspace ?? workspace}
nextArea="collect"
device={parameters.device ?? 'connected'}
deviceName="Muse 2"
>
<Story />
</AppShell>
);

const meta: Meta<typeof PrepareSteps> = {
title: 'Domain/PrepareSteps',
component: PrepareSteps,
parameters: { layout: 'fullscreen' },
decorators: [withPrepareChrome],
args: {
...FACES_HOUSES,
isPreviewing: false,
hasPreviewed: false,
onStep: fn(),
onCollect: fn(),
onPreviewStart: fn(),
onPreviewStop: fn(),
onPreviewAgain: fn(),
},
};
export default meta;
type Story = StoryObj<typeof PrepareSteps>;

/** Clickable stepper and preview state, so a reviewer can walk the whole lesson from any story. */
function InteractiveStep({
initialStep,
fixture = FACES_HOUSES,
}: {
initialStep: PrepareStepId;
fixture?: PrepareFixture;
}) {
const [step, setStep] = useState<PrepareStepId>(initialStep);
const [isPreviewing, setIsPreviewing] = useState(false);
const [hasPreviewed, setHasPreviewed] = useState(false);
return (
<PrepareSteps
{...fixture}
step={step}
isPreviewing={isPreviewing}
hasPreviewed={hasPreviewed}
onStep={setStep}
onCollect={() => {}}
onPreviewStart={() => {
setIsPreviewing(true);
setHasPreviewed(true);
}}
onPreviewStop={() => setIsPreviewing(false)}
onPreviewAgain={() => setIsPreviewing(true)}
/>
);
}

/** P01 — Overview. Gold current-step pill in the secondary bar; one forward action to Background. */
export const Overview: Story = {
render: () => <InteractiveStep initialStep="overview" />,
};

/** P02 — Background. Centered lesson column with Back and Next: Protocol. */
export const Background: Story = {
render: () => <InteractiveStep initialStep="background" />,
};

/** P03 — Protocol. Static stimulus → key diagram beside a vertical task timeline generated from `flow`. */
export const Protocol: Story = {
render: () => <InteractiveStep initialStep="protocol" />,
};

/** P04 — Stroop protocol: four ink colors, four keys; 8 practice + 96 recorded trials. */
export const ProtocolStroop: Story = {
parameters: { workspace: stroopWorkspace },
render: () => <InteractiveStep initialStep="protocol" fixture={STROOP} />,
};

/** P05 — Visual Search protocol: target present / absent on b / n. */
export const ProtocolSearch: Story = {
parameters: { workspace: searchWorkspace },
render: () => <InteractiveStep initialStep="protocol" fixture={SEARCH} />,
};

/** P06 — PreviewStopped. Nothing recorded; Try the experiment is primary. */
export const PreviewStopped: Story = {
render: () => <InteractiveStep initialStep="preview" />,
};

/** P07 — PreviewRunning. Shared PreviewLabel beside Stop preview; expected keys under the experiment area. */
export const PreviewRunning: Story = {
render: () => (
<PrepareSteps
{...FACES_HOUSES}
{...NOOP_HANDLERS}
step="preview"
isPreviewing
hasPreviewed
/>
),
};

/** P08 — PreviewFinished. Run & record is primary; Preview again secondary. */
export const PreviewFinished: Story = {
render: () => (
<PrepareSteps
{...FACES_HOUSES}
{...NOOP_HANDLERS}
step="preview"
isPreviewing={false}
hasPreviewed
/>
),
};

/**
* 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: () => <InteractiveStep initialStep="overview" fixture={STROOP} />,
};

/** 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: () => (
<PrepareSteps
{...FACES_HOUSES}
{...NOOP_HANDLERS}
step="background"
mediaFallback={SACKS_STAND_IN}
isPreviewing={false}
hasPreviewed={false}
/>
),
};
Loading
Loading