@adriansteffan/reactive 0.0.42 → 0.0.44

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 (37) hide show
  1. package/.claude/settings.local.json +11 -2
  2. package/README.md +106 -3
  3. package/dist/{mod-UqYdghJl.js → mod-D6DlS3ur.js} +6483 -6120
  4. package/dist/mod.d.ts +54 -2
  5. package/dist/reactive.es.js +42 -33
  6. package/dist/reactive.umd.js +38 -36
  7. package/dist/style.css +1 -1
  8. package/dist/{web-pL-YTTVv.js → web-D7VcCd-t.js} +1 -1
  9. package/dist/{web-eGzX65_f.js → web-o3I0sgwu.js} +1 -1
  10. package/package.json +1 -1
  11. package/src/components/canvasblock.tsx +112 -70
  12. package/src/components/checkdevice.tsx +15 -0
  13. package/src/components/enterfullscreen.tsx +5 -3
  14. package/src/components/exitfullscreen.tsx +4 -1
  15. package/src/components/experimentprovider.tsx +7 -2
  16. package/src/components/experimentrunner.tsx +66 -52
  17. package/src/components/microphonecheck.tsx +3 -0
  18. package/src/components/mobilefilepermission.tsx +3 -0
  19. package/src/components/plaininput.tsx +17 -0
  20. package/src/components/prolificending.tsx +3 -0
  21. package/src/components/quest.tsx +58 -8
  22. package/src/components/storeui.tsx +15 -11
  23. package/src/components/text.tsx +11 -0
  24. package/src/components/upload.tsx +56 -271
  25. package/src/mod.tsx +1 -0
  26. package/src/utils/bytecode.ts +50 -0
  27. package/src/utils/simulation.ts +268 -0
  28. package/src/utils/upload.ts +299 -0
  29. package/template/README.md +59 -0
  30. package/template/backend/src/backend.ts +1 -0
  31. package/template/package.json +2 -0
  32. package/template/simulate.ts +15 -0
  33. package/template/src/Experiment.tsx +58 -5
  34. package/template/src/main.tsx +1 -1
  35. package/template/tsconfig.json +2 -3
  36. package/tsconfig.json +1 -0
  37. package/vite.config.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { W as P, b as x, E } from "./mod-UqYdghJl.js";
