@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.
@@ -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
- }