@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.
Files changed (41) hide show
  1. package/.claude/settings.local.json +14 -1
  2. package/README.md +232 -3
  3. package/dist/{mod-D6W3wq3h.js → mod-D9lwPIrH.js} +6739 -6389
  4. package/dist/mod.d.ts +70 -22
  5. package/dist/reactive.es.js +46 -36
  6. package/dist/reactive.umd.js +40 -38
  7. package/dist/style.css +1 -1
  8. package/dist/{web-B1hJOwit.js → web-DUIQX1PV.js} +1 -1
  9. package/dist/{web-BYSmfdtR.js → web-DXP3LAJm.js} +1 -1
  10. package/package.json +1 -1
  11. package/src/components/canvasblock.tsx +125 -74
  12. package/src/components/checkdevice.tsx +18 -0
  13. package/src/components/enterfullscreen.tsx +7 -3
  14. package/src/components/exitfullscreen.tsx +6 -1
  15. package/src/components/experimentprovider.tsx +7 -2
  16. package/src/components/experimentrunner.tsx +85 -58
  17. package/src/components/microphonecheck.tsx +6 -1
  18. package/src/components/mobilefilepermission.tsx +3 -0
  19. package/src/components/plaininput.tsx +20 -0
  20. package/src/components/prolificending.tsx +5 -0
  21. package/src/components/quest.tsx +60 -0
  22. package/src/components/storeui.tsx +18 -11
  23. package/src/components/text.tsx +14 -0
  24. package/src/components/upload.tsx +69 -286
  25. package/src/index.css +0 -20
  26. package/src/mod.tsx +2 -0
  27. package/src/utils/bytecode.ts +61 -9
  28. package/src/utils/common.ts +4 -1
  29. package/src/utils/simulation.ts +269 -0
  30. package/src/utils/upload.ts +201 -0
  31. package/template/README.md +59 -0
  32. package/template/backend/package-lock.json +280 -156
  33. package/template/backend/src/backend.ts +1 -0
  34. package/template/package-lock.json +1693 -771
  35. package/template/package.json +2 -0
  36. package/template/simulate.ts +15 -0
  37. package/template/src/Experiment.tsx +62 -5
  38. package/template/src/main.tsx +1 -1
  39. package/template/tsconfig.json +2 -3
  40. package/tsconfig.json +1 -0
  41. 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
- npx @adriansteffan/reactive
63
- npm uninstall @adriansteffan/reactive && npm link @adriansteffan/reactive
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