diff --git a/frontend/css/style.css b/frontend/css/style.css
index a718222..c874e1b 100644
--- a/frontend/css/style.css
+++ b/frontend/css/style.css
@@ -449,6 +449,72 @@ footer {
border-top: 3px solid var(--primary-blue);
}
+/* Inline Notification */
+.notice {
+ max-width: 1100px;
+ margin: 16px auto 0;
+ padding: 12px 16px;
+ border-radius: 6px;
+ border: 1px solid var(--border-medium);
+ background: var(--bg-white);
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ box-shadow: 0 1px 2px var(--shadow-light);
+}
+
+.notice--info {
+ border-color: var(--border-blue);
+ background: var(--bg-blue-light);
+}
+
+.notice--success {
+ border-color: #A7F3D0;
+ background: #ECFDF5;
+}
+
+.notice--warning {
+ border-color: var(--warning-border);
+ background: var(--warning-bg);
+}
+
+.notice--error {
+ border-color: var(--error-bg-dark);
+ background: #FEE2E2;
+}
+
+.notice__content {
+ flex: 1;
+}
+
+.notice__actions {
+ display: flex;
+ gap: 8px;
+}
+
+.notice__actions button {
+ padding: 6px 12px;
+ border-radius: 4px;
+ border: 1px solid var(--border-medium);
+ background: var(--bg-white);
+ cursor: pointer;
+}
+
+.notice__actions .btn-primary {
+ background: var(--primary-blue);
+ color: var(--text-white);
+ border: none;
+}
+
+.notice__close {
+ background: none;
+ border: none;
+ font-size: 18px;
+ cursor: pointer;
+ color: var(--text-secondary);
+}
+
@media (max-width: 900px) {
.panel-grid {
flex-direction: column;
diff --git a/frontend/index.html b/frontend/index.html
index b307869..065b902 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -13,6 +13,13 @@
+
+
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);