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
All checks were successful
Forgejo Docker Build / Root app tests (push) Successful in 47s
Forgejo Docker Build / Build Docker image (push) Successful in 8s
Forgejo Docker Build / End-to-end (browser) (push) Successful in 5s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-13 21:11:36 +02:00
parent 4de76dbfd9
commit 684a8920e9
5 changed files with 46 additions and 22 deletions

View file

@ -101,9 +101,8 @@
<span id="mr-status" role="status" style="font-size:12px;color:var(--g600);"></span>
</div>
<!-- An illustration the model asked for. It renders here rather than in an
image history, because this feature does not have one and telling
someone to look somewhere that does not exist is worse than silence. -->
<!-- The status of an illustration the model asked for. The picture itself
goes into the deck and the Images tab below, never into this page. -->
<div id="mr-images" style="display:flex;flex-direction:column;gap:10px;"></div>
</div>
</div>

View file

@ -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;
}
}

View file

@ -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');

View file

@ -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) => {

View file

@ -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; \}/);
});