@adriansteffan/reactive 0.0.26 → 0.0.28
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/dist/{mod-Dqf5zajq.js → mod-CjZm1Ta9.js} +11254 -10012
- package/dist/mod.d.ts +199 -49
- package/dist/reactive.es.js +27 -15
- package/dist/reactive.umd.js +37 -39
- package/dist/style.css +1 -1
- package/dist/{web-6wmUWZwq.js → web-B_I1xy51.js} +1 -1
- package/dist/{web-CnAMKrLX.js → web-DfpITFBR.js} +1 -1
- package/package.json +7 -2
- package/src/components/canvasblock.tsx +519 -0
- package/src/components/checkdevice.tsx +158 -0
- package/src/components/enterfullscreen.tsx +114 -31
- package/src/components/exitfullscreen.tsx +98 -21
- package/src/components/experimentprovider.tsx +34 -20
- package/src/components/experimentrunner.tsx +387 -0
- package/src/components/index.ts +13 -0
- package/src/components/mobilefilepermission.tsx +12 -19
- package/src/components/plaininput.tsx +7 -8
- package/src/components/prolificending.tsx +10 -4
- package/src/components/quest.tsx +27 -31
- package/src/components/settingsscreen.tsx +770 -0
- package/src/components/text.tsx +48 -3
- package/src/components/upload.tsx +218 -47
- package/src/mod.tsx +3 -12
- package/src/types/array.d.ts +6 -0
- package/src/utils/array.ts +113 -0
- package/src/utils/bytecode.ts +178 -0
- package/src/utils/common.ts +170 -39
- package/template/.env.template +2 -1
- package/template/src/{App.tsx → Experiment.tsx} +4 -4
- package/template/src/main.tsx +4 -4
- package/template/tsconfig.json +1 -0
- package/src/components/experiment.tsx +0 -371
|
@@ -1,371 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
|
|
3
|
-
import { ExperimentConfig, now, Store, TrialData } from '../utils/common';
|
|
4
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
-
import { ComponentType } from 'react';
|
|
6
|
-
|
|
7
|
-
// Default components
|
|
8
|
-
import Upload from './upload';
|
|
9
|
-
import Text from './text';
|
|
10
|
-
import PlainInput from './plaininput';
|
|
11
|
-
import ProlificEnding from './prolificending';
|
|
12
|
-
import Quest from './quest';
|
|
13
|
-
import EnterFullscreen from './enterfullscreen';
|
|
14
|
-
import ExitFullscreen from './exitfullscreen';
|
|
15
|
-
import MicrophoneCheck from './microphonecheck';
|
|
16
|
-
import RequestFilePermission from './mobilefilepermission';
|
|
17
|
-
|
|
18
|
-
// Default Custom Questions
|
|
19
|
-
import VoicerecorderQuestionComponent from './voicerecorder';
|
|
20
|
-
|
|
21
|
-
type ComponentsMap = {
|
|
22
|
-
[key: string]: ComponentType<any>;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// Default components map
|
|
26
|
-
const defaultComponents: ComponentsMap = {
|
|
27
|
-
Text,
|
|
28
|
-
ProlificEnding,
|
|
29
|
-
EnterFullscreen,
|
|
30
|
-
ExitFullscreen,
|
|
31
|
-
Quest,
|
|
32
|
-
Upload,
|
|
33
|
-
MicrophoneCheck,
|
|
34
|
-
PlainInput,
|
|
35
|
-
RequestFilePermission
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const defaultCustomQuestions = {
|
|
39
|
-
voicerecorder: VoicerecorderQuestionComponent,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
interface ComponentTrial {
|
|
43
|
-
name: string;
|
|
44
|
-
type: string;
|
|
45
|
-
props?: Record<string, any> | ((store: Store, data: TrialData[]) => Record<string, any>);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// The | string parts need some refactoring in the future, but right now this prevents the consumer from having to write "as const" behind every type
|
|
50
|
-
|
|
51
|
-
interface MarkerTrial {
|
|
52
|
-
type: 'MARKER' | string;
|
|
53
|
-
id: string;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface IfGotoTrial {
|
|
57
|
-
type: 'IF_GOTO' | string;
|
|
58
|
-
cond: (store: Store, data: TrialData[]) => boolean;
|
|
59
|
-
marker: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface UpdateStoreTrial {
|
|
63
|
-
type: 'UPDATE_STORE' | string;
|
|
64
|
-
fun: (store: Store, data: TrialData[]) => Store;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
interface IfBlockTrial {
|
|
68
|
-
type: 'IF_BLOCK' | string;
|
|
69
|
-
cond: (store: Store, data: TrialData[]) => boolean;
|
|
70
|
-
timeline: ExperimentTrial[];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
interface WhileBlockTrial {
|
|
74
|
-
type: 'WHILE_BLOCK' | string;
|
|
75
|
-
cond: (store: Store, data: TrialData[]) => boolean;
|
|
76
|
-
timeline: ExperimentTrial[];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
type ExperimentTrial =
|
|
80
|
-
| MarkerTrial
|
|
81
|
-
| IfGotoTrial
|
|
82
|
-
| UpdateStoreTrial
|
|
83
|
-
| IfBlockTrial
|
|
84
|
-
| WhileBlockTrial
|
|
85
|
-
| ComponentTrial;
|
|
86
|
-
|
|
87
|
-
interface ComponentInstruction {
|
|
88
|
-
type: 'Component';
|
|
89
|
-
content: ComponentTrial;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface IfGotoInstruction {
|
|
93
|
-
type: 'IfGoto';
|
|
94
|
-
cond: (store: Store, data: TrialData[]) => boolean;
|
|
95
|
-
marker: string;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface UpdateStoreInstruction {
|
|
99
|
-
type: 'UpdateStore';
|
|
100
|
-
fun: (store: Store, data: TrialData[]) => Store;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
type BytecodeInstruction = ComponentInstruction | IfGotoInstruction | UpdateStoreInstruction;
|
|
104
|
-
|
|
105
|
-
const renderComponentTrial = (
|
|
106
|
-
componentTrial: ComponentTrial,
|
|
107
|
-
key: number,
|
|
108
|
-
next: (data: any) => void,
|
|
109
|
-
updateStore: (update: Store) => void,
|
|
110
|
-
data: any,
|
|
111
|
-
componentsMap: ComponentsMap,
|
|
112
|
-
customQuestions: ComponentsMap,
|
|
113
|
-
store: Store,
|
|
114
|
-
) => {
|
|
115
|
-
const Component = componentsMap[componentTrial.type];
|
|
116
|
-
|
|
117
|
-
if (!Component) {
|
|
118
|
-
throw new Error(`No component found for type: ${componentTrial.type}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const componentProps =
|
|
122
|
-
typeof componentTrial.props === 'function'
|
|
123
|
-
? componentTrial.props(store, data)
|
|
124
|
-
: componentTrial.props || {};
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<Component
|
|
128
|
-
next={next}
|
|
129
|
-
updateStore={updateStore}
|
|
130
|
-
key={key}
|
|
131
|
-
data={data}
|
|
132
|
-
{...(componentTrial.type === 'Quest' ? { customQuestions: customQuestions } : {})}
|
|
133
|
-
{...componentProps}
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
function prefixUserMarkers(marker: string) {
|
|
139
|
-
return `user_${marker}`;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let uniqueMarkerCounter = 0;
|
|
143
|
-
function generateUniqueMarker(prefix: string): string {
|
|
144
|
-
return `${prefix}_auto_${uniqueMarkerCounter++}`;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function compileTimeline(timeline: ExperimentTrial[]): {
|
|
148
|
-
instructions: BytecodeInstruction[];
|
|
149
|
-
markers: { [key: string]: number };
|
|
150
|
-
} {
|
|
151
|
-
const instructions: BytecodeInstruction[] = [];
|
|
152
|
-
const markers: { [key: string]: number } = {};
|
|
153
|
-
|
|
154
|
-
function processTimeline(trials: ExperimentTrial[]) {
|
|
155
|
-
for (let i = 0; i < trials.length; i++) {
|
|
156
|
-
const trial = trials[i];
|
|
157
|
-
|
|
158
|
-
switch (trial.type) {
|
|
159
|
-
case 'MARKER':
|
|
160
|
-
markers[prefixUserMarkers((trial as MarkerTrial).id)] = instructions.length;
|
|
161
|
-
break;
|
|
162
|
-
|
|
163
|
-
case 'IF_GOTO':
|
|
164
|
-
const ifgotoTrial = trial as IfGotoTrial;
|
|
165
|
-
instructions.push({
|
|
166
|
-
type: 'IfGoto',
|
|
167
|
-
cond: ifgotoTrial.cond,
|
|
168
|
-
marker: prefixUserMarkers(ifgotoTrial.marker),
|
|
169
|
-
});
|
|
170
|
-
break;
|
|
171
|
-
|
|
172
|
-
case 'UPDATE_STORE':
|
|
173
|
-
instructions.push({
|
|
174
|
-
type: 'UpdateStore',
|
|
175
|
-
fun: (trial as UpdateStoreTrial).fun,
|
|
176
|
-
});
|
|
177
|
-
break;
|
|
178
|
-
|
|
179
|
-
case 'IF_BLOCK': {
|
|
180
|
-
const ifBlockTrial = trial as IfBlockTrial;
|
|
181
|
-
|
|
182
|
-
const endMarker = generateUniqueMarker('if_end');
|
|
183
|
-
|
|
184
|
-
instructions.push({
|
|
185
|
-
type: 'IfGoto',
|
|
186
|
-
cond: (store, data) => !ifBlockTrial.cond(store, data), // Negate condition to skip if false
|
|
187
|
-
marker: endMarker,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
processTimeline(ifBlockTrial.timeline);
|
|
191
|
-
|
|
192
|
-
markers[endMarker] = instructions.length;
|
|
193
|
-
break;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
case 'WHILE_BLOCK': {
|
|
197
|
-
const whileBlockTrial = trial as WhileBlockTrial;
|
|
198
|
-
|
|
199
|
-
const startMarker = generateUniqueMarker('while_start');
|
|
200
|
-
const endMarker = generateUniqueMarker('while_end');
|
|
201
|
-
|
|
202
|
-
markers[startMarker] = instructions.length;
|
|
203
|
-
|
|
204
|
-
instructions.push({
|
|
205
|
-
type: 'IfGoto',
|
|
206
|
-
cond: (store, data) => !whileBlockTrial.cond(store, data),
|
|
207
|
-
marker: endMarker,
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
processTimeline(whileBlockTrial.timeline);
|
|
211
|
-
|
|
212
|
-
instructions.push({
|
|
213
|
-
type: 'IfGoto',
|
|
214
|
-
cond: () => true, // Always jump back
|
|
215
|
-
marker: startMarker,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
markers[endMarker] = instructions.length;
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
default:
|
|
223
|
-
instructions.push({
|
|
224
|
-
type: 'Component',
|
|
225
|
-
content: trial as ComponentTrial,
|
|
226
|
-
});
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
processTimeline(timeline);
|
|
233
|
-
|
|
234
|
-
return { instructions, markers };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export default function Experiment({
|
|
238
|
-
timeline,
|
|
239
|
-
config = {
|
|
240
|
-
showProgressBar: true,
|
|
241
|
-
},
|
|
242
|
-
components = {},
|
|
243
|
-
questions = {},
|
|
244
|
-
}: {
|
|
245
|
-
timeline: ExperimentTrial[];
|
|
246
|
-
config?: ExperimentConfig;
|
|
247
|
-
components?: ComponentsMap;
|
|
248
|
-
questions?: ComponentsMap;
|
|
249
|
-
}) {
|
|
250
|
-
const trialByteCode = useMemo(() => {
|
|
251
|
-
return compileTimeline(timeline);
|
|
252
|
-
}, [timeline]);
|
|
253
|
-
|
|
254
|
-
const [trialCounter, setTrialCounter] = useState(0);
|
|
255
|
-
const [totalTrialsCompleted, setTotalTrialsCompleted] = useState(0);
|
|
256
|
-
const [data, setData] = useState<TrialData[]>([]);
|
|
257
|
-
const trialStartTimeRef = useRef(now());
|
|
258
|
-
const experimentStoreRef = useRef({});
|
|
259
|
-
|
|
260
|
-
const componentsMap = { ...defaultComponents, ...components };
|
|
261
|
-
const customQuestions: ComponentsMap = { ...defaultCustomQuestions, ...questions };
|
|
262
|
-
|
|
263
|
-
const progress = trialCounter / (trialByteCode.instructions.length - 1);
|
|
264
|
-
|
|
265
|
-
useEffect(() => {
|
|
266
|
-
window.scrollTo(0, 0);
|
|
267
|
-
}, [trialCounter]);
|
|
268
|
-
|
|
269
|
-
function updateStore(update: Store) {
|
|
270
|
-
const updatedStore = update;
|
|
271
|
-
experimentStoreRef.current = { ...experimentStoreRef.current, ...updatedStore };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function next(newData?: object): void {
|
|
275
|
-
const currentTime = now();
|
|
276
|
-
const currentTrial = (trialByteCode.instructions[trialCounter] as ComponentInstruction).content;
|
|
277
|
-
|
|
278
|
-
if (currentTrial && data) {
|
|
279
|
-
const trialData: TrialData = {
|
|
280
|
-
index: trialCounter,
|
|
281
|
-
trialNumber: totalTrialsCompleted,
|
|
282
|
-
type: currentTrial.type,
|
|
283
|
-
name: currentTrial.name,
|
|
284
|
-
data: newData,
|
|
285
|
-
start: trialStartTimeRef.current,
|
|
286
|
-
end: currentTime,
|
|
287
|
-
duration: currentTime - trialStartTimeRef.current,
|
|
288
|
-
};
|
|
289
|
-
setData([...data, trialData]);
|
|
290
|
-
setTotalTrialsCompleted(totalTrialsCompleted + 1);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
let nextCounter = trialCounter + 1;
|
|
294
|
-
let foundNextComponent = false;
|
|
295
|
-
|
|
296
|
-
// Process control flow instructions until we find a Component or reach the end
|
|
297
|
-
while (!foundNextComponent && nextCounter < trialByteCode.instructions.length) {
|
|
298
|
-
const nextInstruction = trialByteCode.instructions[nextCounter];
|
|
299
|
-
|
|
300
|
-
switch (nextInstruction.type) {
|
|
301
|
-
case 'IfGoto':
|
|
302
|
-
if (nextInstruction.cond(experimentStoreRef.current, data)) {
|
|
303
|
-
const markerIndex = trialByteCode.markers[nextInstruction.marker];
|
|
304
|
-
if (markerIndex !== undefined) {
|
|
305
|
-
nextCounter = markerIndex;
|
|
306
|
-
} else {
|
|
307
|
-
console.error(`Marker ${nextInstruction.marker} not found`);
|
|
308
|
-
nextCounter++;
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
nextCounter++;
|
|
312
|
-
}
|
|
313
|
-
break;
|
|
314
|
-
|
|
315
|
-
case 'UpdateStore':
|
|
316
|
-
updateStore(nextInstruction.fun(experimentStoreRef.current, data));
|
|
317
|
-
nextCounter++;
|
|
318
|
-
break;
|
|
319
|
-
|
|
320
|
-
case 'Component':
|
|
321
|
-
foundNextComponent = true;
|
|
322
|
-
break;
|
|
323
|
-
|
|
324
|
-
default: // Unknown, skip
|
|
325
|
-
nextCounter++;
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
trialStartTimeRef.current = now();
|
|
330
|
-
setTrialCounter(nextCounter);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return (
|
|
334
|
-
<div className='w-full'>
|
|
335
|
-
<div
|
|
336
|
-
className={` ${
|
|
337
|
-
config.showProgressBar ? '' : 'hidden '
|
|
338
|
-
} px-4 mt-4 sm:mt-12 max-w-2xl mx-auto flex-1 h-6 bg-gray-200 rounded-full overflow-hidden`}
|
|
339
|
-
>
|
|
340
|
-
<div
|
|
341
|
-
className={`h-full bg-gray-200 rounded-full duration-300 ${
|
|
342
|
-
progress > 0 ? ' border-black border-2' : ''
|
|
343
|
-
}`}
|
|
344
|
-
style={{
|
|
345
|
-
width: `${progress * 100}%`,
|
|
346
|
-
backgroundImage: `repeating-linear-gradient(
|
|
347
|
-
-45deg,
|
|
348
|
-
#E5E7EB,
|
|
349
|
-
#E5E7EB 10px,
|
|
350
|
-
#D1D5DB 10px,
|
|
351
|
-
#D1D5DB 20px
|
|
352
|
-
)`,
|
|
353
|
-
transition: 'width 300ms',
|
|
354
|
-
}}
|
|
355
|
-
/>
|
|
356
|
-
</div>
|
|
357
|
-
{trialCounter < trialByteCode.instructions.length &&
|
|
358
|
-
trialByteCode.instructions[trialCounter].type === 'Component' &&
|
|
359
|
-
renderComponentTrial(
|
|
360
|
-
(trialByteCode.instructions[trialCounter] as ComponentInstruction).content,
|
|
361
|
-
totalTrialsCompleted,
|
|
362
|
-
next,
|
|
363
|
-
updateStore,
|
|
364
|
-
data,
|
|
365
|
-
componentsMap,
|
|
366
|
-
customQuestions,
|
|
367
|
-
experimentStoreRef.current, // Pass the current store
|
|
368
|
-
)}
|
|
369
|
-
</div>
|
|
370
|
-
);
|
|
371
|
-
}
|