This commit is contained in:
Shaheed Azaad
2025-07-01 16:53:07 +02:00
parent 2761943d9d
commit 34239413b1
19 changed files with 1404 additions and 22 deletions

View File

@@ -1,2 +1,4 @@
# jspsych-npm-template # Suspicion-probe Experiment 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

337
index.js
View File

@@ -1,11 +1,31 @@
import { initJsPsych } from "jspsych"; import { initJsPsych } from 'jspsych';
import "jspsych/css/jspsych.css"; import 'jspsych/css/jspsych.css';
import "./styles.css"; import jsPsychHtmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response';
import { delayed_redirect } from "./utils/helpers.js"; import generateUniqueUsernames from './scripts/name-gen.js';
import jsPsychHtmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; import jsPsychFullscreen from '@jspsych/plugin-fullscreen';
import { textStimuli } from './scripts/text_stimuli'; import jsPsychHtmlButtonResponse from '@jspsych/plugin-html-button-response';
import jsPsychLobby from './scripts/plugin-lobby.js';
import jsPsychSurvey from '@jspsych/plugin-survey';
import '@jspsych/plugin-survey/css/survey.css';
import './styles.css';
import { getStimulusMap } from './scripts/text-stimuli.js';
import jsPsychObjectMoving from './scripts/plugin-object-moving.js';
const debug = import.meta.env.VITE_DEBUG; const debug = import.meta.env.VITE_DEBUG === 'true';
const total_participants = import.meta.env.VITE_TOTAL_PARTICIPANTS;
const uniqueUsernames = generateUniqueUsernames(total_participants);
const experiment_name = import.meta.env.VITE_EXPERIMENT_NAME;
let prolific_id;
let probe_condition; // P in the params, O = open, S = suspicion mentioned
let mapping; // M in the params, TB = together blue, AB = alone blue
let short_version = false;
function delayed_redirect(url) {
setTimeout(() => {
window.location = url;
}, 5000);
}
const jsPsych = initJsPsych({ const jsPsych = initJsPsych({
on_finish: function() { on_finish: function() {
@@ -21,12 +41,309 @@ const jsPsych = initJsPsych({
}, },
}); });
const demo_trial = { prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID');
mapping = jsPsych.data.getURLVariable('M');
short_version = jsPsych.data.getURLVariable('S') === 'true';
const together_colour = mapping === 'TB' ? 'blue' : 'red';
const stimulusMap = getStimulusMap(together_colour);
probe_condition = jsPsych.data.getURLVariable('P');
let probe_text;
if (probe_condition === 'O') {
probe_text =
'Did you have any thoughts or observations about the experiment?';
} else if (probe_condition === 'S') {
probe_text =
'Did you have any thoughts, observations, or suspicions about the experiment?';
} else if (probe_condition === 'D') {
probe_text =
'Many studies use deception to create the appearance that you are interacting with a real person. Did you suspect that you were not interacting with a real person?';
} else if (probe_condition === 'R') {
probe_text =
'Our study used deception to create the appearance that you are interacting with a real person. Did you suspect that you were not interacting with a real person?';
} else {
probe_text =
'Did you have any thoughts or observations about the experiment?';
}
jsPsych.data.addProperties({
condition: probe_condition,
together_colour: together_colour,
prolific_id: prolific_id,
experiment_name: experiment_name,
probe_text: probe_text,
});
const timeline = [];
const pre_consent_info = {
type: jsPsychHtmlKeyboardResponse, type: jsPsychHtmlKeyboardResponse,
stimulus: `<h1 class="text-2xl font-bold">Hello, world!</h1>`,
choices: [' '], choices: [' '],
stimulus: stimulusMap.get('pre_consent_info'),
}; };
const timeline = [demo_trial]; const enter_fullscreen = {
type: jsPsychFullscreen,
fullscreen_mode: true,
};
const consent_form = {
type: jsPsychHtmlButtonResponse,
stimulus: stimulusMap.get('consent'),
choices: ['Exit', 'Continue'],
on_finish: function(data) {
if (data.response === 0) {
jsPsych.abortExperiment(stimulusMap.get('no_consent'));
}
},
};
const initial_lobby = {
type: jsPsychLobby,
user_names: uniqueUsernames,
end_number: total_participants,
start_text: 'Searching for a partner',
end_text: 'Partner found, the experiment will begin shortly.',
join_interval: 5000,
show_avatars: false,
};
const lobby_slow = {
type: jsPsychLobby,
user_names: uniqueUsernames,
end_number: total_participants,
start_text: 'Waiting for your partner',
end_text: 'Your partner is ready, the experiment will continue shortly.',
show_avatars: false,
join_interval: 7000,
};
const lobby_fast = {
type: jsPsychLobby,
user_names: uniqueUsernames,
end_number: total_participants,
start_text: 'Waiting for your partner',
end_text: 'Your partner is ready, the experiment will continue shortly.',
show_avatars: false,
join_interval: 500,
};
const multi_user_instructions = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('multi_user_instructions'),
};
const instructions_1 = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('instructions_1'),
};
const instructions_2 = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('instructions_2'),
};
const instructions_3 = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('instructions_3'),
};
const instructions_4 = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('instructions_4'),
};
const pre_practice_instructions = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('pre_practice_instructions'),
};
const pre_task_instructions = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('pre_task_instructions'),
};
const debrief = {
type: jsPsychHtmlKeyboardResponse,
choices: [' '],
stimulus: stimulusMap.get('debrief'),
};
const survey = {
type: jsPsychSurvey,
survey_json: {
showQuestionNumbers: false,
completeText: 'Done!',
pageNextText: 'Continue',
pagePrevText: 'Previous',
showPrevButton: false,
pages: [
{
name: 'page1',
elements: [
{
type: 'radiogroup',
title: 'Please indicate your gender',
choices: ['Male', 'Female', 'Other'],
isRequired: debug ? false : true,
colCount: 0,
name: 'gender',
},
{
type: 'radiogroup',
title: 'Please indicate your handedness',
choices: ['Right', 'Left', 'Ambidextrous/Other'],
isRequired: debug ? false : true,
colCount: 0,
name: 'handedness',
},
{
type: 'text',
title: 'How old are you?',
name: 'age',
isRequired: debug ? false : false,
inputType: 'number',
min: 18,
max: 100,
defaultValue: 18,
},
],
},
{
name: 'page2',
elements: [
{
type: 'comment',
title: probe_text,
name: 'probe',
isRequired: debug ? false : true,
},
],
},
{
name: 'page3',
elements: [
{
type: 'matrix',
name:
'To what extent do you agree with the following statements about your partner?',
alternateRows: true,
isAllRowRequired: debug ? false : true,
rows: [
{
text: 'I did not believe that my partner was a real person.',
value: 'SuspicionPartner',
},
{
text: 'I believed that my partner was a real person.',
value: 'ConfidencePartner',
},
],
columns: [
{
value: 5,
text: 'Strongly agree',
},
{
value: 4,
text: 'Agree',
},
{
value: 3,
text: 'Neutral',
},
{
value: 2,
text: 'Disagree',
},
{
value: 1,
text: 'Strongly disagree',
},
],
},
],
},
],
},
};
const factors = {
selector: ['partner', 'participant'],
together_side: ['left', 'right'],
location: short_version ? [33, 50, 66] : [32, 33, 34, 49, 50, 51, 65, 66, 67],
};
const practice_factors = {
selector: ['partner', 'participant'],
together_side: ['left', 'right'],
location: [50],
};
const trials = jsPsych.randomization.factorial(factors, 1);
if (debug) {
console.log(factors);
console.log(trials);
}
const practice_trials = jsPsych.randomization.factorial(practice_factors, 1);
const object_moving_trials = {
timeline: [
{
type: jsPsychObjectMoving,
selector: jsPsych.timelineVariable('selector'),
together_side: jsPsych.timelineVariable('together_side'),
location: jsPsych.timelineVariable('location'),
together_colour: together_colour,
},
],
timeline_variables: trials,
};
const object_moving_practice = {
timeline: [
{
type: jsPsychObjectMoving,
selector: jsPsych.timelineVariable('selector'),
together_side: jsPsych.timelineVariable('together_side'),
location: jsPsych.timelineVariable('location'),
together_colour: together_colour,
},
],
timeline_variables: practice_trials,
};
if (debug) {
timeline.push(survey);
timeline.push(debrief);
}
if (!debug) {
timeline.push(pre_consent_info);
timeline.push(consent_form);
timeline.push(enter_fullscreen);
timeline.push(multi_user_instructions);
timeline.push(initial_lobby);
timeline.push(instructions_1);
timeline.push(instructions_2);
timeline.push(instructions_3);
timeline.push(instructions_4);
timeline.push(pre_practice_instructions);
timeline.push(lobby_slow);
timeline.push(object_moving_practice);
timeline.push(pre_task_instructions);
timeline.push(lobby_fast);
timeline.push(object_moving_trials);
timeline.push(survey);
timeline.push(debrief);
}
jsPsych.run(timeline); jsPsych.run(timeline);

