From 690dc328c5efd1b28bb89dbf6078746905ea2f3b Mon Sep 17 00:00:00 2001 From: Chris Bennight Date: Sun, 12 Nov 2023 18:52:34 -0500 Subject: [PATCH 001/185] update docker registry in compose file to github from dockerhub --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 8edc89f99..13387f544 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -4,7 +4,7 @@ services: build: .. container_name: sillytavern hostname: sillytavern - image: sillytavern/sillytavern:latest + image: ghcr.io/sillytavern/sillytavern:latest ports: - "8000:8000" volumes: From 52c07e089539a04241967b2a94909dc658b047a0 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:36:01 +0900 Subject: [PATCH 002/185] setup Aphrodite-specific API flag handling --- public/index.html | 58 +++++++-------- public/scripts/textgen-settings.js | 111 +++++++++++++++++++---------- 2 files changed, 101 insertions(+), 68 deletions(-) diff --git a/public/index.html b/public/index.html index ac6eda88f..65af5d4ee 100644 --- a/public/index.html +++ b/public/index.html @@ -1142,17 +1142,17 @@ -
+
Repetition Penalty
-
+
Repetition Penalty Range
-
+
Encoder Penalty @@ -1167,7 +1167,7 @@
-
+
No Repeat Ngram Size @@ -1178,22 +1178,22 @@
+ + + - + + +
-
+
Seed
@@ -1312,17 +1312,17 @@
-
+

CFG

