npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@slopit/adapter-jspsych

v0.1.0

Published

jsPsych integration adapter for slopit behavioral capture

Readme

@slopit/adapter-jspsych

jsPsych 8.x adapter for slopit behavioral capture.

Installation

pnpm add @slopit/adapter-jspsych

Peer Dependencies:

  • jspsych ^8.0.0

Quick Start

import { initJsPsych } from "jspsych";
import { SlopitSurveyTextPlugin, exportToSlopit } from "@slopit/adapter-jspsych";

const jsPsych = initJsPsych({
  on_finish: () => {
    // export all data in slopit format
    const session = exportToSlopit(jsPsych, {
      participantId: jsPsych.data.urlVariables().PROLIFIC_PID,
      studyId: "my-study-001",
    });

    // send to server
    fetch("/api/submit", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(session),
    });
  },
});

const timeline = [
  {
    type: SlopitSurveyTextPlugin,
    prompt: "Describe your morning routine in detail:",
    rows: 6,
    minLength: 50,
    slopit: {
      keystroke: { enabled: true },
      focus: { enabled: true },
      paste: { enabled: true, prevent: false },
    },
  },
];

jsPsych.run(timeline);

API

SlopitSurveyTextPlugin

A jsPsych plugin for text responses with integrated behavioral capture.

Parameters

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | prompt | string | required | The prompt or question to display (supports HTML) | | placeholder | string | "" | Placeholder text for the textarea | | rows | number | 5 | Number of rows for the textarea | | columns | number | 40 | Number of columns for the textarea | | required | boolean | true | Whether a response is required | | minLength | number | 0 | Minimum character count (0 for no minimum) | | maxLength | number | 0 | Maximum character count (0 for no maximum) | | buttonLabel | string | "Continue" | Text for the submit button | | slopit | SlopitConfig | default config | Behavioral capture configuration |

Data Generated

| Field | Type | Description | |-------|------|-------------| | response | string | The participant's text response | | rt | number | Response time in milliseconds | | characterCount | number | Character count of response | | wordCount | number | Word count of response | | slopit | object | Behavioral capture data and flags |

Basic Usage

import { SlopitSurveyTextPlugin } from "@slopit/adapter-jspsych";

const trial = {
  type: SlopitSurveyTextPlugin,
  prompt: "What are your thoughts on this topic?",
};

With All Options

const trial = {
  type: SlopitSurveyTextPlugin,
  prompt: `
    <h3>Essay Question</h3>
    <p>Please write a detailed response about your experience.</p>
  `,
  placeholder: "Type your response here...",
  rows: 8,
  columns: 60,
  required: true,
  minLength: 100,
  maxLength: 2000,
  buttonLabel: "Submit Response",
  slopit: {
    keystroke: {
      enabled: true,
      captureKeyUp: true,
      includeModifiers: true,
    },
    focus: {
      enabled: true,
      useVisibilityAPI: true,
      useBlurFocus: true,
    },
    paste: {
      enabled: true,
      prevent: false,
      capturePreview: true,
      previewLength: 200,
    },
    clipboard: {
      enabled: true,
      capturePaste: true,
      captureCopyCut: true,
    },
    mouse: {
      enabled: true,
      captureClicks: true,
      captureMovement: true,
      throttleMs: 50,
    },
    scroll: {
      enabled: true,
      throttleMs: 100,
    },
    inputDuration: {
      enabled: true,
      trackKeystrokes: true,
      trackPastes: true,
    },
  },
};

Preventing Paste

const trial = {
  type: SlopitSurveyTextPlugin,
  prompt: "Write your response without copying from other sources:",
  slopit: {
    paste: {
      enabled: true,
      prevent: true,
      warnMessage: "Pasting is not allowed for this question.",
    },
  },
};

exportToSlopit

Exports jsPsych data to the SlopitSession format for server-side analysis.

