diff --git a/static/app.js b/static/app.js index 014631e..0a5511f 100644 --- a/static/app.js +++ b/static/app.js @@ -1970,12 +1970,15 @@ document.querySelectorAll('.s-eye-btn').forEach(btn => { }); }); $('settings-btn')?.addEventListener('click', async () => { await loadSettings(); openSettings(false); }); -$('s-close-btn').addEventListener('click', async () => { await loadSettings(); toast('Settings reloaded', 'success'); }); +document.addEventListener('click', async e => { + if (e.target.closest('.s-reload-btn')) { await loadSettings(); toast('Settings reloaded', 'success'); } +}); $('s-use-parakeet-asr')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-nvidia-asr-url').value || 'http://host.docker.internal:8092'; }); $('s-use-nvidia-router')?.addEventListener('click', () => { const url = $('s-nvidia-router-url').value || 'http://host.docker.internal:8090'; $('s-whisper-url').value = url; $('s-nvidia-tts-url').value = url; }); $('s-use-faster-whisper')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-faster-whisper-url').value || 'http://host.docker.internal:8000'; }); $('s-use-whisper-cpp')?.addEventListener('click', () => { $('s-whisper-url').value = $('s-whisper-cpp-url').value || 'http://host.docker.internal:8080'; }); -$('s-save-btn').addEventListener('click', async () => { +document.addEventListener('click', async e => { if (!e.target.closest('.s-save-btn')) return; +{ let ttsExtraParamsByBackend = {}; const paramFields = [ ['voice_clone', 's-tts-extra-voice-clone', 'Voice Clone/Base'], @@ -2036,7 +2039,7 @@ $('s-save-btn').addEventListener('click', async () => { markSettingsSeen(); renderIntegrationSnippets(); toast('Settings saved', 'success'); -}); +}}); // Theme select in General settings document.addEventListener('change', e => { diff --git a/static/nav.js b/static/nav.js index 4a9195c..bbe1351 100644 --- a/static/nav.js +++ b/static/nav.js @@ -29,6 +29,15 @@ if (name === 'getvoices' && typeof loadGetVoices === 'function') loadGetVoices(); } + function applySettingsPage(cat) { + document.querySelectorAll('.s-settings-page').forEach(function (p) { + p.classList.toggle('is-active', p.dataset.page === cat); + }); + document.querySelectorAll('[data-settings-cat]').forEach(function (el) { + el.classList.toggle('is-active', el.dataset.settingsCat === cat); + }); + } + function showSection(sectionId) { SECTIONS.forEach(function (id) { var el = document.getElementById(id); @@ -41,16 +50,19 @@ // Voices nav tree var voicesTree = document.getElementById('nav-voices-tree'); var voicesChevron = document.getElementById('nav-voices-chevron'); - var isVoices = sectionId === 's-voices'; - if (voicesTree) voicesTree.classList.toggle('open', isVoices); - if (voicesChevron) voicesChevron.classList.toggle('open', isVoices); + if (voicesTree) voicesTree.classList.toggle('open', sectionId === 's-voices'); + if (voicesChevron) voicesChevron.classList.toggle('open', sectionId === 's-voices'); // Settings nav tree var settingsTree = document.getElementById('nav-settings-tree'); var settingsChevron = document.getElementById('nav-settings-chevron'); - var isSettings = sectionId === 's-settings'; - if (settingsTree) settingsTree.classList.toggle('open', isSettings); - if (settingsChevron) settingsChevron.classList.toggle('open', isSettings); + if (settingsTree) settingsTree.classList.toggle('open', sectionId === 's-settings'); + if (settingsChevron) settingsChevron.classList.toggle('open', sectionId === 's-settings'); + + // When entering settings, show the active sub-page (default: connections) + if (sectionId === 's-settings') { + applySettingsPage(window._settingsSidebarCat || 'connections'); + } var main = document.getElementById('main-content'); if (main) main.scrollTop = 0; @@ -96,27 +108,13 @@ }; // ── Settings tree sub-navigation ────────────────────────────────────────── - window._settingsSidebarCat = null; + window._settingsSidebarCat = 'connections'; window.navSettingsCat = function (cat) { - navTo('s-settings'); window._settingsSidebarCat = cat; - document.querySelectorAll('[data-settings-cat]').forEach(function (el) { - el.classList.toggle('is-active', el.dataset.settingsCat === cat); - }); + navTo('s-settings'); // showSection will call applySettingsPage(cat) if (cat === 'logs' && typeof loadSettingsLogs === 'function') loadSettingsLogs(); - // Scroll inside main-content to the target section - var target = document.getElementById('s-settings-' + cat); - if (target) { - setTimeout(function () { - var main = document.getElementById('main-content'); - if (main) { - var mainRect = main.getBoundingClientRect(); - var targetRect = target.getBoundingClientRect(); - main.scrollTo({ top: main.scrollTop + targetRect.top - mainRect.top - 16, behavior: 'smooth' }); - } - }, 50); - } + if (cat === 'about' && typeof renderSettingsAbout === 'function') renderSettingsAbout(); }; // Show default section on load diff --git a/static/sections/s-settings.html b/static/sections/s-settings.html index ca3365c..b2470b4 100644 --- a/static/sections/s-settings.html +++ b/static/sections/s-settings.html @@ -7,20 +7,14 @@
-
- -
-
-

