updated survey

This commit is contained in:
2026-02-19 22:30:33 +01:00
parent 55f927b564
commit 39ddf07973
6 changed files with 128 additions and 935 deletions

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,14 +8,10 @@ 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">
@@ -180,169 +159,14 @@ 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>
<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">
Press
<strong>SPACE</strong>
to continue.
</p>
TEST
`
);
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>
<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>
<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">
Press
<strong>SPACE</strong>
to continue.
</p>
`
);
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>
<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">
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.
</p>
`
);
stimulusMap.set(
'debrief',