Lumi/plugins/lumi_ai/public/assistant.js
2026-06-11 06:35:43 +02:00

123 lines
5.1 KiB
JavaScript

(() => {
const root = document.querySelector("[data-lumi-ai]");
if (!root) return;
const endpoint = root.dataset.endpoint;
const panel = root.querySelector("[data-lumi-ai-panel]");
const toggle = root.querySelector("[data-lumi-ai-toggle]");
const close = root.querySelector("[data-lumi-ai-close]");
const state = root.querySelector("[data-lumi-ai-state]");
const status = root.querySelector("[data-lumi-ai-status]");
const messages = root.querySelector("[data-lumi-ai-messages]");
const form = root.querySelector("[data-lumi-ai-form]");
const input = form.querySelector("textarea");
const setOpen = (open) => {
if (open) positionPanel();
panel.classList.toggle("open", open);
panel.setAttribute("aria-hidden", String(!open));
toggle.setAttribute("aria-expanded", String(open));
if (open) input.focus();
};
const positionPanel = () => {
const viewportHeight = window.innerHeight;
const desiredHeight = Math.max(180, viewportHeight / 6);
const footerRect = document.querySelector(".site-footer")?.getBoundingClientRect();
const bottomLimit = footerRect && footerRect.top < viewportHeight && footerRect.bottom > 0
? Math.max(8, footerRect.top - 8)
: viewportHeight - 8;
const anchor = toggle.getBoundingClientRect();
let top = anchor.bottom + 8;
if (top + desiredHeight > bottomLimit) {
const overflow = top + desiredHeight - bottomLimit;
top -= overflow / 2;
if (top + desiredHeight > bottomLimit) top = bottomLimit - desiredHeight;
}
top = Math.max(8, Math.min(top, viewportHeight - desiredHeight - 8));
panel.style.setProperty("--lumi-ai-top", `${top}px`);
panel.style.height = `${Math.min(desiredHeight, viewportHeight - top - 8)}px`;
};
const addMessage = (text, type, confirmation) => {
const item = document.createElement("div");
item.className = `lumi-ai-message ${type}`;
item.textContent = text;
if (confirmation) {
const actions = document.createElement("div");
actions.className = "lumi-ai-confirm";
for (const [label, route] of [["Confirm", "confirm"], ["Cancel", "cancel"]]) {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.addEventListener("click", async () => {
button.disabled = true;
try {
const response = await fetch(`${endpoint}/assistant/${route}`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: confirmation.id })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Action failed.");
addMessage(route === "confirm" ? "Confirmed and completed." : "Cancelled.", "assistant");
actions.remove();
} catch (error) { addMessage(error.message, "assistant error"); }
});
actions.append(button);
}
item.append(actions);
}
messages.append(item);
messages.scrollTop = messages.scrollHeight;
};
const refreshStatus = async () => {
try {
const response = await fetch(`${endpoint}/api/status`);
const data = await response.json();
const ready = response.ok && data.enabled && data.runtime?.healthy;
state.className = `lumi-ai-state ${ready ? "ready" : "error"}`;
if (ready) status.textContent = `${data.model_id} ready`;
else if (!data.enabled) status.textContent = "Disabled by administrator";
else if (!data.runtime?.runtime_installed) status.textContent = "Runtime not installed";
else if (!data.runtime?.model_downloaded) status.textContent = "Selected model missing";
else if (data.runtime?.state === "error") status.textContent = "Runtime error";
else status.textContent = "Runtime stopped";
} catch {
state.className = "lumi-ai-state error";
status.textContent = "Status unavailable";
}
};
toggle.addEventListener("click", () => setOpen(!panel.classList.contains("open")));
close.addEventListener("click", () => setOpen(false));
form.addEventListener("submit", async (event) => {
event.preventDefault();
const message = input.value.trim();
if (!message) return;
addMessage(message, "user");
input.value = "";
input.disabled = true;
try {
const response = await fetch(`${endpoint}/assistant/message`, {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message })
});
const data = await response.json();
if (!response.ok) throw new Error(data.error || "Request failed.");
addMessage(data.text, "assistant", data.confirmation);
} catch (error) {
addMessage(error.message, "assistant error");
} finally {
input.disabled = false;
input.focus();
}
});
input.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
form.requestSubmit();
}
});
window.addEventListener("resize", () => {
if (panel.classList.contains("open")) positionPanel();
});
refreshStatus();
window.setInterval(refreshStatus, 15000);
})();