@adriansteffan/reactive 0.0.44 → 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 +8 -1
- package/README.md +126 -0
- package/dist/{mod-D6DlS3ur.js → mod-D9lwPIrH.js} +3480 -3498
- package/dist/mod.d.ts +19 -23
- package/dist/reactive.es.js +12 -11
- package/dist/reactive.umd.js +38 -38
- package/dist/style.css +1 -1
- package/dist/{web-D7VcCd-t.js → web-DUIQX1PV.js} +1 -1
- package/dist/{web-o3I0sgwu.js → web-DXP3LAJm.js} +1 -1
- package/package.json +1 -1
- package/src/components/canvasblock.tsx +14 -5
- package/src/components/checkdevice.tsx +3 -0
- package/src/components/enterfullscreen.tsx +2 -0
- package/src/components/exitfullscreen.tsx +2 -0
- package/src/components/experimentrunner.tsx +19 -6
- package/src/components/microphonecheck.tsx +3 -1
- package/src/components/plaininput.tsx +3 -0
- package/src/components/prolificending.tsx +2 -0
- package/src/components/quest.tsx +3 -0
- package/src/components/storeui.tsx +3 -0
- package/src/components/text.tsx +3 -0
- package/src/components/upload.tsx +18 -20
- package/src/index.css +0 -20
- package/src/mod.tsx +1 -0
- package/src/utils/bytecode.ts +13 -11
- package/src/utils/common.ts +4 -1
- package/src/utils/simulation.ts +6 -5
- package/src/utils/upload.ts +106 -204
- package/template/backend/package-lock.json +280 -156
- package/template/package-lock.json +1693 -771
- package/template/src/Experiment.tsx +5 -1
package/package.json
CHANGED
|
@@ -10,11 +10,20 @@ import {
|
|
|
10
10
|
UnifiedBytecodeInstruction,
|
|
11
11
|
ExecuteContentInstruction,
|
|
12
12
|
Store,
|
|
13
|
-
|
|
13
|
+
TrialResult,
|
|
14
14
|
CanvasResultData,
|
|
15
15
|
} from '../utils/bytecode';
|
|
16
16
|
import { BaseComponentProps, isFullscreen, now } from '../utils/common';
|
|
17
17
|
import { registerSimulation, ParticipantState } from '../utils/simulation';
|
|
18
|
+
import { registerFlattener } from '../utils/upload';
|
|
19
|
+
|
|
20
|
+
registerFlattener('CanvasBlock', 'canvas', (item) => {
|
|
21
|
+
const responseData = item.responseData;
|
|
22
|
+
if (Array.isArray(responseData)) {
|
|
23
|
+
return responseData.map((i) => ({ block: item.name, ...i }));
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
});
|
|
18
27
|
|
|
19
28
|
export type SlideSimulatorResult = {
|
|
20
29
|
key: string | null;
|
|
@@ -37,12 +46,12 @@ interface CanvasSlide {
|
|
|
37
46
|
allowedKeys?: string[] | boolean;
|
|
38
47
|
metadata?:
|
|
39
48
|
| Record<string, any>
|
|
40
|
-
| ((data?:
|
|
49
|
+
| ((data?: TrialResult[], store?: Store) => Record<string, any>);
|
|
41
50
|
nestMetadata?: boolean;
|
|
42
51
|
simulate?: SlideSimulator;
|
|
43
52
|
}
|
|
44
53
|
|
|
45
|
-
type DynamicCanvasSlideGenerator = (data:
|
|
54
|
+
type DynamicCanvasSlideGenerator = (data: TrialResult[], store: Store) => CanvasSlide;
|
|
46
55
|
|
|
47
56
|
function isDynamicCanvasSlideGenerator(content: any): content is DynamicCanvasSlideGenerator {
|
|
48
57
|
return typeof content === 'function';
|
|
@@ -77,7 +86,7 @@ export default function CanvasBlock({
|
|
|
77
86
|
const isDrawingVisibleRef = useRef<boolean>(true);
|
|
78
87
|
const responseRegisteredRef = useRef<null | Record<string, any>>(null);
|
|
79
88
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
80
|
-
const dataRef = useRef<
|
|
89
|
+
const dataRef = useRef<TrialResult[]>([]);
|
|
81
90
|
const storeRef = useRef<Store>(store ?? {});
|
|
82
91
|
const animationFrameRef = useRef<number | null>(null);
|
|
83
92
|
const contentInstructionsCompletedRef = useRef(0);
|
|
@@ -513,7 +522,7 @@ registerSimulation('CanvasBlock', (trialProps, experimentState, simulators, part
|
|
|
513
522
|
let slideNumber = 0;
|
|
514
523
|
|
|
515
524
|
const getStore = () => innerStore;
|
|
516
|
-
const getData = () => innerData as
|
|
525
|
+
const getData = () => innerData as TrialResult[];
|
|
517
526
|
const onUpdateStore = (s: Store) => { innerStore = s; };
|
|
518
527
|
|
|
519
528
|
let pointer = advanceToNextContent(bytecode, 0, getStore, getData, onUpdateStore);
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { BaseComponentProps } from '../mod';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { registerSimulation } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
5
|
+
|
|
6
|
+
registerFlattener('CheckDevice', 'session');
|
|
4
7
|
|
|
5
8
|
registerSimulation('CheckDevice', (_trialProps, _experimentState, _simulators, participant) => {
|
|
6
9
|
const deviceInfo = {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React, { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { BaseComponentProps, getPlatform, isFullscreen } from '../utils/common';
|
|
3
3
|
import { registerSimulation, noopSimulate } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
4
5
|
import Text from '../components/text';
|
|
5
6
|
import { StatusBar } from '@capacitor/status-bar';
|
|
6
7
|
import { ImmersiveMode } from '@adriansteffan/immersive-mode';
|
|
7
8
|
import { Capacitor } from '@capacitor/core';
|
|
8
9
|
|
|
10
|
+
registerFlattener('EnterFullscreen', 'session');
|
|
9
11
|
registerSimulation('EnterFullscreen', noopSimulate, {});
|
|
10
12
|
|
|
11
13
|
export default function EnterFullscreen({
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useCallback, useRef, useEffect } from 'react';
|
|
2
2
|
import { BaseComponentProps, getPlatform, isFullscreen } from '../utils/common';
|
|
3
3
|
import { registerSimulation, noopSimulate } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
4
5
|
import { StatusBar } from '@capacitor/status-bar';
|
|
5
6
|
import { ImmersiveMode } from '@adriansteffan/immersive-mode';
|
|
6
7
|
import { Capacitor } from '@capacitor/core';
|
|
7
8
|
|
|
9
|
+
registerFlattener('ExitFullscreen', 'session');
|
|
8
10
|
registerSimulation('ExitFullscreen', noopSimulate, {});
|
|
9
11
|
|
|
10
12
|
export default function ExitFullscreen({
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
advanceToNextContent,
|
|
9
9
|
applyMetadata,
|
|
10
10
|
TimelineItem,
|
|
11
|
-
|
|
11
|
+
TrialResult,
|
|
12
12
|
ComponentResultData,
|
|
13
13
|
} from '../utils/bytecode';
|
|
14
14
|
import {
|
|
@@ -60,13 +60,14 @@ const defaultCustomQuestions: ComponentsMap = {
|
|
|
60
60
|
interface RuntimeComponentContent {
|
|
61
61
|
name?: string;
|
|
62
62
|
type: string;
|
|
63
|
+
csv?: string | string[];
|
|
63
64
|
collectRefreshRate?: boolean;
|
|
64
65
|
hideSettings?: string[] | boolean;
|
|
65
66
|
metadata?:
|
|
66
67
|
| Record<string, any>
|
|
67
|
-
| ((data:
|
|
68
|
+
| ((data: TrialResult[], store: Store) => Record<string, any>);
|
|
68
69
|
nestMetadata?: boolean;
|
|
69
|
-
props?: Record<string, any> | ((data:
|
|
70
|
+
props?: Record<string, any> | ((data: TrialResult[], store: Store) => Record<string, any>);
|
|
70
71
|
simulate?: SimulateFunction | boolean;
|
|
71
72
|
simulators?: Record<string, any>;
|
|
72
73
|
}
|
|
@@ -95,8 +96,7 @@ export default function ExperimentRunner({
|
|
|
95
96
|
return compileTimeline(timeline);
|
|
96
97
|
}, [timeline]);
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
-
const dataRef = useRef<RefinedTrialData[]>((() => {
|
|
99
|
+
const dataRef = useRef<TrialResult[]>((() => {
|
|
100
100
|
const urlParams: Record<string, any> = {};
|
|
101
101
|
const searchParams = new URLSearchParams(window.location.search);
|
|
102
102
|
for (const [key, value] of searchParams.entries()) {
|
|
@@ -150,6 +150,16 @@ export default function ExperimentRunner({
|
|
|
150
150
|
const lastTrialEndTimeRef = useRef(now());
|
|
151
151
|
const experimentStoreRef = useRef<Store>({});
|
|
152
152
|
|
|
153
|
+
const [instructionPointer, setInstructionPointer] = useState(() =>
|
|
154
|
+
advanceToNextContent(
|
|
155
|
+
trialByteCode,
|
|
156
|
+
0,
|
|
157
|
+
() => experimentStoreRef.current,
|
|
158
|
+
() => dataRef.current,
|
|
159
|
+
(s) => { experimentStoreRef.current = { ...experimentStoreRef.current, ...s }; },
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
|
|
153
163
|
const simulationMode =
|
|
154
164
|
(!disableHybridSimulation && getParam('hybridSimulation', false, 'boolean'))
|
|
155
165
|
? 'hybrid' as const
|
|
@@ -199,6 +209,7 @@ export default function ExperimentRunner({
|
|
|
199
209
|
duration: endTime - startTime,
|
|
200
210
|
type: content.type,
|
|
201
211
|
name: content.name ?? '',
|
|
212
|
+
...(content.csv !== undefined ? { csv: content.csv } : {}),
|
|
202
213
|
responseData: componentResponseData,
|
|
203
214
|
};
|
|
204
215
|
|
|
@@ -396,8 +407,9 @@ export default function ExperimentRunner({
|
|
|
396
407
|
<div
|
|
397
408
|
className={` ${
|
|
398
409
|
config.showProgressBar ? '' : 'hidden '
|
|
399
|
-
} px-4 mt-4 sm:mt-12 max-w-2xl mx-auto
|
|
410
|
+
} px-4 mt-4 sm:mt-12 max-w-2xl mx-auto`}
|
|
400
411
|
>
|
|
412
|
+
<div className='flex-1 h-6 bg-gray-200 rounded-full overflow-hidden'>
|
|
401
413
|
<div
|
|
402
414
|
className={`h-full bg-gray-200 rounded-full duration-300 ${
|
|
403
415
|
progress > 0 ? ' border-black border-2' : ''
|
|
@@ -410,6 +422,7 @@ export default function ExperimentRunner({
|
|
|
410
422
|
transition: 'width 300ms ease-in-out',
|
|
411
423
|
}}
|
|
412
424
|
/>
|
|
425
|
+
</div>
|
|
413
426
|
</div>
|
|
414
427
|
{componentToRender}
|
|
415
428
|
</div>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import { VoiceRecorder } from './voicerecorder';
|
|
3
3
|
import { registerSimulation, noopSimulate } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
4
5
|
|
|
6
|
+
registerFlattener('MicrophoneCheck', 'session');
|
|
5
7
|
registerSimulation('MicrophoneCheck', noopSimulate, {});
|
|
6
8
|
|
|
7
9
|
interface MicrophoneDevice {
|
|
@@ -156,7 +158,7 @@ const MicrophoneCheck = ({ next }: { next: (data: object) => void }) => {
|
|
|
156
158
|
<div className='mt-16 flex justify-center'>
|
|
157
159
|
<button
|
|
158
160
|
onClick={() => next({})}
|
|
159
|
-
className='bg-white px-8 py-3 border-2 border-black font-bold text-black text-lg rounded-xl shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none'
|
|
161
|
+
className='bg-white cursor-pointer px-8 py-3 border-2 border-black font-bold text-black text-lg rounded-xl shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none'
|
|
160
162
|
>
|
|
161
163
|
Next
|
|
162
164
|
</button>
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { BaseComponentProps } from '../mod';
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { registerSimulation } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
5
|
+
|
|
6
|
+
registerFlattener('PlainInput', 'session');
|
|
4
7
|
|
|
5
8
|
registerSimulation('PlainInput', (trialProps, _experimentState, simulators, participant) => {
|
|
6
9
|
const result = simulators.respond(trialProps, participant);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { BaseComponentProps, getParam, registerComponentParams } from '../utils/common';
|
|
2
2
|
import Text from '../components/text';
|
|
3
3
|
import { registerSimulation, noopSimulate } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
4
5
|
|
|
6
|
+
registerFlattener('ProlificEnding', null);
|
|
5
7
|
registerSimulation('ProlificEnding', noopSimulate, {});
|
|
6
8
|
|
|
7
9
|
registerComponentParams('ProlificEnding', [
|
package/src/components/quest.tsx
CHANGED
|
@@ -4,6 +4,9 @@ import { ReactQuestionFactory, Survey, SurveyQuestionElementBase } from 'survey-
|
|
|
4
4
|
import { ContrastLight } from 'survey-core/themes';
|
|
5
5
|
import 'survey-core/survey-core.min.css';
|
|
6
6
|
import { registerSimulation } from '../utils/simulation';
|
|
7
|
+
import { registerFlattener } from '../utils/upload';
|
|
8
|
+
|
|
9
|
+
registerFlattener('Quest', 'session');
|
|
7
10
|
|
|
8
11
|
registerSimulation('Quest', (trialProps, _experimentState, simulators, participant) => {
|
|
9
12
|
const responseData: Record<string, any> = {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { BaseComponentProps } from '../mod';
|
|
2
2
|
import React, { useState, useEffect } from 'react';
|
|
3
3
|
import { registerSimulation } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
5
|
+
|
|
6
|
+
registerFlattener('StoreUI', 'storeui');
|
|
4
7
|
|
|
5
8
|
interface BaseFieldConfig {
|
|
6
9
|
type: 'string' | 'integer' | 'float' | 'boolean';
|
package/src/components/text.tsx
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { BaseComponentProps, now } from '../mod';
|
|
3
3
|
import { registerSimulation } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
5
|
+
|
|
6
|
+
registerFlattener('Text', 'text');
|
|
4
7
|
|
|
5
8
|
registerSimulation('Text', (trialProps, _experimentState, simulators, participant) => {
|
|
6
9
|
const result = simulators.respond(trialProps, participant);
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '../utils/common';
|
|
15
15
|
import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js';
|
|
16
16
|
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
|
17
|
-
import { buildUploadFiles, convertArrayOfObjectsToCSV
|
|
17
|
+
import { buildUploadFiles, convertArrayOfObjectsToCSV } from '../utils/upload';
|
|
18
18
|
import { registerSimulation, getBackendUrl, getInitialParticipant } from '../utils/simulation';
|
|
19
19
|
|
|
20
20
|
registerSimulation('Upload', async (trialProps, experimentState, _simulators, participant) => {
|
|
@@ -24,8 +24,7 @@ registerSimulation('Upload', async (trialProps, experimentState, _simulators, pa
|
|
|
24
24
|
data: experimentState.data || [],
|
|
25
25
|
store: experimentState.store,
|
|
26
26
|
generateFiles: trialProps.generateFiles,
|
|
27
|
-
|
|
28
|
-
trialCSVBuilder: trialProps.trialCSVBuilder,
|
|
27
|
+
sessionData: trialProps.sessionData,
|
|
29
28
|
uploadRaw: trialProps.uploadRaw ?? true,
|
|
30
29
|
});
|
|
31
30
|
|
|
@@ -72,21 +71,21 @@ interface UploadResponse {
|
|
|
72
71
|
message?: string;
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
registerComponentParams('Upload', [
|
|
74
|
+
const UPLOAD_PARAMS = [
|
|
77
75
|
{
|
|
78
76
|
name: 'upload',
|
|
79
77
|
defaultValue: true,
|
|
80
|
-
type: 'boolean',
|
|
78
|
+
type: 'boolean' as const,
|
|
81
79
|
description: 'Upload the data at the end of the experiment?',
|
|
82
80
|
},
|
|
83
81
|
{
|
|
84
82
|
name: 'download',
|
|
85
83
|
defaultValue: false,
|
|
86
|
-
type: 'boolean',
|
|
84
|
+
type: 'boolean' as const,
|
|
87
85
|
description: 'Locally download the data at the end of the experiment?',
|
|
88
86
|
},
|
|
89
|
-
]
|
|
87
|
+
];
|
|
88
|
+
registerComponentParams('Upload', UPLOAD_PARAMS);
|
|
90
89
|
|
|
91
90
|
|
|
92
91
|
interface FileBackend {
|
|
@@ -224,18 +223,16 @@ export default function Upload({
|
|
|
224
223
|
store,
|
|
225
224
|
sessionID,
|
|
226
225
|
generateFiles,
|
|
227
|
-
|
|
228
|
-
trialCSVBuilder,
|
|
226
|
+
sessionData,
|
|
229
227
|
uploadRaw = true,
|
|
230
228
|
autoUpload = false,
|
|
231
229
|
androidFolderName,
|
|
232
230
|
}: BaseComponentProps & {
|
|
233
231
|
sessionID?: string | null;
|
|
234
|
-
generateFiles
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
autoUpload: boolean;
|
|
232
|
+
generateFiles?: (sessionID: string, data: TrialData[], store?: Store) => FileUpload[];
|
|
233
|
+
sessionData?: Record<string, any>;
|
|
234
|
+
uploadRaw?: boolean;
|
|
235
|
+
autoUpload?: boolean;
|
|
239
236
|
androidFolderName?: string;
|
|
240
237
|
}) {
|
|
241
238
|
const [uploadState, setUploadState] = useState<'initial' | 'uploading' | 'success' | 'error'>(
|
|
@@ -243,8 +240,10 @@ export default function Upload({
|
|
|
243
240
|
);
|
|
244
241
|
const uploadInitiatedRef = useRef(false);
|
|
245
242
|
|
|
246
|
-
const
|
|
247
|
-
const
|
|
243
|
+
const uploadParam = UPLOAD_PARAMS.find(p => p.name === 'upload')!;
|
|
244
|
+
const downloadParam = UPLOAD_PARAMS.find(p => p.name === 'download')!;
|
|
245
|
+
const shouldUpload = getParam(uploadParam.name, uploadParam.defaultValue, uploadParam.type);
|
|
246
|
+
const shouldDownload = getParam(downloadParam.name, downloadParam.defaultValue, downloadParam.type);
|
|
248
247
|
|
|
249
248
|
const uploadData = useMutation({
|
|
250
249
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -299,8 +298,7 @@ export default function Upload({
|
|
|
299
298
|
data,
|
|
300
299
|
store,
|
|
301
300
|
generateFiles,
|
|
302
|
-
|
|
303
|
-
trialCSVBuilder,
|
|
301
|
+
sessionData,
|
|
304
302
|
uploadRaw,
|
|
305
303
|
});
|
|
306
304
|
|
|
@@ -401,7 +399,7 @@ export default function Upload({
|
|
|
401
399
|
</div>
|
|
402
400
|
<button
|
|
403
401
|
onClick={handleUpload}
|
|
404
|
-
className='px-
|
|
402
|
+
className='cursor-pointer bg-white px-8 py-3 border-2 border-black font-bold text-black text-lg rounded-xl shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-none'
|
|
405
403
|
>
|
|
406
404
|
Try Again
|
|
407
405
|
</button>
|
package/src/index.css
CHANGED
|
@@ -31,26 +31,6 @@
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
@keyframes slideDown {
|
|
35
|
-
0% {
|
|
36
|
-
transform: translateY(-10px);
|
|
37
|
-
opacity: 0;
|
|
38
|
-
}
|
|
39
|
-
100% {
|
|
40
|
-
transform: translateY(0);
|
|
41
|
-
opacity: 1;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
@keyframes fadeIn {
|
|
46
|
-
0% {
|
|
47
|
-
opacity: 0;
|
|
48
|
-
}
|
|
49
|
-
100% {
|
|
50
|
-
opacity: 1;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
34
|
/*
|
|
55
35
|
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
|
56
36
|
so we've added these compatibility styles to make sure everything still
|
package/src/mod.tsx
CHANGED
|
@@ -5,6 +5,7 @@ export type { BaseComponentProps, ExperimentConfig };
|
|
|
5
5
|
export * from './utils/array';
|
|
6
6
|
export * from './utils/common';
|
|
7
7
|
export * from './utils/simulation';
|
|
8
|
+
export { registerFlattener } from './utils/upload';
|
|
8
9
|
export * from './components';
|
|
9
10
|
|
|
10
11
|
export * from 'react-toastify';
|
package/src/utils/bytecode.ts
CHANGED
|
@@ -11,6 +11,8 @@ type BaseTrialData = {
|
|
|
11
11
|
export type ComponentResultData = BaseTrialData & {
|
|
12
12
|
type: string;
|
|
13
13
|
name: string;
|
|
14
|
+
/** Per-trial override for which CSV file(s) this trial's data goes into. When set on a timeline item, overrides the component type's default from the flattener registry. */
|
|
15
|
+
csv?: string | string[];
|
|
14
16
|
responseData?: any;
|
|
15
17
|
metadata?: Record<string, any>;
|
|
16
18
|
};
|
|
@@ -21,16 +23,16 @@ export type CanvasResultData = BaseTrialData & {
|
|
|
21
23
|
reactionTime: number | null;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
|
-
export type
|
|
26
|
+
export type TrialResult = ComponentResultData | CanvasResultData;
|
|
25
27
|
|
|
26
28
|
export interface MarkerItem {
|
|
27
29
|
type: 'MARKER';
|
|
28
30
|
id: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
export type ConditionalFunction = (data?:
|
|
33
|
+
export type ConditionalFunction = (data?: TrialResult[], store?: Store) => boolean;
|
|
32
34
|
|
|
33
|
-
export type StoreUpdateFunction = (data?:
|
|
35
|
+
export type StoreUpdateFunction = (data?: TrialResult[], store?: Store) => Record<string, any>;
|
|
34
36
|
|
|
35
37
|
export interface IfGotoItem {
|
|
36
38
|
type: 'IF_GOTO';
|
|
@@ -65,12 +67,12 @@ export interface ExecuteContentInstruction {
|
|
|
65
67
|
}
|
|
66
68
|
export interface IfGotoInstruction {
|
|
67
69
|
type: 'IfGoto';
|
|
68
|
-
cond: (store: Store, data:
|
|
70
|
+
cond: (store: Store, data: TrialResult[]) => boolean;
|
|
69
71
|
marker: string;
|
|
70
72
|
}
|
|
71
73
|
export interface UpdateStoreInstruction {
|
|
72
74
|
type: 'UpdateStore';
|
|
73
|
-
fun: (store: Store, data:
|
|
75
|
+
fun: (store: Store, data: TrialResult[]) => Store;
|
|
74
76
|
}
|
|
75
77
|
export type UnifiedBytecodeInstruction =
|
|
76
78
|
| ExecuteContentInstruction
|
|
@@ -95,16 +97,16 @@ export function compileTimeline(timeline: TimelineItem[]): {
|
|
|
95
97
|
|
|
96
98
|
function adaptCondition(
|
|
97
99
|
userCondition: ConditionalFunction,
|
|
98
|
-
): (store: Store, data:
|
|
99
|
-
return (runtimeStore: Store, runtimeData:
|
|
100
|
+
): (store: Store, data: TrialResult[]) => boolean {
|
|
101
|
+
return (runtimeStore: Store, runtimeData: TrialResult[]): boolean => {
|
|
100
102
|
return userCondition(runtimeData, runtimeStore);
|
|
101
103
|
};
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
function adaptUpdate(
|
|
105
107
|
userUpdateFunction: StoreUpdateFunction,
|
|
106
|
-
): (store: Store, data:
|
|
107
|
-
return (runtimeStore: Store, runtimeData:
|
|
108
|
+
): (store: Store, data: TrialResult[]) => Store {
|
|
109
|
+
return (runtimeStore: Store, runtimeData: TrialResult[]): Store => {
|
|
108
110
|
const updates = userUpdateFunction(runtimeData, runtimeStore);
|
|
109
111
|
if (typeof updates === 'object' && updates !== null) {
|
|
110
112
|
return {
|
|
@@ -215,7 +217,7 @@ export function compileTimeline(timeline: TimelineItem[]): {
|
|
|
215
217
|
export function applyMetadata<T extends Record<string, any>>(
|
|
216
218
|
trialData: T,
|
|
217
219
|
content: { metadata?: any; nestMetadata?: boolean },
|
|
218
|
-
data:
|
|
220
|
+
data: TrialResult[],
|
|
219
221
|
store: Store,
|
|
220
222
|
): T {
|
|
221
223
|
const metadata = typeof content.metadata === 'function' ? content.metadata(data, store) : content.metadata;
|
|
@@ -228,7 +230,7 @@ export function advanceToNextContent(
|
|
|
228
230
|
bytecode: { instructions: UnifiedBytecodeInstruction[]; markers: Record<string, number> },
|
|
229
231
|
fromPointer: number,
|
|
230
232
|
getStore: () => Store,
|
|
231
|
-
getData: () =>
|
|
233
|
+
getData: () => TrialResult[],
|
|
232
234
|
onUpdateStore: (newStore: Store) => void,
|
|
233
235
|
): number {
|
|
234
236
|
let pointer = fromPointer;
|
package/src/utils/common.ts
CHANGED
|
@@ -24,6 +24,8 @@ export interface TrialData {
|
|
|
24
24
|
trialNumber: number;
|
|
25
25
|
type: string;
|
|
26
26
|
name: string;
|
|
27
|
+
/** Populated from the timeline item's csv field. Overrides the component type's default CSV target from the flattener registry. */
|
|
28
|
+
csv?: string | string[];
|
|
27
29
|
responseData: any;
|
|
28
30
|
start: number;
|
|
29
31
|
end: number;
|
|
@@ -68,13 +70,14 @@ export function getParam<T extends ParamType>(
|
|
|
68
70
|
defaultValue: ParamValue<T>,
|
|
69
71
|
type: T = 'string' as T,
|
|
70
72
|
description?: string,
|
|
73
|
+
uiDefault?: string,
|
|
71
74
|
): ParamValue<T> {
|
|
72
75
|
let registryEntry = sharedRegistry.find((p) => p.name === name);
|
|
73
76
|
|
|
74
77
|
if (!registryEntry) {
|
|
75
78
|
registryEntry = {
|
|
76
79
|
name,
|
|
77
|
-
defaultValue,
|
|
80
|
+
defaultValue: uiDefault !== undefined ? uiDefault : defaultValue,
|
|
78
81
|
type,
|
|
79
82
|
description,
|
|
80
83
|
value: undefined,
|
package/src/utils/simulation.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
compileTimeline,
|
|
5
5
|
advanceToNextContent,
|
|
6
6
|
applyMetadata,
|
|
7
|
-
|
|
7
|
+
TrialResult,
|
|
8
8
|
ComponentResultData,
|
|
9
9
|
TimelineItem,
|
|
10
10
|
Store,
|
|
@@ -23,7 +23,7 @@ export type SimulatorResult = {
|
|
|
23
23
|
export type SimulateFunction = (
|
|
24
24
|
trialProps: Record<string, any>,
|
|
25
25
|
experimentState: {
|
|
26
|
-
data:
|
|
26
|
+
data: TrialResult[];
|
|
27
27
|
store: Store;
|
|
28
28
|
},
|
|
29
29
|
simulators: Record<string, any>,
|
|
@@ -55,7 +55,7 @@ export const noopSimulate: SimulateFunction = (_trialProps, _experimentState, _s
|
|
|
55
55
|
participantState: participant,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
export function resolveSimulation(content: any, data:
|
|
58
|
+
export function resolveSimulation(content: any, data: TrialResult[], store: Store) {
|
|
59
59
|
const trialProps =
|
|
60
60
|
typeof content.props === 'function' ? content.props(data, store) : content.props || {};
|
|
61
61
|
const registration = simulationRegistry[content.type];
|
|
@@ -80,12 +80,12 @@ export function getInitialParticipant() { return _initialParticipant; }
|
|
|
80
80
|
export async function simulateParticipant(
|
|
81
81
|
timeline: TimelineItem[],
|
|
82
82
|
participant: ParticipantState,
|
|
83
|
-
): Promise<
|
|
83
|
+
): Promise<TrialResult[]> {
|
|
84
84
|
_initialParticipant = { ...participant };
|
|
85
85
|
let currentParticipantState = { ...participant };
|
|
86
86
|
const bytecode = compileTimeline(timeline);
|
|
87
87
|
let store: Store = {};
|
|
88
|
-
const data:
|
|
88
|
+
const data: TrialResult[] = [
|
|
89
89
|
{
|
|
90
90
|
index: -1,
|
|
91
91
|
trialNumber: -1,
|
|
@@ -138,6 +138,7 @@ export async function simulateParticipant(
|
|
|
138
138
|
duration,
|
|
139
139
|
type: content.type,
|
|
140
140
|
name: content.name ?? '',
|
|
141
|
+
...(content.csv !== undefined ? { csv: content.csv } : {}),
|
|
141
142
|
responseData: result.responseData,
|
|
142
143
|
};
|
|
143
144
|
|