import { exportToSlopit } from "@slopit/adapter-jspsych";

const session = exportToSlopit(jsPsych, options);

ExportOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | participantId | string | undefined | Participant ID (e.g., from Prolific) | | studyId | string | undefined | Study ID for grouping sessions | | trialFilter | function | undefined | Filter function to select trials | | includePlatformData | boolean | true | Whether to include raw jsPsych trial data |

Basic Export

const session = exportToSlopit(jsPsych);
console.log(JSON.stringify(session, null, 2));

With Participant Metadata

const urlParams = jsPsych.data.urlVariables();

const session = exportToSlopit(jsPsych, {
  participantId: urlParams.PROLIFIC_PID,
  studyId: "attention-check-study-v2",
});

Filtering Trials

// only export trials with slopit data
const session = exportToSlopit(jsPsych, {
  trialFilter: (trial) => trial.slopit !== undefined,
});

// only export specific trial types
const session = exportToSlopit(jsPsych, {
  trialFilter: (trial) => trial.trial_type === "slopit-survey-text",
});

Excluding Platform Data

// minimize payload size by excluding raw jsPsych data
const session = exportToSlopit(jsPsych, {
  includePlatformData: false,
});

getSlopitDataFromTrial

Retrieves slopit data from a specific trial.

import { getSlopitDataFromTrial } from "@slopit/adapter-jspsych";

const data = getSlopitDataFromTrial(jsPsych, trialIndex);
if (data) {
  console.log("Behavioral data:", data.behavioral);
  console.log("Flags:", data.flags);
}

Configuration

SlopitConfig

The slopit parameter accepts the same configuration as BehavioralCaptureConfig from @slopit/behavioral, plus an optional enabled flag.

interface SlopitConfig {
  enabled?: boolean;          // enable/disable slopit for this trial (default: true)
  keystroke?: KeystrokeCaptureConfig;
  focus?: FocusCaptureConfig;
  paste?: PasteCaptureConfig;
  clipboard?: ClipboardCaptureConfig;
  mouse?: MouseCaptureConfig;
  scroll?: ScrollCaptureConfig;
  inputDuration?: InputDurationCaptureConfig;
}

Default Configuration

When no slopit parameter is provided, the plugin uses these defaults:

{
  keystroke: { enabled: true },
  focus: { enabled: true },
  paste: { enabled: true, prevent: false },
}

Examples

Complete Experiment

import { initJsPsych } from "jspsych";
import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
import { SlopitSurveyTextPlugin, exportToSlopit } from "@slopit/adapter-jspsych";

const jsPsych = initJsPsych({
  on_finish: async () => {
    const session = exportToSlopit(jsPsych, {
      participantId: jsPsych.data.urlVariables().PROLIFIC_PID,
      studyId: "writing-study-v1",
    });

    await fetch("/api/submit", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(session),
    });

    // redirect to completion page
    window.location.href = "https://example.com/complete";
  },
});

const timeline = [];

// instructions
timeline.push({
  type: htmlKeyboardResponse,
  stimulus: `
    <h2>Writing Task</h2>
    <p>You will be asked to write several short responses.</p>
    <p>Please type your responses yourself without using AI assistance.</p>
    <p>Press any key to continue.</p>
  `,
});

// writing trials
const prompts = [
  "Describe a memorable experience from your childhood.",
  "Explain your opinion on a current social issue.",
  "Write about a goal you hope to achieve in the next year.",
];

for (const prompt of prompts) {
  timeline.push({
    type: SlopitSurveyTextPlugin,
    prompt: `<p>${prompt}</p>`,
    rows: 6,
    minLength: 100,
    slopit: {
      keystroke: { enabled: true, captureKeyUp: true },
      focus: { enabled: true },
      paste: { enabled: true },
      mouse: { enabled: true, throttleMs: 100 },
    },
  });
}

