Skip to content

sweetbean.stimulus.RSVP

BilateralRSVP

Bases: _BaseStimulus

Two-stream (left/right) wrapper around window.jsPsychBilateralRsvp. You provide left/right item lists (or Variables). The wrapper composes targets/distractors using convenience arrays (scalars/lists/Variables) and delegates to the base RSVP plugin.

Streams are pure content; all decoration/color/HTML is via params.

Source code in sweetbean/stimulus/RSVP.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
class BilateralRSVP(_BaseStimulus):
    """
    Two-stream (left/right) wrapper around window.jsPsychBilateralRsvp.
    You provide left/right item lists (or Variables). The wrapper composes
    targets/distractors using convenience arrays (scalars/lists/Variables) and
    delegates to the base RSVP plugin.

    Streams are **pure content**; all decoration/color/HTML is via params.
    """

    type = "jsPsychBilateralRsvp"

    def __init__(
        self,
        # ---------------- Content ----------------
        left: Any,
        right: Any,
        # ---------------- Targets (scalars OR lists OR Variables) ----------------
        target_side: Optional[Union[StrLike, StrListLike]] = "left",  # "left" | "right"
        target_index: Optional[Union[IntLike, IntListLike]] = 0,
        target_shape: Optional[
            Union[StrLike, StrListLike]
        ] = "circle",  # "circle"|"square"|"underline"|"none"
        target_color: Optional[Union[CssLike, List[CssLike], Var]] = None,
        target_html: Optional[Union[HtmlLike, List[HtmlLike], Var]] = None,
        # ---------------- Distractors (scalars OR lists OR Variables) ----------------
        distractor_index: Optional[Union[IntLike, IntListLike]] = None,
        distractor_side: Optional[
            Union[StrLike, StrListLike]
        ] = None,  # if omitted, plugin infers opposite of target_side
        distractor_shape: Optional[Union[StrLike, StrListLike]] = None,
        distractor_color: Optional[Union[CssLike, List[CssLike], Var]] = None,
        distractor_html: Optional[Union[HtmlLike, List[HtmlLike], Var]] = None,
        # ---------------- Presentation/timing ----------------
        stimulus_duration: IntLike = 100,
        isi: IntLike = 0,
        choices: Union[StrLike, List[StrLike]] = "ALL",
        mask_html: Optional[HtmlLike] = None,
        color: CssLike = "#ffffff",
        background: CssLike = "#000000",
        token_box_size: CssLike = "18vmin",
        token_font_size: CssLike = "10vmin",
        token_padding: CssLike = "0.25em 0.45em",
        trial_duration: Optional[IntLike] = None,
        # ---------------- Convenience ----------------
        end_on_response: BoolLike = False,  # maps to response_ends_trial in underlying plugin
        # ---------------- SweetBean generic ----------------
        duration: Optional[IntLike] = None,
        side_effects: Optional[Dict[str, Any]] = None,
    ):
        """
        Parameters
        ----------
        left, right : list | Variable
            Content lists for the left/right streams (e.g., ["O","Q","O",...]).
            May be Variables that evaluate to such lists.

        target_side : "left" | "right" | list[...] | Variable
            Side(s) containing the target(s). Lists broadcast with other target_* arrays.
        target_index : int | list[int|Variable] | Variable
            Index/indices of target(s) within the chosen stream(s).
        target_shape : {"circle","square","underline","none"} | list[...] | Variable
            Per-item decoration; "none" means color-only (no outline).
        target_color : CSS color | list[...] | Variable
            Per-item color (glyph color when shape == "none"; else border/underline color).
        target_html : str (template or full HTML) | list[...] | Variable
            If includes {{content}} / {CONTENT}, wraps the stream glyph; otherwise full override.

        distractor_index : int | list[int|Variable] | Variable
            Index/indices of distractor(s). If omitted and you set target_index,
            Bilateral defaults to the same index as each target (per-item).
        distractor_side : "left" | "right" | list[...] | Variable
            Side(s) for distractor(s). If omitted and target_side is provided,
            Bilateral infers the **opposite** side for each item.
        distractor_shape : {"circle","square","underline","none"} | list[...] | Variable
            Per-item distractor decoration.
        distractor_color : CSS color | list[...] | Variable
            Per-item distractor color (glyph if shape=="none"; else border/underline).
        distractor_html : str (template/full) | list[...] | Variable
            Per-item HTML for distractors (same templating as targets).

        stimulus_duration : int | Variable
            Milliseconds each token is displayed.
        isi : int | Variable
            Inter-stimulus interval (ms) between tokens.
        choices : "ALL" | "NO_KEYS" | list[str] | Variable
            Allowed keys during RSVP. Use "NO_KEYS" when collecting responses afterward.
        mask_html : str | None | Variable
            HTML mask shown during ISI (e.g., "•").
        color, background, token_box_size, token_font_size, token_padding : CSS | Variable
            Visual defaults passed through to the underlying RSVP plugin.
        trial_duration : int | None | Variable
            Hard stop for the RSVP (ms).

        end_on_response : bool | Variable
            Convenience; maps to the underlying plugin’s `response_ends_trial`.

        duration : int | None | Variable
            SweetBean alias mirrored into `trial_duration`.
        side_effects :
            Optional side-effect configuration passed to the runtime. This expects
                a list of SideEffect definitions (see SweetBean docs) which can be
                used to update global data like overall score or trial counter.
        """
        if distractor_index is None:
            distractor_index = target_index
        super().__init__(locals(), side_effects)

    def _add_special_param(self):
        # Mirror SweetBean `duration` → jsPsych `trial_duration`
        if self.arg_js.get("duration") not in (None, "null"):
            self.arg_js["trial_duration"] = self.arg_js["duration"]

        # Map convenience alias end_on_response -> response_ends_trial (passed through)
        try:
            if "end_on_response" in self.arg_js:
                self.arg_js["response_ends_trial"] = bool(
                    self.arg_js["end_on_response"]
                )
        except Exception:
            pass

    def _process_response(self):
        # The underlying RSVP provides key_press/rt; add SweetBean convenience fields:
        self.js_data += 'data["bean_key"] = data["key_press"];'
        self.js_data += 'data["bean_rt"] = data["rt"];'
        self.js_data += (
            'data["bean_any_hit"] = '
            '(Array.isArray(data["targets"]) && data["targets"].some(t => t.hit));'
        )

    def _set_before(self):
        pass

    def process_l(self, prompts, get_input, multi_turn, datum=None):
        raise NotImplementedError

