diff --git a/AI/README-AI.md b/AI/README-AI.md index 465e4e0..2b2c6cc 100644 --- a/AI/README-AI.md +++ b/AI/README-AI.md @@ -69,7 +69,7 @@ buries it. `--kind=readme` is the hard filter for that case: bash AI/ai_query.sh --kind=readme "what is Varaverk" ``` -`--kind` filters on where a chunk came from — `header`, `readme`, `manual`, `template`, `doc` — +`--kind` filters on where a chunk came from — `header`, `readme`, `manual`, `template`, `doc`, `ui` — and composes with `--section`. Prefer it over `--section` for "what is" and "why does this exist" questions, where the answer is narrative rather than a header field. @@ -109,6 +109,7 @@ Roughly 2,900 chunks across ~180 files: | `manual` | Every `Manual-*.md` | | `template` | `Deployment/*.template` — the versioned conf schema | | `doc` | Top-level `README.md`, `Manual.md`, design notes | +| `ui` | `Plugin/unraid/pages/readme/*.md` — the WebGUI's own help panels | **Script bodies are not indexed.** Headers state intent, code states mechanism; for the questions this answers, intent retrieves better and costs far less. @@ -239,7 +240,7 @@ dock. | `varaverk` | 3 | yes | answers only from the index, with citations. The default. | | `chat` | 8 | **no** | ordinary conversation. Holds zero capabilities, deliberately. | | `code` | 4 | no | drafts shell for Custom Scripts; scans its own output for destructive ops | -| `troubleshoot` | 2 | yes | reasons from an open log first, docs second. May file bug reports. | +| `troubleshoot` | 2 | yes | reasons from evidence — an open log, or one you name. May file bug reports. | Capabilities are granted per profile — retrieval, live health, run evidence, scoped log, incidents, conf lookup, bug filing, code scanning. `chat` holding an empty list is a guarantee, diff --git a/Plugin/unraid/Tools/ai_chat_worker.php b/Plugin/unraid/Tools/ai_chat_worker.php index f5a0dcb..ba24b1b 100644 --- a/Plugin/unraid/Tools/ai_chat_worker.php +++ b/Plugin/unraid/Tools/ai_chat_worker.php @@ -444,9 +444,27 @@ if ($profile === 'chat') { // profile of its own. The assistant's contract is "answer only from the passages, refuse if // absent" — exactly wrong here, where the evidence is a log that is not in the index and // never should be. This one reasons from the log first and the documentation second. + // What it says it has is what it was actually given. This line used to assert a log tail + // unconditionally, which was safe only while the profile could be reached exclusively by + // opening one. It is now a button as well, so it is reachable with nothing attached — and + // telling a model it holds evidence it does not hold is precisely how you get a confident + // reading of a log that was never there, which is the one failure this profile exists to + // avoid. Stated from $attached, so the prompt cannot drift from the inputs. + $haveLog = isset($attached['log_tail']); $system = "You are helping the operator work out why something on their Varaverk server did " - . "not do what they expected. You have the tail of the relevant log, live system " - . "state measured just now, and documentation passages about the scripts involved.\n\n" + . "not do what they expected. " + . ($haveLog + ? "You have the tail of the relevant log, live system state measured just now, " + . "and documentation passages about the scripts involved.\n\n" + : "You have NOT been given a log for this question — none was open and none was " + . "named that resolves to one. You have live system state and documentation " + . "passages only.\n\n" + . "Say that plainly before anything else, and name what would fix it: open the " + . "log on the Scheduler tab, or name the script in the question. Do not describe " + . "what a log 'would' show, do not infer an outcome from the script's name, and " + . "never write that you are reading or checking a log — you have none to read. " + . "An invented reading is worse here than anywhere else, because the operator " + . "came to this profile specifically for evidence.\n\n") . "HOW TO ANSWER\n" . "Lead with what the log actually shows. Quote the line that matters. Then say what " . "it means, using the documentation to explain what the script was trying to do.\n\n" diff --git a/Plugin/unraid/include/ai_chat.php b/Plugin/unraid/include/ai_chat.php index 7d5f44d..23bfe81 100644 --- a/Plugin/unraid/include/ai_chat.php +++ b/Plugin/unraid/include/ai_chat.php @@ -238,6 +238,9 @@ function vv_ai_chat_assets(): void { white-space:nowrap; flex:1; min-width:0; } .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-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; } .vv-ai-crow-x { font-size:11px; color:#333; flex-shrink:0; padding:0 2px; visibility:hidden; } @@ -652,6 +655,36 @@ vv_ai_profiles_script(); }); const newBtn = $('new'); if (newBtn) newBtn.addEventListener('click', newChat); + // ── Expand / collapse ──────────────────────────────────────────────── + // Both heights come from the markup, so the page decides them and this only switches + // between them. Remembered per instance: the dashboard chat and the tab's are different + // sizes for different reasons, and a shared key would make one of them wrong. + // + // Scroll position is pinned to the bottom afterwards. Growing the box leaves the transcript + // scrolled where it was, which puts the newest answer off-screen at the exact moment you + // asked for more room to read it. + const growBtn = $('grow'); + if (growBtn) { + const el = chatEl(); + const base = el.dataset.h || ''; + const tall = el.dataset.hTall || ''; + const KEY = 'vv-ai-tall-' + P; + const apply = big => { + if (!base || !tall) return; + el.style.height = big ? tall : base; + growBtn.textContent = big ? '⤡' : '⤢'; + growBtn.title = big ? 'Back to the smaller view' : 'Give the conversation more room'; + growBtn.classList.toggle('vv-ai-grow-on', big); + scroll(); + }; + growBtn.addEventListener('click', () => { + const big = el.style.height !== tall; + localStorage.setItem(KEY, big ? '1' : '0'); + apply(big); + }); + if (localStorage.getItem(KEY) === '1') apply(true); + } + // 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 // diagnosis session. @@ -759,7 +792,12 @@ vv_ai_profiles_script(); // profile which profile button starts active // compact card-sized chrome, for a chat living inside a Monitor card // height transcript height; a fixed value in compact mode, since a card in a grid row -// cannot grow with its content without dragging the row's other cards with it +// cannot grow with its content without dragging the row's other cards with it. +// Setting it also adds the expand control — a fixed height is exactly the situation +// where you sometimes want more room, and there is nothing to expand without one. +// 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 // empty empty-state text // controls extra markup dropped into the composer's control strip function vv_ai_chat_markup(string $prefix, array $o = []): void { @@ -767,8 +805,21 @@ function vv_ai_chat_markup(string $prefix, array $o = []): void { $compact = !empty($o['compact']); $height = $o['height'] ?? ''; $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;"' : ''; + . ';max-height:none;"' + . ' data-h="' . htmlspecialchars($height, ENT_QUOTES) . '"' + . ' data-h-tall="' . htmlspecialchars($tall, ENT_QUOTES) . '"' : ''; ?>