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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
4de76dbfd9
commit
684a8920e9
5 changed files with 46 additions and 22 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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; \}/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue