Rebuild workspace widgets and behavioral graph views

This commit is contained in:
2026-03-13 16:48:24 +00:00
parent f8a6d1d41c
commit 57269770b5
47 changed files with 2951 additions and 1077 deletions

View File

@@ -18,6 +18,7 @@
const replyBanner = config.replyBanner;
const replyBannerText = config.replyBannerText;
const replyClearBtn = config.replyClearBtn;
const historyLoader = config.historyLoader;
const platformSelect = config.platformSelect;
const contactSelect = config.contactSelect;
const hiddenService = config.hiddenService;
@@ -29,6 +30,9 @@
let lastTs = core.toInt(thread.dataset.lastTs);
let beforeContextReset = null;
state.loadingOlder = false;
state.olderExhausted = false;
const nearBottom = function () {
return thread.scrollHeight - thread.scrollTop - thread.clientHeight < 120;
};
@@ -39,6 +43,21 @@
}
};
const setHistoryLoader = function (message, hidden) {
if (!historyLoader) {
return;
}
historyLoader.textContent = String(
message || "Scroll up to load older messages."
);
historyLoader.classList.toggle("is-hidden", !!hidden);
};
const getOldestTs = function () {
const firstRow = thread.querySelector(".compose-row");
return core.toInt(firstRow && firstRow.dataset ? firstRow.dataset.ts : 0);
};
const queryParams = function (extraParams) {
const params = new URLSearchParams();
params.set("service", thread.dataset.service || "");
@@ -204,8 +223,28 @@
});
if (rows.length) {
scrollToBottom(shouldStick);
setHistoryLoader("", false);
}
ensureEmptyState();
if (!thread.querySelector(".compose-row")) {
setHistoryLoader("", true);
}
};
const prependMessageHtml = function (html) {
const rows = parseMessageRows(html);
if (!rows.length) {
return 0;
}
const previousHeight = thread.scrollHeight;
const previousTop = thread.scrollTop;
rows.forEach(function (msg) {
upsertMessageRow(msg);
});
thread.scrollTop = previousTop + (thread.scrollHeight - previousHeight);
setHistoryLoader("", false);
ensureEmptyState();
return rows.length;
};
const applyTyping = function (payload) {
@@ -267,6 +306,47 @@
}
};
const loadOlder = async function () {
if (state.loadingOlder || state.olderExhausted) {
return;
}
const oldestTs = getOldestTs();
if (!oldestTs) {
state.olderExhausted = true;
setHistoryLoader("Start of conversation.", false);
return;
}
state.loadingOlder = true;
setHistoryLoader("Loading older messages...", false);
try {
const response = await fetch(
thread.dataset.pollUrl + "?" + queryParams({ before_ts: String(oldestTs) }),
{
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" },
}
);
if (!response.ok) {
setHistoryLoader("Could not load older messages.", false);
return;
}
const payload = await response.json();
const inserted = prependMessageHtml(payload.messages_html || "");
state.olderExhausted = !payload.has_older || inserted === 0;
setHistoryLoader(
state.olderExhausted
? "Start of conversation."
: "Scroll up to load older messages.",
false
);
} catch (_err) {
setHistoryLoader("Could not load older messages.", false);
} finally {
state.loadingOlder = false;
}
};
const setupWebSocket = function () {
const wsPath = String(thread.dataset.wsUrl || "").trim();
if (!wsPath || !window.WebSocket) {
@@ -359,8 +439,14 @@
clearReplyTarget();
closeSocket();
lastTs = 0;
state.loadingOlder = false;
state.olderExhausted = false;
thread.dataset.lastTs = "0";
thread.innerHTML = "";
if (historyLoader) {
thread.appendChild(historyLoader);
}
setHistoryLoader("Loading recent messages...", false);
ensureEmptyState("Loading messages...");
applyTyping({ typing: false });
poll(true);
@@ -466,6 +552,12 @@
setReplyTarget(row.dataset.messageId || "", row.dataset.replySnippet || "");
textarea.focus();
});
thread.addEventListener("scroll", function () {
if (thread.scrollTop <= 48) {
loadOlder();
}
});
};
const init = function () {
@@ -473,6 +565,7 @@
bindContextSelectors();
applyTyping(core.parseJsonSafe(panel.dataset.initialTyping || "{}", {}));
ensureEmptyState();
setHistoryLoader("", !thread.querySelector(".compose-row"));
scrollToBottom(true);
setupWebSocket();
@@ -492,6 +585,9 @@
init: init,
poll: poll,
queryParams: queryParams,
scrollToLatest: function () {
scrollToBottom(true);
},
setBeforeContextReset: function (callback) {
beforeContextReset = callback;
},