@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
package/template/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
+
};
|
package/template/src/main.tsx
CHANGED
|
@@ -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>
|
package/template/tsconfig.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"noImplicitAny": false,
|
|
4
|
-
"target": "
|
|
4
|
+
"target": "ES2020",
|
|
5
5
|
"useDefineForClassFields": true,
|
|
6
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"
|
|
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
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',
|