/** * 🏷️ Widget UI Builder * Builds all UI sections for the string multiline tag editor * Extracted for modularity - preserves 100% of original logic */ import { TagUtilities } from "./tag-utilities.js"; import { loadSupportedLanguages } from "./language-constants.js"; // Load supported languages into memory on module load loadSupportedLanguages(); export function buildHistorySection(state, storageKey) { const historySection = document.createElement("div"); historySection.style.marginBottom = "8px"; historySection.style.paddingBottom = "8px"; historySection.style.borderBottom = "1px solid #444"; const historyLabel = document.createElement("div"); historyLabel.textContent = "History"; historyLabel.style.fontWeight = "bold"; historyLabel.style.marginBottom = "5px"; historyLabel.style.fontSize = "11px"; const historyControls = document.createElement("div"); historyControls.style.display = "flex"; historyControls.style.gap = "5px"; historyControls.style.marginBottom = "5px"; const undoBtn = document.createElement("button"); undoBtn.textContent = "↶"; undoBtn.title = "Undo (Alt+Z)"; undoBtn.style.flex = "1"; undoBtn.style.padding = "4px"; undoBtn.style.cursor = "pointer"; undoBtn.style.fontSize = "12px"; undoBtn.style.background = "#3a3a3a"; undoBtn.style.color = "#eee"; undoBtn.style.border = "1px solid #555"; undoBtn.style.borderRadius = "2px"; const redoBtn = document.createElement("button"); redoBtn.textContent = "↷"; redoBtn.title = "Redo (Alt+Shift+Z)"; redoBtn.style.flex = "1"; redoBtn.style.padding = "4px"; redoBtn.style.cursor = "pointer"; redoBtn.style.fontSize = "12px"; redoBtn.style.background = "#3a3a3a"; redoBtn.style.color = "#eee"; redoBtn.style.border = "1px solid #555"; redoBtn.style.borderRadius = "2px"; const historyStatus = document.createElement("div"); historyStatus.style.fontSize = "10px"; historyStatus.style.textAlign = "center"; historyStatus.style.color = "#999"; historyControls.appendChild(undoBtn); historyControls.appendChild(redoBtn); historySection.appendChild(historyLabel); historySection.appendChild(historyControls); historySection.appendChild(historyStatus); return { historySection, undoBtn, redoBtn, historyStatus }; } export function buildCharacterSection(state, storageKey, onManageAliases) { const charSection = document.createElement("div"); charSection.style.marginBottom = "8px"; charSection.style.paddingBottom = "8px"; charSection.style.borderBottom = "1px solid #444"; const charLabel = document.createElement("div"); charLabel.textContent = "Character"; charLabel.style.fontWeight = "bold"; charLabel.style.marginBottom = "5px"; charLabel.style.fontSize = "11px"; charLabel.style.cursor = "pointer"; charLabel.style.outline = "none"; charLabel.style.transition = "color 120ms ease"; charLabel.tabIndex = 0; charLabel.setAttribute("role", "button"); charLabel.setAttribute("aria-label", "Manage character aliases"); charLabel.title = "Manage character aliases"; const setLabelActive = active => { charLabel.style.color = active ? "#38bdf8" : ""; }; charLabel.addEventListener("mouseenter", () => setLabelActive(true)); charLabel.addEventListener("mouseleave", () => { if (document.activeElement !== charLabel) setLabelActive(false); }); charLabel.addEventListener("focus", () => setLabelActive(true)); charLabel.addEventListener("blur", () => setLabelActive(false)); charLabel.addEventListener("click", () => onManageAliases?.()); charLabel.addEventListener("keydown", event => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); onManageAliases?.(); }); const charSelect = document.createElement("select"); charSelect.style.width = "100%"; charSelect.style.marginBottom = "4px"; charSelect.style.padding = "3px"; charSelect.style.fontSize = "10px"; charSelect.style.background = "#2a2a2a"; charSelect.style.color = "#eee"; charSelect.style.border = "1px solid #444"; charSelect.innerHTML = ""; const populateCharacters = async (forceApi = false) => { try { const previousValue = charSelect.value; let characters = []; if (!forceApi && state.discoveredCharacters && typeof state.discoveredCharacters === 'object') { if (!Array.isArray(state.discoveredCharacters)) { characters = Object.keys(state.discoveredCharacters); } else { characters = state.discoveredCharacters; } } if (characters.length === 0) { try { const response = await fetch("/api/tts-audio-suite/available-characters"); if (response.ok) { const data = await response.json(); if (data.characters && Array.isArray(data.characters)) { characters = data.characters; } } } catch (err) {} } if (characters.length === 0) { characters = ["Alice", "Bob", "Charlie", "Diana", "Emma", "Frank", "Grace", "Henry"]; } charSelect.innerHTML = ""; characters.forEach(char => { const option = document.createElement("option"); option.value = char; option.textContent = char; charSelect.appendChild(option); }); if (characters.includes(previousValue)) charSelect.value = previousValue; console.log(`✅ Loaded ${characters.length} character voices`); } catch (err) { console.warn("Could not populate characters:", err); } }; populateCharacters(); const charInput = document.createElement("input"); charInput.type = "text"; charInput.placeholder = "Custom"; charInput.style.width = "100%"; charInput.style.marginBottom = "4px"; charInput.style.padding = "3px"; charInput.style.fontSize = "10px"; charInput.style.background = "#2a2a2a"; charInput.style.color = "#eee"; charInput.style.border = "1px solid #444"; charInput.value = state.lastCharacter; charInput.addEventListener("change", () => { state.lastCharacter = charInput.value; state.saveToLocalStorage(storageKey); }); const addCharBtn = document.createElement("button"); addCharBtn.textContent = "Add Char"; addCharBtn.title = "Insert selected character tag at cursor position"; addCharBtn.style.width = "100%"; addCharBtn.style.padding = "4px"; addCharBtn.style.cursor = "pointer"; addCharBtn.style.fontSize = "10px"; addCharBtn.style.background = "#3a3a3a"; addCharBtn.style.color = "#eee"; addCharBtn.style.border = "1px solid #555"; addCharBtn.style.borderRadius = "2px"; charSection.appendChild(charLabel); charSection.appendChild(charSelect); charSection.appendChild(charInput); charSection.appendChild(addCharBtn); return { charSection, charSelect, charInput, addCharBtn, refreshCharacters: () => populateCharacters(true), }; } export function buildLanguageSection(state, storageKey) { const langSection = document.createElement("div"); langSection.style.marginBottom = "8px"; langSection.style.paddingBottom = "8px"; langSection.style.borderBottom = "1px solid #444"; const langLabel = document.createElement("div"); langLabel.textContent = "Language"; langLabel.style.fontWeight = "bold"; langLabel.style.marginBottom = "5px"; langLabel.style.fontSize = "11px"; const langSelect = document.createElement("select"); langSelect.style.width = "100%"; langSelect.style.marginBottom = "4px"; langSelect.style.padding = "3px"; langSelect.style.fontSize = "10px"; langSelect.style.background = "#2a2a2a"; langSelect.style.color = "#eee"; langSelect.style.border = "1px solid #444"; langSelect.innerHTML = ""; const populateLanguages = async () => { try { const response = await fetch("/api/tts-audio-suite/available-languages"); if (response.ok) { const data = await response.json(); if (data.languages && Array.isArray(data.languages)) { langSelect.innerHTML = ""; data.languages.forEach(lang => { const option = document.createElement("option"); option.value = lang; option.textContent = lang.toUpperCase(); langSelect.appendChild(option); }); // Restore saved language AFTER options are populated langSelect.value = state.lastLanguage; console.log(`✅ Loaded ${data.languages.length} language codes`); } } } catch (err) { console.warn("Could not load languages from API, using fallback:", err); // Fallback to hardcoded list const fallbackLanguages = ["en", "de", "fr", "ja", "es", "it", "pt", "th", "no"]; langSelect.innerHTML = ""; fallbackLanguages.forEach(lang => { const option = document.createElement("option"); option.value = lang; option.textContent = lang.toUpperCase(); langSelect.appendChild(option); }); // Restore saved language AFTER options are populated langSelect.value = state.lastLanguage; } }; populateLanguages(); langSelect.addEventListener("change", () => { state.lastLanguage = langSelect.value; state.saveToLocalStorage(storageKey); }); const addLangBtn = document.createElement("button"); addLangBtn.textContent = "Add Language Tag"; addLangBtn.title = "Insert language tag [lang:] or [lang:Character] at cursor"; addLangBtn.style.width = "100%"; addLangBtn.style.padding = "4px"; addLangBtn.style.cursor = "pointer"; addLangBtn.style.fontSize = "10px"; addLangBtn.style.background = "#3a3a3a"; addLangBtn.style.color = "#eee"; addLangBtn.style.border = "1px solid #555"; addLangBtn.style.borderRadius = "2px"; langSection.appendChild(langLabel); langSection.appendChild(langSelect); langSection.appendChild(addLangBtn); return { langSection, langSelect, addLangBtn }; } export function buildValidationSection() { const validSection = document.createElement("div"); const formatBtn = document.createElement("button"); formatBtn.textContent = "Format"; formatBtn.title = "Normalize spacing and structure of tags and text"; formatBtn.style.width = "100%"; formatBtn.style.padding = "4px"; formatBtn.style.marginBottom = "4px"; formatBtn.style.cursor = "pointer"; formatBtn.style.fontSize = "10px"; formatBtn.style.background = "#3a3a3a"; formatBtn.style.color = "#eee"; formatBtn.style.border = "1px solid #555"; formatBtn.style.borderRadius = "2px"; const validateBtn = document.createElement("button"); validateBtn.textContent = "Validate"; validateBtn.title = "Check tag syntax for errors and issues"; validateBtn.style.width = "100%"; validateBtn.style.padding = "4px"; validateBtn.style.cursor = "pointer"; validateBtn.style.fontSize = "10px"; validateBtn.style.background = "#3a3a3a"; validateBtn.style.color = "#eee"; validateBtn.style.border = "1px solid #555"; validateBtn.style.borderRadius = "2px"; validSection.appendChild(formatBtn); validSection.appendChild(validateBtn); return { validSection, formatBtn, validateBtn }; }