Embedding blocks with thoughts in messages - cierru/st-stepped-thinking GitHub Wiki

Description

Notice: This guide applies only to the "Separated Thoughts" mode

Have you ever wanted thoughts to appear as part of a message instead of a separate one? If so, this article is for you!

image

There is a script for Tampermonkey (which also works with Violentmonkey) that implements this feature. It was tested on Firefox, but it should work on Chrome as well.

How to install

  1. Download the Tampermonkey (or Violentmonkey) extension for your browser. For example, use this link.
  2. Open the extension menu and press the "+" button to create a new script.
  3. Copy and paste the script below into the new script editor.
  4. Save the script.
  5. Ensure the script is active on your SillyTavern page. Reload the page for the script to take effect.

Script

Script
// ==UserScript==
// @name         Embedded Thoughts
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  Embeds thoughts into messages.
// @author       You
// @match        http://127.0.0.1:8000/
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Select the target node
    const chatDiv = document.getElementById("chat");

    let thoughtmesid = null;
    let thoughts_for = null;

    // Ensure the #chat element exists
    if (!chatDiv) {
        console.error("[ET] #chat element not found!");
        return;
    }

    // Create a MutationObserver to monitor child nodes
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if(chatDiv.childNodes.length === 0) {
                console.debug("[ET] Chat is empty.");
            }
            if (mutation.type === "childList") {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE && node.tagName === "DIV") {
                        let mesid = node.getAttribute("mesid");
                        // Reset if a wild mesid=0 appears
                        if (mesid === "0") {
                            thoughtmesid = null;
                            thoughts_for = null;
                            console.debug("[ET] Reset.");
                        }
                        // If message is a thought
                        if (SillyTavern.getContext().chat[mesid].is_thoughts) {
                            // check if previous message was a thought
                            if(thoughtmesid) {
                                console.debug("[ET] Redundand thought found.", SillyTavern.getContext().chat[thoughtmesid]);
                                chatDiv.querySelector(`[mesid="${thoughtmesid}"]`).style.display = "";
                                chatDiv.querySelector(`[mesid="${thoughtmesid}"]`).style.opacity = "0.3";
                            }
                            thoughtmesid = mesid;
                            thoughts_for = SillyTavern.getContext().chat[mesid].thoughts_for;
                        }
                        // If node is a normal message and we have a thought for it
                        if (!SillyTavern.getContext().chat[mesid].is_thoughts && thoughtmesid && SillyTavern.getContext().chat[mesid].name === thoughts_for) {
                            // assign a random id to thoughtmes and mes to create a relationship between them
                            let thoughtid = Math.floor(Math.random() * 10000)
                            chatDiv.querySelector(`[mesid="${thoughtmesid}"]`).setAttribute("thoughtid", thoughtid);
                            chatDiv.querySelector(`[mesid="${mesid}"]`).setAttribute("relthoughtid", thoughtid);
                            console.debug("[ET] Message with a thought found..", node);
                            // hide thought mes
                            chatDiv.querySelector(`[mesid="${thoughtmesid}"]`).style.display = "none";
                            // Create thought elements in message
                            createThoughtElements(node);
                            transferThoughts(node);
                            //add event to msg delete
                            const deleteButton = node.querySelector(".mes_block .ch_name .mes_edit_buttons .mes_edit_delete");
                            if (deleteButton) {
                                // Add your listener to run in the capture phase
                                deleteButton.addEventListener('click', function (event) {
                                    let relthoughtid = event.currentTarget.parentNode.parentNode.parentNode.parentNode.getAttribute("relthoughtid");
                                    //Delayed deletion of the thought message. Deleting it now breaks current active jquery selector.
                                    setTimeout(() => deleteThought(relthoughtid), 200);
                                });
                            }
                            thoughtmesid = null;
                            thoughts_for = null;
                        }
                    }
                });
            }
        });
    });

    // Create observer for chat div
    const config = { childList: true, subtree: false };
    observer.observe(chatDiv, config);
    console.debug("[ET] Observing #chat for new divs...");

    // Add some styling
    const thoughtsStyle = `
.mes_thought div details pre code{
  background-color: rgba(0,0,0,0);
  border-style: none;
}

.mes_thought div details pre code i{
  display: none;
}
    `;

    // Inject the styles into the document
    const styleElement = document.createElement('style');
    styleElement.textContent = thoughtsStyle;
    document.head.appendChild(styleElement);

    function deleteThought(thoughtid) {
        // Deletes a message by thoughtid with SlashCommands

        const thoughtmsg = chatDiv.querySelector(`[thoughtid="${thoughtid}"]`);
        let thoughtmsgid = thoughtmsg.getAttribute('mesid');
        SillyTavern.getContext().executeSlashCommands(`/cut ${thoughtmsgid}`);
        console.debug("[ET] Thought message deleted.", thoughtid);
    }

    function transferThoughts(node) {
        // Transfers the content of the original thought message to the message


        // Find the target details element in the provided node
        const targetDetails = node.querySelector(".mes_block .mes_thought div details");
        if (!targetDetails) {
            console.error("[ET] Target details not found in the provided node.");
            return;
        }

        // Find the source details element in the global thoughtDiv
        const sourceDetails = chatDiv.querySelector(`[thoughtid="${node.getAttribute('relthoughtid')}"]`).querySelector(".mes_block .mes_text details");
        if (!sourceDetails) {
            console.error("Source details not found in the global thoughtDiv.");
            return;
        }

        targetDetails.appendChild(document.createElement("hr"));

        // clone the pre elements to the message
        const preElements = sourceDetails.querySelectorAll("pre");
        preElements.forEach(pre => {
            targetDetails.appendChild(pre.cloneNode(true));
            targetDetails.appendChild(document.createElement("hr"));
        });

        // Some styling
        targetDetails.querySelectorAll("pre").forEach(element => {
            element.style.fontSize = "smaller";
        });
        targetDetails.querySelectorAll("pre, pre code, pre code span").forEach(element => {
            //pre.style.fontSize = "smaller";
            element.style.fontFamily = "var(--mainFontFamily)";
        });
        console.debug("[ET] Child elements copied successfully.");
    }

    function createThoughtElements(node) {
        // Creates the basic structure in the message where the thought elements get cloned into

        // Find the .mes_block inside node
        const mesBlock = node.querySelector(".mes_block");
        const mesText = mesBlock.querySelector(".mes_text");
        if (!mesBlock) {
            console.error("[ET] .mes_block not found in NodeB");
            return;
        }
        if (!mesText) {
            console.error("[ET] .mes_text not found in NodeB");
            return;
        }

        // Create the new structure
        const mesThoughtDiv = document.createElement("div");
        mesThoughtDiv.className = "mes_thought";

        const innerDiv = document.createElement("div");

        const details = document.createElement("details");
        details.setAttribute("type", "executing");

        const summary = document.createElement("summary");
        summary.style.fontSize = "smaller";
        summary.style.fontFamilty = "var(--mainFontFamily)";
        summary.textContent = "Thoughts 💭";

        // Append elements to build the structure
        details.appendChild(summary);
        innerDiv.appendChild(details);

        mesThoughtDiv.appendChild(innerDiv);

        // Append the new structure to the .mes_block of NodeB
        mesBlock.insertBefore(mesThoughtDiv, mesText);

    }
})();

Troubleshooting

If the script does not load, try modifying the 7th line of the script (e.g., by removing the trailing slash):

// @match        http://127.0.0.1:8000/

Future

A similar UI is planned to become the default in future versions of the extension.

⚠️ **GitHub.com Fallback** ⚠️