@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
@@ -47,6 +47,7 @@
47
47
  "dev": "vite",
48
48
  "backend": "npm run dev --prefix backend",
49
49
  "dev:all": "concurrently -n \"FRONT,BACK\" -c \"blue,green\" \"npm run dev\" \"npm run backend\"",
50
+ "simulate": "npx tsx simulate.ts",
50
51
  "build": "tsc && vite build",
51
52
  "test": "vitest --silent=false",
52
53
  "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
@@ -85,6 +86,7 @@
85
86
  "@typescript-eslint/parser": "^8.16.0",
86
87
  "@vitejs/plugin-react": "^4.2.1",
87
88
  "concurrently": "^9.1.0",
89
+ "tsx": "^4.19.2",
88
90
  "electron": "^35.0.0",
89
91
  "electron-builder": "^25.1.8",
90
92
  "electron-vite": "^3.0.0",
@@ -0,0 +1,15 @@
1
+ import { orchestrateSimulation, simulateParticipant, setBackendUrl } from '@adriansteffan/reactive';
2
+ import { experiment, simulationConfig } from './src/Experiment';
3
+
4
+ // Each simulated participant runs as a separate subprocess to get fresh module-level
5
+ // randomization (e.g., group assignment). The orchestrator spawns workers that re-run
6
+ // this script with _REACTIVE_WORKER_INDEX set.
7
+ if (process.env._REACTIVE_WORKER_INDEX) {
8
+ const index = parseInt(process.env._REACTIVE_WORKER_INDEX);
9
+ const participants = simulationConfig.participants;
10
+ const participant = Array.isArray(participants) ? participants[index] : participants.generator(index);
11
+ setBackendUrl(process.env._REACTIVE_BACKEND_URL!);
12
+ await simulateParticipant(experiment, participant);
13
+ } else {
14
+ await orchestrateSimulation(simulationConfig, import.meta.filename);
15
+ }
@@ -1,12 +1,15 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { useState } from 'react';
3
- import { ExperimentRunner, BaseComponentProps, ExperimentConfig } from '@adriansteffan/reactive';
2
+ import { useState, useRef } from 'react';
3
+ import { ExperimentRunner, BaseComponentProps, ExperimentConfig, registerSimulation, registerFlattener } from '@adriansteffan/reactive';
4
4
 
5
5
 
6
6
  const config: ExperimentConfig = { showProgressBar: true };
7
7
 
