design(WS3): Explore EEG Storybook pass - #282
Merged
Merged
Conversation
…tions, tick fallback, segment review, rainbow traces
…ntro headline, plain-language copy, accurate blink label
…ble rainbow colors
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Design pass for Workstream 3 (Explore EEG), following
docs/uxr/2026-09-25-ws3-design-brief.md(copied into this branch) and plan §5, §10.2 and §11 WS3. This is Storybook only; runtime behavior does not change.EEGExplorationComponent.tsx,ExploreLessonFlow.tsx,ViewerComponent,EEGViewer.js, the explore constants andexploreSignal.tsare untouched. Engineering integrates after product approves.Round 2 applies the review decisions: a lighter Ready state, the real head diagram in place of the text sensor list, device legend, quiz-style prediction options with immediate feedback, the detection-time tick fallback, a segment-based review plot, the rainbow trace colors, and copy fixes throughout.
Layout decision
ExploreSurface): the overall status sits above the plot. The left column (320px) renders the existing signal-quality design unmodified:SignalQualityIndicatorComponentat its real height 250 with hover/click, plusExploreSensorCard, sharinghoveredChannel/onHoveredChannelChangeexactly asEEGExplorationComponent.tsx:106-121wires them (hover state is story-localuseState). Both are fed by the fixture observable/sample. The right column holds the live plot and the two lesson cards. Nothing scrolls.BlinkLessonView,EyesClosedView): instruction and expected action sit in a 340px step panel beside the plot (plan §5.2), with progress pips and Exit/Back/Next local to the lesson — Analyze's walkthrough idiom turned vertical. The copy region isoverflow-y-autoonly as a safety net; it fits at both sizes without scrolling (numbers below).outline-brand. Landing:Connect a headset. Lesson picker: two equal outlinedStartbuttons (reviewed decision).The live plot is a webview — fixture stand-in and how it maps
ViewerComponent/EEGViewerrender inside an Electron<webview>and cannot run in Storybook, soFixturePlot(React SVG) stands in. It consumes exactly the real viewer's data contract —EEGSnapshotandPlotAnnotation[]fromshared/eegVizTypes, the same propsViewerComponenttakes (snapshot,annotations,amplitudeScale) — so integration swaps one component for the other:axisLeftstyle), time running left→right with the offset axis and whole-second ticks (-5s … 0s), exactlybuildTimeAxis.TONE_STYLEScopied verbatim for the blink bands (gold, dashed edges, start pill ending at the band's left edge, pills skipped under 40px width). Two deliberate deltas: end pills are drawn just inside the plot's bottom edge instead of atplotHeight + 6(where the real viewer collides with the tick labels), andmarkerStyle="tick"adds the fallback detection-time bar described below. Integration should adopt both.getBBox), and the stand-in scales uniformly into its card (viewBox) instead of re-laying-out to the exact box.The traces are a synthetic fixture (seeded PRNG in
fixtures.ts): 10 Hz / 5 Hz background rhythms, sensor noise, blink humps weighted to AF7/AF8, and a steady back-of-head rhythm weighted to TP9/TP10 while the eyes are closed. Not recorded data; story descriptions say "example", UI copy reads "your signal", same convention as #277.Blink marker viability — what the detector actually outputs
ExploreSession.detect()(utils/eeg/exploreSignal.ts) emitsBlinkEventwith both onset and offset:startTimeis the threshold crossing on the bilateral low-passed channel pair,endTimeis the return-to-baseline crossing (capped at 400 ms), and accepted events are gated to 100–400 ms duration with a 250 ms refractory. So the detector supplies a true interval, not just an instant.markerStyle="band", the default): the interval maps directly onto the annotation band the real viewer already draws.markerStyle="tick", storyBlinkTickMarker): a 3px gold bar at the detection instant, for sources that can only report one. Both styles are fixture-side; the real viewer keeps drawing bands.Review segments — back-of-envelope math
The review plot shows an eyes-open segment above an eyes-closed segment of equal length (3 s), both on one ±50 µV scale. Typical Muse amplitudes: eyes open posterior ≈ 5–15 µV peak-to-peak; eyes closed the posterior rhythm runs ≈ 15–45 µV ptp (2–3× eyes open). Rendered geometry measured in Chromium:
At ~1 µV/px a 20 µV rhythm is ~20 px tall (visible even at 10 µV ≈ 9 px), and with 3 s segments a 10 Hz cycle spans ~26–29 px — clearly an oscillation at the viewer's 1.75 px stroke, with real EEG, not just the fixture. A 10 s segment compresses the cycle to ~8 px (mush at this stroke); 2 s would give ~39–43 px. Chose 3 s: enough cycles to read as a rhythm, wide enough to feel like a moment of the interval. The fixture segments carry the same amplitudes (eyes open ~17 µV ptp, eyes closed ~35–45 µV ptp).
Trace colors:
d3.interpolateRainbow, keyed by channelutils/eeg/traceColors.tsexportschannelColor(name, allChannels): the rainbow sampled atallChannels.indexOf(name) / allChannels.length. The scale is built from the device's full channel list, so a channel's color never changes when a view down-selects — AF7/AF8 in the two-channel blink views keep the exact colors they have in the four-channel plot, and the review strips and theExamplekeep TP9/TP10's. Every plot passes the full device list.Legibility on white was checked (contrast: purple 7.05:1, red 2.99:1, teal 2.10:1, yellow-green 1.36:1); the user signed off on the contrast, so the sampling stays exactly
index / n.Stories
Domain/Explore, all in the real AppShell with no workspace (Explore is workspace-free):Connect a headset; the once-connected list is removed) · Waiting (connected, no data: explicit waiting state, lessons disabled until signal arrives)Plot rangecontrol to play with) · BlinkTickMarker (the detection-time fallback marker) · BlinkNotDetected (lesson continues gracefully)Begin;Examplemini-plot) · EyesClosedCountdown (visible 3–2–1, reduced-motion safe) · EyesClosedInterval · EyesClosedEndCue ("Open your eyes. Let's look at your brainwaves.") · EyesClosedReview (equal segments on one scale + the measured comparison)Examplereference as a small line plot in a dashed card, TP9/TP10 named as Muse's look at the back of the head. NoEffect is a valid, encouraging outcome.Measurements
All 25 stories × 2 sizes verified after the round-4 fixes (50 PNGs in
/tmp/ws3-design/{1366x768,1280x720}/, raw numbers in/tmp/ws3-design/measurements.json):document.scrollingElement.scrollHeightvsclientHeight): 768/768 at 1366×768 and 720/720 at 1280×720 on every story. The app's content container (flex-1 overflow-y-auto): 704/704 and 656/656 on every story — zero overflow, zero tolerance (an earlier check with a +2px tolerance missed a real 2px overflow on StreamError; that tolerance is gone).<header>, no bare<main>.FixturePlotfor any future end labels.AF7electrode shows the sensor card's location help — tooltip first lineAF7 · Left forehead, present at both 1366×768 and 1280×720.Review agenda
Open questions
i / nas ordered, or adopt(i + 0.5) / n/ darken the yellow-green samples?±150 µV/±50 µV) implements the "playing with the plot range" idea. Keep both ranges, or add zoom in/out like the real viewer?Constraints not met / notes for integration
FixturePlotis a stand-in (mapping above). Integration swaps it forViewerComponentand keeps the surrounding cards.traceColors.tsimportsinterpolateRainbowfromd3(installed); d3 ships no types and@types/d3is not installed — the import is untyped undernoImplicitAny: false. Adding@types/d3later would type it (needs a package.json change, out of scope here).ExploreLessonFlow's live annotation still says "eye muscle".app.global.cssgained only new scoped classes in round 1 (.explore-countdown-*, reduced-motion guarded). Round 2 touched no CSS. No existing shared class changed. No new dependencies;package.json/lockfile untouched.deviceInfo.channels), and each channel always keeps its color.Verification
npx tsc --noEmit: 0 errors./tmp/ws3-design/; measurements as above. Raw numbers:/tmp/ws3-design/measurements.json.