Compare commits

...

11 Commits

Author SHA1 Message Date
0b966e95b5 removed text 2025-07-25 17:24:02 +02:00
1df612aba7 chnaged lift point 2025-07-25 17:21:23 +02:00
936514217a removed skip 2025-07-25 17:20:33 +02:00
776f71dac0 optional waveform 2025-07-25 15:16:46 +02:00
3eec087640 fixed duration issue 2025-07-25 14:43:49 +02:00
229d725bd1 fixed waveform 2025-07-24 22:27:29 +02:00
367540a380 fixed text fo Thomas 2025-07-22 17:03:06 +02:00
dfbc26ca3d added waveform 2025-07-22 00:08:30 +02:00
beaed99ba7 added new plugin 2025-07-21 20:12:48 +02:00
e69c000eb8 changed complete text 2025-07-21 20:11:58 +02:00
cef99c1bb0 changed browser check text 2025-07-21 20:08:39 +02:00
4 changed files with 649 additions and 55 deletions

View File

@@ -41,7 +41,7 @@ const browser_check = {
return ['chrome', 'edge-chromium'].includes(data.browser);
},
exclusion_message: data => {
return `<p>You must use Google Chrome or Microsoft Edge to complete this experiment.</p>`;
return `<p>You must use Google Chrome or Microsoft Edge on desktop/laptop to participate in this study.</p>`;
},
};

View File

