@adriansteffan/reactive 0.0.24 → 0.0.27

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