Redesign Character sheets progress modal into a big split view (v1.14.16)

Was a small 420px dialog with a collapsible one-line JSON preview and a
cramped 2-column chip grid. Now a big panel matching the audiobook
casting live view's shape: passage text (left) and live JSON output
(right) side by side, with a character sidebar on the right edge whose
just-updated entries are highlighted — so the user can watch each sheet
actually being built from its source passage instead of a tiny opaque
progress bar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-09 11:53:57 +02:00
parent b96968ac5d
commit b5ffa655d8
5 changed files with 90 additions and 57 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.14.16] — 2026-07-09
### Changed
- **Redesigned the Character sheets progress modal** — was a small (420px) dialog with a collapsible single-line JSON preview and a cramped chip grid. Now a large panel (like the audiobook casting live view): the passage currently being analysed on the left, the LLM's live JSON output filling in on the right, and a character sidebar on the right edge with the character(s) just updated highlighted — so you can watch each sheet actually being built from its source text.
---
## [1.14.15] — 2026-07-09 ## [1.14.15] — 2026-07-09
### Added ### Added

View File

@ -1 +1 @@
1.14.15 1.14.16

View File

@ -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.15"> <meta name="app-version" content="1.14.16">
<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.15"> <link rel="stylesheet" href="/static/style.css?v=1.14.16">
<!-- ── 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.15"></script> <script src="/static/loader.js?v=1.14.16"></script>
</body> </body>
</html> </html>

View File

