Release v1.14.19
This commit is contained in:
parent
17716ac740
commit
8f3fcbae04
11
CHANGELOG.md
11
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
|
## [1.14.18] — 2026-07-09
|
||||||
|
|
||||||
### Fixed
|
### 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.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.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
|
[1.2.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.1.0...v1.2.0
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="theme-color" content="#2563EB">
|
<meta name="theme-color" content="#2563EB">
|
||||||
<meta name="app-version" content="1.14.18">
|
<meta name="app-version" content="1.14.19">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||||
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||||
<link rel="stylesheet" href="/static/style.css?v=1.14.18">
|
<link rel="stylesheet" href="/static/style.css?v=1.14.19">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||||
<script src="/static/loader.js?v=1.14.18"></script>
|
<script src="/static/loader.js?v=1.14.19"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -3291,7 +3291,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
|
|||||||
const continueBtnHtml = resumable
|
const continueBtnHtml = resumable
|
||||||
? `<button class="btn-primary btn-sm" id="ab-cv-continue" style="margin-right:8px;" title="Continue casting from passage ${_audiobook.completedChunks + 1} of ${_audiobook.completedTotal}, keeping already-cast passages"><span class="mdi mdi-play-circle-outline"></span> Continue casting</button>`
|
? `<button class="btn-primary btn-sm" id="ab-cv-continue" style="margin-right:8px;" title="Continue casting from passage ${_audiobook.completedChunks + 1} of ${_audiobook.completedTotal}, keeping already-cast passages"><span class="mdi mdi-play-circle-outline"></span> Continue casting</button>`
|
||||||
: '';
|
: '';
|
||||||
foot.innerHTML = `<span style="flex:1"></span>${continueBtnHtml}<button class="btn-secondary btn-sm" id="ab-cv-recast-unk" style="margin-right:8px; color:var(--error);" title="Re-run only the Unknown segments with the current prompt"><span class="mdi mdi-account-question-outline"></span> Recast unknown</button><button class="btn-secondary btn-sm" id="ab-cv-recast" style="margin-right:8px;" title="Re-run the entire document to apply new settings or prompt tweaks"><span class="mdi mdi-refresh"></span> Recast all</button><button class="btn-secondary btn-sm" id="ab-cv-verify" style="margin-right:8px; border-color:var(--accent); color:var(--accent); font-weight:600;" title="2nd Quality run — resolves Unknown speakers with stricter context checks while preserving the existing cast"><span class="mdi mdi-shield-check-outline"></span> 2nd Quality Run</button><button class="btn-secondary btn-sm" id="ab-cv-cast-chars" style="margin-right:8px;" title="Generate character sheets and match voices to each found character"><span class="mdi mdi-account-details-outline"></span> Cast Characters</button><button class="btn-secondary btn-sm" id="ab-cv-export-md" style="margin-right:8px;" title="Download one zip: the cast as a readable Markdown script plus a Markdown sheet per character"><span class="mdi mdi-folder-zip-outline"></span> Export cast .zip</button><button class="btn-primary btn-sm" id="ab-cv-open-reh" title="Assign voices to each character and synthesise in Script Rehearser"><span class="mdi mdi-drama-masks"></span> Edit Characters</button>`;
|
foot.innerHTML = `<span style="flex:1"></span>${continueBtnHtml}<button class="btn-secondary btn-sm" id="ab-cv-recast-unk" style="margin-right:8px; color:var(--error);" title="Re-run only the Unknown segments with the current prompt"><span class="mdi mdi-account-question-outline"></span> Recast unknown</button><button class="btn-secondary btn-sm" id="ab-cv-recast" style="margin-right:8px;" title="Re-run the entire document to apply new settings or prompt tweaks"><span class="mdi mdi-refresh"></span> Recast all</button><button class="btn-secondary btn-sm" id="ab-cv-verify" style="margin-right:8px; border-color:var(--accent); color:var(--accent); font-weight:600;" title="2nd Quality run — resolves Unknown speakers with stricter context checks while preserving the existing cast"><span class="mdi mdi-shield-check-outline"></span> 2nd Quality Run</button><button class="btn-secondary btn-sm" id="ab-cv-cast-chars" style="margin-right:8px;" title="Open the character-sheets stage for this book"><span class="mdi mdi-account-details-outline"></span> Cast Characters</button><button class="btn-secondary btn-sm" id="ab-cv-export-md" style="margin-right:8px;" title="Download one zip: the cast as a readable Markdown script plus a Markdown sheet per character"><span class="mdi mdi-folder-zip-outline"></span> Export cast .zip</button><button class="btn-primary btn-sm" id="ab-cv-open-reh" title="Assign voices to each character and synthesise in Script Rehearser"><span class="mdi mdi-drama-masks"></span> Edit Characters</button>`;
|
||||||
|
|
||||||
const runVerificationPass = () => {
|
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.
|
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');
|
const wrap = document.createElement('span');
|
||||||
wrap.className = 'ab-cv-castchars-group';
|
wrap.className = 'ab-cv-castchars-group';
|
||||||
wrap.innerHTML =
|
wrap.innerHTML =
|
||||||
'<button class="btn-secondary btn-sm ab-cv-castchars-view" title="Open this book\'s characters in the Library"><span class="mdi mdi-account-box-multiple-outline"></span> View Characters</button>' +
|
'<button class="btn-secondary btn-sm ab-cv-castchars-view" title="Open this book\'s cast in the Library"><span class="mdi mdi-account-box-multiple-outline"></span> View Cast</button>' +
|
||||||
'<button class="btn-secondary btn-sm ab-cv-castchars-menu" title="Recast options"><span class="mdi mdi-chevron-down"></span></button>';
|
'<button class="btn-secondary btn-sm ab-cv-castchars-menu" title="Recast options"><span class="mdi mdi-chevron-down"></span></button>';
|
||||||
castBtn.replaceWith(wrap);
|
castBtn.replaceWith(wrap);
|
||||||
wrap.querySelector('.ab-cv-castchars-view').addEventListener('click', () => {
|
wrap.querySelector('.ab-cv-castchars-view').addEventListener('click', () => {
|
||||||
|
|||||||
@ -63,6 +63,7 @@ function csLlmUrl() {
|
|||||||
}
|
}
|
||||||
function csLlmModel() { return $('reh-llm-model')?.value || ''; }
|
function csLlmModel() { return $('reh-llm-model')?.value || ''; }
|
||||||
function csLang() { return $('reh-design-lang')?.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).
|
// Build page-annotated text from the current reader scope ([p.N] at page changes).
|
||||||
function csReaderText() {
|
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 (_cs.running) return null;
|
||||||
if (!text) { toast('Nothing to analyse', 'error'); return null; }
|
if (!text) { toast('Nothing to analyse', 'error'); return null; }
|
||||||
const chunks = (typeof splitTextIntoChunks === 'function') ? splitTextIntoChunks(text, CS_CHUNK_CHARS) : [text];
|
const chunks = (typeof splitTextIntoChunks === 'function') ? splitTextIntoChunks(text, CS_CHUNK_CHARS) : [text];
|
||||||
_cs.running = true; _cs.cancel = false;
|
_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 llm_url = csLlmUrl(), model = csLlmModel();
|
||||||
const selectedLanguage = csLang();
|
const selectedLanguage = csLang();
|
||||||
let language = selectedLanguage;
|
let language = selectedLanguage;
|
||||||
@ -409,16 +410,24 @@ async function csGenerate(text, cacheKey, initialRoster) {
|
|||||||
return sheets;
|
return sheets;
|
||||||
}
|
}
|
||||||
|
|
||||||
function csProgress(total) {
|
function csProgress(total, hostEl = null) {
|
||||||
let ov = document.getElementById('cs-progress');
|
const usingHost = !!hostEl;
|
||||||
if (!ov) {
|
let root = usingHost ? hostEl : document.getElementById('cs-progress');
|
||||||
ov = document.createElement('div');
|
if (!root) {
|
||||||
ov.id = 'cs-progress'; ov.className = 'audiobook-overlay';
|
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
|
// Big two-pane layout, mirroring the audiobook casting live view: the
|
||||||
// passage being analysed on the left, the LLM's raw JSON output filling
|
// 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)
|
// in live on the right, and a character sidebar (current one highlighted)
|
||||||
// so the user can watch each sheet actually being built.
|
// so the user can watch each sheet actually being built.
|
||||||
ov.innerHTML = `<div class="audiobook-box cs-progress-box cs-progress-box-big">
|
root.innerHTML = `<div class="audiobook-box cs-progress-box cs-progress-box-big">
|
||||||
<div class="cs-progress-head">
|
<div class="cs-progress-head">
|
||||||
<div class="audiobook-title">
|
<div class="audiobook-title">
|
||||||
<span class="mdi mdi-account-details-outline"></span> Character sheets
|
<span class="mdi mdi-account-details-outline"></span> Character sheets
|
||||||
@ -456,46 +465,47 @@ function csProgress(total) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="cs-progress-cancel">Cancel</button></div>
|
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="cs-progress-cancel">Cancel</button></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(ov);
|
root.querySelector('#cs-progress-cancel')?.addEventListener('click', () => { _cs.cancel = true; });
|
||||||
ov.querySelector('#cs-progress-cancel').addEventListener('click', () => { _cs.cancel = true; });
|
|
||||||
|
|
||||||
// Prompt panel — mirrors the audiobook casting "Prompt" button: toggle
|
// Prompt panel — mirrors the audiobook casting "Prompt" button: toggle
|
||||||
// visibility, load/save/delete named presets (localStorage), and persist
|
// visibility, load/save/delete named presets (localStorage), and persist
|
||||||
// the active text as the settings default so csGenerate() picks it up.
|
// the active text as the settings default so csGenerate() picks it up.
|
||||||
const promptBtn = ov.querySelector('#cs-prompt-btn');
|
const promptBtn = root.querySelector('#cs-prompt-btn');
|
||||||
const promptPanel = ov.querySelector('#cs-prompt-panel');
|
const promptPanel = root.querySelector('#cs-prompt-panel');
|
||||||
const promptText = ov.querySelector('#cs-prompt-text');
|
const promptText = root.querySelector('#cs-prompt-text');
|
||||||
promptText.value = (typeof _appSettings !== 'undefined' && _appSettings.character_sheets_prompt) || CS_DEFAULT_PROMPT;
|
if (promptText) promptText.value = (typeof _appSettings !== 'undefined' && _appSettings.character_sheets_prompt) || CS_DEFAULT_PROMPT;
|
||||||
promptBtn.addEventListener('click', () => {
|
promptBtn?.addEventListener('click', () => {
|
||||||
|
if (!promptPanel) return;
|
||||||
promptPanel.hidden = !promptPanel.hidden;
|
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';
|
if (chevron) chevron.className = promptPanel.hidden ? 'mdi mdi-chevron-down' : 'mdi mdi-chevron-up';
|
||||||
});
|
});
|
||||||
|
|
||||||
let savedCsPrompts = [];
|
let savedCsPrompts = [];
|
||||||
try { savedCsPrompts = JSON.parse(localStorage.getItem('ttsvc_cs_prompts') || '[]'); } catch (_) { savedCsPrompts = []; }
|
try { savedCsPrompts = JSON.parse(localStorage.getItem('ttsvc_cs_prompts') || '[]'); } catch (_) { savedCsPrompts = []; }
|
||||||
const libSelect = ov.querySelector('#cs-prompt-lib');
|
const libSelect = root.querySelector('#cs-prompt-lib');
|
||||||
const delBtn = ov.querySelector('#cs-prompt-del');
|
const delBtn = root.querySelector('#cs-prompt-del');
|
||||||
const nameInput = ov.querySelector('#cs-prompt-name');
|
const nameInput = root.querySelector('#cs-prompt-name');
|
||||||
const renderCsPromptLib = (selectedIdx = -1) => {
|
const renderCsPromptLib = (selectedIdx = -1) => {
|
||||||
|
if (!libSelect || !delBtn) return;
|
||||||
libSelect.innerHTML = '<option value="">— Load saved prompt —</option>' +
|
libSelect.innerHTML = '<option value="">— Load saved prompt —</option>' +
|
||||||
savedCsPrompts.map((p, i) => `<option value="${i}">${escHtml(p.name)}</option>`).join('');
|
savedCsPrompts.map((p, i) => `<option value="${i}">${escHtml(p.name)}</option>`).join('');
|
||||||
if (selectedIdx >= 0) { libSelect.value = selectedIdx; delBtn.style.display = 'inline-flex'; }
|
if (selectedIdx >= 0) { libSelect.value = selectedIdx; delBtn.style.display = 'inline-flex'; }
|
||||||
else { libSelect.value = ''; delBtn.style.display = 'none'; }
|
else { libSelect.value = ''; delBtn.style.display = 'none'; }
|
||||||
};
|
};
|
||||||
renderCsPromptLib();
|
renderCsPromptLib();
|
||||||
libSelect.addEventListener('change', () => {
|
libSelect?.addEventListener('change', () => {
|
||||||
const idx = parseInt(libSelect.value, 10);
|
const idx = parseInt(libSelect.value, 10);
|
||||||
if (!isNaN(idx) && savedCsPrompts[idx]) {
|
if (!isNaN(idx) && savedCsPrompts[idx]) {
|
||||||
promptText.value = savedCsPrompts[idx].prompt;
|
if (promptText) promptText.value = savedCsPrompts[idx].prompt;
|
||||||
nameInput.value = savedCsPrompts[idx].name;
|
if (nameInput) nameInput.value = savedCsPrompts[idx].name;
|
||||||
delBtn.style.display = 'inline-flex';
|
delBtn.style.display = 'inline-flex';
|
||||||
} else {
|
} else {
|
||||||
nameInput.value = '';
|
if (nameInput) nameInput.value = '';
|
||||||
delBtn.style.display = 'none';
|
delBtn.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
delBtn.addEventListener('click', () => {
|
delBtn?.addEventListener('click', () => {
|
||||||
const idx = parseInt(libSelect.value, 10);
|
const idx = parseInt(libSelect.value, 10);
|
||||||
if (isNaN(idx)) return;
|
if (isNaN(idx)) return;
|
||||||
if (confirm('Delete this saved prompt preset?')) {
|
if (confirm('Delete this saved prompt preset?')) {
|
||||||
@ -505,11 +515,11 @@ function csProgress(total) {
|
|||||||
toast('Prompt deleted', 'success');
|
toast('Prompt deleted', 'success');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ov.querySelector('#cs-prompt-save').addEventListener('click', async () => {
|
root.querySelector('#cs-prompt-save')?.addEventListener('click', async () => {
|
||||||
const val = promptText.value.trim();
|
const val = promptText?.value.trim() || '';
|
||||||
if (!val) { toast('Prompt is empty', 'error'); return; }
|
if (!val) { toast('Prompt is empty', 'error'); return; }
|
||||||
const name = nameInput.value.trim() || 'Custom Prompt ' + (savedCsPrompts.length + 1);
|
const name = nameInput?.value.trim() || 'Custom Prompt ' + (savedCsPrompts.length + 1);
|
||||||
let targetIdx = parseInt(libSelect.value, 10);
|
let targetIdx = parseInt(libSelect?.value || '', 10);
|
||||||
if (!isNaN(targetIdx) && savedCsPrompts[targetIdx] && savedCsPrompts[targetIdx].name === name) {
|
if (!isNaN(targetIdx) && savedCsPrompts[targetIdx] && savedCsPrompts[targetIdx].name === name) {
|
||||||
savedCsPrompts[targetIdx].prompt = val;
|
savedCsPrompts[targetIdx].prompt = val;
|
||||||
} else {
|
} else {
|
||||||
@ -524,7 +534,7 @@ function csProgress(total) {
|
|||||||
// per-passage prompt lookup — and the next time this panel opens —
|
// per-passage prompt lookup — and the next time this panel opens —
|
||||||
// always reflect the latest text, including mid-run edits.
|
// always reflect the latest text, including mid-run edits.
|
||||||
let promptSaveTimer = null;
|
let promptSaveTimer = null;
|
||||||
promptText.addEventListener('input', () => {
|
promptText?.addEventListener('input', () => {
|
||||||
clearTimeout(promptSaveTimer);
|
clearTimeout(promptSaveTimer);
|
||||||
promptSaveTimer = setTimeout(() => {
|
promptSaveTimer = setTimeout(() => {
|
||||||
const val = promptText.value;
|
const val = promptText.value;
|
||||||
@ -533,14 +543,14 @@ function csProgress(total) {
|
|||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ov.style.display = 'flex';
|
if (!usingHost) root.style.display = 'flex';
|
||||||
const fill = ov.querySelector('#cs-progress-fill');
|
const fill = root.querySelector('#cs-progress-fill');
|
||||||
const msg = ov.querySelector('#cs-progress-msg');
|
const msg = root.querySelector('#cs-progress-msg');
|
||||||
const chars = ov.querySelector('#cs-progress-chars');
|
const chars = root.querySelector('#cs-progress-chars');
|
||||||
const sideTitle = ov.querySelector('#cs-progress-side-title');
|
const sideTitle = root.querySelector('#cs-progress-side-title');
|
||||||
const passagePre = ov.querySelector('#cs-progress-passage-pre');
|
const passagePre = root.querySelector('#cs-progress-passage-pre');
|
||||||
const livePre = ov.querySelector('#cs-progress-live-pre');
|
const livePre = root.querySelector('#cs-progress-live-pre');
|
||||||
const liveStatus = ov.querySelector('#cs-progress-live-status');
|
const liveStatus = root.querySelector('#cs-progress-live-status');
|
||||||
return {
|
return {
|
||||||
// Called once per passage before its request starts, so both panes are
|
// Called once per passage before its request starts, so both panes are
|
||||||
// reset and honestly labelled before we know whether this model streams
|
// reset and honestly labelled before we know whether this model streams
|
||||||
@ -583,7 +593,9 @@ function csProgress(total) {
|
|||||||
`).join('') : '<div class="cs-progress-empty">reading…</div>';
|
`).join('') : '<div class="cs-progress-empty">reading…</div>';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
done() { ov.style.display = 'none'; },
|
done() {
|
||||||
|
if (!usingHost) root.style.display = 'none';
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -845,7 +857,100 @@ function csToMarkdown(sheets) {
|
|||||||
return md.trim();
|
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();
|
document.getElementById('cs-overlay')?.remove();
|
||||||
|
|
||||||
// Enrich sheets with stored profile images from the character library.
|
// Enrich sheets with stored profile images from the character library.
|
||||||
@ -861,13 +966,14 @@ async function csShow(sheets, title, sourceText, book) {
|
|||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ov = document.createElement('div');
|
|
||||||
ov.id = 'cs-overlay'; ov.className = 'audiobook-overlay';
|
|
||||||
const main = sheets.filter(s => s.tier === 'main');
|
const main = sheets.filter(s => s.tier === 'main');
|
||||||
const supp = sheets.filter(s => s.tier !== 'main');
|
const supp = sheets.filter(s => s.tier !== 'main');
|
||||||
const group = (label, list) => list.length
|
const group = (label, list) => list.length
|
||||||
? `<div class="cs-group-label">${label}</div>` + list.map(csCardHtml).join('')
|
? `<div class="cs-group-label">${label}</div>` + list.map(csCardHtml).join('')
|
||||||
: '';
|
: '';
|
||||||
|
const ov = inline ? hostEl : document.createElement('div');
|
||||||
|
if (!inline) {
|
||||||
|
ov.id = 'cs-overlay'; ov.className = 'audiobook-overlay';
|
||||||
ov.innerHTML = `<div class="audiobook-box cs-box">
|
ov.innerHTML = `<div class="audiobook-box cs-box">
|
||||||
<div class="cs-titlebar">
|
<div class="cs-titlebar">
|
||||||
<span class="audiobook-title"><span class="mdi mdi-account-details-outline"></span>
|
<span class="audiobook-title"><span class="mdi mdi-account-details-outline"></span>
|
||||||
@ -881,83 +987,24 @@ async function csShow(sheets, title, sourceText, book) {
|
|||||||
<div class="cs-list">${group('Main characters', main)}${group('Supporting characters', supp)}</div>
|
<div class="cs-list">${group('Main characters', main)}${group('Supporting characters', supp)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(ov);
|
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(); });
|
ov.addEventListener('click', e => { if (e.target === ov) ov.remove(); });
|
||||||
|
} else {
|
||||||
// Wire avatar upload (click on avatar → file picker → save to library)
|
ov.innerHTML = `<div class="audiobook-box cs-box cs-page-shell" style="width:100%; max-width:none; max-height:none; height:auto;">
|
||||||
const bkCtx = book || title || '';
|
<div class="cs-titlebar">
|
||||||
ov.querySelectorAll('.cs-avatar').forEach(av => {
|
<span class="audiobook-title"><span class="mdi mdi-account-details-outline"></span>
|
||||||
av.addEventListener('click', e => {
|
${escHtml(title || 'Character sheets')}
|
||||||
e.stopPropagation();
|
<span class="audiobook-count">${sheets.length} character${sheets.length !== 1 ? 's' : ''}</span>
|
||||||
const name = av.dataset.name;
|
</span>
|
||||||
const inp = document.createElement('input');
|
<span style="flex:1"></span>
|
||||||
inp.type = 'file'; inp.accept = 'image/*';
|
<button class="btn-secondary btn-sm" id="cs-back-cast-audio"><span class="mdi mdi-arrow-left"></span> Cast Audiobook</button>
|
||||||
inp.onchange = async () => {
|
<button class="btn-secondary btn-sm" id="cs-open-cast"><span class="mdi mdi-folder-account-outline"></span> Open Cast</button>
|
||||||
const file = inp.files[0]; if (!file) return;
|
<button class="btn-secondary btn-sm" id="cs-copy"><span class="mdi mdi-content-copy"></span> Copy</button>
|
||||||
const reader = new FileReader();
|
</div>
|
||||||
reader.onload = async ev => {
|
<div class="card-subtitle" style="margin:0 0 12px 0;">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.</div>
|
||||||
const dataUrl = ev.target.result;
|
<div class="cs-list">${group('Main characters', main)}${group('Supporting characters', supp)}</div>
|
||||||
const id = `${bkCtx}::${name}`.toLowerCase();
|
</div>`;
|
||||||
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');
|
csWireResultInteractions(ov, sheets, sourceText, book, inline);
|
||||||
};
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Entry points (reader + rehearser) ────────────────────────────────────────
|
// ── Entry points (reader + rehearser) ────────────────────────────────────────
|
||||||
@ -975,6 +1022,7 @@ function csGoToLibrary() {
|
|||||||
if (typeof navTo === 'function') navTo('s-library');
|
if (typeof navTo === 'function') navTo('s-library');
|
||||||
if (typeof navLibraryView === 'function') navLibraryView('characters');
|
if (typeof navLibraryView === 'function') navLibraryView('characters');
|
||||||
else if (typeof libraryRender === 'function') libraryRender('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
|
// 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 text = csReaderText();
|
||||||
const book = readerState.title || 'Untitled book';
|
const book = readerState.title || 'Untitled book';
|
||||||
const key = 'reader:' + (readerState.title || '') + ':' + (typeof readerScopeIndices === 'function' ? readerScopeIndices().length : 0);
|
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 knownRoster = csKnownReaderRoster();
|
||||||
const sheets = await csGenerate(text, key, knownRoster);
|
const sheets = await csGenerate(text, key, knownRoster, { pageHost });
|
||||||
if (!sheets) return;
|
if (!sheets) return;
|
||||||
if (!sheets.length) { toast('No characters found', 'error'); return; }
|
if (!sheets.length) { toast('No characters found', 'error'); return; }
|
||||||
const ab = (typeof _audiobook !== 'undefined') ? _audiobook : window._audiobook;
|
const ab = (typeof _audiobook !== 'undefined') ? _audiobook : window._audiobook;
|
||||||
@ -1006,8 +1061,8 @@ async function csForReader() {
|
|||||||
csAttachLineCounts(sheets, counts);
|
csAttachLineCounts(sheets, counts);
|
||||||
}
|
}
|
||||||
await csSaveToLibrary(book, sheets);
|
await csSaveToLibrary(book, sheets);
|
||||||
csGoToLibrary();
|
if (pageHost) await csShow(sheets, book, text, book, pageHost);
|
||||||
toast(sheets.length + ' character sheets saved — Library → Characters / Cast', 'success');
|
toast(sheets.length + ' character sheets saved — Library → Cast', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh sheet data for a hand-picked subset of an already-cast book's
|
// 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);
|
await csSaveToLibrary(book, filtered);
|
||||||
csGoToLibrary();
|
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() {
|
async function csForRehearser() {
|
||||||
@ -1070,8 +1125,15 @@ async function csForRehearser() {
|
|||||||
const book = $('reh-script-title')?.value.trim() || 'Untitled script';
|
const book = $('reh-script-title')?.value.trim() || 'Untitled script';
|
||||||
const text = csRehearserText();
|
const text = csRehearserText();
|
||||||
const key = 'reh:' + title + ':' + ((rehState.lines || []).length);
|
const key = 'reh:' + title + ':' + ((rehState.lines || []).length);
|
||||||
if (_cs.cache[key]) { csGoToLibrary(); return; }
|
if (typeof navTo === 'function') navTo('s-reader');
|
||||||
const sheets = await csGenerate(text, key);
|
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) return;
|
||||||
if (!sheets.length) { toast('No characters found', 'error'); return; }
|
if (!sheets.length) { toast('No characters found', 'error'); return; }
|
||||||
if (window.rehState?.lines?.length) {
|
if (window.rehState?.lines?.length) {
|
||||||
@ -1084,8 +1146,8 @@ async function csForRehearser() {
|
|||||||
csAttachLineCounts(sheets, counts);
|
csAttachLineCounts(sheets, counts);
|
||||||
}
|
}
|
||||||
await csSaveToLibrary(book, sheets);
|
await csSaveToLibrary(book, sheets);
|
||||||
csGoToLibrary();
|
if (pageHost) await csShow(sheets, title, text, book, pageHost);
|
||||||
toast(sheets.length + ' character sheets saved — Library → Characters / Cast', 'success');
|
toast(sheets.length + ' character sheets saved — Library → Cast', 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
window.csForReader = csForReader;
|
window.csForReader = csForReader;
|
||||||
|
|||||||
@ -420,10 +420,19 @@ $('s-import-voices-file')?.addEventListener('change', async function () {
|
|||||||
|
|
||||||
function appendThinking(bubble, delta) {
|
function appendThinking(bubble, delta) {
|
||||||
ensureAssistantParts(bubble);
|
ensureAssistantParts(bubble);
|
||||||
bubble.querySelector('.conv-think-wrap').hidden = false;
|
const wrap = bubble.querySelector('.conv-think-wrap');
|
||||||
// Stays collapsed regardless of streaming state — the user only sees the
|
const toggle = bubble.querySelector('.conv-think-toggle');
|
||||||
// reasoning text if they click the "Thinking" chevron to expand it.
|
const body = bubble.querySelector('.conv-think-body');
|
||||||
bubble.querySelector('.conv-think-body').textContent += delta;
|
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) {
|
function setAnswerText(bubble, text) {
|
||||||
|
|||||||
@ -26,9 +26,9 @@ window.navLibraryView = function (view) {
|
|||||||
document.querySelectorAll('[data-library-panel]').forEach(function (el) {
|
document.querySelectorAll('[data-library-panel]').forEach(function (el) {
|
||||||
el.classList.toggle('is-active', el.dataset.libraryPanel === view);
|
el.classList.toggle('is-active', el.dataset.libraryPanel === view);
|
||||||
});
|
});
|
||||||
// Characters/Cast is the pipeline's "Cast Characters" stop — keep the
|
// Cast roster is its own pipeline stop; keep the stepper highlight in sync
|
||||||
// stepper's current-step highlight in sync when the tab is opened directly.
|
// when the tab is opened directly.
|
||||||
if (view === 'characters' && typeof refreshWorkflowCrumbs === 'function') refreshWorkflowCrumbs('chars');
|
if (view === 'characters' && typeof refreshWorkflowCrumbs === 'function') refreshWorkflowCrumbs('castlib');
|
||||||
libraryRender(view);
|
libraryRender(view);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1954,6 +1954,7 @@ window.addEventListener('resize', (function () {
|
|||||||
window.showReaderView = function (view) {
|
window.showReaderView = function (view) {
|
||||||
const main = $('reader-main-view');
|
const main = $('reader-main-view');
|
||||||
const cast = $('reader-audiobook-panel');
|
const cast = $('reader-audiobook-panel');
|
||||||
|
const chars = $('reader-charsheets-panel');
|
||||||
const lib = $('reader-library-card');
|
const lib = $('reader-library-card');
|
||||||
if (main) main.hidden = (view !== 'main');
|
if (main) main.hidden = (view !== 'main');
|
||||||
if (cast) {
|
if (cast) {
|
||||||
@ -1968,6 +1969,20 @@ window.showReaderView = function (view) {
|
|||||||
cast.className = 'ab-castpanel-inline card';
|
cast.className = 'ab-castpanel-inline card';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (chars) {
|
||||||
|
chars.hidden = (view !== 'chars');
|
||||||
|
if (view === 'chars' && !chars.innerHTML.trim()) {
|
||||||
|
chars.innerHTML = '<div class="card" style="padding:24px; min-height:260px; display:flex; flex-direction:column; gap:12px; justify-content:center;">'
|
||||||
|
+ '<div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Character sheets</div>'
|
||||||
|
+ '<p class="card-subtitle" style="margin:0;">This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.</p>'
|
||||||
|
+ '<div class="btn-row" style="margin-top:4px;">'
|
||||||
|
+ '<button class="btn-primary btn-sm" onclick="if (typeof csForReader === \'function\') csForReader();"><span class="mdi mdi-account-details-outline"></span> Cast from Reader</button>'
|
||||||
|
+ '<button class="btn-secondary btn-sm" onclick="if (typeof csForRehearser === \'function\') csForRehearser();"><span class="mdi mdi-theater"></span> Cast from Rehearser</button>'
|
||||||
|
+ '<button class="btn-secondary btn-sm" onclick="if (typeof navLibraryView === \'function\') navLibraryView(\'characters\');"><span class="mdi mdi-folder-account-outline"></span> Go to Cast</button>'
|
||||||
|
+ '</div></div>';
|
||||||
|
chars.className = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
if (lib) {
|
if (lib) {
|
||||||
lib.hidden = (view !== 'library');
|
lib.hidden = (view !== 'library');
|
||||||
if (view === 'library') {
|
if (view === 'library') {
|
||||||
@ -1978,7 +1993,9 @@ window.showReaderView = function (view) {
|
|||||||
readerRenderLibrary(); // Re-fetch on open
|
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) {
|
window.navReaderView = function (view) {
|
||||||
|
|||||||
@ -786,7 +786,7 @@ document.addEventListener('click', e => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ── Cross-workflow pipeline stepper ─────────────────────────────────────────
|
// ── 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
|
// → Generate MP3s → Audiobook. A persistent strip (rendered into any element
|
||||||
// with class .wf-stepper — currently one in s-reader.html, one in
|
// with class .wf-stepper — currently one in s-reader.html, one in
|
||||||
// s-rehearser.html) that lets you jump directly to any reachable stage
|
// 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: 'source', label: 'Source', enabled: () => true },
|
||||||
{ key: 'cast', label: 'Cast Audiobook', enabled: () => !!window.readerState?.sentences?.length },
|
{ key: 'cast', label: 'Cast Audiobook', enabled: () => !!window.readerState?.sentences?.length },
|
||||||
{ key: 'chars', label: 'Cast Characters', enabled: () => !!window.readerState?.sentences?.length, optional: true },
|
{ 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: 'rehearser', label: 'Script Rehearser',enabled: () => !!window.rehState?.lines?.length },
|
||||||
{ key: 'mp3', label: 'Generate MP3s', enabled: () => !!window.rehState?.lines?.length },
|
{ key: 'mp3', label: 'Generate MP3s', enabled: () => !!window.rehState?.lines?.length },
|
||||||
{ key: 'audiobook', label: 'Audiobook', enabled: () => !!window.rehState?.clips?.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 navTo === 'function') navTo('s-reader');
|
||||||
if (typeof audiobookOpenCastView === 'function') audiobookOpenCastView();
|
if (typeof audiobookOpenCastView === 'function') audiobookOpenCastView();
|
||||||
} else if (key === 'chars') {
|
} else if (key === 'chars') {
|
||||||
// Navigate to the Characters/Cast library view — generating sheets stays
|
// Navigate to the character-sheet stage in Read Aloud.
|
||||||
// on the explicit "Cast Characters" buttons; a navigation stop must never
|
if (typeof navTo === 'function') navTo('s-reader');
|
||||||
// kick off an LLM run as a side effect.
|
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 navTo === 'function') navTo('s-library');
|
||||||
if (typeof navLibraryView === 'function') navLibraryView('characters');
|
if (typeof navLibraryView === 'function') navLibraryView('characters');
|
||||||
_wfActive = 'chars';
|
_wfActive = 'castlib';
|
||||||
refreshWorkflowCrumbs();
|
refreshWorkflowCrumbs();
|
||||||
} else if (key === 'rehearser') {
|
} else if (key === 'rehearser') {
|
||||||
if (typeof navTo === 'function') navTo('s-rehearser');
|
if (typeof navTo === 'function') navTo('s-rehearser');
|
||||||
@ -887,7 +892,7 @@ function refreshWorkflowCrumbs(active) {
|
|||||||
window.refreshWorkflowCrumbs = refreshWorkflowCrumbs;
|
window.refreshWorkflowCrumbs = refreshWorkflowCrumbs;
|
||||||
|
|
||||||
// Rehearser's own phase tabs (Library/Cast/Stage/Summary) map onto the last
|
// 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) {
|
window.onRehearserPhaseChange = function (n) {
|
||||||
refreshWorkflowCrumbs(n === 3 ? 'mp3' : n === 4 ? 'audiobook' : 'rehearser');
|
refreshWorkflowCrumbs(n === 3 ? 'mp3' : n === 4 ? 'audiobook' : 'rehearser');
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,7 +11,7 @@
|
|||||||
<div class="lib-tabs">
|
<div class="lib-tabs">
|
||||||
<button class="lib-tab is-active" data-library-view="books" onclick="navLibraryView('books')"><span class="mdi mdi-book-open-page-variant-outline"></span> Books</button>
|
<button class="lib-tab is-active" data-library-view="books" onclick="navLibraryView('books')"><span class="mdi mdi-book-open-page-variant-outline"></span> Books</button>
|
||||||
<button class="lib-tab" data-library-view="plays" onclick="navLibraryView('plays')"><span class="mdi mdi-theater"></span> Theater Plays</button>
|
<button class="lib-tab" data-library-view="plays" onclick="navLibraryView('plays')"><span class="mdi mdi-theater"></span> Theater Plays</button>
|
||||||
<button class="lib-tab" data-library-view="characters" onclick="navLibraryView('characters')"><span class="mdi mdi-account-box-multiple-outline"></span> Characters / Cast</button>
|
<button class="lib-tab" data-library-view="characters" onclick="navLibraryView('characters')"><span class="mdi mdi-account-box-multiple-outline"></span> Cast</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Books -->
|
<!-- Books -->
|
||||||
|
|||||||
@ -191,6 +191,9 @@
|
|||||||
<!-- ⑦ Audiobook Casting Panel ──────────────────────────────────── -->
|
<!-- ⑦ Audiobook Casting Panel ──────────────────────────────────── -->
|
||||||
<div id="reader-audiobook-panel" hidden></div>
|
<div id="reader-audiobook-panel" hidden></div>
|
||||||
|
|
||||||
|
<!-- ⑧ Character Sheets Stage ──────────────────────────────────── -->
|
||||||
|
<div id="reader-charsheets-panel" hidden></div>
|
||||||
|
|
||||||
<!-- Saved books now live in the combined Library → Books section. -->
|
<!-- Saved books now live in the combined Library → Books section. -->
|
||||||
|
|
||||||
</div><!-- /tab-reader -->
|
</div><!-- /tab-reader -->
|
||||||
|
|||||||
@ -5715,6 +5715,17 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
|
|
||||||
/* Character sheets overlay */
|
/* Character sheets overlay */
|
||||||
.cs-box { width: min(1120px, 96vw); max-height: 90vh; display: flex; flex-direction: column; }
|
.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-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-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; }
|
.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; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user