123 lines
5.1 KiB
JavaScript
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);
|
|
})();
|