diff --git a/README.md b/README.md index 1bad263..047c42b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# jspsych-npm-template +# Suspicion-probe Experiment 1 + + diff --git a/images/blue/instructions_1.png b/images/blue/instructions_1.png new file mode 100644 index 0000000..ea2a49f Binary files /dev/null and b/images/blue/instructions_1.png differ diff --git a/images/blue/instructions_2.png b/images/blue/instructions_2.png new file mode 100644 index 0000000..15240ed Binary files /dev/null and b/images/blue/instructions_2.png differ diff --git a/images/blue/instructions_3.png b/images/blue/instructions_3.png new file mode 100644 index 0000000..f5d7f2f Binary files /dev/null and b/images/blue/instructions_3.png differ diff --git a/images/blue/instructions_4.png b/images/blue/instructions_4.png new file mode 100644 index 0000000..6f4ec11 Binary files /dev/null and b/images/blue/instructions_4.png differ diff --git a/images/instruction_images.afphoto b/images/instruction_images.afphoto new file mode 100644 index 0000000..ae97cc9 Binary files /dev/null and b/images/instruction_images.afphoto differ diff --git a/images/red/instructions_1.png b/images/red/instructions_1.png new file mode 100644 index 0000000..910b5fa Binary files /dev/null and b/images/red/instructions_1.png differ diff --git a/images/red/instructions_2.png b/images/red/instructions_2.png new file mode 100644 index 0000000..01584fc Binary files /dev/null and b/images/red/instructions_2.png differ diff --git a/images/red/instructions_3.png b/images/red/instructions_3.png new file mode 100644 index 0000000..18d9e2f Binary files /dev/null and b/images/red/instructions_3.png differ diff --git a/images/red/instructions_4.png b/images/red/instructions_4.png new file mode 100644 index 0000000..db5e98d Binary files /dev/null and b/images/red/instructions_4.png differ diff --git a/index.js b/index.js index b8c6dce..fb302f3 100644 --- a/index.js +++ b/index.js @@ -1,11 +1,31 @@ -import { initJsPsych } from "jspsych"; -import "jspsych/css/jspsych.css"; -import "./styles.css"; -import { delayed_redirect } from "./utils/helpers.js"; -import jsPsychHtmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response"; -import { textStimuli } from './scripts/text_stimuli'; +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'; -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({ 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, - stimulus: `

Hello, world!

`, - 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); diff --git a/scripts/colours.js b/scripts/colours.js new file mode 100644 index 0000000..bb490e7 --- /dev/null +++ b/scripts/colours.js @@ -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; +} diff --git a/scripts/deploy.js b/scripts/deploy.js new file mode 100644 index 0000000..8990114 --- /dev/null +++ b/scripts/deploy.js @@ -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); + }); diff --git a/scripts/name-gen.js b/scripts/name-gen.js new file mode 100644 index 0000000..9fa92da --- /dev/null +++ b/scripts/name-gen.js @@ -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; diff --git a/scripts/plugin-lobby.js b/scripts/plugin-lobby.js new file mode 100644 index 0000000..574895b --- /dev/null +++ b/scripts/plugin-lobby.js @@ -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`
+
+
+
`; + + 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 = + '

' + + trial.start_text + + ' (' + + current_participants + + '/' + + trial.end_number + + ') ' + + '

'; + + refresh_warning_div.innerHTML = + `

Please do not refresh or close this page unless you are not connected to your partner within 5 minutes

`; + + 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` + + + + `; + + const createParticipantsHtml = () => { + let html = ''; + for (let i = 0; i < participants_list.length; i++) { +/* if (trial.show_avatars) { + html += `
${avatar_svg_template( + trial.user_object[participants_list[i]] + )}
`; + } */ + if (participants_list[i] === trial.user_names[0]) { + html += `

` + participants_list[i] + ` (you)

`; + } else { + html += `

` + participants_list[i] + `

