From 25361fafdb405f95beec807fcaa16821a92381a1 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Mon, 10 Aug 2026 22:18:06 -0400 Subject: [PATCH] Let a long conversation list be filtered instead of scanned --- Plugin/unraid/include/ai_chat.php | 47 +++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/Plugin/unraid/include/ai_chat.php b/Plugin/unraid/include/ai_chat.php index febd534..4718065 100644 --- a/Plugin/unraid/include/ai_chat.php +++ b/Plugin/unraid/include/ai_chat.php @@ -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 ``; - }).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('') + : `
nothing matching “${esc(q)}”
`; + }; + + // The box only earns its line once there is enough to sift through. + const searchable = rows.length >= 6; + histMenu.innerHTML = + (searchable ? `` : '') + + `
`; + 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 = '
could not read the store
'; });