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; }
|
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>';
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user