bead 0.1.0__py3-none-any.whl

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 (231) hide show
  1. bead/__init__.py +11 -0
  2. bead/__main__.py +11 -0
  3. bead/active_learning/__init__.py +15 -0
  4. bead/active_learning/config.py +231 -0
  5. bead/active_learning/loop.py +566 -0
  6. bead/active_learning/models/__init__.py +24 -0
  7. bead/active_learning/models/base.py +852 -0
  8. bead/active_learning/models/binary.py +910 -0
  9. bead/active_learning/models/categorical.py +943 -0
  10. bead/active_learning/models/cloze.py +862 -0
  11. bead/active_learning/models/forced_choice.py +956 -0
  12. bead/active_learning/models/free_text.py +773 -0
  13. bead/active_learning/models/lora.py +365 -0
  14. bead/active_learning/models/magnitude.py +835 -0
  15. bead/active_learning/models/multi_select.py +795 -0
  16. bead/active_learning/models/ordinal_scale.py +811 -0
  17. bead/active_learning/models/peft_adapter.py +155 -0
  18. bead/active_learning/models/random_effects.py +639 -0
  19. bead/active_learning/selection.py +354 -0
  20. bead/active_learning/strategies.py +391 -0
  21. bead/active_learning/trainers/__init__.py +26 -0
  22. bead/active_learning/trainers/base.py +210 -0
  23. bead/active_learning/trainers/data_collator.py +172 -0
  24. bead/active_learning/trainers/dataset_utils.py +261 -0
  25. bead/active_learning/trainers/huggingface.py +304 -0
  26. bead/active_learning/trainers/lightning.py +324 -0
  27. bead/active_learning/trainers/metrics.py +424 -0
  28. bead/active_learning/trainers/mixed_effects.py +551 -0
  29. bead/active_learning/trainers/model_wrapper.py +509 -0
  30. bead/active_learning/trainers/registry.py +104 -0
  31. bead/adapters/__init__.py +11 -0
  32. bead/adapters/huggingface.py +61 -0
  33. bead/behavioral/__init__.py +116 -0
  34. bead/behavioral/analytics.py +646 -0
  35. bead/behavioral/extraction.py +343 -0
  36. bead/behavioral/merging.py +343 -0
  37. bead/cli/__init__.py +11 -0
  38. bead/cli/active_learning.py +513 -0
  39. bead/cli/active_learning_commands.py +779 -0
  40. bead/cli/completion.py +359 -0
  41. bead/cli/config.py +624 -0
  42. bead/cli/constraint_builders.py +286 -0
  43. bead/cli/deployment.py +859 -0
  44. bead/cli/deployment_trials.py +493 -0
  45. bead/cli/deployment_ui.py +332 -0
  46. bead/cli/display.py +378 -0
  47. bead/cli/items.py +960 -0
  48. bead/cli/items_factories.py +776 -0
  49. bead/cli/list_constraints.py +714 -0
  50. bead/cli/lists.py +490 -0
  51. bead/cli/main.py +430 -0
  52. bead/cli/models.py +877 -0
  53. bead/cli/resource_loaders.py +621 -0
  54. bead/cli/resources.py +1036 -0
  55. bead/cli/shell.py +356 -0
  56. bead/cli/simulate.py +840 -0
  57. bead/cli/templates.py +1158 -0
  58. bead/cli/training.py +1080 -0
  59. bead/cli/utils.py +614 -0
  60. bead/cli/workflow.py +1273 -0
  61. bead/config/__init__.py +68 -0
  62. bead/config/active_learning.py +1009 -0
  63. bead/config/config.py +192 -0
  64. bead/config/defaults.py +118 -0
  65. bead/config/deployment.py +217 -0
  66. bead/config/env.py +147 -0
  67. bead/config/item.py +45 -0
  68. bead/config/list.py +193 -0
  69. bead/config/loader.py +149 -0
  70. bead/config/logging.py +42 -0
  71. bead/config/model.py +49 -0
  72. bead/config/paths.py +46 -0
  73. bead/config/profiles.py +320 -0
  74. bead/config/resources.py +47 -0
  75. bead/config/serialization.py +210 -0
  76. bead/config/simulation.py +206 -0
  77. bead/config/template.py +238 -0
  78. bead/config/validation.py +267 -0
  79. bead/data/__init__.py +65 -0
  80. bead/data/base.py +87 -0
  81. bead/data/identifiers.py +97 -0
  82. bead/data/language_codes.py +61 -0
  83. bead/data/metadata.py +270 -0
  84. bead/data/range.py +123 -0
  85. bead/data/repository.py +358 -0
  86. bead/data/serialization.py +249 -0
  87. bead/data/timestamps.py +89 -0
  88. bead/data/validation.py +349 -0
  89. bead/data_collection/__init__.py +11 -0
  90. bead/data_collection/jatos.py +223 -0
  91. bead/data_collection/merger.py +154 -0
  92. bead/data_collection/prolific.py +198 -0
  93. bead/deployment/__init__.py +5 -0
  94. bead/deployment/distribution.py +402 -0
  95. bead/deployment/jatos/__init__.py +1 -0
  96. bead/deployment/jatos/api.py +200 -0
  97. bead/deployment/jatos/exporter.py +210 -0
  98. bead/deployment/jspsych/__init__.py +9 -0
  99. bead/deployment/jspsych/biome.json +44 -0
  100. bead/deployment/jspsych/config.py +411 -0
  101. bead/deployment/jspsych/generator.py +598 -0
  102. bead/deployment/jspsych/package.json +51 -0
  103. bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
  104. bead/deployment/jspsych/randomizer.py +299 -0
  105. bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
  106. bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
  107. bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
  108. bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
  109. bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
  110. bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
  111. bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
  112. bead/deployment/jspsych/src/plugins/rating.ts +248 -0
  113. bead/deployment/jspsych/src/slopit/index.ts +9 -0
  114. bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
  115. bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
  116. bead/deployment/jspsych/templates/experiment.css +1 -0
  117. bead/deployment/jspsych/templates/experiment.js.template +289 -0
  118. bead/deployment/jspsych/templates/index.html +51 -0
  119. bead/deployment/jspsych/templates/randomizer.js +241 -0
  120. bead/deployment/jspsych/templates/randomizer.js.template +313 -0
  121. bead/deployment/jspsych/trials.py +723 -0
  122. bead/deployment/jspsych/tsconfig.json +23 -0
  123. bead/deployment/jspsych/tsup.config.ts +30 -0
  124. bead/deployment/jspsych/ui/__init__.py +1 -0
  125. bead/deployment/jspsych/ui/components.py +383 -0
  126. bead/deployment/jspsych/ui/styles.py +411 -0
  127. bead/dsl/__init__.py +80 -0
  128. bead/dsl/ast.py +168 -0
  129. bead/dsl/context.py +178 -0
  130. bead/dsl/errors.py +71 -0
  131. bead/dsl/evaluator.py +570 -0
  132. bead/dsl/grammar.lark +81 -0
  133. bead/dsl/parser.py +231 -0
  134. bead/dsl/stdlib.py +929 -0
  135. bead/evaluation/__init__.py +13 -0
  136. bead/evaluation/convergence.py +485 -0
  137. bead/evaluation/interannotator.py +398 -0
  138. bead/items/__init__.py +40 -0
  139. bead/items/adapters/__init__.py +70 -0
  140. bead/items/adapters/anthropic.py +224 -0
  141. bead/items/adapters/api_utils.py +167 -0
  142. bead/items/adapters/base.py +216 -0
  143. bead/items/adapters/google.py +259 -0
  144. bead/items/adapters/huggingface.py +1074 -0
  145. bead/items/adapters/openai.py +323 -0
  146. bead/items/adapters/registry.py +202 -0
  147. bead/items/adapters/sentence_transformers.py +224 -0
  148. bead/items/adapters/togetherai.py +309 -0
  149. bead/items/binary.py +515 -0
  150. bead/items/cache.py +558 -0
  151. bead/items/categorical.py +593 -0
  152. bead/items/cloze.py +757 -0
  153. bead/items/constructor.py +784 -0
  154. bead/items/forced_choice.py +413 -0
  155. bead/items/free_text.py +681 -0
  156. bead/items/generation.py +432 -0
  157. bead/items/item.py +396 -0
  158. bead/items/item_template.py +787 -0
  159. bead/items/magnitude.py +573 -0
  160. bead/items/multi_select.py +621 -0
  161. bead/items/ordinal_scale.py +569 -0
  162. bead/items/scoring.py +448 -0
  163. bead/items/validation.py +723 -0
  164. bead/lists/__init__.py +30 -0
  165. bead/lists/balancer.py +263 -0
  166. bead/lists/constraints.py +1067 -0
  167. bead/lists/experiment_list.py +286 -0
  168. bead/lists/list_collection.py +378 -0
  169. bead/lists/partitioner.py +1141 -0
  170. bead/lists/stratification.py +254 -0
  171. bead/participants/__init__.py +73 -0
  172. bead/participants/collection.py +699 -0
  173. bead/participants/merging.py +312 -0
  174. bead/participants/metadata_spec.py +491 -0
  175. bead/participants/models.py +276 -0
  176. bead/resources/__init__.py +29 -0
  177. bead/resources/adapters/__init__.py +19 -0
  178. bead/resources/adapters/base.py +104 -0
  179. bead/resources/adapters/cache.py +128 -0
  180. bead/resources/adapters/glazing.py +508 -0
  181. bead/resources/adapters/registry.py +117 -0
  182. bead/resources/adapters/unimorph.py +796 -0
  183. bead/resources/classification.py +856 -0
  184. bead/resources/constraint_builders.py +329 -0
  185. bead/resources/constraints.py +165 -0
  186. bead/resources/lexical_item.py +223 -0
  187. bead/resources/lexicon.py +744 -0
  188. bead/resources/loaders.py +209 -0
  189. bead/resources/template.py +441 -0
  190. bead/resources/template_collection.py +707 -0
  191. bead/resources/template_generation.py +349 -0
  192. bead/simulation/__init__.py +29 -0
  193. bead/simulation/annotators/__init__.py +15 -0
  194. bead/simulation/annotators/base.py +175 -0
  195. bead/simulation/annotators/distance_based.py +135 -0
  196. bead/simulation/annotators/lm_based.py +114 -0
  197. bead/simulation/annotators/oracle.py +182 -0
  198. bead/simulation/annotators/random.py +181 -0
  199. bead/simulation/dsl_extension/__init__.py +3 -0
  200. bead/simulation/noise_models/__init__.py +13 -0
  201. bead/simulation/noise_models/base.py +42 -0
  202. bead/simulation/noise_models/random_noise.py +82 -0
  203. bead/simulation/noise_models/systematic.py +132 -0
  204. bead/simulation/noise_models/temperature.py +86 -0
  205. bead/simulation/runner.py +144 -0
  206. bead/simulation/strategies/__init__.py +23 -0
  207. bead/simulation/strategies/base.py +123 -0
  208. bead/simulation/strategies/binary.py +103 -0
  209. bead/simulation/strategies/categorical.py +123 -0
  210. bead/simulation/strategies/cloze.py +224 -0
  211. bead/simulation/strategies/forced_choice.py +127 -0
  212. bead/simulation/strategies/free_text.py +105 -0
  213. bead/simulation/strategies/magnitude.py +116 -0
  214. bead/simulation/strategies/multi_select.py +129 -0
  215. bead/simulation/strategies/ordinal_scale.py +131 -0
  216. bead/templates/__init__.py +27 -0
  217. bead/templates/adapters/__init__.py +17 -0
  218. bead/templates/adapters/base.py +128 -0
  219. bead/templates/adapters/cache.py +178 -0
  220. bead/templates/adapters/huggingface.py +312 -0
  221. bead/templates/combinatorics.py +103 -0
  222. bead/templates/filler.py +605 -0
  223. bead/templates/renderers.py +177 -0
  224. bead/templates/resolver.py +178 -0
  225. bead/templates/strategies.py +1806 -0
  226. bead/templates/streaming.py +195 -0
  227. bead-0.1.0.dist-info/METADATA +212 -0
  228. bead-0.1.0.dist-info/RECORD +231 -0
  229. bead-0.1.0.dist-info/WHEEL +4 -0
  230. bead-0.1.0.dist-info/entry_points.txt +2 -0
  231. bead-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Type declarations for jsPsych 8.x plugin API.
