Add a scope-aware assistant dock to the scheduler right panel
This commit is contained in:
@@ -69,6 +69,12 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), [
|
||||
'SCRIPTS_DIR' => SCRIPTS_DIR,
|
||||
]);
|
||||
|
||||
// The assistant dock renders only where the AI tab itself would: HOST1, with AI_ENABLED true.
|
||||
// Same two conditions, checked the same way, so the page cannot offer a chat the endpoint will
|
||||
// refuse — api/ai.php 404s every action off HOST1 regardless of what this page draws.
|
||||
$_vv_ai_on = vv_is_ai_host()
|
||||
&& strtolower(trim(vv_conf_vars()['AI_ENABLED'] ?? 'false')) === 'true';
|
||||
|
||||
// Setup mode — auto-open a conf file and force the editing sequence
|
||||
$vv_setup_conf = preg_match('/^[\w.]+\.conf$/', $_GET['vv_setup'] ?? '')
|
||||
? $_GET['vv_setup'] : '';
|
||||
@@ -1040,6 +1046,27 @@ Still the same two servers, two households, the same media stack running itself.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($_vv_ai_on): ?>
|
||||
<!-- Assistant dock. Sits below every view in the right panel rather than inside any one of
|
||||
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
|
||||
worth asking about.
|
||||
At rest it is one input row. Answers expand it upward and the views above shrink to
|
||||
suit, which is why vvFitRight() subtracts its height. It pushes rather than overlays
|
||||
so the thing you are asking about stays on screen. Collapsing keeps the conversation. -->
|
||||
<div id="vv-ai-dock">
|
||||
<div id="vv-ai-dock-body" style="display:none"></div>
|
||||
<div id="vv-ai-dock-bar">
|
||||
<span id="vv-ai-dock-chip" title="What the assistant will answer about — follows the view you have open">Assistant · Scheduler</span>
|
||||
<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-hide" onclick="vvAiDockCollapse()"
|
||||
style="display:none" title="Collapse — the conversation is kept">▾</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="vv-sched-footer vv-sched-info vv-snap-footer" id="vv-sched-info-footer">
|
||||
<span class="vv-snap-item">
|
||||
<span class="vv-snap-label">CPU</span>
|
||||
@@ -1162,7 +1189,16 @@ function vvFitRight() {
|
||||
rf.style.height = lf.offsetHeight + 'px';
|
||||
right.style.height = left.offsetHeight + 'px';
|
||||
|
||||
const contentH = Math.max(80, lf.getBoundingClientRect().top - 32 - toolbar.getBoundingClientRect().bottom);
|
||||
// The assistant dock sits below every view, so its height comes off the space the views get.
|
||||
// Measured, not assumed — it is one row at rest and taller once an answer is open.
|
||||
const dock = document.getElementById('vv-ai-dock');
|
||||
const dockH = dock ? dock.offsetHeight : 0;
|
||||
const contentH = Math.max(80, lf.getBoundingClientRect().top - 32
|
||||
- toolbar.getBoundingClientRect().bottom - dockH);
|
||||
// Cap the conversation at roughly 40% of the panel and let it scroll internally, so a long
|
||||
// answer shrinks the view above by a bounded amount instead of swallowing it.
|
||||
const dockBody = document.getElementById('vv-ai-dock-body');
|
||||
if (dockBody) dockBody.style.maxHeight = Math.max(90, Math.round(contentH * 0.4)) + 'px';
|
||||
if (pre.style.display !== 'none') {
|
||||
pre.style.maxHeight = 'none';
|
||||
pre.style.height = contentH + 'px';
|
||||
@@ -1207,6 +1243,7 @@ window.addEventListener('resize', vvFitRight);
|
||||
new ResizeObserver(vvFitRight).observe(document.getElementById('vv-sched-left'));
|
||||
|
||||
function vvShowLogMode(id) {
|
||||
vvAiDockScope('varaverk', String(id).replace(/\.sh$/, '') + ' log');
|
||||
document.getElementById('vv-suggestions').style.display = 'none';
|
||||
document.getElementById('vv-editor').style.display = 'none';
|
||||
document.getElementById('vv-si-view').style.display = 'none';
|
||||
@@ -1257,6 +1294,7 @@ function vvRestoreInvert() {
|
||||
}
|
||||
|
||||
function vvBackToSuggestions() {
|
||||
vvAiDockScope('varaverk', 'Scheduler');
|
||||
vvEditorReset();
|
||||
document.getElementById('vv-editor-status').classList.remove('vv-ed-active');
|
||||
document.getElementById('vv-undo-btn').style.display = 'none';
|
||||
@@ -1730,6 +1768,7 @@ function vvEditScript(id) {
|
||||
}
|
||||
|
||||
function vvShowEditorMode(title) {
|
||||
vvAiDockScope('code', title || 'a custom script');
|
||||
vvEditorReset();
|
||||
document.getElementById('vv-editor').classList.add('vv-editor-hl');
|
||||
document.getElementById('vv-editor-status').classList.add('vv-ed-active');
|
||||
@@ -1985,6 +2024,7 @@ function _vvImpDoImport() {
|
||||
}
|
||||
|
||||
function vvShowConfMode(title) {
|
||||
vvAiDockScope('varaverk', (title || 'a script') + ' settings');
|
||||
document.getElementById('vv-suggestions').style.display = 'none';
|
||||
document.getElementById('vv-log-pre').style.display = 'none';
|
||||
document.getElementById('vv-si-view').style.display = 'none';
|
||||
@@ -2242,6 +2282,137 @@ function vvClickCog(el) {
|
||||
vvShowScriptInfoMode(id.replace(/\.sh$/, '').split('/').pop(), '', id);
|
||||
}
|
||||
|
||||
// ── Assistant dock ────────────────────────────────────────────────────────────
|
||||
// One component, one instance. Every view calls vvAiDockScope() to say where the user now is;
|
||||
// nothing else about it is per-view. Building a chat per card would mean N implementations of
|
||||
// the same thing, which is how the multipart POST bug became 21 call sites across 7 pages.
|
||||
|
||||
let vvAiProfile = 'varaverk';
|
||||
let vvAiScope = 'Scheduler';
|
||||
let vvAiHist = []; // what the model is told; reset when the scope changes
|
||||
let vvAiBusy = false;
|
||||
let vvAiPoll = null;
|
||||
|
||||
function vvAiDockOn() { return !!document.getElementById('vv-ai-dock'); }
|
||||
|
||||
// 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
|
||||
// wrong inference costs a glance instead of a confidently wrong answer.
|
||||
function vvAiDockScope(profile, label) {
|
||||
if (!vvAiDockOn()) return;
|
||||
if (profile === vvAiProfile && label === vvAiScope) return;
|
||||
|
||||
const had = vvAiHist.length > 0;
|
||||
vvAiProfile = profile;
|
||||
vvAiScope = label;
|
||||
document.getElementById('vv-ai-dock-chip').textContent =
|
||||
(profile === 'code' ? 'Code' : 'Assistant') + ' · ' + 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 = [];
|
||||
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.
|
||||
const input = document.getElementById('vv-ai-dock-input');
|
||||
if (input.value.trim() !== '') {
|
||||
const chip = document.getElementById('vv-ai-dock-chip');
|
||||
chip.classList.remove('vv-ai-chip-flash');
|
||||
void chip.offsetWidth;
|
||||
chip.classList.add('vv-ai-chip-flash');
|
||||
}
|
||||
}
|
||||
|
||||
function vvAiDockAppend(html) {
|
||||
const body = document.getElementById('vv-ai-dock-body');
|
||||
body.insertAdjacentHTML('beforeend', html);
|
||||
body.scrollTop = body.scrollHeight;
|
||||
}
|
||||
|
||||
function vvAiDockExpand() {
|
||||
document.getElementById('vv-ai-dock-body').style.display = '';
|
||||
document.getElementById('vv-ai-dock-hide').style.display = '';
|
||||
requestAnimationFrame(vvFitRight);
|
||||
}
|
||||
|
||||
// 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';
|
||||
requestAnimationFrame(vvFitRight);
|
||||
}
|
||||
|
||||
function vvAiDockSend() {
|
||||
if (vvAiBusy) return;
|
||||
const input = document.getElementById('vv-ai-dock-input');
|
||||
const q = input.value.trim();
|
||||
if (!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,
|
||||
think: '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 });
|
||||
vvAiDockPoll(d.token);
|
||||
}).catch(e => vvAiDockDone('Request failed: ' + e, true));
|
||||
}
|
||||
|
||||
function vvAiDockPoll(token) {
|
||||
clearTimeout(vvAiPoll);
|
||||
vvAiPoll = setTimeout(() => {
|
||||
fetch('/plugins/varaverk/api/ai.php?action=poll&token=' + encodeURIComponent(token))
|
||||
.then(r => r.json()).then(j => {
|
||||
if (j.status === 'done') {
|
||||
vvAiHist.push({ role: 'assistant', content: j.answer || '' });
|
||||
vvAiDockDone(j.answer || '(empty answer)', false, j.sources || []);
|
||||
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);
|
||||
}).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;
|
||||
const body = document.getElementById('vv-ai-dock-body');
|
||||
body.scrollTop = body.scrollHeight;
|
||||
}
|
||||
|
||||
// ── Advanced mode toggle ──────────────────────────────────────────────────────
|
||||
|
||||
let vvAdvancedMode = false;
|
||||
@@ -3027,6 +3198,7 @@ function vvEditRawConf(file) {
|
||||
}
|
||||
|
||||
function vvShowRawConfMode(title) {
|
||||
vvAiDockScope('varaverk', title || 'a conf file');
|
||||
vvEditorReset();
|
||||
vvRestoreEditorPrefs();
|
||||
document.getElementById('vv-editor-status').classList.add('vv-ed-active');
|
||||
@@ -3851,6 +4023,7 @@ function vvSiRetitle(hdr, suffix) {
|
||||
}
|
||||
|
||||
function vvShowScriptInfoMode(name, hdr, id) {
|
||||
vvAiDockScope('varaverk', name || 'a script');
|
||||
document.getElementById('vv-suggestions').style.display = 'none';
|
||||
document.getElementById('vv-log-pre').style.display = 'none';
|
||||
document.getElementById('vv-editor').style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user