|
let mediaRecorder;
|
|
let audioChunks = [];
|
|
let audioBlob;
|
|
let sessionData = {};
|
|
|
|
|
|
const SESSION_STORAGE_KEY = 'whisperSessionData';
|
|
const CURRENT_ROW_KEY = 'whisperCurrentRow';
|
|
|
|
|
|
const NAVIGATION_BUTTONS = ['recordBtn', 'prevBtn', 'skipBtn'];
|
|
|
|
|
|
const MIN_FONT_SIZE = 17;
|
|
const MAX_FONT_SIZE = 33;
|
|
const FONT_SIZE_STEP = 2;
|
|
|
|
|
|
let pendingUploads = new Map();
|
|
|
|
|
|
const MAX_RECORDING_DURATION = 30000;
|
|
let recordingTimeout;
|
|
|
|
|
|
let isAuthenticated = false;
|
|
|
|
|
|
let audioPlayer = null;
|
|
|
|
|
|
let isSaving = false;
|
|
|
|
|
|
function getCSRFToken() {
|
|
return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
}
|
|
|
|
|
|
let audioContext = null;
|
|
let scriptProcessor = null;
|
|
let audioInput = null;
|
|
let rawPCMData = [];
|
|
let isRecordingPCM = false;
|
|
|
|
|
|
function getCsrfToken() {
|
|
const metaTag = document.querySelector('meta[name="csrf-token"]');
|
|
if (!metaTag) {
|
|
console.error("CSRF token meta tag not found");
|
|
return '';
|
|
}
|
|
const token = metaTag.getAttribute('content');
|
|
if (!token) {
|
|
console.error("CSRF token is empty");
|
|
return '';
|
|
}
|
|
return token;
|
|
}
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
const authEnabled = document.body.dataset.authEnabled === 'true';
|
|
|
|
|
|
isAuthenticated = !authEnabled || document.getElementById('authCheck')?.dataset.authenticated === 'true';
|
|
|
|
if (!isAuthenticated && authEnabled) {
|
|
|
|
document.getElementById('initialMessage').innerHTML = `
|
|
<div class="text-center empty-state">
|
|
<img src="/static/lock-icon.svg" alt="Lock" width="64" height="64">
|
|
<h5 class="mt-4" style="color: #202124;">Authentication Required</h5>
|
|
<p class="text-muted">
|
|
Please <a href="/login">sign in</a> to start recording.
|
|
</p>
|
|
</div>
|
|
`;
|
|
disableRecordingControls(true);
|
|
return;
|
|
}
|
|
|
|
|
|
const userDataElem = document.getElementById('userData');
|
|
let userData = null;
|
|
|
|
|
|
if (userDataElem) {
|
|
try {
|
|
userData = JSON.parse(userDataElem.textContent);
|
|
} catch (e) {
|
|
console.error('Error parsing user data:', e);
|
|
}
|
|
}
|
|
|
|
|
|
loadDomains(userData).then(() => {
|
|
loadUserProfile();
|
|
});
|
|
|
|
const savedSession = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
if (savedSession) {
|
|
const sessionData = JSON.parse(savedSession);
|
|
|
|
Object.keys(sessionData).forEach(key => {
|
|
const element = document.getElementById(key);
|
|
if (element) {
|
|
element.value = sessionData[key];
|
|
if (element.value) {
|
|
element.parentElement.classList.add('is-filled');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
const recordingInterface = document.getElementById('recordingInterface');
|
|
recordingInterface.classList.add('disabled-interface');
|
|
|
|
|
|
updateInterfaceState();
|
|
|
|
|
|
|
|
updateProgressDisplay(0, 0);
|
|
|
|
|
|
const savedFontSize = localStorage.getItem('transcriptFontSize');
|
|
if (savedFontSize) {
|
|
const transcript = document.getElementById('currentTranscript');
|
|
const sizeDisplay = document.getElementById('fontSizeDisplay');
|
|
transcript.style.setProperty('--transcript-font-size', `${savedFontSize}px`);
|
|
sizeDisplay.textContent = savedFontSize;
|
|
}
|
|
});
|
|
|
|
|
|
async function loadDomains(userData = null) {
|
|
try {
|
|
const domainSelect = document.getElementById('domain');
|
|
domainSelect.innerHTML = '<option value="">Loading available domains...</option>';
|
|
domainSelect.disabled = true;
|
|
|
|
const response = await fetch('/domains');
|
|
const data = await response.json();
|
|
|
|
domainSelect.innerHTML = '';
|
|
|
|
if (data.status === 'success' && data.domains) {
|
|
|
|
const domainCount = Object.keys(data.domains).length;
|
|
|
|
if (domainCount === 0) {
|
|
domainSelect.innerHTML = '<option value="" disabled>No domains available</option>';
|
|
domainSelect.disabled = true;
|
|
return;
|
|
}
|
|
|
|
|
|
const preferredDomain = userData?.domain || null;
|
|
let preferredDomainExists = false;
|
|
|
|
|
|
Object.entries(data.domains).forEach(([code, name]) => {
|
|
const option = document.createElement('option');
|
|
option.value = code;
|
|
option.textContent = `${name} (${code})`;
|
|
|
|
|
|
if (preferredDomain && code === preferredDomain) {
|
|
option.selected = true;
|
|
domainSelect.parentElement.classList.add('is-filled');
|
|
preferredDomainExists = true;
|
|
}
|
|
|
|
domainSelect.appendChild(option);
|
|
});
|
|
|
|
|
|
if (!preferredDomainExists && domainSelect.options.length > 0) {
|
|
domainSelect.options[0].selected = true;
|
|
domainSelect.parentElement.classList.add('is-filled');
|
|
}
|
|
|
|
|
|
domainSelect.disabled = false;
|
|
|
|
|
|
if (domainSelect.value) {
|
|
|
|
await loadSubdomains(domainSelect.value, userData?.subdomain || null);
|
|
}
|
|
} else {
|
|
domainSelect.innerHTML = '<option value="" disabled>No domains available</option>';
|
|
domainSelect.disabled = true;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading domains:', error);
|
|
const domainSelect = document.getElementById('domain');
|
|
domainSelect.innerHTML = '<option value="" disabled>Error loading domains</option>';
|
|
domainSelect.disabled = true;
|
|
}
|
|
}
|
|
|
|
|
|
async function loadSubdomains(domainCode, preferredSubdomain = null) {
|
|
try {
|
|
const subdomainSelect = document.getElementById('subdomain');
|
|
|
|
if (!domainCode) {
|
|
subdomainSelect.innerHTML = '<option value="">All Subdomains</option>';
|
|
subdomainSelect.disabled = true;
|
|
return;
|
|
}
|
|
|
|
subdomainSelect.innerHTML = '<option value="">Loading...</option>';
|
|
subdomainSelect.disabled = true;
|
|
|
|
const response = await fetch(`/domains/${domainCode}/subdomains`);
|
|
const data = await response.json();
|
|
|
|
subdomainSelect.innerHTML = '';
|
|
|
|
if (data.status === 'success' && data.subdomains) {
|
|
let preferredSubdomainExists = false;
|
|
|
|
data.subdomains.forEach(subdomain => {
|
|
const option = document.createElement('option');
|
|
option.value = subdomain.mnemonic;
|
|
option.textContent = `${subdomain.name} (${subdomain.mnemonic})`;
|
|
|
|
|
|
if (preferredSubdomain && subdomain.mnemonic === preferredSubdomain) {
|
|
option.selected = true;
|
|
subdomainSelect.parentElement.classList.add('is-filled');
|
|
preferredSubdomainExists = true;
|
|
}
|
|
|
|
subdomainSelect.appendChild(option);
|
|
});
|
|
|
|
|
|
if (!preferredSubdomainExists && subdomainSelect.options.length > 0) {
|
|
subdomainSelect.options[0].selected = true;
|
|
subdomainSelect.parentElement.classList.add('is-filled');
|
|
}
|
|
|
|
subdomainSelect.disabled = false;
|
|
} else {
|
|
|
|
subdomainSelect.innerHTML = '<option value="">No subdomains available</option>';
|
|
subdomainSelect.disabled = true;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading subdomains:', error);
|
|
const subdomainSelect = document.getElementById('subdomain');
|
|
subdomainSelect.innerHTML = '<option value="">Error loading subdomains</option>';
|
|
subdomainSelect.disabled = true;
|
|
}
|
|
}
|
|
|
|
|
|
document.getElementById('domain').addEventListener('change', function() {
|
|
|
|
const userDataElem = document.getElementById('userData');
|
|
let preferredSubdomain = null;
|
|
|
|
if (userDataElem) {
|
|
try {
|
|
const userData = JSON.parse(userDataElem.textContent);
|
|
|
|
if (userData.domain === this.value) {
|
|
preferredSubdomain = userData.subdomain;
|
|
}
|
|
} catch (e) {
|
|
console.error('Error parsing user data:', e);
|
|
}
|
|
}
|
|
|
|
|
|
loadSubdomains(this.value, preferredSubdomain);
|
|
});
|
|
|
|
|
|
function loadUserProfile() {
|
|
const userDataElem = document.getElementById('userData');
|
|
if (!userDataElem) return;
|
|
|
|
try {
|
|
const userData = JSON.parse(userDataElem.textContent);
|
|
|
|
|
|
const fields = ['gender', 'age_group', 'country', 'state', 'city', 'accent', 'language'];
|
|
fields.forEach(field => {
|
|
const elem = document.getElementById(field);
|
|
if (elem && userData[field]) {
|
|
elem.value = userData[field];
|
|
elem.parentElement.classList.add('is-filled');
|
|
|
|
|
|
if (field === 'country' && userData['state']) {
|
|
const event = new Event('change');
|
|
elem.dispatchEvent(event);
|
|
|
|
setTimeout(() => {
|
|
const stateElem = document.getElementById('state');
|
|
if (stateElem) {
|
|
stateElem.value = userData['state'];
|
|
stateElem.parentElement.classList.add('is-filled');
|
|
}
|
|
}, 100);
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
} catch (e) {
|
|
console.error('Error loading user profile:', e);
|
|
}
|
|
}
|
|
|
|
|
|
function updateButtonStates(state) {
|
|
const states = {
|
|
'initial': {
|
|
'recordBtn': false,
|
|
'playBtn': true,
|
|
'saveBtn': true,
|
|
'rerecordBtn': true,
|
|
'prevBtn': false,
|
|
'skipBtn': false
|
|
},
|
|
'recording': {
|
|
'recordBtn': false,
|
|
'playBtn': true,
|
|
'saveBtn': true,
|
|
'rerecordBtn': true,
|
|
'prevBtn': true,
|
|
'skipBtn': true
|
|
},
|
|
'recorded': {
|
|
'recordBtn': true,
|
|
'playBtn': false,
|
|
'saveBtn': false,
|
|
'rerecordBtn': false,
|
|
'prevBtn': true,
|
|
'skipBtn': true
|
|
},
|
|
'saving': {
|
|
'recordBtn': true,
|
|
'playBtn': true,
|
|
'saveBtn': true,
|
|
'rerecordBtn': true,
|
|
'prevBtn': false,
|
|
'skipBtn': false
|
|
}
|
|
};
|
|
|
|
const buttonStates = states[state];
|
|
if (!buttonStates) return;
|
|
|
|
Object.keys(buttonStates).forEach(buttonId => {
|
|
const button = document.getElementById(buttonId);
|
|
if (button) {
|
|
button.disabled = buttonStates[buttonId];
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function disableRecordingControls(disabled = true) {
|
|
if (disabled) {
|
|
updateButtonStates('initial');
|
|
|
|
NAVIGATION_BUTTONS.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.disabled = true;
|
|
}
|
|
});
|
|
} else {
|
|
|
|
NAVIGATION_BUTTONS.forEach(id => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.disabled = false;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
disableRecordingControls(true);
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const setupFormScroll = document.querySelector('.setup-form-scroll');
|
|
|
|
setupFormScroll.addEventListener('scroll', () => {
|
|
if (setupFormScroll.scrollTop > 0) {
|
|
setupFormScroll.classList.add('scrolled');
|
|
} else {
|
|
setupFormScroll.classList.remove('scrolled');
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
function showConfirmDialog(message) {
|
|
return new Promise((resolve) => {
|
|
const modal = document.createElement('div');
|
|
modal.className = 'modal fade show';
|
|
modal.style.display = 'block';
|
|
modal.innerHTML = `
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content" style="border-radius: 8px; border: none; box-shadow: 0 2px 6px rgba(60, 64, 67, 0.15);">
|
|
<div class="modal-header" style="border-bottom: 1px solid #dadce0; padding: 16px 24px;">
|
|
<h5 class="modal-title" style="color: #202124; font-family: 'Google Sans', sans-serif; font-size: 16px;">Confirm Update</h5>
|
|
<button type="button" class="btn-close" style="color: #5f6368;" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" style="padding: 24px; color: #5f6368;">
|
|
<p style="margin-bottom: 0; font-size: 14px;">${message}</p>
|
|
</div>
|
|
<div class="modal-footer" style="border-top: 1px solid #dadce0; padding: 16px 24px; gap: 8px;">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary confirm-btn">Continue</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'modal-backdrop fade show';
|
|
backdrop.style.backgroundColor = 'rgba(32, 33, 36, 0.6)';
|
|
|
|
document.body.appendChild(modal);
|
|
document.body.appendChild(backdrop);
|
|
document.body.classList.add('modal-open');
|
|
|
|
const closeModal = () => {
|
|
modal.remove();
|
|
backdrop.remove();
|
|
document.body.classList.remove('modal-open');
|
|
};
|
|
|
|
modal.querySelector('.btn-close').onclick = () => {
|
|
closeModal();
|
|
resolve(false);
|
|
};
|
|
|
|
modal.querySelector('.btn-outline-secondary').onclick = () => {
|
|
closeModal();
|
|
resolve(false);
|
|
};
|
|
|
|
modal.querySelector('.confirm-btn').onclick = () => {
|
|
closeModal();
|
|
resolve(true);
|
|
};
|
|
|
|
|
|
backdrop.onclick = () => {
|
|
closeModal();
|
|
resolve(false);
|
|
};
|
|
});
|
|
}
|
|
|
|
|
|
document.getElementById('sessionForm').addEventListener('submit', async function(e) {
|
|
e.preventDefault();
|
|
|
|
|
|
const csrfToken = getCsrfToken();
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('csrf_token', csrfToken);
|
|
|
|
|
|
const speakerName = document.getElementById('speakerName')?.value || '';
|
|
const gender = document.getElementById('gender')?.value || '';
|
|
const language = document.getElementById('language')?.value || '';
|
|
const country = document.getElementById('country')?.value || '';
|
|
const state = document.getElementById('state')?.value || '';
|
|
const city = document.getElementById('city')?.value || '';
|
|
const ageGroup = document.getElementById('age_group')?.value || '';
|
|
const accent = document.getElementById('accent')?.value || '';
|
|
const domain = document.getElementById('domain')?.value || '';
|
|
const subdomain = document.getElementById('subdomain')?.value || '';
|
|
|
|
|
|
if (!language) {
|
|
showToast('Please select a language', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!domain) {
|
|
showToast('Please select a domain', 'error');
|
|
return;
|
|
}
|
|
|
|
if (!subdomain) {
|
|
showToast('Please select a subdomain', 'error');
|
|
return;
|
|
}
|
|
|
|
|
|
formData.append('speakerName', speakerName);
|
|
formData.append('gender', gender);
|
|
formData.append('language', language);
|
|
formData.append('country', country);
|
|
formData.append('state', state);
|
|
formData.append('city', city);
|
|
formData.append('age_group', ageGroup);
|
|
formData.append('accent', accent);
|
|
formData.append('domain', domain);
|
|
formData.append('subdomain', subdomain);
|
|
|
|
const submitButton = document.querySelector('#sessionForm button[type="submit"]');
|
|
const isUpdate = submitButton && submitButton.textContent === 'Update Session';
|
|
|
|
if (isUpdate) {
|
|
const confirmed = await showConfirmDialog('Warning: Updating the session will apply these changes to all future recordings. Continue?');
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/start_session', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': csrfToken
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
|
|
if (response.status === 403) {
|
|
showToast('Session authentication error. Please refresh the page and try again.', 'error');
|
|
return;
|
|
}
|
|
|
|
|
|
if (response.status === 401) {
|
|
const data = await response.json();
|
|
if (data.code === 'AUTH_ERROR') {
|
|
showToast('Your session has expired. Redirecting to login page...', 'error');
|
|
setTimeout(() => {
|
|
window.location.href = '/login';
|
|
}, 2000);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const data = await response.json();
|
|
if (response.ok) {
|
|
|
|
document.getElementById('speakerName').value = data.speaker_name;
|
|
|
|
|
|
const sessionData = {
|
|
gender: document.getElementById('gender').value,
|
|
language: document.getElementById('language').value,
|
|
country: document.getElementById('country').value,
|
|
state: document.getElementById('state').value,
|
|
city: document.getElementById('city').value,
|
|
speaker_name: data.speaker_name,
|
|
age_group: document.getElementById('age_group').value,
|
|
accent: document.getElementById('accent').value
|
|
};
|
|
sessionStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(sessionData));
|
|
|
|
|
|
showToast('Session started successfully', 'success');
|
|
|
|
|
|
document.querySelector('.initial-message').style.display = 'none';
|
|
document.querySelector('.transcript-container').style.display = 'block';
|
|
|
|
|
|
const recordingInterface = document.getElementById('recordingInterface');
|
|
recordingInterface.classList.remove('disabled-interface');
|
|
disableRecordingControls(false);
|
|
updateButtonStates('initial');
|
|
|
|
|
|
await loadNextTranscript();
|
|
|
|
|
|
submitButton.textContent = 'Update Session';
|
|
|
|
|
|
updateInterfaceState();
|
|
|
|
|
|
if (window.userData) {
|
|
window.userData = {
|
|
...window.userData,
|
|
gender,
|
|
language,
|
|
country,
|
|
state_province: state,
|
|
city,
|
|
age_group: ageGroup,
|
|
accent
|
|
};
|
|
}
|
|
|
|
|
|
const settingsPanel = document.getElementById('settingsPanel');
|
|
if (settingsPanel.classList.contains('show')) {
|
|
settingsPanel.classList.remove('show');
|
|
const overlay = document.querySelector('.overlay');
|
|
if (overlay) {
|
|
overlay.classList.remove('show');
|
|
setTimeout(() => overlay.remove(), 300);
|
|
}
|
|
}
|
|
|
|
} else {
|
|
showToast(data.error || 'Failed to start session', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
showToast('Error starting session', 'error');
|
|
}
|
|
});
|
|
|
|
async function loadNextTranscript() {
|
|
try {
|
|
const response = await fetch('/next_transcript');
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to load next transcript');
|
|
}
|
|
|
|
if (data.finished && data.current >= data.total) {
|
|
showToast('Recording session completed!', 'success');
|
|
setTimeout(() => {
|
|
sessionStorage.removeItem(CURRENT_ROW_KEY);
|
|
window.location.reload();
|
|
}, 2000);
|
|
return;
|
|
}
|
|
|
|
updateTranscriptDisplay(data);
|
|
|
|
|
|
if (data.current > 0) {
|
|
sessionStorage.setItem(CURRENT_ROW_KEY, data.current.toString());
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading transcript:', error);
|
|
showToast(error.message, 'error');
|
|
}
|
|
}
|
|
|
|
|
|
function updateProgressDisplay(current, total) {
|
|
document.getElementById('progress').textContent = current;
|
|
document.getElementById('total').textContent = total;
|
|
}
|
|
|
|
|
|
let audioStream = null;
|
|
let recorder = null;
|
|
|
|
|
|
function setupAudioContext() {
|
|
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
audioContext = new AudioContext({
|
|
sampleRate: 48000,
|
|
latencyHint: 'interactive'
|
|
});
|
|
return audioContext;
|
|
}
|
|
|
|
|
|
document.getElementById('recordBtn').addEventListener('click', async () => {
|
|
const recordBtn = document.getElementById('recordBtn');
|
|
const isRecording = recordBtn.classList.contains('recording');
|
|
|
|
if (!isRecording) {
|
|
|
|
try {
|
|
|
|
if (!audioContext) {
|
|
audioContext = setupAudioContext();
|
|
} else if (audioContext.state === 'suspended') {
|
|
await audioContext.resume();
|
|
}
|
|
|
|
|
|
audioStream = await navigator.mediaDevices.getUserMedia({
|
|
audio: {
|
|
channelCount: 1,
|
|
sampleRate: 48000,
|
|
echoCancellation: true,
|
|
noiseSuppression: true,
|
|
autoGainControl: true
|
|
}
|
|
});
|
|
|
|
|
|
audioInput = audioContext.createMediaStreamSource(audioStream);
|
|
|
|
|
|
scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
|
|
|
|
|
|
rawPCMData = [];
|
|
isRecordingPCM = true;
|
|
|
|
|
|
scriptProcessor.onaudioprocess = (event) => {
|
|
if (isRecordingPCM) {
|
|
|
|
const inputData = event.inputBuffer.getChannelData(0);
|
|
|
|
|
|
const pcmChunk = new Float32Array(inputData.length);
|
|
pcmChunk.set(inputData);
|
|
|
|
|
|
rawPCMData.push(pcmChunk);
|
|
}
|
|
};
|
|
|
|
|
|
audioInput.connect(scriptProcessor);
|
|
scriptProcessor.connect(audioContext.destination);
|
|
|
|
|
|
recordBtn.innerHTML = '<span class="recording-dot"></span> Stop Recording';
|
|
recordBtn.classList.add('recording', 'btn-danger', 'is-recording');
|
|
recordBtn.classList.remove('btn-primary');
|
|
updateButtonStates('recording');
|
|
|
|
|
|
recordingTimeout = setTimeout(() => {
|
|
if (isRecordingPCM) {
|
|
stopPCMRecording();
|
|
showToast('Maximum recording duration reached (30 seconds)', 'warning');
|
|
}
|
|
}, MAX_RECORDING_DURATION);
|
|
|
|
} catch (err) {
|
|
console.error('Audio recording error:', err);
|
|
showToast('Error accessing microphone: ' + err.message, 'error');
|
|
}
|
|
} else {
|
|
|
|
stopPCMRecording();
|
|
}
|
|
});
|
|
|
|
|
|
function stopPCMRecording() {
|
|
if (isRecordingPCM) {
|
|
|
|
isRecordingPCM = false;
|
|
clearTimeout(recordingTimeout);
|
|
|
|
|
|
if (scriptProcessor && audioInput) {
|
|
audioInput.disconnect(scriptProcessor);
|
|
scriptProcessor.disconnect(audioContext.destination);
|
|
}
|
|
|
|
|
|
if (audioStream) {
|
|
audioStream.getTracks().forEach(track => track.stop());
|
|
audioStream = null;
|
|
}
|
|
|
|
|
|
processPCMData();
|
|
|
|
|
|
const recordBtn = document.getElementById('recordBtn');
|
|
recordBtn.innerHTML = 'Start Recording';
|
|
recordBtn.classList.remove('recording', 'btn-danger', 'is-recording');
|
|
recordBtn.classList.add('btn-primary');
|
|
updateButtonStates('recorded');
|
|
}
|
|
}
|
|
|
|
|
|
function processPCMData() {
|
|
|
|
let totalLength = 0;
|
|
for (const chunk of rawPCMData) {
|
|
totalLength += chunk.length;
|
|
}
|
|
|
|
|
|
const sampleRate = audioContext.sampleRate || 48000;
|
|
const fadeInSamples = Math.min(sampleRate * 0.3, totalLength * 0.1);
|
|
const endTrimSamples = Math.min(sampleRate * 0.15, totalLength * 0.05);
|
|
const fadeOutSamples = Math.min(sampleRate * 0.15, totalLength * 0.04);
|
|
|
|
|
|
const fullMergedPCM = new Float32Array(totalLength);
|
|
|
|
let offset = 0;
|
|
for (const chunk of rawPCMData) {
|
|
fullMergedPCM.set(chunk, offset);
|
|
offset += chunk.length;
|
|
}
|
|
|
|
|
|
for (let i = 0; i < fadeInSamples; i++) {
|
|
|
|
const fadeRatio = i / fadeInSamples;
|
|
|
|
|
|
const smoothFade = fadeRatio * fadeRatio * fadeRatio;
|
|
|
|
|
|
fullMergedPCM[i] *= smoothFade;
|
|
}
|
|
|
|
|
|
const trimmedLength = Math.max(0, totalLength - endTrimSamples);
|
|
const trimmedPCM = fullMergedPCM.slice(0, trimmedLength);
|
|
|
|
|
|
const fadeOutStartIndex = trimmedLength - fadeOutSamples;
|
|
for (let i = 0; i < fadeOutSamples; i++) {
|
|
if (fadeOutStartIndex + i >= trimmedLength) break;
|
|
|
|
|
|
const fadeRatio = 1 - (i / fadeOutSamples);
|
|
|
|
|
|
const smoothFade = fadeRatio * fadeRatio * fadeRatio;
|
|
|
|
|
|
trimmedPCM[fadeOutStartIndex + i] *= smoothFade;
|
|
}
|
|
|
|
|
|
const pcm16bit = convertFloat32ToInt16(trimmedPCM);
|
|
|
|
|
|
audioBlob = new Blob([pcm16bit], { type: 'audio/pcm' });
|
|
|
|
|
|
audioProcessed = true;
|
|
}
|
|
|
|
|
|
function convertFloat32ToInt16(float32Array) {
|
|
const int16Array = new Int16Array(float32Array.length);
|
|
for (let i = 0; i < float32Array.length; i++) {
|
|
|
|
|
|
const s = Math.max(-1, Math.min(1, float32Array[i]));
|
|
int16Array[i] = s < 0 ? s * 0x8000 : s * 0x7FFF;
|
|
}
|
|
return int16Array;
|
|
}
|
|
|
|
|
|
document.getElementById('playBtn').addEventListener('click', () => {
|
|
if (audioBlob) {
|
|
if (audioPlayer && !audioPlayer.paused) {
|
|
|
|
audioPlayer.pause();
|
|
document.getElementById('playBtn').textContent = 'Play';
|
|
audioPlayer = null;
|
|
} else {
|
|
|
|
const playbackContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function(e) {
|
|
try {
|
|
|
|
const arrayBuffer = e.target.result;
|
|
|
|
|
|
const int16Array = new Int16Array(arrayBuffer);
|
|
|
|
|
|
const sampleRate = audioContext ? audioContext.sampleRate : 48000;
|
|
const audioBuffer = playbackContext.createBuffer(1, int16Array.length, sampleRate);
|
|
|
|
|
|
const channelData = audioBuffer.getChannelData(0);
|
|
|
|
|
|
for (let i = 0; i < int16Array.length; i++) {
|
|
|
|
channelData[i] = int16Array[i] / (int16Array[i] < 0 ? 32768 : 32767);
|
|
}
|
|
|
|
|
|
const source = playbackContext.createBufferSource();
|
|
source.buffer = audioBuffer;
|
|
source.connect(playbackContext.destination);
|
|
|
|
|
|
source.start(0);
|
|
document.getElementById('playBtn').textContent = 'Stop';
|
|
|
|
|
|
audioPlayer = {
|
|
pause: function() {
|
|
source.stop(0);
|
|
},
|
|
paused: false,
|
|
currentTime: 0
|
|
};
|
|
|
|
|
|
source.onended = function() {
|
|
document.getElementById('playBtn').textContent = 'Play';
|
|
audioPlayer = null;
|
|
};
|
|
} catch (error) {
|
|
console.error('Error playing audio:', error);
|
|
showToast('Error playing audio: ' + error.message, 'error');
|
|
document.getElementById('playBtn').textContent = 'Play';
|
|
audioPlayer = null;
|
|
}
|
|
};
|
|
|
|
|
|
reader.readAsArrayBuffer(audioBlob);
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
function stopPlayback() {
|
|
if (audioPlayer) {
|
|
audioPlayer.pause();
|
|
document.getElementById('playBtn').textContent = 'Play';
|
|
audioPlayer = null;
|
|
}
|
|
}
|
|
|
|
|
|
['recordBtn', 'saveBtn', 'rerecordBtn'].forEach(buttonId => {
|
|
document.getElementById(buttonId).addEventListener('click', stopPlayback);
|
|
});
|
|
|
|
|
|
document.getElementById('saveBtn').addEventListener('click', async () => {
|
|
|
|
isSaving = true;
|
|
updateButtonStates('saving');
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
|
formData.append('csrf_token', getCsrfToken());
|
|
|
|
|
|
formData.append('audio', audioBlob, 'recording.pcm');
|
|
formData.append('sampleRate', '48000');
|
|
formData.append('bitsPerSample', '16');
|
|
formData.append('channels', '1');
|
|
formData.append('trimmed', 'true');
|
|
|
|
try {
|
|
const response = await fetch('/save_recording', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRF-Token': getCsrfToken()
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
if (data.storage.includes('huggingface')) {
|
|
|
|
showToast('Starting upload to Hugging Face...', 'info');
|
|
|
|
|
|
pendingUploads.set(data.upload_id, {
|
|
timestamp: Date.now(),
|
|
attempts: 0
|
|
});
|
|
|
|
|
|
pollUploadStatus(data.upload_id);
|
|
|
|
|
|
updateUploadStatus();
|
|
} else if (data.storage.includes('local')) {
|
|
|
|
showToast('Recording saved', 'success');
|
|
} else if (data.storage.includes('memory')) {
|
|
|
|
showToast('Recording saved in memory', 'success');
|
|
}
|
|
|
|
|
|
if (data.next_transcript) {
|
|
document.getElementById('currentTranscript').textContent = data.next_transcript.text;
|
|
updateProgressDisplay(data.next_transcript.current, data.next_transcript.total);
|
|
sessionStorage.setItem(CURRENT_ROW_KEY, data.next_transcript.current.toString());
|
|
} else if (data.session_complete) {
|
|
showToast('Recording session completed!', 'success');
|
|
setTimeout(() => window.location.reload(), 2000);
|
|
return;
|
|
}
|
|
|
|
|
|
isSaving = false;
|
|
updateButtonStates('initial');
|
|
resetRecordingControls();
|
|
|
|
} else {
|
|
const error = await response.json();
|
|
showToast('Error saving recording: ' + error.error, 'error');
|
|
|
|
|
|
isSaving = false;
|
|
updateButtonStates('recorded');
|
|
}
|
|
} catch (error) {
|
|
showToast('Error saving recording: ' + error, 'error');
|
|
|
|
|
|
isSaving = false;
|
|
updateButtonStates('recorded');
|
|
}
|
|
});
|
|
|
|
|
|
document.getElementById('rerecordBtn').addEventListener('click', () => {
|
|
audioChunks = [];
|
|
audioBlob = null;
|
|
updateButtonStates('initial');
|
|
});
|
|
|
|
|
|
function resetRecordingControls() {
|
|
if (mediaRecorder && mediaRecorder.stream) {
|
|
mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
|
}
|
|
clearTimeout(recordingTimeout);
|
|
audioChunks = [];
|
|
audioBlob = null;
|
|
updateButtonStates('initial');
|
|
|
|
|
|
const recordBtn = document.getElementById('recordBtn');
|
|
recordBtn.innerHTML = 'Start Recording';
|
|
recordBtn.classList.remove('recording', 'btn-danger', 'is-recording');
|
|
recordBtn.classList.add('btn-primary');
|
|
}
|
|
|
|
|
|
document.querySelectorAll('.material-input .form-control').forEach(input => {
|
|
|
|
input.setAttribute('placeholder', ' ');
|
|
|
|
|
|
input.addEventListener('animationstart', function(e) {
|
|
if (e.animationName === 'onAutoFillStart') {
|
|
this.parentElement.classList.add('is-filled');
|
|
}
|
|
});
|
|
|
|
input.addEventListener('input', function() {
|
|
if (this.value) {
|
|
this.parentElement.classList.add('is-filled');
|
|
} else {
|
|
this.parentElement.classList.remove('is-filled');
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
const settingsToggle = document.getElementById('settingsToggle');
|
|
const settingsPanel = document.getElementById('settingsPanel');
|
|
const body = document.body;
|
|
|
|
if (settingsToggle) {
|
|
settingsToggle.addEventListener('click', () => {
|
|
settingsPanel.classList.toggle('show');
|
|
|
|
|
|
let overlay = document.querySelector('.overlay');
|
|
if (!overlay) {
|
|
overlay = document.createElement('div');
|
|
overlay.className = 'overlay';
|
|
body.appendChild(overlay);
|
|
}
|
|
overlay.classList.toggle('show');
|
|
|
|
|
|
overlay.addEventListener('click', () => {
|
|
settingsPanel.classList.remove('show');
|
|
overlay.classList.remove('show');
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
const transcriptContainer = document.querySelector('.transcript-container');
|
|
if (transcriptContainer) {
|
|
transcriptContainer.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
|
|
|
});
|
|
|
|
|
|
function updateInterfaceState() {
|
|
const recordingInterface = document.getElementById('recordingInterface');
|
|
const initialMessage = document.getElementById('initialMessage');
|
|
const transcriptContainer = document.querySelector('.transcript-container');
|
|
|
|
if (!isAuthenticated) {
|
|
recordingInterface.classList.add('disabled-interface');
|
|
initialMessage.style.display = 'block';
|
|
transcriptContainer.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const sessionData = sessionStorage.getItem(SESSION_STORAGE_KEY);
|
|
|
|
if (!sessionData) {
|
|
recordingInterface.classList.add('disabled-interface');
|
|
initialMessage.style.display = 'block';
|
|
transcriptContainer.style.display = 'none';
|
|
|
|
|
|
initialMessage.innerHTML = `
|
|
<div class="text-center empty-state">
|
|
<img src="static/microphone-icon.svg" alt="Microphone" width="64" height="64">
|
|
<h5 class="mt-4" style="color: #202124;">Interface Disabled</h5>
|
|
<p class="text-muted">
|
|
Please complete all required settings to begin recording.
|
|
</p>
|
|
<div class="mt-3 requirements-list">
|
|
<div class="requirement ${sessionData ? 'complete' : 'incomplete'}">
|
|
<span class="icon">⬤</span>
|
|
<span>Complete Settings</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} else {
|
|
recordingInterface.classList.remove('disabled-interface');
|
|
initialMessage.style.display = 'none';
|
|
transcriptContainer.style.display = 'block';
|
|
disableRecordingControls(false);
|
|
}
|
|
}
|
|
|
|
|
|
function increaseFontSize() {
|
|
const transcript = document.getElementById('currentTranscript');
|
|
const sizeDisplay = document.getElementById('fontSizeDisplay');
|
|
const currentSize = parseInt(window.getComputedStyle(transcript).fontSize);
|
|
|
|
if (currentSize < MAX_FONT_SIZE) {
|
|
const newSize = currentSize + FONT_SIZE_STEP;
|
|
transcript.style.setProperty('--transcript-font-size', `${newSize}px`);
|
|
sizeDisplay.textContent = newSize;
|
|
localStorage.setItem('transcriptFontSize', newSize);
|
|
}
|
|
}
|
|
|
|
function decreaseFontSize() {
|
|
const transcript = document.getElementById('currentTranscript');
|
|
const sizeDisplay = document.getElementById('fontSizeDisplay');
|
|
const currentSize = parseInt(window.getComputedStyle(transcript).fontSize);
|
|
|
|
if (currentSize > MIN_FONT_SIZE) {
|
|
const newSize = currentSize - FONT_SIZE_STEP;
|
|
transcript.style.setProperty('--transcript-font-size', `${newSize}px`);
|
|
sizeDisplay.textContent = newSize;
|
|
localStorage.setItem('transcriptFontSize', newSize);
|
|
}
|
|
}
|
|
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
|
|
if (document.getElementById('recordingInterface').classList.contains('disabled-interface')) {
|
|
return;
|
|
}
|
|
|
|
|
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') {
|
|
return;
|
|
}
|
|
|
|
const recordBtn = document.getElementById('recordBtn');
|
|
|
|
switch (e.key.toLowerCase()) {
|
|
case 'r':
|
|
if (!recordBtn.disabled) {
|
|
recordBtn.click();
|
|
}
|
|
break;
|
|
case ' ':
|
|
e.preventDefault();
|
|
if (!document.getElementById('playBtn').disabled) {
|
|
document.getElementById('playBtn').click();
|
|
}
|
|
break;
|
|
case 'enter':
|
|
if (!document.getElementById('saveBtn').disabled) {
|
|
document.getElementById('saveBtn').click();
|
|
}
|
|
break;
|
|
case 'backspace':
|
|
if (!document.getElementById('rerecordBtn').disabled) {
|
|
e.preventDefault();
|
|
document.getElementById('rerecordBtn').click();
|
|
}
|
|
break;
|
|
case 'arrowleft':
|
|
if (!document.getElementById('prevBtn').disabled) {
|
|
document.getElementById('prevBtn').click();
|
|
}
|
|
break;
|
|
case 'arrowright':
|
|
if (!document.getElementById('skipBtn').disabled) {
|
|
document.getElementById('skipBtn').click();
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
|
|
|
|
function updateTranscriptScrollState() {
|
|
const transcriptBox = document.querySelector('.transcript-box');
|
|
if (transcriptBox) {
|
|
const isScrollable = transcriptBox.scrollHeight > transcriptBox.clientHeight;
|
|
transcriptBox.classList.toggle('scrollable', isScrollable);
|
|
}
|
|
}
|
|
|
|
|
|
const originalLoadNextTranscript = loadNextTranscript;
|
|
loadNextTranscript = async function() {
|
|
await originalLoadNextTranscript.apply(this, arguments);
|
|
updateTranscriptScrollState();
|
|
};
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
|
|
const transcriptBox = document.querySelector('.transcript-box');
|
|
if (transcriptBox) {
|
|
transcriptBox.addEventListener('scroll', () => {
|
|
const hasReachedBottom =
|
|
transcriptBox.scrollHeight - transcriptBox.scrollTop <= transcriptBox.clientHeight + 1;
|
|
transcriptBox.classList.toggle('at-bottom', hasReachedBottom);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.getElementById('uploadToast');
|
|
const toastHeader = toast.querySelector('.toast-header');
|
|
const toastBody = toast.querySelector('.toast-body');
|
|
|
|
|
|
toast.classList.remove('info', 'success', 'error', 'warning');
|
|
const oldIcon = toastHeader.querySelector('.toast-icon');
|
|
if (oldIcon) oldIcon.remove();
|
|
|
|
|
|
toast.classList.add(type);
|
|
|
|
|
|
const icon = document.createElement('div');
|
|
icon.className = 'toast-icon';
|
|
icon.innerHTML = getToastIcon(type);
|
|
toastHeader.insertBefore(icon, toastHeader.firstChild);
|
|
|
|
toastBody.textContent = message;
|
|
|
|
|
|
const bsToast = new bootstrap.Toast(toast, {
|
|
autohide: true,
|
|
delay: 3000
|
|
});
|
|
bsToast.show();
|
|
}
|
|
|
|
function getToastIcon(type) {
|
|
const icons = {
|
|
info: `<svg viewBox="0 0 24 24" fill="#1a73e8">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
|
|
</svg>`,
|
|
success: `<svg viewBox="0 0 24 24" fill="#34a853">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
</svg>`,
|
|
error: `<svg viewBox="0 0 24 24" fill="#ea4335">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
</svg>`,
|
|
warning: `<svg viewBox="0 0 24 24" fill="#fbbc04">
|
|
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
</svg>`
|
|
};
|
|
return icons[type] || icons.info;
|
|
}
|
|
|
|
async function pollUploadStatus(uploadId) {
|
|
const upload = pendingUploads.get(uploadId);
|
|
if (!upload || upload.attempts > 30) {
|
|
pendingUploads.delete(uploadId);
|
|
updateUploadStatus();
|
|
if (upload && upload.attempts > 30) {
|
|
showToast('Upload timed out. Please try again.', 'warning');
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/check_upload/${uploadId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.complete) {
|
|
pendingUploads.delete(uploadId);
|
|
updateUploadStatus();
|
|
showToast('Recording uploaded successfully! 🎉', 'success');
|
|
} else {
|
|
upload.attempts++;
|
|
showToast(`Upload in progress... (${Math.round((upload.attempts/30)*100)}%)`, 'info');
|
|
setTimeout(() => pollUploadStatus(uploadId), 2000);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking upload status:', error);
|
|
upload.attempts++;
|
|
setTimeout(() => pollUploadStatus(uploadId), 5000);
|
|
showToast('Error checking upload status. Retrying...', 'error');
|
|
}
|
|
}
|
|
|
|
function updateUploadStatus() {
|
|
const statusContainer = document.getElementById('uploadStatus');
|
|
if (!statusContainer) return;
|
|
|
|
if (pendingUploads.size > 0) {
|
|
showToast(`Uploading: ${pendingUploads.size} files pending...`);
|
|
statusContainer.textContent = `Uploading: ${pendingUploads.size} pending`;
|
|
statusContainer.style.display = 'block';
|
|
} else {
|
|
statusContainer.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
const countrySelect = document.getElementById('country');
|
|
const stateSelect = document.getElementById('state');
|
|
|
|
|
|
for (const [code, name] of Object.entries(country_and_states.country)) {
|
|
const option = document.createElement('option');
|
|
option.value = code;
|
|
option.textContent = name;
|
|
countrySelect.appendChild(option);
|
|
}
|
|
|
|
|
|
countrySelect.addEventListener('change', () => {
|
|
const selectedCountry = countrySelect.value;
|
|
|
|
stateSelect.innerHTML = '<option value="">Select State/Province</option>';
|
|
if (selectedCountry && country_and_states.states[selectedCountry]) {
|
|
country_and_states.states[selectedCountry].forEach(state => {
|
|
const option = document.createElement('option');
|
|
option.value = state.code;
|
|
option.textContent = state.name;
|
|
stateSelect.appendChild(option);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const consentCheckbox = document.getElementById('consentCheckbox');
|
|
const startSessionBtn = document.getElementById('startSessionBtn');
|
|
|
|
consentCheckbox.addEventListener('change', function() {
|
|
startSessionBtn.disabled = !this.checked;
|
|
});
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
const settingsToggle = document.getElementById('settingsToggle');
|
|
const settingsPanel = document.getElementById('settingsPanel');
|
|
const settingsCloseBtn = document.getElementById('settingsCloseBtn');
|
|
|
|
function closeSettingsPanel() {
|
|
settingsPanel.classList.remove('show');
|
|
const overlay = document.querySelector('.overlay');
|
|
if (overlay) {
|
|
overlay.classList.remove('show');
|
|
setTimeout(() => overlay.remove(), 300);
|
|
}
|
|
}
|
|
|
|
if (settingsToggle) {
|
|
settingsToggle.addEventListener('click', () => {
|
|
settingsPanel.classList.add('show');
|
|
|
|
|
|
if (!document.querySelector('.overlay')) {
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'overlay';
|
|
document.body.appendChild(overlay);
|
|
|
|
|
|
overlay.addEventListener('click', closeSettingsPanel);
|
|
|
|
|
|
setTimeout(() => overlay.classList.add('show'), 10);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
if (settingsCloseBtn) {
|
|
settingsCloseBtn.addEventListener('click', closeSettingsPanel);
|
|
}
|
|
});
|
|
|
|
|
|
function clearSession() {
|
|
|
|
sessionStorage.removeItem(SESSION_STORAGE_KEY);
|
|
sessionStorage.removeItem(CURRENT_ROW_KEY);
|
|
|
|
|
|
document.getElementById('currentTranscript').textContent = '';
|
|
updateProgressDisplay(0, 0);
|
|
|
|
|
|
updateInterfaceState();
|
|
|
|
|
|
resetRecordingControls();
|
|
disableRecordingControls(true);
|
|
}
|
|
|
|
|
|
function handleNavigationError(data) {
|
|
switch(data.code) {
|
|
case 'NO_SESSION':
|
|
showToast('Please start a session first', 'warning');
|
|
break;
|
|
case 'NO_ROW':
|
|
showToast('Please enter a row number', 'warning');
|
|
break;
|
|
case 'INVALID_ROW':
|
|
showToast(data.error || 'Invalid row number', 'warning');
|
|
break;
|
|
case 'DATA_ERROR':
|
|
showToast('Error accessing transcript data', 'error');
|
|
console.error('Data error:', data.details);
|
|
break;
|
|
default:
|
|
showToast(data.error || 'Navigation error', 'error');
|
|
}
|
|
}
|
|
|
|
|
|
function updateTranscriptDisplay(data) {
|
|
|
|
document.getElementById('currentTranscript').textContent = data.transcript;
|
|
|
|
|
|
updateProgressDisplay(data.current, data.total);
|
|
|
|
|
|
const recordingStatus = document.getElementById('recordingStatus');
|
|
if (recordingStatus) {
|
|
if (data.previously_recorded) {
|
|
recordingStatus.style.display = 'inline-block';
|
|
recordingStatus.classList.remove('d-none');
|
|
} else {
|
|
recordingStatus.style.display = 'none';
|
|
recordingStatus.classList.add('d-none');
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
async function loadNextTranscript() {
|
|
try {
|
|
const response = await fetch('/next_transcript');
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to load next transcript');
|
|
}
|
|
|
|
if (data.finished && data.current >= data.total) {
|
|
showToast('Recording session completed!', 'success');
|
|
setTimeout(() => {
|
|
sessionStorage.removeItem(CURRENT_ROW_KEY);
|
|
window.location.reload();
|
|
}, 2000);
|
|
return;
|
|
}
|
|
|
|
updateTranscriptDisplay(data);
|
|
|
|
|
|
if (data.current > 0) {
|
|
sessionStorage.setItem(CURRENT_ROW_KEY, data.current.toString());
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error loading transcript:', error);
|
|
showToast(error.message, 'error');
|
|
}
|
|
}
|
|
|
|
|
|
function debounce(func, wait) {
|
|
let timeout;
|
|
return function(...args) {
|
|
const context = this;
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => func.apply(context, args), wait);
|
|
};
|
|
}
|
|
|
|
|
|
let isPrevBtnDisabled = false;
|
|
let isSkipBtnDisabled = false;
|
|
|
|
|
|
document.getElementById('prevBtn').addEventListener('click', debounce(async () => {
|
|
|
|
if (isPrevBtnDisabled) return;
|
|
|
|
try {
|
|
|
|
isPrevBtnDisabled = true;
|
|
document.getElementById('prevBtn').disabled = true;
|
|
|
|
const response = await fetch('/prev_transcript');
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
if (data.transcript) {
|
|
updateTranscriptDisplay(data);
|
|
sessionStorage.setItem(CURRENT_ROW_KEY, data.current.toString());
|
|
}
|
|
|
|
|
|
if (data.code === 'BOUNDARY_ERROR') {
|
|
showToast('Already at first transcript', 'info');
|
|
}
|
|
} else {
|
|
handleNavigationError(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Navigation error:', error);
|
|
showToast('Error navigating transcripts', 'error');
|
|
} finally {
|
|
|
|
setTimeout(() => {
|
|
isPrevBtnDisabled = false;
|
|
document.getElementById('prevBtn').disabled = false;
|
|
}, 300);
|
|
}
|
|
}, 300));
|
|
|
|
|
|
document.getElementById('skipBtn').addEventListener('click', debounce(async () => {
|
|
|
|
if (isSkipBtnDisabled) return;
|
|
|
|
try {
|
|
|
|
isSkipBtnDisabled = true;
|
|
document.getElementById('skipBtn').disabled = true;
|
|
|
|
const response = await fetch('/skip_transcript');
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
|
|
|
|
if (data.transcript) {
|
|
updateTranscriptDisplay(data);
|
|
sessionStorage.setItem(CURRENT_ROW_KEY, data.current.toString());
|
|
}
|
|
|
|
|
|
|
|
if (data.code === 'BOUNDARY_ERROR') {
|
|
showToast('Already at last transcript', 'info');
|
|
|
|
setTimeout(() => {
|
|
document.getElementById('prevBtn').disabled = false;
|
|
isPrevBtnDisabled = false;
|
|
}, 100);
|
|
}
|
|
} else {
|
|
handleNavigationError(data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Navigation error:', error);
|
|
showToast('Error navigating transcripts', 'error');
|
|
} finally {
|
|
|
|
setTimeout(() => {
|
|
isSkipBtnDisabled = false;
|
|
document.getElementById('skipBtn').disabled = false;
|
|
|
|
|
|
|
|
if (document.getElementById('prevBtn').disabled) {
|
|
document.getElementById('prevBtn').disabled = false;
|
|
isPrevBtnDisabled = false;
|
|
}
|
|
}, 300);
|
|
}
|
|
}, 300));
|
|
|