diff --git a/index.js b/index.js index af3aa57..33bda1e 100644 --- a/index.js +++ b/index.js @@ -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); diff --git a/scripts/colours.js b/scripts/colours.js deleted file mode 100644 index bb490e7..0000000 --- a/scripts/colours.js +++ /dev/null @@ -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; -} diff --git a/scripts/name-gen.js b/scripts/name-gen.js deleted file mode 100644 index 9fa92da..0000000 --- a/scripts/name-gen.js +++ /dev/null @@ -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; diff --git a/scripts/plugin-lobby.js b/scripts/plugin-lobby.js deleted file mode 100644 index 574895b..0000000 --- a/scripts/plugin-lobby.js +++ /dev/null @@ -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`
- - - `; - - 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 += `` + 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 deleted file mode 100644 index 293fc3e..0000000 --- a/scripts/plugin-object-moving.js +++ /dev/null @@ -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` -@@ -180,169 +159,14 @@ function getStimulusMap(together_colour) { ` ); - 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. -
-- Press - SPACE - to continue. -
+ TEST ` ); - 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). -
-- 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. -
-- 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. -
-- 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',