One profile picker for both surfaces, and open where you left off

This commit is contained in:
Gmer4Lfe
2026-08-09 00:56:04 -04:00
parent d9225d06d9
commit 6cdb0d1eab
4 changed files with 185 additions and 32 deletions
+57 -2
View File
@@ -1073,7 +1073,25 @@ Still the same two servers, two households, the same media stack running itself.
<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>
<!-- The chip is now the trigger for the profile picker as well as the scope readout.
#vv-ai-dock-chip stays the label span it always was, so every existing write to
its textContent still lands; the button around it carries the decoration. -->
<div class="vv-ai-picker" id="vv-ai-dock-picker">
<button class="vv-ai-chip" id="vv-ai-dock-chipbtn" type="button"
aria-haspopup="listbox" aria-expanded="false"
title="What the assistant will answer about — follows the view you have open. Click to change profile.">
<span id="vv-ai-dock-chip">Assistant · Scheduler</span><span class="vv-ai-chip-c">▾</span>
</button>
<div class="vv-ai-menu" id="vv-ai-dock-menu" role="listbox">
<?php foreach (vv_ai_profiles_ui() as $key => $def): ?>
<button class="vv-ai-opt" data-prof="<?= htmlspecialchars($key, ENT_QUOTES) ?>"
type="button" role="option">
<span class="vv-ai-opt-l"><?= htmlspecialchars($def['label']) ?></span>
<span class="vv-ai-opt-h"><?= htmlspecialchars($def['hint']) ?></span>
</button>
<?php endforeach; ?>
</div>
</div>
<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();}">
@@ -2358,6 +2376,7 @@ function vvClickCog(el) {
let vvAiProfile = 'varaverk';
let vvAiScope = 'Scheduler';
let vvAiScopeLabel = 'Scheduler'; // what the chip reads; the target is the id behind it
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;
@@ -2365,6 +2384,39 @@ let vvAiPoll = null;
function vvAiDockOn() { return !!document.getElementById('vv-ai-dock'); }
// ── Profile picker ───────────────────────────────────────────────────────────
// The chip states the contract and the thing being asked about; clicking it changes the first
// without moving the second. Same control as the Monitor card's, same stylesheet — the dock keeps
// its own bar because that is a real difference, but which profile answers is not a per-surface
// idea and should not have a per-surface control.
//
// Routes through vvAiDockScope() rather than assigning vvAiProfile, so a profile change gets the
// history reset and the on-screen marker that a scope change already gets. Picking a new contract
// mid-thread and silently feeding it the previous one's turns is the bug that rule exists for.
(function vvAiPickerInit() {
const picker = document.getElementById('vv-ai-dock-picker');
if (!picker) return;
const btn = document.getElementById('vv-ai-dock-chipbtn');
const menu = document.getElementById('vv-ai-dock-menu');
const close = () => { picker.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); };
btn.addEventListener('click', e => {
e.stopPropagation();
const open = picker.classList.toggle('open');
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
menu.querySelectorAll('.vv-ai-opt').forEach(o =>
o.classList.toggle('active', o.dataset.prof === vvAiProfile));
});
menu.addEventListener('click', e => {
const o = e.target.closest('.vv-ai-opt');
if (!o) return;
close();
vvAiDockScope(o.dataset.prof, vvAiScopeLabel, vvAiScope);
});
document.addEventListener('click', e => { if (!picker.contains(e.target)) close(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
})();
// 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.
@@ -2379,6 +2431,7 @@ function vvAiDockScope(profile, label, target) {
const had = vvAiHist.length > 0;
vvAiProfile = profile;
vvAiScope = target;
vvAiScopeLabel = label; // kept so the picker can change profile without moving the scope
// Short label from the registry, not a literal map. The map that used to be here was the
// fifth place a profile got defined, and it silently fell back to "Assistant" for any id it
// had not been told about — so a new profile would have shown up in the chip as the strict one.
@@ -2400,7 +2453,9 @@ function vvAiDockScope(profile, label, target) {
// 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');
// The button, not the label span inside it. The flash animates a background, and the
// background moved to the button when the chip became the picker's trigger.
const chip = document.getElementById('vv-ai-dock-chipbtn');
chip.classList.remove('vv-ai-chip-flash');
void chip.offsetWidth;
chip.classList.add('vv-ai-chip-flash');