@ -314,7 +314,7 @@ async function csGenerate(text, cacheKey, initialRoster) {
? `${knownTotal} cast characters queued · ${detailed} profiles with details` ? `${knownTotal} cast characters queued · ${detailed} profiles with details`
: `${map.size} characters found`; : `${map.size} characters found`;
prog.update(i, `Passage ${i + 1} / ${chunks.length}`, charLabel, csProgressSnapshot(map)); prog.update(i, `Passage ${i + 1} / ${chunks.length}`, charLabel, csProgressSnapshot(map));
prog.startPassage(); prog.startPassage(chunks[i]);
try { try {
const body = { text: chunks[i], known_characters: roster.slice(0, 120), target_mode: targetMode, existing: csExistingSummary(map), language, llm_url, model }; const body = { text: chunks[i], known_characters: roster.slice(0, 120), target_mode: targetMode, existing: csExistingSummary(map), language, llm_url, model };
let data = null; let data = null;
@ -368,15 +368,32 @@ function csProgress(total) {
if (!ov) { if (!ov) {
ov = document.createElement('div'); ov = document.createElement('div');
ov.id = 'cs-progress'; ov.className = 'audiobook-overlay'; ov.id = 'cs-progress'; ov.className = 'audiobook-overlay';
ov.innerHTML = `<div class="audiobook-box cs-progress-box"> // Big two-pane layout, mirroring the audiobook casting live view: the
<div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Character sheets</div> // passage being analysed on the left, the LLM's raw JSON output filling
<div class="audiobook-msg" id="cs-progress-msg">Analysing</div> // in live on the right, and a character sidebar (current one highlighted)
<div class="reader-synth-track" style="margin-bottom:4px"><div class="reader-synth-fill" id="cs-progress-fill"></div></div> // so the user can watch each sheet actually being built.
<details class="cs-progress-live" id="cs-progress-live" open> ov.innerHTML = `<div class="audiobook-box cs-progress-box cs-progress-box-big">
<summary><span class="mdi mdi-text-long"></span> Live output <span class="cs-progress-live-status" id="cs-progress-live-status">watching</span></summary> <div class="cs-progress-head">
<pre class="cs-progress-live-pre" id="cs-progress-live-pre"></pre> <div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Character sheets</div>
</details> <div class="audiobook-msg" id="cs-progress-msg">Analysing</div>
<div class="cs-progress-chars" id="cs-progress-chars"></div> <div class="reader-synth-track" style="margin-bottom:0"><div class="reader-synth-fill" id="cs-progress-fill"></div></div>
</div>
<div class="cs-progress-body">
<div class="cs-progress-main">
<div class="cs-progress-col">
<div class="cs-progress-col-label"><span class="mdi mdi-text-long"></span> Passage</div>
<pre class="cs-progress-passage-pre" id="cs-progress-passage-pre"></pre>
</div>
<div class="cs-progress-col">
<div class="cs-progress-col-label"><span class="mdi mdi-code-json"></span> Live output <span class="cs-progress-live-status" id="cs-progress-live-status">watching</span></div>
<pre class="cs-progress-live-pre" id="cs-progress-live-pre"></pre>
</div>
</div>
<div class="cs-progress-side">
<div class="cs-progress-side-title" id="cs-progress-side-title">Characters found</div>
<div class="cs-progress-chars" id="cs-progress-chars"></div>
</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); document.body.appendChild(ov);
@ -386,13 +403,16 @@ function csProgress(total) {
const fill = ov.querySelector('#cs-progress-fill'); const fill = ov.querySelector('#cs-progress-fill');
const msg = ov.querySelector('#cs-progress-msg'); const msg = ov.querySelector('#cs-progress-msg');
const chars = ov.querySelector('#cs-progress-chars'); 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 livePre = ov.querySelector('#cs-progress-live-pre');
const liveStatus = ov.querySelector('#cs-progress-live-status'); const liveStatus = ov.querySelector('#cs-progress-live-status');
return { return {
// Called once per passage before its request starts, so the live-output // Called once per passage before its request starts, so both panes are
// pane is empty and honestly labelled before we know whether this model // reset and honestly labelled before we know whether this model streams
// streams real content or the request falls back to the blocking path. // real content or the request falls back to the blocking path.
startPassage() { startPassage(text) {
if (passagePre) passagePre.textContent = text || '';
if (livePre) livePre.textContent = ''; if (livePre) livePre.textContent = '';
if (liveStatus) { liveStatus.textContent = 'watching…'; liveStatus.className = 'cs-progress-live-status'; } if (liveStatus) { liveStatus.textContent = 'watching…'; liveStatus.className = 'cs-progress-live-status'; }
}, },
@ -411,26 +431,22 @@ function csProgress(total) {
update(d, label, charLabel, roster = []) { update(d, label, charLabel, roster = []) {
if (fill) fill.style.width = (d / total * 100) + '%'; if (fill) fill.style.width = (d / total * 100) + '%';
if (msg && label) msg.textContent = label; if (msg && label) msg.textContent = label;
if (sideTitle) sideTitle.textContent = charLabel || 'Characters found';
if (chars) { if (chars) {
const list = Array.isArray(roster) ? roster : []; const list = Array.isArray(roster) ? roster : [];
chars.innerHTML = ` chars.innerHTML = list.length ? list.map(c => `
<div class="cs-progress-summary">${escHtml(charLabel || '')}</div> <div class="cs-progress-chip ${c.status ? 'is-' + escHtml(c.status) : ''}">
${list.length ? `<div class="cs-progress-list"> <span class="cs-progress-dot">${escHtml((c.name || '?')[0].toUpperCase())}</span>
${list.map(c => ` <span class="cs-progress-meta">
<div class="cs-progress-chip ${c.status ? 'is-' + escHtml(c.status) : ''}"> <span class="cs-progress-name">${escHtml(c.name || 'Unknown')}</span>
<span class="cs-progress-dot">${escHtml((c.name || '?')[0].toUpperCase())}</span> ${c.alias ? `<span class="cs-progress-alias">${escHtml(c.alias)}</span>` : ''}
<span class="cs-progress-meta"> </span>
<span class="cs-progress-name">${escHtml(c.name || 'Unknown')}</span> <span class="cs-progress-fillmeta" title="${escHtml(String(c.filled || 0))} profile fields filled">
${c.alias ? `<span class="cs-progress-alias">${escHtml(c.alias)}</span>` : ''} <span style="width:${Math.min(100, Math.round(((c.filled || 0) / CS_DETAIL_FIELDS.length) * 100))}%"></span>
</span> </span>
<span class="cs-progress-fillmeta" title="${escHtml(String(c.filled || 0))} profile fields filled"> ${c.status ? `<span class="cs-progress-badge">${escHtml(c.status)}</span>` : ''}
<span style="width:${Math.min(100, Math.round(((c.filled || 0) / CS_DETAIL_FIELDS.length) * 100))}%"></span> </div>
</span> `).join('') : '<div class="cs-progress-empty">reading…</div>';
${c.status ? `<span class="cs-progress-badge">${escHtml(c.status)}</span>` : ''}
</div>
`).join('')}
</div>` : ''}
`;
} }
}, },
done() { ov.style.display = 'none'; }, done() { ov.style.display = 'none'; },

View File

@ -5780,32 +5780,42 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
} }
.cs-arc-note { font-size: 11px; color: var(--subtext); margin-top: 3px; font-style: italic; } .cs-arc-note { font-size: 11px; color: var(--subtext); margin-top: 3px; font-style: italic; }
.cs-progress-box { width: min(520px, 92vw); } .cs-progress-box { width: min(520px, 92vw); }
.cs-progress-live { margin-bottom: 10px; border: 1px solid var(--border); border-radius: 8px; } /* Big layout: passage | live output side by side, character sidebar on the
.cs-progress-live summary { right (current/just-updated characters highlighted) same shape as the
display: flex; align-items: center; gap: 6px; padding: 7px 10px; font-size: 11.5px; audiobook casting live view, so you can watch each sheet being built. */
font-weight: 700; color: var(--subtext); cursor: pointer; list-style: none; .cs-progress-box-big { width: min(1400px, 96vw); height: min(880px, 90vh); display: flex; flex-direction: column; padding: 20px 22px; }
.cs-progress-head { flex-shrink: 0; }
.cs-progress-head .audiobook-msg { margin: 8px 0 10px; }
.cs-progress-body { flex: 1; min-height: 0; display: flex; gap: 14px; margin-top: 12px; }
.cs-progress-main { flex: 1; min-width: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.cs-progress-col { min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 6px; }
.cs-progress-col-label {
display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 800;
text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); flex-shrink: 0;
} }
.cs-progress-live summary::-webkit-details-marker { display: none; } .cs-progress-passage-pre, .cs-progress-live-pre {
.cs-progress-live-status { margin-left: auto; font-weight: 500; font-style: italic; opacity: .8; } flex: 1; min-height: 0; margin: 0; padding: 10px 12px; overflow-y: auto; white-space: pre-wrap;
.cs-progress-live-status.is-live { color: var(--accent); opacity: 1; } overflow-wrap: anywhere; font-size: 12px; line-height: 1.55; color: var(--text);
.cs-progress-live-pre { background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
margin: 0; padding: 8px 10px; max-height: 160px; overflow-y: auto; white-space: pre-wrap;
overflow-wrap: anywhere; font-size: 11px; line-height: 1.5; color: var(--text);
background: var(--bg); border-top: 1px solid var(--border);
}
.cs-progress-chars { font-size: 11px; color: var(--subtext); min-height: 16px; margin-bottom: 8px; }
.cs-progress-summary { text-align: center; margin-bottom: 8px; color: var(--subtext); }
.cs-progress-list {
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px;
max-height: 210px; overflow: auto; padding: 2px 2px 4px;
} }
.cs-progress-live-pre { background: rgba(37,99,235,.05); }
.cs-progress-side { width: 260px; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; min-height: 0; }
.cs-progress-side-title { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; color: var(--subtext); flex-shrink: 0; }
.cs-progress-chars { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; padding: 2px 2px 4px; }
.cs-progress-empty { font-size: 12px; color: var(--subtext); font-style: italic; text-align: center; padding: 20px 0; }
.cs-progress-chip { .cs-progress-chip {
display: grid; grid-template-columns: 22px minmax(0, 1fr) 44px auto; align-items: center; gap: 6px; display: grid; grid-template-columns: 26px minmax(0, 1fr) 44px auto; align-items: center; gap: 8px;
min-height: 34px; padding: 5px 6px; border: 1px solid var(--border); border-radius: 7px; min-height: 38px; padding: 6px 8px; border: 1px solid var(--border); border-radius: 8px;
background: var(--surface); color: var(--text); background: var(--surface); color: var(--text); flex-shrink: 0;
} }
.cs-progress-chip.is-new { border-color: rgba(34,197,94,.45); background: rgba(34,197,94,.08); } .cs-progress-chip.is-new { border-color: rgba(34,197,94,.45); background: rgba(34,197,94,.08); }
.cs-progress-chip.is-updated { border-color: rgba(59,130,246,.45); background: rgba(59,130,246,.08); } .cs-progress-chip.is-updated { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 10%, transparent); box-shadow: 0 0 0 1px var(--accent); }
@media (max-width: 900px) {
.cs-progress-box-big { width: 96vw; height: 92vh; }
.cs-progress-body { flex-direction: column; }
.cs-progress-main { grid-template-columns: 1fr; }
.cs-progress-side { width: 100%; max-height: 180px; }
}
.cs-progress-dot { .cs-progress-dot {
width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;
background: var(--accent); color: #fff; font-size: 10px; font-weight: 800; background: var(--accent); color: #fff; font-size: 10px; font-weight: 800;