1
+ import { W as P, b as x, E } from "./mod-D6DlS3ur.js";
2
2
  function m(w) {
3
3
  const e = w.split("/").filter((t) => t !== "."), r = [];
4
4
  return e.forEach((t) => {
@@ -1,4 +1,4 @@
1
- import { W as e } from "./mod-UqYdghJl.js";
1
+ import { W as e } from "./mod-D6DlS3ur.js";
2
2
  class s extends e {
3
3
  async enable() {
4
4
  console.log("Immersive mode is only available on Android");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adriansteffan/reactive",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -4,6 +4,8 @@ import { useRef, useEffect, useMemo, useCallback } from 'react';
4
4
 
5
5
  import {
6
6
  compileTimeline,
7
+ advanceToNextContent,
8
+ applyMetadata,
7
9
  TimelineItem,
8
10
  UnifiedBytecodeInstruction,
9
11
  ExecuteContentInstruction,
@@ -12,6 +14,18 @@ import {
12
14
  CanvasResultData,
13
15
  } from '../utils/bytecode';
14
16
  import { BaseComponentProps, isFullscreen, now } from '../utils/common';
17
+ import { registerSimulation, ParticipantState } from '../utils/simulation';
18
+
19
+ export type SlideSimulatorResult = {
20
+ key: string | null;
21
+ reactionTime: number | null;
22
+ participantState: ParticipantState;
23
+ };
24
+
25
+ export type SlideSimulator = (
26
+ slide: Record<string, any>,
27
+ participant: ParticipantState,
28
+ ) => SlideSimulatorResult;
15
29
 
16
30
  interface CanvasSlide {
17
31
  draw: (ctx: CanvasRenderingContext2D, width: number, height: number) => void;
@@ -25,6 +39,7 @@ interface CanvasSlide {
25
39
  | Record<string, any>
26
40
  | ((data?: RefinedTrialData[], store?: Store) => Record<string, any>);
27
41
  nestMetadata?: boolean;
42
+ simulate?: SlideSimulator;
28
43
  }
29
44
 
30
45
  type DynamicCanvasSlideGenerator = (data: RefinedTrialData[], store: Store) => CanvasSlide;
@@ -227,74 +242,41 @@ export default function CanvasBlock({
227
242
  }, [instructions, resolveSlideContent, clearCanvas]);
228
243
 
229
244
  const processControlFlow = useCallback(() => {
230
- let foundContentToExecute = false;
231
245
  if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
232
246
  animationFrameRef.current = null;
233
247
 
234
- while (!foundContentToExecute && instructionPointerRef.current < instructions.length) {
235
- const currentInstruction = instructions[instructionPointerRef.current];
236
-
237
- switch (currentInstruction.type) {
238
- case 'IfGoto':
239
- if (currentInstruction.cond(storeRef.current, dataRef.current)) {
240
- const markerIndex = markers[currentInstruction.marker];
241
- if (markerIndex !== undefined) {
242
- instructionPointerRef.current = markerIndex;
243
- } else {
244
- console.error(`Marker ${currentInstruction.marker} not found`);
245
- instructionPointerRef.current++;
246
- }
247
- } else {
248
- instructionPointerRef.current++;
249
- }
250
- continue;
251
-
252
- case 'UpdateStore': {
253
- storeRef.current = {
254
- ...storeRef.current,
255
- ...currentInstruction.fun(storeRef.current, dataRef.current),
256
- };
257
- instructionPointerRef.current++;
258
- continue;
259
- }
260
- case 'ExecuteContent': {
261
- foundContentToExecute = true;
262
- if (canvasRef.current) {
263
- const currentSlide = resolveSlideContent(currentInstruction);
264
- if (currentSlide) {
265
- drawSlideInternal(currentSlide);
266
- const displayDuration = currentSlide.displayDuration ?? Infinity;
267
- const responseTimeLimit = currentSlide.responseTimeLimit ?? Infinity;
268
- if (displayDuration !== Infinity || responseTimeLimit !== Infinity) {
269
- animationFrameRef.current = requestAnimationFrame(tick);
270
- }
271
- } else {
272
- console.error(
273
- 'Failed to resolve slide content during control flow:',
274
- currentInstruction,
275
- );
276
- instructionPointerRef.current++;
277
- foundContentToExecute = false;
278
- continue;
279
- }
280
- } else {
281
- console.error('Canvas element not found during control flow.');
282
- return;
283
- }
284
- break;
285
- }
286
- default:
287
- console.warn('Unknown instruction type during control flow:', currentInstruction);
288
- instructionPointerRef.current++;
289
- continue;
290
- }
291
- }
248
+ const pointer = advanceToNextContent(
249
+ { instructions, markers },
250
+ instructionPointerRef.current,
251
+ () => storeRef.current,
252
+ () => dataRef.current,
253
+ (s) => { storeRef.current = s; },
254
+ );
255
+ instructionPointerRef.current = pointer;
292
256
 
293
- if (!foundContentToExecute && instructionPointerRef.current >= instructions.length) {
294
- if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
295
- animationFrameRef.current = null;
257
+ if (pointer >= instructions.length) {
296
258
  updateStore(storeRef.current);
297
259
  next(dataRef.current);
260
+ return;
261
+ }
262
+
263
+ if (!canvasRef.current) {
264
+ console.error('Canvas element not found during control flow.');
265
+ return;
266
+ }
267
+
268
+ const currentSlide = resolveSlideContent(instructions[pointer]);
269
+ if (currentSlide) {
270
+ drawSlideInternal(currentSlide);
271
+ const displayDuration = currentSlide.displayDuration ?? Infinity;
272
+ const responseTimeLimit = currentSlide.responseTimeLimit ?? Infinity;
273
+ if (displayDuration !== Infinity || responseTimeLimit !== Infinity) {
274
+ animationFrameRef.current = requestAnimationFrame(tick);
275
+ }
276
+ } else {
277
+ console.error('Failed to resolve slide content during control flow:', instructions[pointer]);
278
+ instructionPointerRef.current++;
279
+ processControlFlow();
298
280
  }
299
281
  }, [instructions, markers, resolveSlideContent, drawSlideInternal, tick, next]);
300
282
 
@@ -326,14 +308,7 @@ export default function CanvasBlock({
326
308
  reactionTime: responseData ? responseData.reactionTime : null,
327
309
  } as CanvasResultData;
328
310
 
329
- const metadata =
330
- typeof slide.metadata === 'function'
331
- ? slide.metadata(dataRef.current, storeRef.current)
332
- : slide.metadata;
333
-
334
- if (slide.nestMetadata) {
335
- trialData = { ...trialData, metadata: metadata };
336
- } else trialData = { ...metadata, ...trialData };
311
+ trialData = applyMetadata(trialData, slide, dataRef.current, storeRef.current);
337
312
 
338
313
  dataRef.current.push(trialData);
339
314
  }
@@ -524,3 +499,70 @@ export default function CanvasBlock({
524
499
  </div>
525
500
  );
526
501
  }
502
+
503
+ // --- Default simulator ---
504
+
505
+ registerSimulation('CanvasBlock', (trialProps, experimentState, simulators, participant) => {
506
+ const timeline = trialProps.timeline;
507
+ if (!timeline) return { responseData: [], participantState: participant };
508
+
509
+ const bytecode = compileTimeline(timeline);
510
+ const innerData: CanvasResultData[] = [];
511
+ let innerStore: Store = { ...(experimentState.store || {}) };
512
+ let currentTime = 0;
513
+ let slideNumber = 0;
514
+
515
+ const getStore = () => innerStore;
516
+ const getData = () => innerData as RefinedTrialData[];
517
+ const onUpdateStore = (s: Store) => { innerStore = s; };
518
+
519
+ let pointer = advanceToNextContent(bytecode, 0, getStore, getData, onUpdateStore);
520
+
521
+ while (pointer < bytecode.instructions.length) {
522
+ let slide = (bytecode.instructions[pointer] as ExecuteContentInstruction).content;
523
+ if (typeof slide === 'function') slide = slide(innerData, innerStore);
524
+
525
+ const sim = slide.simulate || simulators.respondToSlide;
526
+ const result = sim(slide, participant);
527
+ participant = result.participantState;
528
+
529
+ if (!slide.ignoreData) {
530
+ const duration = slide.displayDuration || result.reactionTime || 1000;
531
+
532
+ let td: CanvasResultData = {
533
+ index: pointer,
534
+ trialNumber: slideNumber,
535
+ start: currentTime,
536
+ end: currentTime + duration,
537
+ duration,
538
+ key: result.key,
539
+ reactionTime: result.reactionTime,
540
+ };
541
+
542
+ td = applyMetadata(td, slide, innerData, innerStore);
543
+
544
+ innerData.push(td);
545
+ currentTime += duration;
546
+ }
547
+
548
+ slideNumber++;
549
+ pointer = advanceToNextContent(bytecode, pointer + 1, getStore, getData, onUpdateStore);
550
+ }
551
+
552
+ return { responseData: innerData, participantState: participant, storeUpdates: innerStore, duration: currentTime };
553
+ }, {
554
+ respondToSlide: (slide: any, participant: any) => {
555
+ const keys = slide.allowedKeys;
556
+ if (keys === true) {
557
+ return { key: ' ', reactionTime: 200 + Math.random() * 600, participantState: participant };
558
+ }
559
+ if (Array.isArray(keys) && keys.length > 0) {
560
+ return {
561
+ key: keys[Math.floor(Math.random() * keys.length)],
562
+ reactionTime: 200 + Math.random() * 600,
563
+ participantState: participant,
564
+ };
565
+ }
566
+ return { key: null, reactionTime: null, participantState: participant };
567
+ },
568
+ });
@@ -1,5 +1,20 @@
1
1
  import { BaseComponentProps } from '../mod';
2
2
  import { useEffect, useRef, useState } from 'react';
3
+ import { registerSimulation } from '../utils/simulation';
4
+
5
+ registerSimulation('CheckDevice', (_trialProps, _experimentState, _simulators, participant) => {
6
+ const deviceInfo = {
7
+ windowWidth: 1920, windowHeight: 1080, screenWidth: 1920, screenHeight: 1080,
8
+ browser: 'Simulated', browserVersion: '1.0', isMobile: false,
9
+ operatingSystem: 'Simulated', hasWebAudio: true, hasFullscreen: true,
10
+ hasWebcam: true, hasMicrophone: true,
11
+ };
12
+ return {
13
+ responseData: deviceInfo,
14
+ participantState: participant,
15
+ storeUpdates: { _reactiveDeviceInfo: deviceInfo },
16
+ };
17
+ }, {});
3
18
 
4
19
  interface DeviceInfo {
5
20
  windowWidth: number;
@@ -1,11 +1,13 @@
1
- import React, { useState, useCallback, useRef, useEffect } from 'react'; // Import useState
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
2
2
  import { BaseComponentProps, getPlatform, isFullscreen } from '../utils/common';
3
+ import { registerSimulation, noopSimulate } from '../utils/simulation';
3
4
  import Text from '../components/text';
4
-
5
5
  import { StatusBar } from '@capacitor/status-bar';
6
6
  import { ImmersiveMode } from '@adriansteffan/immersive-mode';
7
7
  import { Capacitor } from '@capacitor/core';
8
8
 
9
+ registerSimulation('EnterFullscreen', noopSimulate, {});
10
+
9
11
  export default function EnterFullscreen({
10
12
  content,
11
13
  buttonText,
@@ -28,7 +30,7 @@ export default function EnterFullscreen({
28
30
 
29
31
  const [isWaiting, setIsWaiting] = useState(false);
30
32
  const listenerFallbackActive = useRef(false);
31
- const timeoutId = useRef<NodeJS.Timeout | null>(null);
33
+ const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
32
34
 
33
35
  const cancelPendingTimeout = useCallback(() => {
34
36
  if (timeoutId.current) {
@@ -1,9 +1,12 @@
1
1
  import { useCallback, useRef, useEffect } from 'react';
2
2
  import { BaseComponentProps, getPlatform, isFullscreen } from '../utils/common';
3
+ import { registerSimulation, noopSimulate } from '../utils/simulation';
3
4
  import { StatusBar } from '@capacitor/status-bar';
4
5
  import { ImmersiveMode } from '@adriansteffan/immersive-mode';
5
6
  import { Capacitor } from '@capacitor/core';
6
7
 
8
+ registerSimulation('ExitFullscreen', noopSimulate, {});
9
+
7
10
  export default function ExitFullscreen({
8
11
  next,
9
12
  delayMs = 0,
@@ -12,7 +15,7 @@ export default function ExitFullscreen({
12
15
  } & BaseComponentProps) {
13
16
 
14
17
  const listenerFallbackActive = useRef(false);
15
- const timeoutId = useRef<NodeJS.Timeout | null>(null);
18
+ const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);
16
19
 
17
20
  const cancelPendingTimeout = useCallback(() => {
18
21
  if (timeoutId.current) {
@@ -3,11 +3,14 @@ import { ToastContainer } from 'react-toastify';
3
3
 
4
4
  const queryClient = new QueryClient();
5
5
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6
- import { ReactNode } from 'react';
6
+ import { createContext, ReactNode, useContext } from 'react';
7
7
  import { SettingsScreen } from './settingsscreen';
8
8
  import { Param } from '../utils/common';
9
9
 
10
- export default function ExperimentProvider({ children, disableSettings }: { children: ReactNode, disableSettings?: boolean }) {
10
+ const HybridSimulationContext = createContext(false);
11
+ export const useHybridSimulationDisabled = () => useContext(HybridSimulationContext);
12
+
13
+ export default function ExperimentProvider({ children, disableSettings, disableHybridSimulation }: { children: ReactNode, disableSettings?: boolean, disableHybridSimulation?: boolean }) {
11
14
 
12
15
  if (window.location.pathname.endsWith('/settings') && !disableSettings) {
13
16
  return (
@@ -20,7 +23,9 @@ export default function ExperimentProvider({ children, disableSettings }: { chil
20
23
 
21
24
  return (
22
25
  <QueryClientProvider client={queryClient}>
26
+ <HybridSimulationContext.Provider value={!!disableHybridSimulation}>
23
27
  {children}
28
+ </HybridSimulationContext.Provider>
24
29
  <ToastContainer
25
30
  position='top-center'
26
31
  autoClose={3000}
@@ -1,14 +1,22 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
 
3
3
  import '../index.css';
4
- import { ExperimentConfig, Store, Param, now } from '../utils/common';
4
+ import { ExperimentConfig, Store, Param, now, getParam } from '../utils/common';
5
5
  import { useCallback, useEffect, useMemo, useRef, useState, ComponentType } from 'react';
6
6
  import {
7
7
  compileTimeline,
8
+ advanceToNextContent,
9
+ applyMetadata,
8
10
  TimelineItem,
9
11
  RefinedTrialData,
10
12
  ComponentResultData,
11
13
  } from '../utils/bytecode';
14
+ import {
15
+ ParticipantState,
16
+ SimulateFunction,
17
+ resolveSimulation,
18
+ } from '../utils/simulation';
19
+ import { useHybridSimulationDisabled } from './experimentprovider';
12
20
 
13
21
  import Upload from './upload';
14
22
  import Text from './text';
@@ -59,6 +67,8 @@ interface RuntimeComponentContent {
59
67
  | ((data: RefinedTrialData[], store: Store) => Record<string, any>);
60
68
  nestMetadata?: boolean;
61
69
  props?: Record<string, any> | ((data: RefinedTrialData[], store: Store) => Record<string, any>);
70
+ simulate?: SimulateFunction | boolean;
71
+ simulators?: Record<string, any>;
62
72
  }
63
73
 
64
74
  function isRuntimeComponentContent(content: any): content is RuntimeComponentContent {
@@ -72,12 +82,15 @@ export default function ExperimentRunner({
72
82
  },
73
83
  components = {},
74
84
  questions = {},
85
+ hybridParticipant,
75
86
  }: {
76
87
  timeline: TimelineItem[];
77
88
  config?: ExperimentConfig;
78
89
  components?: ComponentsMap;
79
90
  questions?: ComponentsMap;
91
+ hybridParticipant?: ParticipantState;
80
92
  }) {
93
+ const disableHybridSimulation = useHybridSimulationDisabled();
81
94
  const trialByteCode = useMemo(() => {
82
95
  return compileTimeline(timeline);
83
96
  }, [timeline]);
@@ -137,6 +150,15 @@ export default function ExperimentRunner({
137
150
  const lastTrialEndTimeRef = useRef(now());
138
151
  const experimentStoreRef = useRef<Store>({});
139
152
 
153
+ const simulationMode =
154
+ (!disableHybridSimulation && getParam('hybridSimulation', false, 'boolean'))
155
+ ? 'hybrid' as const
156
+ : 'none' as const;
157
+
158
+ const participantRef = useRef<ParticipantState>(hybridParticipant || {});
159
+ // Guards against duplicate simulation in React strict mode
160
+ const lastSimulatedPointerRef = useRef(-1);
161
+
140
162
  const componentsMap = { ...defaultComponents, ...components };
141
163
  const customQuestionsMap: ComponentsMap = { ...defaultCustomQuestions, ...questions };
142
164
 
@@ -180,14 +202,7 @@ export default function ExperimentRunner({
180
202
  responseData: componentResponseData,
181
203
  };
182
204
 
183
- const metadata =
184
- typeof content.metadata === 'function'
185
- ? content.metadata(dataRef.current, experimentStoreRef.current)
186
- : content.metadata;
187
-
188
- if (content.nestMetadata) {
189
- trialData = { ...trialData, metadata: metadata };
190
- } else trialData = { ...metadata, ...trialData };
205
+ trialData = applyMetadata(trialData, content, dataRef.current, experimentStoreRef.current);
191
206
 
192
207
  dataRef.current = [...dataRef.current, trialData];
193
208
  setTotalTrialsCompleted((prevCount) => prevCount + 1);
@@ -199,49 +214,17 @@ export default function ExperimentRunner({
199
214
  }
200
215
  }
201
216
 
202
- let nextPointer = instructionPointer + 1;
203
- let foundNextContent = false;
204
-
205
- while (nextPointer < trialByteCode.instructions.length) {
206
- const nextInstruction = trialByteCode.instructions[nextPointer];
207
-
208
- switch (nextInstruction.type) {
209
- case 'IfGoto':
210
- if (nextInstruction.cond(experimentStoreRef.current, dataRef.current)) {
211
- const markerIndex = trialByteCode.markers[nextInstruction.marker];
212
- if (markerIndex !== undefined) {
213
- nextPointer = markerIndex;
214
- continue;
215
- } else {
216
- console.error(`Marker ${nextInstruction.marker} not found`);
217
- nextPointer++;
218
- }
219
- } else {
220
- nextPointer++;
221
- }
222
- break;
223
-
224
- case 'UpdateStore':
225
- updateStore(nextInstruction.fun(experimentStoreRef.current, dataRef.current));
226
- nextPointer++;
227
- break;
228
-
229
- case 'ExecuteContent':
230
- foundNextContent = true;
231
- lastTrialEndTimeRef.current = now();
232
- setInstructionPointer(nextPointer);
233
- return;
234
-
235
- default:
236
- console.error('Unknown instruction type encountered:', nextInstruction);
237
- nextPointer++;
238
- break;
239
- }
240
- }
241
-
242
- if (!foundNextContent) {
243
- setInstructionPointer(nextPointer);
217
+ const nextPointer = advanceToNextContent(
218
+ trialByteCode,
219
+ instructionPointer + 1,
220
+ () => experimentStoreRef.current,
221
+ () => dataRef.current,
222
+ (s) => updateStore(s),
223
+ );
224
+ if (nextPointer < trialByteCode.instructions.length) {
225
+ lastTrialEndTimeRef.current = now();
244
226
  }
227
+ setInstructionPointer(nextPointer);
245
228
  }
246
229
 
247
230
  const collectRefreshRate = useCallback((callback: (refreshRate: number | null) => void) => {
@@ -341,8 +324,39 @@ export default function ExperimentRunner({
341
324
 
342
325
  const currentInstruction = trialByteCode.instructions[instructionPointer];
343
326
 
327
+ const shouldSimulate = useMemo(() => {
328
+ if (simulationMode === 'none') return false;
329
+ if (currentInstruction?.type !== 'ExecuteContent') return false;
330
+ const content = currentInstruction.content;
331
+ if (!isRuntimeComponentContent(content)) return false;
332
+ if (content.simulate === false) return false;
333
+ return !!content.simulate || !!content.simulators;
334
+ }, [instructionPointer, simulationMode, currentInstruction]);
335
+
336
+ useEffect(() => {
337
+ if (!shouldSimulate) return;
338
+ if (lastSimulatedPointerRef.current === instructionPointer) return;
339
+ lastSimulatedPointerRef.current = instructionPointer;
340
+
341
+ (async () => {
342
+ const content = (currentInstruction as any).content;
343
+ const { trialProps, simulateFn, simulators } = resolveSimulation(content, dataRef.current, experimentStoreRef.current);
344
+
345
+ const result = await simulateFn(
346
+ trialProps,
347
+ { data: dataRef.current, store: experimentStoreRef.current },
348
+ simulators,
349
+ participantRef.current,
350
+ );
351
+
352
+ participantRef.current = result.participantState;
353
+ if (result.storeUpdates) updateStore(result.storeUpdates);
354
+ next(result.responseData);
355
+ })();
356
+ }, [shouldSimulate, instructionPointer]);
357
+
344
358
  let componentToRender = null;
345
- if (currentInstruction?.type === 'ExecuteContent') {
359
+ if (currentInstruction?.type === 'ExecuteContent' && !shouldSimulate) {
346
360
  const content = currentInstruction.content;
347
361
  if (isRuntimeComponentContent(content)) {
348
362
  const Component = componentsMap[content.type];
@@ -1,5 +1,8 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { VoiceRecorder } from './voicerecorder';
3
+ import { registerSimulation, noopSimulate } from '../utils/simulation';
4
+
5
+ registerSimulation('MicrophoneCheck', noopSimulate, {});
3
6
 
4
7
  interface MicrophoneDevice {
5
8
  deviceId: string;
@@ -1,8 +1,11 @@
1
1
  import { BaseComponentProps, getPlatform } from '../utils/common';
2
2
  import { useEffect, useState } from 'react';
3
+ import { registerSimulation, noopSimulate } from '../utils/simulation';
3
4
  import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
4
5
  import { Capacitor } from '@capacitor/core';
5
6
 
7
+ registerSimulation('RequestFilePermission', noopSimulate, {});
8
+
6
9
  export default function RequestFilePermission({ next }: BaseComponentProps) {
7
10
  const [permissionStatus, setPermissionStatus] = useState('checking');
8
11
 
@@ -1,5 +1,22 @@
1
1
  import { BaseComponentProps } from '../mod';
2
2
  import { useState } from 'react';
3
+ import { registerSimulation } from '../utils/simulation';
4
+
5
+ registerSimulation('PlainInput', (trialProps, _experimentState, simulators, participant) => {
6
+ const result = simulators.respond(trialProps, participant);
7
+ const typingDuration = String(result.value).length * (50 + Math.random() * 100);
8
+ return {
9
+ responseData: { value: result.value },
10
+ participantState: result.participantState,
11
+ storeUpdates: trialProps.storeupdate ? trialProps.storeupdate(result.value) : undefined,
12
+ duration: typingDuration,
13
+ };
14
+ }, {
15
+ respond: (_input: any, participant: any) => ({
16
+ value: 'simulated_input',
17
+ participantState: participant,
18
+ }),
19
+ });
3
20
 
4
21
  function PlainInput({
5
22
  content,
@@ -1,5 +1,8 @@
1
1
  import { BaseComponentProps, getParam, registerComponentParams } from '../utils/common';
2
2
  import Text from '../components/text';
3
+ import { registerSimulation, noopSimulate } from '../utils/simulation';
4
+
5
+ registerSimulation('ProlificEnding', noopSimulate, {});
3
6
 
4
7
  registerComponentParams('ProlificEnding', [
5
8
  { name: 'cc', defaultValue: '', type: 'string', description: 'Completion code of the Profilic experiment (more commonly supplied via the code)' },
@@ -3,6 +3,63 @@ import { defaultCss, Model, Question, Serializer } from 'survey-core';
3
3
  import { ReactQuestionFactory, Survey, SurveyQuestionElementBase } from 'survey-react-ui';
4
4
  import { ContrastLight } from 'survey-core/themes';
5
5
  import 'survey-core/survey-core.min.css';
6
+ import { registerSimulation } from '../utils/simulation';
7
+
8
+ registerSimulation('Quest', (trialProps, _experimentState, simulators, participant) => {
9
+ const responseData: Record<string, any> = {};
10
+ let totalDuration = 0;
11
+ const pages = trialProps.surveyJson?.pages || [{ elements: trialProps.surveyJson?.elements || [] }];
12
+ for (const page of pages) {
13
+ for (const el of page.elements || []) {
14
+ if (!el.name) continue;
15
+ const result = simulators.answerQuestion(el, participant);
16
+ participant = result.participantState;
17
+ responseData[el.name] = result.value;
18
+ totalDuration += result.duration ?? 0;
19
+ }
20
+ }
21
+ return { responseData, participantState: participant, duration: totalDuration };
22
+ }, {
23
+ answerQuestion: (question: any, participant: any) => {
24
+ let value;
25
+ switch (question.type) {
26
+ case 'rating': {
27
+ const min = question.rateMin ?? 1, max = question.rateMax ?? 5;
28
+ value = min + Math.floor(Math.random() * (max - min + 1));
29
+ break;
30
+ }
31
+ case 'boolean': value = Math.random() > 0.5; break;
32
+ case 'text': case 'comment': value = 'simulated_response'; break;
33
+ case 'radiogroup': case 'dropdown': {
34
+ const c = question.choices?.[Math.floor(Math.random() * (question.choices?.length || 0))];
35
+ value = c !== undefined ? (typeof c === 'object' ? c.value : c) : null;
36
+ break;
37
+ }
38
+ case 'checkbox': {
39
+ if (question.choices?.length) {
40
+ const n = 1 + Math.floor(Math.random() * question.choices.length);
41
+ value = [...question.choices]
42
+ .sort(() => Math.random() - 0.5).slice(0, n)
43
+ .map((c: any) => typeof c === 'object' ? c.value : c);
44
+ }
45
+ break;
46
+ }
47
+ case 'matrix': {
48
+ if (question.rows?.length && question.columns?.length) {
49
+ value = Object.fromEntries(
50
+ question.rows.map((r: any) => {
51
+ const col = question.columns[Math.floor(Math.random() * question.columns.length)];
52
+ return [typeof r === 'object' ? r.value : r, typeof col === 'object' ? col.value : col];
53
+ }),
54
+ );
55
+ }
56
+ break;
57
+ }
58
+ default: value = null;
59
+ }
60
+ return { value, participantState: participant, duration: 1000 + Math.random() * 4000 };
61
+ },
62
+ });
6
63
 
7
64
  type ComponentsMap = {
8
65
  [key: string]: ComponentType<any>;
@@ -73,14 +130,7 @@ function Quest({
73
130
 
74
131
  const saveResults = useCallback(
75
132
  (sender: any) => {
76
- const resultData: Record<string, any> = {};
77
- for (const key in sender.data) {
78
- const question = sender.getQuestionByName(key);
79
- if (question) {
80
- resultData[key] = question.value;
81
- }
82
- }
83
- next(resultData);
133
+ next({ ...sender.data });
84
134
  },
85
135
  [next],
86
136
  );