Settings

-

Configure the service URLs you use. Advanced payloads, folders, and keys are below.

+ +
+
+
+

General

+

Interface preferences.

- Local stack -
- - -
-
@@ -28,15 +22,24 @@ - Switch between light and dark interface. + Switch between light and dark interface. Saved instantly.
+
- -
- -

Service URLs for each backend. Change these first when setting up.

+ +
+
+
+
+
+

Connections

+

Service URLs for each backend. Change these first when setting up.

+
+ Local stack +
+
@@ -139,16 +142,25 @@
- Used for Groq Whisper STT and LLM. Free: 2 000 req/day. Fastest cloud transcription. + Used for Groq Whisper STT and LLM. Free: 2 000 req/day.
-
- -
- -

Controls how previews play and how OpenAI-compatible requests are shaped.

+
+ + +
+
+
+ + +
+
+
+

Playback

+

Controls how previews play and how OpenAI-compatible requests are shaped.

+
@@ -170,23 +182,21 @@ Controls payload shape for the normal TTS API URL.
+
+ + +
+
- -
- - -
- - -
- - - Payloads - JSON extras sent to each backend - - -
+ +
+
+
+

Payloads

+

Extra JSON fields sent alongside each backend request. Usually only needed for advanced tuning.

+
+
@@ -228,17 +238,21 @@ Extra JSON fields for Kokoro. Usually empty — voice is selected from the dropdown.
-
+
+ + +
+
+
- -
- - - Storage - container paths and Portainer volume mounts - - -
+ +
+
+
+

Storage

+

Container paths for voice files. Match these to your Portainer volume mounts.

+
+
@@ -250,17 +264,21 @@ New cloned/exported voices are saved here.
-
+
+ + +
+
+ - -
- - - API Keys - usually empty for local containers - - -
+ +
+
+
+

API Keys

+

Usually empty for local containers. Set a dummy value like sk-local if a backend requires an Authorization header.

+
+
@@ -283,13 +301,21 @@
-
+
+ + +
+ + - -
-
Backup & restore
-

Export all voices and settings as a ZIP for backup or migration. Import to restore. API keys are excluded from exports.

-
+ +
+
+
+

Backup & restore

+

Export all voices and settings as a ZIP for backup or migration. API keys are excluded from exports.

+
+
Export voices
+
- -
- -

Recent server activity. Useful for debugging backend connections and API errors.

+ +
+
+
+

Logs

+

Recent server activity. Useful for debugging backend connections and API errors.

+
@@ -322,10 +352,14 @@
Click Refresh to load logs
+
- -
- + +
+
+
+

About

+
TTS Voice Creator

Clone, design, and deploy custom voices using local AI backends. Compatible with Qwen3-TTS, Kokoro FastAPI, NVIDIA Magpie, and any OpenAI-compatible TTS/STT endpoint.

