diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e1c32a..a11e9e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.14.19] — 2026-07-09 + +### Changed +- **Cast Characters is now a dedicated page instead of a popup** — the audiobook cast flow now has a new `Cast Characters` stage between `Cast Audiobook` and `Cast`, with the old `Cast Characters` stage renamed to `Cast`. The new page keeps the cast-character content and actions, but uses the same large-page layout as `Cast Audiobook` so the UI is easier to work with. + +### Fixed +- **Conversation Playground thinking text was no longer visible while streaming** — the reasoning panel now auto-opens as soon as the first `thinking` chunk arrives, instead of staying collapsed until clicked. + ## [1.14.18] — 2026-07-09 ### Fixed @@ -1631,7 +1639,8 @@ Initial feature-complete release. --- -[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.8.1...HEAD +[Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.19...HEAD +[1.14.19]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.18...v1.14.19 [1.8.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.7.0...v1.8.0 [1.2.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.1.0...v1.2.0 diff --git a/VERSION b/VERSION index c442523..6620f60 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.14.18 +1.14.19 diff --git a/static/index.html b/static/index.html index 99db6ba..f913522 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 3284b6d..c872f62 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -3291,7 +3291,7 @@ STRIKTE FORMAT- UND TEXTREGELN: const continueBtnHtml = resumable ? `` : ''; - foot.innerHTML = `${continueBtnHtml}`; + foot.innerHTML = `${continueBtnHtml}`; const runVerificationPass = () => { const verificationPrompt = `Du bist ein Qualitätsprüfer für die Analyse eines deutschen Hörbuchs. Eine erste KI hat den Textauszug bereits in Segmente unterteilt. Deine Aufgabe ist es, unbekannte Sprecher zu lösen und falsche Unknown/Narrator-Zuweisungen zu korrigieren, ohne bereits klare Sprecher unnötig zu verändern. @@ -3388,7 +3388,7 @@ ABSOLUTE REGELN: const wrap = document.createElement('span'); wrap.className = 'ab-cv-castchars-group'; wrap.innerHTML = - '' + + '' + ''; castBtn.replaceWith(wrap); wrap.querySelector('.ab-cv-castchars-view').addEventListener('click', () => { diff --git a/static/js/character-sheets.js b/static/js/character-sheets.js index a8deeb7..70d7014 100644 --- a/static/js/character-sheets.js +++ b/static/js/character-sheets.js @@ -63,6 +63,7 @@ function csLlmUrl() { } function csLlmModel() { return $('reh-llm-model')?.value || ''; } function csLang() { return $('reh-design-lang')?.value || ''; } +function csReaderPageHost() { return $('reader-charsheets-panel'); } // Build page-annotated text from the current reader scope ([p.N] at page changes). function csReaderText() { @@ -324,12 +325,12 @@ async function csGenerateStream(body, onDelta, outerSignal, idleTimeoutMs = 6000 } } -async function csGenerate(text, cacheKey, initialRoster) { +async function csGenerate(text, cacheKey, initialRoster, opts = {}) { if (_cs.running) return null; if (!text) { toast('Nothing to analyse', 'error'); return null; } const chunks = (typeof splitTextIntoChunks === 'function') ? splitTextIntoChunks(text, CS_CHUNK_CHARS) : [text]; _cs.running = true; _cs.cancel = false; - const prog = csProgress(chunks.length); + const prog = csProgress(chunks.length, opts.pageHost || null); const llm_url = csLlmUrl(), model = csLlmModel(); const selectedLanguage = csLang(); let language = selectedLanguage; @@ -409,16 +410,24 @@ async function csGenerate(text, cacheKey, initialRoster) { return sheets; } -function csProgress(total) { - let ov = document.getElementById('cs-progress'); - if (!ov) { - ov = document.createElement('div'); - ov.id = 'cs-progress'; ov.className = 'audiobook-overlay'; +function csProgress(total, hostEl = null) { + const usingHost = !!hostEl; + let root = usingHost ? hostEl : document.getElementById('cs-progress'); + if (!root) { + if (usingHost) return null; + root = document.createElement('div'); + root.id = 'cs-progress'; + root.className = 'audiobook-overlay'; + document.body.appendChild(root); + } + const needsShell = !root.querySelector('#cs-progress-fill'); + if (needsShell) { + if (usingHost) root.innerHTML = ''; // Big two-pane layout, mirroring the audiobook casting live view: the // passage being analysed on the left, the LLM's raw JSON output filling // in live on the right, and a character sidebar (current one highlighted) // so the user can watch each sheet actually being built. - ov.innerHTML = `
+ root.innerHTML = `
Character sheets @@ -456,46 +465,47 @@ function csProgress(total) {
`; - document.body.appendChild(ov); - ov.querySelector('#cs-progress-cancel').addEventListener('click', () => { _cs.cancel = true; }); + root.querySelector('#cs-progress-cancel')?.addEventListener('click', () => { _cs.cancel = true; }); // Prompt panel — mirrors the audiobook casting "Prompt" button: toggle // visibility, load/save/delete named presets (localStorage), and persist // the active text as the settings default so csGenerate() picks it up. - const promptBtn = ov.querySelector('#cs-prompt-btn'); - const promptPanel = ov.querySelector('#cs-prompt-panel'); - const promptText = ov.querySelector('#cs-prompt-text'); - promptText.value = (typeof _appSettings !== 'undefined' && _appSettings.character_sheets_prompt) || CS_DEFAULT_PROMPT; - promptBtn.addEventListener('click', () => { + const promptBtn = root.querySelector('#cs-prompt-btn'); + const promptPanel = root.querySelector('#cs-prompt-panel'); + const promptText = root.querySelector('#cs-prompt-text'); + if (promptText) promptText.value = (typeof _appSettings !== 'undefined' && _appSettings.character_sheets_prompt) || CS_DEFAULT_PROMPT; + promptBtn?.addEventListener('click', () => { + if (!promptPanel) return; promptPanel.hidden = !promptPanel.hidden; - const chevron = ov.querySelector('#cs-prompt-chevron'); + const chevron = root.querySelector('#cs-prompt-chevron'); if (chevron) chevron.className = promptPanel.hidden ? 'mdi mdi-chevron-down' : 'mdi mdi-chevron-up'; }); let savedCsPrompts = []; try { savedCsPrompts = JSON.parse(localStorage.getItem('ttsvc_cs_prompts') || '[]'); } catch (_) { savedCsPrompts = []; } - const libSelect = ov.querySelector('#cs-prompt-lib'); - const delBtn = ov.querySelector('#cs-prompt-del'); - const nameInput = ov.querySelector('#cs-prompt-name'); + const libSelect = root.querySelector('#cs-prompt-lib'); + const delBtn = root.querySelector('#cs-prompt-del'); + const nameInput = root.querySelector('#cs-prompt-name'); const renderCsPromptLib = (selectedIdx = -1) => { + if (!libSelect || !delBtn) return; libSelect.innerHTML = '' + savedCsPrompts.map((p, i) => ``).join(''); if (selectedIdx >= 0) { libSelect.value = selectedIdx; delBtn.style.display = 'inline-flex'; } else { libSelect.value = ''; delBtn.style.display = 'none'; } }; renderCsPromptLib(); - libSelect.addEventListener('change', () => { + libSelect?.addEventListener('change', () => { const idx = parseInt(libSelect.value, 10); if (!isNaN(idx) && savedCsPrompts[idx]) { - promptText.value = savedCsPrompts[idx].prompt; - nameInput.value = savedCsPrompts[idx].name; + if (promptText) promptText.value = savedCsPrompts[idx].prompt; + if (nameInput) nameInput.value = savedCsPrompts[idx].name; delBtn.style.display = 'inline-flex'; } else { - nameInput.value = ''; + if (nameInput) nameInput.value = ''; delBtn.style.display = 'none'; } }); - delBtn.addEventListener('click', () => { + delBtn?.addEventListener('click', () => { const idx = parseInt(libSelect.value, 10); if (isNaN(idx)) return; if (confirm('Delete this saved prompt preset?')) { @@ -505,11 +515,11 @@ function csProgress(total) { toast('Prompt deleted', 'success'); } }); - ov.querySelector('#cs-prompt-save').addEventListener('click', async () => { - const val = promptText.value.trim(); + root.querySelector('#cs-prompt-save')?.addEventListener('click', async () => { + const val = promptText?.value.trim() || ''; if (!val) { toast('Prompt is empty', 'error'); return; } - const name = nameInput.value.trim() || 'Custom Prompt ' + (savedCsPrompts.length + 1); - let targetIdx = parseInt(libSelect.value, 10); + const name = nameInput?.value.trim() || 'Custom Prompt ' + (savedCsPrompts.length + 1); + let targetIdx = parseInt(libSelect?.value || '', 10); if (!isNaN(targetIdx) && savedCsPrompts[targetIdx] && savedCsPrompts[targetIdx].name === name) { savedCsPrompts[targetIdx].prompt = val; } else { @@ -524,7 +534,7 @@ function csProgress(total) { // per-passage prompt lookup — and the next time this panel opens — // always reflect the latest text, including mid-run edits. let promptSaveTimer = null; - promptText.addEventListener('input', () => { + promptText?.addEventListener('input', () => { clearTimeout(promptSaveTimer); promptSaveTimer = setTimeout(() => { const val = promptText.value; @@ -533,14 +543,14 @@ function csProgress(total) { }, 500); }); } - ov.style.display = 'flex'; - const fill = ov.querySelector('#cs-progress-fill'); - const msg = ov.querySelector('#cs-progress-msg'); - const chars = ov.querySelector('#cs-progress-chars'); - const sideTitle = ov.querySelector('#cs-progress-side-title'); - const passagePre = ov.querySelector('#cs-progress-passage-pre'); - const livePre = ov.querySelector('#cs-progress-live-pre'); - const liveStatus = ov.querySelector('#cs-progress-live-status'); + if (!usingHost) root.style.display = 'flex'; + const fill = root.querySelector('#cs-progress-fill'); + const msg = root.querySelector('#cs-progress-msg'); + const chars = root.querySelector('#cs-progress-chars'); + const sideTitle = root.querySelector('#cs-progress-side-title'); + const passagePre = root.querySelector('#cs-progress-passage-pre'); + const livePre = root.querySelector('#cs-progress-live-pre'); + const liveStatus = root.querySelector('#cs-progress-live-status'); return { // Called once per passage before its request starts, so both panes are // reset and honestly labelled before we know whether this model streams @@ -583,7 +593,9 @@ function csProgress(total) { `).join('') : '
reading…
'; } }, - done() { ov.style.display = 'none'; }, + done() { + if (!usingHost) root.style.display = 'none'; + }, }; } @@ -845,7 +857,100 @@ function csToMarkdown(sheets) { return md.trim(); } -async function csShow(sheets, title, sourceText, book) { +function csWireResultInteractions(root, sheets, sourceText, book, inline = false) { + const copyBtn = root.querySelector('#cs-copy'); + copyBtn?.addEventListener('click', () => { + navigator.clipboard?.writeText(csToMarkdown(sheets)) + .then(() => toast('Copied as Markdown', 'success'), () => toast('Copy failed', 'error')); + }); + + const closeBtn = root.querySelector('#cs-close'); + closeBtn?.addEventListener('click', () => { + if (inline) { + if (typeof navLibraryView === 'function') navLibraryView('characters'); + } else { + document.getElementById('cs-overlay')?.remove(); + } + }); + + root.querySelector('#cs-open-cast')?.addEventListener('click', () => csGoToLibrary()); + root.querySelector('#cs-back-cast-audio')?.addEventListener('click', () => { + if (typeof navTo === 'function') navTo('s-reader'); + if (typeof showReaderView === 'function') showReaderView('cast'); + }); + + const bkCtx = book || ''; + root.querySelectorAll('.cs-avatar').forEach(av => { + av.addEventListener('click', e => { + e.stopPropagation(); + const name = av.dataset.name; + const inp = document.createElement('input'); + inp.type = 'file'; inp.accept = 'image/*'; + inp.onchange = async () => { + const file = inp.files[0]; if (!file) return; + const reader = new FileReader(); + reader.onload = async ev => { + const dataUrl = ev.target.result; + const id = `${bkCtx}::${name}`.toLowerCase(); + if (typeof clSetImage === 'function') await clSetImage(id, dataUrl); + const s = sheets.find(x => x.name === name); + if (s) s._image = dataUrl; + const card = root.querySelector(`.cs-card[data-name="${CSS.escape(name)}"]`); + if (card) { + const oldAv = card.querySelector('.cs-avatar'); + if (oldAv) { + oldAv.outerHTML = csAvatarHtml(s); + card.querySelector('.cs-avatar')?.addEventListener('click', av.onclick); + } + } + toast('Profile picture saved', 'success'); + }; + reader.readAsDataURL(file); + }; + inp.click(); + }); + }); + + root.querySelectorAll('.cs-deep-btn').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const sheet = sheets.find(s => s.name === btn.dataset.name); + if (sheet) csDeepAnalysis(sheet, sourceText); + }); + }); + + root.querySelectorAll('.cs-img-btn').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const sheet = sheets.find(s => s.name === btn.dataset.name); + if (!sheet) return; + navigator.clipboard?.writeText(csBuildImagePrompt(sheet)) + .then(() => toast('Image prompt copied', 'success'), () => toast('Copy failed', 'error')); + }); + }); + + root.querySelectorAll('.cs-voice-btn').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const sheet = sheets.find(s => s.name === btn.dataset.name); + if (!sheet) return; + navigator.clipboard?.writeText(csBuildVoicePrompt(sheet)) + .then(() => toast('Voice prompt copied — paste into Design a Voice', 'success'), () => toast('Copy failed', 'error')); + }); + }); + + root.querySelectorAll('.cs-source-link, .cs-source-mark').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const pg = parseInt(btn.dataset.page, 10); + if (pg && typeof window.readerJumpToPage === 'function') window.readerJumpToPage(pg); + else if (pg && typeof toast === 'function') toast(`Source: page ${pg}`, 'info'); + }); + }); +} + +async function csShow(sheets, title, sourceText, book, hostEl = null) { + const inline = !!hostEl; document.getElementById('cs-overlay')?.remove(); // Enrich sheets with stored profile images from the character library. @@ -861,103 +966,45 @@ async function csShow(sheets, title, sourceText, book) { } catch (_) {} } - const ov = document.createElement('div'); - ov.id = 'cs-overlay'; ov.className = 'audiobook-overlay'; const main = sheets.filter(s => s.tier === 'main'); const supp = sheets.filter(s => s.tier !== 'main'); const group = (label, list) => list.length ? `
${label}
` + list.map(csCardHtml).join('') : ''; - ov.innerHTML = `
-
- - ${escHtml(title || 'Character sheets')} - ${sheets.length} character${sheets.length !== 1 ? 's' : ''} - - - - -
-
${group('Main characters', main)}${group('Supporting characters', supp)}
-
`; - document.body.appendChild(ov); - ov.querySelector('#cs-close').addEventListener('click', () => ov.remove()); - ov.querySelector('#cs-copy').addEventListener('click', () => { - navigator.clipboard?.writeText(csToMarkdown(sheets)) - .then(() => toast('Copied as Markdown', 'success'), () => toast('Copy failed', 'error')); - }); - ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); }); - - // Wire avatar upload (click on avatar → file picker → save to library) - const bkCtx = book || title || ''; - ov.querySelectorAll('.cs-avatar').forEach(av => { - av.addEventListener('click', e => { - e.stopPropagation(); - const name = av.dataset.name; - const inp = document.createElement('input'); - inp.type = 'file'; inp.accept = 'image/*'; - inp.onchange = async () => { - const file = inp.files[0]; if (!file) return; - const reader = new FileReader(); - reader.onload = async ev => { - const dataUrl = ev.target.result; - const id = `${bkCtx}::${name}`.toLowerCase(); - if (typeof clSetImage === 'function') await clSetImage(id, dataUrl); - // Update in-memory sheet and re-render avatar - const s = sheets.find(x => x.name === name); - if (s) s._image = dataUrl; - const card = ov.querySelector(`.cs-card[data-name="${CSS.escape(name)}"]`); - if (card) { - const oldAv = card.querySelector('.cs-avatar'); - if (oldAv) { oldAv.outerHTML = csAvatarHtml(s); card.querySelector('.cs-avatar').addEventListener('click', av.onclick); } - } - toast('Profile picture saved', 'success'); - }; - reader.readAsDataURL(file); - }; - inp.click(); - }); - }); - - // Wire Deep Analysis buttons - ov.querySelectorAll('.cs-deep-btn').forEach(btn => { - btn.addEventListener('click', e => { - e.stopPropagation(); - const sheet = sheets.find(s => s.name === btn.dataset.name); - if (sheet) csDeepAnalysis(sheet, sourceText); - }); - }); - - // Wire Image prompt buttons - ov.querySelectorAll('.cs-img-btn').forEach(btn => { - btn.addEventListener('click', e => { - e.stopPropagation(); - const sheet = sheets.find(s => s.name === btn.dataset.name); - if (!sheet) return; - navigator.clipboard?.writeText(csBuildImagePrompt(sheet)) - .then(() => toast('Image prompt copied', 'success'), () => toast('Copy failed', 'error')); - }); - }); - - // Wire Voice prompt buttons - ov.querySelectorAll('.cs-voice-btn').forEach(btn => { - btn.addEventListener('click', e => { - e.stopPropagation(); - const sheet = sheets.find(s => s.name === btn.dataset.name); - if (!sheet) return; - navigator.clipboard?.writeText(csBuildVoicePrompt(sheet)) - .then(() => toast('Voice prompt copied — paste into Design a Voice', 'success'), () => toast('Copy failed', 'error')); - }); - }); - - ov.querySelectorAll('.cs-source-link, .cs-source-mark').forEach(btn => { - btn.addEventListener('click', e => { - e.stopPropagation(); - const pg = parseInt(btn.dataset.page, 10); - if (pg && typeof window.readerJumpToPage === 'function') window.readerJumpToPage(pg); - else if (pg && typeof toast === 'function') toast(`Source: page ${pg}`, 'info'); - }); - }); + const ov = inline ? hostEl : document.createElement('div'); + if (!inline) { + ov.id = 'cs-overlay'; ov.className = 'audiobook-overlay'; + ov.innerHTML = `
+
+ + ${escHtml(title || 'Character sheets')} + ${sheets.length} character${sheets.length !== 1 ? 's' : ''} + + + + +
+
${group('Main characters', main)}${group('Supporting characters', supp)}
+
`; + document.body.appendChild(ov); + ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); }); + } else { + ov.innerHTML = `
+
+ + ${escHtml(title || 'Character sheets')} + ${sheets.length} character${sheets.length !== 1 ? 's' : ''} + + + + + +
+
Generated sheets are saved into the cast library. Use the buttons above to jump back to the audiobook cast or continue to the roster stage.
+
${group('Main characters', main)}${group('Supporting characters', supp)}
+
`; + } + csWireResultInteractions(ov, sheets, sourceText, book, inline); } // ── Entry points (reader + rehearser) ──────────────────────────────────────── @@ -975,6 +1022,7 @@ function csGoToLibrary() { if (typeof navTo === 'function') navTo('s-library'); if (typeof navLibraryView === 'function') navLibraryView('characters'); else if (typeof libraryRender === 'function') libraryRender('characters'); + if (typeof refreshWorkflowCrumbs === 'function') refreshWorkflowCrumbs('castlib'); } // Best-effort dialogue-line tally so the Library's table view can show a @@ -994,9 +1042,16 @@ async function csForReader() { const text = csReaderText(); const book = readerState.title || 'Untitled book'; const key = 'reader:' + (readerState.title || '') + ':' + (typeof readerScopeIndices === 'function' ? readerScopeIndices().length : 0); - if (_cs.cache[key]) { csGoToLibrary(); return; } + if (typeof navTo === 'function') navTo('s-reader'); + if (typeof showReaderView === 'function') showReaderView('chars'); + const pageHost = csReaderPageHost(); + if (_cs.cache[key]) { + await csSaveToLibrary(book, _cs.cache[key]); + if (pageHost) await csShow(_cs.cache[key], book, text, book, pageHost); + return; + } const knownRoster = csKnownReaderRoster(); - const sheets = await csGenerate(text, key, knownRoster); + const sheets = await csGenerate(text, key, knownRoster, { pageHost }); if (!sheets) return; if (!sheets.length) { toast('No characters found', 'error'); return; } const ab = (typeof _audiobook !== 'undefined') ? _audiobook : window._audiobook; @@ -1006,8 +1061,8 @@ async function csForReader() { csAttachLineCounts(sheets, counts); } await csSaveToLibrary(book, sheets); - csGoToLibrary(); - toast(sheets.length + ' character sheets saved — Library → Characters / Cast', 'success'); + if (pageHost) await csShow(sheets, book, text, book, pageHost); + toast(sheets.length + ' character sheets saved — Library → Cast', 'success'); } // Refresh sheet data for a hand-picked subset of an already-cast book's @@ -1062,7 +1117,7 @@ async function csForReaderSelective(selectedNames) { } await csSaveToLibrary(book, filtered); csGoToLibrary(); - toast(filtered.length + ' character' + (filtered.length !== 1 ? 's' : '') + ' recast — Library → Characters / Cast', 'success'); + toast(filtered.length + ' character' + (filtered.length !== 1 ? 's' : '') + ' recast — Library → Cast', 'success'); } async function csForRehearser() { @@ -1070,8 +1125,15 @@ async function csForRehearser() { const book = $('reh-script-title')?.value.trim() || 'Untitled script'; const text = csRehearserText(); const key = 'reh:' + title + ':' + ((rehState.lines || []).length); - if (_cs.cache[key]) { csGoToLibrary(); return; } - const sheets = await csGenerate(text, key); + if (typeof navTo === 'function') navTo('s-reader'); + if (typeof showReaderView === 'function') showReaderView('chars'); + const pageHost = csReaderPageHost(); + if (_cs.cache[key]) { + await csSaveToLibrary(book, _cs.cache[key]); + if (pageHost) await csShow(_cs.cache[key], title, text, book, pageHost); + return; + } + const sheets = await csGenerate(text, key, null, { pageHost }); if (!sheets) return; if (!sheets.length) { toast('No characters found', 'error'); return; } if (window.rehState?.lines?.length) { @@ -1084,8 +1146,8 @@ async function csForRehearser() { csAttachLineCounts(sheets, counts); } await csSaveToLibrary(book, sheets); - csGoToLibrary(); - toast(sheets.length + ' character sheets saved — Library → Characters / Cast', 'success'); + if (pageHost) await csShow(sheets, title, text, book, pageHost); + toast(sheets.length + ' character sheets saved — Library → Cast', 'success'); } window.csForReader = csForReader; diff --git a/static/js/conversation.js b/static/js/conversation.js index 9fa1f0d..44508f7 100644 --- a/static/js/conversation.js +++ b/static/js/conversation.js @@ -420,10 +420,19 @@ $('s-import-voices-file')?.addEventListener('change', async function () { function appendThinking(bubble, delta) { ensureAssistantParts(bubble); - bubble.querySelector('.conv-think-wrap').hidden = false; - // Stays collapsed regardless of streaming state — the user only sees the - // reasoning text if they click the "Thinking" chevron to expand it. - bubble.querySelector('.conv-think-body').textContent += delta; + const wrap = bubble.querySelector('.conv-think-wrap'); + const toggle = bubble.querySelector('.conv-think-toggle'); + const body = bubble.querySelector('.conv-think-body'); + wrap.hidden = false; + body.textContent += delta; + + // Auto-open the panel the first time reasoning arrives so the user can see + // the model thinking live. After that, preserve the user's manual toggle. + if (!bubble.dataset.thinkingSeen) { + bubble.dataset.thinkingSeen = '1'; + toggle.setAttribute('aria-expanded', 'true'); + body.hidden = false; + } } function setAnswerText(bubble, text) { diff --git a/static/js/library.js b/static/js/library.js index c69a339..89685cf 100644 --- a/static/js/library.js +++ b/static/js/library.js @@ -26,9 +26,9 @@ window.navLibraryView = function (view) { document.querySelectorAll('[data-library-panel]').forEach(function (el) { el.classList.toggle('is-active', el.dataset.libraryPanel === view); }); - // Characters/Cast is the pipeline's "Cast Characters" stop — keep the - // stepper's current-step highlight in sync when the tab is opened directly. - if (view === 'characters' && typeof refreshWorkflowCrumbs === 'function') refreshWorkflowCrumbs('chars'); + // Cast roster is its own pipeline stop; keep the stepper highlight in sync + // when the tab is opened directly. + if (view === 'characters' && typeof refreshWorkflowCrumbs === 'function') refreshWorkflowCrumbs('castlib'); libraryRender(view); }; diff --git a/static/js/reader.js b/static/js/reader.js index 322174f..e06af06 100644 --- a/static/js/reader.js +++ b/static/js/reader.js @@ -1954,6 +1954,7 @@ window.addEventListener('resize', (function () { window.showReaderView = function (view) { const main = $('reader-main-view'); const cast = $('reader-audiobook-panel'); + const chars = $('reader-charsheets-panel'); const lib = $('reader-library-card'); if (main) main.hidden = (view !== 'main'); if (cast) { @@ -1968,6 +1969,20 @@ window.showReaderView = function (view) { cast.className = 'ab-castpanel-inline card'; } } + if (chars) { + chars.hidden = (view !== 'chars'); + if (view === 'chars' && !chars.innerHTML.trim()) { + chars.innerHTML = '
' + + '
Character sheets
' + + '

This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.

' + + '
' + + '' + + '' + + '' + + '
'; + chars.className = ''; + } + } if (lib) { lib.hidden = (view !== 'library'); if (view === 'library') { @@ -1978,7 +1993,9 @@ window.showReaderView = function (view) { readerRenderLibrary(); // Re-fetch on open } } - if (typeof refreshWorkflowCrumbs === 'function') refreshWorkflowCrumbs(view === 'cast' ? 'cast' : 'source'); + if (typeof refreshWorkflowCrumbs === 'function') { + refreshWorkflowCrumbs(view === 'cast' ? 'cast' : view === 'chars' ? 'chars' : 'source'); + } }; window.navReaderView = function (view) { diff --git a/static/js/utils.js b/static/js/utils.js index 427e0ff..0dc4b25 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -786,7 +786,7 @@ document.addEventListener('click', e => { }); // ── Cross-workflow pipeline stepper ───────────────────────────────────────── -// PDF → Text → Cast Audiobook → Cast Characters (optional) → Script Rehearser +// PDF → Text → Cast Audiobook → Cast Characters → Cast → Script Rehearser // → Generate MP3s → Audiobook. A persistent strip (rendered into any element // with class .wf-stepper — currently one in s-reader.html, one in // s-rehearser.html) that lets you jump directly to any reachable stage @@ -803,6 +803,7 @@ const WF_STEPS = [ { key: 'source', label: 'Source', enabled: () => true }, { key: 'cast', label: 'Cast Audiobook', enabled: () => !!window.readerState?.sentences?.length }, { key: 'chars', label: 'Cast Characters', enabled: () => !!window.readerState?.sentences?.length, optional: true }, + { key: 'castlib', label: 'Cast', enabled: () => !!window.readerState?.sentences?.length }, { key: 'rehearser', label: 'Script Rehearser',enabled: () => !!window.rehState?.lines?.length }, { key: 'mp3', label: 'Generate MP3s', enabled: () => !!window.rehState?.lines?.length }, { key: 'audiobook', label: 'Audiobook', enabled: () => !!window.rehState?.clips?.length }, @@ -818,12 +819,16 @@ function workflowCrumbGo(key) { if (typeof navTo === 'function') navTo('s-reader'); if (typeof audiobookOpenCastView === 'function') audiobookOpenCastView(); } else if (key === 'chars') { - // Navigate to the Characters/Cast library view — generating sheets stays - // on the explicit "Cast Characters" buttons; a navigation stop must never - // kick off an LLM run as a side effect. + // Navigate to the character-sheet stage in Read Aloud. + if (typeof navTo === 'function') navTo('s-reader'); + if (typeof showReaderView === 'function') showReaderView('chars'); + _wfActive = 'chars'; + refreshWorkflowCrumbs(); + } else if (key === 'castlib') { + // Navigate to the cast roster/library view. if (typeof navTo === 'function') navTo('s-library'); if (typeof navLibraryView === 'function') navLibraryView('characters'); - _wfActive = 'chars'; + _wfActive = 'castlib'; refreshWorkflowCrumbs(); } else if (key === 'rehearser') { if (typeof navTo === 'function') navTo('s-rehearser'); @@ -887,7 +892,7 @@ function refreshWorkflowCrumbs(active) { window.refreshWorkflowCrumbs = refreshWorkflowCrumbs; // Rehearser's own phase tabs (Library/Cast/Stage/Summary) map onto the last -// three stepper stops — keep the stepper's "current" highlight following them. +// four stepper stops — keep the stepper's "current" highlight following them. window.onRehearserPhaseChange = function (n) { refreshWorkflowCrumbs(n === 3 ? 'mp3' : n === 4 ? 'audiobook' : 'rehearser'); }; diff --git a/static/sections/s-library.html b/static/sections/s-library.html index e4fafb6..af01035 100644 --- a/static/sections/s-library.html +++ b/static/sections/s-library.html @@ -11,7 +11,7 @@
- +
diff --git a/static/sections/s-reader.html b/static/sections/s-reader.html index 5222fcc..35f0798 100644 --- a/static/sections/s-reader.html +++ b/static/sections/s-reader.html @@ -191,6 +191,9 @@ + + +
diff --git a/static/style.css b/static/style.css index 2c13c2e..99b2cc8 100644 --- a/static/style.css +++ b/static/style.css @@ -5715,6 +5715,17 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami /* Character sheets overlay */ .cs-box { width: min(1120px, 96vw); max-height: 90vh; display: flex; flex-direction: column; } +.cs-page-shell { width: 100%; max-width: none; } +#s-reader #reader-charsheets-panel { display: flex; flex-direction: column; min-height: 0; } +#s-reader #reader-charsheets-panel .cs-progress-box-big, +#s-reader #reader-charsheets-panel .cs-page-shell { + width: 100%; + max-width: none; +} +#s-reader #reader-charsheets-panel .cs-progress-box-big { + min-height: min(880px, 90vh); + height: auto; +} .cs-titlebar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; } .cs-list { overflow-y: auto; padding-right: 4px; display: grid; grid-template-columns: repeat(auto-fill, minmax(430px, 1fr)); gap: 14px; align-content: start; } .cs-group-label { grid-column: 1 / -1; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); margin-top: 4px; }