Give every AI setting a control, and every page one set to draw it with
The AI tab could show three per-turn checkboxes and no configuration at all; the forty-three keys behind it were editable only by hand.
This commit is contained in:
+114
-2
@@ -71,6 +71,7 @@
|
||||
// api/readscript.php source viewer contents
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
require_once dirname(__DIR__) . '/include/confui.php';
|
||||
|
||||
// Build stamp. The tab bar uses Unraid's localURL, which swaps content by AJAX without tearing
|
||||
// down the previous page's JavaScript — so a stale copy of this script can keep running, and
|
||||
@@ -246,9 +247,20 @@ if (is_dir('/var/log/varaverk')) {
|
||||
border-top:1px solid #1a1a1a; }
|
||||
.vv-ai-set-r:first-child { border-top:none; }
|
||||
.vv-ai-set-l { font-size:11px; color:#8a8a8a; min-width:120px; flex-shrink:0; }
|
||||
/* The line between "this conversation" and "this installation". Heavier than the row separators
|
||||
above it, because crossing it is the difference between a checkbox and a conf write. */
|
||||
.vv-ai-conf-wrap { margin-top:12px; border-top:1px solid #2a2a2a; padding-top:10px; }
|
||||
.vv-ai-conf-h { display:flex; align-items:center; gap:9px; font-size:9px; letter-spacing:.08em;
|
||||
text-transform:uppercase; color:#6a6a6a; margin-bottom:9px; }
|
||||
.vv-ai-conf-sub { text-transform:none; letter-spacing:0; font-size:10px; color:#4a4a4a;
|
||||
font-family:monospace; }
|
||||
.vv-ai-conf-h .vv-ai-btn { margin-left:auto; padding:3px 12px; font-size:11px; }
|
||||
/* Scrolls on its own rather than growing the card past the viewport — forty-three fields is
|
||||
taller than the screen, and the Save button belongs where it can still be reached. */
|
||||
#vv-ai-conf { max-height:52vh; overflow-y:auto; padding-right:4px; }
|
||||
.vv-ai-set-d { font-size:10px; color:#4a4a4a; line-height:1.5; }
|
||||
</style>
|
||||
<?php vv_ai_chat_assets(); ?>
|
||||
<?php vv_ai_chat_assets(); vv_conf_ui_assets(); ?>
|
||||
|
||||
<div id="vv-ai-wrap">
|
||||
|
||||
@@ -404,6 +416,24 @@ vv_ai_chat_markup('vv-ai', [
|
||||
with the question — seconds for a greeting, ~18s for a substantive one.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Everything above shapes the next question and is forgotten after it. Everything below
|
||||
is written to master.conf and outlives the tab. One card, because "where do I change
|
||||
the AI" has one answer — but a hard divider and its own heading, because a control that
|
||||
edits a file every script sources should never be mistaken for a per-turn checkbox.
|
||||
|
||||
Loaded when the card is first opened rather than on page load: forty-three fields off
|
||||
two conf files is real work, and this card is shut the overwhelming majority of the
|
||||
time it exists. -->
|
||||
<div class="vv-ai-conf-wrap">
|
||||
<div class="vv-ai-conf-h">
|
||||
Configuration
|
||||
<span class="vv-ai-conf-sub">written to master.conf</span>
|
||||
<span class="vv-ai-set-sum" id="vv-ai-conf-sum"></span>
|
||||
<button class="vv-ai-btn" id="vv-ai-conf-save" type="button" disabled>Save</button>
|
||||
</div>
|
||||
<div id="vv-ai-conf"><div class="vv-ai-none">opens when you do</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,6 +457,10 @@ vv_ai_chat_markup('vv-ai', [
|
||||
<script>
|
||||
(function () {
|
||||
const API = '/plugins/varaverk/api/ai.php';
|
||||
// The conf form's own endpoint, not this page's. Settings are written by the same guarded path
|
||||
// the Scheduler uses, so there is one allowlist and one write path rather than an AI-shaped
|
||||
// copy of both — api/ai.php deliberately has no conf-writing action at all.
|
||||
const API_CONF = '/plugins/varaverk/api/confform.php';
|
||||
|
||||
// The conversation itself — profiles, transcript, composer, source viewer, storage — is
|
||||
// include/ai_chat.php. What remains on this page is everything that surrounds it and exists
|
||||
@@ -1048,7 +1082,85 @@ vv_ai_chat_markup('vv-ai', [
|
||||
}
|
||||
$('vv-ai-set-t').addEventListener('click', () => {
|
||||
$('vv-ai-set-t').classList.toggle('open');
|
||||
$('vv-ai-set-b').classList.toggle('open');
|
||||
const open = $('vv-ai-set-b').classList.toggle('open');
|
||||
// First open only. Re-reading on every toggle would discard edits in progress the moment
|
||||
// someone collapsed the card to look at something else.
|
||||
if (open) loadConf();
|
||||
});
|
||||
|
||||
// ── AI configuration ────────────────────────────────────────────────────
|
||||
// The conf sections whose header names AI, drawn by the shared renderer and written back
|
||||
// through the same endpoint the Scheduler's per-script form uses. Nothing about which fields
|
||||
// exist, what they are called or what control each gets is decided here — all of it is read
|
||||
// from master.conf, so a key added tomorrow appears with no change to this page.
|
||||
let confLoaded = false;
|
||||
|
||||
function loadConf() {
|
||||
if (confLoaded) return;
|
||||
confLoaded = true;
|
||||
fetch(API_CONF + '?sections=ai').then(r => r.json())
|
||||
.then(d => {
|
||||
if (!d.ok) { $('vv-ai-conf').innerHTML = `<div class="vv-ai-none">${esc(d.error || 'could not be read')}</div>`; return; }
|
||||
VvConfUI.render('vv-ai-conf', d.groups || []);
|
||||
VvConfUI.wire('vv-ai-conf', confDirty);
|
||||
confDirty();
|
||||
})
|
||||
.catch(e => {
|
||||
// Retryable: the flag goes back so reopening the card tries again rather than leaving a
|
||||
// permanent error where the settings should be.
|
||||
confLoaded = false;
|
||||
$('vv-ai-conf').innerHTML = `<div class="vv-ai-none">could not be read: ${esc(String(e))}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
function confDirty() {
|
||||
const n = VvConfUI.dirtyCount('vv-ai-conf');
|
||||
$('vv-ai-conf-save').disabled = n === 0;
|
||||
$('vv-ai-conf-sum').innerHTML = n
|
||||
? `<span class="vv-ai-warn">${n} unsaved</span>`
|
||||
: '';
|
||||
}
|
||||
|
||||
$('vv-ai-conf-save').addEventListener('click', () => {
|
||||
const changes = VvConfUI.collect('vv-ai-conf');
|
||||
if (!changes.length) return;
|
||||
|
||||
const btn = $('vv-ai-conf-save');
|
||||
btn.disabled = true;
|
||||
$('vv-ai-conf-sum').innerHTML = 'saving…';
|
||||
|
||||
// URLSearchParams, not FormData. A multipart POST to this plugin's endpoints hangs with no
|
||||
// status ever returned — the request leaves the browser and never reaches PHP.
|
||||
fetch(API_CONF, { method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: new URLSearchParams({ changes: JSON.stringify(changes) }) })
|
||||
.then(r => r.text())
|
||||
.then(t => {
|
||||
let d;
|
||||
try { d = JSON.parse(t); }
|
||||
catch (_) {
|
||||
// The CSRF prepend terminates with an empty body, so this is the shape that failure
|
||||
// takes. Naming it beats a bare parse error.
|
||||
throw new Error(t.trim() ? 'unparseable response: ' + t.slice(0, 120)
|
||||
: 'empty response — the request was rejected before the endpoint ran');
|
||||
}
|
||||
if (!d.ok) throw new Error(d.error || 'save failed');
|
||||
// Rebased rather than refetched: a reload would repaint every control including the one
|
||||
// just edited, and the values on screen are now the values on disk.
|
||||
VvConfUI.commit('vv-ai-conf');
|
||||
confDirty();
|
||||
$('vv-ai-conf-sum').innerHTML =
|
||||
`<span class="vv-ai-ok">saved ${changes.length} change${changes.length > 1 ? 's' : ''}</span>`;
|
||||
// The banner reads AI_ENABLED and the rest of the gates, so it is now stale.
|
||||
loadBanner();
|
||||
loadQueue();
|
||||
})
|
||||
.catch(e => {
|
||||
// Left dirty on purpose. A refused write changed nothing, and clearing the marks would
|
||||
// say it had — see the same rule on the findings card.
|
||||
$('vv-ai-conf-sum').innerHTML = `<span class="vv-ai-bad">${esc(e.message || String(e))}</span>`;
|
||||
confDirty();
|
||||
});
|
||||
});
|
||||
$('vv-ai-kind').addEventListener('change', setSummary);
|
||||
$('vv-ai-think').addEventListener('change', setSummary);
|
||||
|
||||
Reference in New Issue
Block a user