`; + } + } + 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`${trial.end_text}`; + loading_animation_div.classList.remove('loading'); + setTimeout(() => { + this.jsPsych.finishTrial(); + }, 2000); + }; + + const participants_interval = setInterval( + updateParticipants, + trial.join_interval + ); + } +} + +export default jsPsychLobby; + + + \ No newline at end of file diff --git a/scripts/plugin-object-moving.js b/scripts/plugin-object-moving.js new file mode 100644 index 0000000..b20e7ba --- /dev/null +++ b/scripts/plugin-object-moving.js @@ -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` +
+
+
+
+
+
+ `; + + 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` +
+
+ `; + + 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`Awaiting your partner's selection +
` + + 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`
+
${left_goal.charAt(0).toUpperCase() + left_goal.slice(1)}
+
${right_goal.charAt(0).toUpperCase() + right_goal.slice(1)}
+
`; + + 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`
Go
`; + 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; + + + \ No newline at end of file diff --git a/scripts/text-stimuli.js b/scripts/text-stimuli.js new file mode 100644 index 0000000..35ddf8f --- /dev/null +++ b/scripts/text-stimuli.js @@ -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` +

+ 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. +

+

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'consent', + html` +
+

+ General Participant Information and Consent +

+

+ Title of the study: Decision-making with Others +

+

1. Description of the research project

+ 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. +

+ 2. Voluntary participation and anonymity +

+ 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. +

3. Compensation

+ 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. +

+ 4. Scope of data collection and processing +

+ 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. +

5. Legal basis

+ The legal basis for processing the personal data mentioned is the + consent in accordance with Art. 6 (1) letter a EU GDPR. +

6. Revocation

+ 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 +

+ 7. Name and Address of the person responsible +

+ 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 +

+ 8. Contact details of the data protection officer +

+ 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 +

+ 9. Reference to the rights of those affected +

+

+ 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 +

+ +

+ 10. Consent to the collection and processing of personal data +

+ 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. +
+ ` + ); + + stimulusMap.set( + 'no_consent', + html` +

+ You are being redirected to Prolific. If you are not redirected within 10 seconds, please paste the following link into your browser: + + ${import.meta.env.VITE_NO_CONSENT_URL} + +

+ ` + ); + + stimulusMap.set( + 'multi_user_instructions', + html` +

+ In this experiment, you will connect and work with a partner. +

+

+ It is + critical + that you only continue if you are able to complete the entire experiment + without interruption and with a stable internet connection. +

+

+ Press + SPACE + once you are ready to be paired with a partner. +

+ ` + ); + + stimulusMap.set( + 'instructions_1', + html` +

+ In this study, you will work with your partner to complete a task + involving moving objects to one of two goals. +

+

+ In each round, one of you will be assigned the role of the + Selector. + The Selector will choose to which goal the object(s) will be moved. +

+ instructions_1 +

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'instructions_2', + // prettier-ignore + html` +

+ One of the goals (labelled + + Together) + will require both of you to move an object each. The other goal + (labelled + + Alone) + will + only + require the Selector to move an object. +

+

+ 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). +

+ instructions_2 +

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'instructions_3', + html` +

+ If you are not the Selector, you will need to wait until your partner + makes a decision. +

+

+ Once a decision is made, you will first need to click the + Go + button to make your object movable. Then, once you click an object, it + will teleport to the goal location. +

+ instructions_3 +

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'instructions_4', + html` +

+ If the Selector chooses the + Alone + goal, one object will disappear, and the Selector will move the + remaining object. +

+

+ Otherwise, if the Selector chooses + + Together + + , 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. +

+ instructions_4 +

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'pre_practice_instructions', + html` +

+ You will now complete a short practice task with your partner. +

+

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'pre_task_instructions', + html` +

+ Practice complete. You will now complete the main task. This will take + around five minutes. +

+

+ Press + SPACE + to continue. +

+ ` + ); + + stimulusMap.set( + 'debrief', + html` +
+

Debriefing statement

+

+ Title of the study: Decision-making with others +

+

+ 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. +

+

+ 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. +

+

+ Should you have any additional questions, you may contact Dr Shaheed + Azaad at sazaad@uni-muenster.de. +

+

+ Press + SPACE + to return to Prolific. +

+
+ ` + ); + + 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: + + ${import.meta.env.VITE_COMPLETE_URL} + + ` + ); + return stimulusMap; +} + +export { getStimulusMap, html }; diff --git a/scripts/text_stimuli.js b/scripts/text_stimuli.js deleted file mode 100644 index e60af76..0000000 --- a/scripts/text_stimuli.js +++ /dev/null @@ -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: - - ${import.meta.env.VITE_COMPLETE_URL} - - `, -}; diff --git a/styles.css b/styles.css index a461c50..1554e7f 100644 --- a/styles.css +++ b/styles.css @@ -1 +1,32 @@ -@import "tailwindcss"; \ No newline at end of file +@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; +} \ No newline at end of file