Fold long answers and offer a way back to the newest line
This commit is contained in:
@@ -164,6 +164,24 @@ function vv_ai_chat_assets(): void {
|
||||
15" panels with no pointer near them, so a control that only exists on hover does not exist. */
|
||||
/* Sits directly above the composer, so what it acts on — the exchange just above — reads left to
|
||||
right into the box you would retype it in. */
|
||||
/* Purely a positioning context. Deliberately NOT a flex item that grows: the parent is a column
|
||||
flex container, .vv-ai-chat was flex:0 1 auto there, and it carries its own height — set inline
|
||||
by setHeights() for the placements whose size is a share of a panel. A wrapper with flex:1 1
|
||||
auto would stretch past the transcript and leave a dead strip under it, which is the same shape
|
||||
of bug as the grid-stretch one. Block-level, no padding, no flex: the geometry is unchanged and
|
||||
only the coordinate system is new. min-height:0 because flex items default to auto and would
|
||||
refuse to shrink. */
|
||||
.vv-ai-chat-wrap { position:relative; min-width:0; min-height:0; }
|
||||
.vv-ai-jump { position:absolute; left:50%; transform:translateX(-50%); bottom:8px; z-index:3;
|
||||
background:#242424; border:1px solid #3a3a3a; color:#ccc; font-size:10px;
|
||||
padding:3px 12px; border-radius:11px; cursor:pointer; opacity:.94; }
|
||||
.vv-ai-jump:hover { background:#2e2e2e; color:#fff; }
|
||||
|
||||
/* Folded answers cap at a readable height with a hard bottom edge rather than a fade — a fade
|
||||
over a code block reads as a rendering fault. */
|
||||
.vv-ai-fold { max-height:420px; overflow:hidden; }
|
||||
.vv-ai-more { display:block; margin:4px 0 2px; }
|
||||
|
||||
.vv-ai-last { display:flex; gap:10px; padding:0 2px 4px; }
|
||||
.vv-ai-lnk { background:none; border:none; padding:0; font-size:10px; color:#4a4a4a;
|
||||
cursor:pointer; text-decoration:underline; text-underline-offset:2px; }
|
||||
@@ -550,6 +568,15 @@ vv_ai_profiles_script();
|
||||
return (c.scrollHeight - c.scrollTop - c.clientHeight) < 40;
|
||||
};
|
||||
|
||||
// The other half of stick-to-bottom. Scrolling up during a stream deliberately stops the
|
||||
// transcript chasing you, which means text is now arriving off-screen with nothing saying so.
|
||||
// The pill is that acknowledgement, and the way back.
|
||||
function syncJump() {
|
||||
const p = $('jump');
|
||||
if (!p) return;
|
||||
p.hidden = nearBottom();
|
||||
}
|
||||
|
||||
// Rendered through the same fmt() as a finished answer, so a fence that is still being written
|
||||
// formats as it arrives rather than snapping from plain text to a code block at the end. fmt
|
||||
// escapes, so a half-written tag cannot break out of the bubble mid-stream.
|
||||
@@ -561,6 +588,7 @@ vv_ai_profiles_script();
|
||||
s.hidden = false;
|
||||
s.innerHTML = fmt(text);
|
||||
if (stick) scroll();
|
||||
syncJump();
|
||||
}
|
||||
|
||||
// Paths ride in data attributes rather than an onclick. They come out of the index, and a
|
||||
@@ -616,7 +644,25 @@ vv_ai_profiles_script();
|
||||
h += `<div class="vv-ai-meta">${t.tokens} tok · ${t.tok_s} tok/s · `
|
||||
+ `retrieve ${t.retrieve_ms}ms · generate ${(t.generate_ms/1000).toFixed(1)}s</div>`;
|
||||
}
|
||||
chatEl().appendChild(el(h + '</div>')); scroll();
|
||||
const node = el(h + '</div>');
|
||||
chatEl().appendChild(node);
|
||||
// Measured after it is in the DOM — scrollHeight is 0 on a detached node, so a fold decided
|
||||
// before appending would either never fire or fire on everything.
|
||||
foldIfLong(node);
|
||||
scroll();
|
||||
}
|
||||
|
||||
// A long answer buries the composer on a 15" panel, and the composer is where the next thing
|
||||
// happens. Folded to a readable height with the control always visible — never hover-revealed,
|
||||
// because the surfaces this runs on have no pointer near them.
|
||||
const FOLD_PX = 420;
|
||||
function foldIfLong(node) {
|
||||
const body = node.querySelector('.vv-ai-body');
|
||||
if (!body || body.scrollHeight <= FOLD_PX) return;
|
||||
body.classList.add('vv-ai-fold');
|
||||
body.insertAdjacentHTML('afterend',
|
||||
`<button type="button" class="vv-ai-lnk vv-ai-more" data-more>Show the rest`
|
||||
+ ` (${Math.round(body.scrollHeight / 20)} lines)</button>`);
|
||||
}
|
||||
|
||||
function addError(msg) {
|
||||
@@ -663,6 +709,14 @@ vv_ai_profiles_script();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const more = e.target.closest('[data-more]');
|
||||
if (more) {
|
||||
const body = more.previousElementSibling;
|
||||
if (body) body.classList.remove('vv-ai-fold');
|
||||
more.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
const stog = e.target.closest('[data-src-toggle]');
|
||||
if (stog) {
|
||||
const box = stog.parentElement;
|
||||
@@ -1311,6 +1365,13 @@ vv_ai_profiles_script();
|
||||
$('input').addEventListener('input', saveDraft);
|
||||
restoreDraft();
|
||||
syncLast();
|
||||
|
||||
const jumpEl = $('jump');
|
||||
if (jumpEl) {
|
||||
jumpEl.addEventListener('click', () => { scroll(); syncJump(); });
|
||||
// passive: this only reads scroll position, so it must never delay the scroll itself.
|
||||
chatEl().addEventListener('scroll', syncJump, { passive: true });
|
||||
}
|
||||
const newBtn = $('new'); if (newBtn) newBtn.addEventListener('click', newChat);
|
||||
|
||||
// ── Expand / collapse ────────────────────────────────────────────────
|
||||
@@ -1786,9 +1847,14 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Positioned wrapper so the jump pill can sit over the transcript's bottom edge without
|
||||
being clipped by its overflow, and without taking a row of its own when hidden. -->
|
||||
<div class="vv-ai-chat-wrap">
|
||||
<div class="vv-ai-chat" id="<?= $p ?>-chat"<?= $style ?>>
|
||||
<div class="vv-ai-empty"><?= htmlspecialchars($empty) ?></div>
|
||||
</div>
|
||||
<button type="button" class="vv-ai-jump" id="<?= $p ?>-jump" hidden>↓ newest</button>
|
||||
</div>
|
||||
|
||||
<div class="vv-ai-composer">
|
||||
<!-- Acts on the last exchange only, and is hidden until there is one. Redoing the previous turn
|
||||
|
||||
Reference in New Issue
Block a user