Skip to content
wiki.fftac.org

Workspace - Source Excerpt 01

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

(function () {
    const config = window.SpiralistWorkspace || {};
    const apiRoot = config.apiRoot || "";
    const spiralScriptApiRoot = config.spiralScriptApiRoot || "";
    const nonce = config.nonce || "";
    const locale = config.locale || "";
    const uiText = typeof config.uiText === "object" && config.uiText !== null ? config.uiText : {};
    const t = (key, fallback = "") => {
        const value = uiText[key];
        return typeof value === "string" && value !== "" ? value : fallback;
    };
    const core = typeof window.SpiralistCore === "object" && window.SpiralistCore !== null
        ? window.SpiralistCore
        : {};
    const runAction = typeof core.runAction === "function"
        ? core.runAction
        : (action) => Promise.resolve().then(() => action());
    const storage = core.storage || {
        get: (key, fallback = "") => window.localStorage.getItem(key) || fallback,
        set: (key, value) => window.localStorage.setItem(key, value),
        remove: (key) => window.localStorage.removeItem(key),
    };
    const pendingEditablePromptSaveKey = "spiralist.pendingEditablePromptSave.v1";
    const canSavePrompts = Boolean(config.canSavePrompts && apiRoot && nonce);
    const copyText = typeof core.copyText === "function"
        ? core.copyText
        : async (text) => {
            if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
                await navigator.clipboard.writeText(text);
                return;
            }

            const textarea = document.createElement("textarea");
            textarea.value = text;
            textarea.setAttribute("readonly", "");
            textarea.style.position = "fixed";
            textarea.style.left = "-9999px";
            document.body.appendChild(textarea);
            textarea.select();
            document.execCommand("copy");
            textarea.remove();
        };
    const formatText = (key, fallback = "", replacements = {}) => {
        let value = t(key, fallback);

        Object.entries(replacements).forEach(([name, replacement]) => {
            value = value.split("{" + name + "}").join(String(replacement));
        });

        return value;
    };

    if (!apiRoot) {
        return;
    }

    const buildRoute = (path) => {
        const cleaned = String(path || "").replace(/^\/+|\/+$/g, "");
        const prefix = locale ? "/" + String(locale).replace(/[^A-Za-z0-9_-]/g, "") : "";
        return prefix + "/" + cleaned + "/";
    };
    const parseJsonResponse = (text) => text ? JSON.parse(text) : {};

    const apiFetch = async (path, options = {}) => {
        const init = {
            credentials: "same-origin",
            headers: {
                "Content-Type": "application/json",
                "X-WP-Nonce": nonce,
            },
            ...options,
        };

        const response = await fetch(apiRoot.replace(/\/$/, "") + "/" + path.replace(/^\//, ""), init);
        const text = await response.text();
        const data = parseJsonResponse(text);

        if (!response.ok) {
            const message = data.message || t("requestFailed", "Request failed.");
            throw new Error(message);
        }

        return data;
    };

    const spiralScriptFetch = async (path, options = {}) => {
        if (!spiralScriptApiRoot) {
            throw new Error(t("spiralScriptApiUnavailable", "Spiral Script API is not available."));
        }

        const init = {
            credentials: "same-origin",
            headers: {
                "Content-Type": "application/json",
                "X-WP-Nonce": nonce,
            },
            ...options,
        };

        const response = await fetch(spiralScriptApiRoot.replace(/\/$/, "") + "/" + path.replace(/^\//, ""), init);
        const text = await response.text();
        const data = parseJsonResponse(text);

        if (!response.ok) {
            throw new Error(data.message || t("spiralScriptRequestFailed", "Spiral Script request failed."));
        }

        return data;
    };

    const setResult = (form, message, ok = true) => {
        const target = form.querySelector("[data-form-result]");
        if (!target) {
            return;
        }

        target.textContent = message;
        target.style.color = ok ? "" : "#f0a5a5";
        target.setAttribute("aria-live", "polite");

        if (message && !message.endsWith("...")) {
            target.setAttribute("tabindex", "-1");
            target.focus({ preventScroll: true });
        }
    };

    const setInlineResult = (wrapper, message, ok = true) => {
        const target = wrapper.querySelector("[data-memory-result]");
        if (!target) {
            return;
        }

        target.textContent = message;
        target.style.color = ok ? "" : "#f0a5a5";
    };
    const runFormAction = (form, action, failureMessage, context = "", options = {}) => runAction(action, {
        ...options,
        context,
        fallback: failureMessage,
        onError: (error, message) => setResult(form, message, false),
    });
    const runInlineAction = (wrapper, action, failureMessage, context = "", options = {}) => runAction(action, {
        ...options,
        context,
        fallback: failureMessage,
        onError: (error, message) => setInlineResult(wrapper, message, false),
    });
    const runButtonAction = (button, action, failureMessage, context = "") => runAction(action, {
        context,
        fallback: failureMessage,
        onError: (error, message) => {
            if (button && button.dataset && button.dataset.errorTarget) {
                const target = document.querySelector(button.dataset.errorTarget);
                if (target) {
                    target.textContent = message;
                    target.style.color = "#f0a5a5";
                }
                return;
            }

            window.alert(message);
        },
    });
    const setDisabled = (controls, disabled) => {
        const items = controls instanceof NodeList || Array.isArray(controls)
            ? Array.from(controls)
            : [controls];

        items.forEach((item) => {
            if (item && "disabled" in item) {
                item.disabled = disabled;
            }
        });
    };

    const downloadTextFile = (filename, text, contentType = "text/plain") => {
        const blob = new Blob([text], { type: contentType });
        const url = URL.createObjectURL(blob);
        const link = document.createElement("a");

        link.href = url;
        link.download = filename || "conversation-prompt.md";
        document.body.appendChild(link);
        link.click();
        link.remove();
        window.setTimeout(() => URL.revokeObjectURL(url), 500);
    };

    const filenameSlug = (value, fallback = "prompt") => {
        const slug = String(value || "")
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, "-")
            .replace(/^-+|-+$/g, "");

        return slug || fallback;
    };
    const toCleanPromptText = (value) => String(value || "").replace(/\r\n/g, "\n").trim();
    const editablePromptText = (surface) => {
        const block = surface ? surface.querySelector("[data-editable-prompt-block]") : null;

        return block ? toCleanPromptText(block.textContent || "") : "";
    };
    const setEditablePromptStatus = (surface, message, ok = true) => {
        const target = surface ? surface.querySelector("[data-editable-prompt-status]") : null;

        if (!target) {
            return;
        }

        target.textContent = message;
        target.style.color = ok ? "" : "#f0a5a5";
    };
    const insertPlainTextAtSelection = (text) => {
        if (typeof document.execCommand === "function" && document.queryCommandSupported && document.queryCommandSupported("insertText")) {
            document.execCommand("insertText", false, text);
            return;
        }

        const selection = window.getSelection ? window.getSelection() : null;
        if (!selection || selection.rangeCount === 0) {
            return;
        }

        const range = selection.getRangeAt(0);
        range.deleteContents();
        range.insertNode(document.createTextNode(text));
        range.collapse(false);
        selection.removeAllRanges();
        selection.addRange(range);
    };
    const promptTitleFromText = (text, fallback) => {
        const heading = String(text || "")
            .split("\n")
            .map((line) => line.trim())
            .find((line) => /^#{1,6}\s+/.test(line));
        const title = heading ? heading.replace(/^#{1,6}\s+/, "").trim() : String(fallback || "").trim();