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:
@@ -237,29 +237,42 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Conversations takes its height from the Assistant rather than setting its own. align-self
|
/* Explicit placement, two rows. The left column carries AI over Tokens; Conversations and the
|
||||||
overrides the row's align-items:start for this one card, so it fills the track the Assistant
|
Assistant span both rows, so the Assistant sets the height and the two stacked cards divide
|
||||||
defines — level at rest, and level again after expanding, since the track grows with it.
|
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
|
/* Conversations and Tokens take their height from the Assistant rather than setting their own.
|
||||||
inherited a cap from `.vv-card > div { flex:1; overflow-y:auto }`. Out here the list is
|
align-self overrides the row's align-items:start for these two, so they fill the track the
|
||||||
content-driven, so an install with forty saved chats would render forty rows and become the
|
Assistant defines — level at rest, and level again after expanding, since the track grows
|
||||||
tallest card itself — setting the row height instead of following it, which is the inverse of
|
with it.
|
||||||
what this rule is for. The h3 pins and the list takes what is left, the same shape the monitor
|
|
||||||
grid above uses. */
|
They need their own scroll to do that, which Conversations did not need inside #vv-monitor:
|
||||||
#vv-monitor-ai #vv-ai-chats-card {
|
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;
|
align-self: stretch;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#vv-monitor-ai #vv-ai-chats-card h3 { flex-shrink: 0; }
|
#vv-monitor-ai #vv-ai-chats-card h3,
|
||||||
/* height:0 with flex:1 is the part that makes this follow rather than lead. flex-basis is
|
#vv-monitor-ai #vv-ai-tokens-card h3 { flex-shrink: 0; }
|
||||||
already 0, so it changes nothing about the layout; what it changes is the height the list
|
/* 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
|
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
|
height of its whole contents, and the track it was meant to fit inside grows to fit it
|
||||||
supposed to set. It grows back to fill the track through flex, then scrolls. */
|
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-chats-card > div,
|
||||||
|
#vv-monitor-ai #vv-ai-tokens-card > div {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 0;
|
height: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -267,15 +280,57 @@ body.vv-fullscreen #displaybox { padding-left: 1rem !important; padding-top: .5r
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: 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
|
/* 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. */
|
survive the narrower grid: 5 of 4 would silently overflow the track. */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
#vv-monitor-ai { grid-template-columns: repeat(4, 1fr) !important; }
|
#vv-monitor-ai { grid-template-columns: repeat(4, 1fr) !important; }
|
||||||
#vv-ai-stats-card { grid-column: span 1 !important; }
|
#vv-ai-stats-card { grid-column: 1 / span 1 !important; grid-row: 1 !important; }
|
||||||
#vv-ai-chats-card { grid-column: span 3 !important; }
|
#vv-ai-tokens-card { grid-column: 1 / span 1 !important; grid-row: 2 !important; }
|
||||||
#vv-ai-assistant-card { grid-column: span 4 !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
|
/* 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. */
|
a scroll region inside it is a trap for a thumb. */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#vv-monitor-ai { grid-template-columns: 1fr !important; }
|
#vv-monitor-ai { grid-template-columns: 1fr !important; }
|
||||||
#vv-monitor-ai > .vv-card { grid-column: 1 / -1 !important; }
|
/* grid-row has to be released along with grid-column. The explicit rows above are the whole
|
||||||
#vv-monitor-ai #vv-ai-chats-card { display: block; overflow: visible; }
|
reason Tokens sits under AI, and left in place on a single-column grid they would stack
|
||||||
/* height:0 is only inert while this is a flex item — as a plain block it would collapse the
|
cards into the same cell on top of each other rather than in sequence. */
|
||||||
list to nothing, so it goes back with the flex context that justified it. */
|
#vv-monitor-ai > .vv-card { grid-column: 1 / -1 !important; grid-row: auto !important; }
|
||||||
#vv-monitor-ai #vv-ai-chats-card > div { height: auto; min-height: auto; overflow-y: visible; }
|
#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 */
|
/* Monitor responsive — 4-column grid at medium width */
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// 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,
|
// Tokens are deliberately absent. This block once carried today's total for a single line on the
|
||||||
// and the ledger read is 15ms, so it is fetched here rather than widening the payload everything
|
// AI card; that line is now a card of its own, which fetches the whole ledger from
|
||||||
// else carries.
|
// 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 {
|
function vv_ai_monitor_block(array $stats): array {
|
||||||
return [
|
return [
|
||||||
'model' => $stats['model'] ?? '',
|
'model' => $stats['model'] ?? '',
|
||||||
'runtime' => $stats['runtime'] ?? [],
|
'runtime' => $stats['runtime'] ?? [],
|
||||||
'index' => $stats['index'] ?? [],
|
'index' => $stats['index'] ?? [],
|
||||||
'tokens' => vv_ai_token_stats()['today'] ?? null,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
.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; }
|
border:1px solid #262626; border-radius:6px; overflow:hidden; }
|
||||||
@media (max-width:900px) { .vv-ai-tok { grid-template-columns:1fr; } }
|
@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; }
|
/* The rows and tiles this panel is built from live in css/varaverk.css, not here. The Monitor
|
||||||
.vv-ai-hostrow:hover { background:#141414; }
|
tab renders the same ledger in a card of its own, and a style block on this page is only
|
||||||
.vv-ai-hostrow.active { background:#14202c; border-color:#2d4a6a; }
|
loaded when this page is — which is why they had to leave. What stays above is this panel's
|
||||||
.vv-ai-hostrow-n { color:#8a8a8a; }
|
own chrome: the two-column split and the border around it, which the Monitor card does not
|
||||||
.vv-ai-hostrow-h { color:#4a4a4a; font-family:monospace; font-size:10px; }
|
share because it is a .vv-card and already has one.
|
||||||
.vv-ai-hostrow-v { margin-left:auto; color:#c8c8c8; font-family:monospace; font-size:11px; flex-shrink:0; }
|
.vv-ai-hostrow* host rows, including the active state the scope click sets
|
||||||
.vv-ai-hostrow-v.none { color:#3a3a3a; font-style:italic; font-family:inherit; font-size:10px; }
|
.vv-ai-tok-* the Today / 7-day / All-time tiles and the footer line */
|
||||||
.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; }
|
|
||||||
|
|
||||||
/* ── Assistant-filed bug reports ─────────────────────────────────────────── */
|
/* ── Assistant-filed bug reports ─────────────────────────────────────────── */
|
||||||
.vv-ai-bug { border-left:2px solid #5a3a2a; background:#140f0c; border-radius:0 3px 3px 0;
|
.vv-ai-bug { border-left:2px solid #5a3a2a; background:#140f0c; border-radius:0 3px 3px 0;
|
||||||
|
|||||||
@@ -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
|
it is not here. api/ai.php refuses every action independently, so this is presentation
|
||||||
rather than access control. -->
|
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>
|
<h3>
|
||||||
<span style="display:flex;align-items:center;gap:5px;">
|
<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>
|
<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 id="vv-ai-stats-body">Loading...</div>
|
||||||
</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>
|
<h3>
|
||||||
<span style="display:flex;align-items:center;gap:5px;">
|
<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>
|
<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
|
<!-- 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. -->
|
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>
|
<h3>
|
||||||
<span style="display:flex;align-items:center;gap:5px;">
|
<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>
|
<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) {
|
function vvRenderAi(ai) {
|
||||||
const box = document.getElementById('vv-ai-stats-body');
|
const box = document.getElementById('vv-ai-stats-body');
|
||||||
if (!box) return;
|
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) =>
|
const line = (label, value, cls) =>
|
||||||
`<div style="display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:6px;">`
|
`<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 (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;
|
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
|
// 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.
|
// data newer — it only decides how soon the page notices the writer's update.
|
||||||
vvPollRunner(vvPollMonitor, 5000);
|
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 '
|
empty: 'Ask anything. Questions about this installation are handed to the Varaverk '
|
||||||
+ 'assistant automatically.',
|
+ 'assistant automatically.',
|
||||||
onChats: id => { if (vvMonChatList) vvMonChatList.setActive(id); },
|
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 = VvAiChatList({ into: 'vv-mon-ai-chats', chat: vvMonChat });
|
||||||
vvMonChatList.reload();
|
vvMonChatList.reload();
|
||||||
|
|||||||
Reference in New Issue
Block a user