Fix triple-duplicated passage in audiobook casting's live LLM view (v1.14.7)
The "LLM Reading…" split view showed the same passage text three times (reader pane, Passage column, and a full-width block toggled by the chevron that duplicated the Passage column). Removed the redundant full-width block, repurposed the chevron to show/hide the two-column view itself, and swapped the columns to Passage (left) / Thinking (right). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ed76af4976
commit
c969150095
@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.14.7] — 2026-07-08
|
||||
|
||||
### Fixed
|
||||
- **Audiobook casting's "LLM Reading…" live view showed the passage three times** — once in the reader pane above it, once in a "Passage" column, and once more in a full-width block toggled by the chevron (which duplicated the "Passage" column exactly). Removed the redundant full-width block; the chevron now shows/hides the two-column view itself (previously it appeared automatically once the LLM's first token arrived and had no way to be hidden again). Swapped the column order to Passage (left) / Thinking (right) to match the Conversation Playground's layout.
|
||||
|
||||
---
|
||||
|
||||
## [1.14.6] — 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.14.6">
|
||||
<meta name="app-version" content="1.14.7">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,7 +27,7 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.14.6">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.14.7">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.14.6"></script>
|
||||
<script src="/static/loader.js?v=1.14.7"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -3051,64 +3051,57 @@ STRIKTE FORMAT- UND TEXTREGELN:
|
||||
this._procRow.className = 'ab-cv-row is-processing ab-cv-llm-row';
|
||||
const isLong = text.length > 160;
|
||||
const preview = escHtml(text.slice(0, 160)) + (isLong ? '…' : '');
|
||||
// Split view while the LLM works: left = its live thinking stream (fed
|
||||
// by view.thinking via the SSE endpoint), right = the passage it reads.
|
||||
// Split view (collapsed by default, toggled via the chevron): left =
|
||||
// the passage being read, right = the LLM's live thinking/raw-output
|
||||
// stream (fed by view.thinking via the SSE endpoint).
|
||||
this._procRow.innerHTML = `
|
||||
<div class="ab-cv-llm-head">
|
||||
<span class="ab-cv-spk" style="color:var(--blue)"><span class="mdi mdi-loading mdi-spin"></span> LLM Reading…</span>
|
||||
${isLong ? '<button class="ab-cv-expand-btn" title="Show full passage"><span class="mdi mdi-chevron-down"></span></button>' : ''}
|
||||
<button class="ab-cv-expand-btn" type="button" title="Show passage & thinking" aria-expanded="false"><span class="mdi mdi-chevron-right"></span></button>
|
||||
</div>
|
||||
<div class="ab-cv-llm-preview">${preview}</div>
|
||||
<div class="ab-cv-llm-split" hidden>
|
||||
<div class="ab-cv-llm-col">
|
||||
<div class="ab-cv-llm-col-label ab-cv-think-label"><span class="mdi mdi-head-dots-horizontal-outline"></span> LLM Thinking…</div>
|
||||
<pre class="ab-cv-think"></pre>
|
||||
</div>
|
||||
<div class="ab-cv-llm-col">
|
||||
<div class="ab-cv-llm-col-label"><span class="mdi mdi-text-long"></span> Passage</div>
|
||||
<pre class="ab-cv-llm-pre">${escHtml(text)}</pre>
|
||||
</div>
|
||||
<div class="ab-cv-llm-col">
|
||||
<div class="ab-cv-llm-col-label ab-cv-think-label"><span class="mdi mdi-head-dots-horizontal-outline"></span> LLM Thinking…</div>
|
||||
<pre class="ab-cv-think"></pre>
|
||||
</div>
|
||||
</div>
|
||||
${isLong ? `<div class="ab-cv-llm-full" hidden><pre class="ab-cv-llm-pre">${escHtml(text)}</pre></div>` : ''}
|
||||
`;
|
||||
if (isLong) {
|
||||
this._procRow.querySelector('.ab-cv-expand-btn').addEventListener('click', function () {
|
||||
const full = this.closest('.ab-cv-llm-row').querySelector('.ab-cv-llm-full');
|
||||
full.hidden = !full.hidden;
|
||||
const icon = this.querySelector('span');
|
||||
if (icon) icon.className = full.hidden ? 'mdi mdi-chevron-down' : 'mdi mdi-chevron-up';
|
||||
});
|
||||
}
|
||||
this._procRow.querySelector('.ab-cv-expand-btn').addEventListener('click', function () {
|
||||
const split = this.closest('.ab-cv-llm-row').querySelector('.ab-cv-llm-split');
|
||||
const open = this.getAttribute('aria-expanded') === 'true';
|
||||
this.setAttribute('aria-expanded', String(!open));
|
||||
split.hidden = open;
|
||||
const icon = this.querySelector('span');
|
||||
if (icon) icon.className = open ? 'mdi mdi-chevron-right' : 'mdi mdi-chevron-down';
|
||||
});
|
||||
_abPage().appendChild(this._procRow);
|
||||
trim();
|
||||
},
|
||||
// Live LLM output for the passage currently processing. First delta swaps
|
||||
// the plain preview for the split thinking/passage view; subsequent deltas
|
||||
// append and keep the thinking pane scrolled to the newest text.
|
||||
// Live LLM output for the passage currently processing. Streams into the
|
||||
// (collapsed-by-default) thinking column; the user reveals it via the
|
||||
// chevron button set up in processing() above.
|
||||
thinking(delta) {
|
||||
if (!this._procRow || !delta || this._thinkDone) return;
|
||||
this._thinkRaw = (this._thinkRaw || '') + delta;
|
||||
const split = this._procRow.querySelector('.ab-cv-llm-split');
|
||||
const think = this._procRow.querySelector('.ab-cv-think');
|
||||
const label = this._procRow.querySelector('.ab-cv-think-label');
|
||||
if (!split || !think) return;
|
||||
if (split.hidden) {
|
||||
split.hidden = false;
|
||||
const prev = this._procRow.querySelector('.ab-cv-llm-preview');
|
||||
if (prev) prev.hidden = true;
|
||||
}
|
||||
if (!think) return;
|
||||
// Some models wrap real reasoning in <think>...</think>; others ignore
|
||||
// that instruction and stream straight into the JSON answer. Show
|
||||
// whichever is actually arriving — live progress beats nothing — but
|
||||
// label it honestly instead of calling raw JSON "thinking".
|
||||
const openAt = this._thinkRaw.indexOf('<think>');
|
||||
let shown, isReal = false;
|
||||
let shown, isReal = false, closeAt = -1;
|
||||
if (openAt >= 0) {
|
||||
const afterOpen = this._thinkRaw.slice(openAt + '<think>'.length);
|
||||
const closeAt = afterOpen.indexOf('</think>');
|
||||
closeAt = afterOpen.indexOf('</think>');
|
||||
shown = closeAt >= 0 ? afterOpen.slice(0, closeAt) : afterOpen;
|
||||
isReal = true;
|
||||
if (closeAt >= 0) this._thinkDone = true;
|
||||
} else {
|
||||
shown = this._thinkRaw;
|
||||
}
|
||||
|
||||
@ -5375,7 +5375,6 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
font-size: 12px; color: var(--subtext); font-style: italic;
|
||||
padding: 2px 0 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.ab-cv-llm-full { margin-top: 6px; }
|
||||
.ab-cv-llm-pre {
|
||||
font-family: inherit; font-size: 12px; color: var(--text);
|
||||
white-space: pre-wrap; word-break: break-word;
|
||||
@ -5403,8 +5402,9 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
/* Every word in the narration/dialogue text is individually clickable — the
|
||||
basis for "click a name in the text to assign", including names the app
|
||||
doesn't already know (drag across a multi-word one to select it all). */
|
||||
/* Live LLM view while a passage is processing: thinking stream left, the
|
||||
passage it reads right. Swapped in by view.thinking() on the first delta. */
|
||||
/* Live LLM view while a passage is processing: passage left, the LLM's live
|
||||
thinking/raw-output stream right. Collapsed by default; toggled by the
|
||||
chevron button in .ab-cv-llm-head. */
|
||||
.ab-cv-llm-split { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 6px; }
|
||||
.ab-cv-llm-col { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.ab-cv-llm-col-label { font-size: 10.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em; color: var(--subtext); display: flex; align-items: center; gap: 5px; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user