From dfa25a17962424f44e541d5a204a6c708243dd63 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 16 Oct 2023 02:12:12 -0400 Subject: [PATCH 1/2] Commands: Add ask command The ask command aims to ask another character about a question or topic from the current chat. Essentially, the current chat is taken out and prompted to another character. This command also requires a few fixes to sprites and prompt creation. Signed-off-by: kingbri --- public/script.js | 2 +- .../scripts/extensions/expressions/index.js | 41 ++++----- public/scripts/slash-commands.js | 88 ++++++++++++++++++- 3 files changed, 109 insertions(+), 22 deletions(-) diff --git a/public/script.js b/public/script.js index 33edadb92..b7bd31349 100644 --- a/public/script.js +++ b/public/script.js @@ -3548,7 +3548,7 @@ export function getBiasStrings(textareaText, type) { */ function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) { const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR; - const characterName = (selected_group || chatItem.force_avatar) ? chatItem.name : name2; + const characterName = chatItem?.name ? chatItem.name : name2; const itemName = chatItem.is_user ? chatItem['name'] : characterName; const shouldPrependName = !isNarratorType; diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index aa1b40def..627742b73 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -1,4 +1,4 @@ -import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js"; +import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced, this_chid } from "../../../script.js"; import { dragElement, isMobile } from "../../RossAscends-mods.js"; import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplate } from "../../extensions.js"; import { loadMovingUIState, power_user } from "../../power-user.js"; @@ -493,24 +493,6 @@ async function moduleWorker() { return; } - // character changed - if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { - removeExpression(); - spriteCache = {}; - - //clear expression - let imgElement = document.getElementById('expression-image'); - if (imgElement && imgElement instanceof HTMLImageElement) { - imgElement.src = ""; - } - - //set checkbox to global var - $('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead); - if (extension_settings.expressions.talkinghead) { - setTalkingHeadState(extension_settings.expressions.talkinghead); - } - } - const vnMode = isVisualNovelMode(); const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible'); @@ -531,7 +513,7 @@ async function moduleWorker() { } const currentLastMessage = getLastCharacterMessage(); - let spriteFolderName = getSpriteFolderName(currentLastMessage, currentLastMessage.name); + let spriteFolderName = context.groupId ? getSpriteFolderName(currentLastMessage, currentLastMessage.name) : getSpriteFolderName(); // character has no expressions or it is not loaded if (Object.keys(spriteCache).length === 0) { @@ -1492,6 +1474,25 @@ function setExpressionOverrideHtml(forceClear = false) { moduleWorker(); dragElement($("#expression-holder")) eventSource.on(event_types.CHAT_CHANGED, () => { + // character changed + const context = getContext(); + if (context.groupId !== lastCharacter && context.characterId !== lastCharacter) { + removeExpression(); + spriteCache = {}; + + //clear expression + let imgElement = document.getElementById('expression-image'); + if (imgElement && imgElement instanceof HTMLImageElement) { + imgElement.src = ""; + } + + //set checkbox to global var + $('#image_type_toggle').prop('checked', extension_settings.expressions.talkinghead); + if (extension_settings.expressions.talkinghead) { + setTalkingHeadState(extension_settings.expressions.talkinghead); + } + } + setExpressionOverrideHtml(); if (isVisualNovelMode()) { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index fd65cae34..861bbe0c5 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -21,9 +21,12 @@ import { reloadCurrentChat, sendMessageAsUser, name1, + Generate, + this_chid, + setCharacterName, } from "../script.js"; import { getMessageTimeStamp } from "./RossAscends-mods.js"; -import { resetSelectedGroup } from "./group-chats.js"; +import { resetSelectedGroup, selected_group } from "./group-chats.js"; import { getRegexedString, regex_placement } from "./extensions/regex/engine.js"; import { chat_styles, power_user } from "./power-user.js"; import { autoSelectPersona } from "./personas.js"; @@ -131,6 +134,7 @@ parser.addCommand('flat', setFlatModeCallback, ['default'], ' – sets the messa parser.addCommand('continue', continueChatCallback, ['cont'], ' – continues the last message in the chat', true, true); parser.addCommand('go', goToCharacterCallback, ['char'], '(name) – opens up a chat with the character by its name', true, true); parser.addCommand('sysgen', generateSystemMessage, [], '(prompt) – generates a system message using a specified prompt', true, true); +parser.addCommand('ask', askCharacter, [], '(prompt) – asks a specified character card a prompt', true, true); parser.addCommand('delname', deleteMessagesByNameCallback, ['cancel'], '(name) – deletes all messages attributed to a specified name', true, true); parser.addCommand('send', sendUserMessageCallback, ['add'], '(text) – adds a user message to the chat log without triggering a generation', true, true); @@ -138,6 +142,88 @@ const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; +async function askCharacter(_, text) { + // Prevent generate recursion + $('#send_textarea').val(''); + + // Not supported in group chats + // TODO: Maybe support group chats? + if (selected_group) { + toastr.error("Cannot run this command in a group chat!"); + return; + } + + if (!text) { + console.warn('WARN: No text provided for /ask command') + } + + const parts = text.split('\n'); + if (parts.length <= 1) { + toastr.warning('Both character name and message are required. Separate them with a new line.'); + return; + } + + // Grabbing the message + const name = parts.shift().trim(); + let mesText = parts.join('\n').trim(); + const prevChId = this_chid; + + // Find the character + const chId = characters.findIndex((e) => e.name === name); + if (!characters[chId] || chId === -1) { + toastr.error("Character not found."); + return; + } + + // Override character and send a user message + setCharacterId(chId); + + // TODO: Maybe look up by filename instead of name + const character = characters[chId]; + let force_avatar, original_avatar; + + if (character && character.avatar !== 'none') { + force_avatar = getThumbnailUrl('avatar', character.avatar); + original_avatar = character.avatar; + } + else { + force_avatar = default_avatar; + original_avatar = default_avatar; + } + + setCharacterName(character.name); + + sendMessageAsUser(mesText) + + const restoreCharacter = () => { + setCharacterId(prevChId); + setCharacterName(characters[prevChId].name); + + // Only force the new avatar if the character name is the same + // This skips if an error was fired + const lastMessage = chat[chat.length - 1]; + if (lastMessage && lastMessage?.name === character.name) { + lastMessage.force_avatar = force_avatar; + lastMessage.original_avatar = original_avatar; + } + + // Kill this callback once the event fires + eventSource.removeListener(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter) + } + + // Run generate and restore previous character on error + try { + toastr.info(`Asking ${character.name} something...`); + await Generate('ask_command') + } catch { + restoreCharacter() + } + + // Restore previous character once message renders + // Hack for generate + eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, restoreCharacter); +} + async function sendUserMessageCallback(_, text) { if (!text) { console.warn('WARN: No text provided for /send command'); From ec8d30a19d19629835bb40d8196a23a38a1b1551 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 19 Oct 2023 00:26:00 +0300 Subject: [PATCH 2/2] Remember confirm for assets list --- public/scripts/extensions/assets/confirm.html | 9 +++++ public/scripts/extensions/assets/index.js | 35 ++++++++++++------- 2 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 public/scripts/extensions/assets/confirm.html diff --git a/public/scripts/extensions/assets/confirm.html b/public/scripts/extensions/assets/confirm.html new file mode 100644 index 000000000..1dd52f26c --- /dev/null +++ b/public/scripts/extensions/assets/confirm.html @@ -0,0 +1,9 @@ +
+ Are you sure you want to connect to '{{url}}'? +
+
+ +
diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index 721be64f5..78ea75061 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -4,11 +4,11 @@ TODO: //const DEBUG_TONY_SAMA_FORK_MODE = true import { getRequestHeaders, callPopup } from "../../../script.js"; -import { deleteExtension, extensionNames, installExtension } from "../../extensions.js"; -import { isValidUrl } from "../../utils.js"; +import { deleteExtension, extensionNames, installExtension, renderExtensionTemplate } from "../../extensions.js"; +import { getStringHash, isValidUrl } from "../../utils.js"; export { MODULE_NAME }; -const MODULE_NAME = 'Assets'; +const MODULE_NAME = 'assets'; const DEBUG_PREFIX = " "; let previewAudio = null; let ASSETS_JSON_URL = "https://raw.githubusercontent.com/SillyTavern/SillyTavern-Content/main/index.json" @@ -75,18 +75,18 @@ function downloadAssetsList(url) { label.addClass("fa-check"); this.classList.remove('asset-download-button-loading'); element.on("click", assetDelete); - element.on("mouseenter", function(){ + element.on("mouseenter", function () { label.removeClass("fa-check"); label.addClass("fa-trash"); label.addClass("redOverlayGlow"); - }).on("mouseleave", function(){ + }).on("mouseleave", function () { label.addClass("fa-check"); label.removeClass("fa-trash"); label.removeClass("redOverlayGlow"); }); }; - const assetDelete = async function() { + const assetDelete = async function () { element.off("click"); await deleteAsset(assetType, asset["id"]); label.removeClass("fa-check"); @@ -102,11 +102,11 @@ function downloadAssetsList(url) { label.toggleClass("fa-download"); label.toggleClass("fa-check"); element.on("click", assetDelete); - element.on("mouseenter", function(){ + element.on("mouseenter", function () { label.removeClass("fa-check"); label.addClass("fa-trash"); label.addClass("redOverlayGlow"); - }).on("mouseleave", function(){ + }).on("mouseleave", function () { label.addClass("fa-check"); label.removeClass("fa-trash"); label.removeClass("redOverlayGlow"); @@ -274,24 +274,35 @@ async function updateCurrentAssets() { // This function is called when the extension is loaded jQuery(async () => { // This is an example of loading HTML from a file - const windowHtml = $(await $.get(`${extensionFolderPath}/window.html`)); + const windowHtml = $(renderExtensionTemplate(MODULE_NAME, 'window', {})); const assetsJsonUrl = windowHtml.find('#assets-json-url-field'); assetsJsonUrl.val(ASSETS_JSON_URL); const connectButton = windowHtml.find('#assets-connect-button'); connectButton.on("click", async function () { - const confirmation = await callPopup(`Are you sure you want to connect to '${assetsJsonUrl.val()}'?`, 'confirm') + const url = String(assetsJsonUrl.val()); + const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`; + const skipConfirm = localStorage.getItem(rememberKey) === 'true'; + + const template = renderExtensionTemplate(MODULE_NAME, 'confirm', { url }); + const confirmation = skipConfirm || await callPopup(template, 'confirm'); + if (confirmation) { try { + if (!skipConfirm) { + const rememberValue = Boolean($('#assets-remember').prop('checked')); + localStorage.setItem(rememberKey, String(rememberValue)); + } + console.debug(DEBUG_PREFIX, "Confimation, loading assets..."); - downloadAssetsList(assetsJsonUrl.val()); + downloadAssetsList(url); connectButton.removeClass("fa-plug-circle-exclamation"); connectButton.removeClass("redOverlayGlow"); connectButton.addClass("fa-plug-circle-check"); } catch (error) { console.error('Error:', error); - toastr.error(`Cannot get assets list from ${assetsJsonUrl.val()}`); + toastr.error(`Cannot get assets list from ${url}`); connectButton.removeClass("fa-plug-circle-check"); connectButton.addClass("fa-plug-circle-exclamation"); connectButton.removeClass("redOverlayGlow");