Lumi/plugins/lumi_ai/public/improvement-center.js
2026-07-22 10:20:32 +02:00

205 lines
8.2 KiB
JavaScript

(() => {
let stream = null;
let refreshTimer = null;
let refreshing = false;
let queuedRefresh = false;
const status = () => document.querySelector("[data-improvement-status]");
const workspace = () => document.querySelector("[data-improvement-workspace]");
const historyDialog = () => document.getElementById("improvement-history");
function showStatus(message, tone = "info") {
const target = status();
if (!target) return;
target.textContent = message || "";
target.dataset.tone = tone;
target.hidden = !message;
if (message && tone !== "error") window.setTimeout(() => {
if (target.textContent === message) target.hidden = true;
}, 5000);
}
function historyParams() {
const dialog = historyDialog();
const form = dialog?.querySelector("[data-improvement-history-search]");
const params = new URLSearchParams();
const query = form?.elements?.history_q?.value?.trim();
const outcome = form?.elements?.history_status?.value;
if (query) params.set("history_q", query);
if (outcome) params.set("history_status", outcome);
return params;
}
async function refreshWorkspace(url = null, options = {}) {
if (refreshing) {
queuedRefresh = true;
return;
}
refreshing = true;
const wasHistoryOpen = options.openHistory ?? Boolean(historyDialog()?.open);
const activeElementId = document.activeElement?.id || "";
try {
const params = url ? new URL(url, window.location.origin).searchParams : historyParams();
const response = await fetch(`/plugins/lumi_ai/improvement_center?${params.toString()}`, {
headers: { Accept: "text/html", "X-Lumi-Live-Refresh": "1" },
cache: "no-store"
});
if (!response.ok) throw new Error("The latest feedback queue could not be loaded.");
const html = await response.text();
const documentCopy = new DOMParser().parseFromString(html, "text/html");
const next = documentCopy.querySelector("[data-improvement-workspace]");
const current = workspace();
if (!next || !current) throw new Error("The refreshed feedback view was incomplete.");
current.replaceWith(next);
window.LumiInteractions?.init?.(next);
if (wasHistoryOpen) historyDialog()?.showModal();
if (activeElementId) document.getElementById(activeElementId)?.focus?.();
} catch (error) {
showStatus(error.message || "The feedback queue could not be refreshed.", "error");
} finally {
refreshing = false;
if (queuedRefresh) {
queuedRefresh = false;
refreshWorkspace(null, { openHistory: wasHistoryOpen });
}
}
}
function scheduleRefresh(message) {
if (message) showStatus(message);
window.clearTimeout(refreshTimer);
refreshTimer = window.setTimeout(() => refreshWorkspace(), 120);
}
document.addEventListener("click", (event) => {
const opener = event.target.closest("[data-open-dialog]");
if (opener) {
document.getElementById(opener.dataset.openDialog)?.showModal();
return;
}
if (event.target.closest("[data-open-history]")) {
historyDialog()?.showModal();
historyDialog()?.querySelector("input[type='search']")?.focus();
return;
}
const closer = event.target.closest("[data-close-dialog]");
if (closer) {
closer.closest("dialog")?.close();
return;
}
if (event.target.closest("[data-history-clear]")) {
const form = historyDialog()?.querySelector("[data-improvement-history-search]");
form?.reset();
refreshWorkspace("/plugins/lumi_ai/improvement_center", { openHistory: true });
return;
}
const pageLink = event.target.closest("[data-improvement-page], [data-improvement-history-page]");
if (pageLink && !pageLink.classList.contains("disabled")) {
event.preventDefault();
refreshWorkspace(pageLink.href, { openHistory: pageLink.matches("[data-improvement-history-page]") });
}
});
document.addEventListener("submit", async (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
if (form.matches("[data-improvement-history-search]")) {
event.preventDefault();
const params = new URLSearchParams(new FormData(form));
params.delete("history_page");
return refreshWorkspace(`/plugins/lumi_ai/improvement_center?${params.toString()}`, { openHistory: true });
}
if (!form.matches("[data-improvement-action]")) return;
// The shared timed-confirmation handler runs in the capture phase. Let it
// issue a token before AJAX handles permanent deletes.
if (event.defaultPrevented && !form.elements.confirmation_token?.value) return;
event.preventDefault();
const submitter = event.submitter || form.querySelector("button[type='submit']");
const originalLabel = submitter?.textContent || "";
if (submitter) {
submitter.disabled = true;
submitter.textContent = "Saving…";
}
try {
const action = window.LumiForms?.action(form) || form.getAttribute("action") || window.location.href;
const body = new URLSearchParams();
for (const [key, value] of new FormData(form).entries()) {
if (typeof value === "string") body.append(key, value);
}
const response = await fetch(action, {
method: window.LumiForms?.method(form) || form.getAttribute("method") || "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
"X-Lumi-Response-Format": "json"
},
body,
credentials: "same-origin",
cache: "no-store"
});
const responseText = await response.text();
let result = {};
try { result = responseText ? JSON.parse(responseText) : {}; } catch {}
if (response.redirected) {
throw new Error("Your session changed while saving. Refresh the page, sign in again if needed, and retry.");
}
if (!response.ok || result.success === false) {
const fallback = response.status === 403
? "You no longer have permission to perform this feedback action."
: response.status === 404
? "This feedback record is no longer available. Refresh the queue and try again."
: `The feedback could not be updated (HTTP ${response.status}).`;
throw new Error(result.message || result.error || fallback);
}
if (!result.message && !responseText) {
throw new Error("Lumi returned an empty response while saving feedback. Check the server diagnostics and try again.");
}
form.closest("dialog")?.close();
showStatus(result.message || "Feedback updated.", "success");
await refreshWorkspace(null, { openHistory: Boolean(historyDialog()?.open) });
} catch (error) {
showStatus(error.message || "The feedback could not be updated.", "error");
if (submitter) {
submitter.disabled = false;
submitter.textContent = originalLabel;
}
}
});
function connectLiveUpdates() {
if (!window.EventSource || stream) return;
const handleChange = (event) => {
try {
const data = JSON.parse(event.data || "{}");
scheduleRefresh(data.message || "The feedback queue was updated by another reviewer.");
} catch {
scheduleRefresh("The feedback queue was updated by another reviewer.");
}
};
if (window.LumiEvents?.subscribe) {
const unsubscribe = window.LumiEvents.subscribe("ai:improvement_changed", handleChange);
const handleStatus = (event) => {
if (event.detail?.status === "disconnected") {
showStatus("Live updates are reconnecting. Your saved actions are still safe.", "warning");
}
};
window.addEventListener("lumi:event-status", handleStatus);
stream = {
close() {
unsubscribe();
window.removeEventListener("lumi:event-status", handleStatus);
}
};
return;
}
stream = new EventSource("/api/events");
stream.addEventListener("ai:improvement_changed", handleChange);
stream.onerror = () => {
showStatus("Live updates are reconnecting. Your saved actions are still safe.", "warning");
};
window.addEventListener("pagehide", () => stream?.close(), { once: true });
}
connectLiveUpdates();
})();