@@ -338,6 +372,6 @@
-
+
diff --git a/static/style.css b/static/style.css index 490e56f..f2a03b5 100644 --- a/static/style.css +++ b/static/style.css @@ -317,24 +317,20 @@ audio { width: 100%; } .preview-match-transcript { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; color: var(--subtext); font-family: monospace; font-size: 12px; line-height: 1.4; max-height: 88px; overflow: auto; white-space: pre-wrap; } @media (max-width: 900px) { .preview-match-panel { grid-template-columns: 1fr; } } -/* ── Settings ───────────────────────────────────────────────────────────── */ -#settings-box { display:flex; flex-direction:column; gap:0; width:100%; max-width:none; box-sizing:border-box; padding:0; overflow:hidden; } +/* ── Settings pages ─────────────────────────────────────────────────────── */ +.s-settings-page { display:none; flex-direction:column; gap:16px; } +.s-settings-page.is-active { display:flex; } -/* Header */ -.s-header { display:flex; justify-content:space-between; align-items:flex-start; gap:20px; padding:28px 32px 24px; border-bottom:1px solid var(--border); } -.s-header h2 { font-size:18px; font-weight:700; margin:0 0 5px; } -.s-subtitle { font-size:13px; color:var(--subtext); line-height:1.55; margin:0; max-width:580px; } -.s-stack-badge { display:inline-flex; align-items:center; gap:5px; border:1px solid color-mix(in srgb,var(--green) 40%,transparent); background:rgba(22,163,74,.07); color:var(--green); border-radius:999px; padding:5px 11px; font-size:12px; font-weight:600; white-space:nowrap; flex-shrink:0; margin-top:2px; } +/* Page card head */ +.s-page-head { border-bottom:1px solid var(--border); padding-bottom:14px; } +.s-page-head h3 { font-size:16px; font-weight:700; margin:0 0 4px; color:var(--text); } +.s-page-head p { font-size:13px; color:var(--subtext); margin:0; line-height:1.5; } +.s-page-head-row { display:flex; justify-content:space-between; align-items:flex-start; gap:16px; } +.s-stack-badge { display:inline-flex; align-items:center; gap:5px; border:1px solid color-mix(in srgb,var(--green) 40%,transparent); background:rgba(22,163,74,.07); color:var(--green); border-radius:999px; padding:5px 11px; font-size:12px; font-weight:600; white-space:nowrap; flex-shrink:0; } -/* Sections */ -.s-section { padding:26px 32px; border-bottom:1px solid var(--border); display:flex; flex-direction:column; gap:22px; } -.s-section-last { border-bottom:none; } -.s-section-label { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:var(--subtext); } -.s-section-desc { font-size:13px; color:var(--subtext); margin:0; line-height:1.5; } - -/* Field groups within a section */ -.s-group { display:flex; flex-direction:column; gap:14px; padding-top:4px; } -.s-group + .s-group { border-top:1px solid var(--border); padding-top:22px; } +/* Groups */ +.s-group { display:flex; flex-direction:column; gap:14px; } +.s-group + .s-group { border-top:1px solid var(--border); padding-top:18px; } .s-group-head { display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; } .s-group-head strong { font-size:14px; font-weight:600; color:var(--text); } .s-group-head span { font-size:12px; color:var(--subtext); } @@ -344,8 +340,9 @@ audio { width: 100%; } .settings-grid.three { grid-template-columns:repeat(auto-fit, minmax(240px,1fr)); } .settings-grid.compact { grid-template-columns:repeat(2, minmax(220px,1fr)); gap:14px; } .settings-behavior-grid { grid-template-columns:minmax(220px,.6fr) minmax(220px,.6fr); gap:16px; align-items:start; } +.settings-param-grid textarea { min-height:58px; } -/* Individual field */ +/* Fields */ .s-field { display:flex; flex-direction:column; gap:6px; } .s-field label { font-size:13px; color:var(--text); font-weight:500; } .s-label-note { font-weight:400; color:var(--subtext); } @@ -363,21 +360,9 @@ audio { width: 100%; } .settings-mini-actions { margin-top:4px; gap:8px; flex-wrap:wrap; } .settings-mini-actions button { min-height:32px; padding:6px 11px; font-size:12px; } -/* Expandable details */ -.s-details { border-bottom:1px solid var(--border); } -.s-details summary { list-style:none; cursor:pointer; padding:16px 32px; display:flex; align-items:center; gap:10px; color:var(--text); font-size:14px; font-weight:600; user-select:none; transition:background .15s; } -.s-details summary:hover { background:rgba(0,0,0,.02); } -.s-details summary::-webkit-details-marker { display:none; } -.s-details-title { flex:1; } -.s-details summary small { color:var(--subtext); font-weight:400; font-size:13px; } -.s-details-chevron { color:var(--subtext); font-size:18px; transition:transform .2s; flex-shrink:0; } -.s-details[open] .s-details-chevron { transform:rotate(180deg); } -.s-details-body { padding:4px 32px 24px; } -.settings-param-grid textarea { min-height:58px; } - -/* Actions row */ -.settings-actions { display:flex; gap:10px; align-items:center; padding:20px 32px; border-top:1px solid var(--border); border-bottom:1px solid var(--border); } -.settings-actions button { min-width:128px; } +/* Page save/reload actions */ +.s-page-actions { border-top:1px solid var(--border); padding-top:14px; display:flex; gap:10px; } +.s-page-actions button { min-width:120px; } /* Log viewer */ .s-log-toolbar { display:flex; align-items:center; gap:8px; flex-wrap:wrap; } @@ -388,7 +373,7 @@ audio { width: 100%; } .s-log-filter { border:1px solid var(--border); background:transparent; color:var(--subtext); border-radius:999px; padding:3px 10px; font-size:12px; cursor:pointer; transition:all .15s; } .s-log-filter:hover { border-color:var(--accent); color:var(--accent); } .s-log-filter.is-active { background:var(--accent); border-color:var(--accent); color:#fff; } -.s-log-viewer { background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:12px 14px; font-family:monospace; font-size:12px; line-height:1.6; max-height:380px; overflow-y:auto; display:flex; flex-direction:column; gap:1px; } +.s-log-viewer { background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:12px 14px; font-family:monospace; font-size:12px; line-height:1.6; max-height:440px; overflow-y:auto; display:flex; flex-direction:column; gap:1px; } .s-log-empty { color:var(--subtext); font-family:inherit; text-align:center; padding:24px 0; } .s-log-row { display:grid; grid-template-columns:130px 56px 1fr; gap:8px; align-items:baseline; padding:2px 0; border-bottom:1px solid color-mix(in srgb,var(--border) 50%,transparent); } .s-log-row:last-child { border-bottom:none; } @@ -407,7 +392,7 @@ audio { width: 100%; } .s-about-desc { font-size:14px; color:var(--subtext); line-height:1.65; margin:0; } .s-about-stack { display:flex; flex-wrap:wrap; gap:8px; } .s-about-chip { display:inline-flex; align-items:center; gap:5px; background:var(--panel); border:1px solid var(--border); border-radius:var(--radius); padding:4px 10px; font-size:12px; color:var(--subtext); } -.s-about-backends { display:flex; flex-wrap:wrap; gap:8px; } +.s-about-backends { display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; } .s-about-backend { display:inline-flex; align-items:center; gap:6px; border:1px solid var(--border); border-radius:var(--radius); padding:5px 10px; font-size:12px; color:var(--subtext); } .s-about-backend.online { border-color:color-mix(in srgb,var(--green) 50%,transparent); color:var(--green); } .s-about-backend.offline { opacity:.55; } @@ -415,12 +400,7 @@ audio { width: 100%; } /* Responsive */ @media (max-width:1100px) { .settings-grid.compact { grid-template-columns:1fr; } .settings-behavior-grid { grid-template-columns:1fr; } } -@media (max-width:900px) { - .stt-settings-grid { grid-template-columns:1fr; } - .s-header, .s-section, .settings-actions { padding-left:18px; padding-right:18px; } - .s-details summary { padding-left:18px; padding-right:18px; } - .s-details-body { padding-left:18px; padding-right:18px; } -} +@media (max-width:900px) { .stt-settings-grid { grid-template-columns:1fr; } } /* ── Toast ──────────────────────────────────────────────────────────────── */ #toast { position: fixed; bottom: 24px; right: 24px; background: var(--surface); border: 1px solid var(--border); color: var(--text); padding: 14px 20px; border-radius: var(--radius); font-size: 14px; opacity: 0; transition: opacity .3s; pointer-events: none; z-index: 200; max-width: 380px; box-shadow: var(--shadow); }