(() => { 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); })();