diff --git a/Plugin/unraid/include/ai_chat.php b/Plugin/unraid/include/ai_chat.php index 4fda2fd..4de7eaa 100644 --- a/Plugin/unraid/include/ai_chat.php +++ b/Plugin/unraid/include/ai_chat.php @@ -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) . '"' : ''; ?>
+ +
+ + + + + +
+
>
diff --git a/Plugin/unraid/pages/ai.php b/Plugin/unraid/pages/ai.php index 29a9c12..b378b2f 100644 --- a/Plugin/unraid/pages/ai.php +++ b/Plugin/unraid/pages/ai.php @@ -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?', diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index 8a8d04a..7bd6c2c 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -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. -->
-

- - - Assistant - -

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' => '', + // 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…', diff --git a/Plugin/unraid/pages/scheduler.php b/Plugin/unraid/pages/scheduler.php index d591ba8..b6d6d49 100644 --- a/Plugin/unraid/pages/scheduler.php +++ b/Plugin/unraid/pages/scheduler.php @@ -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.