feat/frontend-phase1 #1

Merged
stwhite merged 34 commits from feat/frontend-phase1 into main 2025-08-14 15:44:25 +00:00
3 changed files with 171 additions and 25 deletions
Showing only changes of commit 41f95cdee3 - Show all commits

View File

@ -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;

View File

@ -13,6 +13,13 @@
</div>
</header>
<!-- Global inline notification area -->
<div id="global-notice" class="notice" role="status" aria-live="polite" style="display:none;">
<div class="notice__content" id="global-notice-content"></div>
<div class="notice__actions" id="global-notice-actions"></div>
<button class="notice__close" id="global-notice-close" aria-label="Close notification">&times;</button>
</div>
<main class="container" role="main">
<div class="panel-grid">
<section id="dialog-editor" class="panel full-width-panel" aria-labelledby="dialog-editor-title">

View File

@ -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 = '<li>Error loading speakers. See console for details.</li>';
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);