@@ -73,6 +73,10 @@ import html from '../utils/html.js';
return;
}
// Check if waveform should be shown
const showWaveform = import.meta.env.VITE_SHOW_WAVEFORM === 'true';
const allowSkip = import.meta.env.VITE_ALLOW_SKIP_BUTTONS === 'true';
display_element.innerHTML = `
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@@ -117,26 +121,17 @@ import html from '../utils/html.js';
<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.
Press <span class="key-icon space">SPACE</span> to play the audio and <span class="key-icon space">SPACE</span> again to pause at the lifting point.
</li>
<li>
You can use the keyboard or the buttons below to mark lift points.
You may adjust the lifting point by clicking and dragging the red line on the playback indicator.
</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>
Once you have paused at the lifting point, press <span class="key-icon">ENTER</span> to confirm your selection before submitting.
</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;">
@@ -192,6 +187,7 @@ import html from '../utils/html.js';
-->
</div>
${allowSkip ? `
<div style="margin: 20px 0; display: flex; gap: 10px; align-items: center; justify-content: center; flex-wrap: wrap;">
<button id="skip-back-btn" style="
padding: 8px 16px;
@@ -213,6 +209,7 @@ import html from '../utils/html.js';
cursor: pointer;
">Skip Forward 0.1s <span class="key-icon"><i class="fas fa-arrow-right"></i></span> ⏩</button>
</div>
` : ''}
<!--
<div id="current-time-display" style="
@@ -231,13 +228,65 @@ import html from '../utils/html.js';
-->
<div style="margin: 20px 0;">
<!-- Progress Bar -->
${showWaveform ? `
<!-- Waveform Display -->
<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="position: relative; width: 100%; height: 120px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;">
<canvas id="waveform-canvas" style="width: 100%; height: 100%; display: block;"></canvas>
<div id="playback-indicator" style="
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
background-color: #ff4444;
z-index: 10;
cursor: grab;
"></div>
</div>
</div>
` : `
<!-- Simple time display with invisible scrubbing area when waveform is disabled -->
<div style="margin-bottom: 20px;">
<div style="display: flex; justify-content: center; font-size: 14px; color: #666; margin-bottom: 10px;">
<span>Current time: <span id="current-time">0:00</span> / <span id="end-time">0:00</span></span>
</div>
<!-- Invisible scrubbing area -->
<div id="scrub-area" style="
position: relative;
width: 100%;
height: 40px;
background: transparent;
border: 2px dashed #ccc;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 12px;
">
Click and drag the red line to adjust the lifting point.
<div id="scrub-indicator" style="
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
background-color: #ff4444;
z-index: 10;
"></div>
</div>
</div>
`}
<!-- Progress Bar (kept as fallback) -->
<div style="margin-bottom: 20px; display: ${showWaveform ? 'none' : 'block'};" id="fallback-progress">
<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>
@@ -252,7 +301,7 @@ import html from '../utils/html.js';
border: none;
border-radius: 5px;
cursor: pointer;
">Mark This as Lift Point <span class="key-icon">ENTER</span></button>
">Confirm lifting point <span class="key-icon">ENTER</span></button>
</div>
</div>
@@ -276,7 +325,7 @@ import html from '../utils/html.js';
border-radius: 5px;
cursor: pointer;
margin-top: 20px;
">Submit Lift Point</button>
">Submit lifting point</button>
</div>
`;
@@ -286,10 +335,10 @@ import html from '../utils/html.js';
console.log('Audio source set to blob URL:', audioData);
console.log('Audio blob size:', audioBlob.size, 'bytes');
this.setupMarkingEvents(recordingData, audioBlob);
this.setupMarkingEvents(recordingData, audioBlob, showWaveform);
}
setupMarkingEvents(recordingData, audioBlob) {
setupMarkingEvents(recordingData, audioBlob, showWaveform = true) {
const audio = document.getElementById('playback-audio');
const playPauseBtn = document.getElementById('play-pause-btn');
// Progress bar elements
@@ -303,9 +352,23 @@ import html from '../utils/html.js';
const markedPointDisplay = document.getElementById('marked-point-display');
const submitButton = document.getElementById('submit-lift-point-btn');
// Waveform elements
const waveformCanvas = document.getElementById('waveform-canvas');
const playbackIndicator = document.getElementById('playback-indicator');
const ctx = waveformCanvas ? waveformCanvas.getContext('2d') : null;
// Scrubbing area elements (for when waveform is disabled)
const scrubArea = document.getElementById('scrub-area');
const scrubIndicator = document.getElementById('scrub-indicator');
let audioDuration = 0;
let liftPointTime = null;
let isPlaying = false;
let waveformData = null;
let canvasWidth = 0;
let canvasHeight = 0;
let isDragging = false;
let wasPlayingBeforeDrag = false;
// Format time as MM:SS
const formatTime = (seconds) => {
@@ -321,13 +384,140 @@ import html from '../utils/html.js';
return `${minutes}:${secs.padStart(5, '0')}s`;
};
// Setup canvas dimensions
const setupCanvas = () => {
if (!waveformCanvas || !ctx) {
console.error('Canvas or context not available');
return false;
}
const rect = waveformCanvas.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
console.warn('Canvas has zero dimensions, retrying...');
return false;
}
const dpr = window.devicePixelRatio || 1;
canvasWidth = rect.width * dpr;
canvasHeight = rect.height * dpr;
waveformCanvas.width = canvasWidth;
waveformCanvas.height = canvasHeight;
ctx.scale(dpr, dpr);
return true;
};
// Generate waveform data from audio
const generateWaveform = async () => {
try {
// Ensure canvas is properly set up
if (!setupCanvas()) {
console.error('Failed to setup canvas for waveform');
throw new Error('Canvas setup failed');
}
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const arrayBuffer = await audioBlob.arrayBuffer();
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
const rawData = audioBuffer.getChannelData(0);
const samples = Math.floor(canvasWidth / 2); // Sample every 2 pixels
// Guard against invalid sample count
if (samples <= 0) {
throw new Error('Invalid canvas width for waveform generation');
}
const blockSize = Math.floor(rawData.length / samples);
const filteredData = [];
for (let i = 0; i < samples; i++) {
let blockStart = blockSize * i;
let sum = 0;
for (let j = 0; j < blockSize; j++) {
sum += Math.abs(rawData[blockStart + j]);
}
filteredData.push(sum / blockSize);
}
waveformData = filteredData;
drawWaveform();
} catch (error) {
console.error('Error generating waveform:', error);
// Fallback to progress bar if waveform fails
document.getElementById('fallback-progress').style.display = 'block';
waveformCanvas.parentElement.style.display = 'none';
}
};
// Draw waveform on canvas
const drawWaveform = () => {
if (!waveformData || !ctx) return;
// Use the container's logical dimensions for consistent rendering
const rect = waveformCanvas.getBoundingClientRect();
const logicalWidth = rect.width;
const logicalHeight = rect.height;
// Clear the entire canvas using its actual dimensions
ctx.clearRect(0, 0, logicalWidth, logicalHeight);
ctx.fillStyle = '#e0e0e0';
ctx.fillRect(0, 0, logicalWidth, logicalHeight);
// Guard against invalid dimensions
if (logicalWidth <= 0 || logicalHeight <= 0 || waveformData.length === 0) return;
const barWidth = logicalWidth / waveformData.length;
const maxAmplitude = Math.max(...waveformData);
// Guard against invalid amplitude data
if (maxAmplitude <= 0) return;
ctx.fillStyle = '#007cba';
for (let i = 0; i < waveformData.length; i++) {
const barHeight = (waveformData[i] / maxAmplitude) * logicalHeight * 0.8;
const x = i * barWidth;
const y = (logicalHeight - barHeight) / 2;
ctx.fillRect(x, y, Math.max(1, barWidth - 1), barHeight);
}
};
// Seek to specific time
const seekTo = (seconds) => {
if (audioDuration > 0) {
audio.currentTime = Math.max(0, Math.min(seconds, audioDuration));
updateDisplay();
}
};
// Update display function
const updateDisplay = () => {
const currentTime = audio.currentTime;
const progress = audioDuration > 0 ? (currentTime / audioDuration * 100) : 0;
currentTimeDisplay.textContent = formatTime(currentTime);
progressBar.style.width = `${progress}%`;
if (currentTimeDisplay) {
currentTimeDisplay.textContent = formatTime(currentTime);
}
// Update progress bar (fallback)
if (progressBar) {
progressBar.style.width = `${progress}%`;
}
// Update waveform playback indicator (only if not dragging)
if (showWaveform && playbackIndicator && waveformCanvas && !isDragging && audioDuration > 0) {
const rect = waveformCanvas.getBoundingClientRect();
const indicatorPosition = (progress / 100) * rect.width;
playbackIndicator.style.left = `${Math.max(0, Math.min(indicatorPosition, rect.width - 2))}px`;
}
// Update scrub area indicator when waveform is disabled
if (!showWaveform && scrubIndicator && scrubArea && !isDragging && audioDuration > 0) {
const rect = scrubArea.getBoundingClientRect();
const indicatorPosition = (progress / 100) * rect.width;
scrubIndicator.style.left = `${Math.max(0, Math.min(indicatorPosition, rect.width - 2))}px`;
}
};
// Update submit button state
@@ -382,8 +572,8 @@ import html from '../utils/html.js';
// 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>
<p style="margin: 5px 0; color: #2c3e50;">✓ Lifting point confirmed at: ${formatPreciseTime(liftPointTime)}</p>
<p style="margin: 5px 0; font-size: 14px; color: #2c3e50;">Preview completed. Press Submit to continue, <span class="key-icon">ENTER</span> to play again, or mark a new point.</p>
</div>
`;
@@ -405,25 +595,34 @@ import html from '../utils/html.js';
const setupAudioControls = () => {
console.log(`Setup attempt ${retryCount + 1}, duration:`, audio.duration, 'ready state:', audio.readyState, 'stored duration:', storedDuration);
console.log('Audio duration isFinite:', isFinite(audio.duration), 'Audio duration !== Infinity:', audio.duration !== Infinity);
// Use stored duration if available and valid
if (storedDuration && isFinite(storedDuration) && storedDuration > 0) {
audioDuration = storedDuration;
startTimeDisplay.textContent = '0:00';
endTimeDisplay.textContent = formatTime(audioDuration);
if (startTimeDisplay) startTimeDisplay.textContent = '0:00';
if (endTimeDisplay) endTimeDisplay.textContent = formatTime(audioDuration);
updateDisplay();
updateSubmitButton();
// Initialize waveform only if enabled
if (showWaveform) {
generateWaveform();
}
console.log('Using stored duration:', audioDuration);
return;
}
// Otherwise try to get duration from audio element
if (audio.duration && isFinite(audio.duration) && audio.duration > 0) {
// Otherwise try to get duration from audio element (reject Infinity)
if (audio.duration && isFinite(audio.duration) && audio.duration > 0 && audio.duration !== Infinity) {
audioDuration = audio.duration;
startTimeDisplay.textContent = '0:00';
endTimeDisplay.textContent = formatTime(audioDuration);
if (startTimeDisplay) startTimeDisplay.textContent = '0:00';
if (endTimeDisplay) endTimeDisplay.textContent = formatTime(audioDuration);
updateDisplay();
updateSubmitButton();
// Initialize waveform only if enabled
if (showWaveform) {
generateWaveform();
}
console.log('Audio controls setup complete, duration:', audioDuration);
} else if (retryCount < maxRetries) {
// Retry after a short delay if duration is not available
@@ -434,10 +633,14 @@ 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;
startTimeDisplay.textContent = '0:00';
endTimeDisplay.textContent = formatTime(audioDuration);
if (startTimeDisplay) startTimeDisplay.textContent = '0:00';
if (endTimeDisplay) endTimeDisplay.textContent = formatTime(audioDuration);
updateDisplay();
updateSubmitButton();
// Initialize waveform even with estimated duration (only if enabled)
if (showWaveform) {
generateWaveform();
}
console.log('Using estimated duration:', audioDuration);
}
};
@@ -466,6 +669,114 @@ import html from '../utils/html.js';
// Force load the audio
audio.load();
// Handle window resize to redraw waveform (only if waveform is enabled)
if (showWaveform) {
window.addEventListener('resize', () => {
if (waveformData) {
if (setupCanvas()) {
drawWaveform();
}
}
});
}
// Mouse event handlers for dragging the playback indicator
const handleMouseDown = (e) => {
isDragging = true;
wasPlayingBeforeDrag = isPlaying;
if (isPlaying) {
audio.pause();
}
if (showWaveform && playbackIndicator) {
playbackIndicator.style.cursor = 'grabbing';
}
e.preventDefault();
handleMouseMove(e); // Immediately update position
};
const handleMouseMove = (e) => {
if (!isDragging || audioDuration === 0) return;
let rect, indicator;
if (showWaveform && waveformCanvas) {
rect = waveformCanvas.getBoundingClientRect();
indicator = playbackIndicator;
} else if (!showWaveform && scrubArea) {
rect = scrubArea.getBoundingClientRect();
indicator = scrubIndicator;
} else {
return;
}
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width - 2));
const progress = x / rect.width;
const newTime = progress * audioDuration;
// Update indicator position immediately
if (indicator) {
indicator.style.left = `${x}px`;
}
// Update time display
if (currentTimeDisplay) {
currentTimeDisplay.textContent = formatTime(newTime);
}
// Update audio position
seekTo(newTime);
};
const handleMouseUp = () => {
if (!isDragging) return;
isDragging = false;
if (showWaveform && playbackIndicator) {
playbackIndicator.style.cursor = 'grab';
}
// Resume playback if it was playing before drag
if (wasPlayingBeforeDrag) {
audio.play();
}
};
// Add event listeners for dragging
if (showWaveform && playbackIndicator) {
playbackIndicator.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
} else if (!showWaveform && scrubIndicator) {
scrubIndicator.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
}
// Also allow clicking anywhere to seek
if (showWaveform && waveformCanvas) {
waveformCanvas.addEventListener('click', (e) => {
if (isDragging) return; // Don't handle click if we're dragging
const rect = waveformCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width - 2));
const progress = x / rect.width;
const newTime = progress * audioDuration;
seekTo(newTime);
});
} else if (!showWaveform && scrubArea) {
scrubArea.addEventListener('click', (e) => {
if (isDragging) return; // Don't handle click if we're dragging
const rect = scrubArea.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width - 2));
const progress = x / rect.width;
const newTime = progress * audioDuration;
seekTo(newTime);
});
}
// Speed control buttons (commented out - using fixed 0.5x speed)
/*
const speedButtons = document.querySelectorAll('.speed-btn');
@@ -487,18 +798,22 @@ import html from '../utils/html.js';
});
*/
// Skip buttons (0.1 second increments)
skipBackBtn.addEventListener('click', () => {
const skipAmount = 0.1; // 0.1 seconds
audio.currentTime = Math.max(0, audio.currentTime - skipAmount);
updateDisplay();
});
// Skip buttons (0.1 second increments) - only if waveform is enabled
if (showWaveform && skipBackBtn) {
skipBackBtn.addEventListener('click', () => {
const skipAmount = 0.1; // 0.1 seconds
audio.currentTime = Math.max(0, audio.currentTime - skipAmount);
updateDisplay();
});
}
skipForwardBtn.addEventListener('click', () => {
const skipAmount = 0.1; // 0.1 seconds
audio.currentTime = Math.min(audioDuration, audio.currentTime + skipAmount);
updateDisplay();
});
if (showWaveform && skipForwardBtn) {
skipForwardBtn.addEventListener('click', () => {
const skipAmount = 0.1; // 0.1 seconds
audio.currentTime = Math.min(audioDuration, audio.currentTime + skipAmount);
updateDisplay();
});
}
// Play/Pause button
playPauseBtn.addEventListener('click', () => {
@@ -545,19 +860,23 @@ import html from '../utils/html.js';
break;
case 'ArrowLeft':
e.preventDefault();
// Skip back with left arrow (0.1 seconds)
const skipBackAmount = 0.1;
audio.currentTime = Math.max(0, audio.currentTime - skipBackAmount);
updateDisplay();
if (showWaveform) {
e.preventDefault();
// 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 (0.1 seconds)
const skipForwardAmount = 0.1;
audio.currentTime = Math.min(audioDuration, audio.currentTime + skipForwardAmount);
updateDisplay();
if (showWaveform) {
e.preventDefault();
// Skip forward with right arrow (0.1 seconds)
const skipForwardAmount = 0.1;
audio.currentTime = Math.min(audioDuration, audio.currentTime + skipForwardAmount);
updateDisplay();
}
break;
case 'Enter':

View File

@@ -1,7 +1,7 @@
import html from '../utils/html.js';
export const textStimuli = {
complete: html`Experiment complete. Please paste the following link into your browser to confirm completion on Prolific:
complete: html`Study complete. Please paste the following link into your browser to confirm completion on Prolific:
<span class="text-blue-500">
${import.meta.env.VITE_COMPLETE_URL}
</a>

275
scripts/transcribe-call.js Normal file
View File

@@ -0,0 +1,275 @@
import { ParameterType } from 'jspsych';
import html from '../utils/html.js';
const info = {
name: "transcribe-call",
parameters: {
},
};
class jsPsychTranscribeCall {
constructor(jsPsych) {
this.jsPsych = jsPsych;
}
static {
this.info = info;
}
trial(display_element, trial) {
// Get the original recording data from the record-call trial
// Look through recent trials to find one with recording response
const allData = this.jsPsych.data.get();
const recentTrials = allData.trials.slice(-10); // Look at last 10 trials
let recordingData = null;
for (let i = recentTrials.length - 1; i >= 0; i--) {
const trial = recentTrials[i];
if (trial.response && typeof trial.response === 'string' && trial.response.length > 100) {
// Found a trial with audio recording data
recordingData = trial;
break;
}
}
if (!recordingData || !recordingData.response) {
display_element.innerHTML = `
<div style="text-align: center; padding: 20px;">
<p style="color: red;">No recording found from the previous trial.</p>
<button onclick="this.jsPsych.finishTrial()" style="
padding: 10px 20px;
font-size: 14px;
background-color: #007cba;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
">Continue</button>
</div>
`;
return;
}
// Convert base64 back to audio blob for playback
let audioData;
let audioBlob;
try {
const byteCharacters = atob(recordingData.response);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
audioBlob = new Blob([byteArray], { type: 'audio/ogg' });
audioData = URL.createObjectURL(audioBlob);
} catch (error) {
console.error('Error creating audio blob:', error);
display_element.innerHTML = `
<div style="text-align: center; padding: 20px;">
<p style="color: red;">Error loading audio data.</p>
<button onclick="this.jsPsych.finishTrial({})" style="
padding: 10px 20px;
font-size: 14px;
background-color: #007cba;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
">Continue</button>
</div>
`;
return;
}
display_element.innerHTML = `
<div style="text-align: center; padding: 20px;">
<h2 style="margin-bottom: 20px;">Recording Transcription</h2>
<div style="margin: 20px 0;">
<audio id="transcription-playback-audio" controls style="display: block; margin: 10px auto;"></audio>
</div>
<div style="margin: 20px; text-align: left; max-width: 600px; margin-left: auto; margin-right: auto;">
<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 0 auto;
">Submit Answers</button>
</div>
</div>
`;
// Set the audio source
const audio = document.getElementById('transcription-playback-audio');
audio.src = audioData;
this.setupTranscriptionEvents(recordingData);
}
setupTranscriptionEvents(recordingData) {
const audio = document.getElementById('transcription-playback-audio');
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');
// Language selection logic
languageSelect.addEventListener('change', () => {
const selectedLanguage = languageSelect.value;
if (selectedLanguage === 'Other') {
otherLanguageSection.style.display = 'block';
otherLanguageInput.required = true;
} else {
otherLanguageSection.style.display = 'none';
otherLanguageInput.required = false;
otherLanguageInput.value = '';
}
if (selectedLanguage && selectedLanguage !== 'English' && selectedLanguage !== '') {
translationSection.style.display = 'block';
translationInput.required = true;
} else {
translationSection.style.display = 'none';
translationInput.required = false;
translationInput.value = '';
}
});
// Form validation and submission
const validateForm = () => {
const spelling = spellingInput.value.trim();
const language = languageSelect.value;
const otherLanguage = otherLanguageInput.value.trim();
const translation = translationInput.value.trim();
const meaning = meaningInput.value.trim();
let isValid = true;
if (!spelling) {
spellingInput.style.borderColor = '#f44336';
isValid = false;
} else {
spellingInput.style.borderColor = '#ddd';
}
if (!language) {
languageSelect.style.borderColor = '#f44336';
isValid = false;
} else {
languageSelect.style.borderColor = '#ddd';
}
if (language === 'Other' && !otherLanguage) {
otherLanguageInput.style.borderColor = '#f44336';
isValid = false;
} else {
otherLanguageInput.style.borderColor = '#ddd';
}
if (language && language !== 'English' && language !== '' && !translation) {
translationInput.style.borderColor = '#f44336';
isValid = false;
} else {
translationInput.style.borderColor = '#ddd';
}
if (!meaning) {
meaningInput.style.borderColor = '#f44336';
isValid = false;
} else {
meaningInput.style.borderColor = '#ddd';
}
return isValid;
};
// Submit button event
submitButton.addEventListener('click', () => {
if (!validateForm()) {
alert('Please fill in all required fields.');
return;
}
const finalLanguage = languageSelect.value === 'Other' ? otherLanguageInput.value.trim() : languageSelect.value;
const trialData = {
spelling: spellingInput.value.trim(),
language: finalLanguage,
translation: translationInput.value.trim(),
meaning: meaningInput.value.trim(),
original_recording_data: {
response: recordingData.response,
rt: recordingData.rt,
stimulus: recordingData.stimulus,
audio_duration: recordingData.audio_duration
}
};
// Clean up object URL to prevent memory leaks
if (audio.src && audio.src.startsWith('blob:')) {
URL.revokeObjectURL(audio.src);
}
this.jsPsych.finishTrial(trialData);
});
// Auto-focus on first input
spellingInput.focus();
}
}
export default jsPsychTranscribeCall;