@adriansteffan/reactive 0.0.43 → 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 +14 -1
- package/README.md +232 -3
- package/dist/{mod-D6W3wq3h.js → mod-D9lwPIrH.js} +6739 -6389
- package/dist/mod.d.ts +70 -22
- package/dist/reactive.es.js +46 -36
- package/dist/reactive.umd.js +40 -38
- package/dist/style.css +1 -1
- package/dist/{web-B1hJOwit.js → web-DUIQX1PV.js} +1 -1
- package/dist/{web-BYSmfdtR.js → web-DXP3LAJm.js} +1 -1
- package/package.json +1 -1
- package/src/components/canvasblock.tsx +125 -74
- package/src/components/checkdevice.tsx +18 -0
- package/src/components/enterfullscreen.tsx +7 -3
- package/src/components/exitfullscreen.tsx +6 -1
- package/src/components/experimentprovider.tsx +7 -2
- package/src/components/experimentrunner.tsx +85 -58
- package/src/components/microphonecheck.tsx +6 -1
- package/src/components/mobilefilepermission.tsx +3 -0
- package/src/components/plaininput.tsx +20 -0
- package/src/components/prolificending.tsx +5 -0
- package/src/components/quest.tsx +60 -0
- package/src/components/storeui.tsx +18 -11
- package/src/components/text.tsx +14 -0
- package/src/components/upload.tsx +69 -286
- package/src/index.css +0 -20
- package/src/mod.tsx +2 -0
- package/src/utils/bytecode.ts +61 -9
- package/src/utils/common.ts +4 -1
- package/src/utils/simulation.ts +269 -0
- package/src/utils/upload.ts +201 -0
- package/template/README.md +59 -0
- package/template/backend/package-lock.json +280 -156
- package/template/backend/src/backend.ts +1 -0
- package/template/package-lock.json +1693 -771
- package/template/package.json +2 -0
- package/template/simulate.ts +15 -0
- package/template/src/Experiment.tsx +62 -5
- package/template/src/main.tsx +1 -1
- package/template/tsconfig.json +2 -3
- package/tsconfig.json +1 -0
- package/vite.config.ts +1 -1
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { BaseComponentProps, getParam, registerComponentParams } from '../utils/common';
|
|
2
2
|
import Text from '../components/text';
|
|
3
|
+
import { registerSimulation, noopSimulate } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
5
|
+
|
|
6
|
+
registerFlattener('ProlificEnding', null);
|
|
7
|
+
registerSimulation('ProlificEnding', noopSimulate, {});
|
|
3
8
|
|
|
4
9
|
registerComponentParams('ProlificEnding', [
|
|
5
10
|
{ name: 'cc', defaultValue: '', type: 'string', description: 'Completion code of the Profilic experiment (more commonly supplied via the code)' },
|
package/src/components/quest.tsx
CHANGED
|
@@ -3,6 +3,66 @@ 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
|
+
import { registerFlattener } from '../utils/upload';
|
|
8
|
+
|
|
9
|
+
registerFlattener('Quest', 'session');
|
|
10
|
+
|
|
11
|
+
registerSimulation('Quest', (trialProps, _experimentState, simulators, participant) => {
|
|
12
|
+
const responseData: Record<string, any> = {};
|
|
13
|
+
let totalDuration = 0;
|
|
14
|
+
const pages = trialProps.surveyJson?.pages || [{ elements: trialProps.surveyJson?.elements || [] }];
|
|
15
|
+
for (const page of pages) {
|
|
16
|
+
for (const el of page.elements || []) {
|
|
17
|
+
if (!el.name) continue;
|
|
18
|
+
const result = simulators.answerQuestion(el, participant);
|
|
19
|
+
participant = result.participantState;
|
|
20
|
+
responseData[el.name] = result.value;
|
|
21
|
+
totalDuration += result.duration ?? 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { responseData, participantState: participant, duration: totalDuration };
|
|
25
|
+
}, {
|
|
26
|
+
answerQuestion: (question: any, participant: any) => {
|
|
27
|
+
let value;
|
|
28
|
+
switch (question.type) {
|
|
29
|
+
case 'rating': {
|
|
30
|
+
const min = question.rateMin ?? 1, max = question.rateMax ?? 5;
|
|
31
|
+
value = min + Math.floor(Math.random() * (max - min + 1));
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case 'boolean': value = Math.random() > 0.5; break;
|
|
35
|
+
case 'text': case 'comment': value = 'simulated_response'; break;
|
|
36
|
+
case 'radiogroup': case 'dropdown': {
|
|
37
|
+
const c = question.choices?.[Math.floor(Math.random() * (question.choices?.length || 0))];
|
|
38
|
+
value = c !== undefined ? (typeof c === 'object' ? c.value : c) : null;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'checkbox': {
|
|
42
|
+
if (question.choices?.length) {
|
|
43
|
+
const n = 1 + Math.floor(Math.random() * question.choices.length);
|
|
44
|
+
value = [...question.choices]
|
|
45
|
+
.sort(() => Math.random() - 0.5).slice(0, n)
|
|
46
|
+
.map((c: any) => typeof c === 'object' ? c.value : c);
|
|
47
|
+
}
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
case 'matrix': {
|
|
51
|
+
if (question.rows?.length && question.columns?.length) {
|
|
52
|
+
value = Object.fromEntries(
|
|
53
|
+
question.rows.map((r: any) => {
|
|
54
|
+
const col = question.columns[Math.floor(Math.random() * question.columns.length)];
|
|
55
|
+
return [typeof r === 'object' ? r.value : r, typeof col === 'object' ? col.value : col];
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
default: value = null;
|
|
62
|
+
}
|
|
63
|
+
return { value, participantState: participant, duration: 1000 + Math.random() * 4000 };
|
|
64
|
+
},
|
|
65
|
+
});
|
|
6
66
|
|
|
7
67
|
type ComponentsMap = {
|
|
8
68
|
[key: string]: ComponentType<any>;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { BaseComponentProps } from '../mod';
|
|
2
2
|
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { registerSimulation } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
5
|
+
|
|
6
|
+
registerFlattener('StoreUI', 'storeui');
|
|
3
7
|
|
|
4
8
|
interface BaseFieldConfig {
|
|
5
9
|
type: 'string' | 'integer' | 'float' | 'boolean';
|
|
@@ -33,6 +37,19 @@ interface BooleanFieldConfig extends BaseFieldConfig {
|
|
|
33
37
|
|
|
34
38
|
type FieldConfig = StringFieldConfig | NumberFieldConfig | BooleanFieldConfig;
|
|
35
39
|
|
|
40
|
+
function buildFieldValues(fields: FieldConfig[], store?: Record<string, any>): Record<string, any> {
|
|
41
|
+
const values: Record<string, any> = {};
|
|
42
|
+
fields.forEach(field => {
|
|
43
|
+
values[field.storeKey] = store?.[field.storeKey] !== undefined ? store[field.storeKey] : field.defaultValue;
|
|
44
|
+
});
|
|
45
|
+
return values;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
registerSimulation('StoreUI', (trialProps, experimentState, _simulators, participant) => {
|
|
49
|
+
const values = buildFieldValues(trialProps.fields || [], experimentState.store);
|
|
50
|
+
return { responseData: values, participantState: participant, storeUpdates: values };
|
|
51
|
+
}, {});
|
|
52
|
+
|
|
36
53
|
interface StoreUIProps extends BaseComponentProps {
|
|
37
54
|
title?: string;
|
|
38
55
|
description?: string;
|
|
@@ -56,17 +73,7 @@ function StoreUI({
|
|
|
56
73
|
|
|
57
74
|
|
|
58
75
|
useEffect(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
fields.forEach(field => {
|
|
62
|
-
|
|
63
|
-
initialValues[field.storeKey] =
|
|
64
|
-
store?.[field.storeKey] !== undefined
|
|
65
|
-
? store[field.storeKey]
|
|
66
|
-
: field.defaultValue;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
setValues(initialValues);
|
|
76
|
+
setValues(buildFieldValues(fields, store));
|
|
70
77
|
|
|
71
78
|
const initialTouched: Record<string, boolean> = {};
|
|
72
79
|
fields.forEach(field => {
|
package/src/components/text.tsx
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { BaseComponentProps, now } from '../mod';
|
|
3
|
+
import { registerSimulation } from '../utils/simulation';
|
|
4
|
+
import { registerFlattener } from '../utils/upload';
|
|
5
|
+
|
|
6
|
+
registerFlattener('Text', 'text');
|
|
7
|
+
|
|
8
|
+
registerSimulation('Text', (trialProps, _experimentState, simulators, participant) => {
|
|
9
|
+
const result = simulators.respond(trialProps, participant);
|
|
10
|
+
return { responseData: result.value, participantState: result.participantState, duration: result.value.reactionTime };
|
|
11
|
+
}, {
|
|
12
|
+
respond: (_input: any, participant: any) => ({
|
|
13
|
+
value: { key: 'button', reactionTime: 500 + Math.random() * 1500 },
|
|
14
|
+
participantState: participant,
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
3
17
|
|
|
4
18
|
function Text({
|
|
5
19
|
content,
|
|
@@ -13,8 +13,53 @@ import {
|
|
|
13
13
|
TrialData,
|
|
14
14
|
} from '../utils/common';
|
|
15
15
|
import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js';
|
|
16
|
-
|
|
17
16
|
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
|
17
|
+
import { buildUploadFiles, convertArrayOfObjectsToCSV } from '../utils/upload';
|
|
18
|
+
import { registerSimulation, getBackendUrl, getInitialParticipant } from '../utils/simulation';
|
|
19
|
+
|
|
20
|
+
registerSimulation('Upload', async (trialProps, experimentState, _simulators, participant) => {
|
|
21
|
+
const sessionID = trialProps.sessionID || `sim_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
22
|
+
const files = buildUploadFiles({
|
|
23
|
+
sessionID,
|
|
24
|
+
data: experimentState.data || [],
|
|
25
|
+
store: experimentState.store,
|
|
26
|
+
generateFiles: trialProps.generateFiles,
|
|
27
|
+
sessionData: trialProps.sessionData,
|
|
28
|
+
uploadRaw: trialProps.uploadRaw ?? true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const initialParticipant = getInitialParticipant();
|
|
32
|
+
if (initialParticipant) {
|
|
33
|
+
files.push({
|
|
34
|
+
filename: `${sessionID}_participant_initial.csv`,
|
|
35
|
+
content: convertArrayOfObjectsToCSV([initialParticipant]),
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
});
|
|
38
|
+
files.push({
|
|
39
|
+
filename: `${sessionID}_participant_final.csv`,
|
|
40
|
+
content: convertArrayOfObjectsToCSV([participant]),
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const backendUrl = getBackendUrl();
|
|
46
|
+
if (backendUrl) {
|
|
47
|
+
const payload = {
|
|
48
|
+
sessionId: sessionID,
|
|
49
|
+
files: files.map((f) => ({ ...f, encoding: f.encoding ?? 'utf8' })),
|
|
50
|
+
};
|
|
51
|
+
const res = await fetch(`${backendUrl}/data`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify(payload),
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
console.error(`Simulation upload failed: ${res.status} ${res.statusText}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { responseData: { files }, participantState: participant };
|
|
62
|
+
}, {});
|
|
18
63
|
|
|
19
64
|
interface UploadPayload {
|
|
20
65
|
sessionId: string;
|
|
@@ -26,195 +71,22 @@ interface UploadResponse {
|
|
|
26
71
|
message?: string;
|
|
27
72
|
}
|
|
28
73
|
|
|
29
|
-
|
|
30
|
-
registerComponentParams('Upload', [
|
|
74
|
+
const UPLOAD_PARAMS = [
|
|
31
75
|
{
|
|
32
76
|
name: 'upload',
|
|
33
77
|
defaultValue: true,
|
|
34
|
-
type: 'boolean',
|
|
78
|
+
type: 'boolean' as const,
|
|
35
79
|
description: 'Upload the data at the end of the experiment?',
|
|
36
80
|
},
|
|
37
81
|
{
|
|
38
82
|
name: 'download',
|
|
39
83
|
defaultValue: false,
|
|
40
|
-
type: 'boolean',
|
|
84
|
+
type: 'boolean' as const,
|
|
41
85
|
description: 'Locally download the data at the end of the experiment?',
|
|
42
86
|
},
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
type DataObject = {
|
|
46
|
-
[key: string]: string | number | boolean | null | undefined;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
function escapeCsvValue(value: any): string {
|
|
50
|
-
if (value === null || value === undefined) {
|
|
51
|
-
return '';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const stringValue = String(value);
|
|
55
|
-
|
|
56
|
-
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
|
|
57
|
-
const escapedValue = stringValue.replace(/"/g, '""');
|
|
58
|
-
return `"${escapedValue}"`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return stringValue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function convertArrayOfObjectsToCSV(data: DataObject[]): string {
|
|
65
|
-
if (!data || data.length === 0) {
|
|
66
|
-
return '';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const headerSet = new Set<string>();
|
|
70
|
-
data.forEach((obj) => {
|
|
71
|
-
Object.keys(obj).forEach((key) => {
|
|
72
|
-
headerSet.add(key);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
const headers = Array.from(headerSet);
|
|
76
|
-
|
|
77
|
-
const headerRow = headers.map((header) => escapeCsvValue(header)).join(',');
|
|
78
|
-
|
|
79
|
-
const dataRows = data.map((obj) => {
|
|
80
|
-
return headers
|
|
81
|
-
.map((header) => {
|
|
82
|
-
const value = obj[header];
|
|
83
|
-
return escapeCsvValue(value);
|
|
84
|
-
})
|
|
85
|
-
.join(',');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
return [headerRow, ...dataRows].join('\n');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// TODO: for better cohesion move this into the components on registration
|
|
92
|
-
const defaultFlatteningFunctions = {
|
|
93
|
-
'CanvasBlock': (item: TrialData) => {
|
|
94
|
-
const responseData = item.responseData;
|
|
95
|
-
if (Array.isArray(responseData)) {
|
|
96
|
-
return responseData.map((i) => ({
|
|
97
|
-
block: item.name,
|
|
98
|
-
...i,
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const transform = ({responseData, ...obj}: any) => ({ ...obj, ...Object.entries(responseData || {}).reduce((acc, [k, v]) => ({...acc, [`data_${k}`]: v}), {}) });
|
|
106
|
-
|
|
107
|
-
function combineTrialsToCsv(
|
|
108
|
-
data: any[],
|
|
109
|
-
filename: string,
|
|
110
|
-
names: string[],
|
|
111
|
-
flatteningFunctions: Record<string, (item: any) => any[] | Record<string, any[]>>,
|
|
112
|
-
fun?: (obj: any) => any,
|
|
113
|
-
): FileUpload | FileUpload[] {
|
|
114
|
-
|
|
115
|
-
// Collect all flattener results first, filtering out completely empty results
|
|
116
|
-
const allResults: (any[] | Record<string, any[]>)[] = names.flatMap((name) => {
|
|
117
|
-
const matchingItems = data.filter((d) => d.name === name);
|
|
118
|
-
|
|
119
|
-
return matchingItems.map((item) => {
|
|
120
|
-
const flattener = item.type && flatteningFunctions[item.type];
|
|
121
|
-
const result = flattener ? flattener(item) : [transform(item)];
|
|
122
|
-
|
|
123
|
-
// Filter out completely empty results
|
|
124
|
-
if (Array.isArray(result) && result.length === 0) {
|
|
125
|
-
return null; // Signal this trial should be completely skipped
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
129
|
-
// Check if all arrays in the object are empty
|
|
130
|
-
const hasAnyData = Object.values(result).some(val =>
|
|
131
|
-
Array.isArray(val) && val.length > 0
|
|
132
|
-
);
|
|
133
|
-
if (!hasAnyData) {
|
|
134
|
-
return null; // Signal this trial should be completely skipped
|
|
135
|
-
}
|
|
136
|
-
}
|
|
87
|
+
];
|
|
88
|
+
registerComponentParams('Upload', UPLOAD_PARAMS);
|
|
137
89
|
|
|
138
|
-
return result;
|
|
139
|
-
});
|
|
140
|
-
}).filter(result => result !== null);
|
|
141
|
-
|
|
142
|
-
// Check if any result is a multi-table object (has string keys with array values)
|
|
143
|
-
const hasMultiTable = allResults.some((result) =>
|
|
144
|
-
result &&
|
|
145
|
-
typeof result === 'object' &&
|
|
146
|
-
!Array.isArray(result) &&
|
|
147
|
-
Object.keys(result).some(key => Array.isArray(result[key]))
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
if (!hasMultiTable) {
|
|
151
|
-
// all results are arrays, combine them into one CSV
|
|
152
|
-
const processedData = allResults
|
|
153
|
-
.flatMap((result) => Array.isArray(result) ? result : [])
|
|
154
|
-
.map((x) => (fun ? fun(x) : x));
|
|
155
|
-
|
|
156
|
-
// Skip creating CSV if all flatteners returned empty arrays
|
|
157
|
-
if (processedData.length === 0) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
filename,
|
|
163
|
-
encoding: 'utf8' as const,
|
|
164
|
-
content: convertArrayOfObjectsToCSV(processedData),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// handle multi-table results
|
|
169
|
-
// Collect all table keys from all results
|
|
170
|
-
const allTableKeys = new Set<string>();
|
|
171
|
-
allResults.forEach((result) => {
|
|
172
|
-
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
173
|
-
Object.keys(result).forEach(key => {
|
|
174
|
-
if (Array.isArray(result[key])) {
|
|
175
|
-
allTableKeys.add(key);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Create separate CSV files for each table key
|
|
182
|
-
const files: FileUpload[] = [];
|
|
183
|
-
|
|
184
|
-
for (const tableKey of allTableKeys) {
|
|
185
|
-
const tableData = allResults.flatMap((result) => {
|
|
186
|
-
if (Array.isArray(result)) {
|
|
187
|
-
// If this result is a simple array, include it in all tables for backward compatibility
|
|
188
|
-
return result;
|
|
189
|
-
} else if (result && typeof result === 'object' && result[tableKey]) {
|
|
190
|
-
// If this result has data for this table key, include it
|
|
191
|
-
return result[tableKey];
|
|
192
|
-
}
|
|
193
|
-
return [];
|
|
194
|
-
}).map((x) => (fun ? fun(x) : x));
|
|
195
|
-
|
|
196
|
-
// Skip creating CSV if all flatteners returned empty arrays for this table
|
|
197
|
-
if (tableData.length === 0) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Remove file extension from filename and add table key
|
|
202
|
-
const baseFilename = filename.replace(/\.csv$/, '');
|
|
203
|
-
|
|
204
|
-
files.push({
|
|
205
|
-
filename: `${baseFilename}_${tableKey}.csv`,
|
|
206
|
-
encoding: 'utf8' as const,
|
|
207
|
-
content: convertArrayOfObjectsToCSV(tableData),
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Return empty array if no files were created
|
|
212
|
-
if (files.length === 0) {
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return files.length === 1 ? files[0] : files;
|
|
217
|
-
}
|
|
218
90
|
|
|
219
91
|
interface FileBackend {
|
|
220
92
|
directoryExists(path: string): Promise<boolean>;
|
|
@@ -345,30 +217,22 @@ const getUniqueDirectoryName = async (
|
|
|
345
217
|
return uniqueSessionID;
|
|
346
218
|
};
|
|
347
219
|
|
|
348
|
-
type CSVBuilder = {
|
|
349
|
-
filename?: string;
|
|
350
|
-
trials?: string[];
|
|
351
|
-
fun?: (row: Record<string, any>) => Record<string, any>;
|
|
352
|
-
};
|
|
353
|
-
|
|
354
220
|
export default function Upload({
|
|
355
221
|
data,
|
|
356
222
|
next,
|
|
357
223
|
store,
|
|
358
224
|
sessionID,
|
|
359
225
|
generateFiles,
|
|
360
|
-
|
|
361
|
-
trialCSVBuilder,
|
|
226
|
+
sessionData,
|
|
362
227
|
uploadRaw = true,
|
|
363
228
|
autoUpload = false,
|
|
364
229
|
androidFolderName,
|
|
365
230
|
}: BaseComponentProps & {
|
|
366
231
|
sessionID?: string | null;
|
|
367
|
-
generateFiles
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
autoUpload: boolean;
|
|
232
|
+
generateFiles?: (sessionID: string, data: TrialData[], store?: Store) => FileUpload[];
|
|
233
|
+
sessionData?: Record<string, any>;
|
|
234
|
+
uploadRaw?: boolean;
|
|
235
|
+
autoUpload?: boolean;
|
|
372
236
|
androidFolderName?: string;
|
|
373
237
|
}) {
|
|
374
238
|
const [uploadState, setUploadState] = useState<'initial' | 'uploading' | 'success' | 'error'>(
|
|
@@ -376,8 +240,10 @@ export default function Upload({
|
|
|
376
240
|
);
|
|
377
241
|
const uploadInitiatedRef = useRef(false);
|
|
378
242
|
|
|
379
|
-
const
|
|
380
|
-
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);
|
|
381
247
|
|
|
382
248
|
const uploadData = useMutation({
|
|
383
249
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -427,97 +293,14 @@ export default function Upload({
|
|
|
427
293
|
|
|
428
294
|
const sessionIDUpload = sessionID ?? uuidv4();
|
|
429
295
|
|
|
430
|
-
const files
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (sessionCSVBuilder) {
|
|
440
|
-
type ParamDetails = {
|
|
441
|
-
value?: any;
|
|
442
|
-
defaultValue: any;
|
|
443
|
-
};
|
|
444
|
-
let paramsDict: Record<string, any> = {};
|
|
445
|
-
const paramsSource: Record<string, ParamDetails | any> | undefined =
|
|
446
|
-
data?.[0]?.responseData?.params;
|
|
447
|
-
if (paramsSource && typeof paramsSource === 'object' && paramsSource !== null) {
|
|
448
|
-
paramsDict = Object.entries(paramsSource).reduce(
|
|
449
|
-
(
|
|
450
|
-
accumulator: Record<string, any>,
|
|
451
|
-
[paramName, paramDetails]: [string, ParamDetails | any],
|
|
452
|
-
) => {
|
|
453
|
-
if (
|
|
454
|
-
paramDetails &&
|
|
455
|
-
typeof paramDetails === 'object' &&
|
|
456
|
-
'defaultValue' in paramDetails
|
|
457
|
-
) {
|
|
458
|
-
const chosenValue = paramDetails.value ?? paramDetails.defaultValue;
|
|
459
|
-
accumulator[paramName] = chosenValue;
|
|
460
|
-
}
|
|
461
|
-
return accumulator;
|
|
462
|
-
},
|
|
463
|
-
{} as Record<string, any>,
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
let content = {
|
|
468
|
-
sessionID: sessionIDUpload,
|
|
469
|
-
userAgent: data[0].responseData.userAgent,
|
|
470
|
-
...paramsDict,
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
sessionCSVBuilder.trials &&
|
|
475
|
-
Array.isArray(sessionCSVBuilder.trials) &&
|
|
476
|
-
sessionCSVBuilder.trials.length > 0
|
|
477
|
-
) {
|
|
478
|
-
for (const trialName of sessionCSVBuilder.trials) {
|
|
479
|
-
const matchingDataElement = data.find((element) => element.name === trialName);
|
|
480
|
-
|
|
481
|
-
if (matchingDataElement?.responseData) {
|
|
482
|
-
if (
|
|
483
|
-
typeof matchingDataElement.responseData === 'object' &&
|
|
484
|
-
matchingDataElement.responseData !== null
|
|
485
|
-
) {
|
|
486
|
-
content = { ...content, ...matchingDataElement.responseData };
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
files.push({
|
|
493
|
-
content: convertArrayOfObjectsToCSV([
|
|
494
|
-
sessionCSVBuilder.fun ? sessionCSVBuilder.fun(content) : content,
|
|
495
|
-
]),
|
|
496
|
-
filename: `${sessionIDUpload}${sessionCSVBuilder.filename}.csv`,
|
|
497
|
-
encoding: 'utf8' as const,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (trialCSVBuilder) {
|
|
502
|
-
for (const builder of trialCSVBuilder.builders) {
|
|
503
|
-
const result = combineTrialsToCsv(
|
|
504
|
-
data,
|
|
505
|
-
`${sessionIDUpload}${builder.filename}.csv`,
|
|
506
|
-
builder.trials ?? [],
|
|
507
|
-
{...defaultFlatteningFunctions, ...trialCSVBuilder.flatteners},
|
|
508
|
-
builder.fun,
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
if (Array.isArray(result)) {
|
|
512
|
-
// Only push files if the array is not empty
|
|
513
|
-
if (result.length > 0) {
|
|
514
|
-
files.push(...result);
|
|
515
|
-
}
|
|
516
|
-
} else {
|
|
517
|
-
files.push(result);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
296
|
+
const files = buildUploadFiles({
|
|
297
|
+
sessionID: sessionIDUpload,
|
|
298
|
+
data,
|
|
299
|
+
store,
|
|
300
|
+
generateFiles,
|
|
301
|
+
sessionData,
|
|
302
|
+
uploadRaw,
|
|
303
|
+
});
|
|
521
304
|
|
|
522
305
|
try {
|
|
523
306
|
const payload: UploadPayload = {
|
|
@@ -616,7 +399,7 @@ export default function Upload({
|
|
|
616
399
|
</div>
|
|
617
400
|
<button
|
|
618
401
|
onClick={handleUpload}
|
|
619
|
-
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'
|
|
620
403
|
>
|
|
621
404
|
Try Again
|
|
622
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
|
@@ -4,6 +4,8 @@ export type { BaseComponentProps, ExperimentConfig };
|
|
|
4
4
|
|
|
5
5
|
export * from './utils/array';
|
|
6
6
|
export * from './utils/common';
|
|
7
|
+
export * from './utils/simulation';
|
|
8
|
+
export { registerFlattener } from './utils/upload';
|
|
7
9
|
export * from './components';
|
|
8
10
|
|
|
9
11
|
export * from 'react-toastify';
|