From 684a8920e90e9421f8d8050b119cb1ca587c2cac Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 21:11:36 +0200 Subject: [PATCH] fix: a deck's illustration stays out of the page (status line only, tile under Images); a streaming table head is held back, never shown as pipes Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU --- public/components/my-resources.html | 5 ++-- public/js/clinicalAssistant.js | 34 +++++++++++++++++-------- public/js/myResources.js | 15 ++++++----- test/assistant-streaming-blocks.test.js | 10 +++++++- test/backend-hardening.test.js | 4 ++- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/public/components/my-resources.html b/public/components/my-resources.html index d69ca3bc..8a492e95 100644 --- a/public/components/my-resources.html +++ b/public/components/my-resources.html @@ -101,9 +101,8 @@ - +
diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 61fa3c49..d1895ad4 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -704,23 +704,35 @@ import { // chart would be handed to its renderer on every frame and fail there. var growing = partialTableMarkdown(tail); var openFence = (tail.match(/```/g) || []).length % 2 === 1; - // A header row with nothing under it yet is a table markdown-it would - // happily draw — two lines of pipes and an empty body. Not yet. - var headerOnly = !growing && TABLE_ROW.test(tail.trim().split('\n')[0] || ''); + // Pipe-led lines at the end of the tail that are not yet a drawable table + // — a header row, a header and its rule, a half-typed row — are held back + // rather than shown as pipes. They appear a frame later, as a table, once + // the first body row has arrived. Showing them raw was the flash of table + // syntax the reader saw at the top of every table. + var shown = growing; + if (!shown) { + var kept = tail.split('\n'); + while (kept.length && /^\s*\|/.test(kept[kept.length - 1])) kept.pop(); + shown = kept.join('\n'); + } // A bold run whose closing ** has not arrived yet shows as literal // asterisks for a frame or two; closing it for the render keeps the text // bold from its first character. The real text is untouched. - var balanced = tail; - if ((tail.match(/\*\*/g) || []).length % 2 === 1) balanced = tail + '**'; - if (tail && !openFence && !headerOnly) { - state.tail.className = 'assistant-stream-tail assistant-stream-partial'; - state.tail.innerHTML = renderAssistantBubbleHtml(growing || balanced, sources, false); - renderEmbeddedBlocks(state.tail); - state.tail.hidden = false; - } else { + var balanced = shown; + if ((shown.match(/\*\*/g) || []).length % 2 === 1) balanced = shown + '**'; + if (openFence) { state.tail.className = 'assistant-stream-tail assistant-streaming-text'; state.tail.textContent = tail; state.tail.hidden = !tail; + } else if (!balanced.trim()) { + state.tail.className = 'assistant-stream-tail assistant-stream-partial'; + state.tail.textContent = ''; + state.tail.hidden = true; + } else { + state.tail.className = 'assistant-stream-tail assistant-stream-partial'; + state.tail.innerHTML = renderAssistantBubbleHtml(balanced, sources, false); + renderEmbeddedBlocks(state.tail); + state.tail.hidden = false; } } diff --git a/public/js/myResources.js b/public/js/myResources.js index ad600c8c..ceafed56 100644 --- a/public/js/myResources.js +++ b/public/js/myResources.js @@ -346,18 +346,21 @@ : failures.length + ' illustrations could not be started.', 'error'); } + // The picture belongs to the deck and to the Images tab, not to the page: + // a full-size illustration under the form read as the output of the run, + // and stayed there looking like a leak. Here only the job's status line is + // shown, and when the picture is ready the library is refreshed so it turns + // up as a tile under Images, where every figure this account made lives. function showIllustrations(jobs) { var box = document.getElementById('mr-images'); if (!box) return; box.innerHTML = ''; if (!jobs.length) return; import('/js/generatedImages.js').then(function (m) { - m.renderImageJobs(box, jobs, 'my_resources', function (card, data) { - var img = document.createElement('img'); - img.alt = 'Generated teaching illustration'; - img.style.maxWidth = '100%'; - card.append(img); - m.hydrateImage(img, data.imageUrl).catch(function () { img.alt = 'Private image unavailable'; }); + m.renderImageJobs(box, jobs, 'my_resources', function (card) { + var line = card.querySelector('[role="status"]'); + if (line) line.textContent = 'Illustration ready \u2014 it is in the deck and under Images.'; + loadLibrary(); }); }).catch(function () { if (typeof showToast === 'function') showToast('An illustration was generated but could not be displayed.', 'info'); diff --git a/test/assistant-streaming-blocks.test.js b/test/assistant-streaming-blocks.test.js index 8b017889..fb69f434 100644 --- a/test/assistant-streaming-blocks.test.js +++ b/test/assistant-streaming-blocks.test.js @@ -124,7 +124,10 @@ test('the settled part is real markdown and the tail is markdown too, short of a assert.ok(settled.querySelector('table'), 'the finished table should be a table'); assert.equal(settled.querySelectorAll('table tbody tr').length, 2); const tail = bubble.querySelector('.assistant-stream-tail'); - assert.equal(tail.textContent.trim(), '| Half | tab'); + // A half-typed row of a table that has no body yet is held back, not shown + // as pipes: the reader sees nothing until the row can be drawn as a table. + assert.equal(tail.textContent.trim(), ''); + assert.ok(tail.hidden); assert.equal(tail.querySelector('table'), null, 'the unfinished row must not be parsed'); }); @@ -255,6 +258,11 @@ test('a header with no body row yet is not yet a table', (t) => { const bubble = bubbleFor(ctx); ctx.renderStreamingInto(bubble, 'Doses:\n\n' + TABLE_HEAD, []); assert.equal(bubble.querySelector('table'), null); + assert.doesNotMatch(bubble.textContent, /\|/, 'no raw pipe syntax on screen while the head streams'); + // Text before the head in the same block still shows; only the head waits. + ctx.renderStreamingInto(bubble, 'Doses:\n\nHere they are:\n' + TABLE_HEAD, []); + assert.doesNotMatch(bubble.textContent, /\|/); + assert.match(bubble.textContent, /Here they are:/); }); test('the finished table settles, and is not left rendered twice', (t) => { diff --git a/test/backend-hardening.test.js b/test/backend-hardening.test.js index ce5d4573..21f4b73a 100644 --- a/test/backend-hardening.test.js +++ b/test/backend-hardening.test.js @@ -333,6 +333,8 @@ test('opening the share panel does not redraw the library, and the Nextcloud upl }); test('a streaming tail with an unclosed bold run is rendered closed, and the starter questions keep clear of the box', () => { - assert.match(read('public/js/clinicalAssistant.js'), /if \(\(tail\.match\(\/\\\*\\\*\/g\) \|\| \[\]\)\.length % 2 === 1\) balanced = tail \+ '\*\*';/); + assert.match(read('public/js/clinicalAssistant.js'), /if \(\(shown\.match\(\/\\\*\\\*\/g\) \|\| \[\]\)\.length % 2 === 1\) balanced = shown \+ '\*\*';/); + // Pipe-led lines that are not yet a table are held back, never shown raw. + assert.match(read('public/js/clinicalAssistant.js'), /while \(kept\.length && \/\^\\s\*\\\|\/\.test\(kept\[kept\.length - 1\]\)\) kept\.pop\(\);/); assert.match(read('public/css/assistant.css'), /\.assistant-empty \{ margin:0 auto 28px; \}/); });