Put the Scheduler dock on the same conversation store as everything else
This commit is contained in:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user