feat: slim icon-rail sidebar, collapsible panels, Read Aloud fit-height (v1.12.0)
- Sidebar collapses to a 56px icon rail; hovering flies the full menu out as an overlay (icons + titles/nested items). Language picker moved to Settings. - Unify settings collapsibles to the app's standard card-collapse style: Conversation, Read Aloud (drag-&-drop now inside), Try It Out, Casting panel. - Read Aloud: reordered (settings → toolbar → document → transport/synth) and the document fits the viewport height so controls below stay visible; remove the redundant My Books card (lives in Library → Books). - Conversation: stacked full-width config, fills viewport height; fix intermittent webm decode in hands-free mode (recorder restarts cleanly, in-browser WAV encode); barge-in via Live agent. - Fix casting feed overflow that pushed the sidebar off-screen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fb7dcb52f8
commit
a4128788d6
19
CHANGELOG.md
19
CHANGELOG.md
@ -9,6 +9,25 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.12.0] — 2026-06-27
|
||||
|
||||
### Added
|
||||
- **Slim icon-rail sidebar**: the button where the flag used to be collapses the sidebar to a narrow icon rail (desktop); hovering the rail flies the full menu out as an overlay (titles + nested items). State persists.
|
||||
- **Language picker moved to Settings → General** (was the sidebar flag). English / Deutsch.
|
||||
- **Collapsible settings panels** (using the app's standard `card` collapse style) to free vertical space: Conversation engine/prompt config, Read Aloud voice & synthesis settings (with the drag-&-drop inside), the Try It Out voice/playback box, and the Casting-audiobook LLM/prompt panel (prompt collapsed by default).
|
||||
|
||||
### Changed
|
||||
- **Read Aloud layout** reorganised: collapsible settings + drag-&-drop on top, zoom toolbar above the document, the document as the central area that **fits the viewport height**, transport + synthesis controls below it (always visible — only the document scrolls).
|
||||
- **Read Aloud "My Books"** card removed — saved books now live in the combined **Library → Books**.
|
||||
- **Conversation Playground** fills the viewport height (only the chat scrolls), and the config stacks full-width.
|
||||
- Collapsible-card initialisation now also runs when a section opens, so lazily-loaded sections get consistent collapse chevrons.
|
||||
|
||||
### Fixed
|
||||
- **Conversation microphone failed intermittently** ("EBML header parsing failed") in hands-free/live mode: a silence reset cleared the recording buffer in place, dropping the webm header so later utterances were undecodable. The recorder now restarts cleanly, keeping every utterance valid. (Recordings are also decoded in-browser to 16 kHz WAV, bypassing server ffmpeg.) Barge-in (interrupt the agent while it speaks) works via the **Live agent** toggle.
|
||||
- **Casting-audiobook overflow**: a long unbroken passage string widened the layout and pushed the sidebar off-screen; the feed now wraps and is width-constrained.
|
||||
|
||||
---
|
||||
|
||||
## [1.10.1] — 2026-06-27
|
||||
|
||||
### Added
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.10.1">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.12.0">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -107,13 +107,17 @@
|
||||
|
||||
<!-- App shell -->
|
||||
<div id="app-shell">
|
||||
<button id="sidebar-expand-btn" class="sidebar-expand-btn" type="button" title="Expand sidebar" aria-label="Expand sidebar" onclick="toggleSidebarCollapsed()"><span class="mdi mdi-menu"></span></button>
|
||||
|
||||
<!-- Sidebar (renders immediately — no JS needed) -->
|
||||
<aside id="sidebar" tabindex="0" aria-label="Navigation">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-brand-text">
|
||||
<h1><span class="mdi mdi-microphone-variant"></span> Voice Creator</h1>
|
||||
<p>Clone · Design · Deploy</p>
|
||||
</div>
|
||||
<button id="sidebar-collapse-btn" class="sidebar-collapse-btn" type="button" title="Collapse sidebar" aria-label="Collapse sidebar" onclick="toggleSidebarCollapsed()"><span class="mdi mdi-chevron-double-left"></span></button>
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<div class="nav-group-label">Voices</div>
|
||||
@ -332,7 +336,7 @@
|
||||
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.10.1"></script>
|
||||
<script src="/static/loader.js?v=1.12.0"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -623,7 +623,7 @@ initLlmSnippets(document);
|
||||
|
||||
// ── Collapsible cards ──────────────────────────────────────────────────────
|
||||
|
||||
(function initCollapsibleCards() {
|
||||
window.initCollapsibleCards = function initCollapsibleCards() {
|
||||
const LS_KEY = 'card-collapse-v1';
|
||||
const load = () => { try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } catch { return {}; } };
|
||||
const save = (k, v) => { const s = load(); s[k] = v; try { localStorage.setItem(LS_KEY, JSON.stringify(s)); } catch {} };
|
||||
@ -677,7 +677,8 @@ initLlmSnippets(document);
|
||||
save(key, nowOpen);
|
||||
});
|
||||
});
|
||||
})();
|
||||
};
|
||||
window.initCollapsibleCards();
|
||||
|
||||
// ── Collapsible integration cards (collapsed by default) ───────────────────
|
||||
|
||||
|
||||
@ -198,7 +198,9 @@ function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) {
|
||||
<span class="mdi mdi-drama-masks"></span>
|
||||
<span class="ab-castpanel-title">Casting audiobook</span>
|
||||
<span class="ab-castpanel-count" id="ab-cv-count">passage 0 / ${total}</span>
|
||||
<button class="ab-castpanel-btn" id="ab-cv-settings-toggle" title="Show / hide casting settings" style="width:auto; padding:2px 6px; margin-left:6px;"><span class="mdi mdi-chevron-up" id="ab-cv-settings-chevron"></span></button>
|
||||
<span style="flex:1"></span>
|
||||
<span id="ab-cv-settings" style="display:flex; align-items:center; gap:4px;">
|
||||
<label for="ab-cv-llm-url" style="font-size:12px; font-weight:600; color:var(--subtext); margin-right:4px;">LLM Engine</label>
|
||||
<select id="ab-cv-llm-url" style="width:260px; margin-right:4px; padding:2px 6px; font-size:12px; border:1px solid var(--border); border-radius:4px; background:var(--surface); color:var(--text); outline:none; appearance:auto; cursor:pointer;" aria-label="LLM API URL">
|
||||
<option value="${escHtml(llmUrl)}">${escHtml(llmUrl || '— Select an endpoint —')}</option>
|
||||
@ -209,8 +211,9 @@ function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) {
|
||||
</select>
|
||||
<span style="margin-right:8px;"></span>
|
||||
<button class="ab-castpanel-btn" id="ab-cv-prompt-btn" title="Edit Casting Prompt" style="display:flex; align-items:center; gap:4px; padding:4px 8px; font-weight:600; font-size:12px; width:auto; height:auto; min-height:28px;"><span class="mdi mdi-text-box-edit-outline"></span> Prompt <span class="mdi mdi-chevron-down" id="ab-cv-prompt-chevron"></span></button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="ab-castpanel-prompt" id="ab-cv-prompt-panel" style="padding:10px 12px; background:var(--panel); border-bottom:1px solid var(--border);">
|
||||
<div class="ab-castpanel-prompt" id="ab-cv-prompt-panel" hidden style="padding:10px 12px; background:var(--panel); border-bottom:1px solid var(--border);">
|
||||
<textarea id="ab-cv-prompt-text" rows="8" style="width:100%; font-family:monospace; font-size:13px; font-weight:500; padding:10px; line-height:1.5; border:1px solid var(--border); border-radius:6px; background:var(--surface); color:var(--text); resize:vertical;" placeholder="LLM casting instructions..."></textarea>
|
||||
<div style="display:flex; justify-content:flex-end; margin-top:8px; gap:8px; align-items:center;">
|
||||
<select id="ab-cv-prompt-lib" class="btn-secondary btn-sm" style="max-width:200px; outline:none; border:1px solid var(--border); border-radius:4px; padding:4px 8px; font-size:12px; background:var(--surface);" aria-label="Load a saved prompt">
|
||||
@ -299,6 +302,26 @@ STRIKTE FORMAT- UND TEXTREGELN:
|
||||
}
|
||||
});
|
||||
|
||||
// Collapse the whole settings block (LLM engine + prompt) to free vertical space.
|
||||
(function () {
|
||||
const tBtn = panel.querySelector('#ab-cv-settings-toggle');
|
||||
const sBox = panel.querySelector('#ab-cv-settings');
|
||||
const chev = panel.querySelector('#ab-cv-settings-chevron');
|
||||
function apply(collapsed) {
|
||||
if (sBox) sBox.style.display = collapsed ? 'none' : 'flex';
|
||||
if (collapsed) { const p = panel.querySelector('#ab-cv-prompt-panel'); if (p) p.hidden = true; }
|
||||
if (chev) chev.className = 'mdi ' + (collapsed ? 'mdi-chevron-down' : 'mdi-chevron-up');
|
||||
}
|
||||
let collapsed = false;
|
||||
try { collapsed = localStorage.getItem('ttsvc_ab_settings_collapsed') === '1'; } catch (_) {}
|
||||
apply(collapsed);
|
||||
if (tBtn) tBtn.addEventListener('click', () => {
|
||||
collapsed = !collapsed;
|
||||
try { localStorage.setItem('ttsvc_ab_settings_collapsed', collapsed ? '1' : '0'); } catch (_) {}
|
||||
apply(collapsed);
|
||||
});
|
||||
})();
|
||||
|
||||
// Prompt Library logic
|
||||
let savedPrompts = [];
|
||||
try { savedPrompts = JSON.parse(localStorage.getItem('ttsvc_ab_prompts') || '[]'); } catch (_) { savedPrompts = []; }
|
||||
|
||||
@ -758,18 +758,13 @@ $('s-import-voices-file')?.addEventListener('change', async function () {
|
||||
}
|
||||
if (silence >= silenceLimit) {
|
||||
if (!vadHadSpeech) {
|
||||
if (liveMicEnabled) {
|
||||
// Hot mic: keep listening through silence without retaining silent chunks forever.
|
||||
recChunks = [];
|
||||
recStart = Date.now();
|
||||
vadLastVoice = Date.now();
|
||||
if (micStatus) micStatus.textContent = 'Listening…';
|
||||
vadRafId = requestAnimationFrame(vadTick);
|
||||
return;
|
||||
}
|
||||
// Silence the whole time - cancel without calling STT
|
||||
// Silence with no speech → cancel this segment (don't call STT).
|
||||
// In hot-mic mode onstop restarts recording with a FRESH
|
||||
// MediaRecorder, so every utterance keeps its own webm/EBML
|
||||
// header and stays decodable. (Clearing recChunks in place would
|
||||
// drop the header chunk → "EBML header parsing failed".)
|
||||
cancelNextBlob = true;
|
||||
if (micStatus) micStatus.textContent = 'Ready';
|
||||
if (micStatus) micStatus.textContent = liveMicEnabled ? 'Listening…' : 'Ready';
|
||||
}
|
||||
stopRecording();
|
||||
return;
|
||||
|
||||
@ -121,27 +121,20 @@ window.setAppLang = function (lang) {
|
||||
location.reload();
|
||||
};
|
||||
|
||||
// Flag of a language code (flag-icons classes; English → GB).
|
||||
function _langFlag(code) { return code === 'en' ? 'gb' : code; }
|
||||
|
||||
// Inject a flag toggle next to the "Voice Creator" headline. Clicking cycles to
|
||||
// the next language (a simple toggle while there are two).
|
||||
window.initLangPicker = function () {
|
||||
const brand = document.querySelector('.sidebar-brand');
|
||||
const head = brand && brand.querySelector('h1');
|
||||
if (!head || document.getElementById('app-lang-toggle')) return;
|
||||
const codes = Object.keys(I18N_LANGS);
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'app-lang-toggle';
|
||||
btn.className = 'app-lang-toggle';
|
||||
btn.type = 'button';
|
||||
const nextOf = () => codes[(codes.indexOf(_appLang) + 1) % codes.length];
|
||||
const render = () => {
|
||||
btn.innerHTML = `<span class="fi fi-${_langFlag(_appLang)}"></span>`;
|
||||
const label = 'Language: ' + I18N_LANGS[_appLang] + ' — switch to ' + I18N_LANGS[nextOf()];
|
||||
btn.title = label; btn.setAttribute('aria-label', label);
|
||||
};
|
||||
render();
|
||||
btn.addEventListener('click', () => window.setAppLang(nextOf()));
|
||||
head.appendChild(btn);
|
||||
// ── Language picker — lives in Settings → General ────────────────────────────
|
||||
// Populate + sync the <select id="s-lang-select">. The Settings fragment loads
|
||||
// lazily, so this is idempotent and safe to call whenever Settings opens.
|
||||
window.populateLangSelect = function () {
|
||||
const sel = document.getElementById('s-lang-select');
|
||||
if (!sel) return;
|
||||
if (!sel.dataset.filled) {
|
||||
sel.innerHTML = Object.keys(I18N_LANGS)
|
||||
.map(c => `<option value="${c}">${I18N_LANGS[c]}</option>`).join('');
|
||||
sel.dataset.filled = '1';
|
||||
}
|
||||
sel.value = _appLang;
|
||||
};
|
||||
|
||||
// Back-compat: loader.js calls initLangPicker() once at startup. The picker now
|
||||
// lives in Settings → General, so this just tries to populate it.
|
||||
window.initLangPicker = function () { window.populateLangSelect(); };
|
||||
|
||||
@ -1481,3 +1481,4 @@ window.readerOnShow = function () {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@ -471,6 +471,7 @@ async function loadSettings() {
|
||||
if (seedTextEl) seedTextEl.value = s.seed_finder_text || '';
|
||||
const themeEl = $('s-theme-select');
|
||||
if (themeEl) themeEl.value = document.documentElement.dataset.theme || 'dark';
|
||||
if (window.populateLangSelect) window.populateLangSelect();
|
||||
// Captures settings
|
||||
const sttLang = $('s-stt-language'); if (sttLang) sttLang.value = s.stt_language || '';
|
||||
const sttPref = $('s-stt-preferred-backend'); if (sttPref) sttPref.value = s.stt_preferred_backend || '';
|
||||
@ -603,6 +604,7 @@ document.addEventListener('click', async e => { if (!e.target.closest('.s-save-b
|
||||
// Theme select in General settings
|
||||
document.addEventListener('change', e => {
|
||||
if (e.target.id === 's-theme-select') applyTheme(e.target.value);
|
||||
if (e.target.id === 's-lang-select' && window.setAppLang) window.setAppLang(e.target.value);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -125,6 +125,7 @@
|
||||
if (sectionId !== 's-reader' && typeof window.readerStop === 'function') window.readerStop();
|
||||
if (sectionId === 's-reader' && typeof window.readerOnShow === 'function') window.readerOnShow();
|
||||
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) {
|
||||
@ -369,6 +370,15 @@
|
||||
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';
|
||||
};
|
||||
|
||||
// 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 () {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
|
||||
<!-- Config bar -->
|
||||
<div class="conv-config-bar card" style="padding:14px">
|
||||
<h2><span class="mdi mdi-tune-variant"></span> Engines & prompt</h2>
|
||||
<div class="engine-setup-row" style="margin-bottom:8px">
|
||||
<div class="engine-setup-col">
|
||||
<label class="engine-setup-label"><span class="mdi mdi-microphone"></span> Speech to Text <a href="#" class="engine-setup-link" onclick="navTo('s-llms'); navEnginesCat('stt'); return false;" title="Configure Speech to Text settings"><span class="mdi mdi-cog-outline"></span></a></label>
|
||||
|
||||
@ -9,8 +9,9 @@
|
||||
<div class="tab-content" id="tab-reader">
|
||||
|
||||
<div id="reader-main-view">
|
||||
<!-- ① Import + controls ─────────────────────────────────────────── -->
|
||||
<div class="card" style="padding:14px">
|
||||
<!-- ① Import + controls (collapsible) ───────────────────────────── -->
|
||||
<div class="card reader-config-card" id="reader-config-card" style="padding:14px">
|
||||
<h2><span class="mdi mdi-tune-variant"></span> Voice & synthesis settings</h2>
|
||||
<div class="engine-setup-row" style="margin-bottom:8px">
|
||||
<div class="engine-setup-col" style="flex:1">
|
||||
<label class="engine-setup-label"><span class="mdi mdi-account-voice"></span> Text to Speech <a href="#" class="engine-setup-link" onclick="navTo('s-llms'); navEnginesCat('tts'); return false;" title="Configure Text to Speech settings"><span class="mdi mdi-cog-outline"></span></a></label>
|
||||
@ -59,17 +60,38 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="reader-backend-hint" id="reader-backend-hint" style="padding-left:14px; margin-top:8px"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ② Drag & drop / empty state ─────────────────────────────────── -->
|
||||
<div class="reader-dropzone" id="reader-dropzone">
|
||||
<!-- ② Drag & drop / empty state (inside the collapsible box) ────── -->
|
||||
<div class="reader-dropzone" id="reader-dropzone" style="margin-top:12px">
|
||||
<span class="mdi mdi-cloud-upload-outline reader-dropzone-icon"></span>
|
||||
<strong>Drag & drop a document here</strong>
|
||||
<span class="note">.pdf · .txt · .md — or use the Import button above</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ③ Transport ─────────────────────────────────────────────────── -->
|
||||
<!-- ③ Zoom toolbar + status legend (PDF) — above the document ────── -->
|
||||
<div class="reader-toolbar" id="reader-toolbar" hidden>
|
||||
<div class="reader-zoom" id="reader-zoom">
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-fitw" title="Fit width"><span class="mdi mdi-arrow-expand-horizontal"></span> Fit width</button>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-fith" title="Fit height"><span class="mdi mdi-arrow-expand-vertical"></span> Fit height</button>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-two" title="Two pages side by side"><span class="mdi mdi-book-open-outline"></span> Two pages</button>
|
||||
<span class="reader-zoom-sep"></span>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-out" title="Zoom out"><span class="mdi mdi-minus"></span></button>
|
||||
<span class="reader-zoom-pct" id="reader-zoom-pct">100%</span>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-in" title="Zoom in"><span class="mdi mdi-plus"></span></button>
|
||||
</div>
|
||||
<div class="reader-legend">
|
||||
<span class="reader-leg reader-leg-pending">Not synthesised</span>
|
||||
<span class="reader-leg reader-leg-synth">Synthesising</span>
|
||||
<span class="reader-leg reader-leg-ready">Ready</span>
|
||||
<span class="reader-leg reader-leg-reading">Reading</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ④ Document viewport — central main content ───────────────────── -->
|
||||
<div class="reader-doc" id="reader-doc"></div>
|
||||
|
||||
<!-- ⑤ Transport — below the document ─────────────────────────────── -->
|
||||
<div class="reader-transport" id="reader-transport" hidden>
|
||||
<div class="reader-tb-group">
|
||||
<button class="reh-tb-btn" id="reader-prev" title="Previous sentence"><span class="mdi mdi-skip-previous"></span></button>
|
||||
@ -84,7 +106,7 @@
|
||||
<span class="reader-doc-title" id="reader-doc-title"></span>
|
||||
</div>
|
||||
|
||||
<!-- ④ Synthesise-ahead bar ──────────────────────────────────────── -->
|
||||
<!-- ⑥ Synthesise-ahead bar — below the document ──────────────────── -->
|
||||
<div class="reader-synthbar" id="reader-synthbar" hidden>
|
||||
<button class="btn-primary btn-sm" id="reader-synth-all" title="Pre-synthesise the chosen range for gap-free reading">
|
||||
<span class="mdi mdi-lightning-bolt"></span> Synthesise <span id="reader-synth-scope">all</span>
|
||||
@ -117,40 +139,11 @@
|
||||
<button class="btn-secondary btn-sm" id="reader-synth-cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⑤ Zoom toolbar + status legend (PDF) ────────────────────────── -->
|
||||
<div class="reader-toolbar" id="reader-toolbar" hidden>
|
||||
<div class="reader-zoom" id="reader-zoom">
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-fitw" title="Fit width"><span class="mdi mdi-arrow-expand-horizontal"></span> Fit width</button>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-fith" title="Fit height"><span class="mdi mdi-arrow-expand-vertical"></span> Fit height</button>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-two" title="Two pages side by side"><span class="mdi mdi-book-open-outline"></span> Two pages</button>
|
||||
<span class="reader-zoom-sep"></span>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-out" title="Zoom out"><span class="mdi mdi-minus"></span></button>
|
||||
<span class="reader-zoom-pct" id="reader-zoom-pct">100%</span>
|
||||
<button class="btn-secondary btn-sm" id="reader-zoom-in" title="Zoom in"><span class="mdi mdi-plus"></span></button>
|
||||
</div>
|
||||
<div class="reader-legend">
|
||||
<span class="reader-leg reader-leg-pending">Not synthesised</span>
|
||||
<span class="reader-leg reader-leg-synth">Synthesising</span>
|
||||
<span class="reader-leg reader-leg-ready">Ready</span>
|
||||
<span class="reader-leg reader-leg-reading">Reading</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⑥ Document viewport ─────────────────────────────────────────── -->
|
||||
<div class="reader-doc" id="reader-doc"></div>
|
||||
</div><!-- /reader-main-view -->
|
||||
|
||||
<!-- ⑦ Audiobook Casting Panel ──────────────────────────────────── -->
|
||||
<div id="reader-audiobook-panel" hidden></div>
|
||||
|
||||
<!-- ⑧ Saved books library ─────────────────────────────────────────── -->
|
||||
<div class="card reader-library-card" id="reader-library-card" hidden>
|
||||
<div class="reader-lib-head">
|
||||
<h2 style="margin:0"><span class="mdi mdi-bookshelf"></span> My books</h2>
|
||||
<span class="note">Saved documents with their synthesised audio — reopen to continue.</span>
|
||||
</div>
|
||||
<div class="reader-lib-list" id="reader-lib-list"></div>
|
||||
</div>
|
||||
<!-- Saved books now live in the combined Library → Books section. -->
|
||||
|
||||
</div><!-- /tab-reader -->
|
||||
|
||||
@ -24,6 +24,11 @@
|
||||
</select>
|
||||
<span class="s-hint">Switch between light and dark interface. Saved instantly.</span>
|
||||
</div>
|
||||
<div class="s-field">
|
||||
<label>Language</label>
|
||||
<select id="s-lang-select"></select>
|
||||
<span class="s-hint">Interface language. Saved instantly — the app reloads to apply.</span>
|
||||
</div>
|
||||
<div class="s-field" style="grid-column: 1 / -1">
|
||||
<label>Default Seed Finder text</label>
|
||||
<textarea id="s-seed-finder-text" placeholder="Leave empty for language-specific defaults..." style="min-height:60px; font-family:inherit; resize:vertical;"></textarea>
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
|
||||
<!-- ① Voice selection ─────────────────────────────────────────── -->
|
||||
<div class="card" style="padding:14px">
|
||||
<h2><span class="mdi mdi-account-voice"></span> Voice & playback</h2>
|
||||
<div class="engine-setup-row" style="margin-bottom:8px">
|
||||
<div class="engine-setup-col" style="flex:1">
|
||||
<label class="engine-setup-label"><span class="mdi mdi-account-voice"></span> Text to Speech <a href="#" class="engine-setup-link" onclick="navTo('s-llms'); navEnginesCat('tts'); return false;" title="Configure Text to Speech settings"><span class="mdi mdi-cog-outline"></span></a></label>
|
||||
|
||||
@ -102,6 +102,53 @@ body {
|
||||
.sidebar-brand {
|
||||
padding: 18px 16px 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: flex-start; justify-content: space-between; gap: 8px;
|
||||
}
|
||||
.sidebar-brand-text { min-width: 0; }
|
||||
.sidebar-collapse-btn, .sidebar-expand-btn {
|
||||
background: none; border: none; cursor: pointer; color: var(--subtext);
|
||||
padding: 4px; border-radius: 6px; font-size: 18px; line-height: 1;
|
||||
flex-shrink: 0; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.sidebar-collapse-btn:hover { background: var(--panel); color: var(--text); }
|
||||
#sidebar { transition: width .18s ease; }
|
||||
.sidebar-expand-btn { display: none; }
|
||||
|
||||
/* ── Collapsed = slim icon rail; hovering it flies the full menu out as an
|
||||
overlay (icons always visible, titles + nested items appear on hover) ── */
|
||||
@media (min-width: 768px) {
|
||||
body.sidebar-collapsed #sidebar { width: 56px; overflow-x: hidden; }
|
||||
body.sidebar-collapsed #sidebar:hover {
|
||||
width: var(--sidebar-width); overflow-x: hidden;
|
||||
position: absolute; top: 0; bottom: 0; left: 0; z-index: 60;
|
||||
box-shadow: 6px 0 24px rgba(0,0,0,.22);
|
||||
}
|
||||
/* Rail (not hovered): show only the leading icons, centered */
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-label,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-badge,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .ntc,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-chevron,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-group-label,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-group-head,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .sidebar-brand-text { display: none; }
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .sidebar-brand { justify-content: center; padding-left: 0; padding-right: 0; }
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-item,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-tree-item,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-tree-head {
|
||||
padding-left: 18px; padding-right: 0; gap: 0; white-space: nowrap;
|
||||
font-size: 0; /* hide raw-text labels + counts, keep the icon */
|
||||
}
|
||||
/* restore the leading icon size (its text was zeroed above) */
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-item .mdi,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-tree-item .mdi,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-tree-head .mdi { font-size: 15px; }
|
||||
/* nested tree-items: align their icon into the rail too */
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-tree .nav-tree-item,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-tree .nav-tree-head.nav-subhead,
|
||||
body.sidebar-collapsed #sidebar:not(:hover) .nav-tree .nav-tree .nav-tree-item { padding-left: 18px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.sidebar-collapse-btn, .sidebar-expand-btn { display: none !important; }
|
||||
}
|
||||
.sidebar-brand h1 {
|
||||
font-size: 15px; font-weight: 800; color: var(--accent);
|
||||
@ -3018,11 +3065,44 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
|
||||
/* Layout */
|
||||
.conv-main { display: flex; gap: 14px; margin-top: 14px; }
|
||||
/* Chat panel: fixed height so the input bar never gets pushed off-screen */
|
||||
/* Chat panel grows to fill remaining height; input bar stays pinned at bottom */
|
||||
.conv-chat-panel {
|
||||
flex: 1; display: flex; flex-direction: column; gap: 0; min-width: 0;
|
||||
height: calc(100vh - 340px); min-height: 440px;
|
||||
}
|
||||
|
||||
/* Collapsible engine/prompt config */
|
||||
.conv-config-toggle {
|
||||
display: flex; align-items: center; gap: 8px; cursor: pointer;
|
||||
font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em;
|
||||
color: var(--subtext); user-select: none; padding-bottom: 2px;
|
||||
}
|
||||
.conv-config-toggle:hover { color: var(--text); }
|
||||
.conv-config-toggle .conv-config-chevron { margin-left: auto; transition: transform .18s ease; font-size: 18px; }
|
||||
.conv-config-summary { font-weight: 500; text-transform: none; letter-spacing: 0; opacity: .8; font-size: 11.5px; }
|
||||
.conv-config-body { margin-top: 10px; }
|
||||
.conv-config-bar.collapsed .conv-config-body { display: none; }
|
||||
.conv-config-bar.collapsed .conv-config-chevron { transform: rotate(180deg); }
|
||||
.conv-config-bar.collapsed .conv-config-summary { display: inline; }
|
||||
.conv-config-bar:not(.collapsed) .conv-config-summary { display: none; }
|
||||
|
||||
/* Desktop: make the Conversation fill the viewport height — no page scroll;
|
||||
only the chat window scrolls internally. Collapsing the config frees space. */
|
||||
@media (min-width: 768px) {
|
||||
#s-conversation.is-active { display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
||||
#s-conversation .conv-main { flex: 1; min-height: 0; margin-top: 12px; }
|
||||
#s-conversation .conv-chat-panel { height: auto; min-height: 0; }
|
||||
#s-conversation .conv-stats-panel { overflow-y: auto; }
|
||||
}
|
||||
|
||||
/* Read Aloud — fit the document area into the viewport height so the transport
|
||||
+ synthesise bar below it stay visible (only the document scrolls). */
|
||||
@media (min-width: 768px) {
|
||||
#s-reader.is-active { display: flex; flex-direction: column; height: 100%; min-height: 0; }
|
||||
#s-reader #tab-reader { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
#s-reader #reader-main-view { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
||||
#s-reader #reader-doc { flex: 1; min-height: 0; overflow-y: auto; }
|
||||
}
|
||||
.conv-stats-panel { width: 280px; flex-shrink: 0; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; display: flex; flex-direction: column; gap: 8px; }
|
||||
|
||||
/* Chat window fills the space above the input bar; warning box scrolls inside it */
|
||||
@ -4218,9 +4298,14 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ab-cv-body { display: grid; grid-template-columns: 1fr 220px; gap: 12px; padding: 12px; flex: 1; min-height: 0; }
|
||||
.ab-cv-feed-wrap { position: relative; min-height: 0; display: flex; flex-direction: column; }
|
||||
.ab-cv-feed { flex: 1; min-height: 0; overflow-y: auto; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; background: var(--panel); display: flex; flex-direction: column; gap: 3px; }
|
||||
.ab-cv-body { display: grid; grid-template-columns: minmax(0, 1fr) 220px; gap: 12px; padding: 12px; flex: 1; min-height: 0; min-width: 0; }
|
||||
.ab-cv-feed-wrap { position: relative; min-height: 0; min-width: 0; display: flex; flex-direction: column; }
|
||||
.ab-cv-feed { flex: 1; min-height: 0; min-width: 0; overflow-y: auto; overflow-x: hidden; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; background: var(--panel); display: flex; flex-direction: column; gap: 3px; }
|
||||
/* Long unbroken passage strings must wrap, never widen the layout (it was
|
||||
pushing the sidebar out of frame). */
|
||||
.ab-cv-row { min-width: 0; overflow-wrap: anywhere; word-break: break-word; }
|
||||
.ab-cv-llm-preview { overflow-wrap: anywhere; word-break: break-word; }
|
||||
.ab-cv-llm-pre { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; max-width: 100%; margin: 0; }
|
||||
.ab-cv-jump-btn {
|
||||
position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user