Monitor conversation list: collapsible section for the card's own profile above the full list
A flat list buries 'where was I in General Chat' under whatever the tabs have been doing, since the busiest profile takes the top regardless of which card you are looking at.
This commit is contained in:
@@ -2163,9 +2163,60 @@ vv_ai_profiles_script();
|
||||
if (!box) return null;
|
||||
let rows = [], activeId = '';
|
||||
|
||||
// Optional first section: this card's own profile, collapsed, holding its most recent
|
||||
// conversations. Underneath it the full list carries on unchanged.
|
||||
//
|
||||
// The two answer different questions. "Where was I in General Chat" is the one asked on a
|
||||
// card pinned to General Chat, and a single flat list buries it under whatever the tabs have
|
||||
// been doing — the profile with the most threads wins the top of the list regardless of which
|
||||
// card you are looking at. Collapsed by default because the card resumes the newest of these
|
||||
// anyway; opening it is for reaching the other nine.
|
||||
const GROUP = o.groupProfile || '';
|
||||
const GROUP_MAX = o.groupMax || 10;
|
||||
const OPEN_KEY = 'vvAiListOpen:' + (o.into || '');
|
||||
let groupOpen = false;
|
||||
try { groupOpen = localStorage.getItem(OPEN_KEY) === '1'; } catch (_) {}
|
||||
|
||||
function rowHtml(c, P) {
|
||||
const prof = (c.profile && c.profile !== 'chat' && P[c.profile]) ? P[c.profile].short : '';
|
||||
const tag = [prof, c.scope || ''].filter(Boolean).join(' · ');
|
||||
return `<div class="vv-ai-crow${c.id === activeId ? ' active' : ''}" data-id="${esc(c.id)}">`
|
||||
+ `<span class="vv-ai-crow-t" title="${esc((tag ? tag + ' — ' : '') + c.title
|
||||
+ ' · ' + chatAges(c))}">`
|
||||
+ (tag ? `<span class="vv-ai-crow-s">${esc(tag)}</span> ` : '')
|
||||
+ `${esc(c.title)}</span>`
|
||||
+ `<span class="vv-ai-crow-m">${esc(ago(c.ts))}</span>`
|
||||
+ `<span class="vv-ai-crow-x" data-del="${esc(c.id)}" title="Delete">×</span>`
|
||||
+ `</div>`;
|
||||
}
|
||||
|
||||
function groupHtml() {
|
||||
if (!GROUP) return '';
|
||||
const P = window.VvAiProfiles || {};
|
||||
const name = (P[GROUP] && P[GROUP].short) || (GROUP === 'chat' ? 'General Chat' : GROUP);
|
||||
const mine = rows.filter(c => (c.profile || 'chat') === GROUP)
|
||||
.sort((a, b) => (b.updated || b.ts || 0) - (a.updated || a.ts || 0))
|
||||
.slice(0, GROUP_MAX);
|
||||
return `<div class="vv-ai-cgroup">`
|
||||
+ `<div class="vv-ai-chead" data-grp="1" style="display:flex;align-items:center;gap:6px;
|
||||
cursor:pointer;padding:3px 4px;user-select:none;">`
|
||||
+ `<span style="color:#666;font-size:10px;">${groupOpen ? '▾' : '▸'}</span>`
|
||||
+ `<span style="color:#888;font-size:10px;font-weight:600;">${esc(name)}</span>`
|
||||
+ `<span style="color:#333;font-size:9px;">${mine.length}</span>`
|
||||
+ `</div>`
|
||||
+ (groupOpen
|
||||
? `<div class="vv-ai-clist">`
|
||||
+ (mine.length ? mine.map(c => rowHtml(c, P)).join('')
|
||||
: '<div class="vv-ai-none">none yet</div>')
|
||||
+ `</div>`
|
||||
: '')
|
||||
+ `<div style="border-bottom:1px solid #1b1b1b;margin:5px 0;"></div>`
|
||||
+ `</div>`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!rows.length) {
|
||||
box.innerHTML = '<div class="vv-ai-none">no saved conversations yet</div>';
|
||||
box.innerHTML = groupHtml() + '<div class="vv-ai-none">no saved conversations yet</div>';
|
||||
return;
|
||||
}
|
||||
// The scope is shown, not just stored. A Scheduler thread is about one script or log,
|
||||
@@ -2176,18 +2227,8 @@ vv_ai_profiles_script();
|
||||
// makes the rows that are genuinely different harder to pick out, which is the opposite
|
||||
// of the point.
|
||||
const P = window.VvAiProfiles || {};
|
||||
box.innerHTML = '<div class="vv-ai-clist">' + rows.map(c => {
|
||||
const prof = (c.profile && c.profile !== 'chat' && P[c.profile]) ? P[c.profile].short : '';
|
||||
const tag = [prof, c.scope || ''].filter(Boolean).join(' · ');
|
||||
return `<div class="vv-ai-crow${c.id === activeId ? ' active' : ''}" data-id="${esc(c.id)}">`
|
||||
+ `<span class="vv-ai-crow-t" title="${esc((tag ? tag + ' — ' : '') + c.title
|
||||
+ ' · ' + chatAges(c))}">`
|
||||
+ (tag ? `<span class="vv-ai-crow-s">${esc(tag)}</span> ` : '')
|
||||
+ `${esc(c.title)}</span>`
|
||||
+ `<span class="vv-ai-crow-m">${esc(ago(c.ts))}</span>`
|
||||
+ `<span class="vv-ai-crow-x" data-del="${esc(c.id)}" title="Delete">×</span>`
|
||||
+ `</div>`;
|
||||
}).join('') + '</div>';
|
||||
box.innerHTML = groupHtml()
|
||||
+ '<div class="vv-ai-clist">' + rows.map(c => rowHtml(c, P)).join('') + '</div>';
|
||||
}
|
||||
|
||||
function load() {
|
||||
@@ -2197,6 +2238,14 @@ vv_ai_profiles_script();
|
||||
}
|
||||
|
||||
box.addEventListener('click', e => {
|
||||
// Before the row handler: the header sits above the rows and must not be read as one.
|
||||
const grp = e.target.closest('[data-grp]');
|
||||
if (grp) {
|
||||
groupOpen = !groupOpen;
|
||||
try { localStorage.setItem(OPEN_KEY, groupOpen ? '1' : '0'); } catch (_) {}
|
||||
render();
|
||||
return;
|
||||
}
|
||||
const del = e.target.closest('[data-del]');
|
||||
if (del) {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -2867,7 +2867,14 @@ if (document.getElementById('vv-mon-ai-chat')) {
|
||||
// for the poll to reflect it reads as the card being broken.
|
||||
onTurn: () => vvLoadTokens(),
|
||||
});
|
||||
vvMonChatList = VvAiChatList({ into: 'vv-mon-ai-chats', chat: vvMonChat });
|
||||
vvMonChatList = VvAiChatList({
|
||||
into: 'vv-mon-ai-chats',
|
||||
chat: vvMonChat,
|
||||
// This card is pinned to General Chat, so its own threads get the top section rather than
|
||||
// competing for position with everything the tabs have been doing.
|
||||
groupProfile: 'chat',
|
||||
groupMax: 10,
|
||||
});
|
||||
vvMonChatList.reload();
|
||||
|
||||
// Re-measured rather than computed once. The Assistant changes height when the transcript is
|
||||
|
||||
Reference in New Issue
Block a user