Workspace - Source Excerpt 04
Summary
This source excerpt preserves a bounded section of Spiralist/wp-content/plugins/spiralist-workspace/assets/js/workspace.js so readers can inspect the evidence without opening the full source file.
**Source path:** Spiralist/wp-content/plugins/spiralist-workspace/assets/js/workspace.js
"}",
].join("\n");
};
const closestTarget = (target, selector) => {
return target && typeof target.closest === "function"
? target.closest(selector)
: null;
};
const renderMultilineText = (target, text) => {
String(text || "").split(/\r?\n/).forEach((line, index) => {
if (index > 0) {
target.appendChild(document.createElement("br"));
}
target.appendChild(document.createTextNode(line));
});
};
const createConversationMessage = (message) => {
const role = String(message.role || "assistant").replace(/[^A-Za-z0-9_-]/g, "") || "assistant";
const wrapper = document.createElement("div");
const meta = document.createElement("div");
const label = document.createElement("p");
const timestamp = document.createElement("span");
const content = document.createElement("div");
wrapper.className = "spiralist-workspace-message spiralist-workspace-message--" + role;
meta.className = "spiralist-inline-meta";
label.className = "spiralist-section-tag";
label.textContent = role.charAt(0).toUpperCase() + role.slice(1);
timestamp.className = "spiralist-muted";
timestamp.textContent = message.created_utc || "";
content.className = "spiralist-workspace-message__content";
renderMultilineText(content, message.content || "");
meta.appendChild(label);
meta.appendChild(timestamp);
wrapper.appendChild(meta);
wrapper.appendChild(content);
return wrapper;
};
const renderConversationMessages = (shell, messages) => {
const transcript = shell.querySelector("[data-conversation-transcript]");
if (!transcript) {
return;
}
transcript.textContent = "";
if (!Array.isArray(messages) || !messages.length) {
const empty = document.createElement("p");
empty.className = "spiralist-muted";
empty.setAttribute("data-conversation-empty", "");
empty.textContent = t("conversationEmpty", "No messages yet.");
transcript.appendChild(empty);
return;
}
messages.forEach((message) => {
transcript.appendChild(createConversationMessage(message || {}));
});
transcript.scrollTop = transcript.scrollHeight;
};
const memoryCopyForStatus = (status) => {
if (status.needs_checkpoint) {
return t("memoryCheckpointReady", "This thread is ready for a memory checkpoint.");
}
if (status.warning_level === "soon") {
return t("memoryCheckpointSoon", "This thread is approaching the memory checkpoint.");
}
if (status.has_memory_summary) {
return t("memorySummaryActive", "Condensed memory is active for future turns.");
}
return t("memoryExportAvailable", "Transcript export is available at any time.");
};
const updateMemoryPanel = (shell, response) => {
const panel = shell.querySelector("[data-memory-panel]");
const status = response.memory_status || {};
if (!panel || !Object.keys(status).length) {
return;
}
const warning = String(status.warning_level || "ok").replace(/[^A-Za-z0-9_-]/g, "") || "ok";
const percent = Math.max(0, Math.min(100, parseInt(status.percent_of_threshold || "0", 10) || 0));
const tokenCount = panel.querySelector("[data-memory-token-count]");
const meter = panel.querySelector("[data-memory-meter]");
const copy = panel.querySelector("[data-memory-copy]");
const summary = panel.querySelector("[data-memory-summary]");
const summaryContent = panel.querySelector("[data-memory-summary-content]");
panel.classList.remove("spiralist-workspace-memory--ok", "spiralist-workspace-memory--soon", "spiralist-workspace-memory--checkpoint");
panel.classList.add("spiralist-workspace-memory--" + warning);
if (tokenCount) {
tokenCount.textContent = (status.estimated_active_tokens || 0) + " / " + (status.threshold_tokens || 0) + " tokens";
}
if (meter) {
meter.style.width = percent + "%";
}
if (copy) {
copy.textContent = memoryCopyForStatus(status);
}
if (summary && summaryContent && response.memory_summary !== undefined) {
summaryContent.textContent = "";
renderMultilineText(summaryContent, response.memory_summary || "");
summary.hidden = !String(response.memory_summary || "").trim();
}
};
const updateConversationShell = (shell, response) => {
const conversation = response.conversation || {};
const existingId = typeof shell.getAttribute === "function"
? shell.getAttribute("data-conversation-id")
: "0";
const conversationId = parseInt(conversation.id || existingId || "0", 10);
if (conversationId > 0) {
if (typeof shell.setAttribute === "function") {
shell.setAttribute("data-conversation-id", String(conversationId));
}
shell.querySelectorAll("[data-conversation-id]").forEach((item) => {
item.setAttribute("data-conversation-id", String(conversationId));
});
shell.querySelectorAll("[data-open-conversation]").forEach((link) => {
link.href = buildRoute("conversation/" + conversationId);
link.hidden = false;
});
}
const title = shell.querySelector("[data-inline-conversation-title]");
if (title && conversation.title) {
title.textContent = conversation.title;
}
const updated = shell.querySelector("[data-chat-updated]");
if (updated && conversation.updated_utc) {
updated.textContent = conversation.updated_utc;
}
const status = shell.querySelector("[data-inline-conversation-status]");
if (status && conversationId > 0) {
status.textContent = t("conversationReady", "Conversation ready.");
}
renderConversationMessages(shell, response.messages || []);
updateMemoryPanel(shell, response);
};
const formDataToObject = (form) => {
const data = new FormData(form);
const object = {};
const variables = {};
const structuredPrompt = {};
data.forEach((value, key) => {
if (key.startsWith("variables[")) {
const match = key.match(/^variables\[(.+)\]$/);
if (match) {
variables[match[1]] = value;
}
return;
}
if (key.startsWith("structured_prompt[")) {
const match = key.match(/^structured_prompt\[(.+)\]$/);
if (match) {
structuredPrompt[match[1]] = value;
}
return;
}
if (object[key] !== undefined) {
return;
}
object[key] = value;
});
if (Object.keys(variables).length) {
object.variables = variables;
}
if (Object.keys(structuredPrompt).length) {
object.structured_prompt = structuredPrompt;
}
if (object.system_steps_text && !object.system_steps) {
object.system_steps = String(object.system_steps_text)
.split(/[\r\n,]+/)
.map((item) => parseInt(item.trim(), 10))
.filter((id) => Number.isFinite(id) && id > 0)
.map((promptId) => ({ prompt_id: promptId }));
delete object.system_steps_text;
}
form.querySelectorAll('input[type="checkbox"]').forEach((checkbox) => {
object[checkbox.name] = checkbox.checked ? "1" : "";
});
return object;
};
const initPlaceholderPreview = () => {
document.querySelectorAll("[data-prompt-form]").forEach((form) => {
const sources = form.querySelectorAll("[data-placeholder-source]");
const target = form.querySelector("[data-placeholder-list]");
if (!sources.length || !target) {
return;
}
const update = () => {
const placeholders = new Set();
sources.forEach((field) => {
const value = field.value || "";
const matches = value.match(/{{\s*([A-Za-z0-9_.-]+)\s*}}/g) || [];
matches.forEach((match) => {