__init__(left, right, target_side='left', target_index=0, target_shape='circle', target_color=None, target_html=None, distractor_index=None, distractor_side=None, distractor_shape=None, distractor_color=None, distractor_html=None, stimulus_duration=100, isi=0, choices='ALL', mask_html=None, color='#ffffff', background='#000000', token_box_size='18vmin', token_font_size='10vmin', token_padding='0.25em 0.45em', trial_duration=None, end_on_response=False, duration=None, side_effects=None)

Parameters

left, right : list | Variable Content lists for the left/right streams (e.g., ["O","Q","O",...]). May be Variables that evaluate to such lists.

"left" | "right" | list[...] | Variable

Side(s) containing the target(s). Lists broadcast with other target_* arrays.

target_index : int | list[int|Variable] | Variable Index/indices of target(s) within the chosen stream(s). target_shape : {"circle","square","underline","none"} | list[...] | Variable Per-item decoration; "none" means color-only (no outline). target_color : CSS color | list[...] | Variable Per-item color (glyph color when shape == "none"; else border/underline color). target_html : str (template or full HTML) | list[...] | Variable If includes {{content}} / {CONTENT}, wraps the stream glyph; otherwise full override.

int | list[int|Variable] | Variable

Index/indices of distractor(s). If omitted and you set target_index, Bilateral defaults to the same index as each target (per-item).

distractor_side : "left" | "right" | list[...] | Variable Side(s) for distractor(s). If omitted and target_side is provided, Bilateral infers the opposite side for each item. distractor_shape : {"circle","square","underline","none"} | list[...] | Variable Per-item distractor decoration. distractor_color : CSS color | list[...] | Variable Per-item distractor color (glyph if shape=="none"; else border/underline). distractor_html : str (template/full) | list[...] | Variable Per-item HTML for distractors (same templating as targets).

int | Variable

Milliseconds each token is displayed.

isi : int | Variable Inter-stimulus interval (ms) between tokens. choices : "ALL" | "NO_KEYS" | list[str] | Variable Allowed keys during RSVP. Use "NO_KEYS" when collecting responses afterward. mask_html : str | None | Variable HTML mask shown during ISI (e.g., "•"). color, background, token_box_size, token_font_size, token_padding : CSS | Variable Visual defaults passed through to the underlying RSVP plugin. trial_duration : int | None | Variable Hard stop for the RSVP (ms).

bool | Variable

Convenience; maps to the underlying plugin’s response_ends_trial.

int | None | Variable

SweetBean alias mirrored into trial_duration.

side_effects : Optional side-effect configuration passed to the runtime. This expects a list of SideEffect definitions (see SweetBean docs) which can be used to update global data like overall score or trial counter.

