Skip to content
wiki.fftac.org

Workspace - Source Excerpt 04

Back to Workspace

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) => {