Skip to content

Commit 66a4ce2

Browse files
Fixes local deployment for jsPsych experiments
The local (non-JATOS) deployment had three issues preventing it from running: ESM export statements in scripts loaded as classic <script> tags, JATOS-dependent initialization in the local code path, and missing plugin scripts and CSS. Adds ESM export stripping in the generator, conditional JATOS/local initialization in the experiment template, gallery bundle copying, plugin type resolution, and forced-choice CSS.
1 parent ecc0cf6 commit 66a4ce2

4 files changed

Lines changed: 85 additions & 6 deletions

File tree

bead/deployment/jspsych/generator.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import annotations
88

99
import json
10+
import re
1011
from pathlib import Path
1112
from uuid import UUID
1213

@@ -203,6 +204,7 @@ def generate(
203204
self._generate_experiment_script()
204205
self._generate_config_file()
205206
self._copy_list_distributor_script()
207+
self._copy_gallery_bundle()
206208

207209
# copy slopit bundle if enabled
208210
if self.config.slopit.enabled:
@@ -395,6 +397,11 @@ def _write_distribution_config(self) -> None:
395397
f"Strategy type: {self.config.distribution_strategy.strategy_type}"
396398
) from e
397399

400+
@staticmethod
401+
def _strip_esm_exports(content: str) -> str:
402+
"""Strip ESM export statements so the file works as a classic <script>."""
403+
return re.sub(r"^export\s*\{[^}]*\}\s*;?\s*$", "", content, flags=re.MULTILINE)
404+
398405
def _copy_list_distributor_script(self) -> None:
399406
"""Copy list_distributor.js from compiled dist/ to js/ directory.
400407
@@ -416,13 +423,33 @@ def _copy_list_distributor_script(self) -> None:
416423
)
417424

418425
try:
419-
output_path.write_text(dist_path.read_text())
426+
content = self._strip_esm_exports(dist_path.read_text())
427+
output_path.write_text(content)
420428
except OSError as e:
421429
raise OSError(
422430
f"Failed to copy list_distributor.js to {output_path}: {e}. "
423431
f"Check write permissions."
424432
) from e
425433

434+
def _copy_gallery_bundle(self) -> None:
435+
"""Copy gallery-bundle.js (IIFE) from dist/ to js/ directory."""
436+
dist_path = Path(__file__).parent / "dist" / "gallery-bundle.global.js"
437+
output_path = self.output_dir / "js" / "gallery-bundle.js"
438+
439+
if not dist_path.exists():
440+
raise FileNotFoundError(
441+
f"gallery-bundle.global.js not found at {dist_path}. "
442+
f"Run 'npm run build:gallery' in the jspsych directory."
443+
)
444+
445+
try:
446+
output_path.write_text(dist_path.read_text())
447+
except OSError as e:
448+
raise OSError(
449+
f"Failed to copy gallery-bundle.js to {output_path}: {e}. "
450+
f"Check write permissions."
451+
) from e
452+
426453
def _create_directory_structure(self) -> None:
427454
"""Create output directory structure.
428455
@@ -606,7 +633,7 @@ def _copy_slopit_bundle(self) -> None:
606633

