diff --git a/Plugin/unraid/include/ai_chat.php b/Plugin/unraid/include/ai_chat.php index 0a24e92..bace1c2 100644 --- a/Plugin/unraid/include/ai_chat.php +++ b/Plugin/unraid/include/ai_chat.php @@ -212,25 +212,20 @@ function vv_ai_chat_assets(): void { /* 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; } +.vv-ai-head { display:flex; align-items:center; gap:6px; margin-bottom:8px; } +/* The title takes the slack, so the two controls group together at the right rather than being + spread apart — with three children, space-between would strand the size button in the middle. */ +.vv-ai-head-t { display:flex; align-items:center; gap:5px; flex:1 1 auto; min-width:0; + font-size:13px; color:#888; text-transform:uppercase; letter-spacing:0.05em; + overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } -/* Shortcuts strip. Shut it reads as one quiet line; open it is a two-column list that wraps to - one on a narrow card. Nothing here competes with the transcript below it. - - Shut, it is about half the height it was. Two things were paying for that: the label inherited - a 1.5 line-height it did not need at 10px, and the block carried a margin-bottom that duplicated - the gap the wrapper already puts between its children. The gap alone spaces it now. */ -.vv-ai-keys { line-height:1; } -.vv-ai-keys-t { display:flex; align-items:center; gap:5px; background:none; border:none; padding:0; - font-family:inherit; font-size:10px; line-height:1.1; color:#4a4a4a; - cursor:pointer; letter-spacing:0.04em; text-transform:uppercase; } -.vv-ai-keys-t:hover { color:#777; } -.vv-ai-keys-c { display:inline-block; font-size:8px; transition:transform .12s; - transform:rotate(-90deg); } -.vv-ai-keys.open .vv-ai-keys-c { transform:rotate(0deg); } +/* Shortcuts. The toggle lives in the banner with the other window controls; this is only the list + it opens. Shut, the block occupies nothing at all — no line, no margin — because the control + that speaks for it is already on screen above. Open, it is a two-column list that wraps to one + on a narrow card, and the transcript below gives up exactly its height. */ +.vv-ai-keys { line-height:1; } +.vv-ai-keysbtn { font-size:10px !important; line-height:1; padding:4px 10px !important; + letter-spacing:0.04em; flex-shrink:0; } /* line-height restored: the wrapper drops it to 1 to tighten the collapsed line, and inheriting that here would stack the open rows on top of each other. */ .vv-ai-keys-b { display:none; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); @@ -240,8 +235,8 @@ function vv_ai_chat_assets(): void { .vv-ai-key { display:flex; align-items:baseline; gap:7px; font-size:10px; min-width:0; } .vv-ai-key-k { display:flex; gap:3px; flex-shrink:0; } .vv-ai-key-d { color:#5a5a5a; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } -.vv-ai-key kbd { font-family:inherit; font-size:9px; color:#8a8a8a; background:#181818; - border:1px solid #2a2a2a; border-radius:3px; padding:1px 5px; line-height:1.5; } +.vv-ai-key kbd { font-family:inherit; font-size:8px; color:#8a8a8a; background:#181818; + border:1px solid #2a2a2a; border-radius:3px; padding:1px 4px; line-height:1.5; } /* ── 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 @@ -904,13 +899,32 @@ vv_ai_profiles_script(); let big = false, large = false; const growBtn = $('grow'), sizeBtn = $('size'); + // What the open shortcuts list costs, including the margin that separates it from the + // transcript. Zero while shut. Measured rather than assumed: it is a grid that reflows to one + // column on a narrow card, so its height is a function of the width it was given. + function keysCost() { + const w = $('keys'), b = $('keys-b'); + if (!w || !b || !w.classList.contains('open')) return 0; + const m = parseFloat(getComputedStyle(b).marginTop) || 0; + return b.offsetHeight + m; + } + function applyHeights() { 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; + const want = big ? (large ? big2 : med) : base; + // Subtracted, not added to. The height the page asked for is the height the whole window + // keeps; the list is taken out of the conversation's share of it. calc() because that height + // may be a viewport share while this cost is only ever pixels. + // + // Floored, because on the smallest placements the list is taller than the transcript it is + // being taken from — the Scheduler collapsed at a tenth of its panel is a couple of rows. + // Leaving a transcript of zero would be worse than briefly outgrowing the budget. + const k = keysCost(); + el.style.height = k ? 'max(56px, calc(' + want + ' - ' + k + 'px))' : want; if (growBtn) { growBtn.textContent = big ? '⤡' : '⤢'; @@ -961,17 +975,22 @@ vv_ai_profiles_script(); const KEYS_OPEN = 'vv-ai-keys-open'; const keysWrap = $('keys'), keysT = $('keys-t'); if (keysWrap && keysT) { + // applyHeights() after the class, never before: the cost is measured off the rendered list, + // and while it is still display:none that measurement is zero. const applyKeys = open => { keysWrap.classList.toggle('open', open); keysT.setAttribute('aria-expanded', open ? 'true' : 'false'); + keysT.classList.toggle('vv-ai-grow-on', open); + applyHeights(); }; applyKeys(localStorage.getItem(KEYS_OPEN) !== '0'); keysT.addEventListener('click', () => { const open = !keysWrap.classList.contains('open'); localStorage.setItem(KEYS_OPEN, open ? '1' : '0'); applyKeys(open); - // The panel is above the transcript, so opening it takes height the page may have to - // account for — the same reason the size controls announce themselves. + scroll(); + // The total is meant to hold, but the floor above can break that on a small placement, so + // the page is told either way rather than being left to find out by clipping. onResize(big); }); } @@ -1272,6 +1291,10 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void { + +
-