import { getSpeakers, addSpeaker, deleteSpeaker, generateDialog } from './api.js'; import { API_BASE_URL, API_BASE_URL_FOR_FILES } from './config.js'; document.addEventListener('DOMContentLoaded', async () => { console.log('DOM fully loaded and parsed'); initializeSpeakerManagement(); await initializeDialogEditor(); // Now properly awaiting the async function initializeResultsDisplay(); // Placeholder for now }); // --- Speaker Management --- // const speakerListUL = document.getElementById('speaker-list'); const addSpeakerForm = document.getElementById('add-speaker-form'); function initializeSpeakerManagement() { loadSpeakers(); if (addSpeakerForm) { addSpeakerForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(addSpeakerForm); const speakerName = formData.get('name'); const audioFile = formData.get('audio_file'); if (!speakerName || !audioFile || audioFile.size === 0) { alert('Please provide a speaker name and an audio file.'); return; } try { const newSpeaker = await addSpeaker(formData); alert(`Speaker added: ${newSpeaker.name} (ID: ${newSpeaker.id})`); addSpeakerForm.reset(); loadSpeakers(); // Refresh speaker list } catch (error) { console.error('Failed to add speaker:', error); alert('Error adding speaker: ' + error.message); } }); } } async function loadSpeakers() { if (!speakerListUL) return; try { const speakers = await getSpeakers(); speakerListUL.innerHTML = ''; // Clear existing list if (speakers.length === 0) { const listItem = document.createElement('li'); listItem.textContent = 'No speakers available.'; speakerListUL.appendChild(listItem); return; } speakers.forEach(speaker => { const listItem = document.createElement('li'); // Create a container for the speaker name and delete button const container = document.createElement('div'); container.style.display = 'flex'; container.style.justifyContent = 'space-between'; container.style.alignItems = 'center'; container.style.width = '100%'; // Add speaker name const nameSpan = document.createElement('span'); nameSpan.textContent = speaker.name; container.appendChild(nameSpan); // Add delete button const deleteBtn = document.createElement('button'); deleteBtn.textContent = 'Delete'; deleteBtn.classList.add('delete-speaker-btn'); deleteBtn.onclick = () => handleDeleteSpeaker(speaker.id); container.appendChild(deleteBtn); listItem.appendChild(container); speakerListUL.appendChild(listItem); }); } catch (error) { console.error('Failed to load speakers:', error); speakerListUL.innerHTML = '
  • Error loading speakers. See console for details.
  • '; alert('Error loading speakers: ' + error.message); } } async function handleDeleteSpeaker(speakerId) { if (!speakerId) { alert('Cannot delete speaker: Speaker ID is missing.'); return; } if (!confirm(`Are you sure you want to delete speaker ${speakerId}?`)) return; try { await deleteSpeaker(speakerId); alert(`Speaker ${speakerId} deleted successfully.`); loadSpeakers(); // Refresh speaker list } catch (error) { console.error(`Failed to delete speaker ${speakerId}:`, error); alert(`Error deleting speaker: ${error.message}`); } } // --- Dialog Editor --- // let dialogItems = []; // Holds the sequence of speech/silence items let availableSpeakersCache = []; // To populate speaker dropdown // Utility: ensure each dialog item has audioUrl, isGenerating, error function normalizeDialogItem(item) { const normalized = { ...item, audioUrl: item.audioUrl || null, isGenerating: item.isGenerating || false, error: item.error || null }; // Add TTS settings for speech items with defaults if (item.type === 'speech') { normalized.exaggeration = item.exaggeration ?? 0.5; normalized.cfg_weight = item.cfg_weight ?? 0.5; normalized.temperature = item.temperature ?? 0.8; } return normalized; } async function initializeDialogEditor() { const dialogItemsContainer = document.getElementById('dialog-items-container'); const addSpeechLineBtn = document.getElementById('add-speech-line-btn'); const addSilenceLineBtn = document.getElementById('add-silence-line-btn'); const outputBaseNameInput = document.getElementById('output-base-name'); const generateDialogBtn = document.getElementById('generate-dialog-btn'); const saveScriptBtn = document.getElementById('save-script-btn'); const loadScriptBtn = document.getElementById('load-script-btn'); const loadScriptInput = document.getElementById('load-script-input'); // Results Display Elements const generationLogPre = document.getElementById('generation-log-content'); // Corrected ID const audioPlayer = document.getElementById('concatenated-audio-player'); // Corrected ID // audioSource will be the audioPlayer itself, no separate element by default in the HTML const downloadZipLink = document.getElementById('zip-archive-link'); // Corrected ID const zipArchivePlaceholder = document.getElementById('zip-archive-placeholder'); const resultsDisplaySection = document.getElementById('results-display'); // Load speakers at startup try { availableSpeakersCache = await getSpeakers(); console.log(`Loaded ${availableSpeakersCache.length} speakers for dialog editor`); } catch (error) { console.error('Error loading speakers at startup:', error); // Continue without speakers - they'll be loaded when needed } // Function to render the current dialogItems array to the DOM as table rows function renderDialogItems() { if (!dialogItemsContainer) return; dialogItemsContainer.innerHTML = ''; dialogItems = dialogItems.map(normalizeDialogItem); // Ensure all fields present dialogItems.forEach((item, index) => { const tr = document.createElement('tr'); // Type column const typeTd = document.createElement('td'); typeTd.classList.add('type-icon-cell'); if (item.type === 'speech') { typeTd.innerHTML = '🗣️'; } else { typeTd.innerHTML = '🤫'; } tr.appendChild(typeTd); // Speaker column const speakerTd = document.createElement('td'); if (item.type === 'speech') { const speakerSelect = document.createElement('select'); speakerSelect.className = 'dialog-speaker-select'; availableSpeakersCache.forEach(speaker => { const option = document.createElement('option'); option.value = speaker.id; option.textContent = speaker.name; if (speaker.id === item.speaker_id) option.selected = true; speakerSelect.appendChild(option); }); speakerSelect.onchange = (e) => { dialogItems[index].speaker_id = e.target.value; }; speakerTd.appendChild(speakerSelect); } else { speakerTd.textContent = '—'; } tr.appendChild(speakerTd); // Text/Duration column const textTd = document.createElement('td'); textTd.className = 'dialog-editable-cell'; if (item.type === 'speech') { let txt = item.text.length > 60 ? item.text.substring(0, 57) + '…' : item.text; textTd.textContent = `"${txt}"`; textTd.title = item.text; } else { textTd.textContent = `${item.duration}s`; } // Double-click to edit textTd.ondblclick = () => { textTd.innerHTML = ''; let input; if (item.type === 'speech') { // Use textarea for speech text to enable multi-line editing input = document.createElement('textarea'); input.className = 'dialog-edit-textarea'; input.value = item.text; input.rows = Math.max(2, Math.ceil(item.text.length / 50)); // Auto-size based on content } else { // Use number input for duration input = document.createElement('input'); input.className = 'dialog-edit-input'; input.type = 'number'; input.value = item.duration; input.min = 0.1; input.step = 0.1; } input.onblur = saveEdit; input.onkeydown = (e) => { if (e.key === 'Enter' && item.type === 'silence') { // Only auto-save on Enter for duration inputs input.blur(); } else if (e.key === 'Escape') { renderDialogItems(); } else if (e.key === 'Enter' && e.ctrlKey && item.type === 'speech') { // Ctrl+Enter to save for speech text input.blur(); } }; textTd.appendChild(input); input.focus(); function saveEdit() { if (item.type === 'speech') { dialogItems[index].text = input.value; dialogItems[index].audioUrl = null; // Invalidate audio if text edited } else { let val = parseFloat(input.value); if (!isNaN(val) && val > 0) dialogItems[index].duration = val; dialogItems[index].audioUrl = null; } renderDialogItems(); } }; tr.appendChild(textTd); // Actions column const actionsTd = document.createElement('td'); actionsTd.classList.add('actions'); // Up button const upBtn = document.createElement('button'); upBtn.innerHTML = '↑'; upBtn.title = 'Move up'; upBtn.className = 'move-up-btn'; upBtn.disabled = index === 0; upBtn.onclick = () => { if (index > 0) { [dialogItems[index - 1], dialogItems[index]] = [dialogItems[index], dialogItems[index - 1]]; renderDialogItems(); } }; actionsTd.appendChild(upBtn); // Down button const downBtn = document.createElement('button'); downBtn.innerHTML = '↓'; downBtn.title = 'Move down'; downBtn.className = 'move-down-btn'; downBtn.disabled = index === dialogItems.length - 1; downBtn.onclick = () => { if (index < dialogItems.length - 1) { [dialogItems[index], dialogItems[index + 1]] = [dialogItems[index + 1], dialogItems[index]]; renderDialogItems(); } }; actionsTd.appendChild(downBtn); // Remove button const removeBtn = document.createElement('button'); removeBtn.innerHTML = '×'; // Unicode multiplication sign (X) removeBtn.classList.add('remove-dialog-item-btn', 'x-remove-btn'); removeBtn.setAttribute('aria-label', 'Remove dialog line'); removeBtn.title = 'Remove'; removeBtn.onclick = () => { dialogItems.splice(index, 1); renderDialogItems(); }; actionsTd.appendChild(removeBtn); // --- NEW: Per-line Generate button --- const generateBtn = document.createElement('button'); generateBtn.innerHTML = '⚡'; generateBtn.title = 'Generate audio for this line'; generateBtn.className = 'generate-line-btn'; generateBtn.disabled = item.isGenerating; if (item.isGenerating) generateBtn.classList.add('loading'); generateBtn.onclick = async () => { dialogItems[index].isGenerating = true; dialogItems[index].error = null; renderDialogItems(); try { const { generateLine } = await import('./api.js'); const payload = { ...item }; // Remove fields not needed by backend delete payload.audioUrl; delete payload.isGenerating; delete payload.error; console.log('Sending payload:', payload); const result = await generateLine(payload); console.log('Received result:', result); if (result && result.audio_url) { dialogItems[index].audioUrl = result.audio_url; console.log('Set audioUrl to:', result.audio_url); } else { console.error('Invalid result structure:', result); throw new Error('Invalid response: missing audio_url'); } } catch (err) { console.error('Error in generateLine:', err); dialogItems[index].error = err.message || 'Failed to generate audio.'; alert(dialogItems[index].error); } finally { dialogItems[index].isGenerating = false; renderDialogItems(); } }; actionsTd.appendChild(generateBtn); // --- NEW: Per-line Play button --- const playBtn = document.createElement('button'); playBtn.innerHTML = '⏵'; playBtn.title = item.audioUrl ? 'Play generated audio' : 'No audio generated yet'; playBtn.className = 'play-line-btn'; playBtn.disabled = !item.audioUrl; playBtn.onclick = () => { if (!item.audioUrl) return; let audioUrl = item.audioUrl.startsWith('http') ? item.audioUrl : `${API_BASE_URL_FOR_FILES}${item.audioUrl}`; // Use a shared audio element or create one per play let audio = new window.Audio(audioUrl); audio.play(); }; actionsTd.appendChild(playBtn); // --- NEW: Settings button for speech items --- if (item.type === 'speech') { const settingsBtn = document.createElement('button'); settingsBtn.innerHTML = '⚙️'; settingsBtn.title = 'TTS Settings'; settingsBtn.className = 'settings-line-btn'; settingsBtn.onclick = () => { showTTSSettingsModal(item, index); }; actionsTd.appendChild(settingsBtn); } // Show error if present if (item.error) { const errorSpan = document.createElement('span'); errorSpan.className = 'line-error-msg'; errorSpan.textContent = item.error; actionsTd.appendChild(errorSpan); } tr.appendChild(actionsTd); dialogItemsContainer.appendChild(tr); }); } const tempInputArea = document.getElementById('temp-input-area'); function clearTempInputArea() { if (tempInputArea) tempInputArea.innerHTML = ''; } if (addSpeechLineBtn) { addSpeechLineBtn.addEventListener('click', async () => { clearTempInputArea(); // Clear any previous inputs if (availableSpeakersCache.length === 0) { try { availableSpeakersCache = await getSpeakers(); } catch (error) { alert('Could not load speakers. Please try again.'); console.error('Error fetching speakers for dialog:', error); return; } } if (availableSpeakersCache.length === 0) { alert('No speakers available. Please add a speaker first.'); return; } const speakerSelectLabel = document.createElement('label'); speakerSelectLabel.textContent = 'Speaker: '; speakerSelectLabel.htmlFor = 'temp-speaker-select'; const speakerSelect = document.createElement('select'); speakerSelect.id = 'temp-speaker-select'; availableSpeakersCache.forEach(speaker => { const option = document.createElement('option'); option.value = speaker.id; option.textContent = speaker.name; speakerSelect.appendChild(option); }); const textInputLabel = document.createElement('label'); textInputLabel.textContent = ' Text: '; textInputLabel.htmlFor = 'temp-speech-text'; const textInput = document.createElement('textarea'); textInput.id = 'temp-speech-text'; textInput.rows = 2; textInput.placeholder = 'Enter speech text'; const addButton = document.createElement('button'); addButton.textContent = 'Add Speech'; addButton.onclick = () => { const speakerId = speakerSelect.value; const text = textInput.value.trim(); if (!speakerId || !text) { alert('Please select a speaker and enter text.'); return; } dialogItems.push(normalizeDialogItem({ type: 'speech', speaker_id: speakerId, text: text })); renderDialogItems(); clearTempInputArea(); }; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.onclick = clearTempInputArea; if (tempInputArea) { tempInputArea.appendChild(speakerSelectLabel); tempInputArea.appendChild(speakerSelect); tempInputArea.appendChild(textInputLabel); tempInputArea.appendChild(textInput); tempInputArea.appendChild(addButton); tempInputArea.appendChild(cancelButton); } }); } if (addSilenceLineBtn) { addSilenceLineBtn.addEventListener('click', () => { clearTempInputArea(); // Clear any previous inputs const durationInputLabel = document.createElement('label'); durationInputLabel.textContent = 'Duration (s): '; durationInputLabel.htmlFor = 'temp-silence-duration'; const durationInput = document.createElement('input'); durationInput.type = 'number'; durationInput.id = 'temp-silence-duration'; durationInput.step = '0.1'; durationInput.min = '0.1'; durationInput.placeholder = 'e.g., 0.5'; const addButton = document.createElement('button'); addButton.textContent = 'Add Silence'; addButton.onclick = () => { const duration = parseFloat(durationInput.value); if (isNaN(duration) || duration <= 0) { alert('Invalid duration. Please enter a positive number.'); return; } dialogItems.push(normalizeDialogItem({ type: 'silence', duration: duration })); renderDialogItems(); clearTempInputArea(); }; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.onclick = clearTempInputArea; if (tempInputArea) { tempInputArea.appendChild(durationInputLabel); tempInputArea.appendChild(durationInput); tempInputArea.appendChild(addButton); tempInputArea.appendChild(cancelButton); } }); } if (generateDialogBtn && outputBaseNameInput) { generateDialogBtn.addEventListener('click', async () => { const outputBaseName = outputBaseNameInput.value.trim(); if (!outputBaseName) { alert('Please enter an output base name.'); outputBaseNameInput.focus(); return; } if (dialogItems.length === 0) { alert('Please add at least one speech or silence line to the dialog.'); return; // Prevent further execution if no dialog items } // Smart dialog-wide generation: use pre-generated audio where present const dialogItemsToGenerate = dialogItems.map(item => { // Only send minimal fields for items that need generation if (item.audioUrl) { return { ...item, use_existing_audio: true, audio_url: item.audioUrl }; } else { // Remove frontend-only fields const payload = { ...item }; delete payload.audioUrl; delete payload.isGenerating; delete payload.error; return payload; } }); const payload = { output_base_name: outputBaseName, dialog_items: dialogItemsToGenerate }; try { console.log('Generating dialog with payload:', JSON.stringify(payload, null, 2)); const result = await generateDialog(payload); console.log('Dialog generation successful:', result); if (generationLogPre) generationLogPre.textContent = result.log || 'No log output.'; if (result.concatenated_audio_url && audioPlayer) { // Check audioPlayer, not audioSource // Cache-busting: append timestamp to force reload let audioUrl = result.concatenated_audio_url.startsWith('http') ? result.concatenated_audio_url : `${API_BASE_URL_FOR_FILES}${result.concatenated_audio_url}`; audioUrl += (audioUrl.includes('?') ? '&' : '?') + 't=' + Date.now(); audioPlayer.src = audioUrl; audioPlayer.load(); // Call load() after setting new source audioPlayer.style.display = 'block'; } else { if (audioPlayer) audioPlayer.style.display = 'none'; // Ensure it's hidden if no URL if (generationLogPre) generationLogPre.textContent += '\nNo concatenated audio URL found.'; } if (result.zip_archive_url && downloadZipLink) { downloadZipLink.href = result.zip_archive_url.startsWith('http') ? result.zip_archive_url : `${API_BASE_URL_FOR_FILES}${result.zip_archive_url}`; downloadZipLink.textContent = `Download ${outputBaseName}.zip`; downloadZipLink.style.display = 'block'; if (zipArchivePlaceholder) zipArchivePlaceholder.style.display = 'none'; // Hide placeholder } else { if (downloadZipLink) downloadZipLink.style.display = 'none'; if (zipArchivePlaceholder) zipArchivePlaceholder.style.display = 'block'; // Show placeholder if no link if (generationLogPre) generationLogPre.textContent += '\nNo ZIP archive URL found.'; } } catch (error) { console.error('Dialog generation failed:', error); if (generationLogPre) generationLogPre.textContent = `Error generating dialog: ${error.message}`; alert(`Error generating dialog: ${error.message}`); } }); } // --- Save/Load Script Functionality --- function saveDialogScript() { if (dialogItems.length === 0) { alert('No dialog items to save. Please add some speech or silence lines first.'); return; } // Filter out UI-specific fields and create clean data for export const exportData = dialogItems.map(item => { const cleanItem = { type: item.type }; if (item.type === 'speech') { cleanItem.speaker_id = item.speaker_id; cleanItem.text = item.text; // Include TTS parameters if they exist (will use defaults if not present) if (item.exaggeration !== undefined) cleanItem.exaggeration = item.exaggeration; if (item.cfg_weight !== undefined) cleanItem.cfg_weight = item.cfg_weight; if (item.temperature !== undefined) cleanItem.temperature = item.temperature; } else if (item.type === 'silence') { cleanItem.duration = item.duration; } return cleanItem; }); // Convert to JSONL format (one JSON object per line) const jsonlContent = exportData.map(item => JSON.stringify(item)).join('\n'); // Create and download file const blob = new Blob([jsonlContent], { type: 'application/jsonl' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); // Generate filename with timestamp const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const filename = `dialog_script_${timestamp}.jsonl`; link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); console.log(`Dialog script saved as ${filename}`); } function loadDialogScript(file) { if (!file) { alert('Please select a file to load.'); return; } const reader = new FileReader(); reader.onload = async function(e) { try { const content = e.target.result; const lines = content.trim().split('\n'); const loadedItems = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // Skip empty lines try { const item = JSON.parse(line); const validatedItem = validateDialogItem(item, i + 1); if (validatedItem) { loadedItems.push(normalizeDialogItem(validatedItem)); } } catch (parseError) { console.error(`Error parsing line ${i + 1}:`, parseError); alert(`Error parsing line ${i + 1}: ${parseError.message}`); return; } } if (loadedItems.length === 0) { alert('No valid dialog items found in the file.'); return; } // Confirm replacement if existing items if (dialogItems.length > 0) { const confirmed = confirm( `This will replace your current dialog (${dialogItems.length} items) with the loaded script (${loadedItems.length} items). Continue?` ); if (!confirmed) return; } // Ensure speakers are loaded before rendering if (availableSpeakersCache.length === 0) { try { 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.'); } } // Replace current dialog dialogItems.splice(0, dialogItems.length, ...loadedItems); renderDialogItems(); console.log(`Loaded ${loadedItems.length} dialog items from script`); alert(`Successfully loaded ${loadedItems.length} dialog items.`); } catch (error) { console.error('Error loading dialog script:', error); alert(`Error loading dialog script: ${error.message}`); } }; reader.onerror = function() { alert('Error reading file. Please try again.'); }; reader.readAsText(file); } function validateDialogItem(item, lineNumber) { if (!item || typeof item !== 'object') { throw new Error(`Line ${lineNumber}: Invalid item format`); } if (!item.type || !['speech', 'silence'].includes(item.type)) { throw new Error(`Line ${lineNumber}: Invalid or missing type. Must be 'speech' or 'silence'`); } if (item.type === 'speech') { if (!item.speaker_id || typeof item.speaker_id !== 'string') { throw new Error(`Line ${lineNumber}: Speech items must have a valid speaker_id`); } if (!item.text || typeof item.text !== 'string') { throw new Error(`Line ${lineNumber}: Speech items must have text`); } // Check if speaker exists in available speakers const speakerExists = availableSpeakersCache.some(speaker => speaker.id === item.speaker_id); if (availableSpeakersCache.length > 0 && !speakerExists) { console.warn(`Line ${lineNumber}: Speaker '${item.speaker_id}' not found in available speakers`); // Don't throw error, just warn - speaker might be added later } return { type: 'speech', speaker_id: item.speaker_id, text: item.text }; } else if (item.type === 'silence') { if (typeof item.duration !== 'number' || item.duration <= 0) { throw new Error(`Line ${lineNumber}: Silence items must have a positive duration number`); } return { type: 'silence', duration: item.duration }; } } // Event handlers for save/load if (saveScriptBtn) { saveScriptBtn.addEventListener('click', saveDialogScript); } if (loadScriptBtn && loadScriptInput) { loadScriptBtn.addEventListener('click', () => { loadScriptInput.click(); }); loadScriptInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { loadDialogScript(file); // Reset input so same file can be loaded again e.target.value = ''; } }); } console.log('Dialog Editor Initialized'); renderDialogItems(); // Initial render (empty) // Add a function to show TTS settings modal function showTTSSettingsModal(item, index) { const modal = document.getElementById('tts-settings-modal'); const exaggerationSlider = document.getElementById('tts-exaggeration'); const exaggerationValue = document.getElementById('tts-exaggeration-value'); const cfgWeightSlider = document.getElementById('tts-cfg-weight'); const cfgWeightValue = document.getElementById('tts-cfg-weight-value'); const temperatureSlider = document.getElementById('tts-temperature'); const temperatureValue = document.getElementById('tts-temperature-value'); const saveBtn = document.getElementById('tts-settings-save'); const cancelBtn = document.getElementById('tts-settings-cancel'); const closeBtn = document.getElementById('tts-modal-close'); // Set current values exaggerationSlider.value = item.exaggeration || 0.5; exaggerationValue.textContent = exaggerationSlider.value; cfgWeightSlider.value = item.cfg_weight || 0.5; cfgWeightValue.textContent = cfgWeightSlider.value; temperatureSlider.value = item.temperature || 0.8; temperatureValue.textContent = temperatureSlider.value; // Update value displays when sliders change const updateValueDisplay = (slider, display) => { display.textContent = slider.value; }; exaggerationSlider.oninput = () => updateValueDisplay(exaggerationSlider, exaggerationValue); cfgWeightSlider.oninput = () => updateValueDisplay(cfgWeightSlider, cfgWeightValue); temperatureSlider.oninput = () => updateValueDisplay(temperatureSlider, temperatureValue); // Show modal modal.style.display = 'flex'; // Save settings const saveSettings = () => { dialogItems[index].exaggeration = parseFloat(exaggerationSlider.value); dialogItems[index].cfg_weight = parseFloat(cfgWeightSlider.value); dialogItems[index].temperature = parseFloat(temperatureSlider.value); // Clear any existing audio since settings changed dialogItems[index].audioUrl = null; closeModal(); renderDialogItems(); // Re-render to reflect changes console.log('TTS settings updated for item:', dialogItems[index]); }; // Close modal const closeModal = () => { modal.style.display = 'none'; // Clean up event listeners exaggerationSlider.oninput = null; cfgWeightSlider.oninput = null; temperatureSlider.oninput = null; saveBtn.onclick = null; cancelBtn.onclick = null; closeBtn.onclick = null; modal.onclick = null; }; // Event listeners saveBtn.onclick = saveSettings; cancelBtn.onclick = closeModal; closeBtn.onclick = closeModal; // Close modal when clicking outside modal.onclick = (e) => { if (e.target === modal) { closeModal(); } }; // Close modal on Escape key const handleEscape = (e) => { if (e.key === 'Escape') { closeModal(); document.removeEventListener('keydown', handleEscape); } }; document.addEventListener('keydown', handleEscape); } } // --- Results Display --- // function initializeResultsDisplay() { const generationLogContent = document.getElementById('generation-log-content'); const concatenatedAudioPlayer = document.getElementById('concatenated-audio-player'); const zipArchiveLink = document.getElementById('zip-archive-link'); const zipArchivePlaceholder = document.getElementById('zip-archive-placeholder'); // Functions to update these elements will be called by the generateDialog handler // e.g., updateLog(message), setAudioSource(url), setZipLink(url) console.log('Results Display Initialized'); }