Separate whether the chat is expanded from how big expanded is
One is an action taken constantly and the other a preference set once, so the banner names the size and the composer keeps the toggle.
This commit is contained in:
@@ -205,7 +205,14 @@ function vv_ai_chat_assets(): void {
|
||||
.vv-ai-ctrls select { background:#0a0a0a; border:1px solid #222; color:#8a8a8a; font-size:11px;
|
||||
padding:4px 7px; border-radius:3px; }
|
||||
/* Square-ish, so the two icon buttons read as a pair distinct from the worded ones. */
|
||||
.vv-ai-grow, .vv-ai-histbtn { min-width:30px; text-align:center; }
|
||||
.vv-ai-histbtn { min-width:30px; text-align:center; }
|
||||
|
||||
/* Banner. The title takes the same shape a .vv-card h3 does, so an instance dropped into a card
|
||||
reads as that card's heading rather than as a second one underneath it. */
|
||||
.vv-ai-head { display:flex; align-items:center; justify-content:space-between; gap:8px;
|
||||
margin-bottom:8px; }
|
||||
.vv-ai-head-t { display:flex; align-items:center; gap:5px; font-size:13px; color:#888;
|
||||
text-transform:uppercase; letter-spacing:0.05em; min-width:0; }
|
||||
|
||||
/* ── Compact form, for a chat living inside a Monitor card ──────────────── */
|
||||
/* Not a different component — the same markup with the chrome pulled in. The card supplies the
|
||||
@@ -266,7 +273,8 @@ function vv_ai_chat_assets(): void {
|
||||
.vv-ai-crow.active .vv-ai-crow-t { color:#9bd; }
|
||||
.vv-ai-crow-m { font-size:9px; color:#3a3a3a; font-family:monospace; flex-shrink:0; }
|
||||
.vv-ai-chat { transition:height .14s ease; }
|
||||
.vv-ai-grow { font-size:13px !important; line-height:1; padding:4px 9px !important; }
|
||||
.vv-ai-size { font-size:10px !important; line-height:1; padding:4px 10px !important;
|
||||
letter-spacing:0.04em; flex-shrink:0; }
|
||||
.vv-ai-grow-on { color:#9bd !important; border-color:#2d4a6a !important; }
|
||||
.vv-ai-crow-s { font-size:9px; color:#5c7cfa; font-family:monospace; }
|
||||
.vv-ai-c .vv-ai-crow-s { color:#7a9ae0; }
|
||||
@@ -860,19 +868,39 @@ vv_ai_profiles_script();
|
||||
// whose sizes are not knowable when the markup is written can rewrite them later — the
|
||||
// Scheduler's panel takes its heights as a share of whatever room the panel has, which is a
|
||||
// number that only exists after layout and changes on every resize. See setHeights().
|
||||
let big = false;
|
||||
const growBtn = $('grow');
|
||||
// Two independent pieces of state, not one three-way. `big` is whether the conversation is
|
||||
// expanded — an action, taken often. `large` is how much expanded is worth — a setting, chosen
|
||||
// once. Collapsing and then expanding again returns to the size you picked rather than
|
||||
// resetting it, which is the whole reason they are separate.
|
||||
let big = false, large = false;
|
||||
const growBtn = $('grow'), sizeBtn = $('size');
|
||||
|
||||
function applyHeights() {
|
||||
const el = chatEl();
|
||||
const base = el.dataset.h || '';
|
||||
const tall = el.dataset.hTall || '';
|
||||
if (!base || !tall) return;
|
||||
el.style.height = big ? tall : base;
|
||||
const el = chatEl();
|
||||
const base = el.dataset.h || '';
|
||||
const med = el.dataset.hTall || '';
|
||||
const big2 = el.dataset.hLarge || med;
|
||||
if (!base || !med) return;
|
||||
el.style.height = big ? (large ? big2 : med) : base;
|
||||
|
||||
if (growBtn) {
|
||||
growBtn.textContent = big ? '⤡' : '⤢';
|
||||
growBtn.title = big ? 'Back to the smaller view' : 'Give the conversation more room';
|
||||
growBtn.classList.toggle('vv-ai-grow-on', big);
|
||||
}
|
||||
if (sizeBtn) {
|
||||
// Nothing to choose between while collapsed — it would name a size that is not on screen
|
||||
// and does not become so until the button beside it is pressed.
|
||||
sizeBtn.style.display = big ? '' : 'none';
|
||||
// Names the size in force, not the one it would switch to. A button labelled with its own
|
||||
// effect reads as a statement of current state to about half of everyone who sees it, and
|
||||
// the two readings disagree about what clicking does — so it states, and the title says
|
||||
// what happens.
|
||||
sizeBtn.textContent = large ? 'Large' : 'Medium';
|
||||
sizeBtn.title = large ? 'Expanded is large — click for medium'
|
||||
: 'Expanded is medium — click for large';
|
||||
sizeBtn.classList.toggle('vv-ai-grow-on', large);
|
||||
}
|
||||
}
|
||||
if (growBtn) {
|
||||
// onResize fires only here, on the operator's own toggle — never from setHeights(). A page
|
||||
@@ -890,6 +918,16 @@ vv_ai_profiles_script();
|
||||
onResize(big);
|
||||
});
|
||||
}
|
||||
if (sizeBtn) {
|
||||
// Only meaningful while expanded, and only reachable then, so this always changes the
|
||||
// height on screen — and therefore always has to be announced like the toggle does.
|
||||
sizeBtn.addEventListener('click', () => {
|
||||
large = !large;
|
||||
applyHeights();
|
||||
scroll();
|
||||
onResize(big);
|
||||
});
|
||||
}
|
||||
|
||||
// Surface any script error into the transcript. Without it a throw anywhere on the page is
|
||||
// invisible unless the console happens to be open, which is how a silent hang survives a
|
||||
@@ -939,9 +977,10 @@ vv_ai_profiles_script();
|
||||
// Heights supplied after the fact, for a placement whose room is a share of a panel rather
|
||||
// than a constant. Re-applies immediately at whichever of the two states is current, so a
|
||||
// resize while expanded stays expanded instead of snapping back.
|
||||
setHeights(base, tall) {
|
||||
setHeights(base, tallMed, tallLarge) {
|
||||
const el = chatEl();
|
||||
el.dataset.h = base; el.dataset.hTall = tall;
|
||||
el.dataset.h = base; el.dataset.hTall = tallMed;
|
||||
if (tallLarge) el.dataset.hLarge = tallLarge;
|
||||
applyHeights();
|
||||
},
|
||||
|
||||
@@ -1065,12 +1104,16 @@ vv_ai_profiles_script();
|
||||
//
|
||||
// profile which profile starts active
|
||||
// compact card-sized chrome, for a chat living inside a card
|
||||
// height resting transcript height. Defaults per mode rather than being left empty: the
|
||||
// expand control is standard, and a control with no second height to move to is a
|
||||
// button that does nothing on the one surface that forgot to pass a number.
|
||||
// tall the expanded height. Defaults to 2.5x height, which is the point where a long
|
||||
// answer stops needing a scroll for most questions without the card swallowing the
|
||||
// page it sits on
|
||||
// height resting transcript height, the collapsed state. Defaults per mode rather than being
|
||||
// left empty: the expand control is standard, and a control with no second height to
|
||||
// move to is a button that does nothing on the surface that forgot to pass a number.
|
||||
// tall expanded at the Medium setting. Defaults to 2x height.
|
||||
// tallLarge expanded at the Large setting. Defaults to 3x height.
|
||||
// Two expanded sizes because ⤢ and the banner's Medium/Large answer different
|
||||
// questions: whether the conversation is expanded, and how much that is worth. The
|
||||
// first is an action taken constantly, the second a preference set once.
|
||||
// title banner text. Defaults to "Assistant"
|
||||
// icon raw SVG for the banner, page-supplied and emitted unescaped
|
||||
// empty empty-state text
|
||||
// scope what this conversation is about — string, or a function re-read at send time for a
|
||||
// surface whose subject follows the view the operator has open
|
||||
@@ -1082,24 +1125,48 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void {
|
||||
// none: see `height` above.
|
||||
$height = $o['height'] ?? ($compact ? '300px' : '46vh');
|
||||
$empty = $o['empty'] ?? 'Ask Varaverk about itself.';
|
||||
// 2.5x by default, computed here so the page can override with a value that suits its layout
|
||||
// rather than the include guessing at one it cannot see.
|
||||
$tall = $o['tall'] ?? '';
|
||||
if ($height !== '' && $tall === '' && preg_match('/^(\d+(?:\.\d+)?)(px|vh|em|rem)$/', $height, $hm)) {
|
||||
$n = (float)$hm[1] * 2.5;
|
||||
// vh is a share of the viewport, so 2.5x runs off the bottom of the screen — an expand
|
||||
// that puts the composer out of reach is worse than no expand. Clamped to something that
|
||||
// still leaves the page scrollable to its own controls.
|
||||
if ($hm[2] === 'vh') $n = min($n, 85);
|
||||
$tall = rtrim(rtrim(number_format($n, 2, '.', ''), '0'), '.') . $hm[2];
|
||||
}
|
||||
$style = $height !== '' ? ' style="height:' . htmlspecialchars($height, ENT_QUOTES)
|
||||
. ';max-height:none;"'
|
||||
. ' data-h="' . htmlspecialchars($height, ENT_QUOTES) . '"'
|
||||
. ' data-h-tall="' . htmlspecialchars($tall, ENT_QUOTES) . '"' : '';
|
||||
|
||||
// Derived from the resting height, so a page need only state the one number it actually cares
|
||||
// about. Large is 3x, capped at 85vh — vh is a share of the viewport and an expand that puts
|
||||
// the composer out of reach is worse than no expand.
|
||||
//
|
||||
// Medium is placed between resting and large rather than at a multiple of its own, because a
|
||||
// fixed multiple collapses under that cap: at 52vh resting, 2x and 3x are both 85vh after
|
||||
// clamping, and the Medium/Large button would have had two settings that did the same thing.
|
||||
$sizes = function (string $from): array {
|
||||
if (!preg_match('/^(\d+(?:\.\d+)?)(px|vh|em|rem)$/', $from, $m)) return ['', ''];
|
||||
$fmt = fn(float $n) => rtrim(rtrim(number_format($n, 2, '.', ''), '0'), '.') . $m[2];
|
||||
$base = (float)$m[1];
|
||||
$lg = $base * 3.0;
|
||||
if ($m[2] === 'vh') $lg = min($lg, 85.0);
|
||||
return [$fmt($base + ($lg - $base) * 0.55), $fmt($lg)];
|
||||
};
|
||||
[$defMed, $defLarge] = $sizes($height);
|
||||
$tall = $o['tall'] ?? $defMed;
|
||||
$large = $o['tallLarge'] ?? $defLarge;
|
||||
|
||||
$style = $height !== '' ? ' style="height:' . htmlspecialchars($height, ENT_QUOTES)
|
||||
. ';max-height:none;"'
|
||||
. ' data-h="' . htmlspecialchars($height, ENT_QUOTES) . '"'
|
||||
. ' data-h-tall="' . htmlspecialchars($tall, ENT_QUOTES) . '"'
|
||||
. ' data-h-large="' . htmlspecialchars($large, ENT_QUOTES) . '"' : '';
|
||||
?>
|
||||
<div class="vv-ai-chatwrap<?= $compact ? ' vv-ai-c' : '' ?>" style="display:flex;flex-direction:column;gap:<?= $compact ? '4px' : '12px' ?>;min-width:0;">
|
||||
|
||||
<!-- Banner. Names the thing, and carries the setting rather than the action: how big expanded
|
||||
is, not whether it is expanded. ⤢ below does the expanding, which is a thing you do to the
|
||||
conversation and belongs with the other things you do to it. This picks what that action
|
||||
will be worth, which you set once and rarely revisit.
|
||||
Hidden while collapsed, because at that point it describes nothing on screen. -->
|
||||
<div class="vv-ai-head">
|
||||
<span class="vv-ai-head-t">
|
||||
<?php if (!empty($o['icon'])): ?><span class="vv-ico"><?= $o['icon'] ?></span><?php endif; ?>
|
||||
<?= htmlspecialchars($o['title'] ?? 'Assistant') ?>
|
||||
</span>
|
||||
<button class="vv-ai-btn ghost vv-ai-size" id="<?= $p ?>-size" type="button"
|
||||
style="display:none">Medium</button>
|
||||
</div>
|
||||
|
||||
<div class="vv-ai-chat" id="<?= $p ?>-chat"<?= $style ?>>
|
||||
<div class="vv-ai-empty"><?= htmlspecialchars($empty) ?></div>
|
||||
</div>
|
||||
|
||||
@@ -232,8 +232,12 @@ if (is_dir('/var/log/varaverk')) {
|
||||
// most room, so it rests taller than the Monitor card and expands to most of the viewport.
|
||||
vv_ai_chat_markup('vv-ai', [
|
||||
'profile' => 'varaverk',
|
||||
// All three stated rather than derived. Large is at the viewport cap here, and leaving medium
|
||||
// to be worked out from the resting height would land it on that same cap — two settings
|
||||
// doing the same thing.
|
||||
'height' => '52vh',
|
||||
'tall' => '85vh',
|
||||
'tall' => '68vh',
|
||||
'tallLarge' => '85vh',
|
||||
'empty' => "Ask Varaverk about itself. Answers come only from this installation's own "
|
||||
. 'documentation, with sources.',
|
||||
'placeholder' => 'e.g. what stops rsync and the mover running at once?',
|
||||
|
||||
@@ -360,13 +360,12 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
|
||||
wrapper that still carries the #vv-ai-dock id, and two elements answering to that name
|
||||
on one page would be a trap the day anything queries it globally. -->
|
||||
<div class="vv-card" id="vv-ai-assistant-card">
|
||||
<h3>
|
||||
<span style="display:flex;align-items:center;gap:5px;">
|
||||
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><path d="M4.4 4.6C4.4 3.7 5.1 3.1 6 3.1C6.9 3.1 7.6 3.7 7.6 4.5C7.6 5.9 6 5.7 6 7"/><circle cx="6" cy="8.8" r="0.6" fill="#888" stroke="none"/></svg></span>
|
||||
Assistant
|
||||
</span>
|
||||
</h3>
|
||||
<?php
|
||||
// No <h3> of its own, unlike every other card here. The component draws its own banner now
|
||||
// — it carries the size control — and a card heading above that banner would be the same
|
||||
// words twice. The banner is styled as a card heading so the row still reads level with the
|
||||
// cards beside it.
|
||||
//
|
||||
// Starts on General Chat, unlike the AI tab. This is the box you type an idle question
|
||||
// into while watching the dashboard, and the worker escalates anything about this install
|
||||
// to the assistant on its own — so landing on the strict profile here would refuse
|
||||
@@ -374,7 +373,15 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
|
||||
vv_ai_chat_markup('vv-mon-ai', [
|
||||
'profile' => 'chat',
|
||||
'compact' => true,
|
||||
'title' => 'Assistant',
|
||||
'icon' => '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><path d="M4.4 4.6C4.4 3.7 5.1 3.1 6 3.1C6.9 3.1 7.6 3.7 7.6 4.5C7.6 5.9 6 5.7 6 7"/><circle cx="6" cy="8.8" r="0.6" fill="#888" stroke="none"/></svg>',
|
||||
// Deliberately unchanged. The control is wired in so the card behaves like every other
|
||||
// instance, but the numbers are the ones this card has always used: 300px collapsed and
|
||||
// 750px expanded, which is now Large. Medium is provisional and sits between them until
|
||||
// this card gets the same re-cut the Scheduler's shares just had.
|
||||
'height' => '300px',
|
||||
'tall' => '500px',
|
||||
'tallLarge' => '750px',
|
||||
'empty' => 'Ask anything. Questions about this installation are handed to the '
|
||||
. 'Varaverk assistant automatically.',
|
||||
'placeholder' => 'Ask the assistant…',
|
||||
|
||||
@@ -1224,10 +1224,11 @@ function vvFitRight() {
|
||||
// its floor, and nothing ever raised it again.
|
||||
const availH = Math.max(80, lf.getBoundingClientRect().top - 32
|
||||
- toolbar.getBoundingClientRect().bottom);
|
||||
// A fifth of the panel at rest, two fifths expanded. Both were double this, which made the
|
||||
// assistant the largest thing in the panel before it had been asked anything — and the view
|
||||
// above is what the question is usually about. Expanded still leaves the majority to the log,
|
||||
// conf or script being discussed, so a follow-up does not need the panel handed back first.
|
||||
// Three shares of the panel: 13% collapsed, 20% expanded at Medium, 40% at Large. Collapsed is
|
||||
// deliberately small — enough to see the last exchange and type, while the log, conf or script
|
||||
// being discussed keeps the panel, which is what you are usually asking about. Medium is for
|
||||
// following an answer without giving up the view; Large is for sitting and reading one, and
|
||||
// still leaves the majority above it so a follow-up does not need the panel handed back first.
|
||||
//
|
||||
// It grows upward: the panel is pinned at the bottom and everything above it is sized from what
|
||||
// is left, a few lines down. So the top edge rises and the thing being read stays where it is.
|
||||
@@ -1250,7 +1251,8 @@ function vvFitRight() {
|
||||
// discussed on screen too; a chat that covers it entirely is not worth the room it took.
|
||||
const VIEW_MIN = 90;
|
||||
const room = Math.max(60, availH - VIEW_MIN - chrome);
|
||||
vvSchedChat.setHeights(Math.min(room, Math.max(70, Math.round(availH * 0.20))) + 'px',
|
||||
vvSchedChat.setHeights(Math.min(room, Math.max(60, Math.round(availH * 0.13))) + 'px',
|
||||
Math.min(room, Math.max(100, Math.round(availH * 0.20))) + 'px',
|
||||
Math.min(room, Math.max(140, Math.round(availH * 0.40))) + 'px');
|
||||
}
|
||||
// Measured after the heights are applied, so this reads the clamped height, not the natural one.
|
||||
|
||||
Reference in New Issue
Block a user