27
scripts/colours.js Normal file
View File

@@ -0,0 +1,27 @@
export default function getColourMap(together_colour) {
const colourMap = new Map();
// Some reduncancy here, but Tailwind wont recognise the classes otherwise
colourMap.set('go', 'text-lime-600');
colourMap.set('warning', 'text-yellow-600');
colourMap.set('self-username', 'text-sky-600');
colourMap.set('success', 'text-green-600');
colourMap.set('gobg', 'bg-lime-600');
colourMap.set('object-enabled', 'bg-gray-600');
colourMap.set('object-disabled', 'bg-gray-400');
if (together_colour === 'blue') {
colourMap.set('alone', 'text-rose-400');
colourMap.set('together', 'text-blue-400');
colourMap.set('alonebg', 'bg-rose-400');
colourMap.set('togetherbg', 'bg-blue-400');
} else if (together_colour === 'red') {
colourMap.set('alone', 'text-blue-400');
colourMap.set('together', 'text-rose-400');
colourMap.set('alonebg', 'bg-blue-400');
colourMap.set('togetherbg', 'bg-rose-400');
}
return colourMap;
}

86
scripts/deploy.js Normal file
View File

@@ -0,0 +1,86 @@
const { execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const archiver = require('archiver');
const dotenv = require('dotenv');
// Load environment variables based on NODE_ENV
const envFile =
process.env.NODE_ENV === 'production'
? '.env.production'
: '.env.development';
dotenv.config({ path: path.join(__dirname, '..', envFile) });
// Function to create and send zip file
function sendZipFile() {
return new Promise((resolve, reject) => {
const distPath = path.join(__dirname, '../dist');
const zipPath = path.join(__dirname, '../dist.zip');
// Create a file to stream archive data to
const output = fs.createWriteStream(zipPath);
const archive = archiver('zip', {
zlib: { level: 9 }, // Sets the compression level
});
// Listen for all archive data to be written
output.on('close', () => {
console.log(`✅ Archive created: ${archive.pointer()} total bytes`);
// Get the deployment URL from environment
const deployUrl = process.env.VITE_DEPLOY_URL;
if (!deployUrl) {
console.error('❌ VITE_DEPLOY_URL not found in environment variables');
process.exit(1);
}
// Send the zip file via curl
try {
console.log(`📤 Sending zip file to ${deployUrl}...`);
execSync(`curl -X POST -F "file=@${zipPath}" ${deployUrl}`, {
stdio: 'inherit',
});
console.log('✅ Zip file sent successfully');
} catch (error) {
console.error('❌ Failed to send zip file:', error.message);
process.exit(1);
}
// Clean up the zip file
fs.unlinkSync(zipPath);
resolve();
});
// Handle warnings and errors
archive.on('warning', err => {
if (err.code === 'ENOENT') {
console.warn('⚠️ Archive warning:', err);
} else {
reject(err);
}
});
archive.on('error', err => {
reject(err);
});
// Pipe archive data to the file
archive.pipe(output);
// Add the dist directory to the archive
archive.directory(distPath, false);
// Finalize the archive
archive.finalize();
});
}
// Run the deployment
sendZipFile()
.then(() => {
console.log('✅ Deployment successful!');
})
.catch(error => {
console.error('❌ Deployment failed:', error.message);
process.exit(1);
});

77
scripts/name-gen.js Normal file
View File

@@ -0,0 +1,77 @@
function generateRandomUsername(usedAdjectives, usedNouns) {
const adjectives = [
'Round',
'Square',
'Tall',
'Wide',
'Small',
'Large',
'Flat',
'Smooth',
'Long',
'Short',
'Straight',
'Curved',
'Solid',
'Hollow',
'Dense',
'Light',
'Deep',
'Broad',
'Thin',
'Thick',
];
const nouns = [
'Table',
'Chair',
'Lamp',
'Spoon',
'Book',
'Door',
'Window',
'Pencil',
'Paper',
'Bowl',
'Plate',
'Cup',
'Box',
'Shelf',
'Frame',
'Desk',
'Mirror',
'Basket',
'Button',
'Bottle',
];
let randomAdjective;
do {
randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
} while (usedAdjectives.has(randomAdjective));
let randomNoun;
do {
randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
} while (usedNouns.has(randomNoun));
usedAdjectives.add(randomAdjective);
usedNouns.add(randomNoun);
const randomInt = Math.floor(Math.random() * 100);
return randomAdjective + randomNoun + randomInt;
}
function generateUniqueUsernames(count) {
const usernames = new Set();
const usedAdjectives = new Set();
const usedNouns = new Set();
while (usernames.size < count) {
usernames.add(generateRandomUsername(usedAdjectives, usedNouns));
}
return Array.from(usernames);
}
export default generateUniqueUsernames;

179
scripts/plugin-lobby.js Normal file
View File

@@ -0,0 +1,179 @@
import { ParameterType } from 'jspsych';
import { html } from './text-stimuli';
import getColourMap from './colours';
const info = {
name: 'lobby',
version: "1.0",
parameters: {
user_names: {
type: ParameterType.STRING,
pretty_name: 'User names',
default: [],
array: true,
},
start_text: {
type: ParameterType.STRING,
pretty_name: 'Start text',
},
end_text: {
type: ParameterType.STRING,
pretty_name: 'End text',
},
start_number: {
type: ParameterType.INT,
pretty_name: 'Start number',
default: null,
},
end_number: {
type: ParameterType.INT,
pretty_name: 'End number',
default: 3,
},
join_interval: {
type: ParameterType.INT,
pretty_name: 'Join interval',
default: 5000,
},
user_object: {
type: ParameterType.OBJECT,
pretty_name: 'User object',
default: {},
},
show_avatars: {
type: ParameterType.BOOL,
pretty_name: 'Show avatar',
default: false,
},
data: {
type: ParameterType.OBJECT,
pretty_name: 'Data',
default: {},
},
},
// prettier-ignore
citations: '__CITATIONS__'
};
class jsPsychLobby {
constructor(jsPsych) {
this.jsPsych = jsPsych;
}
static {
this.info = info;
}
trial(display_element, trial) {
let current_participants;
if (!trial.start_number) {
current_participants = Math.floor(Math.random() * trial.end_number) + 1;
} else {
current_participants = trial.start_number;
}
const colourMap = getColourMap(' ');
display_element.innerHTML = html`<div id="search-text" class="mb-4"></div>
<div id="participants-list"></div>
<div id="loading-animation" class="loading"></div>
<div id="refresh-warning" class="mt-4"></div>`;
const search_text_div = document.getElementById('search-text');
const participants_list_div = document.getElementById('participants-list');
const refresh_warning_div = document.getElementById('refresh-warning');
const loading_animation_div = document.getElementById('loading-animation');
search_text_div.innerHTML =
'<p class="text-lg font-semibold" id="search-text-p">' +
trial.start_text +
' (<span id="current-participants">' +
current_participants +
'</span>/' +
trial.end_number +
') ' +
'</p>';
refresh_warning_div.innerHTML =
`<p class="text-sm font-semibold ${colourMap.get('warning')}">Please do not refresh or close this page unless you are not connected to your partner within 5 minutes</p>`;
const current_participants_span = document.getElementById('current-participants');
let participants_list = trial.user_names.slice(1, current_participants);
let ended = false;
const avatar_svg_template = colour => html`
<svg
xmlns="http://www.w3.org/2000/svg"
fill="${colour}"
viewBox="0 0 24 24"
stroke-width="0.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
/>
</svg>
`;
const createParticipantsHtml = () => {
let html = '';
for (let i = 0; i < participants_list.length; i++) {
/* if (trial.show_avatars) {
html += `<div style="width: 24px; height: 24px;">${avatar_svg_template(
trial.user_object[participants_list[i]]
)}</div>`;
} */
if (participants_list[i] === trial.user_names[0]) {
html += `<p class="text-base ${colourMap.get('self-username')}">` + participants_list[i] + ` (you)</p>`;
} else {
html += `<p class="text-base">` + participants_list[i] + ` </p>`;
}
}
return (html);
};
participants_list_div.innerHTML = createParticipantsHtml();
setTimeout(() => {
participants_list.push(trial.user_names[0]);
participants_list_div.innerHTML = createParticipantsHtml();
}, 100);
const updateParticipants = () => {
if (current_participants >= trial.end_number && !ended) {
ended = true;
clearInterval(participants_interval);
setTimeout(this.foundParticipants, 1500);
} else {
participants_list.push(trial.user_names[current_participants]);
participants_list_div.innerHTML = createParticipantsHtml();
current_participants++;
if (current_participants <= trial.end_number && !ended) {
current_participants_span.innerHTML = current_participants;
}
}
};
this.foundParticipants = () => {
participants_list_div.innerHTML = html`<span class="${colourMap.get('success')} font-semibold">${trial.end_text}</span>`;
loading_animation_div.classList.remove('loading');
setTimeout(() => {
this.jsPsych.finishTrial();
}, 2000);
};
const participants_interval = setInterval(
updateParticipants,
trial.join_interval
);
}
}
export default jsPsychLobby;

View File

@@ -0,0 +1,302 @@
import { ParameterType } from 'jspsych';
import { html } from './text-stimuli';
import getColourMap from './colours';
const info = {
name: 'object-moving',
version: "1.1",
parameters: {
selector: {
type: ParameterType.STRING,
pretty_name: 'Selector',
},
together_side: {
type: ParameterType.STRING,
pretty_name: 'together side',
},
location: {
type: ParameterType.INT,
pretty_name: 'Location',
},
together_colour: {
type: ParameterType.STRING,
pretty_name: 'Together colour',
},
},
// prettier-ignore
citations: '__CITATIONS__'
};
class jsPsychObjectMoving {
constructor(jsPsych) {
this.jsPsych = jsPsych;
}
static {
this.info = info;
}
trial(display_element, trial) {
const colourMap = getColourMap(trial.together_colour);
const left_goal = trial.together_side === 'left' ? 'together' : 'alone';
const right_goal = trial.together_side === 'left' ? 'alone' : 'together';
const left_colour = trial.together_side === 'left' ? colourMap.get('togetherbg') : colourMap.get('alonebg');
const right_colour = trial.together_side === 'left' ? colourMap.get('alonebg') : colourMap.get('togetherbg');
display_element.innerHTML = html`
<div class="flex flex-row min-w-screen absolute top-0 left-0 min-h-screen">
<div id="${left_goal}-goal" class="w-1/12 ${left_colour}"></div>
<div id="workspace" class="w-10/12"></div>
<div id="${right_goal}-goal" class="w-1/12 ${right_colour}"></div>
<div id="info-box" class="z-10 absolute bottom-1/40 min-w-screen justify-center items-center"></div>
</div>
`;
const workspace = document.getElementById('workspace');
const info_box = document.getElementById('info-box');
const workspace_width = workspace.clientWidth;
const viewport_width = window.visualViewport.width;
const goal_offset = viewport_width * (1/12);
const box_width = viewport_width * (3/100);
const workspace_offset = workspace_width * trial.location/100;
const offset_left = workspace_offset + goal_offset - box_width/2;
const viewport_height = window.visualViewport.height;
const top_offset = viewport_height * .5;
const separation = box_width * 1.5;
const objects = html`
<div id="object-partner" class="object-moving-box ${colourMap.get('object-disabled')} absolute z-10" style="left: ${offset_left}px; top: ${top_offset - separation}px"></div>
<div id="object-participant" class="object-moving-box ${colourMap.get('object-disabled')} absolute z-10" style="left: ${offset_left}px; top: ${top_offset + separation}px"></div>
`;
workspace.innerHTML = objects;
const object_partner = document.getElementById('object-partner');
const object_participant = document.getElementById('object-participant');
object_partner.addEventListener('click', () => issue_alert('partner_click'));
object_participant.addEventListener('click', () => issue_alert('early_click_choice'));
const partner_turn = html`<span class="font-semi-bold text-gray-800 text-xl">Awaiting your partner's selection</span>
<div class='loading'></div>`
let choice;
let go_clicked = false;
let partner_finished = false;
let participant_finished = false;
let partner_choice_made = false;
const jsPsych = this.jsPsych;
let start_time = 0;
let response_time = 0;
if (trial.selector === 'participant') {
display_choices();
} else {
info_box.innerHTML = partner_turn;
const partner_choice_delay = jsPsych.randomization.sampleExGaussian(2000, 250, 1/100, true);
let random = Math.random();
//These probabilities are based on Azaad and Sebanz (2025)
if (trial.location > 48 && trial.location < 52) {
choice = random < 0.64 ? 'together' : 'alone';
} else if (trial.location > 31 && trial.location < 35) {
choice = random < 0.58 ? 'together' : 'alone';
} else if (trial.location > 64 && trial.location < 68) {
choice = random < 0.67 ? 'together' : 'alone';
}
setTimeout(() => {
info_box.innerHTML = '';
clear_boxes();
display_go();
initiate_partner_action();
}, partner_choice_delay);
}
function display_choices(){
info_box.innerHTML = html`<div class="flex flex-row justify-center items-center gap-2">
<div id="${left_goal}-button" class="w-1/12 py-1 text-slate-50 ${left_colour}">${left_goal.charAt(0).toUpperCase() + left_goal.slice(1)}</div>
<div id="${right_goal}-button" class="w-1/12 py-1 text-slate-50 ${right_colour}">${right_goal.charAt(0).toUpperCase() + right_goal.slice(1)}</div>
</div>`;
const left_goal_button = document.getElementById(`${left_goal}-button`);
const right_goal_button = document.getElementById(`${right_goal}-button`);
start_time = Date.now();
left_goal_button.addEventListener('click', () => {
choice = left_goal;
handle_choice_made(left_goal_button,right_goal_button);
});
right_goal_button.addEventListener('click', () => {
choice = right_goal;
handle_choice_made(left_goal_button,right_goal_button);
});
}
function handle_choice_made(left_goal_button,right_goal_button){
left_goal_button.removeEventListener('click', handle_choice_made);
right_goal_button.removeEventListener('click', handle_choice_made);
left_goal_button.remove();
right_goal_button.remove();
clear_boxes();
display_go();
response_time = Date.now() - start_time;
if (choice === 'together') {
initiate_partner_action();
}
}
function initiate_partner_action(){
const go_delay = trial.selector === 'participant' ? 1200 : 800
const partner_go_delay = jsPsych.randomization.sampleExGaussian(go_delay, 300, 1/100, true);
const partner_teleport_delay = jsPsych.randomization.sampleExGaussian(1200, 200, 1/100, true);
setTimeout(() => {
change_object_colour(object_partner);
partner_choice_made = true;
}, partner_go_delay);
setTimeout(() => {
teleport_object(object_partner);
partner_finished = true;
check_finished();
}, partner_teleport_delay + partner_go_delay);
}
function clear_boxes() {
if (choice === 'together') {
return;
}
if (trial.selector === 'participant') {
object_partner.remove();
} else {
object_participant.remove();
}
}
let go_button;
function display_go() {
if (choice === 'alone' && trial.selector === 'partner') {
return;
}
info_box.innerHTML = html`<div id="go-button" class="mx-auto font-semi-bold ${colourMap.get('gobg')} text-slate-50 text-xl w-1/20 p-2">Go</div>`;
go_button = document.getElementById('go-button');
object_participant.addEventListener('click', () => issue_alert('early_click_go'));
go_button.addEventListener('click', () => handle_go_click(go_button));
}
function handle_go_click() {
go_button.removeEventListener('click', handle_go_click);
go_button.removeEventListener('click', () => issue_alert('early_click_go'));
go_clicked = true;
go_button.remove();
change_object_colour(object_participant);
object_participant.addEventListener('click', () => handle_teleport_click());
}
function handle_teleport_click(){
teleport_object(object_participant);
participant_finished = true;
check_finished();
}
function change_object_colour(object){
object.classList.remove(colourMap.get('object-disabled'));
object.classList.add(colourMap.get('object-enabled'));
}
function teleport_object(object){
let goal_side = 'left';
if (choice === 'together' && trial.together_side === 'right') {
goal_side = 'right';
}
if (choice === 'alone' && trial.together_side === 'left') {
goal_side = 'right';
}
const mid_goal_offset = viewport_width * (1/24);
const new_left_offset = goal_side === 'left' ? mid_goal_offset - box_width/2 : viewport_width - mid_goal_offset - box_width/2;
object.style.transition = 'left 0.35s';
object.style.left = `${new_left_offset}px`;
}
function check_finished(){
let finished = false;
if (choice === 'together') {
if (partner_finished && participant_finished) {
finished = true;
}
}
if (choice === 'alone') {
if (partner_finished || participant_finished) {
finished = true;
}
}
if (finished) {
object_partner.removeEventListener('click', () => issue_alert('partner_click'));
object_participant.removeEventListener('click', () => issue_alert('early_click_choice'));
const save_data = {
choice: choice,
selector: trial.selector,
together_side: trial.together_side,
response_time: response_time,
}
setTimeout(() => {
jsPsych.finishTrial(save_data);
}, 500);
}
}
async function show_alert(message) {
const dialog = document.createElement("dialog");
document.body.appendChild(dialog);
dialog.className = "z-20 fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-4 bg-slate-100 text-red-700 rounded-lg shadow-lg";
dialog.innerText = message;
dialog.show();
setTimeout(function () {
dialog.close();
}, 2000);
}
function issue_alert(issue){
if (issue === 'partner_click'){
show_alert(`That is your partner's object. Please click the lower object.`)
}
if (issue === 'early_click_go' && !go_clicked){
show_alert(`You need to click 'Go' before clicking your object.`)
}
if (issue === 'early_click_choice'){
if (!partner_choice_made && trial.selector === 'partner'){
show_alert(`Please wait for your partner to make a choice.`)
}
if (!choice && trial.selector === 'participant'){
show_alert(`Please make a choice using the buttons below before clicking your object.`)
}
}
}
}
}
export default jsPsychObjectMoving;

370
scripts/text-stimuli.js Normal file
View File

@@ -0,0 +1,370 @@
import getColourMap from './colours.js';
// Define image URLs
const blueImages = {
instructions_1: '/images/blue/instructions_1.png',
instructions_2: '/images/blue/instructions_2.png',
instructions_3: '/images/blue/instructions_3.png',
instructions_4: '/images/blue/instructions_4.png',
};
const redImages = {
instructions_1: '/images/red/instructions_1.png',
instructions_2: '/images/red/instructions_2.png',
instructions_3: '/images/red/instructions_3.png',
instructions_4: '/images/red/instructions_4.png',
};
const html = (strings, ...values) =>
strings
.reduce((result, str, i) => result + str + (values[i] || ''), '')
.trim();
function getStimulusMap(together_colour) {
const colourMap = getColourMap(together_colour);
const stimulusMap = new Map();
// Select the appropriate image map based on the color
const imageMap = together_colour === 'blue' ? blueImages : redImages;
stimulusMap.set(
'pre_consent_info',
html`
<p class="leading-relaxed">
This experiment will need some browser permissions. Please ensure that,
when prompted, you allow the experiment to run in fullscreen and that
you allow yourself to be redirected to Prolific after the experiment is
complete.
</p>
<p class="mt-6">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
stimulusMap.set(
'consent',
html`
<div class="text-left leading-relaxed">
<h1 class="text-2xl font-bold mt-4">
General Participant Information and Consent
</h1>
<h2 class="text-xl font-bold mt-2">
Title of the study: Decision-making with Others
</h2>
<p class="font-semibold mt-2">1. Description of the research project</p>
Welcome to our study on Decision-making with Others. We are
investigating how people choose courses of action when working with
others. The experiment will take less than 20 minutes, with short pauses
in between. However, since this is a multi-participant study, we ask
that you complete the task fully without taking additional breaks. If
you have any further questions, please contact the investigator.
<p class="font-semibold mt-2">
2. Voluntary participation and anonymity
</p>
Participation in the study is voluntary. You can withdraw your consent
to participate in this study at any time and without giving reasons and
without suffering any disadvantages. Even if you terminate the study
early, you are entitled to compensation for your time up to that point.
<p class="font-semibold mt-2">3. Compensation</p>
For participating in the study, you will receive the rate indicated on
the Prolific.com page for this study. The fee will be paid through the
Prolific platform.
<p class="font-semibold mt-2">
4. Scope of data collection and processing
</p>
We will save your responses and judgements to the questions and stimuli
presented in this study. We will also collect information about your
response times. We also ask for your gender, age, and thoughts about the
study following completion. These data will be de-identified, so that
your responses are not saved in a way that one could identify you from
your responses (or which responses are yours). The results and data of
this study will be published as a scientific publication. This will be
done in an anonymized form, i.e. without the data being assigned to a
specific person. The completely anonymized data of this study will be
made available as open data in a secure, internet-based data archive
(osf.io). This study thus follows the recommendations of the German
Research Foundation (DFG) and the German Society for Psychology (DGPs)
for quality assurance in research.
<p class="font-semibold mt-2">5. Legal basis</p>
The legal basis for processing the personal data mentioned is the
consent in accordance with Art. 6 (1) letter a EU GDPR.
<p class="font-semibold mt-2">6. Revocation</p>
You have the right to revoke your consent to data protection at any
time. The revocation of your consent does not affect the legality of the
processing carried out on the basis of your consent until the
revocation. (Revocation with effect for the future). Address your
revocation to the person responsible. You will not suffer any
disadvantages as a result of the revocation
<p class="font-semibold mt-2">
7. Name and Address of the person responsible
</p>
The controller within the meaning of the EU General Data Protection
Regulation (GDPR) and other national data protection laws of the member
states, as well as other data protection regulations is the Westfälische
Wilhelms-Universität Münster (WWU), represented by the Rector, Prof. Dr.
Johannes Wessels, Schlossplatz 2, 48149 MünsterTel.: + 49 251
83-0E-Mail: verwaltung@uni-muenster.de
<p class="font-semibold mt-2">
8. Contact details of the data protection officer
</p>
The data protection officer of the WWU Münster is: Nina Meyer-Pachur
Schlossplatz 2, 48149 Münster Tel.: + 49 251 83-22446 E-Mail:
datenschutz@uni-muenster.de
<p class="font-semibold mt-2">
9. Reference to the rights of those affected
</p>
<p class="leading-relaxed">
According to the General Data Protection Regulation, you basically
have the right to: information (Article 15 GDPR), objection (Article
21 GDPR), data portability (Article 20 GDPR), erasure (Article 17
GDPR), restriction of processing (Article 18 GDPR), rectification
(Article 16 GDPR). If you would like to exercise one of these rights,
please contact one of the contact persons mentioned. You also have the
right to lodge a complaint with the supervisory authority: State
Commissioner for Data Protection and Freedom of Information North
Rhine-Westphalia Helga Block Kavalleriestraße 2-440213
DüsseldorfTelephone: 02 11/384 24-0E-Mail: poststelle@ldi.nrw.de
Homepage: http://www.ldi.nrw.de
</p>
<p class="font-semibold mt-2">
10. Consent to the collection and processing of personal data
</p>
By clicking continue, I hereby voluntarily consent to the collection and
processing of my personal data as part of the research project
Decision-making with others. I have read the data protection
declaration for the project in question, have been adequately informed
and have had the opportunity to ask questions. I have been informed of
the consequences of revoking my consent under data protection law at any
time. I have been informed that my revocation of my consent does not
affect the legality of the processing carried out on the basis of the
consent up to the time of revocation.
</div>
`
);
stimulusMap.set(
'no_consent',
html`
<p class="leading-relaxed mt-6">
You are being redirected to Prolific. If you are not redirected within 10 seconds, please paste the following link into your browser:
<span class="text-blue-500">
${import.meta.env.VITE_NO_CONSENT_URL}
</a>
</p>
`
);
stimulusMap.set(
'multi_user_instructions',
html`
<p class="leading-relaxed">
In this experiment, you will connect and work with a partner.
</p>
<p class="leading-relaxed">
It is
<i>critical</i>
that you only continue if you are able to complete the entire experiment
without interruption and with a stable internet connection.
</p>
<p class="leading-relaxed mt-6">
Press
<strong>SPACE</strong>
once you are ready to be paired with a partner.
</p>
`
);
stimulusMap.set(
'instructions_1',
html`
<p class="leading-relaxed">
In this study, you will work with your partner to complete a task
involving moving objects to one of two goals.
</p>
<p class="leading-relaxed mt-2">
In each round, one of you will be assigned the role of the
<strong>Selector.</strong>
The Selector will choose to which goal the object(s) will be moved.
</p>
<img
class="w-6/12 mx-auto my-8 border-3 border-gray-500 rounded-md"
src="${imageMap.instructions_1}"
alt="instructions_1"
/>
<p class="leading-relaxed mt-6">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
stimulusMap.set(
'instructions_2',
// prettier-ignore
html`
<p class="leading-relaxed">
One of the goals (labelled
<span class="font-semibold ${colourMap.get('together')}">
Together</span>)
will require both of you to move an object each. The other goal
(labelled
<span class="font-semibold ${colourMap.get('alone')}">
Alone</span>)
will
<i>only</i>
require the Selector to move an object.
</p>
<p class="leading-relaxed mt-2">
When you are the Selector, you will see two buttons at the bottom of your
screen. The buttons colours correspond to the colour of the goal location
(e.g., red button for the red goal).
</p>
<img
class="w-6/12 mx-auto my-8 border-3 border-gray-500 rounded-md"
src="${imageMap.instructions_2}"
alt="instructions_2"
/>
<p class="leading-relaxed mt-6">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
stimulusMap.set(
'instructions_3',
html`
<p class="leading-relaxed">
If you are not the Selector, you will need to wait until your partner
makes a decision.
</p>
<p class="leading-relaxed mt-2">
Once a decision is made, you will first need to click the
<span class="${colourMap.get('go')} font-semibold">Go</span>
button to make your object movable. Then, once you click an object, it
will teleport to the goal location.
</p>
<img
class="w-6/12 mx-auto my-8 border-3 border-gray-500 rounded-md"
src="${imageMap.instructions_3}"
alt="instructions_3"
/>
<p class="leading-relaxed mt-6">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
stimulusMap.set(
'instructions_4',
html`
<p class="leading-relaxed">
If the Selector chooses the
<span class="font-semibold ${colourMap.get('alone')}">Alone</span>
goal, one object will disappear, and the Selector will move the
remaining object.
</p>
<p class="leading-relaxed mt-2">
Otherwise, if the Selector chooses
<span class="font-semibold ${colourMap.get('together')}">
Together
</span>
, each of you will need to click an object - only click the object that
is lower on your screen. The other is for your partner.
</p>
<img
class="w-6/12 mx-auto my-8 border-3 border-gray-500 rounded-md"
src="${imageMap.instructions_4}"
alt="instructions_4"
/>
<p class="leading-relaxed mt-6">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
stimulusMap.set(
'pre_practice_instructions',
html`
<p class="leading-relaxed">
You will now complete a short practice task with your partner.
</p>
<p class="leading-relaxed mt-6">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
stimulusMap.set(
'pre_task_instructions',
html`
<p class="leading-relaxed">
Practice complete. You will now complete the main task. This will take
around five minutes.
</p>
<p class="leading-relaxed">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
stimulusMap.set(
'debrief',
html`
<div class="text-left leading-relaxed">
<h1 class="text-2xl font-bold">Debriefing statement</h1>
<h2 class="text-xl font-bold mt-2">
Title of the study: Decision-making with others
</h2>
<p class="mt-2">
Thank you for participating in our study. In this study, we seek to
investigate 1) to what extent online participants suspect that their
interaction partners are not real people and 2) how and when they
report such suspicions.
</p>
<p class="mt-2">
To this end, we created the appearance of additional participants in
the experiment in reality, you performed the task alone. The results
of our study will lend insight into the mechanisms by which we form a
shared understanding of the world around us.
</p>
<p class="mt-2">
Should you have any additional questions, you may contact Dr Shaheed
Azaad at sazaad@uni-muenster.de.
</p>
<p class="mt-6">
Press
<strong>SPACE</strong>
to return to Prolific.
</p>
</div>
`
);
stimulusMap.set(
'complete',
html`You are being redirected to Prolific. If you are not redirected within 10 seconds, please paste the following link into your browser:
<span class="text-blue-500">
${import.meta.env.VITE_COMPLETE_URL}
</a>
`
);
return stimulusMap;
}
export { getStimulusMap, html };

View File

@@ -1,9 +0,0 @@
import html from '../utils/html.js';
export const textStimuli = {
complete: html`Experiment complete. Please paste the following link into your browser to confirm completion on Prolific:
<span class="text-blue-500">
${import.meta.env.VITE_COMPLETE_URL}
</a>
`,
};

View File

@@ -1 +1,32 @@
@import "tailwindcss"; @import "tailwindcss";
.loading:after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
-webkit-animation: ellipsis steps(4, end) 1500ms infinite;
animation: ellipsis steps(4, end) 1500ms infinite;
content: '\2026';
/* ascii code for the ellipsis character */
width: 0px;
}
@keyframes ellipsis {
to {
width: 40px;
}
}
@-webkit-keyframes ellipsis {
to {
width: 40px;
}
}
.jspsych-content {
max-width: 90%;
}
.object-moving-box {
width: 3vw;
height: 3vw;
}