// debrief
timeline.push({
  type: htmlKeyboardResponse,
  stimulus: `
    <h2>Thank you!</h2>
    <p>Your responses have been recorded.</p>
    <p>Press any key to complete the study.</p>
  `,
});

jsPsych.run(timeline);

With Intervention Warnings

import { initJsPsych } from "jspsych";
import { SlopitSurveyTextPlugin, getSlopitDataFromTrial } from "@slopit/adapter-jspsych";

const jsPsych = initJsPsych();

const writingTrial = {
  type: SlopitSurveyTextPlugin,
  prompt: "Write a short paragraph about your hobbies:",
  rows: 5,
  minLength: 50,
  slopit: {
    keystroke: { enabled: true },
    paste: { enabled: true },
  },
};

// check for suspicious behavior after trial
const checkTrial = {
  type: "call-function",
  func: () => {
    const lastTrialIndex = jsPsych.data.get().count() - 1;
    const data = getSlopitDataFromTrial(jsPsych, lastTrialIndex);

    if (data?.flags && data.flags.length > 0) {
      const pasteFlags = data.flags.filter(f => f.type === "paste_detected");
      if (pasteFlags.length > 0) {
        console.log("Warning: Paste detected in response");
      }
    }
  },
};

const timeline = [writingTrial, checkTrial];
jsPsych.run(timeline);

Multiple Response Types

import { initJsPsych } from "jspsych";
import surveyMultiChoice from "@jspsych/plugin-survey-multi-choice";
import { SlopitSurveyTextPlugin, exportToSlopit } from "@slopit/adapter-jspsych";

const jsPsych = initJsPsych();

const timeline = [
  // multiple choice (no slopit capture)
  {
    type: surveyMultiChoice,
    questions: [
      {
        prompt: "What is your age range?",
        options: ["18-24", "25-34", "35-44", "45+"],
        required: true,
      },
    ],
  },

  // text response with slopit
  {
    type: SlopitSurveyTextPlugin,
    prompt: "Please describe your educational background:",
    rows: 4,
    slopit: {
      keystroke: { enabled: true },
      focus: { enabled: true },
    },
  },

  // longer essay with full capture
  {
    type: SlopitSurveyTextPlugin,
    prompt: "Write about a challenge you overcame:",
    rows: 8,
    minLength: 200,
    slopit: {
      keystroke: { enabled: true, captureKeyUp: true },
      focus: { enabled: true },
      paste: { enabled: true },
      mouse: { enabled: true },
      scroll: { enabled: true },
      inputDuration: { enabled: true },
    },
  },
];

jsPsych.run(timeline);

Accessing Data During Experiment

import { initJsPsych } from "jspsych";
import htmlButtonResponse from "@jspsych/plugin-html-button-response";
import { SlopitSurveyTextPlugin, getSlopitDataFromTrial } from "@slopit/adapter-jspsych";

const jsPsych = initJsPsych();

const timeline = [
  {
    type: SlopitSurveyTextPlugin,
    prompt: "Write about your day:",
    rows: 5,
  },
  {
    type: htmlButtonResponse,
    stimulus: () => {
      const lastIndex = jsPsych.data.get().count() - 1;
      const data = getSlopitDataFromTrial(jsPsych, lastIndex);

      if (!data) return "<p>No behavioral data captured.</p>";

      const metrics = data.behavioral?.metrics?.keystroke;
      const timing = data.behavioral?.metrics?.timing;

      return `
        <h3>Your Writing Statistics</h3>
        <p>Keystrokes: ${metrics?.totalKeystrokes ?? 0}</p>
        <p>Mean typing interval: ${Math.round(metrics?.meanIKI ?? 0)}ms</p>
        <p>Characters per minute: ${Math.round(timing?.charactersPerMinute ?? 0)}</p>
      `;
    },
    choices: ["Continue"],
  },
];

jsPsych.run(timeline);

Data Structure

Trial Data Output

Each SlopitSurveyTextPlugin trial generates data in this structure:

