Put the Scheduler dock on the same conversation store as everything else

This commit is contained in:
Gmer4Lfe
2026-08-09 00:16:09 -04:00
parent f4fd17be4d
commit 073352b21e
4 changed files with 105 additions and 9 deletions
+64 -6
View File
@@ -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() {