(function () { 'use strict'; const TAB_SECTION_MAP = { library: 's-voices', source: 's-clone', save: 's-clone', design: 's-design', custom: 's-design', getvoices: 's-studio', generation: 's-tryout', 'stt-tts': 's-tryout', performance: 's-performance', routing: 's-routing', integrations: 's-connect', howto: 's-connect', settings: 's-settings', llms: 's-llms' }; const SECTIONS = ['s-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser', 's-reader', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation', 's-library']; function storedSection() { try { return localStorage.getItem('ttsvc_section') || ''; } catch (_) { return ''; } } function setStoredSection(sectionId) { try { localStorage.setItem('ttsvc_section', sectionId); } catch (_) {} } function hashSection() { var raw = String(location.hash || '').replace(/^#/, '').trim(); if (!raw) return ''; if (raw.indexOf('section=') === 0) raw = raw.slice(8); raw = decodeURIComponent(raw).replace(/^\/?/, ''); return SECTIONS.includes(raw) ? raw : ''; } function setSectionHash(sectionId) { if (!SECTIONS.includes(sectionId)) return; var next = '#' + encodeURIComponent(sectionId); if (location.hash === next) return; try { history.replaceState(null, '', location.pathname + location.search + next); } catch (_) { location.hash = next; } } function runSideEffects(name) { if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText(); if (name === 'library' && typeof scheduleVoiceLibraryLoad === 'function') scheduleVoiceLibraryLoad(); if ((name === 'integrations' || name === 'howto') && typeof renderIntegrationSnippets === 'function') { if (typeof scheduleVoiceLibraryLoad === 'function' && !(window._voices && window._voices.length)) scheduleVoiceLibraryLoad(renderIntegrationSnippets); renderIntegrationSnippets(); } if (name === 'routing' && typeof loadRoutingTab === 'function') loadRoutingTab(); if (name === 'getvoices') { if (typeof window.initElevenLabsBrowser === 'function') window.initElevenLabsBrowser(); if (typeof loadGetVoices === 'function') loadGetVoices(); } if (name === 'performance' && typeof window.populateBatchBenchmarkFromLibrary === 'function') window.populateBatchBenchmarkFromLibrary(); if (name === 'performance' && typeof window.loadBenchmarkSectionData === 'function') window.loadBenchmarkSectionData(); } 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); }); } // ── Tree open/close state — each tree is independent ───────────────────── // Load persisted open state (default: open voices + the active section's tree) var _treeOpen = {}; try { _treeOpen = JSON.parse(localStorage.getItem('ttsvc_trees') || '{}'); } catch(_) {} // Every collapsible tree must be listed so _applyTreeStates toggles its // open class + chevron. `section` (optional) auto-opens the tree (and its // ancestor trees, handled in _openTreesForSection) when that section opens. var TREES = [ { tree: 'nav-voices-tree', chevron: 'nav-voices-chevron', section: 's-voices' }, { tree: 'nav-tags-tree', chevron: 'nav-tags-chevron' }, { tree: 'nav-actions-tree', chevron: 'nav-actions-chevron' }, { tree: 'nav-speak-tree', chevron: 'nav-speak-chevron' }, { tree: 'nav-reader-tree', chevron: 'nav-reader-chevron', section: 's-reader' }, { tree: 'nav-rehearser-tree', chevron: 'nav-rehearser-chevron', section: 's-rehearser' }, { tree: 'nav-library-tree', chevron: 'nav-library-chevron', section: 's-library' }, { tree: 'nav-setup-tree', chevron: 'nav-setup-chevron' }, { tree: 'nav-engines-tree', chevron: 'nav-engines-chevron', section: 's-llms' }, { tree: 'nav-integrations-tree', chevron: 'nav-integrations-chevron' }, { tree: 'nav-settings-tree', chevron: 'nav-settings-chevron', section: 's-settings' }, ]; // Open the tree(s) for a section: the head's own tree (via TREES.section) // plus every ancestor .nav-tree of the active nav element, so nested items // become visible. Persists once if anything changed. function _openTreesForSection(sectionId) { var changed = false; TREES.forEach(function (t) { if (t.section === sectionId && !_treeOpen[t.tree]) { _treeOpen[t.tree] = true; changed = true; } }); var el = document.querySelector('[data-nav-section="' + sectionId + '"]'); while (el) { if (el.classList && el.classList.contains('nav-tree') && el.id && !_treeOpen[el.id]) { _treeOpen[el.id] = true; changed = true; } el = el.parentElement; } if (changed) localStorage.setItem('ttsvc_trees', JSON.stringify(_treeOpen)); } function _applyTreeStates() { TREES.forEach(function (t) { var open = !!_treeOpen[t.tree]; var treeEl = document.getElementById(t.tree); var chevEl = document.getElementById(t.chevron); if (treeEl) treeEl.classList.toggle('open', open); if (chevEl) chevEl.classList.toggle('open', open); }); } // Show/hide an animated dot on a nav section head to signal a background process. // Call setNavBusy('s-reader', true) when synthesis/playback starts, false when done. window.setNavBusy = function (sectionId, busy) { var el = document.querySelector('[data-nav-section="' + sectionId + '"]'); if (!el) return; var dot = el.querySelector('.nav-busy-dot'); if (busy && !dot) { dot = document.createElement('span'); dot.className = 'nav-busy-dot'; dot.title = 'Running…'; el.appendChild(dot); } else if (!busy && dot) { dot.remove(); } }; window.toggleNavTree = function (treeId, chevronId) { _treeOpen[treeId] = !_treeOpen[treeId]; localStorage.setItem('ttsvc_trees', JSON.stringify(_treeOpen)); _applyTreeStates(); }; function showSection(sectionId) { if (!SECTIONS.includes(sectionId)) sectionId = 's-voices'; // Leaving the Read Aloud reader: stop playback so audio doesn't keep running if (sectionId !== 's-reader' && typeof window.readerStop === 'function') window.readerStop(); // Capture before readerOnShow (async) eventually consumes it: a caller that // requested a specific reader view via _readerStartView must not have that // choice overridden by the cast-session restore below. var _explicitReaderView = window._readerStartView; if (sectionId === 's-reader' && typeof window.readerOnShow === 'function') window.readerOnShow(); // If a cast session panel is active, restore it instead of showing the main // PDF view. Synchronous on purpose: callers that want a different view just // call showReaderView(...) after navTo() and their choice wins — no deferred // override racing whatever the caller set (the old setTimeout version hijacked // the view a tick later and needed a global suppress flag to defeat). if (sectionId === 's-reader' && !_explicitReaderView) { var _castPnl = document.getElementById('reader-audiobook-panel'); if (_castPnl && _castPnl.querySelector('#ab-cv-feed') && typeof window.showReaderView === 'function') { window.showReaderView('cast'); } } if (sectionId === 's-library' && typeof window.libraryRender === 'function') window.libraryRender(window._libraryView || 'books'); if (typeof window.initCollapsibleCards === 'function') window.initCollapsibleCards(); setStoredSection(sectionId); setSectionHash(sectionId); SECTIONS.forEach(function (id) { var el = document.getElementById(id); if (el) el.classList.toggle('is-active', id === sectionId); }); document.querySelectorAll('[data-nav-section]').forEach(function (item) { item.classList.toggle('active', item.dataset.navSection === sectionId); }); // Navigating to a section auto-opens its tree AND every ancestor tree // (so a nested item is actually visible), but never closes others. _openTreesForSection(sectionId); _applyTreeStates(); // When entering settings, show the active sub-page (default: connections) if (sectionId === 's-settings') { applySettingsPage(window._settingsSidebarCat || 'connections'); } // When entering engines, show the active sub-page (default: llm) and load containers if (sectionId === 's-llms') { applyEnginesPage(window._enginesSidebarCat || 'llm'); if (typeof loadLocalContainers === 'function') loadLocalContainers(); } var main = document.getElementById('main-content'); if (main) main.scrollTop = 0; } window.navTo = function (sectionId) { showSection(sectionId); var tabName = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === sectionId; }); if (tabName) runSideEffects(tabName); // Keep the Reader/Casting/Characters/Rehearser breadcrumb in sync with // plain sidebar navigation (not just the crumb's own buttons). if (typeof window.refreshWorkflowCrumbs === 'function') { if (sectionId === 's-rehearser') window.refreshWorkflowCrumbs('rehearser'); else if (sectionId === 's-reader') window.refreshWorkflowCrumbs('source'); } }; // Pulsing dot on the Read Aloud nav item while casting is in progress window.setNavCastingBadge = function (active) { var el = document.querySelector('[data-nav-section="s-reader"]'); if (!el) return; var existing = el.querySelector('.nav-casting-dot'); if (active && !existing) { var dot = document.createElement('span'); dot.className = 'nav-casting-dot'; el.appendChild(dot); } else if (!active && existing) { existing.remove(); } }; window.switchTab = function (name) { var sectionId = TAB_SECTION_MAP[name]; if (sectionId) showSection(sectionId); runSideEffects(name); return true; }; // ── Voice tree sub-navigation ───────────────────────────────────────────── window._voiceSidebarCat = 'all'; window._voiceTagFilter = ''; window.navVoicesCat = function (cat) { navTo('s-voices'); window._voiceSidebarCat = cat; window._voiceTagFilter = ''; // a fixed category clears any tag subfolder filter document.querySelectorAll('[data-voice-cat]').forEach(function (el) { el.classList.toggle('is-active', el.dataset.voiceCat === cat); }); document.querySelectorAll('[data-voice-tag]').forEach(function (el) { el.classList.remove('is-active'); }); if (typeof renderVoiceList === 'function') renderVoiceList(); }; // Click a tag "subfolder" in the My Voices tree → filter the list to that tag. window.navVoicesTag = function (tag) { navTo('s-voices'); window._voiceTagFilter = (window._voiceTagFilter === tag) ? '' : tag; // toggle off if re-clicked window._voiceSidebarCat = 'all'; document.querySelectorAll('[data-voice-cat]').forEach(function (el) { el.classList.toggle('is-active', el.dataset.voiceCat === 'all' && !window._voiceTagFilter); }); document.querySelectorAll('[data-voice-tag]').forEach(function (el) { el.classList.toggle('is-active', el.dataset.voiceTag === window._voiceTagFilter); }); if (typeof renderVoiceList === 'function') renderVoiceList(); }; window.updateVoiceTree = function (voices) { if (!voices) return; function n(fn) { return voices.filter(fn).length; } function set(id, v) { var el = document.getElementById(id); if (el) el.textContent = v || ''; } set('ntc-all', n(function () { return true; })); set('ntc-favorites',n(function (v) { return (v.rating || 0) >= 4; })); set('ntc-hidden', n(function (v) { return v.enabled === false; })); set('nav-voices-count', voices.length); window.buildTagsTree(voices); }; // ── Tags tree — distinct voice tags with counts, plus the computed // Cloned / Designed / Favourites predicates that moved out of Library ── window.buildTagsTree = function (voices) { voices = voices || window._voices; var tree = document.getElementById('nav-tags-tree'); if (!tree || !voices) return; tree.innerHTML = ''; function n(fn) { return voices.filter(fn).length; } // Computed predicate rows (not literal tag strings) → navVoicesCat var preds = [ { cat: 'cloned', icon: 'microphone-variant', label: 'Cloned', count: n(function (v) { return v.has_ref; }) }, { cat: 'designed', icon: 'auto-fix', label: 'Designed', count: n(function (v) { return !v.has_ref; }) }, { cat: 'favorites', icon: 'star-outline', label: 'Favourites', count: n(function (v) { return (v.rating || 0) >= 4; }) }, ]; preds.forEach(function (p) { var el = document.createElement('div'); el.className = 'nav-tree-item' + (window._voiceSidebarCat === p.cat && !window._voiceTagFilter ? ' is-active' : ''); el.dataset.voiceCat = p.cat; el.innerHTML = ' ' + p.label + ' ' + (p.count || '') + ''; el.addEventListener('click', function () { window.navVoicesCat(p.cat); }); tree.appendChild(el); }); // Literal tag strings → navVoicesTag (filters via _voiceTagFilter) var counts = {}; voices.forEach(function (v) { String(v.tag || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean) .forEach(function (t) { counts[t] = (counts[t] || 0) + 1; }); }); var names = Object.keys(counts).sort(function (a, b) { return a.toLowerCase().localeCompare(b.toLowerCase()); }); names.forEach(function (tag) { var el = document.createElement('div'); el.className = 'nav-tree-item' + (window._voiceTagFilter === tag ? ' is-active' : ''); el.dataset.voiceTag = tag; var icon = document.createElement('span'); icon.className = 'mdi mdi-tag-outline'; el.appendChild(icon); el.appendChild(document.createTextNode(' ' + tag + ' ')); var c = document.createElement('span'); c.className = 'ntc'; c.textContent = counts[tag]; el.appendChild(c); el.addEventListener('click', function () { window.navVoicesTag(tag); }); tree.appendChild(el); }); }; // ── Settings tree sub-navigation ────────────────────────────────────────── window._settingsSidebarCat = 'connections'; window.navSettingsCat = function (cat) { window._settingsSidebarCat = cat; localStorage.setItem('ttsvc_settings_cat', cat); navTo('s-settings'); // showSection will call applySettingsPage(cat) if (cat === 'logs' && typeof loadSettingsLogs === 'function') loadSettingsLogs(); if (cat === 'about' && typeof renderSettingsAbout === 'function') renderSettingsAbout(); }; // ── Engines tree sub-navigation ─────────────────────────────────────────── function applyEnginesPage(cat) { document.querySelectorAll('.s-engines-page').forEach(function (p) { p.classList.toggle('is-active', p.dataset.page === cat); }); document.querySelectorAll('[data-engines-cat]').forEach(function (el) { el.classList.toggle('is-active', el.dataset.enginesCat === cat); }); } window._enginesSidebarCat = 'llm'; window.navEnginesCat = function (cat) { window._enginesSidebarCat = cat; localStorage.setItem('ttsvc_engines_cat', cat); navTo('s-llms'); // showSection will call applyEnginesPage(cat) if (typeof loadLocalContainers === 'function') loadLocalContainers(); }; // ── Rehearser tree sub-navigation ──────────────────────────────────────── window.navRehearserImpEx = function () { navTo('s-rehearser'); // Clear phase highlights — Import/Export is its own panel, not a numbered phase document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) { el.classList.remove('is-active'); }); document.getElementById('nav-reh-impex')?.classList.add('is-active'); if (typeof showRehImpEx === 'function') showRehImpEx(); else window._rehearserStartImpEx = true; }; window.navRehearserPhase = function (phase) { navTo('s-rehearser'); // highlight the correct tree item immediately document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) { el.classList.toggle('is-active', Number(el.dataset.rehearserPhase) === phase); }); // delegate to rehearser's own showPhase() when it's ready if (typeof showPhase === 'function') { showPhase(phase); } else { // rehearser.js not yet loaded — store the intent and let init pick it up window._rehearserStartPhase = phase; } }; // Keep sidebar tree in sync when rehearser changes phase internally window.onRehearserPhaseChange = function (phase) { document.querySelectorAll('[data-rehearser-phase]').forEach(function (el) { el.classList.toggle('is-active', Number(el.dataset.rehearserPhase) === phase); }); }; // ── Reader tree sub-navigation ─────────────────────────────────────────── window.navReaderView = function (view) { navTo('s-reader'); document.querySelectorAll('[data-reader-view]').forEach(function (el) { el.classList.toggle('is-active', el.dataset.readerView === view); }); if (typeof showReaderView === 'function') { showReaderView(view); } else { window._readerStartView = view; } }; // Restore last-used section from hash/localStorage (fallback: My Voices) var _savedSection = storedSection(); // On very first visit, default voices tree open if (!localStorage.getItem('ttsvc_trees')) { _treeOpen['nav-voices-tree'] = true; } window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections'; window._enginesSidebarCat = localStorage.getItem('ttsvc_engines_cat') || 'llm'; var _startSection = hashSection() || ((_savedSection && SECTIONS.includes(_savedSection)) ? _savedSection : 's-voices'); showSection(_startSection); window.addEventListener('hashchange', function () { var section = hashSection(); if (section && section !== storedSection()) showSection(section); }); var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library'; runSideEffects(_startTab); // ── Mobile sidebar drawer ──────────────────────────────────────────────── var sidebar = document.getElementById('sidebar'); var backdrop = document.getElementById('sidebar-backdrop'); var toggleBtn = document.getElementById('sidebar-toggle'); function openSidebar() { if (!sidebar) return; sidebar.classList.add('open'); if (backdrop) backdrop.classList.add('open'); document.body.style.overflow = 'hidden'; // prevent scroll behind drawer } function closeSidebar() { if (!sidebar) return; sidebar.classList.remove('open'); if (backdrop) backdrop.classList.remove('open'); document.body.style.overflow = ''; } if (toggleBtn) toggleBtn.addEventListener('click', openSidebar); if (backdrop) backdrop.addEventListener('click', closeSidebar); // ── Desktop sidebar collapse/expand (button where the flag used to be) ──── try { if (localStorage.getItem('ttsvc_sidebar_collapsed') === '1') document.body.classList.add('sidebar-collapsed'); } catch (_) {} window.toggleSidebarCollapsed = function () { var collapsed = document.body.classList.toggle('sidebar-collapsed'); try { localStorage.setItem('ttsvc_sidebar_collapsed', collapsed ? '1' : '0'); } catch (_) {} var btn = document.getElementById('sidebar-collapse-btn'); if (btn) btn.title = collapsed ? 'Expand sidebar' : 'Collapse sidebar'; if (!collapsed) _railTip.style.display = 'none'; }; // ── Icon-rail per-item label tooltip ──────────────────────────────────── var _railTip = document.createElement('div'); _railTip.id = 'nav-rail-tooltip'; document.body.appendChild(_railTip); var _railTipActive = null; var _sidebarEl = document.getElementById('sidebar'); _sidebarEl.addEventListener('mouseover', function (e) { if (!document.body.classList.contains('sidebar-collapsed')) return; var item = e.target.closest('.nav-item, .nav-tree-item, .nav-tree-head'); if (!item || item === _railTipActive) return; var label = item.querySelector('.nav-label'); var text = ''; if (label) { text = label.textContent.trim(); } else { var clone = item.cloneNode(true); clone.querySelectorAll('.mdi, .ntc, .nav-badge, .nav-chevron').forEach(function (el) { el.remove(); }); text = clone.textContent.replace(/\s+/g, ' ').trim(); } if (!text) return; var r = item.getBoundingClientRect(); _railTip.textContent = text; _railTip.style.left = (r.right + 10) + 'px'; _railTip.style.top = (r.top + r.height / 2) + 'px'; _railTip.style.display = 'block'; _railTipActive = item; }); _sidebarEl.addEventListener('mouseout', function (e) { if (!e.relatedTarget || !e.relatedTarget.closest('#sidebar')) { _railTip.style.display = 'none'; _railTipActive = null; } }); // Close drawer when a nav item is tapped on mobile document.querySelectorAll('#sidebar .nav-item, #sidebar .nav-tree-item').forEach(function (el) { el.addEventListener('click', function () { if (window.innerWidth <= 767) closeSidebar(); }); }); // ── Mobile voice inspector: back button ────────────────────────────────── // When a voice row is selected on mobile, show inspector full-height. // The inspector JS will call window.onMobileInspectorOpen/Close as hooks. window.onMobileInspectorOpen = function () { var wb = document.querySelector('.voices-workbench'); if (!wb || window.innerWidth > 767) return; wb.classList.add('mobile-inspector-open'); // Inject back button if not already there var insp = document.getElementById('voices-inspector'); if (insp && !insp.querySelector('.insp-mobile-back')) { var back = document.createElement('div'); back.className = 'insp-mobile-back'; back.innerHTML = ' All voices'; back.addEventListener('click', function () { window.onMobileInspectorClose && window.onMobileInspectorClose(); }); insp.insertBefore(back, insp.firstChild); } }; window.onMobileInspectorClose = function () { var wb = document.querySelector('.voices-workbench'); if (wb) wb.classList.remove('mobile-inspector-open'); var back = document.querySelector('.insp-mobile-back'); if (back) back.remove(); }; })();