(async function() {
'use strict';
/**
* Some easy-to-use config for the script handler,
* for if you want to implement custom JS behavior on app load, etc
*
* @property {() => boolean} onBeforeLoad - dis/allows JSTScript loading (return false to disallow)
* @property {(e: Error) => void} onError - executes when JSTscript would throw an error, may not catch internal ST errors
* @property {(sillyService: SillyService) => void} onStart - executes when the user accepts the "Load JS app" popup, can access the ST context (eg.: conversation) already
*/
const CONFIG = Object.freeze({
onBeforeLoad: () => {
console.log("Init start...");
//put JS logic here whether to load the app or not (optional)
return true; //return false to stop init
},
//
onError: (e) => {
console.trace("Unhandled exception", e);
//put error handler JS code here (optional)
},
//
onStart: (sillyService) => {
console.log("Welcome popup accepted");
if (sillyService.getIsLoaded()) {
sillyService.renderDevTools();
sillyService.applySTProxies();
//put your JS application code here, the following is an example/showcase
console.log("Variables", JSON.stringify(sillyService.getServiceContext().getChatContext().chatMetadata.variables ?? {}));
//sillyService.sendSystemMessage("test system msg");
//sillyService.sendNarratorMessage("test narrator msg");
} else {
console.error("SillyService failed to load");
}
}
});
/**
* DO NOT FUCK AROUND WITH THE STUFF BELOW
* UNLESS YOU KNOW WHAT YOU ARE DOING
*/
/**
* The main service provider
* - hooks into the internal SillyTavern context (eg.: characters, conversation variables, utils for sending messages, etc)
* - provides easy-to-use utility methods (eg.: sending a system message, showing a confirm popup)
* - handles the JSTScript dev tools window
* - applies proxy functions over some ST callback (eg.: on sending a message)
*/
class SillyService {
/**
* Whether SillyService is inited
*/
_isLoaded = false;
/**
* Contains the SillyTavern context
*/
_ctx = null;
/**
* Inits the SillyService app, loads the ST context, applies proxies, renders dev toos, etc
*/
init = async () => {
await new Promise(r => setTimeout(r, 1000));
const { libs, getContext } = window["SillyTavern"];
//console.log("libs", libs);
const context = await getContext();
console.table("context", context);
const {
chat,
Popup,
callPopup, //callPopup(text, type = 'text' | 'confirm' | 'input', inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
callGenericPopup,
addOneMessage,
characterId,
characters,
chatId,
chatMetadata,
eventSource,
generate,
generateQuietPrompt,
getCurrentChatId,
getRequestHeaders,
//getTokenCount,
getTokenizerModel,
isMobile,
isToolCallingSupported,
mainApi,
maxContext,
messageFormatting,
name1, //player
name2, //ST system
onlineStatus,
registerDebugFunction,
registerDataBankScraper,
registerFunctionTool,
unregisterFunctionTool,
registerHelper,
registerMacro,
unregisterMacro,
registerSlashCommand,
reloadCurrentChat,
saveChat,
saveReply,
saveMetadata,
sendGenerationRequest,
sendSreamingRequest,
hideLoader,
showLoader,
stopGeneration,
streamingProcessor,
substituteParams,
substituteParamsExtended,
t, //translate?
translate,
timestampToMoment,
updateChatMetadata
} = context;
//wait for app/chat to load
await new Promise(async r => {
while(true) {
await new Promise(_r => setTimeout(_r, 500));
if (getCurrentChatId()) {
console.log("Loaded app", getCurrentChatId());
break;
}
}
r();
});
const {
Generate,
deleteLastMessage,
deleteSwipe,
deactivateSendButtons,
activateSendButtons,
generateRaw,
getMaxContextSite,
getNextMessageId,
getSettings,
getThumbnailUrl,
isChatSaving,
isStreamingEnabled,
pingServer,
scrollChatToBottom,
sendMessageAsUser,
sendSystemMessage,
sendTextareaMessage,
startStatusLoading,
stopStatusLoading,
chat_metadata,
getMaxContextSize,
} = await import('/script.js')
.then(module => {
console.log("script.js module", module);
return module;
})
.catch(e => console.error("Failed to script.js module", e));
const {
getTokenCountAsync,
getTokenCount
} = await import('/scripts/tokenizers.js')
.then(module => {
console.log("tokenizers.js module", module);
return module;
})
.catch(e => console.error("Failed to tokenizers.js module", e));
const {
sendNarratorMessage
} = await import('/scripts/slash-commands.js')
.then(module => {
console.log("slash-commands.js module", module);
return module;
})
.catch(e => console.error("Failed to slash-commands.js module", e));
console.table("Variables", chat_metadata, JSON.stringify(chat_metadata.variables));
this._isLoaded = true;
this._ctx = {
getChatContext: getContext,
chat,
Popup,
callPopup, //callPopup(text, type = 'text' | 'confirm' | 'input', inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
callGenericPopup,
addOneMessage,
characterId,
characters,
chatId,
chatMetadata,
eventSource,
generate,
generateQuietPrompt,
getCurrentChatId,
getRequestHeaders,
getTokenCount,
getTokenizerModel,
isMobile,
isToolCallingSupported,
mainApi,
maxContext,
messageFormatting,
name1, //player
name2, //ST system
onlineStatus,
registerDebugFunction,
registerDataBankScraper,
registerFunctionTool,
unregisterFunctionTool,
registerHelper,
registerMacro,
unregisterMacro,
registerSlashCommand,
reloadCurrentChat,
saveChat,
saveReply,
saveMetadata,
sendGenerationRequest,
sendSreamingRequest,
hideLoader,
showLoader,
stopGeneration,
streamingProcessor,
substituteParams,
substituteParamsExtended,
t, //translate?
translate,
timestampToMoment,
updateChatMetadata,
Generate,
deleteLastMessage,
deleteSwipe,
deactivateSendButtons,
activateSendButtons,
generateRaw,
getMaxContextSite,
getNextMessageId,
getSettings,
getThumbnailUrl,
isChatSaving,
isStreamingEnabled,
pingServer,
scrollChatToBottom,
sendMessageAsUser,
sendSystemMessage,
sendTextareaMessage,
startStatusLoading,
stopStatusLoading,
chat_metadata,
libs,
sendNarratorMessage,
getMaxContextSize,
getTokenCountAsync,
getTokenCount
};
};
/**
* Logs errors, shows an error snackbar in ST
*/
static onError = (e, msg = "Unhandled exception") => {
console.error(msg, e);
CONFIG.onError(e);
SillyService.showSnackbar(String(e), "error");
};
/**
* Whether SillyService is inited, resolves the ref to the class property
*/
getIsLoaded = () => !!this._isLoaded;
/**
* Returns an immutable copy of the ST context (does not resolve nested refs)
*/
getServiceContext = () => Object.freeze(this._ctx);
/**
* Shows a confirmation popup to the ST user
*
* @param {string} text - The popup text (probably can be an HTML string)
* @param {() => void} [onOkClick] - optional, what should happen on confirming the modal
* @param {string} [okButtonText] - optional, the button text label
*/
showConfirmPopup = (text, onOkClick = () => console.log("popup OK click"), okButtonText = "Ok") => {
try {
const popup = document.querySelector("#dialogue_popup_holder");
const popupHeader = popup.querySelector("#dialogue_popup_text");
const popupInput = popup.querySelector("#dialogue_popup_input");
const popupControls = popup.querySelector("#dialogue_popup_controls");
const okButton = popupControls.querySelector("#dialogue_popup_ok");
okButton.style.width = "max-content";
const _onOkClick = (e) => {
SillyService.showSnackbar("Loaded custom JS application");
try {
onOkClick();
} catch (e) {
//noop
}
okButton.removeEventListener("click", _onOkClick);
};
okButton.addEventListener("click", _onOkClick);
this._ctx.callPopup(text, 'confirm', '', { okButton: okButtonText });
} catch (e) {
SillyService.onError(e, "Failed to execute showPopup");
}
};
/**
* Sends a ghost system message (visible to the user, but not added to the chat history)
*
* @param {string} text - The message text
*/
sendSystemMessage = (text) => {
try {
this._ctx.sendSystemMessage('generic', text);
} catch (e) {
SillyService.onError(e, "Failed to execute sendSystemMessage");
}
};
/**
* Sends a narrator system message (visible to the user, AND added to the chat history)
*
* @param {string} text - The message text
*/
sendNarratorMessage = (text) => {
try {
this._ctx.sendNarratorMessage(
{
compact: "false", //this has to be a string because :^)
at: this._ctx.getNextMessageId(),
},
text
);
} catch (e) {
SillyService.onError(e, "Failed to execute sendNarratorMessage");
}
};
/**
* Shows a toast message to the ST user
*
* @param {string} message - The message text
* @param {"info" | "warn" | "error"} - For color coding, iconography
*/
static showSnackbar = (message, type = "info") => {
//toastr is always defined if ST is running
toastr?.[type]?.(message);
};
/**
* Gets the currently loaded conversation message history
*
* @param {boolean} includeSystem - Whether to also include system/ghost messages
*/
getChatHistory = (includeSystem) => {
const maxTokenCount = this._ctx.getMaxContextSize();
let currentTokenCount = 0;
const chatHistory = [];
for (const message of this._ctx.chat) {
if (!includeSystem && (message.is_system || message.name === "System")) {
continue;
}
const msgTokenCount = this._ctx.getTokenCount(message.mes);
if (currentTokenCount + msgTokenCount >= maxTokenCount) {
break;
}
currentTokenCount += msgTokenCount;
chatHistory.push(message);
}
return chatHistory;
};
/**
* Renders the JSTScript devtools sidebar
*/
renderDevTools = () => {
const body = document.querySelector("body");
const renderToggle = () => {
const toggleElement = document.createElement("div");
toggleElement.style.cursor = "pointer";
toggleElement.textContent = "[Toggle SillyDevTools]";
return toggleElement;
};
const renderWarningText = (text) => {
const warningTextElement = document.createElement("div");
warningTextElement.style.color = "var(--SmartThemeQuoteColor)";
warningTextElement.style.fontSize = "calc(var(--mainFontSize) * 0.7)";
warningTextElement.style.marginTop = "6px";
warningTextElement.textContent = text;
return warningTextElement;
};
const renderHelperText = (text) => {
const helperTextElement = document.createElement("div");
helperTextElement.classList.add("toggle-description");
helperTextElement.textContent = text;
return helperTextElement;
};
const renderCheckbox = (labelText) => {
const checkboxLabelElement = document.createElement("label");
checkboxLabelElement.style.display = "flex";
checkboxLabelElement.style.flexDirection = "row";
checkboxLabelElement.style.alignItems = "center";
const checkboxElement = document.createElement("input");
checkboxElement.setAttribute("type", "checkbox");
const checkboxTextElement = document.createElement("span");
checkboxTextElement.textContent = labelText;
checkboxLabelElement.appendChild(checkboxElement);
checkboxLabelElement.appendChild(checkboxTextElement);
return { checkboxLabelElement, checkboxElement };
};
const renderOpenableSection = (title, renderContent, isOpen = false) => {
const sectionElement = document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
sectionElement.style.marginTop = "12px";
sectionElement.style.borderBottom = "1px silver dashed";
const titleElement = document.createElement("b");
titleElement.textContent = title;
titleElement.style.cursor = "pointer";
sectionElement.appendChild(titleElement);
const contentElement = renderContent();
contentElement.style.display = isOpen ? "flex" : "none";
contentElement.style.paddingLeft = "8px";
sectionElement.appendChild(contentElement);
titleElement.addEventListener("click", () => {
const isShown = contentElement.style.display != "none";
contentElement.style.display = isShown ? "none" : "flex";
});
return sectionElement;
};
const renderMonitorTopSection = (el = null) => {
const renderVariablesSection = (el = null) => {
//TODO: niceify
const renderVariablesString = (textarea) => {
const variables = this._ctx.getChatContext().chatMetadata.variables;
textarea.value = "";
for (const varname in variables) {
textarea.value += `${varname}: ${JSON.stringify(variables[varname])}` + '\n';
}
};
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
const helperTextElement = renderHelperText("Shows all current vars for the active chat.");
sectionElement.appendChild(helperTextElement);
const textareaElement = document.createElement("textarea");
textareaElement.style.height = "200px";
textareaElement.setAttribute("disabled", "disabled");
renderVariablesString(textareaElement);
sectionElement.appendChild(textareaElement);
const refreshButtonElement = document.createElement("button");
refreshButtonElement.textContent = "Refresh"
refreshButtonElement.classList.add("menu_button");
refreshButtonElement.addEventListener("click", () => renderVariablesString(textareaElement));
sectionElement.appendChild(refreshButtonElement);
return sectionElement;
};
const renderInjectsSection = (el = null) => {
//TODO: niceify
//https://github.com/SillyTavern/SillyTavern/blob/fb48d250419cfbf327cfd13c4609b35bce1e600c/public/scripts/slash-commands.js#L1931C5-L1940C7
const buildTextValue = (textElement) => {
//TODO: put into _ctx from https://github.com/SillyTavern/SillyTavern/blob/fb48d250419cfbf327cfd13c4609b35bce1e600c/public/script.js#L588
const promptPositions = {
NONE: -1,
IN_PROMPT: 0,
IN_CHAT: 1,
BEFORE_PROMPT: 2,
};
//TODO: put into _ctx from https://github.com/SillyTavern/SillyTavern/blob/fb48d250419cfbf327cfd13c4609b35bce1e600c/public/script.js#L598C1-L602C2
const promptRoles = {
SYSTEM: 0,
USER: 1,
ASSISTANT: 2,
};
const injectsStr = Object.entries(this._ctx.getChatContext().chatMetadata.script_injects ?? {})
.map(([id, inject]) => {
const position = Object.entries(promptPositions);
const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
return `<li>${id}: <code>${inject.value}</code> (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? promptRoles.SYSTEM})</li>`;
})
.join('\n');
textElement.innerHTML = injectsStr || 'No script injections for the current chat';
};
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
const helperTextElement = renderHelperText("Shows all current injects for the active chat.");
sectionElement.appendChild(helperTextElement);
const injectsTextElement = document.createElement("ul");
buildTextValue(injectsTextElement);
sectionElement.appendChild(injectsTextElement);
const refreshButtonElement = document.createElement("button");
refreshButtonElement.textContent = "Refresh"
refreshButtonElement.classList.add("menu_button");
refreshButtonElement.addEventListener("click", () => buildTextValue(injectsTextElement));
sectionElement.appendChild(refreshButtonElement);
//TODO: create / edit popup
return sectionElement;
};
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
sectionElement.style.gap = "8px";
const helperTextElement = renderHelperText("Chat and prompt monitoring.");
sectionElement.appendChild(helperTextElement);
const variablesSection = renderOpenableSection("Variables", renderVariablesSection);
sectionElement.appendChild(variablesSection);
const injectsSection = renderOpenableSection("Injects", renderInjectsSection);
sectionElement.appendChild(injectsSection);
return sectionElement;
};
const renderChatUtilsTopSection = (el = null) => {
const renderSystemMessageSection = (el = null) => {
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
const helperTextElement = renderHelperText("Send a system message to the current chat.");
sectionElement.appendChild(helperTextElement);
const textareaElement = document.createElement("textarea");
textareaElement.style.height = "120px";
textareaElement.placeholder = "Enter your message here...";
sectionElement.appendChild(textareaElement);
const controlsContainerElement = document.createElement("div");
controlsContainerElement.style.display = "flex";
controlsContainerElement.style.flexDirection = "row";
sectionElement.appendChild(controlsContainerElement);
const submitButtonElement = document.createElement("button");
submitButtonElement.textContent = "Send"
submitButtonElement.classList.add("menu_button");
controlsContainerElement.appendChild(submitButtonElement);
const { checkboxLabelElement, checkboxElement } = renderCheckbox("Send as ghost (invisible to the AI)");
controlsContainerElement.appendChild(checkboxLabelElement);
submitButtonElement.addEventListener("click", async () => {
try {
this._ctx.deactivateSendButtons();
submitButtonElement.style.display = "none"; //poor man's disable
const isGhost = Boolean(checkboxElement.checked);
await this[isGhost ? "sendSystemMessage" : "sendNarratorMessage"](textareaElement.value);
} catch (e) {
SillyService.onError(e, "Failed to send system message");
} finally {
textareaElement.value = "";
this._ctx.activateSendButtons();
submitButtonElement.style.display = "block";
}
});
return sectionElement;
};
const renderGenerateSection = (el = null) => {
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
sectionElement.style.gap = "8px";
const helperTextElement = renderHelperText("Appends message returned by JS code to the end of the prompt as if sent by the user. (history: { mes:string; name:string }[], sillyService: SillyService) => string");
sectionElement.appendChild(helperTextElement);
const warningTextElement = renderWarningText("Do NOT run scripts you can't verify as not harmful!");
sectionElement.appendChild(warningTextElement);
const { checkboxElement: systemCheckboxElement, checkboxLabelElement: systemCheckboxLabelElement } = renderCheckbox("Include system messages");
sectionElement.appendChild(systemCheckboxLabelElement);
const textareaElement = document.createElement("textarea");
textareaElement.style.height = "200px";
textareaElement.value = "(history, sillyService) => { return history.map(m => m.mes).join('').toUpperCase(); }";
sectionElement.appendChild(textareaElement);
const submitButtonElement = document.createElement("button");
submitButtonElement.textContent = "Send"
submitButtonElement.classList.add("menu_button");
sectionElement.appendChild(submitButtonElement);
submitButtonElement.addEventListener("click", async () => {
try {
this._ctx.deactivateSendButtons();
submitButtonElement.style.display = "none"; //poor man's disable
const func = eval(textareaElement.value);
const chatHistory = this.getChatHistory(systemCheckboxElement.checked);
const normalizedChatHistory = func(chatHistory, this);
await this._ctx.Generate('normal', {
quiet_prompt: normalizedChatHistory,
quietToLoud: false
});
} catch (e) {
SillyService.onError(e, "Failed to generate");
} finally {
this._ctx.activateSendButtons();
submitButtonElement.style.display = "block";
}
});
return sectionElement;
};
const renderGenrawSection = (el = null) => {
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
sectionElement.style.gap = "8px";
const helperTextElement = renderHelperText("Generate messages independently of the chat; does not use the prompt set.");
sectionElement.appendChild(helperTextElement);
const warningTextElement = renderWarningText("May not work with APIs that require streaming.");
sectionElement.appendChild(warningTextElement);
const { checkboxElement: clearCheckboxElement, checkboxLabelElement: clearCheckboxLabelElement } = renderCheckbox("Clear input after sending");
sectionElement.appendChild(clearCheckboxLabelElement);
const textareaElement = document.createElement("textarea");
textareaElement.style.height = "120px";
textareaElement.placeholder = "Enter your message here...";
sectionElement.appendChild(textareaElement);
const submitButtonElement = document.createElement("button");
submitButtonElement.textContent = "Send"
submitButtonElement.classList.add("menu_button");
sectionElement.appendChild(submitButtonElement);
submitButtonElement.addEventListener("click", async () => {
try {
this._ctx.deactivateSendButtons();
submitButtonElement.style.display = "none"; //poor man's disable
const result = await this._ctx.generateRaw(textareaElement.value);
console.log("genraw result", result);
await this.sendNarratorMessage(result);
} catch (e) {
SillyService.onError(e, "Failed to genraw");
} finally {
if (clearCheckboxElement.checked) {
textareaElement.value = "";
}
this._ctx.activateSendButtons();
submitButtonElement.style.display = "block";
}
});
return sectionElement;
};
const renderSummarizeSection = (el = null) => {
const replaceHistoryPlaceholder = (prompt, messageNumber, includeSystem) => {
const chatHistory = this._ctx.chat.slice(-1 * messageNumber, this._ctx.chat.length);
const normalizedChatHistory = chatHistory
.filter(({ is_system, name }) => includeSystem ? true : !(is_system || name === "System"))
.map(({ mes, name }) => ({
mes,
name
}));
return prompt.replace(/{{chatHistory}}/g, JSON.stringify(normalizedChatHistory));
};
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
sectionElement.style.gap = "8px";
const helperTextElement = renderHelperText("Summarize the last n messages; does not use the prompt set.");
sectionElement.appendChild(helperTextElement);
const warningTextElement = renderWarningText("May not work with APIs that require streaming.");
sectionElement.appendChild(warningTextElement);
const numberInputLabelElement = document.createElement("label");
numberInputLabelElement.style.display = "flex";
numberInputLabelElement.style.flexDirection = "row";
numberInputLabelElement.style.alignItems = "center";
const numberInputElement = document.createElement("input");
numberInputElement.setAttribute("type", "number");
numberInputElement.value = 5;
numberInputElement.classList.add("text_pole");
numberInputElement.style.width = "50px";
const numberInputTextElement = document.createElement("span");
numberInputTextElement.textContent = "Number of messages to build history";
numberInputLabelElement.appendChild(numberInputElement);
numberInputLabelElement.appendChild(numberInputTextElement);
sectionElement.appendChild(numberInputLabelElement);
const { checkboxElement: sysMsgCheckboxElement, checkboxLabelElement: sysMsgCheckboxLabelElement } = renderCheckbox("Include system messages");
sectionElement.appendChild(sysMsgCheckboxLabelElement);
const textareaElement = document.createElement("textarea");
textareaElement.style.height = "120px";
textareaElement.placeholder = "Enter your summarization prompt here...";
textareaElement.value = "<instruction>Write a summary of the key events in the following conversation</instruction>\n\n<conversation>{{chatHistory}}</conversation>";
sectionElement.appendChild(textareaElement);
const submitButtonElement = document.createElement("button");
submitButtonElement.textContent = "Send"
submitButtonElement.classList.add("menu_button");
sectionElement.appendChild(submitButtonElement);
submitButtonElement.addEventListener("click", async () => {
try {
this._ctx.deactivateSendButtons();
submitButtonElement.style.display = "none"; //poor man's disable
const prompt = replaceHistoryPlaceholder(textareaElement.value, Number(numberInputElement.value), Boolean(sysMsgCheckboxElement.checked));
console.log("genraw summary prompt", prompt);
const result = await this._ctx.generateRaw(prompt);
console.log("genraw summary result", result);
await this.sendNarratorMessage(result);
} catch (e) {
SillyService.onError(e, "Failed to genraw summary");
} finally {
this._ctx.activateSendButtons();
submitButtonElement.style.display = "block";
}
});
return sectionElement;
};
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
sectionElement.style.gap = "8px";
const helperTextElement = renderHelperText("Utilities for sending messages and manipulating the conversation.");
sectionElement.appendChild(helperTextElement);
const systemMessageSection = renderOpenableSection("System messages", renderSystemMessageSection);
sectionElement.appendChild(systemMessageSection);
const generateSection = renderOpenableSection("Generate", renderGenerateSection);
sectionElement.appendChild(generateSection);
const genrawSection = renderOpenableSection("Genraw", renderGenrawSection);
sectionElement.appendChild(genrawSection);
const summarizeSection = renderOpenableSection("Summarization", renderSummarizeSection);
sectionElement.appendChild(summarizeSection);
return sectionElement;
};
const renderJSTopSection = (el = null) => {
const renderExecuteJSSection = (el = null) => {
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
const helperTextElement = renderHelperText("Execute custom JS code using ST utils. (sillyService: SillyService) => void");
sectionElement.appendChild(helperTextElement);
const warningTextElement = renderWarningText("Do NOT run scripts you can't verify as not harmful!");
sectionElement.appendChild(warningTextElement);
const textareaElement = document.createElement("textarea");
textareaElement.style.height = "200px";
textareaElement.value = "(sillyService) => { alert(sillyService.getIsLoaded()); }";
sectionElement.appendChild(textareaElement);
const submitButtonElement = document.createElement("button");
submitButtonElement.textContent = "Run"
submitButtonElement.classList.add("menu_button");
sectionElement.appendChild(submitButtonElement);
submitButtonElement.addEventListener("click", () => {
try {
const func = eval(textareaElement.value);
func(this);
} catch (e) {
SillyService.onError(e, "Failed to execute custom JS");
}
});
return sectionElement;
};
const renderHooksSection = (el = null) => {
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
const { newSendButton, oldSendButton } = (() => {
try {
const oldSendButton = document.querySelector("#send_but");
const newSendButton = oldSendButton.cloneNode(true);
return { oldSendButton, newSendButton };
} catch (e) {
console.log("Failed to mount fake send button", e);
return {};
}
})();
if (!oldSendButton || !newSendButton) {
const warningTextElement = renderWarningText("Failed to load! See the console for the error log.");
sectionElement.appendChild(warningTextElement);
return sectionElement;
}
const helperTextElement = renderHelperText("Temp transform the loaded chat before passing it to the model when sending a message. Does not include the currently typed in message. (chat: Message[]; sillyService: SillyService) => Message[]");
sectionElement.appendChild(helperTextElement);
const warningTextElement3 = renderWarningText("Must be a pure function! Do NOT mutate the message objects in place! Do NOT remove or add messages!");
sectionElement.appendChild(warningTextElement3);
const warningTextElement2 = renderWarningText("Only works with the send button, not on enter keypress etc.");
sectionElement.appendChild(warningTextElement2);
const warningTextElement = renderWarningText("Do NOT run scripts you can't verify as not harmful!");
sectionElement.appendChild(warningTextElement);
const textareaElement = document.createElement("textarea");
textareaElement.style.height = "200px";
textareaElement.value += `//remove <thinking> blocks from the messages`;
textareaElement.value += "\n";
textareaElement.value += `(chat, sillyService) => chat.map(msg => ({ ...msg, mes: msg.mes.replace(/<thinking(\\s[^>]*?(?:\\\\")?[^>]*?)?>([\\s\\S]*?)<\\/thinking>/g, '') }))`;
sectionElement.appendChild(textareaElement);
const { checkboxElement: enableCheckboxElement, checkboxLabelElement: enableCheckboxLabelElement } = renderCheckbox("Enable");
sectionElement.appendChild(enableCheckboxLabelElement);
oldSendButton.parentNode.appendChild(newSendButton);
oldSendButton.style.display = "none";
newSendButton.style.borderBottom = "1px solid red";
newSendButton.addEventListener("click", () => {
if (!enableCheckboxElement.checked) {
oldSendButton.click();
return;
};
const originalChat = [...this._ctx.chat];
try {
const func = eval(textareaElement.value);
const transformedChat = func(this._ctx.chat, this);
console.log({ originalChat, transformedChat });
for (let i = 0; i < transformedChat.length; ++i) {
this._ctx.chat[i] = transformedChat[i];
}
oldSendButton.click();
} catch (e) {
SillyService.onError(e, "Failed to execute custom JS");
} finally {
//TODO: remove hax, add a temp eventEmitter for the successful generation (or mutate observe the oldSendButton's classlist)
setTimeout(() => {
for (let i = 0; i < originalChat.length; ++i) {
this._ctx.chat[i] = originalChat[i];
}
}, 500);
}
});
return sectionElement;
};
const sectionElement = el ?? document.createElement("div");
sectionElement.style.display = "flex";
sectionElement.style.flexDirection = "column";
sectionElement.style.gap = "8px";
const helperTextElement = renderHelperText("JS utils.");
sectionElement.appendChild(helperTextElement);
const executeJSSection = renderOpenableSection("Execute JS", renderExecuteJSSection);
sectionElement.appendChild(executeJSSection);
const hooksSection = renderOpenableSection("Hooks", renderHooksSection);
sectionElement.appendChild(hooksSection);
return sectionElement;
};
const renderContent = () => {
const contentContainerElement = document.createElement("div");
const monitoringTopSection = renderOpenableSection("Monitoring", renderMonitorTopSection);
contentContainerElement.appendChild(monitoringTopSection);
const chatUtilsTopSection = renderOpenableSection("Chat utils", renderChatUtilsTopSection);
contentContainerElement.appendChild(chatUtilsTopSection);
const scriptingTopSection = renderOpenableSection("Scripting", renderJSTopSection);
contentContainerElement.appendChild(scriptingTopSection);
//TODO: info retrieval / system CoT
//TODO: send as (manual + with genraw); show list of characters and show thumbnail when selected
return contentContainerElement;
};
const renderContainer = () => {
const containerElement = document.createElement("div");
containerElement.style.top = "0";
containerElement.style.right = "0";
containerElement.style.position = "fixed";
containerElement.style.background = "var(--SmartThemeBlurTintColor)";
containerElement.style.padding = "8px";
containerElement.style.zIndex = "9999"; //9998 for behind default ST backdrops
containerElement.style.borderLeft = "3px silver solid";
containerElement.style.maxHeight = "100vh";
containerElement.style.overflowY = "auto";
containerElement.style.overflowX = "hidden";
const toggleElement = renderToggle();
containerElement.appendChild(toggleElement);
const contentContainerElement = renderContent();
contentContainerElement.style.display = "none";
containerElement.appendChild(contentContainerElement);
toggleElement.addEventListener("click", () => {
const isShown = contentContainerElement.style.display != "none";
contentContainerElement.style.display = isShown ? "none" : "block";
contentContainerElement.style.minWidth = isShown ? "auto" : "calc((100dvw - var(--sheldWidth) - 3px) /2)";
});
return containerElement;
};
try {
const container = renderContainer();
body.appendChild(container);
} catch (e) {
SillyService.onError(e, "Failed to execute renderDevTools");
}
};
/**
* Applies custom proxy functions over ST internals (eg.: on LLM gen)
*/
applySTProxies = () => {
console.log("Applying proxies...");
const generateProxy = new Proxy(this._ctx.Generate, {
apply: function (target, thisArg, args) {
console.log("Called Generate with", ...args);
return target(...args);
}
});
console.log("Applied proxies");
};
}
const main = async () => {
try {
if (!(CONFIG.onBeforeLoad?.() ?? true)) {
console.log("Main script stopped due to CONFIG.onBeforeLoad() returning falsy value");
return;
}
const sillyService = new SillyService();
await sillyService.init();
sillyService.showConfirmPopup("Load custom JS application? If you're not sure why this popup is here say no.", () => CONFIG.onStart(sillyService), "Yes, I'm aware of the risks");
} catch (e) {
SillyService.onError(e, "Failed to execute main");
}
};
await main();
})();