Lumi/plugins/lumi_ai/public/settings.js
2026-06-14 04:18:36 +02:00

290 lines
14 KiB
JavaScript

(() => {
const actions = document.querySelector("[data-ai-runtime-actions]");
const state = document.querySelector("[data-runtime-state]");
const downloadStatus = document.querySelector("[data-download-status]");
const testForm = document.querySelector("[data-ai-test-form]");
const testOutput = document.querySelector("[data-ai-test-output]");
const testToolsNotice = document.querySelector("[data-ai-test-tools-notice]");
const gpuControl = document.querySelector("[data-gpu-control]");
const accessForm = document.querySelector("[data-ai-access-form]");
if (actions) {
actions.addEventListener("click", async (event) => {
const button = event.target.closest("[data-runtime-action]");
if (!button) return;
button.disabled = true;
try {
const response = await fetch(`/plugins/lumi_ai/runtime/${button.dataset.runtimeAction}`, { method: "POST" });
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Runtime action failed.");
if (data.state) state.textContent = data.state;
if (["self-test", "verify-runtime", "verify-model", "verify-gate-model"].includes(button.dataset.runtimeAction)) {
const labels = { "self-test": "Runtime self-test passed.", "verify-runtime": "Runtime installation verified.", "verify-model": "Model verification passed.", "verify-gate-model": "Gate model verification passed." };
window.alert(labels[button.dataset.runtimeAction]);
}
} catch (error) {
window.alert(error.message);
} finally {
button.disabled = false;
}
});
}
const pollDownloads = async () => {
if (!downloadStatus) return;
try {
const response = await fetch("/plugins/lumi_ai/api/downloads");
if (!response.ok) return;
const jobs = Object.values(await response.json());
const active = jobs.filter((job) => !["complete", "error"].includes(job.state));
if (!jobs.length) return;
downloadStatus.hidden = false;
downloadStatus.textContent = jobs.map((job) => {
const percent = job.total ? Math.floor(job.downloaded / job.total * 100) : 0;
return `${job.id}: ${job.state}${job.total ? ` ${percent}%` : ""}${job.error ? ` - ${job.error}` : ""}`;
}).join(" | ");
if (active.length) window.setTimeout(pollDownloads, 1000);
} catch {}
};
if (testForm && testOutput) {
const updateTestToolsNotice = () => {
if (!testToolsNotice) return;
const enabled = testForm.elements.allow_tools?.checked === true;
testToolsNotice.textContent = enabled
? "Tools are enabled. This test uses normal discovery, prompt exposure, permission checks, and execution."
: "Tools are disabled for this test; this result will not exercise tool discovery or execution.";
};
testForm.elements.allow_tools?.addEventListener("change", updateTestToolsNotice);
updateTestToolsNotice();
testForm.addEventListener("submit", async (event) => {
event.preventDefault();
const form = new FormData(testForm);
testOutput.hidden = false;
testOutput.textContent = "Running...";
try {
const response = await fetch("/plugins/lumi_ai/assistant/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
role: form.get("role"),
origin: form.get("origin"),
message: form.get("message"),
allow_tools: form.get("allow_tools") === "on",
show_raw_prompt: form.get("show_raw_prompt") === "on",
show_raw_output: form.get("show_raw_output") === "on"
})
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Test failed.");
if (form.get("show_raw_output") !== "on") delete data.raw_response;
testOutput.textContent = JSON.stringify(data, null, 2);
} catch (error) {
testOutput.textContent = error.message;
}
});
}
if (gpuControl) {
const model = document.querySelector("[data-gpu-model]");
const context = document.querySelector("[data-gpu-context]");
const workload = gpuControl.querySelector("[data-gpu-workload]");
const slider = gpuControl.querySelector("[data-gpu-slider]");
const value = gpuControl.querySelector("[data-gpu-value]");
const intentLabel = gpuControl.querySelector("[data-gpu-intent]");
const actualLabel = gpuControl.querySelector("[data-gpu-actual]");
const limit = gpuControl.querySelector("[data-gpu-limit]");
const backend = gpuControl.querySelector("[data-gpu-backend]");
const memory = gpuControl.querySelector("[data-gpu-memory]");
const totalVram = gpuControl.querySelector("[data-gpu-total-vram]");
const freeVram = gpuControl.querySelector("[data-gpu-free-vram]");
const externalVram = gpuControl.querySelector("[data-gpu-external-vram]");
const warning = gpuControl.querySelector("[data-gpu-warning]");
let maximum = Number.parseInt(limit.textContent.match(/\d+/)?.[0], 10) || 0;
let capacityTimer = null;
const formatBytes = (megabytes) => {
if (!megabytes) return "0 B";
return megabytes >= 1024 ? `${(megabytes / 1024).toFixed(1)} GB` : `${Math.round(megabytes)} MB`;
};
const clampNewIntent = () => {
const next = Math.max(0, Math.min(maximum, Number(workload.value) || 0));
workload.value = String(next);
value.textContent = `${next}% intent`;
intentLabel.textContent = `${next}%`;
return next;
};
const applyCapacity = (data) => {
maximum = Math.max(0, Math.min(100, Number(data.gpu_allocation_max_safe_percent) || 0));
const intent = Math.max(0, Math.min(100, Number(data.gpu_allocation_intent_percent) || 0));
const actual = Math.max(0, Math.min(maximum, Number(data.gpu_allocation_actual_percent) || 0));
slider.style.setProperty("--gpu-max", `${maximum}%`);
slider.style.setProperty("--gpu-actual", `${actual}%`);
slider.title = `Intended ${intent}%, actual ${actual}%, maximum safe ${maximum}%`;
limit.textContent = `Maximum safe: ${maximum}%`;
workload.value = String(intent);
value.textContent = `${intent}% intent`;
intentLabel.textContent = `${intent}%`;
actualLabel.textContent = `${actual}%`;
backend.textContent = String(data.backend || "cpu").toUpperCase();
memory.dataset.fullOffloadMb = String(Number(data.estimated_full_offload_mb) || 0);
memory.textContent = formatBytes(data.managed_model_vram_mb);
totalVram.textContent = formatBytes(data.total_vram_mb);
freeVram.textContent = formatBytes(data.free_vram_mb);
externalVram.textContent = formatBytes(data.external_vram_estimate_mb);
warning.hidden = !data.warning;
warning.textContent = data.warning || "";
};
const refreshCapacity = async () => {
const query = new URLSearchParams({
model_id: model.value,
context_size: context.value,
intent_percent: workload.value
});
try {
const response = await fetch(`${gpuControl.dataset.endpoint}?${query}`, { cache: "no-store" });
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Capacity check failed.");
applyCapacity(data);
} catch (error) {
applyCapacity({ max_percent: 0, backend: "cpu", warning: error.message });
}
};
const scheduleCapacity = () => {
window.clearTimeout(capacityTimer);
capacityTimer = window.setTimeout(refreshCapacity, 250);
};
workload.addEventListener("input", () => {
const selected = clampNewIntent();
const actual = Math.min(selected, maximum);
slider.style.setProperty("--gpu-actual", `${actual}%`);
actualLabel.textContent = `${actual}%`;
memory.textContent = formatBytes(
(Number(memory.dataset.fullOffloadMb) || 0) * actual / 100
);
});
workload.addEventListener("change", refreshCapacity);
model.addEventListener("change", refreshCapacity);
context.addEventListener("input", scheduleCapacity);
refreshCapacity();
}
if (accessForm) {
const search = accessForm.querySelector("[data-user-search]");
const userId = accessForm.querySelector("[data-user-id]");
const results = accessForm.querySelector("[data-user-results]");
const preview = accessForm.querySelector("[data-user-preview]");
const action = accessForm.querySelector("[data-access-action]");
const timeoutField = accessForm.querySelector("[data-timeout-field]");
let searchTimer = null;
const updateTimeoutVisibility = () => {
const visible = action.value === "timeout";
timeoutField.hidden = !visible;
const input = timeoutField.querySelector("input");
input.required = visible;
if (!visible) input.value = "";
};
const selectUser = (user) => {
userId.value = user.id;
search.value = user.username;
const identities = user.identities.map((identity) =>
`${identity.provider}: ${identity.display_name || identity.provider_user_id}`).join(" | ");
preview.textContent = `${user.username} | ${user.id}${identities ? ` | ${identities}` : ""}`;
preview.hidden = false;
results.hidden = true;
};
const renderUsers = (users) => {
results.replaceChildren();
for (const user of users) {
const button = document.createElement("button");
button.type = "button";
button.className = "ai-user-result";
const identity = user.identities[0];
button.textContent = identity
? `${user.username} | ${identity.display_name || identity.provider_user_id} (${identity.provider})`
: `${user.username} | ${user.id}`;
button.addEventListener("click", () => selectUser(user));
results.append(button);
}
results.hidden = !users.length;
};
search.addEventListener("input", () => {
userId.value = "";
preview.hidden = true;
window.clearTimeout(searchTimer);
const query = search.value.trim();
if (query.length < 2) {
results.hidden = true;
return;
}
searchTimer = window.setTimeout(async () => {
try {
const response = await fetch(`/plugins/lumi_ai/api/users/search?q=${encodeURIComponent(query)}`, { cache: "no-store" });
const data = await response.json();
renderUsers(response.ok ? data.users || [] : []);
} catch {
renderUsers([]);
}
}, 180);
});
accessForm.addEventListener("submit", (event) => {
if (!userId.value) {
event.preventDefault();
search.setCustomValidity("Select a known Lumi user.");
search.reportValidity();
}
});
search.addEventListener("input", () => search.setCustomValidity(""));
action.addEventListener("change", updateTimeoutVisibility);
updateTimeoutVisibility();
}
const assistantDiagnostics = document.querySelector("[data-assistant-diagnostics]");
if (assistantDiagnostics) {
const status = assistantDiagnostics.querySelector("[data-assistant-status]");
const reason = assistantDiagnostics.querySelector("[data-assistant-reason]");
const conditions = assistantDiagnostics.querySelector("[data-assistant-conditions]");
const endpointStatus = assistantDiagnostics.querySelector("[data-panel-endpoint-status]");
const htmlLength = assistantDiagnostics.querySelector("[data-panel-html-length]");
const htmlError = assistantDiagnostics.querySelector("[data-panel-html-error]");
const mountError = assistantDiagnostics.querySelector("[data-panel-mount-error]");
const refreshDiagnostics = () => fetch(assistantDiagnostics.dataset.endpoint, { cache: "no-store" })
.then(async (response) => {
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Diagnostic unavailable.");
status.textContent = data.conditions?.every((condition) => condition.passed) ? "Mounted" : data.available ? "Backend ready" : "Hidden";
reason.textContent = data.reason;
for (const condition of data.conditions || []) {
const row = conditions?.querySelector(`[data-condition="${condition.key}"] strong`);
if (!row) continue;
row.textContent = condition.passed ? "Pass" : "Fail";
row.className = condition.passed ? "pass" : "fail";
}
if (endpointStatus) endpointStatus.textContent = data.panel_endpoint_status || "Not requested";
if (htmlLength) htmlLength.textContent = data.panel_html_length || 0;
if (htmlError) htmlError.textContent = data.panel_html_error || "None";
if (mountError) mountError.textContent = data.mount_error || "None";
})
.catch((error) => {
status.textContent = "Unknown";
reason.textContent = error.message;
});
refreshDiagnostics();
window.setInterval(refreshDiagnostics, 5000);
}
const logContent = document.querySelector("[data-log-content]");
const logFilter = document.querySelector("[data-log-filter]");
const logCopy = document.querySelector("[data-log-copy]");
if (logContent && logFilter) {
const originalLines = logContent.textContent.split(/\r?\n/);
logFilter.addEventListener("input", () => {
const term = logFilter.value.trim().toLowerCase();
logContent.textContent = term
? originalLines.filter((line) => line.toLowerCase().includes(term)).join("\n")
: originalLines.join("\n");
});
logCopy?.addEventListener("click", async () => {
await navigator.clipboard.writeText(logContent.textContent);
const original = logCopy.textContent;
logCopy.textContent = "Copied";
window.setTimeout(() => { logCopy.textContent = original; }, 1200);
});
}
pollDownloads();
})();