diff --git a/Plugin/unraid/include/ai_chat.php b/Plugin/unraid/include/ai_chat.php
index 8a39801..d4a6bb4 100644
--- a/Plugin/unraid/include/ai_chat.php
+++ b/Plugin/unraid/include/ai_chat.php
@@ -2171,11 +2171,17 @@ vv_ai_profiles_script();
// 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 GROUPED = o.grouped !== false; // one collapsible section per profile
+ const GROUP_MAX = o.groupMax || 10; // per profile, and for the recent list below
const OPEN_KEY = 'vvAiListOpen:' + (o.into || '');
- let groupOpen = false;
- try { groupOpen = localStorage.getItem(OPEN_KEY) === '1'; } catch (_) {}
+
+ // Which sections are open, by profile key. Remembered as a set rather than one flag: the
+ // point of per-profile sections is that they are asked about independently.
+ let openSet = {};
+ try { openSet = JSON.parse(localStorage.getItem(OPEN_KEY) || '{}') || {}; } catch (_) { openSet = {}; }
+ // The card's own profile starts open — it is the one its operator is most likely to want,
+ // and on a card pinned to a profile an all-collapsed list says nothing on arrival.
+ if (o.groupProfile && !(o.groupProfile in openSet)) openSet[o.groupProfile] = true;
function rowHtml(c, P) {
const prof = (c.profile && c.profile !== 'chat' && P[c.profile]) ? P[c.profile].short : '';
@@ -2190,33 +2196,62 @@ vv_ai_profiles_script();
+ ``;
}
+ // One collapsible section per profile, each holding that profile's most recent GROUP_MAX.
+ //
+ // Profile order comes from the registry, not from the data, so the sections stay in the same
+ // places as threads come and go — a list that reorders itself under the cursor is a list you
+ // have to re-read every time. Profiles with nothing in them are still shown, with a count of
+ // zero: their absence would otherwise read as a bug on the day you first look for one.
function groupHtml() {
- if (!GROUP) return '';
+ if (!GROUPED) 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))
+ const keys = Object.keys(P);
+ if (!keys.length) return '';
+
+ const byProf = {};
+ for (const c of rows) (byProf[c.profile || 'chat'] = byProf[c.profile || 'chat'] || []).push(c);
+
+ let html = '';
+ for (const k of keys) {
+ const name = (P[k] && P[k].short) || k;
+ const mine = (byProf[k] || [])
+ .slice().sort((a, b) => (b.updated || b.ts || 0) - (a.updated || a.ts || 0))
.slice(0, GROUP_MAX);
- return `
`
- + `
`
- + `${groupOpen ? '▾' : '▸'}`
- + `${esc(name)}`
- + `${mine.length}`
- + `
`
- + (groupOpen
- ? `
`
- + (mine.length ? mine.map(c => rowHtml(c, P)).join('')
- : '
none yet
')
- + `
`
- : '')
- + `
`
+ const open = !!openSet[k];
+ html += `
`
+ + `
`
+ + `${open ? '▾' : '▸'}`
+ + `${esc(name)}`
+ + `${mine.length}`
+ + `
`
+ + (open
+ ? `
`
+ + (mine.length ? mine.map(c => rowHtml(c, P)).join('')
+ : '
none yet
')
+ + `
`
+ : '')
+ + `
`;
+ }
+ return html;
+ }
+
+ // The banner is a heading, not decoration: without it the rows below read as a sixth profile
+ // section rather than as the cross-profile recents.
+ function recentBanner(n) {
+ return `
`
+ + `
`
+ + `
Last ${n} conversations`
+ + `
`
+ `
`;
}
function render() {
if (!rows.length) {
- box.innerHTML = groupHtml() + '
no saved conversations yet
';
+ // No sections on a genuinely empty store — five headings all reading 0 is a shape that
+ // implies something is filtered out rather than that nothing has been said yet.
+ box.innerHTML = '
no saved conversations yet
';
return;
}
// The scope is shown, not just stored. A Scheduler thread is about one script or log,
@@ -2227,8 +2262,14 @@ 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 || {};
+ // Capped to the same GROUP_MAX. Uncapped, this repeated most of what the sections above
+ // already show and pushed them off the top of a card that is only 300px tall.
+ const recent = rows.slice()
+ .sort((a, b) => (b.updated || b.ts || 0) - (a.updated || a.ts || 0))
+ .slice(0, GROUP_MAX);
box.innerHTML = groupHtml()
- + '
' + rows.map(c => rowHtml(c, P)).join('') + '
';
+ + (GROUPED ? recentBanner(recent.length) : '')
+ + '
' + recent.map(c => rowHtml(c, P)).join('') + '
';
}
function load() {
@@ -2241,8 +2282,9 @@ vv_ai_profiles_script();
// 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 (_) {}
+ const k = grp.dataset.grp;
+ openSet[k] = !openSet[k];
+ try { localStorage.setItem(OPEN_KEY, JSON.stringify(openSet)); } catch (_) {}
render();
return;
}
diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php
index 8364c15..d774a03 100644
--- a/Plugin/unraid/pages/monitor.php
+++ b/Plugin/unraid/pages/monitor.php
@@ -2870,8 +2870,7 @@ if (document.getElementById('vv-mon-ai-chat')) {
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.
+ // Pinned to General Chat, so that section is the one open on arrival; the rest collapse.
groupProfile: 'chat',
groupMax: 10,
});