working prototype
This commit is contained in:
92
index.js
92
index.js
@@ -8,13 +8,17 @@ import '@jspsych/plugin-survey/css/survey.css';
|
|||||||
import './styles.css';
|
import './styles.css';
|
||||||
import { getStimulusMap } from './scripts/text-stimuli.js';
|
import { getStimulusMap } from './scripts/text-stimuli.js';
|
||||||
import { textStimuli } from './scripts/text-stimuli.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 experiment_name = import.meta.env.VITE_EXPERIMENT_NAME;
|
const experiment_name = import.meta.env.VITE_EXPERIMENT_NAME;
|
||||||
|
|
||||||
let prolific_id;
|
let prolific_id;
|
||||||
let probe_condition; // will be set to ai or human based on the condition.
|
let probe_condition; // will be set to ai or human based on the condition.
|
||||||
let debug = false;
|
let debug;
|
||||||
let probe_order; // will be set to ai_first or human_first based on the condition
|
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) {
|
function delayed_redirect(url) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -34,9 +38,14 @@ const jsPsych = initJsPsych({
|
|||||||
console.log(jsPsych.data.get().json());
|
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';
|
||||||
|
|
||||||
|
console.log('debug:', debug);
|
||||||
|
|
||||||
prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID');
|
prolific_id = jsPsych.data.getURLVariable('PROLIFIC_PID');
|
||||||
|
|
||||||
@@ -54,22 +63,28 @@ const probe_text_difficulty =
|
|||||||
'while the die roll was random, the difficulty of the captcha task was pre-determined and unrelated to the die roll. ' +
|
'while the die roll was random, the difficulty of the captcha task was pre-determined and unrelated to the die roll. ' +
|
||||||
probe_closing_text;
|
probe_closing_text;
|
||||||
|
|
||||||
|
let die_result = 3;
|
||||||
|
|
||||||
switch (COND) {
|
switch (COND) {
|
||||||
case 0:
|
case 0:
|
||||||
probe_condition = 'die';
|
probe_condition = 'die';
|
||||||
probe_order = 'die_first';
|
probe_order = 'die_first';
|
||||||
|
die_result = 4;
|
||||||
break;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
probe_condition = 'die';
|
probe_condition = 'die';
|
||||||
probe_order = 'die_first';
|
probe_order = 'die_first';
|
||||||
|
die_result = 4;
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
probe_condition = 'difficulty';
|
probe_condition = 'difficulty';
|
||||||
probe_order = 'die_first';
|
probe_order = 'die_first';
|
||||||
|
die_result = 4;
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
probe_condition = 'difficulty';
|
probe_condition = 'difficulty';
|
||||||
probe_order = 'die_first';
|
probe_order = 'die_first';
|
||||||
|
die_result = 4;
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
probe_condition = 'die';
|
probe_condition = 'die';
|
||||||
@@ -135,6 +150,18 @@ const instructions_1 = {
|
|||||||
stimulus: stimulusMap.get('instructions_1'),
|
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 debrief = {
|
const debrief = {
|
||||||
type: jsPsychHtmlKeyboardResponse,
|
type: jsPsychHtmlKeyboardResponse,
|
||||||
choices: [' '],
|
choices: [' '],
|
||||||
@@ -147,6 +174,40 @@ const pre_survey_info = {
|
|||||||
stimulus: stimulusMap.get('pre_survey_info'),
|
stimulus: stimulusMap.get('pre_survey_info'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 captcha_trials = Array.from({ length: CAPTCHA_TRIAL_COUNT }, (_, index) =>
|
||||||
|
create_captcha_trial(index)
|
||||||
|
);
|
||||||
|
|
||||||
const die_probe_row = {
|
const die_probe_row = {
|
||||||
text:
|
text:
|
||||||
'I suspected that the die roll was not random, or the number I received was pre-determined.',
|
'I suspected that the die roll was not random, or the number I received was pre-determined.',
|
||||||
@@ -209,6 +270,14 @@ const survey = {
|
|||||||
text: `I found the captcha task difficult.`,
|
text: `I found the captcha task difficult.`,
|
||||||
value: 'Difficulty',
|
value: 'Difficulty',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: `I felt lucky during the die roll.`,
|
||||||
|
value: 'Luck',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: `I felt unlucky during the die roll.`,
|
||||||
|
value: 'Bad_luck',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: `I think I solved all the captchas correctly.`,
|
text: `I think I solved all the captchas correctly.`,
|
||||||
value: 'Accuracy',
|
value: 'Accuracy',
|
||||||
@@ -331,18 +400,25 @@ const survey = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const main_experiment_timeline = [
|
||||||
|
instructions_1,
|
||||||
|
instructions_2,
|
||||||
|
die_roll_trial,
|
||||||
|
instructions_3,
|
||||||
|
...captcha_trials,
|
||||||
|
pre_survey_info,
|
||||||
|
survey,
|
||||||
|
debrief,
|
||||||
|
];
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
timeline.push(survey);
|
timeline.push(...main_experiment_timeline);
|
||||||
timeline.push(debrief);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!debug) {
|
if (!debug) {
|
||||||
timeline.push(pre_consent_info);
|
timeline.push(pre_consent_info);
|
||||||
timeline.push(consent_form);
|
timeline.push(consent_form);
|
||||||
timeline.push(enter_fullscreen);
|
timeline.push(enter_fullscreen);
|
||||||
timeline.push(instructions_1);
|
timeline.push(...main_experiment_timeline);
|
||||||
timeline.push(pre_survey_info);
|
|
||||||
timeline.push(survey);
|
|
||||||
timeline.push(debrief);
|
|
||||||
}
|
}
|
||||||
jsPsych.run(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;
|
||||||
209
plugins/jspsych-die-roll.js
Normal file
209
plugins/jspsych-die-roll.js
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
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}}.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class DieRollPlugin {
|
||||||
|
constructor(jsPsych) {
|
||||||
|
this.jsPsych = jsPsych;
|
||||||
|
}
|
||||||
|
|
||||||
|
trial(display_element, trial) {
|
||||||
|
const start_time = performance.now();
|
||||||
|
const sanitized_rigged_value = this.get_rigged_value(trial.rigged_value);
|
||||||
|
const final_value =
|
||||||
|
sanitized_rigged_value ??
|
||||||
|
this.jsPsych.randomization.sampleWithoutReplacement(
|
||||||
|
[1, 2, 3, 4, 5, 6],
|
||||||
|
1
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
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'
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_rolling = false;
|
||||||
|
let has_rolled = false;
|
||||||
|
let roll_started_at = null;
|
||||||
|
let roll_stopped_at = null;
|
||||||
|
let die_click_count = 0;
|
||||||
|
|
||||||
|
const clear_timeouts = () => {
|
||||||
|
this.jsPsych.pluginAPI.clearAllTimeouts();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cycle_face = () => {
|
||||||
|
if (!is_rolling) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value = this.random_face(final_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();
|
||||||
|
face_element.textContent = final_value;
|
||||||
|
face_element.classList.remove('jspsych_die_roll_face--active');
|
||||||
|
face_element.classList.remove('jspsych_die_roll_face--interactive');
|
||||||
|
face_element.classList.add('jspsych_die_roll_face--locked');
|
||||||
|
face_element.setAttribute('aria-disabled', 'true');
|
||||||
|
face_element.removeAttribute('tabindex');
|
||||||
|
face_element.style.pointerEvents = 'none';
|
||||||
|
result_element.textContent = trial.result_template.replace(
|
||||||
|
/\{\{value\}\}/gi,
|
||||||
|
final_value
|
||||||
|
);
|
||||||
|
button_element.disabled = false;
|
||||||
|
button_element.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const begin_roll = () => {
|
||||||
|
if (is_rolling || has_rolled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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 = () => {
|
||||||
|
if (!is_rolling && !has_rolled) {
|
||||||
|
begin_roll();
|
||||||
|
} else if (is_rolling && !has_rolled) {
|
||||||
|
finalize_roll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
button_element.disabled = true;
|
||||||
|
face_element.textContent = '';
|
||||||
|
result_element.textContent =
|
||||||
|
'Click the die to start, then click it again to lock in your number.';
|
||||||
|
face_element.classList.add('jspsych_die_roll_face--interactive');
|
||||||
|
face_element.setAttribute('tabindex', '0');
|
||||||
|
face_element.setAttribute('role', 'button');
|
||||||
|
face_element.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
'Virtual die. Click to start and stop.'
|
||||||
|
);
|
||||||
|
|
||||||
|
face_element.addEventListener('click', handle_face_interaction);
|
||||||
|
face_element.addEventListener('keydown', event => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
event.preventDefault();
|
||||||
|
handle_face_interaction();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const end_trial = () => {
|
||||||
|
is_rolling = false;
|
||||||
|
clear_timeouts();
|
||||||
|
const trial_data = {
|
||||||
|
roll: final_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,
|
||||||
|
};
|
||||||
|
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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
random_face(exclude_value) {
|
||||||
|
let value = Math.floor(Math.random() * 6) + 1;
|
||||||
|
if (value === exclude_value) {
|
||||||
|
value = (value % 6) + 1;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get_rigged_value(value) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
if (Number.isFinite(numeric) && numeric >= 1 && numeric <= 6) {
|
||||||
|
return Math.round(numeric);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DieRollPlugin.info = info;
|
||||||
|
|
||||||
|
export default DieRollPlugin;
|
||||||
@@ -9,15 +9,20 @@ export const textStimuli = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getStimulusMap() {
|
function getStimulusMap() {
|
||||||
|
|
||||||
const stimulusMap = new Map();
|
const stimulusMap = new Map();
|
||||||
|
|
||||||
|
stimulusMap.set(
|
||||||
stimulusMap.set('pre_survey_info', html`
|
'pre_survey_info',
|
||||||
<p class="leading-relaxed">
|
html`
|
||||||
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.
|
<p class="leading-relaxed>
|
||||||
|
The captcha task is now complete.
|
||||||
</p>
|
</p>
|
||||||
<p class ="leading-relaxed mt-2">
|
<p class="leading-relaxed mt-2"">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
<p class="leading-relaxed mt-2">
|
||||||
Nonsense or random answers may lead to your submission being rejected.
|
Nonsense or random answers may lead to your submission being rejected.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-6">
|
<p class="mt-6">
|
||||||
@@ -28,7 +33,6 @@ function getStimulusMap() {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
stimulusMap.set(
|
stimulusMap.set(
|
||||||
'pre_consent_info',
|
'pre_consent_info',
|
||||||
html`
|
html`
|
||||||
@@ -105,15 +109,16 @@ function getStimulusMap() {
|
|||||||
</p>
|
</p>
|
||||||
The controller within the meaning of the EU General Data Protection
|
The controller within the meaning of the EU General Data Protection
|
||||||
Regulation (GDPR) and other national data protection laws of the member
|
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.
|
states, as well as other data protection regulations is the University
|
||||||
Johannes Wessels, Schlossplatz 2, 48149 MünsterTel.: + 49 251
|
of Muenster, represented by the Rector, Prof. Dr. Johannes Wessels,
|
||||||
83-0E-Mail: verwaltung@uni-muenster.de
|
Schlossplatz 2, 48149 MünsterTel.: + 49 251 83-0E-Mail:
|
||||||
|
verwaltung@uni-muenster.de
|
||||||
<p class="font-semibold mt-2">
|
<p class="font-semibold mt-2">
|
||||||
8. Contact details of the data protection officer
|
8. Contact details of the data protection officer
|
||||||
</p>
|
</p>
|
||||||
The data protection officer of the University of Muenster is: Nina Meyer-Pachur
|
The data protection officer of the University of Muenster is: Nina
|
||||||
Schlossplatz 2, 48149 Münster Tel.: + 49 251 83-22446 E-Mail:
|
Meyer-Pachur Schlossplatz 2, 48149 Münster Tel.: + 49 251 83-22446
|
||||||
datenschutz@uni-muenster.de
|
E-Mail: datenschutz@uni-muenster.de
|
||||||
<p class="font-semibold mt-2">
|
<p class="font-semibold mt-2">
|
||||||
9. Reference to the rights of those affected
|
9. Reference to the rights of those affected
|
||||||
</p>
|
</p>
|
||||||
@@ -159,14 +164,68 @@ function getStimulusMap() {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
stimulusMap.set(
|
stimulusMap.set(
|
||||||
'instructions_1',
|
'instructions_1',
|
||||||
html`
|
html`
|
||||||
TEST
|
<p>In this experiment, you will be solving some captchas.</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
This will involve identifying slightly distorted letters and numbers
|
||||||
|
from an image.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
Your task is to solve the captchas as quickly and accurately as
|
||||||
|
possible. You will not be given feedback on your performance.
|
||||||
|
</p>
|
||||||
|
<p class="mt-6">
|
||||||
|
Press
|
||||||
|
<strong>SPACE</strong>
|
||||||
|
to continue.
|
||||||
|
</p>
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
stimulusMap.set(
|
||||||
|
'instructions_2',
|
||||||
|
html`
|
||||||
|
<p>
|
||||||
|
To randomise the difficulty of the captchas you will be solving, we will
|
||||||
|
first ask you to roll a virtual die. You will then be shown captchas of
|
||||||
|
a difficulty corresponding to the number you rolled.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
The higher the number you roll, the more difficult the captchas will be.
|
||||||
|
</p>
|
||||||
|
<p class="mt-6">
|
||||||
|
Press
|
||||||
|
<strong>SPACE</strong>
|
||||||
|
to continue.
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
stimulusMap.set(
|
||||||
|
'instructions_3',
|
||||||
|
html`
|
||||||
|
<p>
|
||||||
|
You will now proceed to the captcha task, in which you will need to
|
||||||
|
solve 12 captchas in a row as quickly and accurately as possible.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
Please ensure that you are in a quiet environment and that you will not
|
||||||
|
be interrupted for the next few minutes.
|
||||||
|
</p>
|
||||||
|
<p class="mt-2">
|
||||||
|
Please also ensure that you solve these captchas to the best of your
|
||||||
|
ability. Nonsense or random answers may lead to your submission being
|
||||||
|
rejected.
|
||||||
|
</p>
|
||||||
|
<p class="mt-6 text-red-500">
|
||||||
|
Press
|
||||||
|
<strong>SPACE</strong>
|
||||||
|
when you are ready to begin.
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
stimulusMap.set(
|
stimulusMap.set(
|
||||||
'debrief',
|
'debrief',
|
||||||
@@ -184,10 +243,13 @@ function getStimulusMap() {
|
|||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
To this end, we created the appearance of additional participants in
|
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>
|
||||||
<p class="mt-2">
|
<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>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
Should you have any additional questions, you may contact Dr Shaheed
|
Should you have any additional questions, you may contact Dr Shaheed
|
||||||
|
|||||||
125
styles.css
125
styles.css
@@ -30,3 +30,128 @@
|
|||||||
width: 3vw;
|
width: 3vw;
|
||||||
height: 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.8;
|
||||||
|
box-shadow: 0 15px 25px rgba(15, 23, 42, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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