Source code in sweetbean/stimulus/RSVP.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def __init__(
    self,
    # ---------------- Content ----------------
    left: Any,
    right: Any,
    # ---------------- Targets (scalars OR lists OR Variables) ----------------
    target_side: Optional[Union[StrLike, StrListLike]] = "left",  # "left" | "right"
    target_index: Optional[Union[IntLike, IntListLike]] = 0,
    target_shape: Optional[
        Union[StrLike, StrListLike]
    ] = "circle",  # "circle"|"square"|"underline"|"none"
    target_color: Optional[Union[CssLike, List[CssLike], Var]] = None,
    target_html: Optional[Union[HtmlLike, List[HtmlLike], Var]] = None,
    # ---------------- Distractors (scalars OR lists OR Variables) ----------------
    distractor_index: Optional[Union[IntLike, IntListLike]] = None,
    distractor_side: Optional[
        Union[StrLike, StrListLike]
    ] = None,  # if omitted, plugin infers opposite of target_side
    distractor_shape: Optional[Union[StrLike, StrListLike]] = None,
    distractor_color: Optional[Union[CssLike, List[CssLike], Var]] = None,
    distractor_html: Optional[Union[HtmlLike, List[HtmlLike], Var]] = None,
    # ---------------- Presentation/timing ----------------
    stimulus_duration: IntLike = 100,
    isi: IntLike = 0,
    choices: Union[StrLike, List[StrLike]] = "ALL",
    mask_html: Optional[HtmlLike] = None,
    color: CssLike = "#ffffff",
    background: CssLike = "#000000",
    token_box_size: CssLike = "18vmin",
    token_font_size: CssLike = "10vmin",
    token_padding: CssLike = "0.25em 0.45em",
    trial_duration: Optional[IntLike] = None,
    # ---------------- Convenience ----------------
    end_on_response: BoolLike = False,  # maps to response_ends_trial in underlying plugin
    # ---------------- SweetBean generic ----------------
    duration: Optional[IntLike] = None,
    side_effects: Optional[Dict[str, Any]] = None,
):
    """
    Parameters
    ----------
    left, right : list | Variable
        Content lists for the left/right streams (e.g., ["O","Q","O",...]).
        May be Variables that evaluate to such lists.

    target_side : "left" | "right" | list[...] | Variable
        Side(s) containing the target(s). Lists broadcast with other target_* arrays.
    target_index : int | list[int|Variable] | Variable
        Index/indices of target(s) within the chosen stream(s).
    target_shape : {"circle","square","underline","none"} | list[...] | Variable
        Per-item decoration; "none" means color-only (no outline).
    target_color : CSS color | list[...] | Variable
        Per-item color (glyph color when shape == "none"; else border/underline color).
    target_html : str (template or full HTML) | list[...] | Variable
        If includes {{content}} / {CONTENT}, wraps the stream glyph; otherwise full override.

    distractor_index : int | list[int|Variable] | Variable
        Index/indices of distractor(s). If omitted and you set target_index,
        Bilateral defaults to the same index as each target (per-item).
    distractor_side : "left" | "right" | list[...] | Variable
        Side(s) for distractor(s). If omitted and target_side is provided,
        Bilateral infers the **opposite** side for each item.
    distractor_shape : {"circle","square","underline","none"} | list[...] | Variable
        Per-item distractor decoration.
    distractor_color : CSS color | list[...] | Variable
        Per-item distractor color (glyph if shape=="none"; else border/underline).
    distractor_html : str (template/full) | list[...] | Variable
        Per-item HTML for distractors (same templating as targets).

    stimulus_duration : int | Variable
        Milliseconds each token is displayed.
    isi : int | Variable
        Inter-stimulus interval (ms) between tokens.
    choices : "ALL" | "NO_KEYS" | list[str] | Variable
        Allowed keys during RSVP. Use "NO_KEYS" when collecting responses afterward.
    mask_html : str | None | Variable
        HTML mask shown during ISI (e.g., "•").
    color, background, token_box_size, token_font_size, token_padding : CSS | Variable
        Visual defaults passed through to the underlying RSVP plugin.
    trial_duration : int | None | Variable
        Hard stop for the RSVP (ms).

    end_on_response : bool | Variable
        Convenience; maps to the underlying plugin’s `response_ends_trial`.

    duration : int | None | Variable
        SweetBean alias mirrored into `trial_duration`.
    side_effects :
        Optional side-effect configuration passed to the runtime. This expects
            a list of SideEffect definitions (see SweetBean docs) which can be
            used to update global data like overall score or trial counter.
    """
    if distractor_index is None:
        distractor_index = target_index
    super().__init__(locals(), side_effects)

RSVP

Bases: _BaseStimulus

General RSVP wrapper for your jsPsych plugin: window.jsPsychRsvp (plugin name: "rsvp" / class var: jsPsychRsvp)

