Lumi/plugins/lumi_ai/views/settings.ejs
2026-06-11 06:35:43 +02:00

220 lines
15 KiB
Plaintext

<%- include("../../../src/web/views/partials/layout-top", { title }) %>
<link rel="stylesheet" href="/plugins/lumi_ai/assets/settings.css?v=<%= assetVersion %>" />
<section class="ai-titlebar">
<div>
<h1>Lumi AI</h1>
<p>Managed local inference, assistant access, and guarded plugin tools.</p>
</div>
<div class="ai-runtime-badge <%= runtimeStatus.healthy ? 'ready' : 'offline' %>">
<span></span><%= runtimeStatus.healthy ? "Runtime ready" : "Runtime offline" %>
</div>
</section>
<nav class="ai-tabs" aria-label="Lumi AI settings">
<a href="#overview">Overview</a>
<a href="#models">Models</a>
<a href="#runtime">Runtime</a>
<a href="#assistant">Assistant</a>
<a href="#metrics">Metrics</a>
</nav>
<section class="ai-band" id="overview">
<div class="ai-section-heading">
<div><h2>Overview</h2><p>Current installation and host capacity.</p></div>
</div>
<div class="ai-stat-grid">
<div><span>Provider</span><strong>llama.cpp</strong></div>
<div><span>Selected model</span><strong><%= models.find((model) => model.id === config.selected_model_id)?.label || config.selected_model_id %></strong></div>
<div><span>RAM</span><strong><%= Math.round(hardware.total_ram_mb / 1024) %> GB</strong></div>
<div><span>Free disk</span><strong><%= formatBytes(hardware.free_disk_mb * 1048576) %></strong></div>
<div><span>CPU threads</span><strong><%= hardware.cpu_threads %></strong></div>
<div><span>GPU</span><strong><%= hardware.gpu.present ? hardware.gpu.name : "Not detected" %></strong></div>
</div>
</section>
<section class="ai-band" id="models">
<div class="ai-section-heading">
<div><h2>Models</h2><p>Pinned GGUF files downloaded directly from Hugging Face and verified by SHA-256.</p></div>
</div>
<div class="ai-model-list">
<% models.forEach((model) => { %>
<article class="ai-model-row">
<div class="ai-model-main">
<strong><%= model.label %></strong>
<span><%= formatBytes(model.size) %> &middot; <%= model.ram_gb %> GB recommended RAM &middot; <%= model.repo %></span>
</div>
<span class="ai-tag <%= model.downloaded ? 'installed' : model.compatible ? '' : 'warning' %>">
<%= model.downloaded ? "Installed" : model.compatible ? "Available" : "Exceeds host" %>
</span>
<% if (!model.downloaded) { %>
<form method="post" action="/plugins/lumi_ai/download/model/<%= model.id %>" class="ai-inline-form">
<% if (!model.compatible) { %>
<label title="Allow download despite detected capacity"><input type="checkbox" name="override_compatibility" /> Override</label>
<% } %>
<button class="button subtle" type="submit">Download</button>
</form>
<% } %>
</article>
<% }) %>
</div>
</section>
<section class="ai-band" id="runtime">
<div class="ai-section-heading">
<div><h2>Runtime</h2><p>Official llama.cpp release, bound to localhost and stored inside this plugin.</p></div>
<div class="ai-actions" data-ai-runtime-actions>
<button class="button" type="button" data-runtime-action="start">Start</button>
<button class="button subtle" type="button" data-runtime-action="self-test">Run self-test</button>
<button class="button subtle" type="button" data-runtime-action="verify-runtime">Verify runtime</button>
<button class="button subtle" type="button" data-runtime-action="verify-model">Verify model</button>
<button class="button subtle" type="button" data-runtime-action="restart">Restart</button>
<button class="button danger" type="button" data-runtime-action="stop">Stop</button>
</div>
</div>
<div class="ai-runtime-grid">
<div class="ai-diagnostic">
<span>Installed</span><strong><%= runtimeStatus.runtime_installed ? "Yes" : "No" %></strong>
<span>Process</span><strong data-runtime-state><%= runtimeStatus.state %></strong>
<span>Health</span><strong><%= runtimeStatus.healthy ? "Healthy" : "Unavailable" %></strong>
<span>PID</span><strong><%= runtimeStatus.pid || "None" %></strong>
<span>Last stop</span><strong><%= runtimeState.last_stop_reason %></strong>
<span>Platform</span><strong><%= hardware.platform %>-<%= hardware.architecture %></strong>
<span>Self-test</span><strong><%= runtimeStatus.last_self_test?.success ? "Passed" : runtimeStatus.last_self_test ? "Failed" : "Not run" %></strong>
<span>Runtime folder</span><strong><%= formatBytes(runtimeFolderSize) %></strong>
<span>Runtime archive</span><strong><%= runtimeTarget ? formatBytes(runtimeTarget.size) : "Unavailable" %></strong>
<span>Model installed</span><strong><%= formatBytes(modelFileSize) %></strong>
<span>Model download</span><strong><%= formatBytes(models.find((model) => model.id === config.selected_model_id)?.size || 0) %></strong>
</div>
<div>
<% if (runtimeTarget) { %>
<p><strong>Managed release <%= runtimeManifest?.version || "b9592" %></strong></p>
<p class="hint"><%= runtimeTarget.filename %> &middot; <%= formatBytes(runtimeTarget.size) %></p>
<form method="post" action="/plugins/lumi_ai/download/runtime">
<button class="button subtle" type="submit"><%= runtimeStatus.runtime_installed ? "Reinstall runtime" : "Download runtime" %></button>
</form>
<% } else { %>
<div class="callout">No managed runtime build is available for this OS and architecture.</div>
<% } %>
<% if (runtimeStatus.last_error) { %><div class="callout danger"><%= runtimeStatus.last_error %></div><% } %>
</div>
</div>
<div class="ai-download-status" data-download-status hidden></div>
</section>
<section class="ai-band" id="runtime-diagnostics">
<div class="ai-section-heading">
<div><h2>Runtime diagnostics</h2><p>Latest plugin-local runtime failure and remediation details.</p></div>
<a class="button subtle" href="/plugins/lumi_ai/diagnostics/download">Download diagnostics</a>
</div>
<% if (latestDiagnostic) { %>
<div class="callout danger">
<strong><%= latestDiagnostic.code %>: <%= latestDiagnostic.message %></strong>
<p><%= latestDiagnostic.category %> / <%= latestDiagnostic.severity %></p>
</div>
<% if (latestDiagnostic.remediation_steps?.length) { %>
<ol class="ai-remediation"><% latestDiagnostic.remediation_steps.forEach((step) => { %><li><%= step %></li><% }) %></ol>
<% } %>
<details class="ai-raw-diagnostic">
<summary>Raw diagnostic details</summary>
<pre><%= JSON.stringify(latestDiagnostic, null, 2) %></pre>
</details>
<% } else { %>
<p class="hint">No runtime diagnostic has been recorded.</p>
<% } %>
<% if (hardware.network_path_warning) { %>
<div class="callout">The plugin path may be a mapped or network-like location. A local disk path is more reliable for native runtime DLL loading.</div>
<% } %>
<% if (hardware.long_path_warning) { %><div class="callout">The plugin path is unusually long for Windows native loading. Consider a shorter local installation path.</div><% } %>
</section>
<form method="post" action="/plugins/lumi_ai/settings">
<section class="ai-band" id="assistant">
<div class="ai-section-heading">
<div><h2>Assistant</h2><p>Configuration remains admin-only. Visibility controls only the sidebar assistant.</p></div>
<button class="button" type="submit">Save settings</button>
</div>
<div class="form-grid ai-form">
<div class="field">
<label>AI enabled</label>
<label class="switch"><input class="switch-input" type="checkbox" name="enabled" <%= config.enabled ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Available</span></label>
</div>
<div class="field">
<label for="selected-model">Selected model</label>
<select id="selected-model" name="selected_model_id"><% models.forEach((model) => { %><option value="<%= model.id %>" <%= model.id === config.selected_model_id ? "selected" : "" %>><%= model.label %></option><% }) %></select>
</div>
<div class="field"><label>Context size</label><input type="number" name="context_size" min="512" max="131072" value="<%= config.context_size %>" /></div>
<div class="field"><label>CPU threads (0 = auto)</label><input type="number" name="threads" min="0" max="256" value="<%= config.threads %>" /></div>
<div class="field"><label>Concurrent requests</label><input type="number" name="concurrency" min="1" max="8" value="<%= config.concurrency %>" /></div>
<div class="field"><label>Maximum queue</label><input type="number" name="max_queue_length" min="1" max="100" value="<%= config.max_queue_length %>" /></div>
<div class="field"><label>Timeout (ms)</label><input type="number" name="request_timeout_ms" min="5000" max="600000" value="<%= config.request_timeout_ms %>" /></div>
<div class="field"><label>Requests per user/minute</label><input type="number" name="per_user_requests_per_minute" min="1" max="120" value="<%= config.per_user_requests_per_minute %>" /></div>
<div class="field"><label>Admin rate-limit bypass</label><label class="switch"><input class="switch-input" type="checkbox" name="admin_bypass_rate_limit" <%= config.admin_bypass_rate_limit ? "checked" : "" %> /><span class="switch-track"></span><span class="switch-text">Bypass</span></label></div>
<fieldset class="field full ai-fieldset">
<legend>Sidebar visibility</legend>
<label><input type="checkbox" name="visibility_admins" <%= config.assistant_visibility.admins ? "checked" : "" %> /> Administrators</label>
<label><input type="checkbox" name="visibility_mods" <%= config.assistant_visibility.mods ? "checked" : "" %> /> Moderators</label>
<label><input type="checkbox" name="visibility_users" <%= config.assistant_visibility.users ? "checked" : "" %> /> Users</label>
</fieldset>
<div class="field full"><label>Identity</label><textarea name="identity" rows="2"><%= config.instructions.identity %></textarea></div>
<div class="field full"><label>Response style</label><textarea name="style" rows="2"><%= config.instructions.style %></textarea></div>
<div class="field full"><label>Allowed topics</label><textarea name="allowed_topics" rows="2"><%= config.instructions.allowed_topics %></textarea></div>
<div class="field full"><label>Out-of-scope response</label><textarea name="out_of_scope_response" rows="2"><%= config.instructions.out_of_scope_response %></textarea></div>
<div class="field"><label>Maximum answer length</label><input type="number" name="maximum_answer_length" min="100" max="4000" value="<%= config.instructions.maximum_answer_length %>" /></div>
<div class="field"><label>Roleplay intensity (0-10)</label><input type="number" name="roleplay_intensity" min="0" max="10" value="<%= config.instructions.roleplay_intensity || 0 %>" /></div>
<div class="field full"><label>Community tone</label><textarea name="community_tone" rows="2"><%= config.instructions.community_tone %></textarea></div>
<div class="field full"><label>Admin custom instructions</label><textarea name="admin_custom" rows="4"><%= config.instructions.admin_custom %></textarea><span class="hint">Hard scope, role, tool, and confirmation rules cannot be overridden.</span></div>
<fieldset class="field full ai-fieldset">
<legend>Logging</legend>
<% [["log_prompts","Prompts"],["log_responses","Responses"],["log_tool_calls","Tool calls"],["log_metrics","Metrics"],["log_internal_audit","Internal audit"]].forEach(([key,label]) => { %>
<label><input type="checkbox" name="<%= key %>" <%= config.logging[key] ? "checked" : "" %> /> <%= label %></label>
<% }) %>
</fieldset>
</div>
</section>
</form>
<section class="ai-band" id="test-console">
<div class="ai-section-heading"><div><h2>Test console</h2><p>Run a request as a simulated role without changing the logged-in actor.</p></div></div>
<form class="form-grid ai-form" data-ai-test-form>
<div class="field"><label>Simulated role</label><select name="role"><option value="admin">Admin</option><option value="mod">Moderator</option><option value="user">User</option></select></div>
<div class="field full"><label>Message</label><textarea name="message" rows="3" required>Where can I find Twitch configuration?</textarea></div>
<div class="field full ai-fieldset">
<label><input type="checkbox" name="show_raw_prompt" /> Show assembled prompt</label>
<label><input type="checkbox" name="show_raw_output" /> Show raw model response</label>
</div>
<div class="field full"><button class="button" type="submit">Run test</button></div>
</form>
<pre class="ai-test-output" data-ai-test-output hidden></pre>
</section>
<section class="ai-band" id="metrics">
<div class="ai-section-heading"><div><h2>Metrics</h2><p>Plugin-local operational counters and recent requests.</p></div></div>
<div class="ai-stat-grid compact">
<div><span>Requests</span><strong><%= metrics.total_requests %></strong></div>
<div><span>Successful</span><strong><%= metrics.successful %></strong></div>
<div><span>Failed</span><strong><%= metrics.failed %></strong></div>
<div><span>Refused</span><strong><%= metrics.refusals %></strong></div>
<div><span>Average</span><strong><%= formatDuration(metrics.average_response_ms) %></strong></div>
<div><span>Median</span><strong><%= formatDuration(metrics.median_response_ms) %></strong></div>
</div>
<div class="table-wrap">
<table class="table"><thead><tr><th>Time</th><th>Kind</th><th>Status</th><th>Role</th><th>Duration</th></tr></thead><tbody>
<% history.forEach((entry) => { %><tr><td><%= entry.timestamp %></td><td><%= entry.kind %></td><td><%= entry.status %></td><td><%= entry.role || "-" %></td><td><%= formatDuration(entry.duration_ms) %></td></tr><% }) %>
<% if (!history.length) { %><tr><td colspan="5">No requests recorded.</td></tr><% } %>
</tbody></table>
</div>
<% if (logFiles.length) { %><p class="hint">Runtime logs: <%= logFiles.map((file) => `${file.name} (${formatBytes(file.size)})`).join(", ") %></p><% } %>
</section>
<section class="ai-band">
<div class="ai-section-heading"><div><h2>Privacy and troubleshooting</h2><p>Local inference remains on this host.</p></div></div>
<div class="callout">
Models are downloaded from pinned Hugging Face revisions. The managed runtime is downloaded from the official llama.cpp release and verified by SHA-256. No cloud inference is used. Prompt and response logging are off by default.
</div>
<p class="hint">If startup fails, confirm that the runtime and selected model show as installed, the plugin directory is writable, and enough RAM and disk are available. Runtime logs are stored under <code>plugins/lumi_ai/data/logs/</code>.</p>
</section>
<script src="/plugins/lumi_ai/assets/settings.js?v=<%= assetVersion %>" defer></script>
<%- include("../../../src/web/views/partials/layout-bottom") %>