Give the token ledger its own card on the dashboard

The AI card answers whether the model is healthy now; what it has spent over a month was a
different question crowding it out one line at a time.
This commit is contained in:
Gmer4Lfe
2026-08-09 11:00:00 -04:00
parent 33e696071e
commit 1afeaf0e15
4 changed files with 226 additions and 48 deletions
+85 -25
View File
@@ -237,29 +237,42 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
align-items: start;
}
/* Conversations takes its height from the Assistant rather than setting its own. align-self
overrides the row's align-items:start for this one card, so it fills the track the Assistant
defines — level at rest, and level again after expanding, since the track grows with it.
/* Explicit placement, two rows. The left column carries AI over Tokens; Conversations and the
Assistant span both rows, so the Assistant sets the height and the two stacked cards divide
what it sets. This has to be written as row+column rather than the spans that were inline on
the markup: a span says how wide, never which row, and auto-placement would drop Tokens into
the first gap it found rather than under AI. */
#vv-ai-stats-card { grid-column: 1 / span 1; grid-row: 1; }
#vv-ai-tokens-card { grid-column: 1 / span 1; grid-row: 2; }
#vv-ai-chats-card { grid-column: 2 / span 2; grid-row: 1 / span 2; }
#vv-ai-assistant-card { grid-column: 4 / span 5; grid-row: 1 / span 2; }
It needs its own scroll to do that, which it did not need inside #vv-monitor: there it
inherited a cap from `.vv-card > div { flex:1; overflow-y:auto }`. Out here the list is
content-driven, so an install with forty saved chats would render forty rows and become the
tallest card itself — setting the row height instead of following it, which is the inverse of
what this rule is for. The h3 pins and the list takes what is left, the same shape the monitor
grid above uses. */
#vv-monitor-ai #vv-ai-chats-card {
/* Conversations and Tokens take their height from the Assistant rather than setting their own.
align-self overrides the row's align-items:start for these two, so they fill the track the
Assistant defines — level at rest, and level again after expanding, since the track grows
with it.
They need their own scroll to do that, which Conversations did not need inside #vv-monitor:
there it inherited a cap from `.vv-card > div { flex:1; overflow-y:auto }`. Out here both are
content-driven, so forty saved chats — or a ledger with both hosts and a footer — would make
the card the tallest thing in its column and set the height it is supposed to be following.
The h3 pins and the body takes what is left, the same shape the monitor grid above uses. */
#vv-monitor-ai #vv-ai-chats-card,
#vv-monitor-ai #vv-ai-tokens-card {
align-self: stretch;
display: flex;
flex-direction: column;
overflow: hidden;
}
#vv-monitor-ai #vv-ai-chats-card h3 { flex-shrink: 0; }
/* height:0 with flex:1 is the part that makes this follow rather than lead. flex-basis is
already 0, so it changes nothing about the layout; what it changes is the height the list
#vv-monitor-ai #vv-ai-chats-card h3,
#vv-monitor-ai #vv-ai-tokens-card h3 { flex-shrink: 0; }
/* height:0 with flex:1 is the part that makes these follow rather than lead. flex-basis is
already 0, so it changes nothing about the layout; what it changes is the height the body
reports upward. Without it the card can still hand the auto-sized grid track a max-content
height of the whole list, and forty conversations would set the row that the Assistant is
supposed to set. It grows back to fill the track through flex, then scrolls. */
#vv-monitor-ai #vv-ai-chats-card > div {
height of its whole contents, and the track it was meant to fit inside grows to fit it
instead. It grows back to fill the track through flex, then scrolls. */
#vv-monitor-ai #vv-ai-chats-card > div,
#vv-monitor-ai #vv-ai-tokens-card > div {
flex: 1;
height: 0;
min-height: 0;
@@ -267,15 +280,57 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
-ms-overflow-style: none;
scrollbar-width: none;
}
#vv-monitor-ai #vv-ai-chats-card > div::-webkit-scrollbar { display: none; }
#vv-monitor-ai #vv-ai-chats-card > div::-webkit-scrollbar,
#vv-monitor-ai #vv-ai-tokens-card > div::-webkit-scrollbar { display: none; }
/* ── AI token ledger — shared by the AI tab and the Monitor card ──────────────
Moved out of pages/ai.php so both surfaces can draw the same rows. A tab's inline <style> is
only present while that tab is, and the Monitor renders this ledger too. The AI panel's own
chrome (.vv-ai-tok, the two-column split) stayed behind — it is that panel's frame, not part
of the ledger. */
.vv-ai-hostrow { display:flex; align-items:center; gap:8px; font-size:11px; padding:4px 6px;
border-radius:4px; cursor:pointer; border:1px solid transparent; }
.vv-ai-hostrow:hover { background:#141414; }
.vv-ai-hostrow.active { background:#14202c; border-color:#2d4a6a; }
.vv-ai-hostrow-n { color:#8a8a8a; }
.vv-ai-hostrow-h { color:#4a4a4a; font-family:monospace; font-size:10px; }
.vv-ai-hostrow-v { margin-left:auto; color:#c8c8c8; font-family:monospace; font-size:11px; flex-shrink:0; }
.vv-ai-hostrow-v.none { color:#3a3a3a; font-style:italic; font-family:inherit; font-size:10px; }
.vv-ai-tok-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:12px; }
.vv-ai-tok-l { font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:#4a4a4a; }
.vv-ai-tok-v { font-size:17px; font-weight:bold; color:#c8c8c8; font-family:monospace; line-height:1.3; }
.vv-ai-tok-s { font-size:10px; color:#5a5a5a; }
.vv-ai-tok-foot { margin-top:9px; padding-top:7px; border-top:1px solid #1a1a1a; font-size:10px;
color:#4a4a4a; display:flex; gap:14px; flex-wrap:wrap; }
/* The ledger in a 1-of-8 column. Every rule here is the same content given a sixth of the width
it has on the AI tab: the tiles stop sitting side by side, the host id drops under its name
instead of competing with the total for one line, and the footer stacks. */
/* Name and total keep the first line, the host id takes the second on its own. Ordered rather
than reordered in the markup, because the AI tab wants all three on one line and this is the
same HTML. flex-basis:100% on the id is what forces the break; without the explicit order the
total would follow it onto a third line instead of staying beside the name. */
#vv-ai-tokens-card .vv-ai-hostrow { flex-wrap: wrap; gap: 1px 6px; padding: 4px 5px; }
#vv-ai-tokens-card .vv-ai-hostrow-v { order: 1; }
#vv-ai-tokens-card .vv-ai-hostrow-h { order: 2; flex-basis: 100%; }
#vv-ai-tokens-card .vv-ai-tok-grid { grid-template-columns: 1fr; gap: 9px; }
#vv-ai-tokens-card .vv-ai-tok-v { font-size: 15px; }
#vv-ai-tokens-card .vv-ai-tok-foot { flex-direction: column; gap: 3px; }
/* Section headings inside the card. .vv-ai-tok-l is already the 9px uppercase label this wants,
so it is reused rather than restated — with room above it to separate the two halves. */
#vv-ai-tokens-card .vv-ai-tok-head { margin: 11px 0 5px; }
#vv-ai-tokens-card .vv-ai-tok-head:first-child { margin-top: 0; }
/* Medium width — 4 columns, mirroring #vv-monitor's own breakpoint. The 1/2/5 spans do not
survive the narrower grid: 5 of 4 would silently overflow the track. */
@media (max-width: 1024px) {
#vv-monitor-ai { grid-template-columns: repeat(4, 1fr) !important; }
#vv-ai-stats-card { grid-column: span 1 !important; }
#vv-ai-chats-card { grid-column: span 3 !important; }
#vv-ai-assistant-card { grid-column: span 4 !important; }
#vv-ai-stats-card { grid-column: 1 / span 1 !important; grid-row: 1 !important; }
#vv-ai-tokens-card { grid-column: 1 / span 1 !important; grid-row: 2 !important; }
#vv-ai-chats-card { grid-column: 2 / span 3 !important; grid-row: 1 / span 2 !important; }
/* The Assistant drops to its own full-width row here. Five columns of eight is a readable
transcript; three of four beside a list is not, and it is the card you sit and read. */
#vv-ai-assistant-card { grid-column: 1 / span 4 !important; grid-row: 3 !important; }
}
/* Phone — single column, natural heights, page scrolls. Matching heights is meaningless once
@@ -283,11 +338,16 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
a scroll region inside it is a trap for a thumb. */
@media (max-width: 480px) {
#vv-monitor-ai { grid-template-columns: 1fr !important; }
#vv-monitor-ai > .vv-card { grid-column: 1 / -1 !important; }
#vv-monitor-ai #vv-ai-chats-card { display: block; overflow: visible; }
/* height:0 is only inert while this is a flex item — as a plain block it would collapse the
list to nothing, so it goes back with the flex context that justified it. */
#vv-monitor-ai #vv-ai-chats-card > div { height: auto; min-height: auto; overflow-y: visible; }
/* grid-row has to be released along with grid-column. The explicit rows above are the whole
reason Tokens sits under AI, and left in place on a single-column grid they would stack
cards into the same cell on top of each other rather than in sequence. */
#vv-monitor-ai > .vv-card { grid-column: 1 / -1 !important; grid-row: auto !important; }
#vv-monitor-ai #vv-ai-chats-card,
#vv-monitor-ai #vv-ai-tokens-card { display: block; overflow: visible; }
/* height:0 is only inert while these are flex items — as plain blocks it would collapse the
body to nothing, so it goes back with the flex context that justified it. */
#vv-monitor-ai #vv-ai-chats-card > div,
#vv-monitor-ai #vv-ai-tokens-card > div { height: auto; min-height: auto; overflow-y: visible; }
}
/* Monitor responsive — 4-column grid at medium width */
+5 -4
View File
@@ -511,15 +511,16 @@ function vv_ai_stats_cached(bool $live = false): array {
// cache write, and — worse — the dashboard and the AI tab could disagree about whether the model
// is resident, because they would have asked at different moments.
//
// Tokens are not part of vv_ai_stats(): that function's shape is the AI tab's banner contract,
// and the ledger read is 15ms, so it is fetched here rather than widening the payload everything
// else carries.
// Tokens are deliberately absent. This block once carried today's total for a single line on the
// AI card; that line is now a card of its own, which fetches the whole ledger from
// api/ai.php?action=tokens because it shows the week and all-time beside today. Leaving the field
// here would have run vv_ai_token_stats() on every cache write — once a minute, forever — and
// discarded all but one number of it that nothing reads.
function vv_ai_monitor_block(array $stats): array {
return [
'model' => $stats['model'] ?? '',
'runtime' => $stats['runtime'] ?? [],
'index' => $stats['index'] ?? [],
'tokens' => vv_ai_token_stats()['today'] ?? null,
];
}
+8 -14
View File
@@ -127,20 +127,14 @@ if (is_dir('/var/log/varaverk')) {
.vv-ai-tok { display:grid; grid-template-columns:var(--vv-tok-split,2fr 3fr); gap:1px; background:#1a1a1a;
border:1px solid #262626; border-radius:6px; overflow:hidden; }
@media (max-width:900px) { .vv-ai-tok { grid-template-columns:1fr; } }
.vv-ai-hostrow { display:flex; align-items:center; gap:8px; font-size:11px; padding:4px 6px;
border-radius:4px; cursor:pointer; border:1px solid transparent; }
.vv-ai-hostrow:hover { background:#141414; }
.vv-ai-hostrow.active { background:#14202c; border-color:#2d4a6a; }
.vv-ai-hostrow-n { color:#8a8a8a; }
.vv-ai-hostrow-h { color:#4a4a4a; font-family:monospace; font-size:10px; }
.vv-ai-hostrow-v { margin-left:auto; color:#c8c8c8; font-family:monospace; font-size:11px; flex-shrink:0; }
.vv-ai-hostrow-v.none { color:#3a3a3a; font-style:italic; font-family:inherit; font-size:10px; }
.vv-ai-tok-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:12px; }
.vv-ai-tok-l { font-size:9px; letter-spacing:.08em; text-transform:uppercase; color:#4a4a4a; }
.vv-ai-tok-v { font-size:17px; font-weight:bold; color:#c8c8c8; font-family:monospace; line-height:1.3; }
.vv-ai-tok-s { font-size:10px; color:#5a5a5a; }
.vv-ai-tok-foot { margin-top:9px; padding-top:7px; border-top:1px solid #1a1a1a; font-size:10px;
color:#4a4a4a; display:flex; gap:14px; flex-wrap:wrap; }
/* The rows and tiles this panel is built from live in css/varaverk.css, not here. The Monitor
tab renders the same ledger in a card of its own, and a style block on this page is only
loaded when this page is — which is why they had to leave. What stays above is this panel's
own chrome: the two-column split and the border around it, which the Monitor card does not
share because it is a .vv-card and already has one.
.vv-ai-hostrow* host rows, including the active state the scope click sets
.vv-ai-tok-* the Today / 7-day / All-time tiles and the footer line */
/* ── Assistant-filed bug reports ─────────────────────────────────────────── */
.vv-ai-bug { border-left:2px solid #5a3a2a; background:#140f0c; border-radius:0 3px 3px 0;
+128 -5
View File
@@ -315,7 +315,12 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
it is not here. api/ai.php refuses every action independently, so this is presentation
rather than access control. -->
<div class="vv-card" id="vv-ai-stats-card" style="grid-column:span 1;">
<!-- Placement for this row lives in css/varaverk.css, not on these cards. It is two rows
deep — AI over Tokens in the first column — and a grid-column span here cannot say which
row a card belongs to, so an inline span would only fight the stylesheet for half the
answer. -->
<div class="vv-card" id="vv-ai-stats-card">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><rect x="2.5" y="2.5" width="7" height="7" rx="1.2"/><circle cx="4.7" cy="5" r="0.7" fill="#888" stroke="none"/><circle cx="7.3" cy="5" r="0.7" fill="#888" stroke="none"/><line x1="4.5" y1="7.3" x2="7.5" y2="7.3"/><line x1="6" y1="2.5" x2="6" y2="0.8"/><line x1="2.5" y1="6" x2="0.8" y2="6"/><line x1="9.5" y1="6" x2="11.2" y2="6"/></svg></span>
@@ -326,7 +331,22 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<div id="vv-ai-stats-body">Loading...</div>
</div>
<div class="vv-card" id="vv-ai-chats-card" style="grid-column:span 2;">
<!-- The token ledger, the same one the AI tab draws, under the card whose subsystem spends
them. Its own card rather than three more lines on the AI card above: that card answers
"is the model healthy right now", and cost accumulated over a month is a different
question that was crowding it out one line at a time. -->
<div class="vv-card" id="vv-ai-tokens-card">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round"><line x1="2" y1="10.3" x2="2" y2="6.5"/><line x1="6" y1="10.3" x2="6" y2="2.4"/><line x1="10" y1="10.3" x2="10" y2="7.6"/></svg></span>
Tokens
</span>
<a href="/plugins/varaverk/Varaverk.page?tab=ai" class="vv-card-cog" title="AI tab">⚙</a>
</h3>
<div id="vv-ai-tokens-body">Loading...</div>
</div>
<div class="vv-card" id="vv-ai-chats-card">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="13" height="12" viewBox="0 0 13 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><path d="M1 2.2C1 1.5 1.5 1 2.2 1H10.8C11.5 1 12 1.5 12 2.2V6.8C12 7.5 11.5 8 10.8 8H4.5L2 10.5V8H2.2C1.5 8 1 7.5 1 6.8Z"/></svg></span>
@@ -338,7 +358,7 @@ $_vv_doc_vars = array_merge(vv_conf_vars(), ['SCRIPTS_DIR' => SCRIPTS_DIR]);
<!-- Named for what it is, not "dock" — the Scheduler tab's vv-ai-dock is a different
component and vvAiDockOn() there tests for that id by name. -->
<div class="vv-card" id="vv-ai-assistant-card" style="grid-column:span 5;">
<div class="vv-card" id="vv-ai-assistant-card">
<h3>
<span style="display:flex;align-items:center;gap:5px;">
<span class="vv-ico"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="#aaa" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="5"/><path d="M4.4 4.6C4.4 3.7 5.1 3.1 6 3.1C6.9 3.1 7.6 3.7 7.6 4.5C7.6 5.9 6 5.7 6 7"/><circle cx="6" cy="8.8" r="0.6" fill="#888" stroke="none"/></svg></span>
@@ -1939,7 +1959,7 @@ function vvPollMonitor(live) {
function vvRenderAi(ai) {
const box = document.getElementById('vv-ai-stats-body');
if (!box) return;
const rt = ai.runtime ?? {}, ix = ai.index ?? {}, tok = ai.tokens ?? null;
const rt = ai.runtime ?? {}, ix = ai.index ?? {};
const line = (label, value, cls) =>
`<div style="display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:6px;">`
@@ -1977,11 +1997,111 @@ function vvRenderAi(ai) {
if (ix.stale) h += `<div style="font-size:10px;color:#8a6a3a;margin:-2px 0 7px;">source newer — reindex</div>`;
}
if (tok && tok.turns) h += line('Today', tok.total.toLocaleString() + ' tok');
box.innerHTML = h;
}
// ── AI token ledger card ─────────────────────────────────────────────────────
// Not part of the monitor payload. The cache writer runs once a minute and this is the one card
// on the page whose numbers only move when a turn finishes, so it is fetched on its own from
// api/ai.php?action=tokens — the same endpoint and the same shape the AI tab reads, so the two
// surfaces cannot disagree about what has been spent.
let vvTokData = null, vvTokScope = 'all';
function vvTokAgo(ts) {
if (!ts) return '—';
const d = Math.floor(Date.now()/1000) - ts;
if (d < 60) return 'just now';
if (d < 3600) return Math.floor(d/60) + 'm';
if (d < 86400) return Math.floor(d/3600) + 'h';
return Math.floor(d/86400) + 'd';
}
function vvLoadTokens() {
return fetch('/plugins/varaverk/api/ai.php?action=tokens')
.then(r => r.json())
.then(d => { if (d.ok) { vvTokData = d.tokens; vvRenderTokens(); } })
.catch(() => {});
}
function vvRenderTokens() {
const box = document.getElementById('vv-ai-tokens-body');
if (!box || !vvTokData) return;
const num = n => (n || 0).toLocaleString();
const hosts = vvTokData.hosts || {};
// A host with no rows reads "not collected here", never 0. Each host writes to its own data/
// and only ai_token_sync.sh moves a ledger between them, so a zero would claim the partner did
// no work when the truth is that this host cannot see its ledger at all.
let h = `<div class="vv-ai-tok-l vv-ai-tok-head">Nodes</div>`
+ `<div class="vv-ai-hostrow${vvTokScope === 'all' ? ' active' : ''}" data-scope="all">`
+ `<span class="vv-ai-hostrow-n">All hosts</span>`
+ `<span class="vv-ai-hostrow-v">${num(vvTokData.all.total)}</span></div>`;
Object.keys(hosts).forEach(id => {
const x = hosts[id];
const tag = x.self ? ' · this host' : x.synced ? ' · synced ' + vvTokAgo(x.synced) : '';
h += `<div class="vv-ai-hostrow${vvTokScope === id ? ' active' : ''}" data-scope="${vvEscAttr(id)}">`
+ `<span class="vv-ai-hostrow-n">${vvEscHtml(x.name)}</span>`
+ `<span class="vv-ai-hostrow-h">${vvEscHtml(id)}${vvEscHtml(tag)}</span>`
+ (x.seen ? `<span class="vv-ai-hostrow-v">${num(x.all.total)}</span>`
: `<span class="vv-ai-hostrow-v none">not collected here</span>`)
+ `</div>`;
});
const sel = vvTokScope === 'all' ? vvTokData : hosts[vvTokScope];
const scope = vvTokScope === 'all' ? 'all hosts'
: (hosts[vvTokScope] ? hosts[vvTokScope].name : vvTokScope);
h += `<div class="vv-ai-tok-l vv-ai-tok-head">Token usage — ${vvEscHtml(scope)}</div>`;
if (!sel || !sel.all.turns) {
h += vvTokScope === 'all'
? `<div class="vv-ai-none">no turns recorded yet</div>`
: `<div class="vv-ai-none">no rows from this host in the local ledger</div>`;
box.innerHTML = h;
return;
}
const cell = (label, b) =>
`<div><div class="vv-ai-tok-l">${label}</div>`
+ `<div class="vv-ai-tok-v">${num(b.total)}</div>`
+ `<div class="vv-ai-tok-s">${num(b.turns)} turn${b.turns === 1 ? '' : 's'}`
+ ` · ${num(b.prompt)} in / ${num(b.completion)} out</div></div>`;
h += `<div class="vv-ai-tok-grid">`
+ cell('Today', sel.today) + cell('Last 7 days', sel.week) + cell('All time', sel.all)
+ `</div>`;
// Profile and source splits describe the whole ledger, so they appear only under the all-hosts
// scope rather than sitting beneath a host heading they do not describe.
const foot = [];
if (vvTokData.first) foot.push(`since ${vvEscHtml(vvTokData.first)} · ${vvTokData.days} day${vvTokData.days === 1 ? '' : 's'}`);
if (vvTokData.best_tok_s) foot.push(`best ${vvEscHtml(vvTokData.best_tok_s)} tok/s`);
if (vvTokScope === 'all') {
const by = o => Object.keys(o || {}).map(k => `${vvEscHtml(k)} ${num(o[k])}`).join(' · ');
if (Object.keys(vvTokData.profiles || {}).length) foot.push('profile: ' + by(vvTokData.profiles));
if (Object.keys(vvTokData.sources || {}).length) foot.push('source: ' + by(vvTokData.sources));
}
if (foot.length) h += `<div class="vv-ai-tok-foot">`
+ foot.map(f => `<span>${f}</span>`).join('') + `</div>`;
box.innerHTML = h;
}
// Delegated on the container, which survives every render — the rows inside do not, and binding
// per row would rebind the whole list on each poll to no purpose.
if (document.getElementById('vv-ai-tokens-body')) {
document.getElementById('vv-ai-tokens-body').addEventListener('click', e => {
const row = e.target.closest('.vv-ai-hostrow');
if (!row) return;
vvTokScope = row.dataset.scope;
vvRenderTokens(); // scope is a view of data already held; no refetch
});
// 60s, not the 5s the dashboard polls at. The ledger only moves when a turn completes, and a
// turn completing here refreshes the card directly — this interval exists for turns taken on
// the AI tab and for the partner ledger that ai_token_sync.sh pulls every two hours.
vvPollRunner(vvLoadTokens, 60000);
}
// 5s against a payload the cache writer refreshes once a minute. Polling faster cannot make the
// data newer — it only decides how soon the page notices the writer's update.
vvPollRunner(vvPollMonitor, 5000);
@@ -2696,6 +2816,9 @@ if (document.getElementById('vv-mon-ai-chat')) {
empty: 'Ask anything. Questions about this installation are handed to the Varaverk '
+ 'assistant automatically.',
onChats: id => { if (vvMonChatList) vvMonChatList.setActive(id); },
// The turn that just finished is the one spend you watched happen. Waiting up to a minute
// for the poll to reflect it reads as the card being broken.
onTurn: () => vvLoadTokens(),
});
vvMonChatList = VvAiChatList({ into: 'vv-mon-ai-chats', chat: vvMonChat });
vvMonChatList.reload();