updated based on T's feedback

This commit is contained in:
2025-07-21 12:37:32 +02:00
parent 85ffeb480d
commit c960316774
4 changed files with 196 additions and 230 deletions

View File

@@ -8,6 +8,7 @@ import jsPsychHtmlButtonResponse from '@jspsych/plugin-html-button-response';
import jsPsychSurvey from '@jspsych/plugin-survey';
import jsPsychRecordCall from './scripts/record-call.js';
import jsPsychMarkCall from './scripts/mark-call.js';
import jsPsychTranscribeCall from './scripts/transcribe-call.js';
import '@jspsych/plugin-survey/css/survey.css'
import jsPsychInitializeMicrophone from '@jspsych/plugin-initialize-microphone';
import jsPsychBrowserCheck from '@jspsych/plugin-browser-check';
@@ -69,7 +70,7 @@ const publication_consent = {
const continue_button = document.getElementsByClassName('jspsych-btn')[0];
continue_button.disabled = true;
let timeLeft = 15;
let timeLeft = 17;
if(debug) {
timeLeft = 0;
@@ -129,6 +130,10 @@ const mark_page = {
type: jsPsychMarkCall,
};
const transcribe_page = {
type: jsPsychTranscribeCall,
};
const data_quality_warning = {
type: jsPsychHtmlButtonResponse,
stimulus: textStimuli.data_quality_warning,
@@ -142,7 +147,7 @@ const exit_page = {
};
const recording_loop = {
timeline: [recording_page, mark_page, exit_page],
timeline: [recording_page, mark_page, transcribe_page, exit_page],
loop_function: function(data){
const last_trial_data = jsPsych.data.getLastTrialData();
if(jsPsych.pluginAPI.compareKeys(last_trial_data.trials[0].response, 'q')){
@@ -170,8 +175,6 @@ const post_experiment_survey = {
let timeline = []
if (debug) {
timeline.push(data_quality_warning);
timeline.push(browser_check);
timeline.push(initialize_microphone);
timeline.push(recording_loop);

View File

@@ -74,6 +74,33 @@ import html from '../utils/html.js';
}
display_element.innerHTML = `
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.key-icon {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 28px;
height: 24px;
padding: 2px 6px;
background: linear-gradient(145deg, #f0f0f0, #e0e0e0);
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 2px rgba(0,0,0,0.1), inset 0 1px 0 rgba(255,255,255,0.6);
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 11px;
font-weight: 600;
color: #333;
margin: 0 2px;
vertical-align: middle;
}
.key-icon.space {
min-width: 60px;
}
.key-icon i {
font-size: 12px;
}
</style>
<div style="text-align: center; padding: 20px;">
<div style="margin: 20px 0;">
<audio style="display: none;" id="playback-audio">
@@ -83,23 +110,55 @@ import html from '../utils/html.js';
<div style="margin: 30px 0;">
<p style="font-weight: bold; margin-bottom: 15px;">Listen to your recording and mark when you would lift:</p>
<p style="margin: 10px 0; font-size: 14px; color: #666;">You can use keyboard controls (space and arrow keys) to play and skip the recording.</p>
<p style="margin: 10px 0; font-size: 14px; color: #666;">Adjust playback speed for more precise control.</p>
<div class="space-y-4 text-sm text-gray-700">
<p class="text-base font-semibold text-black">
🎧 Listen to your recording and mark where you would lift:
</p>
<ul class="list-disc list-inside space-y-2">
<li>
Pause the audio at the lift point and click
<span class="italic">'Mark this as lifting point'</span> or press
<span class="key-icon">ENTER</span> to save it.
</li>
<li>
You can use the keyboard or the buttons below to mark lift points.
</li>
<li>
Keyboard shortcuts:
<ul class="list-inside list-disc ml-5 mt-1 space-y-1 text-gray-600">
<li><span class="key-icon space">SPACE</span> Play/Pause</li>
<li><span class="key-icon"><i class="fas fa-arrow-left"></i></span> Skip back 0.1s</li>
<li><span class="key-icon"><i class="fas fa-arrow-right"></i></span> Skip forward 0.1s</li>
<li><span class="key-icon">ENTER</span> Mark lift point</li>
</ul>
</li>
<li>
Adjust playback speed if you need more precise control.
</li>
</ul>
</div>
<div style="max-width: 500px; margin: 0 auto; background: #f9f9f9; padding: 20px; border-radius: 10px;">
<div style="margin-bottom: 20px; display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap;">
<button id="play-pause-btn" style="
padding: 12px 24px;
font-size: 16px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
min-width: 100px;
">▶ Play</button>
<div style="margin-bottom: 20px; text-align: center;">
<div style="margin-bottom: 15px; font-size: 14px; color: #666;">
Playing at 0.5x speed
</div>
<div style="display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap;">
<button id="play-pause-btn" style="
padding: 12px 24px;
font-size: 16px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
min-width: 140px;
">▶ Play <span class="key-icon space">SPACE</span></button>
</div>
<!--
<div style="display: flex; gap: 5px; align-items: center;">
<span style="font-size: 14px; color: #666;">Speed:</span>
<button id="speed-025" class="speed-btn" data-speed="0.25" style="
@@ -130,6 +189,7 @@ import html from '../utils/html.js';
cursor: pointer;
">1x</button>
</div>
-->
</div>
<div style="margin: 20px 0; display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap;">
@@ -141,7 +201,7 @@ import html from '../utils/html.js';
border: none;
border-radius: 5px;
cursor: pointer;
">⏪ Skip Back 5%</button>
">⏪ Skip Back 0.1s <span class="key-icon"><i class="fas fa-arrow-left"></i></span></button>
<button id="skip-forward-btn" style="
padding: 8px 16px;
@@ -151,9 +211,10 @@ import html from '../utils/html.js';
border: none;
border-radius: 5px;
cursor: pointer;
">Skip Forward 5% ⏩</button>
">Skip Forward 0.1s <span class="key-icon"><i class="fas fa-arrow-right"></i></span> ⏩</button>
</div>
<!--
<div id="current-time-display" style="
font-size: 18px;
font-weight: bold;
@@ -167,21 +228,32 @@ import html from '../utils/html.js';
<span>Duration: <span id="duration-display">0:00</span></span>
<span>Progress: <span id="progress-display">0%</span></span>
</div>
-->
<div style="font-size: 11px; color: #888; text-align: center; margin-bottom: 15px; line-height: 1.4;">
<strong>Keyboard shortcuts:</strong> Spacebar = Play/Pause | ← → = Skip Back/Forward | Enter = Mark Lift Point
</div>
<div style="margin: 20px 0; text-align: center;">
<button id="mark-lift-point-btn" style="
padding: 12px 24px;
font-size: 16px;
background-color: #ff9800;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
">Mark This as Lift Point</button>
<div style="margin: 20px 0;">
<!-- Progress Bar -->
<div style="margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; font-size: 12px; color: #666; margin-bottom: 5px;">
<span id="start-time">0:00</span>
<span id="current-time">0:00</span>
<span id="end-time">0:00</span>
</div>
<div style="width: 100%; background-color: #ddd; border-radius: 10px; height: 8px; position: relative;">
<div id="progress-bar" style="background-color: #007cba; height: 100%; border-radius: 10px; width: 0%; transition: width 0.1s ease;"></div>
</div>
</div>
<div style="text-align: center;">
<button id="mark-lift-point-btn" style="
padding: 12px 24px;
font-size: 16px;
background-color: #ff9800;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
">Mark This as Lift Point <span class="key-icon">ENTER</span></button>
</div>
</div>
<div id="marked-point-display" style="
@@ -220,9 +292,11 @@ import html from '../utils/html.js';
setupMarkingEvents(recordingData, audioBlob) {
const audio = document.getElementById('playback-audio');
const playPauseBtn = document.getElementById('play-pause-btn');
const currentTimeDisplay = document.getElementById('current-time-display');
const durationDisplay = document.getElementById('duration-display');
const progressDisplay = document.getElementById('progress-display');
// Progress bar elements
const startTimeDisplay = document.getElementById('start-time');
const currentTimeDisplay = document.getElementById('current-time');
const endTimeDisplay = document.getElementById('end-time');
const progressBar = document.getElementById('progress-bar');
const skipBackBtn = document.getElementById('skip-back-btn');
const skipForwardBtn = document.getElementById('skip-forward-btn');
const markLiftPointBtn = document.getElementById('mark-lift-point-btn');
@@ -250,15 +324,16 @@ import html from '../utils/html.js';
// Update display function
const updateDisplay = () => {
const currentTime = audio.currentTime;
const progress = audioDuration > 0 ? (currentTime / audioDuration * 100).toFixed(1) : 0;
const progress = audioDuration > 0 ? (currentTime / audioDuration * 100) : 0;
currentTimeDisplay.textContent = `Current position: ${formatPreciseTime(currentTime)}`;
progressDisplay.textContent = `${progress}%`;
currentTimeDisplay.textContent = formatTime(currentTime);
progressBar.style.width = `${progress}%`;
};
// Update submit button state
let isPlaybackConfirmed = false;
const updateSubmitButton = () => {
if (liftPointTime !== null) {
if (liftPointTime !== null && isPlaybackConfirmed) {
submitButton.disabled = false;
submitButton.style.opacity = '1';
submitButton.style.cursor = 'pointer';
@@ -269,6 +344,58 @@ import html from '../utils/html.js';
}
};
// Function to play back from marked point for confirmation
const playbackFromMarkedPoint = () => {
if (liftPointTime === null) return;
// Store current position to restore later
const originalPosition = audio.currentTime;
// Show confirmation message
markedPointDisplay.innerHTML = `
<div style="text-align: center; padding: 10px; background: #fff3cd; border: 1px solid #ffd93d; border-radius: 5px; margin: 10px 0;">
<p style="margin: 5px 0; color: #856404;">🔊 Playing back from marked point...</p>
<p style="margin: 5px 0; font-size: 14px; color: #856404;">If this sounds right, press Submit. Otherwise, choose a new mark point.</p>
</div>
`;
// Set audio to marked point and play
audio.currentTime = liftPointTime;
updateDisplay();
// Play for a few seconds (3 seconds or until end)
const playbackDuration = 3;
const endTime = Math.min(liftPointTime + playbackDuration, audioDuration);
audio.play();
// Set up event to stop playback after duration and restore position
const onTimeUpdate = () => {
if (audio.currentTime >= endTime) {
audio.pause();
audio.removeEventListener('timeupdate', onTimeUpdate);
// Restore original position
audio.currentTime = originalPosition;
updateDisplay();
// Update display to show confirmation
markedPointDisplay.innerHTML = `
<div style="text-align: center; padding: 10px; background: #d1edff; border: 1px solid #3498db; border-radius: 5px; margin: 10px 0;">
<p style="margin: 5px 0; color: #2c3e50;">✓ Lift point marked at: ${formatPreciseTime(liftPointTime)}</p>
<p style="margin: 5px 0; font-size: 14px; color: #2c3e50;">Preview completed. Press Submit to confirm, <span class="key-icon">ENTER</span> to play again, or mark a new point.</p>
</div>
`;
// Enable submit button
isPlaybackConfirmed = true;
updateSubmitButton();
}
};
audio.addEventListener('timeupdate', onTimeUpdate);
};
// Check if we have a stored duration from the recording
const storedDuration = recordingData.audio_duration;
@@ -282,7 +409,8 @@ import html from '../utils/html.js';
// Use stored duration if available and valid
if (storedDuration && isFinite(storedDuration) && storedDuration > 0) {
audioDuration = storedDuration;
durationDisplay.textContent = formatTime(audioDuration);
startTimeDisplay.textContent = '0:00';
endTimeDisplay.textContent = formatTime(audioDuration);
updateDisplay();
updateSubmitButton();
console.log('Using stored duration:', audioDuration);
@@ -292,7 +420,8 @@ import html from '../utils/html.js';
// Otherwise try to get duration from audio element
if (audio.duration && isFinite(audio.duration) && audio.duration > 0) {
audioDuration = audio.duration;
durationDisplay.textContent = formatTime(audioDuration);
startTimeDisplay.textContent = '0:00';
endTimeDisplay.textContent = formatTime(audioDuration);
updateDisplay();
updateSubmitButton();
console.log('Audio controls setup complete, duration:', audioDuration);
@@ -305,7 +434,8 @@ import html from '../utils/html.js';
// Fallback: estimate duration based on blob size (rough approximation)
const estimatedDuration = Math.max(1, audioBlob.size / 8000); // ~8KB per second rough estimate
audioDuration = estimatedDuration;
durationDisplay.textContent = formatTime(audioDuration) + ' (est)';
startTimeDisplay.textContent = '0:00';
endTimeDisplay.textContent = formatTime(audioDuration);
updateDisplay();
updateSubmitButton();
console.log('Using estimated duration:', audioDuration);
@@ -336,7 +466,8 @@ import html from '../utils/html.js';
// Force load the audio
audio.load();
// Speed control buttons
// Speed control buttons (commented out - using fixed 0.5x speed)
/*
const speedButtons = document.querySelectorAll('.speed-btn');
speedButtons.forEach(btn => {
btn.addEventListener('click', () => {
@@ -354,16 +485,17 @@ import html from '../utils/html.js';
console.log('Playback speed set to:', speed);
});
});
*/
// Skip buttons (5% of total duration)
// Skip buttons (0.1 second increments)
skipBackBtn.addEventListener('click', () => {
const skipAmount = audioDuration * 0.05; // 5% of duration
const skipAmount = 0.1; // 0.1 seconds
audio.currentTime = Math.max(0, audio.currentTime - skipAmount);
updateDisplay();
});
skipForwardBtn.addEventListener('click', () => {
const skipAmount = audioDuration * 0.05; // 5% of duration
const skipAmount = 0.1; // 0.1 seconds
audio.currentTime = Math.min(audioDuration, audio.currentTime + skipAmount);
updateDisplay();
});
@@ -380,14 +512,14 @@ import html from '../utils/html.js';
// Audio play event
audio.addEventListener('play', () => {
isPlaying = true;
playPauseBtn.textContent = '⏸ Pause';
playPauseBtn.innerHTML = '⏸ Pause <span class="key-icon space">SPACE</span>';
playPauseBtn.style.backgroundColor = '#f44336';
});
// Audio pause event
audio.addEventListener('pause', () => {
isPlaying = false;
playPauseBtn.textContent = '▶ Play';
playPauseBtn.innerHTML = '▶ Play <span class="key-icon space">SPACE</span>';
playPauseBtn.style.backgroundColor = '#4caf50';
});
@@ -414,16 +546,16 @@ import html from '../utils/html.js';
case 'ArrowLeft':
e.preventDefault();
// Skip back with left arrow
const skipBackAmount = audioDuration * 0.05;
// Skip back with left arrow (0.1 seconds)
const skipBackAmount = 0.1;
audio.currentTime = Math.max(0, audio.currentTime - skipBackAmount);
updateDisplay();
break;
case 'ArrowRight':
e.preventDefault();
// Skip forward with right arrow
const skipForwardAmount = audioDuration * 0.05;
// Skip forward with right arrow (0.1 seconds)
const skipForwardAmount = 0.1;
audio.currentTime = Math.min(audioDuration, audio.currentTime + skipForwardAmount);
updateDisplay();
break;
@@ -432,8 +564,9 @@ import html from '../utils/html.js';
e.preventDefault();
// Mark lift point with Enter
liftPointTime = audio.currentTime;
markedPointDisplay.textContent = `✓ Lift point marked at: ${formatPreciseTime(liftPointTime)}`;
isPlaybackConfirmed = false;
updateSubmitButton();
playbackFromMarkedPoint();
break;
}
});
@@ -441,8 +574,9 @@ import html from '../utils/html.js';
// Mark lift point button
markLiftPointBtn.addEventListener('click', () => {
liftPointTime = audio.currentTime;
markedPointDisplay.textContent = `✓ Lift point marked at: ${formatPreciseTime(liftPointTime)}`;
isPlaybackConfirmed = false;
updateSubmitButton();
playbackFromMarkedPoint();
});
// Submit button

View File

@@ -90,63 +90,6 @@ import { ParameterType } from 'jspsych';
margin: 5px;
">Accept Recording</button>
</div>
<div id="questions-section" style="display: none; margin: 20px; text-align: left; max-width: 600px; margin-left: auto; margin-right: auto;">
<audio id="final-playback-audio" controls style="display: block; margin: 10px auto;"></audio>
<h3 style="text-align: center; margin-bottom: 20px;">Please answer the following questions:</h3>
<div style="margin-bottom: 15px;">
<label for="spelling-input" style="display: block; margin-bottom: 5px; font-weight: bold;">How would you spell what you have just recorded?</label>
<input type="text" id="spelling-input" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
</div>
<div style="margin-bottom: 15px;">
<label for="language-select" style="display: block; margin-bottom: 5px; font-weight: bold;">What language is it in?</label>
<select id="language-select" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" required>
<option value="">Select a language...</option>
<option value="English">English</option>
<option value="Spanish">Spanish</option>
<option value="French">French</option>
<option value="German">German</option>
<option value="Italian">Italian</option>
<option value="Portuguese">Portuguese</option>
<option value="Russian">Russian</option>
<option value="Chinese">Chinese</option>
<option value="Japanese">Japanese</option>
<option value="Korean">Korean</option>
<option value="Arabic">Arabic</option>
<option value="Hindi">Hindi</option>
<option value="Other">Other</option>
</select>
</div>
<div id="other-language-section" style="margin-bottom: 15px; display: none;">
<label for="other-language-input" style="display: block; margin-bottom: 5px; font-weight: bold;">Please specify the language:</label>
<input type="text" id="other-language-input" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div id="translation-section" style="margin-bottom: 15px; display: none;">
<label for="translation-input" style="display: block; margin-bottom: 5px; font-weight: bold;">How would you translate it to English?</label>
<input type="text" id="translation-input" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
</div>
<div style="margin-bottom: 15px;">
<label for="meaning-input" style="display: block; margin-bottom: 5px; font-weight: bold;">Does it have a meaning? If so, write it here in English:</label>
<input type="text" id="meaning-input" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" placeholder="Write the meaning or 'No meaning' if it doesn't have one">
</div>
<button id="submit-answers-button" style="
padding: 12px 24px;
font-size: 16px;
background-color: #007cba;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
display: block;
margin: 20px auto;
opacity: 0.5;
" disabled>Submit Answers</button>
</div>
</div>
`;
@@ -295,84 +238,10 @@ import { ParameterType } from 'jspsych';
});
acceptButton.addEventListener("click", () => {
document.getElementById("recording-controls").style.display = "none";
document.getElementById("questions-section").style.display = "block";
const finalAudio = document.getElementById("final-playback-audio");
const originalAudio = document.getElementById("playback-audio");
finalAudio.src = originalAudio.src;
document.getElementById("recording-status").textContent = "Please answer all questions to continue:";
this.setupQuestionEvents(trial);
this.endTrial(trial);
});
}
setupQuestionEvents(trial) {
const spellingInput = document.getElementById("spelling-input");
const languageSelect = document.getElementById("language-select");
const otherLanguageSection = document.getElementById("other-language-section");
const otherLanguageInput = document.getElementById("other-language-input");
const translationSection = document.getElementById("translation-section");
const translationInput = document.getElementById("translation-input");
const meaningInput = document.getElementById("meaning-input");
const submitButton = document.getElementById("submit-answers-button");
const validateForm = () => {
const spelling = spellingInput.value.trim();
const language = languageSelect.value;
const otherLanguage = otherLanguageInput.value.trim();
const meaning = meaningInput.value.trim();
const needsOtherLanguage = language === "Other";
const needsTranslation = language && language !== "English";
const translation = translationInput.value.trim();
const isValid = spelling && language && meaning &&
(!needsOtherLanguage || otherLanguage) &&
(!needsTranslation || translation);
submitButton.disabled = !isValid;
submitButton.style.opacity = isValid ? "1" : "0.5";
submitButton.style.cursor = isValid ? "pointer" : "not-allowed";
};
languageSelect.addEventListener("change", () => {
const selectedLanguage = languageSelect.value;
const isEnglish = selectedLanguage === "English";
const isOther = selectedLanguage === "Other";
translationSection.style.display = isEnglish ? "none" : "block";
otherLanguageSection.style.display = isOther ? "block" : "none";
if (isEnglish) {
translationInput.value = "";
}
if (!isOther) {
otherLanguageInput.value = "";
}
validateForm();
});
[spellingInput, languageSelect, otherLanguageInput, translationInput, meaningInput].forEach(element => {
element.addEventListener("input", validateForm);
element.addEventListener("change", validateForm);
});
submitButton.addEventListener("click", () => {
if (!submitButton.disabled) {
const finalLanguage = languageSelect.value === "Other" ?
otherLanguageInput.value.trim() :
languageSelect.value;
this.endTrialWithAnswers(trial, {
spelling: spellingInput.value.trim(),
language: finalLanguage,
translation: translationInput.value.trim() || null,
meaning: meaningInput.value.trim()
});
}
});
}
startRecording() {
try {
@@ -396,7 +265,7 @@ import { ParameterType } from 'jspsych';
}
}
endTrialWithAnswers(trial, answers) {
endTrial(trial) {
this.recorder.removeEventListener("dataavailable", this.data_available_handler);
this.recorder.removeEventListener("start", this.start_event_handler);
this.recorder.removeEventListener("stop", this.stop_event_handler);
@@ -412,10 +281,6 @@ import { ParameterType } from 'jspsych';
stimulus: trial.stimulus,
response: response,
estimated_stimulus_onset: this.recorder_start_time ? Math.round(this.stimulus_start_time - this.recorder_start_time) : null,
spelling: answers.spelling,
language: answers.language,
translation: answers.translation,
meaning: answers.meaning,
audio_duration: this.audio_duration
};
@@ -430,47 +295,11 @@ import { ParameterType } from 'jspsych';
stimulus: trial.stimulus,
response: null,
estimated_stimulus_onset: null,
spelling: answers.spelling,
language: answers.language,
translation: answers.translation,
meaning: answers.meaning,
audio_duration: this.audio_duration
});
}
}
endTrial(display_element, trial) {
this.recorder.removeEventListener("dataavailable", this.data_available_handler);
this.recorder.removeEventListener("start", this.start_event_handler);
this.recorder.removeEventListener("stop", this.stop_event_handler);
this.jsPsych.pluginAPI.clearAllTimeouts();
const reader = new FileReader();
reader.addEventListener("load", () => {
const response = reader.result.split(",")[1];
let trial_data = {
rt: this.stimulus_start_time ? Date.now() - this.stimulus_start_time : null,
stimulus: trial.stimulus,
response: response,
estimated_stimulus_onset: this.recorder_start_time ? Math.round(this.stimulus_start_time - this.recorder_start_time) : null,
};
this.jsPsych.finishTrial(trial_data);
});
if (this.current_recording) {
reader.readAsDataURL(this.current_recording);
} else {
this.jsPsych.finishTrial({
rt: null,
stimulus: trial.stimulus,
response: null,
estimated_stimulus_onset: null,
});
}
}
}
export default jsPsychRecordCall;

