Make the Scheduler panel an instance, not a second chat

It kept its own bar, send loop and poll, which is why a thread there died on reload while the
other two were saved and why its controls had drifted from the same controls everywhere else.
This commit is contained in:
Gmer4Lfe
2026-08-09 12:22:07 -04:00
parent d0ff0c7d5c
commit 8eeb4c3d6c
3 changed files with 203 additions and 352 deletions
+38 -19
View File
@@ -1,9 +1,9 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════════════════════════
// PURPOSE
// The conversation surface itself — profile bar, transcript, composer, source viewer and the
// stored-chat list — rendered wherever a full chat belongs. Currently the AI tab and the
// Monitor tab's AI row.
// The conversation surface itself — transcript, composer, profile and history pickers, source
// viewer and the stored-chat list — rendered wherever a chat belongs. All three surfaces use
// it: the AI tab, the Monitor tab's AI row, and the Scheduler's right-hand panel.
//
// WHY THIS IS AN INCLUDE
// There were two places that needed a real transcript and one implementation, which meant the
@@ -12,10 +12,18 @@
// teardown of a stale instance across an Unraid tab swap — is subtle because each of them
// already cost a diagnosis session once. A copy inherits none of the fixes that come after it.
//
// The Scheduler tab's dock is deliberately NOT built on this. It is a one-line bar that follows
// the view you have open, with its own scope chip, fix flow and incident capture; it is a
// different component that happens to talk to the same endpoint. Folding it in here would mean
// one widget with two personalities and a mode flag deciding which.
// The Scheduler panel used to be exactly that copy: its own bar, send loop, poll and store
// handling. It proved the point — a thread there died on reload while the other two were saved,
// and its bar drifted into a different shape from the same control everywhere else. It is an
// instance now. What was genuinely particular to it turned into options rather than a second
// implementation: `scope` as a function for a subject that follows the open view, `beforeSend`
// for the one reply that is recorded rather than asked, `think` for reasoning on diagnosis only,
// `setHeights()` for a placement whose size is a share of a panel and not knowable until layout.
//
// WHAT A PLACEMENT MAY CHOOSE
// How much room it gets, and which extra controls belong to that surface. Not the shape: the
// control row, its order, and what the buttons do are fixed. A chat that rearranges itself per
// tab is three components wearing one name, which is the state this replaced.
//
// INSTANCES
// Every id is composed from a prefix, so two chats can coexist on one page. Each instance
@@ -69,10 +77,10 @@ require_once __DIR__ . '/ai_profiles.php';
// The conversation store, on its own, with no styling and no chat widget attached.
//
// Emitted separately because the surface that most needs it is the one that does not want the
// rest: the Scheduler dock draws its own one-line bar, with its own scope chip and fix flow, and
// pulling in the full transcript stylesheet to reach a save function would restyle a component
// that was deliberately built to look different.
// Emitted separately because a page may want the store without the component — a list of saved
// conversations, or a save, with no transcript rendered. Every surface currently takes both, but
// the two are not the same dependency and pulling in a stylesheet to reach a save function is how
// they would become one.
//
// Where a conversation lives is not a presentation decision, so it gets one answer for every
// surface. Anything added to the store — a new field, a new cap, a changed prune rule — arrives
@@ -132,8 +140,8 @@ function vv_ai_chat_assets(): void {
.vv-ai-input:focus { outline:none; border-color:#2d4a6a; }
.vv-ai-toggle { font-size:11px; color:#6a6a6a; display:flex; align-items:center; gap:5px; cursor:pointer; }
/* Profile picker styling lives in css/varaverk.css — the Scheduler dock uses the same control
and loads none of this stylesheet. */
/* Profile picker styling lives in css/varaverk.css, alongside the menus it shares its shape
with, rather than here. */
.vv-ai-switch { text-align:center; font-size:10px; color:#3a3a3a; margin:10px 0;
border-top:1px dashed #1e1e1e; padding-top:8px; }
@@ -222,9 +230,9 @@ function vv_ai_chat_assets(): void {
background:#161616; border-color:#333; }
.vv-ai-c .vv-ai-input:focus { border-color:#6495ed; }
/* Styled after the Scheduler dock: 12px, tighter leading, questions marked with a chevron rather
than a role label. The role labels cost a line each and say the same two things forever — on a
card this size that is a third of the visible transcript spent on "You" and "Varaverk". */
/* 12px, tighter leading, questions marked with a chevron rather than a role label. The role
labels cost a line each and say the same two things forever — in a space this size that is a
third of the visible transcript spent on "You" and "Varaverk". */
.vv-ai-c .vv-ai-role { display:none; }
.vv-ai-c .vv-ai-body { font-size:12px; line-height:1.55; }
.vv-ai-c .vv-ai-msg.user .vv-ai-body { color:#8fb0c4; }
@@ -300,7 +308,7 @@ function vv_ai_chat_assets(): void {
<?php
// The profile registry, served rather than restated. This used to be a literal table in the
// script below that had already drifted from the PHP — it knew three profiles where the server
// knew four, which is why troubleshoot could not be offered here and the Scheduler dock had to
// knew four, which is why troubleshoot could not be offered here and the Scheduler had to
// hand-roll its own labels.
vv_ai_profiles_script();
?>
@@ -493,6 +501,12 @@ vv_ai_profiles_script();
const q = $('input').value.trim();
if (!q) return;
// A page may claim what was typed instead of asking it. The Scheduler does this once, after
// an offer is accepted, when the next thing typed is the fix being recorded rather than a
// question. Checked here rather than in a wrapper the page calls, because Ask and Ctrl+Enter
// both land here directly and a wrapper would only catch the ones routed through it.
if (o.beforeSend && o.beforeSend(q)) { $('input').value = ''; return; }
busy = true;
$('send').disabled = true;
addUser(q);
@@ -516,7 +530,12 @@ vv_ai_profiles_script();
question: q,
history: JSON.stringify(sendable()),
kind: (PROFILES[profile].kind && kindEl) ? kindEl.value : '',
think: (thinkEl ? thinkEl.checked : true) ? '1' : '0',
// A checkbox where the page offers one, otherwise whatever the page decides from the
// profile, otherwise on. The Scheduler reasons only when diagnosing: working out what a
// log means is worth waiting ~15s for, and a lookup like "what does this setting do" is
// not — an inline answer that stalls reads as broken.
think: (typeof o.think === 'function' ? o.think(profile)
: thinkEl ? thinkEl.checked : true) ? '1' : '0',
});
res = fetch(API, { method: 'POST', headers: POST_HEAD, body });
} catch (e) {
@@ -966,7 +985,7 @@ vv_ai_profiles_script();
box.innerHTML = '<div class="vv-ai-none">no saved conversations yet</div>';
return;
}
// The scope is shown, not just stored. A Scheduler dock thread is about one script or log,
// The scope is shown, not just stored. A Scheduler thread is about one script or log,
// and a list of titles alone makes "why does this one talk about a log I never opened"
// an unanswerable question.
// Tagged only when the tag says something. A scope always does; a profile does unless it