Component-first architecture
- Everything is a component: pages compose components.
- Special features are components: matchmaking, chat, live workspaces, and AI agents ship as first‑class components.
- Hierarchies and groups: components can nest. A component group wraps related subcomponents and exposes a concise config.
- Survey: a
Surveygroup manages a sequence ofSurveyQuestioncomponents, including paging and validation. - ButtonGroup: coordinates multiple
Buttoncomponents with shared behavior. - Declarative YAML: declare components, their props, and layout. YAML mirrors JSX structure.
- Chat and agents:
- A
chatcomponent can run solo. - Attach an AI agent by listing it in
props.agents. - Or use
matchmakingto assign agroupId, then join chat rooms shared by that group. -
Live workspaces: a
live-workspacecomponent plugs into pages like any other component. -
Events: all interactive components automatically emit structured events for research data collection. Configure custom event metadata to capture experiment-specific details.
- Button clicks, survey submissions, media interactions, matchmaking lifecycle, chat messages, and form changes all generate trackable events.
- Events include standard metadata (sessionId, pageId, componentId) plus configurable custom data.
- See Event Hooks below for the full list.
Result: a consistent, declarative system where pages stay lightweight and features drop in as components.
Conditional Rendering
Any component supports a when property to conditionally show or hide it based on session_state:
Components without when always render. See Expressions for the full syntax.
Available Components
| Component | Description |
|---|---|
| Text | Display text and markdown content |
| Buttons | Navigation and action buttons |
| Survey | Multi-question forms with validation |
| Form | General-purpose form fields |
| Chat | Real-time messaging |
| Matchmaking | Group participants together |
| Randomization | Assign treatment conditions |
| Agents | Server-hosted AI agents |
| Media | Images, videos, and audio |
| Timer | Countdown timer with auto-navigation |
| Workspace | Collaborative workspaces |
| Custom | Mount custom React components |
Event Hooks
Every interactive component can emit structured events. Use the events key in your config to attach custom metadata.
| Component | Events |
|---|---|
| buttons | onClick |
| survey | onSubmit (automatic) |
| media | onPlay, onPause, onSeek, onComplete, onError |
| matchmaking | onRequestStart, onMatchFound, onTimeout, onCancel |
| chat | onMessageSend, onMessageReceive |
| timer | onStart, onWarning, onExpiry |
| live-workspace | onEdit |
Runtime adapters
- Each interactive component owns a
*.runtime.tsadapter colocated with its UI module. Example:apps/lab/app/src/components/ui/Button/runtime.tsxregisters the button runtime wrapper and handles event submission. - UI modules and their adapters live in PascalCase directories (
Button/Button.tsx,Button/runtime.tsx) so the filesystem mirrors React component naming and stays stable on case-insensitive hosts. - Adapters call
defineRuntimeComponent()(seeapps/lab/app/src/runtime/define-runtime-component.ts) to register renderers with the runtime registry without pulling registry code into UI files. apps/lab/app/src/components/runtime.tsimports every adapter for side effects so the bundle includes all registrations. Adding a new component means updating its UI module, creating the matching runtime adapter, then adding one import line to this manifest.- Component-owned normalization or validation can live inside the adapter by extending the returned object, keeping the central runtime agnostic of component specifics.