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:
@@ -667,37 +667,19 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
|
|||||||
.vv-sug-configured { color: #4caf50; font-size: 11px; }
|
.vv-sug-configured { color: #4caf50; font-size: 11px; }
|
||||||
/* Info sections inside Scheduler Information panel */
|
/* Info sections inside Scheduler Information panel */
|
||||||
/* ── Assistant dock (scheduler right panel) ────────────────────────────────────────────────── */
|
/* ── Assistant dock (scheduler right panel) ────────────────────────────────────────────────── */
|
||||||
#vv-ai-dock { border: 1px solid #262626; border-radius: 6px; background: #0e0e0e;
|
/* The frame only. Everything the dock used to style for itself — its bar, input, transcript,
|
||||||
margin-top: 6px; flex-shrink: 0; }
|
question and answer rows, sources, separators, its own grow button — is the shared chat
|
||||||
#vv-ai-dock-bar { display: flex; align-items: center; gap: 7px; padding: 6px 8px; }
|
component's now, drawn from the same rules as the AI tab and the Monitor card. What is left is
|
||||||
#vv-ai-dock-chip { white-space: nowrap; max-width: 240px; overflow: hidden;
|
the box the panel puts it in. */
|
||||||
text-overflow: ellipsis; }
|
#vv-ai-dock { border: 1px solid #262626; border-radius: 6px; background: #0e0e0e;
|
||||||
/* The button around it is .vv-ai-chip and carries the colour, border and padding. */
|
margin-top: 6px; flex-shrink: 0; padding: 6px 8px; }
|
||||||
|
/* The chip states profile and subject together here, and a long script name would otherwise push
|
||||||
|
the action group off the end of a narrow panel. */
|
||||||
|
#vv-ai-dock .vv-ai-chip { max-width: 240px; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
/* Flashed when the subject moves under text already typed — see vvAiDockScope(). */
|
||||||
.vv-ai-chip-flash { animation: vvAiChipFlash .9s ease-out 2; }
|
.vv-ai-chip-flash { animation: vvAiChipFlash .9s ease-out 2; }
|
||||||
@keyframes vvAiChipFlash { 0%,100% { background:#12191d; color:#5a7a8a; }
|
@keyframes vvAiChipFlash { 0%,100% { background:#12191d; color:#5a7a8a; }
|
||||||
50% { background:#2a2312; color:#d8b25a; } }
|
50% { background:#2a2312; color:#d8b25a; } }
|
||||||
#vv-ai-dock-input { flex: 1; min-width: 0; background: #0a0a0a; border: 1px solid #262626;
|
|
||||||
border-radius: 4px; color: #c8c8c8; font-family: inherit; font-size: 12px;
|
|
||||||
padding: 5px 9px; }
|
|
||||||
#vv-ai-dock-input:focus { outline: none; border-color: #2d4a6a; }
|
|
||||||
#vv-ai-dock-body { border-bottom: 1px solid #1c1c1c; padding: 9px 10px; overflow-y: auto;
|
|
||||||
font-size: 12px; line-height: 1.55; }
|
|
||||||
/* Always present, unlike the collapse button next to it — the size of the conversation is worth
|
|
||||||
changing before there is one, when you already know the answer will be long. */
|
|
||||||
.vv-ai-dock-grow { flex-shrink: 0; min-width: 26px; }
|
|
||||||
.vv-ai-dock-grow-on { color: #8fb0c4; border-color: #2d4a6a; background: #12191d; }
|
|
||||||
.vv-ai-dock-q { color: #8fb0c4; margin: 0 0 5px; }
|
|
||||||
.vv-ai-dock-q::before { content: '› '; color: #3a5a6a; }
|
|
||||||
.vv-ai-dock-a { color: #b8b8b8; margin: 0 0 11px; white-space: normal; }
|
|
||||||
.vv-ai-dock-a code { background: #1a1a1a; border: 1px solid #333; border-radius: 2px;
|
|
||||||
padding: 0 4px; font-size: 11px; color: #9ab; }
|
|
||||||
.vv-ai-dock-a strong { color: #ddd; }
|
|
||||||
.vv-ai-dock-wait { color: #5a5a5a; font-style: italic; }
|
|
||||||
.vv-ai-dock-err { color: #e57; }
|
|
||||||
.vv-ai-dock-src { margin-top: 6px; padding-top: 5px; border-top: 1px solid #1c1c1c;
|
|
||||||
font-size: 10px; color: #4a4a4a; font-family: monospace; }
|
|
||||||
.vv-ai-dock-sep { margin: 4px 0 9px; font-size: 10px; color: #4a4a4a; text-align: center;
|
|
||||||
border-top: 1px solid #1c1c1c; padding-top: 6px; }
|
|
||||||
|
|
||||||
.vv-info-block .vv-sug-title { color: #9ab; }
|
.vv-info-block .vv-sug-title { color: #9ab; }
|
||||||
/* Depth switch on the help header. Outlined rather than filled — it changes what you are
|
/* Depth switch on the help header. Outlined rather than filled — it changes what you are
|
||||||
@@ -1208,15 +1190,11 @@ code.vv-unknown-var { color: #ff9800; background: #1f130d; }
|
|||||||
.vv-rsync-save-btn { background: #1a2e1a; border-color: #2a4a2a; color: #6aaa6a; }
|
.vv-rsync-save-btn { background: #1a2e1a; border-color: #2a4a2a; color: #6aaa6a; }
|
||||||
.vv-rsync-save-btn:hover { background: #22382a; }
|
.vv-rsync-save-btn:hover { background: #22382a; }
|
||||||
|
|
||||||
/* ── AI profile picker ────────────────────────────────────────────────────────
|
/* ── Profile picker ───────────────────────────────────────────────────────────
|
||||||
Global rather than emitted with the chat widget, because both surfaces that show it are
|
Global rather than emitted with the chat component. It was shared out here when the Scheduler
|
||||||
styled differently in every other respect: the Monitor card renders a full transcript from
|
drew its own bar and loaded none of the component's stylesheet; that is no longer true — all
|
||||||
include/ai_chat.php, the Scheduler dock renders its own one-line bar and loads none of that
|
three surfaces render the same markup now — but the picker is also used by the menus around it
|
||||||
stylesheet. The picker is the one control they share, so it lives where .vv-ai-dock-* already
|
and reads more naturally beside them than inside the component's own block. */
|
||||||
does instead of being emitted twice. */
|
|
||||||
/* ── Profile picker ─────────────────────────────────────────────────────── */
|
|
||||||
/* The chip borrows the Scheduler dock's language deliberately — same muted blue, same weight,
|
|
||||||
same 10px — so the two surfaces read as one control that happens to appear in two places. */
|
|
||||||
.vv-ai-picker { position:relative; flex-shrink:0; }
|
.vv-ai-picker { position:relative; flex-shrink:0; }
|
||||||
.vv-ai-chip { display:inline-flex; align-items:center; gap:5px; font-size:10px; color:#5a7a8a;
|
.vv-ai-chip { display:inline-flex; align-items:center; gap:5px; font-size:10px; color:#5a7a8a;
|
||||||
background:#12191d; border:1px solid #24343d; border-radius:3px; padding:3px 8px;
|
background:#12191d; border:1px solid #24343d; border-radius:3px; padding:3px 8px;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
// PURPOSE
|
// PURPOSE
|
||||||
// The conversation surface itself — profile bar, transcript, composer, source viewer and the
|
// The conversation surface itself — transcript, composer, profile and history pickers, source
|
||||||
// stored-chat list — rendered wherever a full chat belongs. Currently the AI tab and the
|
// viewer and the stored-chat list — rendered wherever a chat belongs. All three surfaces use
|
||||||
// Monitor tab's AI row.
|
// it: the AI tab, the Monitor tab's AI row, and the Scheduler's right-hand panel.
|
||||||
//
|
//
|
||||||
// WHY THIS IS AN INCLUDE
|
// WHY THIS IS AN INCLUDE
|
||||||
// There were two places that needed a real transcript and one implementation, which meant the
|
// 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
|
// 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.
|
// 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 Scheduler panel used to be exactly that copy: its own bar, send loop, poll and store
|
||||||
// the view you have open, with its own scope chip, fix flow and incident capture; it is a
|
// handling. It proved the point — a thread there died on reload while the other two were saved,
|
||||||
// different component that happens to talk to the same endpoint. Folding it in here would mean
|
// and its bar drifted into a different shape from the same control everywhere else. It is an
|
||||||
// one widget with two personalities and a mode flag deciding which.
|
// 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
|
// INSTANCES
|
||||||
// Every id is composed from a prefix, so two chats can coexist on one page. Each instance
|
// 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.
|
// 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
|
// Emitted separately because a page may want the store without the component — a list of saved
|
||||||
// rest: the Scheduler dock draws its own one-line bar, with its own scope chip and fix flow, and
|
// conversations, or a save, with no transcript rendered. Every surface currently takes both, but
|
||||||
// pulling in the full transcript stylesheet to reach a save function would restyle a component
|
// the two are not the same dependency and pulling in a stylesheet to reach a save function is how
|
||||||
// that was deliberately built to look different.
|
// they would become one.
|
||||||
//
|
//
|
||||||
// Where a conversation lives is not a presentation decision, so it gets one answer for every
|
// 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
|
// 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-input:focus { outline:none; border-color:#2d4a6a; }
|
||||||
.vv-ai-toggle { font-size:11px; color:#6a6a6a; display:flex; align-items:center; gap:5px; cursor:pointer; }
|
.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
|
/* Profile picker styling lives in css/varaverk.css, alongside the menus it shares its shape
|
||||||
and loads none of this stylesheet. */
|
with, rather than here. */
|
||||||
.vv-ai-switch { text-align:center; font-size:10px; color:#3a3a3a; margin:10px 0;
|
.vv-ai-switch { text-align:center; font-size:10px; color:#3a3a3a; margin:10px 0;
|
||||||
border-top:1px dashed #1e1e1e; padding-top:8px; }
|
border-top:1px dashed #1e1e1e; padding-top:8px; }
|
||||||
|
|
||||||
@@ -222,9 +230,9 @@ function vv_ai_chat_assets(): void {
|
|||||||
background:#161616; border-color:#333; }
|
background:#161616; border-color:#333; }
|
||||||
.vv-ai-c .vv-ai-input:focus { border-color:#6495ed; }
|
.vv-ai-c .vv-ai-input:focus { border-color:#6495ed; }
|
||||||
|
|
||||||
/* Styled after the Scheduler dock: 12px, tighter leading, questions marked with a chevron rather
|
/* 12px, tighter leading, questions marked with a chevron rather than a role label. The role
|
||||||
than a role label. The role labels cost a line each and say the same two things forever — on a
|
labels cost a line each and say the same two things forever — in a space this size that is a
|
||||||
card this size that is a third of the visible transcript spent on "You" and "Varaverk". */
|
third of the visible transcript spent on "You" and "Varaverk". */
|
||||||
.vv-ai-c .vv-ai-role { display:none; }
|
.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-body { font-size:12px; line-height:1.55; }
|
||||||
.vv-ai-c .vv-ai-msg.user .vv-ai-body { color:#8fb0c4; }
|
.vv-ai-c .vv-ai-msg.user .vv-ai-body { color:#8fb0c4; }
|
||||||
@@ -300,7 +308,7 @@ function vv_ai_chat_assets(): void {
|
|||||||
<?php
|
<?php
|
||||||
// The profile registry, served rather than restated. This used to be a literal table in the
|
// 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
|
// 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.
|
// hand-roll its own labels.
|
||||||
vv_ai_profiles_script();
|
vv_ai_profiles_script();
|
||||||
?>
|
?>
|
||||||
@@ -493,6 +501,12 @@ vv_ai_profiles_script();
|
|||||||
const q = $('input').value.trim();
|
const q = $('input').value.trim();
|
||||||
if (!q) return;
|
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;
|
busy = true;
|
||||||
$('send').disabled = true;
|
$('send').disabled = true;
|
||||||
addUser(q);
|
addUser(q);
|
||||||
@@ -516,7 +530,12 @@ vv_ai_profiles_script();
|
|||||||
question: q,
|
question: q,
|
||||||
history: JSON.stringify(sendable()),
|
history: JSON.stringify(sendable()),
|
||||||
kind: (PROFILES[profile].kind && kindEl) ? kindEl.value : '',
|
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 });
|
res = fetch(API, { method: 'POST', headers: POST_HEAD, body });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -966,7 +985,7 @@ vv_ai_profiles_script();
|
|||||||
box.innerHTML = '<div class="vv-ai-none">no saved conversations yet</div>';
|
box.innerHTML = '<div class="vv-ai-none">no saved conversations yet</div>';
|
||||||
return;
|
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"
|
// and a list of titles alone makes "why does this one talk about a log I never opened"
|
||||||
// an unanswerable question.
|
// an unanswerable question.
|
||||||
// Tagged only when the tag says something. A scope always does; a profile does unless it
|
// Tagged only when the tag says something. A scope always does; a profile does unless it
|
||||||
|
|||||||
+150
-296
@@ -65,8 +65,8 @@
|
|||||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||||
require_once dirname(__DIR__) . '/include/docs.php';
|
require_once dirname(__DIR__) . '/include/docs.php';
|
||||||
require_once dirname(__DIR__) . '/include/ai_profiles.php';
|
require_once dirname(__DIR__) . '/include/ai_profiles.php';
|
||||||
// For vv_ai_chat_store_script() only — this page renders no chat widget. The store is shared;
|
// The chat component itself now, not only the store: the assistant panel in the right-hand pane
|
||||||
// the presentation deliberately is not.
|
// is an instance of it rather than a second implementation.
|
||||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||||
|
|
||||||
// Live values for the `$VAR` markers in pages/readme/*.md. Conf variables, plus the derived
|
// Live values for the `$VAR` markers in pages/readme/*.md. Conf variables, plus the derived
|
||||||
@@ -1060,49 +1060,36 @@ Still the same two servers, two households, the same media stack running itself.
|
|||||||
them: the Scheduler Info card is only on screen in the suggestions view, so a chat box
|
them: the Scheduler Info card is only on screen in the suggestions view, so a chat box
|
||||||
living there would vanish exactly when the context — a conf, a log, a script — is most
|
living there would vanish exactly when the context — a conf, a log, a script — is most
|
||||||
worth asking about.
|
worth asking about.
|
||||||
At rest it is one input row. Answers expand it upward and the views above shrink to
|
It takes a share of the panel and the views above shrink to suit, which is why
|
||||||
suit, which is why vvFitRight() subtracts its height. It pushes rather than overlays
|
vvFitRight() subtracts its height. It pushes rather than overlays, so the thing being
|
||||||
so the thing you are asking about stays on screen. Collapsing keeps the conversation. -->
|
asked about stays on screen; ⤢ trades more of the panel for the conversation and back. -->
|
||||||
<?php
|
<?php
|
||||||
// The registry and the conversation store — not the chat widget. This dock draws its own
|
// The registry, the store, and now the component itself. vv_ai_chat_assets() is what
|
||||||
// bar and must keep looking like itself; what it must not have is its own answer to where
|
// defines VvAiChat and carries the transcript and composer styling — without it the markup
|
||||||
// a conversation lives.
|
// below renders as inert boxes with no factory to bring them to life. It was absent while
|
||||||
|
// this panel drew its own bar.
|
||||||
vv_ai_profiles_script();
|
vv_ai_profiles_script();
|
||||||
vv_ai_chat_store_script();
|
vv_ai_chat_store_script();
|
||||||
|
vv_ai_chat_assets();
|
||||||
?>
|
?>
|
||||||
|
<!-- The same component the AI tab and the Monitor card render, at this panel's size. It
|
||||||
|
used to be a second implementation with its own bar, send loop and poll — which is how
|
||||||
|
a thread here died on reload while the other two were stored, and how this bar drifted
|
||||||
|
into a different shape from the same control everywhere else.
|
||||||
|
|
||||||
|
The wrapper keeps the #vv-ai-dock id: vvAiDockOn() reads its presence to decide whether
|
||||||
|
to draw the "why?" buttons on the activity rows, and vvFitRight() measures it.
|
||||||
|
|
||||||
|
Heights are set at runtime, not here — see vvFitRight(). This panel's share of the page
|
||||||
|
is not a number that exists until layout has run. -->
|
||||||
<div id="vv-ai-dock">
|
<div id="vv-ai-dock">
|
||||||
<div id="vv-ai-dock-body" style="display:none"></div>
|
<?php vv_ai_chat_markup('vv-sched-ai', [
|
||||||
<div id="vv-ai-dock-bar">
|
'profile' => 'varaverk',
|
||||||
<!-- The chip is now the trigger for the profile picker as well as the scope readout.
|
'compact' => true,
|
||||||
#vv-ai-dock-chip stays the label span it always was, so every existing write to
|
'scopeLabel' => 'Scheduler',
|
||||||
its textContent still lands; the button around it carries the decoration. -->
|
'empty' => 'Ask about what is on screen.',
|
||||||
<div class="vv-ai-picker" id="vv-ai-dock-picker">
|
'placeholder' => 'Ask about what is on screen…',
|
||||||
<button class="vv-ai-chip" id="vv-ai-dock-chipbtn" type="button"
|
]); ?>
|
||||||
aria-haspopup="listbox" aria-expanded="false"
|
|
||||||
title="What the assistant will answer about — follows the view you have open. Click to change profile.">
|
|
||||||
<span id="vv-ai-dock-chip">Assistant · Scheduler</span><span class="vv-ai-chip-c">▾</span>
|
|
||||||
</button>
|
|
||||||
<div class="vv-ai-menu" id="vv-ai-dock-menu" role="listbox">
|
|
||||||
<?php foreach (vv_ai_profiles_ui() as $key => $def): ?>
|
|
||||||
<button class="vv-ai-opt" data-prof="<?= htmlspecialchars($key, ENT_QUOTES) ?>"
|
|
||||||
type="button" role="option">
|
|
||||||
<span class="vv-ai-opt-l"><?= htmlspecialchars($def['label']) ?></span>
|
|
||||||
<span class="vv-ai-opt-h"><?= htmlspecialchars($def['hint']) ?></span>
|
|
||||||
</button>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="text" id="vv-ai-dock-input" autocomplete="off"
|
|
||||||
placeholder="Ask about what is on screen…"
|
|
||||||
onkeydown="if(event.key==='Enter'){event.preventDefault();vvAiDockSend();}">
|
|
||||||
<button class="vv-btn-sm" id="vv-ai-dock-send" onclick="vvAiDockSend()">Ask</button>
|
|
||||||
<button class="vv-btn-sm" id="vv-ai-dock-fix" onclick="vvAiFixStart()"
|
|
||||||
style="display:none" title="Record what actually fixed this, against this script">Save fix</button>
|
|
||||||
<button class="vv-btn-sm" id="vv-ai-dock-hide" onclick="vvAiDockCollapse()"
|
|
||||||
style="display:none" title="Collapse — the conversation is kept">▾</button>
|
|
||||||
<button class="vv-btn-sm vv-ai-dock-grow" id="vv-ai-dock-grow" onclick="vvAiDockGrow()"
|
|
||||||
title="Give the conversation more of the panel">⤢</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<div class="vv-sched-footer vv-sched-info vv-snap-footer" id="vv-sched-info-footer">
|
<div class="vv-sched-footer vv-sched-info vv-snap-footer" id="vv-sched-info-footer">
|
||||||
@@ -1205,11 +1192,6 @@ function vvPost(url, data) {
|
|||||||
}).then(r => r.json());
|
}).then(r => r.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether the assistant dock is holding the larger share of the panel. Persisted: someone working
|
|
||||||
// through a long answer wants it to still be big after a reload, not to re-expand every visit.
|
|
||||||
// Declared here rather than with the other dock state because vvFitRight() below reads it.
|
|
||||||
let vvAiDockBig = localStorage.getItem('vv-ai-dock-big') === '1';
|
|
||||||
|
|
||||||
function vvFitRight() {
|
function vvFitRight() {
|
||||||
const right = document.getElementById('vv-sched-right');
|
const right = document.getElementById('vv-sched-right');
|
||||||
if (!right.classList.contains('vv-panel-visible')) return;
|
if (!right.classList.contains('vv-panel-visible')) return;
|
||||||
@@ -1245,10 +1227,14 @@ function vvFitRight() {
|
|||||||
// Expanded takes most of the panel rather than all of it. The view above is what the question
|
// Expanded takes most of the panel rather than all of it. The view above is what the question
|
||||||
// is usually about — a log, a conf, a script — and a chat that covers it entirely turns every
|
// is usually about — a log, a conf, a script — and a chat that covers it entirely turns every
|
||||||
// follow-up into a round trip through the collapse button.
|
// follow-up into a round trip through the collapse button.
|
||||||
const dockBody = document.getElementById('vv-ai-dock-body');
|
// Both heights handed to the chat rather than one cap applied to it, because the component owns
|
||||||
const dockCap = vvAiDockBig ? 0.82 : 0.4;
|
// which of the two it is currently at. Recomputed on every fit so a resize while expanded stays
|
||||||
if (dockBody) dockBody.style.maxHeight = Math.max(90, Math.round(availH * dockCap)) + 'px';
|
// expanded at the new panel size instead of snapping back to the resting share.
|
||||||
// Measured after the cap is applied, so this reads the clamped height, not the natural one.
|
if (vvSchedChat) {
|
||||||
|
vvSchedChat.setHeights(Math.max(90, Math.round(availH * 0.40)) + 'px',
|
||||||
|
Math.max(90, Math.round(availH * 0.82)) + 'px');
|
||||||
|
}
|
||||||
|
// Measured after the heights are applied, so this reads the clamped height, not the natural one.
|
||||||
const dock = document.getElementById('vv-ai-dock');
|
const dock = document.getElementById('vv-ai-dock');
|
||||||
const dockH = dock ? dock.offsetHeight : 0;
|
const dockH = dock ? dock.offsetHeight : 0;
|
||||||
const contentH = Math.max(80, availH - dockH);
|
const contentH = Math.max(80, availH - dockH);
|
||||||
@@ -2377,48 +2363,42 @@ function vvClickCog(el) {
|
|||||||
let vvAiProfile = 'varaverk';
|
let vvAiProfile = 'varaverk';
|
||||||
let vvAiScope = 'Scheduler';
|
let vvAiScope = 'Scheduler';
|
||||||
let vvAiScopeLabel = 'Scheduler'; // what the chip reads; the target is the id behind it
|
let vvAiScopeLabel = 'Scheduler'; // what the chip reads; the target is the id behind it
|
||||||
let vvAiHist = []; // what the model is told; reset when the scope changes
|
let vvSchedChat = null; // the shared chat instance, once the row has rendered
|
||||||
let vvAiChatId = ""; // the stored conversation this thread is appending to
|
|
||||||
let vvAiBusy = false;
|
|
||||||
let vvAiPoll = null;
|
|
||||||
|
|
||||||
function vvAiDockOn() { return !!document.getElementById('vv-ai-dock'); }
|
// Presence of the instance, not of an element. Everything that guards on this — the "why?" buttons
|
||||||
|
// on the activity rows, the panel fit — needs a working chat, and on a host without the AI row
|
||||||
|
// there is neither.
|
||||||
|
function vvAiDockOn() { return !!vvSchedChat; }
|
||||||
|
|
||||||
// ── Profile picker ───────────────────────────────────────────────────────────
|
// The profile picker, the send loop, the poll, the transcript and the store are all the component's
|
||||||
// The chip states the contract and the thing being asked about; clicking it changes the first
|
// now. What stays here is the part that is genuinely this tab's: which script, log or conf the
|
||||||
// without moving the second. Same control as the Monitor card's, same stylesheet — the dock keeps
|
// question is about, and how that follows the view the operator has open.
|
||||||
// its own bar because that is a real difference, but which profile answers is not a per-surface
|
if (document.getElementById('vv-sched-ai-chat')) {
|
||||||
// idea and should not have a per-surface control.
|
vvSchedChat = VvAiChat({
|
||||||
//
|
prefix: 'vv-sched-ai',
|
||||||
// Routes through vvAiDockScope() rather than assigning vvAiProfile, so a profile change gets the
|
profile: vvAiProfile,
|
||||||
// history reset and the on-screen marker that a scope change already gets. Picking a new contract
|
scopeLabel: vvAiScopeLabel,
|
||||||
// mid-thread and silently feeding it the previous one's turns is the bug that rule exists for.
|
// Re-read at send time rather than captured, because the operator moves around this tab
|
||||||
(function vvAiPickerInit() {
|
// between asking and sending.
|
||||||
const picker = document.getElementById('vv-ai-dock-picker');
|
scope: () => vvAiScope,
|
||||||
if (!picker) return;
|
// Reasoning for diagnosis only — see the note in the component's ask payload.
|
||||||
const btn = document.getElementById('vv-ai-dock-chipbtn');
|
think: p => p === 'troubleshoot',
|
||||||
const menu = document.getElementById('vv-ai-dock-menu');
|
// The picker is the component's, so this is how the page learns the contract changed.
|
||||||
const close = () => { picker.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); };
|
onProfile: p => { vvAiProfile = p; },
|
||||||
|
// Claims what was typed rather than asking it, once, after a fix offer is accepted. Also the
|
||||||
btn.addEventListener('click', e => {
|
// one place that reliably sees every question, so it is where the symptom is remembered.
|
||||||
e.stopPropagation();
|
beforeSend: q => {
|
||||||
const open = picker.classList.toggle('open');
|
if (vvAiFixPending) { vvAiFixSave(q); return true; }
|
||||||
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
vvAiLastQ = q;
|
||||||
menu.querySelectorAll('.vv-ai-opt').forEach(o =>
|
return false;
|
||||||
o.classList.toggle('active', o.dataset.prof === vvAiProfile));
|
},
|
||||||
|
onTurn: () => { vvAiFixArm(); requestAnimationFrame(vvFitRight); },
|
||||||
|
onOffer: (kind, yes) => { if (kind === 'fix') vvAiFixAnswer(yes); },
|
||||||
});
|
});
|
||||||
menu.addEventListener('click', e => {
|
}
|
||||||
const o = e.target.closest('.vv-ai-opt');
|
|
||||||
if (!o) return;
|
|
||||||
close();
|
|
||||||
vvAiDockScope(o.dataset.prof, vvAiScopeLabel, vvAiScope);
|
|
||||||
});
|
|
||||||
document.addEventListener('click', e => { if (!picker.contains(e.target)) close(); });
|
|
||||||
document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Called by every view switch. Profile and scope are derived from what is open and shown on the
|
// Called by every view switch. Profile and scope are derived from what is open and shown on the
|
||||||
// chip — never chosen, never hidden. If the user can see what it thinks it is looking at, a
|
// chip — never chosen, never hidden. If the operator can see what it thinks it is looking at, a
|
||||||
// wrong inference costs a glance instead of a confidently wrong answer.
|
// wrong inference costs a glance instead of a confidently wrong answer.
|
||||||
// label is what the chip shows; target is what the worker resolves. They differ for logs, where
|
// label is what the chip shows; target is what the worker resolves. They differ for logs, where
|
||||||
// the chip wants "daily_sync_maintenance log" and the worker needs the script id it can turn
|
// the chip wants "daily_sync_maintenance log" and the worker needs the script id it can turn
|
||||||
@@ -2428,179 +2408,107 @@ function vvAiDockScope(profile, label, target) {
|
|||||||
target = target || label;
|
target = target || label;
|
||||||
if (profile === vvAiProfile && target === vvAiScope) return;
|
if (profile === vvAiProfile && target === vvAiScope) return;
|
||||||
|
|
||||||
const had = vvAiHist.length > 0;
|
vvAiProfile = profile;
|
||||||
vvAiProfile = profile;
|
vvAiScope = target;
|
||||||
vvAiScope = target;
|
vvAiScopeLabel = label;
|
||||||
vvAiScopeLabel = label; // kept so the picker can change profile without moving the scope
|
// One call for both, so changing profile and subject together leaves one line in the transcript
|
||||||
// Short label from the registry, not a literal map. The map that used to be here was the
|
// rather than two saying nearly the same thing. The component keeps the transcript and moves
|
||||||
// fifth place a profile got defined, and it silently fell back to "Assistant" for any id it
|
// only the floor the model is told about.
|
||||||
// had not been told about — so a new profile would have shown up in the chip as the strict one.
|
vvSchedChat.retarget(profile, label, 'now looking at ' + label);
|
||||||
const _p = (window.VvAiProfiles || {})[profile];
|
// A fix offer belongs to the thing it was diagnosed against; moving the subject abandons it.
|
||||||
document.getElementById('vv-ai-dock-chip').textContent =
|
vvAiFixWatch = null;
|
||||||
(_p ? _p.short : profile) + ' · ' + label;
|
|
||||||
|
|
||||||
// Transcript stays, history sent to the model resets — the same rule the AI tab's profile
|
|
||||||
// buttons already use. A troubleshooting thread carrying log excerpts must not bleed into a
|
|
||||||
// question about a conf key, but hiding that the earlier exchange happened is worse.
|
|
||||||
if (had) {
|
|
||||||
vvAiHist = [];
|
|
||||||
// A new stored conversation too, not a continuation of the last one. The scope is part of
|
|
||||||
// the record, so appending turns about a different thing to the same row would produce a
|
|
||||||
// conversation whose stored scope describes only its first half.
|
|
||||||
vvAiChatId = '';
|
|
||||||
vvAiDockAppend('<div class="vv-ai-dock-sep">now looking at ' + vvEscHtml(label) + '</div>');
|
|
||||||
}
|
|
||||||
// The scope moved under text already typed. Not blocked — just never silent.
|
// The scope moved under text already typed. Not blocked — just never silent.
|
||||||
const input = document.getElementById('vv-ai-dock-input');
|
const input = document.getElementById('vv-sched-ai-input');
|
||||||
if (input.value.trim() !== '') {
|
if (input && input.value.trim() !== '') {
|
||||||
// The button, not the label span inside it. The flash animates a background, and the
|
// The button, not the label span inside it. The flash animates a background, and the
|
||||||
// background moved to the button when the chip became the picker's trigger.
|
// background lives on the button that triggers the picker.
|
||||||
const chip = document.getElementById('vv-ai-dock-chipbtn');
|
const chip = document.getElementById('vv-sched-ai-chip');
|
||||||
chip.classList.remove('vv-ai-chip-flash');
|
if (chip) {
|
||||||
void chip.offsetWidth;
|
chip.classList.remove('vv-ai-chip-flash');
|
||||||
chip.classList.add('vv-ai-chip-flash');
|
void chip.offsetWidth;
|
||||||
|
chip.classList.add('vv-ai-chip-flash');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Anything that changes what is in the dock has to re-run the fit, or the dock keeps the height
|
|
||||||
// the new content wants and pushes the panel past the bottom of the page.
|
|
||||||
function vvAiDockAppend(html) {
|
|
||||||
const body = document.getElementById('vv-ai-dock-body');
|
|
||||||
body.insertAdjacentHTML('beforeend', html);
|
|
||||||
body.scrollTop = body.scrollHeight;
|
|
||||||
requestAnimationFrame(vvFitRight);
|
|
||||||
}
|
|
||||||
|
|
||||||
function vvAiDockExpand() {
|
|
||||||
document.getElementById('vv-ai-dock-body').style.display = '';
|
|
||||||
document.getElementById('vv-ai-dock-hide').style.display = '';
|
|
||||||
vvAiDockApplyGrow();
|
|
||||||
requestAnimationFrame(vvFitRight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// One control, two jobs, decided by what is on screen: grow the conversation, or — once it is
|
|
||||||
// already holding the panel — close it. A dedicated shrink-back would be a third state nobody
|
|
||||||
// asked for; at full size the only thing left to want is the view above.
|
|
||||||
//
|
|
||||||
// Closing from expanded also gives the size back. Without that, 82% is a one-way door: the next
|
|
||||||
// question reopens at 82%, the button is a collapse again, and nothing ever restores the 40%.
|
|
||||||
function vvAiDockGrow() {
|
|
||||||
const open = vvAiDockBodyOpen();
|
|
||||||
vvAiDockBig = !(vvAiDockBig && open);
|
|
||||||
localStorage.setItem('vv-ai-dock-big', vvAiDockBig ? '1' : '0');
|
|
||||||
if (vvAiDockBig) vvAiDockExpand();
|
|
||||||
else vvAiDockCollapse();
|
|
||||||
}
|
|
||||||
|
|
||||||
function vvAiDockBodyOpen() {
|
|
||||||
const b = document.getElementById('vv-ai-dock-body');
|
|
||||||
return !!b && b.style.display !== 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by expand and collapse as well as on load, so the button always describes the action it
|
|
||||||
// will actually perform. A stored "expanded" preference with the conversation shut still reads as
|
|
||||||
// grow — collapsing something already closed is not an action worth offering.
|
|
||||||
function vvAiDockApplyGrow() {
|
|
||||||
const b = document.getElementById('vv-ai-dock-grow');
|
|
||||||
if (!b) return;
|
|
||||||
const collapses = vvAiDockBig && vvAiDockBodyOpen();
|
|
||||||
b.textContent = collapses ? '▾' : '⤢';
|
|
||||||
b.title = collapses ? 'Collapse — the conversation is kept'
|
|
||||||
: 'Give the conversation more of the panel';
|
|
||||||
b.classList.toggle('vv-ai-dock-grow-on', collapses);
|
|
||||||
// Two collapse buttons side by side is one too many. While this one collapses, the other hides.
|
|
||||||
const hide = document.getElementById('vv-ai-dock-hide');
|
|
||||||
if (hide && collapses) hide.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collapse is not clear. Re-expanding shows the same conversation, so you can shrink it to read
|
|
||||||
// the conf the answer was about and open it again without paying for the turn twice.
|
|
||||||
function vvAiDockCollapse() {
|
|
||||||
document.getElementById('vv-ai-dock-body').style.display = 'none';
|
|
||||||
document.getElementById('vv-ai-dock-hide').style.display = 'none';
|
|
||||||
// Closing hands the panel back, whichever button did it, so the next question opens at the
|
|
||||||
// ordinary size instead of inheriting a decision made about a different answer.
|
|
||||||
vvAiDockBig = false;
|
|
||||||
localStorage.setItem('vv-ai-dock-big', '0');
|
|
||||||
vvAiDockApplyGrow();
|
|
||||||
requestAnimationFrame(vvFitRight);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Recording a fix ───────────────────────────────────────────────────────────
|
// ── Recording a fix ───────────────────────────────────────────────────────────
|
||||||
// The symptom is taken from the question that was being asked; the fix is typed by the operator.
|
// The symptom is taken from the question that was being asked; the fix is typed by the operator.
|
||||||
// The model's diagnosis is deliberately not saved — it is a reading of evidence, and writing a
|
// The model's diagnosis is deliberately not saved — it is a reading of evidence, and writing a
|
||||||
// hypothesis into institutional memory as settled fact is how a wrong answer outlives the
|
// hypothesis into institutional memory as settled fact is how a wrong answer outlives the
|
||||||
// incident it came from. What gets remembered is what actually worked.
|
// incident it came from. What gets remembered is what actually worked.
|
||||||
let vvAiFixMode = false;
|
//
|
||||||
let vvAiLastQ = '';
|
// Offered, not buttoned. This used to be a "Save fix" button that appeared after any answer that
|
||||||
|
// was not an error, which meant it was on screen after nearly every question — so it read as
|
||||||
|
// furniture and was mostly ignored. The offer instead waits for the one fact that says the
|
||||||
|
// trouble is actually over: the script that was being diagnosed runs again and exits clean. That
|
||||||
|
// arrives once per episode, at the moment it is true, and it comes from the run record rather
|
||||||
|
// than from a guess about the conversation.
|
||||||
|
let vvAiFixWatch = null; // { scope, symptom, since } while a diagnosis is waiting on a good run
|
||||||
|
let vvAiFixAsk = null; // { scope, symptom } while the offer is open and awaiting an answer
|
||||||
|
|
||||||
function vvAiFixStart() {
|
// Armed when a troubleshoot turn finishes. The question is the symptom; the scope is whatever was
|
||||||
const input = document.getElementById('vv-ai-dock-input');
|
// being diagnosed. `since` guards against a run that had already completed before the diagnosis
|
||||||
vvAiFixMode = true;
|
// started being read as proof that the diagnosis worked.
|
||||||
input.placeholder = 'What actually fixed it? (saved against ' + vvAiScope + ')';
|
function vvAiFixArm() {
|
||||||
input.value = '';
|
if (vvAiProfile !== 'troubleshoot' || !vvAiScope) return;
|
||||||
input.focus();
|
vvAiFixWatch = {
|
||||||
document.getElementById('vv-ai-dock-send').textContent = 'Save';
|
scope: vvAiScope,
|
||||||
document.getElementById('vv-ai-dock-fix').style.display = 'none';
|
symptom: vvAiLastQ || 'diagnosed from the log',
|
||||||
|
since: Math.floor(Date.now() / 1000),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function vvAiFixCancel() {
|
// Called from the activity poll with each completed run. One offer per episode: the watch is
|
||||||
vvAiFixMode = false;
|
// cleared as it fires.
|
||||||
const input = document.getElementById('vv-ai-dock-input');
|
function vvAiFixRunOk(id, startedAt) {
|
||||||
input.placeholder = 'Ask about what is on screen…';
|
const w = vvAiFixWatch;
|
||||||
document.getElementById('vv-ai-dock-send').textContent = 'Ask';
|
if (!w || !vvAiDockOn()) return;
|
||||||
if (vvAiLastQ) document.getElementById('vv-ai-dock-fix').style.display = '';
|
if (id !== w.scope || startedAt < w.since) return;
|
||||||
|
vvAiFixWatch = null;
|
||||||
|
vvAiFixAsk = { scope: w.scope, symptom: w.symptom };
|
||||||
|
// Raw, not escaped — offer() renders through the component's fmt(), which escapes first.
|
||||||
|
vvSchedChat.offer('fix', id + ' just ran clean. Want me to record what fixed it?');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vvAiFixAnswer(yes) {
|
||||||
|
const ask = vvAiFixAsk;
|
||||||
|
vvAiFixAsk = null;
|
||||||
|
if (!yes || !ask) return;
|
||||||
|
// The fix itself is still typed, because only the operator knows what they actually changed.
|
||||||
|
// The next thing sent is captured as the fix rather than asked as a question.
|
||||||
|
vvAiFixPending = ask;
|
||||||
|
const input = document.getElementById('vv-sched-ai-input');
|
||||||
|
if (input) {
|
||||||
|
input.placeholder = 'What actually fixed it? (saved against ' + ask.scope + ')';
|
||||||
|
input.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let vvAiFixPending = null;
|
||||||
|
|
||||||
function vvAiFixSave(text) {
|
function vvAiFixSave(text) {
|
||||||
|
const ask = vvAiFixPending;
|
||||||
|
vvAiFixPending = null;
|
||||||
|
const input = document.getElementById('vv-sched-ai-input');
|
||||||
|
if (input) input.placeholder = 'Ask about what is on screen…';
|
||||||
|
if (!ask) return;
|
||||||
fetch('/plugins/varaverk/api/ai.php', {
|
fetch('/plugins/varaverk/api/ai.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||||
body: new URLSearchParams({ action: 'incident_add', scope: vvAiScope,
|
body: new URLSearchParams({ action: 'incident_add', scope: ask.scope,
|
||||||
symptom: vvAiLastQ, fix: text }),
|
symptom: ask.symptom, fix: text }),
|
||||||
}).then(r => r.json()).then(d => {
|
}).then(r => r.json()).then(d => {
|
||||||
vvAiDockAppend(d.ok
|
vvSchedChat.note(d.ok
|
||||||
? '<div class="vv-ai-dock-sep">saved against ' + vvEscHtml(vvAiScope)
|
? 'saved against ' + ask.scope + ' — it will be shown next time this is diagnosed'
|
||||||
+ ' — it will be shown next time this is diagnosed</div>'
|
: 'could not save: ' + (d.error || 'unknown'));
|
||||||
: '<div class="vv-ai-dock-a vv-ai-dock-err">Could not save: '
|
}).catch(e => vvSchedChat.note('save failed: ' + e));
|
||||||
+ vvEscHtml(d.error || 'unknown') + '</div>');
|
|
||||||
}).catch(e => vvAiDockAppend('<div class="vv-ai-dock-a vv-ai-dock-err">Save failed: '
|
|
||||||
+ vvEscHtml(String(e)) + '</div>'));
|
|
||||||
vvAiFixCancel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function vvAiDockSend() {
|
// The last question asked, kept so a fix filed later has a symptom to go with it. Written by the
|
||||||
if (vvAiBusy) return;
|
// component's beforeSend hook, which is the one place every question passes through.
|
||||||
const input = document.getElementById('vv-ai-dock-input');
|
let vvAiLastQ = '';
|
||||||
const q = input.value.trim();
|
|
||||||
if (!q) return;
|
|
||||||
if (vvAiFixMode) { input.value = ''; vvAiFixSave(q); return; }
|
|
||||||
input.value = '';
|
|
||||||
vvAiBusy = true;
|
|
||||||
document.getElementById('vv-ai-dock-send').disabled = true;
|
|
||||||
|
|
||||||
vvAiDockExpand();
|
|
||||||
vvAiDockAppend('<div class="vv-ai-dock-q">' + vvEscHtml(q) + '</div>'
|
|
||||||
+ '<div class="vv-ai-dock-a vv-ai-dock-wait" id="vv-ai-dock-pending">thinking…</div>');
|
|
||||||
|
|
||||||
fetch('/plugins/varaverk/api/ai.php', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
|
||||||
body: new URLSearchParams({
|
|
||||||
action: 'ask', question: q, profile: vvAiProfile, scope: vvAiScope,
|
|
||||||
// Thinking on for diagnosis only. Working out what a log means is reasoning, and it is
|
|
||||||
// the one thing here worth waiting ~15s for; a lookup like "what does this setting do"
|
|
||||||
// is not, and inline answers that stall feel broken.
|
|
||||||
think: vvAiProfile === 'troubleshoot' ? '1' : '0',
|
|
||||||
history: JSON.stringify(vvAiHist),
|
|
||||||
}),
|
|
||||||
}).then(r => r.json()).then(d => {
|
|
||||||
if (!d.ok || !d.token) return vvAiDockDone(d.error || 'Could not start.', true);
|
|
||||||
vvAiHist.push({ role: 'user', content: q });
|
|
||||||
vvAiLastQ = q;
|
|
||||||
vvAiDockPoll(d.token);
|
|
||||||
}).catch(e => vvAiDockDone('Request failed: ' + e, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asks about one run from the Recent Activity list, in a single click.
|
// Asks about one run from the Recent Activity list, in a single click.
|
||||||
//
|
//
|
||||||
@@ -2614,68 +2522,11 @@ function vvAiDockSend() {
|
|||||||
// request, so what was asked on their behalf is never a mystery.
|
// request, so what was asked on their behalf is never a mystery.
|
||||||
function vvAiAskRun(id, failed, ev) {
|
function vvAiAskRun(id, failed, ev) {
|
||||||
if (ev) ev.stopPropagation();
|
if (ev) ev.stopPropagation();
|
||||||
if (!vvAiDockOn() || vvAiBusy) return;
|
if (!vvAiDockOn() || vvSchedChat.busy()) return;
|
||||||
vvOpenRight(id);
|
vvOpenRight(id);
|
||||||
const input = document.getElementById('vv-ai-dock-input');
|
const input = document.getElementById('vv-sched-ai-input');
|
||||||
input.value = failed ? 'Why did this run fail?' : 'How did this run go?';
|
input.value = failed ? 'Why did this run fail?' : 'How did this run go?';
|
||||||
vvAiDockSend();
|
vvSchedChat.send();
|
||||||
}
|
|
||||||
|
|
||||||
function vvAiDockPoll(token, started) {
|
|
||||||
started = started || Date.now();
|
|
||||||
clearTimeout(vvAiPoll);
|
|
||||||
if (Date.now() - started > 300000) return vvAiDockDone('Timed out waiting for a response.', true);
|
|
||||||
vvAiPoll = setTimeout(() => {
|
|
||||||
fetch('/plugins/varaverk/api/ai.php?action=poll&token=' + encodeURIComponent(token))
|
|
||||||
.then(r => r.json()).then(d => {
|
|
||||||
if (!d.ok) return vvAiDockDone(d.error || 'Poll failed', true);
|
|
||||||
const j = d.job || {};
|
|
||||||
if (j.status === 'done') {
|
|
||||||
vvAiHist.push({ role: 'assistant', content: j.answer || '' });
|
|
||||||
vvAiDockDone(j.answer || '(empty answer)', false, j.sources || []);
|
|
||||||
// Same store as the AI tab and the Monitor row, through the same writer. This dock
|
|
||||||
// kept its conversation in a JavaScript array and nothing else, so a thread died on
|
|
||||||
// reload — including the troubleshooting ones, which are the most expensive to have
|
|
||||||
// had and the most annoying to lose. The scope goes with it so reopening the thread
|
|
||||||
// restores what it was reasoned about rather than continuing it against whatever
|
|
||||||
// happens to be on screen.
|
|
||||||
if (window.VvAiChatSave) {
|
|
||||||
window.VvAiChatSave(vvAiHist, vvAiProfile, vvAiScope, vvAiChatId)
|
|
||||||
.then(id => { if (id) vvAiChatId = id; });
|
|
||||||
}
|
|
||||||
fetch('/plugins/varaverk/api/ai.php', { method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
|
||||||
body: new URLSearchParams({ action: 'clear', token }) }).catch(() => {});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (j.status === 'error') return vvAiDockDone(j.error || 'Unknown error', true);
|
|
||||||
vvAiDockPoll(token, started);
|
|
||||||
}).catch(e => vvAiDockDone('Poll failed: ' + e, true));
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function vvAiDockDone(text, isErr, sources) {
|
|
||||||
const pending = document.getElementById('vv-ai-dock-pending');
|
|
||||||
let html = vvEscHtml(text)
|
|
||||||
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
||||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
||||||
.replace(/\n/g, '<br>');
|
|
||||||
if (sources && sources.length) {
|
|
||||||
html += '<div class="vv-ai-dock-src">' + sources.slice(0, 4).map(s =>
|
|
||||||
vvEscHtml([s.path, s.section, s.heading].filter(Boolean).join(' › '))).join('<br>') + '</div>';
|
|
||||||
}
|
|
||||||
if (pending) {
|
|
||||||
pending.classList.remove('vv-ai-dock-wait');
|
|
||||||
if (isErr) pending.classList.add('vv-ai-dock-err');
|
|
||||||
pending.innerHTML = html;
|
|
||||||
pending.removeAttribute('id');
|
|
||||||
}
|
|
||||||
vvAiBusy = false;
|
|
||||||
document.getElementById('vv-ai-dock-send').disabled = false;
|
|
||||||
// Offered only once there is something to attach a fix to.
|
|
||||||
if (!isErr && vvAiLastQ) document.getElementById('vv-ai-dock-fix').style.display = '';
|
|
||||||
const body = document.getElementById('vv-ai-dock-body');
|
|
||||||
requestAnimationFrame(() => { vvFitRight(); body.scrollTop = body.scrollHeight; });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Advanced mode toggle ──────────────────────────────────────────────────────
|
// ── Advanced mode toggle ──────────────────────────────────────────────────────
|
||||||
@@ -3315,6 +3166,10 @@ function vvLoadRecentActivity() {
|
|||||||
const cls = r.status === 'ok' ? 'vv-stat-ok' : r.status === 'warn' ? 'vv-stat-warn' : 'vv-stat-error';
|
const cls = r.status === 'ok' ? 'vv-stat-ok' : r.status === 'warn' ? 'vv-stat-warn' : 'vv-stat-error';
|
||||||
const bad = r.status !== 'ok' && r.status !== 'skipped';
|
const bad = r.status !== 'ok' && r.status !== 'skipped';
|
||||||
if (bad) errors++;
|
if (bad) errors++;
|
||||||
|
// The one fact that says a diagnosis worked: the script it was about ran again and exited
|
||||||
|
// clean. This poll is already watching for it, so the offer costs a comparison rather than
|
||||||
|
// a second source of truth.
|
||||||
|
if (r.status === 'ok') vvAiFixRunOk(r.id, r.start);
|
||||||
// The question is on the row because that is where the operator already is when they want
|
// The question is on the row because that is where the operator already is when they want
|
||||||
// it. Reaching the same answer otherwise means noticing the red dot, clicking through to
|
// it. Reaching the same answer otherwise means noticing the red dot, clicking through to
|
||||||
// the log, finding the dock, and typing out what the row already knows.
|
// the log, finding the dock, and typing out what the row already knows.
|
||||||
@@ -5058,7 +4913,6 @@ document.querySelectorAll('.vv-sched-card').forEach(card => {
|
|||||||
|
|
||||||
requestAnimationFrame(function() {
|
requestAnimationFrame(function() {
|
||||||
vvRestoreInvert();
|
vvRestoreInvert();
|
||||||
vvAiDockApplyGrow();
|
|
||||||
vvRestoreAdvancedMode();
|
vvRestoreAdvancedMode();
|
||||||
vvRestoreSugStates();
|
vvRestoreSugStates();
|
||||||
vvApplyHighlighting();
|
vvApplyHighlighting();
|
||||||
|
|||||||
Reference in New Issue
Block a user