updated survey

This commit is contained in:
2026-02-19 22:30:33 +01:00
parent 55f927b564
commit 39ddf07973
6 changed files with 128 additions and 935 deletions

296
index.js
View File

@@ -1,28 +1,20 @@
import { initJsPsych } from 'jspsych';
import 'jspsych/css/jspsych.css';
import jsPsychHtmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response';
import generateUniqueUsernames from './scripts/name-gen.js';
import jsPsychFullscreen from '@jspsych/plugin-fullscreen';
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';
import { textStimuli } from './scripts/text-stimuli.js';
const total_participants = import.meta.env.VITE_TOTAL_PARTICIPANTS || 2;
const uniqueUsernames = generateUniqueUsernames(total_participants);
const experiment_name = import.meta.env.VITE_EXPERIMENT_NAME;
let prolific_id;
let probe_condition; // will be set to neutral or reveal based on the condition
let probe_condition; // will be set to ai or human based on the condition.
let debug = false;
const short_version = true; // just using the short version of the task
let probe_order; // will be set to ai_first or human_first based on the condition
function delayed_redirect(url) {
setTimeout(() => {
@@ -31,13 +23,13 @@ function delayed_redirect(url) {
}
const jsPsych = initJsPsych({
on_finish: function () {
on_finish: function() {
jsPsych.getDisplayElement().innerHTML = textStimuli.complete;
},
on_close: function () {
on_close: function() {
delayed_redirect(import.meta.env.VITE_CLOSED_URL);
},
on_data_update: function () {
on_data_update: function() {
if (debug) {
console.log(jsPsych.data.get().json());
}
@@ -50,43 +42,62 @@ prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID');
const COND = Number(jsPsych.data.getURLVariable('C'));
const probe_preamble = 'In this experiment, you worked together with a partner. ';
const probe_closing_text = 'Please share any suspicions you had about your partner during the experiment by indicating your agreement with the following statements.';
const probe_preamble = `In this experiment, we told you that you would roll a die to determine the difficulty of the captcha task.\nHowever, `;
const probe_closing_text = `\n\nPlease share your thoughts and suspicions about this by indicating your agreement with the following statements.`;
const probe_text_neutral = probe_preamble + probe_closing_text;
const probe_text_reveal = probe_preamble + 'However, due to the difficulty of implementing multi-participant online studies, your partner was actually an AI agent trained to behave like a human participant. ' + probe_closing_text;
let together_colour;
const probe_text_die =
probe_preamble +
'the die roll was rigged, so that the number you received (and therefore the difficulty of the captcha task) was pre-determined. ' +
probe_closing_text;
const probe_text_difficulty =
probe_preamble +
'while the die roll was random, the difficulty of the captcha task was pre-determined and unrelated to the die roll. ' +
probe_closing_text;
switch (COND) {
case 0:
probe_condition = 'neutral';
together_colour = 'blue';
probe_condition = 'die';
probe_order = 'die_first';
break;
case 1:
probe_condition = 'neutral';
together_colour = 'red';
probe_condition = 'die';
probe_order = 'die_first';
break;
case 2:
probe_condition = 'reveal';
together_colour = 'blue';
probe_condition = 'difficulty';
probe_order = 'die_first';
break;
case 3:
probe_condition = 'reveal';
together_colour = 'red';
probe_condition = 'difficulty';
probe_order = 'die_first';
break;
case 4:
probe_condition = 'die';
probe_order = 'difficulty_first';
break;
case 5:
probe_condition = 'die';
probe_order = 'difficulty_first';
break;
case 6:
probe_condition = 'difficulty';
probe_order = 'difficulty_first';
break;
case 7:
probe_condition = 'difficulty';
probe_order = 'difficulty_first';
break;
}
const stimulusMap = getStimulusMap(together_colour);
const stimulusMap = getStimulusMap();
const props = {
condition: probe_condition,
together_colour: together_colour,
prolific_id: prolific_id,
experiment_name: experiment_name,
probe_order: probe_order,
cond: COND,
}
};
if (debug) {
console.log(props);
@@ -111,85 +122,19 @@ const consent_form = {
type: jsPsychHtmlButtonResponse,
stimulus: stimulusMap.get('consent'),
choices: ['Exit', 'Continue'],
on_finish: function (data) {
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: [' '],
@@ -202,14 +147,27 @@ const pre_survey_info = {
stimulus: stimulusMap.get('pre_survey_info'),
};
const survey_function = (survey) => {
survey.onAfterRenderPage.add(function (sender, options) {
const die_probe_row = {
text:
'I suspected that the die roll was not random, or the number I received was pre-determined.',
value: 'SuspicionDie',
};
const difficulty_probe_row = {
text:
'I suspected that, contrary to what I was told, the captcha task difficulty was not determined by the die roll.',
value: 'SuspicionDifficulty',
};
const survey_function = survey => {
survey.onAfterRenderPage.add(function(sender, options) {
console.log('Survey page rendered:', sender.currentPage);
if (survey.activePage.name === 'page1') {
if (survey.activePage.name === 'page2') {
const nextButton = document.querySelector('#sv-nav-next > div > input');
if (nextButton) {
let seconds = 15;
const originalText = nextButton.value.replace(/\s*\(\d+\)$/, '') || 'Continue';
let seconds = 20;
const originalText =
nextButton.value.replace(/\s*\(\d+\)$/, '') || 'Continue';
nextButton.disabled = true;
nextButton.value = `${originalText} (${seconds})`;
const interval = setInterval(() => {
@@ -224,8 +182,7 @@ const survey_function = (survey) => {
}
}
});
}
};
const survey = {
type: jsPsychSurvey,
@@ -242,21 +199,27 @@ const survey = {
elements: [
{
type: 'matrix',
name: probe_condition === 'neutral' ? probe_text_neutral : probe_text_reveal,
name:
'Please answer the following questions about your experience in the captcha task.',
alternateRows: true,
isAllRowRequired: debug ? false : true,
rowOrder: 'random',
rows: [
{
text: 'I believed that my partner was actually an AI agent or a bot.',
value: 'SuspicionPartner',
text: `I found the captcha task difficult.`,
value: 'Difficulty',
},
{
text: `There's no question here. Select 'Disagree' to show that you're paying attention.`,
value: 'AttentionCheck',
text: `I think I solved all the captchas correctly.`,
value: 'Accuracy',
},
{
text: 'I believed that my partner was another, human, participant.',
value: 'ConfidencePartner',
text: `I think I solved captchas faster than most other participants would have.`,
value: 'Relative_performance',
},
{
text: `I think I solved captchas slower than most other participants would have.`,
value: 'Relative_performance_slow',
},
],
columns: [
@@ -286,6 +249,55 @@ const survey = {
},
{
name: 'page2',
elements: [
{
type: 'matrix',
name:
probe_condition === 'die'
? probe_text_die
: probe_text_difficulty,
alternateRows: true,
isAllRowRequired: debug ? false : true,
rowOrder: 'random',
rows: [
probe_order === 'die_first'
? die_probe_row
: difficulty_probe_row,
{
text: `There's no question here. Select 'Disagree' to show that you're paying attention.`,
value: 'AttentionCheck',
},
probe_order === 'die_first'
? difficulty_probe_row
: die_probe_row,
],
columns: [
{
value: 5,
text: 'Strongly agree',
},
{
value: 4,
text: 'Agree',
},
{
value: 3,
text: 'Neutral',
},
{
value: 2,
text: 'Disagree',
},
{
value: 1,
text: 'Strongly disagree',
},
],
},
],
},
{
name: 'page3',
elements: [
{
type: 'radiogroup',
@@ -314,60 +326,13 @@ const survey = {
defaultValue: 18,
},
],
}
],
},
};
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(survey);
timeline.push(debrief);
}
@@ -375,18 +340,7 @@ 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(pre_survey_info);
timeline.push(survey);
timeline.push(debrief);

View File

@@ -1,27 +0,0 @@
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;
}

View File

@@ -1,77 +0,0 @@
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;

View File

@@ -1,179 +0,0 @@
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

@@ -1,302 +0,0 @@
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' ? 1400 : 1000
const partner_go_delay = jsPsych.randomization.sampleExGaussian(go_delay, 500, 1/100, true);
const partner_teleport_delay = jsPsych.randomization.sampleExGaussian(1400, 300, 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;

View File

@@ -1,20 +1,3 @@
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',
};
import html from '../utils/html.js';
export const textStimuli = {
@@ -25,14 +8,10 @@ export const textStimuli = {
`,
};
function getStimulusMap(together_colour) {
const colourMap = getColourMap(together_colour);
function getStimulusMap() {
const stimulusMap = new Map();
// Select the appropriate image map based on the color
const imageMap = together_colour === 'blue' ? blueImages : redImages;
stimulusMap.set('pre_survey_info', html`
<p class="leading-relaxed">
@@ -180,169 +159,14 @@ function getStimulusMap(together_colour) {
`
);
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>
TEST
`
);
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',