@adriansteffan/reactive 0.0.43 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +14 -1
- package/README.md +232 -3
- package/dist/{mod-D6W3wq3h.js → mod-D9lwPIrH.js} +6739 -6389
- package/dist/mod.d.ts +70 -22
- package/dist/reactive.es.js +46 -36
- package/dist/reactive.umd.js +40 -38
- package/dist/style.css +1 -1
- package/dist/{web-B1hJOwit.js → web-DUIQX1PV.js} +1 -1
- package/dist/{web-BYSmfdtR.js → web-DXP3LAJm.js} +1 -1
- package/package.json +1 -1
- package/src/components/canvasblock.tsx +125 -74
- package/src/components/checkdevice.tsx +18 -0
- package/src/components/enterfullscreen.tsx +7 -3
- package/src/components/exitfullscreen.tsx +6 -1
- package/src/components/experimentprovider.tsx +7 -2
- package/src/components/experimentrunner.tsx +85 -58
- package/src/components/microphonecheck.tsx +6 -1
- package/src/components/mobilefilepermission.tsx +3 -0
- package/src/components/plaininput.tsx +20 -0
- package/src/components/prolificending.tsx +5 -0
- package/src/components/quest.tsx +60 -0
- package/src/components/storeui.tsx +18 -11
- package/src/components/text.tsx +14 -0
- package/src/components/upload.tsx +69 -286
- package/src/index.css +0 -20
- package/src/mod.tsx +2 -0
- package/src/utils/bytecode.ts +61 -9
- package/src/utils/common.ts +4 -1
- package/src/utils/simulation.ts +269 -0
- package/src/utils/upload.ts +201 -0
- package/template/README.md +59 -0
- package/template/backend/package-lock.json +280 -156
- package/template/backend/src/backend.ts +1 -0
- package/template/package-lock.json +1693 -771
- package/template/package.json +2 -0
- package/template/simulate.ts +15 -0
- package/template/src/Experiment.tsx +62 -5
- package/template/src/main.tsx +1 -1
- package/template/tsconfig.json +2 -3
- package/tsconfig.json +1 -0
- package/vite.config.ts +1 -1
|
@@ -5,7 +5,20 @@
|
|
|
5
5
|
"Bash(npm run:*)",
|
|
6
6
|
"WebSearch",
|
|
7
7
|
"WebFetch(domain:github.com)",
|
|
8
|
-
"WebFetch(domain:surveyjs.io)"
|
|
8
|
+
"WebFetch(domain:surveyjs.io)",
|
|
9
|
+
"Read(//Users/adriansteffan/Projects/rtest/**)",
|
|
10
|
+
"Bash(npm link:*)",
|
|
11
|
+
"Bash(npx tsc:*)",
|
|
12
|
+
"Bash(grep:*)",
|
|
13
|
+
"Bash(cp:*)",
|
|
14
|
+
"Bash(cd:*)",
|
|
15
|
+
"mcp__playwright__browser_take_screenshot",
|
|
16
|
+
"mcp__playwright__browser_evaluate",
|
|
17
|
+
"mcp__playwright__browser_run_code",
|
|
18
|
+
"Bash(npm test:*)",
|
|
19
|
+
"WebFetch(domain:raw.githubusercontent.com)",
|
|
20
|
+
"Bash(ls /Users/adriansteffan/Projects/reactive/README*)",
|
|
21
|
+
"Bash(ls /Users/adriansteffan/Projects/reactive/*.md)"
|
|
9
22
|
],
|
|
10
23
|
"deny": [],
|
|
11
24
|
"ask": []
|
package/README.md
CHANGED
|
@@ -42,6 +42,231 @@ Premade components available so far:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
## Simulation
|
|
46
|
+
|
|
47
|
+
Reactive includes a simulation system that lets you run experiments headlessly, generating synthetic data from simulated participants. This is useful for some basic computational modeling, verifying data pipelines, and sample size planning.
|
|
48
|
+
|
|
49
|
+
### Quick start
|
|
50
|
+
|
|
51
|
+
Define your participant generator in `Experiment.tsx`:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
export const simulationConfig = {
|
|
55
|
+
participants: {
|
|
56
|
+
generator: (i) => ({ id: i, nickname: `participant_${i}` }),
|
|
57
|
+
count: 10,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Run the simulation:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
npm run simulate
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This starts the backend, simulates all participants through the experiment, uploads data via the real backend (just like real participants would), and shuts down.
|
|
69
|
+
|
|
70
|
+
### How it works
|
|
71
|
+
|
|
72
|
+
Each built-in component (Text, Quest, CanvasBlock, Upload, etc.) registers a **simulate function** and **default simulators**. The simulate function contains the trial logic. The simulators are replaceable decision functions that model participant behavior at each interaction point.
|
|
73
|
+
|
|
74
|
+
For example, Quest's simulate function iterates through questions and calls `simulators.answerQuestion()` for each one. The default `answerQuestion` picks random valid answers. You can override it to model specific participant behavior.
|
|
75
|
+
|
|
76
|
+
### Overriding simulators on a trial
|
|
77
|
+
|
|
78
|
+
Add a `simulators` property to any timeline item to override specific decision functions:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
{
|
|
82
|
+
type: 'PlainInput',
|
|
83
|
+
props: { content: <p>What is your name?</p> },
|
|
84
|
+
simulators: {
|
|
85
|
+
respond: (_trialProps, participant) => ({
|
|
86
|
+
value: participant.nickname,
|
|
87
|
+
participantState: participant,
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The override is merged with the registered defaults — you only need to specify the decision functions you want to change.
|
|
94
|
+
|
|
95
|
+
### Custom components
|
|
96
|
+
|
|
97
|
+
Register a simulation for your custom components using `registerSimulation`:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
registerSimulation('MyTrial',
|
|
101
|
+
// Simulate function: uses shared trial logic + decision functions
|
|
102
|
+
(trialProps, experimentState, simulators, participant) => {
|
|
103
|
+
const choice = simulators.decide(trialProps, participant);
|
|
104
|
+
return { responseData: { choice: choice.value }, participantState: choice.participantState };
|
|
105
|
+
},
|
|
106
|
+
// Default simulators: one per decision point
|
|
107
|
+
{
|
|
108
|
+
decide: (_trialProps, participant) => ({
|
|
109
|
+
value: 'default_choice',
|
|
110
|
+
participantState: participant,
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The simulate function orchestrates the trial logic. The decision functions are the parts where a human would interact — these are what users override to model different participant behaviors.
|
|
117
|
+
|
|
118
|
+
### Hybrid mode
|
|
119
|
+
|
|
120
|
+
During development, you can auto-advance simulated trials while manually interacting with others. Add `?hybridSimulation=true` to the URL during development:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
http://localhost:5173?hybridSimulation=true
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Trials with `simulators` or `simulate: true` defined on them will auto-advance. Trials without them render normally for human interaction.
|
|
127
|
+
|
|
128
|
+
Hybrid mode is enabled by default during development. For production, set `VITE_DISABLE_HYBRID_SIMULATION=true` to disable it regardless of URL parameters.
|
|
129
|
+
|
|
130
|
+
### Built-in simulator decision functions
|
|
131
|
+
|
|
132
|
+
| Component | Decision functions | Default behavior |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| Text | `respond` | Click button, random reaction time |
|
|
135
|
+
| PlainInput | `respond` | Returns `'simulated_input'` |
|
|
136
|
+
| Quest | `answerQuestion` | Random valid answer per question type |
|
|
137
|
+
| CanvasBlock | `respondToSlide` | Random key from `allowedKeys`, random RT |
|
|
138
|
+
| Upload | *(none)* | Builds CSVs and POSTs to backend |
|
|
139
|
+
| StoreUI | *(none)* | Uses field default values |
|
|
140
|
+
| CheckDevice | *(none)* | Returns simulated device info |
|
|
141
|
+
| EnterFullscreen, ExitFullscreen, MicrophoneCheck, ProlificEnding, RequestFilePermission | *(none)* | No-op, advances immediately |
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
## Data Saving
|
|
145
|
+
|
|
146
|
+
Reactive automatically builds CSV files from experiment data using a registry-based system. Each component type registers a default CSV target, and the Upload component discovers these at the end of the experiment.
|
|
147
|
+
|
|
148
|
+
### How it works
|
|
149
|
+
|
|
150
|
+
Each component type registers where its data should go via `registerFlattener`:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
registerFlattener('PlainInput', 'session'); // merge into session CSV
|
|
154
|
+
registerFlattener('CanvasBlock', 'canvas', flattenFn); // own CSV with custom flattener
|
|
155
|
+
registerFlattener('ProlificEnding', null); // no CSV output
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Built-in components come pre-registered. The Upload component produces CSVs automatically with no props needed:
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
{ name: 'upload', type: 'Upload' }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Built-in defaults
|
|
165
|
+
|
|
166
|
+
| Component | Default CSV | Notes |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| PlainInput, Quest, CheckDevice, EnterFullscreen, ExitFullscreen, MicrophoneCheck | `session` | Merged into single session row, namespaced by trial name |
|
|
169
|
+
| Text | `text` | One row per Text component |
|
|
170
|
+
| CanvasBlock | `canvas` | One row per slide, with built-in flattener |
|
|
171
|
+
| StoreUI | `storeui` | One row per StoreUI occurrence |
|
|
172
|
+
| ProlificEnding, Upload | *(none)* | No CSV output |
|
|
173
|
+
|
|
174
|
+
### Output files
|
|
175
|
+
|
|
176
|
+
For a session `abc123`, the Upload component produces:
|
|
177
|
+
- `abc123.raw.json` — full raw data
|
|
178
|
+
- `session.abc123.{timestamp}.csv` — one row with params, userAgent, and all session-level trial data namespaced by trial name (e.g. `nickname_value`, `devicecheck_browser`)
|
|
179
|
+
- `canvas.abc123.{timestamp}.csv` — multi-row CSV from CanvasBlock trials
|
|
180
|
+
- One CSV per additional group (text, storeui, or any custom group)
|
|
181
|
+
|
|
182
|
+
### Per-item CSV override
|
|
183
|
+
|
|
184
|
+
Override the default target on any timeline item:
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
{ name: 'practice', type: 'CanvasBlock', csv: 'practice', ... } // separate from main canvas
|
|
188
|
+
{ name: 'main', type: 'CanvasBlock', ... } // uses default 'canvas'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Route a trial to multiple CSVs with an array:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
{ name: 'survey', type: 'Quest', csv: ['session', 'survey'], ... } // both session row and own file
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Adding session-level data
|
|
198
|
+
|
|
199
|
+
Use `sessionData` on Upload to inject extra fields into the session CSV:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
// Static
|
|
203
|
+
{
|
|
204
|
+
type: 'Upload',
|
|
205
|
+
props: {
|
|
206
|
+
sessionData: { group: 'control', experimentVersion: 2 },
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Dynamic (computed from store/data)
|
|
211
|
+
{
|
|
212
|
+
type: 'Upload',
|
|
213
|
+
props: (data, store) => ({
|
|
214
|
+
sessionData: { group: store.assignedGroup, condition: store.condition },
|
|
215
|
+
}),
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Custom flatteners
|
|
220
|
+
|
|
221
|
+
Register a flattener for custom components to control how `responseData` becomes CSV rows:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
registerFlattener('MyGame', 'games', (item) => {
|
|
225
|
+
return item.responseData.moves.map((move) => ({
|
|
226
|
+
moveType: move.type,
|
|
227
|
+
score: move.score,
|
|
228
|
+
}));
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Each row automatically gets standard trial fields prefixed with `trial_` (`trial_index`, `trial_name`, `trial_start`, etc.) plus any metadata from the timeline item. The flattener output overwrites these if keys collide.
|
|
233
|
+
|
|
234
|
+
### Multi-CSV components
|
|
235
|
+
|
|
236
|
+
Call `registerFlattener` multiple times for one component to produce multiple CSV files:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
registerFlattener('SportsGame', 'sports_actions', (item) => flattenActions(item.responseData));
|
|
240
|
+
registerFlattener('SportsGame', 'sports_players', (item) => flattenPlayers(item.responseData));
|
|
241
|
+
registerFlattener('SportsGame', 'sports_matches', (item) => flattenMatches(item.responseData));
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Upload props
|
|
245
|
+
|
|
246
|
+
| Prop | Type | Default | Description |
|
|
247
|
+
|---|---|---|---|
|
|
248
|
+
| `sessionID` | `string` | random UUID | Custom session identifier used in filenames and folder names |
|
|
249
|
+
| `sessionData` | `Record<string, any>` | — | Extra key-value pairs added to the session CSV row |
|
|
250
|
+
| `generateFiles` | `(sessionID, data, store) => FileUpload[]` | — | Produce custom files alongside auto-generated CSVs |
|
|
251
|
+
| `uploadRaw` | `boolean` | `true` | Include raw JSON dump of all trial data |
|
|
252
|
+
| `autoUpload` | `boolean` | `false` | Upload immediately on mount instead of showing a submit button |
|
|
253
|
+
|
|
254
|
+
### Metadata
|
|
255
|
+
|
|
256
|
+
Add `metadata` to timeline items to include extra columns in every CSV row that trial produces:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
{
|
|
260
|
+
name: 'block1',
|
|
261
|
+
type: 'CanvasBlock',
|
|
262
|
+
metadata: { difficulty: 'hard', block: 2 },
|
|
263
|
+
props: { ... },
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
For session-level items, metadata is namespaced by trial name (e.g. `block1_difficulty`). For non-session items, metadata columns appear unprefixed.
|
|
268
|
+
|
|
269
|
+
|
|
45
270
|
## Development
|
|
46
271
|
|
|
47
272
|
|
|
@@ -56,11 +281,15 @@ Then create a global link (only needs to run once during setup);
|
|
|
56
281
|
npm link
|
|
57
282
|
```
|
|
58
283
|
|
|
59
|
-
Then set up a local testing project:
|
|
284
|
+
Then set up a local testing project (run from the parent directory so it's created as a sibling):
|
|
60
285
|
|
|
61
286
|
```
|
|
62
|
-
|
|
63
|
-
|
|
287
|
+
cd ..
|
|
288
|
+
node reactive/bin/setup.js
|
|
289
|
+
cd <project-name>
|
|
290
|
+
npm pkg set dependencies.@adriansteffan/reactive="*"
|
|
291
|
+
npm i && npm i --prefix backend
|
|
292
|
+
npm link @adriansteffan/reactive
|
|
64
293
|
```
|
|
65
294
|
|
|
66
295
|
|