Streams are pure content (letters/digits). All shapes/colors/HTML wrapping are specified via target/distractor parameters (explicit arrays OR convenience arrays that accept scalars/lists/Variables and are broadcast by the plugin).

No short-form normalization is performed here—Variables pass through untouched.

Source code in sweetbean/stimulus/RSVP.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class RSVP(_BaseStimulus):
    """
    General RSVP wrapper for your jsPsych plugin: window.jsPsychRsvp
    (plugin name: "rsvp" / class var: jsPsychRsvp)

    Streams are **pure content** (letters/digits). All shapes/colors/HTML wrapping
    are specified via target/distractor parameters (explicit arrays OR convenience
    arrays that accept scalars/lists/Variables and are broadcast by the plugin).

    No short-form normalization is performed here—Variables pass through untouched.
    """

    type = "jsPsychRsvp"

    def __init__(
        self,
        # ---------------- Appearance / layout ----------------
        background: CssLike = "#000000",
        color: CssLike = "#ffffff",
        direction: StrLike = "row",  # "row" | "column"
        stream_order: Optional[StrLike] = None,  # e.g., "left,right"
        gap: CssLike = "6rem",
        # ---------------- Token sizing ----------------
        token_box_size: CssLike = "18vmin",  # fixed square box
        token_font_size: CssLike = "10vmin",  # glyph size
        token_padding: CssLike = "0.25em 0.45em",  # inner padding for outlines
        # ---------------- Streams & timing ----------------
        streams: Optional[List[Any]] = None,  # preferred: [{"id":..., "items":[...]}]
        stimulus_duration: IntLike = 100,  # ms each token is shown
        isi: IntLike = 0,  # ms between tokens (SOA = dur + isi)
        mask_html: Optional[HtmlLike] = None,  # HTML shown during ISI (e.g., "•")
        # ---------------- Responses ----------------
        choices: Union[StrLike, List[StrLike]] = "ALL",  # "ALL" | "NO_KEYS" | ["f","j"]
        end_on_response: BoolLike = False,  # convenience → response_ends_trial
        response_window: Optional[IntLike] = None,  # ms; None = unlimited
        correct_keys: Optional[StrLike] = None,  # e.g., "f,j" for scoring
        # ---------------- Targets (timing + decoration) ----------------
        decorate_targets: BoolLike = True,  # show decoration if shape != "none"
        target_shape: StrLike = "none",  # default shape if per-target omits
        target_stroke: CssLike = "3px",  # outline/underline thickness
        targets: Optional[
            List[Dict[str, Any]]
        ] = None,  # explicit array [{stream_id, index, ...}]
        # Convenience (scalars OR lists OR Variables) — broadcast by plugin:
        target_index: Optional[
            Union[IntLike, IntListLike]
        ] = None,  # number | [number|Var,...]
        target_side: Optional[
            Union[StrLike, StrListLike]
        ] = None,  # stream ids ("left"/"right"/custom)
        target_color: Optional[
            Union[CssLike, List[CssLike], Var]
        ] = None,  # CSS color; with shape:"none" colors text
        target_html: Optional[
            Union[HtmlLike, List[HtmlLike], Var]
        ] = None,  # HTML template or full override
        # ---------------- Distractors (decoration only) ----------------
        decorate_distractors: BoolLike = False,
        distractor_shape: StrLike = "none",
        distractor_color: CssLike = "#888888",
        distractor_stroke: CssLike = "2px",
        distractors: Optional[List[Dict[str, Any]]] = None,  # explicit array
        # Convenience (scalars OR lists OR Variables) — broadcast by plugin:
        distractor_index: Optional[Union[IntLike, IntListLike]] = None,
        distractor_side: Optional[Union[StrLike, StrListLike]] = None,  # stream ids
        distractor_color2: Optional[
            Union[CssLike, List[CssLike], Var]
        ] = None,  # per-item override (else distractor_color)
        distractor_html: Optional[
            Union[HtmlLike, List[HtmlLike], Var]
        ] = None,  # HTML template or full override
        # ---------------- Lifetime & data ----------------
        trial_duration: Optional[
            IntLike
        ] = None,  # hard stop; else ends after last token
        record_timestamps: BoolLike = True,  # include per-token onset/offset in data["schedule"]
        # ---------------- SweetBean generic ----------------
        duration: Optional[IntLike] = None,  # alias mirrored into trial_duration
        side_effects: Optional[Dict[str, Any]] = None,
    ):
        """
        Parameters
        ----------
        background : CSS color | Variable
            Background color for the RSVP screen (e.g., "#000", "black").
        color : CSS color | Variable
            Default text/border color (used when a per-item color is not provided).
        direction : {"row","column"} | Variable
            Layout of streams (left–right for "row", top–bottom for "column").
        stream_order : str | None | Variable
            Comma-separated DOM order of stream IDs (e.g., "left,right").
            If omitted and there are exactly two streams with row layout, it is
            auto-filled from the two stream IDs (e.g., "left,right").
        gap : CSS length | Variable
            Gap between streams in non-bilateral layouts.

        token_box_size : CSS length | Variable
            Size of the fixed token box (prevents wobble when borders appear).
        token_font_size : CSS length | Variable
            Font size for the glyphs inside each token box.
        token_padding : CSS length | Variable
            Inner padding used by outlined/underlined shapes.

        streams : list | Variable
            Per-trial stream specs. Prefer object form:
              [{"id":"left","items":["O","Q",...]}, {"id":"right","items":["1","2",...]}]
            Streams are **pure content**; do NOT embed "circle"/"square"/colors here.

        stimulus_duration : int | Variable
            Milliseconds each token is displayed.
        isi : int | Variable
            Inter-stimulus interval (ms) between tokens.
        mask_html : str | None | Variable
            Optional HTML shown during the ISI (e.g., "•").

        choices : "ALL" | "NO_KEYS" | list[str] | Variable
            Allowed keys during RSVP. Use "NO_KEYS" when you collect responses afterward.
        end_on_response : bool | Variable
            If True, RSVP ends immediately after the first valid keypress
            (mapped to plugin's `response_ends_trial`).
        response_window : int | None | Variable
            Time window (ms) for scoring target hits; None = unlimited.
        correct_keys : str | None | Variable
            Comma-separated keys used for per-target hit scoring (optional).

        decorate_targets : bool | Variable
            Whether to render target decorations (ignored if shape == "none").
        target_shape : {"circle","square","underline","none"} | Variable
            Default shape if a target omits its own shape.
        target_stroke : CSS length | Variable
            Outline/underline thickness for targets.
        targets : list[dict] | None
            Explicit target list. Each item may include:
              stream_id, index, label, response_window, correct_keys,
              shape, color, stroke, padding, html, style, className.

        target_index : int | list[int|Variable] | Variable
            Convenience form—position(s) of target(s) in their streams.
        target_side : str | list[str|Variable] | Variable
            Convenience—stream id(s) (e.g., "left"/"right"/custom ids).
        target_color : CSS color | list | Variable
            Per-item color. If the item's shape is "none", this colors the glyph itself.
        target_html : str (template) | list[str] | Variable
            Per-item HTML. If it contains {{content}} or {CONTENT}, the stream item’s
            text is injected; otherwise treated as full override.

        decorate_distractors : bool | Variable
            Whether to render distractor decorations.
        distractor_shape : {"circle","square","underline","none"} | Variable
            Default shape for distractors that omit a shape.
        distractor_color : CSS color | Variable
            Default distractor color (border or underline; or glyph if shape == "none").
        distractor_stroke : CSS length | Variable
            Outline/underline thickness for distractors.
        distractors : list[dict] | None
            Explicit distractor list. Each item may include:
              stream_id, index, label, shape, color, stroke, padding, html, style, className.

        distractor_index : int | list[int|Variable] | Variable
            Convenience—position(s) of distractor(s).
        distractor_side : str | list[str|Variable] | Variable
            Convenience—stream id(s) for distractor(s).
        distractor_color2 : CSS color | list | Variable
            Per-item color override (falls back to `distractor_color` if not set).
        distractor_html : str (template) | list[str] | Variable
            Per-item HTML for distractors (same templating as target_html).

        trial_duration : int | None | Variable
            Hard stop (ms). If None, the trial ends after the last token (+ ISI).
        record_timestamps : bool | Variable
            If True, includes per-token onsets/offsets in data["schedule"].

        duration : int | None | Variable
            SweetBean alias mirrored into `trial_duration`.
        side_effects : dict | None
            Optional side effects passed along at runtime.
        """
        if streams is None:
            streams = []
        if targets is None:
            targets = []
        if distractors is None:
            distractors = []
        if distractor_index is None and distractors is None:
            distractor_index = target_index

        # ⛔️ Do NOT normalize/transform streams here—must support Variables unchanged.
        super().__init__(locals(), side_effects)

    # ---- SweetBean hooks ----

    def _add_special_param(self):
        # Mirror SweetBean `duration` → jsPsych `trial_duration`
        if self.arg_js.get("duration") not in (None, "null"):
            self.arg_js["trial_duration"] = self.arg_js["duration"]

        # Map convenience alias end_on_response -> response_ends_trial
        try:
            if "end_on_response" in self.arg_js:
                self.arg_js["response_ends_trial"] = bool(
                    self.arg_js["end_on_response"]
                )
        except Exception:
            pass

        # Auto stream_order if exactly two streams in row layout and not explicitly set
        try:
            streams = self.arg_js.get("streams") or []
            if (
                (not self.arg_js.get("stream_order"))
                and self.arg_js.get("direction", "row") == "row"
                and isinstance(streams, list)
                and len(streams) == 2
            ):
                a, b = (streams[0] or {}).get("id"), (streams[1] or {}).get("id")
                if a and b:
                    self.arg_js["stream_order"] = f"{a},{b}"
        except Exception:
            pass

        # If any distractor lacks index AND there is exactly one target, copy target's index
        try:
            tlist = self.arg_js.get("targets") or []
            dlist = self.arg_js.get("distractors") or []
            if (
                isinstance(tlist, list)
                and len(tlist) == 1
                and isinstance(dlist, list)
                and len(dlist) > 0
            ):
                t_idx = tlist[0].get("index", None)
                for d in dlist:
                    if "index" not in d or d.get("index") in (None, "null"):
                        d["index"] = t_idx
                self.arg_js["distractors"] = dlist
        except Exception:
            pass

    def _process_response(self):
        # Add SweetBean-style convenience fields to data
        self.js_data += 'data["bean_key"] = data["key_press"];'
        self.js_data += 'data["bean_rt"] = data["rt"];'
        self.js_data += (
            'data["bean_any_hit"] = '
            '(Array.isArray(data["targets"]) && data["targets"].some(t => t.hit));'
        )

    def _set_before(self):
        pass

    def process_l(self, prompts, get_input, multi_turn, datum=None):
        raise NotImplementedError

