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.
- bead/__init__.py +11 -0
- bead/__main__.py +11 -0
- bead/active_learning/__init__.py +15 -0
- bead/active_learning/config.py +231 -0
- bead/active_learning/loop.py +566 -0
- bead/active_learning/models/__init__.py +24 -0
- bead/active_learning/models/base.py +852 -0
- bead/active_learning/models/binary.py +910 -0
- bead/active_learning/models/categorical.py +943 -0
- bead/active_learning/models/cloze.py +862 -0
- bead/active_learning/models/forced_choice.py +956 -0
- bead/active_learning/models/free_text.py +773 -0
- bead/active_learning/models/lora.py +365 -0
- bead/active_learning/models/magnitude.py +835 -0
- bead/active_learning/models/multi_select.py +795 -0
- bead/active_learning/models/ordinal_scale.py +811 -0
- bead/active_learning/models/peft_adapter.py +155 -0
- bead/active_learning/models/random_effects.py +639 -0
- bead/active_learning/selection.py +354 -0
- bead/active_learning/strategies.py +391 -0
- bead/active_learning/trainers/__init__.py +26 -0
- bead/active_learning/trainers/base.py +210 -0
- bead/active_learning/trainers/data_collator.py +172 -0
- bead/active_learning/trainers/dataset_utils.py +261 -0
- bead/active_learning/trainers/huggingface.py +304 -0
- bead/active_learning/trainers/lightning.py +324 -0
- bead/active_learning/trainers/metrics.py +424 -0
- bead/active_learning/trainers/mixed_effects.py +551 -0
- bead/active_learning/trainers/model_wrapper.py +509 -0
- bead/active_learning/trainers/registry.py +104 -0
- bead/adapters/__init__.py +11 -0
- bead/adapters/huggingface.py +61 -0
- bead/behavioral/__init__.py +116 -0
- bead/behavioral/analytics.py +646 -0
- bead/behavioral/extraction.py +343 -0
- bead/behavioral/merging.py +343 -0
- bead/cli/__init__.py +11 -0
- bead/cli/active_learning.py +513 -0
- bead/cli/active_learning_commands.py +779 -0
- bead/cli/completion.py +359 -0
- bead/cli/config.py +624 -0
- bead/cli/constraint_builders.py +286 -0
- bead/cli/deployment.py +859 -0
- bead/cli/deployment_trials.py +493 -0
- bead/cli/deployment_ui.py +332 -0
- bead/cli/display.py +378 -0
- bead/cli/items.py +960 -0
- bead/cli/items_factories.py +776 -0
- bead/cli/list_constraints.py +714 -0
- bead/cli/lists.py +490 -0
- bead/cli/main.py +430 -0
- bead/cli/models.py +877 -0
- bead/cli/resource_loaders.py +621 -0
- bead/cli/resources.py +1036 -0
- bead/cli/shell.py +356 -0
- bead/cli/simulate.py +840 -0
- bead/cli/templates.py +1158 -0
- bead/cli/training.py +1080 -0
- bead/cli/utils.py +614 -0
- bead/cli/workflow.py +1273 -0
- bead/config/__init__.py +68 -0
- bead/config/active_learning.py +1009 -0
- bead/config/config.py +192 -0
- bead/config/defaults.py +118 -0
- bead/config/deployment.py +217 -0
- bead/config/env.py +147 -0
- bead/config/item.py +45 -0
- bead/config/list.py +193 -0
- bead/config/loader.py +149 -0
- bead/config/logging.py +42 -0
- bead/config/model.py +49 -0
- bead/config/paths.py +46 -0
- bead/config/profiles.py +320 -0
- bead/config/resources.py +47 -0
- bead/config/serialization.py +210 -0
- bead/config/simulation.py +206 -0
- bead/config/template.py +238 -0
- bead/config/validation.py +267 -0
- bead/data/__init__.py +65 -0
- bead/data/base.py +87 -0
- bead/data/identifiers.py +97 -0
- bead/data/language_codes.py +61 -0
- bead/data/metadata.py +270 -0
- bead/data/range.py +123 -0
- bead/data/repository.py +358 -0
- bead/data/serialization.py +249 -0
- bead/data/timestamps.py +89 -0
- bead/data/validation.py +349 -0
- bead/data_collection/__init__.py +11 -0
- bead/data_collection/jatos.py +223 -0
- bead/data_collection/merger.py +154 -0
- bead/data_collection/prolific.py +198 -0
- bead/deployment/__init__.py +5 -0
- bead/deployment/distribution.py +402 -0
- bead/deployment/jatos/__init__.py +1 -0
- bead/deployment/jatos/api.py +200 -0
- bead/deployment/jatos/exporter.py +210 -0
- bead/deployment/jspsych/__init__.py +9 -0
- bead/deployment/jspsych/biome.json +44 -0
- bead/deployment/jspsych/config.py +411 -0
- bead/deployment/jspsych/generator.py +598 -0
- bead/deployment/jspsych/package.json +51 -0
- bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
- bead/deployment/jspsych/randomizer.py +299 -0
- bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
- bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
- bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
- bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
- bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
- bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
- bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
- bead/deployment/jspsych/src/plugins/rating.ts +248 -0
- bead/deployment/jspsych/src/slopit/index.ts +9 -0
- bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
- bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
- bead/deployment/jspsych/templates/experiment.css +1 -0
- bead/deployment/jspsych/templates/experiment.js.template +289 -0
- bead/deployment/jspsych/templates/index.html +51 -0
- bead/deployment/jspsych/templates/randomizer.js +241 -0
- bead/deployment/jspsych/templates/randomizer.js.template +313 -0
- bead/deployment/jspsych/trials.py +723 -0
- bead/deployment/jspsych/tsconfig.json +23 -0
- bead/deployment/jspsych/tsup.config.ts +30 -0
- bead/deployment/jspsych/ui/__init__.py +1 -0
- bead/deployment/jspsych/ui/components.py +383 -0
- bead/deployment/jspsych/ui/styles.py +411 -0
- bead/dsl/__init__.py +80 -0
- bead/dsl/ast.py +168 -0
- bead/dsl/context.py +178 -0
- bead/dsl/errors.py +71 -0
- bead/dsl/evaluator.py +570 -0
- bead/dsl/grammar.lark +81 -0
- bead/dsl/parser.py +231 -0
- bead/dsl/stdlib.py +929 -0
- bead/evaluation/__init__.py +13 -0
- bead/evaluation/convergence.py +485 -0
- bead/evaluation/interannotator.py +398 -0
- bead/items/__init__.py +40 -0
- bead/items/adapters/__init__.py +70 -0
- bead/items/adapters/anthropic.py +224 -0
- bead/items/adapters/api_utils.py +167 -0
- bead/items/adapters/base.py +216 -0
- bead/items/adapters/google.py +259 -0
- bead/items/adapters/huggingface.py +1074 -0
- bead/items/adapters/openai.py +323 -0
- bead/items/adapters/registry.py +202 -0
- bead/items/adapters/sentence_transformers.py +224 -0
- bead/items/adapters/togetherai.py +309 -0
- bead/items/binary.py +515 -0
- bead/items/cache.py +558 -0
- bead/items/categorical.py +593 -0
- bead/items/cloze.py +757 -0
- bead/items/constructor.py +784 -0
- bead/items/forced_choice.py +413 -0
- bead/items/free_text.py +681 -0
- bead/items/generation.py +432 -0
- bead/items/item.py +396 -0
- bead/items/item_template.py +787 -0
- bead/items/magnitude.py +573 -0
- bead/items/multi_select.py +621 -0
- bead/items/ordinal_scale.py +569 -0
- bead/items/scoring.py +448 -0
- bead/items/validation.py +723 -0
- bead/lists/__init__.py +30 -0
- bead/lists/balancer.py +263 -0
- bead/lists/constraints.py +1067 -0
- bead/lists/experiment_list.py +286 -0
- bead/lists/list_collection.py +378 -0
- bead/lists/partitioner.py +1141 -0
- bead/lists/stratification.py +254 -0
- bead/participants/__init__.py +73 -0
- bead/participants/collection.py +699 -0
- bead/participants/merging.py +312 -0
- bead/participants/metadata_spec.py +491 -0
- bead/participants/models.py +276 -0
- bead/resources/__init__.py +29 -0
- bead/resources/adapters/__init__.py +19 -0
- bead/resources/adapters/base.py +104 -0
- bead/resources/adapters/cache.py +128 -0
- bead/resources/adapters/glazing.py +508 -0
- bead/resources/adapters/registry.py +117 -0
- bead/resources/adapters/unimorph.py +796 -0
- bead/resources/classification.py +856 -0
- bead/resources/constraint_builders.py +329 -0
- bead/resources/constraints.py +165 -0
- bead/resources/lexical_item.py +223 -0
- bead/resources/lexicon.py +744 -0
- bead/resources/loaders.py +209 -0
- bead/resources/template.py +441 -0
- bead/resources/template_collection.py +707 -0
- bead/resources/template_generation.py +349 -0
- bead/simulation/__init__.py +29 -0
- bead/simulation/annotators/__init__.py +15 -0
- bead/simulation/annotators/base.py +175 -0
- bead/simulation/annotators/distance_based.py +135 -0
- bead/simulation/annotators/lm_based.py +114 -0
- bead/simulation/annotators/oracle.py +182 -0
- bead/simulation/annotators/random.py +181 -0
- bead/simulation/dsl_extension/__init__.py +3 -0
- bead/simulation/noise_models/__init__.py +13 -0
- bead/simulation/noise_models/base.py +42 -0
- bead/simulation/noise_models/random_noise.py +82 -0
- bead/simulation/noise_models/systematic.py +132 -0
- bead/simulation/noise_models/temperature.py +86 -0
- bead/simulation/runner.py +144 -0
- bead/simulation/strategies/__init__.py +23 -0
- bead/simulation/strategies/base.py +123 -0
- bead/simulation/strategies/binary.py +103 -0
- bead/simulation/strategies/categorical.py +123 -0
- bead/simulation/strategies/cloze.py +224 -0
- bead/simulation/strategies/forced_choice.py +127 -0
- bead/simulation/strategies/free_text.py +105 -0
- bead/simulation/strategies/magnitude.py +116 -0
- bead/simulation/strategies/multi_select.py +129 -0
- bead/simulation/strategies/ordinal_scale.py +131 -0
- bead/templates/__init__.py +27 -0
- bead/templates/adapters/__init__.py +17 -0
- bead/templates/adapters/base.py +128 -0
- bead/templates/adapters/cache.py +178 -0
- bead/templates/adapters/huggingface.py +312 -0
- bead/templates/combinatorics.py +103 -0
- bead/templates/filler.py +605 -0
- bead/templates/renderers.py +177 -0
- bead/templates/resolver.py +178 -0
- bead/templates/strategies.py +1806 -0
- bead/templates/streaming.py +195 -0
- bead-0.1.0.dist-info/METADATA +212 -0
- bead-0.1.0.dist-info/RECORD +231 -0
- bead-0.1.0.dist-info/WHEEL +4 -0
- bead-0.1.0.dist-info/entry_points.txt +2 -0
- bead-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bead-cloze-multi plugin
|
|
3
|
+
*
|
|
4
|
+
* jsPsych plugin for fill-in-the-blank tasks with multiple gaps.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Multiple gaps in text
|
|
8
|
+
* - Dropdown (extensional constraints)
|
|
9
|
+
* - Text input (intensional constraints or free text)
|
|
10
|
+
* - Mixed field types
|
|
11
|
+
* - Per-gap response time tracking
|
|
12
|
+
* - Material Design form controls
|
|
13
|
+
* - Preserves all item and template metadata
|
|
14
|
+
*
|
|
15
|
+
* @author Bead Project
|
|
16
|
+
* @version 0.1.0
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { JsPsych, JsPsychPlugin, PluginInfo } from "../types/jspsych.js";
|
|
20
|
+
|
|
21
|
+
/** Field type for cloze fields */
|
|
22
|
+
type ClozeFieldType = "dropdown" | "text";
|
|
23
|
+
|
|
24
|
+
/** Unfilled slot from bead metadata */
|
|
25
|
+
interface UnfilledSlot {
|
|
26
|
+
slot_name: string;
|
|
27
|
+
position: number;
|
|
28
|
+
constraint_ids: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Field configuration for cloze task */
|
|
32
|
+
export interface ClozeFieldConfig {
|
|
33
|
+
slot_name?: string;
|
|
34
|
+
position?: number;
|
|
35
|
+
type: ClozeFieldType;
|
|
36
|
+
options?: string[];
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Bead item/template metadata */
|
|
41
|
+
interface BeadMetadata {
|
|
42
|
+
unfilled_slots?: UnfilledSlot[];
|
|
43
|
+
[key: string]: unknown;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Cloze trial parameters */
|
|
47
|
+
export interface ClozeTrialParams {
|
|
48
|
+
/** The text with gaps (use %% or {{slot_name}} for gaps) */
|
|
49
|
+
text: string | null;
|
|
50
|
+
/** Field configurations (auto-generated from unfilled_slots) */
|
|
51
|
+
fields: ClozeFieldConfig[];
|
|
52
|
+
/** Whether to require all fields to be filled */
|
|
53
|
+
require_all: boolean;
|
|
54
|
+
/** Text for the continue button */
|
|
55
|
+
button_label: string;
|
|
56
|
+
/** Complete item and template metadata */
|
|
57
|
+
metadata: BeadMetadata;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Per-field responses */
|
|
61
|
+
type ClozeResponses = Record<string, string>;
|
|
62
|
+
|
|
63
|
+
/** Per-field response times */
|
|
64
|
+
type ClozeResponseTimes = Record<string, number>;
|
|
65
|
+
|
|
66
|
+
/** Per-field start times */
|
|
67
|
+
type FieldStartTimes = Record<string, number>;
|
|
68
|
+
|
|
69
|
+
/** Plugin info constant */
|
|
70
|
+
const info: PluginInfo = {
|
|
71
|
+
name: "bead-cloze-multi",
|
|
72
|
+
parameters: {
|
|
73
|
+
text: {
|
|
74
|
+
type: 8, // ParameterType.HTML_STRING
|
|
75
|
+
default: null,
|
|
76
|
+
},
|
|
77
|
+
fields: {
|
|
78
|
+
type: 13, // ParameterType.COMPLEX
|
|
79
|
+
default: [],
|
|
80
|
+
array: true,
|
|
81
|
+
},
|
|
82
|
+
require_all: {
|
|
83
|
+
type: 0, // ParameterType.BOOL
|
|
84
|
+
default: true,
|
|
85
|
+
},
|
|
86
|
+
button_label: {
|
|
87
|
+
type: 1, // ParameterType.STRING
|
|
88
|
+
default: "Continue",
|
|
89
|
+
},
|
|
90
|
+
metadata: {
|
|
91
|
+
type: 12, // ParameterType.OBJECT
|
|
92
|
+
default: {},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* BeadClozeMultiPlugin - jsPsych plugin for fill-in-the-blank tasks
|
|
99
|
+
*/
|
|
100
|
+
class BeadClozeMultiPlugin implements JsPsychPlugin<typeof info, ClozeTrialParams> {
|
|
101
|
+
static info = info;
|
|
102
|
+
|
|
103
|
+
private jsPsych: JsPsych;
|
|
104
|
+
|
|
105
|
+
constructor(jsPsych: JsPsych) {
|
|
106
|
+
this.jsPsych = jsPsych;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
trial(display_element: HTMLElement, trial: ClozeTrialParams): void {
|
|
110
|
+
const responses: ClozeResponses = {};
|
|
111
|
+
const response_times: ClozeResponseTimes = {};
|
|
112
|
+
const field_start_times: FieldStartTimes = {};
|
|
113
|
+
|
|
114
|
+
const start_time = performance.now();
|
|
115
|
+
|
|
116
|
+
// Auto-generate fields from metadata if not provided
|
|
117
|
+
if (trial.fields.length === 0 && trial.metadata.unfilled_slots) {
|
|
118
|
+
trial.fields = trial.metadata.unfilled_slots.map((slot) => ({
|
|
119
|
+
slot_name: slot.slot_name,
|
|
120
|
+
position: slot.position,
|
|
121
|
+
type: (slot.constraint_ids.length > 0 ? "dropdown" : "text") as ClozeFieldType,
|
|
122
|
+
options: [], // Would be populated from constraints in real implementation
|
|
123
|
+
placeholder: slot.slot_name,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Generate HTML
|
|
128
|
+
let html = '<div class="bead-cloze-container">';
|
|
129
|
+
|
|
130
|
+
if (trial.text) {
|
|
131
|
+
// Replace gaps with input fields
|
|
132
|
+
let processed_text = trial.text;
|
|
133
|
+
trial.fields.forEach((field, index) => {
|
|
134
|
+
const field_id = `bead-cloze-field-${index}`;
|
|
135
|
+
let field_html: string;
|
|
136
|
+
|
|
137
|
+
if (field.type === "dropdown" && field.options && field.options.length > 0) {
|
|
138
|
+
// Dropdown field
|
|
139
|
+
const optionsHtml = field.options
|
|
140
|
+
.map((opt) => `<option value="${opt}">${opt}</option>`)
|
|
141
|
+
.join("");
|
|
142
|
+
field_html = `
|
|
143
|
+
<select class="bead-dropdown bead-cloze-field" id="${field_id}" data-field="${index}">
|
|
144
|
+
<option value="">Select...</option>
|
|
145
|
+
${optionsHtml}
|
|
146
|
+
</select>
|
|
147
|
+
`;
|
|
148
|
+
} else {
|
|
149
|
+
// Text input field
|
|
150
|
+
field_html = `
|
|
151
|
+
<input type="text"
|
|
152
|
+
class="bead-text-field bead-cloze-field"
|
|
153
|
+
id="${field_id}"
|
|
154
|
+
data-field="${index}"
|
|
155
|
+
placeholder="${field.placeholder ?? ""}" />
|
|
156
|
+
`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Replace placeholder in text
|
|
160
|
+
const placeholder = field.slot_name ? `{{${field.slot_name}}}` : "%%";
|
|
161
|
+
processed_text = processed_text.replace(placeholder, field_html);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
html += `<div class="bead-cloze-text">${processed_text}</div>`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Continue button
|
|
168
|
+
html += `
|
|
169
|
+
<div class="bead-cloze-button-container">
|
|
170
|
+
<button class="bead-button bead-continue-button" id="bead-cloze-continue" ${trial.require_all ? "disabled" : ""}>
|
|
171
|
+
${trial.button_label}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
html += "</div>"; // Close container
|
|
177
|
+
|
|
178
|
+
display_element.innerHTML = html;
|
|
179
|
+
|
|
180
|
+
// Add event listeners for input fields
|
|
181
|
+
const input_fields = display_element.querySelectorAll<HTMLInputElement | HTMLSelectElement>(
|
|
182
|
+
".bead-cloze-field",
|
|
183
|
+
);
|
|
184
|
+
for (const field of input_fields) {
|
|
185
|
+
const field_index = field.getAttribute("data-field");
|
|
186
|
+
if (field_index === null) continue;
|
|
187
|
+
|
|
188
|
+
// Track when user starts interacting with this field
|
|
189
|
+
field.addEventListener("focus", () => {
|
|
190
|
+
if (field_start_times[field_index] === undefined) {
|
|
191
|
+
field_start_times[field_index] = performance.now();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Track responses
|
|
196
|
+
field.addEventListener("change", () => {
|
|
197
|
+
responses[field_index] = field.value;
|
|
198
|
+
|
|
199
|
+
// Record response time for this field
|
|
200
|
+
const startTime = field_start_times[field_index];
|
|
201
|
+
if (startTime !== undefined) {
|
|
202
|
+
response_times[field_index] = performance.now() - startTime;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
check_completion();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
field.addEventListener("input", () => {
|
|
209
|
+
responses[field_index] = field.value;
|
|
210
|
+
check_completion();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Continue button listener
|
|
215
|
+
const continue_button =
|
|
216
|
+
display_element.querySelector<HTMLButtonElement>("#bead-cloze-continue");
|
|
217
|
+
if (continue_button) {
|
|
218
|
+
continue_button.addEventListener("click", () => {
|
|
219
|
+
end_trial();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const check_completion = (): void => {
|
|
224
|
+
if (trial.require_all && continue_button) {
|
|
225
|
+
// Check if all fields are filled
|
|
226
|
+
const all_filled = trial.fields.every((_field, index) => {
|
|
227
|
+
const response = responses[index.toString()];
|
|
228
|
+
return response !== undefined && response.trim() !== "";
|
|
229
|
+
});
|
|
230
|
+
continue_button.disabled = !all_filled;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const end_trial = (): void => {
|
|
235
|
+
// Gather all responses
|
|
236
|
+
const trial_data: Record<string, unknown> = {
|
|
237
|
+
...trial.metadata, // Preserve all metadata
|
|
238
|
+
responses: responses,
|
|
239
|
+
response_times: response_times,
|
|
240
|
+
total_rt: performance.now() - start_time,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Clear display
|
|
244
|
+
display_element.innerHTML = "";
|
|
245
|
+
|
|
246
|
+
// End trial
|
|
247
|
+
this.jsPsych.finishTrial(trial_data);
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export { BeadClozeMultiPlugin };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bead-forced-choice plugin
|
|
3
|
+
*
|
|
4
|
+
* jsPsych plugin for comparative judgments and forced choice tasks.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Side-by-side stimulus display
|
|
8
|
+
* - Button or keyboard selection
|
|
9
|
+
* - Optional similarity rating after choice
|
|
10
|
+
* - Material Design card layout
|
|
11
|
+
* - Preserves all item and template metadata
|
|
12
|
+
*
|
|
13
|
+
* @author Bead Project
|
|
14
|
+
* @version 0.1.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { JsPsych, JsPsychPlugin, KeyboardResponseInfo, PluginInfo } from "../types/jspsych.js";
|
|
18
|
+
|
|
19
|
+
/** Bead rendered elements from metadata */
|
|
20
|
+
interface RenderedElements {
|
|
21
|
+
[key: string]: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Bead item/template metadata */
|
|
25
|
+
interface BeadMetadata {
|
|
26
|
+
rendered_elements?: RenderedElements;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Position type for left/right alternatives */
|
|
31
|
+
type Position = "left" | "right";
|
|
32
|
+
|
|
33
|
+
/** Forced choice trial parameters */
|
|
34
|
+
export interface ForcedChoiceTrialParams {
|
|
35
|
+
/** The prompt/question to display */
|
|
36
|
+
prompt: string;
|
|
37
|
+
/** Array of alternatives to choose from */
|
|
38
|
+
alternatives: string[];
|
|
39
|
+
/** Whether to randomize left/right position */
|
|
40
|
+
randomize_position: boolean;
|
|
41
|
+
/** Enable keyboard responses (1/2 or left/right arrow) */
|
|
42
|
+
enable_keyboard: boolean;
|
|
43
|
+
/** Whether to require a response */
|
|
44
|
+
require_response: boolean;
|
|
45
|
+
/** Text for the continue button (if applicable) */
|
|
46
|
+
button_label: string;
|
|
47
|
+
/** Complete item and template metadata */
|
|
48
|
+
metadata: BeadMetadata;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Forced choice response data */
|
|
52
|
+
interface ForcedChoiceResponse {
|
|
53
|
+
choice: string | null;
|
|
54
|
+
choice_index: number | null;
|
|
55
|
+
position: Position | null;
|
|
56
|
+
rt: number | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Plugin info constant */
|
|
60
|
+
const info: PluginInfo = {
|
|
61
|
+
name: "bead-forced-choice",
|
|
62
|
+
parameters: {
|
|
63
|
+
prompt: {
|
|
64
|
+
type: 8, // ParameterType.HTML_STRING
|
|
65
|
+
default: "Which do you prefer?",
|
|
66
|
+
},
|
|
67
|
+
alternatives: {
|
|
68
|
+
type: 1, // ParameterType.STRING
|
|
69
|
+
default: [],
|
|
70
|
+
array: true,
|
|
71
|
+
},
|
|
72
|
+
randomize_position: {
|
|
73
|
+
type: 0, // ParameterType.BOOL
|
|
74
|
+
default: true,
|
|
75
|
+
},
|
|
76
|
+
enable_keyboard: {
|
|
77
|
+
type: 0, // ParameterType.BOOL
|
|
78
|
+
default: true,
|
|
79
|
+
},
|
|
80
|
+
require_response: {
|
|
81
|
+
type: 0, // ParameterType.BOOL
|
|
82
|
+
default: true,
|
|
83
|
+
},
|
|
84
|
+
button_label: {
|
|
85
|
+
type: 1, // ParameterType.STRING
|
|
86
|
+
default: "Continue",
|
|
87
|
+
},
|
|
88
|
+
metadata: {
|
|
89
|
+
type: 12, // ParameterType.OBJECT
|
|
90
|
+
default: {},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* BeadForcedChoicePlugin - jsPsych plugin for comparative judgments
|
|
97
|
+
*/
|
|
98
|
+
class BeadForcedChoicePlugin implements JsPsychPlugin<typeof info, ForcedChoiceTrialParams> {
|
|
99
|
+
static info = info;
|
|
100
|
+
|
|
101
|
+
private jsPsych: JsPsych;
|
|
102
|
+
|
|
103
|
+
constructor(jsPsych: JsPsych) {
|
|
104
|
+
this.jsPsych = jsPsych;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
trial(display_element: HTMLElement, trial: ForcedChoiceTrialParams): void {
|
|
108
|
+
const response: ForcedChoiceResponse = {
|
|
109
|
+
choice: null,
|
|
110
|
+
choice_index: null,
|
|
111
|
+
position: null,
|
|
112
|
+
rt: null,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const start_time = performance.now();
|
|
116
|
+
|
|
117
|
+
// Extract alternatives from metadata if not provided
|
|
118
|
+
if (trial.alternatives.length === 0 && trial.metadata.rendered_elements) {
|
|
119
|
+
const elements = trial.metadata.rendered_elements;
|
|
120
|
+
const choice_keys = Object.keys(elements)
|
|
121
|
+
.filter(
|
|
122
|
+
(k) => k.startsWith("choice_") || k.startsWith("option_"), // Fixed: startswith -> startsWith
|
|
123
|
+
)
|
|
124
|
+
.sort();
|
|
125
|
+
|
|
126
|
+
if (choice_keys.length >= 2) {
|
|
127
|
+
trial.alternatives = choice_keys.map((k) => elements[k] ?? "");
|
|
128
|
+
} else {
|
|
129
|
+
// Fallback: use all rendered elements
|
|
130
|
+
trial.alternatives = Object.values(elements);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Randomize position if requested
|
|
135
|
+
let left_index = 0;
|
|
136
|
+
let right_index = 1;
|
|
137
|
+
if (trial.randomize_position && Math.random() < 0.5) {
|
|
138
|
+
left_index = 1;
|
|
139
|
+
right_index = 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create HTML
|
|
143
|
+
let html = '<div class="bead-forced-choice-container">';
|
|
144
|
+
|
|
145
|
+
if (trial.prompt) {
|
|
146
|
+
html += `<div class="bead-forced-choice-prompt">${trial.prompt}</div>`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
html += '<div class="bead-forced-choice-alternatives">';
|
|
150
|
+
|
|
151
|
+
// Left alternative
|
|
152
|
+
html += `
|
|
153
|
+
<div class="bead-card bead-alternative" data-index="${left_index}" data-position="left">
|
|
154
|
+
<div class="bead-alternative-label">Option 1</div>
|
|
155
|
+
<div class="bead-alternative-content">${trial.alternatives[left_index] ?? "Alternative A"}</div>
|
|
156
|
+
<button class="bead-button bead-choice-button" data-index="${left_index}" data-position="left">
|
|
157
|
+
Select
|
|
158
|
+
</button>
|
|
159
|
+
</div>
|
|
160
|
+
`;
|
|
161
|
+
|
|
162
|
+
// Right alternative
|
|
163
|
+
html += `
|
|
164
|
+
<div class="bead-card bead-alternative" data-index="${right_index}" data-position="right">
|
|
165
|
+
<div class="bead-alternative-label">Option 2</div>
|
|
166
|
+
<div class="bead-alternative-content">${trial.alternatives[right_index] ?? "Alternative B"}</div>
|
|
167
|
+
<button class="bead-button bead-choice-button" data-index="${right_index}" data-position="right">
|
|
168
|
+
Select
|
|
169
|
+
</button>
|
|
170
|
+
</div>
|
|
171
|
+
`;
|
|
172
|
+
|
|
173
|
+
html += "</div>"; // Close alternatives
|
|
174
|
+
|
|
175
|
+
html += "</div>"; // Close container
|
|
176
|
+
|
|
177
|
+
display_element.innerHTML = html;
|
|
178
|
+
|
|
179
|
+
// Add event listeners for choice buttons
|
|
180
|
+
const choice_buttons =
|
|
181
|
+
display_element.querySelectorAll<HTMLButtonElement>(".bead-choice-button");
|
|
182
|
+
for (const button of choice_buttons) {
|
|
183
|
+
button.addEventListener("click", (e) => {
|
|
184
|
+
const target = e.target as HTMLButtonElement;
|
|
185
|
+
const indexAttr = target.getAttribute("data-index");
|
|
186
|
+
const positionAttr = target.getAttribute("data-position") as Position | null;
|
|
187
|
+
if (indexAttr !== null && positionAttr !== null) {
|
|
188
|
+
const index = Number.parseInt(indexAttr, 10);
|
|
189
|
+
select_choice(index, positionAttr);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Keyboard listener
|
|
195
|
+
let keyboard_listener: unknown = null;
|
|
196
|
+
if (trial.enable_keyboard) {
|
|
197
|
+
keyboard_listener = this.jsPsych.pluginAPI.getKeyboardResponse({
|
|
198
|
+
callback_function: (info: KeyboardResponseInfo) => {
|
|
199
|
+
const key = info.key;
|
|
200
|
+
if (key === "1" || key === "ArrowLeft") {
|
|
201
|
+
select_choice(left_index, "left");
|
|
202
|
+
} else if (key === "2" || key === "ArrowRight") {
|
|
203
|
+
select_choice(right_index, "right");
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
valid_responses: ["1", "2", "ArrowLeft", "ArrowRight"],
|
|
207
|
+
rt_method: "performance",
|
|
208
|
+
persist: false,
|
|
209
|
+
allow_held_key: false,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const select_choice = (index: number, position: Position): void => {
|
|
214
|
+
// Update response
|
|
215
|
+
response.choice = trial.alternatives[index] ?? null;
|
|
216
|
+
response.choice_index = index;
|
|
217
|
+
response.position = position;
|
|
218
|
+
response.rt = performance.now() - start_time;
|
|
219
|
+
|
|
220
|
+
// Visual feedback
|
|
221
|
+
const alternative_cards =
|
|
222
|
+
display_element.querySelectorAll<HTMLDivElement>(".bead-alternative");
|
|
223
|
+
for (const card of alternative_cards) {
|
|
224
|
+
card.classList.remove("selected");
|
|
225
|
+
}
|
|
226
|
+
const selected_card = display_element.querySelector<HTMLDivElement>(
|
|
227
|
+
`.bead-alternative[data-position="${position}"]`,
|
|
228
|
+
);
|
|
229
|
+
if (selected_card) {
|
|
230
|
+
selected_card.classList.add("selected");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// End trial immediately or after delay
|
|
234
|
+
setTimeout(() => {
|
|
235
|
+
end_trial();
|
|
236
|
+
}, 300); // Small delay for visual feedback
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const end_trial = (): void => {
|
|
240
|
+
// Kill keyboard listener
|
|
241
|
+
if (keyboard_listener) {
|
|
242
|
+
this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboard_listener);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Preserve all metadata and add response data
|
|
246
|
+
const trial_data: Record<string, unknown> = {
|
|
247
|
+
...trial.metadata, // Spread all metadata
|
|
248
|
+
choice: response.choice,
|
|
249
|
+
choice_index: response.choice_index,
|
|
250
|
+
position_chosen: response.position,
|
|
251
|
+
left_index: left_index,
|
|
252
|
+
right_index: right_index,
|
|
253
|
+
rt: response.rt,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Clear display
|
|
257
|
+
display_element.innerHTML = "";
|
|
258
|
+
|
|
259
|
+
// End trial
|
|
260
|
+
this.jsPsych.finishTrial(trial_data);
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export { BeadForcedChoicePlugin };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for bead jsPsych plugins.
|
|
3
|
+
*
|
|
4
|
+
* Tests plugin structure, info validation, and instantiation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test, vi } from "vitest";
|
|
8
|
+
import type { JsPsych } from "../types/jspsych.js";
|
|
9
|
+
import { BeadClozeMultiPlugin } from "./cloze-dropdown.js";
|
|
10
|
+
import { BeadForcedChoicePlugin } from "./forced-choice.js";
|
|
11
|
+
import { BeadRatingPlugin } from "./rating.js";
|
|
12
|
+
|
|
13
|
+
// Mock jsPsych instance
|
|
14
|
+
function createMockJsPsych(): JsPsych {
|
|
15
|
+
return {
|
|
16
|
+
pluginAPI: {
|
|
17
|
+
getKeyboardResponse: vi.fn(),
|
|
18
|
+
cancelKeyboardResponse: vi.fn(),
|
|
19
|
+
},
|
|
20
|
+
finishTrial: vi.fn(),
|
|
21
|
+
} as unknown as JsPsych;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("bead-rating plugin", () => {
|
|
25
|
+
describe("info structure", () => {
|
|
26
|
+
test("has correct plugin name", () => {
|
|
27
|
+
expect(BeadRatingPlugin.info.name).toBe("bead-rating");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("has required parameters", () => {
|
|
31
|
+
const params = BeadRatingPlugin.info.parameters;
|
|
32
|
+
expect(params["prompt"]).toBeDefined();
|
|
33
|
+
expect(params["scale_min"]).toBeDefined();
|
|
34
|
+
expect(params["scale_max"]).toBeDefined();
|
|
35
|
+
expect(params["scale_labels"]).toBeDefined();
|
|
36
|
+
expect(params["require_response"]).toBeDefined();
|
|
37
|
+
expect(params["button_label"]).toBeDefined();
|
|
38
|
+
expect(params["metadata"]).toBeDefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("has correct parameter defaults", () => {
|
|
42
|
+
const params = BeadRatingPlugin.info.parameters;
|
|
43
|
+
expect(params["scale_min"]?.default).toBe(1);
|
|
44
|
+
expect(params["scale_max"]?.default).toBe(7);
|
|
45
|
+
expect(params["require_response"]?.default).toBe(true);
|
|
46
|
+
expect(params["button_label"]?.default).toBe("Continue");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("plugin instantiation", () => {
|
|
51
|
+
test("can be instantiated", () => {
|
|
52
|
+
const mockJsPsych = createMockJsPsych();
|
|
53
|
+
const plugin = new BeadRatingPlugin(mockJsPsych);
|
|
54
|
+
expect(plugin).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("has trial method", () => {
|
|
58
|
+
const mockJsPsych = createMockJsPsych();
|
|
59
|
+
const plugin = new BeadRatingPlugin(mockJsPsych);
|
|
60
|
+
expect(typeof plugin.trial).toBe("function");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("bead-forced-choice plugin", () => {
|
|
66
|
+
describe("info structure", () => {
|
|
67
|
+
test("has correct plugin name", () => {
|
|
68
|
+
expect(BeadForcedChoicePlugin.info.name).toBe("bead-forced-choice");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("has required parameters", () => {
|
|
72
|
+
const params = BeadForcedChoicePlugin.info.parameters;
|
|
73
|
+
expect(params["alternatives"]).toBeDefined();
|
|
74
|
+
expect(params["prompt"]).toBeDefined();
|
|
75
|
+
expect(params["button_label"]).toBeDefined();
|
|
76
|
+
expect(params["require_response"]).toBeDefined();
|
|
77
|
+
expect(params["randomize_position"]).toBeDefined();
|
|
78
|
+
expect(params["enable_keyboard"]).toBeDefined();
|
|
79
|
+
expect(params["metadata"]).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("has correct parameter defaults", () => {
|
|
83
|
+
const params = BeadForcedChoicePlugin.info.parameters;
|
|
84
|
+
expect(params["require_response"]?.default).toBe(true);
|
|
85
|
+
expect(params["randomize_position"]?.default).toBe(true);
|
|
86
|
+
expect(params["enable_keyboard"]?.default).toBe(true);
|
|
87
|
+
expect(params["button_label"]?.default).toBe("Continue");
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("plugin instantiation", () => {
|
|
92
|
+
test("can be instantiated", () => {
|
|
93
|
+
const mockJsPsych = createMockJsPsych();
|
|
94
|
+
const plugin = new BeadForcedChoicePlugin(mockJsPsych);
|
|
95
|
+
expect(plugin).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("has trial method", () => {
|
|
99
|
+
const mockJsPsych = createMockJsPsych();
|
|
100
|
+
const plugin = new BeadForcedChoicePlugin(mockJsPsych);
|
|
101
|
+
expect(typeof plugin.trial).toBe("function");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("bead-cloze-multi plugin", () => {
|
|
107
|
+
describe("info structure", () => {
|
|
108
|
+
test("has correct plugin name", () => {
|
|
109
|
+
expect(BeadClozeMultiPlugin.info.name).toBe("bead-cloze-multi");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("has required parameters", () => {
|
|
113
|
+
const params = BeadClozeMultiPlugin.info.parameters;
|
|
114
|
+
expect(params["text"]).toBeDefined();
|
|
115
|
+
expect(params["fields"]).toBeDefined();
|
|
116
|
+
expect(params["require_all"]).toBeDefined();
|
|
117
|
+
expect(params["button_label"]).toBeDefined();
|
|
118
|
+
expect(params["metadata"]).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("has correct parameter defaults", () => {
|
|
122
|
+
const params = BeadClozeMultiPlugin.info.parameters;
|
|
123
|
+
expect(params["require_all"]?.default).toBe(true);
|
|
124
|
+
expect(params["button_label"]?.default).toBe("Continue");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("plugin instantiation", () => {
|
|
129
|
+
test("can be instantiated", () => {
|
|
130
|
+
const mockJsPsych = createMockJsPsych();
|
|
131
|
+
const plugin = new BeadClozeMultiPlugin(mockJsPsych);
|
|
132
|
+
expect(plugin).toBeDefined();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("has trial method", () => {
|
|
136
|
+
const mockJsPsych = createMockJsPsych();
|
|
137
|
+
const plugin = new BeadClozeMultiPlugin(mockJsPsych);
|
|
138
|
+
expect(typeof plugin.trial).toBe("function");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|