-
+
Scale
-
+
Negative Prompt @@ -1849,7 +1849,7 @@
@@ -4707,4 +4707,4 @@ - + \ No newline at end of file diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 4c9f0a5bf..6c898cebb 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -71,8 +71,8 @@ const textgenerationwebui_settings = { banned_tokens: '', //n_aphrodite: 1, //best_of_aphrodite: 1, - //ignore_eos_token_aphrodite: false, - //spaces_between_special_tokens_aphrodite: true, + ignore_eos_token_aphrodite: false, + spaces_between_special_tokens_aphrodite: true, //logits_processors_aphrodite: [], //log_probs_aphrodite: 0, //prompt_log_probs_aphrodite: 0, @@ -124,8 +124,8 @@ const setting_names = [ "legacy_api", //'n_aphrodite', //'best_of_aphrodite', - //'ignore_eos_token_aphrodite', - //'spaces_between_special_tokens_aphrodite', + 'ignore_eos_token_aphrodite', + 'spaces_between_special_tokens_aphrodite', //'logits_processors_aphrodite', //'log_probs_aphrodite', //'prompt_log_probs_aphrodite' @@ -249,6 +249,16 @@ function loadTextGenSettings(data, settings) { $('#textgen_type').val(textgenerationwebui_settings.type); showTypeSpecificControls(textgenerationwebui_settings.type); + //this is needed because showTypeSpecificControls() does not handle NOT declarations + if (isAphrodite()) { + $('[data-forAphro=False]').each(function () { + $(this).hide() + }) + } else { + $('[data-forAphro=False]').each(function () { + $(this).show() + }) + } } export function isMancer() { @@ -277,25 +287,33 @@ jQuery(function () { const type = String($(this).val()); textgenerationwebui_settings.type = type; - /* if (type === 'aphrodite') { - $('[data-forAphro=False]').each(function () { - $(this).hide() - }) - $('[data-forAphro=True]').each(function () { - $(this).show() - }) - $('#mirostat_mode_textgenerationwebui').attr('step', 2) //Aphro disallows mode 1 - $("#do_sample_textgenerationwebui").prop('checked', true) //Aphro should always do sample; 'otherwise set temp to 0 to mimic no sample' - $("#ban_eos_token_textgenerationwebui").prop('checked', false) //Aphro should not ban EOS, just ignore it; 'add token '2' to ban list do to this' - } else { - $('[data-forAphro=False]').each(function () { - $(this).show() - }) - $('[data-forAphro=True]').each(function () { - $(this).hide() - }) - $('#mirostat_mode_textgenerationwebui').attr('step', 1) - } */ + if (isAphrodite()) { + //this is needed because showTypeSpecificControls() does not handle NOT declarations + $('[data-forAphro=False]').each(function () { + $(this).hide() + }) + $('#mirostat_mode_textgenerationwebui').attr('step', 2) //Aphro disallows mode 1 + $("#do_sample_textgenerationwebui").prop('checked', true) //Aphro should always do sample; 'otherwise set temp to 0 to mimic no sample' + $("#ban_eos_token_textgenerationwebui").prop('checked', false) //Aphro should not ban EOS, just ignore it; 'add token '2' to ban list do to this' + //special handling for Aphrodite topK -1 disable state + $('#top_k_textgenerationwebui').attr('min', -1) + if ($('#top_k_textgenerationwebui').val() === '0' || textgenerationwebui_settings['top_k'] === 0) { + textgenerationwebui_settings['top_k'] = -1 + $('#top_k_textgenerationwebui').val('-1').trigger('input') + } + } else { + //this is needed because showTypeSpecificControls() does not handle NOT declarations + $('[data-forAphro=False]').each(function () { + $(this).show() + }) + $('#mirostat_mode_textgenerationwebui').attr('step', 1) + //undo special Aphrodite setup for topK + $('#top_k_textgenerationwebui').attr('min', 0) + if ($('#top_k_textgenerationwebui').val() === '-1' || textgenerationwebui_settings['top_k'] === -1) { + textgenerationwebui_settings['top_k'] = 0 + $('#top_k_textgenerationwebui').val('0').trigger('input') + } + } showTypeSpecificControls(type); setOnlineStatus('no_connection'); @@ -330,8 +348,12 @@ jQuery(function () { const value = Number($(this).val()); $(`#${id}_counter_textgenerationwebui`).val(value); textgenerationwebui_settings[id] = value; + //special handling for aphrodite using -1 as disabled instead of 0 + if ($(this).attr('id') === 'top_k_textgenerationwebui' && isAphrodite() && value === 0) { + textgenerationwebui_settings[id] = -1 + $(this).val(-1) + } } - saveSettingsDebounced(); }); } @@ -481,33 +503,24 @@ function getModel() { } export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImpersonate, cfgValues) { - return { + let APIflags = { 'prompt': finalPrompt, 'model': getModel(), 'max_new_tokens': this_amount_gen, 'max_tokens': this_amount_gen, - 'do_sample': textgenerationwebui_settings.do_sample, 'temperature': textgenerationwebui_settings.temp, - 'temperature_last': textgenerationwebui_settings.temperature_last, 'top_p': textgenerationwebui_settings.top_p, 'typical_p': textgenerationwebui_settings.typical_p, 'min_p': textgenerationwebui_settings.min_p, 'repetition_penalty': textgenerationwebui_settings.rep_pen, - 'repetition_penalty_range': textgenerationwebui_settings.rep_pen_range, - 'encoder_repetition_penalty': textgenerationwebui_settings.encoder_rep_pen, 'frequency_penalty': textgenerationwebui_settings.freq_pen, 'presence_penalty': textgenerationwebui_settings.presence_pen, 'top_k': textgenerationwebui_settings.top_k, 'min_length': textgenerationwebui_settings.min_length, 'min_tokens': textgenerationwebui_settings.min_length, - 'no_repeat_ngram_size': textgenerationwebui_settings.no_repeat_ngram_size, 'num_beams': textgenerationwebui_settings.num_beams, - 'penalty_alpha': textgenerationwebui_settings.penalty_alpha, 'length_penalty': textgenerationwebui_settings.length_penalty, 'early_stopping': textgenerationwebui_settings.early_stopping, - 'guidance_scale': cfgValues?.guidanceScale?.value ?? textgenerationwebui_settings.guidance_scale ?? 1, - 'negative_prompt': cfgValues?.negativePrompt ?? textgenerationwebui_settings.negative_prompt ?? '', - 'seed': textgenerationwebui_settings.seed, 'add_bos_token': textgenerationwebui_settings.add_bos_token, 'stopping_strings': getStoppingStrings(isImpersonate), 'stop': getStoppingStrings(isImpersonate), @@ -521,20 +534,40 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso 'mirostat_mode': textgenerationwebui_settings.mirostat_mode, 'mirostat_tau': textgenerationwebui_settings.mirostat_tau, 'mirostat_eta': textgenerationwebui_settings.mirostat_eta, - 'grammar_string': textgenerationwebui_settings.grammar_string, 'custom_token_bans': isAphrodite() ? toIntArray(getCustomTokenBans()) : getCustomTokenBans(), 'use_mancer': isMancer(), 'use_aphrodite': isAphrodite(), 'use_ooba': isOoba(), 'api_server': isMancer() ? MANCER_SERVER : api_server_textgenerationwebui, 'legacy_api': textgenerationwebui_settings.legacy_api && !isMancer(), + }; + let aphroditeExclusionFlags = { + 'repetition_penalty_range': textgenerationwebui_settings.rep_pen_range, + 'encoder_repetition_penalty': textgenerationwebui_settings.encoder_rep_pen, + 'no_repeat_ngram_size': textgenerationwebui_settings.no_repeat_ngram_size, + 'penalty_alpha': textgenerationwebui_settings.penalty_alpha, + 'temperature_last': textgenerationwebui_settings.temperature_last, + 'do_sample': textgenerationwebui_settings.do_sample, + 'seed': textgenerationwebui_settings.seed, + 'guidance_scale': cfgValues?.guidanceScale?.value ?? textgenerationwebui_settings.guidance_scale ?? 1, + 'negative_prompt': cfgValues?.negativePrompt ?? textgenerationwebui_settings.negative_prompt ?? '', + 'grammar_string': textgenerationwebui_settings.grammar_string, + } + let aphroditeFlags = { //'n': textgenerationwebui_settings.n_aphrodite, //'best_of': textgenerationwebui_settings.n_aphrodite, //n must always == best_of and vice versa - //'ignore_eos': textgenerationwebui_settings.ignore_eos_token_aphrodite, - //'spaces_between_special_tokens': textgenerationwebui_settings.spaces_between_special_tokens_aphrodite, - // 'logits_processors': textgenerationwebui_settings.logits_processors_aphrodite, + 'ignore_eos': textgenerationwebui_settings.ignore_eos_token_aphrodite, + 'spaces_between_special_tokens': textgenerationwebui_settings.spaces_between_special_tokens_aphrodite, + //'logits_processors': textgenerationwebui_settings.logits_processors_aphrodite, //'logprobs': textgenerationwebui_settings.log_probs_aphrodite, //'prompt_logprobs': textgenerationwebui_settings.prompt_log_probs_aphrodite, - }; + } + if (isAphrodite()) { + APIflags = Object.assign(APIflags, aphroditeFlags); + } else { + APIflags = Object.assign(APIflags, aphroditeExclusionFlags); + } + + return APIflags } From 61764a9a2149c7a19a4537757a893f1b6979a212 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:13:39 +0200 Subject: [PATCH 003/185] Change mancer base URL via debug menu --- public/scripts/textgen-settings.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 6c898cebb..2da9e5916 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -11,6 +11,7 @@ import { import { power_user, + registerDebugFunction, } from "./power-user.js"; import { getTextTokens, tokenizers } from "./tokenizers.js"; import { onlyUnique } from "./utils.js"; @@ -29,7 +30,10 @@ export const textgen_types = { }; // Maybe let it be configurable in the future? -export const MANCER_SERVER = 'https://neuro.mancer.tech'; +// (7 days later) The future has come. +const MANCER_SERVER_KEY = 'mancer_server'; +const MANCER_SERVER_DEFAULT = 'https://neuro.mancer.tech'; +export let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEFAULT; const textgenerationwebui_settings = { temp: 0.7, @@ -259,6 +263,15 @@ function loadTextGenSettings(data, settings) { $(this).show() }) } + + registerDebugFunction('change-mancer-url', 'Change Mancer base URL', 'Change Mancer API server base URL', () => { + const result = prompt(`Enter Mancer base URL\nDefault: ${MANCER_SERVER_DEFAULT}`, MANCER_SERVER); + + if (result) { + localStorage.setItem(MANCER_SERVER_KEY, result); + MANCER_SERVER = result; + } + }); } export function isMancer() { From 5fe8f70eb126918d6a9c95bd7c6abd84d80b74b3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 00:16:41 +0200 Subject: [PATCH 004/185] #1345 Add API endpoint for web search interaction --- public/scripts/secrets.js | 1 + server.js | 3 +++ src/secrets.js | 1 + src/serpapi.js | 39 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 44 insertions(+) create mode 100644 src/serpapi.js diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index edf4123dc..1e0133c6e 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -12,6 +12,7 @@ export const SECRET_KEYS = { AI21: 'api_key_ai21', SCALE_COOKIE: 'scale_cookie', PALM: 'api_key_palm', + SERPAPI: 'api_key_serpapi', } const INPUT_MAP = { diff --git a/server.js b/server.js index d86e8df05..c3f47880b 100644 --- a/server.js +++ b/server.js @@ -3528,6 +3528,9 @@ require('./src/classify').registerEndpoints(app, jsonParser); // Image captioning require('./src/caption').registerEndpoints(app, jsonParser); +// Web search extension +require('./src/serpapi').registerEndpoints(app, jsonParser); + const tavernUrl = new URL( (cliArguments.ssl ? 'https://' : 'http://') + (listen ? '0.0.0.0' : '127.0.0.1') + diff --git a/src/secrets.js b/src/secrets.js index f59da2779..6da764529 100644 --- a/src/secrets.js +++ b/src/secrets.js @@ -21,6 +21,7 @@ const SECRET_KEYS = { ONERING_URL: 'oneringtranslator_url', DEEPLX_URL: 'deeplx_url', PALM: 'api_key_palm', + SERPAPI: 'api_key_serpapi', } /** diff --git a/src/serpapi.js b/src/serpapi.js new file mode 100644 index 000000000..38fc6928a --- /dev/null +++ b/src/serpapi.js @@ -0,0 +1,39 @@ +const fetch = require('node-fetch').default; +const { readSecret } = require('./secrets'); + +/** + * Registers the SerpApi endpoints. + * @param {import("express").Express} app + * @param {any} jsonParser + */ +function registerEndpoints(app, jsonParser) { + app.post('/api/serpapi/search', jsonParser, async (request, response) => { + try { + const key = readSecret('serpapi_key'); + + if (!key) { + console.log('No SerpApi key found'); + return response.sendStatus(401); + } + + const { query } = request.body; + const result = await fetch(`https://serpapi.com/search.json?q=${encodeURIComponent(query)}&api_key=${key}`); + + if (!result.ok) { + const text = await result.text(); + console.log('SerpApi request failed', result.statusText, text); + return response.status(500).send(text); + } + + const data = await result.json(); + return response.json(data); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); +} + +module.exports = { + registerEndpoints, +}; From 9169938448b3cee0d3b5fb51126310475a9d3f26 Mon Sep 17 00:00:00 2001 From: Tony Ribeiro Date: Mon, 13 Nov 2023 23:20:36 +0100 Subject: [PATCH 005/185] Fix listing of live2d model file for non-model3 type models. --- src/assets.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/assets.js b/src/assets.js index 0087d3a62..93e2239df 100644 --- a/src/assets.js +++ b/src/assets.js @@ -94,7 +94,7 @@ function registerEndpoints(app, jsonParser) { //console.debug("FILE FOUND:",files) for (let file of files) { file = path.normalize(file.replace('public'+path.sep, '')); - if (file.endsWith("model3.json")) { + if (file.includes("model") && file.endsWith(".json")) { //console.debug("Asset live2d model found:",file) output[folder].push(path.normalize(path.join(file))); } @@ -273,7 +273,7 @@ function registerEndpoints(app, jsonParser) { if (fs.statSync(live2dModelPath).isDirectory()) { for (let file of fs.readdirSync(live2dModelPath)) { //console.debug("Character live2d model found:", file) - if (file.includes("model")) + if (file.includes("model") && file.endsWith(".json")) output.push(path.join("characters", name, category, modelFolder, file)); } } From b6fb624c99a469c361469ac75ba1512bbb2a6521 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 00:36:04 +0200 Subject: [PATCH 006/185] Change flag hint for ooba --- public/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/index.html b/public/index.html index 65af5d4ee..b8ba253b9 100644 --- a/public/index.html +++ b/public/index.html @@ -1618,7 +1618,7 @@ oobabooga/text-generation-webui - Make sure you run it with --extensions openai flag + Make sure you run it with --api flag
@@ -4707,4 +4707,4 @@ - \ No newline at end of file + From 3f4a62d22c7ca102694c4c2cb40b8c7c204a4021 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:53:26 +0900 Subject: [PATCH 007/185] ext button to left, stack buttons on mobile --- public/css/mobile-styles.css | 14 +++++- public/css/st-tailwind.css | 6 ++- public/index.html | 18 +++++--- public/script.js | 6 +-- public/scripts/extensions.js | 6 +-- .../scripts/extensions/quick-reply/style.css | 2 +- public/style.css | 46 +++++++++++++------ 7 files changed, 68 insertions(+), 30 deletions(-) diff --git a/public/css/mobile-styles.css b/public/css/mobile-styles.css index 80df97ff6..5b885aba6 100644 --- a/public/css/mobile-styles.css +++ b/public/css/mobile-styles.css @@ -369,6 +369,18 @@ top: unset; bottom: unset; } + + + #leftSendForm, + #rightSendForm { + width: 1.15em; + flex-wrap: wrap; + height: unset; + } + + #extensionsMenuButton { + order: 1; + } } /*iOS specific*/ @@ -445,4 +457,4 @@ #horde_model { height: unset; } -} +} \ No newline at end of file diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css index d86d53f49..015cf6f51 100644 --- a/public/css/st-tailwind.css +++ b/public/css/st-tailwind.css @@ -297,6 +297,10 @@ align-content: flex-start; } +.alignContentCenter { + align-content: center; +} + .overflowHidden { overflow: hidden; } @@ -526,4 +530,4 @@ textarea:disabled { height: 30px; text-align: center; padding: 5px; -} +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 65af5d4ee..23ed2435e 100644 --- a/public/index.html +++ b/public/index.html @@ -4568,14 +4568,18 @@
-
- -
-
- +
+
+
+
+ +
+
+ +
+
+
-
-
diff --git a/public/script.js b/public/script.js index 1e1f825a2..825429ab3 100644 --- a/public/script.js +++ b/public/script.js @@ -8023,14 +8023,14 @@ jQuery(async function () { return menu.is(':hover') || button.is(':hover'); } - button.on('mouseenter click', function () { showMenu(); }); - button.on('mouseleave', function () { + button.on('click', function () { showMenu(); }); + button.on('blur', function () { //delay to prevent menu hiding when mouse leaves button into menu setTimeout(() => { if (!isMouseOverButtonOrMenu()) { hideMenu(); } }, 100) }); - menu.on('mouseleave', function () { + menu.on('blur', function () { //delay to prevent menu hide when mouseleaves menu into button setTimeout(() => { if (!isMouseOverButtonOrMenu()) { hideMenu(); } diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 19509085b..b95228af0 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -347,14 +347,14 @@ function addExtensionsButtonAndMenu() { $(document.body).append(extensionsMenuHTML); - $('#send_but_sheld').prepend(buttonHTML); + $('#leftSendForm').prepend(buttonHTML); const button = $('#extensionsMenuButton'); const dropdown = $('#extensionsMenu'); //dropdown.hide(); let popper = Popper.createPopper(button.get(0), dropdown.get(0), { - placement: 'top-end', + placement: 'top-start', }); $(button).on('click', function () { @@ -592,7 +592,7 @@ function getModuleInformation() { * Generates the HTML strings for all extensions and displays them in a popup. */ async function showExtensionsDetails() { - try{ + try { showLoader(); let htmlDefault = '

Built-in Extensions:

'; let htmlExternal = '

Installed Extensions:

'; diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 3bcef5385..a83cbebb2 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -12,7 +12,7 @@ display: none; max-width: 100%; overflow-x: auto; - order: 10; + order: 1; } #quickReplies { diff --git a/public/style.css b/public/style.css index 3a8b1bcef..a5a0534b7 100644 --- a/public/style.css +++ b/public/style.css @@ -559,10 +559,9 @@ hr { background-color: var(--crimson70a) !important; } -#send_but_sheld { +#nonQRFormItems { padding: 0; border: 0; - height: var(--bottomFormBlockSize); position: relative; background-position: center; display: flex; @@ -570,10 +569,28 @@ hr { column-gap: 5px; font-size: var(--bottomFormIconSize); overflow: hidden; - order: 1003; + order: 2; + width: 100%; } -#send_but_sheld>div { +#leftSendForm, +#rightSendForm { + display: flex; + flex-wrap: wrap; +} + +#leftSendForm { + order: 1; + padding-left: 2px; + +} + +#rightSendForm { + order: 3; + padding-right: 2px; +} + +#send_form>#nonQRFormItems>div>div:not(.mes_stop) { width: var(--bottomFormBlockSize); height: var(--bottomFormBlockSize); margin: 0; @@ -585,25 +602,26 @@ hr { display: flex; align-items: center; justify-content: center; + transition: all 300ms; } -#options_button:hover, -#send_but_sheld>div:hover { +#send_form>#nonQRFormItems>div>div:hover { opacity: 1; filter: brightness(1.2); } #send_but { - order: 99999; + order: 2; } #mes_continue { - order: 99998; + order: 1; } -#send_but_sheld .mes_stop { +#send_form .mes_stop { display: none; - order: 99997; + order: 2; + padding-right: 2px; } #options_button { @@ -622,7 +640,7 @@ hr { transition: 0.3s; display: flex; align-items: center; - order: 1001; + order: 2; } .font-family-reset { @@ -670,7 +688,7 @@ hr { } #extensionsMenuButton { - order: 100; + order: 4; padding: 1px; } @@ -968,7 +986,7 @@ select { margin: 0; text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); flex: 1; - order: 1002; + order: 3; } .text_pole::placeholder { @@ -3742,4 +3760,4 @@ a { height: 100vh; z-index: 9999; } -} +} \ No newline at end of file From 50f3def2eb8342720da6a47e06f4783ff65ee85c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:36:57 +0200 Subject: [PATCH 008/185] Decrease icon size and text padding --- public/style.css | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/style.css b/public/style.css index a5a0534b7..2b6be1eac 100644 --- a/public/style.css +++ b/public/style.css @@ -74,8 +74,8 @@ color-scheme: only light; /* Send form variables */ - --bottomFormBlockPadding: calc(var(--mainFontSize) / 3); - --bottomFormIconSize: calc(var(--mainFontSize) * 2); + --bottomFormBlockPadding: calc(var(--mainFontSize) / 2.5); + --bottomFormIconSize: calc(var(--mainFontSize) * 1.9); --bottomFormBlockSize: calc(var(--bottomFormIconSize) + var(--bottomFormBlockPadding)); /*Top Bar Scaling Variables*/ @@ -569,7 +569,7 @@ hr { column-gap: 5px; font-size: var(--bottomFormIconSize); overflow: hidden; - order: 2; + order: 25; width: 100%; } @@ -981,7 +981,7 @@ select { background-color: rgba(255, 0, 0, 0); border: 0; box-shadow: none; - padding-top: 6px; + padding: 3px; font-family: "Noto Sans", "Noto Color Emoji", sans-serif; margin: 0; text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); @@ -3760,4 +3760,4 @@ a { height: 100vh; z-index: 9999; } -} \ No newline at end of file +} From b55918772216183f14dc2eb817de750b5008cd22 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 12:48:55 +0200 Subject: [PATCH 009/185] Autoset height of QR slots. Revert textarea padding --- public/scripts/extensions/quick-reply/index.js | 2 +- public/style.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 83c8086cd..1a9281e8e 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -294,7 +294,7 @@ function generateQuickReplyElements() { quickReplyHtml += `
- +
`; } diff --git a/public/style.css b/public/style.css index 2b6be1eac..a1ca12026 100644 --- a/public/style.css +++ b/public/style.css @@ -981,7 +981,7 @@ select { background-color: rgba(255, 0, 0, 0); border: 0; box-shadow: none; - padding: 3px; + padding: 6px; font-family: "Noto Sans", "Noto Color Emoji", sans-serif; margin: 0; text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor); From c6ac4459a3768ad51fa5c168a79d3552ef9276f6 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:19:39 +0200 Subject: [PATCH 010/185] Move image inlining toggle. GPT-4V via OpenRouter --- public/index.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/public/index.html b/public/index.html index 1d256980f..49b2f9dd9 100644 --- a/public/index.html +++ b/public/index.html @@ -1383,6 +1383,17 @@
+
+ +
-
From abb8b0f0cca8e19f7a4f7a279fcf14c0af237810 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:37:37 +0200 Subject: [PATCH 011/185] Update hide / unhide commands to accept range --- public/script.js | 2 +- public/scripts/chats.js | 6 ++--- public/scripts/slash-commands.js | 41 ++++++++++++++++++++++---------- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/public/script.js b/public/script.js index 825429ab3..3d0bca026 100644 --- a/public/script.js +++ b/public/script.js @@ -4685,7 +4685,7 @@ async function renamePastChats(newAvatar, newValue) { } } -function saveChatDebounced() { +export function saveChatDebounced() { const chid = this_chid; const selectedGroup = selected_group; diff --git a/public/scripts/chats.js b/public/scripts/chats.js index d716a968f..50a63beaa 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -4,7 +4,7 @@ import { chat, getCurrentChatId, hideSwipeButtons, - saveChatConditional, + saveChatDebounced, showSwipeButtons, } from "../script.js"; @@ -30,7 +30,7 @@ export async function hideChatMessage(messageId, messageBlock) { hideSwipeButtons(); showSwipeButtons(); - await saveChatConditional(); + saveChatDebounced(); } /** @@ -55,7 +55,7 @@ export async function unhideChatMessage(messageId, messageBlock) { hideSwipeButtons(); showSwipeButtons(); - await saveChatConditional(); + saveChatDebounced(); } jQuery(function() { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index b1af99469..9fb31fbe9 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -32,6 +32,7 @@ import { chat_styles, power_user } from "./power-user.js"; import { autoSelectPersona } from "./personas.js"; import { getContext } from "./extensions.js"; import { hideChatMessage, unhideChatMessage } from "./chats.js"; +import { stringToRange } from "./utils.js"; export { executeSlashCommands, registerSlashCommand, @@ -145,8 +146,8 @@ parser.addCommand('ask', askCharacter, [], '(prompt)(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); parser.addCommand('trigger', triggerGroupMessageCallback, [], '(member index or name) – triggers a message generation for the specified group member', true, true); -parser.addCommand('hide', hideMessageCallback, [], '(message index) – hides a chat message from the prompt', true, true); -parser.addCommand('unhide', unhideMessageCallback, [], '(message index) – unhides a message from the prompt', true, true); +parser.addCommand('hide', hideMessageCallback, [], '(message index or range) – hides a chat message from the prompt', true, true); +parser.addCommand('unhide', unhideMessageCallback, [], '(message index or range) – unhides a message from the prompt', true, true); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; @@ -240,15 +241,23 @@ async function hideMessageCallback(_, arg) { return; } - const messageId = Number(arg); - const messageBlock = $(`.mes[mesid="${messageId}"]`); + const range = stringToRange(arg, 0, chat.length - 1); - if (!messageBlock.length) { - console.warn(`WARN: No message found with ID ${messageId}`); + if (!range) { + console.warn(`WARN: Invalid range provided for /hide command: ${arg}`); return; } - await hideChatMessage(messageId, messageBlock); + for (let messageId = range.start; messageId <= range.end; messageId++) { + const messageBlock = $(`.mes[mesid="${messageId}"]`); + + if (!messageBlock.length) { + console.warn(`WARN: No message found with ID ${messageId}`); + return; + } + + await hideChatMessage(messageId, messageBlock); + } } async function unhideMessageCallback(_, arg) { @@ -257,15 +266,23 @@ async function unhideMessageCallback(_, arg) { return; } - const messageId = Number(arg); - const messageBlock = $(`.mes[mesid="${messageId}"]`); + const range = stringToRange(arg, 0, chat.length - 1); - if (!messageBlock.length) { - console.warn(`WARN: No message found with ID ${messageId}`); + if (!range) { + console.warn(`WARN: Invalid range provided for /unhide command: ${arg}`); return; } - await unhideChatMessage(messageId, messageBlock); + for (let messageId = range.start; messageId <= range.end; messageId++) { + const messageBlock = $(`.mes[mesid="${messageId}"]`); + + if (!messageBlock.length) { + console.warn(`WARN: No message found with ID ${messageId}`); + return; + } + + await unhideChatMessage(messageId, messageBlock); + } } async function triggerGroupMessageCallback(_, arg) { From ea583e0ff55eff1d3d3d7ae36a67b8285baa5ac8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:43:08 +0200 Subject: [PATCH 012/185] Add fuzzy search to /bg command --- public/scripts/slash-commands.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 9fb31fbe9..33efad164 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -131,7 +131,7 @@ parser.addCommand('?', helpCommandCallback, ['help'], ' – get help on macros, parser.addCommand('name', setNameCallback, ['persona'], '(name) – sets user name and persona avatar (if set)', true, true); parser.addCommand('sync', syncCallback, [], ' – syncs user name in user-attributed messages in the current chat', true, true); parser.addCommand('lock', bindCallback, ['bind'], ' – locks/unlocks a persona (name and avatar) to the current chat', true, true); -parser.addCommand('bg', setBackgroundCallback, ['background'], '(filename) – sets a background according to filename, partial names allowed, will set the first one alphabetically if multiple files begin with the provided argument string', false, true); +parser.addCommand('bg', setBackgroundCallback, ['background'], '(filename) – sets a background according to filename, partial names allowed', false, true); parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe":
/sendas Chloe
Hello, guys!
`, true, true); parser.addCommand('sys', sendNarratorMessage, ['nar'], '(text) – sends message as a system narrator', false, true); parser.addCommand('sysname', setNarratorName, [], '(name) – sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', true, true); @@ -709,11 +709,23 @@ function setBackgroundCallback(_, bg) { if (!bg) { return; } - console.log('Set background to ' + bg); - const bgElement = $(`.bg_example[bgfile^="${bg.trim()}"`); - if (bgElement.length) { - bgElement.get(0).click(); + console.log('Set background to ' + bg); + + const bgElements = Array.from(document.querySelectorAll(`.bg_example`)).map((x) => ({ element: x, bgfile: x.getAttribute('bgfile') })); + + const fuse = new Fuse(bgElements, { keys: ['bgfile'] }); + const result = fuse.search(bg); + + if (!result.length) { + toastr.error(`No background found with name "${bg}"`); + return; + } + + const bgElement = result[0].item.element; + + if (bgElement instanceof HTMLElement) { + bgElement.click(); } } From 4277aac9748f9ec940a6060669395a244da1809d Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:07:32 +0200 Subject: [PATCH 013/185] Don't prompt to create persona if replacing an image --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 3d0bca026..cc202374d 100644 --- a/public/script.js +++ b/public/script.js @@ -5187,7 +5187,7 @@ async function uploadUserAvatar(e) { reloadUserAvatar(true); } - if (data.path) { + if (!name && data.path) { await getUserAvatars(); await delay(500); await createPersona(data.path); From 314aca3f2cab2c556284327271bfc35a38d0dbd4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:27:07 +0200 Subject: [PATCH 014/185] Allow disabling system marker prompts --- public/scripts/PromptManager.js | 9 ++++++++- public/scripts/openai.js | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js index dce7b7f88..69d740dbd 100644 --- a/public/scripts/PromptManager.js +++ b/public/scripts/PromptManager.js @@ -721,6 +721,12 @@ PromptManagerModule.prototype.getTokenHandler = function () { return this.tokenHandler; } +PromptManagerModule.prototype.isPromptDisabledForActiveCharacter = function (identifier) { + const promptOrderEntry = this.getPromptOrderEntry(this.activeCharacter, identifier); + if (promptOrderEntry) return !promptOrderEntry.enabled; + return false; +} + /** * Add a prompt to the current character's prompt list. * @param {object} prompt - The prompt to be added. @@ -859,7 +865,8 @@ PromptManagerModule.prototype.isPromptEditAllowed = function (prompt) { * @returns {boolean} True if the prompt can be deleted, false otherwise. */ PromptManagerModule.prototype.isPromptToggleAllowed = function (prompt) { - return prompt.marker ? false : !this.configuration.toggleDisabled.includes(prompt.identifier); + const forceTogglePrompts = ['charDescription', 'charPersonality', 'scenario', 'personaDescription', 'worldInfoBefore', 'worldInfoAfter']; + return prompt.marker && !forceTogglePrompts.includes(prompt.identifier) ? false : !this.configuration.toggleDisabled.includes(prompt.identifier); } /** diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 897850df9..77bd36d97 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -750,6 +750,11 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm // We need the prompts array to determine a position for the source. if (false === prompts.has(source)) return; + if (promptManager.isPromptDisabledForActiveCharacter(source)) { + promptManager.log(`Skipping prompt ${source} because it is disabled`); + return; + } + const prompt = prompts.get(source); const index = target ? prompts.index(target) : prompts.index(source); const collection = new MessageCollection(source); From 7be808c2ffbca728a9681e7fdc6c24c963fa7bcd Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:41:47 +0200 Subject: [PATCH 015/185] Disable position select for system prompts --- public/scripts/PromptManager.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js index 69d740dbd..6fa24c0a3 100644 --- a/public/scripts/PromptManager.js +++ b/public/scripts/PromptManager.js @@ -179,6 +179,13 @@ class PromptCollection { } function PromptManagerModule() { + this.systemPrompts = [ + 'main', + 'nsfw', + 'jailbreak', + 'enhanceDefinitions', + ]; + this.configuration = { version: 1, prefix: '', @@ -398,6 +405,10 @@ PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSetti document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value = prompt.injection_position ?? 0; document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value = prompt.injection_depth ?? DEFAULT_DEPTH; document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block').style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden'; + + if (!this.systemPrompts.includes(promptId)) { + document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').removeAttribute('disabled'); + } } // Append prompt to selected character @@ -1121,6 +1132,11 @@ PromptManagerModule.prototype.loadPromptIntoEditForm = function (prompt) { injectionPositionField.value = prompt.injection_position ?? INJECTION_POSITION.RELATIVE; injectionDepthField.value = prompt.injection_depth ?? DEFAULT_DEPTH; injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden'; + injectionPositionField.removeAttribute('disabled'); + + if (this.systemPrompts.includes(prompt.identifier)) { + injectionPositionField.setAttribute('disabled', 'disabled'); + } const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset'); if (true === prompt.system_prompt) { @@ -1205,6 +1221,7 @@ PromptManagerModule.prototype.clearEditForm = function () { roleField.selectedIndex = 0; promptField.value = ''; injectionPositionField.selectedIndex = 0; + injectionPositionField.removeAttribute('disabled'); injectionDepthField.value = DEFAULT_DEPTH; injectionDepthBlock.style.visibility = 'unset'; From dcf913336b011d88d419db8e4bbbc132ede26df1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:54:16 +0200 Subject: [PATCH 016/185] Add macros for character's main and jailbreak prompts --- public/script.js | 2 ++ public/scripts/templates/macros.html | 2 ++ 2 files changed, 4 insertions(+) diff --git a/public/script.js b/public/script.js index cc202374d..9b5ee2931 100644 --- a/public/script.js +++ b/public/script.js @@ -1907,6 +1907,8 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh if (_replaceCharacterCard) { const fields = getCharacterCardFields(); + content = content.replace(/{{charPrompt}}/gi, fields.system || ''); + content = content.replace(/{{charJailbreak}}/gi, fields.jailbreak || ''); content = content.replace(/{{description}}/gi, fields.description || ''); content = content.replace(/{{personality}}/gi, fields.personality || ''); content = content.replace(/{{scenario}}/gi, fields.scenario || ''); diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index dc2844439..2b2a5ed10 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -2,6 +2,8 @@ System-wide Replacement Macros (in order of evaluation):
  • {{original}} – global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.
  • {{input}} – the user input
  • +
  • {{charPrompt}rcub; – the Character's Main Prompt override
  • +
  • {{charJailbreak}} – the Character's Jailbreak Prompt override
  • {{description}} – the Character's Description
  • {{personality}} – the Character's Personality
  • {{scenario}} – the Character's Scenario
  • From f24aae546cd3db2476a1270a85567c92b61f1f3a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 14 Nov 2023 23:54:08 +0200 Subject: [PATCH 017/185] Filter WI entries by tags --- public/script.js | 2 ++ public/scripts/world-info.js | 57 +++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/public/script.js b/public/script.js index 9b5ee2931..0fc77a74d 100644 --- a/public/script.js +++ b/public/script.js @@ -6797,6 +6797,8 @@ window["SillyTavern"].getContext = function () { extensionSettings: extension_settings, ModuleWorkerWrapper: ModuleWorkerWrapper, getTokenizerModel: getTokenizerModel, + tags: tags, + tagMap: tag_map, }; }; diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 7909e542c..7c4c0ef6f 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -7,6 +7,7 @@ import { getDeviceInfo } from "./RossAscends-mods.js"; import { FILTER_TYPES, FilterHelper } from "./filters.js"; import { getTokenCount } from "./tokenizers.js"; import { power_user } from "./power-user.js"; +import { getTagKeyForCharacter } from "./tags.js"; export { world_info, @@ -640,7 +641,7 @@ function getWorldEntry(name, data, entry) { const value = $(this).prop("checked"); characterFilterLabel.text(value ? "Exclude Character(s)" : "Filter to Character(s)"); if (data.entries[uid].characterFilter) { - if (!value && data.entries[uid].characterFilter.names.length === 0) { + if (!value && data.entries[uid].characterFilter.names.length === 0 && data.entries[uid].characterFilter.tags.length === 0) { delete data.entries[uid].characterFilter; } else { data.entries[uid].characterFilter.isExclude = value @@ -651,7 +652,8 @@ function getWorldEntry(name, data, entry) { { characterFilter: { isExclude: true, - names: [] + names: [], + tags: [], } } ); @@ -673,13 +675,25 @@ function getWorldEntry(name, data, entry) { closeOnSelect: false, }); } + const characters = getContext().characters; characters.forEach((character) => { const option = document.createElement('option'); - const name = character.avatar.replace(/\.[^/.]+$/, "") ?? character.name - option.innerText = name - option.selected = entry.characterFilter?.names.includes(name) - characterFilter.append(option) + const name = character.avatar.replace(/\.[^/.]+$/, "") ?? character.name; + option.innerText = name; + option.selected = entry.characterFilter?.names?.includes(name); + option.setAttribute('data-type', 'character'); + characterFilter.append(option); + }); + + const tags = getContext().tags; + tags.forEach((tag) => { + const option = document.createElement('option'); + option.innerText = `[Tag] ${tag.name}`; + option.selected = entry.characterFilter?.tags?.includes(tag.id); + option.value = tag.id; + option.setAttribute('data-type', 'tag'); + characterFilter.append(option); }); characterFilter.on('mousedown change', async function (e) { @@ -690,16 +704,19 @@ function getWorldEntry(name, data, entry) { } const uid = $(this).data("uid"); - const value = $(this).val(); - if ((!value || value?.length === 0) && !data.entries[uid].characterFilter?.isExclude) { + const selected = $(this).find(':selected'); + if ((!selected || selected?.length === 0) && !data.entries[uid].characterFilter?.isExclude) { delete data.entries[uid].characterFilter; } else { + const names = selected.filter('[data-type="character"]').map((_, e) => e instanceof HTMLOptionElement && e.innerText).toArray(); + const tags = selected.filter('[data-type="tag"]').map((_, e) => e instanceof HTMLOptionElement && e.value).toArray(); Object.assign( data.entries[uid], { characterFilter: { isExclude: data.entries[uid].characterFilter?.isExclude ?? false, - names: value + names: names, + tags: tags, } } ); @@ -1465,15 +1482,35 @@ async function checkWorldInfo(chat, maxContext) { for (let entry of sortedEntries) { // Check if this entry applies to the character or if it's excluded - if (entry.characterFilter && entry.characterFilter?.names.length > 0) { + if (entry.characterFilter && entry.characterFilter?.names?.length > 0) { const nameIncluded = entry.characterFilter.names.includes(getCharaFilename()); const filtered = entry.characterFilter.isExclude ? nameIncluded : !nameIncluded if (filtered) { + console.debug(`WI entry ${entry.uid} filtered out by character`); continue; } } + if (entry.characterFilter && entry.characterFilter?.tags?.length > 0) { + const tagKey = getTagKeyForCharacter(this_chid); + + if (tagKey) { + const tagMapEntry = context.tagMap[tagKey]; + + if (Array.isArray(tagMapEntry)) { + // If tag map intersects with the tag exclusion list, skip + const includesTag = tagMapEntry.some((tag) => entry.characterFilter.tags.includes(tag)); + const filtered = entry.characterFilter.isExclude ? includesTag : !includesTag; + + if (filtered) { + console.debug(`WI entry ${entry.uid} filtered out by tag`); + continue; + } + } + } + } + if (failedProbabilityChecks.has(entry)) { continue; } From 9bef9f43323a8fd3ab733287ac1dca3240c1839a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Nov 2023 00:27:46 +0200 Subject: [PATCH 018/185] Fix delete message without checkboxes --- public/script.js | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/public/script.js b/public/script.js index 0fc77a74d..cfe059837 100644 --- a/public/script.js +++ b/public/script.js @@ -654,7 +654,7 @@ let api_server_textgenerationwebui = ""; let is_send_press = false; //Send generation -let this_del_mes = 0; +let this_del_mes = -1; //message editing and chat scroll position persistence var this_edit_mes_text = ""; @@ -5548,6 +5548,7 @@ function openMessageDelete(fromSlashCommand) { selected_group: ${selected_group} is_group_generating: ${is_group_generating}`); } + this_del_mes = -1; is_delete_mode = true; } @@ -8169,9 +8170,8 @@ jQuery(async function () { $(this).parent().css("background", css_mes_bg); $(this).prop("checked", false); }); - this_del_mes = 0; - console.debug('canceled del msgs, calling showswipesbtns'); showSwipeButtons(); + this_del_mes = -1; is_delete_mode = false; }); @@ -8185,21 +8185,26 @@ jQuery(async function () { $(this).parent().css("background", css_mes_bg); $(this).prop("checked", false); }); - $(".mes[mesid='" + this_del_mes + "']") - .nextAll("div") - .remove(); - $(".mes[mesid='" + this_del_mes + "']").remove(); - chat.length = this_del_mes; - count_view_mes = this_del_mes; - await saveChatConditional(); - var $textchat = $("#chat"); - $textchat.scrollTop($textchat[0].scrollHeight); - eventSource.emit(event_types.MESSAGE_DELETED, chat.length); - this_del_mes = 0; - $('#chat .mes').last().addClass('last_mes'); - $('#chat .mes').eq(-2).removeClass('last_mes'); - console.debug('confirmed del msgs, calling showswipesbtns'); + + if (this_del_mes >= 0) { + $(".mes[mesid='" + this_del_mes + "']") + .nextAll("div") + .remove(); + $(".mes[mesid='" + this_del_mes + "']").remove(); + chat.length = this_del_mes; + count_view_mes = this_del_mes; + await saveChatConditional(); + var $textchat = $("#chat"); + $textchat.scrollTop($textchat[0].scrollHeight); + eventSource.emit(event_types.MESSAGE_DELETED, chat.length); + $('#chat .mes').last().addClass('last_mes'); + $('#chat .mes').eq(-2).removeClass('last_mes'); + } else { + console.log('this_del_mes is not >= 0, not deleting'); + } + showSwipeButtons(); + this_del_mes = -1; is_delete_mode = false; }); From 22161c22649fcd96778cbcf0738b93e7f4fc834b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Nov 2023 00:59:44 +0200 Subject: [PATCH 019/185] Add backup/restore for tags --- public/scripts/tags.js | 123 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 6 deletions(-) diff --git a/public/scripts/tags.js b/public/scripts/tags.js index a746bb7b7..c71e6287a 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -7,11 +7,12 @@ import { getCharacters, entitiesFilter, printCharacters, + getEmptyBlock, } from "../script.js"; import { FILTER_TYPES, FilterHelper } from "./filters.js"; -import { groupCandidatesFilter, selected_group } from "./group-chats.js"; -import { onlyUnique, uuidv4 } from "./utils.js"; +import { groupCandidatesFilter, groups, selected_group } from "./group-chats.js"; +import { download, onlyUnique, parseJsonFile, uuidv4 } from "./utils.js"; export { tags, @@ -482,9 +483,20 @@ function onViewTagsListClick() { $(list).append(`

    Tag Management

    -
    @@ -494,13 +506,110 @@ function onViewTagsListClick() {
    `); - for (const tag of tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase()))) { + const sortedTags = tags.slice().sort((a, b) => a?.name?.toLowerCase()?.localeCompare(b?.name?.toLowerCase())); + for (const tag of sortedTags) { appendViewTagToList(list, tag, everything); } callPopup(list, 'text'); } +async function onTagRestoreFileSelect(e) { + const file = e.target.files[0]; + + if (!file) { + console.log('Tag restore: No file selected.'); + return; + } + + const data = await parseJsonFile(file); + + if (!data) { + toastr.warning('Tag restore: Empty file data.'); + console.log('Tag restore: File data empty.'); + return; + } + + if (!data.tags || !data.tag_map || !Array.isArray(data.tags) || typeof data.tag_map !== 'object') { + toastr.warning('Tag restore: Invalid file format.'); + console.log('Tag restore: Invalid file format.'); + return; + } + + const warnings = []; + + // Import tags + for (const tag of data.tags) { + if (!tag.id || !tag.name) { + warnings.push(`Tag object is invalid: ${JSON.stringify(tag)}.`); + continue; + } + + if (tags.find(x => x.id === tag.id)) { + warnings.push(`Tag with id ${tag.id} already exists.`); + continue; + } + + tags.push(tag); + } + + // Import tag_map + for (const key of Object.keys(data.tag_map)) { + const tagIds = data.tag_map[key]; + + if (!Array.isArray(tagIds)) { + warnings.push(`Tag map for key ${key} is invalid: ${JSON.stringify(tagIds)}.`); + continue; + } + + // Verify that the key points to a valid character or group. + const characterExists = characters.some(x => String(x.avatar) === String(key)); + const groupExists = groups.some(x => String(x.id) === String(key)); + + if (!characterExists && !groupExists) { + warnings.push(`Tag map key ${key} does not exist.`); + continue; + } + + // Get existing tag ids for this key or empty array. + const existingTagIds = tag_map[key] || []; + // Merge existing and new tag ids. Remove duplicates. + tag_map[key] = existingTagIds.concat(tagIds).filter(onlyUnique); + // Verify that all tags exist. Remove tags that don't exist. + tag_map[key] = tag_map[key].filter(x => tags.some(y => String(y.id) === String(x))); + } + + if (warnings.length) { + toastr.success('Tags restored with warnings. Check console for details.'); + console.warn(`TAG RESTORE REPORT\n====================\n${warnings.join('\n')}`); + } else { + toastr.success('Tags restored successfully.'); + } + + $('#tag_view_restore_input').val(''); + saveSettingsDebounced(); + printCharacters(true); + onViewTagsListClick(); +} + +function onBackupRestoreClick() { + $('#tag_view_restore_input') + .off('change') + .on('change', onTagRestoreFileSelect) + .trigger('click'); +} + +function onTagsBackupClick() { + const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); + const filename = `tags_${timestamp}.json`; + const data = { + tags: tags, + tag_map: tag_map, + }; + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + download(blob, filename, 'application/json'); +} + function onTagCreateClick() { const tag = createNewTag('New Tag'); appendViewTagToList($('#tag_view_list'), tag, []); @@ -623,4 +732,6 @@ $(document).ready(() => { $(document).on("click", ".tag_delete", onTagDeleteClick); $(document).on("input", ".tag_view_name", onTagRenameInput); $(document).on("click", ".tag_view_create", onTagCreateClick); + $(document).on("click", ".tag_view_backup", onTagsBackupClick); + $(document).on("click", ".tag_view_restore", onBackupRestoreClick); }); From 5b5e42361acecfcbdcde21bf2ab8c066ebc7c93f Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Nov 2023 01:06:27 +0200 Subject: [PATCH 020/185] Fix chat backups saving with incorrect file extension --- public/scripts/tags.js | 2 +- server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/tags.js b/public/scripts/tags.js index c71e6287a..d479d177e 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -718,7 +718,7 @@ function onTagListHintClick() { $(this).siblings(".innerActionable").toggleClass('hidden'); } -$(document).ready(() => { +jQuery(() => { createTagInput('#tagInput', '#tagList'); createTagInput('#groupTagInput', '#groupTagList'); diff --git a/server.js b/server.js index c3f47880b..7ff4de46b 100644 --- a/server.js +++ b/server.js @@ -3640,7 +3640,7 @@ function backupChat(name, chat) { // replace non-alphanumeric characters with underscores name = sanitize(name).replace(/[^a-z0-9]/gi, '_').toLowerCase(); - const backupFile = path.join(DIRECTORIES.backups, `chat_${name}_${generateTimestamp()}.json`); + const backupFile = path.join(DIRECTORIES.backups, `chat_${name}_${generateTimestamp()}.jsonl`); writeFileAtomicSync(backupFile, chat, 'utf-8'); removeOldBackups(`chat_${name}_`); From 31433565230a0a6ae3075e862cd8d35c79748554 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Nov 2023 01:16:31 +0200 Subject: [PATCH 021/185] Skill issue --- public/scripts/tags.js | 1 - 1 file changed, 1 deletion(-) diff --git a/public/scripts/tags.js b/public/scripts/tags.js index d479d177e..2cf83e20e 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -7,7 +7,6 @@ import { getCharacters, entitiesFilter, printCharacters, - getEmptyBlock, } from "../script.js"; import { FILTER_TYPES, FilterHelper } from "./filters.js"; From 3c3594c52f828caa9dc669c1b7c7658d4c4ec7e3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Nov 2023 02:09:40 +0200 Subject: [PATCH 022/185] Add backup/restore for Personas --- public/index.html | 36 +++++++++++---- public/script.js | 5 +- public/scripts/personas.js | 95 +++++++++++++++++++++++++++++++++++++- public/scripts/tags.js | 4 +- 4 files changed, 125 insertions(+), 15 deletions(-) diff --git a/public/index.html b/public/index.html index 49b2f9dd9..8f3689f9c 100644 --- a/public/index.html +++ b/public/index.html @@ -3093,8 +3093,33 @@
    -

    Persona Management

    - How do I use this? +
    +
    +

    Persona Management

    + + + +
    +
    + + + + + +
    +

    Name

    @@ -3134,13 +3159,6 @@

    Your Persona - -

    +
    diff --git a/public/script.js b/public/script.js index cfe059837..030635bb4 100644 --- a/public/script.js +++ b/public/script.js @@ -5085,11 +5085,10 @@ export async function getUserAvatars() { $("#user_avatar_block").append('
    +
    '); for (var i = 0; i < getData.length; i++) { - //console.log(1); appendUserAvatar(getData[i]); } - //var aa = JSON.parse(getData[0]); - //const load_ch_coint = Object.getOwnPropertyNames(getData); + + return getData; } } diff --git a/public/scripts/personas.js b/public/scripts/personas.js index 86cbc7826..cad220aaa 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -1,7 +1,7 @@ import { callPopup, characters, chat_metadata, default_avatar, eventSource, event_types, getRequestHeaders, getThumbnailUrl, getUserAvatars, name1, saveMetadata, saveSettingsDebounced, setUserName, this_chid, user_avatar } from "../script.js"; import { persona_description_positions, power_user } from "./power-user.js"; import { getTokenCount } from "./tokenizers.js"; -import { debounce, delay } from "./utils.js"; +import { debounce, delay, download, parseJsonFile } from "./utils.js"; /** * Uploads an avatar file to the server @@ -486,6 +486,96 @@ function setChatLockedPersona() { updateUserLockIcon(); } +function onBackupPersonas() { + const timestamp = new Date().toISOString().split('T')[0].replace(/-/g, ''); + const filename = `personas_${timestamp}.json`; + const data = JSON.stringify({ + "personas": power_user.personas, + "persona_descriptions": power_user.persona_descriptions, + "default_persona": power_user.default_persona, + }, null, 2); + + const blob = new Blob([data], { type: 'application/json' }); + download(blob, filename, 'application/json'); +} + +async function onPersonasRestoreInput(e) { + const file = e.target.files[0]; + + if (!file) { + console.debug('No file selected'); + return; + } + + const data = await parseJsonFile(file); + + if (!data) { + toastr.warning('Invalid file selected', 'Persona Management'); + console.debug('Invalid file selected'); + return; + } + + if (!data.personas || !data.persona_descriptions || typeof data.personas !== 'object' || typeof data.persona_descriptions !== 'object') { + toastr.warning('Invalid file format', 'Persona Management'); + console.debug('Invalid file selected'); + return; + } + + const avatarsList = await getUserAvatars(); + const warnings = []; + + // Merge personas with existing ones + for (const [key, value] of Object.entries(data.personas)) { + if (key in power_user.personas) { + warnings.push(`Persona "${key}" (${value}) already exists, skipping`); + continue; + } + + power_user.personas[key] = value; + + // If the avatar is missing, upload it + if (!avatarsList.includes(key)) { + warnings.push(`Persona image "${key}" (${value}) is missing, uploading default avatar`); + await uploadUserAvatar(default_avatar, key); + } + } + + // Merge persona descriptions with existing ones + for (const [key, value] of Object.entries(data.persona_descriptions)) { + if (key in power_user.persona_descriptions) { + warnings.push(`Persona description for "${key}" (${power_user.personas[key]}) already exists, skipping`); + continue; + } + + if (!power_user.personas[key]) { + warnings.push(`Persona for "${key}" does not exist, skipping`); + continue; + } + + power_user.persona_descriptions[key] = value; + } + + if (data.default_persona) { + if (data.default_persona in power_user.personas) { + power_user.default_persona = data.default_persona; + } else { + warnings.push(`Default persona "${data.default_persona}" does not exist, skipping`); + } + } + + if (warnings.length) { + toastr.success('Personas restored with warnings. Check console for details.'); + console.warn(`PERSONA RESTORE REPORT\n====================\n${warnings.join('\n')}`); + } else { + toastr.success('Personas restored successfully.'); + } + + await getUserAvatars(); + setPersonaDescription(); + saveSettingsDebounced(); + $('#personas_restore_input').val(''); +} + export function initPersonas() { $(document).on('click', '.bind_user_name', bindUserNameToPersona); $(document).on('click', '.set_default_persona', setDefaultPersona); @@ -494,6 +584,9 @@ export function initPersonas() { $("#create_dummy_persona").on('click', createDummyPersona); $('#persona_description').on('input', onPersonaDescriptionInput); $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); + $('#personas_backup').on('click', onBackupPersonas); + $('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click')); + $('#personas_restore_input').on('change', onPersonasRestoreInput); eventSource.on("charManagementDropdown", (target) => { if (target === 'convert_to_persona') { diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 2cf83e20e..d989313b3 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -524,13 +524,13 @@ async function onTagRestoreFileSelect(e) { const data = await parseJsonFile(file); if (!data) { - toastr.warning('Tag restore: Empty file data.'); + toastr.warning('Empty file data', 'Tag restore'); console.log('Tag restore: File data empty.'); return; } if (!data.tags || !data.tag_map || !Array.isArray(data.tags) || typeof data.tag_map !== 'object') { - toastr.warning('Tag restore: Invalid file format.'); + toastr.warning('Invalid file format', 'Tag restore'); console.log('Tag restore: Invalid file format.'); return; } From 5136b708825f95c1b4f9d0352acec4ed878dcea3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 15 Nov 2023 02:16:42 +0200 Subject: [PATCH 023/185] #1355 Update summary settings button to make it more visible --- public/scripts/extensions/memory/index.js | 17 +++++++++-------- public/scripts/extensions/vectors/settings.html | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 6ed2561a9..d60d81623 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -651,14 +651,12 @@ jQuery(function () {
    - +
    - Current summary: -
    - +
    - +
    -
    - +
    From 53c3fc16c15dc4ba6828c00eaa834126dd8d8da1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 00:40:21 +0200 Subject: [PATCH 047/185] Assorted SD fixes --- public/script.js | 27 +++++------- public/scripts/extensions.js | 7 ++-- .../extensions/stable-diffusion/index.js | 9 ++-- server.js | 41 ++++++++++--------- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/public/script.js b/public/script.js index 072369c57..846cce102 100644 --- a/public/script.js +++ b/public/script.js @@ -9055,19 +9055,12 @@ jQuery(async function () { }); $(document).on('click', '.mes .avatar', function () { - - //console.log(isMobile()); - //console.log($('body').hasClass('waifuMode')); - - /* if (isMobile() === true && !$('body').hasClass('waifuMode')) { - console.debug('saw mobile regular mode, returning'); - return; - } else { console.debug('saw valid env for zoomed display') } */ - - let thumbURL = $(this).children('img').attr('src'); - let charsPath = '/characters/' - let targetAvatarImg = thumbURL.substring(thumbURL.lastIndexOf("=") + 1); - let charname = targetAvatarImg.replace('.png', ''); + const messageElement = $(this).closest('.mes'); + const thumbURL = $(this).children('img').attr('src'); + const charsPath = '/characters/' + const targetAvatarImg = thumbURL.substring(thumbURL.lastIndexOf("=") + 1); + const charname = targetAvatarImg.replace('.png', ''); + const isValidCharacter = characters.some(x => x.avatar === targetAvatarImg); // Remove existing zoomed avatars for characters that are not the clicked character when moving UI is not enabled if (!power_user.movingUI) { @@ -9080,7 +9073,7 @@ jQuery(async function () { }); } - let avatarSrc = isDataURL(thumbURL) ? thumbURL : charsPath + targetAvatarImg; + const avatarSrc = isDataURL(thumbURL) ? thumbURL : charsPath + targetAvatarImg; if ($(`.zoomed_avatar[forChar="${charname}"]`).length) { console.debug('removing container as it already existed') $(`.zoomed_avatar[forChar="${charname}"]`).remove(); @@ -9094,11 +9087,11 @@ jQuery(async function () { newElement.find('.drag-grabber').attr('id', `zoomFor_${charname}header`); $('body').append(newElement); - if ($(this).parent().parent().attr('is_user') == 'true') { //handle user avatars + if (messageElement.attr('is_user') == 'true') { //handle user avatars $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL); - } else if ($(this).parent().parent().attr('is_system') == 'true') { //handle system avatars + } else if (messageElement.attr('is_system') == 'true' && !isValidCharacter) { //handle system avatars $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL); - } else if ($(this).parent().parent().attr('is_user') == 'false') { //handle char avatars + } else if (messageElement.attr('is_user') == 'false') { //handle char avatars $(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', avatarSrc); } loadMovingUIState(); diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 836300768..22f71df94 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -365,10 +365,9 @@ function addExtensionsButtonAndMenu() { }); $("html").on('touchstart mousedown', function (e) { - // let clickTarget = $(e.target); - if (dropdown.is(':visible') - /*&& clickTarget.closest(button).length == 0 - && clickTarget.closest(dropdown).length == 0*/) { + const clickTarget = $(e.target); + const noCloseTargets = ['#sd_gen']; + if (dropdown.is(':visible') && !noCloseTargets.some(id => clickTarget.closest(id).length > 0)) { $(dropdown).fadeOut(animation_duration); } }); diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 5984a603d..a1346b856 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -7,10 +7,11 @@ import { getRequestHeaders, event_types, eventSource, - appendImageToMessage, generateQuietPrompt, this_chid, getCurrentChatId, + animation_duration, + appendMediaToMessage, } from "../../../script.js"; import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from "../../extensions.js"; import { selected_group } from "../../group-chats.js"; @@ -1764,10 +1765,10 @@ function addSDGenButtons() { if (target.is(button) && !dropdown.is(":visible") && $("#send_but").is(":visible")) { e.preventDefault(); - dropdown.fadeIn(250); + dropdown.fadeIn(animation_duration); popper.update(); } else { - dropdown.fadeOut(250); + dropdown.fadeOut(animation_duration); } }); } @@ -1865,7 +1866,7 @@ async function sdMessageButton(e) { message.extra.image = image; message.extra.title = prompt; message.extra.generationType = generationType; - appendImageToMessage(message, $mes); + appendMediaToMessage(message, $mes); context.saveChat(); } diff --git a/server.js b/server.js index b1619e0e3..914a7ade0 100644 --- a/server.js +++ b/server.js @@ -2493,27 +2493,28 @@ app.post('/uploadimage', jsonParser, async (request, response) => { return response.status(400).send({ error: "No image data provided" }); } - // Extracting the base64 data and the image format - const match = request.body.image.match(/^data:image\/(png|jpg|webp|jpeg|gif);base64,(.+)$/); - if (!match) { - return response.status(400).send({ error: "Invalid image format" }); - } - - const [, format, base64Data] = match; - - // Constructing filename and path - let filename = `${Date.now()}.${format}`; - if (request.body.filename) { - filename = `${request.body.filename}.${format}`; - } - - // if character is defined, save to a sub folder for that character - let pathToNewFile = path.join(DIRECTORIES.userImages, filename); - if (request.body.ch_name) { - pathToNewFile = path.join(DIRECTORIES.userImages, request.body.ch_name, filename); - } - try { + // Extracting the base64 data and the image format + const splitParts = request.body.image.split(','); + const format = splitParts[0].split(';')[0].split('/')[1]; + const base64Data = splitParts[1]; + const validFormat = ['png', 'jpg', 'webp', 'jpeg', 'gif'].includes(format); + if (!validFormat) { + return response.status(400).send({ error: "Invalid image format" }); + } + + // Constructing filename and path + let filename = `${Date.now()}.${format}`; + if (request.body.filename) { + filename = `${request.body.filename}.${format}`; + } + + // if character is defined, save to a sub folder for that character + let pathToNewFile = path.join(DIRECTORIES.userImages, filename); + if (request.body.ch_name) { + pathToNewFile = path.join(DIRECTORIES.userImages, request.body.ch_name, filename); + } + ensureDirectoryExistence(pathToNewFile); const imageBuffer = Buffer.from(base64Data, 'base64'); await fs.promises.writeFile(pathToNewFile, imageBuffer); From 685bb9742e91f0aa1102ad33ee019110dc501c45 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 01:33:54 +0200 Subject: [PATCH 048/185] Fix update button icons --- public/scripts/extensions.js | 14 ++++++++------ public/scripts/extensions/assets/index.js | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 22f71df94..2c68b949a 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -512,8 +512,8 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt isUpToDate = data.isUpToDate; displayVersion = ` (${branch}-${commitHash.substring(0, 7)})`; updateButton = isUpToDate ? - `` : - ``; + `` : + ``; originHtml = ``; } @@ -641,6 +641,7 @@ async function showExtensionsDetails() { */ async function onUpdateClick() { const extensionName = $(this).data('name'); + $(this).find('i').addClass('fa-spin'); await updateExtension(extensionName, false); } @@ -658,6 +659,11 @@ async function updateExtension(extensionName, quiet) { }); const data = await response.json(); + + if (!quiet) { + showExtensionsDetails(); + } + if (data.isUpToDate) { if (!quiet) { toastr.success('Extension is already up to date'); @@ -665,10 +671,6 @@ async function updateExtension(extensionName, quiet) { } else { toastr.success(`Extension ${extensionName} updated to ${data.shortCommitHash}`); } - - if (!quiet) { - showExtensionsDetails(); - } } catch (error) { console.error('Error:', error); } diff --git a/public/scripts/extensions/assets/index.js b/public/scripts/extensions/assets/index.js index fe6cd12ef..1c49917ce 100644 --- a/public/scripts/extensions/assets/index.js +++ b/public/scripts/extensions/assets/index.js @@ -67,7 +67,7 @@ function downloadAssetsList(url) { const asset = availableAssets[assetType][i]; const elemId = `assets_install_${assetType}_${i}`; let element = $('
    +
    + +
    + + +
    + +

    Important: run ComfyUI with the --enable-cors-header http://127.0.0.1:8000 argument (adjust URL according to your SillyTavern setup)! The server must be accessible from the SillyTavern host machine.

    +
    @@ -124,6 +142,10 @@ +
    + + +
    +
    +
    + +
    +

    API URL

    + Example: http://127.0.0.1:5001 + +
    +
    - +
    -

    Important: run ComfyUI with the --enable-cors-header http://127.0.0.1:8000 argument (adjust URL according to your SillyTavern setup)! The server must be accessible from the SillyTavern host machine.

    +

    Important: The server must be accessible from the SillyTavern host machine.

    diff --git a/src/stable-diffusion.js b/src/stable-diffusion.js index 6443f51d1..303bbaf7e 100644 --- a/src/stable-diffusion.js +++ b/src/stable-diffusion.js @@ -347,6 +347,122 @@ function registerEndpoints(app, jsonParser) { return response.send({ prompt: originalPrompt }); } }); + + + app.post('/api/sd/comfy/ping', jsonParser, async(request, response)=>{ + try { + const url = new URL(request.body.url); + url.pathname = '/system_stats' + + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + + return response.sendStatus(200); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); + + app.post('/api/sd/comfy/samplers', jsonParser, async(request, response)=>{ + try { + const url = new URL(request.body.url); + url.pathname = '/object_info' + + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + + const data = await result.json(); + return response.send(data.KSampler.input.required.sampler_name[0]); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); + + app.post('/api/sd/comfy/models', jsonParser, async(request, response)=>{ + try { + const url = new URL(request.body.url); + url.pathname = '/object_info' + + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + const data = await result.json(); + return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it=>({value:it,text:it}))); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); + + app.post('/api/sd/comfy/schedulers', jsonParser, async(request, response)=>{ + try { + const url = new URL(request.body.url); + url.pathname = '/object_info' + + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + + const data = await result.json(); + return response.send(data.KSampler.input.required.scheduler[0]); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); + + app.post('/api/sd/comfy/generate', jsonParser, async(request, response)=>{ + try { + const url = new URL(request.body.url); + url.pathname = '/prompt' + + const promptResult = await fetch(url, { + method: 'POST', + body: request.body.prompt, + }); + if (!promptResult.ok) { + throw new Error('ComfyUI returned an error.'); + } + + const data = await promptResult.json(); + const id = data.prompt_id; + let item; + const historyUrl = new URL(request.body.url); + historyUrl.pathname = '/history'; + while (true) { + const result = await fetch(historyUrl); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + const history = await result.json(); + item = history[id]; + if (item) { + break; + } + await delay(100); + } + const imgInfo = Object.keys(item.outputs).map(it=>item.outputs[it].images).flat()[0]; + const imgUrl = new URL(request.body.url); + imgUrl.pathname = '/view'; + imgUrl.search = `?filename=${imgInfo.filename}&subfolder=${imgInfo.subfolder}&type=${imgInfo.type}`; + const imgResponse = await fetch(imgUrl); + if (!imgResponse.ok) { + throw new Error('ComfyUI returned an error.'); + } + const imgBuffer = await imgResponse.buffer(); + return response.send(imgBuffer.toString('base64')); + } catch (error) { + return response.sendStatus(500); + } + }); } module.exports = { From 96b87641ca5655c4a1f5d1f0821ab21d80b0f709 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 20:30:34 +0200 Subject: [PATCH 059/185] Add OpenAI Whisper API --- server.js | 2 +- src/openai.js | 57 ++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/server.js b/server.js index 400045f16..8e7393b7f 100644 --- a/server.js +++ b/server.js @@ -3530,7 +3530,7 @@ async function fetchJSON(url, args = {}) { // ** END ** // OpenAI API -require('./src/openai').registerEndpoints(app, jsonParser); +require('./src/openai').registerEndpoints(app, jsonParser, urlencodedParser); // Tokenizers require('./src/tokenizers').registerEndpoints(app, jsonParser); diff --git a/src/openai.js b/src/openai.js index 654545713..6c5d551d3 100644 --- a/src/openai.js +++ b/src/openai.js @@ -1,12 +1,15 @@ const { readSecret, SECRET_KEYS } = require("./secrets"); const fetch = require('node-fetch').default; +const FormData = require('form-data'); +const fs = require('fs'); /** * Registers the OpenAI endpoints. - * @param {import("express").Express} app - * @param {any} jsonParser + * @param {import("express").Express} app Express app + * @param {any} jsonParser JSON parser + * @param {any} urlencodedParser Form data parser */ -function registerEndpoints(app, jsonParser) { +function registerEndpoints(app, jsonParser, urlencodedParser) { app.post('/api/openai/caption-image', jsonParser, async (request, response) => { try { let key = ''; @@ -85,6 +88,54 @@ function registerEndpoints(app, jsonParser) { } }); + app.post('/api/openai/transcribe-audio', urlencodedParser, async (request, response) => { + try { + const key = readSecret(SECRET_KEYS.OPENAI); + + if (!key) { + console.log('No OpenAI key found'); + return response.sendStatus(401); + } + + if (!request.file) { + console.log('No audio file found'); + return response.sendStatus(400); + } + + const formData = new FormData(); + console.log('Processing audio file', request.file.path); + formData.append('file', fs.createReadStream(request.file.path), { filename: 'audio.wav', contentType: 'audio/wav' }); + formData.append('model', request.body.model); + + if (request.body.language) { + formData.append('language', request.body.language); + } + + const result = await fetch('https://api.openai.com/v1/audio/transcriptions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${key}`, + ...formData.getHeaders(), + }, + body: formData, + }); + + if (!result.ok) { + const text = await result.text(); + console.log('OpenAI request failed', result.statusText, text); + return response.status(500).send(text); + } + + fs.rmSync(request.file.path); + const data = await result.json(); + console.log('OpenAI transcription response', data); + return response.json(data); + } catch (error) { + console.error('OpenAI transcription failed', error); + response.status(500).send('Internal server error'); + } + }); + app.post('/api/openai/generate-voice', jsonParser, async (request, response) => { try { const key = readSecret(SECRET_KEYS.OPENAI); From 9e3072f89be151d286223c74285dfdffb18b2344 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:05:58 +0200 Subject: [PATCH 060/185] Explicitly add form-data to package.json --- package-lock.json | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 21f21c1a2..37ad9a260 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "csrf-csrf": "^2.2.3", "device-detector-js": "^3.0.3", "express": "^4.18.2", + "form-data": "^4.0.0", "google-translate-api-browser": "^3.0.1", "gpt3-tokenizer": "^1.1.5", "ip-matching": "^2.1.2", diff --git a/package.json b/package.json index fb5f2d13a..2b9f1030d 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@agnai/sentencepiece-js": "^1.1.1", "@agnai/web-tokenizers": "^0.1.3", "@dqbd/tiktoken": "^1.0.2", + "bing-translate-api": "^2.9.1", "command-exists": "^1.2.9", "compression": "^1", "cookie-parser": "^1.4.6", @@ -10,8 +11,8 @@ "csrf-csrf": "^2.2.3", "device-detector-js": "^3.0.3", "express": "^4.18.2", + "form-data": "^4.0.0", "google-translate-api-browser": "^3.0.1", - "bing-translate-api": "^2.9.1", "gpt3-tokenizer": "^1.1.5", "ip-matching": "^2.1.2", "ipaddr.js": "^2.0.1", From 9dcc23825a5d761e769f6ffe137b0d3a479678f1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:17:02 +0200 Subject: [PATCH 061/185] [chore] Reformat --- .../extensions/stable-diffusion/index.js | 18 +-- .../extensions/stable-diffusion/style.css | 18 ++- src/stable-diffusion.js | 149 +++++++++--------- 3 files changed, 96 insertions(+), 89 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 6ea16df8c..c9e2fc29e 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -906,7 +906,6 @@ async function validateComfyUrl() { throw new Error('URL is not set.'); } - const result = await fetch(`/api/sd/comfy/ping`, { method: 'POST', headers: getRequestHeaders(), @@ -1180,7 +1179,6 @@ async function loadComfySamplers() { } try { - const result = await fetch(`/api/sd/comfy/samplers`, { method: 'POST', headers: getRequestHeaders(), @@ -2037,7 +2035,7 @@ async function generateOpenAiImage(prompt) { /** * Generates an image in ComfyUI using the provided prompt and configuration settings. - * + * * @param {string} prompt - The main instruction used to guide the image generation. * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. */ @@ -2054,8 +2052,8 @@ async function generateComfyImage(prompt) { ]; let workflow = extension_settings.sd.comfy_workflow.replace('"%prompt%"', JSON.stringify(prompt)); - workflow = workflow.replace('"%seed%"', JSON.stringify(Math.round(Math.random()*Number.MAX_SAFE_INTEGER))); - placeholders.forEach(ph=>{ + workflow = workflow.replace('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER))); + placeholders.forEach(ph => { workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph])); }); console.log(`{ @@ -2071,18 +2069,18 @@ async function generateComfyImage(prompt) { }`, }) }); - return {format:'png', data:await promptResult.text()}; + return { format: 'png', data: await promptResult.text() }; } async function onComfyOpenWorkflowEditorClick() { const editorHtml = $(await $.get('scripts/extensions/stable-diffusion/comfyWorkflowEditor.html')); - const popupResult = callPopup(editorHtml, "confirm", undefined, {okButton: "Save", wide:true, large:true, rows:1 }); - const checkPlaceholders = ()=>{ + const popupResult = callPopup(editorHtml, "confirm", undefined, { okButton: "Save", wide: true, large: true, rows: 1 }); + const checkPlaceholders = () => { const workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); - $('.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]').each(function(idx) { + $('.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]').each(function (idx) { const key = this.getAttribute('data-placeholder'); const found = workflow.search(`"%${key}%"`) != -1; - this.classList[found?'remove':'add']('sd_comfy_workflow_editor_not_found'); + this.classList[found ? 'remove' : 'add']('sd_comfy_workflow_editor_not_found'); }); }; $('#sd_comfy_workflow_editor_workflow').val(extension_settings.sd.comfy_workflow); diff --git a/public/scripts/extensions/stable-diffusion/style.css b/public/scripts/extensions/stable-diffusion/style.css index 374546312..7b9fdd551 100644 --- a/public/scripts/extensions/stable-diffusion/style.css +++ b/public/scripts/extensions/stable-diffusion/style.css @@ -34,28 +34,35 @@ gap: 10px; width: fit-content; } + #sd_comfy_workflow_editor_template { height: 100%; } + .sd_comfy_workflow_editor { display: flex; flex-direction: column; height: 100%; } + .sd_comfy_workflow_editor_content { display: flex; flex: 1 1 auto; flex-direction: row; } + .sd_comfy_workflow_editor_workflow_container { flex: 1 1 auto; } + #sd_comfy_workflow_editor_workflow { font-family: monospace; } + .sd_comfy_workflow_editor_placeholder_container { flex: 0 0 auto; } + .sd_comfy_workflow_editor_placeholder_list { font-size: x-small; list-style: none; @@ -63,12 +70,15 @@ padding: 3px 5px; text-align: left; } -.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]:before { + +.sd_comfy_workflow_editor_placeholder_list>li[data-placeholder]:before { content: "✅ "; } -.sd_comfy_workflow_editor_placeholder_list > li.sd_comfy_workflow_editor_not_found:before { + +.sd_comfy_workflow_editor_placeholder_list>li.sd_comfy_workflow_editor_not_found:before { content: "❌ "; } -.sd_comfy_workflow_editor_placeholder_list > li > .notes-link { + +.sd_comfy_workflow_editor_placeholder_list>li>.notes-link { cursor: help; -} \ No newline at end of file +} diff --git a/src/stable-diffusion.js b/src/stable-diffusion.js index 303bbaf7e..cfec97e98 100644 --- a/src/stable-diffusion.js +++ b/src/stable-diffusion.js @@ -348,91 +348,90 @@ function registerEndpoints(app, jsonParser) { } }); + app.post('/api/sd/comfy/ping', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/system_stats' - app.post('/api/sd/comfy/ping', jsonParser, async(request, response)=>{ - try { - const url = new URL(request.body.url); - url.pathname = '/system_stats' + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } - const result = await fetch(url); - if (!result.ok) { - throw new Error('ComfyUI returned an error.'); - } - - return response.sendStatus(200); - } catch (error) { - console.log(error); - return response.sendStatus(500); - } - }); + return response.sendStatus(200); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); - app.post('/api/sd/comfy/samplers', jsonParser, async(request, response)=>{ - try { - const url = new URL(request.body.url); - url.pathname = '/object_info' + app.post('/api/sd/comfy/samplers', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/object_info' - const result = await fetch(url); - if (!result.ok) { - throw new Error('ComfyUI returned an error.'); - } + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } - const data = await result.json(); - return response.send(data.KSampler.input.required.sampler_name[0]); - } catch (error) { - console.log(error); - return response.sendStatus(500); - } - }); - - app.post('/api/sd/comfy/models', jsonParser, async(request, response)=>{ - try { - const url = new URL(request.body.url); - url.pathname = '/object_info' - - const result = await fetch(url); - if (!result.ok) { - throw new Error('ComfyUI returned an error.'); - } const data = await result.json(); - return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it=>({value:it,text:it}))); - } catch (error) { - console.log(error); - return response.sendStatus(500); - } - }); + return response.send(data.KSampler.input.required.sampler_name[0]); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); - app.post('/api/sd/comfy/schedulers', jsonParser, async(request, response)=>{ - try { - const url = new URL(request.body.url); - url.pathname = '/object_info' + app.post('/api/sd/comfy/models', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/object_info' - const result = await fetch(url); - if (!result.ok) { - throw new Error('ComfyUI returned an error.'); - } + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + const data = await result.json(); + return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it }))); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); - const data = await result.json(); - return response.send(data.KSampler.input.required.scheduler[0]); - } catch (error) { - console.log(error); - return response.sendStatus(500); - } - }); + app.post('/api/sd/comfy/schedulers', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/object_info' - app.post('/api/sd/comfy/generate', jsonParser, async(request, response)=>{ - try { - const url = new URL(request.body.url); - url.pathname = '/prompt' + const result = await fetch(url); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } - const promptResult = await fetch(url, { + const data = await result.json(); + return response.send(data.KSampler.input.required.scheduler[0]); + } catch (error) { + console.log(error); + return response.sendStatus(500); + } + }); + + app.post('/api/sd/comfy/generate', jsonParser, async (request, response) => { + try { + const url = new URL(request.body.url); + url.pathname = '/prompt' + + const promptResult = await fetch(url, { method: 'POST', body: request.body.prompt, }); - if (!promptResult.ok) { - throw new Error('ComfyUI returned an error.'); - } + if (!promptResult.ok) { + throw new Error('ComfyUI returned an error.'); + } - const data = await promptResult.json(); + const data = await promptResult.json(); const id = data.prompt_id; let item; const historyUrl = new URL(request.body.url); @@ -449,7 +448,7 @@ function registerEndpoints(app, jsonParser) { } await delay(100); } - const imgInfo = Object.keys(item.outputs).map(it=>item.outputs[it].images).flat()[0]; + const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0]; const imgUrl = new URL(request.body.url); imgUrl.pathname = '/view'; imgUrl.search = `?filename=${imgInfo.filename}&subfolder=${imgInfo.subfolder}&type=${imgInfo.type}`; @@ -459,10 +458,10 @@ function registerEndpoints(app, jsonParser) { } const imgBuffer = await imgResponse.buffer(); return response.send(imgBuffer.toString('base64')); - } catch (error) { - return response.sendStatus(500); - } - }); + } catch (error) { + return response.sendStatus(500); + } + }); } module.exports = { From a39ee32f9303209559d5a4c3dd25ec543134cad4 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:30:08 +0200 Subject: [PATCH 062/185] Horde fire and forget delete request --- public/script.js | 6 +----- public/scripts/horde.js | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/public/script.js b/public/script.js index 4c22c2ccf..58daa02c4 100644 --- a/public/script.js +++ b/public/script.js @@ -3777,11 +3777,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } reject(exception); - $("#send_textarea").removeAttr('disabled'); - is_send_press = false; - activateSendButtons(); - showSwipeButtons(); - setGenerationProgress(0); + unblockGeneration(); console.log(exception); streamingProcessor = null; }; diff --git a/public/scripts/horde.js b/public/scripts/horde.js index 37a59185c..50819f688 100644 --- a/public/scripts/horde.js +++ b/public/scripts/horde.js @@ -31,8 +31,8 @@ let horde_settings = { trusted_workers_only: false, }; -const MAX_RETRIES = 240; -const CHECK_INTERVAL = 5000; +const MAX_RETRIES = 480; +const CHECK_INTERVAL = 2500; const MIN_LENGTH = 16; const getRequestArgs = () => ({ method: "GET", @@ -152,7 +152,7 @@ async function generateHorde(prompt, params, signal, reportProgress) { for (let retryNumber = 0; retryNumber < MAX_RETRIES; retryNumber++) { if (signal.aborted) { - await fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${task_id}`, { + fetch(`https://horde.koboldai.net/api/v2/generate/text/status/${task_id}`, { method: 'DELETE', headers: { "Client-Agent": CLIENT_VERSION, From a02446c4cc7a90fb1fb136a730045ba8674fa427 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:40:23 +0200 Subject: [PATCH 063/185] Cancel deletion mode on switching chats --- public/script.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/script.js b/public/script.js index 58daa02c4..d7e5d1786 100644 --- a/public/script.js +++ b/public/script.js @@ -1360,6 +1360,9 @@ async function printMessages() { async function clearChat() { count_view_mes = 0; extension_prompts = {}; + if (is_delete_mode) { + $("#dialogue_del_mes_cancel").trigger('click'); + } $("#chat").children().remove(); if ($('.zoomed_avatar[forChar]').length) { console.debug('saw avatars to remove') From ac07c8324d4c46758c3decb44d2dc635d9f35217 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 21:57:54 +0200 Subject: [PATCH 064/185] Configurable chat truncation amount --- public/index.html | 13 +++++++++++++ public/script.js | 9 ++++----- public/scripts/power-user.js | 10 ++++++++++ public/style.css | 8 ++++++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/public/index.html b/public/index.html index c1e24f2c5..af512ce43 100644 --- a/public/index.html +++ b/public/index.html @@ -2756,6 +2756,19 @@
    +
    +
    + Chat Truncation (0 = unlimited) +
    +
    +
    + +
    +
    + +
    +
    +
diff --git a/public/script.js b/public/script.js index d7e5d1786..ba56be923 100644 --- a/public/script.js +++ b/public/script.js @@ -1297,11 +1297,9 @@ async function replaceCurrentChat() { } } -const TRUNCATION_THRESHOLD = 100; - export function showMoreMessages() { let messageId = Number($('#chat').children('.mes').first().attr('mesid')); - let count = TRUNCATION_THRESHOLD; + let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); const prevHeight = $('#chat').prop('scrollHeight'); @@ -1322,9 +1320,10 @@ export function showMoreMessages() { async function printMessages() { let startIndex = 0; + let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; - if (chat.length > TRUNCATION_THRESHOLD) { - count_view_mes = chat.length - TRUNCATION_THRESHOLD; + if (chat.length > count) { + count_view_mes = chat.length - count; startIndex = count_view_mes; $('#chat').append('
Show more messages
'); } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 3bf57c9d4..e8314353f 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -107,6 +107,7 @@ let power_user = { target_length: 400, }, markdown_escape_strings: '', + chat_truncation: 100, ui_mode: ui_mode.POWER, fast_ui_mode: true, @@ -1373,6 +1374,9 @@ function loadPowerUserSettings(settings, data) { $("#token_padding").val(power_user.token_padding); $("#aux_field").val(power_user.aux_field); + $("#chat_truncation").val(power_user.chat_truncation); + $('#chat_truncation_counter').val(power_user.chat_truncation); + $("#font_scale").val(power_user.font_scale); $("#font_scale_counter").val(power_user.font_scale); @@ -2559,6 +2563,12 @@ $(document).ready(() => { setHotswapsDebounced(); }); + $('#chat_truncation').on('input', function () { + power_user.chat_truncation = Number($('#chat_truncation').val()); + $('#chat_truncation_counter').val(power_user.chat_truncation); + saveSettingsDebounced(); + }); + $(`input[name="font_scale"]`).on('input', async function (e) { power_user.font_scale = Number(e.target.value); $("#font_scale_counter").val(power_user.font_scale); diff --git a/public/style.css b/public/style.css index d48cbd9f1..b46c1f8cb 100644 --- a/public/style.css +++ b/public/style.css @@ -3730,11 +3730,15 @@ a { #show_more_messages { text-align: center; - margin: 10px 0; + margin: 10px auto; font-weight: 500; - text-decoration: underline; order: -1; cursor: pointer; + padding: 0.5em 1em; + background-color: var(--SmartThemeBlurTintColor); + width: fit-content; + border-radius: 10px; + outline: 1px solid var(--SmartThemeBorderColor); } .draggable img { From 48034eb6c9e6e20c7c19c7da7803df3944dbe7c7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Sun, 19 Nov 2023 23:01:39 +0200 Subject: [PATCH 065/185] More info for mancer models --- public/scripts/mancer-settings.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/scripts/mancer-settings.js b/public/scripts/mancer-settings.js index 14f9bbcaa..5db00ec71 100644 --- a/public/scripts/mancer-settings.js +++ b/public/scripts/mancer-settings.js @@ -38,9 +38,13 @@ function getMancerModelTemplate(option) { return option.text; } + const creditsPerPrompt = (model.limits?.context - model.limits?.completion) * model.pricing?.prompt; + const creditsPerCompletion = model.limits?.completion * model.pricing?.completion; + const creditsTotal = Math.round(creditsPerPrompt + creditsPerCompletion).toFixed(0); + return $((`
-
${DOMPurify.sanitize(model.name)} | ${model.limits?.context} ctx
+
${DOMPurify.sanitize(model.name)} | ${model.limits?.context} ctx / ${model.limits?.completion} res | Credits per request (max): ${creditsTotal}
`)); } From d87e44ff034f10a2881380fce1bd5c75d904b598 Mon Sep 17 00:00:00 2001 From: ThisIsPIRI Date: Mon, 20 Nov 2023 08:51:37 +0900 Subject: [PATCH 066/185] Corrections to /help macros, /help format --- public/scripts/templates/formatting.html | 2 +- public/scripts/templates/macros.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/templates/formatting.html b/public/scripts/templates/formatting.html index 71671a3b0..4b562fdab 100644 --- a/public/scripts/templates/formatting.html +++ b/public/scripts/templates/formatting.html @@ -8,7 +8,7 @@ Text formatting commands:
 like this
  • `text` - displays as inline code
  • -
  • text - displays as a blockquote (note the space after >)
  • +
  • > text - displays as a blockquote (note the space after >)
  • like this
  • # text - displays as a large header (note the space)
  • like this

    diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 73386118b..9c55db112 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -17,8 +17,8 @@ System-wide Replacement Macros (in order of evaluation):
  • {{time}} – the current time
  • {{date}} – the current date
  • {{weekday}} – the current weekday
  • -
  • {{isotime}} – the current ISO date (YYYY-MM-DD)
  • -
  • {{isodate}} – the current ISO time (24-hour clock)
  • +
  • {{isotime}} – the current ISO time (24-hour clock)
  • +
  • {{isodate}} – the current ISO date (YYYY-MM-DD)
  • {{datetimeformat …}} – the current date/time in the specified format, e. g. for German date/time: {{datetimeformat DD.MM.YYYY HH:mm}}
  • {{time_UTC±#}} – the current time in the specified UTC time zone offset, e.g. UTC-4 or UTC+2
  • {{idle_duration}} – the time since the last user message was sent
  • From 1dd1cd69ac146cfbebea4fe1086140804e5eca6e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 20 Nov 2023 11:57:55 +0000 Subject: [PATCH 067/185] fix all popups being large/wide after first one --- public/script.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/script.js b/public/script.js index 4c22c2ccf..7947dade7 100644 --- a/public/script.js +++ b/public/script.js @@ -6230,10 +6230,14 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, large } if (wide) { $("#dialogue_popup").addClass("wide_dialogue_popup"); + } else { + $("#dialogue_popup").removeClass("wide_dialogue_popup"); } if (large) { $("#dialogue_popup").addClass("large_dialogue_popup"); + } else { + $("#dialogue_popup").removeClass("large_dialogue_popup"); } $("#dialogue_popup_cancel").css("display", "inline-block"); From d5b9dd34b70b3f116a4a9fb96486f2a240760a7b Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 20 Nov 2023 10:16:18 +0000 Subject: [PATCH 068/185] remove unused comfy method --- public/scripts/extensions/stable-diffusion/index.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index c9e2fc29e..6d9aae232 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -846,12 +846,6 @@ function onComfyUrlInput() { saveSettingsDebounced(); } -function onComfyPromptInput() { - extension_settings.sd.comfy_prompt = $('#sd_comfy_prompt').val(); - resetScrollHeight($(this)); - saveSettingsDebounced(); -} - async function validateAutoUrl() { try { if (!extension_settings.sd.auto_url) { From 5e5c111d25af72cea8c6511619407d162ca5e4e4 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Mon, 20 Nov 2023 10:35:11 +0000 Subject: [PATCH 069/185] add VAE selection --- .../stable-diffusion/comfyWorkflowEditor.html | 1 + .../extensions/stable-diffusion/index.js | 69 ++++++++++++++++++- .../extensions/stable-diffusion/settings.html | 4 ++ src/stable-diffusion.js | 18 +++++ 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html b/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html index e8b438f84..db4e81b13 100644 --- a/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html +++ b/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html @@ -12,6 +12,7 @@
  • "%prompt%"
  • "%negative_prompt%"
  • "%model%"
  • +
  • "%vae%"
  • "%sampler%"
  • "%scheduler%"
  • "%steps%"
  • diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 6d9aae232..ad597d2e9 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -186,6 +186,7 @@ const defaultSettings = { negative_prompt: defaultNegative, sampler: 'DDIM', model: '', + vae: '', // Automatic1111/Horde exclusives restore_faces: false, @@ -480,7 +481,7 @@ async function loadSettings() { toggleSourceControls(); addPromptTemplates(); - await Promise.all([loadSamplers(), loadModels(), loadSchedulers()]); + await Promise.all([loadSamplers(), loadModels(), loadSchedulers(), loadVaes()]); } function addPromptTemplates() { @@ -914,6 +915,7 @@ async function validateComfyUrl() { await loadSamplers(); await loadSchedulers(); await loadModels(); + await loadVaes(); toastr.success('ComfyUI API connected.'); } catch (error) { toastr.error(`Could not validate ComfyUI API: ${error.message}`); @@ -960,6 +962,10 @@ async function getAutoRemoteModel() { } } +async function onVaeChange() { + extension_settings.sd.vae = $('#sd_vae').find(':selected').val(); +} + async function getAutoRemoteUpscalers() { try { const result = await fetch('/api/sd/upscalers', { @@ -1473,6 +1479,65 @@ async function loadComfySchedulers() { } } +async function loadVaes() { + $('#sd_vae').empty(); + let vaes = []; + + switch (extension_settings.sd.source) { + case sources.extras: + vaes = ['N/A']; + break; + case sources.horde: + vaes = ['N/A']; + break; + case sources.auto: + vaes = ['N/A']; + break; + case sources.novel: + vaes = ['N/A']; + break; + case sources.vlad: + vaes = ['N/A']; + break; + case sources.openai: + vaes = ['N/A']; + break; + case sources.comfy: + vaes = await loadComfyVaes(); + break; + } + + for (const vae of vaes) { + const option = document.createElement('option'); + option.innerText = vae; + option.value = vae; + option.selected = vae === extension_settings.sd.vae; + $('#sd_vae').append(option); + } +} + +async function loadComfyVaes() { + if (!extension_settings.sd.comfy_url) { + return []; + } + + try { + const result = await fetch(`/api/sd/comfy/vaes`, { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + url: extension_settings.sd.comfy_url, + }) + }); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + return await result.json(); + } catch (error) { + return []; + } +} + function getGenerationType(prompt) { let mode = generationMode.FREE; @@ -2037,6 +2102,7 @@ async function generateComfyImage(prompt) { const placeholders = [ 'negative_prompt', 'model', + 'vae', 'sampler', 'scheduler', 'steps', @@ -2294,6 +2360,7 @@ jQuery(async () => { $('#sd_scale').on('input', onScaleInput); $('#sd_steps').on('input', onStepsInput); $('#sd_model').on('change', onModelChange); + $('#sd_vae').on('change', onVaeChange); $('#sd_sampler').on('change', onSamplerChange); $('#sd_scheduler').on('change', onSchedulerChange); $('#sd_prompt_prefix').on('input', onPromptPrefixInput); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index cb0a20cd4..00a0e3673 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -150,6 +150,10 @@
+
+ + +
diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index eaec3dcc7..24a937a22 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -15,7 +15,7 @@ import { registerDebugFunction, } from "./power-user.js"; import { SENTENCEPIECE_TOKENIZERS, getTextTokens, tokenizers } from "./tokenizers.js"; -import { onlyUnique } from "./utils.js"; +import { getSortableDelay, onlyUnique } from "./utils.js"; export { textgenerationwebui_settings, @@ -38,6 +38,7 @@ const MANCER_SERVER_KEY = 'mancer_server'; const MANCER_SERVER_DEFAULT = 'https://neuro.mancer.tech'; export let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEFAULT; +const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5]; const textgenerationwebui_settings = { temp: 0.7, temperature_last: true, @@ -86,6 +87,7 @@ const textgenerationwebui_settings = { type: textgen_types.OOBA, mancer_model: 'mytholite', legacy_api: false, + sampler_order: KOBOLDCPP_ORDER, }; export let textgenerationwebui_banned_in_macros = []; @@ -136,6 +138,7 @@ const setting_names = [ //'logits_processors_aphrodite', //'log_probs_aphrodite', //'prompt_log_probs_aphrodite' + "sampler_order", ]; async function selectPreset(name) { @@ -311,7 +314,41 @@ export function getTextGenUrlSourceId() { } } +/** + * Sorts the sampler items by the given order. + * @param {any[]} orderArray Sampler order array. + */ +function sortItemsByOrder(orderArray) { + console.debug('Preset samplers order: ' + orderArray); + const $draggableItems = $("#koboldcpp_order"); + + for (let i = 0; i < orderArray.length; i++) { + const index = orderArray[i]; + const $item = $draggableItems.find(`[data-id="${index}"]`).detach(); + $draggableItems.append($item); + } +} + jQuery(function () { + $('#koboldcpp_order').sortable({ + delay: getSortableDelay(), + stop: function () { + const order = []; + $('#koboldcpp_order').children().each(function () { + order.push($(this).data('id')); + }); + textgenerationwebui_settings.sampler_order = order; + console.log('Samplers reordered:', textgenerationwebui_settings.sampler_order); + saveSettingsDebounced(); + }, + }); + + $('#koboldcpp_default_order').on('click', function () { + textgenerationwebui_settings.sampler_order = KOBOLDCPP_ORDER; + sortItemsByOrder(textgenerationwebui_settings.sampler_order); + saveSettingsDebounced(); + }); + $('#textgen_type').on('change', function () { const type = String($(this).val()); textgenerationwebui_settings.type = type; @@ -399,26 +436,33 @@ function showTypeSpecificControls(type) { }); } -function setSettingByName(i, value, trigger) { +function setSettingByName(setting, value, trigger) { if (value === null || value === undefined) { return; } - const isCheckbox = $(`#${i}_textgenerationwebui`).attr('type') == 'checkbox'; - const isText = $(`#${i}_textgenerationwebui`).attr('type') == 'text' || $(`#${i}_textgenerationwebui`).is('textarea'); + if ('sampler_order' === setting) { + value = Array.isArray(value) ? value : KOBOLDCPP_ORDER; + sortItemsByOrder(value); + textgenerationwebui_settings.sampler_order = value; + return; + } + + const isCheckbox = $(`#${setting}_textgenerationwebui`).attr('type') == 'checkbox'; + const isText = $(`#${setting}_textgenerationwebui`).attr('type') == 'text' || $(`#${setting}_textgenerationwebui`).is('textarea'); if (isCheckbox) { const val = Boolean(value); - $(`#${i}_textgenerationwebui`).prop('checked', val); + $(`#${setting}_textgenerationwebui`).prop('checked', val); } else if (isText) { - $(`#${i}_textgenerationwebui`).val(value); + $(`#${setting}_textgenerationwebui`).val(value); } else { const val = parseFloat(value); - $(`#${i}_textgenerationwebui`).val(val); - $(`#${i}_counter_textgenerationwebui`).val(val); + $(`#${setting}_textgenerationwebui`).val(val); + $(`#${setting}_counter_textgenerationwebui`).val(val); if (power_user.enableZenSliders) { - let zenSlider = $(`#${i}_textgenerationwebui_zenslider`).slider() + let zenSlider = $(`#${setting}_textgenerationwebui_zenslider`).slider() zenSlider.slider('option', 'value', val) zenSlider.slider('option', 'slide') .call(zenSlider, null, { @@ -428,7 +472,7 @@ function setSettingByName(i, value, trigger) { } if (trigger) { - $(`#${i}_textgenerationwebui`).trigger('input'); + $(`#${setting}_textgenerationwebui`).trigger('input'); } } @@ -571,6 +615,7 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso 'use_ooba': isOoba(), 'api_server': isMancer() ? MANCER_SERVER : api_server_textgenerationwebui, 'legacy_api': textgenerationwebui_settings.legacy_api && !isMancer(), + 'sampler_order': isKoboldCpp() ? textgenerationwebui_settings.sampler_order : undefined, }; let aphroditeExclusionFlags = { 'repetition_penalty_range': textgenerationwebui_settings.rep_pen_range, From 52d9855916d9177c442ff2b521ae4898a5949818 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 02:00:50 +0200 Subject: [PATCH 086/185] Code lint --- default/content/Default_Comfy_Workflow.json | 170 +++++++++--------- .../stable-diffusion/comfyWorkflowEditor.html | 60 +++---- .../extensions/stable-diffusion/index.js | 16 +- src/stable-diffusion.js | 4 +- 4 files changed, 122 insertions(+), 128 deletions(-) diff --git a/default/content/Default_Comfy_Workflow.json b/default/content/Default_Comfy_Workflow.json index 2f8082d83..10bb0cc3c 100644 --- a/default/content/Default_Comfy_Workflow.json +++ b/default/content/Default_Comfy_Workflow.json @@ -1,86 +1,86 @@ { - "3": { - "class_type": "KSampler", - "inputs": { - "cfg": "%scale%", - "denoise": 1, - "latent_image": [ - "5", - 0 - ], - "model": [ - "4", - 0 - ], - "negative": [ - "7", - 0 - ], - "positive": [ - "6", - 0 - ], - "sampler_name": "%sampler%", - "scheduler": "%scheduler%", - "seed": "%seed%", - "steps": "%steps%" - } - }, - "4": { - "class_type": "CheckpointLoaderSimple", - "inputs": { - "ckpt_name": "%model%" - } - }, - "5": { - "class_type": "EmptyLatentImage", - "inputs": { - "batch_size": 1, - "height": "%height%", - "width": "%width%" - } - }, - "6": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "%prompt%" - } - }, - "7": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "%negative_prompt%" - } - }, - "8": { - "class_type": "VAEDecode", - "inputs": { - "samples": [ - "3", - 0 - ], - "vae": [ - "4", - 2 - ] - } - }, - "9": { - "class_type": "SaveImage", - "inputs": { - "filename_prefix": "SillyTavern", - "images": [ - "8", - 0 - ] - } - } -} \ No newline at end of file + "3": { + "class_type": "KSampler", + "inputs": { + "cfg": "%scale%", + "denoise": 1, + "latent_image": [ + "5", + 0 + ], + "model": [ + "4", + 0 + ], + "negative": [ + "7", + 0 + ], + "positive": [ + "6", + 0 + ], + "sampler_name": "%sampler%", + "scheduler": "%scheduler%", + "seed": "%seed%", + "steps": "%steps%" + } + }, + "4": { + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "%model%" + } + }, + "5": { + "class_type": "EmptyLatentImage", + "inputs": { + "batch_size": 1, + "height": "%height%", + "width": "%width%" + } + }, + "6": { + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "%prompt%" + } + }, + "7": { + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "%negative_prompt%" + } + }, + "8": { + "class_type": "VAEDecode", + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + } + }, + "9": { + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "SillyTavern", + "images": [ + "8", + 0 + ] + } + } +} diff --git a/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html b/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html index 1839c67e8..29ebb9197 100644 --- a/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html +++ b/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html @@ -1,31 +1,31 @@ \ No newline at end of file +
+

ComfyUI Workflow Editor:

+
+
+ + +
+
+
Placeholders
+
    +
  • "%prompt%"
  • +
  • "%negative_prompt%"
  • +
  • "%model%"
  • +
  • "%vae%"
  • +
  • "%sampler%"
  • +
  • "%scheduler%"
  • +
  • "%steps%"
  • +
  • "%scale%"
  • +
  • "%width%"
  • +
  • "%height%"
  • +

  • +
  • + "%seed%" + ? +
  • +
+
+
+
+
diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index 4e2b1ccc9..ca1a872ca 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -659,7 +659,7 @@ async function onSourceChange() { extension_settings.sd.sampler = null; toggleSourceControls(); saveSettingsDebounced(); - await Promise.all([loadModels(), loadSamplers()]); + await loadSettingOptions(); } async function onOpenAiStyleSelect() { @@ -791,8 +791,7 @@ async function validateAutoUrl() { throw new Error('SD WebUI returned an error.'); } - await loadSamplers(); - await loadModels(); + await loadSettingOptions(); toastr.success('SD WebUI API connected.'); } catch (error) { toastr.error(`Could not validate SD WebUI API: ${error.message}`); @@ -815,8 +814,7 @@ async function validateVladUrl() { throw new Error('SD.Next returned an error.'); } - await loadSamplers(); - await loadModels(); + await loadSettingOptions(); toastr.success('SD.Next API connected.'); } catch (error) { toastr.error(`Could not validate SD.Next API: ${error.message}`); @@ -840,11 +838,7 @@ async function validateComfyUrl() { throw new Error('ComfyUI returned an error.'); } - await loadSamplers(); - await loadSchedulers(); - await loadModels(); - await loadVaes(); - await loadComfyWorkflows(); + await loadSettingOptions(); toastr.success('ComfyUI API connected.'); } catch (error) { toastr.error(`Could not validate ComfyUI API: ${error.message}`); @@ -2444,7 +2438,7 @@ jQuery(async () => { }) eventSource.on(event_types.EXTRAS_CONNECTED, async () => { - await Promise.all([loadSamplers(), loadModels()]); + await loadSettingOptions(); }); eventSource.on(event_types.CHAT_CHANGED, onChatChanged); diff --git a/src/stable-diffusion.js b/src/stable-diffusion.js index 3880b9bd2..fd6a9c8fb 100644 --- a/src/stable-diffusion.js +++ b/src/stable-diffusion.js @@ -45,7 +45,7 @@ function removePattern(x, pattern) { function getComfyWorkflows() { return fs .readdirSync(DIRECTORIES.comfyWorkflows) - .filter(file => file[0]!='.' && file.toLowerCase().endsWith('.json')) + .filter(file => file[0] != '.' && file.toLowerCase().endsWith('.json')) .sort(Intl.Collator().compare); } @@ -465,7 +465,7 @@ function registerEndpoints(app, jsonParser) { } const data = fs.readFileSync( path, - {encoding:'utf-8'} + { encoding: 'utf-8' } ); return response.send(JSON.stringify(data)); } catch (error) { From 01b629bd493a474fd76875d89e208185a59ed6c1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 02:54:04 +0200 Subject: [PATCH 087/185] New syntax for sendas command --- public/scripts/slash-commands.js | 58 ++++++++++++++++++++------------ public/style.css | 2 +- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 31cf55209..2a5066036 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -77,21 +77,22 @@ class SlashCommandParser { let unnamedArg; if (args.length > 0) { - const argsArray = args.split(' '); - for (let arg of argsArray) { - const equalsIndex = arg.indexOf('='); - if (equalsIndex !== -1) { - const key = arg.substring(0, equalsIndex); - const value = arg.substring(equalsIndex + 1); - // Replace "wrapping quotes" used for escaping spaces - argObj[key] = value.replace(/(^")|("$)/g, ''); - } - else { - break; - } + // Match named arguments + const namedArgPattern = /(\w+)=("(?:\\.|[^"\\])*"|\S+)/g; + let match; + while ((match = namedArgPattern.exec(args)) !== null) { + const key = match[1]; + const value = match[2]; + // Remove the quotes around the value, if any + argObj[key] = value.replace(/(^")|("$)/g, ''); } - unnamedArg = argsArray.slice(Object.keys(argObj).length).join(' '); + // Match unnamed argument + const unnamedArgPattern = /(?:\w+=(?:"(?:\\.|[^"\\])*"|\S+)\s*)*(.*)/s; + match = unnamedArgPattern.exec(args); + if (match !== null) { + unnamedArg = match[1].trim(); + } // Excluded commands format in their own function if (!excludedFromRegex.includes(command)) { @@ -132,7 +133,7 @@ parser.addCommand('name', setNameCallback, ['persona'], '(filename) – sets a background according to filename, partial names allowed', false, true); -parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe":
/sendas Chloe
Hello, guys!
`, true, true); +parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe": /sendas name="Chloe" Hello, guys!`, true, true); parser.addCommand('sys', sendNarratorMessage, ['nar'], '(text) – sends message as a system narrator', false, true); parser.addCommand('sysname', setNarratorName, [], '(name) – sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', true, true); parser.addCommand('comment', sendCommentMessage, [], '(text) – adds a note/comment message not part of the chat', false, true); @@ -499,19 +500,32 @@ async function setNarratorName(_, text) { await saveChatConditional(); } -export async function sendMessageAs(_, text) { +export async function sendMessageAs(namedArgs, text) { if (!text) { return; } - 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; - } + let name; + let mesText; - const name = parts.shift().trim(); - let mesText = parts.join('\n').trim(); + if (namedArgs.name) { + name = namedArgs.name.trim(); + mesText = text.trim(); + + if (!name && !text) { + toastr.warning('You must specify a name and text to send as'); + return; + } + } else { + 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; + } + + name = parts.shift().trim(); + mesText = parts.join('\n').trim(); + } // Requires a regex check after the slash command is pushed to output mesText = getRegexedString(mesText, regex_placement.SLASH_COMMAND, { characterOverride: name }); diff --git a/public/style.css b/public/style.css index b46c1f8cb..2637870c6 100644 --- a/public/style.css +++ b/public/style.css @@ -218,7 +218,7 @@ table.responsiveTable { color: var(--white50a); } -.mes[is_system="true"] .mes_text br { +.mes[is_system="true"][ch_name="SillyTavern System"] .mes_text br { display: none; } From 9f16b329c5a3a7561c1c66eaa3182336429f0035 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Tue, 21 Nov 2023 01:26:43 +0000 Subject: [PATCH 088/185] ensure checkboxes have contrast --- public/scripts/power-user.js | 5 +++++ public/style.css | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index e8314353f..807941eaf 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -917,6 +917,11 @@ function applyChatWidth(type) { async function applyThemeColor(type) { if (type === 'main') { document.documentElement.style.setProperty('--SmartThemeBodyColor', power_user.main_text_color); + const color = power_user.main_text_color.split('(')[1].split(')')[0].split(','); + document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorR', color[0]); + document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorG', color[1]); + document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorB', color[2]); + document.documentElement.style.setProperty('--SmartThemeCheckboxBgColorA', color[3]); } if (type === 'italics') { document.documentElement.style.setProperty('--SmartThemeEmColor', power_user.italics_text_color); diff --git a/public/style.css b/public/style.css index b46c1f8cb..14dd75980 100644 --- a/public/style.css +++ b/public/style.css @@ -58,6 +58,25 @@ --SmartThemeBlurStrength: calc(var(--blurStrength) * 1px); --SmartThemeShadowColor: rgba(0, 0, 0, 0.5); --SmartThemeBorderColor: rgba(0, 0, 0, 0.5); + --SmartThemeCheckboxBgColorR: 220; + --SmartThemeCheckboxBgColorG: 220; + --SmartThemeCheckboxBgColorB: 210; + --SmartThemeCheckboxTickColorValue: calc( + ( + ( + ( + (var(--SmartThemeCheckboxBgColorR) * 299) + + (var(--SmartThemeCheckboxBgColorG) * 587) + + (var(--SmartThemeCheckboxBgColorB) * 114) + ) / 1000 + ) - 128 + ) * -1000 + ); + --SmartThemeCheckboxTickColor: rgb( + var(--SmartThemeCheckboxTickColorValue), + var(--SmartThemeCheckboxTickColorValue), + var(--SmartThemeCheckboxTickColorValue) + ); --sheldWidth: 50vw; @@ -2242,7 +2261,7 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button height: 0.65em; transform: scale(0); transition: 120ms transform ease-in-out; - box-shadow: inset 1em 1em var(--SmartThemeBlurTintColor); + box-shadow: inset 1em 1em var(--SmartThemeCheckboxTickColor); transform-origin: bottom left; clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); } From bcad0d4e512a4d58ba2d3a77ef419db737095fef Mon Sep 17 00:00:00 2001 From: Danil Boldyrev Date: Tue, 21 Nov 2023 13:16:56 +0300 Subject: [PATCH 089/185] add XTTS --- public/scripts/extensions/tts/index.js | 2 + public/scripts/extensions/tts/xtts.js | 191 +++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 public/scripts/extensions/tts/xtts.js diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 6736c5bb9..de3041013 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -10,6 +10,7 @@ import { NovelTtsProvider } from './novel.js' import { power_user } from '../../power-user.js' import { registerSlashCommand } from '../../slash-commands.js' import { OpenAITtsProvider } from './openai.js' +import {XTTSTtsProvider} from "./xtts.js" export { talkingAnimation }; const UPDATE_INTERVAL = 1000 @@ -70,6 +71,7 @@ export function getPreviewString(lang) { let ttsProviders = { ElevenLabs: ElevenLabsTtsProvider, Silero: SileroTtsProvider, + XTTSv2: XTTSTtsProvider, System: SystemTtsProvider, Coqui: CoquiTtsProvider, Edge: EdgeTtsProvider, diff --git a/public/scripts/extensions/tts/xtts.js b/public/scripts/extensions/tts/xtts.js new file mode 100644 index 000000000..2d3764ca9 --- /dev/null +++ b/public/scripts/extensions/tts/xtts.js @@ -0,0 +1,191 @@ +import { doExtrasFetch, getApiUrl, modules } from "../../extensions.js" +import { saveTtsProviderSettings } from "./index.js" + +export { XTTSTtsProvider } + +class XTTSTtsProvider { + //########// + // Config // + //########// + + settings + ready = false + voices = [] + separator = ' .. ' + + languageLabels = { + "Arabic": "ar", + "Brazilian Portuguese": "pt", + "Chinese": "zh-cn", + "Czech": "cs", + "Dutch": "nl", + "English": "en", + "French": "fr", + "German": "de", + "Italian": "it", + "Polish": "pl", + "Russian": "ru", + "Spanish": "es", + "Turkish": "tr", + "Japanese": "ja", + "Korean": "ko", + "Hungarian": "hu" + } + + defaultSettings = { + provider_endpoint: "http://localhost:8020", + language: "en", + voiceMap: {} + } + + get settingsHtml() { + let html = ` + + + + + + `; + + html += ` + + + Use XTTSv2 TTS Server. + `; + + return html; + } + onSettingsChange() { + // Used when provider settings are updated from UI + this.settings.provider_endpoint = $('#xtts_tts_endpoint').val() + this.settings.language = $('#xtts_api_language').val() + saveTtsProviderSettings() + } + + async loadSettings(settings) { + // Pupulate Provider UI given input settings + if (Object.keys(settings).length == 0) { + console.info("Using default TTS Provider settings") + } + + // Only accept keys defined in defaultSettings + this.settings = this.defaultSettings + + for (const key in settings){ + if (key in this.settings){ + this.settings[key] = settings[key] + } else { + throw `Invalid setting passed to TTS Provider: ${key}` + } + } + + const apiCheckInterval = setInterval(() => { + // Use Extras API if TTS support is enabled + if (modules.includes('tts') || modules.includes('xtts-tts')) { + const baseUrl = new URL(getApiUrl()); + baseUrl.pathname = '/api/tts'; + this.settings.provider_endpoint = baseUrl.toString(); + $('#xtts_tts_endpoint').val(this.settings.provider_endpoint); + clearInterval(apiCheckInterval); + } + }, 2000); + + $('#xtts_tts_endpoint').val(this.settings.provider_endpoint) + $('#xtts_tts_endpoint').on("input", () => {this.onSettingsChange()}) + $('#xtts_api_language').val(this.settings.language) + $('#xtts_api_language').on("change", () => {this.onSettingsChange()}) + + await this.checkReady() + + console.debug("XTTS: Settings loaded") + } + + // Perform a simple readiness check by trying to fetch voiceIds + async checkReady(){ + + const response = await this.fetchTtsVoiceObjects() + } + + async onRefreshClick() { + return + } + + //#################// + // TTS Interfaces // + //#################// + + async getVoice(voiceName) { + if (this.voices.length == 0) { + this.voices = await this.fetchTtsVoiceObjects() + } + const match = this.voices.filter( + XTTSVoice => XTTSVoice.name == voiceName + )[0] + if (!match) { + throw `TTS Voice name ${voiceName} not found` + } + return match + } + + async generateTts(text, voiceId){ + const response = await this.fetchTtsGeneration(text, voiceId) + return response + } + + //###########// + // API CALLS // + //###########// + async fetchTtsVoiceObjects() { + const response = await doExtrasFetch(`${this.settings.provider_endpoint}/speakers`) + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.json()}`) + } + const responseJson = await response.json() + return responseJson + } + + async fetchTtsGeneration(inputText, voiceId) { + console.info(`Generating new TTS for voice_id ${voiceId}`) + const response = await doExtrasFetch( + `${this.settings.provider_endpoint}/tts_to_audio/`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache' // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23 + }, + body: JSON.stringify({ + "text": inputText, + "speaker_wav": voiceId, + "language": this.settings.language + }) + } + ) + if (!response.ok) { + toastr.error(response.statusText, 'TTS Generation Failed'); + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + return response + } + + // Interface not used by XTTS TTS + async fetchTtsFromHistory(history_item_id) { + return Promise.resolve(history_item_id); + } + +} From 73e081dd992eff0b6393a136dcf5ed93affad860 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:38:15 +0200 Subject: [PATCH 090/185] Don't use global state to build Chat Completion prompts --- public/script.js | 11 +++- public/scripts/openai.js | 117 +++++++++++++++++++++++---------------- 2 files changed, 78 insertions(+), 50 deletions(-) diff --git a/public/script.js b/public/script.js index fab6e4783..504e034eb 100644 --- a/public/script.js +++ b/public/script.js @@ -3105,10 +3105,13 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, const storyString = renderStoryString(storyStringParams); + let oaiMessages = []; + let oaiMessageExamples = []; + if (main_api === 'openai') { message_already_generated = ''; - setOpenAIMessages(coreChat); - setOpenAIMessageExamples(mesExamplesArray); + oaiMessages = setOpenAIMessages(coreChat); + oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray); } // hack for regeneration of the first message @@ -3537,7 +3540,9 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, cyclePrompt: cyclePrompt, systemPromptOverride: system, jailbreakPromptOverride: jailbreak, - personaDescription: persona + personaDescription: persona, + messages: oaiMessages, + messageExamples: oaiMessageExamples, }, dryRun); generate_data = { prompt: prompt }; diff --git a/public/scripts/openai.js b/public/scripts/openai.js index be1fdab27..b5a5e8ad6 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -64,7 +64,6 @@ import { } from "./instruct-mode.js"; export { - openai_msgs, openai_messages_count, oai_settings, loadOpenAISettings, @@ -79,8 +78,6 @@ export { MessageCollection } -let openai_msgs = []; -let openai_msgs_example = []; let openai_messages_count = 0; let openai_narrator_messages_count = 0; @@ -388,10 +385,15 @@ function convertChatCompletionToInstruct(messages, type) { return prompt; } +/** + * Formats chat messages into chat completion messages. + * @param {object[]} chat - Array containing all messages. + * @returns {object[]} - Array containing all messages formatted for chat completion. + */ function setOpenAIMessages(chat) { let j = 0; // clean openai msgs - openai_msgs = []; + const messages = []; openai_narrator_messages_count = 0; for (let i = chat.length - 1; i >= 0; i--) { let role = chat[j]['is_user'] ? 'user' : 'assistant'; @@ -418,21 +420,29 @@ function setOpenAIMessages(chat) { if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`; const name = chat[j]['name']; const image = chat[j]?.extra?.image; - openai_msgs[i] = { "role": role, "content": content, name: name, "image": image }; + messages[i] = { "role": role, "content": content, name: name, "image": image }; j++; } + + return messages } +/** + * Formats chat messages into chat completion messages. + * @param {string[]} mesExamplesArray - Array containing all examples. + * @returns {object[]} - Array containing all examples formatted for chat completion. + */ function setOpenAIMessageExamples(mesExamplesArray) { // get a nice array of all blocks of all example messages = array of arrays (important!) - openai_msgs_example = []; + const examples = []; for (let item of mesExamplesArray) { // remove {Example Dialogue:} and replace \r\n with just \n let replaced = item.replace(//i, "{Example Dialogue:}").replace(/\r/gm, ''); let parsed = parseExampleIntoIndividual(replaced); // add to the example message blocks array - openai_msgs_example.push(parsed); + examples.push(parsed); } + return examples; } /** @@ -554,8 +564,9 @@ function formatWorldInfo(value) { * This function populates the injections in the conversation. * * @param {Prompt[]} prompts - Array containing injection prompts. + * @param {Object[]} messages - Array containing all messages. */ -function populationInjectionPrompts(prompts) { +function populationInjectionPrompts(prompts, messages) { let totalInsertedMessages = 0; for (let i = 0; i <= MAX_INJECTION_DEPTH; i++) { @@ -581,12 +592,13 @@ function populationInjectionPrompts(prompts) { if (roleMessages.length) { const injectIdx = i + totalInsertedMessages; - openai_msgs.splice(injectIdx, 0, ...roleMessages); + messages.splice(injectIdx, 0, ...roleMessages); totalInsertedMessages += roleMessages.length; } } - openai_msgs = openai_msgs.reverse(); + messages = messages.reverse(); + return messages; } export function isOpenRouterWithInstruct() { @@ -595,13 +607,13 @@ export function isOpenRouterWithInstruct() { /** * Populates the chat history of the conversation. - * + * @param {object[]} messages - Array containing all messages. * @param {PromptCollection} prompts - Map object containing all prompts where the key is the prompt identifier and the value is the prompt object. * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts. * @param type * @param cyclePrompt */ -async function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = null) { +async function populateChatHistory(messages, prompts, chatCompletion, type = null, cyclePrompt = null) { chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory')); let names = (selected_group && groups.find(x => x.id === selected_group)?.members.map(member => characters.find(c => c.avatar === member)?.name).filter(Boolean).join(', ')) || ''; @@ -632,7 +644,7 @@ async function populateChatHistory(prompts, chatCompletion, type = null, cyclePr chatCompletion.reserveBudget(continueMessage); } - const lastChatPrompt = openai_msgs[openai_msgs.length - 1]; + const lastChatPrompt = messages[messages.length - 1]; const message = new Message('user', oai_settings.send_if_empty, 'emptyUserMessageReplacement'); if (lastChatPrompt && lastChatPrompt.role === 'assistant' && oai_settings.send_if_empty && chatCompletion.canAfford(message)) { chatCompletion.insert(message, 'chatHistory'); @@ -641,13 +653,13 @@ async function populateChatHistory(prompts, chatCompletion, type = null, cyclePr const imageInlining = isImageInliningSupported(); // Insert chat messages as long as there is budget available - const chatPool = [...openai_msgs].reverse(); + const chatPool = [...messages].reverse(); for (let index = 0; index < chatPool.length; index++) { const chatPrompt = chatPool[index]; // We do not want to mutate the prompt const prompt = new Prompt(chatPrompt); - prompt.identifier = `chatHistory-${openai_msgs.length - index}`; + prompt.identifier = `chatHistory-${messages.length - index}`; const chatMessage = Message.fromPrompt(promptManager.preparePrompt(prompt)); if (true === promptManager.serviceSettings.names_in_completion && prompt.name) { @@ -688,12 +700,13 @@ async function populateChatHistory(prompts, chatCompletion, type = null, cyclePr * * @param {PromptCollection} prompts - Map object containing all prompts where the key is the prompt identifier and the value is the prompt object. * @param {ChatCompletion} chatCompletion - An instance of ChatCompletion class that will be populated with the prompts. + * @param {Object[]} messageExamples - Array containing all message examples. */ -function populateDialogueExamples(prompts, chatCompletion) { +function populateDialogueExamples(prompts, chatCompletion, messageExamples) { chatCompletion.add(new MessageCollection('dialogueExamples'), prompts.index('dialogueExamples')); - if (openai_msgs_example.length) { + if (Array.isArray(messageExamples) && messageExamples.length) { const newExampleChat = new Message('system', oai_settings.new_example_chat_prompt, 'newChat'); - [...openai_msgs_example].forEach((dialogue, dialogueIndex) => { + [...messageExamples].forEach((dialogue, dialogueIndex) => { let examplesAdded = 0; if (chatCompletion.canAfford(newExampleChat)) chatCompletion.insert(newExampleChat, 'dialogueExamples'); @@ -744,8 +757,12 @@ function getPromptPosition(position) { * @param {string} options.quietPrompt - Instruction prompt for extras * @param {string} options.quietImage - Image prompt for extras * @param {string} options.type - The type of the chat, can be 'impersonate'. + * @param {string} options.cyclePrompt - The last prompt in the conversation. + * @param {object[]} options.messages - Array containing all messages. + * @param {object[]} options.messageExamples - Array containing all message examples. + * @returns {Promise} */ -async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt } = {}) { +async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples } = {}) { // Helper function for preparing a prompt, that already exists within the prompt collection, for completion const addToChatCompletion = (source, target = null) => { // We need the prompts array to determine a position for the source. @@ -852,15 +869,15 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm } // Add in-chat injections - populationInjectionPrompts(userAbsolutePrompts); + messages = populationInjectionPrompts(userAbsolutePrompts, messages); // Decide whether dialogue examples should always be added if (power_user.pin_examples) { - populateDialogueExamples(prompts, chatCompletion); - await populateChatHistory(prompts, chatCompletion, type, cyclePrompt); + populateDialogueExamples(prompts, chatCompletion, messageExamples); + await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt); } else { - await populateChatHistory(prompts, chatCompletion, type, cyclePrompt); - populateDialogueExamples(prompts, chatCompletion); + await populateChatHistory(messages, prompts, chatCompletion, type, cyclePrompt); + populateDialogueExamples(prompts, chatCompletion, messageExamples); } chatCompletion.freeBudget(controlPrompts); @@ -998,6 +1015,8 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor * @param {string} content.quietPrompt - The quiet prompt to be used in the conversation. * @param {string} content.cyclePrompt - The last prompt used for chat message continuation. * @param {Array} content.extensionPrompts - An array of additional prompts. + * @param {object[]} content.messages - An array of messages to be used as chat history. + * @param {string[]} content.messageExamples - An array of messages to be used as dialogue examples. * @param dryRun - Whether this is a live call or not. * @returns {(*[]|boolean)[]} An array where the first element is the prepared chat and the second element is a boolean flag. */ @@ -1016,7 +1035,9 @@ export async function prepareOpenAIMessages({ cyclePrompt, systemPromptOverride, jailbreakPromptOverride, - personaDescription + personaDescription, + messages, + messageExamples, } = {}, dryRun) { // Without a character selected, there is no way to accurately calculate tokens if (!promptManager.activeCharacter && dryRun) return [null, false]; @@ -1042,11 +1063,13 @@ export async function prepareOpenAIMessages({ extensionPrompts, systemPromptOverride, jailbreakPromptOverride, - personaDescription + personaDescription, + messages, + messageExamples, }); // Fill the chat completion with as much context as the budget allows - await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt }); + await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples }); } catch (error) { if (error instanceof TokenBudgetExceededError) { toastr.error('An error occurred while counting tokens: Token budget exceeded.') @@ -1117,7 +1140,7 @@ function checkQuotaError(data) { } } -async function sendWindowAIRequest(openai_msgs_tosend, signal, stream) { +async function sendWindowAIRequest(messages, signal, stream) { if (!('ai' in window)) { return showWindowExtensionError(); } @@ -1172,7 +1195,7 @@ async function sendWindowAIRequest(openai_msgs_tosend, signal, stream) { const generatePromise = window.ai.generateText( { - messages: openai_msgs_tosend, + messages: messages, }, { temperature: temperature, @@ -1349,11 +1372,11 @@ function openRouterGroupByVendor(array) { }, new Map()); } -async function sendAltScaleRequest(openai_msgs_tosend, logit_bias, signal, type) { +async function sendAltScaleRequest(messages, logit_bias, signal, type) { const generate_url = '/generate_altscale'; let firstSysMsgs = [] - for (let msg of openai_msgs_tosend) { + for (let msg of messages) { if (msg.role === 'system') { firstSysMsgs.push(substituteParams(msg.name ? msg.name + ": " + msg.content : msg.content)); } else { @@ -1361,20 +1384,20 @@ async function sendAltScaleRequest(openai_msgs_tosend, logit_bias, signal, type) } } - let subsequentMsgs = openai_msgs_tosend.slice(firstSysMsgs.length); + let subsequentMsgs = messages.slice(firstSysMsgs.length); const joinedSysMsgs = substituteParams(firstSysMsgs.join("\n")); const joinedSubsequentMsgs = subsequentMsgs.reduce((acc, obj) => { return acc + obj.role + ": " + obj.content + "\n"; }, ""); - openai_msgs_tosend = substituteParams(joinedSubsequentMsgs); + messages = substituteParams(joinedSubsequentMsgs); const messageId = getNextMessageId(type); - replaceItemizedPromptText(messageId, openai_msgs_tosend); + replaceItemizedPromptText(messageId, messages); const generate_data = { sysprompt: joinedSysMsgs, - prompt: openai_msgs_tosend, + prompt: messages, temp: Number(oai_settings.temp_openai), top_p: Number(oai_settings.top_p_openai), max_tokens: Number(oai_settings.openai_max_tokens), @@ -1392,18 +1415,18 @@ async function sendAltScaleRequest(openai_msgs_tosend, logit_bias, signal, type) return data.output; } -async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { +async function sendOpenAIRequest(type, messages, signal) { // Provide default abort signal if (!signal) { signal = new AbortController().signal; } // HACK: Filter out null and non-object messages - if (!Array.isArray(openai_msgs_tosend)) { - throw new Error('openai_msgs_tosend must be an array'); + if (!Array.isArray(messages)) { + throw new Error('messages must be an array'); } - openai_msgs_tosend = openai_msgs_tosend.filter(msg => msg && typeof msg === 'object'); + messages = messages.filter(msg => msg && typeof msg === 'object'); let logit_bias = {}; const messageId = getNextMessageId(type); @@ -1419,23 +1442,23 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { const stream = oai_settings.stream_openai && !isQuiet && !isScale && !isAI21 && !isPalm; if (isTextCompletion && isOpenRouter) { - openai_msgs_tosend = convertChatCompletionToInstruct(openai_msgs_tosend, type); - replaceItemizedPromptText(messageId, openai_msgs_tosend); + messages = convertChatCompletionToInstruct(messages, type); + replaceItemizedPromptText(messageId, messages); } if (isAI21 || isPalm) { - const joinedMsgs = openai_msgs_tosend.reduce((acc, obj) => { + const joinedMsgs = messages.reduce((acc, obj) => { const prefix = prefixMap[obj.role]; return acc + (prefix ? (selected_group ? "\n" : prefix + " ") : "") + obj.content + "\n"; }, ""); - openai_msgs_tosend = substituteParams(joinedMsgs) + (isImpersonate ? `${name1}:` : `${name2}:`); - replaceItemizedPromptText(messageId, openai_msgs_tosend); + messages = substituteParams(joinedMsgs) + (isImpersonate ? `${name1}:` : `${name2}:`); + replaceItemizedPromptText(messageId, messages); } // If we're using the window.ai extension, use that instead // Doesn't support logit bias yet if (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) { - return sendWindowAIRequest(openai_msgs_tosend, signal, stream); + return sendWindowAIRequest(messages, signal, stream); } const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.SCALE]; @@ -1448,12 +1471,12 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { } if (isScale && oai_settings.use_alt_scale) { - return sendAltScaleRequest(openai_msgs_tosend, logit_bias, signal, type); + return sendAltScaleRequest(messages, logit_bias, signal, type); } const model = getChatCompletionModel(); const generate_data = { - "messages": openai_msgs_tosend, + "messages": messages, "model": model, "temperature": Number(oai_settings.temp_openai), "frequency_penalty": Number(oai_settings.freq_pen_openai), From 5f77b2f81668ecb97d4d273a37254f7d1ab31302 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 20:07:37 +0200 Subject: [PATCH 091/185] Add Claude 2.1 --- public/index.html | 1 + public/scripts/openai.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 7046c9a62..0522f9bee 100644 --- a/public/index.html +++ b/public/index.html @@ -1869,6 +1869,7 @@ + diff --git a/public/scripts/openai.js b/public/scripts/openai.js index b5a5e8ad6..3beedfefa 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -108,6 +108,7 @@ const max_8k = 8191; const max_16k = 16383; const max_32k = 32767; const max_128k = 128 * 1000; +const max_200k = 200 * 1000; const scale_max = 8191; const claude_max = 9000; // We have a proper tokenizer, so theoretically could be larger (up to 9k) const palm2_max = 7500; // The real context window is 8192, spare some for padding due to using turbo tokenizer @@ -3127,7 +3128,10 @@ async function onModelChange() { if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { if (oai_settings.max_context_unlocked) { - $('#openai_max_context').attr('max', unlocked_max); + $('#openai_max_context').attr('max', max_200k); + } + else if (value == 'claude-2.1' || value == 'claude-2') { + $('#openai_max_context').attr('max', max_200k); } else if (value.endsWith('100k') || value.startsWith('claude-2') || value === 'claude-instant-1.2') { $('#openai_max_context').attr('max', claude_100k_max); From df4ed389bfe97708f995fbeb8950c80989a91c33 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 22:11:26 +0200 Subject: [PATCH 092/185] System prompt for Claude 2 --- server.js | 3 ++- src/chat-completion.js | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index 8e7393b7f..a07e6cb56 100644 --- a/server.js +++ b/server.js @@ -3043,7 +3043,8 @@ async function sendClaudeRequest(request, response) { controller.abort(); }); - let requestPrompt = convertClaudePrompt(request.body.messages, true, !request.body.exclude_assistant); + let doSystemPrompt = request.body.model === 'claude-2' || request.body.model === 'claude-2.1'; + let requestPrompt = convertClaudePrompt(request.body.messages, true, !request.body.exclude_assistant, doSystemPrompt); if (request.body.assistant_prefill && !request.body.exclude_assistant) { requestPrompt += request.body.assistant_prefill; diff --git a/src/chat-completion.js b/src/chat-completion.js index 0924a63ad..e16cdd017 100644 --- a/src/chat-completion.js +++ b/src/chat-completion.js @@ -3,10 +3,11 @@ * @param {object[]} messages Array of messages * @param {boolean} addHumanPrefix Add Human prefix * @param {boolean} addAssistantPostfix Add Assistant postfix + * @param {boolean} withSystemPrompt Build system prompt before "\n\nHuman: " * @returns {string} Prompt for Claude * @copyright Prompt Conversion script taken from RisuAI by kwaroran (GPLv3). */ -function convertClaudePrompt(messages, addHumanPrefix, addAssistantPostfix) { +function convertClaudePrompt(messages, addHumanPrefix, addAssistantPostfix, withSystemPrompt) { // Claude doesn't support message names, so we'll just add them to the message content. for (const message of messages) { if (message.name && message.role !== "system") { @@ -15,6 +16,18 @@ function convertClaudePrompt(messages, addHumanPrefix, addAssistantPostfix) { } } + let systemPrompt = ''; + if (withSystemPrompt) { + for (const message of messages) { + if (message.role === "system" && !message.name) { + systemPrompt += message.content + '\n\n'; + messages.splice(messages.indexOf(message), 1); + } else { + break; + } + } + } + let requestPrompt = messages.map((v) => { let prefix = ''; switch (v.role) { @@ -46,6 +59,10 @@ function convertClaudePrompt(messages, addHumanPrefix, addAssistantPostfix) { requestPrompt = requestPrompt + '\n\nAssistant: '; } + if (withSystemPrompt) { + requestPrompt = systemPrompt + requestPrompt; + } + return requestPrompt; } From 2dc8f8f2f725d6124daf057c58a6e6135bca7360 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 22:35:59 +0200 Subject: [PATCH 093/185] Add 5 group control commands --- public/scripts/slash-commands.js | 122 ++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 2a5066036..6d5a03945 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -26,7 +26,7 @@ import { setCharacterName, } from "../script.js"; import { getMessageTimeStamp } from "./RossAscends-mods.js"; -import { findGroupMemberId, is_group_generating, resetSelectedGroup, selected_group } from "./group-chats.js"; +import { findGroupMemberId, groups, is_group_generating, resetSelectedGroup, saveGroupChat, 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"; @@ -151,6 +151,11 @@ parser.addCommand('hide', hideMessageCallback, [], '(mes parser.addCommand('unhide', unhideMessageCallback, [], '(message index or range) – unhides a message from the prompt', true, true); parser.addCommand('disable', disableGroupMemberCallback, [], '(member index or name) – disables a group member from being drafted for replies', true, true); parser.addCommand('enable', enableGroupMemberCallback, [], '(member index or name) – enables a group member to be drafted for replies', true, true); +parser.addCommand('memberadd', addGroupMemberCallback, ['addmember'], '(character name) – adds a new group member to the group chat', true, true); +parser.addCommand('memberremove', removeGroupMemberCallback, ['removemember'], '(member index or name) – removes a group member from the group chat', true, true); +parser.addCommand('memberup', moveGroupMemberUpCallback, ['upmember'], '(member index or name) – moves a group member up in the group chat list', true, true); +parser.addCommand('memberdown', moveGroupMemberDownCallback, ['downmember'], '(member index or name) – moves a group member down in the group chat list', true, true); +parser.addCommand('peek', peekCallback, [], '(message index or range) – shows a group member character card without switching chats', true, true); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; @@ -320,6 +325,121 @@ async function enableGroupMemberCallback(_, arg) { $(`.group_member[chid="${chid}"] [data-action="enable"]`).trigger('click'); } +async function moveGroupMemberUpCallback(_, arg) { + if (!selected_group) { + toastr.warning("Cannot run /memberup command outside of a group chat."); + return; + } + + const chid = findGroupMemberId(arg); + + if (chid === undefined) { + console.warn(`WARN: No group member found for argument ${arg}`); + return; + } + + $(`.group_member[chid="${chid}"] [data-action="up"]`).trigger('click'); +} + +async function moveGroupMemberDownCallback(_, arg) { + if (!selected_group) { + toastr.warning("Cannot run /memberdown command outside of a group chat."); + return; + } + + const chid = findGroupMemberId(arg); + + if (chid === undefined) { + console.warn(`WARN: No group member found for argument ${arg}`); + return; + } + + $(`.group_member[chid="${chid}"] [data-action="down"]`).trigger('click'); +} + +async function peekCallback(_, arg) { + if (!selected_group) { + toastr.warning("Cannot run /peek command outside of a group chat."); + return; + } + + if (is_group_generating) { + toastr.warning("Cannot run /peek command while the group reply is generating."); + return; + } + + const chid = findGroupMemberId(arg); + + if (chid === undefined) { + console.warn(`WARN: No group member found for argument ${arg}`); + return; + } + + $(`.group_member[chid="${chid}"] [data-action="view"]`).trigger('click'); +} + +async function removeGroupMemberCallback(_, arg) { + if (!selected_group) { + toastr.warning("Cannot run /memberremove command outside of a group chat."); + return; + } + + if (is_group_generating) { + toastr.warning("Cannot run /memberremove command while the group reply is generating."); + return; + } + + const chid = findGroupMemberId(arg); + + if (chid === undefined) { + console.warn(`WARN: No group member found for argument ${arg}`); + return; + } + + $(`.group_member[chid="${chid}"] [data-action="remove"]`).trigger('click'); +} + +async function addGroupMemberCallback(_, arg) { + if (!selected_group) { + toastr.warning("Cannot run /memberadd command outside of a group chat."); + return; + } + + if (!arg) { + console.warn('WARN: No argument provided for /memberadd command'); + return; + } + + arg = arg.trim(); + const chid = findCharacterIndex(arg); + + if (chid === -1) { + console.warn(`WARN: No character found for argument ${arg}`); + return; + } + + const character = characters[chid]; + const group = groups.find(x => x.id === selected_group); + + if (!group || !Array.isArray(group.members)) { + console.warn(`WARN: No group found for ID ${selected_group}`); + return; + } + + const avatar = character.avatar; + + if (group.members.includes(avatar)) { + toastr.warning(`${character.name} is already a member of this group.`); + return; + } + + group.members.push(avatar); + await saveGroupChat(selected_group, true); + + // Trigger to reload group UI + $('#rm_button_selected_ch').trigger('click'); +} + async function triggerGroupMessageCallback(_, arg) { if (!selected_group) { toastr.warning("Cannot run /trigger command outside of a group chat."); From 284bd7658903b4506ea90175e8a0ff6e9dbca136 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:28:11 +0200 Subject: [PATCH 094/185] Add /delswipe command --- public/scripts/slash-commands.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 6d5a03945..0c19dab16 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -156,11 +156,40 @@ parser.addCommand('memberremove', removeGroupMemberCallback, ['removemember'], ' parser.addCommand('memberup', moveGroupMemberUpCallback, ['upmember'], '(member index or name) – moves a group member up in the group chat list', true, true); parser.addCommand('memberdown', moveGroupMemberDownCallback, ['downmember'], '(member index or name) – moves a group member down in the group chat list', true, true); parser.addCommand('peek', peekCallback, [], '(message index or range) – shows a group member character card without switching chats', true, true); +parser.addCommand('delswipe', deleteSwipeCallback, [], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; +async function deleteSwipeCallback(_, arg) { + const lastMessage = chat[chat.length - 1]; + + if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) { + toastr.warning("No messages to delete swipes from."); + return; + } + + if (lastMessage.swipes.length <= 1) { + toastr.warning("Can't delete the last swipe."); + return; + } + + const swipeId = arg && !isNaN(Number(arg)) ? (Number(arg) - 1) : lastMessage.swipe_id; + lastMessage.swipes.splice(swipeId, 1); + + if (Array.isArray(lastMessage.swipe_info) && lastMessage.swipe_info.length) { + lastMessage.swipe_info.splice(swipeId, 1); + } + + const newSwipeId = Math.min(swipeId, lastMessage.swipes.length - 1); + lastMessage.swipe_id = newSwipeId; + lastMessage.mes = lastMessage.swipes[newSwipeId]; + + await saveChatConditional(); + await reloadCurrentChat(); +} + async function askCharacter(_, text) { // Prevent generate recursion $('#send_textarea').val(''); From 1b4d955aec57f9c8c52c927ceb91939fdfb3c3e3 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:33:20 +0200 Subject: [PATCH 095/185] Add swipe id validation for /delswipe --- public/scripts/slash-commands.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 0c19dab16..7bb7dbddb 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -176,6 +176,12 @@ async function deleteSwipeCallback(_, arg) { } const swipeId = arg && !isNaN(Number(arg)) ? (Number(arg) - 1) : lastMessage.swipe_id; + + if (swipeId < 0 || swipeId >= lastMessage.swipes.length) { + toastr.warning(`Invalid swipe ID: ${swipeId + 1}`); + return; + } + lastMessage.swipes.splice(swipeId, 1); if (Array.isArray(lastMessage.swipe_info) && lastMessage.swipe_info.length) { From 4b78ddbc8a5890c5d9862b44ddcedb6abcb31bbc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:39:17 +0200 Subject: [PATCH 096/185] First steps in slash command piping --- public/scripts/slash-commands.js | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 7bb7dbddb..5586f1a0c 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -84,7 +84,7 @@ class SlashCommandParser { const key = match[1]; const value = match[2]; // Remove the quotes around the value, if any - argObj[key] = value.replace(/(^")|("$)/g, ''); + argObj[key] = substituteParams(value.replace(/(^")|("$)/g, '')); } // Match unnamed argument @@ -157,11 +157,36 @@ parser.addCommand('memberup', moveGroupMemberUpCallback, ['upmember'], '(member index or name) – moves a group member down in the group chat list', true, true); parser.addCommand('peek', peekCallback, [], '(message index or range) – shows a group member character card without switching chats', true, true); parser.addCommand('delswipe', deleteSwipeCallback, [], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); +parser.addCommand('echo', echoCallback, [], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); +parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; +async function generateCallback(_, arg) { + if (!arg) { + console.warn('WARN: No argument provided for /gen command'); + return; + } + + // Prevent generate recursion + $('#send_textarea').val(''); + + const result = await generateQuietPrompt(arg, false, false, ''); + return result; +} + +async function echoCallback(_, arg) { + if (!arg) { + console.warn('WARN: No argument provided for /echo command'); + return; + } + + toastr.info(arg); + return arg; +} + async function deleteSwipeCallback(_, arg) { const lastMessage = chat[chat.length - 1]; @@ -895,6 +920,7 @@ async function executeSlashCommands(text) { const linesToRemove = []; let interrupt = false; + let pipeResult = ''; for (let index = 0; index < lines.length; index++) { const trimmedLine = lines[index].trim(); @@ -914,7 +940,8 @@ async function executeSlashCommands(text) { } console.debug('Slash command executing:', result); - await result.command.callback(result.args, result.value); + const unnamedArg = pipeResult || result.value; + pipeResult = await result.command.callback(result.args, unnamedArg); if (result.command.interruptsGeneration) { interrupt = true; From e5f7b0b5c76e8157ddc7e4e4728e42bb65f42382 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:43:33 +0200 Subject: [PATCH 097/185] Use explicit unnamed argument first if exists --- public/scripts/slash-commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 5586f1a0c..e35be81f8 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -940,7 +940,7 @@ async function executeSlashCommands(text) { } console.debug('Slash command executing:', result); - const unnamedArg = pipeResult || result.value; + const unnamedArg = result.value || pipeResult; pipeResult = await result.command.callback(result.args, unnamedArg); if (result.command.interruptsGeneration) { From fe21a7c25b38c7040141f1fcbca8428e93822757 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Wed, 22 Nov 2023 07:48:35 +0900 Subject: [PATCH 098/185] Add toggle for hiding muted group member sprites --- public/index.html | 6 +++++- .../scripts/extensions/expressions/index.js | 3 ++- public/scripts/group-chats.js | 21 ++++++++++++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 0522f9bee..a09e83e72 100644 --- a/public/index.html +++ b/public/index.html @@ -3510,6 +3510,10 @@ Auto Mode +
@@ -4843,4 +4847,4 @@ - + \ No newline at end of file diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js index 40e0c36c4..eea4a8655 100644 --- a/public/scripts/extensions/expressions/index.js +++ b/public/scripts/extensions/expressions/index.js @@ -4,6 +4,7 @@ import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper import { loadMovingUIState, power_user } from "../../power-user.js"; import { registerSlashCommand } from "../../slash-commands.js"; import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from "../../utils.js"; +import { hideMutedSprites } from "../../group-chats.js"; export { MODULE_NAME }; const MODULE_NAME = 'expressions'; @@ -118,7 +119,7 @@ async function visualNovelSetCharacterSprites(container, name, expression) { const isDisabled = group.disabled_members.includes(avatar); // skip disabled characters - if (isDisabled) { + if (isDisabled && hideMutedSprites) { continue; } diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 12f20839e..89244f0f3 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -76,6 +76,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js'; export { selected_group, is_group_automode_enabled, + hideMutedSprites, is_group_generating, group_generation_id, groups, @@ -92,6 +93,7 @@ export { let is_group_generating = false; // Group generation flag let is_group_automode_enabled = false; +let hideMutedSprites = true; let groups = []; let selected_group = null; let group_generation_id = null; @@ -1172,7 +1174,7 @@ function printGroupCandidates() { function printGroupMembers() { const storageKey = 'GroupMembers_PerPage'; - $(".rm_group_members_pagination").each(function() { + $(".rm_group_members_pagination").each(function () { $(this).pagination({ dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }), pageRange: 1, @@ -1258,6 +1260,15 @@ async function onGroupSelfResponsesClick() { } } +async function onHideMutedSpritesClick(value) { + if (openGroupId) { + let _thisGroup = groups.find((x) => x.id == openGroupId); + _thisGroup.hideMutedSprites = value; + console.log(`_thisGroup.hideMutedSprites = ${_thisGroup.hideMutedSprites}`) + await editGroup(openGroupId, false, false); + } +} + function select_group_chats(groupId, skipAnimation) { openGroupId = groupId; newGroupMembers = []; @@ -1287,6 +1298,7 @@ function select_group_chats(groupId, skipAnimation) { const groupHasMembers = !!$("#rm_group_members").children().length; $("#rm_group_submit").prop("disabled", !groupHasMembers); $("#rm_group_allow_self_responses").prop("checked", group && group.allow_self_responses); + $("#rm_group_hidemutedsprites").prop("checked", group && group.hideMutedSprites); // bottom buttons if (openGroupId) { @@ -1517,6 +1529,7 @@ async function createGroup() { members: members, avatar_url: isValidImageUrl(avatar_url) ? avatar_url : default_avatar, allow_self_responses: allowSelfResponses, + hideMutedSprites: hideMutedSprites, activation_strategy: activationStrategy, generation_mode: generationMode, disabled_members: [], @@ -1784,6 +1797,12 @@ jQuery(() => { is_group_automode_enabled = value; eventSource.once(event_types.GENERATION_STOPPED, stopAutoModeGeneration); }); + $("#rm_group_hidemutedsprites").on("input", function () { + const value = $(this).prop("checked"); + hideMutedSprites = value; + onHideMutedSpritesClick(value); + + }); $("#send_textarea").on("keyup", onSendTextareaInput); $("#groupCurrentMemberPopoutButton").on('click', doCurMemberListPopout); $("#rm_group_chat_name").on("input", onGroupNameInput) From 59e558fba5a176e7d5bee5608424ededffa98f5b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:50:41 +0200 Subject: [PATCH 099/185] Don't execute commands on dry runs --- public/script.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/script.js b/public/script.js index 504e034eb..fb754a56e 100644 --- a/public/script.js +++ b/public/script.js @@ -2220,8 +2220,8 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q }); } -async function processCommands(message, type) { - if (type == "regenerate" || type == "swipe" || type == 'quiet') { +async function processCommands(message, type, dryRun) { + if (dryRun || type == "regenerate" || type == "swipe" || type == 'quiet') { return null; } @@ -2776,7 +2776,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `; - const interruptedByCommand = await processCommands($("#send_textarea").val(), type); + const interruptedByCommand = await processCommands($("#send_textarea").val(), type, dryRun); if (interruptedByCommand) { $("#send_textarea").val('').trigger('input'); From 4f7523b8964c4aa42f59dedb44397fda82641e83 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 00:58:06 +0200 Subject: [PATCH 100/185] Parallelize extensions auto-update + add a toast --- public/scripts/extensions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 2c68b949a..36474c486 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -846,12 +846,15 @@ async function checkForExtensionUpdates(force) { } async function autoUpdateExtensions() { + toastr.info('Auto-updating extensions. This may take several minutes.', 'Please wait...', { timeOut: 10000, extendedTimeOut: 20000 }); + const promises = []; for (const [id, manifest] of Object.entries(manifests)) { if (manifest.auto_update && id.startsWith('third-party')) { console.debug(`Auto-updating 3rd-party extension: ${manifest.display_name} (${id})`); - await updateExtension(id.replace('third-party', ''), true); + promises.push(updateExtension(id.replace('third-party', ''), true)); } } + await Promise.allSettled(promises); } /** From 55af72cb173e3a49a11acf750bba5a7929f52085 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 01:26:17 +0200 Subject: [PATCH 101/185] /addswipe command --- public/script.js | 3 ++ public/scripts/slash-commands.js | 56 ++++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/public/script.js b/public/script.js index fb754a56e..df4f25c4c 100644 --- a/public/script.js +++ b/public/script.js @@ -1394,6 +1394,9 @@ export async function reloadCurrentChat() { await printMessages(); await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); } + + hideSwipeButtons(); + showSwipeButtons(); } function messageFormatting(mes, ch_name, isSystem, isUser) { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index e35be81f8..54d80c9cc 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -133,7 +133,7 @@ parser.addCommand('name', setNameCallback, ['persona'], '(filename) – sets a background according to filename, partial names allowed', false, true); -parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe": /sendas name="Chloe" Hello, guys!`, true, true); +parser.addCommand('sendas', sendMessageAs, [], ` – sends message as a specific character. Uses character avatar if it exists in the characters list. Example that will send "Hello, guys!" from "Chloe": /sendas name="Chloe" Hello, guys!`, true, true); parser.addCommand('sys', sendNarratorMessage, ['nar'], '(text) – sends message as a system narrator', false, true); parser.addCommand('sysname', setNarratorName, [], '(name) – sets a name for future system narrator messages in this chat (display only). Default: System. Leave empty to reset.', true, true); parser.addCommand('comment', sendCommentMessage, [], '(text) – adds a note/comment message not part of the chat', false, true); @@ -156,9 +156,10 @@ parser.addCommand('memberremove', removeGroupMemberCallback, ['removemember'], ' parser.addCommand('memberup', moveGroupMemberUpCallback, ['upmember'], '(member index or name) – moves a group member up in the group chat list', true, true); parser.addCommand('memberdown', moveGroupMemberDownCallback, ['downmember'], '(member index or name) – moves a group member down in the group chat list', true, true); parser.addCommand('peek', peekCallback, [], '(message index or range) – shows a group member character card without switching chats', true, true); -parser.addCommand('delswipe', deleteSwipeCallback, [], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); +parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); +parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; @@ -187,6 +188,57 @@ async function echoCallback(_, arg) { return arg; } +async function addSwipeCallback(_, arg) { + const lastMessage = chat[chat.length - 1]; + + if (!lastMessage) { + toastr.warning("No messages to add swipes to."); + return; + } + + if (!arg) { + console.warn('WARN: No argument provided for /addswipe command'); + return; + } + + if (lastMessage.is_user) { + toastr.warning("Can't add swipes to user messages."); + return; + } + + if (lastMessage.is_system) { + toastr.warning("Can't add swipes to system messages."); + return; + } + + if (lastMessage.extra?.image) { + toastr.warning("Can't add swipes to message containing an image."); + return; + } + + if (!Array.isArray(lastMessage.swipes)) { + lastMessage.swipes = [lastMessage.mes]; + lastMessage.swipe_info = [{}]; + lastMessage.swipe_id = 0; + } + + lastMessage.swipes.push(arg); + lastMessage.swipe_info.push({ + send_date: getMessageTimeStamp(), + gen_started: null, + gen_finished: null, + extra: { + bias: extractMessageBias(arg), + gen_id: Date.now(), + api: 'manual', + model: 'slash command', + } + }); + + await saveChatConditional(); + await reloadCurrentChat(); +} + async function deleteSwipeCallback(_, arg) { const lastMessage = chat[chat.length - 1]; From 668a14989851523a565e711498496e40a2c3131e Mon Sep 17 00:00:00 2001 From: XXpE3 Date: Wed, 22 Nov 2023 13:41:46 +0800 Subject: [PATCH 102/185] Optimized the Chinese translation in i18n. --- public/i18n.json | 57 ++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/public/i18n.json b/public/i18n.json index ee0b08706..51b06d857 100644 --- a/public/i18n.json +++ b/public/i18n.json @@ -38,36 +38,36 @@ "Temperature": "温度", "Frequency Penalty": "频率惩罚", "Presence Penalty": "存在惩罚", - "Top-p": "Top-p", + "Top-p": "Top P", "Display bot response text chunks as they are generated": "显示机器人生成的响应文本块", - "Top A": "Top-a", + "Top A": "Top A", "Typical Sampling": "典型采样", "Tail Free Sampling": "无尾采样", - "Rep. Pen. Slope": "重复惩罚斜率", + "Rep. Pen. Slope": "重复惩罚梯度", "Single-line mode": "单行模式", - "Top K": "Top-k", - "Top P": "Top-p", - "Typical P": "典型P", - "Do Sample": "采样", - "Add BOS Token": "添加BOS Token", - "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "在提示的开头添加bos_token。禁用此功能可以让回复更加创造性.", - "Ban EOS Token": "禁止EOS Token", - "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止eos_token。这会迫使模型不会过早结束生成", - "Skip Special Tokens": "跳过特殊Tokens", - "Beam search": "光束搜索", - "Number of Beams": "光束数目", + "Top K": "Top-K", + "Top P": "Top-P", + "Typical P": "典型 P", + "Do Sample": "样本测试", + "Add BOS Token": "添加 BOS Token", + "Add the bos_token to the beginning of prompts. Disabling this can make the replies more creative.": "在提示的开头添加 bos_token,禁用此功能可以让回复更加创造性。", + "Ban EOS Token": "禁止 EOS Token", + "Ban the eos_token. This forces the model to never end the generation prematurely": "禁止 EOS Token,这会迫使模型不会过早结束生成。", + "Skip Special Tokens": "跳过特殊 Tokens", + "Beam search": "Beam 搜索", + "Number of Beams": "Beams 的数量", "Length Penalty": "长度惩罚", "Early Stopping": "提前终止", "Contrastive search": "对比搜索", "Penalty Alpha": "惩罚系数", - "Seed": "种子", + "Seed": "随机种子", "Inserts jailbreak as a last system message.": "插入越狱作为最后一个系统消息", "This tells the AI to ignore its usual content restrictions.": "这告诉人工智能忽略其通常的内容限制", "NSFW Encouraged": "NSFW鼓励", - "Tell the AI that NSFW is allowed.": "告诉人工智能,NSFW是允许的。", - "NSFW Prioritized": "NSFW优先", + "Tell the AI that NSFW is allowed.": "告诉人工智能,NSFW 是允许的。", + "NSFW Prioritized": "NSFW 优先", "NSFW prompt text goes first in the prompt to emphasize its effect.": "NSFW 提示文本排在提示的顶部,以强调其效果", - "Streaming": "流式回复", + "Streaming": "流式生成", "Display the response bit by bit as it is generated.": "在生成响应时逐字显示。", "When this is off, responses will be displayed all at once when they are complete.": "关闭此选项后,响应将在全部完成后立即显示。", "Generate only one line per request (KoboldAI only, ignored by KoboldCpp).": "每个请求仅生成一行(仅限 KoboldAI,被 KoboldCpp 忽略)。", @@ -109,7 +109,7 @@ "For privacy reasons": "出于隐私原因,您的 API 密钥将在您重新加载页面后隐藏", "Model": "模型", "Hold Control / Command key to select multiple models.": "按住控制/命令键选择多个模型。", - "Horde models not loaded": "未加载Horde模型。", + "Horde models not loaded": "未加载 Horde 模型。", "Not connected": "未连接", "Novel API key": "NovelAI API 密钥", "Follow": "跟随", @@ -126,11 +126,11 @@ "OpenAI Model": "OpenAI模型", "View API Usage Metrics": "查看 API 使用情况", "Bot": "Bot", - "Connect to the API": "连接到API", + "Connect to the API": "连接到 API", "Auto-connect to Last Server": "自动连接到最后设置的 API 服务器", "View hidden API keys": "查看隐藏的 API 密钥", - "Advanced Formatting": "高级格式", - "AutoFormat Overrides": "自动套用格式替代", + "Advanced Formatting": "高级格式化", + "AutoFormat Overrides": "覆盖自动格式化", "Disable description formatting": "禁用描述格式", "Disable personality formatting": "禁用人设格式", "Disable scenario formatting": "禁用场景格式", @@ -166,7 +166,6 @@ "Style then Character": "样式然后字符", "Character Anchor": "字符锚点", "Style Anchor": "样式锚点", - "World Info": "", "Scan Depth": "扫描深度", "depth": "深度", "Token Budget": "Token 预算", @@ -400,8 +399,8 @@ "Samplers Order": "采样器顺序", "Samplers will be applied in a top-down order. Use with caution.": "采样器将按从上到下的顺序应用。谨慎使用。", "Repetition Penalty": "重复惩罚", - "Epsilon Cutoff": "Epsilon切断", - "Eta Cutoff": "Eta切断", + "Epsilon Cutoff": "Epsilon 切断", + "Eta Cutoff": "Eta 切断", "Rep. Pen. Range.": "重复惩罚范围", "Rep. Pen. Freq.": "重复频率惩罚", "Rep. Pen. Presence": "重复存在惩罚", @@ -483,7 +482,7 @@ "removes blur and uses alternative background color for divs": "去除模糊并为div使用替代的背景颜色", "If checked and the character card contains a prompt override (System Prompt), use that instead.": "如果选中并且角色卡包含提示覆盖(系统提示),请改用该选项。", "If checked and the character card contains a jailbreak override (Post History Instruction), use that instead.": "如果选中并且角色卡包含越狱覆盖(发布历史指令),请改用该选项。", - "AI Response Formatting": "AI回复格式", + "AI Response Formatting": "AI 回复格式", "Change Background Image": "更改背景图片", "Extensions": "扩展", "Click to set a new User Name": "点击设置新用户名", @@ -493,7 +492,7 @@ "Character Management": "角色管理", "Locked = Character Management panel will stay open": "锁定=角色管理面板将保持打开状态", "Select/Create Characters": "选择/创建角色", - "Token counts may be inaccurate and provided just for reference.": "Token计数可能不准确,仅供参考。", + "Token counts may be inaccurate and provided just for reference.": "Token 计数可能不准确,仅供参考。", "Click to select a new avatar for this character": "点击选择此角色的新头像", "Add to Favorites": "添加到收藏夹", "Advanced Definition": "高级定义", @@ -525,7 +524,7 @@ "Associate one or more auxillary Lorebooks with this character.": "将一个或多个辅助的 Lorebook 与这个角色关联。", "NOTE: These choices are optional and won't be preserved on character export!": "注意:这些选择是可选的,不会在导出角色时保留!", "Rename chat file": "重命名聊天文件", - "Export JSONL chat file": "导出JSONL聊天文件", + "Export JSONL chat file": "导出 JSONL 聊天文件", "Download chat as plain text document": "将聊天内容下载为纯文本文档", "Delete chat file": "删除聊天文件", "Delete tag": "删除标签", @@ -553,7 +552,7 @@ "Add": "添加", "Abort request": "取消请求", "Send a message": "发送消息", - "Ask AI to write your message for you": "让AI代替你写消息", + "Ask AI to write your message for you": "让 AI 代替你写消息", "Continue the last message": "继续上一条消息", "Bind user name to that avatar": "将用户名绑定到该头像", "Select this as default persona for the new chats.": "将此选择为新聊天的默认角色。", From 61908935f56298c9514d6b44b2c2961b909dd9f8 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:16:48 +0200 Subject: [PATCH 103/185] Stop string for user-continue. Trim spaces after name2 --- public/script.js | 31 ++++++++++++++++++++---------- public/scripts/kai-settings.js | 3 ++- public/scripts/nai-settings.js | 8 ++++---- public/scripts/openai.js | 3 ++- public/scripts/textgen-settings.js | 10 +++++----- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/public/script.js b/public/script.js index df4f25c4c..17cece591 100644 --- a/public/script.js +++ b/public/script.js @@ -2156,13 +2156,23 @@ function diceRollReplace(input, invalidRollPlaceholder = '') { }); } -function getStoppingStrings(isImpersonate) { +/** + * Gets stopping sequences for the prompt. + * @param {boolean} isImpersonate A request is made to impersonate a user + * @param {boolean} isContinue A request is made to continue the message + * @returns {string[]} Array of stopping strings + */ +function getStoppingStrings(isImpersonate, isContinue) { const charString = `\n${name2}:`; const userString = `\n${name1}:`; const result = isImpersonate ? [charString] : [userString]; result.push(userString); + if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) { + result.push(charString); + } + // Add other group members as the stopping strings if (selected_group) { const group = groups.find(x => x.id === selected_group); @@ -2717,10 +2727,10 @@ export async function generateRaw(prompt, api) { break; case 'novel': const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; - generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, null); + generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null); break; case 'textgenerationwebui': - generateData = getTextGenGenerationData(prompt, amount_gen, false, null); + generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null); break; case 'openai': generateData = [{ role: 'user', content: prompt.trim() }]; @@ -3521,11 +3531,11 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } } else if (main_api == 'textgenerationwebui') { - generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, cfgValues); + generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues); } else if (main_api == 'novel') { const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; - generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, cfgValues); + generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, isContinue, cfgValues); } else if (main_api == 'openai') { let [prompt, counts] = await prepareOpenAIMessages({ @@ -4328,7 +4338,7 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete getMessage = substituteParams(power_user.user_prompt_bias) + getMessage; } - const stoppingStrings = getStoppingStrings(isImpersonate); + const stoppingStrings = getStoppingStrings(isImpersonate, isContinue); for (const stoppingString of stoppingStrings) { if (stoppingString.length) { @@ -4370,13 +4380,13 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete } if (nameToTrim && getMessage.indexOf(`${nameToTrim}:`) == 0) { - getMessage = getMessage.substr(0, getMessage.indexOf(`${nameToTrim}:`)); + getMessage = getMessage.substring(0, getMessage.indexOf(`${nameToTrim}:`)); } if (nameToTrim && getMessage.indexOf(`\n${nameToTrim}:`) >= 0) { - getMessage = getMessage.substr(0, getMessage.indexOf(`\n${nameToTrim}:`)); + getMessage = getMessage.substring(0, getMessage.indexOf(`\n${nameToTrim}:`)); } if (getMessage.indexOf('<|endoftext|>') != -1) { - getMessage = getMessage.substr(0, getMessage.indexOf('<|endoftext|>')); + getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>')); } const isInstruct = power_user.instruct.enabled && main_api !== 'openai'; if (isInstruct && power_user.instruct.stop_sequence) { @@ -4421,7 +4431,8 @@ function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete } if (!power_user.allow_name2_display) { - getMessage = getMessage.replace(new RegExp(`(^|\n)${name2}:`, 'g'), "$1"); + const name2Escaped = escapeRegex(name2); + getMessage = getMessage.replace(new RegExp(`(^|\n)${name2Escaped}:\\s*`, 'g'), "$1"); } if (isImpersonate) { diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js index 3242dfcc2..ac1ccd8bb 100644 --- a/public/scripts/kai-settings.js +++ b/public/scripts/kai-settings.js @@ -105,6 +105,7 @@ export function loadKoboldSettings(preset) { */ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxContextLength, isHorde, type) { const isImpersonate = type === 'impersonate'; + const isContinue = type === 'continue'; const sampler_order = kai_settings.sampler_order || settings.sampler_order; let generate_data = { @@ -132,7 +133,7 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon s7: sampler_order[6], use_world_info: false, singleline: false, - stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate) : undefined, + stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate, isContinue) : undefined, streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet', can_abort: kai_flags.can_use_streaming, mirostat: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat : undefined, diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js index 74e513f6f..5e38522d6 100644 --- a/public/scripts/nai-settings.js +++ b/public/scripts/nai-settings.js @@ -409,7 +409,7 @@ function getBadWordPermutations(text) { return result.filter(onlyUnique); } -export function getNovelGenerationData(finalPrompt, this_settings, this_amount_gen, isImpersonate, cfgValues) { +export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, cfgValues) { if (cfgValues && cfgValues.guidanceScale && cfgValues.guidanceScale?.value !== 1) { cfgValues.negativePrompt = (getCfgPrompt(cfgValues.guidanceScale, true))?.value; } @@ -419,7 +419,7 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g const tokenizerType = kayra ? tokenizers.NERD2 : (clio ? tokenizers.NERD : tokenizers.NONE); const stopSequences = (tokenizerType !== tokenizers.NONE) - ? getStoppingStrings(isImpersonate) + ? getStoppingStrings(isImpersonate, isContinue) .map(t => getTextTokens(tokenizerType, t)) : undefined; @@ -440,7 +440,7 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g "model": nai_settings.model_novel, "use_string": true, "temperature": Number(nai_settings.temperature), - "max_length": this_amount_gen < maximum_output_length ? this_amount_gen : maximum_output_length, + "max_length": maxLength < maximum_output_length ? maxLength : maximum_output_length, "min_length": Number(nai_settings.min_length), "tail_free_sampling": Number(nai_settings.tail_free_sampling), "repetition_penalty": Number(nai_settings.repetition_penalty), @@ -464,7 +464,7 @@ export function getNovelGenerationData(finalPrompt, this_settings, this_amount_g "use_cache": false, "return_full_text": false, "prefix": prefix, - "order": nai_settings.order || this_settings.order || default_order, + "order": nai_settings.order || settings.order || default_order, }; } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 3beedfefa..594591c5f 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -1440,6 +1440,7 @@ async function sendOpenAIRequest(type, messages, signal) { const isTextCompletion = (isOAI && textCompletionModels.includes(oai_settings.openai_model)) || (isOpenRouter && oai_settings.openrouter_force_instruct && power_user.instruct.enabled); const isQuiet = type === 'quiet'; const isImpersonate = type === 'impersonate'; + const isContinue = type === 'continue'; const stream = oai_settings.stream_openai && !isQuiet && !isScale && !isAI21 && !isPalm; if (isTextCompletion && isOpenRouter) { @@ -1523,7 +1524,7 @@ async function sendOpenAIRequest(type, messages, signal) { generate_data['use_fallback'] = oai_settings.openrouter_use_fallback; if (isTextCompletion) { - generate_data['stop'] = getStoppingStrings(isImpersonate); + generate_data['stop'] = getStoppingStrings(isImpersonate, isContinue); } } diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 24a937a22..a23bec16f 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -575,12 +575,12 @@ function getModel() { return undefined; } -export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImpersonate, cfgValues) { +export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues) { let APIflags = { 'prompt': finalPrompt, 'model': getModel(), - 'max_new_tokens': this_amount_gen, - 'max_tokens': this_amount_gen, + 'max_new_tokens': maxTokens, + 'max_tokens': maxTokens, 'temperature': textgenerationwebui_settings.temp, 'top_p': textgenerationwebui_settings.top_p, 'typical_p': textgenerationwebui_settings.typical_p, @@ -595,8 +595,8 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso 'length_penalty': textgenerationwebui_settings.length_penalty, 'early_stopping': textgenerationwebui_settings.early_stopping, 'add_bos_token': textgenerationwebui_settings.add_bos_token, - 'stopping_strings': getStoppingStrings(isImpersonate), - 'stop': getStoppingStrings(isImpersonate), + 'stopping_strings': getStoppingStrings(isImpersonate, isContinue), + 'stop': getStoppingStrings(isImpersonate, isContinue), 'truncation_length': max_context, 'ban_eos_token': textgenerationwebui_settings.ban_eos_token, 'skip_special_tokens': textgenerationwebui_settings.skip_special_tokens, From 73eeab9ace6804920b5a6d8aa595028414f06c5a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:21:43 +0200 Subject: [PATCH 104/185] Don't display incomplete sentences in quiet-to-loud prompts if trim is enabled --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 17cece591..101d865d1 100644 --- a/public/script.js +++ b/public/script.js @@ -3703,7 +3703,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } //Formating - const displayIncomplete = type == 'quiet'; + const displayIncomplete = type === 'quiet' && !quietToLoud; getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete); if (getMessage.length > 0) { From 57f303223b2805786c79d53610b48c9a0e21832c Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 16:34:25 +0200 Subject: [PATCH 105/185] Don't add extra space on non-instruct continue --- public/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index 101d865d1..4d4b711a9 100644 --- a/public/script.js +++ b/public/script.js @@ -3340,7 +3340,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, } // Add a space if prompt cache doesn't start with one - if (!/^\s/.test(promptCache) && !isInstruct) { + if (!/^\s/.test(promptCache) && !isInstruct && !isContinue) { promptCache = ' ' + promptCache; } From 56b63c0e02e36cef718c5a39a45fa19db3a294b7 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:36:34 +0200 Subject: [PATCH 106/185] #1386 Fix PaLM API --- src/constants.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/constants.js b/src/constants.js index e078657db..79442e213 100644 --- a/src/constants.js +++ b/src/constants.js @@ -104,10 +104,6 @@ const UNSAFE_EXTENSIONS = [ ]; const PALM_SAFETY = [ - { - category: "HARM_CATEGORY_UNSPECIFIED", - threshold: "BLOCK_NONE" - }, { category: "HARM_CATEGORY_DEROGATORY", threshold: "BLOCK_NONE" From f802fe1797cd66a18830df84df315e76e9c2878a Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:47:58 +0200 Subject: [PATCH 107/185] Fix xtts separator --- public/scripts/extensions/tts/index.js | 4 ++ public/scripts/extensions/tts/xtts.js | 66 +++++++++++++------------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index de3041013..3b85868b2 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -502,6 +502,10 @@ async function processTtsQueue() { const partJoiner = (ttsProvider?.separator || ' ... '); text = matches ? matches.join(partJoiner) : text; } + + // Collapse newlines and spaces into single space + text = text.replace(/\s+/g, ' '); + console.log(`TTS: ${text}`) const char = currentTtsJob.name diff --git a/public/scripts/extensions/tts/xtts.js b/public/scripts/extensions/tts/xtts.js index 2d3764ca9..8d4389f08 100644 --- a/public/scripts/extensions/tts/xtts.js +++ b/public/scripts/extensions/tts/xtts.js @@ -11,25 +11,25 @@ class XTTSTtsProvider { settings ready = false voices = [] - separator = ' .. ' + separator = '. ' languageLabels = { - "Arabic": "ar", - "Brazilian Portuguese": "pt", - "Chinese": "zh-cn", - "Czech": "cs", - "Dutch": "nl", - "English": "en", - "French": "fr", - "German": "de", - "Italian": "it", - "Polish": "pl", - "Russian": "ru", - "Spanish": "es", - "Turkish": "tr", - "Japanese": "ja", - "Korean": "ko", - "Hungarian": "hu" + "Arabic": "ar", + "Brazilian Portuguese": "pt", + "Chinese": "zh-cn", + "Czech": "cs", + "Dutch": "nl", + "English": "en", + "French": "fr", + "German": "de", + "Italian": "it", + "Polish": "pl", + "Russian": "ru", + "Spanish": "es", + "Turkish": "tr", + "Japanese": "ja", + "Korean": "ko", + "Hungarian": "hu" } defaultSettings = { @@ -38,15 +38,15 @@ class XTTSTtsProvider { voiceMap: {} } - get settingsHtml() { + get settingsHtml() { let html = ` - + `; html += ` - + Use XTTSv2 TTS Server. `; - + return html; } onSettingsChange() { @@ -86,8 +86,8 @@ class XTTSTtsProvider { // Only accept keys defined in defaultSettings this.settings = this.defaultSettings - for (const key in settings){ - if (key in this.settings){ + for (const key in settings) { + if (key in this.settings) { this.settings[key] = settings[key] } else { throw `Invalid setting passed to TTS Provider: ${key}` @@ -106,9 +106,9 @@ class XTTSTtsProvider { }, 2000); $('#xtts_tts_endpoint').val(this.settings.provider_endpoint) - $('#xtts_tts_endpoint').on("input", () => {this.onSettingsChange()}) + $('#xtts_tts_endpoint').on("input", () => { this.onSettingsChange() }) $('#xtts_api_language').val(this.settings.language) - $('#xtts_api_language').on("change", () => {this.onSettingsChange()}) + $('#xtts_api_language').on("change", () => { this.onSettingsChange() }) await this.checkReady() @@ -116,8 +116,8 @@ class XTTSTtsProvider { } // Perform a simple readiness check by trying to fetch voiceIds - async checkReady(){ - + async checkReady() { + const response = await this.fetchTtsVoiceObjects() } @@ -142,7 +142,7 @@ class XTTSTtsProvider { return match } - async generateTts(text, voiceId){ + async generateTts(text, voiceId) { const response = await this.fetchTtsGeneration(text, voiceId) return response } @@ -167,7 +167,7 @@ class XTTSTtsProvider { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache' // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23 + 'Cache-Control': 'no-cache' // Added this line to disable caching of file so new files are always played - Rolyat 7/7/23 }, body: JSON.stringify({ "text": inputText, From 35349dd8d7ab985178941cb1a8574a4ae7524280 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:59:46 +0200 Subject: [PATCH 108/185] Hide page overflow --- public/style.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/style.css b/public/style.css index 885b635d0..56446d75a 100644 --- a/public/style.css +++ b/public/style.css @@ -65,8 +65,8 @@ ( ( ( - (var(--SmartThemeCheckboxBgColorR) * 299) + - (var(--SmartThemeCheckboxBgColorG) * 587) + + (var(--SmartThemeCheckboxBgColorR) * 299) + + (var(--SmartThemeCheckboxBgColorG) * 587) + (var(--SmartThemeCheckboxBgColorB) * 114) ) / 1000 ) - 128 @@ -138,6 +138,7 @@ body { font-family: "Noto Sans", "Noto Color Emoji", sans-serif; font-size: var(--mainFontSize); color: var(--SmartThemeBodyColor); + overflow: hidden; } ::-webkit-scrollbar { From 10716d110131db82179012712ae0fbccb93e8b60 Mon Sep 17 00:00:00 2001 From: aikitoria <151776613+aikitoria@users.noreply.github.com> Date: Wed, 22 Nov 2023 19:18:00 +0100 Subject: [PATCH 109/185] Add manifest.json for Chrome Android --- public/index.html | 1 + public/manifest.json | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 public/manifest.json diff --git a/public/index.html b/public/index.html index 4da67bb0a..b5085c46f 100644 --- a/public/index.html +++ b/public/index.html @@ -8,6 +8,7 @@ + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 000000000..28df3de4c --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "SillyTavern", + "short_name": "SillyTavern", + "start_url": "/", + "display": "standalone", + "theme_color": "#202124", + "background_color": "#202124", + "icons": [ + { + "src": "img/apple-icon-57x57.png", + "sizes": "57x57", + "type": "image/png" + }, + { + "src": "img/apple-icon-72x72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "img/apple-icon-114x114.png", + "sizes": "114x114", + "type": "image/png" + }, + { + "src": "img/apple-icon-144x144.png", + "sizes": "144x144", + "type": "image/png" + } + ] +} From 45b714fb9e6dac2df06749c6c3124df45b678b1b Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Nov 2023 00:59:34 +0200 Subject: [PATCH 110/185] Don't crash server if google translate fails --- src/translate.js | 52 +++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/translate.js b/src/translate.js index 7d22ccc0c..12a184e2b 100644 --- a/src/translate.js +++ b/src/translate.js @@ -58,34 +58,44 @@ function registerEndpoints(app, jsonParser) { }); app.post('/api/translate/google', jsonParser, async (request, response) => { - const { generateRequestUrl, normaliseResponse } = require('google-translate-api-browser'); - const text = request.body.text; - const lang = request.body.lang; + try { + const { generateRequestUrl, normaliseResponse } = require('google-translate-api-browser'); + const text = request.body.text; + const lang = request.body.lang; - if (!text || !lang) { - return response.sendStatus(400); - } + if (!text || !lang) { + return response.sendStatus(400); + } - console.log('Input text: ' + text); + console.log('Input text: ' + text); - const url = generateRequestUrl(text, { to: lang }); + const url = generateRequestUrl(text, { to: lang }); - https.get(url, (resp) => { - let data = ''; + https.get(url, (resp) => { + let data = ''; - resp.on('data', (chunk) => { - data += chunk; + resp.on('data', (chunk) => { + data += chunk; + }); + + resp.on('end', () => { + try { + const result = normaliseResponse(JSON.parse(data)); + console.log('Translated text: ' + result.text); + return response.send(result.text); + } catch (error) { + console.log("Translation error", error); + return response.sendStatus(500); + } + }); + }).on("error", (err) => { + console.log("Translation error: " + err.message); + return response.sendStatus(500); }); - - resp.on('end', () => { - const result = normaliseResponse(JSON.parse(data)); - console.log('Translated text: ' + result.text); - return response.send(result.text); - }); - }).on("error", (err) => { - console.log("Translation error: " + err.message); + } catch (error) { + console.log("Translation error", error); return response.sendStatus(500); - }); + } }); app.post('/api/translate/deepl', jsonParser, async (request, response) => { From b4afb10fab7874697fe781044dc7b85b46134226 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Nov 2023 02:03:41 +0200 Subject: [PATCH 111/185] Change # of beams min value --- public/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index a09e83e72..c7c65dcaa 100644 --- a/public/index.html +++ b/public/index.html @@ -1228,8 +1228,8 @@
# of Beams - - + +
Length Penalty @@ -4847,4 +4847,4 @@ - \ No newline at end of file + From e445aeec14cc2eb8b641469910aa55141d79361a Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 23 Nov 2023 00:09:58 -0500 Subject: [PATCH 112/185] Tabby: Fix model name return on error Tabby's model API is always /v1/model/list, so return "None" if the request fails since that means a model is most likely not loaded. Signed-off-by: kingbri --- public/index.html | 2 +- server.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index c7c65dcaa..cc3ad9163 100644 --- a/public/index.html +++ b/public/index.html @@ -1513,7 +1513,7 @@ - +
diff --git a/server.js b/server.js index a07e6cb56..12375c627 100644 --- a/server.js +++ b/server.js @@ -596,6 +596,10 @@ app.post("/api/textgenerationwebui/status", jsonParser, async function (request, const modelName = modelInfo?.id; result = modelName || result; + } else { + // TabbyAPI returns an error 400 if a model isn't loaded + + result = "None" } } catch (error) { console.error(`Failed to get TabbyAPI model info: ${error}`); From e672a7fe991b3bfe2fe84afb02b50a48eea6e121 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:20:31 +0900 Subject: [PATCH 113/185] 'New Chat' in Manage Chats & showLoader when delChat-ing --- public/index.html | 1 + public/script.js | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index a09e83e72..c4a0e829c 100644 --- a/public/index.html +++ b/public/index.html @@ -3789,6 +3789,7 @@ Chat History
+
diff --git a/public/script.js b/public/script.js index 4d4b711a9..4cca3d855 100644 --- a/public/script.js +++ b/public/script.js @@ -7833,20 +7833,22 @@ jQuery(async function () { if (popup_type == "del_chat") { //close past chat popup - $("#select_chat_cross").click(); - + $("#select_chat_cross").trigger('click'); + showLoader() if (selected_group) { await deleteGroupChat(selected_group, chat_file_for_del); } else { await delChat(chat_file_for_del); } - //open the history view again after 100ms + //open the history view again after 2seconds (delay to avoid edge cases for deleting last chat) //hide option popup menu setTimeout(function () { $("#option_select_chat").click(); $("#options").hide(); + hideLoader() }, 2000); + } if (popup_type == "del_ch") { const deleteChats = !!$("#del_char_checkbox").prop("checked"); @@ -8306,6 +8308,17 @@ jQuery(async function () { hideMenu(); }); + $("#newChatFromManageScreenButton").on('click', function () { + setTimeout(() => { + $("#option_start_new_chat").trigger('click'); + }, 1); + setTimeout(() => { + $("#dialogue_popup_ok").trigger('click'); + }, 1); + $("#select_chat_cross").trigger('click') + + }) + ////////////////////////////////////////////////////////////////////////////////////////////// //functionality for the cancel delete messages button, reverts to normal display of input form From e0e303b3390e0dd72979490a3e0aec8a20720a94 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Thu, 23 Nov 2023 19:49:15 +0900 Subject: [PATCH 114/185] Sortable QuickReplies --- .../scripts/extensions/quick-reply/index.js | 80 ++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 1a9281e8e..2daa0cff4 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -1,6 +1,6 @@ import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } from "../../../script.js"; import { getContext, extension_settings } from "../../extensions.js"; -import { initScrollHeight, resetScrollHeight } from "../../utils.js"; +import { initScrollHeight, resetScrollHeight, getSortableDelay } from "../../utils.js"; import { executeSlashCommands, registerSlashCommand } from "../../slash-commands.js"; export { MODULE_NAME }; @@ -248,6 +248,50 @@ async function saveQuickReplyPreset() { } } +//just a copy of save function with the name hardcoded to currently selected preset +async function updateQuickReplyPreset() { + const name = $("#quickReplyPresets").val() + + if (!name) { + return; + } + + const quickReplyPreset = { + name: name, + quickReplyEnabled: extension_settings.quickReply.quickReplyEnabled, + quickReplySlots: extension_settings.quickReply.quickReplySlots, + numberOfSlots: extension_settings.quickReply.numberOfSlots, + AutoInputInject: extension_settings.quickReply.AutoInputInject, + selectedPreset: name, + } + + const response = await fetch('/savequickreply', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify(quickReplyPreset) + }); + + if (response.ok) { + const quickReplyPresetIndex = presets.findIndex(x => x.name == name); + + if (quickReplyPresetIndex == -1) { + presets.push(quickReplyPreset); + const option = document.createElement('option'); + option.selected = true; + option.value = name; + option.innerText = name; + $('#quickReplyPresets').append(option); + } + else { + presets[quickReplyPresetIndex] = quickReplyPreset; + $(`#quickReplyPresets option[value="${name}"]`).prop('selected', true); + } + saveSettingsDebounced(); + } else { + toastr.warning('Failed to save Quick Reply Preset.') + } +} + async function onQuickReplyNumberOfSlotsInput() { const $input = $('#quickReplyNumberOfSlots'); let numberOfSlots = Number($input.val()); @@ -291,8 +335,10 @@ function generateQuickReplyElements() { let quickReplyHtml = ''; for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { + let itemNumber = i + 1 quickReplyHtml += ` -
+
+
@@ -352,6 +398,25 @@ async function doQR(_, text) { whichQR.trigger('click') } +function saveQROrder() { + //update html-level order data to match new sort + let i = 1 + $('#quickReplyContainer').children().each(function () { + $(this).attr('data-order', i) + $(this).find('input').attr('id', `quickReply${i}Label`) + $(this).find('textarea').attr('id', `quickReply${i}Mes`) + i++ + }); + + //rebuild the extension_Settings array based on new order + i = 1 + $('#quickReplyContainer').children().each(function () { + onQuickReplyLabelInput(i) + onQuickReplyInput(i) + i++ + }); +} + jQuery(async () => { moduleWorker(); setInterval(moduleWorker, UPDATE_INTERVAL); @@ -386,7 +451,10 @@ jQuery(async () => { +
@@ -413,6 +481,12 @@ jQuery(async () => { $('#quickReplyEnabled').on('input', onQuickReplyEnabledInput); $('#quickReplyNumberOfSlotsApply').on('click', onQuickReplyNumberOfSlotsInput); $("#quickReplyPresetSaveButton").on('click', saveQuickReplyPreset); + $("#quickReplyPresetUpdateButton").on('click', updateQuickReplyPreset); + + $('#quickReplyContainer').sortable({ + delay: getSortableDelay(), + stop: saveQROrder, + }); $("#quickReplyPresets").on('change', async function () { const quickReplyPresetSelected = $(this).find(':selected').val(); From cc426e98978df03ef0b3723ff85b9dce2fbf4a67 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 23 Nov 2023 12:21:25 +0000 Subject: [PATCH 115/185] add qr context menus --- .../scripts/extensions/quick-reply/index.js | 66 ++++++++++++++++++- .../extensions/quick-reply/src/ContextMenu.js | 46 +++++++++++++ .../extensions/quick-reply/src/MenuHeader.js | 27 ++++++++ .../extensions/quick-reply/src/MenuItem.js | 43 ++++++++++++ .../extensions/quick-reply/src/SubMenu.js | 55 ++++++++++++++++ .../scripts/extensions/quick-reply/style.css | 34 ++++++++++ 6 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/src/ContextMenu.js create mode 100644 public/scripts/extensions/quick-reply/src/MenuHeader.js create mode 100644 public/scripts/extensions/quick-reply/src/MenuItem.js create mode 100644 public/scripts/extensions/quick-reply/src/SubMenu.js diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 1a9281e8e..fbedc200a 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -2,6 +2,9 @@ import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } import { getContext, extension_settings } from "../../extensions.js"; import { initScrollHeight, resetScrollHeight } from "../../utils.js"; import { executeSlashCommands, registerSlashCommand } from "../../slash-commands.js"; +import { ContextMenu } from "./src/ContextMenu.js"; +import { MenuItem } from "./src/MenuItem.js"; +import { MenuHeader } from "./src/MenuHeader.js"; export { MODULE_NAME }; @@ -99,7 +102,12 @@ function onQuickReplyInput(id) { function onQuickReplyLabelInput(id) { extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val(); - $(`#quickReply${id}`).text(String($(`#quickReply${id}Label`).val())); + let quickReplyLabel = extension_settings.quickReply.quickReplySlots[id - 1]?.label || ''; + const parts = quickReplyLabel.split('...'); + if (parts.length > 1) { + quickReplyLabel = `${parts.shift()}…`; + } + $(`#quickReply${id}`).text(quickReplyLabel); saveSettingsDebounced(); } @@ -129,13 +137,15 @@ async function onAutoInputInject() { } async function sendQuickReply(index) { - const existingText = $("#send_textarea").val(); const prompt = extension_settings.quickReply.quickReplySlots[index]?.mes || ''; - + await performQuickReply(prompt, index); +} +async function performQuickReply(prompt, index) { if (!prompt) { console.warn(`Quick reply slot ${index} is empty! Aborting.`); return; } + const existingText = $("#send_textarea").val(); let newText; @@ -170,6 +180,44 @@ async function sendQuickReply(index) { } +function buildContextMenu(qr, chainMes=null, hierarchy=[]) { + const tree = { + label: qr.label, + mes: (chainMes&&qr.mes ? `${chainMes} | ` : '') + qr.mes, + children: [], + }; + const parts = qr.label.split('...'); + if (parts.length > 1) { + tree.label = parts.shift(); + parts.forEach(subName=>{ + let chain = false; + if (subName[0] == '!') { + chain = true; + subName = subName.substring(1); + } + const sub = presets.find(it=>it.name == subName); + if (sub) { + // prevent circular references + if (hierarchy.indexOf(sub.name) == -1) { + tree.children.push(new MenuHeader(sub.name)); + sub.quickReplySlots.forEach(subQr=>{ + const subInfo = buildContextMenu(subQr, chain?tree.mes:null, [...hierarchy, sub.name]); + tree.children.push(new MenuItem( + subInfo.label, + subInfo.mes, + (evt)=>{ + evt.stopPropagation(); + performQuickReply(subInfo.mes); + }, + subInfo.children, + )); + }); + } + } + }); + } + return tree; +} function addQuickReplyBar() { $('#quickReplyBar').remove(); let quickReplyButtonHtml = ''; @@ -177,6 +225,10 @@ function addQuickReplyBar() { for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { let quickReplyMes = extension_settings.quickReply.quickReplySlots[i]?.mes || ''; let quickReplyLabel = extension_settings.quickReply.quickReplySlots[i]?.label || ''; + const parts = quickReplyLabel.split('...'); + if (parts.length > 1) { + quickReplyLabel = `${parts.shift()}…`; + } quickReplyButtonHtml += `
${quickReplyLabel}
`; } @@ -194,6 +246,14 @@ function addQuickReplyBar() { let index = $(this).data('index'); sendQuickReply(index); }); + $('.quickReplyButton').on('contextmenu', function (evt) { + evt.preventDefault(); + let index = $(this).data('index'); + const qr = extension_settings.quickReply.quickReplySlots[index]; + const tree = buildContextMenu(qr); + const menu = new ContextMenu(tree.children); + menu.show(evt); + }); } async function moduleWorker() { diff --git a/public/scripts/extensions/quick-reply/src/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ContextMenu.js new file mode 100644 index 000000000..be27dede9 --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/ContextMenu.js @@ -0,0 +1,46 @@ +import { MenuItem } from "./MenuItem.js"; + +export class ContextMenu { + /**@type {HTMLElement}*/ root; + /**@type {HTMLElement}*/ menu; + + /**@type {MenuItem[]}*/ itemList = []; + + + + + constructor(/**@type {MenuItem[]}*/items) { + this.itemList = items; + } + + render() { + if (!this.root) { + const blocker = document.createElement('div'); { + this.root = blocker; + blocker.classList.add('ctx-blocker'); + blocker.addEventListener('click', ()=>this.hide()); + const menu = document.createElement('ul'); { + this.menu = menu; + menu.classList.add('list-group'); + menu.classList.add('ctx-menu'); + this.itemList.forEach(it=>menu.append(it.render())); + blocker.append(menu); + } + } + } + return this.root; + } + + + + + show({screenX, screenY}) { + this.render(); + this.menu.style.bottom = `${window.innerHeight - screenY}px`; + this.menu.style.left = `${screenX}px`; + document.body.append(this.root); + } + hide() { + this.root.remove(); + } +} \ No newline at end of file diff --git a/public/scripts/extensions/quick-reply/src/MenuHeader.js b/public/scripts/extensions/quick-reply/src/MenuHeader.js new file mode 100644 index 000000000..51ff2c5bd --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/MenuHeader.js @@ -0,0 +1,27 @@ +import { SubMenu } from "./SubMenu.js"; + +export class MenuHeader { + /**@type {String}*/ label; + + /**@type {HTMLElement}*/ root; + + + + + constructor(/**@type {String}*/label) { + this.label = label; + } + + + render() { + if (!this.root) { + const item = document.createElement('li'); { + this.root = item; + item.classList.add('list-group-item'); + item.classList.add('ctx-header'); + item.append(this.label); + } + } + return this.root; + } +} \ No newline at end of file diff --git a/public/scripts/extensions/quick-reply/src/MenuItem.js b/public/scripts/extensions/quick-reply/src/MenuItem.js new file mode 100644 index 000000000..6b223d63f --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/MenuItem.js @@ -0,0 +1,43 @@ +import { SubMenu } from "./SubMenu.js"; + +export class MenuItem { + /**@type {String}*/ label; + /**@type {Object}*/ value; + /**@type {Function}*/ callback; + /**@type {MenuItem[]}*/ childList = []; + + /**@type {HTMLElement}*/ root; + + + + + constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children=[]) { + this.label = label; + this.value = value; + this.callback = callback; + this.childList = children; + } + + + render() { + if (!this.root) { + const item = document.createElement('li'); { + this.root = item; + item.classList.add('list-group-item'); + item.classList.add('ctx-item'); + item.title = this.value; + if (this.callback) { + item.addEventListener('click', (evt)=>this.callback(evt, this)); + } + if (this.childList.length > 0) { + item.classList.add('ctx-has-children'); + const sub = new SubMenu(this.childList); + item.addEventListener('pointerover', ()=>sub.show(item)); + item.addEventListener('pointerleave', ()=>sub.hide()); + } + item.append(this.label); + } + } + return this.root; + } +} \ No newline at end of file diff --git a/public/scripts/extensions/quick-reply/src/SubMenu.js b/public/scripts/extensions/quick-reply/src/SubMenu.js new file mode 100644 index 000000000..7c131dd80 --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/SubMenu.js @@ -0,0 +1,55 @@ +import { MenuItem } from "./MenuItem.js"; + +export class SubMenu { + /**@type {MenuItem[]}*/ itemList = []; + /**@type {Boolean}*/ isActive = false; + + /**@type {HTMLElement}*/ root; + + + + + constructor(/**@type {MenuItem[]}*/items) { + this.itemList = items; + } + + render() { + if (!this.root) { + const menu = document.createElement('ul'); { + this.root = menu; + menu.classList.add('list-group'); + menu.classList.add('ctx-menu'); + menu.classList.add('ctx-sub-menu'); + this.itemList.forEach(it=>menu.append(it.render())); + } + } + return this.root; + } + + + + + show(/**@type {HTMLElement}*/parent) { + if (this.isActive) return; + this.isActive = true; + this.render(); + parent.append(this.root); + requestAnimationFrame(()=>{ + const rect = this.root.getBoundingClientRect(); + console.log(window.innerHeight, rect); + if (rect.bottom > window.innerHeight - 5) { + this.root.style.top = `${window.innerHeight - 5 - rect.bottom}px`; + } + if (rect.right > window.innerWidth - 5) { + this.root.style.left = 'unset'; + this.root.style.right = '100%'; + } + }); + } + hide() { + this.root.remove(); + this.root.style.top = ''; + this.root.style.left = ''; + this.isActive = false; + } +} \ No newline at end of file diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index a83cbebb2..7cbc6c581 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -44,4 +44,38 @@ opacity: 1; filter: brightness(1.2); cursor: pointer; +} + + +.ctx-blocker { + backdrop-filter: blur(1px); + background-color: rgba(0 0 0 / 10%); + bottom: 0; + left: 0; + position: fixed; + right: 0; + top: 0; + z-index: 999; +} +.ctx-menu { + position: fixed; + overflow: visible; +} +.list-group .list-group-item.ctx-header { + font-weight: bold; + cursor: default; +} +.ctx-item + .ctx-header { + border-top: 1px solid; +} +.ctx-item { + position: relative; +} +.ctx-item.ctx-has-children:after { + content: " >"; +} +.ctx-sub-menu { + position: absolute; + top: 0; + left: 100%; } \ No newline at end of file From 4f7c925dc600b155c5010f12eb4a6db3feb6c260 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:10:23 +0900 Subject: [PATCH 116/185] properly round left menu borders --- public/style.css | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/public/style.css b/public/style.css index 56446d75a..fe905d227 100644 --- a/public/style.css +++ b/public/style.css @@ -61,22 +61,10 @@ --SmartThemeCheckboxBgColorR: 220; --SmartThemeCheckboxBgColorG: 220; --SmartThemeCheckboxBgColorB: 210; - --SmartThemeCheckboxTickColorValue: calc( - ( - ( - ( - (var(--SmartThemeCheckboxBgColorR) * 299) + - (var(--SmartThemeCheckboxBgColorG) * 587) + - (var(--SmartThemeCheckboxBgColorB) * 114) - ) / 1000 - ) - 128 - ) * -1000 - ); - --SmartThemeCheckboxTickColor: rgb( - var(--SmartThemeCheckboxTickColorValue), - var(--SmartThemeCheckboxTickColorValue), - var(--SmartThemeCheckboxTickColorValue) - ); + --SmartThemeCheckboxTickColorValue: calc(((((var(--SmartThemeCheckboxBgColorR) * 299) + (var(--SmartThemeCheckboxBgColorG) * 587) + (var(--SmartThemeCheckboxBgColorB) * 114)) / 1000) - 128) * -1000); + --SmartThemeCheckboxTickColor: rgb(var(--SmartThemeCheckboxTickColorValue), + var(--SmartThemeCheckboxTickColorValue), + var(--SmartThemeCheckboxTickColorValue)); --sheldWidth: 50vw; @@ -679,6 +667,7 @@ hr { backdrop-filter: blur(var(--SmartThemeBlurStrength)); box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); flex-flow: column; + border-radius: 10px; } .options-content, @@ -3782,4 +3771,4 @@ a { height: 100vh; z-index: 9999; } -} +} \ No newline at end of file From 73d680140697d4dde1d72efb3e1e879a0c945f52 Mon Sep 17 00:00:00 2001 From: RossAscends <124905043+RossAscends@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:32:47 +0900 Subject: [PATCH 117/185] slashcommand /movingui to set a MUI preset --- public/scripts/power-user.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 807941eaf..7daeb8431 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -2352,6 +2352,28 @@ async function setThemeCallback(_, text) { saveSettingsDebounced(); } +async function setmovingUIPreset(_, text) { + const fuse = new Fuse(movingUIPresets, { + keys: [ + { name: 'name', weight: 1 }, + ], + }); + + const results = fuse.search(text); + console.debug('movingUI preset fuzzy search results for ' + text, results); + const preset = results[0]?.item; + + if (!preset) { + toastr.warning(`Could not find preset with name: ${text}`); + return; + } + + power_user.movingUIPreset = preset.name; + applyMovingUIPreset(preset.name); + $("#movingUIPresets").val(preset.name); + saveSettingsDebounced(); +} + /** * Gets the custom stopping strings from the power user settings. * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings. @@ -3027,4 +3049,5 @@ $(document).ready(() => { registerSlashCommand('resetpanels', doResetPanels, ['resetui'], '– resets UI panels to original state.', true, true); registerSlashCommand('bgcol', setAvgBG, [], '– WIP test of auto-bg avg coloring', true, true); registerSlashCommand('theme', setThemeCallback, [], '(name) – sets a UI theme by name', true, true); + registerSlashCommand('movingui', setmovingUIPreset, [], '(name) – activates a movingUI preset by name', true, true); }); From d32224041a74c38b568b67fdfda52b9e1656da88 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 23 Nov 2023 15:57:14 +0000 Subject: [PATCH 118/185] add parent placeholders --- public/scripts/extensions/quick-reply/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index fc8669ab4..ed0c0b0f4 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -180,7 +180,7 @@ async function performQuickReply(prompt, index) { } -function buildContextMenu(qr, chainMes=null, hierarchy=[]) { +function buildContextMenu(qr, chainMes=null, hierarchy=[], labelHierarchy=[]) { const tree = { label: qr.label, mes: (chainMes&&qr.mes ? `${chainMes} | ` : '') + qr.mes, @@ -199,15 +199,19 @@ function buildContextMenu(qr, chainMes=null, hierarchy=[]) { if (sub) { // prevent circular references if (hierarchy.indexOf(sub.name) == -1) { + const nextHierarchy = [...hierarchy, sub.name]; + const nextLabelHierarchy = [...labelHierarchy, tree.label]; tree.children.push(new MenuHeader(sub.name)); sub.quickReplySlots.forEach(subQr=>{ - const subInfo = buildContextMenu(subQr, chain?tree.mes:null, [...hierarchy, sub.name]); + const subInfo = buildContextMenu(subQr, chain?tree.mes:null, nextHierarchy, nextLabelHierarchy); tree.children.push(new MenuItem( subInfo.label, subInfo.mes, (evt)=>{ evt.stopPropagation(); - performQuickReply(subInfo.mes); + performQuickReply(subInfo.mes.replace(/%%parent(-\d+)?%%/g, (_, index)=>{ + return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0]; + })); }, subInfo.children, )); From af2b1087301082447db12a8f8f6622be5737c693 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 23 Nov 2023 17:42:19 +0000 Subject: [PATCH 119/185] add context menu editor --- .../quick-reply/contextMenuEditor.html | 21 +++ .../scripts/extensions/quick-reply/index.js | 145 ++++++++++++------ 2 files changed, 120 insertions(+), 46 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/contextMenuEditor.html diff --git a/public/scripts/extensions/quick-reply/contextMenuEditor.html b/public/scripts/extensions/quick-reply/contextMenuEditor.html new file mode 100644 index 000000000..00e77c0ac --- /dev/null +++ b/public/scripts/extensions/quick-reply/contextMenuEditor.html @@ -0,0 +1,21 @@ +
+
+

Quick Reply Context Menu Editor

+
+ +
+
+ +
+
+
diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index ed0c0b0f4..6b2f4f3ad 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -103,14 +103,69 @@ function onQuickReplyInput(id) { function onQuickReplyLabelInput(id) { extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val(); let quickReplyLabel = extension_settings.quickReply.quickReplySlots[id - 1]?.label || ''; - const parts = quickReplyLabel.split('...'); - if (parts.length > 1) { - quickReplyLabel = `${parts.shift()}…`; - } - $(`#quickReply${id}`).text(quickReplyLabel); + $(`#quickReply${id}`).text(quickReplyLabel + (extension_settings.quickReply.quickReplySlots[id - 1]?.contextMenu?.length?'…':'')); saveSettingsDebounced(); } +async function onQuickReplyContextMenuChange(id) { + extension_settings.quickReply.quickReplySlots[id - 1].contextMenu = JSON.parse($(`#quickReplyContainer > [data-order="${id}"]`).attr('data-contextMenu')) + saveSettingsDebounced(); +} + +async function onQuickReplyCtxButtonClick(id) { + const editorHtml = $(await $.get('scripts/extensions/quick-reply/contextMenuEditor.html')); + const popupResult = callPopup(editorHtml, "confirm", undefined, { okButton: "Save", wide:false, large:false, rows: 1 }); + const qr = extension_settings.quickReply.quickReplySlots[id - 1]; + if (!qr.contextMenu) { + qr.contextMenu = []; + } + /**@type {HTMLTemplateElement}*/ + const tpl = document.querySelector('#quickReply_contextMenuEditor_itemTemplate'); + const fillPresetSelect = (select, item) => { + [{name:'Select a preset', value:''}, ...presets].forEach(preset=>{ + const opt = document.createElement('option'); { + opt.value = preset.value ?? preset.name; + opt.textContent = preset.name; + opt.selected = preset.name == item.preset; + select.append(opt); + } + }); + }; + const addCtxItem = (item, idx) => { + const dom = tpl.content.cloneNode(true); + const ctxItem = dom.querySelector('.quickReplyContextMenuEditor_item'); + ctxItem.setAttribute('data-order', idx); + const select = ctxItem.querySelector('.quickReply_contextMenuEditor_preset'); + fillPresetSelect(select, item); + dom.querySelector('.quickReply_contextMenuEditor_chaining').checked = item.chain; + $('.quickReply_contextMenuEditor_remove', ctxItem).on('click', ()=>ctxItem.remove()); + document.querySelector('#quickReply_contextMenuEditor_content').append(ctxItem); + } + [...qr.contextMenu, {}].forEach((item,idx)=>{ + addCtxItem(item, idx) + }); + $('#quickReply_contextMenuEditor_addPreset').on('click', ()=>{ + addCtxItem({}, document.querySelector('#quickReply_contextMenuEditor_content').children.length); + }); + + $('#quickReply_contextMenuEditor_content').sortable({ + delay: getSortableDelay(), + stop: ()=>{}, + }); + + if (await popupResult) { + qr.contextMenu = Array.from(document.querySelectorAll('#quickReply_contextMenuEditor_content > .quickReplyContextMenuEditor_item')) + .map(item=>({ + preset: item.querySelector('.quickReply_contextMenuEditor_preset').value, + chain: item.querySelector('.quickReply_contextMenuEditor_chaining').checked, + })) + .filter(item=>item.preset); + $(`#quickReplyContainer[data-order="${id}"]`).attr('data-contextMenu', JSON.stringify(qr.contextMenu)); + updateQuickReplyPreset(); + onQuickReplyLabelInput(id); + } +} + async function onQuickReplyEnabledInput() { let isEnabled = $(this).prop('checked') extension_settings.quickReply.quickReplyEnabled = !!isEnabled; @@ -186,40 +241,33 @@ function buildContextMenu(qr, chainMes=null, hierarchy=[], labelHierarchy=[]) { mes: (chainMes&&qr.mes ? `${chainMes} | ` : '') + qr.mes, children: [], }; - const parts = qr.label.split('...'); - if (parts.length > 1) { - tree.label = parts.shift(); - parts.forEach(subName=>{ - let chain = false; - if (subName[0] == '!') { - chain = true; - subName = subName.substring(1); + qr.contextMenu?.forEach(ctxItem=>{ + let chain = ctxItem.chain; + let subName = ctxItem.preset; + const sub = presets.find(it=>it.name == subName); + if (sub) { + // prevent circular references + if (hierarchy.indexOf(sub.name) == -1) { + const nextHierarchy = [...hierarchy, sub.name]; + const nextLabelHierarchy = [...labelHierarchy, tree.label]; + tree.children.push(new MenuHeader(sub.name)); + sub.quickReplySlots.forEach(subQr=>{ + const subInfo = buildContextMenu(subQr, chain?tree.mes:null, nextHierarchy, nextLabelHierarchy); + tree.children.push(new MenuItem( + subInfo.label, + subInfo.mes, + (evt)=>{ + evt.stopPropagation(); + performQuickReply(subInfo.mes.replace(/%%parent(-\d+)?%%/g, (_, index)=>{ + return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0]; + })); + }, + subInfo.children, + )); + }); } - const sub = presets.find(it=>it.name == subName); - if (sub) { - // prevent circular references - if (hierarchy.indexOf(sub.name) == -1) { - const nextHierarchy = [...hierarchy, sub.name]; - const nextLabelHierarchy = [...labelHierarchy, tree.label]; - tree.children.push(new MenuHeader(sub.name)); - sub.quickReplySlots.forEach(subQr=>{ - const subInfo = buildContextMenu(subQr, chain?tree.mes:null, nextHierarchy, nextLabelHierarchy); - tree.children.push(new MenuItem( - subInfo.label, - subInfo.mes, - (evt)=>{ - evt.stopPropagation(); - performQuickReply(subInfo.mes.replace(/%%parent(-\d+)?%%/g, (_, index)=>{ - return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0]; - })); - }, - subInfo.children, - )); - }); - } - } - }); - } + } + }); return tree; } function addQuickReplyBar() { @@ -229,9 +277,8 @@ function addQuickReplyBar() { for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { let quickReplyMes = extension_settings.quickReply.quickReplySlots[i]?.mes || ''; let quickReplyLabel = extension_settings.quickReply.quickReplySlots[i]?.label || ''; - const parts = quickReplyLabel.split('...'); - if (parts.length > 1) { - quickReplyLabel = `${parts.shift()}…`; + if (extension_settings.quickReply.quickReplySlots[i]?.contextMenu?.length) { + quickReplyLabel = `${quickReplyLabel}…`; } quickReplyButtonHtml += `
${quickReplyLabel}
`; } @@ -251,12 +298,14 @@ function addQuickReplyBar() { sendQuickReply(index); }); $('.quickReplyButton').on('contextmenu', function (evt) { - evt.preventDefault(); let index = $(this).data('index'); const qr = extension_settings.quickReply.quickReplySlots[index]; - const tree = buildContextMenu(qr); - const menu = new ContextMenu(tree.children); - menu.show(evt); + if (qr.contextMenu?.length) { + evt.preventDefault(); + const tree = buildContextMenu(qr); + const menu = new ContextMenu(tree.children); + menu.show(evt); + } }); } @@ -401,9 +450,10 @@ function generateQuickReplyElements() { for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { let itemNumber = i + 1 quickReplyHtml += ` -
+
+
`; @@ -414,6 +464,8 @@ function generateQuickReplyElements() { for (let i = 1; i <= extension_settings.quickReply.numberOfSlots; i++) { $(`#quickReply${i}Mes`).on('input', function () { onQuickReplyInput(i); }); $(`#quickReply${i}Label`).on('input', function () { onQuickReplyLabelInput(i); }); + $(`#quickReply${i}CtxButton`).on('click', function () { onQuickReplyCtxButtonClick(i); }); + $(`#quickReplyContainer > [data-order="${i}"]`).attr('data-contextMenu', JSON.stringify(extension_settings.quickReply.quickReplySlots[i-1]?.contextMenu??[])); } $('.quickReplySettings .inline-drawer-toggle').off('click').on('click', function () { @@ -475,6 +527,7 @@ function saveQROrder() { //rebuild the extension_Settings array based on new order i = 1 $('#quickReplyContainer').children().each(function () { + onQuickReplyContextMenuChange(i) onQuickReplyLabelInput(i) onQuickReplyInput(i) i++ From 81f135fa7c76f62b1031a51f8af846567805397e Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 23 Nov 2023 17:42:31 +0000 Subject: [PATCH 120/185] use client coords not screen coords --- public/scripts/extensions/quick-reply/src/ContextMenu.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/quick-reply/src/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ContextMenu.js index be27dede9..33336b4cc 100644 --- a/public/scripts/extensions/quick-reply/src/ContextMenu.js +++ b/public/scripts/extensions/quick-reply/src/ContextMenu.js @@ -34,10 +34,10 @@ export class ContextMenu { - show({screenX, screenY}) { + show({clientX, clientY}) { this.render(); - this.menu.style.bottom = `${window.innerHeight - screenY}px`; - this.menu.style.left = `${screenX}px`; + this.menu.style.bottom = `${window.innerHeight - clientY}px`; + this.menu.style.left = `${clientX}px`; document.body.append(this.root); } hide() { From 22e17cd6810168e4fdab76aa917985c03f192006 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Nov 2023 20:50:08 +0200 Subject: [PATCH 121/185] Pass image type to thumbnail creator --- public/scripts/extensions/shared.js | 2 +- public/scripts/utils.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js index 37dbec96a..26164d8d0 100644 --- a/public/scripts/extensions/shared.js +++ b/public/scripts/extensions/shared.js @@ -23,7 +23,7 @@ export async function getMultimodalCaption(base64Img, prompt) { const compressionLimit = 2 * 1024 * 1024; if (extension_settings.caption.multimodal_api === 'openrouter' && base64Bytes > compressionLimit) { const maxSide = 1024; - base64Img = await createThumbnail(base64Img, maxSide, maxSide); + base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg'); } const apiResult = await fetch('/api/openai/caption-image', { diff --git a/public/scripts/utils.js b/public/scripts/utils.js index d8b99956f..571583240 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -973,9 +973,10 @@ export function loadFileToDocument(url, type) { * @param {string} dataUrl The data URL encoded data of the image. * @param {number} maxWidth The maximum width of the thumbnail. * @param {number} maxHeight The maximum height of the thumbnail. + * @param {string} [type='image/jpeg'] The type of the thumbnail. * @returns {Promise} A promise that resolves to the thumbnail data URL. */ -export function createThumbnail(dataUrl, maxWidth, maxHeight) { +export function createThumbnail(dataUrl, maxWidth, maxHeight, type = 'image/jpeg') { return new Promise((resolve, reject) => { const img = new Image(); img.src = dataUrl; @@ -1000,7 +1001,7 @@ export function createThumbnail(dataUrl, maxWidth, maxHeight) { ctx.drawImage(img, 0, 0, thumbnailWidth, thumbnailHeight); // Convert the canvas to a data URL and resolve the promise - const thumbnailDataUrl = canvas.toDataURL('image/jpeg'); + const thumbnailDataUrl = canvas.toDataURL(type); resolve(thumbnailDataUrl); }; From cdbcd6cfb2e251043667eb097a6af76e2191f530 Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 23 Nov 2023 20:34:20 +0000 Subject: [PATCH 122/185] add a healthy dose of mobile copium --- .../scripts/extensions/quick-reply/index.js | 19 +++++++-- .../extensions/quick-reply/src/ContextMenu.js | 25 ++++++++++-- .../extensions/quick-reply/src/MenuHeader.js | 12 ++---- .../extensions/quick-reply/src/MenuItem.js | 39 +++++++++++++++++-- .../extensions/quick-reply/src/SubMenu.js | 15 +++++-- .../scripts/extensions/quick-reply/style.css | 18 ++++++++- 6 files changed, 104 insertions(+), 24 deletions(-) diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 6b2f4f3ad..60a14a071 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -102,8 +102,7 @@ function onQuickReplyInput(id) { function onQuickReplyLabelInput(id) { extension_settings.quickReply.quickReplySlots[id - 1].label = $(`#quickReply${id}Label`).val(); - let quickReplyLabel = extension_settings.quickReply.quickReplySlots[id - 1]?.label || ''; - $(`#quickReply${id}`).text(quickReplyLabel + (extension_settings.quickReply.quickReplySlots[id - 1]?.contextMenu?.length?'…':'')); + addQuickReplyBar(); saveSettingsDebounced(); } @@ -277,10 +276,11 @@ function addQuickReplyBar() { for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { let quickReplyMes = extension_settings.quickReply.quickReplySlots[i]?.mes || ''; let quickReplyLabel = extension_settings.quickReply.quickReplySlots[i]?.label || ''; + let expander = ''; if (extension_settings.quickReply.quickReplySlots[i]?.contextMenu?.length) { - quickReplyLabel = `${quickReplyLabel}…`; + expander = ''; } - quickReplyButtonHtml += `
${quickReplyLabel}
`; + quickReplyButtonHtml += `
${quickReplyLabel}${expander}
`; } const quickReplyBarFullHtml = ` @@ -297,6 +297,17 @@ function addQuickReplyBar() { let index = $(this).data('index'); sendQuickReply(index); }); + $('.quickReplyButton > .ctx-expander').on('click', function (evt) { + evt.stopPropagation(); + let index = $(this.closest('.quickReplyButton')).data('index'); + const qr = extension_settings.quickReply.quickReplySlots[index]; + if (qr.contextMenu?.length) { + evt.preventDefault(); + const tree = buildContextMenu(qr); + const menu = new ContextMenu(tree.children); + menu.show(evt); + } + }) $('.quickReplyButton').on('contextmenu', function (evt) { let index = $(this).data('index'); const qr = extension_settings.quickReply.quickReplySlots[index]; diff --git a/public/scripts/extensions/quick-reply/src/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ContextMenu.js index 33336b4cc..37ba3d5af 100644 --- a/public/scripts/extensions/quick-reply/src/ContextMenu.js +++ b/public/scripts/extensions/quick-reply/src/ContextMenu.js @@ -1,16 +1,23 @@ import { MenuItem } from "./MenuItem.js"; export class ContextMenu { + /**@type {MenuItem[]}*/ itemList = []; + /**@type {Boolean}*/ isActive = false; + /**@type {HTMLElement}*/ root; /**@type {HTMLElement}*/ menu; - /**@type {MenuItem[]}*/ itemList = []; - constructor(/**@type {MenuItem[]}*/items) { this.itemList = items; + items.forEach(item=>{ + item.onExpand = ()=>{ + items.filter(it=>it!=item) + .forEach(it=>it.collapse()); + }; + }); } render() { @@ -35,12 +42,24 @@ export class ContextMenu { show({clientX, clientY}) { + if (this.isActive) return; + this.isActive = true; this.render(); this.menu.style.bottom = `${window.innerHeight - clientY}px`; this.menu.style.left = `${clientX}px`; document.body.append(this.root); } hide() { - this.root.remove(); + if (this.root) { + this.root.remove(); + } + this.isActive = false; + } + toggle(/**@type {PointerEvent}*/evt) { + if (this.isActive) { + this.hide(); + } else { + this.show(evt); + } } } \ No newline at end of file diff --git a/public/scripts/extensions/quick-reply/src/MenuHeader.js b/public/scripts/extensions/quick-reply/src/MenuHeader.js index 51ff2c5bd..65c35af86 100644 --- a/public/scripts/extensions/quick-reply/src/MenuHeader.js +++ b/public/scripts/extensions/quick-reply/src/MenuHeader.js @@ -1,15 +1,9 @@ +import { MenuItem } from "./MenuItem.js"; import { SubMenu } from "./SubMenu.js"; -export class MenuHeader { - /**@type {String}*/ label; - - /**@type {HTMLElement}*/ root; - - - - +export class MenuHeader extends MenuItem { constructor(/**@type {String}*/label) { - this.label = label; + super(label, null, null); } diff --git a/public/scripts/extensions/quick-reply/src/MenuItem.js b/public/scripts/extensions/quick-reply/src/MenuItem.js index 6b223d63f..cbce352ff 100644 --- a/public/scripts/extensions/quick-reply/src/MenuItem.js +++ b/public/scripts/extensions/quick-reply/src/MenuItem.js @@ -5,8 +5,12 @@ export class MenuItem { /**@type {Object}*/ value; /**@type {Function}*/ callback; /**@type {MenuItem[]}*/ childList = []; + /**@type {SubMenu}*/ subMenu; + /**@type {Boolean}*/ isForceExpanded = false; /**@type {HTMLElement}*/ root; + + /**@type {Function}*/ onExpand; @@ -29,15 +33,44 @@ export class MenuItem { if (this.callback) { item.addEventListener('click', (evt)=>this.callback(evt, this)); } + item.append(this.label); if (this.childList.length > 0) { item.classList.add('ctx-has-children'); const sub = new SubMenu(this.childList); - item.addEventListener('pointerover', ()=>sub.show(item)); - item.addEventListener('pointerleave', ()=>sub.hide()); + this.subMenu = sub; + const trigger = document.createElement('div'); { + trigger.classList.add('ctx-expander'); + trigger.textContent = '⋮'; + trigger.addEventListener('click', (evt)=>{ + evt.stopPropagation(); + this.toggle(); + }); + item.append(trigger); + } + item.addEventListener('mouseover', ()=>sub.show(item)); + item.addEventListener('mouseleave', ()=>sub.hide()); + } - item.append(this.label); } } return this.root; } + + + expand() { + this.subMenu?.show(this.root); + if (this.onExpand) { + this.onExpand(); + } + } + collapse() { + this.subMenu?.hide(); + } + toggle() { + if (this.subMenu.isActive) { + this.expand(); + } else { + this.collapse(); + } + } } \ No newline at end of file diff --git a/public/scripts/extensions/quick-reply/src/SubMenu.js b/public/scripts/extensions/quick-reply/src/SubMenu.js index 7c131dd80..328e5a1ee 100644 --- a/public/scripts/extensions/quick-reply/src/SubMenu.js +++ b/public/scripts/extensions/quick-reply/src/SubMenu.js @@ -47,9 +47,18 @@ export class SubMenu { }); } hide() { - this.root.remove(); - this.root.style.top = ''; - this.root.style.left = ''; + if (this.root) { + this.root.remove(); + this.root.style.top = ''; + this.root.style.left = ''; + } this.isActive = false; } + toggle(/**@type {HTMLElement}*/parent) { + if (this.isActive) { + this.hide(); + } else { + this.show(parent); + } + } } \ No newline at end of file diff --git a/public/scripts/extensions/quick-reply/style.css b/public/scripts/extensions/quick-reply/style.css index 7cbc6c581..c64cb3d64 100644 --- a/public/scripts/extensions/quick-reply/style.css +++ b/public/scripts/extensions/quick-reply/style.css @@ -71,11 +71,25 @@ .ctx-item { position: relative; } -.ctx-item.ctx-has-children:after { - content: " >"; +.ctx-expander { + border-left: 1px solid; + margin-left: 1em; + text-align: center; + width: 2em; +} +.ctx-expander:hover { + font-weight: bold; } .ctx-sub-menu { position: absolute; top: 0; left: 100%; +} +@media screen and (max-width: 1000px) { + .ctx-blocker { + position: absolute; + } + .list-group .list-group-item.ctx-item { + padding: 1em; + } } \ No newline at end of file From 6d0982e823e46e0be5886883e4487389cf8e59a2 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:36:48 +0200 Subject: [PATCH 123/185] Unleash the variables / STscript alpha --- public/script.js | 2 + public/scripts/slash-commands.js | 13 ++- public/scripts/templates/macros.html | 17 +++- public/scripts/variables.js | 142 +++++++++++++++++++++------ 4 files changed, 138 insertions(+), 36 deletions(-) diff --git a/public/script.js b/public/script.js index 4cca3d855..d3e40c55e 100644 --- a/public/script.js +++ b/public/script.js @@ -194,6 +194,7 @@ import { hideLoader, showLoader } from "./scripts/loader.js"; import { CharacterContextMenu, BulkEditOverlay } from "./scripts/BulkEditOverlay.js"; import { loadMancerModels } from "./scripts/mancer-settings.js"; import { hasPendingFileAttachment, populateFileAttachment } from "./scripts/chats.js"; +import { replaceVariableMacros } from "./scripts/variables.js"; //exporting functions and vars for mods export { @@ -2008,6 +2009,7 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh content = content.replace(/{{original}}/i, _original); } + content = replaceVariableMacros(content); content = content.replace(/{{input}}/gi, String($('#send_textarea').val())); if (_replaceCharacterCard) { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 54d80c9cc..ba8a315b1 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -33,6 +33,7 @@ import { autoSelectPersona } from "./personas.js"; import { getContext } from "./extensions.js"; import { hideChatMessage, unhideChatMessage } from "./chats.js"; import { stringToRange } from "./utils.js"; +import { registerVariableCommands } from "./variables.js"; export { executeSlashCommands, registerSlashCommand, @@ -160,6 +161,7 @@ parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); +registerVariableCommands(); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; @@ -179,12 +181,12 @@ async function generateCallback(_, arg) { } async function echoCallback(_, arg) { - if (!arg) { + if (!String(arg)) { console.warn('WARN: No argument provided for /echo command'); return; } - toastr.info(arg); + toastr.info(String(arg)); return arg; } @@ -961,6 +963,11 @@ function setBackgroundCallback(_, bg) { } } +/** + * Executes slash commands in the provided text + * @param {string} text Slash command text + * @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>} + */ async function executeSlashCommands(text) { if (!text) { return false; @@ -1006,7 +1013,7 @@ async function executeSlashCommands(text) { const newText = lines.filter(x => linesToRemove.indexOf(x) === -1).join('\n'); - return { interrupt, newText }; + return { interrupt, newText, pipe: pipeResult }; } function setSlashCommandAutocomplete(textarea) { diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 9c55db112..8e33b15cd 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -1,4 +1,6 @@ -System-wide Replacement Macros (in order of evaluation): +
+ System-wide Replacement Macros (in order of evaluation): +
  • {{original}} – global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.
  • {{input}} – the user input
  • @@ -27,3 +29,16 @@ System-wide Replacement Macros (in order of evaluation):
  • {{roll:(formula)}} – rolls a dice. (ex: {{roll:1d6}} will roll a 6- sided dice and return a number between 1 and 6)
  • {{banned "text here"}} – dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.
+
+ Chat variables Macros: +
+
Local variables = unique to the current chat
+
Global variables = works in any chat for any character
+
    +
  • {{getvar::name}} – replaced with the value of the local variable "name"
  • +
  • {{setvar::name::value}} – replaced with empty string, sets the local variable "name" to "value"
  • +
  • {{addvar::name::increment}} – replaced with the result of addition numeric value of "increment" to the local variable "name"
  • +
  • {{getglobalvar::name}} – replaced with the value of the global variable "name"
  • +
  • {{setglobalvar::name::value}} – replaced with empty string, sets the global variable "name" to "value"
  • +
  • {{addglobalvar::name::value}} – replaced with the result of addition numeric value of "increment" to the global variable "name"
  • +
diff --git a/public/scripts/variables.js b/public/scripts/variables.js index d4b0227fc..eb54579f4 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1,6 +1,6 @@ import { chat_metadata, getCurrentChatId, sendSystemMessage, system_message_types } from "../script.js"; import { extension_settings } from "./extensions.js"; -import { registerSlashCommand } from "./slash-commands.js"; +import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js"; function getLocalVariable(name) { const localVariable = chat_metadata?.variables[name]; @@ -14,6 +14,7 @@ function setLocalVariable(name, value) { } chat_metadata.variables[name] = value; + return value; } function getGlobalVariable(name) { @@ -26,6 +27,42 @@ function setGlobalVariable(name, value) { extension_settings.variables.global[name] = value; } +function addLocalVariable(name, value) { + const currentValue = getLocalVariable(name) || 0; + const increment = Number(value); + + if (isNaN(increment)) { + return ''; + } + + const newValue = Number(currentValue) + increment; + + if (isNaN(newValue)) { + return ''; + } + + setLocalVariable(name, newValue); + return newValue; +} + +function addGlobalVariable(name, value) { + const currentValue = getGlobalVariable(name) || 0; + const increment = Number(value); + + if (isNaN(increment)) { + return ''; + } + + const newValue = Number(currentValue) + increment; + + if (isNaN(newValue)) { + return ''; + } + + setGlobalVariable(name, newValue); + return newValue; +} + export function replaceVariableMacros(str) { // Replace {{getvar::name}} with the value of the variable name str = str.replace(/{{getvar::([^}]+)}}/gi, (_, name) => { @@ -43,21 +80,7 @@ export function replaceVariableMacros(str) { // Replace {{addvar::name::value}} with empty string and add value to the variable value str = str.replace(/{{addvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => { name = name.toLowerCase().trim(); - const currentValue = getLocalVariable(name) || 0; - const increment = Number(value); - - if (isNaN(increment)) { - return ''; - } - - const newValue = Number(currentValue) + increment; - - if (isNaN(newValue)) { - return ''; - } - - setLocalVariable(name, newValue); - return ''; + return addLocalVariable(name, value);; }); // Replace {{getglobalvar::name}} with the value of the global variable name @@ -76,21 +99,7 @@ export function replaceVariableMacros(str) { // Replace {{addglobalvar::name::value}} with empty string and add value to the global variable value str = str.replace(/{{addglobalvar::([^:]+)::([^}]+)}}/gi, (_, name, value) => { name = name.toLowerCase().trim(); - const currentValue = getGlobalVariable(name) || 0; - const increment = Number(value); - - if (isNaN(increment)) { - return ''; - } - - const newValue = Number(currentValue) + increment; - - if (isNaN(newValue)) { - return ''; - } - - setGlobalVariable(name, newValue); - return ''; + return addGlobalVariable(name, value); }); return str; @@ -110,11 +119,80 @@ function listVariablesCallback() { const converter = new showdown.Converter(); const message = `### Local variables (${chatName}):\n${localVariablesString}\n\n### Global variables:\n${globalVariablesString}`; - const htmlMessage = converter.makeHtml(message); + const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message)); sendSystemMessage(system_message_types.GENERIC, htmlMessage); } +async function ifCallback(args, command) { + const a = getLocalVariable(args.a) || getGlobalVariable(args.a) || Number(args.a); + const b = getLocalVariable(args.b) || getGlobalVariable(args.b) || Number(args.b); + const rule = args.rule; + + if (!a || !b || !rule) { + toastr.warning('Both operands and the rule must be specified for the /if command.', 'Invalid /if command'); + return ''; + } + + const aNumber = Number(a); + const bNumber = Number(b); + + if (isNaN(aNumber) || isNaN(bNumber)) { + toastr.warning('Both operands must be numbers for the /if command.', 'Invalid /if command'); + return ''; + } + + let result = false; + + switch (rule) { + case 'gt': + result = aNumber > bNumber; + break; + case 'gte': + result = aNumber >= bNumber; + break; + case 'lt': + result = aNumber < bNumber; + break; + case 'lte': + result = aNumber <= bNumber; + break; + case 'eq': + result = aNumber === bNumber; + break; + default: + toastr.warning('Unknown rule for the /if command.', 'Invalid /if command'); + return ''; + } + + if (result && command) { + if (command.startsWith('"')) { + command = command.slice(1); + } + + if (command.endsWith('"')) { + command = command.slice(0, -1); + } + + const result = await executeSlashCommands(command); + + if (!result || typeof result !== 'object') { + return ''; + } + + return result?.pipe || ''; + } + + return ''; +} + export function registerVariableCommands() { registerSlashCommand('listvar', listVariablesCallback, [''], ' – list registered chat variables', true, true); + registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value), [], 'key=varname (value) – set a local variable value and pass it down the pipe, e.g. /setvar key=color green', true, true); + registerSlashCommand('getvar', (_, value) => getLocalVariable(value), [], '(key) – get a local variable value and pass it down the pipe, e.g. /getvar height', true, true); + registerSlashCommand('addvar', (args, value) => addLocalVariable(args.key || args.name, value), [], 'key=varname (increment) – add a value to a local variable and pass the result down the pipe, e.g. /addvar score 10', true, true); + registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], 'key=varname (value) – set a global variable value and pass it down the pipe, e.g. /setglobalvar key=color green', true, true); + registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '(key) – get a global variable value and pass it down the pipe, e.g. /getglobalvar height', true, true); + registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], 'key=varname (increment) – add a value to a global variable and pass the result down the pipe, e.g. /addglobalvar score 10', true, true); + registerSlashCommand('if', ifCallback, [], 'a=varname1 b=varname2 rule=comparison "(command)" – compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b e.g. /if a=score a=10 rule=gte "/speak You win" triggers a /speak command if the value of "score" is greater or equals 10.', true, true); } From 86819b6f4f8088169dcc4c50c0927ec6222b1967 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:50:13 +0200 Subject: [PATCH 124/185] Add /genraw command --- public/scripts/slash-commands.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index ba8a315b1..9bef091a1 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -24,6 +24,7 @@ import { Generate, this_chid, setCharacterName, + generateRaw, } from "../script.js"; import { getMessageTimeStamp } from "./RossAscends-mods.js"; import { findGroupMemberId, groups, is_group_generating, resetSelectedGroup, saveGroupChat, selected_group } from "./group-chats.js"; @@ -160,6 +161,7 @@ parser.addCommand('peek', peekCallback, [], '(message in parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); +parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card.', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); registerVariableCommands(); @@ -167,6 +169,19 @@ const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; +async function generateRawCallback(_, arg) { + if (!arg) { + console.warn('WARN: No argument provided for /genraw command'); + return; + } + + // Prevent generate recursion + $('#send_textarea').val(''); + + const result = await generateRaw(arg, ''); + return result; +} + async function generateCallback(_, arg) { if (!arg) { console.warn('WARN: No argument provided for /gen command'); From 3594c4aac701e26398d8388e125d8768a6a19dbc Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Thu, 23 Nov 2023 22:56:52 +0200 Subject: [PATCH 125/185] Add {{newline}} and {{pipe}} macros --- public/script.js | 1 + public/scripts/slash-commands.js | 7 ++++++- public/scripts/templates/macros.html | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/script.js b/public/script.js index d3e40c55e..3e3b2ebee 100644 --- a/public/script.js +++ b/public/script.js @@ -2010,6 +2010,7 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh } content = replaceVariableMacros(content); + content = content.replace(/{{newline}}/gi, "\n"); content = content.replace(/{{input}}/gi, String($('#send_textarea').val())); if (_replaceCharacterCard) { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 9bef091a1..552b57cd2 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -1014,7 +1014,12 @@ async function executeSlashCommands(text) { } console.debug('Slash command executing:', result); - const unnamedArg = result.value || pipeResult; + let unnamedArg = result.value || pipeResult; + + if (typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) { + unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult); + } + pipeResult = await result.command.callback(result.args, unnamedArg); if (result.command.interruptsGeneration) { diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index 8e33b15cd..fac3d145c 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -2,6 +2,8 @@ System-wide Replacement Macros (in order of evaluation):
    +
  • {{pipe}} – only for slash command batching. Replaced with the returned result of the previous command.
  • +
  • {{newline}} – just inserts a newline.
  • {{original}} – global prompts defined in API settings. Only valid in Advanced Definitions prompt overrides.
  • {{input}} – the user input
  • {{charPrompt}} – the Character's Main Prompt override
  • From c50ed4bf6a461d568211a2729ea406d3c2daa260 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:18:07 +0200 Subject: [PATCH 126/185] STscript improvements (see below) /abort command, {{pipe}} macro in named args, subcommand batch escaping with backslash, string literals and rules for /if, else clause for /if --- public/scripts/slash-commands.js | 35 ++++++++- public/scripts/variables.js | 124 ++++++++++++++++++++----------- 2 files changed, 112 insertions(+), 47 deletions(-) diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 552b57cd2..caf9b9794 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -163,12 +163,18 @@ parser.addCommand('echo', echoCallback, [], '(text)(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card.', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); +parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); registerVariableCommands(); const NARRATOR_NAME_KEY = 'narrator_name'; const NARRATOR_NAME_DEFAULT = 'System'; export const COMMENT_NAME_DEFAULT = 'Note'; +function abortCallback() { + $('#send_textarea').val(''); + throw new Error('/abort command executed'); +} + async function generateRawCallback(_, arg) { if (!arg) { console.warn('WARN: No argument provided for /genraw command'); @@ -250,7 +256,7 @@ async function addSwipeCallback(_, arg) { api: 'manual', model: 'slash command', } - }); + }); await saveChatConditional(); await reloadCurrentChat(); @@ -981,16 +987,29 @@ function setBackgroundCallback(_, bg) { /** * Executes slash commands in the provided text * @param {string} text Slash command text + * @param {boolean} unescape Whether to unescape the batch separator * @returns {Promise<{interrupt: boolean, newText: string, pipe: string} | boolean>} */ -async function executeSlashCommands(text) { +async function executeSlashCommands(text, unescape = false) { if (!text) { return false; } + // Unescape the pipe character + if (unescape) { + text = text.replace(/\\\|/g, '|'); + } + // Hack to allow multi-line slash commands // All slash command messages should begin with a slash - const lines = text.split('|').map(line => line.trim()); + const placeholder = '\u200B'; // Use a zero-width space as a placeholder + const chars = text.split(''); + for (let i = 1; i < chars.length; i++) { + if (chars[i] === '|' && chars[i - 1] !== '\\') { + chars[i] = placeholder; + } + } + const lines = chars.join('').split(placeholder).map(line => line.trim()); const linesToRemove = []; let interrupt = false; @@ -1016,7 +1035,15 @@ async function executeSlashCommands(text) { console.debug('Slash command executing:', result); let unnamedArg = result.value || pipeResult; - if (typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) { + if (pipeResult && typeof result.args === 'object') { + for (const [key, value] of Object.entries(result.args)) { + if (typeof value === 'string' && /{{pipe}}/i.test(value)) { + result.args[key] = value.replace(/{{pipe}}/i, pipeResult); + } + } + } + + if (pipeResult && typeof unnamedArg === 'string' && /{{pipe}}/i.test(unnamedArg)) { unnamedArg = unnamedArg.replace(/{{pipe}}/i, pipeResult); } diff --git a/public/scripts/variables.js b/public/scripts/variables.js index eb54579f4..4490668ae 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -3,6 +3,10 @@ import { extension_settings } from "./extensions.js"; import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js"; function getLocalVariable(name) { + if (!chat_metadata.variables) { + chat_metadata.variables = {}; + } + const localVariable = chat_metadata?.variables[name]; return localVariable || ''; @@ -125,67 +129,101 @@ function listVariablesCallback() { } async function ifCallback(args, command) { - const a = getLocalVariable(args.a) || getGlobalVariable(args.a) || Number(args.a); - const b = getLocalVariable(args.b) || getGlobalVariable(args.b) || Number(args.b); + // Resultion order: numeric literal, local variable, global variable, string literal + const a = isNaN(Number(args.a)) ? (getLocalVariable(args.a) || getGlobalVariable(args.a) || args.a || '') : Number(args.a); + const b = isNaN(Number(args.b)) ? (getLocalVariable(args.b) || getGlobalVariable(args.b) || args.b || '') : Number(args.b); const rule = args.rule; - if (!a || !b || !rule) { + if (!rule) { toastr.warning('Both operands and the rule must be specified for the /if command.', 'Invalid /if command'); return ''; } - const aNumber = Number(a); - const bNumber = Number(b); - - if (isNaN(aNumber) || isNaN(bNumber)) { - toastr.warning('Both operands must be numbers for the /if command.', 'Invalid /if command'); + if ((typeof a === 'number' && isNaN(a)) || (typeof a === 'string' && a === '')) { + toastr.warning('The first operand must be a number, string or a variable name for the /if command.', 'Invalid /if command'); return ''; } let result = false; - switch (rule) { - case 'gt': - result = aNumber > bNumber; - break; - case 'gte': - result = aNumber >= bNumber; - break; - case 'lt': - result = aNumber < bNumber; - break; - case 'lte': - result = aNumber <= bNumber; - break; - case 'eq': - result = aNumber === bNumber; - break; - default: - toastr.warning('Unknown rule for the /if command.', 'Invalid /if command'); - return ''; + if (typeof a === 'string') { + const aString = String(a).toLowerCase(); + const bString = String(b).toLowerCase(); + + switch (rule) { + case 'in': + result = aString.includes(bString); + break; + case 'nin': + result = !aString.includes(bString); + break; + case 'eq': + result = aString === bString; + break; + case 'neq': + result = aString !== bString; + break; + default: + toastr.warning('Unknown rule for the /if command for type string.', 'Invalid /if command'); + return ''; + } + } else if (typeof a === 'number') { + const aNumber = Number(a); + const bNumber = Number(b); + + switch (rule) { + case 'gt': + result = aNumber > bNumber; + break; + case 'gte': + result = aNumber >= bNumber; + break; + case 'lt': + result = aNumber < bNumber; + break; + case 'lte': + result = aNumber <= bNumber; + break; + case 'eq': + result = aNumber === bNumber; + break; + case 'neq': + result = aNumber !== bNumber; + break; + default: + toastr.warning('Unknown rule for the /if command for type number.', 'Invalid /if command'); + return ''; + } } if (result && command) { - if (command.startsWith('"')) { - command = command.slice(1); - } - - if (command.endsWith('"')) { - command = command.slice(0, -1); - } - - const result = await executeSlashCommands(command); - - if (!result || typeof result !== 'object') { - return ''; - } - - return result?.pipe || ''; + return await executeSubCommands(command); + } else if (!result && args.else && typeof args.else === 'string' && args.else !== '') { + return await executeSubCommands(args.else); } return ''; } +async function executeSubCommands(command) { + if (command.startsWith('"')) { + command = command.slice(1); + } + + if (command.endsWith('"')) { + command = command.slice(0, -1); + } + + const unescape = true; + const result = await executeSlashCommands(command, unescape); + + if (!result || typeof result !== 'object') { + return ''; + } + + return result?.pipe || ''; +} + export function registerVariableCommands() { registerSlashCommand('listvar', listVariablesCallback, [''], ' – list registered chat variables', true, true); registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value), [], 'key=varname (value) – set a local variable value and pass it down the pipe, e.g. /setvar key=color green', true, true); @@ -194,5 +232,5 @@ export function registerVariableCommands() { registerSlashCommand('setglobalvar', (args, value) => setGlobalVariable(args.key || args.name, value), [], 'key=varname (value) – set a global variable value and pass it down the pipe, e.g. /setglobalvar key=color green', true, true); registerSlashCommand('getglobalvar', (_, value) => getGlobalVariable(value), [], '(key) – get a global variable value and pass it down the pipe, e.g. /getglobalvar height', true, true); registerSlashCommand('addglobalvar', (args, value) => addGlobalVariable(args.key || args.name, value), [], 'key=varname (increment) – add a value to a global variable and pass the result down the pipe, e.g. /addglobalvar score 10', true, true); - registerSlashCommand('if', ifCallback, [], 'a=varname1 b=varname2 rule=comparison "(command)" – compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b e.g. /if a=score a=10 rule=gte "/speak You win" triggers a /speak command if the value of "score" is greater or equals 10.', true, true); + registerSlashCommand('if', ifCallback, [], 'a=varname1 b=varname2 rule=comparison else="(alt.command)" "(command)" – compare the value of variable "a" with the value of variable "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for "a" and "b" supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, in (strings) => a includes b, nin (strings) => a not includes b, e.g. /if a=score a=10 rule=gte "/speak You win" triggers a /speak command if the value of "score" is greater or equals 10.', true, true); } From c2e3bfa06d8e2c7f05526f41cf38c34018992b56 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:36:35 +0200 Subject: [PATCH 127/185] /genraw instruct=off --- public/script.js | 9 ++++++--- public/scripts/slash-commands.js | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/public/script.js b/public/script.js index 3e3b2ebee..4acbc3457 100644 --- a/public/script.js +++ b/public/script.js @@ -2701,14 +2701,17 @@ class StreamingProcessor { * Generates a message using the provided prompt. * @param {string} prompt Prompt to generate a message from * @param {string} api API to use. Main API is used if not specified. + * @param {string} instructOverride If 0, false or off, disables instruct formatting + * @returns {Promise} Generated message */ -export async function generateRaw(prompt, api) { +export async function generateRaw(prompt, api, instructOverride) { if (!api) { api = main_api; } const abortController = new AbortController(); - const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel'; + const instructDisabled = instructOverride === '0' || instructOverride === 'false' || instructOverride === 'off'; + const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel' && !instructDisabled; prompt = substituteParams(prompt); prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt; @@ -7564,7 +7567,7 @@ function addDebugFunctions() { registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => { const text = prompt('Input text:', 'Hello'); toastr.info('Working on it...'); - const message = await generateRaw(text, null); + const message = await generateRaw(text, null, ''); alert(message); }); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index caf9b9794..9243c6eee 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -161,7 +161,7 @@ parser.addCommand('peek', peekCallback, [], '(message in parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); -parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card.', true, true); +parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. /genraw instruct=off Why is the sky blue?', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); registerVariableCommands(); @@ -175,8 +175,8 @@ function abortCallback() { throw new Error('/abort command executed'); } -async function generateRawCallback(_, arg) { - if (!arg) { +async function generateRawCallback(args, value) { + if (!value) { console.warn('WARN: No argument provided for /genraw command'); return; } @@ -184,7 +184,7 @@ async function generateRawCallback(_, arg) { // Prevent generate recursion $('#send_textarea').val(''); - const result = await generateRaw(arg, ''); + const result = await generateRaw(value, '', args.instruct); return result; } From 863554fea6c81a528c0486cc242db9a5f09bf697 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:51:27 +0200 Subject: [PATCH 128/185] Add ephemeral stop strings to /genraw --- public/script.js | 2 + public/scripts/power-user.js | 84 ++++++++++++++++++++------------ public/scripts/slash-commands.js | 17 ++++++- 3 files changed, 71 insertions(+), 32 deletions(-) diff --git a/public/script.js b/public/script.js index 4acbc3457..c35cf5221 100644 --- a/public/script.js +++ b/public/script.js @@ -82,6 +82,7 @@ import { registerDebugFunction, ui_mode, switchSimpleMode, + flushEphemeralStoppingStrings, } from "./scripts/power-user.js"; import { @@ -3837,6 +3838,7 @@ function unblockGeneration() { activateSendButtons(); showSwipeButtons(); setGenerationProgress(0); + flushEphemeralStoppingStrings(); $("#send_textarea").removeAttr('disabled'); } diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 7daeb8431..1c29cd1fb 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -2374,45 +2374,69 @@ async function setmovingUIPreset(_, text) { saveSettingsDebounced(); } +const EPHEMERAL_STOPPING_STRINGS = []; + +/** + * Adds a stopping string to the list of stopping strings that are only used for the next generation. + * @param {string} value The stopping string to add + */ +export function addEphemeralStoppingString(value) { + if (!EPHEMERAL_STOPPING_STRINGS.includes(value)) { + console.debug('Adding ephemeral stopping string:', value); + EPHEMERAL_STOPPING_STRINGS.push(value); + } +} + +export function flushEphemeralStoppingStrings() { + console.debug('Flushing ephemeral stopping strings:', EPHEMERAL_STOPPING_STRINGS); + EPHEMERAL_STOPPING_STRINGS.length = 0; +} + /** * Gets the custom stopping strings from the power user settings. * @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings. * @returns {string[]} An array of custom stopping strings */ export function getCustomStoppingStrings(limit = undefined) { - try { - // If there's no custom stopping strings, return an empty array - if (!power_user.custom_stopping_strings) { + function getPermanent() { + try { + // If there's no custom stopping strings, return an empty array + if (!power_user.custom_stopping_strings) { + return []; + } + + // Parse the JSON string + let strings = JSON.parse(power_user.custom_stopping_strings); + + // Make sure it's an array + if (!Array.isArray(strings)) { + return []; + } + + // Make sure all the elements are strings and non-empty. + strings = strings.filter(s => typeof s === 'string' && s.length > 0); + + // Substitute params if necessary + if (power_user.custom_stopping_strings_macro) { + strings = strings.map(x => substituteParams(x)); + } + + // Apply the limit. If limit is 0, return all strings. + if (limit > 0) { + strings = strings.slice(0, limit); + } + + return strings; + } catch (error) { + // If there's an error, return an empty array + console.warn('Error parsing custom stopping strings:', error); return []; } - - // Parse the JSON string - let strings = JSON.parse(power_user.custom_stopping_strings); - - // Make sure it's an array - if (!Array.isArray(strings)) { - return []; - } - - // Make sure all the elements are strings and non-empty. - strings = strings.filter(s => typeof s === 'string' && s.length > 0); - - // Substitute params if necessary - if (power_user.custom_stopping_strings_macro) { - strings = strings.map(x => substituteParams(x)); - } - - // Apply the limit. If limit is 0, return all strings. - if (limit > 0) { - strings = strings.slice(0, limit); - } - - return strings; - } catch (error) { - // If there's an error, return an empty array - console.warn('Error parsing custom stopping strings:', error); - return []; } + + const permanent = getPermanent(); + const ephemeral = EPHEMERAL_STOPPING_STRINGS; + return [...permanent, ...ephemeral]; } $(document).ready(() => { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 9243c6eee..4709179a6 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -29,7 +29,7 @@ import { import { getMessageTimeStamp } from "./RossAscends-mods.js"; import { findGroupMemberId, groups, is_group_generating, resetSelectedGroup, saveGroupChat, 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 { addEphemeralStoppingString, chat_styles, power_user } from "./power-user.js"; import { autoSelectPersona } from "./personas.js"; import { getContext } from "./extensions.js"; import { hideChatMessage, unhideChatMessage } from "./chats.js"; @@ -161,7 +161,7 @@ parser.addCommand('peek', peekCallback, [], '(message in parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); -parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. /genraw instruct=off Why is the sky blue?', true, true); +parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. /genraw instruct=off Why is the sky blue?. Use stop=... with a JSON-serializer array to add one-time custom stop strings, e.g. /genraw stop=["\\n"] Say hi', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); registerVariableCommands(); @@ -184,6 +184,19 @@ async function generateRawCallback(args, value) { // Prevent generate recursion $('#send_textarea').val(''); + if (typeof args.stop === 'string' && args.stop.length) { + try { + const stopStrings = JSON.parse(args.stop); + if (Array.isArray(stopStrings)) { + for (const stopString of stopStrings) { + addEphemeralStoppingString(stopString); + } + } + } catch { + // Do nothing + } + } + const result = await generateRaw(value, '', args.instruct); return result; } From c4e1fff1bcafe3a43e994e39f6b36875a3248778 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Nov 2023 00:54:23 +0200 Subject: [PATCH 129/185] Respect # limit for ephemeral stop strings --- public/scripts/power-user.js | 14 ++++++++------ public/scripts/slash-commands.js | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 1c29cd1fb..8b522da92 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -2421,11 +2421,6 @@ export function getCustomStoppingStrings(limit = undefined) { strings = strings.map(x => substituteParams(x)); } - // Apply the limit. If limit is 0, return all strings. - if (limit > 0) { - strings = strings.slice(0, limit); - } - return strings; } catch (error) { // If there's an error, return an empty array @@ -2436,7 +2431,14 @@ export function getCustomStoppingStrings(limit = undefined) { const permanent = getPermanent(); const ephemeral = EPHEMERAL_STOPPING_STRINGS; - return [...permanent, ...ephemeral]; + const strings = [...permanent, ...ephemeral]; + + // Apply the limit. If limit is 0, return all strings. + if (limit > 0) { + return strings.slice(0, limit); + } + + return strings; } $(document).ready(() => { diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 4709179a6..f11ce59ef 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -161,7 +161,7 @@ parser.addCommand('peek', peekCallback, [], '(message in parser.addCommand('delswipe', deleteSwipeCallback, ['swipedel'], '(optional 1-based id) – deletes a swipe from the last chat message. If swipe id not provided - deletes the current swipe.', true, true); parser.addCommand('echo', echoCallback, [], '(text) – echoes the text to toast message. Useful for pipes debugging.', true, true); parser.addCommand('gen', generateCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe.', true, true); -parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. /genraw instruct=off Why is the sky blue?. Use stop=... with a JSON-serializer array to add one-time custom stop strings, e.g. /genraw stop=["\\n"] Say hi', true, true); +parser.addCommand('genraw', generateRawCallback, [], '(prompt) – generates text using the provided prompt and passes it to the next command through the pipe. Does not include chat history or character card. Use instruct=off to skip instruct formatting, e.g. /genraw instruct=off Why is the sky blue?. Use stop=... with a JSON-serialized array to add one-time custom stop strings, e.g. /genraw stop=["\\n"] Say hi', true, true); parser.addCommand('addswipe', addSwipeCallback, ['swipeadd'], '(text) – adds a swipe to the last chat message.', true, true); parser.addCommand('abort', abortCallback, [], ' – aborts the slash command batch execution', true, true); registerVariableCommands(); From e593dd4dbd2e2db413cfcbdb138ac915893ce338 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Nov 2023 01:32:02 +0200 Subject: [PATCH 130/185] Auto-executable QR --- .../quick-reply/contextMenuEditor.html | 35 +++-- .../scripts/extensions/quick-reply/index.js | 72 +++++++--- .../extensions/quick-reply/src/ContextMenu.js | 106 +++++++-------- .../extensions/quick-reply/src/MenuHeader.js | 31 ++--- .../extensions/quick-reply/src/MenuItem.js | 128 +++++++++--------- .../extensions/quick-reply/src/SubMenu.js | 104 +++++++------- public/scripts/variables.js | 6 +- 7 files changed, 265 insertions(+), 217 deletions(-) diff --git a/public/scripts/extensions/quick-reply/contextMenuEditor.html b/public/scripts/extensions/quick-reply/contextMenuEditor.html index 00e77c0ac..c915b9aa2 100644 --- a/public/scripts/extensions/quick-reply/contextMenuEditor.html +++ b/public/scripts/extensions/quick-reply/contextMenuEditor.html @@ -3,19 +3,30 @@

    Quick Reply Context Menu Editor

    +
    + + + + +
    +
    - -
    + + +

    Auto-Execute

    +
    + + +
    diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 60a14a071..551ba8b73 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -1,4 +1,4 @@ -import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams } from "../../../script.js"; +import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types } from "../../../script.js"; import { getContext, extension_settings } from "../../extensions.js"; import { initScrollHeight, resetScrollHeight, getSortableDelay } from "../../utils.js"; import { executeSlashCommands, registerSlashCommand } from "../../slash-commands.js"; @@ -113,7 +113,7 @@ async function onQuickReplyContextMenuChange(id) { async function onQuickReplyCtxButtonClick(id) { const editorHtml = $(await $.get('scripts/extensions/quick-reply/contextMenuEditor.html')); - const popupResult = callPopup(editorHtml, "confirm", undefined, { okButton: "Save", wide:false, large:false, rows: 1 }); + const popupResult = callPopup(editorHtml, "confirm", undefined, { okButton: "Save", wide: false, large: false, rows: 1 }); const qr = extension_settings.quickReply.quickReplySlots[id - 1]; if (!qr.contextMenu) { qr.contextMenu = []; @@ -121,7 +121,7 @@ async function onQuickReplyCtxButtonClick(id) { /**@type {HTMLTemplateElement}*/ const tpl = document.querySelector('#quickReply_contextMenuEditor_itemTemplate'); const fillPresetSelect = (select, item) => { - [{name:'Select a preset', value:''}, ...presets].forEach(preset=>{ + [{ name: 'Select a preset', value: '' }, ...presets].forEach(preset => { const opt = document.createElement('option'); { opt.value = preset.value ?? preset.name; opt.textContent = preset.name; @@ -137,28 +137,43 @@ async function onQuickReplyCtxButtonClick(id) { const select = ctxItem.querySelector('.quickReply_contextMenuEditor_preset'); fillPresetSelect(select, item); dom.querySelector('.quickReply_contextMenuEditor_chaining').checked = item.chain; - $('.quickReply_contextMenuEditor_remove', ctxItem).on('click', ()=>ctxItem.remove()); + $('.quickReply_contextMenuEditor_remove', ctxItem).on('click', () => ctxItem.remove()); document.querySelector('#quickReply_contextMenuEditor_content').append(ctxItem); } - [...qr.contextMenu, {}].forEach((item,idx)=>{ + [...qr.contextMenu, {}].forEach((item, idx) => { addCtxItem(item, idx) }); - $('#quickReply_contextMenuEditor_addPreset').on('click', ()=>{ + $('#quickReply_contextMenuEditor_addPreset').on('click', () => { addCtxItem({}, document.querySelector('#quickReply_contextMenuEditor_content').children.length); }); $('#quickReply_contextMenuEditor_content').sortable({ delay: getSortableDelay(), - stop: ()=>{}, + stop: () => { }, + }); + + $('#quickReply_autoExecute_userMessage').prop('checked', qr.autoExecute_userMessage ?? false); + $('#quickReply_autoExecute_botMessage').prop('checked', qr.autoExecute_botMessage ?? false); + + $('#quickReply_autoExecute_userMessage').on('input', () => { + const state = !!$('#quickReply_autoExecute_userMessage').prop('checked'); + qr.autoExecute_userMessage = state; + saveSettingsDebounced(); + }); + + $('#quickReply_autoExecute_botMessage').on('input', () => { + const state = !!$('#quickReply_autoExecute_botMessage').prop('checked'); + qr.autoExecute_botMessage = state; + saveSettingsDebounced(); }); if (await popupResult) { qr.contextMenu = Array.from(document.querySelectorAll('#quickReply_contextMenuEditor_content > .quickReplyContextMenuEditor_item')) - .map(item=>({ + .map(item => ({ preset: item.querySelector('.quickReply_contextMenuEditor_preset').value, chain: item.querySelector('.quickReply_contextMenuEditor_chaining').checked, })) - .filter(item=>item.preset); + .filter(item => item.preset); $(`#quickReplyContainer[data-order="${id}"]`).attr('data-contextMenu', JSON.stringify(qr.contextMenu)); updateQuickReplyPreset(); onQuickReplyLabelInput(id); @@ -234,30 +249,30 @@ async function performQuickReply(prompt, index) { } -function buildContextMenu(qr, chainMes=null, hierarchy=[], labelHierarchy=[]) { +function buildContextMenu(qr, chainMes = null, hierarchy = [], labelHierarchy = []) { const tree = { label: qr.label, - mes: (chainMes&&qr.mes ? `${chainMes} | ` : '') + qr.mes, + mes: (chainMes && qr.mes ? `${chainMes} | ` : '') + qr.mes, children: [], }; - qr.contextMenu?.forEach(ctxItem=>{ + qr.contextMenu?.forEach(ctxItem => { let chain = ctxItem.chain; let subName = ctxItem.preset; - const sub = presets.find(it=>it.name == subName); + const sub = presets.find(it => it.name == subName); if (sub) { // prevent circular references if (hierarchy.indexOf(sub.name) == -1) { const nextHierarchy = [...hierarchy, sub.name]; const nextLabelHierarchy = [...labelHierarchy, tree.label]; tree.children.push(new MenuHeader(sub.name)); - sub.quickReplySlots.forEach(subQr=>{ - const subInfo = buildContextMenu(subQr, chain?tree.mes:null, nextHierarchy, nextLabelHierarchy); + sub.quickReplySlots.forEach(subQr => { + const subInfo = buildContextMenu(subQr, chain ? tree.mes : null, nextHierarchy, nextLabelHierarchy); tree.children.push(new MenuItem( subInfo.label, subInfo.mes, - (evt)=>{ + (evt) => { evt.stopPropagation(); - performQuickReply(subInfo.mes.replace(/%%parent(-\d+)?%%/g, (_, index)=>{ + performQuickReply(subInfo.mes.replace(/%%parent(-\d+)?%%/g, (_, index) => { return nextLabelHierarchy.slice(parseInt(index ?? '-1'))[0]; })); }, @@ -476,7 +491,7 @@ function generateQuickReplyElements() { $(`#quickReply${i}Mes`).on('input', function () { onQuickReplyInput(i); }); $(`#quickReply${i}Label`).on('input', function () { onQuickReplyLabelInput(i); }); $(`#quickReply${i}CtxButton`).on('click', function () { onQuickReplyCtxButtonClick(i); }); - $(`#quickReplyContainer > [data-order="${i}"]`).attr('data-contextMenu', JSON.stringify(extension_settings.quickReply.quickReplySlots[i-1]?.contextMenu??[])); + $(`#quickReplyContainer > [data-order="${i}"]`).attr('data-contextMenu', JSON.stringify(extension_settings.quickReply.quickReplySlots[i - 1]?.contextMenu ?? [])); } $('.quickReplySettings .inline-drawer-toggle').off('click').on('click', function () { @@ -545,6 +560,24 @@ function saveQROrder() { }); } +async function onMessageReceived() { + for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { + const qr = extension_settings.quickReply.quickReplySlots[i]; + if (qr?.autoExecute_botMessage) { + await sendQuickReply(i); + } + } +} + +async function onMessageSent() { + for (let i = 0; i < extension_settings.quickReply.numberOfSlots; i++) { + const qr = extension_settings.quickReply.quickReplySlots[i]; + if (qr?.autoExecute_userMessage) { + await sendQuickReply(i); + } + } +} + jQuery(async () => { moduleWorker(); setInterval(moduleWorker, UPDATE_INTERVAL); @@ -625,6 +658,9 @@ jQuery(async () => { await loadSettings('init'); addQuickReplyBar(); + + eventSource.on(event_types.MESSAGE_RECEIVED, onMessageReceived); + eventSource.on(event_types.MESSAGE_SENT, onMessageSent); }); jQuery(() => { diff --git a/public/scripts/extensions/quick-reply/src/ContextMenu.js b/public/scripts/extensions/quick-reply/src/ContextMenu.js index 37ba3d5af..93360157a 100644 --- a/public/scripts/extensions/quick-reply/src/ContextMenu.js +++ b/public/scripts/extensions/quick-reply/src/ContextMenu.js @@ -1,65 +1,65 @@ import { MenuItem } from "./MenuItem.js"; export class ContextMenu { - /**@type {MenuItem[]}*/ itemList = []; - /**@type {Boolean}*/ isActive = false; - - /**@type {HTMLElement}*/ root; - /**@type {HTMLElement}*/ menu; + /**@type {MenuItem[]}*/ itemList = []; + /**@type {Boolean}*/ isActive = false; + + /**@type {HTMLElement}*/ root; + /**@type {HTMLElement}*/ menu; - constructor(/**@type {MenuItem[]}*/items) { - this.itemList = items; - items.forEach(item=>{ - item.onExpand = ()=>{ - items.filter(it=>it!=item) - .forEach(it=>it.collapse()); - }; - }); - } + constructor(/**@type {MenuItem[]}*/items) { + this.itemList = items; + items.forEach(item => { + item.onExpand = () => { + items.filter(it => it != item) + .forEach(it => it.collapse()); + }; + }); + } - render() { - if (!this.root) { - const blocker = document.createElement('div'); { - this.root = blocker; - blocker.classList.add('ctx-blocker'); - blocker.addEventListener('click', ()=>this.hide()); - const menu = document.createElement('ul'); { - this.menu = menu; - menu.classList.add('list-group'); - menu.classList.add('ctx-menu'); - this.itemList.forEach(it=>menu.append(it.render())); - blocker.append(menu); - } - } - } - return this.root; - } + render() { + if (!this.root) { + const blocker = document.createElement('div'); { + this.root = blocker; + blocker.classList.add('ctx-blocker'); + blocker.addEventListener('click', () => this.hide()); + const menu = document.createElement('ul'); { + this.menu = menu; + menu.classList.add('list-group'); + menu.classList.add('ctx-menu'); + this.itemList.forEach(it => menu.append(it.render())); + blocker.append(menu); + } + } + } + return this.root; + } - show({clientX, clientY}) { - if (this.isActive) return; - this.isActive = true; - this.render(); - this.menu.style.bottom = `${window.innerHeight - clientY}px`; - this.menu.style.left = `${clientX}px`; - document.body.append(this.root); - } - hide() { - if (this.root) { - this.root.remove(); - } - this.isActive = false; - } - toggle(/**@type {PointerEvent}*/evt) { - if (this.isActive) { - this.hide(); - } else { - this.show(evt); - } - } -} \ No newline at end of file + show({ clientX, clientY }) { + if (this.isActive) return; + this.isActive = true; + this.render(); + this.menu.style.bottom = `${window.innerHeight - clientY}px`; + this.menu.style.left = `${clientX}px`; + document.body.append(this.root); + } + hide() { + if (this.root) { + this.root.remove(); + } + this.isActive = false; + } + toggle(/**@type {PointerEvent}*/evt) { + if (this.isActive) { + this.hide(); + } else { + this.show(evt); + } + } +} diff --git a/public/scripts/extensions/quick-reply/src/MenuHeader.js b/public/scripts/extensions/quick-reply/src/MenuHeader.js index 65c35af86..f1531c057 100644 --- a/public/scripts/extensions/quick-reply/src/MenuHeader.js +++ b/public/scripts/extensions/quick-reply/src/MenuHeader.js @@ -1,21 +1,20 @@ import { MenuItem } from "./MenuItem.js"; -import { SubMenu } from "./SubMenu.js"; export class MenuHeader extends MenuItem { - constructor(/**@type {String}*/label) { - super(label, null, null); - } + constructor(/**@type {String}*/label) { + super(label, null, null); + } - render() { - if (!this.root) { - const item = document.createElement('li'); { - this.root = item; - item.classList.add('list-group-item'); - item.classList.add('ctx-header'); - item.append(this.label); - } - } - return this.root; - } -} \ No newline at end of file + render() { + if (!this.root) { + const item = document.createElement('li'); { + this.root = item; + item.classList.add('list-group-item'); + item.classList.add('ctx-header'); + item.append(this.label); + } + } + return this.root; + } +} diff --git a/public/scripts/extensions/quick-reply/src/MenuItem.js b/public/scripts/extensions/quick-reply/src/MenuItem.js index cbce352ff..902771873 100644 --- a/public/scripts/extensions/quick-reply/src/MenuItem.js +++ b/public/scripts/extensions/quick-reply/src/MenuItem.js @@ -1,76 +1,76 @@ import { SubMenu } from "./SubMenu.js"; export class MenuItem { - /**@type {String}*/ label; - /**@type {Object}*/ value; - /**@type {Function}*/ callback; - /**@type {MenuItem[]}*/ childList = []; - /**@type {SubMenu}*/ subMenu; - /**@type {Boolean}*/ isForceExpanded = false; + /**@type {String}*/ label; + /**@type {Object}*/ value; + /**@type {Function}*/ callback; + /**@type {MenuItem[]}*/ childList = []; + /**@type {SubMenu}*/ subMenu; + /**@type {Boolean}*/ isForceExpanded = false; - /**@type {HTMLElement}*/ root; - - /**@type {Function}*/ onExpand; + /**@type {HTMLElement}*/ root; + + /**@type {Function}*/ onExpand; - constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children=[]) { - this.label = label; - this.value = value; - this.callback = callback; - this.childList = children; - } + constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) { + this.label = label; + this.value = value; + this.callback = callback; + this.childList = children; + } - render() { - if (!this.root) { - const item = document.createElement('li'); { - this.root = item; - item.classList.add('list-group-item'); - item.classList.add('ctx-item'); - item.title = this.value; - if (this.callback) { - item.addEventListener('click', (evt)=>this.callback(evt, this)); - } - item.append(this.label); - if (this.childList.length > 0) { - item.classList.add('ctx-has-children'); - const sub = new SubMenu(this.childList); - this.subMenu = sub; - const trigger = document.createElement('div'); { - trigger.classList.add('ctx-expander'); - trigger.textContent = '⋮'; - trigger.addEventListener('click', (evt)=>{ - evt.stopPropagation(); - this.toggle(); - }); - item.append(trigger); - } - item.addEventListener('mouseover', ()=>sub.show(item)); - item.addEventListener('mouseleave', ()=>sub.hide()); - - } - } - } - return this.root; - } + render() { + if (!this.root) { + const item = document.createElement('li'); { + this.root = item; + item.classList.add('list-group-item'); + item.classList.add('ctx-item'); + item.title = this.value; + if (this.callback) { + item.addEventListener('click', (evt) => this.callback(evt, this)); + } + item.append(this.label); + if (this.childList.length > 0) { + item.classList.add('ctx-has-children'); + const sub = new SubMenu(this.childList); + this.subMenu = sub; + const trigger = document.createElement('div'); { + trigger.classList.add('ctx-expander'); + trigger.textContent = '⋮'; + trigger.addEventListener('click', (evt) => { + evt.stopPropagation(); + this.toggle(); + }); + item.append(trigger); + } + item.addEventListener('mouseover', () => sub.show(item)); + item.addEventListener('mouseleave', () => sub.hide()); + + } + } + } + return this.root; + } - expand() { - this.subMenu?.show(this.root); - if (this.onExpand) { - this.onExpand(); - } - } - collapse() { - this.subMenu?.hide(); - } - toggle() { - if (this.subMenu.isActive) { - this.expand(); - } else { - this.collapse(); - } - } -} \ No newline at end of file + expand() { + this.subMenu?.show(this.root); + if (this.onExpand) { + this.onExpand(); + } + } + collapse() { + this.subMenu?.hide(); + } + toggle() { + if (this.subMenu.isActive) { + this.expand(); + } else { + this.collapse(); + } + } +} diff --git a/public/scripts/extensions/quick-reply/src/SubMenu.js b/public/scripts/extensions/quick-reply/src/SubMenu.js index 328e5a1ee..bc5e293ce 100644 --- a/public/scripts/extensions/quick-reply/src/SubMenu.js +++ b/public/scripts/extensions/quick-reply/src/SubMenu.js @@ -1,64 +1,64 @@ import { MenuItem } from "./MenuItem.js"; export class SubMenu { - /**@type {MenuItem[]}*/ itemList = []; - /**@type {Boolean}*/ isActive = false; - - /**@type {HTMLElement}*/ root; + /**@type {MenuItem[]}*/ itemList = []; + /**@type {Boolean}*/ isActive = false; + + /**@type {HTMLElement}*/ root; - constructor(/**@type {MenuItem[]}*/items) { - this.itemList = items; - } + constructor(/**@type {MenuItem[]}*/items) { + this.itemList = items; + } - render() { - if (!this.root) { - const menu = document.createElement('ul'); { - this.root = menu; - menu.classList.add('list-group'); - menu.classList.add('ctx-menu'); - menu.classList.add('ctx-sub-menu'); - this.itemList.forEach(it=>menu.append(it.render())); - } - } - return this.root; - } + render() { + if (!this.root) { + const menu = document.createElement('ul'); { + this.root = menu; + menu.classList.add('list-group'); + menu.classList.add('ctx-menu'); + menu.classList.add('ctx-sub-menu'); + this.itemList.forEach(it => menu.append(it.render())); + } + } + return this.root; + } - show(/**@type {HTMLElement}*/parent) { - if (this.isActive) return; - this.isActive = true; - this.render(); - parent.append(this.root); - requestAnimationFrame(()=>{ - const rect = this.root.getBoundingClientRect(); - console.log(window.innerHeight, rect); - if (rect.bottom > window.innerHeight - 5) { - this.root.style.top = `${window.innerHeight - 5 - rect.bottom}px`; - } - if (rect.right > window.innerWidth - 5) { - this.root.style.left = 'unset'; - this.root.style.right = '100%'; - } - }); - } - hide() { - if (this.root) { - this.root.remove(); - this.root.style.top = ''; - this.root.style.left = ''; - } - this.isActive = false; - } - toggle(/**@type {HTMLElement}*/parent) { - if (this.isActive) { - this.hide(); - } else { - this.show(parent); - } - } -} \ No newline at end of file + show(/**@type {HTMLElement}*/parent) { + if (this.isActive) return; + this.isActive = true; + this.render(); + parent.append(this.root); + requestAnimationFrame(() => { + const rect = this.root.getBoundingClientRect(); + console.log(window.innerHeight, rect); + if (rect.bottom > window.innerHeight - 5) { + this.root.style.top = `${window.innerHeight - 5 - rect.bottom}px`; + } + if (rect.right > window.innerWidth - 5) { + this.root.style.left = 'unset'; + this.root.style.right = '100%'; + } + }); + } + hide() { + if (this.root) { + this.root.remove(); + this.root.style.top = ''; + this.root.style.left = ''; + } + this.isActive = false; + } + toggle(/**@type {HTMLElement}*/parent) { + if (this.isActive) { + this.hide(); + } else { + this.show(parent); + } + } +} diff --git a/public/scripts/variables.js b/public/scripts/variables.js index 4490668ae..27d2eea76 100644 --- a/public/scripts/variables.js +++ b/public/scripts/variables.js @@ -1,5 +1,5 @@ -import { chat_metadata, getCurrentChatId, sendSystemMessage, system_message_types } from "../script.js"; -import { extension_settings } from "./extensions.js"; +import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from "../script.js"; +import { extension_settings, saveMetadataDebounced } from "./extensions.js"; import { executeSlashCommands, registerSlashCommand } from "./slash-commands.js"; function getLocalVariable(name) { @@ -18,6 +18,7 @@ function setLocalVariable(name, value) { } chat_metadata.variables[name] = value; + saveMetadataDebounced(); return value; } @@ -29,6 +30,7 @@ function getGlobalVariable(name) { function setGlobalVariable(name, value) { extension_settings.variables.global[name] = value; + saveSettingsDebounced(); } function addLocalVariable(name, value) { From 461e8d79292518f275041bf046404ce5428595a9 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 24 Nov 2023 01:39:39 +0200 Subject: [PATCH 131/185] Update the tooltip on the vertical ellipsis button --- public/scripts/extensions/quick-reply/contextMenuEditor.html | 2 +- public/scripts/extensions/quick-reply/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/scripts/extensions/quick-reply/contextMenuEditor.html b/public/scripts/extensions/quick-reply/contextMenuEditor.html index c915b9aa2..ea19e3d06 100644 --- a/public/scripts/extensions/quick-reply/contextMenuEditor.html +++ b/public/scripts/extensions/quick-reply/contextMenuEditor.html @@ -1,6 +1,6 @@
    -

    Quick Reply Context Menu Editor

    +

    Context Menu Editor