3
+ *
4
+ * These types define the interfaces needed for creating jsPsych plugins
5
+ * and interacting with the jsPsych runtime.
6
+ */
7
+
8
+ /** Parameter types available in jsPsych */
9
+ export const ParameterType: {
10
+ readonly BOOL: 0;
11
+ readonly STRING: 1;
12
+ readonly INT: 2;
13
+ readonly FLOAT: 3;
14
+ readonly FUNCTION: 4;
15
+ readonly KEY: 5;
16
+ readonly KEYS: 6;
17
+ readonly SELECT: 7;
18
+ readonly HTML_STRING: 8;
19
+ readonly IMAGE: 9;
20
+ readonly AUDIO: 10;
21
+ readonly VIDEO: 11;
22
+ readonly OBJECT: 12;
23
+ readonly COMPLEX: 13;
24
+ readonly TIMELINE: 14;
25
+ };
26
+
27
+ export type ParameterTypeValue = (typeof ParameterType)[keyof typeof ParameterType];
28
+
29
+ /** Definition of a single parameter in plugin info */
30
+ export interface ParameterInfo {
31
+ type: ParameterTypeValue;
32
+ pretty_name?: string;
33
+ default?: unknown;
34
+ description?: string;
35
+ array?: boolean;
36
+ options?: readonly string[];
37
+ nested?: Record<string, ParameterInfo>;
38
+ }
39
+
40
+ /** Plugin info object that defines the plugin's parameters */
41
+ export interface PluginInfo {
42
+ name: string;
43
+ parameters: Record<string, ParameterInfo>;
44
+ }
45
+
46
+ /** Keyboard response info returned by getKeyboardResponse callback */
47
+ export interface KeyboardResponseInfo {
48
+ key: string;
49
+ rt: number;
50
+ }
51
+
52
+ /** Plugin API methods available to plugins */
53
+ export interface PluginAPI {
54
+ getKeyboardResponse(options: {
55
+ callback_function: (info: KeyboardResponseInfo) => void;
56
+ valid_responses?: string[] | "ALL_KEYS" | "NO_KEYS";
57
+ rt_method?: "performance" | "date";
58
+ persist?: boolean;
59
+ allow_held_key?: boolean;
60
+ minimum_valid_rt?: number;
61
+ }): unknown;
62
+
63
+ cancelKeyboardResponse(listener: unknown): void;
64
+
65
+ cancelAllKeyboardResponses(): void;
66
+
67
+ compareKeys(key1: string | null, key2: string | null): boolean;
68
+
69
+ setTimeout(callback: () => void, delay: number): number;
70
+
71
+ clearAllTimeouts(): void;
72
+ }
73
+
74
+ /** Data API for accessing and modifying experiment data */
75
+ export interface DataAPI {
76
+ get(): {
77
+ json(): string;
78
+ csv(): string;
79
+ values(): TrialData[];
80
+ filter(filters: Record<string, unknown> | ((trial: TrialData) => boolean)): DataAPI;
81
+ filterCustom(fn: (trial: TrialData) => boolean): DataAPI;
82
+ select(column: string): { values: unknown[] };
83
+ count(): number;
84
+ };
85
+
86
+ addProperties(properties: Record<string, unknown>): void;
87
+
88
+ write(data: TrialData): void;
89
+ }
90
+
91
+ /** Trial data object */
92
+ export interface TrialData {
93
+ trial_type?: string;
94
+ trial_index?: number;
95
+ time_elapsed?: number;
96
+ [key: string]: unknown;
97
+ }
98
+
99
+ /** Extension interface for jsPsych extensions */
100
+ export interface JsPsychExtension {
101
+ initialize?(params: Record<string, unknown>): Promise<void> | void;
102
+ on_start?(params: Record<string, unknown>): void;
103
+ on_load?(params: Record<string, unknown>): void;
104
+ on_finish?(params: Record<string, unknown>): Record<string, unknown> | undefined;
105
+ }
106
+
107
+ /** Extension info */
108
+ export interface ExtensionInfo {
109
+ name: string;
110
+ }
111
+
112
+ /** Main jsPsych instance interface */
113
+ export interface JsPsych {
114
+ version(): string;
115
+
116
+ pluginAPI: PluginAPI;
117
+
118
+ data: DataAPI;
119
+
120
+ run(timeline: TimelineNode[]): Promise<void>;
121
+
122
+ finishTrial(data?: Record<string, unknown>): void;
123
+
124
+ endExperiment(end_message?: string, data?: Record<string, unknown>): void;
125
+
126
+ getCurrentTrial(): TimelineNode | null;
127
+
128
+ getDisplayElement(): HTMLElement;
129
+
130
+ getProgressBarCompleted(): number;
131
+
132
+ setProgressBar(value: number): void;
133
+
134
+ timelineVariable(variable_name: string): unknown;
135
+
136
+ pauseExperiment(): void;
137
+
138
+ resumeExperiment(): void;
139
+
140
+ abortCurrentTimeline(): void;
141
+
142
+ abortExperiment(end_message?: string): void;
143
+
144
+ getStartTime(): number;
145
+
146
+ getTotalTime(): number;
147
+
148
+ getDisplayContainerElement(): HTMLElement;
149
+ }
150
+
151
+ /** Timeline node (trial) structure */
152
+ export interface TimelineNode {
153
+ type?: unknown;
154
+ timeline?: TimelineNode[];
155
+ timeline_variables?: Record<string, unknown>[];
156
+ conditional_function?: () => boolean;
157
+ loop_function?: (data: DataAPI) => boolean;
158
+ on_start?: (trial: TimelineNode) => void;
159
+ on_load?: () => void;
160
+ on_finish?: (data: TrialData) => void;
161
+ data?: Record<string, unknown>;
162
+ extensions?: Array<{
163
+ type: unknown;
164
+ params?: Record<string, unknown>;
165
+ }>;
166
+ [key: string]: unknown;
167
+ }
168
+
169
+ /** jsPsych plugin class interface */
170
+ export interface JsPsychPlugin<Info extends PluginInfo, TrialParams = Record<string, unknown>> {
171
+ trial(
172
+ display_element: HTMLElement,
173
+ trial: TrialParams,
174
+ on_load?: () => void,
175
+ ): void | Promise<void>;
176
+ }
177
+
178
+ /** Extract trial parameter types from plugin info */
179
+ export type TrialType<Info extends PluginInfo> = {
180
+ [K in keyof Info["parameters"]]: Info["parameters"][K] extends {
181
+ default: infer D;
182
+ }
183
+ ? Info["parameters"][K]["type"] extends typeof ParameterType.STRING
184
+ ? string
185
+ : Info["parameters"][K]["type"] extends typeof ParameterType.INT
186
+ ? number
187
+ : Info["parameters"][K]["type"] extends typeof ParameterType.FLOAT
188
+ ? number
189
+ : Info["parameters"][K]["type"] extends typeof ParameterType.BOOL
190
+ ? boolean
191
+ : Info["parameters"][K]["type"] extends typeof ParameterType.OBJECT
192
+ ? Record<string, unknown>
193
+ : Info["parameters"][K]["type"] extends typeof ParameterType.HTML_STRING
194
+ ? string
195
+ : D
196
+ : unknown;
197
+ };
198
+
199
+ /** jsPsych module declaration for global access in browser */
200
+ declare global {
201
+ const jsPsychModule: {
202
+ ParameterType: typeof ParameterType;
203
+ };
204
+
205
+ function initJsPsych(options?: {
206
+ display_element?: HTMLElement | string;
207
+ on_trial_start?: (trial: TimelineNode) => void;
208
+ on_trial_finish?: (data: TrialData) => void;
209
+ on_data_update?: (data: TrialData) => void;
210
+ on_interaction_data_update?: (data: Record<string, unknown>) => void;
211
+ on_close?: () => void;
212
+ on_finish?: (data: DataAPI) => void;
213
+ show_progress_bar?: boolean;
214
+ auto_update_progress_bar?: boolean;
215
+ message_progress_bar?: string;
216
+ extensions?: Array<{
217
+ type: unknown;
218
+ params?: Record<string, unknown>;
219
+ }>;
220
+ override_safe_mode?: boolean;
221
+ case_sensitive_responses?: boolean;
222
+ minimum_valid_rt?: number;
223
+ experiment_width?: number;
224
+ }): JsPsych;
225
+
226
+ /** jsPsych HTML keyboard response plugin */
227
+ const jsPsychHtmlKeyboardResponse: unknown;
228
+ }
@@ -0,0 +1 @@
1
+ /* Minimal custom styles - let jsPsych handle the layout */
@@ -0,0 +1,289 @@
1
+ /**
2
+ * jsPsych 8.x Batch Experiment
3
+ * Generated by bead deployment system
4
+ *
5
+ * Experiment: {{ title }}
6
+ * Description: {{ description }}
7
+ */
8
+
9
+ // Global variables
10
+ let distributor = null;
11
+ {% if slopit_enabled %}
12
+ let slopitExtension = null;
13
+ {% endif %}
14
+
15
+ /**
16
+ * Main experiment entry point.
17
+ * Loads data, assigns list, and runs experiment.
18
+ */
19
+ async function runExperiment() {
20
+ try {
21
+ // Load distribution config
22
+ const distConfig = await fetch('data/distribution.json').then(r => r.json());
23
+
24
+ // Load lists from JSONL
25
+ const lists = await loadLists('data/lists.jsonl');
26
+
27
+ // Load items from JSONL
28
+ const items = await loadItems('data/items.jsonl');
29
+
30
+ // Initialize list distributor
31
+ distributor = new ListDistributor(distConfig, lists);
32
+
33
+ // Assign list to this participant
34
+ const listIndex = await distributor.initialize();
35
+ const assignedList = distributor.getAssignedList();
36
+
37
+ console.log(`Assigned to list: ${assignedList.name} (index: ${listIndex})`);
38
+
39
+ {% if slopit_enabled %}
40
+ // Initialize slopit extension
41
+ slopitExtension = new SlopitExtension({
42
+ keystroke: {{ slopit_config.keystroke | tojson }},
43
+ focus: {{ slopit_config.focus | tojson }},
44
+ paste: {{ slopit_config.paste | tojson }},
45
+ targetSelectors: {{ slopit_config.target_selectors | tojson }}
46
+ });
47
+ {% endif %}
48
+
49
+ // Initialize jsPsych
50
+ const jsPsych = initJsPsych({
51
+ {% if slopit_enabled %}
52
+ extensions: [
53
+ { type: slopitExtension }
54
+ ],
55
+ {% endif %}
56
+ {% if use_jatos %}
57
+ on_trial_start: jatos.addAbortButton,
58
+ {% endif %}
59
+ on_finish: async function() {
60
+ // Mark as completed in batch session
61
+ if (distributor) {
62
+ await distributor.markCompleted();
63
+ }
64
+
65
+ {% if use_jatos %}
66
+ // Submit data and end study
67
+ const resultData = jsPsych.data.get().json();
68
+ {% if on_finish_url %}
69
+ jatos.submitResultData(resultData)
70
+ .then(() => jatos.endStudyAndRedirect("{{ on_finish_url }}"))
71
+ .catch((error) => {
72
+ console.error("Failed to submit data:", error);
73
+ jatos.endStudyAndRedirect("{{ on_finish_url }}");
74
+ });
75
+ {% else %}
76
+ jatos.submitResultData(resultData)
77
+ .then(() => jatos.endStudy(true))
78
+ .catch((error) => {
79
+ console.error("Failed to submit data:", error);
80
+ jatos.endStudy(false);
81
+ });
82
+ {% endif %}
83
+ {% else %}
84
+ {% if on_finish_url %}
85
+ window.location.href = "{{ on_finish_url }}";
86
+ {% endif %}
87
+ {% endif %}
88
+ },
89
+ show_progress_bar: {{ 'true' if show_progress_bar else 'false' }},
90
+ auto_update_progress_bar: {{ 'true' if show_progress_bar else 'false' }}
91
+ });
92
+
93
+ {% if use_jatos %}
94
+ // Add JATOS metadata
95
+ jsPsych.data.addProperties({
96
+ experiment_title: "{{ title }}",
97
+ participant_id: jatos.urlQueryParameters.PROLIFIC_PID || jatos.workerId,
98
+ prolific_pid: jatos.urlQueryParameters.PROLIFIC_PID || null,
99
+ prolific_study_id: jatos.urlQueryParameters.STUDY_ID || null,
100
+ prolific_session_id: jatos.urlQueryParameters.SESSION_ID || null,
101
+ jatos_worker_id: jatos.workerId,
102
+ jatos_study_result_id: jatos.studyResultId,
103
+ jatos_component_result_id: jatos.componentResultId,
104
+ assigned_list_index: listIndex,
105
+ assigned_list_name: assignedList.name,
106
+ assigned_list_id: assignedList.id,
107
+ experiment_start_time: new Date().toISOString(),
108
+ });
109
+ {% else %}
110
+ jsPsych.data.addProperties({
111
+ experiment_title: "{{ title }}",
112
+ assigned_list_index: listIndex,
113
+ assigned_list_name: assignedList.name,
114
+ assigned_list_id: assignedList.id,
115
+ experiment_start_time: new Date().toISOString(),
116
+ });
117
+ {% endif %}
118
+
119
+ // Build timeline
120
+ const timeline = [];
121
+
122
+ // Demographics form (if enabled, appears before instructions)
123
+ {% if demographics_enabled %}
124
+ timeline.push({
125
+ type: jsPsychSurvey,
126
+ title: "{{ demographics_title }}",
127
+ pages: [
128
+ [
129
+ {% for field in demographics_fields %}
130
+ {
131
+ name: "{{ field.name }}",
132
+ prompt: "{{ field.label }}",
133
+ required: {{ 'true' if field.required else 'false' }},
134
+ {% if field.field_type == 'text' %}
135
+ type: 'text'{% if field.placeholder %},
136
+ placeholder: "{{ field.placeholder }}"{% endif %}
137
+ {% elif field.field_type == 'number' %}
138
+ type: 'text',
139
+ input_type: 'number'{% if field.range_min is defined %},
140
+ min: {{ field.range_min }},
141
+ max: {{ field.range_max }}{% endif %}{% if field.placeholder %},
142
+ placeholder: "{{ field.placeholder }}"{% endif %}
143
+ {% elif field.field_type == 'dropdown' %}
144
+ type: 'drop-down',
145
+ options: {{ field.options | tojson }}
146
+ {% elif field.field_type == 'radio' %}
147
+ type: 'multi-choice',
148
+ options: {{ field.options | tojson }}
149
+ {% elif field.field_type == 'checkbox' %}
150
+ type: 'multi-select',
151
+ options: {{ field.options | tojson }}
152
+ {% endif %}
153
+ }{% if not loop.last %},{% endif %}
154
+ {% endfor %}
155
+ ]
156
+ ],
157
+ button_label_finish: "{{ demographics_submit_text }}",
158
+ data: {
159
+ trial_type: 'demographics'
160
+ }
161
+ });
162
+ {% endif %}
163
+
164
+ // Instructions
165
+ {% if instructions_is_multi_page %}
166
+ // Multi-page instructions using jsPsych instructions plugin
167
+ timeline.push({
168
+ type: jsPsychInstructions,
169
+ pages: [
170
+ {% for page in instructions_pages %}
171
+ `<div class="instructions-page">
172
+ {% if page.title %}<h2>{{ page.title }}</h2>{% endif %}
173
+ <div>{{ page.content }}</div>
174
+ {% if instructions_show_page_numbers and instructions_pages|length > 1 %}
175
+ <p class="page-number">Page {{ loop.index }} of {{ instructions_pages|length }}</p>
176
+ {% endif %}
177
+ </div>`{% if not loop.last %},{% endif %}
178
+ {% endfor %}
179
+ ],
180
+ show_clickable_nav: true,
181
+ allow_backward: {{ 'true' if instructions_allow_backwards else 'false' }},
182
+ button_label_next: "{{ instructions_button_next }}",
183
+ button_label_previous: "Previous",
184
+ button_label_finish: "{{ instructions_button_finish }}",
185
+ data: {
186
+ trial_type: 'instructions'
187
+ }
188
+ });
189
+ {% elif instructions %}
190
+ // Simple single-page instructions
191
+ timeline.push({
192
+ type: jsPsychHtmlKeyboardResponse,
193
+ stimulus: `
194
+ <div class="instructions">
195
+ <h2>Instructions</h2>
196
+ <p>{{ instructions }}</p>
197
+ <p><em>Press any key to continue</em></p>
198
+ </div>
199
+ `,
200
+ data: {
201
+ trial_type: 'instructions'
202
+ }
203
+ });
204
+ {% endif %}
205
+
206
+ // Get items for assigned list
207
+ const listItemIds = assignedList.item_refs;
208
+ if (!listItemIds || listItemIds.length === 0) {
209
+ throw new Error(
210
+ `Assigned list '${assignedList.name}' has no items. ` +
211
+ `Check that lists were created with items before generate().`
212
+ );
213
+ }
214
+
215
+ // Look up items by UUID
216
+ const listItems = listItemIds.map(itemId => {
217
+ const item = items[itemId];
218
+ if (!item) {
219
+ throw new Error(
220
+ `Item ${itemId} referenced in list '${assignedList.name}' not found in items.jsonl. ` +
221
+ `Verify all items were serialized correctly.`
222
+ );
223
+ }
224
+ return item;
225
+ });
226
+
227
+ // Load pre-generated trials for this list
228
+ const trialsData = await fetch('data/trials.json').then(r => r.json());
229
+ const listTrials = trialsData[assignedList.id];
230
+
231
+ if (!listTrials || listTrials.length === 0) {
232
+ throw new Error(
233
+ `No trials found for list '${assignedList.name}' (id: ${assignedList.id}). ` +
234
+ `Ensure trials were generated correctly.`
235
+ );
236
+ }
237
+
238
+ // Add pre-generated trials to timeline
239
+ for (const trial of listTrials) {
240
+ timeline.push(trial);
241
+ }
242
+
243
+ // Completion
244
+ timeline.push({
245
+ type: jsPsychHtmlKeyboardResponse,
246
+ stimulus: `
247
+ <div class="completion">
248
+ <h2>✓ Complete</h2>
249
+ <p>Thank you for participating!</p>
250
+ <p>Your responses have been recorded.</p>
251
+ {% if on_finish_url %}
252
+ <p><em>You will be redirected shortly...</em></p>
253
+ {% else %}
254
+ <p><em>You may close this window.</em></p>
255
+ {% endif %}
256
+ </div>
257
+ `,
258
+ choices: "NO_KEYS",
259
+ trial_duration: {% if on_finish_url %}2000{% else %}null{% endif %},
260
+ data: {
261
+ trial_type: 'completion'
262
+ }
263
+ });
264
+
265
+ // Run experiment
266
+ await jsPsych.run(timeline);
267
+
268
+ } catch (error) {
269
+ console.error('Experiment error:', error);
270
+ document.body.innerHTML = `
271
+ <div style="max-width: 600px; margin: 100px auto; padding: 20px; border: 2px solid #d00; background: #fee;">
272
+ <h2>Experiment Error</h2>
273
+ <p><strong>Error:</strong> ${error.message}</p>
274
+ <p>Please contact the experiment administrator.</p>
275
+ <pre style="background: #fff; padding: 10px; overflow: auto;">${error.stack}</pre>
276
+ </div>
277
+ `;
278
+ }
279
+ }
280
+
281
+ {% if use_jatos %}
282
+ // Wait for JATOS to load
283
+ jatos.onLoad(() => {
284
+ runExperiment();
285
+ });
286
+ {% else %}
287
+ // Run immediately
288
+ runExperiment();
289
+ {% endif %}
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{{ title }}</title>
7
+
8
+ <!-- jsPsych 8.x CSS -->
9
+ <link href="https://unpkg.com/jspsych@8.0.0/css/jspsych.css" rel="stylesheet" type="text/css" />
10
+
11
+ <!-- Material Design Icons (optional) -->
12
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
13
+
14
+ <!-- Google Fonts -->
15
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
16
+
17
+ <!-- Custom experiment CSS -->
18
+ <link href="css/experiment.css" rel="stylesheet" type="text/css" />
19
+
20
+ <!-- jsPsych 8.x Core -->
21
+ <script src="https://unpkg.com/jspsych@8.0.0"></script>
22
+
23
+ <!-- jsPsych 8.x Plugins -->
24
+ <script src="https://unpkg.com/@jspsych/plugin-html-keyboard-response@2.0.0"></script>
25
+ <script src="https://unpkg.com/@jspsych/plugin-html-button-response@2.0.0"></script>
26
+ <script src="https://unpkg.com/@jspsych/plugin-html-slider-response@2.0.0"></script>
27
+ <script src="https://unpkg.com/@jspsych/plugin-preload@2.0.0"></script>
28
+
29
+ <!-- Seedrandom for reproducible randomization -->
30
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
31
+
32
+ {% if use_jatos %}
33
+ <!-- JATOS integration -->
34
+ <script src="jatos.js"></script>
35
+ {% endif %}
36
+
37
+ <!-- List distribution system -->
38
+ <script src="js/list_distributor.js"></script>
39
+
40
+ {% if slopit_enabled %}
41
+ <!-- Slopit behavioral capture -->
42
+ <script src="js/slopit-bundle.js"></script>
43
+ {% endif %}
44
+ </head>
45
+ <body class="theme-{{ ui_theme }}">
46
+ <div id="jspsych-target"></div>
47
+
48
+ <!-- Main experiment script -->
49
+ <script src="js/experiment.js"></script>
50
+ </body>
51
+ </html>