|
1 | | -import { createContext, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; |
2 | | -import { genUserIdDefault } from './utils.js'; |
3 | | -import { Result, db } from './database.js'; |
4 | | -import { DefaultEndScreen, renderDefaultErrorScreen } from './defaults.js'; |
5 | | -import { LoginOptions } from './login.js'; |
6 | | -import { createRoot } from 'react-dom/client'; |
7 | | - |
8 | | -let errorHandlerEffectRun = false; |
9 | | - |
10 | | -interface ExperimentInternals { |
11 | | - currentTask: string; |
12 | | - registerTask: (id: string) => void; |
13 | | - unregisterTask: (id: string) => void; |
14 | | - advance: () => void; |
15 | | - addResult: (taskId: string, screenId: string, key: string, val: string) => void; |
16 | | -} |
17 | | - |
18 | | -interface ExperimentControls { |
19 | | - login: (userId: string) => void; |
20 | | -} |
21 | | - |
22 | | -const ExperimentInternalsDefault: ExperimentInternals = { |
23 | | - currentTask: '', |
24 | | - registerTask: () => { throw new Error('Experiment ancestor component not found.'); }, |
25 | | - unregisterTask: () => { throw new Error('Experiment ancestor component not found.'); }, |
26 | | - advance: () => { throw new Error('Experiment ancestor component not found.'); }, |
27 | | - addResult: () => { throw new Error('Experiment ancestor component not found.'); }, |
28 | | -}; |
29 | | - |
30 | | -const ExperimentControlsDefault: ExperimentControls = { |
31 | | - login: () => { throw new Error('Experiment ancestor component not found.'); }, |
32 | | -}; |
33 | | - |
34 | | -const ExperimentInternalsContext = createContext(ExperimentInternalsDefault); |
35 | | -const ExperimentControlsContext = createContext(ExperimentControlsDefault); |
36 | | - |
37 | | -type ExperimentProps = { |
38 | | - loginOptions?: LoginOptions; |
39 | | - onResultAdded?: (result: Result) => void; |
40 | | - endScreen?: React.ReactNode; |
41 | | - useErrorHandling?: boolean; |
42 | | - renderErrorScreen?: (event: ErrorEvent | PromiseRejectionEvent) => React.ReactNode; |
43 | | - children: React.ReactNode; |
44 | | -}; |
45 | | - |
46 | | -function Experiment({ |
47 | | - loginOptions = { loginType: 'skip', loginComponent: <> </> }, |
48 | | - endScreen = <DefaultEndScreen />, |
49 | | - useErrorHandling = false, |
50 | | - renderErrorScreen = renderDefaultErrorScreen, |
51 | | - ...otherProps |
52 | | -}: ExperimentProps) { |
53 | | - // valid user ID must not be empty |
54 | | - const [userId, setUserId] = useState(''); |
55 | | - const [ended, setEnded] = useState(false); |
56 | | - const allTasksRef = useRef<string[]>([]); |
57 | | - const taskRef = useRef(''); |
58 | | - const [, forceUpdate] = useReducer(x => x + 1, 0); |
59 | | - |
60 | | - useEffect(() => { |
61 | | - if (loginOptions.loginType == 'skip' && userId == '') { |
62 | | - genUserIdDefault() |
63 | | - .then((id) => { |
64 | | - login(id); |
65 | | - }); |
66 | | - } |
67 | | - if (useErrorHandling && !errorHandlerEffectRun) { |
68 | | - errorHandlerEffectRun = true; |
69 | | - window.addEventListener('error', errorListener); |
70 | | - window.addEventListener('unhandledrejection', errorListener); |
71 | | - } |
72 | | - }, []); |
73 | | - |
74 | | - function errorListener(event: ErrorEvent | PromiseRejectionEvent) { |
75 | | - document.body.innerHTML = ''; |
76 | | - const newDiv = document.createElement('div'); |
77 | | - const root = createRoot(newDiv); |
78 | | - root.render( |
79 | | - renderErrorScreen(event), |
80 | | - ); |
81 | | - document.body.appendChild(newDiv); |
82 | | - } |
83 | | - |
84 | | - const registerTask = useCallback((id: string) => { |
85 | | - // no duplicate IDs allowed |
86 | | - if (allTasksRef.current.includes(id)) { |
87 | | - throw new Error(`Task ID '${id}' already exists.`); |
88 | | - } |
89 | | - console.log(`Task registered with ID ${id}.`); |
90 | | - const newTasks = [...allTasksRef.current, id]; |
91 | | - allTasksRef.current = newTasks; |
92 | | - // if this is the first task, select it |
93 | | - if (newTasks.length == 1) { |
94 | | - updateCurrentTask(id); |
95 | | - } |
96 | | - }, [taskRef, allTasksRef]); |
97 | | - |
98 | | - const unregisterTask = useCallback((id: string) => { |
99 | | - // do nothing if ID is not found |
100 | | - if (allTasksRef.current.includes(id)) { |
101 | | - console.log(`Task unregistered with ID ${id}.`); |
102 | | - const newTasks = allTasksRef.current.filter(task => task != id); |
103 | | - allTasksRef.current = newTasks; |
104 | | - if (newTasks.length == 0) { |
105 | | - updateCurrentTask(''); |
106 | | - } |
107 | | - } |
108 | | - }, [taskRef, allTasksRef]); |
109 | | - |
110 | | - function updateCurrentTask(id: string) { |
111 | | - taskRef.current = id; |
112 | | - forceUpdate(); |
113 | | - }; |
114 | | - |
115 | | - const advance = useCallback(() => { |
116 | | - const curIndex = allTasksRef.current.indexOf(taskRef.current); |
117 | | - if (curIndex < allTasksRef.current.length - 1) { |
118 | | - updateCurrentTask(allTasksRef.current[curIndex + 1]); |
119 | | - } |
120 | | - else { |
121 | | - setEnded(true); |
122 | | - } |
123 | | - }, [taskRef, allTasksRef]); |
124 | | - |
125 | | - const addResult = useCallback(async (taskId: string, screenId: string, key: string, val: string) => { |
126 | | - const result: Result = { taskId, screenId, userId, key, val }; |
127 | | - await db.results.add(result); |
128 | | - // TODO: Do I need error handling? |
129 | | - |
130 | | - if (otherProps.onResultAdded) { |
131 | | - otherProps.onResultAdded(result); |
132 | | - } |
133 | | - }, [userId, otherProps.onResultAdded]); |
134 | | - |
135 | | - const experimentInternals = useMemo(() => ({ |
136 | | - currentTask: taskRef.current, |
137 | | - registerTask, |
138 | | - unregisterTask, |
139 | | - advance, |
140 | | - addResult, |
141 | | - }), [taskRef.current, registerTask, unregisterTask, advance, addResult]); |
142 | | - |
143 | | - const login = useCallback((userId: string) => { |
144 | | - setUserId(userId); |
145 | | - }, [setUserId]); |
146 | | - |
147 | | - const experimentControls = useMemo(() => ({ |
148 | | - login, |
149 | | - }), [login]); |
150 | | - |
151 | | - let toDisplay; |
152 | | - if (ended) { |
153 | | - toDisplay = endScreen; |
154 | | - } |
155 | | - else if (userId == '') { |
156 | | - toDisplay = loginOptions.loginComponent; |
157 | | - } |
158 | | - else { |
159 | | - toDisplay = otherProps.children; |
160 | | - } |
| 1 | +import { ExperimentCore, ExperimentProps } from './core.js'; |
161 | 2 |
|
| 3 | +function Experiment(props: ExperimentProps) { |
162 | 4 | return ( |
163 | | - <ExperimentInternalsContext.Provider value={experimentInternals}> |
164 | | - <ExperimentControlsContext.Provider value={experimentControls}> |
165 | | - {toDisplay} |
166 | | - </ExperimentControlsContext.Provider> |
167 | | - </ExperimentInternalsContext.Provider> |
| 5 | + <ExperimentCore dynamic={false} {...props}> |
| 6 | + {props.children} |
| 7 | + </ExperimentCore> |
168 | 8 | ); |
169 | | -}; |
| 9 | +} |
170 | 10 |
|
171 | | -export { ExperimentInternalsContext, ExperimentControlsContext, ExperimentProps, Experiment }; |
| 11 | +export { Experiment }; |
0 commit comments