607634
output_path = self.output_dir / "js" / "slopit-bundle.js"
608635
try:
609-
output_path.write_text(bundle_path.read_text())
636+
output_path.write_text(self._strip_esm_exports(bundle_path.read_text()))
610637
except OSError as e:
611638
raise OSError(
612639
f"Failed to copy slopit bundle to {output_path}: {e}. "
@@ -699,5 +726,5 @@ def _copy_span_plugin_scripts(self, include_wikidata: bool = False) -> None:
699726
src_path = dist_dir / src_name
700727
dest_path = self.output_dir / dest_name
701728
if src_path.exists():
702-
dest_path.write_text(src_path.read_text())
729+
dest_path.write_text(self._strip_esm_exports(src_path.read_text()))
703730
# silently skip if not built yet (TypeScript may not be compiled)

bead/deployment/jspsych/templates/experiment.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
/* Minimal custom styles - let jsPsych handle the layout */
22

3+
/* === Forced choice styles === */
4+
5+
.bead-forced-choice-container { max-width: 720px; margin: 0 auto; }
6+
.bead-forced-choice-prompt { font-size: 1.3em; font-weight: 500; text-align: center; margin-bottom: 24px; color: #212121; }
7+
8+
.bead-forced-choice-alternatives.bead-layout-horizontal { display: flex; gap: 24px; justify-content: center; align-items: stretch; }
9+
.bead-forced-choice-alternatives.bead-layout-vertical { display: flex; flex-direction: column; gap: 16px; align-items: center; }
10+
11+
.bead-card.bead-alternative { background: #fff; border: 2px solid #e0e0e0; border-radius: 12px; padding: 24px; cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s; flex: 1; max-width: 320px; text-align: center; }
12+
.bead-card.bead-alternative:hover { border-color: #90CAF9; box-shadow: 0 2px 12px rgba(25, 118, 210, 0.15); transform: translateY(-2px); }
13+
.bead-card.bead-alternative.selected { border-color: #1976D2; box-shadow: 0 2px 16px rgba(25, 118, 210, 0.25); background: #E3F2FD; }
14+
15+
.bead-alternative-label { font-size: 0.8em; text-transform: uppercase; letter-spacing: 1px; color: #9E9E9E; margin-bottom: 8px; font-weight: 600; }
16+
.bead-alternative-content { font-size: 1.1em; line-height: 1.5; color: #424242; margin-bottom: 16px; }
17+
18+
.bead-button.bead-choice-button { background: #1976D2; color: white; border: none; border-radius: 6px; padding: 8px 24px; font-size: 0.95em; font-weight: 500; cursor: pointer; transition: background 0.2s; }
19+
.bead-button.bead-choice-button:hover { background: #1565C0; }
20+
321
/* === Span labeling styles === */
422

523
/* Token grid */

bead/deployment/jspsych/templates/experiment.js.template

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,22 @@ async function runExperiment() {
2727
// Load items from JSONL
2828
const items = await loadItems('data/items.jsonl');
2929

30-
// Initialize list distributor
30+
{% if use_jatos %}
31+
// Initialize list distributor (JATOS batch session)
3132
distributor = new ListDistributor(distConfig, lists);
3233

3334
// Assign list to this participant
3435
const listIndex = await distributor.initialize();
3536
const assignedList = distributor.getAssignedList();
3637

3738
console.log(`Assigned to list: ${assignedList.name} (index: ${listIndex})`);
39+
{% else %}
40+
// Local mode: use the first (only) list directly
41+
const listIndex = 0;
42+
const assignedList = lists[0];
43+
44+
console.log(`Local mode - using list: ${assignedList.name}`);
45+
{% endif %}
3846

3947
{% if slopit_enabled %}
4048
// Initialize slopit extension
@@ -57,10 +65,12 @@ async function runExperiment() {
5765
on_trial_start: jatos.addAbortButton,
5866
{% endif %}
5967
on_finish: async function() {
68+
{% if use_jatos %}
6069
// Mark as completed in batch session
6170
if (distributor) {
6271
await distributor.markCompleted();
6372
}
73+
{% endif %}
6474

6575
{% if use_jatos %}
6676
// Submit data and end study
@@ -235,9 +245,30 @@ async function runExperiment() {
235245
);
236246
}
237247

238-
// Add pre-generated trials to timeline
248+
// Map plugin name strings to actual plugin classes
249+
const pluginRegistry = {
250+
'bead-forced-choice': BeadForcedChoicePlugin,
251+
'bead-rating': BeadRatingPlugin,
252+
'bead-binary-choice': BeadBinaryChoicePlugin,
253+
'bead-slider-rating': BeadSliderRatingPlugin,
254+
'bead-cloze-multi': BeadClozeMultiPlugin,
255+
'bead-span-label': BeadSpanLabelPlugin,
256+
'bead-categorical': BeadCategoricalPlugin,
257+
'bead-magnitude': BeadMagnitudePlugin,
258+
'bead-free-text': BeadFreeTextPlugin,
259+
'bead-multi-select': BeadMultiSelectPlugin,
260+
};
261+
262+
// Add pre-generated trials to timeline, resolving plugin types
239263
for (const trial of listTrials) {
240-
timeline.push(trial);
264+
const pluginClass = pluginRegistry[trial.type];
265+
if (!pluginClass) {
266+
throw new Error(
267+
`Unknown plugin type: "${trial.type}". ` +
268+
`Available: ${Object.keys(pluginRegistry).join(', ')}`
269+
);
270+
}
271+
timeline.push({ ...trial, type: pluginClass });
241272
}
242273

243274
// Completion

bead/deployment/jspsych/templates/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
<script src="jatos.js"></script>
3535
{% endif %}
3636

37+
<!-- Bead plugins -->
38+
<script src="js/gallery-bundle.js"></script>
39+
3740
<!-- List distribution system -->
3841
<script src="js/list_distributor.js"></script>
3942

0 commit comments

Comments
 (0)