SAGAT-Freeze
How to install the SAGAT plugin for jsPsych, configure a freeze with items, and read the response data.
This page is the guide to the plugin package
@ecosimlab/jspsych-plugin-videosimlab-sagat: installation, trial
configuration, item formats, data output. One trial of this type is one
SAGAT freeze. The plugin asks the items one after the other, then the trial ends
and the timeline goes on.
Every statement here comes from the repository videosimlab-sagat, tag 1.0.1
(commit e64c9a1, which is also main), package version 1.0.1. The
repository documentation (docs/plugin-videosimlab-sagat.md) is in part still
the empty jsPsych template, and it disagrees with the code in several places.
Where it does, this page gives the state of the code and names the
difference.
Prerequisites
- jsPsych 8.0.0 or later. The repository declares
jspsychas a normal dependency, not as a peer dependency. Take care that a study does not bundle two jsPsych instances. - Node and npm for the installation and the build. The sub-build
src/sagat-itemsis a Vite and Solid project. Its README names pnpm, but the build script callsnpm install. - A browser. The plugin draws into the
display_elementof jsPsych and injects its own CSS. It needs no video player and no data source. The timeline around it hides the video and the display.
Installation
The package is named @ecosimlab/jspsych-plugin-videosimlab-sagat. It is not
in the npm registry. The comment in examples/index.html (“Once this plugin
package is published, you can load it from a CDN instead”) confirms that.
Install it from the public GitLab repository, pinned to a tag:
npm install https://gitlab.com/ecosimlab/videosimlab-sagat#1.0.1
The README still names
#1.0.0in this place. The newest tag is1.0.1, and thepackage.jsonis on1.0.1as well. The tag1.0.1counts.
The package ships its built dist/ folder, so the installation runs no build.
Import. README and package.json name the same package:
import jsPsychVideosimlabSagat from "@ecosimlab/jspsych-plugin-videosimlab-sagat";
The README writes the variable as jsPsychVideoSimLabSagat. The name of the
variable is free. For the browser global, jsPsychVideosimlabSagat from
rollup.config.mjs counts.
In a browser without a bundler. The README leaves this section empty, but
the build delivers the file: dist/index.browser.min.js (also the unpkg
field) sets the global jsPsychVideosimlabSagat. examples/index.html loads it
that way.
Quickstart
examples/index.html loads jsPsych and the built browser bundle directly:
<script src="https://unpkg.com/jspsych"></script>
<script src="../dist/index.browser.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/jspsych/css/jspsych.css" />
The example trial in the repository does not run. It declares only
type: jsPsychVideosimlabSagat without items, and the item component reads
items[0] as soon as it draws. A trial runs from one item on:
const jsPsych = initJsPsych();
const trial = {
type: jsPsychVideosimlabSagat,
sectorId: "sector-01",
replayType: "efficient",
items: [
{
id: "level1-1",
itemType: "range",
stimulus: "Wie schnell sind Sie gerade gefahren?",
options: [
{ text: "Geschwindigkeit", start: 0, min: 0, max: 130, unit: "km/h" },
],
},
],
};
jsPsych.run([trial]);
The sequence is fixed: the heading “Fragen zur Fahrt”, the progress n / total,
one item per page, and the button “Weiter”. Without an input the page shows the
message “Eingabe erforderlich” instead of the next item. After the last item the
trial ends. The plugin draws these strings in German only.
Freezes and items
One freeze is one trial and one items array. Each item has four fields:
| Field | Type | Meaning |
|---|---|---|
id |
string | The item. The comment in the code gives the convention level-number-type: level1, level2 or level3 for the SA level, a running number, and p for power or c for consumption. |
itemType |
string | The item template, see below. |
stimulus |
string | The question on the page. |
options |
object[] | The answer configuration. The fields depend on itemType. |
Take care with the names of the item types. The plugin info, the README
and the repository documentation name four types: radio, select, range and
bar. The code reads six (src/utils.ts, src/sagat-items/src/App.tsx):
radio · selection · range · bar · pedal · dragdrop
select hits no branch of the code. Multiple choice is selection. pedal and
dragdrop work, but no document names them.
radio and selection — a choice
Options: { text, suboptions }, where suboptions is an array of strings.
text is the heading of the group. Each suboption becomes a line with a radio
button (radio) or a check box (selection). The code holds two special cases:
- If
textis one ofpickup/Pickup,pkw/Pkw/PKWorbus/Bus, the page shows a picture of the vehicle instead of the heading. - The suboptions
N,D,RandPget a drive mode picture.
range — a horizontal slider
Options: { text, start, min, max, unit }. An item can hold more than one
slider. The component adds a second display above the slider. It selects that
display by a comparison of strings:
| Condition | Additional display |
|---|---|
unit === "km/h" |
a speedometer |
text holds "SOC" |
a battery symbol in percent |
text holds "Reichweite" |
the range in km |
text holds "Durchschnittlicher Verbrauch" |
the consumption in kWh/100km |
Limit: the component reads min and max for all sliders of an item from
options[0]. Different limits per slider have no effect.
bar — a vertical slider for power and regenerative braking
Options: { text, textTop, textBottom, start, unit }. textTop and
textBottom label the two ends of the bar. text is a heading and appears only
if the item holds more than one bar. The raw range of the slider is -149 to
300, where a negative value is regenerative braking. unit scales the value:
with unit === "kW" to 39 (negative) and 92 (positive), otherwise to 1 and 2.
For a consumption item the range is therefore −1 to 2.
pedal — the energy pedal and the brake pedal
Options: { text, start }. The component draws two vertical sliders
(“Energiepedal”, “Bremspedal”). Each one is raw 0 to 300 and is shown and
stored as a percentage from 0 to 100.
dragdrop — the relative position of a vehicle
Options: { text } per vehicle. The component draws a lane with four drop
fields, pos1 to pos4, from the bottom up, in front of the ego vehicle. Below
it stands a start row with the vehicles from options. The ego vehicle (zoe)
is fixed and cannot be dragged. text also selects the picture of the vehicle
(pickup, pkw, bus, else zoe).
Fields that are not declared: the
optionsstructure of the plugininfoknows onlytext,textTop,textBottomandstart. The components readsuboptions,min,maxandunitas well. Those four fields are missing from the generated parameter documentation, and jsPsych gives them no defaults.
API reference
Default export: the plugin class VideosimlabSagatPlugin (info.name:
plugin-videosimlab-sagat). The browser global and the rollup name are
jsPsychVideosimlabSagat.
Trial parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
sectorId |
STRING | undefined |
The sector. |
replayType |
STRING | undefined |
The efficiency type of the replay. The comment in the code gives efficient and inefficient. |
items |
COMPLEX | undefined |
The items of this freeze, see above. |
The parameters of all jsPsych plugins come on top of these.
TypeScript types (src/utils.ts)
Item, ItemType, Answer, DataItem and SagatItem, plus the types of the
options: SelectionItemOptions, RangeItemOptions, BarItemOptions,
PedalItemOptions and DragDropItemOptions. The built declarations are in
dist/index.d.ts (the typings field).
Events and styles
The plugin has no events and no callbacks of its own. Its only exit is
jsPsych.finishTrial() after the last item. Use on_finish of the trial to
react to a freeze.
The plugin injects the compiled Tailwind styles once, at the first trial, as
<style id="jspsych-videosimlab-sagat-styles">. Every utility carries the
prefix tw:, and the Tailwind preflight is left out on purpose. The styles of
the host page stay as they are.
Trial data
The trial writes an object with three fields into the data of jsPsych. The
names in the code differ from the repository documentation. The code counts
(src/index.ts):
| Field in the code | In the repository documentation | Content |
|---|---|---|
sectroId |
sectorId |
the parameter sectorId — the typing error is in the source code |
replayType |
replayType |
the parameter replayType |
answers |
sagat |
the array of the answered items |
Each entry of answers is a DataItem. It repeats the configuration of the
item and adds the answers:
{
"id": "level1-1",
"itemType": "range",
"stimulus": "Wie schnell sind Sie gerade gefahren?",
"options": [{ "text": "Geschwindigkeit", "start": 0, "min": 0, "max": 130, "unit": "km/h" }],
"answers": [{ "text": "Geschwindigkeit", "answer": 62 }]
}
What answers[].text and answers[].answer hold depends on the item type:
| Type | text |
answer |
|---|---|---|
radio / selection |
"<group>-<suboption>" |
"on" for a selected option, else "" — the data holds every suboption, not only the selected ones |
range |
options[i].text |
the number of the slider |
bar |
options[i].textTop |
the scaled number, see bar above |
pedal |
options[i].text |
the string "energyPedal:<0–100>-brakePedal:<0–100>" |
dragdrop |
options[i].text |
the drop field (pos1 to pos4), or null while the vehicle is not dropped |
There are no time stamps per item. Only the trial times of jsPsych (rt,
time_elapsed) are there. There is no ground truth in the plugin either:
the comment in the code, “the correct one is marked”, does not hold, because
nothing is marked as correct. The comparison with the driving data happens
afterwards, in the analysis.
Development and tests
npm install
npm run build # builds src/sagat-items first, then rollup --config
npm run build:watch
npm run tsc
npm test # jest
npm run test:watch
- The build has two steps.
build:sagat-itemsgoes intosrc/sagat-items, installs there, and builds the Solid and Vite sub-project tosrc/sagat-items/dist/sagat-items.js. Only then does rollup bundle the main package intodist/(ESM, CJS, browser, minified). It usesmakeRollupConfig("jsPsychVideosimlabSagat")of@jspsych/config. - The CSS travels in the bundle. The Vite plugin
scripts/export-css.tsappends the compiled CSS to the entry asexport const __css__ = "…"and deletes the separate.cssfile. The finished package therefore needs no extra stylesheet. - Item templates on their own. In
src/sagat-items,npm run devstarts a Vite development server on port 3001 with the item components alone, without jsPsych. - Tests.
jest.config.cjsuses@jspsych/config/jest.src/index.spec.tsis still the unchanged jsPsych template test: it hands over invented parameters (parameter_name,parameter_name2) and checks only that the trial ends. There is no test coverage of the plugin itself.
License and citation
MIT (LICENSE.txt, copyright 2026 Leonardt Wagner; the same in
package.json).
CITATION.cff names Leonardt Wagner
(ORCID 0009-0009-1759-9674), the title
jsPsychVideosimlabSagat, version 1.0.0, the release date 2026-02-07, and no
DOI. At build time @jspsych/config replaces the placeholder
citations: '__CITATIONS__' in the plugin info with the data from that file.
The citation function of jsPsych reads it.
Do not take two errors of the CITATION.cff over unchecked: url points to
videosimlab-cockpit-replay instead of this repository, and version 1.0.0
disagrees with the package version 1.0.1.
Troubleshooting
Cannot find module "@jspsych-plugin-videosimlab-sagat"— the package carries the scope@ecosimlab/. Import from@ecosimlab/jspsych-plugin-videosimlab-sagat(see Installation).- The trial ends at once, or the page stays white —
itemsis missing or empty. The plugin reads the first item without a check. A trial needs at least one item. - An item does not appear — check
itemType.selectis not a valid value; multiple choice isselection. With an unknown type the area stays empty and no message appears. - The analysis does not find
sagat— the field is namedanswersin the code, and the sector is stored undersectroId. - The layout has no styles — the sub-build is missing. The main package
imports
src/sagat-items/dist/sagat-items.js, which holds the markup and__css__. Withoutnpm run build:sagat-itemsthe rollup build fails. - The pictures of the vehicles are missing (
selection,dragdrop) — the components load the pictures over relative paths such as./cars/zoe.png, and the bundle does not hold them. Serve the files ofsrc/sagat-items/public/cars/from the host page under that path. - A
rangeslider ignoresminandmax— the component reads both for all sliders of an item fromoptions[0]. Put sliders with different limits into items of their own.
Open questions
- The version numbers in the repository. The README (
#1.0.0) andCITATION.cff(1.0.0) are behind the tag and the package version (1.0.1). TheurlofCITATION.cffpoints to the wrong repository.- The names of the data fields.
sectroIdandanswers(code) againstsectorIdandsagat(documentation). Whether the code or the documentation changes is an open decision. This page follows that decision afterwards.selectagainstselection— the same question for the name of the type.- Example configurations. The repository holds no complete item set of a real study. The examples here come from the components, not from a configuration in use.
- Assets that nothing uses.
src/sagat-items/public/road-signs/holds 14 traffic signs that no component reads. They may belong to a planned level 1 item. That is open.