dia-tts-server / ui /script.js
Michael Hu
initial check in of the dia tts server
ac5de5b
// ui/script.js
document.addEventListener('DOMContentLoaded', function () {
// --- Global Flags ---
let isGenerating = false;
let isGenerationCancelled = false;
let wavesurfer = null; // Global wavesurfer instance
// --- Element Selectors ---
const ttsForm = document.getElementById('tts-form');
const textArea = document.getElementById('text');
const charCount = document.getElementById('char-count');
const voiceModeRadios = document.querySelectorAll('input[name="voice_mode"]');
const cloneOptionsDiv = document.getElementById('clone-options');
const cloneReferenceSelect = document.getElementById('clone_reference_select');
const cloneLoadButton = document.getElementById('clone-load-button'); // New ID
const cloneFileInput = document.getElementById('clone-file-input'); // New ID
const generateBtn = document.getElementById('generate-btn');
const loadingOverlay = document.getElementById('loading-overlay');
const loadingMessage = document.getElementById('loading-message');
const loadingStatus = document.getElementById('loading-status'); // New element for status
const loadingCancelBtn = document.getElementById('loading-cancel-btn'); // New ID
const notificationArea = document.getElementById('notification-area');
const audioPlayerContainer = document.getElementById('audio-player-container');
const configSaveBtn = document.getElementById('save-config-btn');
const configRestartBtn = document.getElementById('restart-server-btn');
const configStatus = document.getElementById('config-status');
const genDefaultsSaveBtn = document.getElementById('save-gen-defaults-btn'); // New ID
const genDefaultsStatus = document.getElementById('gen-defaults-status'); // New ID
const themeToggleButton = document.getElementById('theme-toggle-btn'); // New ID
const themeIconLight = document.getElementById('theme-icon-light'); // New ID
const themeIconDark = document.getElementById('theme-icon-dark'); // New ID
const presetsContainer = document.getElementById('presets-container'); // New ID
// --- Initial Setup ---
// Character counter
function updateCharCount() {
if (textArea && charCount) {
charCount.textContent = textArea.value.length;
}
}
if (textArea) {
textArea.addEventListener('input', updateCharCount);
updateCharCount(); // Initial count
}
// Toggle Clone Options Visibility & Required Attribute
function toggleCloneOptions() {
const selectedMode = document.querySelector('input[name="voice_mode"]:checked')?.value;
if (cloneOptionsDiv && cloneReferenceSelect && cloneLoadButton) {
if (selectedMode === 'clone') {
cloneOptionsDiv.classList.remove('hidden');
cloneReferenceSelect.required = true;
cloneLoadButton.classList.remove('hidden');
} else {
cloneOptionsDiv.classList.add('hidden');
cloneReferenceSelect.required = false;
// cloneReferenceSelect.value = 'none'; // Don't reset if user might switch back
cloneLoadButton.classList.add('hidden');
}
}
}
voiceModeRadios.forEach(radio => radio.addEventListener('change', toggleCloneOptions));
toggleCloneOptions(); // Initial check
// Update slider value displays dynamically
const sliders = [
{ id: 'speed_factor', valueId: 'speed_factor_value' },
{ id: 'cfg_scale', valueId: 'cfg_scale_value' },
{ id: 'temperature', valueId: 'temperature_value' },
{ id: 'top_p', valueId: 'top_p_value' },
{ id: 'cfg_filter_top_k', valueId: 'cfg_filter_top_k_value' },
];
sliders.forEach(sliderInfo => {
const slider = document.getElementById(sliderInfo.id);
const valueDisplay = document.getElementById(sliderInfo.valueId);
if (slider && valueDisplay) {
// Set initial display from slider's current value (set by template)
valueDisplay.textContent = slider.value;
// Add event listener to update display on change
slider.addEventListener('input', () => valueDisplay.textContent = slider.value);
}
});
// --- Notifications ---
function showNotification(message, type = 'success', duration = 5000) {
if (!notificationArea) return;
// notificationArea.innerHTML = ''; // Clear previous? Or allow multiple? Let's allow multiple for now.
const colors = {
success: 'notification-success',
error: 'notification-error',
warning: 'notification-warning',
info: 'notification-info' // Add info style if needed
};
const icons = { // SVG icons or classes
success: '<svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" /></svg>',
error: '<svg class="h-5 w-5 text-red-500 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" /></svg>',
warning: '<svg class="h-5 w-5 text-yellow-500 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /></svg>',
info: '<svg class="h-5 w-5 text-sky-500 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" clip-rule="evenodd" /></svg>'
};
const notificationDiv = document.createElement('div');
notificationDiv.className = colors[type] || colors['info']; // Default to info style
notificationDiv.innerHTML = `${icons[type] || icons['info']} <span class="block sm:inline">${message}</span>`;
notificationArea.appendChild(notificationDiv);
// Auto-hide after specified duration
if (duration > 0) {
setTimeout(() => {
notificationDiv.style.transition = 'opacity 0.5s ease-out';
notificationDiv.style.opacity = '0';
setTimeout(() => notificationDiv.remove(), 500);
}, duration);
}
return notificationDiv; // Return the element if manual removal is needed
}
// --- Presets ---
function applyPreset(presetData) {
console.log("Applying preset:", presetData);
if (!presetData) return;
// Update text area
if (textArea && presetData.text !== undefined) {
textArea.value = presetData.text;
updateCharCount(); // Update counter
}
// Update voice mode
if (presetData.voice_mode) {
const radio = document.querySelector(`input[name="voice_mode"][value="${presetData.voice_mode}"]`);
if (radio) {
radio.checked = true;
toggleCloneOptions(); // Update UI based on new mode
}
}
// Update generation parameters
if (presetData.params) {
for (const [key, value] of Object.entries(presetData.params)) {
const slider = document.getElementById(key); // Assumes slider ID matches param key
const valueDisplay = document.getElementById(`${key}_value`);
if (slider) {
slider.value = value;
if (valueDisplay) {
valueDisplay.textContent = value; // Update display
}
} else {
console.warn(`Slider element not found for preset parameter: ${key}`);
}
}
}
showNotification(`Preset "${presetData.name}" loaded.`, 'info', 3000);
}
// Add event listeners to preset buttons (assuming they exist)
// Presets data should be available globally, e.g., from template `window.appPresets = {{ presets | tojson }};`
if (window.appPresets && presetsContainer) {
window.appPresets.forEach((preset, index) => {
const button = document.getElementById(`preset-btn-${index}`);
if (button) {
button.addEventListener('click', () => applyPreset(preset));
}
});
} else if (presetsContainer) {
console.warn("Presets data (window.appPresets) not found, preset buttons will not work.");
}
// --- Audio Player ---
function initializeWaveSurfer(audioUrl) {
if (wavesurfer) {
wavesurfer.destroy();
}
const waveformDiv = document.getElementById('waveform');
const playBtn = document.getElementById('play-btn');
const durationSpan = document.getElementById('audio-duration');
if (!waveformDiv || !playBtn || !durationSpan) {
console.error("Audio player elements not found in the container.");
// Clear the container if elements are missing after generation
if (audioPlayerContainer) audioPlayerContainer.innerHTML = '<p class="text-red-500 dark:text-red-400">Error displaying audio player.</p>';
return;
}
// Ensure button text doesn't wrap
playBtn.classList.add('whitespace-nowrap', 'flex-shrink-0');
const downloadLink = document.getElementById('download-link');
if (downloadLink) downloadLink.classList.add('whitespace-nowrap', 'flex-shrink-0');
wavesurfer = WaveSurfer.create({
container: waveformDiv,
waveColor: document.documentElement.classList.contains('dark') ? '#38bdf8' : '#0ea5e9', // primary-400(dark) / primary-500(light)
progressColor: document.documentElement.classList.contains('dark') ? '#0284c7' : '#0369a1', // primary-600(dark) / primary-700(light)
cursorColor: document.documentElement.classList.contains('dark') ? '#a855f7' : '#9333ea', // purple-500(dark) / purple-600(light)
barWidth: 3,
barRadius: 3,
cursorWidth: 1,
height: 80,
barGap: 2,
responsive: true,
url: audioUrl,
mediaControls: false, // Use custom controls
normalize: true,
});
wavesurfer.on('ready', () => {
const duration = wavesurfer.getDuration();
const minutes = Math.floor(duration / 60);
const seconds = Math.floor(duration % 60);
durationSpan.textContent = `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
playBtn.disabled = false;
playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 mr-1"><path fill-rule="evenodd" d="M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm6.39-2.908a.75.75 0 0 1 .766.027l3.5 2.25a.75.75 0 0 1 0 1.262l-3.5 2.25A.75.75 0 0 1 8 12.25v-4.5a.75.75 0 0 1 .39-.658Z" clip-rule="evenodd" /></svg> Play`;
});
wavesurfer.on('play', () => {
playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 mr-1"><path fill-rule="evenodd" d="M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm5-2.25A.75.75 0 0 1 7.75 7h4.5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-4.5Z" clip-rule="evenodd" /></svg> Pause`;
});
wavesurfer.on('pause', () => {
playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 mr-1"><path fill-rule="evenodd" d="M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm6.39-2.908a.75.75 0 0 1 .766.027l3.5 2.25a.75.75 0 0 1 0 1.262l-3.5 2.25A.75.75 0 0 1 8 12.25v-4.5a.75.75 0 0 1 .39-.658Z" clip-rule="evenodd" /></svg> Play`;
});
wavesurfer.on('finish', () => {
playBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 mr-1"><path fill-rule="evenodd" d="M2 10a8 8 0 1 1 16 0 8 8 0 0 1-16 0Zm6.39-2.908a.75.75 0 0 1 .766.027l3.5 2.25a.75.75 0 0 1 0 1.262l-3.5 2.25A.75.75 0 0 1 8 12.25v-4.5a.75.75 0 0 1 .39-.658Z" clip-rule="evenodd" /></svg> Play`;
});
playBtn.onclick = () => {
wavesurfer.playPause();
};
// Scroll to the player after initialization
setTimeout(() => {
audioPlayerContainer.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100); // Short delay to ensure rendering
}
// Initialize player if audio URL is present on initial page load
// This logic needs to be adapted as the player is now dynamically added
// We'll call initializeWaveSurfer if the template renders the player container
const initialAudioUrlElement = document.querySelector('[data-initial-audio-url]');
if (initialAudioUrlElement && initialAudioUrlElement.dataset.initialAudioUrl) {
console.log("Initializing WaveSurfer for initially loaded audio.");
initializeWaveSurfer(initialAudioUrlElement.dataset.initialAudioUrl);
}
// --- Form Submission & Cancellation ---
if (ttsForm) {
ttsForm.addEventListener('submit', function (event) {
// Client-side validation
const text = textArea.value.trim();
const mode = document.querySelector('input[name="voice_mode"]:checked')?.value;
const cloneFile = cloneReferenceSelect?.value;
if (!text) {
showNotification("Please enter some text.", 'error');
event.preventDefault(); return;
}
if (mode === 'clone' && (!cloneFile || cloneFile === 'none')) {
showNotification("Please select a reference file for clone mode.", 'error');
event.preventDefault(); return;
}
// Handle cancellation of previous request if Generate is clicked again
if (isGenerating) {
console.log("Generate clicked while previous generation in progress. Setting cancel flag.");
showNotification("Cancelling previous request...", 'warning', 2000);
isGenerationCancelled = true;
// We don't actually stop the backend here (Fake Cancel)
// but the result processing will ignore the previous result.
}
// Reset flags and show loading overlay for the new request
isGenerating = true;
isGenerationCancelled = false; // Reset cancel flag for the new request
if (loadingOverlay && generateBtn && loadingCancelBtn) {
loadingMessage.textContent = 'Generating audio...'; // Initial status
loadingStatus.textContent = 'Please wait.';
loadingOverlay.classList.remove('hidden');
generateBtn.disabled = true;
generateBtn.classList.add('opacity-50', 'cursor-not-allowed');
loadingCancelBtn.disabled = false; // Enable cancel button
}
// Allow default form submission to proceed
// The page will reload with results rendered by the template
});
}
// Handle Cancel button click
if (loadingCancelBtn) {
loadingCancelBtn.addEventListener('click', () => {
if (isGenerating) {
console.log("Cancel button clicked.");
isGenerationCancelled = true;
isGenerating = false; // Stop considering it "generating" from UI perspective
if (loadingOverlay && generateBtn) {
loadingOverlay.classList.add('hidden'); // Hide overlay
generateBtn.disabled = false; // Re-enable generate button
generateBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
showNotification("Generation cancelled by user.", 'info');
// Note: Backend request continues, but result will be ignored on page reload/update
}
});
}
// --- Result Handling (on page load after form submission) ---
// This logic runs every time the page loads. We check if specific elements
// indicating a successful generation are present.
const outputUrlElement = document.getElementById('output-file-url-data'); // Need to add this element in HTML
if (outputUrlElement && outputUrlElement.dataset.url) {
const outputUrl = outputUrlElement.dataset.url;
console.log("Page loaded with generation result:", outputUrl);
if (isGenerationCancelled) {
console.log("Generation was cancelled, ignoring result.");
showNotification("Previous generation was cancelled.", "warning");
// Reset flag after checking
isGenerationCancelled = false;
} else {
console.log("Processing successful generation result.");
// The audio player structure should be rendered by the template.
// We just need to initialize wavesurfer for it.
initializeWaveSurfer(outputUrl);
}
}
// Always reset generating flag on page load, as any active generation is now finished or irrelevant
isGenerating = false;
if (generateBtn) { // Re-enable button if page reloads for any reason
generateBtn.disabled = false;
generateBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
// --- Configuration Management ---
async function updateConfigStatus(button, statusElement, message, success = true, duration = 5000) {
const successClass = 'text-green-500 dark:text-green-400';
const errorClass = 'text-red-500 dark:text-red-400';
const savingClass = 'text-yellow-500 dark:text-yellow-400';
statusElement.textContent = message;
statusElement.className = `text-xs ml-2 ${success ? successClass : (message.startsWith('Saving') || message.startsWith('Restarting') ? savingClass : errorClass)}`;
statusElement.classList.remove('hidden');
if (button) button.disabled = true; // Disable button while processing
// Clear status after duration, re-enable button
if (duration > 0) {
setTimeout(() => {
statusElement.classList.add('hidden');
if (button) button.disabled = false;
}, duration);
}
}
// Save Server Configuration
if (configSaveBtn) {
configSaveBtn.addEventListener('click', async () => {
const configData = {};
document.querySelectorAll('#server-config-form input[name]').forEach(input => { // Assume inputs are within a form/div
configData[input.name] = input.value;
});
updateConfigStatus(configSaveBtn, configStatus, 'Saving...', true, 0); // Indefinite until success/error
try {
const response = await fetch('/save_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(configData)
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail || 'Failed to save');
updateConfigStatus(configSaveBtn, configStatus, result.message, true);
if (configRestartBtn) configRestartBtn.classList.remove('hidden'); // Show restart button
} catch (error) {
console.error('Error saving server config:', error);
updateConfigStatus(configSaveBtn, configStatus, `Error: ${error.message}`, false);
}
});
}
// Restart Server
if (configRestartBtn) {
configRestartBtn.addEventListener('click', async () => {
configRestartBtn.disabled = true;
configRestartBtn.innerHTML = `
<svg class="animate-spin h-5 w-5 mr-1 inline-block" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Restarting...`;
updateConfigStatus(configRestartBtn, configStatus, 'Restarting...', true, 0); // Indefinite
try {
const response = await fetch('/restart_server', { method: 'POST' });
const result = await response.json();
if (!response.ok) throw new Error(result.detail || 'Failed to trigger restart');
updateConfigStatus(configRestartBtn, configStatus, result.message + " Page will attempt reload.", true, 15000); // Show longer
// Show main loading overlay during restart check
if (loadingOverlay) {
loadingMessage.textContent = 'Server restarting...';
loadingStatus.textContent = 'Waiting for server to respond...';
loadingCancelBtn.disabled = true; // Disable cancel during restart
loadingOverlay.classList.remove('hidden');
}
// Poll for server readiness
let attempts = 0;
const maxAttempts = 45; // Wait up to 45 seconds
function checkServerReady() {
attempts++;
console.log(`Checking server readiness (Attempt ${attempts}/${maxAttempts})...`);
loadingStatus.textContent = `Waiting for server... (${attempts}/${maxAttempts})`;
fetch('/health?cache=' + Date.now(), { cache: 'no-store', headers: { 'pragma': 'no-cache' } })
.then(res => {
if (res.ok) {
console.log("Server is ready. Reloading page.");
window.location.reload(true); // Force reload from server
} else if (attempts < maxAttempts) {
setTimeout(checkServerReady, 1000); // Check again in 1 second
} else {
throw new Error('Server did not become ready after restart.');
}
})
.catch(() => {
if (attempts < maxAttempts) {
setTimeout(checkServerReady, 1000); // Check again on connection error
} else {
throw new Error('Server did not respond after restart.');
}
});
}
setTimeout(checkServerReady, 3000); // Start checking after 3 seconds
} catch (error) {
console.error('Error restarting server:', error);
updateConfigStatus(configRestartBtn, configStatus, `Restart Error: ${error.message}`, false);
configRestartBtn.disabled = false; // Re-enable button on error
configRestartBtn.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 mr-1 inline-block"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
Restart Server`;
if (loadingOverlay) loadingOverlay.classList.add('hidden');
}
});
}
// Save Generation Defaults
if (genDefaultsSaveBtn) {
genDefaultsSaveBtn.addEventListener('click', async () => {
const genParams = {};
sliders.forEach(s => {
const slider = document.getElementById(s.id);
if (slider) genParams[s.id] = slider.value;
});
updateConfigStatus(genDefaultsSaveBtn, genDefaultsStatus, 'Saving...', true, 0);
try {
const response = await fetch('/save_generation_defaults', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(genParams)
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail || 'Failed to save');
updateConfigStatus(genDefaultsSaveBtn, genDefaultsStatus, result.message, true);
} catch (error) {
console.error('Error saving generation defaults:', error);
updateConfigStatus(genDefaultsSaveBtn, genDefaultsStatus, `Error: ${error.message}`, false);
}
});
}
// --- Reference Audio Upload ---
if (cloneLoadButton && cloneFileInput && cloneReferenceSelect) {
cloneLoadButton.addEventListener('click', () => {
cloneFileInput.click(); // Trigger hidden file input
});
cloneFileInput.addEventListener('change', async (event) => {
const files = event.target.files;
if (!files || files.length === 0) {
return; // No files selected
}
cloneLoadButton.disabled = true;
cloneLoadButton.textContent = 'Uploading...';
showNotification(`Uploading ${files.length} file(s)...`, 'info', 0); // Indefinite
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
try {
const response = await fetch('/upload_reference', {
method: 'POST',
body: formData
// Content-Type is set automatically for FormData
});
const result = await response.json();
// Clear existing notifications before showing results
notificationArea.innerHTML = '';
if (!response.ok) {
throw new Error(result.message || `Upload failed with status ${response.status}`);
}
// Process results
if (result.errors && result.errors.length > 0) {
result.errors.forEach(err => showNotification(err, 'error'));
}
if (result.uploaded_files && result.uploaded_files.length > 0) {
showNotification(`Successfully uploaded: ${result.uploaded_files.join(', ')}`, 'success');
} else if (!result.errors || result.errors.length === 0) {
showNotification("Files processed, but no new files were added (might already exist).", 'info');
}
// Update dropdown
const currentSelection = cloneReferenceSelect.value;
cloneReferenceSelect.innerHTML = '<option value="none">-- Select Reference File --</option>'; // Clear existing options
result.all_reference_files.forEach(filename => {
const option = document.createElement('option');
option.value = filename;
option.textContent = filename;
cloneReferenceSelect.appendChild(option);
});
// Select the first newly uploaded file, or keep current selection if still valid
const firstUploaded = result.uploaded_files ? result.uploaded_files[0] : null;
if (firstUploaded) {
cloneReferenceSelect.value = firstUploaded;
} else if (result.all_reference_files.includes(currentSelection)) {
cloneReferenceSelect.value = currentSelection; // Restore previous valid selection
} else {
cloneReferenceSelect.value = 'none'; // Default if nothing else matches
}
} catch (error) {
console.error('Error uploading reference files:', error);
showNotification(`Upload Error: ${error.message}`, 'error');
} finally {
cloneLoadButton.disabled = false;
cloneLoadButton.textContent = 'Load';
cloneFileInput.value = ''; // Reset file input
}
});
}
// --- Theme Toggle ---
function applyTheme(theme) {
if (theme === 'light') {
document.documentElement.classList.remove('dark');
if (themeIconLight) themeIconLight.classList.remove('hidden');
if (themeIconDark) themeIconDark.classList.add('hidden');
} else {
document.documentElement.classList.add('dark');
if (themeIconLight) themeIconLight.classList.add('hidden');
if (themeIconDark) themeIconDark.classList.remove('hidden');
}
// Update wavesurfer colors if it exists
if (wavesurfer) {
wavesurfer.setOptions({
waveColor: theme === 'light' ? '#0ea5e9' : '#38bdf8',
progressColor: theme === 'light' ? '#0369a1' : '#0284c7',
cursorColor: theme === 'light' ? '#9333ea' : '#a855f7',
});
}
}
if (themeToggleButton) {
// Check localStorage on load
const savedTheme = localStorage.getItem('theme') || 'dark'; // Default to dark
applyTheme(savedTheme);
themeToggleButton.addEventListener('click', () => {
const isDark = document.documentElement.classList.contains('dark');
const newTheme = isDark ? 'light' : 'dark';
applyTheme(newTheme);
localStorage.setItem('theme', newTheme); // Save preference
});
}
}); // End DOMContentLoaded