__init__(background='#000000', color='#ffffff', direction='row', stream_order=None, gap='6rem', token_box_size='18vmin', token_font_size='10vmin', token_padding='0.25em 0.45em', streams=None, stimulus_duration=100, isi=0, mask_html=None, choices='ALL', end_on_response=False, response_window=None, correct_keys=None, decorate_targets=True, target_shape='none', target_stroke='3px', targets=None, target_index=None, target_side=None, target_color=None, target_html=None, decorate_distractors=False, distractor_shape='none', distractor_color='#888888', distractor_stroke='2px', distractors=None, distractor_index=None, distractor_side=None, distractor_color2=None, distractor_html=None, trial_duration=None, record_timestamps=True, duration=None, side_effects=None)

Parameters

background : CSS color | Variable Background color for the RSVP screen (e.g., "#000", "black"). color : CSS color | Variable Default text/border color (used when a per-item color is not provided). direction : {"row","column"} | Variable Layout of streams (left–right for "row", top–bottom for "column"). stream_order : str | None | Variable Comma-separated DOM order of stream IDs (e.g., "left,right"). If omitted and there are exactly two streams with row layout, it is auto-filled from the two stream IDs (e.g., "left,right"). gap : CSS length | Variable Gap between streams in non-bilateral layouts.

CSS length | Variable

