Skip to content

sweetbean.stimulus.RSVP

RSVP

Bases: _BaseStimulus

RSVP stimulus (wraps the jsPsych RSVP plugin)

Source code in sweetbean/stimulus/RSVP.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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
class RSVP(_BaseStimulus):
    """
    RSVP stimulus (wraps the jsPsych RSVP plugin)
    """

    # IMPORTANT: the browser bundle must expose `window.jsPsychRsvp`.
    # Using a string type keeps SweetBean fully declarative.
    type = "jsPsychRsvp"

    def __init__(
        self,
        # Appearance / layout
        background: str = "#000000",
        color: str = "#ffffff",
        direction: str = "row",  # "row" | "column"
        stream_order: Optional[str] = None,  # e.g., "left,right"
        gap: str = "6rem",
        # Token sizing
        token_box_size: str = "18vmin",
        token_font_size: str = "10vmin",
        token_padding: str = "0.25em 0.45em",
        # Streams & timing (PLAIN DICTS)
        # streams: list of { id: str, items: list[str], offset_ms?: int, attrs?: {k:v} }
        streams: Optional[List[Dict[str, Any]]] = None,
        stimulus_duration: int = 100,
        isi: int = 0,
        mask_html: Optional[str] = None,
        # Responses
        choices: Union[str, List[str]] = "ALL",  # "ALL" | "NO_KEYS" | ["f","j"]
        end_on_response: bool = False,
        response_window: Optional[int] = None,  # None => unlimited
        correct_keys: Optional[str] = None,  # e.g., "f,j"
        decorate_targets: bool = True,  # OFF by default
        target_shape: str = "none",  # default decoration
        target_stroke: str = "3px",
        targets: Optional[List[Dict[str, Any]]] = None,
        trial_duration: Optional[int] = None,
        record_timestamps: bool = True,
        duration: Optional[int] = None,
        side_effects: Optional[Dict[str, Any]] = None,
    ):
        if streams is None:
            streams = []
        if targets is None:
            targets = []

        super().__init__(locals(), side_effects)

    # ---- SweetBean hooks ----

    def _add_special_param(self):
        # Mirror other SweetBean stimuli: if `duration` is set, use it as `trial_duration`
        if self.arg_js.get("duration") not in (None, "null"):
            self.arg_js["trial_duration"] = self.arg_js["duration"]

        # Quality-of-life: if exactly two streams, left-to-right default order
        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 len(streams) == 2
            ):
                a, b = streams[0].get("id"), streams[1].get("id")
                if a and b:
                    self.arg_js["stream_order"] = f"{a},{b}"
        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):
        # No on_load JS needed; keep empty (consistent with your simple wrappers)
        pass

    # If you don’t support language mode, keep the default NotImplementedError like ROK:
    def process_l(self, prompts, get_input, multi_turn, datum=None):
        raise NotImplementedError