Put the assistant on the dashboard, where the questions actually occur to you
This commit is contained in:
@@ -29,7 +29,15 @@
|
||||
//
|
||||
// Missing subsystems simply do not render.
|
||||
// No GPU, no UPS, no VMs — the corresponding card is absent rather than showing zeros
|
||||
// or an error. The page is built to be correct on hardware lacking any given part.
|
||||
// or an error. The page is built to be correct on hardware lacking any given part. The
|
||||
// AI row is the same rule applied to a subsystem rather than a device: it exists only
|
||||
// where vv_ai_ui_on() is true, which is the AI host with AI_ENABLED set.
|
||||
//
|
||||
// The assistant here starts on General Chat, where the AI tab starts on Varaverk Assistant.
|
||||
// Different jobs. The tab is where you go to interrogate the installation; this is the
|
||||
// box you type an idle question into while watching the dashboard. The worker escalates
|
||||
// anything genuinely about this install to the strict profile on its own, so starting
|
||||
// loose costs nothing and starting strict would refuse ordinary questions.
|
||||
//
|
||||
// OPERATIONAL SAFEGUARDS
|
||||
// The health roll-up must not default to healthy.
|
||||
@@ -63,17 +71,22 @@
|
||||
// RENDERS
|
||||
// System header, CPU per core, memory breakdown, GPU cards, storage pools and array disks,
|
||||
// network, UPS, VMs, containers, transcode sessions, media now-playing, watchdog summary,
|
||||
// partner node cards
|
||||
// partner node cards, and — on the AI host only — model residency, saved conversations and
|
||||
// an assistant
|
||||
//
|
||||
// DEPENDS ON
|
||||
// include/monitor.php required directly for initial render
|
||||
// include/ai_chat.php the AI row's chat and conversation list, shared with the AI tab
|
||||
// api/monitor.php full payload, slower cycle
|
||||
// api/monitor_fast.php fast-moving values, 1s
|
||||
// api/media.php now-playing sessions
|
||||
// api/docker_action.php container actions
|
||||
// api/flag_toggle.php toggles
|
||||
// api/ai.php the AI row's turns and conversation store
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||
require_once dirname(__DIR__) . '/include/monitor.php';
|
||||
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||
if (vv_ai_ui_on()) vv_ai_chat_assets();
|
||||
?>
|
||||
<style>
|
||||
@keyframes vvRsPulse {
|
||||
@@ -279,6 +292,59 @@ require_once dirname(__DIR__) . '/include/monitor.php';
|
||||
<div id="vv-array-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<?php if (vv_ai_ui_on()): ?>
|
||||
<!-- Row 5: AI residency | Saved conversations | Assistant -->
|
||||
<!-- Present only on the AI host with AI_ENABLED true, on the same footing as the GPU and UPS
|
||||
cards above: a subsystem that is not here does not render an empty card explaining that
|
||||
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;">
|
||||
<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>
|
||||
AI
|
||||
</span>
|
||||
<a href="/plugins/varaverk/Varaverk.page?tab=ai" class="vv-card-cog" title="AI tab">⚙</a>
|
||||
</h3>
|
||||
<div id="vv-ai-stats-body">Loading...</div>
|
||||
</div>
|
||||
|
||||
<div class="vv-card" id="vv-ai-chats-card" style="grid-column:span 2;">
|
||||
<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>
|
||||
Conversations
|
||||
</span>
|
||||
</h3>
|
||||
<?php vv_ai_chat_list_markup('vv-mon-ai'); ?>
|
||||
</div>
|
||||
|
||||
<!-- 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;">
|
||||
<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>
|
||||
Assistant
|
||||
</span>
|
||||
</h3>
|
||||
<?php
|
||||
// Starts on General Chat, unlike the AI tab. This is the box you type an idle question
|
||||
// into while watching the dashboard, and the worker escalates anything about this install
|
||||
// to the assistant on its own — so landing on the strict profile here would refuse
|
||||
// ordinary questions to guard against a mistake the server already prevents.
|
||||
vv_ai_chat_markup('vv-mon-ai', [
|
||||
'profile' => 'chat',
|
||||
'compact' => true,
|
||||
'height' => '300px',
|
||||
'empty' => 'Ask anything. Questions about this installation are handed to the '
|
||||
. 'Varaverk assistant automatically.',
|
||||
'placeholder' => 'Ask the assistant…',
|
||||
]);
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1817,10 +1883,63 @@ function vvPollMonitor(live) {
|
||||
dfData.vms = d.vms ?? { available: false, vms: [] };
|
||||
vvRenderDockerFolders(dfData);
|
||||
|
||||
// ── AI ──────────────────────────────────────────────────────────────────
|
||||
if (d.ai) vvRenderAi(d.ai);
|
||||
|
||||
})
|
||||
.catch(vvPollFailed);
|
||||
}
|
||||
|
||||
// ── AI residency card ────────────────────────────────────────────────────────
|
||||
// Leads with offload, not with size. 100% on this card is the difference between ~62 tok/s and
|
||||
// roughly a quarter of that, and nothing else in the WebGUI surfaces it — a model that has
|
||||
// quietly fallen back to partial CPU offload is otherwise invisible until answers feel slow.
|
||||
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 line = (label, value, cls) =>
|
||||
`<div style="display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin-bottom:6px;">`
|
||||
+ `<span style="font-size:11px;color:#666;">${vvEscHtml(label)}</span>`
|
||||
+ `<span style="font-size:11px;font-family:monospace;${cls || 'color:#999;'}">${vvEscHtml(value)}</span></div>`;
|
||||
|
||||
let h = '';
|
||||
if (!rt.reachable) {
|
||||
h += line('Ollama', 'unreachable', 'color:#e57;');
|
||||
} else if (!rt.loaded) {
|
||||
h += line('Model', 'not loaded', 'color:#ffb74d;');
|
||||
h += `<div style="font-size:10px;color:#444;margin:-2px 0 7px;">loads on first question</div>`;
|
||||
} else {
|
||||
const p = rt.offload_pct;
|
||||
const full = p === 100;
|
||||
h += line('GPU offload', p === null ? '—' : p + '%', full ? 'color:#6fcf97;' : 'color:#ffb74d;');
|
||||
h += `<div style="font-size:10px;color:#444;margin:-2px 0 7px;">`
|
||||
+ (full ? 'all on GPU' : 'layers on CPU — slow') + `</div>`;
|
||||
if (rt.context) h += line('Context', rt.context.toLocaleString());
|
||||
}
|
||||
|
||||
if (rt.gpu) {
|
||||
h += line('VRAM', (rt.gpu.mem_used/1024).toFixed(1) + '/' + (rt.gpu.mem_total/1024).toFixed(1) + ' GB');
|
||||
h += line('GPU util', rt.gpu.util + '%');
|
||||
}
|
||||
|
||||
// Staleness is stated, not implied. An index older than the newest tracked file answers
|
||||
// confidently out of code that has since changed — the one failure a grounded answer cannot
|
||||
// reveal on its own.
|
||||
if (!ix.exists) {
|
||||
h += line('Index', 'not built', 'color:#e57;');
|
||||
} else {
|
||||
h += line('Index', ix.chunks.toLocaleString() + ' chunks',
|
||||
ix.stale ? 'color:#ffb74d;' : 'color:#999;');
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -2481,4 +2600,26 @@ document.addEventListener('click', () => {
|
||||
// on this page ever called the function — there are no such buttons, on this or any other tab.
|
||||
// Removed rather than left as an unreachable handler for the platform's three most destructive
|
||||
// operations. The endpoint stays; see its header for why it is kept unwired.
|
||||
|
||||
// ── AI row ───────────────────────────────────────────────────────────────────
|
||||
// Constructed only when the row rendered. The card markup is behind vv_ai_ui_on(), so on any
|
||||
// other host these ids do not exist and the factories are never called — the row is absent
|
||||
// rather than broken.
|
||||
//
|
||||
// Same store as the AI tab, so a conversation started here is the one you carry on there. The
|
||||
// instance keys itself on its prefix and tears down any predecessor, which matters on this tab
|
||||
// specifically: Unraid swaps tab content by AJAX without unloading the previous page's script,
|
||||
// and this page already has three poll loops that survive that.
|
||||
if (document.getElementById('vv-mon-ai-chat')) {
|
||||
let vvMonChatList = null;
|
||||
const vvMonChat = VvAiChat({
|
||||
prefix: 'vv-mon-ai',
|
||||
profile: 'chat',
|
||||
empty: 'Ask anything. Questions about this installation are handed to the Varaverk '
|
||||
+ 'assistant automatically.',
|
||||
onChats: id => { if (vvMonChatList) vvMonChatList.setActive(id); },
|
||||
});
|
||||
vvMonChatList = VvAiChatList({ into: 'vv-mon-ai-chats', chat: vvMonChat });
|
||||
vvMonChatList.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user