{
  response: "The participant's typed response...",
  rt: 45230,
  characterCount: 234,
  wordCount: 42,
  slopit: {
    behavioral: {
      keystrokes: [
        { time: 1234, key: "T", code: "KeyT", event: "keydown", textLength: 1 },
        { time: 1300, key: "T", code: "KeyT", event: "keyup" },
        // ... more keystrokes
      ],
      focus: [
        { time: 15000, event: "blur" },
        { time: 18500, event: "focus", blurDuration: 3500 },
      ],
      paste: [
        { time: 25000, textLength: 50, precedingKeystrokes: 3, blocked: false },
      ],
      metrics: {
        keystroke: {
          totalKeystrokes: 280,
          printableKeystrokes: 250,
          deletions: 15,
          meanIKI: 152.3,
          stdIKI: 45.2,
          medianIKI: 140,
          pauseCount: 5,
          productProcessRatio: 0.84,
        },
        focus: {
          blurCount: 1,
          totalBlurDuration: 3500,
          hiddenCount: 0,
          totalHiddenDuration: 0,
        },
        timing: {
          firstKeystrokeLatency: 1234,
          totalResponseTime: 45230,
          charactersPerMinute: 310,
        },
      },
    },
    flags: [
      {
        type: "paste_detected",
        severity: "medium",
        message: "1 paste event(s) detected",
        timestamp: 1706123456789,
        details: { count: 1 },
      },
    ],
  },
  trial_type: "slopit-survey-text",
  trial_index: 2,
  time_elapsed: 48500,
}

Exported Session Structure

The exportToSlopit function produces a SlopitSession object:

{
  schemaVersion: "1.0.0",
  sessionId: "session_1706123456789_a1b2c3d4",
  participantId: "P12345",
  studyId: "writing-study-v1",
  platform: {
    name: "jspsych",
    version: "8.0.0",
    adapterVersion: "0.1.0",
  },
  environment: {
    userAgent: "Mozilla/5.0 ...",
    language: "en-US",
    platform: "MacIntel",
    screenWidth: 2560,
    screenHeight: 1440,
    // ... more environment data
  },
  timing: {
    startTime: 1706123400000,
    endTime: 1706123856789,
    duration: 456789,
  },
  trials: [
    {
      trialId: "trial-0",
      trialIndex: 0,
      trialType: "slopit-survey-text",
      startTime: 3270,
      endTime: 48500,
      rt: 45230,
      response: {
        type: "text",
        value: "The participant's typed response...",
        characterCount: 234,
        wordCount: 42,
      },
      behavioral: { /* BehavioralData */ },
      captureFlags: [ /* CaptureFlag[] */ ],
      platformData: { /* raw jsPsych trial data */ },
    },
    // ... more trials
  ],
  globalEvents: {
    focus: [],
    errors: [],
  },
}

Styling

The plugin generates HTML with CSS classes you can style:

/* container */
.slopit-survey-text {
  max-width: 800px;
  margin: 0 auto;
}

/* prompt area */
.slopit-prompt {
  margin-bottom: 1rem;
  font-size: 1.1em;
}

/* textarea container */
.slopit-input-container {
  margin-bottom: 1rem;
}

/* the textarea itself */
#slopit-response {
  width: 100%;
  padding: 0.5rem;
  font-size: 1rem;
  font-family: inherit;
  border: 1px solid #ccc;
  border-radius: 4px;
}

/* character counter */
.slopit-char-counter {
  text-align: right;
  font-size: 0.9em;
  color: #666;
  margin-top: 0.25rem;
}

/* submit button container */
.slopit-button-container {
  text-align: center;
}

/* validation error message */
.slopit-validation-error {
  color: red;
  margin-top: 10px;
  text-align: center;
}

Keyboard Shortcuts

  • Ctrl+Enter or Cmd+Enter: Submit the response

License

MIT