Let a long conversation list be filtered instead of scanned

This commit is contained in:
Gmer4Lfe
2026-08-10 22:18:06 -04:00
parent 081366a682
commit 25361fafdb
+45 -2
View File
@@ -177,6 +177,13 @@ function vv_ai_chat_assets(): void {
padding:3px 12px; border-radius:11px; cursor:pointer; opacity:.94; } padding:3px 12px; border-radius:11px; cursor:pointer; opacity:.94; }
.vv-ai-jump:hover { background:#2e2e2e; color:#fff; } .vv-ai-jump:hover { background:#2e2e2e; color:#fff; }
/* Filter box at the head of the saved-conversation menu. Borderless except underneath, so it
reads as part of the menu rather than a control sitting on top of one. */
.vv-ai-hist-f { width:100%; box-sizing:border-box; background:#0d0d0d; border:none;
border-bottom:1px solid #262626; color:#ccc; font-size:11px;
padding:6px 9px; outline:none; }
.vv-ai-hist-f::placeholder { color:#3f3f3f; }
/* Folded answers cap at a readable height with a hard bottom edge rather than a fade — a fade /* Folded answers cap at a readable height with a hard bottom edge rather than a fade — a fade
over a code block reads as a rendering fault. */ over a code block reads as a rendering fault. */
.vv-ai-fold { max-height:420px; overflow:hidden; } .vv-ai-fold { max-height:420px; overflow:hidden; }
@@ -1323,7 +1330,10 @@ vv_ai_profiles_script();
// Same tagging rule as the Conversations card: a scope always says something, a profile // Same tagging rule as the Conversations card: a scope always says something, a profile
// says something unless it is the ordinary one. // says something unless it is the ordinary one.
const PR = window.VvAiProfiles || {}; const PR = window.VvAiProfiles || {};
histMenu.innerHTML = rows.map(c => {
// Filters the rows already fetched rather than asking the endpoint again — the store is
// small, it is all in hand, and a keystroke should not cost a request.
const rowHtml = c => {
const prof = (c.profile && c.profile !== 'chat' && PR[c.profile]) ? PR[c.profile].short : ''; const prof = (c.profile && c.profile !== 'chat' && PR[c.profile]) ? PR[c.profile].short : '';
const tag = [prof, c.scope || ''].filter(Boolean).join(' · '); const tag = [prof, c.scope || ''].filter(Boolean).join(' · ');
return `<button class="vv-ai-opt${c.id === chatId ? ' active' : ''}" type="button"` return `<button class="vv-ai-opt${c.id === chatId ? ' active' : ''}" type="button"`
@@ -1331,7 +1341,40 @@ vv_ai_profiles_script();
+ `<span class="vv-ai-opt-l">${esc(c.title)}</span>` + `<span class="vv-ai-opt-l">${esc(c.title)}</span>`
+ `<span class="vv-ai-opt-h">${esc([tag, ago(c.ts)].filter(Boolean).join(' · '))}</span>` + `<span class="vv-ai-opt-h">${esc([tag, ago(c.ts)].filter(Boolean).join(' · '))}</span>`
+ `</button>`; + `</button>`;
}).join(''); };
// Title and scope, because those are what a conversation is remembered by — "the one
// about the rsync stall" or "the one pointed at daily_sync".
const match = (c, q) =>
(String(c.title || '') + ' ' + String(c.scope || '') + ' ' + String(c.profile || ''))
.toLowerCase().includes(q);
const draw = q => {
const hits = q ? rows.filter(c => match(c, q)) : rows;
const list = histMenu.querySelector('[data-hist-rows]');
if (!list) return;
list.innerHTML = hits.length
? hits.map(rowHtml).join('')
: `<div class="vv-ai-hist-msg">nothing matching “${esc(q)}”</div>`;
};
// The box only earns its line once there is enough to sift through.
const searchable = rows.length >= 6;
histMenu.innerHTML =
(searchable ? `<input type="text" class="vv-ai-hist-f" data-hist-filter
placeholder="filter ${rows.length} conversations…">` : '')
+ `<div data-hist-rows></div>`;
draw('');
if (searchable) {
const f = histMenu.querySelector('[data-hist-filter]');
f.addEventListener('input', () => draw(f.value.trim().toLowerCase()));
// Escape clears the filter before it closes the menu — one key, one step back.
f.addEventListener('keydown', ev => {
if (ev.key === 'Escape' && f.value !== '') { ev.stopPropagation(); f.value = ''; draw(''); }
});
f.focus();
}
}).catch(() => { }).catch(() => {
histMenu.innerHTML = '<div class="vv-ai-hist-msg">could not read the store</div>'; histMenu.innerHTML = '<div class="vv-ai-hist-msg">could not read the store</div>';
}); });