Let a long conversation list be filtered instead of scanned
This commit is contained in:
@@ -177,6 +177,13 @@ function vv_ai_chat_assets(): void {
|
||||
padding:3px 12px; border-radius:11px; cursor:pointer; opacity:.94; }
|
||||
.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
|
||||
over a code block reads as a rendering fault. */
|
||||
.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
|
||||
// says something unless it is the ordinary one.
|
||||
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 tag = [prof, c.scope || ''].filter(Boolean).join(' · ');
|
||||
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-h">${esc([tag, ago(c.ts)].filter(Boolean).join(' · '))}</span>`
|
||||
+ `</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(() => {
|
||||
histMenu.innerHTML = '<div class="vv-ai-hist-msg">could not read the store</div>';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user