diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef722c..a1e8035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/VERSION b/VERSION index c6ba3bc..52e779f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.14.6 +1.14.7 diff --git a/static/index.html b/static/index.html index 895bfa4..701392e 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 36102c4..1794ede 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -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 = `
LLM Reading… - ${isLong ? '' : ''} +
${preview}
- ${isLong ? `` : ''} `; - 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 ...; 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(''); - let shown, isReal = false; + let shown, isReal = false, closeAt = -1; if (openAt >= 0) { const afterOpen = this._thinkRaw.slice(openAt + ''.length); - const closeAt = afterOpen.indexOf(''); + closeAt = afterOpen.indexOf(''); shown = closeAt >= 0 ? afterOpen.slice(0, closeAt) : afterOpen; isReal = true; - if (closeAt >= 0) this._thinkDone = true; } else { shown = this._thinkRaw; } diff --git a/static/style.css b/static/style.css index dfb6f4b..fc576e0 100644 --- a/static/style.css +++ b/static/style.css @@ -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; }