diff --git a/.dockerignore b/.dockerignore index e91e3fe3e..25d43c004 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,4 @@ readme* Start.bat /dist /backups/ +cloudflared.exe diff --git a/.gitignore b/.gitignore index be3012bd6..f7228af4f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ secrets.json public/movingUI/ public/QuickReplies/ content.log +cloudflared.exe diff --git a/Remote-Link.cmd b/Remote-Link.cmd new file mode 100644 index 000000000..f44f232f2 --- /dev/null +++ b/Remote-Link.cmd @@ -0,0 +1,18 @@ +@echo off +echo ======================================================================================================================== +echo WARNING: Cloudflare Tunnel! +echo ======================================================================================================================== +echo This script downloads and runs the latest cloudflared.exe from Cloudflare to set up an HTTPS tunnel to your SillyTavern! +echo Using the randomly generated temporary tunnel URL, anyone can access your SillyTavern over the Internet while the tunnel +echo is active. Keep the URL safe and secure your SillyTavern installation by setting a username and password in config.conf! +echo. +echo See https://docs.sillytavern.app/usage/remoteconnections/ for more details about how to secure your SillyTavern install. +echo. +echo By continuing you confirm that you're aware of the potential dangers of having a tunnel open and take all responsibility +echo to properly use and secure it! +echo. +echo To abort, press Ctrl+C or close this window now! +echo. +pause +if not exist cloudflared.exe curl -Lo cloudflared.exe https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe +cloudflared.exe tunnel --url localhost:8000 diff --git a/public/index.html b/public/index.html index 4a04a45e8..74c7be0d1 100644 --- a/public/index.html +++ b/public/index.html @@ -1555,30 +1555,55 @@ diff --git a/public/script.js b/public/script.js index a5a84e7c5..4748266c1 100644 --- a/public/script.js +++ b/public/script.js @@ -17,6 +17,7 @@ import { loadTextGenSettings, generateTextGenWithStreaming, getTextGenGenerationData, + formatTextGenURL, } from "./scripts/textgen-settings.js"; import { @@ -721,6 +722,7 @@ let is_get_status = false; let is_get_status_novel = false; let is_api_button_press = false; let is_api_button_press_novel = false; +let api_use_mancer_webui = false; let is_send_press = false; //Send generation let add_mes_without_animation = false; @@ -854,9 +856,9 @@ async function getStatus() { type: "POST", // url: "/getstatus", // data: JSON.stringify({ - api_server: - main_api == "kobold" ? api_server : api_server_textgenerationwebui, + api_server: main_api == "kobold" ? api_server : api_server_textgenerationwebui, main_api: main_api, + use_mancer: main_api == "textgenerationwebui" ? api_use_mancer_webui : false, }), beforeSend: function () { }, cache: false, @@ -883,6 +885,11 @@ async function getStatus() { kai_settings.can_use_streaming = canUseKoboldStreaming(data.koboldVersion); } + // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress. + if (online_status == "no_connection" && data.response) { + toastr.error(data.response, "API Error", {timeOut: 5000, preventDuplicates:true}) + } + //console.log(online_status); resultCheckStatus(); if (online_status !== "no_connection") { @@ -2716,6 +2723,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } else if (main_api == 'textgenerationwebui') { generate_data = getTextGenGenerationData(finalPromt, this_amount_gen, isImpersonate); + generate_data.use_mancer = api_use_mancer_webui; } else if (main_api == 'novel') { const this_settings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; @@ -2978,6 +2986,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, activateSendButtons(); //console.log('runGenerate calling showSwipeBtns'); showSwipeButtons(); + + if (main_api == 'textgenerationwebui' && api_use_mancer_webui) { + const errorText = `

Inferencer endpoint is unhappy!

+ Returned status ${data.status} with the reason:
+ ${data.response}`; + callPopup(errorText, 'text'); + } } console.debug('/savechat called by /Generate'); @@ -4554,7 +4569,9 @@ export function setUserName(value) { name1 = default_user_name; console.log(`User name changed to ${name1}`); $("#your_name").val(name1); - toastr.success(`Your messages will now be sent as ${name1}`, 'Current persona updated'); + if (power_user.persona_show_notifications) { + toastr.success(`Your messages will now be sent as ${name1}`, 'Current persona updated'); + } saveSettings("change_name"); } @@ -4653,7 +4670,7 @@ function setUserAvatar() { const personaName = power_user.personas[user_avatar]; if (personaName && name1 !== personaName) { const lockedPersona = chat_metadata['persona']; - if (lockedPersona && lockedPersona !== user_avatar) { + if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) { toastr.info( `To permanently set "${personaName}" as the selected persona, unlock and relock it using the "Lock" button. Otherwise, the selection resets upon reloading the chat.`, `This chat is locked to a different persona (${power_user.personas[lockedPersona]}).`, @@ -4760,7 +4777,9 @@ async function setDefaultPersona() { } console.log(`Removing default persona ${avatarId}`); - toastr.info('This persona will no longer be used by default when you open a new chat.', `Default persona removed`); + if (power_user.persona_show_notifications) { + toastr.info('This persona will no longer be used by default when you open a new chat.', `Default persona removed`); + } delete power_user.default_persona; } else { const confirm = await callPopup(`

