init
BIN
images/blue/instructions_1.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
images/blue/instructions_2.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
images/blue/instructions_3.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
images/blue/instructions_4.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
images/instruction_images.afphoto
Normal file
BIN
images/red/instructions_1.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
images/red/instructions_2.png
Normal file
|
After Width: | Height: | Size: 108 KiB |
BIN
images/red/instructions_3.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
images/red/instructions_4.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
339
index.js
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
302
scripts/plugin-object-moving.js
Normal 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
@@ -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 };
|
||||||
@@ -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>
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
31
styles.css
@@ -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;
|
||||||
|
}
|
||||||