diff --git a/index.js b/index.js index 33bda1e..55d61de 100644 --- a/index.js +++ b/index.js @@ -8,13 +8,17 @@ import '@jspsych/plugin-survey/css/survey.css'; import './styles.css'; import { getStimulusMap } 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; let prolific_id; 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 +const CAPTCHA_TRIAL_COUNT = debug ? 2 : 12; // set to 10 for main experiment function delayed_redirect(url) { setTimeout(() => { @@ -34,9 +38,14 @@ 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'; + +console.log('debug:', debug); 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. ' + probe_closing_text; +let die_result = 3; + switch (COND) { case 0: probe_condition = 'die'; probe_order = 'die_first'; + die_result = 4; break; case 1: probe_condition = 'die'; probe_order = 'die_first'; + die_result = 4; break; case 2: probe_condition = 'difficulty'; probe_order = 'die_first'; + die_result = 4; break; case 3: probe_condition = 'difficulty'; probe_order = 'die_first'; + die_result = 4; break; case 4: probe_condition = 'die'; @@ -135,6 +150,18 @@ const 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 = { type: jsPsychHtmlKeyboardResponse, choices: [' '], @@ -147,6 +174,40 @@ const pre_survey_info = { stimulus: stimulusMap.get('pre_survey_info'), }; +const die_roll_trial = { + type: JsPsychDieRoll, + prompt: html` +
+ Click the die once to start it rolling, then click it again to stop. +
+ `, + 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: + 'Enter the characters you see into the textbox below.
', + 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 = { text: '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.`, 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.`, 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) { - timeline.push(survey); - timeline.push(debrief); + timeline.push(...main_experiment_timeline); } if (!debug) { timeline.push(pre_consent_info); timeline.push(consent_form); timeline.push(enter_fullscreen); - timeline.push(instructions_1); - timeline.push(pre_survey_info); - timeline.push(survey); - timeline.push(debrief); + timeline.push(...main_experiment_timeline); } jsPsych.run(timeline); diff --git a/plugins/jspsych-captcha.js b/plugins/jspsych-captcha.js new file mode 100644 index 0000000..9d1a25d --- /dev/null +++ b/plugins/jspsych-captcha.js @@ -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 + ? `- 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. -
-- Nonsense or random answers may lead to your submission being rejected. -
-- Press - SPACE - to continue. -
- ` + stimulusMap.set( + 'pre_survey_info', + 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. +
++ Nonsense or random answers may lead to your submission being rejected. +
++ Press + SPACE + to continue. +
+ ` ); - stimulusMap.set( 'pre_consent_info', html` @@ -105,15 +109,16 @@ function getStimulusMap() { 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.de8. Contact details of the data protection officer
- 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.de9. Reference to the rights of those affected
@@ -159,14 +164,68 @@ function getStimulusMap() { ` ); - stimulusMap.set( 'instructions_1', html` - TEST +In this experiment, you will be solving some captchas.
++ This will involve identifying slightly distorted letters and numbers + from an image. +
++ Your task is to solve the captchas as quickly and accurately as + possible. You will not be given feedback on your performance. +
++ Press + SPACE + to continue. +
` ); + stimulusMap.set( + 'instructions_2', + html` ++ 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. +
++ The higher the number you roll, the more difficult the captchas will be. +
++ Press + SPACE + to continue. +
+ ` + ); + + stimulusMap.set( + 'instructions_3', + html` ++ 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. +
++ Please ensure that you are in a quiet environment and that you will not + be interrupted for the next few minutes. +
++ 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. +
++ Press + SPACE + when you are ready to begin. +
+ ` + ); stimulusMap.set( 'debrief', @@ -184,10 +243,13 @@ function getStimulusMap() {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.
- 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.
Should you have any additional questions, you may contact Dr Shaheed diff --git a/styles.css b/styles.css index 1554e7f..bbc53c7 100644 --- a/styles.css +++ b/styles.css @@ -29,4 +29,129 @@ .object-moving-box { width: 3vw; height: 3vw; -} \ No newline at end of file +} + +.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; +}