Are you sure you want to set "${personaName}" as the default persona?

@@ -4772,7 +4791,9 @@ async function setDefaultPersona() { } power_user.default_persona = avatarId; - toastr.success('This persona will be used by default when you open a new chat.', `Default persona set to ${personaName}`); + if (power_user.persona_show_notifications) { + toastr.success('This persona will be used by default when you open a new chat.', `Default persona set to ${personaName}`); + } } saveSettingsDebounced(); @@ -4835,18 +4856,22 @@ function lockUserNameToChat() { console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`); delete chat_metadata['persona']; saveMetadata(); - toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked'); + if (power_user.persona_show_notifications) { + toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked'); + } updateUserLockIcon(); return; } if (!(user_avatar in power_user.personas)) { console.log(`Creating a new persona ${user_avatar}`); - toastr.info( - 'Creating a new persona for currently selected user name and avatar...', - 'Persona not set for this avatar', - { timeOut: 10000, extendedTimeOut: 20000, }, - ); + if (power_user.persona_show_notifications) { + toastr.info( + 'Creating a new persona for currently selected user name and avatar...', + 'Persona not set for this avatar', + { timeOut: 10000, extendedTimeOut: 20000, }, + ); + } power_user.personas[user_avatar] = name1; power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.BEFORE_CHAR }; } @@ -4855,7 +4880,9 @@ function lockUserNameToChat() { saveMetadata(); saveSettingsDebounced(); console.log(`Locking persona for this chat ${user_avatar}`); - toastr.success(`User persona is locked to ${name1} in this chat`); + if (power_user.persona_show_notifications) { + toastr.success(`User persona is locked to ${name1} in this chat`); + } updateUserLockIcon(); } @@ -5076,11 +5103,13 @@ async function getSettings(type) { setWorldInfoSettings(settings, data); - api_server_textgenerationwebui = - settings.api_server_textgenerationwebui; + api_server_textgenerationwebui = settings.api_server_textgenerationwebui; $("#textgenerationwebui_api_url_text").val( api_server_textgenerationwebui ); + api_use_mancer_webui = settings.api_use_mancer_webui + $('#use-mancer-api-checkbox').prop("checked", api_use_mancer_webui); + $('#use-mancer-api-checkbox').trigger("change"); selected_button = settings.selected_button; @@ -5114,6 +5143,7 @@ async function saveSettings(type) { active_group: active_group, api_server: api_server, api_server_textgenerationwebui: api_server_textgenerationwebui, + api_use_mancer_webui: api_use_mancer_webui, preset_settings: preset_settings, user_avatar: user_avatar, amount_gen: amount_gen, @@ -7508,7 +7538,7 @@ $(document).ready(function () {

Delete the character?

THIS IS PERMANENT!


` ); @@ -7691,16 +7721,28 @@ $(document).ready(function () { } }); - $("#api_button_textgenerationwebui").click(function (e) { + $("#use-mancer-api-checkbox").on("change", function (e) { + const enabled = $("#use-mancer-api-checkbox").prop("checked"); + $("#mancer-api-ui").toggle(enabled); + api_use_mancer_webui = enabled; + saveSettingsDebounced(); + getStatus(); + }); + + $("#api_button_textgenerationwebui").click(async function (e) { e.stopPropagation(); if ($("#textgenerationwebui_api_url_text").val() != "") { - let value = formatKoboldUrl($("#textgenerationwebui_api_url_text").val().trim()); - + let value = formatTextGenURL($("#textgenerationwebui_api_url_text").val().trim()) if (!value) { - callPopup('Please enter a valid URL.', 'text'); + callPopup('Please enter a valid URL.
WebUI URLs should end with /api', 'text'); return; } + const mancer_key = $("#api_key_mancer").val().trim(); + if (mancer_key.length) { + await writeSecret(SECRET_KEYS.MANCER, mancer_key); + } + $("#textgenerationwebui_api_url_text").val(value); $("#api_loading_textgenerationwebui").css("display", "inline-block"); $("#api_button_textgenerationwebui").css("display", "none"); diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index cb1188cf3..88090b512 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -534,12 +534,14 @@ export function dragElement(elmnt) { if (elmntHeader.length) { elmntHeader.off('mousedown').on('mousedown', (e) => { - + hasBeenDraggedByUser = true + observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] }); dragMouseDown(e); }); - $(elmnt).off('mousedown').on('mousedown', () => { isMouseDown = true }) - } else { - elmnt.off('mousedown').on('mousedown', dragMouseDown); + $(elmnt).off('mousedown').on('mousedown', () => { + isMouseDown = true + observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] }); + }) } const observer = new MutationObserver((mutations) => { @@ -618,7 +620,7 @@ export function dragElement(elmnt) { } //prevent resizing from top left into the top bar - if (top <= 40 && maxX >= topBarFirstX && left <= topBarFirstX + if (top < 40 && maxX >= topBarFirstX && left <= topBarFirstX ) { console.debug('prevent topbar underlap resize') elmnt.css('width', width - 1 + "px"); @@ -674,8 +676,6 @@ export function dragElement(elmnt) { } }); - observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] }); - function dragMouseDown(e) { if (e) { @@ -745,6 +745,7 @@ export function dragElement(elmnt) { $("body").css("overflow", ""); // Clear the "data-dragged" attribute elmnt.attr('data-dragged', 'false'); + observer.disconnect() console.debug(`Saving ${elmntName} UI position`) saveSettingsDebounced(); diff --git a/public/scripts/extensions/bulk-edit/index.js b/public/scripts/extensions/bulk-edit/index.js new file mode 100644 index 000000000..b72b662a1 --- /dev/null +++ b/public/scripts/extensions/bulk-edit/index.js @@ -0,0 +1,108 @@ +import { characters, getCharacters, saveSettingsDebounced, handleDeleteCharacter } from "../../../script.js"; + +let is_bulk_edit = false; + +/** + * Toggles bulk edit mode on/off when the edit button is clicked. + */ +function onEditButtonClick() { + console.log("Edit button clicked"); + // toggle bulk edit mode + if (is_bulk_edit) { + disableBulkSelect(); + // hide the delete button + $("#bulkDeleteButton").hide(); + is_bulk_edit = false; + } else { + enableBulkSelect(); + // show the delete button + $("#bulkDeleteButton").show(); + is_bulk_edit = true; + } +} + +/** + * Deletes the character with the given chid. + * + * @param {string} this_chid - The chid of the character to delete. + */ +async function deleteCharacter(this_chid) { + await handleDeleteCharacter("del_ch", this_chid); +} + +/** + * Deletes all characters that have been selected via the bulk checkboxes. + */ +async function onDeleteButtonClick() { + console.log("Delete button clicked"); + + // Create a mapping of chid to avatar + let toDelete = []; + $(".bulk_select_checkbox:checked").each((i, el) => { + const chid = $(el).parent().attr("chid"); + const avatar = characters[chid].avatar; + // Add the avatar to the list of avatars to delete + toDelete.push(avatar); + }); + + // Delete the characters + for (const avatar of toDelete) { + console.log(`Deleting character with avatar ${avatar}`); + await getCharacters(); + + //chid should be the key of the character with the given avatar + const chid = Object.keys(characters).find((key) => characters[key].avatar === avatar); + console.log(`Deleting character with chid ${chid}`); + await deleteCharacter(chid); + } +} + +/** + * Adds the bulk edit and delete buttons to the UI. + */ +function addButtons() { + const editButton = $( + "" + ); + const deleteButton = $( + "" + ); + + $("#charListGridToggle").after(editButton, deleteButton); + + $("#bulkEditButton").on("click", onEditButtonClick); + $("#bulkDeleteButton").on("click", onDeleteButtonClick); +} + +/** + * Enables bulk selection by adding a checkbox next to each character. + */ +function enableBulkSelect() { + $(".character_select").each((i, el) => { + const character = $(el).text(); + const checkbox = $(""); + checkbox.on("change", () => { + // Do something when the checkbox is changed + }); + $(el).prepend(checkbox); + }); + // We also need to disable the default click event for the character_select divs + $(document).on("click", ".bulk_select_checkbox", function (event) { + event.stopImmediatePropagation(); + }); +} + +/** + * Disables bulk selection by removing the checkboxes. + */ +function disableBulkSelect() { + $(".bulk_select_checkbox").remove(); +} + +/** + * Entry point that runs on page load. + */ +jQuery(async () => { + addButtons(); + // loadSettings(); +}); diff --git a/public/scripts/extensions/bulk-edit/manifest.json b/public/scripts/extensions/bulk-edit/manifest.json new file mode 100644 index 000000000..b86a2c82b --- /dev/null +++ b/public/scripts/extensions/bulk-edit/manifest.json @@ -0,0 +1,11 @@ +{ + "display_name": "Bulk Card Editor", + "loading_order": 9, + "requires": [], + "optional": [], + "js": "index.js", + "css": "style.css", + "author": "city-unit", + "version": "1.0.0", + "homePage": "https://github.com/city-unit" +} diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index bddc9132c..6656353c5 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -449,7 +449,7 @@ async function loadLiveChar() { function handleImageChange() { const imgElement = document.querySelector('img#expression-image.expression'); - + if (!imgElement) { console.log("Cannot find addExpressionImage()"); return; @@ -459,6 +459,7 @@ function handleImageChange() { previousSrc = imgElement.src; // Method get IP of endpoint const live2dResultFeedSrc = `${getApiUrl()}/api/live2d/result_feed`; + $('#expression-holder').css({ display: '' }); if (imgElement.src !== live2dResultFeedSrc) { const expressionImageElement = document.querySelector('.expression_list_image'); @@ -922,7 +923,7 @@ async function setExpression(character, expression, force) { if (imgElement) { console.log("setting value"); imgElement.src = getApiUrl() + '/api/live2d/result_feed'; - } + } } } diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index f2e9433ea..e33994bc1 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -1,6 +1,7 @@ import { getStringHash, debounce, waitUntilCondition, extractAllWords } from "../../utils.js"; import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from "../../extensions.js"; import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from "../../../script.js"; +import { is_group_generating, selected_group } from "../../group-chats.js"; export { MODULE_NAME }; const MODULE_NAME = '1_memory'; @@ -333,6 +334,10 @@ async function summarizeChat(context) { async function summarizeChatMain(context, force) { try { + // Wait for group to finish generating + if (selected_group) { + await waitUntilCondition(() => is_group_generating === false, 1000, 10); + } // Wait for the send button to be released waitUntilCondition(() => is_send_press === false, 30000, 100); } catch { diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 133db46e8..d28119a61 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -27,7 +27,7 @@ import { import { registerSlashCommand } from "./slash-commands.js"; -import { delay, debounce } from "./utils.js"; +import { delay } from "./utils.js"; export { loadPowerUserSettings, @@ -184,6 +184,7 @@ let power_user = { persona_description: '', persona_description_position: persona_description_positions.BEFORE_CHAR, + persona_show_notifications: true, custom_stopping_strings: '', fuzzy_search: false, @@ -678,6 +679,7 @@ function loadPowerUserSettings(settings, data) { $('#auto_swipe_blacklist_threshold').val(power_user.auto_swipe_blacklist_threshold); $('#custom_stopping_strings').val(power_user.custom_stopping_strings); $('#fuzzy_search_checkbox').prop("checked", power_user.fuzzy_search); + $('#persona_show_notifications').prop("checked", power_user.persona_show_notifications); $("#console_log_prompts").prop("checked", power_user.console_log_prompts); $('#auto_fix_generated_markdown').prop("checked", power_user.auto_fix_generated_markdown); @@ -1998,6 +2000,11 @@ $(document).ready(() => { saveSettingsDebounced(); }); + $('#persona_show_notifications').on('input', function () { + power_user.persona_show_notifications = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + $(window).on('focus', function () { browser_has_focus = true; }); diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 90edb72f0..00203a106 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -2,6 +2,7 @@ import { callPopup, getRequestHeaders } from "../script.js"; export const SECRET_KEYS = { HORDE: 'api_key_horde', + MANCER: 'api_key_mancer', OPENAI: 'api_key_openai', NOVEL: 'api_key_novel', CLAUDE: 'api_key_claude', @@ -11,6 +12,7 @@ export const SECRET_KEYS = { const INPUT_MAP = { [SECRET_KEYS.HORDE]: '#horde_api_key', + [SECRET_KEYS.MANCER]: '#api_key_mancer', [SECRET_KEYS.OPENAI]: '#api_key_openai', [SECRET_KEYS.NOVEL]: '#api_key_novel', [SECRET_KEYS.CLAUDE]: '#api_key_claude', diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 7bdab35e4..cc984e361 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -10,6 +10,7 @@ export { textgenerationwebui_settings, loadTextGenSettings, generateTextGenWithStreaming, + formatTextGenURL, } const textgenerationwebui_settings = { @@ -94,6 +95,16 @@ function selectPreset(name) { saveSettingsDebounced(); } +function formatTextGenURL(value) { + try { + const url = new URL(value); + if (url.pathname.endsWith('/api')) { + return url.toString(); + } + } catch { } // Try and Catch both fall through to the same return. + return null; +} + function convertPresets(presets) { return Array.isArray(presets) ? presets.map(JSON.parse) : []; } diff --git a/public/style.css b/public/style.css index fda0ee5e3..326d6f09d 100644 --- a/public/style.css +++ b/public/style.css @@ -3040,9 +3040,22 @@ h5 { opacity: 0.4; } + .PastChat_cross:hover { color: red; filter: drop-shadow(0 0 2px red); + -webkit-animation: infinite-spinning 1s ease-out 0s infinite normal; + animation: infinite-spinning 1s ease-out 0s infinite normal; +} + +/* HEINOUS */ +@keyframes infinite-spinning { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } } #export_character_div { @@ -4409,6 +4422,10 @@ toolcool-color-picker { width: 50px; } +.indent20p { + margin-left: 20px; +} + .wi-enter-footer-text { font-size: calc(var(--mainFontSize) * 0.8); color: var(--SmartThemeBodyColor); diff --git a/server.js b/server.js index e6a1d0d20..11146e29b 100644 --- a/server.js +++ b/server.js @@ -146,6 +146,14 @@ let response_getstatus; let first_run = true; + +function get_mancer_headers() { + const api_key_mancer = readSecret(SECRET_KEYS.MANCER); + return api_key_mancer ? { "X-API-KEY": api_key_mancer} : {}; +} + + + //RossAscends: Added function to format dates used in files and chat timestamps to a humanized format. //Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected. //During testing, this performs the same as previous date.now() structure. @@ -640,13 +648,22 @@ app.post("/generate_textgenerationwebui", jsonParser, async function (request, r signal: controller.signal, }; + if (request.body.use_mancer) { + args.headers = Object.assign(args.headers, get_mancer_headers()); + } + try { const data = await postAsync(api_server + "/v1/generate", args); console.log(data); return response_generate.send(data); } catch (error) { + retval = { error: true, status: error.status, response: error.statusText }; console.log(error); - return response_generate.send({ error: true }); + try { + retval.response = await error.json(); + retval.response = retval.response.result; + } catch {} + return response_generate.send(retval); } } }); @@ -710,6 +727,11 @@ app.post("/getstatus", jsonParser, async function (request, response_getstatus = var args = { headers: { "Content-Type": "application/json" } }; + + if (main_api == 'textgenerationwebui' && request.body.use_mancer) { + args.headers = Object.assign(args.headers, get_mancer_headers()); + } + var url = api_server + "/v1/model"; let version = ''; let koboldVersion = {}; @@ -730,18 +752,18 @@ app.post("/getstatus", jsonParser, async function (request, response_getstatus = }; } } - client.get(url, args, function (data, response) { + client.get(url, args, async function (data, response) { if (typeof data !== 'object') { data = {}; } if (response.statusCode == 200) { data.version = version; data.koboldVersion = koboldVersion; - if (data.result != "ReadOnly") { - } else { + if (data.result == "ReadOnly") { data.result = "no_connection"; } } else { + data.response = data.result; data.result = "no_connection"; } response_getstatus.send(data); @@ -3453,6 +3475,10 @@ app.post("/tokenize_via_api", jsonParser, async function (request, response) { body: JSON.stringify({ "prompt": text }), headers: { "Content-Type": "application/json" } }; + + if (main_api == 'textgenerationwebui' && request.body.use_mancer) { + args.headers = Object.assign(args.headers, get_mancer_headers()); + } const data = await postAsync(api_server + "/v1/token-count", args); console.log(data); @@ -3659,6 +3685,7 @@ const SECRETS_FILE = './secrets.json'; const SETTINGS_FILE = './public/settings.json'; const SECRET_KEYS = { HORDE: 'api_key_horde', + MANCER: 'api_key_mancer', OPENAI: 'api_key_openai', NOVEL: 'api_key_novel', CLAUDE: 'api_key_claude',