Compare commits
3 Commits
55f927b564
...
d29e0e392b
| Author | SHA1 | Date | |
|---|---|---|---|
| d29e0e392b | |||
| 9b226d60da | |||
| 39ddf07973 |
478
index.js
478
index.js
@@ -1,28 +1,24 @@
|
||||
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';
|
||||
import JsPsychDieRoll from './plugins/jspsych-die-roll.js';
|
||||
import JsPsychCaptcha from './plugins/jspsych-captcha.js';
|
||||
import html from './utils/html.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 debug = false;
|
||||
|
||||
|
||||
const short_version = true; // just using the short version of the task
|
||||
let probe_condition; // will be set to ai or human based on the condition.
|
||||
let debug;
|
||||
let probe_order; // will be set to ai_first or human_first based on the condition
|
||||
const CAPTCHA_TRIAL_COUNT = debug ? 2 : 12; // set to 10 for main experiment
|
||||
|
||||
function delayed_redirect(url) {
|
||||
setTimeout(() => {
|
||||
@@ -42,51 +38,93 @@ const jsPsych = initJsPsych({
|
||||
console.log(jsPsych.data.get().json());
|
||||
}
|
||||
},
|
||||
show_progress_bar: true,
|
||||
});
|
||||
|
||||
debug = jsPsych.data.getURLVariable('debug') === 'true';
|
||||
debug =
|
||||
jsPsych.data.getURLVariable('debug') === 'true' ||
|
||||
import.meta.env.VITE_DEBUG === 'true';
|
||||
|
||||
|
||||
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_closing_text = `<p class ="mt-6">On the next page, you will be asked about your suspicions and thoughts about this aspect of the study.</p>
|
||||
<p class="mt-6">Press <span class = "font-semibold"> SPACE</span> to continue</p>`;
|
||||
|
||||
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;
|
||||
const probe_text_die =
|
||||
`<p> In this study, the die roll to determine captcha difficulty was rigged: the number you rolled was predetermined.</p><p class="mt-2"> Therefore, the difficulty of the captcha task was also predetermined.</p>` +
|
||||
probe_closing_text;
|
||||
const probe_text_difficulty =
|
||||
`In this study, the die roll to determine captcha difficulty was indeed random. </p><p class="mt-2"> However, the difficulty of the captcha task was pre-determined. It was unrelated to the die roll.
|
||||
</p>` +
|
||||
probe_closing_text;
|
||||
|
||||
let together_colour;
|
||||
const probe_text_baseline = probe_closing_text
|
||||
|
||||
const die_result = Math.random() < 0.5 ? 3 : 4;
|
||||
|
||||
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;
|
||||
case 8:
|
||||
probe_condition = 'baseline';
|
||||
probe_order = 'die_first';
|
||||
break;
|
||||
case 9:
|
||||
probe_condition = 'baseline';
|
||||
probe_order = 'die_first';
|
||||
break;
|
||||
case 10:
|
||||
probe_condition = 'baseline';
|
||||
probe_order = 'difficulty_first';
|
||||
break;
|
||||
case 11:
|
||||
probe_condition = 'baseline';
|
||||
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);
|
||||
@@ -118,42 +156,6 @@ const consent_form = {
|
||||
},
|
||||
};
|
||||
|
||||
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: [' '],
|
||||
@@ -172,24 +174,6 @@ const instructions_3 = {
|
||||
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 +186,83 @@ const pre_survey_info = {
|
||||
stimulus: stimulusMap.get('pre_survey_info'),
|
||||
};
|
||||
|
||||
const survey_function = (survey) => {
|
||||
const die_roll_practice_trial = {
|
||||
type: JsPsychDieRoll,
|
||||
prompt: html`
|
||||
<p class="mt-2">Before the real die roll, you can practice here.</p>
|
||||
<p class="mt-2">
|
||||
Click the die once to start it rolling, then click it again to stop.
|
||||
</p>
|
||||
`,
|
||||
practice_trial: true,
|
||||
practice_roll_limit: 3,
|
||||
roll_duration: 2200,
|
||||
result_template:
|
||||
'Practice roll {{roll_number}}/{{roll_limit}}: You rolled a {{value}}.',
|
||||
};
|
||||
|
||||
const die_roll_trial = {
|
||||
type: JsPsychDieRoll,
|
||||
prompt: html`
|
||||
<p class="mt-2">
|
||||
Click the die once to start it rolling, then click it again to stop.
|
||||
</p>
|
||||
`,
|
||||
rigged_value: die_result,
|
||||
roll_duration: 2200,
|
||||
result_template: `You rolled a {{value}} (moderate difficulty). Click 'continue' to proceed.`,
|
||||
};
|
||||
|
||||
const create_captcha_trial = trial_index => {
|
||||
return {
|
||||
type: JsPsychCaptcha,
|
||||
prompt:
|
||||
'<p class="leading-relaxed text-center">Enter the characters you see into the textbox below.</p>',
|
||||
difficulty: die_result,
|
||||
button_label: 'Continue',
|
||||
error_text: '',
|
||||
allow_refresh: false,
|
||||
require_correct: false,
|
||||
data: {
|
||||
trial_id: 'captcha_entry',
|
||||
captcha_index: trial_index + 1,
|
||||
captcha_total: CAPTCHA_TRIAL_COUNT,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const pre_die_roll_info = {
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
choices: [' '],
|
||||
stimulus: `<p>Practice complete. Now you will roll a die to determine the difficulty of the captcha task.</p>
|
||||
<p class="mt-2">Press <span class ="font-bold">SPACE</span> to continue</p>`,
|
||||
};
|
||||
|
||||
const captcha_trials = Array.from({ length: CAPTCHA_TRIAL_COUNT }, (_, index) =>
|
||||
create_captcha_trial(index)
|
||||
);
|
||||
|
||||
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,15 +277,14 @@ const survey_function = (survey) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const survey = {
|
||||
const survey_1 = {
|
||||
type: jsPsychSurvey,
|
||||
survey_function: survey_function,
|
||||
survey_json: {
|
||||
showQuestionNumbers: false,
|
||||
completeText: 'Done!',
|
||||
completeText: 'Continue',
|
||||
pageNextText: 'Continue',
|
||||
pagePrevText: 'Previous',
|
||||
showPrevButton: false,
|
||||
@@ -242,21 +294,19 @@ 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 felt lucky during the die roll.`,
|
||||
value: 'Luck',
|
||||
},
|
||||
{
|
||||
text: `There's no question here. Select 'Disagree' to show that you're paying attention.`,
|
||||
value: 'AttentionCheck',
|
||||
},
|
||||
{
|
||||
text: 'I believed that my partner was another, human, participant.',
|
||||
value: 'ConfidencePartner',
|
||||
text: `I felt unlucky during the die roll.`,
|
||||
value: 'Bad_luck',
|
||||
},
|
||||
],
|
||||
columns: [
|
||||
@@ -286,6 +336,151 @@ const survey = {
|
||||
},
|
||||
{
|
||||
name: 'page2',
|
||||
elements: [
|
||||
{
|
||||
type: 'matrix',
|
||||
name:
|
||||
'Please answer the following questions about your experience in the captcha task.',
|
||||
alternateRows: true,
|
||||
isAllRowRequired: debug ? false : true,
|
||||
rowOrder: 'random',
|
||||
rows: [
|
||||
{
|
||||
text: `I found the captcha task difficult.`,
|
||||
value: 'Difficulty',
|
||||
},
|
||||
{
|
||||
text: `I think I solved all the captchas correctly.`,
|
||||
value: 'Accuracy',
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{
|
||||
value: 5,
|
||||
text: 'Strongly agree',
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
text: 'Agree',
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
text: 'Neutral',
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
text: 'Disagree',
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
text: 'Strongly disagree',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const pre_manip_info = {
|
||||
type: jsPsychHtmlKeyboardResponse,
|
||||
choices: [' '],
|
||||
stimulus: function () {
|
||||
switch (probe_condition) {
|
||||
case 'die':
|
||||
return probe_text_die;
|
||||
case 'difficulty':
|
||||
return probe_text_difficulty;
|
||||
case 'baseline':
|
||||
return probe_text_baseline;
|
||||
default:
|
||||
return 'ERROR: probe condition not recognized';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const survey_2 = {
|
||||
type: jsPsychSurvey,
|
||||
survey_function: survey_function,
|
||||
survey_json: {
|
||||
showQuestionNumbers: false,
|
||||
completeText: 'Done!',
|
||||
pageNextText: 'Continue',
|
||||
pagePrevText: 'Previous',
|
||||
showPrevButton: false,
|
||||
pages: [
|
||||
{
|
||||
name: 'page1',
|
||||
elements: [
|
||||
{
|
||||
type: 'matrix',
|
||||
name: "Please indicate your agreement with the following statements.",
|
||||
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: 'page2',
|
||||
elements: [
|
||||
{
|
||||
type: 'comment',
|
||||
title: `Please write a sentence or two on what you thought the study was about.`,
|
||||
isRequired: debug == true ? false : true,
|
||||
},
|
||||
{
|
||||
type: 'comment',
|
||||
title: ` Indicate any other thoughts or suspicions you had about the study. \n`,
|
||||
isRequired: debug == true ? false : true,
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'page3',
|
||||
elements: [
|
||||
{
|
||||
type: 'radiogroup',
|
||||
@@ -314,81 +509,36 @@ 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 main_experiment_timeline = [
|
||||
instructions_1,
|
||||
instructions_2,
|
||||
die_roll_practice_trial,
|
||||
pre_die_roll_info,
|
||||
die_roll_trial,
|
||||
instructions_3,
|
||||
...captcha_trials,
|
||||
pre_survey_info,
|
||||
survey_1,
|
||||
pre_manip_info,
|
||||
survey_2,
|
||||
debrief,
|
||||
];
|
||||
|
||||
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);
|
||||
timeline.push(die_roll_trial);
|
||||
timeline.push(...main_experiment_timeline);
|
||||
}
|
||||
|
||||
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);
|
||||
timeline.push(consent_form);
|
||||
timeline.push(...main_experiment_timeline);
|
||||
}
|
||||
jsPsych.run(timeline);
|
||||
|
||||
303
plugins/jspsych-captcha.js
Normal file
303
plugins/jspsych-captcha.js
Normal file
@@ -0,0 +1,303 @@
|
||||
import { ParameterType } from 'jspsych';
|
||||
|
||||
const info = {
|
||||
name: 'captcha',
|
||||
parameters: {
|
||||
prompt: {
|
||||
type: ParameterType.HTML_STRING,
|
||||
default: '',
|
||||
},
|
||||
difficulty: {
|
||||
type: ParameterType.INT,
|
||||
default: 3,
|
||||
},
|
||||
button_label: {
|
||||
type: ParameterType.STRING,
|
||||
default: 'Submit',
|
||||
},
|
||||
allow_refresh: {
|
||||
type: ParameterType.BOOL,
|
||||
default: true,
|
||||
},
|
||||
require_correct: {
|
||||
type: ParameterType.BOOL,
|
||||
default: true,
|
||||
},
|
||||
error_text: {
|
||||
type: ParameterType.HTML_STRING,
|
||||
default: 'That entry did not match, please try again.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class CaptchaPlugin {
|
||||
constructor(jsPsych) {
|
||||
this.jsPsych = jsPsych;
|
||||
this.characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
}
|
||||
|
||||
trial(display_element, trial) {
|
||||
const start_time = performance.now();
|
||||
const attempt_counts = { submission_count: 0, refresh_count: 0 };
|
||||
const difficulty_settings = this.resolve_difficulty(trial.difficulty);
|
||||
let captcha_text = this.generate_text(difficulty_settings.length);
|
||||
let last_target_at_submission = captcha_text;
|
||||
|
||||
display_element.innerHTML = this.build_markup(
|
||||
trial.prompt,
|
||||
trial.button_label,
|
||||
trial.allow_refresh
|
||||
);
|
||||
|
||||
const canvas_element = display_element.querySelector(
|
||||
'.jspsych_captcha_canvas'
|
||||
);
|
||||
const canvas_context = canvas_element.getContext('2d');
|
||||
const input_element = display_element.querySelector(
|
||||
'.jspsych_captcha_input'
|
||||
);
|
||||
const button_element = display_element.querySelector(
|
||||
'.jspsych_captcha_submit'
|
||||
);
|
||||
const refresh_button = display_element.querySelector(
|
||||
'.jspsych_captcha_refresh'
|
||||
);
|
||||
const error_element = display_element.querySelector(
|
||||
'.jspsych_captcha_error'
|
||||
);
|
||||
|
||||
const draw_captcha = () => {
|
||||
this.draw_captcha(canvas_context, captcha_text, difficulty_settings);
|
||||
};
|
||||
|
||||
draw_captcha();
|
||||
input_element.focus();
|
||||
|
||||
if (refresh_button) {
|
||||
refresh_button.addEventListener('click', () => {
|
||||
attempt_counts.refresh_count += 1;
|
||||
captcha_text = this.generate_text(difficulty_settings.length);
|
||||
draw_captcha();
|
||||
error_element.textContent = '';
|
||||
input_element.value = '';
|
||||
input_element.focus();
|
||||
});
|
||||
}
|
||||
|
||||
const length_hint_element = display_element.querySelector(
|
||||
'.jspsych_captcha_length_hint'
|
||||
);
|
||||
|
||||
const update_button_state = () => {
|
||||
const current_length = input_element.value.trim().length;
|
||||
const required_length = captcha_text.length;
|
||||
button_element.disabled = current_length < required_length;
|
||||
if (length_hint_element) {
|
||||
length_hint_element.textContent =
|
||||
current_length < required_length
|
||||
? `Type ${required_length - current_length} more character(s) to continue.`
|
||||
: '';
|
||||
}
|
||||
};
|
||||
|
||||
const finish_trial = (response_value, is_correct) => {
|
||||
const trial_data = {
|
||||
rt: performance.now() - start_time,
|
||||
response: response_value,
|
||||
participant_entry: response_value,
|
||||
participant_entry_length: response_value.length,
|
||||
captcha_target: last_target_at_submission,
|
||||
correct: is_correct,
|
||||
submission_count: attempt_counts.submission_count,
|
||||
manual_refreshes: attempt_counts.refresh_count,
|
||||
difficulty_label: difficulty_settings.label,
|
||||
difficulty_length: difficulty_settings.length,
|
||||
difficulty_level: difficulty_settings.level,
|
||||
};
|
||||
display_element.innerHTML = '';
|
||||
this.jsPsych.finishTrial(trial_data);
|
||||
};
|
||||
|
||||
const validate_response = () => {
|
||||
attempt_counts.submission_count += 1;
|
||||
const response_value = input_element.value.trim().toUpperCase();
|
||||
last_target_at_submission = captcha_text;
|
||||
const is_correct = response_value === captcha_text;
|
||||
|
||||
if (response_value.length < captcha_text.length) {
|
||||
error_element.textContent = `Please enter all ${captcha_text.length} characters shown in the captcha.`;
|
||||
update_button_state();
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_correct || !trial.require_correct) {
|
||||
finish_trial(response_value, is_correct);
|
||||
return;
|
||||
}
|
||||
|
||||
error_element.textContent = trial.error_text;
|
||||
captcha_text = this.generate_text(difficulty_settings.length);
|
||||
draw_captcha();
|
||||
input_element.value = '';
|
||||
input_element.focus();
|
||||
};
|
||||
|
||||
button_element.addEventListener('click', validate_response);
|
||||
input_element.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
validate_response();
|
||||
}
|
||||
});
|
||||
input_element.addEventListener('input', update_button_state);
|
||||
update_button_state();
|
||||
}
|
||||
|
||||
build_markup(prompt, button_label, allow_refresh) {
|
||||
const prompt_html = prompt
|
||||
? `<div class="jspsych_captcha_prompt">${prompt}</div>`
|
||||
: '';
|
||||
const refresh_button = allow_refresh
|
||||
? '<button type="button" class="jspsych_captcha_refresh" aria-label="Get a new captcha">↻</button>'
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="jspsych_captcha">
|
||||
${prompt_html}
|
||||
<div class="jspsych_captcha_canvas_wrapper">
|
||||
<canvas class="jspsych_captcha_canvas" width="260" height="110" role="img" aria-label="Captcha"></canvas>
|
||||
${refresh_button}
|
||||
</div>
|
||||
<label class="jspsych_captcha_label" aria-label="Enter the characters you see into the textbox below.">
|
||||
<input type="text" autocomplete="off" autocorrect="off" autocapitalize="characters" spellcheck="false" class="jspsych_captcha_input" />
|
||||
<span class="jspsych_captcha_length_hint"></span>
|
||||
</label>
|
||||
<button type="button" class="jspsych-btn jspsych_captcha_submit">${button_label}</button>
|
||||
<div class="jspsych_captcha_error" role="status" aria-live="polite"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
draw_captcha(canvas_context, captcha_text, settings) {
|
||||
const { width, height } = canvas_context.canvas;
|
||||
canvas_context.clearRect(0, 0, width, height);
|
||||
const gradient = canvas_context.createLinearGradient(0, 0, width, height);
|
||||
gradient.addColorStop(0, '#f8fafc');
|
||||
gradient.addColorStop(1, '#e2e8f0');
|
||||
canvas_context.fillStyle = gradient;
|
||||
canvas_context.fillRect(0, 0, width, height);
|
||||
|
||||
for (let index = 0; index < settings.noise_lines; index++) {
|
||||
canvas_context.beginPath();
|
||||
canvas_context.moveTo(Math.random() * width, Math.random() * height);
|
||||
canvas_context.bezierCurveTo(
|
||||
Math.random() * width,
|
||||
Math.random() * height,
|
||||
Math.random() * width,
|
||||
Math.random() * height,
|
||||
Math.random() * width,
|
||||
Math.random() * height
|
||||
);
|
||||
canvas_context.lineWidth = 1 + Math.random() * 2;
|
||||
canvas_context.strokeStyle = `rgba(30, 41, 59, ${0.15 +
|
||||
Math.random() * 0.2})`;
|
||||
canvas_context.stroke();
|
||||
}
|
||||
|
||||
canvas_context.font = `bold ${settings.font_size}px 'Courier New', monospace`;
|
||||
canvas_context.textBaseline = 'middle';
|
||||
canvas_context.textAlign = 'center';
|
||||
|
||||
const horizontal_step = width / (captcha_text.length + 1);
|
||||
const center_y = height / 2;
|
||||
|
||||
for (let index = 0; index < captcha_text.length; index++) {
|
||||
const current_character = captcha_text[index];
|
||||
canvas_context.save();
|
||||
const jitter_y = this.random_between(-settings.jitter, settings.jitter);
|
||||
canvas_context.translate(
|
||||
horizontal_step * (index + 1),
|
||||
center_y + jitter_y
|
||||
);
|
||||
canvas_context.rotate(
|
||||
this.random_between(-settings.rotation, settings.rotation)
|
||||
);
|
||||
canvas_context.fillStyle = '#000000';
|
||||
canvas_context.fillText(current_character, 0, 0);
|
||||
canvas_context.restore();
|
||||
}
|
||||
|
||||
const noise_pixels = Math.floor(width * height * settings.speckle_density);
|
||||
const image_data = canvas_context.getImageData(0, 0, width, height);
|
||||
for (let index = 0; index < noise_pixels; index++) {
|
||||
const pixel_offset =
|
||||
(Math.floor(Math.random() * width) +
|
||||
Math.floor(Math.random() * height) * width) *
|
||||
4;
|
||||
const shade = Math.random() > 0.5 ? 255 : 0;
|
||||
image_data.data[pixel_offset] = shade;
|
||||
image_data.data[pixel_offset + 1] = shade;
|
||||
image_data.data[pixel_offset + 2] = shade;
|
||||
image_data.data[pixel_offset + 3] = 255;
|
||||
}
|
||||
canvas_context.putImageData(image_data, 0, 0);
|
||||
}
|
||||
|
||||
generate_text(length) {
|
||||
let generated_text = '';
|
||||
for (let index = 0; index < length; index++) {
|
||||
generated_text += this.characters.charAt(
|
||||
Math.floor(Math.random() * this.characters.length)
|
||||
);
|
||||
}
|
||||
return generated_text;
|
||||
}
|
||||
|
||||
resolve_difficulty(difficulty_input) {
|
||||
const default_level = 3;
|
||||
let normalized_level = Number(difficulty_input);
|
||||
if (!Number.isFinite(normalized_level)) {
|
||||
normalized_level = default_level;
|
||||
}
|
||||
normalized_level = Math.round(Math.min(6, Math.max(1, normalized_level)));
|
||||
|
||||
const base_length = 7;
|
||||
const base_font_size = 20;
|
||||
const base_rotation = 0.25;
|
||||
const max_rotation = base_rotation * 3;
|
||||
const base_noise_lines = 4;
|
||||
const base_speckle_density = 0.01;
|
||||
const base_jitter = 25;
|
||||
const noise_line_step = 0.8;
|
||||
const speckle_step = 0.0015;
|
||||
const jitter_step = 1.6;
|
||||
const rotation_step = (max_rotation - base_rotation) / 5;
|
||||
|
||||
const rotation = base_rotation + rotation_step * (normalized_level - 1);
|
||||
const noise_lines = Math.round(
|
||||
base_noise_lines + noise_line_step * (normalized_level - 1)
|
||||
);
|
||||
const speckle_density =
|
||||
base_speckle_density + speckle_step * (normalized_level - 1);
|
||||
const jitter = base_jitter + jitter_step * (normalized_level - 1);
|
||||
|
||||
return {
|
||||
length: base_length,
|
||||
noise_lines: noise_lines,
|
||||
rotation: rotation,
|
||||
jitter: jitter,
|
||||
font_size: base_font_size,
|
||||
speckle_density: speckle_density,
|
||||
label: `level_${normalized_level}`,
|
||||
level: normalized_level,
|
||||
};
|
||||
}
|
||||
|
||||
random_between(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
}
|
||||
|
||||
CaptchaPlugin.info = info;
|
||||
|
||||
export default CaptchaPlugin;
|
||||
346
plugins/jspsych-die-roll.js
Normal file
346
plugins/jspsych-die-roll.js
Normal file
@@ -0,0 +1,346 @@
|
||||
import { ParameterType } from 'jspsych';
|
||||
|
||||
const info = {
|
||||
name: 'die-roll',
|
||||
parameters: {
|
||||
prompt: {
|
||||
type: ParameterType.HTML_STRING,
|
||||
default: '',
|
||||
},
|
||||
rigged_value: {
|
||||
type: ParameterType.INT,
|
||||
default: null,
|
||||
},
|
||||
roll_duration: {
|
||||
type: ParameterType.INT,
|
||||
default: 2000,
|
||||
},
|
||||
animation_interval: {
|
||||
type: ParameterType.INT,
|
||||
default: 120,
|
||||
},
|
||||
button_label: {
|
||||
type: ParameterType.STRING,
|
||||
default: 'Continue',
|
||||
},
|
||||
result_template: {
|
||||
type: ParameterType.STRING,
|
||||
default: 'You rolled a {{value}}.',
|
||||
},
|
||||
practice_trial: {
|
||||
type: ParameterType.BOOL,
|
||||
default: false,
|
||||
},
|
||||
practice_roll_limit: {
|
||||
type: ParameterType.INT,
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
class DieRollPlugin {
|
||||
constructor(jsPsych) {
|
||||
this.jsPsych = jsPsych;
|
||||
}
|
||||
|
||||
trial(display_element, trial) {
|
||||
const start_time = performance.now();
|
||||
const practice_enabled = Boolean(trial.practice_trial);
|
||||
const practice_roll_limit = practice_enabled
|
||||
? this.get_practice_roll_limit(trial.practice_roll_limit)
|
||||
: 1;
|
||||
const sanitized_rigged_value = practice_enabled
|
||||
? null
|
||||
: this.get_rigged_value(trial.rigged_value);
|
||||
const get_next_result_value = () =>
|
||||
sanitized_rigged_value ?? this.get_random_value();
|
||||
let pending_final_value = get_next_result_value();
|
||||
let last_roll_value = null;
|
||||
|
||||
display_element.innerHTML = this.build_markup(
|
||||
trial.prompt,
|
||||
trial.button_label
|
||||
);
|
||||
|
||||
const face_element = display_element.querySelector(
|
||||
'.jspsych_die_roll_face'
|
||||
);
|
||||
const result_element = display_element.querySelector(
|
||||
'.jspsych_die_roll_result'
|
||||
);
|
||||
const button_element = display_element.querySelector(
|
||||
'.jspsych_die_roll_button'
|
||||
);
|
||||
|
||||
const PRACTICE_UNLOCK_DELAY_MS = 600;
|
||||
const MIN_IDLE_CLICK_INTERVAL_MS = 350;
|
||||
let is_rolling = false;
|
||||
let has_rolled = false;
|
||||
let roll_started_at = null;
|
||||
let roll_stopped_at = null;
|
||||
let die_click_count = 0;
|
||||
let practice_rolls_completed = 0;
|
||||
let practice_reenable_timeout = null;
|
||||
let previous_face_value = null;
|
||||
let last_face_interaction_at = null;
|
||||
|
||||
const clear_timeouts = () => {
|
||||
if (practice_enabled && practice_reenable_timeout !== null) {
|
||||
window.clearTimeout(practice_reenable_timeout);
|
||||
practice_reenable_timeout = null;
|
||||
}
|
||||
this.jsPsych.pluginAPI.clearAllTimeouts();
|
||||
};
|
||||
|
||||
const lock_die_face = () => {
|
||||
face_element.classList.remove('jspsych_die_roll_face--interactive');
|
||||
face_element.classList.remove('jspsych_die_roll_face--active');
|
||||
face_element.classList.add('jspsych_die_roll_face--locked');
|
||||
face_element.setAttribute('aria-disabled', 'true');
|
||||
face_element.removeAttribute('tabindex');
|
||||
face_element.style.pointerEvents = 'none';
|
||||
};
|
||||
|
||||
const make_die_interactive = () => {
|
||||
face_element.classList.remove('jspsych_die_roll_face--locked');
|
||||
face_element.classList.add('jspsych_die_roll_face--interactive');
|
||||
face_element.setAttribute('tabindex', '0');
|
||||
face_element.removeAttribute('aria-disabled');
|
||||
face_element.style.pointerEvents = '';
|
||||
};
|
||||
|
||||
const get_next_face_value = () => {
|
||||
let value;
|
||||
do {
|
||||
value = Math.floor(Math.random() * 6) + 1;
|
||||
} while (
|
||||
value === previous_face_value ||
|
||||
(sanitized_rigged_value !== null && value === sanitized_rigged_value)
|
||||
);
|
||||
previous_face_value = value;
|
||||
return value;
|
||||
};
|
||||
|
||||
const cycle_face = () => {
|
||||
if (!is_rolling) {
|
||||
return;
|
||||
}
|
||||
const value = get_next_face_value();
|
||||
face_element.textContent = value;
|
||||
this.jsPsych.pluginAPI.setTimeout(cycle_face, trial.animation_interval);
|
||||
};
|
||||
|
||||
const finalize_roll = () => {
|
||||
if (!is_rolling || has_rolled) {
|
||||
return;
|
||||
}
|
||||
is_rolling = false;
|
||||
has_rolled = true;
|
||||
die_click_count += 1;
|
||||
roll_stopped_at = performance.now();
|
||||
clear_timeouts();
|
||||
last_roll_value = pending_final_value;
|
||||
face_element.textContent = last_roll_value;
|
||||
lock_die_face();
|
||||
practice_rolls_completed += 1;
|
||||
const rolls_remaining = Math.max(
|
||||
practice_roll_limit - practice_rolls_completed,
|
||||
0
|
||||
);
|
||||
const formatted_result = this.render_result_text(
|
||||
trial.result_template,
|
||||
{
|
||||
value: last_roll_value,
|
||||
roll_number: practice_rolls_completed,
|
||||
roll_limit: practice_enabled ? practice_roll_limit : null,
|
||||
rolls_remaining: practice_enabled ? rolls_remaining : null,
|
||||
}
|
||||
);
|
||||
|
||||
if (practice_enabled) {
|
||||
if (rolls_remaining > 0) {
|
||||
result_element.innerHTML =
|
||||
`${formatted_result} <p class = "mt-2"> You may roll again (${rolls_remaining} ` +
|
||||
`practice ${rolls_remaining === 1 ? 'roll' : 'rolls'} remaining) or click 'Continue' to proceed.</p>`;
|
||||
// Brief delay prevents a double-click from immediately starting a new roll.
|
||||
practice_reenable_timeout = window.setTimeout(() => {
|
||||
make_die_interactive();
|
||||
has_rolled = false;
|
||||
practice_reenable_timeout = null;
|
||||
}, PRACTICE_UNLOCK_DELAY_MS);
|
||||
} else {
|
||||
result_element.textContent =
|
||||
`${formatted_result} Practice complete. Click 'Continue' to proceed.`;
|
||||
}
|
||||
} else {
|
||||
result_element.textContent = formatted_result;
|
||||
}
|
||||
|
||||
button_element.disabled = false;
|
||||
if (!practice_enabled || rolls_remaining === 0) {
|
||||
button_element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const begin_roll = () => {
|
||||
if (is_rolling || has_rolled) {
|
||||
return;
|
||||
}
|
||||
previous_face_value = null;
|
||||
pending_final_value = get_next_result_value();
|
||||
is_rolling = true;
|
||||
die_click_count += 1;
|
||||
roll_started_at = performance.now();
|
||||
result_element.textContent = 'Rolling... click again to stop the die.';
|
||||
face_element.classList.add('jspsych_die_roll_face--active');
|
||||
button_element.disabled = true;
|
||||
clear_timeouts();
|
||||
cycle_face();
|
||||
};
|
||||
|
||||
const handle_face_interaction = event => {
|
||||
const event_type = event?.type ?? 'unknown';
|
||||
|
||||
if (
|
||||
(event_type === 'pointerdown' || event_type === 'click') &&
|
||||
typeof event?.button === 'number' &&
|
||||
event.button !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (practice_enabled) {
|
||||
const now = performance.now();
|
||||
if (!is_rolling && last_face_interaction_at !== null) {
|
||||
const elapsed = now - last_face_interaction_at;
|
||||
if (elapsed < MIN_IDLE_CLICK_INTERVAL_MS) {
|
||||
event?.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
last_face_interaction_at = now;
|
||||
}
|
||||
|
||||
if (is_rolling) {
|
||||
finalize_roll();
|
||||
return;
|
||||
}
|
||||
if (!has_rolled) {
|
||||
begin_roll();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
button_element.disabled = true;
|
||||
face_element.textContent = '';
|
||||
result_element.innerHTML = practice_enabled
|
||||
? `<p>Practice rolling the die. You can complete up to ${practice_roll_limit}</p>` +
|
||||
`practice ${practice_roll_limit === 1 ? 'roll' : 'rolls'}. <p class = "mt-2"> Click the die to start, then click it again to stop. </p>`
|
||||
: 'Click the die to start, then click it again to lock in your number.';
|
||||
make_die_interactive();
|
||||
face_element.setAttribute('role', 'button');
|
||||
face_element.setAttribute(
|
||||
'aria-label',
|
||||
'Virtual die. Click to start and stop.'
|
||||
);
|
||||
|
||||
const face_interaction_event =
|
||||
typeof window.PointerEvent === 'function' ? 'pointerdown' : 'click';
|
||||
face_element.addEventListener(face_interaction_event, handle_face_interaction);
|
||||
face_element.addEventListener('keydown', event => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
handle_face_interaction(event);
|
||||
}
|
||||
});
|
||||
|
||||
const end_trial = () => {
|
||||
is_rolling = false;
|
||||
clear_timeouts();
|
||||
const trial_data = {
|
||||
roll: last_roll_value,
|
||||
rigged_value: sanitized_rigged_value,
|
||||
rigged: sanitized_rigged_value !== null,
|
||||
die_click_count: die_click_count,
|
||||
roll_started_after_ms: roll_started_at
|
||||
? roll_started_at - start_time
|
||||
: null,
|
||||
roll_duration_ms:
|
||||
roll_started_at && roll_stopped_at
|
||||
? roll_stopped_at - roll_started_at
|
||||
: null,
|
||||
rt: performance.now() - start_time,
|
||||
practice_trial: practice_enabled,
|
||||
practice_roll_limit: practice_enabled ? practice_roll_limit : null,
|
||||
practice_rolls_completed: practice_enabled
|
||||
? practice_rolls_completed
|
||||
: null,
|
||||
practice_rolls_remaining: practice_enabled
|
||||
? Math.max(practice_roll_limit - practice_rolls_completed, 0)
|
||||
: null,
|
||||
};
|
||||
display_element.innerHTML = '';
|
||||
this.jsPsych.finishTrial(trial_data);
|
||||
};
|
||||
|
||||
button_element.addEventListener('click', () => {
|
||||
if (!button_element.disabled) {
|
||||
end_trial();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
build_markup(prompt, button_label) {
|
||||
const prompt_html = prompt
|
||||
? `<div class="jspsych_die_roll_prompt">${prompt}</div>`
|
||||
: '';
|
||||
return `
|
||||
<div class="jspsych_die_roll">
|
||||
${prompt_html}
|
||||
<div class="jspsych_die_roll_face">?</div>
|
||||
<div class="jspsych_die_roll_result" aria-live="polite"></div>
|
||||
<button class="jspsych-btn jspsych_die_roll_button" type="button">${button_label}</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
get_random_value() {
|
||||
return this.jsPsych.randomization.sampleWithoutReplacement(
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
1
|
||||
)[0];
|
||||
}
|
||||
|
||||
get_rigged_value(value) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric >= 1 && numeric <= 6) {
|
||||
return Math.round(numeric);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get_practice_roll_limit(value) {
|
||||
const numeric = Number(value);
|
||||
if (Number.isFinite(numeric) && numeric >= 1) {
|
||||
return Math.round(numeric);
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
render_result_text(template, replacements) {
|
||||
if (typeof template !== 'string' || template.length === 0) {
|
||||
return '';
|
||||
}
|
||||
return Object.entries(replacements).reduce((text, [key, value]) => {
|
||||
if (value === null || value === undefined) {
|
||||
return text;
|
||||
}
|
||||
const pattern = new RegExp(`\\{\\{${key}\\}\\}`, 'gi');
|
||||
return text.replace(pattern, value);
|
||||
}, template);
|
||||
}
|
||||
}
|
||||
|
||||
DieRollPlugin.info = info;
|
||||
|
||||
export default DieRollPlugin;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
|
||||
@@ -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,21 +8,22 @@ 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">
|
||||
You will now answer a few questions about your experience in the experiment. It is important that you read the questions carefully and answer them honestly.
|
||||
stimulusMap.set(
|
||||
'pre_survey_info',
|
||||
html`
|
||||
<p class="leading-relaxed>
|
||||
The captcha task is now complete.
|
||||
</p>
|
||||
<p class="leading-relaxed mt-2"">
|
||||
You will now answer a few questions about your experience in the
|
||||
experiment.
|
||||
</p>
|
||||
<p class="leading-relaxed mt-2">
|
||||
Nonsense or random answers may lead to your submission being rejected.
|
||||
It is important that you read the questions carefully and
|
||||
answer them honestly.
|
||||
</p>
|
||||
<p class="mt-6">
|
||||
Press
|
||||
@@ -49,7 +33,6 @@ function getStimulusMap(together_colour) {
|
||||
`
|
||||
);
|
||||
|
||||
|
||||
stimulusMap.set(
|
||||
'pre_consent_info',
|
||||
html`
|
||||
@@ -126,15 +109,16 @@ function getStimulusMap(together_colour) {
|
||||
</p>
|
||||
The controller within the meaning of the EU General Data Protection
|
||||
Regulation (GDPR) and other national data protection laws of the member
|
||||
states, as well as other data protection regulations is the University of Muenster, represented by the Rector, Prof. Dr.
|
||||
Johannes Wessels, Schlossplatz 2, 48149 MünsterTel.: + 49 251
|
||||
83-0E-Mail: verwaltung@uni-muenster.de
|
||||
states, as well as other data protection regulations is the University
|
||||
of Muenster, represented by the Rector, Prof. Dr. Johannes Wessels,
|
||||
Schlossplatz 2, 48149 MünsterTel.: + 49 251 83-0E-Mail:
|
||||
verwaltung@uni-muenster.de
|
||||
<p class="font-semibold mt-2">
|
||||
8. Contact details of the data protection officer
|
||||
</p>
|
||||
The data protection officer of the University of Muenster is: Nina Meyer-Pachur
|
||||
Schlossplatz 2, 48149 Münster Tel.: + 49 251 83-22446 E-Mail:
|
||||
datenschutz@uni-muenster.de
|
||||
The data protection officer of the University of Muenster is: Nina
|
||||
Meyer-Pachur Schlossplatz 2, 48149 Münster Tel.: + 49 251 83-22446
|
||||
E-Mail: datenschutz@uni-muenster.de
|
||||
<p class="font-semibold mt-2">
|
||||
9. Reference to the rights of those affected
|
||||
</p>
|
||||
@@ -180,44 +164,18 @@ 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>In this experiment, you will be solving captchas.</p>
|
||||
<p class="mt-2">
|
||||
This will involve identifying slightly distorted letters and numbers
|
||||
from an image.
|
||||
</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">
|
||||
<p class="mt-6">
|
||||
Your task is to solve the captchas as quickly and accurately as
|
||||
possible. </p>
|
||||
<p class="mt-6">
|
||||
Press
|
||||
<strong>SPACE</strong>
|
||||
to continue.
|
||||
@@ -227,31 +185,16 @@ function getStimulusMap(together_colour) {
|
||||
|
||||
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>
|
||||
In this study, we will randomise the difficulty of the captchas you will solve. </p>
|
||||
<p class="mt-2">
|
||||
You will roll a virtual die to determine the difficulty of the captchas you will see.
|
||||
</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 class="mt-2">
|
||||
The higher the number you roll, the more difficult the captchas will be.
|
||||
</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">
|
||||
<p class="mt-6">
|
||||
Press
|
||||
<strong>SPACE</strong>
|
||||
to continue.
|
||||
@@ -262,84 +205,14 @@ function getStimulusMap(together_colour) {
|
||||
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>
|
||||
You will now proceed to the captcha task </p> <p class = "mt-2"> You will need to
|
||||
solve 12 captchas in a row as quickly and accurately as possible.
|
||||
</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">
|
||||
<p class="mt-6 text-red-500">
|
||||
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.
|
||||
when you are ready to begin.
|
||||
</p>
|
||||
`
|
||||
);
|
||||
@@ -360,10 +233,13 @@ function getStimulusMap(together_colour) {
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
To this end, we created the appearance of additional participants in
|
||||
the experiment – in reality, you performed the task alone with your 'partner's' actions being peformed by a computer.
|
||||
the experiment – in reality, you performed the task alone with your
|
||||
'partner's' actions being peformed by a computer.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
You may have also read in the post-experiment questionnaire that your partner was either an AI agent – this was to examine whether people’s suspicions are biased by the way in which they are asked about them.
|
||||
You may have also read in the post-experiment questionnaire that your
|
||||
partner was either an AI agent – this was to examine whether people’s
|
||||
suspicions are biased by the way in which they are asked about them.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
Should you have any additional questions, you may contact Dr Shaheed
|
||||
|
||||
127
styles.css
127
styles.css
@@ -30,3 +30,130 @@
|
||||
width: 3vw;
|
||||
height: 3vw;
|
||||
}
|
||||
|
||||
.jspsych_die_roll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.jspsych_die_roll_prompt {
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
.jspsych_die_roll_face {
|
||||
width: 7.5rem;
|
||||
height: 7.5rem;
|
||||
border-radius: 1rem;
|
||||
border: 4px solid #0f172a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(145deg, #f1f5f9, #e2e8f0);
|
||||
box-shadow: 0 20px 35px rgba(15, 23, 42, 0.2);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
|
||||
}
|
||||
|
||||
.jspsych_die_roll_result {
|
||||
font-weight: 600;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.jspsych_die_roll_face--interactive:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 24px 40px rgba(15, 23, 42, 0.25);
|
||||
}
|
||||
|
||||
.jspsych_die_roll_face--active {
|
||||
animation: pulse 0.6s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.jspsych_die_roll_face--locked {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
box-shadow: 0 15px 25px rgba(15, 23, 42, 0.15);
|
||||
background: linear-gradient(145deg, #d4d8de, #cbd2db);
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
transform: scale(1);
|
||||
}
|
||||
to {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.jspsych_captcha {
|
||||
max-width: 34rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.jspsych_captcha_canvas_wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jspsych_captcha_canvas {
|
||||
border-radius: 0.75rem;
|
||||
border: 2px solid #cbd5f5;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.jspsych_captcha_refresh {
|
||||
border: none;
|
||||
background: #e2e8f0;
|
||||
border-radius: 9999px;
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
font-size: 1.35rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jspsych_captcha_label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
font-weight: 600;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jspsych_captcha_input {
|
||||
border: 2px solid #cbd5f5;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: 0.2rem;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
.jspsych_captcha_length_hint {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
.jspsych_captcha_error {
|
||||
min-height: 1.25rem;
|
||||
color: #b91c1c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user