diff --git a/frontend/js/app.js b/frontend/js/app.js index f2506aa..a0d9981 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,6 +1,64 @@ import { getSpeakers, addSpeaker, deleteSpeaker, generateDialog } from './api.js'; import { API_BASE_URL, API_BASE_URL_FOR_FILES } from './config.js'; +// --- Global Inline Notification Helpers --- // +const noticeEl = document.getElementById('global-notice'); +const noticeContentEl = document.getElementById('global-notice-content'); +const noticeActionsEl = document.getElementById('global-notice-actions'); +const noticeCloseBtn = document.getElementById('global-notice-close'); + +function hideNotice() { + if (!noticeEl) return; + noticeEl.style.display = 'none'; + noticeEl.className = 'notice'; + if (noticeContentEl) noticeContentEl.textContent = ''; + if (noticeActionsEl) noticeActionsEl.innerHTML = ''; +} + +function showNotice(message, type = 'info', options = {}) { + if (!noticeEl || !noticeContentEl || !noticeActionsEl) { + console[type === 'error' ? 'error' : 'log']('[NOTICE]', message); + return () => {}; + } + const { timeout = null, actions = [] } = options; + noticeEl.className = `notice notice--${type}`; + noticeContentEl.textContent = message; + noticeActionsEl.innerHTML = ''; + + actions.forEach(({ text, primary = false, onClick }) => { + const btn = document.createElement('button'); + btn.textContent = text; + if (primary) btn.classList.add('btn-primary'); + btn.onclick = () => { + try { onClick && onClick(); } finally { hideNotice(); } + }; + noticeActionsEl.appendChild(btn); + }); + + if (noticeCloseBtn) noticeCloseBtn.onclick = hideNotice; + noticeEl.style.display = 'flex'; + + let timerId = null; + if (timeout && Number.isFinite(timeout)) { + timerId = window.setTimeout(hideNotice, timeout); + } + return () => { + if (timerId) window.clearTimeout(timerId); + hideNotice(); + }; +} + +function confirmAction(message) { + return new Promise((resolve) => { + showNotice(message, 'warning', { + actions: [ + { text: 'Cancel', primary: false, onClick: () => resolve(false) }, + { text: 'Confirm', primary: true, onClick: () => resolve(true) }, + ], + }); + }); +} + document.addEventListener('DOMContentLoaded', async () => { console.log('DOM fully loaded and parsed'); initializeSpeakerManagement(); @@ -23,18 +81,24 @@ function initializeSpeakerManagement() { const audioFile = formData.get('audio_file'); if (!speakerName || !audioFile || audioFile.size === 0) { - alert('Please provide a speaker name and an audio file.'); + showNotice('Please provide a speaker name and an audio file.', 'warning', { timeout: 4000 }); return; } try { + const submitBtn = addSpeakerForm.querySelector('button[type="submit"]'); + const prevText = submitBtn ? submitBtn.textContent : null; + if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Adding…'; } const newSpeaker = await addSpeaker(formData); - alert(`Speaker added: ${newSpeaker.name} (ID: ${newSpeaker.id})`); + showNotice(`Speaker added: ${newSpeaker.name} (ID: ${newSpeaker.id})`, 'success', { timeout: 3000 }); addSpeakerForm.reset(); loadSpeakers(); // Refresh speaker list } catch (error) { console.error('Failed to add speaker:', error); - alert('Error adding speaker: ' + error.message); + showNotice('Error adding speaker: ' + error.message, 'error'); + } finally { + const submitBtn = addSpeakerForm.querySelector('button[type="submit"]'); + if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Add Speaker'; } } }); } @@ -79,23 +143,24 @@ async function loadSpeakers() { } catch (error) { console.error('Failed to load speakers:', error); speakerListUL.innerHTML = '
  • Error loading speakers. See console for details.
  • '; - alert('Error loading speakers: ' + error.message); + showNotice('Error loading speakers: ' + error.message, 'error'); } } async function handleDeleteSpeaker(speakerId) { if (!speakerId) { - alert('Cannot delete speaker: Speaker ID is missing.'); + showNotice('Cannot delete speaker: Speaker ID is missing.', 'warning', { timeout: 4000 }); return; } - if (!confirm(`Are you sure you want to delete speaker ${speakerId}?`)) return; + const ok = await confirmAction(`Are you sure you want to delete speaker ${speakerId}?`); + if (!ok) return; try { await deleteSpeaker(speakerId); - alert(`Speaker ${speakerId} deleted successfully.`); + showNotice(`Speaker ${speakerId} deleted successfully.`, 'success', { timeout: 3000 }); loadSpeakers(); // Refresh speaker list } catch (error) { console.error(`Failed to delete speaker ${speakerId}:`, error); - alert(`Error deleting speaker: ${error.message}`); + showNotice(`Error deleting speaker: ${error.message}`, 'error'); } } @@ -318,7 +383,7 @@ async function initializeDialogEditor() { } catch (err) { console.error('Error in generateLine:', err); dialogItems[index].error = err.message || 'Failed to generate audio.'; - alert(dialogItems[index].error); + showNotice(dialogItems[index].error, 'error'); } finally { dialogItems[index].isGenerating = false; renderDialogItems(); @@ -380,13 +445,13 @@ async function initializeDialogEditor() { try { availableSpeakersCache = await getSpeakers(); } catch (error) { - alert('Could not load speakers. Please try again.'); + showNotice('Could not load speakers. Please try again.', 'error'); console.error('Error fetching speakers for dialog:', error); return; } } if (availableSpeakersCache.length === 0) { - alert('No speakers available. Please add a speaker first.'); + showNotice('No speakers available. Please add a speaker first.', 'warning', { timeout: 4000 }); return; } @@ -416,7 +481,7 @@ async function initializeDialogEditor() { const speakerId = speakerSelect.value; const text = textInput.value.trim(); if (!speakerId || !text) { - alert('Please select a speaker and enter text.'); + showNotice('Please select a speaker and enter text.', 'warning', { timeout: 4000 }); return; } dialogItems.push(normalizeDialogItem({ type: 'speech', speaker_id: speakerId, text: text })); @@ -458,7 +523,7 @@ async function initializeDialogEditor() { addButton.onclick = () => { const duration = parseFloat(durationInput.value); if (isNaN(duration) || duration <= 0) { - alert('Invalid duration. Please enter a positive number.'); + showNotice('Invalid duration. Please enter a positive number.', 'warning', { timeout: 4000 }); return; } dialogItems.push(normalizeDialogItem({ type: 'silence', duration: duration })); @@ -483,15 +548,18 @@ async function initializeDialogEditor() { generateDialogBtn.addEventListener('click', async () => { const outputBaseName = outputBaseNameInput.value.trim(); if (!outputBaseName) { - alert('Please enter an output base name.'); + showNotice('Please enter an output base name.', 'warning', { timeout: 4000 }); outputBaseNameInput.focus(); return; } if (dialogItems.length === 0) { - alert('Please add at least one speech or silence line to the dialog.'); + showNotice('Please add at least one speech or silence line to the dialog.', 'warning', { timeout: 4000 }); return; // Prevent further execution if no dialog items } + const prevText = generateDialogBtn.textContent; + generateDialogBtn.disabled = true; + generateDialogBtn.textContent = 'Generating…'; // Smart dialog-wide generation: use pre-generated audio where present const dialogItemsToGenerate = dialogItems.map(item => { // Only send minimal fields for items that need generation @@ -543,7 +611,11 @@ async function initializeDialogEditor() { } catch (error) { console.error('Dialog generation failed:', error); if (generationLogPre) generationLogPre.textContent = `Error generating dialog: ${error.message}`; - alert(`Error generating dialog: ${error.message}`); + showNotice(`Error generating dialog: ${error.message}`, 'error'); + } + finally { + generateDialogBtn.disabled = false; + generateDialogBtn.textContent = prevText; } }); } @@ -551,7 +623,7 @@ async function initializeDialogEditor() { // --- Save/Load Script Functionality --- function saveDialogScript() { if (dialogItems.length === 0) { - alert('No dialog items to save. Please add some speech or silence lines first.'); + showNotice('No dialog items to save. Please add some speech or silence lines first.', 'warning', { timeout: 4000 }); return; } @@ -596,11 +668,12 @@ async function initializeDialogEditor() { URL.revokeObjectURL(url); console.log(`Dialog script saved as ${filename}`); + showNotice(`Dialog script saved as ${filename}`, 'success', { timeout: 3000 }); } function loadDialogScript(file) { if (!file) { - alert('Please select a file to load.'); + showNotice('Please select a file to load.', 'warning', { timeout: 4000 }); return; } @@ -623,19 +696,19 @@ async function initializeDialogEditor() { } } catch (parseError) { console.error(`Error parsing line ${i + 1}:`, parseError); - alert(`Error parsing line ${i + 1}: ${parseError.message}`); + showNotice(`Error parsing line ${i + 1}: ${parseError.message}`, 'error'); return; } } if (loadedItems.length === 0) { - alert('No valid dialog items found in the file.'); + showNotice('No valid dialog items found in the file.', 'warning', { timeout: 4000 }); return; } // Confirm replacement if existing items if (dialogItems.length > 0) { - const confirmed = confirm( + const confirmed = await confirmAction( `This will replace your current dialog (${dialogItems.length} items) with the loaded script (${loadedItems.length} items). Continue?` ); if (!confirmed) return; @@ -647,7 +720,7 @@ async function initializeDialogEditor() { availableSpeakersCache = await getSpeakers(); } catch (error) { console.error('Error fetching speakers:', error); - alert('Could not load speakers. Dialog loaded but speaker names may not display correctly.'); + showNotice('Could not load speakers. Dialog loaded but speaker names may not display correctly.', 'warning', { timeout: 5000 }); } } @@ -656,16 +729,16 @@ async function initializeDialogEditor() { renderDialogItems(); console.log(`Loaded ${loadedItems.length} dialog items from script`); - alert(`Successfully loaded ${loadedItems.length} dialog items.`); + showNotice(`Successfully loaded ${loadedItems.length} dialog items.`, 'success', { timeout: 3000 }); } catch (error) { console.error('Error loading dialog script:', error); - alert(`Error loading dialog script: ${error.message}`); + showNotice(`Error loading dialog script: ${error.message}`, 'error'); } }; reader.onerror = function() { - alert('Error reading file. Please try again.'); + showNotice('Error reading file. Please try again.', 'error'); }; reader.readAsText(file);