8
+ // --- Custom Components ---
9
+
8
10
  const CustomTrial = ({ next, maxCount }: BaseComponentProps & { maxCount: number }) => {
9
11
  const [count, setCount] = useState(0);
12
+ const startTime = useRef(performance.now());
10
13
 
11
14
  return (
12
15
  <>
@@ -20,7 +23,7 @@ const CustomTrial = ({ next, maxCount }: BaseComponentProps & { maxCount: number
20
23
  onClick={() => {
21
24
  setCount(count + 1);
22
25
  if (count + 1 === maxCount) {
23
- next({});
26
+ next({ totalTime: performance.now() - startTime.current, clicks: maxCount });
24
27
  }
25
28
  }}
26
29
  className='mt-4 px-4 py-2 bg-blue-500 text-white rounded-sm hover:bg-blue-600 transition-colors'
@@ -31,6 +34,28 @@ const CustomTrial = ({ next, maxCount }: BaseComponentProps & { maxCount: number
31
34
  );
32
35
  };
33
36
 
37
+ // Register a flattener to control how this trial's data appears in the CSV.
38
+ // 'customtrial' is the default CSV file name — override per-item with the csv field.
39
+ registerFlattener('CustomTrial', 'customtrial');
40
+
41
+ // Register a simulation for the custom trial.
42
+ // The decision function determines how fast the participant clicks.
43
+ // The simulate function uses the trial logic (clicking maxCount times) to produce response data.
44
+ registerSimulation('CustomTrial', (trialProps, _experimentState, simulators, participant) => {
45
+ let totalTime = 0;
46
+ for (let i = 0; i < (trialProps.maxCount || 1); i++) {
47
+ const result = simulators.click(trialProps, participant);
48
+ participant = result.participantState;
49
+ totalTime += result.value;
50
+ }
51
+ return { responseData: { totalTime, clicks: trialProps.maxCount }, participantState: participant, duration: totalTime };
52
+ }, {
53
+ click: (_trialProps: any, participant: any) => ({
54
+ value: 200 + Math.random() * 500,
55
+ participantState: participant,
56
+ }),
57
+ });
58
+
34
59
  const CustomQuestion = () => {
35
60
  return (
36
61
  <>
@@ -39,7 +64,9 @@ const CustomQuestion = () => {
39
64
  );
40
65
  };
41
66
 
42
- const experiment = [
67
+ // --- Timeline ---
68
+
69
+ export const experiment = [
43
70
  {
44
71
  name: 'introtext',
45
72
  type: 'Text',
@@ -57,9 +84,25 @@ const experiment = [
57
84
  ),
58
85
  },
59
86
  },
87
+ {
88
+ name: 'nickname',
89
+ type: 'PlainInput',
90
+ props: {
91
+ content: <p>What is your nickname?</p>,
92
+ buttonText: 'Submit',
93
+ placeholder: 'Enter your nickname',
94
+ },
95
+ simulators: {
96
+ respond: (_trialProps: any, participant: any) => ({
97
+ value: participant.nickname,
98
+ participantState: participant,
99
+ }),
100
+ },
101
+ },
60
102
  {
61
103
  name: 'customtrial',
62
104
  type: 'CustomTrial',
105
+ simulate: true,
63
106
  props: {
64
107
  maxCount: 5,
65
108
  },
@@ -115,6 +158,20 @@ export default function Experiment() {
115
158
  timeline={experiment}
116
159
  components={{CustomTrial}}
117
160
  questions={{CustomQuestion}}
161
+ hybridParticipant={{ id: 0, nickname: 'test' }}
118
162
  />
119
163
  );
120
- }
164
+ }
165
+
166
+ // --- Simulation config ---
167
+ // Define how simulated participants are generated.
168
+ // Each participant is an object whose properties are available in simulator decision functions.
169
+ export const simulationConfig = {
170
+ participants: {
171
+ generator: (i: number) => ({
172
+ id: i,
173
+ nickname: `participant_${i}`,
174
+ }),
175
+ count: 10,
176
+ },
177
+ };
@@ -6,7 +6,7 @@ import { ExperimentProvider } from "@adriansteffan/reactive";
6
6
 
7
7
  ReactDOM.createRoot(document.getElementById("root")!).render(
8
8
  <React.StrictMode>
9
- <ExperimentProvider disableSettings={import.meta.env.VITE_DISABLE_SETTINGS}>
9
+ <ExperimentProvider disableSettings={import.meta.env.VITE_DISABLE_SETTINGS} disableHybridSimulation={!!import.meta.env.VITE_DISABLE_HYBRID_SIMULATION}>
10
10
  <Experiment />
11
11
  </ExperimentProvider>
12
12
  </React.StrictMode>
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "noImplicitAny": false,
4
- "target": "ES5",
4
+ "target": "ES2020",
5
5
  "useDefineForClassFields": true,
6
- "lib": ["ES2020", "DOM", "DOM.Iterable", "ES2015", "ES2016", "ES2017"],
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
7
  "module": "ESNext",
8
- "downlevelIteration": true,
9
8
  "skipLibCheck": true,
10
9
 
11
10
  /* Bundler mode */
package/tsconfig.json CHANGED
@@ -4,6 +4,7 @@
4
4
  "target": "ES2020",
5
5
  "useDefineForClassFields": true,
6
6
  "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "types": ["node"],
7
8
  "module": "ESNext",
8
9
  "skipLibCheck": true,
9
10
  "declaration": true,
package/vite.config.ts CHANGED
@@ -26,7 +26,7 @@ export default defineConfig(() => {
26
26
  fileName: (format: string) => `reactive.${format}.js`,
27
27
  },
28
28
  rollupOptions: {
29
- external: ['react', 'react-dom'],
29
+ external: ['react', 'react-dom', 'child_process', 'os'],
30
30
  output: {
31
31
  globals: {
32
32
  react: 'React',