Put the Scheduler dock on the same conversation store as everything else
This commit is contained in:
@@ -256,7 +256,13 @@ if ($action === 'chat_save') {
|
||||
$cap = vv_ai_profiles_max_turns() * 2;
|
||||
if (count($clean) > $cap) $clean = array_slice($clean, -$cap);
|
||||
|
||||
$r = vv_ai_chat_save(trim($_POST['id'] ?? ''), $profile, $clean);
|
||||
// Whitelisted exactly as ask's is, and for the same reason: a scope is only ever a name from
|
||||
// a page's own view state, it is stored and later replayed into a prompt, and anything
|
||||
// richer than a file name is an instruction-injection surface for no benefit.
|
||||
$scope = trim($_POST['scope'] ?? '');
|
||||
if ($scope !== '' && !vv_ai_scope_ok($scope)) $scope = '';
|
||||
|
||||
$r = vv_ai_chat_save(trim($_POST['id'] ?? ''), $profile, $clean, $scope);
|
||||
vv_ai_log('chat_save ' . ($r['ok'] ? 'ok id=' . substr($r['id'], 0, 12)
|
||||
: 'FAILED: ' . $r['error']));
|
||||
echo json_encode($r);
|
||||
|
||||
@@ -1207,6 +1207,7 @@ function vv_ai_chats_list(): array {
|
||||
'id' => $d['id'],
|
||||
'ts' => (int)($d['ts'] ?? 0),
|
||||
'profile' => (string)($d['profile'] ?? 'chat'),
|
||||
'scope' => (string)($d['scope'] ?? ''),
|
||||
'title' => (string)($d['title'] ?? 'Untitled conversation'),
|
||||
'turns' => (int)($d['turns'] ?? count($d['messages'] ?? [])),
|
||||
];
|
||||
@@ -1244,7 +1245,7 @@ function vv_ai_chats_prune(?int $max = null): int {
|
||||
// Writes a whole conversation. An empty id mints one; a known id overwrites in place, which is
|
||||
// what makes a continued conversation stay one row in the list instead of breeding a new one per
|
||||
// turn. Written to a temp file and renamed, so a reader never sees half a transcript.
|
||||
function vv_ai_chat_save(string $id, string $profile, array $messages): array {
|
||||
function vv_ai_chat_save(string $id, string $profile, array $messages, string $scope = ''): array {
|
||||
if (!$messages) return ['ok' => false, 'error' => 'Nothing to save'];
|
||||
|
||||
if ($id === '') $id = bin2hex(random_bytes(16));
|
||||
@@ -1257,12 +1258,19 @@ function vv_ai_chat_save(string $id, string $profile, array $messages): array {
|
||||
$prev = vv_ai_chat_read($id);
|
||||
$created = (int)($prev['created'] ?? time());
|
||||
|
||||
// Scope travels with the conversation. A Scheduler dock thread is bound to what the operator
|
||||
// had open — a script, a log, a conf key — and its turns carry log excerpts chosen for that
|
||||
// thing. Storing the scope means reopening the thread anywhere restores the context it was
|
||||
// reasoned in, rather than silently continuing a troubleshooting conversation against
|
||||
// whatever happens to be on screen. The isolation the dock enforces in memory becomes a
|
||||
// property of the record instead of something lost the moment it is saved.
|
||||
$rec = [
|
||||
'id' => $id,
|
||||
'created' => $created,
|
||||
'ts' => $created,
|
||||
'updated' => time(),
|
||||
'profile' => $profile,
|
||||
'scope' => $scope,
|
||||
'title' => vv_ai_chat_title($messages),
|
||||
'turns' => (int)floor(count($messages) / 2),
|
||||
'messages' => $messages,
|
||||
|
||||
@@ -70,10 +70,51 @@ require_once __DIR__ . '/ai_profiles.php';
|
||||
// Emitted once even if two instances are rendered. A second copy of the script would re-register
|
||||
// the factories harmlessly but would also install a second Escape handler and a second copy of
|
||||
// every keyframe, so the guard is cheaper than reasoning about whether it matters.
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// everywhere at once because there is only one writer.
|
||||
function vv_ai_chat_store_script(): void {
|
||||
static $done = false;
|
||||
if ($done) return;
|
||||
$done = true;
|
||||
?>
|
||||
<script>
|
||||
(function () {
|
||||
const API = '/plugins/varaverk/api/ai.php';
|
||||
// Returns the id so a caller can keep appending to one conversation instead of minting a row
|
||||
// per turn. Fire and forget on failure: a store that cannot be written is not a reason to lose
|
||||
// the answer already on screen.
|
||||
window.VvAiChatSave = function (msgs, profile, scope, id) {
|
||||
if (!msgs || !msgs.length) return Promise.resolve(null);
|
||||
return fetch(API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
||||
body: new URLSearchParams({
|
||||
action: 'chat_save', id: id || '', profile: profile || 'chat',
|
||||
scope: scope || '', messages: JSON.stringify(msgs),
|
||||
}),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(d => (d && d.ok) ? d.id : null)
|
||||
.catch(() => null);
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
function vv_ai_chat_assets(): void {
|
||||
static $done = false;
|
||||
if ($done) return;
|
||||
$done = true;
|
||||
vv_ai_chat_store_script();
|
||||
?>
|
||||
<style>
|
||||
/* ── Shared tone ─────────────────────────────────────────────────────────── */
|
||||
@@ -197,6 +238,8 @@ 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-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; }
|
||||
.vv-ai-crow:hover .vv-ai-crow-x { visibility:visible; }
|
||||
.vv-ai-crow-x:hover { color:#e57; }
|
||||
@@ -518,6 +561,7 @@ vv_ai_profiles_script();
|
||||
fetch(API, { method: 'POST', headers: POST_HEAD,
|
||||
body: new URLSearchParams({
|
||||
action: 'chat_save', id: chatId, profile,
|
||||
scope: (typeof o.scope === 'function' ? o.scope() : (o.scope || '')),
|
||||
messages: JSON.stringify(messages),
|
||||
}) })
|
||||
.then(r => r.json())
|
||||
@@ -525,6 +569,7 @@ vv_ai_profiles_script();
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
|
||||
// A reopened conversation renders as plain turns. Sources, reasoning and timings are not
|
||||
// stored: they describe one generation, and redrawing them beside a transcript that may be
|
||||
// continued under a different profile would be citing evidence for an answer that is no
|
||||
@@ -655,12 +700,25 @@ vv_ai_profiles_script();
|
||||
box.innerHTML = '<div class="vv-ai-none">no saved conversations yet</div>';
|
||||
return;
|
||||
}
|
||||
box.innerHTML = '<div class="vv-ai-clist">' + rows.map(c =>
|
||||
`<div class="vv-ai-crow${c.id === activeId ? ' active' : ''}" data-id="${esc(c.id)}">`
|
||||
+ `<span class="vv-ai-crow-t" title="${esc(c.title)}">${esc(c.title)}</span>`
|
||||
+ `<span class="vv-ai-crow-m">${esc(ago(c.ts))}</span>`
|
||||
+ `<span class="vv-ai-crow-x" data-del="${esc(c.id)}" title="Delete">×</span>`
|
||||
+ `</div>`).join('') + '</div>';
|
||||
// The scope is shown, not just stored. A Scheduler dock 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
|
||||
// is the ordinary one — labelling every General Chat row "Chat" is a column of noise that
|
||||
// makes the rows that are genuinely different harder to pick out, which is the opposite
|
||||
// of the point.
|
||||
const P = window.VvAiProfiles || {};
|
||||
box.innerHTML = '<div class="vv-ai-clist">' + rows.map(c => {
|
||||
const prof = (c.profile && c.profile !== 'chat' && P[c.profile]) ? P[c.profile].short : '';
|
||||
const tag = [prof, c.scope || ''].filter(Boolean).join(' · ');
|
||||
return `<div class="vv-ai-crow${c.id === activeId ? ' active' : ''}" data-id="${esc(c.id)}">`
|
||||
+ `<span class="vv-ai-crow-t" title="${esc(tag ? tag + ' — ' + c.title : c.title)}">`
|
||||
+ (tag ? `<span class="vv-ai-crow-s">${esc(tag)}</span> ` : '')
|
||||
+ `${esc(c.title)}</span>`
|
||||
+ `<span class="vv-ai-crow-m">${esc(ago(c.ts))}</span>`
|
||||
+ `<span class="vv-ai-crow-x" data-del="${esc(c.id)}" title="Delete">×</span>`
|
||||
+ `</div>`;
|
||||
}).join('') + '</div>';
|
||||
}
|
||||
|
||||
function load() {
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
require_once dirname(__DIR__) . '/include/scheduler.php';
|
||||
require_once dirname(__DIR__) . '/include/docs.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 presentation deliberately is not.
|
||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
|
||||
// Live values for the `$VAR` markers in pages/readme/*.md. Conf variables, plus the derived
|
||||
// path constants — those are not conf keys, but they are exactly what a reader needs resolved
|
||||
@@ -1060,7 +1063,13 @@ Still the same two servers, two households, the same media stack running itself.
|
||||
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. -->
|
||||
<?php vv_ai_profiles_script(); ?>
|
||||
<?php
|
||||
// The registry and the conversation store — not the chat widget. This dock draws its own
|
||||
// bar and must keep looking like itself; what it must not have is its own answer to where
|
||||
// a conversation lives.
|
||||
vv_ai_profiles_script();
|
||||
vv_ai_chat_store_script();
|
||||
?>
|
||||
<div id="vv-ai-dock">
|
||||
<div id="vv-ai-dock-body" style="display:none"></div>
|
||||
<div id="vv-ai-dock-bar">
|
||||
@@ -2350,6 +2359,7 @@ function vvClickCog(el) {
|
||||
let vvAiProfile = 'varaverk';
|
||||
let vvAiScope = 'Scheduler';
|
||||
let vvAiHist = []; // what the model is told; reset when the scope changes
|
||||
let vvAiChatId = ""; // the stored conversation this thread is appending to
|
||||
let vvAiBusy = false;
|
||||
let vvAiPoll = null;
|
||||
|
||||
@@ -2381,6 +2391,10 @@ function vvAiDockScope(profile, label, target) {
|
||||
// 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.
|
||||
@@ -2564,6 +2578,16 @@ function vvAiDockPoll(token, started) {
|
||||
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(() => {});
|
||||
|
||||
Reference in New Issue
Block a user