Size of the fixed token box (prevents wobble when borders appear).

token_font_size : CSS length | Variable Font size for the glyphs inside each token box. token_padding : CSS length | Variable Inner padding used by outlined/underlined shapes.

list | Variable

Per-trial stream specs. Prefer object form: [{"id":"left","items":["O","Q",...]}, {"id":"right","items":["1","2",...]}] Streams are pure content; do NOT embed "circle"/"square"/colors here.

int | Variable

Milliseconds each token is displayed.

isi : int | Variable Inter-stimulus interval (ms) between tokens. mask_html : str | None | Variable Optional HTML shown during the ISI (e.g., "•").

"ALL" | "NO_KEYS" | list[str] | Variable

Allowed keys during RSVP. Use "NO_KEYS" when you collect responses afterward.

end_on_response : bool | Variable If True, RSVP ends immediately after the first valid keypress (mapped to plugin's response_ends_trial). response_window : int | None | Variable Time window (ms) for scoring target hits; None = unlimited. correct_keys : str | None | Variable Comma-separated keys used for per-target hit scoring (optional).

bool | Variable

Whether to render target decorations (ignored if shape == "none").

target_shape : {"circle","square","underline","none"} | Variable Default shape if a target omits its own shape. target_stroke : CSS length | Variable Outline/underline thickness for targets. targets : list[dict] | None Explicit target list. Each item may include: stream_id, index, label, response_window, correct_keys, shape, color, stroke, padding, html, style, className.

int | list[int|Variable] | Variable

Convenience form—position(s) of target(s) in their streams.

target_side : str | list[str|Variable] | Variable Convenience—stream id(s) (e.g., "left"/"right"/custom ids). target_color : CSS color | list | Variable Per-item color. If the item's shape is "none", this colors the glyph itself. target_html : str (template) | list[str] | Variable Per-item HTML. If it contains {{content}} or {CONTENT}, the stream item’s text is injected; otherwise treated as full override.

bool | Variable

Whether to render distractor decorations.

distractor_shape : {"circle","square","underline","none"} | Variable Default shape for distractors that omit a shape. distractor_color : CSS color | Variable Default distractor color (border or underline; or glyph if shape == "none"). distractor_stroke : CSS length | Variable Outline/underline thickness for distractors. distractors : list[dict] | None Explicit distractor list. Each item may include: stream_id, index, label, shape, color, stroke, padding, html, style, className.

int | list[int|Variable] | Variable

Convenience—position(s) of distractor(s).

distractor_side : str | list[str|Variable] | Variable Convenience—stream id(s) for distractor(s). distractor_color2 : CSS color | list | Variable Per-item color override (falls back to distractor_color if not set). distractor_html : str (template) | list[str] | Variable Per-item HTML for distractors (same templating as target_html).

int | None | Variable

Hard stop (ms). If None, the trial ends after the last token (+ ISI).

record_timestamps : bool | Variable If True, includes per-token onsets/offsets in data["schedule"].

int | None | Variable

SweetBean alias mirrored into trial_duration.

side_effects : dict | None Optional side effects passed along at runtime.

Source code in sweetbean/stimulus/RSVP.py
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def __init__(
    self,
    # ---------------- Appearance / layout ----------------
    background: CssLike = "#000000",
    color: CssLike = "#ffffff",
    direction: StrLike = "row",  # "row" | "column"
    stream_order: Optional[StrLike] = None,  # e.g., "left,right"
    gap: CssLike = "6rem",
    # ---------------- Token sizing ----------------
    token_box_size: CssLike = "18vmin",  # fixed square box
    token_font_size: CssLike = "10vmin",  # glyph size
    token_padding: CssLike = "0.25em 0.45em",  # inner padding for outlines
    # ---------------- Streams & timing ----------------
    streams: Optional[List[Any]] = None,  # preferred: [{"id":..., "items":[...]}]
    stimulus_duration: IntLike = 100,  # ms each token is shown
    isi: IntLike = 0,  # ms between tokens (SOA = dur + isi)
    mask_html: Optional[HtmlLike] = None,  # HTML shown during ISI (e.g., "•")
    # ---------------- Responses ----------------
    choices: Union[StrLike, List[StrLike]] = "ALL",  # "ALL" | "NO_KEYS" | ["f","j"]
    end_on_response: BoolLike = False,  # convenience → response_ends_trial
    response_window: Optional[IntLike] = None,  # ms; None = unlimited
    correct_keys: Optional[StrLike] = None,  # e.g., "f,j" for scoring
    # ---------------- Targets (timing + decoration) ----------------
    decorate_targets: BoolLike = True,  # show decoration if shape != "none"
    target_shape: StrLike = "none",  # default shape if per-target omits
    target_stroke: CssLike = "3px",  # outline/underline thickness
    targets: Optional[
        List[Dict[str, Any]]
    ] = None,  # explicit array [{stream_id, index, ...}]
    # Convenience (scalars OR lists OR Variables) — broadcast by plugin:
    target_index: Optional[
        Union[IntLike, IntListLike]
    ] = None,  # number | [number|Var,...]
    target_side: Optional[
        Union[StrLike, StrListLike]
    ] = None,  # stream ids ("left"/"right"/custom)
    target_color: Optional[
        Union[CssLike, List[CssLike], Var]
    ] = None,  # CSS color; with shape:"none" colors text
    target_html: Optional[
        Union[HtmlLike, List[HtmlLike], Var]
    ] = None,  # HTML template or full override
    # ---------------- Distractors (decoration only) ----------------
    decorate_distractors: BoolLike = False,
    distractor_shape: StrLike = "none",
    distractor_color: CssLike = "#888888",
    distractor_stroke: CssLike = "2px",
    distractors: Optional[List[Dict[str, Any]]] = None,  # explicit array
    # Convenience (scalars OR lists OR Variables) — broadcast by plugin:
    distractor_index: Optional[Union[IntLike, IntListLike]] = None,
    distractor_side: Optional[Union[StrLike, StrListLike]] = None,  # stream ids
    distractor_color2: Optional[
        Union[CssLike, List[CssLike], Var]
    ] = None,  # per-item override (else distractor_color)
    distractor_html: Optional[
        Union[HtmlLike, List[HtmlLike], Var]
    ] = None,  # HTML template or full override
    # ---------------- Lifetime & data ----------------
    trial_duration: Optional[
        IntLike
    ] = None,  # hard stop; else ends after last token
    record_timestamps: BoolLike = True,  # include per-token onset/offset in data["schedule"]
    # ---------------- SweetBean generic ----------------
    duration: Optional[IntLike] = None,  # alias mirrored into trial_duration
    side_effects: Optional[Dict[str, Any]] = None,
):
    """
    Parameters
    ----------
    background : CSS color | Variable
        Background color for the RSVP screen (e.g., "#000", "black").
    color : CSS color | Variable
        Default text/border color (used when a per-item color is not provided).
    direction : {"row","column"} | Variable
        Layout of streams (left–right for "row", top–bottom for "column").
    stream_order : str | None | Variable
        Comma-separated DOM order of stream IDs (e.g., "left,right").
        If omitted and there are exactly two streams with row layout, it is
        auto-filled from the two stream IDs (e.g., "left,right").
    gap : CSS length | Variable
        Gap between streams in non-bilateral layouts.

    token_box_size : CSS length | Variable
        Size of the fixed token box (prevents wobble when borders appear).
    token_font_size : CSS length | Variable
        Font size for the glyphs inside each token box.
    token_padding : CSS length | Variable
        Inner padding used by outlined/underlined shapes.

    streams : list | Variable
        Per-trial stream specs. Prefer object form:
          [{"id":"left","items":["O","Q",...]}, {"id":"right","items":["1","2",...]}]
        Streams are **pure content**; do NOT embed "circle"/"square"/colors here.

    stimulus_duration : int | Variable
        Milliseconds each token is displayed.
    isi : int | Variable
        Inter-stimulus interval (ms) between tokens.
    mask_html : str | None | Variable
        Optional HTML shown during the ISI (e.g., "•").

    choices : "ALL" | "NO_KEYS" | list[str] | Variable
        Allowed keys during RSVP. Use "NO_KEYS" when you collect responses afterward.
    end_on_response : bool | Variable
        If True, RSVP ends immediately after the first valid keypress
        (mapped to plugin's `response_ends_trial`).
    response_window : int | None | Variable
        Time window (ms) for scoring target hits; None = unlimited.
    correct_keys : str | None | Variable
        Comma-separated keys used for per-target hit scoring (optional).

    decorate_targets : bool | Variable
        Whether to render target decorations (ignored if shape == "none").
    target_shape : {"circle","square","underline","none"} | Variable
        Default shape if a target omits its own shape.
    target_stroke : CSS length | Variable
        Outline/underline thickness for targets.
    targets : list[dict] | None
        Explicit target list. Each item may include:
          stream_id, index, label, response_window, correct_keys,
          shape, color, stroke, padding, html, style, className.

    target_index : int | list[int|Variable] | Variable
        Convenience form—position(s) of target(s) in their streams.
    target_side : str | list[str|Variable] | Variable
        Convenience—stream id(s) (e.g., "left"/"right"/custom ids).
    target_color : CSS color | list | Variable
        Per-item color. If the item's shape is "none", this colors the glyph itself.
    target_html : str (template) | list[str] | Variable
        Per-item HTML. If it contains {{content}} or {CONTENT}, the stream item’s
        text is injected; otherwise treated as full override.

    decorate_distractors : bool | Variable
        Whether to render distractor decorations.
    distractor_shape : {"circle","square","underline","none"} | Variable
        Default shape for distractors that omit a shape.
    distractor_color : CSS color | Variable
        Default distractor color (border or underline; or glyph if shape == "none").
    distractor_stroke : CSS length | Variable
        Outline/underline thickness for distractors.
    distractors : list[dict] | None
        Explicit distractor list. Each item may include:
          stream_id, index, label, shape, color, stroke, padding, html, style, className.

    distractor_index : int | list[int|Variable] | Variable
        Convenience—position(s) of distractor(s).
    distractor_side : str | list[str|Variable] | Variable
        Convenience—stream id(s) for distractor(s).
    distractor_color2 : CSS color | list | Variable
        Per-item color override (falls back to `distractor_color` if not set).
    distractor_html : str (template) | list[str] | Variable
        Per-item HTML for distractors (same templating as target_html).

    trial_duration : int | None | Variable
        Hard stop (ms). If None, the trial ends after the last token (+ ISI).
    record_timestamps : bool | Variable
        If True, includes per-token onsets/offsets in data["schedule"].

    duration : int | None | Variable
        SweetBean alias mirrored into `trial_duration`.
    side_effects : dict | None
        Optional side effects passed along at runtime.
    """
    if streams is None:
        streams = []
    if targets is None:
        targets = []
    if distractors is None:
        distractors = []
    if distractor_index is None and distractors is None:
        distractor_index = target_index

    # ⛔️ Do NOT normalize/transform streams here—must support Variables unchanged.
    super().__init__(locals(), side_effects)