Put the assistant on the dashboard, where the questions actually occur to you
This commit is contained in:
@@ -91,6 +91,21 @@ $t = microtime(true);
|
|||||||
// Call vv_api_data() once — result is static-cached for the rest of this process.
|
// Call vv_api_data() once — result is static-cached for the rest of this process.
|
||||||
vv_api_data();
|
vv_api_data();
|
||||||
|
|
||||||
|
// Must stay in step with api/monitor.php's own block. This file is what the Monitor tab
|
||||||
|
// normally reads — the endpoint only assembles a payload on a cache miss — so a key added there
|
||||||
|
// and not here leaves the card that consumes it loading forever on every ordinary page load,
|
||||||
|
// and working on the one request that happens to miss the cache.
|
||||||
|
$_vv_ai = null;
|
||||||
|
if (vv_ai_ui_on()) {
|
||||||
|
require_once $_base . '/include/ai.php';
|
||||||
|
$_vv_ai = [
|
||||||
|
'model' => vv_ai_config()['model'],
|
||||||
|
'runtime' => vv_ai_runtime_stats(),
|
||||||
|
'index' => vv_ai_index_stats(),
|
||||||
|
'tokens' => vv_ai_token_stats()['today'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$monitor = [
|
$monitor = [
|
||||||
'system' => vv_system_info(),
|
'system' => vv_system_info(),
|
||||||
'fallback' => vv_fallback_state(),
|
'fallback' => vv_fallback_state(),
|
||||||
@@ -118,6 +133,7 @@ $monitor = [
|
|||||||
'vms' => vv_get_vms(),
|
'vms' => vv_get_vms(),
|
||||||
'docker_folders' => vv_get_docker_folders(),
|
'docker_folders' => vv_get_docker_folders(),
|
||||||
'remote_hosts' => vv_remote_hosts_stats(),
|
'remote_hosts' => vv_remote_hosts_stats(),
|
||||||
|
'ai' => $_vv_ai,
|
||||||
'_api_status' => vv_api_get_status(),
|
'_api_status' => vv_api_get_status(),
|
||||||
'ts' => time(),
|
'ts' => time(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
// A flat object of the keys listed in the assembly below, plus _api_status and ts.
|
// A flat object of the keys listed in the assembly below, plus _api_status and ts.
|
||||||
//
|
//
|
||||||
// DEPENDS ON
|
// DEPENDS ON
|
||||||
|
// include/ai.php AI residency and index figures, only when vv_ai_ui_on()
|
||||||
// include/config.php vv_cache_read()
|
// include/config.php vv_cache_read()
|
||||||
// include/common.php raw hardware metrics — system, cpu, mem, net, gpu, disks,
|
// include/common.php raw hardware metrics — system, cpu, mem, net, gpu, disks,
|
||||||
// ups, parity, containers, transcodes, remote roll-ups
|
// ups, parity, containers, transcodes, remote roll-ups
|
||||||
@@ -92,6 +93,24 @@ require_once dirname(__DIR__) . '/include/docker_folders.php';
|
|||||||
// Pre-warm the API cache with one request (shared by all API-first functions below).
|
// Pre-warm the API cache with one request (shared by all API-first functions below).
|
||||||
vv_api_data();
|
vv_api_data();
|
||||||
|
|
||||||
|
// AI residency, for the Monitor tab's AI row. Collected here rather than by that card polling
|
||||||
|
// api/ai.php on its own cycle: vv_ai_runtime_stats() calls out to Ollama and shells nvidia-smi,
|
||||||
|
// and this payload is written once a minute by the background writer — a card polling it
|
||||||
|
// directly would pay both costs every five seconds on every open tab.
|
||||||
|
//
|
||||||
|
// Null on any host that is not the AI host or has AI_ENABLED false, which is also what makes the
|
||||||
|
// row absent rather than empty there. Same shape as every other optional subsystem on this page.
|
||||||
|
$_vv_ai = null;
|
||||||
|
if (vv_ai_ui_on()) {
|
||||||
|
require_once dirname(__DIR__) . '/include/ai.php';
|
||||||
|
$_vv_ai = [
|
||||||
|
'model' => vv_ai_config()['model'],
|
||||||
|
'runtime' => vv_ai_runtime_stats(),
|
||||||
|
'index' => vv_ai_index_stats(),
|
||||||
|
'tokens' => vv_ai_token_stats()['today'] ?? null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'system' => vv_system_info(),
|
'system' => vv_system_info(),
|
||||||
'fallback' => vv_fallback_state(),
|
'fallback' => vv_fallback_state(),
|
||||||
@@ -119,6 +138,7 @@ echo json_encode([
|
|||||||
'vms' => vv_get_vms(),
|
'vms' => vv_get_vms(),
|
||||||
'docker_folders' => vv_get_docker_folders(),
|
'docker_folders' => vv_get_docker_folders(),
|
||||||
'remote_hosts' => vv_remote_hosts_stats(),
|
'remote_hosts' => vv_remote_hosts_stats(),
|
||||||
|
'ai' => $_vv_ai,
|
||||||
'_api_status' => vv_api_get_status(),
|
'_api_status' => vv_api_get_status(),
|
||||||
'ts' => time(),
|
'ts' => time(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -29,7 +29,15 @@
|
|||||||
//
|
//
|
||||||
// Missing subsystems simply do not render.
|
// Missing subsystems simply do not render.
|
||||||
// No GPU, no UPS, no VMs — the corresponding card is absent rather than showing zeros
|
// 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
|
// OPERATIONAL SAFEGUARDS
|
||||||
// The health roll-up must not default to healthy.
|
// The health roll-up must not default to healthy.
|
||||||
@@ -63,17 +71,22 @@
|
|||||||
// RENDERS
|
// RENDERS
|
||||||
// System header, CPU per core, memory breakdown, GPU cards, storage pools and array disks,
|
// 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,
|
// 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
|
// DEPENDS ON
|
||||||
// include/monitor.php required directly for initial render
|
// 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.php full payload, slower cycle
|
||||||
// api/monitor_fast.php fast-moving values, 1s
|
// api/monitor_fast.php fast-moving values, 1s
|
||||||
// api/media.php now-playing sessions
|
// api/media.php now-playing sessions
|
||||||
// api/docker_action.php container actions
|
// api/docker_action.php container actions
|
||||||
// api/flag_toggle.php toggles
|
// 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/monitor.php';
|
||||||
|
require_once dirname(__DIR__) . '/include/ai_chat.php';
|
||||||
|
if (vv_ai_ui_on()) vv_ai_chat_assets();
|
||||||
?>
|
?>
|
||||||
<style>
|
<style>
|
||||||
@keyframes vvRsPulse {
|
@keyframes vvRsPulse {
|
||||||
@@ -279,6 +292,59 @@ require_once dirname(__DIR__) . '/include/monitor.php';
|
|||||||
<div id="vv-array-body">Loading...</div>
|
<div id="vv-array-body">Loading...</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
@@ -1817,10 +1883,63 @@ function vvPollMonitor(live) {
|
|||||||
dfData.vms = d.vms ?? { available: false, vms: [] };
|
dfData.vms = d.vms ?? { available: false, vms: [] };
|
||||||
vvRenderDockerFolders(dfData);
|
vvRenderDockerFolders(dfData);
|
||||||
|
|
||||||
|
// ── AI ──────────────────────────────────────────────────────────────────
|
||||||
|
if (d.ai) vvRenderAi(d.ai);
|
||||||
|
|
||||||
})
|
})
|
||||||
.catch(vvPollFailed);
|
.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
|
// 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);
|
||||||
@@ -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.
|
// 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
|
// 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.
|
// 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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user