Embedding blocks with thoughts in messages - cierru/st-stepped-thinking GitHub Wiki
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!
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.
- Download the Tampermonkey (or Violentmonkey) extension for your browser. For example, use this link.
- Open the extension menu and press the "+" button to create a new script.
- Copy and paste the script below into the new script editor.
- Save the script.
- Ensure the script is active on your SillyTavern page. Reload the page for the script to take effect.
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);
}
})();
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/
A similar UI is planned to become the default in future versions of the extension.