View File

@@ -12,7 +12,7 @@ export const textStimuli = {
<p class = "mt-2">Lifting calls are phrases, words or sounds that people say or make when they want to lift a heavy object together with someone else. To build this online collection of lifting calls, we will ask you to record one or more lifting calls that you know. We will also ask you to provide us with a written version of the recorded calls and information about the region and language that the calls are used in.</p>
<p class = "mt-2"> We process this data in accordance with the Austrian Forschungsorganisationsgesetz FOG and Consent under Art 6 (1) (a) GDPR, public interest (e) and (f) legitimate interest. You have the right to withdraw your consent at any time. To do so and to request the deletion of your data, please contact CEUs data protection officer at privacy@ceu.edu. </p>
<p class = "mt-2"> More information about your rights can be found at the controllers website https://www.ceu.edu/privacy. If you have any questions regarding data protection, please contact CEU's data protection officer at privacy@ceu.edu.
In case you are accessing this online form through via Prolific, Prolific acts as a data processor and has access to personal data. You can download Prolifics full privacy notices here: https://prolific.notion.site/Privacy-and-Legal-at-Prolific-395a0b3414cd4d84a2557566256e3d58
In case you are accessing this online form through Prolific, Prolific acts as a data processor and has access to personal data. You can download Prolifics full privacy notices here: https://prolific.notion.site/Privacy-and-Legal-at-Prolific-395a0b3414cd4d84a2557566256e3d58
<p class = "mt-2"> By checking the “I agree” box, you agree to participate in this study. You also confirm you are 18 years or older. To agree: Check the “I agree” box below and then click next to participate in the study. If you do not wish to participate in this study, simply close out of this browser window.</p>
</div>
<div class="mx-auto my-6">