Files
abogen/abogen/webui/static/settings.js
T

883 lines
31 KiB
JavaScript

const form = document.querySelector('.settings__form');
const navButtons = Array.from(document.querySelectorAll('.settings-nav__item'));
const panels = Array.from(document.querySelectorAll('.settings-panel'));
const llmNavButton = navButtons.find((button) => button.dataset.section === 'llm');
const statusSelectors = {
llm: document.querySelector('[data-role="llm-preview-status"]'),
normalization: document.querySelector('[data-role="normalization-preview-status"]'),
calibre: document.querySelector('[data-role="calibre-test-status"]'),
audiobookshelf: document.querySelector('[data-role="audiobookshelf-test-status"]'),
};
const outputAreas = {
llm: document.querySelector('[data-role="llm-preview-output"]'),
normalization: document.querySelector('[data-role="normalization-preview-output"]'),
};
const normalizationAudio = document.querySelector('[data-role="normalization-preview-audio"]');
const folderModal = document.querySelector('[data-role="audiobookshelf-folder-modal"]');
const folderModalOverlay = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-overlay"]') : null;
const folderList = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-list"]') : null;
const folderStatusMessage = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-status"]') : null;
const folderFilter = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-filter"]') : null;
const folderEmptyState = folderModal ? folderModal.querySelector('[data-role="audiobookshelf-folder-empty"]') : null;
const defaultFolderEmptyMessage = folderEmptyState ? folderEmptyState.textContent : 'No folders match your filter.';
let folderModalOpener = null;
let folderModalPreviousFocus = null;
let audiobookshelfFolderSource = [];
const contractionModal = document.querySelector('[data-role="contraction-modal"]');
const contractionModalOverlay = contractionModal ? contractionModal.querySelector('[data-role="contraction-modal-overlay"]') : null;
let contractionModalOpener = null;
let contractionModalPreviousFocus = null;
function setStatus(target, message, state) {
if (!target) {
return;
}
target.textContent = message || '';
if (state) {
target.dataset.state = state;
} else {
delete target.dataset.state;
}
}
function clearStatus(target) {
setStatus(target, '', null);
}
function activatePanel(section) {
if (!section) {
return;
}
navButtons.forEach((button) => {
const isActive = button.dataset.section === section;
button.classList.toggle('is-active', isActive);
});
let activePanel = null;
panels.forEach((panel) => {
const isActive = panel.dataset.section === section;
panel.classList.toggle('is-active', isActive);
if (isActive) {
activePanel = panel;
}
});
if (activePanel) {
const focusable = activePanel.querySelector('input, select, textarea');
if (focusable) {
window.requestAnimationFrame(() => {
focusable.focus({ preventScroll: false });
});
}
}
}
function initNavigation() {
if (!navButtons.length || !panels.length) {
return;
}
navButtons.forEach((button) => {
button.addEventListener('click', () => {
activatePanel(button.dataset.section);
if (button.dataset.section) {
window.history.replaceState(null, '', `#${button.dataset.section}`);
}
});
});
const hash = window.location.hash.replace('#', '');
if (hash && panels.some((panel) => panel.dataset.section === hash)) {
activatePanel(hash);
} else {
const current = navButtons.find((button) => button.classList.contains('is-active'));
if (current) {
activatePanel(current.dataset.section);
}
}
window.addEventListener('hashchange', () => {
const section = window.location.hash.replace('#', '');
if (section) {
activatePanel(section);
}
});
}
function parseNumber(value, fallback) {
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
function normalizeFolderToken(value) {
return String(value || '').trim().toLowerCase();
}
function setFolderModalStatus(message, state) {
if (!folderStatusMessage) {
return;
}
folderStatusMessage.textContent = message || '';
if (state) {
folderStatusMessage.dataset.state = state;
folderStatusMessage.hidden = false;
} else {
delete folderStatusMessage.dataset.state;
folderStatusMessage.hidden = !message;
}
}
function clearFolderModalContents() {
if (folderList) {
folderList.innerHTML = '';
}
if (folderEmptyState) {
folderEmptyState.textContent = defaultFolderEmptyMessage;
folderEmptyState.hidden = true;
}
}
function openFolderModal(opener) {
if (!folderModal) {
return;
}
folderModalOpener = opener || null;
folderModalPreviousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
folderModal.hidden = false;
folderModal.dataset.open = 'true';
document.body.classList.add('modal-open');
if (folderFilter) {
folderFilter.value = '';
folderFilter.disabled = true;
}
clearFolderModalContents();
setFolderModalStatus('Loading folders...', 'loading');
}
function closeFolderModal(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (!folderModal || folderModal.hidden) {
return;
}
folderModal.dataset.open = 'false';
folderModal.hidden = true;
document.body.classList.remove('modal-open');
audiobookshelfFolderSource = [];
if (folderFilter) {
folderFilter.value = '';
folderFilter.disabled = false;
}
clearFolderModalContents();
setFolderModalStatus('', null);
const focusTarget = folderModalPreviousFocus && typeof folderModalPreviousFocus.focus === 'function'
? folderModalPreviousFocus
: folderModalOpener;
if (focusTarget && typeof focusTarget.focus === 'function') {
focusTarget.focus({ preventScroll: false });
}
folderModalPreviousFocus = null;
folderModalOpener = null;
}
function openContractionModal(opener) {
if (!contractionModal) {
return;
}
contractionModalOpener = opener || null;
contractionModalPreviousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
contractionModal.hidden = false;
contractionModal.dataset.open = 'true';
document.body.classList.add('modal-open');
const focusTarget = contractionModal.querySelector('input, button, select, textarea');
if (focusTarget instanceof HTMLElement) {
focusTarget.focus({ preventScroll: true });
}
}
function closeContractionModal(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
if (!contractionModal || contractionModal.hidden) {
return;
}
contractionModal.dataset.open = 'false';
contractionModal.hidden = true;
document.body.classList.remove('modal-open');
const focusTarget =
(contractionModalPreviousFocus && typeof contractionModalPreviousFocus.focus === 'function'
? contractionModalPreviousFocus
: contractionModalOpener) || null;
if (focusTarget && typeof focusTarget.focus === 'function') {
focusTarget.focus({ preventScroll: true });
}
contractionModalPreviousFocus = null;
contractionModalOpener = null;
}
function initContractionModal() {
if (!contractionModal) {
return;
}
const openButton = document.querySelector('[data-action="contraction-modal-open"]');
if (openButton) {
openButton.addEventListener('click', () => openContractionModal(openButton));
}
const closeButtons = contractionModal.querySelectorAll('[data-action="contraction-modal-close"]');
closeButtons.forEach((button) => {
button.addEventListener('click', closeContractionModal);
});
if (contractionModalOverlay) {
contractionModalOverlay.addEventListener('click', closeContractionModal);
}
contractionModal.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
closeContractionModal(event);
}
});
}
function renderFolderList(query) {
if (!folderList) {
return;
}
folderList.innerHTML = '';
const normalizedQuery = normalizeFolderToken(query);
const matches = audiobookshelfFolderSource.filter((entry) => {
const tokens = [
normalizeFolderToken(entry.name),
normalizeFolderToken(entry.path),
normalizeFolderToken(entry.id),
];
return !normalizedQuery || tokens.some((token) => token.includes(normalizedQuery));
});
if (!matches.length) {
if (folderEmptyState) {
folderEmptyState.textContent = normalizedQuery ? defaultFolderEmptyMessage : 'No folders found for this library.';
folderEmptyState.hidden = false;
}
return;
}
if (folderEmptyState) {
folderEmptyState.textContent = defaultFolderEmptyMessage;
folderEmptyState.hidden = true;
}
matches.forEach((entry) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'folder-picker__item';
button.setAttribute('role', 'option');
if (entry.id) {
button.dataset.folderId = entry.id;
}
const displayName = entry.name || entry.path || entry.id || 'Unnamed folder';
const nameEl = document.createElement('span');
nameEl.className = 'folder-picker__item-name';
nameEl.textContent = displayName;
button.appendChild(nameEl);
if (entry.path && (!entry.name || entry.path.toLowerCase() !== entry.name.toLowerCase())) {
const pathEl = document.createElement('span');
pathEl.className = 'folder-picker__item-path';
pathEl.textContent = entry.path;
button.appendChild(pathEl);
}
if (entry.id) {
const idEl = document.createElement('span');
idEl.className = 'folder-picker__item-id';
idEl.textContent = entry.id;
button.appendChild(idEl);
}
button.addEventListener('click', () => handleFolderSelection(entry));
folderList.appendChild(button);
});
}
function populateFolderPicker(entries) {
audiobookshelfFolderSource = Array.isArray(entries) ? entries : [];
if (!audiobookshelfFolderSource.length) {
if (folderFilter) {
folderFilter.value = '';
folderFilter.disabled = true;
}
setFolderModalStatus('No folders found for this library.', 'info');
if (folderEmptyState) {
folderEmptyState.textContent = 'No folders found for this library.';
folderEmptyState.hidden = false;
}
return;
}
if (folderFilter) {
folderFilter.disabled = false;
folderFilter.value = '';
folderFilter.focus({ preventScroll: true });
}
setFolderModalStatus('', null);
if (folderEmptyState) {
folderEmptyState.textContent = defaultFolderEmptyMessage;
folderEmptyState.hidden = true;
}
renderFolderList('');
}
function handleFolderSelection(entry) {
const folderInput = form ? form.querySelector('#audiobookshelf_folder_id') : null;
if (folderInput) {
folderInput.value = entry.id || '';
folderInput.dispatchEvent(new Event('input', { bubbles: true }));
}
closeFolderModal();
const status = statusSelectors.audiobookshelf;
if (status) {
const label = entry.name || entry.path || entry.id || 'selected folder';
setStatus(status, `Selected folder '${label}'.`, 'success');
}
}
function initFolderPicker() {
if (!folderModal) {
return;
}
const closeButtons = folderModal.querySelectorAll('[data-action="audiobookshelf-folder-close"]');
closeButtons.forEach((button) => {
button.addEventListener('click', closeFolderModal);
});
if (folderModalOverlay) {
folderModalOverlay.addEventListener('click', closeFolderModal);
}
if (folderFilter) {
folderFilter.addEventListener('input', () => renderFolderList(folderFilter.value));
}
folderModal.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
event.preventDefault();
closeFolderModal();
}
});
}
function collectLLMFields() {
const baseUrl = form.querySelector('#llm_base_url');
const apiKey = form.querySelector('#llm_api_key');
const model = form.querySelector('#llm_model');
const prompt = form.querySelector('#llm_prompt');
const timeout = form.querySelector('#llm_timeout');
const context = form.querySelector('input[name="llm_context_mode"]:checked');
return {
base_url: baseUrl ? baseUrl.value.trim() : '',
api_key: apiKey ? apiKey.value.trim() : '',
model: model ? model.value.trim() : '',
prompt: prompt ? prompt.value : '',
context_mode: context ? context.value : 'sentence',
timeout: timeout ? parseNumber(timeout.value, 30) : 30,
};
}
function updateModelOptions(models) {
const select = form.querySelector('#llm_model');
if (!select) {
return;
}
const current = select.dataset.currentModel || select.value;
select.innerHTML = '';
if (!Array.isArray(models) || !models.length) {
const option = document.createElement('option');
option.value = '';
option.textContent = 'No models found';
select.appendChild(option);
select.dataset.currentModel = '';
select.disabled = true;
return;
}
const fragment = document.createDocumentFragment();
let matchedCurrent = false;
models.forEach((entry) => {
let identifier = '';
let label = '';
if (typeof entry === 'string') {
identifier = entry;
label = entry;
} else if (entry && typeof entry === 'object') {
identifier = String(entry.id || entry.name || entry.label || '').trim();
label = String(entry.label || entry.name || identifier || '').trim();
}
if (!identifier) {
return;
}
if (!label) {
label = identifier;
}
const option = document.createElement('option');
option.value = identifier;
option.textContent = label;
if (identifier === current) {
option.selected = true;
matchedCurrent = true;
}
fragment.appendChild(option);
});
select.appendChild(fragment);
if (!matchedCurrent && select.options.length) {
select.selectedIndex = 0;
}
select.dataset.currentModel = select.value || '';
select.disabled = false;
}
async function refreshModels(button) {
const status = statusSelectors.llm;
const llmFields = collectLLMFields();
if (!llmFields.base_url) {
setStatus(status, 'Enter a base URL before refreshing models.', 'error');
return;
}
clearStatus(status);
setStatus(status, 'Fetching models…');
button.disabled = true;
try {
const response = await fetch('/api/llm/models', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
base_url: llmFields.base_url,
api_key: llmFields.api_key,
timeout: llmFields.timeout,
}),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Unable to load models.');
}
updateModelOptions(payload.models || []);
const count = Array.isArray(payload.models) ? payload.models.length : 0;
if (count) {
setStatus(status, `Loaded ${count} model${count === 1 ? '' : 's'}.`, 'success');
} else {
setStatus(status, 'No models were returned.', 'error');
}
} catch (error) {
setStatus(status, error instanceof Error ? error.message : 'Failed to load models.', 'error');
} finally {
button.disabled = false;
}
}
async function previewLLM(button) {
const status = statusSelectors.llm;
const output = outputAreas.llm;
const previewText = document.querySelector('#llm_preview_text');
if (!previewText) {
return;
}
const llmFields = collectLLMFields();
if (!llmFields.base_url) {
setStatus(status, 'Enter a base URL to preview.', 'error');
return;
}
if (!llmFields.model) {
setStatus(status, 'Select a model to preview.', 'error');
return;
}
const sample = previewText.value.trim();
if (!sample) {
setStatus(status, 'Add some sample text first.', 'error');
return;
}
clearStatus(status);
if (output) {
output.textContent = '';
}
setStatus(status, 'Generating preview…');
button.disabled = true;
try {
const response = await fetch('/api/llm/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: sample,
base_url: llmFields.base_url,
api_key: llmFields.api_key,
model: llmFields.model,
prompt: llmFields.prompt,
context_mode: llmFields.context_mode,
timeout: llmFields.timeout,
}),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Preview failed.');
}
if (output) {
output.textContent = payload.normalized_text || '';
}
setStatus(status, 'Preview ready.', 'success');
} catch (error) {
if (output) {
output.textContent = '';
}
setStatus(status, error instanceof Error ? error.message : 'Preview failed.', 'error');
} finally {
button.disabled = false;
}
}
function collectNormalizationSettings() {
if (!form) {
return null;
}
const normalization = {
normalization_numbers: Boolean(form.querySelector('input[name="normalization_numbers"]')?.checked),
normalization_currency: Boolean(form.querySelector('input[name="normalization_currency"]')?.checked),
normalization_titles: Boolean(form.querySelector('input[name="normalization_titles"]')?.checked),
normalization_footnotes: Boolean(form.querySelector('input[name="normalization_footnotes"]')?.checked),
normalization_terminal: Boolean(form.querySelector('input[name="normalization_terminal"]')?.checked),
normalization_caps_quotes: Boolean(form.querySelector('input[name="normalization_caps_quotes"]')?.checked),
normalization_phoneme_hints: Boolean(form.querySelector('input[name="normalization_phoneme_hints"]')?.checked),
normalization_apostrophes_contractions: Boolean(form.querySelector('input[name="normalization_apostrophes_contractions"]')?.checked),
normalization_apostrophes_plural_possessives: Boolean(form.querySelector('input[name="normalization_apostrophes_plural_possessives"]')?.checked),
normalization_apostrophes_sibilant_possessives: Boolean(form.querySelector('input[name="normalization_apostrophes_sibilant_possessives"]')?.checked),
normalization_apostrophes_decades: Boolean(form.querySelector('input[name="normalization_apostrophes_decades"]')?.checked),
normalization_apostrophes_leading_elisions: Boolean(form.querySelector('input[name="normalization_apostrophes_leading_elisions"]')?.checked),
normalization_contraction_aux_be: Boolean(form.querySelector('input[name="normalization_contraction_aux_be"]')?.checked),
normalization_contraction_aux_have: Boolean(form.querySelector('input[name="normalization_contraction_aux_have"]')?.checked),
normalization_contraction_modal_will: Boolean(form.querySelector('input[name="normalization_contraction_modal_will"]')?.checked),
normalization_contraction_modal_would: Boolean(form.querySelector('input[name="normalization_contraction_modal_would"]')?.checked),
normalization_contraction_negation_not: Boolean(form.querySelector('input[name="normalization_contraction_negation_not"]')?.checked),
normalization_contraction_let_us: Boolean(form.querySelector('input[name="normalization_contraction_let_us"]')?.checked),
normalization_apostrophe_mode: form.querySelector('input[name="normalization_apostrophe_mode"]:checked')?.value || 'spacy',
};
return normalization;
}
function collectCalibreFields() {
if (!form) {
return {};
}
const enabled = Boolean(form.querySelector('input[name="calibre_opds_enabled"]')?.checked);
const baseUrl = form.querySelector('#calibre_opds_base_url')?.value?.trim() || '';
const username = form.querySelector('#calibre_opds_username')?.value?.trim() || '';
const passwordInput = form.querySelector('#calibre_opds_password');
const password = passwordInput ? passwordInput.value : '';
const hasSecret = passwordInput?.dataset.hasSecret === 'true';
const clearSaved = Boolean(form.querySelector('input[name="calibre_opds_password_clear"]')?.checked);
const useSavedPassword = !password && hasSecret && !clearSaved;
const verify = Boolean(form.querySelector('input[name="calibre_opds_verify_ssl"]')?.checked);
return {
enabled,
base_url: baseUrl,
username,
password,
verify_ssl: verify,
use_saved_password: useSavedPassword,
clear_saved_password: clearSaved,
};
}
function collectAudiobookshelfFields() {
if (!form) {
return {};
}
const baseUrl = form.querySelector('#audiobookshelf_base_url')?.value?.trim() || '';
const libraryId = form.querySelector('#audiobookshelf_library_id')?.value?.trim() || '';
const collectionId = form.querySelector('#audiobookshelf_collection_id')?.value?.trim() || '';
const folderId = form.querySelector('#audiobookshelf_folder_id')?.value?.trim() || '';
const tokenInput = form.querySelector('#audiobookshelf_api_token');
const apiToken = tokenInput?.value?.trim() || '';
const hasSecret = tokenInput?.dataset.hasSecret === 'true';
const clearToken = Boolean(form.querySelector('input[name="audiobookshelf_api_token_clear"]')?.checked);
const useSavedToken = !apiToken && hasSecret && !clearToken;
const timeoutInput = form.querySelector('#audiobookshelf_timeout');
const timeout = parseNumber(timeoutInput?.value, 30);
return {
enabled: Boolean(form.querySelector('input[name="audiobookshelf_enabled"]')?.checked),
auto_send: Boolean(form.querySelector('input[name="audiobookshelf_auto_send"]')?.checked),
verify_ssl: Boolean(form.querySelector('input[name="audiobookshelf_verify_ssl"]')?.checked),
base_url: baseUrl,
library_id: libraryId,
collection_id: collectionId,
folder_id: folderId,
api_token: apiToken,
use_saved_token: useSavedToken,
clear_saved_token: clearToken,
timeout,
send_cover: Boolean(form.querySelector('input[name="audiobookshelf_send_cover"]')?.checked),
send_chapters: Boolean(form.querySelector('input[name="audiobookshelf_send_chapters"]')?.checked),
send_subtitles: Boolean(form.querySelector('input[name="audiobookshelf_send_subtitles"]')?.checked),
};
}
function updateLLMNavState() {
if (!llmNavButton) {
return;
}
const fields = collectLLMFields();
if (fields.base_url && fields.api_key) {
llmNavButton.classList.remove('is-disabled');
} else {
llmNavButton.classList.add('is-disabled');
}
}
async function testCalibre(button) {
const status = statusSelectors.calibre;
const fields = collectCalibreFields();
if (!status) {
return;
}
if (!fields.base_url) {
setStatus(status, 'Enter a Calibre OPDS base URL to test.', 'error');
return;
}
clearStatus(status);
setStatus(status, 'Testing connection…');
button.disabled = true;
try {
const response = await fetch('/api/integrations/calibre-opds/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Calibre test failed.');
}
setStatus(status, payload.message || 'Connection successful.', 'success');
} catch (error) {
setStatus(status, error instanceof Error ? error.message : 'Calibre test failed.', 'error');
} finally {
button.disabled = false;
}
}
async function testAudiobookshelf(button) {
const status = statusSelectors.audiobookshelf;
const fields = collectAudiobookshelfFields();
if (!status) {
return;
}
const hasToken = Boolean(fields.api_token) || Boolean(fields.use_saved_token);
if (!fields.base_url || !hasToken || !fields.library_id || !fields.folder_id) {
setStatus(status, 'Enter the base URL, API token, library ID, and folder name or ID to test.', 'error');
return;
}
clearStatus(status);
setStatus(status, 'Testing connection…');
button.disabled = true;
try {
const response = await fetch('/api/integrations/audiobookshelf/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Audiobookshelf test failed.');
}
setStatus(status, payload.message || 'Connection successful.', 'success');
} catch (error) {
setStatus(status, error instanceof Error ? error.message : 'Audiobookshelf test failed.', 'error');
} finally {
button.disabled = false;
}
}
async function browseAudiobookshelfFolders(button) {
const status = statusSelectors.audiobookshelf;
const fields = collectAudiobookshelfFields();
if (!status) {
return;
}
const hasToken = Boolean(fields.api_token) || Boolean(fields.use_saved_token);
if (!fields.base_url || !hasToken || !fields.library_id) {
setStatus(status, 'Enter the base URL, API token, and library ID before browsing folders.', 'error');
return;
}
clearStatus(status);
openFolderModal(button);
if (!folderModal) {
setStatus(status, 'Folder picker is unavailable in this view.', 'error');
return;
}
button.disabled = true;
try {
const response = await fetch('/api/integrations/audiobookshelf/folders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fields),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Folder lookup failed.');
}
const folders = Array.isArray(payload.folders) ? payload.folders : [];
const modalActive = folderModal && !folderModal.hidden;
if (!folders.length) {
const message = payload.message || 'No folders found for this library.';
setStatus(status, message, 'info');
if (modalActive) {
clearFolderModalContents();
setFolderModalStatus(message, 'info');
}
return;
}
if (!modalActive) {
setStatus(status, 'Folders loaded.', 'info');
return;
}
populateFolderPicker(folders);
setStatus(status, 'Choose a folder below.', 'info');
} catch (error) {
const message = error instanceof Error ? error.message : 'Folder lookup failed.';
setStatus(status, message, 'error');
if (folderModal && !folderModal.hidden) {
clearFolderModalContents();
setFolderModalStatus(message, 'error');
}
} finally {
button.disabled = false;
}
}
async function previewNormalization(button) {
const status = statusSelectors.normalization;
const output = outputAreas.normalization;
const textArea = document.querySelector('#normalization_sample_text');
const voiceSelect = document.querySelector('#normalization_sample_voice');
if (!textArea) {
return;
}
const sample = textArea.value.trim();
if (!sample) {
setStatus(status, 'Enter some text to preview.', 'error');
return;
}
clearStatus(status);
if (output) {
output.textContent = '';
}
if (normalizationAudio) {
normalizationAudio.hidden = true;
normalizationAudio.removeAttribute('src');
}
setStatus(status, 'Building preview…');
const normalization = collectNormalizationSettings();
if (!normalization) {
setStatus(status, 'Unable to gather normalization settings.', 'error');
return;
}
const llmFields = collectLLMFields();
try {
const response = await fetch('/api/normalization/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: sample,
voice: voiceSelect ? voiceSelect.value : undefined,
normalization,
llm: {
llm_base_url: llmFields.base_url,
llm_api_key: llmFields.api_key,
llm_model: llmFields.model,
llm_prompt: llmFields.prompt,
llm_context_mode: llmFields.context_mode,
llm_timeout: llmFields.timeout,
},
max_seconds: 8,
}),
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error || 'Preview failed.');
}
if (output) {
output.textContent = payload.normalized_text || '';
}
if (payload.audio_base64 && normalizationAudio) {
normalizationAudio.src = `data:audio/wav;base64,${payload.audio_base64}`;
normalizationAudio.hidden = false;
normalizationAudio.load();
normalizationAudio.play().catch(() => {
/* autoplay can fail; ignore */
});
}
setStatus(status, 'Preview updated.', 'success');
} catch (error) {
if (output) {
output.textContent = '';
}
if (normalizationAudio) {
normalizationAudio.hidden = true;
normalizationAudio.removeAttribute('src');
}
setStatus(status, error instanceof Error ? error.message : 'Preview failed.', 'error');
} finally {
button.disabled = false;
}
}
function initSampleSelector() {
const select = document.querySelector('#normalization_sample_select');
const textArea = document.querySelector('#normalization_sample_text');
if (!select || !textArea) {
return;
}
select.addEventListener('change', () => {
const option = select.selectedOptions[0];
if (option) {
textArea.value = option.value;
}
});
}
function initActions() {
const refreshButton = document.querySelector('[data-action="llm-refresh-models"]');
if (refreshButton) {
refreshButton.addEventListener('click', () => refreshModels(refreshButton));
}
const llmPreviewButton = document.querySelector('[data-action="llm-preview"]');
if (llmPreviewButton) {
llmPreviewButton.addEventListener('click', () => previewLLM(llmPreviewButton));
}
const normalizationButton = document.querySelector('[data-action="normalization-preview"]');
if (normalizationButton) {
normalizationButton.addEventListener('click', () => previewNormalization(normalizationButton));
}
const calibreTestButton = document.querySelector('[data-action="calibre-test"]');
if (calibreTestButton) {
calibreTestButton.addEventListener('click', () => testCalibre(calibreTestButton));
}
const audiobookshelfTestButton = document.querySelector('[data-action="audiobookshelf-test"]');
if (audiobookshelfTestButton) {
audiobookshelfTestButton.addEventListener('click', () => testAudiobookshelf(audiobookshelfTestButton));
}
const audiobookshelfBrowseButton = document.querySelector('[data-action="audiobookshelf-list-folders"]');
if (audiobookshelfBrowseButton) {
audiobookshelfBrowseButton.addEventListener('click', () => browseAudiobookshelfFolders(audiobookshelfBrowseButton));
}
}
function initLLMStateWatchers() {
const baseUrlInput = form.querySelector('#llm_base_url');
const apiKeyInput = form.querySelector('#llm_api_key');
if (!baseUrlInput || !apiKeyInput) {
return;
}
const handler = () => updateLLMNavState();
baseUrlInput.addEventListener('input', handler);
apiKeyInput.addEventListener('input', handler);
updateLLMNavState();
}
if (form) {
initNavigation();
initSampleSelector();
initActions();
initFolderPicker();
initContractionModal();
initLLMStateWatchers();
}