-
- If both Instruct Mode and this are enabled, the prompt will be formatted by SillyTavern using the current
- advanced formatting settings (except instruct System Prompt). If disabled, the prompt will be formatted by OpenRouter.
-
-
-
+
+
+ To use instruct formatting, switch to OpenRouter under Text Completion API.
+
- If the rule is not provided, it defaults to eq.
+ If the rule is not provided, it defaults to eq.
+
+
If no right operand is provided, it defaults to checking the left value to be truthy.
- A non-empty string or non-zero number is considered truthy, as is the value true.
+ A non-empty string or non-zero number is considered truthy, as is the value true or on.
+ Only acceptable rules for no provided right operand are not, and no provided rule - which default to returning whether it is not or is truthy.
Available rules:
-
gt => a > b
-
gte => a >= b
-
lt => a < b
-
lte => a <= b
-
eq => a == b
-
neq => a != b
-
not => !a
-
in (strings) => a includes b
-
nin (strings) => a not includes b
+
eq => a == b (strings & numbers)
+
neq => a !== b (strings & numbers)
+
in => a includes b (strings & numbers as strings)
+
nin => a not includes b (strings & numbers as strings)
+
gt => a > b (numbers)
+
gte => a >= b (numbers)
+
lt => a < b (numbers)
+
lte => a <= b (numbers)
+
not => !a (truthy)
@@ -1328,9 +1336,13 @@ export function registerVariableCommands() {
executes a subcommand defined as a closure if the given value contains a specified word.
-
/if left=myContent {: /echo "My content had some content." :}
+
/if left=myContent {: /echo My content had some content. :}
executes the defined subcommand, if the provided value of left is truthy (contains some kind of contant that is not empty or false)
+
+
/if left=tree right={{getvar::object}} {: /echo The object is a tree! :}
+ executes the defined subcommand, if the left and right values are equals.
+
`,
@@ -1359,15 +1371,15 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'eq',
enumList: [
- new SlashCommandEnumValue('gt', 'a > b'),
- new SlashCommandEnumValue('gte', 'a >= b'),
- new SlashCommandEnumValue('lt', 'a < b'),
- new SlashCommandEnumValue('lte', 'a <= b'),
- new SlashCommandEnumValue('eq', 'a == b'),
- new SlashCommandEnumValue('neq', 'a !== b'),
- new SlashCommandEnumValue('not', '!a'),
- new SlashCommandEnumValue('in', 'a includes b'),
- new SlashCommandEnumValue('nin', 'a not includes b'),
+ new SlashCommandEnumValue('eq', 'a == b (strings & numbers)'),
+ new SlashCommandEnumValue('neq', 'a !== b (strings & numbers)'),
+ new SlashCommandEnumValue('in', 'a includes b (strings & numbers as strings)'),
+ new SlashCommandEnumValue('nin', 'a not includes b (strings & numbers as strings)'),
+ new SlashCommandEnumValue('gt', 'a > b (numbers)'),
+ new SlashCommandEnumValue('gte', 'a >= b (numbers)'),
+ new SlashCommandEnumValue('lt', 'a < b (numbers)'),
+ new SlashCommandEnumValue('lte', 'a <= b (numbers)'),
+ new SlashCommandEnumValue('not', '!a (truthy)'),
],
forceEnum: true,
}),
@@ -1396,15 +1408,15 @@ export function registerVariableCommands() {
Available rules:
-
gt => a > b
-
gte => a >= b
-
lt => a < b
-
lte => a <= b
-
eq => a == b
-
neq => a != b
-
not => !a
-
in (strings) => a includes b
-
nin (strings) => a not includes b
+
eq => a == b (strings & numbers)
+
neq => a !== b (strings & numbers)
+
in => a includes b (strings & numbers as strings)
+
nin => a not includes b (strings & numbers as strings)
+
gt => a > b (numbers)
+
gte => a >= b (numbers)
+
lt => a < b (numbers)
+
lte => a <= b (numbers)
+
not => !a (truthy)
From 23286d186bc40355b4cebb74935ca8f5fbd7bc73 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 20:44:28 +0200
Subject: [PATCH 030/268] Fix lint issues
---
public/scripts/variables.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index 9edc0940b..443bbbd30 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -523,11 +523,12 @@ export function evalBoolean(rule, a, b) {
if (b === undefined) {
switch (rule) {
case undefined:
- case 'not':
+ case 'not': {
const resultOnTruthy = rule !== 'not';
if (isTrueBoolean(String(a))) return resultOnTruthy;
if (isFalseBoolean(String(a))) return !resultOnTruthy;
- return !!a ? resultOnTruthy : !resultOnTruthy;
+ return a ? resultOnTruthy : !resultOnTruthy;
+ }
default:
throw new Error(`Unknown boolean comparison rule for truthy check. If right operand is not provided, the rule must not provided or be 'not'. Provided: ${rule}`);
}
From 8c87a24e5da71543202272478747080d28a16b24 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 20:51:56 +0200
Subject: [PATCH 031/268] Throw when left operand not provided
---
public/scripts/variables.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index 81e6afc8c..e9cd8c653 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -544,6 +544,10 @@ export function parseBooleanOperands(args) {
* @returns {boolean} True if the rule yields true, false otherwise
*/
export function evalBoolean(rule, a, b) {
+ if (a === undefined) {
+ throw new Error('Left operand is not provided');
+ }
+
// If right-hand side was not provided, whe just check if the left side is truthy
if (b === undefined) {
switch (rule) {
From ff989b3352b085f04063ed8e5a267e3bc2599e0e Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 21:58:46 +0200
Subject: [PATCH 032/268] Move extensions init to function
---
public/script.js | 3 ++-
public/scripts/extensions.js | 4 ++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/public/script.js b/public/script.js
index 8bfbe5a64..cb750e915 100644
--- a/public/script.js
+++ b/public/script.js
@@ -158,7 +158,7 @@ import {
} from './scripts/utils.js';
import { debounce_timeout } from './scripts/constants.js';
-import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
+import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, initExtensions, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
import {
tag_map,
@@ -956,6 +956,7 @@ async function firstLoadInit() {
initCfg();
initLogprobs();
initInputMarkdown();
+ initExtensions();
doDailyExtensionUpdatesCheck();
await hideLoader();
await fixViewport();
diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js
index 1d19e82db..182d6ed64 100644
--- a/public/scripts/extensions.js
+++ b/public/scripts/extensions.js
@@ -1041,7 +1041,7 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
await installExtension(url);
}
-jQuery(async function () {
+export async function initExtensions() {
await addExtensionsButtonAndMenu();
$('#extensionsMenuButton').css('display', 'flex');
@@ -1060,4 +1060,4 @@ jQuery(async function () {
* @listens #third_party_extension_button#click - The click event of the '#third_party_extension_button' element.
*/
$('#third_party_extension_button').on('click', () => openThirdPartyExtensionMenu());
-});
+}
From 8344232fe59b2595e0a089bc3ee2550b48178ad2 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 25 Sep 2024 23:14:28 +0300
Subject: [PATCH 033/268] Add common punctuation to Erato stop strings that
start with a newline #2894
---
public/scripts/nai-settings.js | 27 +++++++++++++++++++++++----
1 file changed, 23 insertions(+), 4 deletions(-)
diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js
index e24353499..f853957ee 100644
--- a/public/scripts/nai-settings.js
+++ b/public/scripts/nai-settings.js
@@ -492,11 +492,32 @@ function getBadWordPermutations(text) {
export function getNovelGenerationData(finalPrompt, settings, maxLength, isImpersonate, isContinue, _cfgValues, type) {
console.debug('NovelAI generation data for', type);
+ const isKayra = nai_settings.model_novel.includes('kayra');
+ const isErato = nai_settings.model_novel.includes('erato');
const tokenizerType = getTokenizerTypeForModel(nai_settings.model_novel);
+ const stoppingStrings = getStoppingStrings(isImpersonate, isContinue);
+
+ // Llama 3 tokenizer, huh?
+ if (isErato) {
+ const additionalStopStrings = [];
+ for (const stoppingString of stoppingStrings) {
+ if (stoppingString.startsWith('\n')) {
+ additionalStopStrings.push('.' + stoppingString);
+ additionalStopStrings.push('!' + stoppingString);
+ additionalStopStrings.push('?' + stoppingString);
+ additionalStopStrings.push('*' + stoppingString);
+ additionalStopStrings.push('"' + stoppingString);
+ additionalStopStrings.push('_' + stoppingString);
+ additionalStopStrings.push('...' + stoppingString);
+ additionalStopStrings.push(')' + stoppingString);
+ }
+ }
+ stoppingStrings.push(...additionalStopStrings);
+ }
+
const stopSequences = (tokenizerType !== tokenizers.NONE)
- ? getStoppingStrings(isImpersonate, isContinue)
- .map(t => getTextTokens(tokenizerType, t))
+ ? stoppingStrings.map(t => getTextTokens(tokenizerType, t))
: undefined;
const badWordIds = (tokenizerType !== tokenizers.NONE)
@@ -515,8 +536,6 @@ export function getNovelGenerationData(finalPrompt, settings, maxLength, isImper
console.log(finalPrompt);
}
- const isKayra = nai_settings.model_novel.includes('kayra');
- const isErato = nai_settings.model_novel.includes('erato');
if (isErato) {
finalPrompt = '<|startoftext|>' + finalPrompt;
From eda7493a33785e949b7e04b40512515269a8defd Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 22:46:19 +0200
Subject: [PATCH 034/268] Add extension enable/disable commands
- /extension-enable
- /extension-disable
- Optional "reload" parameter
- /reload-page
---
public/scripts/extensions.js | 140 ++++++++++++++++++++++++++++++++++-
1 file changed, 138 insertions(+), 2 deletions(-)
diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js
index 182d6ed64..56a176497 100644
--- a/public/scripts/extensions.js
+++ b/public/scripts/extensions.js
@@ -1,8 +1,14 @@
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
-import { hideLoader, showLoader } from './loader.js';
+import { showLoader } from './loader.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
+import { SlashCommand } from './slash-commands/SlashCommand.js';
+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
+import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
+import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
+import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
-import { isSubsetOf, setValueByPath } from './utils.js';
+import { equalsIgnoreCaseAndAccents, isSubsetOf, isTrueBoolean, setValueByPath } from './utils.js';
export {
getContext,
getApiUrl,
@@ -14,7 +20,9 @@ export {
ModuleWorkerWrapper,
};
+/** @type {string[]} */
export let extensionNames = [];
+
let manifests = {};
const defaultUrl = 'http://localhost:5100';
@@ -1041,7 +1049,135 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
await installExtension(url);
}
+/**
+ * @param {boolean} enable - Whether to enable or disable the extension
+ * @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise}
+ */
+function getExtensionToggleCallback(enable) {
+ return async (args, extensionName) => {
+ if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
+ if (typeof extensionName !== 'string') throw new Error('Extension name does only support string. Closures or arrays are not allowed.');
+ if (!extensionName) {
+ toastr.warning(`Extension name must be provided as an argument to ${enable ? 'enable' : 'disable'} this extension.`);
+ return '';
+ }
+
+ const reload = isTrueBoolean(args?.reload);
+
+ const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
+ if (!internalExtensionName) {
+ toastr.warning(`Extension ${extensionName} does not exist.`);
+ return '';
+ }
+ if (enable === !extension_settings.disabledExtensions.includes(internalExtensionName)) {
+ toastr.info(`Extension ${extensionName} is already ${enable ? 'enabled' : 'disabled'}.`);
+ return internalExtensionName;
+ }
+
+ reload && toastr.info(`${enable ? 'Enabling' : 'Disabling'} extension ${extensionName} and reloading...`);
+ await enableExtension(internalExtensionName, reload);
+ toastr.success(`Extension ${extensionName} ${enable ? 'enabled' : 'disabled'}.`);
+
+ return internalExtensionName;
+ };
+}
+
+function registerExtensionSlashCommands() {
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-enable',
+ callback: getExtensionToggleCallback(true),
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'reload',
+ description: 'Whether to reload the page after enabling the extension',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Enables a specified extension.
+
+
+ By default, the page will be reloaded automatically, stopping any further commands.
+ If reload=false named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
+ The page either needs to be refreshed, or /reload-page has to be called.
+
+ By default, the page will be reloaded automatically, stopping any further commands.
+ If reload=false named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
+ The page either needs to be refreshed, or /reload-page has to be called.
+
+
+ Example:
+
+
+
/extension-disable Summarize
+
+
+
+ `,
+ }));
+
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'reload-page',
+ callback: async () => {
+ toastr.info('Reloading the page...');
+ location.reload();
+ return '';
+ },
+ helpString: 'Reloads the current page. All further commands will not be processed.',
+ }));
+}
+
export async function initExtensions() {
+ registerExtensionSlashCommands();
+
await addExtensionsButtonAndMenu();
$('#extensionsMenuButton').css('display', 'flex');
From a6445aee1b1399d0f0de27b0d0cff1037e236cdb Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 23:05:34 +0200
Subject: [PATCH 035/268] Add /extension-toggle
---
public/scripts/extensions.js | 99 +++++++++++++++++++++++++++++++-----
1 file changed, 87 insertions(+), 12 deletions(-)
diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js
index 56a176497..c99255713 100644
--- a/public/scripts/extensions.js
+++ b/public/scripts/extensions.js
@@ -8,7 +8,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
-import { equalsIgnoreCaseAndAccents, isSubsetOf, isTrueBoolean, setValueByPath } from './utils.js';
+import { equalsIgnoreCaseAndAccents, isFalseBoolean, isSubsetOf, isTrueBoolean, setValueByPath } from './utils.js';
export {
getContext,
getApiUrl,
@@ -1050,33 +1050,50 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
}
/**
- * @param {boolean} enable - Whether to enable or disable the extension
+ * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
* @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise}
*/
-function getExtensionToggleCallback(enable) {
+function getExtensionActionCallback(action) {
return async (args, extensionName) => {
if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
if (typeof extensionName !== 'string') throw new Error('Extension name does only support string. Closures or arrays are not allowed.');
if (!extensionName) {
- toastr.warning(`Extension name must be provided as an argument to ${enable ? 'enable' : 'disable'} this extension.`);
+ toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
return '';
}
const reload = isTrueBoolean(args?.reload);
-
const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
return '';
}
- if (enable === !extension_settings.disabledExtensions.includes(internalExtensionName)) {
- toastr.info(`Extension ${extensionName} is already ${enable ? 'enabled' : 'disabled'}.`);
+
+ const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
+
+ if (action === 'enable' && isEnabled) {
+ toastr.info(`Extension ${extensionName} is already enabled.`);
return internalExtensionName;
}
- reload && toastr.info(`${enable ? 'Enabling' : 'Disabling'} extension ${extensionName} and reloading...`);
- await enableExtension(internalExtensionName, reload);
- toastr.success(`Extension ${extensionName} ${enable ? 'enabled' : 'disabled'}.`);
+ if (action === 'disable' && !isEnabled) {
+ toastr.info(`Extension ${extensionName} is already disabled.`);
+ return internalExtensionName;
+ }
+
+ if (action === 'toggle') {
+ action = isEnabled ? 'disable' : 'enable';
+ }
+
+ reload && toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
+
+ if (action === 'enable') {
+ await enableExtension(internalExtensionName, reload);
+ } else {
+ await disableExtension(internalExtensionName, reload);
+ }
+
+ toastr.success(`Extension ${extensionName} ${action}d.`);
return internalExtensionName;
};
@@ -1085,7 +1102,7 @@ function getExtensionToggleCallback(enable) {
function registerExtensionSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-enable',
- callback: getExtensionToggleCallback(true),
+ callback: getExtensionActionCallback('enable'),
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
@@ -1125,7 +1142,7 @@ function registerExtensionSlashCommands() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-disable',
- callback: getExtensionToggleCallback(false),
+ callback: getExtensionActionCallback('enable'),
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
@@ -1163,6 +1180,64 @@ function registerExtensionSlashCommands() {
`,
}));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-toggle',
+ callback: async (args, extensionName) => {
+ if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
+ if (typeof extensionName !== 'string') throw new Error('Extension name does only support string. Closures or arrays are not allowed.');
+
+ const action = isTrueBoolean(args?.state) ? 'enable' :
+ isFalseBoolean(args?.state) ? 'disable' :
+ 'toggle';
+
+ return await getExtensionActionCallback(action)(args, extensionName);
+ },
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'reload',
+ description: 'Whether to reload the page after toggling the extension',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ }),
+ SlashCommandNamedArgument.fromProps({
+ name: 'state',
+ description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Toggles the state of a specified extension.
+
+
+ By default, the page will be reloaded automatically, stopping any further commands.
+ If reload=false named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed.
+ The page either needs to be refreshed, or /reload-page has to be called.
+
+
+ Example:
+
+
+
/extension-toggle Summarize
+
+
+
/extension-toggle Summarize state=true
+
+
+
+ `,
+ }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reload-page',
From 1a6f0c0922b263dbdcac7904996cfe4fce47805a Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 23:10:00 +0200
Subject: [PATCH 036/268] Add /extension-exists and /extension-state
---
public/scripts/extensions.js | 71 +++++++++++++++++++++++++++++++++++-
1 file changed, 69 insertions(+), 2 deletions(-)
diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js
index c99255713..6c72c78e6 100644
--- a/public/scripts/extensions.js
+++ b/public/scripts/extensions.js
@@ -1056,7 +1056,7 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
function getExtensionActionCallback(action) {
return async (args, extensionName) => {
if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
- if (typeof extensionName !== 'string') throw new Error('Extension name does only support string. Closures or arrays are not allowed.');
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
if (!extensionName) {
toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
return '';
@@ -1184,7 +1184,7 @@ function registerExtensionSlashCommands() {
name: 'extension-toggle',
callback: async (args, extensionName) => {
if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
- if (typeof extensionName !== 'string') throw new Error('Extension name does only support string. Closures or arrays are not allowed.');
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
const action = isTrueBoolean(args?.state) ? 'enable' :
isFalseBoolean(args?.state) ? 'disable' :
@@ -1238,6 +1238,73 @@ function registerExtensionSlashCommands() {
`,
}));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-state',
+ callback: async (_, extensionName) => {
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
+ const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
+ if (!internalExtensionName) {
+ toastr.warning(`Extension ${extensionName} does not exist.`);
+ return '';
+ }
+
+ const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
+ return String(isEnabled);
+ },
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Returns the state of a specified extension (true if enabled, false if disabled).
+
+
+ Example:
+
+
+
/extension-state Summarize
+
+
+
+ `,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-exists',
+ aliases: ['extension-installed'],
+ callback: async (_, extensionName) => {
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
+ const exists = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName)) !== undefined;
+ return exists ? 'true' : 'false';
+ },
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Checks if a specified extension exists.
+
+
+ Example:
+
+
+
/extension-exists LALib
+
+
+
+ `,
+ }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reload-page',
From 169504aa68b243178050bccaaef63c5ef7e29f27 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 23:18:37 +0200
Subject: [PATCH 037/268] Refactor extension slash commands into own file
- Weird circle imports again with the slash command classes
---
public/script.js | 2 +
public/scripts/extensions-slashcommands.js | 276 ++++++++++++++++++++
public/scripts/extensions.js | 280 +--------------------
3 files changed, 281 insertions(+), 277 deletions(-)
create mode 100644 public/scripts/extensions-slashcommands.js
diff --git a/public/script.js b/public/script.js
index cb750e915..0e675e3e6 100644
--- a/public/script.js
+++ b/public/script.js
@@ -244,6 +244,7 @@ import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCo
import { initInputMarkdown } from './scripts/input-md-formatting.js';
import { AbortReason } from './scripts/util/AbortReason.js';
import { initSystemPrompts } from './scripts/sysprompt.js';
+import { registerExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
//exporting functions and vars for mods
export {
@@ -957,6 +958,7 @@ async function firstLoadInit() {
initLogprobs();
initInputMarkdown();
initExtensions();
+ registerExtensionSlashCommands();
doDailyExtensionUpdatesCheck();
await hideLoader();
await fixViewport();
diff --git a/public/scripts/extensions-slashcommands.js b/public/scripts/extensions-slashcommands.js
new file mode 100644
index 000000000..62d2e8374
--- /dev/null
+++ b/public/scripts/extensions-slashcommands.js
@@ -0,0 +1,276 @@
+import { disableExtension, enableExtension, extension_settings, extensionNames } from './extensions.js';
+import { SlashCommand } from './slash-commands/SlashCommand.js';
+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
+import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
+import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
+import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';
+
+/**
+ * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
+ * @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise}
+ */
+function getExtensionActionCallback(action) {
+ return async (args, extensionName) => {
+ if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
+ if (!extensionName) {
+ toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
+ return '';
+ }
+
+ const reload = isTrueBoolean(args?.reload);
+ const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
+ if (!internalExtensionName) {
+ toastr.warning(`Extension ${extensionName} does not exist.`);
+ return '';
+ }
+
+ const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
+
+ if (action === 'enable' && isEnabled) {
+ toastr.info(`Extension ${extensionName} is already enabled.`);
+ return internalExtensionName;
+ }
+
+ if (action === 'disable' && !isEnabled) {
+ toastr.info(`Extension ${extensionName} is already disabled.`);
+ return internalExtensionName;
+ }
+
+ if (action === 'toggle') {
+ action = isEnabled ? 'disable' : 'enable';
+ }
+
+ reload && toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
+
+ if (action === 'enable') {
+ await enableExtension(internalExtensionName, reload);
+ } else {
+ await disableExtension(internalExtensionName, reload);
+ }
+
+ toastr.success(`Extension ${extensionName} ${action}d.`);
+
+ return internalExtensionName;
+ };
+}
+
+export function registerExtensionSlashCommands() {
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-enable',
+ callback: getExtensionActionCallback('enable'),
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'reload',
+ description: 'Whether to reload the page after enabling the extension',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Enables a specified extension.
+
+
+ By default, the page will be reloaded automatically, stopping any further commands.
+ If reload=false named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
+ The page either needs to be refreshed, or /reload-page has to be called.
+
+ By default, the page will be reloaded automatically, stopping any further commands.
+ If reload=false named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
+ The page either needs to be refreshed, or /reload-page has to be called.
+
+
+ Example:
+
+
+
/extension-disable Summarize
+
+
+
+ `,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-toggle',
+ callback: async (args, extensionName) => {
+ if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
+
+ const action = isTrueBoolean(args?.state) ? 'enable' :
+ isFalseBoolean(args?.state) ? 'disable' :
+ 'toggle';
+
+ return await getExtensionActionCallback(action)(args, extensionName);
+ },
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'reload',
+ description: 'Whether to reload the page after toggling the extension',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ }),
+ SlashCommandNamedArgument.fromProps({
+ name: 'state',
+ description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Toggles the state of a specified extension.
+
+
+ By default, the page will be reloaded automatically, stopping any further commands.
+ If reload=false named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed.
+ The page either needs to be refreshed, or /reload-page has to be called.
+
+
+ Example:
+
+
+
/extension-toggle Summarize
+
+
+
/extension-toggle Summarize state=true
+
+
+
+ `,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-state',
+ callback: async (_, extensionName) => {
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
+ const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
+ if (!internalExtensionName) {
+ toastr.warning(`Extension ${extensionName} does not exist.`);
+ return '';
+ }
+
+ const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
+ return String(isEnabled);
+ },
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Returns the state of a specified extension (true if enabled, false if disabled).
+
+
+ Example:
+
+
+
/extension-state Summarize
+
+
+
+ `,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'extension-exists',
+ aliases: ['extension-installed'],
+ callback: async (_, extensionName) => {
+ if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
+ const exists = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName)) !== undefined;
+ return exists ? 'true' : 'false';
+ },
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Extension name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ forceEnum: true,
+ }),
+ ],
+ helpString: `
+
+ Checks if a specified extension exists.
+
+
+ Example:
+
+
+
/extension-exists LALib
+
+
+
+ `,
+ }));
+
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'reload-page',
+ callback: async () => {
+ toastr.info('Reloading the page...');
+ location.reload();
+ return '';
+ },
+ helpString: 'Reloads the current page. All further commands will not be processed.',
+ }));
+}
diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js
index 6c72c78e6..8b49c5ec5 100644
--- a/public/scripts/extensions.js
+++ b/public/scripts/extensions.js
@@ -1,14 +1,8 @@
import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
import { showLoader } from './loader.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
-import { SlashCommand } from './slash-commands/SlashCommand.js';
-import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
-import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
-import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
-import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
-import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { renderTemplate, renderTemplateAsync } from './templates.js';
-import { equalsIgnoreCaseAndAccents, isFalseBoolean, isSubsetOf, isTrueBoolean, setValueByPath } from './utils.js';
+import { isSubsetOf, setValueByPath } from './utils.js';
export {
getContext,
getApiUrl,
@@ -249,7 +243,7 @@ function onEnableExtensionClick() {
enableExtension(name, false);
}
-async function enableExtension(name, reload = true) {
+export async function enableExtension(name, reload = true) {
extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name);
stateChanged = true;
await saveSettings();
@@ -260,7 +254,7 @@ async function enableExtension(name, reload = true) {
}
}
-async function disableExtension(name, reload = true) {
+export async function disableExtension(name, reload = true) {
extension_settings.disabledExtensions.push(name);
stateChanged = true;
await saveSettings();
@@ -1049,277 +1043,9 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
await installExtension(url);
}
-/**
- * @param {'enable' | 'disable' | 'toggle'} action - The action to perform on the extension
- * @returns {(args: {[key: string]: string | SlashCommandClosure}, extensionName: string | SlashCommandClosure) => Promise}
- */
-function getExtensionActionCallback(action) {
- return async (args, extensionName) => {
- if (args?.reload instanceof SlashCommandClosure) throw new Error('\'reload\' argument cannot be a closure.');
- if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
- if (!extensionName) {
- toastr.warning(`Extension name must be provided as an argument to ${action} this extension.`);
- return '';
- }
- const reload = isTrueBoolean(args?.reload);
- const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
- if (!internalExtensionName) {
- toastr.warning(`Extension ${extensionName} does not exist.`);
- return '';
- }
-
- const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
-
- if (action === 'enable' && isEnabled) {
- toastr.info(`Extension ${extensionName} is already enabled.`);
- return internalExtensionName;
- }
-
- if (action === 'disable' && !isEnabled) {
- toastr.info(`Extension ${extensionName} is already disabled.`);
- return internalExtensionName;
- }
-
- if (action === 'toggle') {
- action = isEnabled ? 'disable' : 'enable';
- }
-
- reload && toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
-
- if (action === 'enable') {
- await enableExtension(internalExtensionName, reload);
- } else {
- await disableExtension(internalExtensionName, reload);
- }
-
- toastr.success(`Extension ${extensionName} ${action}d.`);
-
- return internalExtensionName;
- };
-}
-
-function registerExtensionSlashCommands() {
- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
- name: 'extension-enable',
- callback: getExtensionActionCallback('enable'),
- namedArgumentList: [
- SlashCommandNamedArgument.fromProps({
- name: 'reload',
- description: 'Whether to reload the page after enabling the extension',
- typeList: [ARGUMENT_TYPE.BOOLEAN],
- defaultValue: 'true',
- enumList: commonEnumProviders.boolean('trueFalse')(),
- }),
- ],
- unnamedArgumentList: [
- SlashCommandArgument.fromProps({
- description: 'Extension name',
- typeList: [ARGUMENT_TYPE.STRING],
- isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
- forceEnum: true,
- }),
- ],
- helpString: `
-
- Enables a specified extension.
-
-
- By default, the page will be reloaded automatically, stopping any further commands.
- If reload=false named argument is passed, the page will not be reloaded, and the extension will stay disabled until refreshed.
- The page either needs to be refreshed, or /reload-page has to be called.
-
- By default, the page will be reloaded automatically, stopping any further commands.
- If reload=false named argument is passed, the page will not be reloaded, and the extension will stay enabled until refreshed.
- The page either needs to be refreshed, or /reload-page has to be called.
-
-
- Example:
-
-
-
/extension-disable Summarize
-
-
-
- `,
- }));
- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
- name: 'extension-toggle',
- callback: async (args, extensionName) => {
- if (args?.state instanceof SlashCommandClosure) throw new Error('\'state\' argument cannot be a closure.');
- if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
-
- const action = isTrueBoolean(args?.state) ? 'enable' :
- isFalseBoolean(args?.state) ? 'disable' :
- 'toggle';
-
- return await getExtensionActionCallback(action)(args, extensionName);
- },
- namedArgumentList: [
- SlashCommandNamedArgument.fromProps({
- name: 'reload',
- description: 'Whether to reload the page after toggling the extension',
- typeList: [ARGUMENT_TYPE.BOOLEAN],
- defaultValue: 'true',
- enumList: commonEnumProviders.boolean('trueFalse')(),
- }),
- SlashCommandNamedArgument.fromProps({
- name: 'state',
- description: 'Explicitly set the state of the extension (true to enable, false to disable). If not provided, the state will be toggled to the opposite of the current state.',
- typeList: [ARGUMENT_TYPE.BOOLEAN],
- enumList: commonEnumProviders.boolean('trueFalse')(),
- }),
- ],
- unnamedArgumentList: [
- SlashCommandArgument.fromProps({
- description: 'Extension name',
- typeList: [ARGUMENT_TYPE.STRING],
- isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
- forceEnum: true,
- }),
- ],
- helpString: `
-
- Toggles the state of a specified extension.
-
-
- By default, the page will be reloaded automatically, stopping any further commands.
- If reload=false named argument is passed, the page will not be reloaded, and the extension will stay in its current state until refreshed.
- The page either needs to be refreshed, or /reload-page has to be called.
-
-
- Example:
-
-
-
/extension-toggle Summarize
-
-
-
/extension-toggle Summarize state=true
-
-
-
- `,
- }));
- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
- name: 'extension-state',
- callback: async (_, extensionName) => {
- if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
- const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
- if (!internalExtensionName) {
- toastr.warning(`Extension ${extensionName} does not exist.`);
- return '';
- }
-
- const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
- return String(isEnabled);
- },
- unnamedArgumentList: [
- SlashCommandArgument.fromProps({
- description: 'Extension name',
- typeList: [ARGUMENT_TYPE.STRING],
- isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
- forceEnum: true,
- }),
- ],
- helpString: `
-
- Returns the state of a specified extension (true if enabled, false if disabled).
-
-
- Example:
-
-
-
/extension-state Summarize
-
-
-
- `,
- }));
- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
- name: 'extension-exists',
- aliases: ['extension-installed'],
- callback: async (_, extensionName) => {
- if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
- const exists = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName)) !== undefined;
- return exists ? 'true' : 'false';
- },
- unnamedArgumentList: [
- SlashCommandArgument.fromProps({
- description: 'Extension name',
- typeList: [ARGUMENT_TYPE.STRING],
- isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
- forceEnum: true,
- }),
- ],
- helpString: `
-
- Checks if a specified extension exists.
-
-
- Example:
-
-
-
/extension-exists LALib
-
-
-
- `,
- }));
-
- SlashCommandParser.addCommandObject(SlashCommand.fromProps({
- name: 'reload-page',
- callback: async () => {
- toastr.info('Reloading the page...');
- location.reload();
- return '';
- },
- helpString: 'Reloads the current page. All further commands will not be processed.',
- }));
-}
export async function initExtensions() {
- registerExtensionSlashCommands();
-
await addExtensionsButtonAndMenu();
$('#extensionsMenuButton').css('display', 'flex');
From 9fbcb1221020c9b3bfc81b9853c7ea7c1abab3ea Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 23:33:00 +0200
Subject: [PATCH 038/268] Fix third party extension findings + enum provider
- Allow extension names without the "third-party/" prefix
- Expand enum provider to show what third-party extensions are
---
public/script.js | 4 +-
public/scripts/extensions-slashcommands.js | 48 +++++++++++++++++-----
2 files changed, 39 insertions(+), 13 deletions(-)
diff --git a/public/script.js b/public/script.js
index 0e675e3e6..7875de32f 100644
--- a/public/script.js
+++ b/public/script.js
@@ -244,7 +244,7 @@ import { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCo
import { initInputMarkdown } from './scripts/input-md-formatting.js';
import { AbortReason } from './scripts/util/AbortReason.js';
import { initSystemPrompts } from './scripts/sysprompt.js';
-import { registerExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
+import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
//exporting functions and vars for mods
export {
@@ -958,7 +958,7 @@ async function firstLoadInit() {
initLogprobs();
initInputMarkdown();
initExtensions();
- registerExtensionSlashCommands();
+ initExtensionSlashCommands();
doDailyExtensionUpdatesCheck();
await hideLoader();
await fixViewport();
diff --git a/public/scripts/extensions-slashcommands.js b/public/scripts/extensions-slashcommands.js
index 62d2e8374..499b86ecb 100644
--- a/public/scripts/extensions-slashcommands.js
+++ b/public/scripts/extensions-slashcommands.js
@@ -3,7 +3,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
-import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
+import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean } from './utils.js';
@@ -21,7 +21,7 @@ function getExtensionActionCallback(action) {
}
const reload = isTrueBoolean(args?.reload);
- const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
+ const internalExtensionName = findExtension(extensionName);
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
return '';
@@ -57,6 +57,33 @@ function getExtensionActionCallback(action) {
};
}
+/**
+ * Finds an extension by name, allowing omission of the "third-party/" prefix.
+ *
+ * @param {string} name - The name of the extension to find
+ * @returns {string?} - The matched extension name or undefined if not found
+ */
+function findExtension(name) {
+ return extensionNames.find(extName => {
+ return equalsIgnoreCaseAndAccents(extName, name) || equalsIgnoreCaseAndAccents(extName, `third-party/${name}`);
+ });
+}
+
+/**
+ * Provides an array of SlashCommandEnumValue objects based on the extension names.
+ * Each object contains the name of the extension and a description indicating if it is a third-party extension.
+ *
+ * @returns {SlashCommandEnumValue[]} An array of SlashCommandEnumValue objects
+ */
+const extensionNamesEnumProvider = () => extensionNames.map(name => {
+ const isThirdParty = name.startsWith('third-party/');
+ if (isThirdParty) name = name.slice('third-party/'.length);
+
+ const description = isThirdParty ? 'third party extension' : null;
+
+ return new SlashCommandEnumValue(name, description, !isThirdParty ? enumTypes.name : enumTypes.enum);
+});
+
export function registerExtensionSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-enable',
@@ -75,7 +102,7 @@ export function registerExtensionSlashCommands() {
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
@@ -115,7 +142,7 @@ export function registerExtensionSlashCommands() {
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
@@ -170,7 +197,7 @@ export function registerExtensionSlashCommands() {
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
@@ -200,7 +227,7 @@ export function registerExtensionSlashCommands() {
name: 'extension-state',
callback: async (_, extensionName) => {
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
- const internalExtensionName = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName));
+ const internalExtensionName = findExtension(extensionName);
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
return '';
@@ -214,7 +241,7 @@ export function registerExtensionSlashCommands() {
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
+ enumProvider: extensionNamesEnumProvider,
forceEnum: true,
}),
],
@@ -237,7 +264,7 @@ export function registerExtensionSlashCommands() {
aliases: ['extension-installed'],
callback: async (_, extensionName) => {
if (typeof extensionName !== 'string') throw new Error('Extension name must be a string. Closures or arrays are not allowed.');
- const exists = extensionNames.find(x => equalsIgnoreCaseAndAccents(x, extensionName)) !== undefined;
+ const exists = findExtension(extensionName) !== undefined;
return exists ? 'true' : 'false';
},
unnamedArgumentList: [
@@ -245,8 +272,7 @@ export function registerExtensionSlashCommands() {
description: 'Extension name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
- enumProvider: () => extensionNames.map(name => new SlashCommandEnumValue(name)),
- forceEnum: true,
+ enumProvider: extensionNamesEnumProvider,
}),
],
helpString: `
@@ -257,7 +283,7 @@ export function registerExtensionSlashCommands() {
Example:
-
/extension-exists LALib
+
/extension-exists SillyTavern-LALib
From 15bc0e4dba7ffd8ad250061b765424e838bf3099 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Wed, 25 Sep 2024 23:53:26 +0200
Subject: [PATCH 039/268] Squish the last bugs
---
public/scripts/extensions-slashcommands.js | 24 +++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
diff --git a/public/scripts/extensions-slashcommands.js b/public/scripts/extensions-slashcommands.js
index 499b86ecb..c8bfa96f3 100644
--- a/public/scripts/extensions-slashcommands.js
+++ b/public/scripts/extensions-slashcommands.js
@@ -20,7 +20,7 @@ function getExtensionActionCallback(action) {
return '';
}
- const reload = isTrueBoolean(args?.reload);
+ const reload = !isFalseBoolean(args?.reload);
const internalExtensionName = findExtension(extensionName);
if (!internalExtensionName) {
toastr.warning(`Extension ${extensionName} does not exist.`);
@@ -43,7 +43,14 @@ function getExtensionActionCallback(action) {
action = isEnabled ? 'disable' : 'enable';
}
- reload && toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
+ if (reload) {
+ toastr.info(`${action.charAt(0).toUpperCase() + action.slice(1)}ing extension ${extensionName} and reloading...`);
+
+ // Clear input, so it doesn't stay because the command didn't "finish",
+ // and wait for a bit to both show the toast and let the clear bubble through.
+ $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true }));
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
if (action === 'enable') {
await enableExtension(internalExtensionName, reload);
@@ -53,6 +60,12 @@ function getExtensionActionCallback(action) {
toastr.success(`Extension ${extensionName} ${action}d.`);
+
+ console.info(`Extension ${action}ed: ${extensionName}`);
+ if (!reload) {
+ console.info('Reload not requested, so page needs to be reloaded manually for changes to take effect.');
+ }
+
return internalExtensionName;
};
}
@@ -88,6 +101,7 @@ export function registerExtensionSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-enable',
callback: getExtensionActionCallback('enable'),
+ returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
@@ -127,7 +141,8 @@ export function registerExtensionSlashCommands() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'extension-disable',
- callback: getExtensionActionCallback('enable'),
+ callback: getExtensionActionCallback('disable'),
+ returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
@@ -177,6 +192,7 @@ export function registerExtensionSlashCommands() {
return await getExtensionActionCallback(action)(args, extensionName);
},
+ returns: 'The internal extension name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'reload',
@@ -236,6 +252,7 @@ export function registerExtensionSlashCommands() {
const isEnabled = !extension_settings.disabledExtensions.includes(internalExtensionName);
return String(isEnabled);
},
+ returns: 'true/false - The state of the extension, whether it is enabled.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
@@ -267,6 +284,7 @@ export function registerExtensionSlashCommands() {
const exists = findExtension(extensionName) !== undefined;
return exists ? 'true' : 'false';
},
+ returns: 'true/false - Whether the extension exists and is installed.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'Extension name',
From d8935be5e5634f9b8f2c873c58875e8fb543f4e8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=82=A6?=
Date: Thu, 26 Sep 2024 15:17:28 +0800
Subject: [PATCH 040/268] Update cosyvoice.js
add Unofficial label
---
public/scripts/extensions/tts/cosyvoice.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/public/scripts/extensions/tts/cosyvoice.js b/public/scripts/extensions/tts/cosyvoice.js
index 5c5a921a6..4f9f6a1d5 100644
--- a/public/scripts/extensions/tts/cosyvoice.js
+++ b/public/scripts/extensions/tts/cosyvoice.js
@@ -52,8 +52,8 @@ class CosyVoiceProvider {
- Windows users Use CosyVoice_For_Windows.
- Macos Users Use CosyVoice_for_MacOs.
+ Windows users Use CosyVoice_For_Windows(Unofficial).
+ Macos Users Use CosyVoice_for_MacOs(Unofficial).
- System Message Sequences
+ System Message Sequences
System Message Prefix
@@ -3361,7 +3361,7 @@
- System Prompt Sequences
+ System Prompt Sequences
-
-
`,
}));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'char-find',
+ aliases: ['findchar'],
+ callback: (args, name) => {
+ if (typeof name !== 'string') throw new Error('name must be a string');
+ if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
+
+ const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent) });
+ return char?.avatar ?? '';
+ },
+ returns: 'the avatar key (unique identifier) of the character',
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'tag',
+ description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name.',
+ typeList: [ARGUMENT_TYPE.STRING],
+ enumProvider: commonEnumProviders.tags('assigned'),
+ acceptsMultiple: true,
+ }),
+ SlashCommandNamedArgument.fromProps({
+ name: 'preferCurrent',
+ description: 'Prefer current character or characters in a group, if multiple characters match',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Character name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ enumProvider: commonEnumProviders.characters('character'),
+ forceEnum: false,
+ }),
+ ],
+ helpString: `
+
+ Searches for a character and returns its avatar key.
+
+
+ This can be used to choose the correct character for something like /sendas or other commands in need of a character name
+ if you have multiple characters with the same name.
+
+
+ Example:
+
+
+
/char-find name="Chloe"
+ Returns the avatar key for "Chloe".
+
+
+
/search name="Chloe" tag="friend"
+ Returns the avatar key for the character "Chloe" that is tagged with "friend".
+ This is useful if you for example have multiple characters named "Chloe", and the others are "foe", "goddess", or anything else,
+ so you can actually select the character you are looking for.
+
+
+
+ `,
+ }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sendas',
callback: sendMessageAs,
@@ -3116,42 +3176,94 @@ async function setNarratorName(_, text) {
return '';
}
+/**
+ * Checks if an argument is a string array (or undefined), and if not, throws an error
+ * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
+ * @param {string} name The name of the argument for the error message
+ * @param {object} [options={}] - The optional arguments
+ * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
+ * @throws {Error} If the argument is not an array
+ * @returns {string[]}
+ */
+export function validateArrayArgString(arg, name, { allowUndefined = true } = {}) {
+ if (arg === undefined) {
+ if (allowUndefined) return undefined;
+ throw new Error(`Argument "${name}" is undefined, but must be a string array`);
+ }
+ if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
+ if (!arg.every(x => typeof x === 'string')) throw new Error(`Argument "${name}" must be an array of strings`);
+ return arg;
+}
+
+/**
+ * Checks if an argument is a string or closure array (or undefined), and if not, throws an error
+ * @param {string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined} arg The named argument to check
+ * @param {string} name The name of the argument for the error message
+ * @param {object} [options={}] - The optional arguments
+ * @param {boolean} [options.allowUndefined=false] - Whether the argument can be undefined
+ * @throws {Error} If the argument is not an array of strings or closures
+ * @returns {(string|SlashCommandClosure)[]}
+ */
+export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
+ if (arg === undefined) {
+ if (allowUndefined) return [];
+ throw new Error(`Argument "${name}" is undefined, but must be an array of strings or closures`);
+ }
+ if (!Array.isArray(arg)) throw new Error(`Argument "${name}" must be an array`);
+ if (!arg.every(x => typeof x === 'string' || x instanceof SlashCommandClosure)) throw new Error(`Argument "${name}" must be an array of strings or closures`);
+ return arg;
+}
+
/**
* Finds a character by name, with optional filtering and precedence for avatars
- * @param {string} name - The name to search for
* @param {object} [options={}] - The options for the search
+ * @param {string?} [options.name=null] - The name to search for
* @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar
* @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
* @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
- * @param {any?} [options.preferCurrentChar=null] - The current character to prefer
+ * @param {boolean} [options.preferCurrentChar=false] - Whether to prefer the current character(s)
+ * @param {boolean} [options.quiet=false] - Whether to suppress warnings
* @returns {any?} - The found character or null if not found
*/
-export function findCharByName(name, { allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = null } = {}) {
- const matches = (char) => (allowAvatar && char.avatar === name) || insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name;
+export function findChar({ name = null, allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = false, quiet = false } = {}) {
+ const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
+
+ // Get the current character(s)
+ const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => characters.find(char => char.avatar === member)) : [characters[this_chid]];
// If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence
- if (preferCurrentChar && !filteredByTags && matches(preferCurrentChar)) {
- return preferCurrentChar;
+ if (preferCurrentChar && !filteredByTags) {
+ const preferredChar = currentChars.find(matches);
+ if (preferredChar) {
+ return preferredChar;
+ }
}
// Filter characters by tags if provided
let filteredCharacters = characters;
if (filteredByTags) {
- filteredCharacters = characters.filter(char => filteredByTags.every(tag => char.tags.includes(tag)));
+ filteredCharacters = characters.filter(char => {
+ const charTags = getTagsList(char.avatar, false);
+ return filteredByTags.every(tagName => charTags.some(x => x.name == tagName));
+ });
}
// If allowAvatar is true, search by avatar first
- if (allowAvatar) {
+ if (allowAvatar && name) {
const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
if (characterByAvatar) {
return characterByAvatar;
}
}
- // Search for a matching character by name
- let character = filteredCharacters.find(matches);
+ // Search for matching characters by name
+ const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
+ if (matchingCharacters.length > 1) {
+ if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
+ else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
+ }
- return character;
+ return matchingCharacters[0] || null;
}
export async function sendMessageAs(args, text) {
diff --git a/public/scripts/slash-commands/SlashCommand.js b/public/scripts/slash-commands/SlashCommand.js
index 5f8726ba0..c56803249 100644
--- a/public/scripts/slash-commands/SlashCommand.js
+++ b/public/scripts/slash-commands/SlashCommand.js
@@ -15,13 +15,13 @@ import { SlashCommandScope } from './SlashCommandScope.js';
* _abortController:SlashCommandAbortController,
* _debugController:SlashCommandDebugController,
* _hasUnnamedArgument:boolean,
- * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[],
+ * [id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined,
* }} NamedArguments
*/
/**
* Alternative object for local JSDocs, where you don't need existing pipe, scope, etc. arguments
- * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]}} NamedArgumentsCapture
+ * @typedef {{[id:string]:string|SlashCommandClosure|(string|SlashCommandClosure)[]|undefined}} NamedArgumentsCapture
*/
/**
diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
index 07df46b3e..4dd22c8fe 100644
--- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
+++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
@@ -2,7 +2,7 @@ import { chat_metadata, characters, substituteParams, chat, extension_prompt_rol
import { extension_settings } from '../extensions.js';
import { getGroupMembers, groups } from '../group-chats.js';
import { power_user } from '../power-user.js';
-import { searchCharByName, getTagsList, tags } from '../tags.js';
+import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
import { world_names } from '../world-info.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
@@ -181,6 +181,18 @@ export const commonEnumProviders = {
*/
personas: () => Object.values(power_user.personas).map(persona => new SlashCommandEnumValue(persona, null, enumTypes.name, enumIcons.persona)),
+ /**
+ * All possible tags, or only those that have been assigned
+ *
+ * @param {('all' | 'assigned')} [mode='all'] - Which types of tags to show
+ * @returns {() => SlashCommandEnumValue[]}
+ */
+ tags: (mode = 'all') => () => {
+ let assignedTags = mode === 'assigned' ? new Set(Object.values(tag_map).flat()) : new Set();
+ return tags.filter(tag => mode === 'all' || (mode === 'assigned' && assignedTags.has(tag.id)))
+ .map(tag => new SlashCommandEnumValue(tag.name, null, enumTypes.command, enumIcons.tag));
+ },
+
/**
* All possible tags for a given char/group entity
*
From 8401cc6032d389bd956f80749587381aab6bcb2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carsten=20Kragelund=20J=C3=B8rgensen?=
Date: Thu, 12 Sep 2024 01:58:58 +0200
Subject: [PATCH 084/268] fix: append continuation message after prefill
---
src/prompt-converters.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/prompt-converters.js b/src/prompt-converters.js
index 5d52adb6e..c41ad8da9 100644
--- a/src/prompt-converters.js
+++ b/src/prompt-converters.js
@@ -132,7 +132,9 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
});
// Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
- if (prefillString) {
+ if (messages[messages.length - 1].role == 'assistant' && prefillString) {
+ messages[messages.length - 1].content = prefillString.trimEnd() + messages[messages.length - 1].content;
+ } else if (prefillString) {
messages.push({
role: 'assistant',
content: prefillString.trimEnd(),
From d5f94577dc8d6e9f6c616cf3651e2c3430c81bff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Carsten=20Kragelund=20J=C3=B8rgensen?=
Date: Sun, 15 Sep 2024 09:18:40 +0000
Subject: [PATCH 085/268] fix: dont trim prefill message, but do trim message
---
src/prompt-converters.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/prompt-converters.js b/src/prompt-converters.js
index c41ad8da9..104a7a9b2 100644
--- a/src/prompt-converters.js
+++ b/src/prompt-converters.js
@@ -133,7 +133,7 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
// Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
if (messages[messages.length - 1].role == 'assistant' && prefillString) {
- messages[messages.length - 1].content = prefillString.trimEnd() + messages[messages.length - 1].content;
+ messages[messages.length - 1].content = prefillString + messages[messages.length - 1].content.trimEnd();
} else if (prefillString) {
messages.push({
role: 'assistant',
From aae934a84937a7219ca79f79a8562301e8b66ea2 Mon Sep 17 00:00:00 2001
From: Carsten Kragelund
Date: Sat, 28 Sep 2024 22:36:56 +0000
Subject: [PATCH 086/268] fix: move prefill continuation handling to
populateChatHistory
---
public/scripts/openai.js | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index df798132b..5a45b3fbe 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -724,6 +724,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
if (chatCompletion.canAfford(chatMessage)) {
if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
+ // in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message
+ if (chatPrompt.role === 'assistant') {
+ const collection = new MessageCollection('continuePrefill', new Message(chatMessage.role, substituteParams(oai_settings.assistant_prefill + '\n\n') + chatMessage.content, chatMessage.identifier));
+ chatCompletion.add(collection, -1);
+ continue;
+ }
const collection = new MessageCollection('continuePrefill', chatMessage);
chatCompletion.add(collection, -1);
continue;
@@ -1771,7 +1777,7 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
// Don't add a prefill on quiet gens (summarization)
- if (!isQuiet) {
+ if (!isQuiet && !isContinue) {
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
}
}
From 4966fb65f713f624a1244053ceedacdc718d098c Mon Sep 17 00:00:00 2001
From: Carsten Kragelund
Date: Sat, 28 Sep 2024 22:46:23 +0000
Subject: [PATCH 087/268] fix: add assistant_prefill to request if people are
using non prefill based continues
---
public/scripts/openai.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 5a45b3fbe..a8fbd667f 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -1777,7 +1777,8 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
// Don't add a prefill on quiet gens (summarization)
- if (!isQuiet && !isContinue) {
+ console.log(isContinue && oai_settings.continue_prefill);
+ if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) {
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
}
}
From 231ea98b25e227e7b273115cebab7178c8866b52 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 00:53:43 +0200
Subject: [PATCH 088/268] Update /sendas to work with findChar
---
public/scripts/slash-commands.js | 24 ++++++++----------------
1 file changed, 8 insertions(+), 16 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index b94ddfbf0..3e67f98ed 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -251,13 +251,6 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
}),
- SlashCommandNamedArgument.fromProps({
- name: 'tag',
- description: 'Supply one or more tags to filter down to the correct character for the provided name, if multiple characters have the same name. (does not apply to the avatar argument)',
- typeList: [ARGUMENT_TYPE.STRING],
- enumProvider: commonEnumProviders.tagsForChar('all'),
- acceptsMultiple: true,
- }),
SlashCommandNamedArgument.fromProps({
name: 'compact',
description: 'Use compact layout',
@@ -3218,14 +3211,14 @@ export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
* Finds a character by name, with optional filtering and precedence for avatars
* @param {object} [options={}] - The options for the search
* @param {string?} [options.name=null] - The name to search for
- * @param {boolean} [options.allowAvatar=false] - Whether to allow searching by avatar
+ * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
* @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
* @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
- * @param {boolean} [options.preferCurrentChar=false] - Whether to prefer the current character(s)
+ * @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s)
* @param {boolean} [options.quiet=false] - Whether to suppress warnings
* @returns {any?} - The found character or null if not found
*/
-export function findChar({ name = null, allowAvatar = false, insensitive = true, filteredByTags = null, preferCurrentChar = false, quiet = false } = {}) {
+export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
// Get the current character(s)
@@ -3304,19 +3297,18 @@ export async function sendMessageAs(args, text) {
const isSystem = bias && !removeMacros(mesText).length;
const compact = isTrueBoolean(args?.compact);
- const chatCharacter = this_chid !== undefined ? characters[this_chid] : null;
- const isNeutralCharacter = !chatCharacter && name2 === neutralCharacterName && name === neutralCharacterName;
+ const character = findChar({ name: name });
- const character = findCharByName(name, { filteredByTags: args?.tags, preferCurrentChar: chatCharacter });
-
- const avatarCharacter = args.avatar ? findCharByName(args.avatar) : character;
+ const avatarCharacter = args.avatar ? findChar({ name: args.avatar }) : character;
if (args.avatar && !avatarCharacter) {
toastr.warning(`Character for avatar ${args.avatar} not found`);
return '';
}
+ const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
+
let force_avatar, original_avatar;
- if (chatCharacter === avatarCharacter || isNeutralCharacter) {
+ if (character === avatarCharacter || isNeutralCharacter) {
// If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
}
else if (avatarCharacter && avatarCharacter.avatar !== 'none') {
From 5a0e70e8fa377ca9cf6e9671c748852ef17e1de0 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 00:55:41 +0200
Subject: [PATCH 089/268] Add "quiet" to /char-find
---
public/scripts/slash-commands.js | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 3e67f98ed..4a801da8b 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -180,8 +180,9 @@ export function initDefaultSlashCommands() {
callback: (args, name) => {
if (typeof name !== 'string') throw new Error('name must be a string');
if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
+ if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error('quiet cannot be a closure or array');
- const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent) });
+ const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
return char?.avatar ?? '';
},
returns: 'the avatar key (unique identifier) of the character',
@@ -199,6 +200,13 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
}),
+ SlashCommandNamedArgument.fromProps({
+ name: 'quiet',
+ description: 'Do not show warning if multiple charactrers are found',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'false',
+ enumProvider: commonEnumProviders.boolean('trueFalse'),
+ }),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
From ca009dee594f4617964d5a9b6f267f50271b201b Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 01:17:13 +0200
Subject: [PATCH 090/268] Respect tags for current char(s) selection
- Respect tags for prefer current char(s)
- Fix preferCurrentChar not being true by default
---
public/scripts/slash-commands.js | 30 ++++++++++++++++++------------
1 file changed, 18 insertions(+), 12 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 4a801da8b..0286fb915 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -182,7 +182,7 @@ export function initDefaultSlashCommands() {
if (args.preferCurrent instanceof SlashCommandClosure || Array.isArray(args.preferCurrent)) throw new Error('preferCurrent cannot be a closure or array');
if (args.quiet instanceof SlashCommandClosure || Array.isArray(args.quiet)) throw new Error('quiet cannot be a closure or array');
- const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: isTrueBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
+ const char = findChar({ name: name, filteredByTags: validateArrayArgString(args.tag, 'tag'), preferCurrentChar: !isFalseBoolean(args.preferCurrent), quiet: isTrueBoolean(args.quiet) });
return char?.avatar ?? '';
},
returns: 'the avatar key (unique identifier) of the character',
@@ -3229,17 +3229,6 @@ export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
- // Get the current character(s)
- const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => characters.find(char => char.avatar === member)) : [characters[this_chid]];
-
- // If we have a current char and prefer it, return that if it matches - unless tags are provided, they have precedence
- if (preferCurrentChar && !filteredByTags) {
- const preferredChar = currentChars.find(matches);
- if (preferredChar) {
- return preferredChar;
- }
- }
-
// Filter characters by tags if provided
let filteredCharacters = characters;
if (filteredByTags) {
@@ -3249,6 +3238,23 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
});
}
+ // Get the current character(s)
+ /** @type {any[]} */
+ const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => filteredCharacters.find(char => char.avatar === member))
+ : [filteredCharacters.find(char => characters[this_chid]?.avatar === char.avatar)];
+
+ // If we have a current char and prefer it, return that if it matches
+ if (preferCurrentChar) {
+ const preferredCharSearch = currentChars.filter(matches);
+ if (preferredCharSearch.length > 1) {
+ if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
+ else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
+ }
+ if (preferredCharSearch.length) {
+ return preferredCharSearch[0];
+ }
+ }
+
// If allowAvatar is true, search by avatar first
if (allowAvatar && name) {
const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
From 1711ef4e04bf274c1f0c3fed74f132b8e47ebcb2 Mon Sep 17 00:00:00 2001
From: Carsten Kragelund
Date: Sat, 28 Sep 2024 23:34:38 +0000
Subject: [PATCH 091/268] fix: remove continue prefill logic from
prompt-converters
---
src/prompt-converters.js | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/prompt-converters.js b/src/prompt-converters.js
index 104a7a9b2..5d52adb6e 100644
--- a/src/prompt-converters.js
+++ b/src/prompt-converters.js
@@ -132,9 +132,7 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
});
// Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
- if (messages[messages.length - 1].role == 'assistant' && prefillString) {
- messages[messages.length - 1].content = prefillString + messages[messages.length - 1].content.trimEnd();
- } else if (prefillString) {
+ if (prefillString) {
messages.push({
role: 'assistant',
content: prefillString.trimEnd(),
From d6e52dbb9727ff67ec7d52b664b17efcb363c208 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 02:14:06 +0200
Subject: [PATCH 092/268] Update /ask with new char find functionality
---
public/scripts/slash-commands.js | 110 +++++++++++++++++++------------
1 file changed, 67 insertions(+), 43 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 0286fb915..2f2e27fdd 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -247,7 +247,7 @@ export function initDefaultSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
@@ -255,7 +255,7 @@ export function initDefaultSlashCommands() {
}),
SlashCommandNamedArgument.fromProps({
name: 'avatar',
- description: 'Character avatar override (Can be either avatar filename or just the character name to pull the avatar from)',
+ description: 'Character avatar override (Can be either avatar key or just the character name to pull the avatar from)',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
}),
@@ -288,6 +288,9 @@ export function initDefaultSlashCommands() {
/sendas name="Chloe" Hello, guys!
will send "Hello, guys!" from "Chloe".
+
+
/sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.
+ will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".
@@ -509,7 +512,7 @@ export function initDefaultSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
@@ -2558,26 +2561,22 @@ async function askCharacter(args, text) {
return '';
}
- let name = '';
-
- if (args?.name) {
- name = args.name.trim();
-
- if (!name) {
- toastr.warning('You must specify a name of the character to ask.');
- return '';
- }
+ if (!args.name) {
+ toastr.warning('You must specify a name of the character to ask.');
+ return '';
}
const prevChId = this_chid;
// Find the character
- const chId = characters.findIndex((e) => e.name === name || e.avatar === name);
- if (!characters[chId] || chId === -1) {
+ const character = findChar({ name: args?.name });
+ if (!character) {
toastr.error('Character not found.');
return '';
}
+ const chId = getCharIndex(character);
+
if (text) {
const mesText = getRegexedString(text.trim(), regex_placement.SLASH_COMMAND);
// Sending a message implicitly saves the chat, so this needs to be done before changing the character
@@ -2588,19 +2587,9 @@ async function askCharacter(args, text) {
// Override character and send a user message
setCharacterId(String(chId));
- const character = characters[chId];
- let force_avatar, original_avatar;
+ const { name, force_avatar, original_avatar } = getNameAndAvatarForMessage(character, args?.name);
- if (character && character.avatar !== 'none') {
- force_avatar = getThumbnailUrl('avatar', character.avatar);
- original_avatar = character.avatar;
- }
- else {
- force_avatar = default_avatar;
- original_avatar = default_avatar;
- }
-
- setCharacterName(character.name);
+ setCharacterName(name);
const restoreCharacter = () => {
if (String(this_chid) !== String(chId)) {
@@ -2613,7 +2602,7 @@ async function askCharacter(args, text) {
// Only force the new avatar if the character name is the same
// This skips if an error was fired
const lastMessage = chat[chat.length - 1];
- if (lastMessage && lastMessage?.name === character.name) {
+ if (lastMessage && lastMessage?.name === name) {
lastMessage.force_avatar = force_avatar;
lastMessage.original_avatar = original_avatar;
}
@@ -2624,7 +2613,7 @@ async function askCharacter(args, text) {
// Run generate and restore previous character
try {
eventSource.once(event_types.MESSAGE_RECEIVED, restoreCharacter);
- toastr.info(`Asking ${character.name} something...`);
+ toastr.info(`Asking ${name} something...`);
askResult = await Generate('ask_command');
} catch (error) {
restoreCharacter();
@@ -3215,6 +3204,41 @@ export function validateArrayArg(arg, name, { allowUndefined = true } = {}) {
return arg;
}
+
+/**
+ * Retrieves the name and avatar information for a message
+ *
+ * The name of the character will always have precendence over the one given as argument. If you want to specify a different name for the message,
+ * explicitly implement this in the code using this.
+ *
+ * @param {object?} character - The character object to get the avatar data for
+ * @param {string?} name - The name to get the avatar data for
+ * @returns {{name: string, force_avatar: string, original_avatar: string}} An object containing the name for the message, forced avatar URL, and original avatar
+ */
+export function getNameAndAvatarForMessage(character, name = null) {
+ const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
+ const currentChar = characters[this_chid];
+
+ let force_avatar, original_avatar;
+ if (character?.avatar === currentChar?.avatar || isNeutralCharacter) {
+ // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
+ }
+ else if (character && character.avatar !== 'none') {
+ force_avatar = getThumbnailUrl('avatar', character.avatar);
+ original_avatar = character.avatar;
+ }
+ else {
+ force_avatar = default_avatar;
+ original_avatar = default_avatar;
+ }
+
+ return {
+ name: character?.name || name,
+ force_avatar: force_avatar,
+ original_avatar: original_avatar,
+ };
+}
+
/**
* Finds a character by name, with optional filtering and precedence for avatars
* @param {object} [options={}] - The options for the search
@@ -3273,6 +3297,19 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
return matchingCharacters[0] || null;
}
+/**
+ * Gets the index of a character based on the character object
+ * @param {object} char - The character object to find the index for
+ * @throws {Error} If the character is not found
+ * @returns {number} The index of the character in the characters array
+ */
+export function getCharIndex(char) {
+ if (!char) throw new Error('Character is undefined');
+ const index = characters.findIndex(c => c.avatar === char.avatar);
+ if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
+ return index;
+}
+
export async function sendMessageAs(args, text) {
if (!text) {
return '';
@@ -3319,23 +3356,10 @@ export async function sendMessageAs(args, text) {
return '';
}
- const isNeutralCharacter = !character && name2 === neutralCharacterName && name === neutralCharacterName;
-
- let force_avatar, original_avatar;
- if (character === avatarCharacter || isNeutralCharacter) {
- // If the targeted character is the currently selected one in a solo chat, we don't need to force any avatars
- }
- else if (avatarCharacter && avatarCharacter.avatar !== 'none') {
- force_avatar = getThumbnailUrl('avatar', avatarCharacter.avatar);
- original_avatar = avatarCharacter.avatar;
- }
- else {
- force_avatar = default_avatar;
- original_avatar = default_avatar;
- }
+ const { name: nameForMessage, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
const message = {
- name: character?.name || name,
+ name: nameForMessage,
is_user: false,
is_system: isSystem,
send_date: getMessageTimeStamp(),
From 145023ba8d88fe6ecca29920dbdc86e446efede0 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 02:36:33 +0200
Subject: [PATCH 093/268] Update existing slash commands to findChar
- Update /gen to utilize new char find functionality
- Update /go to utilize new char find functionality
- Update /delname to utilize new char find functionality
- Change /send persona search to equalsIgnoreCaseAndAccents
---
public/scripts/slash-commands.js | 54 +++++++++++++++++---------------
1 file changed, 28 insertions(+), 26 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 2f2e27fdd..95e8586ec 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -461,12 +461,14 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'go',
callback: goToCharacterCallback,
+ returns: 'The character/group name',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('all'),
+ forceEnum: true,
}),
],
helpString: 'Opens up a chat with the character or group by its name',
@@ -531,7 +533,7 @@ export function initDefaultSlashCommands() {
namedArgumentList: [],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
@@ -936,7 +938,7 @@ export function initDefaultSlashCommands() {
),
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'in-prompt name for instruct mode',
+ description: 'in-prompt character name for instruct mode (or unique character identifier (avatar key), which will be used as name)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'System',
enumProvider: () => [...commonEnumProviders.characters('character')(), new SlashCommandEnumValue('System', null, enumTypes.enum, enumIcons.assistant)],
@@ -2373,7 +2375,8 @@ async function generateCallback(args, value) {
setEphemeralStopStrings(resolveVariable(args?.stop));
const name = args?.name;
- const result = await generateQuietPrompt(value, quietToLoud, false, '', name, length);
+ const char = findChar({ name: name });
+ const result = await generateQuietPrompt(value, quietToLoud, false, '', char?.name ?? name, length);
return result;
} catch (err) {
console.error('Error on /gen generation', err);
@@ -2895,7 +2898,7 @@ function findPersonaByName(name) {
}
for (const persona of Object.entries(power_user.personas)) {
- if (persona[1].toLowerCase() === name.toLowerCase()) {
+ if (equalsIgnoreCaseAndAccents(persona[1], name)) {
return persona[0];
}
}
@@ -2938,7 +2941,9 @@ async function deleteMessagesByNameCallback(_, name) {
return;
}
- name = name.trim();
+ // Search for a matching character to get the real name, or take the name provided
+ const character = findChar({ name: name });
+ name = character?.name || name;
const messagesToDelete = [];
chat.forEach((value) => {
@@ -2996,31 +3001,28 @@ async function goToCharacterCallback(_, name) {
return;
}
- name = name.trim();
- const characterIndex = findCharacterIndex(name);
-
- if (characterIndex !== -1) {
- await openChat(new String(characterIndex));
- setActiveCharacter(characters[characterIndex]?.avatar);
+ const character = findChar({ name: name });
+ if (character) {
+ const chid = getCharIndex(character);
+ await openChat(new String(chid));
+ setActiveCharacter(character.avatar);
setActiveGroup(null);
- return characters[characterIndex]?.name;
- } else {
- const group = groups.find(it => it.name.toLowerCase() == name.toLowerCase());
- if (group) {
- await openGroupById(group.id);
- setActiveCharacter(null);
- setActiveGroup(group.id);
- return group.name;
- } else {
- console.warn(`No matches found for name "${name}"`);
- return '';
- }
+ return character.name;
}
+ const group = groups.find(it => equalsIgnoreCaseAndAccents(it.name, name));
+ if (group) {
+ await openGroupById(group.id);
+ setActiveCharacter(null);
+ setActiveGroup(group.id);
+ return group.name;
+ }
+ console.warn(`No matches found for name "${name}"`);
+ return '';
}
-async function openChat(id) {
+async function openChat(chid) {
resetSelectedGroup();
- setCharacterId(id);
+ setCharacterId(chid);
await delay(1);
await reloadCurrentChat();
}
From edcf52e3a8ece77bb845b85d20465b3a8dccfb0f Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 02:54:12 +0200
Subject: [PATCH 094/268] Update more commands for new char find
- Update /member-add to utilize new char find functionality
- Update /tag-add, /tag-remove, /tag-exists and /tag-list to utilize new char find functionality
. Update /lastsprite to utilize new char find functionality
---
public/scripts/extensions/expressions/index.js | 11 +++++++++--
public/scripts/extensions/gallery/index.js | 1 +
public/scripts/slash-commands.js | 17 +++++++----------
public/scripts/tags.js | 14 ++++++++------
4 files changed, 25 insertions(+), 18 deletions(-)
diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js
index 71be1e422..dc8ce8ba6 100644
--- a/public/scripts/extensions/expressions/index.js
+++ b/public/scripts/extensions/expressions/index.js
@@ -12,6 +12,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { isFunctionCallingSupported } from '../../openai.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
+import { findChar } from '../../slash-commands.js';
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
@@ -2105,14 +2106,20 @@ function migrateSettings() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'lastsprite',
- callback: (_, value) => lastExpression[String(value).trim()] ?? '',
+ callback: (_, name) => {
+ if (typeof name !== 'string') throw new Error('name must be a string');
+ const char = findChar({ name: name });
+ const sprite = lastExpression[char?.name ?? name] ?? '';
+ return sprite;
+ },
returns: 'the last set sprite / expression for the named character.',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
+ forceEnum: true,
}),
],
helpString: 'Returns the last set sprite / expression for the named character.',
diff --git a/public/scripts/extensions/gallery/index.js b/public/scripts/extensions/gallery/index.js
index da617c683..a39634603 100644
--- a/public/scripts/extensions/gallery/index.js
+++ b/public/scripts/extensions/gallery/index.js
@@ -441,6 +441,7 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
description: 'character name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
+ forceEnum: true,
}),
SlashCommandNamedArgument.fromProps({
name: 'group',
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 95e8586ec..4be6bca7b 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -210,7 +210,7 @@ export function initDefaultSlashCommands() {
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: commonEnumProviders.characters('character'),
forceEnum: false,
@@ -700,7 +700,7 @@ export function initDefaultSlashCommands() {
aliases: ['addmember', 'memberadd'],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: () => selected_group ? commonEnumProviders.characters('character')() : [],
@@ -2810,26 +2810,23 @@ async function removeGroupMemberCallback(_, arg) {
return '';
}
-async function addGroupMemberCallback(_, arg) {
+async function addGroupMemberCallback(_, name) {
if (!selected_group) {
toastr.warning('Cannot run /memberadd command outside of a group chat.');
return '';
}
- if (!arg) {
+ if (!name) {
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}`);
+ const character = findChar({ name: name, preferCurrentChar: false });
+ if (!character) {
+ console.warn(`WARN: No character found for argument ${name}`);
return '';
}
- const character = characters[chid];
const group = groups.find(x => x.id === selected_group);
if (!group || !Array.isArray(group.members)) {
diff --git a/public/scripts/tags.js b/public/scripts/tags.js
index c56e0d1cb..447e01ded 100644
--- a/public/scripts/tags.js
+++ b/public/scripts/tags.js
@@ -26,6 +26,7 @@ import { debounce_timeout } from './constants.js';
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { renderTemplateAsync } from './templates.js';
+import { findChar } from './slash-commands.js';
export {
TAG_FOLDER_TYPES,
@@ -507,7 +508,7 @@ export function getTagKeyForEntityElement(element) {
*/
export function searchCharByName(charName, { suppressLogging = false } = {}) {
const entity = charName
- ? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))
+ ? (findChar({ name: charName }) || groups.find(x => equalsIgnoreCaseAndAccents(x.name, charName)))
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
const key = getTagKeyForEntity(entity);
if (!key) {
@@ -1861,8 +1862,9 @@ function registerTagsSlashCommands() {
return String(result);
},
namedArgumentList: [
- SlashCommandNamedArgument.fromProps({ name: 'name',
- description: 'Character name',
+ SlashCommandNamedArgument.fromProps({
+ name: 'name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@@ -1907,7 +1909,7 @@ function registerTagsSlashCommands() {
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: 'name',
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@@ -1950,7 +1952,7 @@ function registerTagsSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
@@ -1993,7 +1995,7 @@ function registerTagsSlashCommands() {
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
- description: 'Character name',
+ description: 'Character name - or unique character identifier (avatar key)',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: '{{char}}',
enumProvider: commonEnumProviders.characters(),
From d7bad6335c34744a7c58875c1d075d61f69cc333 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 03:20:01 +0200
Subject: [PATCH 095/268] Refactor findChar to utils
- Refactor and move finChar to utils, instead of slashcommands function
- Refactor scrapers to use actual init functionality
---
public/script.js | 3 +-
.../scripts/extensions/expressions/index.js | 3 +-
public/scripts/scrapers.js | 21 ++--
public/scripts/slash-commands.js | 98 +------------------
public/scripts/tags.js | 4 +-
public/scripts/utils.js | 75 +++++++++++++-
6 files changed, 94 insertions(+), 110 deletions(-)
diff --git a/public/script.js b/public/script.js
index 6d93bc03b..b0ca08f6a 100644
--- a/public/script.js
+++ b/public/script.js
@@ -230,7 +230,7 @@ import { MacrosParser, evaluateMacros, getLastMessageId } from './scripts/macros
import { currentUser, setUserControls } from './scripts/user.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js';
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
-import { ScraperManager } from './scripts/scrapers.js';
+import { initScrapers, ScraperManager } from './scripts/scrapers.js';
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js';
@@ -959,6 +959,7 @@ async function firstLoadInit() {
initCfg();
initLogprobs();
initInputMarkdown();
+ initScrapers();
doDailyExtensionUpdatesCheck();
await hideLoader();
await fixViewport();
diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js
index dc8ce8ba6..edb19ff05 100644
--- a/public/scripts/extensions/expressions/index.js
+++ b/public/scripts/extensions/expressions/index.js
@@ -2,7 +2,7 @@ import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, ma
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, power_user } from '../../power-user.js';
-import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';
+import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition, findChar } from '../../utils.js';
import { hideMutedSprites } from '../../group-chats.js';
import { isJsonSchemaSupported } from '../../textgen-settings.js';
import { debounce_timeout } from '../../constants.js';
@@ -12,7 +12,6 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { isFunctionCallingSupported } from '../../openai.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
-import { findChar } from '../../slash-commands.js';
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
diff --git a/public/scripts/scrapers.js b/public/scripts/scrapers.js
index 86d2ba567..09d42edb1 100644
--- a/public/scripts/scrapers.js
+++ b/public/scripts/scrapers.js
@@ -13,6 +13,7 @@ import { isValidUrl } from './utils.js';
* @property {string} description
* @property {string} iconClass
* @property {boolean} iconAvailable
+ * @property {() => Promise} [init=null]
* @property {() => Promise} isAvailable
* @property {() => Promise} scrape
*/
@@ -42,6 +43,10 @@ export class ScraperManager {
return;
}
+ if (scraper.init) {
+ scraper.init();
+ }
+
ScraperManager.#scrapers.push(scraper);
}
@@ -462,7 +467,9 @@ class YouTubeScraper {
this.description = 'Download a transcript from a YouTube video.';
this.iconClass = 'fa-brands fa-youtube';
this.iconAvailable = true;
+ }
+ async init() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'yt-script',
callback: async (args, url) => {
@@ -564,9 +571,11 @@ class YouTubeScraper {
}
}
-ScraperManager.registerDataBankScraper(new FileScraper());
-ScraperManager.registerDataBankScraper(new Notepad());
-ScraperManager.registerDataBankScraper(new WebScraper());
-ScraperManager.registerDataBankScraper(new MediaWikiScraper());
-ScraperManager.registerDataBankScraper(new FandomScraper());
-ScraperManager.registerDataBankScraper(new YouTubeScraper());
+export function initScrapers() {
+ ScraperManager.registerDataBankScraper(new FileScraper());
+ ScraperManager.registerDataBankScraper(new Notepad());
+ ScraperManager.registerDataBankScraper(new WebScraper());
+ ScraperManager.registerDataBankScraper(new MediaWikiScraper());
+ ScraperManager.registerDataBankScraper(new FandomScraper());
+ ScraperManager.registerDataBankScraper(new YouTubeScraper());
+}
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 4be6bca7b..1e961beaa 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
-import { debounce, delay, equalsIgnoreCaseAndAccents, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
+import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -68,10 +68,8 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
-import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
-import { getTagsList } from './tags.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@@ -2969,29 +2967,6 @@ async function deleteMessagesByNameCallback(_, name) {
return '';
}
-function findCharacterIndex(name) {
- const matchTypes = [
- (a, b) => a === b,
- (a, b) => a.startsWith(b),
- (a, b) => a.includes(b),
- ];
-
- const exactAvatarMatch = characters.findIndex(x => x.avatar === name);
-
- if (exactAvatarMatch !== -1) {
- return exactAvatarMatch;
- }
-
- for (const matchType of matchTypes) {
- const index = characters.findIndex(x => matchType(x.name.toLowerCase(), name.toLowerCase()));
- if (index !== -1) {
- return index;
- }
- }
-
- return -1;
-}
-
async function goToCharacterCallback(_, name) {
if (!name) {
console.warn('WARN: No character name provided for /go command');
@@ -3238,77 +3213,6 @@ export function getNameAndAvatarForMessage(character, name = null) {
};
}
-/**
- * Finds a character by name, with optional filtering and precedence for avatars
- * @param {object} [options={}] - The options for the search
- * @param {string?} [options.name=null] - The name to search for
- * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
- * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
- * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
- * @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s)
- * @param {boolean} [options.quiet=false] - Whether to suppress warnings
- * @returns {any?} - The found character or null if not found
- */
-export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
- const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
-
- // Filter characters by tags if provided
- let filteredCharacters = characters;
- if (filteredByTags) {
- filteredCharacters = characters.filter(char => {
- const charTags = getTagsList(char.avatar, false);
- return filteredByTags.every(tagName => charTags.some(x => x.name == tagName));
- });
- }
-
- // Get the current character(s)
- /** @type {any[]} */
- const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => filteredCharacters.find(char => char.avatar === member))
- : [filteredCharacters.find(char => characters[this_chid]?.avatar === char.avatar)];
-
- // If we have a current char and prefer it, return that if it matches
- if (preferCurrentChar) {
- const preferredCharSearch = currentChars.filter(matches);
- if (preferredCharSearch.length > 1) {
- if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
- else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
- }
- if (preferredCharSearch.length) {
- return preferredCharSearch[0];
- }
- }
-
- // If allowAvatar is true, search by avatar first
- if (allowAvatar && name) {
- const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
- if (characterByAvatar) {
- return characterByAvatar;
- }
- }
-
- // Search for matching characters by name
- const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
- if (matchingCharacters.length > 1) {
- if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
- else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
- }
-
- return matchingCharacters[0] || null;
-}
-
-/**
- * Gets the index of a character based on the character object
- * @param {object} char - The character object to find the index for
- * @throws {Error} If the character is not found
- * @returns {number} The index of the character in the characters array
- */
-export function getCharIndex(char) {
- if (!char) throw new Error('Character is undefined');
- const index = characters.findIndex(c => c.avatar === char.avatar);
- if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
- return index;
-}
-
export async function sendMessageAs(args, text) {
if (!text) {
return '';
diff --git a/public/scripts/tags.js b/public/scripts/tags.js
index 447e01ded..9a1950d0a 100644
--- a/public/scripts/tags.js
+++ b/public/scripts/tags.js
@@ -15,7 +15,7 @@ import {
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
-import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce } from './utils.js';
+import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight, equalsIgnoreCaseAndAccents, includesIgnoreCaseAndAccents, removeFromArray, getFreeName, debounce, findChar } from './utils.js';
import { power_user } from './power-user.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -26,7 +26,6 @@ import { debounce_timeout } from './constants.js';
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { renderTemplateAsync } from './templates.js';
-import { findChar } from './slash-commands.js';
export {
TAG_FOLDER_TYPES,
@@ -51,7 +50,6 @@ export {
removeTagFromMap,
};
-/** @typedef {import('../scripts/popup.js').Popup} Popup */
/** @typedef {import('../script.js').Character} Character */
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index 9fc1e1e99..9ddd0413c 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -1,10 +1,12 @@
import { getContext } from './extensions.js';
-import { getRequestHeaders } from '../script.js';
+import { characters, getRequestHeaders, this_chid } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js';
import { debounce_timeout } from './constants.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
+import { getTagsList } from './tags.js';
+import { groups, selected_group } from './group-chats.js';
/**
* Pagination status string template.
@@ -2110,3 +2112,74 @@ export async function showFontAwesomePicker(customList = null) {
}
return null;
}
+
+/**
+ * Finds a character by name, with optional filtering and precedence for avatars
+ * @param {object} [options={}] - The options for the search
+ * @param {string?} [options.name=null] - The name to search for
+ * @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
+ * @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
+ * @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
+ * @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s)
+ * @param {boolean} [options.quiet=false] - Whether to suppress warnings
+ * @returns {any?} - The found character or null if not found
+ */
+export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
+ const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
+
+ // Filter characters by tags if provided
+ let filteredCharacters = characters;
+ if (filteredByTags) {
+ filteredCharacters = characters.filter(char => {
+ const charTags = getTagsList(char.avatar, false);
+ return filteredByTags.every(tagName => charTags.some(x => x.name == tagName));
+ });
+ }
+
+ // Get the current character(s)
+ /** @type {any[]} */
+ const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => filteredCharacters.find(char => char.avatar === member))
+ : [filteredCharacters.find(char => characters[this_chid]?.avatar === char.avatar)];
+
+ // If we have a current char and prefer it, return that if it matches
+ if (preferCurrentChar) {
+ const preferredCharSearch = currentChars.filter(matches);
+ if (preferredCharSearch.length > 1) {
+ if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
+ else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
+ }
+ if (preferredCharSearch.length) {
+ return preferredCharSearch[0];
+ }
+ }
+
+ // If allowAvatar is true, search by avatar first
+ if (allowAvatar && name) {
+ const characterByAvatar = filteredCharacters.find(char => char.avatar === name);
+ if (characterByAvatar) {
+ return characterByAvatar;
+ }
+ }
+
+ // Search for matching characters by name
+ const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
+ if (matchingCharacters.length > 1) {
+ if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
+ else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
+ }
+
+ return matchingCharacters[0] || null;
+}
+
+/**
+ * Gets the index of a character based on the character object
+ * @param {object} char - The character object to find the index for
+ * @throws {Error} If the character is not found
+ * @returns {number} The index of the character in the characters array
+ */
+export function getCharIndex(char) {
+ if (!char) throw new Error('Character is undefined');
+ const index = characters.findIndex(c => c.avatar === char.avatar);
+ if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
+ return index;
+}
From 44c8d45957d99190489a449f4705301ac9984eb5 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 04:02:00 +0200
Subject: [PATCH 096/268] missing import
---
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 1e961beaa..157843515 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
-import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
+import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
From ab83138b1ee682baf193980b589c64fb08cf5e00 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 11:02:54 +0200
Subject: [PATCH 097/268] missing closing tag
---
public/scripts/slash-commands.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 157843515..64f6fc83d 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -289,6 +289,7 @@ export function initDefaultSlashCommands() {
/sendas name="Chloe" avatar="BigBadBoss" Hehehe, I am the big bad evil, fear me.
will send a message as the character "Chloe", but utilizing the avatar from a character named "BigBadBoss".
+
From cc527a3a3335c29bce38cf8c23bb097a4f51c7e6 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 11:27:07 +0200
Subject: [PATCH 098/268] Correct async calls to init scrapers
---
public/script.js | 2 +-
public/scripts/scrapers.js | 18 +++++++++---------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/public/script.js b/public/script.js
index b0ca08f6a..b64104cc5 100644
--- a/public/script.js
+++ b/public/script.js
@@ -959,7 +959,7 @@ async function firstLoadInit() {
initCfg();
initLogprobs();
initInputMarkdown();
- initScrapers();
+ await initScrapers();
doDailyExtensionUpdatesCheck();
await hideLoader();
await fixViewport();
diff --git a/public/scripts/scrapers.js b/public/scripts/scrapers.js
index 09d42edb1..5a21dedd0 100644
--- a/public/scripts/scrapers.js
+++ b/public/scripts/scrapers.js
@@ -37,14 +37,14 @@ export class ScraperManager {
* Register a scraper to be used by the Data Bank.
* @param {Scraper} scraper Instance of a scraper to register
*/
- static registerDataBankScraper(scraper) {
+ static async registerDataBankScraper(scraper) {
if (ScraperManager.#scrapers.some(s => s.id === scraper.id)) {
console.warn(`Scraper with ID ${scraper.id} already registered`);
return;
}
if (scraper.init) {
- scraper.init();
+ await scraper.init();
}
ScraperManager.#scrapers.push(scraper);
@@ -571,11 +571,11 @@ class YouTubeScraper {
}
}
-export function initScrapers() {
- ScraperManager.registerDataBankScraper(new FileScraper());
- ScraperManager.registerDataBankScraper(new Notepad());
- ScraperManager.registerDataBankScraper(new WebScraper());
- ScraperManager.registerDataBankScraper(new MediaWikiScraper());
- ScraperManager.registerDataBankScraper(new FandomScraper());
- ScraperManager.registerDataBankScraper(new YouTubeScraper());
+export async function initScrapers() {
+ await ScraperManager.registerDataBankScraper(new FileScraper());
+ await ScraperManager.registerDataBankScraper(new Notepad());
+ await ScraperManager.registerDataBankScraper(new WebScraper());
+ await ScraperManager.registerDataBankScraper(new MediaWikiScraper());
+ await ScraperManager.registerDataBankScraper(new FandomScraper());
+ await ScraperManager.registerDataBankScraper(new YouTubeScraper());
}
From c3d5fba598ca07a36ff28b3697fd6403c628682e Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Sun, 29 Sep 2024 12:48:40 +0300
Subject: [PATCH 099/268] Support multiple in /setpromptentry
---
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 37e99615d..d9bdd4e7a 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -3561,11 +3561,17 @@ function setPromptEntryCallback(args, targetState) {
const prompts = promptManager.serviceSettings.prompts;
function parseArgs(arg) {
+ // Arg is already an array
+ if (Array.isArray(arg)) {
+ return arg;
+ }
const list = [];
try {
+ // Arg is a JSON-stringified array
const parsedArg = JSON.parse(arg);
list.push(...Array.isArray(parsedArg) ? parsedArg : [arg]);
} catch {
+ // Arg is a string
list.push(arg);
}
return list;
From 8e445c75ae706cc636cb6e7b306a695985fc48d0 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 13:38:15 +0200
Subject: [PATCH 100/268] Fix bug with undefined char on empty chats
---
public/scripts/utils.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index 9ddd0413c..93c752a9c 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -2139,7 +2139,7 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
// Get the current character(s)
/** @type {any[]} */
const currentChars = selected_group ? groups.find(group => group.id === selected_group)?.members.map(member => filteredCharacters.find(char => char.avatar === member))
- : [filteredCharacters.find(char => characters[this_chid]?.avatar === char.avatar)];
+ : filteredCharacters.filter(char => characters[this_chid]?.avatar === char.avatar);
// If we have a current char and prefer it, return that if it matches
if (preferCurrentChar) {
From 034a5a48c25b934398149503e0a69f63e2655669 Mon Sep 17 00:00:00 2001
From: RossAscends <124905043+RossAscends@users.noreply.github.com>
Date: Sun, 29 Sep 2024 21:47:18 +0900
Subject: [PATCH 101/268] initial commit, functional, needs proofing
---
public/index.html | 6 ++++-
public/script.js | 32 +++++++++++++++++++------
public/scripts/power-user.js | 45 ++++++++++++++++++++++++++++++++++++
3 files changed, 75 insertions(+), 8 deletions(-)
diff --git a/public/index.html b/public/index.html
index 13f59da05..e6f70355a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4011,6 +4011,10 @@
Compact Input Area
+
+
+ Swipe # for All Messages
+
Characters Hotswap
@@ -5939,7 +5943,7 @@
-
+
diff --git a/public/script.js b/public/script.js
index 6d93bc03b..cf3061708 100644
--- a/public/script.js
+++ b/public/script.js
@@ -83,6 +83,7 @@ import {
resetMovableStyles,
forceCharacterEditorTokenize,
applyPowerUserSettings,
+ switchSwipeNumAllMessages,
} from './scripts/power-user.js';
import {
@@ -2368,9 +2369,20 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
addCopyToCodeBlocks(newMessage);
+ //const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
+
+ // Set the swipes counter for past messages, only visible if 'Show Sipes on All Message' is enabled
+ if (!params.isUser && newMessageId !== 0) {
+ const swipesNum = chat[newMessageId].swipes?.length;
+ const swipeId = chat[newMessageId].swipe_id + 1;
+ newMessage.find('.swipes-counter').text(`${swipeId}\u200B/\u200b${swipesNum}`);
+ }
+
+
if (showSwipes) {
$('#chat .mes').last().addClass('last_mes');
- $('#chat .mes').eq(-2).removeClass('last_mes');
+ $('#chat .mes').eq(-2).removeClass('last_mes')
+ .find('.swipe_right').removeClass('fa-chevron-right'); //otherwise it stays looking like it did when it was last_mes
hideSwipeButtons();
showSwipeButtons();
}
@@ -7489,19 +7501,25 @@ export function showSwipeButtons() {
//console.log((chat[chat.length - 1]));
if ((chat[chat.length - 1].swipes.length - swipeId) === 1) {
//console.log('highlighting R swipe');
- currentMessage.children('.swipe_right').css('opacity', '0.7');
+
+ //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not
+ //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on
+ currentMessage.children('.swipe_right').addClass('fa-chevron-right').css('opacity', '0.7');
}
//console.log(swipesCounterHTML);
- $('.swipes-counter').text(swipeCounterText);
+ //allows for writing individual swipe counters for past messages
+ $('.last_mes .swipes-counter').text(swipeCounterText);
//console.log(swipeId);
//console.log(chat[chat.length - 1].swipes.length);
+
+ switchSwipeNumAllMessages();
}
export function hideSwipeButtons() {
//console.log('hideswipebuttons entered');
- $('#chat').find('.swipe_right').css('display', 'none');
+ $('#chat').find('.last_mes .swipe_right').css('display', 'none');
$('#chat').find('.swipe_left').css('display', 'none');
}
@@ -9395,9 +9413,9 @@ jQuery(async function () {
///// SWIPE BUTTON CLICKS ///////
- $(document).on('click', '.swipe_right', swipe_right);
-
- $(document).on('click', '.swipe_left', swipe_left);
+ //limit swiping to only last message clicks
+ $(document).on('click', '.last_mes .swipe_right', swipe_right);
+ $(document).on('click', '.last_mes .swipe_left', swipe_left);
const debouncedCharacterSearch = debounce((searchQuery) => {
entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery);
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index 8b2a81a16..bfb82a6a5 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -290,6 +290,7 @@ let power_user = {
restore_user_input: true,
reduced_motion: false,
compact_input_area: true,
+ show_swipe_num_all_messages: false,
auto_connect: false,
auto_load_chat: false,
forbid_external_media: true,
@@ -469,6 +470,35 @@ function switchCompactInputArea() {
$('#compact_input_area').prop('checked', power_user.compact_input_area);
}
+export function switchSwipeNumAllMessages() {
+ console.error('switching branch button initialted, function start!');
+ $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
+
+ if (power_user.show_swipe_num_all_messages) {
+
+ $('.mes').each(function () {
+ //if the div also has the .lst_mes class, skip the loop for that item
+ if ($(this).hasClass('last_mes')) {
+ return;
+ }
+ //add the cloned button to every .mes .swipe_right EXCLUDING .mes.last_mes
+ $(this).find('.swipe_right').css('display', 'flex');
+ });
+
+ } else if (!power_user.show_swipe_num_all_messages) {
+ $('.mes:not(.last_mes)').each(function () {
+ if ($(this).hasClass('last_mes')) {
+ return;
+ }
+ //add the cloned button back to its original spot
+ $(this).find('.swipe_right').css('display', 'none');
+ });
+
+ }
+
+
+}
+
var originalSliderValues = [];
async function switchLabMode() {
@@ -1283,6 +1313,13 @@ function applyTheme(name) {
switchCompactInputArea();
},
},
+ {
+ key: 'show_swipe_num_all_messages',
+ action: () => {
+ $('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
+ switchSwipeNumAllMessages();
+ },
+ },
];
for (const { key, selector, type, action } of themeProperties) {
@@ -1352,6 +1389,7 @@ function applyPowerUserSettings() {
switchHideChatAvatars();
switchTokenCount();
switchMessageActions();
+ switchSwipeNumAllMessages();
}
function getExampleMessagesBehavior() {
@@ -2296,6 +2334,7 @@ function getThemeObject(name) {
zoomed_avatar_magnification: power_user.zoomed_avatar_magnification,
reduced_motion: power_user.reduced_motion,
compact_input_area: power_user.compact_input_area,
+ show_swipe_num_all_messages: power_user.show_swipe_num_all_messages,
};
}
@@ -3755,6 +3794,12 @@ $(document).ready(() => {
saveSettingsDebounced();
});
+ $('#show_swipe_num_all_messages').on('input', function () {
+ power_user.show_swipe_num_all_messages = !!$(this).prop('checked');
+ switchSwipeNumAllMessages();
+ saveSettingsDebounced();
+ });
+
$('#auto-connect-checkbox').on('input', function () {
power_user.auto_connect = !!$(this).prop('checked');
saveSettingsDebounced();
From 3614740f4e885637afe78c49bfa466267aa68648 Mon Sep 17 00:00:00 2001
From: RossAscends <124905043+RossAscends@users.noreply.github.com>
Date: Sun, 29 Sep 2024 21:53:06 +0900
Subject: [PATCH 102/268] remove gaudy console log
---
public/scripts/power-user.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index bfb82a6a5..e42981ded 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -471,7 +471,6 @@ function switchCompactInputArea() {
}
export function switchSwipeNumAllMessages() {
- console.error('switching branch button initialted, function start!');
$('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
if (power_user.show_swipe_num_all_messages) {
From 9f0c2300d2d36e43c609fbf75d97e9d68a872c1e Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Sun, 29 Sep 2024 16:24:40 +0300
Subject: [PATCH 103/268] Add connection map aliases
---
public/script.js | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/public/script.js b/public/script.js
index 6d93bc03b..2e8b8d736 100644
--- a/public/script.js
+++ b/public/script.js
@@ -8476,12 +8476,22 @@ const CONNECT_API_MAP = {
selected: 'novel',
button: '#api_button_novel',
},
+ 'koboldcpp': {
+ selected: 'textgenerationwebui',
+ button: '#api_button_textgenerationwebui',
+ type: textgen_types.KOBOLDCPP,
+ },
// KoboldCpp alias
'kcpp': {
selected: 'textgenerationwebui',
button: '#api_button_textgenerationwebui',
type: textgen_types.KOBOLDCPP,
},
+ 'openai': {
+ selected: 'openai',
+ button: '#api_button_openai',
+ source: chat_completion_sources.OPENAI,
+ },
// OpenAI alias
'oai': {
selected: 'openai',
From 7c2fcbc522dc9f80093dde70b1967acad8c113b8 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 17:00:28 +0200
Subject: [PATCH 104/268] Fix non-provided name not correctly preferring cur
---
public/scripts/utils.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index 93c752a9c..6cf92f5c0 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -2125,7 +2125,7 @@ export async function showFontAwesomePicker(customList = null) {
* @returns {any?} - The found character or null if not found
*/
export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
- const matches = (char) => (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
+ const matches = (char) => !name || (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
// Filter characters by tags if provided
let filteredCharacters = characters;
@@ -2145,8 +2145,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
if (preferCurrentChar) {
const preferredCharSearch = currentChars.filter(matches);
if (preferredCharSearch.length > 1) {
- if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
- else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
+ if (!quiet) toastr.warning('Multiple characters found for given conditions.');
+ else console.warn('Multiple characters found for given conditions. Returning the first match.');
}
if (preferredCharSearch.length) {
return preferredCharSearch[0];
@@ -2164,8 +2164,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
// Search for matching characters by name
const matchingCharacters = name ? filteredCharacters.filter(matches) : filteredCharacters;
if (matchingCharacters.length > 1) {
- if (!quiet) toastr.warning(`Multiple characters found for name "${name}" and given conditions.`);
- else console.warn(`Multiple characters found for name "${name}". Returning the first match.`);
+ if (!quiet) toastr.warning('Multiple characters found for given conditions.');
+ else console.warn('Multiple characters found for given conditions. Returning the first match.');
}
return matchingCharacters[0] || null;
From 314771fd9a1a5d9ec6ae3677ae0daf4992018565 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Sun, 29 Sep 2024 17:32:18 +0200
Subject: [PATCH 105/268] Fix /sendas wrong name being used
---
public/scripts/slash-commands.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index aca7e9e03..4efb90e65 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -3260,10 +3260,10 @@ export async function sendMessageAs(args, text) {
return '';
}
- const { name: nameForMessage, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
+ const { name: avatarCharName, force_avatar, original_avatar } = getNameAndAvatarForMessage(avatarCharacter, name);
const message = {
- name: nameForMessage,
+ name: character?.name || name || avatarCharName,
is_user: false,
is_system: isSystem,
send_date: getMessageTimeStamp(),
From 2f43c8e227205a31bb8adb1110ea9d549e4cef17 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Sun, 29 Sep 2024 19:12:26 +0300
Subject: [PATCH 106/268] Fix /ask in neutral chats
---
public/scripts/slash-commands.js | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index d9bdd4e7a..eb81a0218 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -2530,8 +2530,13 @@ async function askCharacter(args, text) {
return;
}
- setCharacterId(prevChId);
- setCharacterName(characters[prevChId].name);
+ if (prevChId !== undefined) {
+ setCharacterId(prevChId);
+ setCharacterName(characters[prevChId].name);
+ } else {
+ setCharacterId(undefined);
+ setCharacterName(neutralCharacterName);
+ }
// Only force the new avatar if the character name is the same
// This skips if an error was fired
From 8061f453687bfc95edb3de7a3d6702dd48c42bd8 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Sun, 29 Sep 2024 19:13:19 +0300
Subject: [PATCH 107/268] RossMods: Debounce online status display
---
public/scripts/RossAscends-mods.js | 33 +++++++++++++++---------------
1 file changed, 17 insertions(+), 16 deletions(-)
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js
index 92278f35c..1a9e2d7d2 100644
--- a/public/scripts/RossAscends-mods.js
+++ b/public/scripts/RossAscends-mods.js
@@ -56,22 +56,25 @@ let counterNonce = Date.now();
const observerConfig = { childList: true, subtree: true };
const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
+const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
+const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
+ if (!(mutation.target instanceof HTMLElement)) {
+ return;
+ }
if (mutation.target.classList.contains('online_status_text')) {
- RA_checkOnlineStatus();
+ checkStatusDebounced();
} else if (mutation.target.parentNode === SelectedCharacterTab) {
- setTimeout(RA_CountCharTokens, 200);
+ countTokensShortDebounced();
} else if (mutation.target.classList.contains('mes_text')) {
- if (mutation.target instanceof HTMLElement) {
- for (const element of mutation.target.getElementsByTagName('math')) {
- element.childNodes.forEach(function (child) {
- if (child.nodeType === Node.TEXT_NODE) {
- child.textContent = '';
- }
- });
- }
+ for (const element of mutation.target.getElementsByTagName('math')) {
+ element.childNodes.forEach(function (child) {
+ if (child.nodeType === Node.TEXT_NODE) {
+ child.textContent = '';
+ }
+ });
}
}
});
@@ -159,8 +162,8 @@ export function shouldSendOnEnter() {
export function humanizedDateTime() {
const now = new Date(Date.now());
const dt = {
- year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
- hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
+ year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
+ hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
};
for (const key in dt) {
dt[key] = dt[key].toString().padStart(2, '0');
@@ -725,9 +728,7 @@ export function addSafariPatch() {
export function initRossMods() {
// initial status check
- setTimeout(() => {
- RA_checkOnlineStatus();
- }, 100);
+ checkStatusDebounced();
if (power_user.auto_load_chat) {
RA_autoloadchat();
@@ -752,7 +753,7 @@ export function initRossMods() {
setTimeout(() => RA_autoconnect(PrevAPI), 100);
});
- $('#api_button').click(function () { setTimeout(RA_checkOnlineStatus, 100); });
+ $('#api_button').on('click', () => checkStatusDebounced());
//toggle pin class when lock toggle clicked
$(RPanelPin).on('click', function () {
From 885a278973253ecfb530f95fb92dd3dcfd94b24a Mon Sep 17 00:00:00 2001
From: RossAscends <124905043+RossAscends@users.noreply.github.com>
Date: Mon, 30 Sep 2024 19:51:13 +0900
Subject: [PATCH 108/268] split counter from chevron, smarter toggling
---
public/index.html | 3 ++-
public/script.js | 28 ++++++++++++++--------------
public/scripts/power-user.js | 25 +------------------------
public/style.css | 18 +++++++++++++++---
4 files changed, 32 insertions(+), 42 deletions(-)
diff --git a/public/index.html b/public/index.html
index e6f70355a..70d6079e2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5943,7 +5943,8 @@
-
+
+
diff --git a/public/script.js b/public/script.js
index cf3061708..cf5c29d61 100644
--- a/public/script.js
+++ b/public/script.js
@@ -83,7 +83,6 @@ import {
resetMovableStyles,
forceCharacterEditorTokenize,
applyPowerUserSettings,
- switchSwipeNumAllMessages,
} from './scripts/power-user.js';
import {
@@ -2381,8 +2380,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
if (showSwipes) {
$('#chat .mes').last().addClass('last_mes');
- $('#chat .mes').eq(-2).removeClass('last_mes')
- .find('.swipe_right').removeClass('fa-chevron-right'); //otherwise it stays looking like it did when it was last_mes
+ $('#chat .mes').eq(-2).removeClass('last_mes');
+ //.find('.swipe_right').hide(); //otherwise it stays looking like it did when it was last_mes
hideSwipeButtons();
showSwipeButtons();
}
@@ -7495,16 +7494,17 @@ export function showSwipeButtons() {
}
//only show right when generate is off, or when next right swipe would not make a generate happen
if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) {
- currentMessage.children('.swipe_right').css('display', 'flex');
- currentMessage.children('.swipe_right').css('opacity', '0.3');
+ console.error('showingSwipeButtons: showing.');
+ currentMessage.find('.swipe_right').css('display', 'flex');
+ currentMessage.find('.swipe_right').css('opacity', '0.3');
}
//console.log((chat[chat.length - 1]));
if ((chat[chat.length - 1].swipes.length - swipeId) === 1) {
- //console.log('highlighting R swipe');
+ console.error('highlighting R swipe');
//chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not
//necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on
- currentMessage.children('.swipe_right').addClass('fa-chevron-right').css('opacity', '0.7');
+ currentMessage.find('.swipe_right').css('opacity', '0.7');
}
//console.log(swipesCounterHTML);
@@ -7514,13 +7514,13 @@ export function showSwipeButtons() {
//console.log(swipeId);
//console.log(chat[chat.length - 1].swipes.length);
- switchSwipeNumAllMessages();
+ //switchSwipeNumAllMessages();
}
export function hideSwipeButtons() {
- //console.log('hideswipebuttons entered');
- $('#chat').find('.last_mes .swipe_right').css('display', 'none');
- $('#chat').find('.swipe_left').css('display', 'none');
+ console.error('hideswipebuttons entered');
+ $('#chat').find('.swipe_right').hide();
+ $('#chat').find('.swipe_left').hide();
}
/**
@@ -8355,8 +8355,8 @@ const swipe_right = () => {
}
const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
- let this_div = currentMessage.children('.swipe_right');
- let this_mes_div = this_div.parent();
+ let this_div = currentMessage.find('.swipe_right');
+ let this_mes_div = this_div.parent().parent();
if (chat[chat.length - 1]['swipe_id'] > chat[chat.length - 1]['swipes'].length) { //if we swipe right while generating (the swipe ID is greater than what we are viewing now)
chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length; //show that message slot (will be '...' while generating)
@@ -8366,7 +8366,7 @@ const swipe_right = () => {
}
// handles animated transitions when swipe right, specifically height transitions between messages
if (run_generate || run_swipe_right) {
- let this_mes_block = this_mes_div.children('.mes_block').children('.mes_text');
+ let this_mes_block = this_mes_div.find('.mes_block .mes_text');
const this_mes_div_height = this_mes_div[0].scrollHeight;
const this_mes_block_height = this_mes_block[0].scrollHeight;
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index e42981ded..79c5eb724 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -472,30 +472,7 @@ function switchCompactInputArea() {
export function switchSwipeNumAllMessages() {
$('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
-
- if (power_user.show_swipe_num_all_messages) {
-
- $('.mes').each(function () {
- //if the div also has the .lst_mes class, skip the loop for that item
- if ($(this).hasClass('last_mes')) {
- return;
- }
- //add the cloned button to every .mes .swipe_right EXCLUDING .mes.last_mes
- $(this).find('.swipe_right').css('display', 'flex');
- });
-
- } else if (!power_user.show_swipe_num_all_messages) {
- $('.mes:not(.last_mes)').each(function () {
- if ($(this).hasClass('last_mes')) {
- return;
- }
- //add the cloned button back to its original spot
- $(this).find('.swipe_right').css('display', 'none');
- });
-
- }
-
-
+ $('.mes:not(.last_mes) .swipes-counter').toggle(power_user.show_swipe_num_all_messages);
}
var originalSliderValues = [];
diff --git a/public/style.css b/public/style.css
index 1c8096d86..6420e4b35 100644
--- a/public/style.css
+++ b/public/style.css
@@ -978,13 +978,21 @@ body .panelControlBar {
justify-content: center;
z-index: 9999;
grid-row-start: 2;
- grid-column-start: 4;
- flex-flow: column;
font-size: 30px;
cursor: pointer;
align-self: center;
+
+}
+.swipe_left{
+position: absolute;
+bottom: 15px;
+flex-flow: column;
+}
+
+.swipeRightBlock {
position: absolute;
- bottom: 15px;
+ right: 0;
+ bottom: 0;
}
.swipes-counter {
@@ -994,6 +1002,9 @@ body .panelControlBar {
font-family: var(--mainFontFamily);
font-weight: 400;
align-self: center;
+ min-width: 40px;
+ display: flex;
+ justify-content: center;
}
.swipe_left {
@@ -1003,6 +1014,7 @@ body .panelControlBar {
.swipe_right {
right: 5px;
+ align-self:end;
}
.ui-settings {
From 1a0b49d5dcf3ff73cfc1d86aeb00afa910d07a41 Mon Sep 17 00:00:00 2001
From: RossAscends <124905043+RossAscends@users.noreply.github.com>
Date: Mon, 30 Sep 2024 21:19:39 +0900
Subject: [PATCH 109/268] fix weird counter text in group chats
---
public/script.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/public/script.js b/public/script.js
index cf5c29d61..cc339b228 100644
--- a/public/script.js
+++ b/public/script.js
@@ -2370,8 +2370,8 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
//const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
- // Set the swipes counter for past messages, only visible if 'Show Sipes on All Message' is enabled
- if (!params.isUser && newMessageId !== 0) {
+ // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message' is enabled
+ if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) {
const swipesNum = chat[newMessageId].swipes?.length;
const swipeId = chat[newMessageId].swipe_id + 1;
newMessage.find('.swipes-counter').text(`${swipeId}\u200B/\u200b${swipesNum}`);
@@ -7510,6 +7510,7 @@ export function showSwipeButtons() {
//allows for writing individual swipe counters for past messages
$('.last_mes .swipes-counter').text(swipeCounterText);
+ $('.last_mes .swipes-counter').show();
//console.log(swipeId);
//console.log(chat[chat.length - 1].swipes.length);
@@ -7520,6 +7521,7 @@ export function showSwipeButtons() {
export function hideSwipeButtons() {
console.error('hideswipebuttons entered');
$('#chat').find('.swipe_right').hide();
+ $('#chat').find('.last_mes .swipes-counter').hide();
$('#chat').find('.swipe_left').hide();
}
From e9e459be14bb3d3b00366e01cad7c989c2fe45ac Mon Sep 17 00:00:00 2001
From: RossAscends <124905043+RossAscends@users.noreply.github.com>
Date: Mon, 30 Sep 2024 21:29:24 +0900
Subject: [PATCH 110/268] make sure old swipe counters are shown after new
message added
---
public/script.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/public/script.js b/public/script.js
index cc339b228..8a13e3487 100644
--- a/public/script.js
+++ b/public/script.js
@@ -83,6 +83,7 @@ import {
resetMovableStyles,
forceCharacterEditorTokenize,
applyPowerUserSettings,
+ switchSwipeNumAllMessages,
} from './scripts/power-user.js';
import {
@@ -7515,7 +7516,7 @@ export function showSwipeButtons() {
//console.log(swipeId);
//console.log(chat[chat.length - 1].swipes.length);
- //switchSwipeNumAllMessages();
+ switchSwipeNumAllMessages();
}
export function hideSwipeButtons() {
From 020741d78b403f302c5f6ee7cc7c928463e040f1 Mon Sep 17 00:00:00 2001
From: RossAscends <124905043+RossAscends@users.noreply.github.com>
Date: Mon, 30 Sep 2024 21:49:39 +0900
Subject: [PATCH 111/268] make AF panel toggles red and opaque when disabled
---
public/index.html | 69 ++++++++++++++++++++++++-----------------------
public/style.css | 23 +++++++++++++---
2 files changed, 56 insertions(+), 36 deletions(-)
diff --git a/public/index.html b/public/index.html
index 13f59da05..0aed22dc3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3248,25 +3248,26 @@
diff --git a/public/style.css b/public/style.css
index fc5ab520e..7d55cf832 100644
--- a/public/style.css
+++ b/public/style.css
@@ -981,19 +981,11 @@ body .panelControlBar {
font-size: 30px;
cursor: pointer;
align-self: center;
-
-}
-.swipe_left{
-position: absolute;
+ position: absolute;
bottom: 15px;
flex-flow: column;
}
-.swipeRightBlock {
- position: absolute;
- right: 0;
- bottom: 0;
-}
.swipes-counter {
color: var(--SmartThemeBodyColor);
From dedb96ec8d6f99c1ff8742ca4ae039e43b913926 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Mon, 30 Sep 2024 15:05:15 +0000
Subject: [PATCH 113/268] Don't trim on whitespace user prefix sequence
---
public/script.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/public/script.js b/public/script.js
index 2e8b8d736..cda7adea3 100644
--- a/public/script.js
+++ b/public/script.js
@@ -5427,6 +5427,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>'));
}
const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
+ const isNotEmpty = (str) => str && str.trim() !== '';
if (isInstruct && power_user.instruct.stop_sequence) {
if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) {
getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence));
@@ -5434,7 +5435,7 @@ export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayInc
}
// Hana: Only use the first sequence (should be <|model|>)
// of the prompt before <|user|> (as KoboldAI Lite does it).
- if (isInstruct && power_user.instruct.input_sequence) {
+ if (isInstruct && isNotEmpty(power_user.instruct.input_sequence)) {
if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) {
getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence));
}
From 5952c3540232a8b3235d9dfbc49a7b27616c689c Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 18:13:13 +0200
Subject: [PATCH 114/268] Refactor if checks on /sendas
---
public/scripts/slash-commands.js | 16 +++-------------
1 file changed, 3 insertions(+), 13 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index eb81a0218..506bf4bc5 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -3107,30 +3107,20 @@ async function setNarratorName(_, text) {
export async function sendMessageAs(args, text) {
if (!text) {
+ toastr.warning('You must specify text to send as');
return '';
}
- let name;
+ let name = args.name?.trim();
let mesText;
- if (args.name) {
- name = args.name.trim();
-
- if (!name && !text) {
- toastr.warning('You must specify a name and text to send as');
- return '';
- }
- } else {
+ if (!name) {
const namelessWarningKey = 'sendAsNamelessWarningShown';
if (localStorage.getItem(namelessWarningKey) !== 'true') {
toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
localStorage.setItem(namelessWarningKey, 'true');
}
name = name2;
- if (!text) {
- toastr.warning('You must specify text to send as');
- return '';
- }
}
mesText = text.trim();
From 0ab74f0819a8f3705b4a630c77b97b821c7d6b29 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 18:30:09 +0200
Subject: [PATCH 115/268] Update return values of send commands
- /sendas
- /sys
- /comment
---
public/scripts/slash-commands.js | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 506bf4bc5..e9d0763c5 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -176,6 +176,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sendas',
callback: sendMessageAs,
+ returns: 'The text of the sent message',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
@@ -222,6 +223,7 @@ export function initDefaultSlashCommands() {
name: 'sys',
callback: sendNarratorMessage,
aliases: ['nar'],
+ returns: 'The text of the sent message',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@@ -276,6 +278,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'comment',
callback: sendCommentMessage,
+ returns: 'The text of the sent message',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@@ -2843,7 +2846,7 @@ function findPersonaByName(name) {
async function sendUserMessageCallback(args, text) {
if (!text) {
- console.warn('WARN: No text provided for /send command');
+ toastr.warning('You must specify text to send');
return;
}
@@ -3205,11 +3208,12 @@ export async function sendMessageAs(args, text) {
await saveChatConditional();
}
- return '';
+ return message.mes;
}
export async function sendNarratorMessage(args, text) {
if (!text) {
+ toastr.warning('You must specify text to send');
return '';
}
@@ -3258,7 +3262,7 @@ export async function sendNarratorMessage(args, text) {
await saveChatConditional();
}
- return '';
+ return message.mes;
}
export async function promptQuietForLoudResponse(who, text) {
@@ -3304,6 +3308,7 @@ export async function promptQuietForLoudResponse(who, text) {
async function sendCommentMessage(args, text) {
if (!text) {
+ toastr.warning('You must specify text to send');
return '';
}
@@ -3346,7 +3351,7 @@ async function sendCommentMessage(args, text) {
await saveChatConditional();
}
- return '';
+ return message.mes;
}
/**
From 1128de91f4c9cfbc2abec1d83c363059c993a1ca Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 18:32:21 +0200
Subject: [PATCH 116/268] /send with return value too
- /send
- return message on `sendMessageAsUser` now
---
public/script.js | 4 +++-
public/scripts/slash-commands.js | 8 +++++---
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/public/script.js b/public/script.js
index cda7adea3..b1e0cffbb 100644
--- a/public/script.js
+++ b/public/script.js
@@ -4784,7 +4784,7 @@ export function removeMacros(str) {
* @param {boolean} [compact] Send as a compact display message.
* @param {string} [name] Name of the user sending the message. Defaults to name1.
* @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar.
- * @returns {Promise} A promise that resolves when the message is inserted.
+ * @returns {Promise} A promise that resolves to the message when it is inserted.
*/
export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) {
messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
@@ -4831,6 +4831,8 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
await saveChatConditional();
}
+
+ return message;
}
/**
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index e9d0763c5..9fe306279 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -478,6 +478,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'send',
callback: sendUserMessageCallback,
+ returns: 'The text of the sent message',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@@ -2862,16 +2863,17 @@ async function sendUserMessageCallback(args, text) {
insertAt = chat.length + insertAt;
}
+ let message;
if ('name' in args) {
const name = args.name || '';
const avatar = findPersonaByName(name) || user_avatar;
- await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
+ message = await sendMessageAsUser(text, bias, insertAt, compact, name, avatar);
}
else {
- await sendMessageAsUser(text, bias, insertAt, compact);
+ message = await sendMessageAsUser(text, bias, insertAt, compact);
}
- return '';
+ return message.mes;
}
async function deleteMessagesByNameCallback(_, name) {
From 1c65a5badd6d9f5546fdbb620028676d21e01638 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 18:38:17 +0200
Subject: [PATCH 117/268] Update /ask toasts to warnings for consistency
---
public/scripts/slash-commands.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 9fe306279..6335e93b1 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -2481,7 +2481,7 @@ async function askCharacter(args, text) {
// Not supported in group chats
// TODO: Maybe support group chats?
if (selected_group) {
- toastr.error('Cannot run /ask command in a group chat!');
+ toastr.warning('Cannot run /ask command in a group chat!');
return '';
}
@@ -2501,7 +2501,7 @@ async function askCharacter(args, text) {
// Find the character
const chId = characters.findIndex((e) => e.name === name || e.avatar === name);
if (!characters[chId] || chId === -1) {
- toastr.error('Character not found.');
+ toastr.warning('Character not found.');
return '';
}
From 140aeb2bb7516e2890181a4065ae88b63c2966bc Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 20:22:44 +0200
Subject: [PATCH 118/268] Fix piping not using array for empty unnamed args
- Lenny said if `splitUnnamedArgument` is enabled, the callback should always receive an array.
- It is intended that if no value was provided, it'll get an array with an empty string. Because.. if no argument was provided for a non-split arg, it'll receive an empty string too.
---
public/scripts/slash-commands/SlashCommandClosure.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/public/scripts/slash-commands/SlashCommandClosure.js b/public/scripts/slash-commands/SlashCommandClosure.js
index 45c4d48ba..0ba1726b5 100644
--- a/public/scripts/slash-commands/SlashCommandClosure.js
+++ b/public/scripts/slash-commands/SlashCommandClosure.js
@@ -508,6 +508,14 @@ export class SlashCommandClosure {
return v;
});
}
+
+ value ??= '';
+
+ // Make sure that if unnamed args are split, it should always return an array
+ if (executor.command.splitUnnamedArgument && !Array.isArray(value)) {
+ value = [value];
+ }
+
return value;
}
From 0d38e634714ccb254799b390293066e54912bc37 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 20:28:52 +0200
Subject: [PATCH 119/268] Why join and then split again, eh?
- Refactor /add actually using the array provided as the array for the internal `parseNumericSeries` inside `performOperation`
---
public/scripts/variables.js | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index e9cd8c653..4e998efae 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -669,8 +669,8 @@ function deleteGlobalVariable(name) {
}
/**
- * Parses a series of numeric values from a string.
- * @param {string} value A space-separated list of numeric values or variable names
+ * Parses a series of numeric values from a string or a string array.
+ * @param {string|string[]} value A space-separated list of numeric values or variable names
* @param {SlashCommandScope} scope Scope
* @returns {number[]} An array of numeric values
*/
@@ -679,9 +679,8 @@ function parseNumericSeries(value, scope = null) {
return [value];
}
- const array = value
- .split(' ')
- .map(i => i.trim())
+ const values = Array.isArray(value) ? value : value.split(' ');
+ const array = values.map(i => i.trim())
.filter(i => i !== '')
.map(i => isNaN(Number(i)) ? Number(resolveVariable(i, scope)) : Number(i))
.filter(i => !isNaN(i));
@@ -1595,7 +1594,7 @@ export function registerVariableCommands() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'add',
- callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),
+ callback: (args, value) => addValuesCallback(args, value),
returns: 'sum of the provided values',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
From 4855f254192de90d39e235c72d3e8511d5581e96 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 20:45:39 +0200
Subject: [PATCH 120/268] Update /mul, /max and /min definition
- Update command definition for /mul, /max and /min to fit the actual code behind, split them too
- Add numbersAndVariables enum provider, to centralize
---
.../SlashCommandCommonEnumsProvider.js | 29 +++++++++++++++++
public/scripts/variables.js | 32 ++++---------------
2 files changed, 36 insertions(+), 25 deletions(-)
diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
index 5612f47b5..a6a589238 100644
--- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
+++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
@@ -152,6 +152,35 @@ export const commonEnumProviders = {
].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value));
},
+ /**
+ * Enum values for numbers and variable names
+ *
+ * Includes all variable names and the ability to specify any number
+ *
+ * @param {SlashCommandExecutor} executor - The executor of the slash command
+ * @param {SlashCommandScope} scope - The scope of the slash command
+ * @returns {SlashCommandEnumValue[]} The enum values
+ */
+ numbersAndVariables: (executor, scope) => [
+ ...commonEnumProviders.variables('all')(executor, scope),
+ new SlashCommandEnumValue(
+ 'any variable name',
+ null,
+ enumTypes.variable,
+ enumIcons.variable,
+ (input) => /^\w*$/.test(input),
+ (input) => input,
+ ),
+ new SlashCommandEnumValue(
+ 'any number',
+ null,
+ enumTypes.number,
+ enumIcons.number,
+ (input) => input == '' || !Number.isNaN(Number(input)),
+ (input) => input,
+ ),
+ ],
+
/**
* All possible char entities, like characters and groups. Can be filtered down to just one type.
*
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index 4e998efae..bb53a487a 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -1602,28 +1602,7 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
acceptsMultiple: true,
- enumProvider: (executor, scope) => {
- const vars = commonEnumProviders.variables('all')(executor, scope);
- vars.push(
- new SlashCommandEnumValue(
- 'any variable name',
- null,
- enumTypes.variable,
- enumIcons.variable,
- (input) => /^\w*$/.test(input),
- (input) => input,
- ),
- new SlashCommandEnumValue(
- 'any number',
- null,
- enumTypes.number,
- enumIcons.number,
- (input) => input == '' || !Number.isNaN(Number(input)),
- (input) => input,
- ),
- );
- return vars;
- },
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
@@ -1653,10 +1632,11 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Performs a multiplication of the set of values and passes the result down the pipe. Can use variable names.
@@ -1681,10 +1661,11 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Returns the maximum value of the set of values and passes the result down the pipe. Can use variable names.
@@ -1709,10 +1690,11 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Returns the minimum value of the set of values and passes the result down the pipe.
From 224249a0d21dd076ebbf73bc990ac8f66e7137f6 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 20:50:18 +0200
Subject: [PATCH 121/268] Fix /sub actually allowing more than two vals now
- Fix by subtracting all from the first value
- Update the definition too
---
public/scripts/variables.js | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index bb53a487a..5e3449f02 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -730,7 +730,7 @@ function maxValuesCallback(args, value) {
}
function subValuesCallback(args, value) {
- return performOperation(value, (array) => array[0] - array[1], false, args._scope);
+ return performOperation(value, (array) => array.reduce((a, b) => a - b, array.shift() ?? 0), false, args._scope);
}
function divValuesCallback(args, value) {
@@ -1716,14 +1716,15 @@ export function registerVariableCommands() {
returns: 'difference of the provided values',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
- description: 'values to find the difference',
+ description: 'values to subtract, starting form the first provided value',
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
acceptsMultiple: true,
- enumProvider: commonEnumProviders.variables('all'),
+ enumProvider: commonEnumProviders.numbersAndVariables,
forceEnum: false,
}),
],
+ splitUnnamedArgument: true,
helpString: `
Performs a subtraction of the set of values and passes the result down the pipe.
From 9af4d62cdfbb7842bb4b665fc90652039b71100c Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Mon, 30 Sep 2024 21:51:24 +0300
Subject: [PATCH 122/268] Extend /sd command
---
.../extensions/stable-diffusion/index.js | 317 ++++++++++++++----
.../extensions/stable-diffusion/settings.html | 15 +-
src/endpoints/stable-diffusion.js | 69 ----
3 files changed, 261 insertions(+), 140 deletions(-)
diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js
index ab0b1bcb5..18da313b5 100644
--- a/public/scripts/extensions/stable-diffusion/index.js
+++ b/public/scripts/extensions/stable-diffusion/index.js
@@ -20,7 +20,7 @@ import {
} from '../../../script.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
import { selected_group } from '../../group-chats.js';
-import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';
+import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce, isFalseBoolean } from '../../utils.js';
import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
@@ -31,6 +31,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { debounce_timeout } from '../../constants.js';
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
+import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
export { MODULE_NAME };
const MODULE_NAME = 'sd';
@@ -221,7 +222,6 @@ const defaultSettings = {
// Refine mode
refine_mode: false,
- expand: false,
interactive_mode: false,
multimodal_captioning: false,
snap: false,
@@ -240,7 +240,7 @@ const defaultSettings = {
drawthings_auth: '',
hr_upscaler: 'Latent',
- hr_scale: 2.0,
+ hr_scale: 1.0,
hr_scale_min: 1.0,
hr_scale_max: 4.0,
hr_scale_step: 0.1,
@@ -260,10 +260,6 @@ const defaultSettings = {
clip_skip: 1,
// NovelAI settings
- novel_upscale_ratio_min: 1.0,
- novel_upscale_ratio_max: 4.0,
- novel_upscale_ratio_step: 0.1,
- novel_upscale_ratio: 1.0,
novel_anlas_guard: false,
novel_sm: false,
novel_sm_dyn: false,
@@ -416,7 +412,6 @@ async function loadSettings() {
$('#sd_hr_scale').val(extension_settings.sd.hr_scale).trigger('input');
$('#sd_denoising_strength').val(extension_settings.sd.denoising_strength).trigger('input');
$('#sd_hr_second_pass_steps').val(extension_settings.sd.hr_second_pass_steps).trigger('input');
- $('#sd_novel_upscale_ratio').val(extension_settings.sd.novel_upscale_ratio).trigger('input');
$('#sd_novel_anlas_guard').prop('checked', extension_settings.sd.novel_anlas_guard);
$('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm);
$('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);
@@ -430,7 +425,6 @@ async function loadSettings() {
$('#sd_restore_faces').prop('checked', extension_settings.sd.restore_faces);
$('#sd_enable_hr').prop('checked', extension_settings.sd.enable_hr);
$('#sd_refine_mode').prop('checked', extension_settings.sd.refine_mode);
- $('#sd_expand').prop('checked', extension_settings.sd.expand);
$('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning);
$('#sd_auto_url').val(extension_settings.sd.auto_url);
$('#sd_auto_auth').val(extension_settings.sd.auto_auth);
@@ -644,37 +638,13 @@ async function onSaveStyleClick() {
saveSettingsDebounced();
}
-async function expandPrompt(prompt) {
- try {
- const response = await fetch('/api/sd/expand', {
- method: 'POST',
- headers: getRequestHeaders(),
- body: JSON.stringify({ prompt: prompt }),
- });
-
- if (!response.ok) {
- throw new Error('API returned an error.');
- }
-
- const data = await response.json();
- return data.prompt;
- } catch {
- return prompt;
- }
-}
-
/**
- * Modifies prompt based on auto-expansion and user inputs.
+ * Modifies prompt based on user inputs.
* @param {string} prompt Prompt to refine
- * @param {boolean} allowExpand Whether to allow auto-expansion
* @param {boolean} isNegative Whether the prompt is a negative one
* @returns {Promise} Refined prompt
*/
-async function refinePrompt(prompt, allowExpand, isNegative = false) {
- if (allowExpand && extension_settings.sd.expand) {
- prompt = await expandPrompt(prompt);
- }
-
+async function refinePrompt(prompt, isNegative) {
if (extension_settings.sd.refine_mode) {
const text = isNegative ? '
- Performs a multiplication of the set of values and passes the result down the pipe. Can use variable names.
+ Performs a multiplication of the set of values and passes the result down the pipe.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Examples:
@@ -1647,6 +1662,9 @@ export function registerVariableCommands() {
- Returns the maximum value of the set of values and passes the result down the pipe. Can use variable names.
+ Returns the maximum value of the set of values and passes the result down the pipe.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Examples:
@@ -1676,6 +1697,9 @@ export function registerVariableCommands() {
Returns the minimum value of the set of values and passes the result down the pipe.
- Can use variable names.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Example:
@@ -1706,6 +1732,9 @@ export function registerVariableCommands() {
/min 10 i 30 j
+
+
/min ["count", 15, 2, "i"]
+
`,
@@ -1717,7 +1746,7 @@ export function registerVariableCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'values to subtract, starting form the first provided value',
- typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
+ typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.LIST],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.numbersAndVariables,
@@ -1728,7 +1757,9 @@ export function registerVariableCommands() {
helpString: `
Performs a subtraction of the set of values and passes the result down the pipe.
- Can use variable names.
+
+
+ Can use variable names, or a JSON array consisting of numbers and variables (with quotes).
Example:
@@ -1736,6 +1767,9 @@ export function registerVariableCommands() {
/sub i 5
+
+
/sub ["count", 4, "i"]
+
`,
From 3fd846fb5b46d876898dde344c26dbcd6da7f222 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Mon, 30 Sep 2024 23:52:01 +0300
Subject: [PATCH 126/268] Allow returning literal Infinity string from math
operations
---
public/scripts/variables.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index 0e18f404a..7c9830a36 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -709,7 +709,7 @@ function performOperation(value, operation, singleOperand = false, scope = null)
const result = singleOperand ? operation(array[0]) : operation(array);
- if (isNaN(result) || !isFinite(result)) {
+ if (isNaN(result)) {
return 0;
}
From 0df5a86ae81a89d8681d729af29f5d40a6e5dbc7 Mon Sep 17 00:00:00 2001
From: notfiz
Date: Tue, 1 Oct 2024 00:53:14 +0330
Subject: [PATCH 127/268] fix google-ai reverse proxy ignoring user provided
path
---
src/endpoints/backends/chat-completions.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index a551f8fbc..ca86db051 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -323,7 +323,7 @@ async function sendMakerSuiteRequest(request, response) {
? (stream ? 'streamGenerateContent' : 'generateContent')
: (isText ? 'generateText' : 'generateMessage');
- const generateResponse = await fetch(`${apiUrl.origin}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
+ const generateResponse = await fetch(`${apiUrl}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`, {
body: JSON.stringify(body),
method: 'POST',
headers: {
From d8379edee7d8fb0a99e742aa929dc02602490860 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 23:32:24 +0200
Subject: [PATCH 128/268] Update return types as optional via named arg
- Update the modified slash commands for chat sending to use the named arg
- Add `slashCommandReturnHelper` for shared funcitonality on return type usage
---
public/scripts/slash-commands.js | 37 ++++++++--
.../SlashCommandCommonEnumsProvider.js | 1 +
.../SlashCommandReturnHelper.js | 74 +++++++++++++++++++
3 files changed, 106 insertions(+), 6 deletions(-)
create mode 100644 public/scripts/slash-commands/SlashCommandReturnHelper.js
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 6335e93b1..599d6b850 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -71,6 +71,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
+import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@@ -176,7 +177,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sendas',
callback: sendMessageAs,
- returns: 'The text of the sent message',
+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
@@ -195,6 +196,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'none',
+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
+ forceEnum: true,
+ }),
],
unnamedArgumentList: [
new SlashCommandArgument(
@@ -223,7 +232,7 @@ export function initDefaultSlashCommands() {
name: 'sys',
callback: sendNarratorMessage,
aliases: ['nar'],
- returns: 'The text of the sent message',
+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@@ -239,6 +248,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'none',
+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
+ forceEnum: true,
+ }),
],
unnamedArgumentList: [
new SlashCommandArgument(
@@ -278,7 +295,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'comment',
callback: sendCommentMessage,
- returns: 'The text of the sent message',
+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@@ -294,6 +311,14 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'none',
+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
+ forceEnum: true,
+ }),
],
unnamedArgumentList: [
new SlashCommandArgument(
@@ -3210,7 +3235,7 @@ export async function sendMessageAs(args, text) {
await saveChatConditional();
}
- return message.mes;
+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
export async function sendNarratorMessage(args, text) {
@@ -3264,7 +3289,7 @@ export async function sendNarratorMessage(args, text) {
await saveChatConditional();
}
- return message.mes;
+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
export async function promptQuietForLoudResponse(who, text) {
@@ -3353,7 +3378,7 @@ async function sendCommentMessage(args, text) {
await saveChatConditional();
}
- return message.mes;
+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
/**
diff --git a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
index 5612f47b5..bd5a7e8a3 100644
--- a/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
+++ b/public/scripts/slash-commands/SlashCommandCommonEnumsProvider.js
@@ -36,6 +36,7 @@ export const enumIcons = {
message: '💬',
voice: '🎤',
server: '🖥️',
+ popup: '🗔',
true: '✔️',
false: '❌',
diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js
new file mode 100644
index 000000000..90ec0d783
--- /dev/null
+++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js
@@ -0,0 +1,74 @@
+import { sendSystemMessage, system_message_types } from '../../script.js';
+import { callGenericPopup, POPUP_TYPE } from '../popup.js';
+import { escapeHtml } from '../utils.js';
+import { enumIcons } from './SlashCommandCommonEnumsProvider.js';
+import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js';
+
+/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */
+
+export const slashCommandReturnHelper = {
+ /**
+ * Gets/creates the enum list of types of return relevant for a slash command
+ *
+ * @param {object} [options={}] Options
+ * @param {boolean} [options.allowPipe=true] Allow option to pipe the return value
+ * @param {boolean} [options.allowObject=false] Allow option to return the value as an object
+ * @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message
+ * @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup
+ * @returns {SlashCommandEnumValue[]} The enum list
+ */
+ enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false } = {}) => [
+ allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
+ allowObject && new SlashCommandEnumValue('object', 'Return as an object to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
+ allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
+ allowChat && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
+ new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
+ new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
+ new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹ️'),
+ new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹ️'),
+ new SlashCommandEnumValue('console', 'Log the return value to the console', enumTypes.enum, '>'),
+ new SlashCommandEnumValue('none', 'No return value'),
+ ].filter(x => !!x),
+
+ /**
+ * Handles the return value based on the specified type
+ *
+ * @param {SlashCommandReturnType} type The type of return
+ * @param {object|number|string} value The value to return
+ * @param {object} [options={}] Options
+ * @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type
+ * @returns {Promise<*>} The processed return value
+ */
+ async doReturn(type, value, { objectToStringFunc = o => o?.toString() } = {}) {
+ const stringValue = typeof value !== 'string' ? objectToStringFunc(value) : value;
+
+ switch (type) {
+ case 'popup-html':
+ case 'popup-text':
+ case 'chat-text':
+ case 'chat-html':
+ case 'toast-text':
+ case 'toast-html': {
+ const shouldHtml = type.endsWith('html');
+ const makeHtml = (str) => (new showdown.Converter()).makeHtml(str);
+
+ if (type.startsWith('popup')) await callGenericPopup(shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue), POPUP_TYPE.TEXT);
+ if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue));
+ if (type.startsWith('toast')) toastr.info(stringValue, null, { escapeHtml: !shouldHtml }); // Toastr handles HTML conversion internally already
+
+ return '';
+ }
+ case 'pipe':
+ return stringValue ?? '';
+ case 'object':
+ return JSON.stringify(value);
+ case 'console':
+ console.info(value);
+ return '';
+ case 'none':
+ return '';
+ default:
+ throw new Error(`Unknown return type: ${type}`);
+ }
+ },
+};
From e3c0c5442c1646970802cb1243bb9e39279d28be Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Mon, 30 Sep 2024 23:37:21 +0200
Subject: [PATCH 129/268] Update /ask with return types, defaulting 'pipe'
---
public/scripts/slash-commands.js | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 599d6b850..792e54bf1 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -456,7 +456,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'ask',
callback: askCharacter,
- returns: 'the generated text',
+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'name',
@@ -465,6 +465,14 @@ export function initDefaultSlashCommands() {
isRequired: true,
enumProvider: commonEnumProviders.characters('character'),
}),
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'pipe',
+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
+ forceEnum: true,
+ }),
],
unnamedArgumentList: [
new SlashCommandArgument(
@@ -2594,7 +2602,9 @@ async function askCharacter(args, text) {
}
}
- return askResult;
+ const message = askResult ? chat[chat.length - 1] : null;
+
+ return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
}
async function hideMessageCallback(_, arg) {
From 7ac7398568c0c1c6fcf8e708f5a8eaf9feeb5c04 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 00:37:33 +0300
Subject: [PATCH 130/268] Remove debug log. Fix comment
---
public/scripts/openai.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index a8fbd667f..20c141316 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -1776,8 +1776,7 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['claude_use_sysprompt'] = oai_settings.claude_use_sysprompt;
generate_data['stop'] = getCustomStoppingStrings(); // Claude shouldn't have limits on stop strings.
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
- // Don't add a prefill on quiet gens (summarization)
- console.log(isContinue && oai_settings.continue_prefill);
+ // Don't add a prefill on quiet gens (summarization) and when using continue prefill.
if (!isQuiet && !(isContinue && oai_settings.continue_prefill)) {
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
}
From 62fd450c592653fd256eaf631372cd05c72745ae Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Tue, 1 Oct 2024 00:06:18 +0200
Subject: [PATCH 131/268] Refactor /listinjects, deprecate its "format" arg
---
public/scripts/slash-commands.js | 74 +++++++++++--------
.../SlashCommandReturnHelper.js | 16 ++--
2 files changed, 55 insertions(+), 35 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 792e54bf1..17352b233 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -1525,11 +1525,20 @@ export function initDefaultSlashCommands() {
name: 'listinjects',
callback: listInjectsCallback,
helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the format argument to change the output format.',
- returns: 'JSON object of script injections',
+ returns: 'Optionalls the JSON object of script injections',
namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'object',
+ enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
+ forceEnum: true,
+ }),
+ // TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
- description: 'output format',
+ description: 'DEPRECATED - use "return" instead - output format',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
@@ -1798,37 +1807,44 @@ function injectCallback(args, value) {
}
async function listInjectsCallback(args) {
- const type = String(args?.format).toLowerCase().trim();
- if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
- type !== 'none' && toastr.info('No script injections for the current chat');
- return JSON.stringify({});
+ /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
+ let returnType = args.return;
+
+ // Old legacy return type handling
+ if (args.format) {
+ toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
+ const type = String(args?.format).toLowerCase().trim();
+ if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
+ type !== 'none' && toastr.info('No script injections for the current chat');
+ }
+ switch (type) {
+ case 'none':
+ returnType = 'none';
+ break;
+ case 'chat':
+ returnType = 'chat-html';
+ break;
+ case 'popup':
+ default:
+ returnType = 'popup-html';
+ break;
+ }
}
- const injects = Object.entries(chat_metadata.script_injects)
- .map(([id, inject]) => {
- const position = Object.entries(extension_prompt_types);
- const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
- return `* **${id}**: ${inject.value} (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
- })
- .join('\n');
+ // Now the actual new return type handling
- const converter = new showdown.Converter();
- const messageText = `### Script injections:\n${injects}`;
- const htmlMessage = DOMPurify.sanitize(converter.makeHtml(messageText));
+ const buildTextValue = (injects) => {
+ const injectsStr = Object.entries(injects)
+ .map(([id, inject]) => {
+ const position = Object.entries(extension_prompt_types);
+ const positionName = position.find(([_, value]) => value === inject.position)?.[0] ?? 'unknown';
+ return `* **${id}**: ${inject.value} (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
+ })
+ .join('\n');
+ return `### Script injections:\n${injectsStr}`;
+ };
- switch (type) {
- case 'none':
- break;
- case 'chat':
- sendSystemMessage(system_message_types.GENERIC, htmlMessage);
- break;
- case 'popup':
- default:
- await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
- break;
- }
-
- return JSON.stringify(chat_metadata.script_injects);
+ return await slashCommandReturnHelper.doReturn(returnType ?? 'object', chat_metadata.script_injects, { objectToStringFunc: buildTextValue });
}
/**
diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js
index 90ec0d783..cdcc914cd 100644
--- a/public/scripts/slash-commands/SlashCommandReturnHelper.js
+++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js
@@ -7,6 +7,9 @@ import { enumTypes, SlashCommandEnumValue } from './SlashCommandEnumValue.js';
/** @typedef {'pipe'|'object'|'chat-html'|'chat-text'|'popup-html'|'popup-text'|'toast-html'|'toast-text'|'console'|'none'} SlashCommandReturnType */
export const slashCommandReturnHelper = {
+ // Without this, VSCode formatter fucks up JS docs. Don't ask me why.
+ _: false,
+
/**
* Gets/creates the enum list of types of return relevant for a slash command
*
@@ -15,18 +18,19 @@ export const slashCommandReturnHelper = {
* @param {boolean} [options.allowObject=false] Allow option to return the value as an object
* @param {boolean} [options.allowChat=false] Allow option to return the value as a chat message
* @param {boolean} [options.allowPopup=false] Allow option to return the value as a popup
+ * @param {boolean}[options.allowTextVersion=true] Used in combination with chat/popup/toast, some of them do not make sense for text versions, e.g.if you are building a HTML string anyway
* @returns {SlashCommandEnumValue[]} The enum list
*/
- enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false } = {}) => [
+ enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [
allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
allowObject && new SlashCommandEnumValue('object', 'Return as an object to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
- allowChat && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
- new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
- new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
+ allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
+ allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
+ allowPopup && allowTextVersion && new SlashCommandEnumValue('popup-text', 'Showing as a popup with the return value - Will only display as text', enumTypes.qr, enumIcons.popup),
new SlashCommandEnumValue('toast-html', 'Show the return value as a toast notification - Can display HTML', enumTypes.command, 'ℹ️'),
- new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹ️'),
- new SlashCommandEnumValue('console', 'Log the return value to the console', enumTypes.enum, '>'),
+ allowTextVersion && new SlashCommandEnumValue('toast-text', 'Show the return value as a toast notification - Will only display as text', enumTypes.qr, 'ℹ️'),
+ new SlashCommandEnumValue('console', 'Log the return value (object, if it can be one) to the console', enumTypes.enum, '>'),
new SlashCommandEnumValue('none', 'No return value'),
].filter(x => !!x),
From 697b3b2034c20ea15e48002929b696f85bfb3a64 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Tue, 1 Oct 2024 00:23:00 +0200
Subject: [PATCH 132/268] Refactor /listvar, deprecate its "format" arg
- Update /listvar
- Fix toasts not doing correct HMTL here
---
public/scripts/slash-commands.js | 9 +--
.../SlashCommandReturnHelper.js | 2 +-
public/scripts/variables.js | 77 ++++++++++++-------
3 files changed, 55 insertions(+), 33 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 17352b233..d74eedbb9 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -1524,21 +1524,21 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'listinjects',
callback: listInjectsCallback,
- helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the format argument to change the output format.',
+ helpString: 'Lists all script injections for the current chat. Displays injects in a popup by default. Use the return argument to change the return type.',
returns: 'Optionalls the JSON object of script injections',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
typeList: [ARGUMENT_TYPE.STRING],
- defaultValue: 'object',
+ defaultValue: 'popup-html',
enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
forceEnum: true,
}),
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
- description: 'DEPRECATED - use "return" instead - output format',
+ description: '!!! DEPRECATED - use "return" instead !!! output format)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
@@ -1832,7 +1832,6 @@ async function listInjectsCallback(args) {
}
// Now the actual new return type handling
-
const buildTextValue = (injects) => {
const injectsStr = Object.entries(injects)
.map(([id, inject]) => {
@@ -1844,7 +1843,7 @@ async function listInjectsCallback(args) {
return `### Script injections:\n${injectsStr}`;
};
- return await slashCommandReturnHelper.doReturn(returnType ?? 'object', chat_metadata.script_injects, { objectToStringFunc: buildTextValue });
+ return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects, { objectToStringFunc: buildTextValue });
}
/**
diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js
index cdcc914cd..941e1f302 100644
--- a/public/scripts/slash-commands/SlashCommandReturnHelper.js
+++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js
@@ -58,7 +58,7 @@ export const slashCommandReturnHelper = {
if (type.startsWith('popup')) await callGenericPopup(shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue), POPUP_TYPE.TEXT);
if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue));
- if (type.startsWith('toast')) toastr.info(stringValue, null, { escapeHtml: !shouldHtml }); // Toastr handles HTML conversion internally already
+ if (type.startsWith('toast')) toastr.info(shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue), null, { escapeHtml: !shouldHtml });
return '';
}
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index e9cd8c653..55e9fe9ab 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -11,6 +11,7 @@ import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureR
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
+import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
@@ -305,7 +306,31 @@ export function replaceVariableMacros(input) {
}
async function listVariablesCallback(args) {
- const type = String(args?.format || '').toLowerCase().trim() || 'popup';
+ /** @type {import('./slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
+ let returnType = args.return;
+
+ // Old legacy return type handling
+ if (args.format) {
+ toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
+ const type = String(args?.format).toLowerCase().trim();
+ if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
+ type !== 'none' && toastr.info('No script injections for the current chat');
+ }
+ switch (type) {
+ case 'none':
+ returnType = 'none';
+ break;
+ case 'chat':
+ returnType = 'chat-html';
+ break;
+ case 'popup':
+ default:
+ returnType = 'popup-html';
+ break;
+ }
+ }
+
+ // Now the actual new return type handling
const scope = String(args?.scope || '').toLowerCase().trim() || 'all';
if (!chat_metadata.variables) {
chat_metadata.variables = {};
@@ -317,35 +342,24 @@ async function listVariablesCallback(args) {
const localVariables = includeLocalVariables ? Object.entries(chat_metadata.variables).map(([name, value]) => `${name}: ${value}`) : [];
const globalVariables = includeGlobalVariables ? Object.entries(extension_settings.variables.global).map(([name, value]) => `${name}: ${value}`) : [];
+ const buildTextValue = (_) => {
+ const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
+ const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
+ const chatName = getCurrentChatId();
+
+ const message = [
+ includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
+ includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
+ ].filter(x => x).join('\n\n');
+ return message;
+ };
+
const jsonVariables = [
...Object.entries(chat_metadata.variables).map(x => ({ key: x[0], value: x[1], scope: 'local' })),
...Object.entries(extension_settings.variables.global).map(x => ({ key: x[0], value: x[1], scope: 'global' })),
];
- const localVariablesString = localVariables.length > 0 ? localVariables.join('\n\n') : 'No local variables';
- const globalVariablesString = globalVariables.length > 0 ? globalVariables.join('\n\n') : 'No global variables';
- const chatName = getCurrentChatId();
-
- const converter = new showdown.Converter();
- const message = [
- includeLocalVariables ? `### Local variables (${chatName}):\n${localVariablesString}` : '',
- includeGlobalVariables ? `### Global variables:\n${globalVariablesString}` : '',
- ].filter(x => x).join('\n\n');
- const htmlMessage = DOMPurify.sanitize(converter.makeHtml(message));
-
- switch (type) {
- case 'none':
- break;
- case 'chat':
- sendSystemMessage(system_message_types.GENERIC, htmlMessage);
- break;
- case 'popup':
- default:
- await callGenericPopup(htmlMessage, POPUP_TYPE.TEXT);
- break;
- }
-
- return JSON.stringify(jsonVariables);
+ return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', jsonVariables, { objectToStringFunc: buildTextValue });
}
/**
@@ -910,7 +924,7 @@ export function registerVariableCommands() {
name: 'listvar',
callback: listVariablesCallback,
aliases: ['listchatvar'],
- helpString: 'List registered chat variables. Displays variables in a popup by default. Use the format argument to change the output format.',
+ helpString: 'List registered chat variables. Displays variables in a popup by default. Use the return argument to change the return type.',
returns: 'JSON list of local variables',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
@@ -926,9 +940,18 @@ export function registerVariableCommands() {
new SlashCommandEnumValue('global', 'Global variables', enumTypes.enum, enumIcons.globalVariable),
],
}),
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'popup-html',
+ enumList: slashCommandReturnHelper.enumList({ allowPipe: false, allowObject: true, allowChat: true, allowPopup: true, allowTextVersion: false }),
+ forceEnum: true,
+ }),
+ // TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
- description: 'output format',
+ description: '!!! DEPRECATED - use "return" instead !!! output format)',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
From 7a1b43eb8943b48747bd3efa139e81186346f964 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Tue, 1 Oct 2024 00:37:21 +0200
Subject: [PATCH 133/268] Refactor /classify-expressions, deprecating...
- Update /classify-expressions, deprecating the old "format"
- Fix some oversights
---
.../scripts/extensions/expressions/index.js | 40 +++++++++++++++----
public/scripts/slash-commands.js | 2 +-
.../SlashCommandReturnHelper.js | 2 +-
public/scripts/variables.js | 2 +-
4 files changed, 36 insertions(+), 10 deletions(-)
diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js
index 71be1e422..a37d011c5 100644
--- a/public/scripts/extensions/expressions/index.js
+++ b/public/scripts/extensions/expressions/index.js
@@ -12,6 +12,8 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { isFunctionCallingSupported } from '../../openai.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
+import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
+import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
@@ -2128,18 +2130,42 @@ function migrateSettings() {
name: 'classify-expressions',
aliases: ['expressions'],
callback: async (args) => {
- const list = await getExpressionsList();
- switch (String(args.format).toLowerCase()) {
- case 'json':
- return JSON.stringify(list);
- default:
- return list.join(', ');
+ /** @type {import('../../slash-commands/SlashCommandReturnHelper.js').SlashCommandReturnType} */
+ // @ts-ignore
+ let returnType = args.return;
+
+ // Old legacy return type handling
+ if (args.format) {
+ toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
+ const type = String(args?.format).toLowerCase().trim();
+ switch (type) {
+ case 'json':
+ returnType = 'object';
+ break;
+ default:
+ returnType = 'pipe';
+ break;
+ }
}
+
+ // Now the actual new return type handling
+ const list = await getExpressionsList();
+
+ return await slashCommandReturnHelper.doReturn(returnType ?? 'pipe', list, { objectToStringFunc: list => list.join(', ') });
},
namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'pipe',
+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
+ forceEnum: true,
+ }),
+ // TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
- description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
+ description: '!!! DEPRECATED - use "return" instead !!! The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
typeList: [ARGUMENT_TYPE.STRING],
enumList: [
new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index d74eedbb9..85744fdd8 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -1538,7 +1538,7 @@ export function initDefaultSlashCommands() {
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
- description: '!!! DEPRECATED - use "return" instead !!! output format)',
+ description: '!!! DEPRECATED - use "return" instead !!! output format',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js
index 941e1f302..9d34846bb 100644
--- a/public/scripts/slash-commands/SlashCommandReturnHelper.js
+++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js
@@ -23,7 +23,7 @@ export const slashCommandReturnHelper = {
*/
enumList: ({ allowPipe = true, allowObject = false, allowChat = false, allowPopup = false, allowTextVersion = true } = {}) => [
allowPipe && new SlashCommandEnumValue('pipe', 'Return to the pipe for the next command', enumTypes.name, '|'),
- allowObject && new SlashCommandEnumValue('object', 'Return as an object to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
+ allowObject && new SlashCommandEnumValue('object', 'Return as an object (or array) to the pipe for the next command', enumTypes.variable, enumIcons.dictionary),
allowChat && new SlashCommandEnumValue('chat-html', 'Sending a chat message with the return value - Can display HTML', enumTypes.command, enumIcons.message),
allowChat && allowTextVersion && new SlashCommandEnumValue('chat-text', 'Sending a chat message with the return value - Will only display as text', enumTypes.qr, enumIcons.message),
allowPopup && new SlashCommandEnumValue('popup-html', 'Showing as a popup with the return value - Can display HTML', enumTypes.command, enumIcons.popup),
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index 55e9fe9ab..67844e375 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -951,7 +951,7 @@ export function registerVariableCommands() {
// TODO remove some day
SlashCommandNamedArgument.fromProps({
name: 'format',
- description: '!!! DEPRECATED - use "return" instead !!! output format)',
+ description: '!!! DEPRECATED - use "return" instead !!! output format',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
forceEnum: true,
From f317b1b764eb5013fd3d88defd91f5165890a58a Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Tue, 1 Oct 2024 00:41:14 +0200
Subject: [PATCH 134/268] Oversight, missed /send in refactoring...
---
public/scripts/slash-commands.js | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 85744fdd8..d1ab9523e 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -511,7 +511,7 @@ export function initDefaultSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'send',
callback: sendUserMessageCallback,
- returns: 'The text of the sent message',
+ returns: 'Optionally the text of the sent message, if specified in the "return" argument',
namedArgumentList: [
new SlashCommandNamedArgument(
'compact',
@@ -534,6 +534,14 @@ export function initDefaultSlashCommands() {
defaultValue: '{{user}}',
enumProvider: commonEnumProviders.personas,
}),
+ SlashCommandNamedArgument.fromProps({
+ name: 'return',
+ description: 'The way how you want the return value to be provided',
+ typeList: [ARGUMENT_TYPE.STRING],
+ defaultValue: 'none',
+ enumList: slashCommandReturnHelper.enumList({ allowObject: true }),
+ forceEnum: true,
+ }),
],
unnamedArgumentList: [
new SlashCommandArgument(
@@ -2923,7 +2931,7 @@ async function sendUserMessageCallback(args, text) {
message = await sendMessageAsUser(text, bias, insertAt, compact);
}
- return message.mes;
+ return await slashCommandReturnHelper.doReturn(args.return ?? 'none', message, { objectToStringFunc: x => x.mes });
}
async function deleteMessagesByNameCallback(_, name) {
From 398ae6ba2e0c23fb12741ee2bfea52454e6a82cd Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Tue, 1 Oct 2024 00:53:19 +0200
Subject: [PATCH 135/268] Update doReturn() API with objectToHtmlFunc
- Allow future commands to provide a different "object to HTML" converter func than "object to text", if need be
---
.../slash-commands/SlashCommandReturnHelper.js | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js
index 9d34846bb..be1a2da5a 100644
--- a/public/scripts/slash-commands/SlashCommandReturnHelper.js
+++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js
@@ -41,10 +41,13 @@ export const slashCommandReturnHelper = {
* @param {object|number|string} value The value to return
* @param {object} [options={}] Options
* @param {(o: object) => string} [options.objectToStringFunc=null] Function to convert the object to a string, if object was provided and 'object' was not the chosen return type
+ * @param {(o: object) => string} [options.objectToHtmlFunc=null] Analog to 'objectToStringFunc', which will be used here if not provided - but can do a different string layout if HTML is requested
* @returns {Promise<*>} The processed return value
*/
- async doReturn(type, value, { objectToStringFunc = o => o?.toString() } = {}) {
- const stringValue = typeof value !== 'string' ? objectToStringFunc(value) : value;
+ async doReturn(type, value, { objectToStringFunc = o => o?.toString(), objectToHtmlFunc = null } = {}) {
+ const shouldHtml = type.endsWith('html');
+ const actualConverterFunc = shouldHtml && objectToHtmlFunc ? objectToHtmlFunc : objectToStringFunc;
+ const stringValue = typeof value !== 'string' ? actualConverterFunc(value) : value;
switch (type) {
case 'popup-html':
@@ -53,12 +56,11 @@ export const slashCommandReturnHelper = {
case 'chat-html':
case 'toast-text':
case 'toast-html': {
- const shouldHtml = type.endsWith('html');
- const makeHtml = (str) => (new showdown.Converter()).makeHtml(str);
+ const htmlOrNotHtml = shouldHtml ? (new showdown.Converter()).makeHtml(stringValue) : escapeHtml(stringValue);
- if (type.startsWith('popup')) await callGenericPopup(shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue), POPUP_TYPE.TEXT);
- if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue));
- if (type.startsWith('toast')) toastr.info(shouldHtml ? makeHtml(stringValue) : escapeHtml(stringValue), null, { escapeHtml: !shouldHtml });
+ if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT);
+ if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
+ if (type.startsWith('toast')) toastr.info(htmlOrNotHtml, null, { escapeHtml: !shouldHtml });
return '';
}
From a63f99b8ce3c3f20370f265ad4359088c5876df0 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 02:11:24 +0300
Subject: [PATCH 136/268] Add /imagine-style command
---
.../extensions/stable-diffusion/index.js | 30 ++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js
index 24eb935dc..dd9c193b8 100644
--- a/public/scripts/extensions/stable-diffusion/index.js
+++ b/public/scripts/extensions/stable-diffusion/index.js
@@ -4058,7 +4058,35 @@ jQuery(async () => {
await onSourceChange();
return extension_settings.sd.source;
},
- }))
+ }));
+
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'imagine-style',
+ aliases: ['sd-style', 'img-style'],
+ returns: 'a name of the current style',
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'style name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: false,
+ forceEnum: true,
+ enumProvider: () => Array.from(document.querySelectorAll('#sd_style > [value]')).map(x => new SlashCommandEnumValue(x.getAttribute('value'), x.textContent)),
+ }),
+ ],
+ helpString: 'If an argument is provided, change the style of the image generation, e.g. /imagine-style MyStyle. Returns the current style.',
+ callback: async (_args, name) => {
+ if (!name) {
+ return extension_settings.sd.style;
+ }
+ const option = document.querySelector(`#sd_style [value="${name}"]`);
+ if (!(option instanceof HTMLOptionElement)) {
+ throw new Error('Could not find the style option in the dropdown.');
+ }
+ option.selected = true;
+ onStyleSelect();
+ return extension_settings.sd.style;
+ },
+ }));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-comfy-workflow',
From e2dd7bfb4b582aeef33a55f7766740d439afd5ab Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 02:15:27 +0300
Subject: [PATCH 137/268] Remove leftovers from prompt expansion
---
default/config.yaml | 1 -
src/transformers.mjs | 6 ------
2 files changed, 7 deletions(-)
diff --git a/default/config.yaml b/default/config.yaml
index 585988686..a7e7a747e 100644
--- a/default/config.yaml
+++ b/default/config.yaml
@@ -118,7 +118,6 @@ extras:
classificationModel: Cohee/distilbert-base-uncased-go-emotions-onnx
captioningModel: Xenova/vit-gpt2-image-captioning
embeddingModel: Cohee/jina-embeddings-v2-base-en
- promptExpansionModel: Cohee/fooocus_expansion-onnx
speechToTextModel: Xenova/whisper-small
textToSpeechModel: Xenova/speecht5_tts
# -- OPENAI CONFIGURATION --
diff --git a/src/transformers.mjs b/src/transformers.mjs
index fe947123a..09714e3bb 100644
--- a/src/transformers.mjs
+++ b/src/transformers.mjs
@@ -31,12 +31,6 @@ const tasks = {
configField: 'extras.embeddingModel',
quantized: true,
},
- 'text-generation': {
- defaultModel: 'Cohee/fooocus_expansion-onnx',
- pipeline: null,
- configField: 'extras.promptExpansionModel',
- quantized: false,
- },
'automatic-speech-recognition': {
defaultModel: 'Xenova/whisper-small',
pipeline: null,
From 56265540dbeafc9afa0b6d8f5f26efa002d1b02f Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Tue, 1 Oct 2024 01:23:34 +0200
Subject: [PATCH 138/268] sanitize HTML on html returns
- I had it in there for some time, I even tested it... likely gone during some commits
---
public/scripts/slash-commands/SlashCommandReturnHelper.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/scripts/slash-commands/SlashCommandReturnHelper.js b/public/scripts/slash-commands/SlashCommandReturnHelper.js
index be1a2da5a..3601c30cd 100644
--- a/public/scripts/slash-commands/SlashCommandReturnHelper.js
+++ b/public/scripts/slash-commands/SlashCommandReturnHelper.js
@@ -56,7 +56,7 @@ export const slashCommandReturnHelper = {
case 'chat-html':
case 'toast-text':
case 'toast-html': {
- const htmlOrNotHtml = shouldHtml ? (new showdown.Converter()).makeHtml(stringValue) : escapeHtml(stringValue);
+ const htmlOrNotHtml = shouldHtml ? DOMPurify.sanitize((new showdown.Converter()).makeHtml(stringValue)) : escapeHtml(stringValue);
if (type.startsWith('popup')) await callGenericPopup(htmlOrNotHtml, POPUP_TYPE.TEXT);
if (type.startsWith('chat')) sendSystemMessage(system_message_types.GENERIC, htmlOrNotHtml);
From f6726db9fb87efe785c57e2d8a32b6a97faee117 Mon Sep 17 00:00:00 2001
From: Wolfsblvt
Date: Tue, 1 Oct 2024 01:30:20 +0200
Subject: [PATCH 139/268] Fix empty script injections on /listinjects
---
public/scripts/slash-commands.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 9347bf016..98738e5c3 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -1929,10 +1929,10 @@ async function listInjectsCallback(args) {
return `* **${id}**: ${inject.value} (${positionName}, depth: ${inject.depth}, scan: ${inject.scan ?? false}, role: ${inject.role ?? extension_prompt_roles.SYSTEM})`;
})
.join('\n');
- return `### Script injections:\n${injectsStr}`;
+ return `### Script injections:\n${injectsStr || 'No script injections for the current chat'}`;
};
- return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects, { objectToStringFunc: buildTextValue });
+ return await slashCommandReturnHelper.doReturn(returnType ?? 'popup-html', chat_metadata.script_injects ?? {}, { objectToStringFunc: buildTextValue });
}
/**
From 6b61abc8edc033c3e5e88e69ec398e70df8305ea Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 02:30:24 +0300
Subject: [PATCH 140/268] Extract logic for enum providers
---
.../extensions/stable-diffusion/index.js | 18 ++++++++++--------
1 file changed, 10 insertions(+), 8 deletions(-)
diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js
index dd9c193b8..7d87757e0 100644
--- a/public/scripts/extensions/stable-diffusion/index.js
+++ b/public/scripts/extensions/stable-diffusion/index.js
@@ -3823,6 +3823,8 @@ function applyCommandArguments(args) {
jQuery(async () => {
await addSDGenButtons();
+ const getSelectEnumProvider = (id, text) => () => Array.from(document.querySelectorAll(`#${id} > [value]`)).map(x => new SlashCommandEnumValue(x.getAttribute('value'), text ? x.textContent : null));
+
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine',
returns: 'URL of the generated image, or an empty string if the generation failed',
@@ -3934,7 +3936,7 @@ jQuery(async () => {
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_model > [value]')).map(o => new SlashCommandEnumValue(o.getAttribute('value'), o.textContent)),
+ enumProvider: getSelectEnumProvider('sd_model', true),
}),
SlashCommandNamedArgument.fromProps({
name: 'sampler',
@@ -3943,7 +3945,7 @@ jQuery(async () => {
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_sampler > [value]')).map(o => new SlashCommandEnumValue(o.getAttribute('value'), o.textContent)),
+ enumProvider: getSelectEnumProvider('sd_sampler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'scheduler',
@@ -3952,7 +3954,7 @@ jQuery(async () => {
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_scheduler > [value]')).map(o => new SlashCommandEnumValue(o.getAttribute('value'), o.textContent)),
+ enumProvider: getSelectEnumProvider('sd_scheduler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'vae',
@@ -3961,7 +3963,7 @@ jQuery(async () => {
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_vae > [value]')).map(o => new SlashCommandEnumValue(o.getAttribute('value'), o.textContent)),
+ enumProvider: getSelectEnumProvider('sd_vae', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'upscaler',
@@ -3970,7 +3972,7 @@ jQuery(async () => {
typeList: [ARGUMENT_TYPE.STRING],
acceptsMultiple: false,
forceEnum: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_hr_upscaler > [value]')).map(o => new SlashCommandEnumValue(o.getAttribute('value'), o.textContent)),
+ enumProvider: getSelectEnumProvider('sd_hr_upscaler', false),
}),
SlashCommandNamedArgument.fromProps({
name: 'hires',
@@ -4038,7 +4040,7 @@ jQuery(async () => {
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
forceEnum: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_source > [value]')).map(x => new SlashCommandEnumValue(x.getAttribute('value'), x.textContent)),
+ enumProvider: getSelectEnumProvider('sd_source', true),
}),
],
helpString: 'If an argument is provided, change the source of the image generation, e.g. /imagine-source comfy. Returns the current source.',
@@ -4070,7 +4072,7 @@ jQuery(async () => {
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
forceEnum: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_style > [value]')).map(x => new SlashCommandEnumValue(x.getAttribute('value'), x.textContent)),
+ enumProvider: getSelectEnumProvider('sd_style', false),
}),
],
helpString: 'If an argument is provided, change the style of the image generation, e.g. /imagine-style MyStyle. Returns the current style.',
@@ -4097,7 +4099,7 @@ jQuery(async () => {
description: 'workflow name',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
- enumProvider: () => Array.from(document.querySelectorAll('#sd_comfy_workflow > [value]')).map(x => x.getAttribute('value')).map(workflow => new SlashCommandEnumValue(workflow)),
+ enumProvider: getSelectEnumProvider('sd_comfy_workflow', false),
}),
],
helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g.
/imagine-comfy-workflow MyWorkflow
',
From 6c110e91f7769f0c12990cf12f61875b3daf8989 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 12:32:32 +0300
Subject: [PATCH 141/268] Exit scripts when try to update non-git installs
---
UpdateAndStart.bat | 11 +++++++++--
UpdateForkAndStart.bat | 10 ++++++++--
2 files changed, 17 insertions(+), 4 deletions(-)
diff --git a/UpdateAndStart.bat b/UpdateAndStart.bat
index 55cee5ce9..8bf70f368 100644
--- a/UpdateAndStart.bat
+++ b/UpdateAndStart.bat
@@ -2,9 +2,15 @@
pushd %~dp0
git --version > nul 2>&1
if %errorlevel% neq 0 (
- echo Git is not installed on this system. Skipping update.
- echo If you installed with a zip file, you will need to download the new zip and install it manually.
+ echo [91mGit is not installed on this system.[0m
+ echo Install it from https://git-scm.com/downloads
+ goto end
) else (
+ if not exist .git (
+ echo [91mNot running from a Git repository. Reinstall using an officially supported method to get updates.[0m
+ echo See: https://docs.sillytavern.app/installation/windows/
+ goto end
+ )
call git pull --rebase --autostash
if %errorlevel% neq 0 (
REM incase there is still something wrong
@@ -14,5 +20,6 @@ if %errorlevel% neq 0 (
set NODE_ENV=production
call npm install --no-audit --no-fund --loglevel=error --no-progress --omit=dev
node server.js %*
+:end
pause
popd
diff --git a/UpdateForkAndStart.bat b/UpdateForkAndStart.bat
index 8bfae8609..8088962bb 100644
--- a/UpdateForkAndStart.bat
+++ b/UpdateForkAndStart.bat
@@ -5,8 +5,14 @@ pushd %~dp0
echo Checking Git installation
git --version > nul 2>&1
if %errorlevel% neq 0 (
- echo Git is not installed on this system. Skipping update.
- echo If you installed with a zip file, you will need to download the new zip and install it manually.
+ echo [91mGit is not installed on this system.[0m
+ echo Install it from https://git-scm.com/downloads
+ goto end
+)
+
+if not exist .git (
+ echo [91mNot running from a Git repository. Reinstall using an officially supported method to get updates.[0m
+ echo See: https://docs.sillytavern.app/installation/windows/
goto end
)
From 9e3307576ef835da5ae399c80f7f865ecd964059 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 12:43:20 +0300
Subject: [PATCH 142/268] Crash when update failed
---
UpdateAndStart.bat | 4 +++-
UpdateForkAndStart.bat | 3 ++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/UpdateAndStart.bat b/UpdateAndStart.bat
index 8bf70f368..fbb3ac9bb 100644
--- a/UpdateAndStart.bat
+++ b/UpdateAndStart.bat
@@ -14,7 +14,9 @@ if %errorlevel% neq 0 (
call git pull --rebase --autostash
if %errorlevel% neq 0 (
REM incase there is still something wrong
- echo There were errors while updating. Please download the latest version manually.
+ echo [91mThere were errors while updating.[0m
+ echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
+ goto end
)
)
set NODE_ENV=production
diff --git a/UpdateForkAndStart.bat b/UpdateForkAndStart.bat
index 8088962bb..d788b9470 100644
--- a/UpdateForkAndStart.bat
+++ b/UpdateForkAndStart.bat
@@ -95,7 +95,8 @@ git pull --rebase --autostash origin %TARGET_BRANCH%
:install
if %errorlevel% neq 0 (
- echo There were errors while updating. Please check manually.
+ echo [91mThere were errors while updating.[0m
+ echo See the update FAQ at https://docs.sillytavern.app/usage/update/#common-update-problems
goto end
)
From b4529e75c6adafd9f66fd138799675ff411d88f8 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 13:16:55 +0300
Subject: [PATCH 143/268] Don't wrap empty sysprompts in instruct mode
---
public/scripts/instruct-mode.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js
index 033f5c3e8..ba641935f 100644
--- a/public/scripts/instruct-mode.js
+++ b/public/scripts/instruct-mode.js
@@ -384,6 +384,10 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
* @returns {string} Formatted instruct mode system prompt.
*/
export function formatInstructModeSystemPrompt(systemPrompt) {
+ if (!systemPrompt) {
+ return '';
+ }
+
const separator = power_user.instruct.wrap ? '\n' : '';
if (power_user.instruct.system_sequence_prefix) {
From 5489dd61a758e991196eedc8e1fb34293f328afc Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 14:14:19 +0300
Subject: [PATCH 144/268] Pass /continue error to slash command executor
---
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 22531eb15..0a30f24da 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -3027,7 +3027,7 @@ async function continueChatCallback(args, prompt) {
resolve();
} catch (error) {
console.error('Error running /continue command:', error);
- reject();
+ reject(error);
}
});
From 42d24dbc477a08256918ebd73c0b895185c61338 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 14:24:27 +0300
Subject: [PATCH 145/268] Fix free mode extension on empty chats when
{{charPrefix}} is used
---
public/scripts/extensions/stable-diffusion/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js
index 7d87757e0..20ffccac1 100644
--- a/public/scripts/extensions/stable-diffusion/index.js
+++ b/public/scripts/extensions/stable-diffusion/index.js
@@ -2445,7 +2445,7 @@ function generateFreeModePrompt(trigger, combineNegatives) {
return message.original_avatar.replace(/\.[^/.]+$/, '');
}
}
- throw new Error('No usable messages found.');
+ return '';
};
const key = getLastCharacterKey();
From ab966db1e5ae9a7a6248314fd55529789f342b0e Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 14:28:34 +0300
Subject: [PATCH 146/268] Refactor ensureSelectionExists for more readability
---
public/scripts/extensions/stable-diffusion/index.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js
index 20ffccac1..e96575d56 100644
--- a/public/scripts/extensions/stable-diffusion/index.js
+++ b/public/scripts/extensions/stable-diffusion/index.js
@@ -2243,7 +2243,9 @@ function ensureSelectionExists(setting, selector) {
if (!selectElement) {
return;
}
- if (selectElement.selectedOptions.length && !Array.from(selectElement.options).some(option => option.value === extension_settings.sd[setting])) {
+ const options = Array.from(selectElement.options);
+ const value = extension_settings.sd[setting];
+ if (selectElement.selectedOptions.length && !options.some(option => option.value === value)) {
extension_settings.sd[setting] = selectElement.selectedOptions[0].value;
}
}
From c76fc7d23c95ea663d22b594d23a965d33dd7a8c Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 15:02:14 +0300
Subject: [PATCH 147/268] /model: Better support for write-in controls
---
public/scripts/slash-commands.js | 47 +++++++++++++++++++++++++-------
1 file changed, 37 insertions(+), 10 deletions(-)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index 0a30f24da..1e0413b7b 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -55,7 +55,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js';
-import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
+import { debounce, delay, equalsIgnoreCaseAndAccents, findChar, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -3542,11 +3542,12 @@ function setBackgroundCallback(_, bg) {
* Retrieves the available model options based on the currently selected main API and its subtype
* @param {boolean} quiet - Whether to suppress toasts
*
- * @returns {{control: HTMLSelectElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
+ * @returns {{control: HTMLSelectElement|HTMLInputElement, options: HTMLOptionElement[]}?} An array of objects representing the available model options, or null if not supported
*/
function getModelOptions(quiet) {
const nullResult = { control: null, options: null };
const modelSelectMap = [
+ { id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA },
{ id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
{ id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
{ id: 'model_infermaticai_select', api: 'textgenerationwebui', type: textgen_types.INFERMATICAI },
@@ -3563,7 +3564,7 @@ function getModelOptions(quiet) {
{ id: 'model_ai21_select', api: 'openai', type: chat_completion_sources.AI21 },
{ id: 'model_google_select', api: 'openai', type: chat_completion_sources.MAKERSUITE },
{ id: 'model_mistralai_select', api: 'openai', type: chat_completion_sources.MISTRALAI },
- { id: 'model_custom_select', api: 'openai', type: chat_completion_sources.CUSTOM },
+ { id: 'custom_model_id', api: 'openai', type: chat_completion_sources.CUSTOM },
{ id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
{ id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
{ id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
@@ -3594,12 +3595,31 @@ function getModelOptions(quiet) {
const modelSelectControl = document.getElementById(modelSelectItem);
- if (!(modelSelectControl instanceof HTMLSelectElement)) {
+ if (!(modelSelectControl instanceof HTMLSelectElement) && !(modelSelectControl instanceof HTMLInputElement)) {
!quiet && toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`);
return nullResult;
}
- const options = Array.from(modelSelectControl.options).filter(x => x.value);
+ /**
+ * Get options from a HTMLSelectElement or HTMLInputElement with a list.
+ * @param {HTMLSelectElement | HTMLInputElement} control Control containing the options
+ * @returns {HTMLOptionElement[]} Array of options
+ */
+ const getOptions = (control) => {
+ if (control instanceof HTMLSelectElement) {
+ return Array.from(control.options);
+ }
+
+ const valueOption = new Option(control.value, control.value);
+
+ if (control instanceof HTMLInputElement && control.list instanceof HTMLDataListElement) {
+ return [valueOption, ...Array.from(control.list.options)];
+ }
+
+ return [valueOption];
+ };
+
+ const options = getOptions(modelSelectControl).filter(x => x.value).filter(onlyUnique);
return { control: modelSelectControl, options };
}
@@ -3618,11 +3638,6 @@ function modelCallback(args, model) {
return '';
}
- if (!options.length) {
- !quiet && toastr.warning('No model options found. Check your API settings.');
- return '';
- }
-
model = String(model || '').trim();
if (!model) {
@@ -3631,6 +3646,18 @@ function modelCallback(args, model) {
console.log('Set model to ' + model);
+ if (modelSelectControl instanceof HTMLInputElement) {
+ modelSelectControl.value = model;
+ $(modelSelectControl).trigger('input');
+ !quiet && toastr.success(`Model set to "${model}"`);
+ return model;
+ }
+
+ if (!options.length) {
+ !quiet && toastr.warning('No model options found. Check your API settings.');
+ return '';
+ }
+
let newSelectedOption = null;
const fuse = new Fuse(options, { keys: ['text', 'value'] });
From 7f9023d3c21cb026914efd5489add59b71e21297 Mon Sep 17 00:00:00 2001
From: Yokayo
Date: Tue, 1 Oct 2024 21:25:20 +0700
Subject: [PATCH 148/268] Work on tl
---
public/locales/ru-ru.json | 147 +++++++++++++++++-
public/script.js | 123 +++++++--------
public/scripts/PromptManager.js | 13 +-
public/scripts/authors-note.js | 19 +--
public/scripts/chats.js | 25 +--
public/scripts/extensions/regex/editor.html | 2 +-
public/scripts/group-chats.js | 20 +--
public/scripts/openai.js | 53 +++----
public/scripts/templates/assistantNote.html | 2 +-
public/scripts/templates/deleteConfirm.html | 5 +
.../scripts/templates/duplicateConfirm.html | 4 +-
public/scripts/templates/hotkeys.html | 14 +-
public/scripts/templates/quotaError.html | 4 +
13 files changed, 286 insertions(+), 145 deletions(-)
create mode 100644 public/scripts/templates/deleteConfirm.html
create mode 100644 public/scripts/templates/quotaError.html
diff --git a/public/locales/ru-ru.json b/public/locales/ru-ru.json
index 7472d199d..8cdb2325b 100644
--- a/public/locales/ru-ru.json
+++ b/public/locales/ru-ru.json
@@ -451,8 +451,8 @@
"Creator's Notes": "Заметки создателя",
"A-Z": "A-Z",
"Z-A": "Z-A",
- "Newest": "Новейшие",
- "Oldest": "Старейшие",
+ "Newest": "Сначала новые",
+ "Oldest": "Сначала старые",
"Favorites": "Избранные",
"Recent": "Последние",
"Most chats": "Больше всего чатов",
@@ -564,7 +564,7 @@
"Advanced Definition": "Расширенное описание",
"Character Lore": "Лор персонажа",
"Export and Download": "Экспортировать и скачать",
- "Duplicate Character": "Дублировать персонажа",
+ "Duplicate Character": "Клонировать персонажа",
"Create Character": "Создать персонажа",
"Delete Character": "Удалить персонажа",
"View all tags": "Показать все тэги",
@@ -1690,7 +1690,7 @@
"Delete Message": "Удалить сообщение",
"Delete Swipe": "Удалить свайп",
"Could not get a reply from API. Check your connection settings / API key and try again.": "Не удалось получить ответ от API. Проверьте настройки соединения и API-ключ и повторите попытку.",
- "Connecting To Proxy": "Подключение к прокси",
+ "Connecting To Proxy": "Подключиться к прокси",
"Are you sure you want to connect to the following proxy URL?": "Вы точно хотите соединиться с прокси по этому адресу?",
"API connection successful!": "Соединение с API установлено!",
"Proxy Saved": "Прокси сохранена",
@@ -1747,5 +1747,142 @@
"Markdown Hotkeys": "Горячие клавиши для разметки",
"markdown_hotkeys_desc": "Включить горячие клавиши для вставки символов разметки в некоторых полях ввода. См. '/help hotkeys'.",
"Save and Update": "Сохранить и обновить",
- "Profile name:": "Название профиля:"
+ "Profile name:": "Название профиля:",
+ "API returned an error": "API вернуло ошибку",
+ "Failed to save preset": "Не удалось сохранить пресет",
+ "Preset name should be unique.": "Название пресета должно быть уникальным.",
+ "Invalid file": "Невалидный файл",
+ "No preset selected": "Пресет не выбран",
+ "Invalid logit bias preset file.": "Файл пресета невалиден.",
+ "Preset was not deleted from server": "Пресет не удалён с сервера",
+ "Preset deleted": "Пресет удалён",
+ "Delete the preset?": "Удалить пресет?",
+ "Preset updated": "Пресет обновлён",
+ "Entered reverse proxy address is not a valid URL": "Введённый адрес прокси невалиден",
+ "An error occurred while counting tokens: Token budget exceeded.": "Ошибка при подсчёте токенов: Превышен бюджет токенов",
+ "An error occurred while counting tokens: Invalid character name": "Ошибка при подсчёте токенов: Невалидное имя персонажа",
+ "Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.": "Недостаточно токенов для всех выбранных промптов. Повысьте лимит токенов или отключите часть промптов.",
+ "The name of at least one character contained whitespaces or special characters. Please check your user and character name.": "В имени одного из персонажей содержится пробел или иной спецсимвол. Проверьте имена пользователя и персонажа.",
+ "An unknown error occurred while counting tokens. Further information may be available in console.": "Неизвестная ошибка при подсчёте токенов. Проверьте консоль, возможно, подробная информация есть там.",
+ "quota_error_1": "При обработке вашего запроса возникла ошибка.",
+ "quota_error_2": "Убедитесь, что на вашем",
+ "quota_error_3": "аккаунте OpenAI",
+ "quota_error_4": "имеется достаточно кредитов.",
+ "quota_error_5": "Если кредитов достаточно, то повторите попытку позднее.",
+ "Proxy preset '${0}' not found": "Пресет '${0}' не найден",
+ "Window.ai returned an error": "Window.ai вернул ошибку",
+ "Get it here:": "Загрузите здесь:",
+ "Extension is not installed": "Расширение не установлено",
+ "Update or remove your reverse proxy settings.": "Измените или удалите ваши настройки прокси.",
+ "An error occurred while importing prompts. More info available in console.": "В процессе импорта произошла ошибка. Подробную информацию см. в консоли.",
+ "Could not import prompts. Export failed validation.": "Не удалось импортировать промпты. Не пройдена валидация при экспорте.",
+ "Prompt import complete.": "Импорт завершён.",
+ "Are you sure you want to delete this prompt?": "Вы точно хотите удалить этот промпт?",
+ "Existing prompts with the same ID will be overridden. Do you want to proceed?": "Имеющиеся промпты с совпадающими идентификаторами будут перезаписаны. Продолжить?",
+ "This will reset the prompt order for this character. You will not lose any prompts.": "Будет сброшен порядок промптов для этого персонажа. Сами промпты вы не потеряете.",
+ "assistant_note_1": "Примечание:",
+ "assistant_note_2": "это временный чат, он будет удалён, как только вы из него выйдете.",
+ "help_hotkeys_20": "Горячие клавиши для разметки",
+ "help_hotkeys_21": "Работают в окне ввода чата, а также в полях, отмеченных этим значком:",
+ "help_hotkeys_22": "**полужирный**",
+ "help_hotkeys_23": "*курсив*",
+ "help_hotkeys_24": "__подчёркивание__",
+ "help_hotkeys_25": "`inline-код`",
+ "help_hotkeys_26": "~~зачёркнутый~~",
+ "ext_regex_only_format_visual_desc": "Содержимое файла с историей чата останется нетронутым, изменения будут лишь визуальными (в UI).",
+ "Could not convert file": "Не удалось сконвертировать файл",
+ "Could not upload file": "Не удалось загрузить файл",
+ "Could not download file": "Не удалось скачать файл",
+ "File is too big. Maximum size is ${0}.": "Слишком большой файл. Максимальный размер: ${0}.",
+ "Binary files are not supported. Select a text file or image.": "Бинарные файлы не поддерживаются. Выберите текстовый файл или изображение.",
+ "No character or group selected": "Не выбрано ни одного персонажа или группы",
+ "Could not delete file": "Не удалось удалить файл",
+ "No attachments selected.": "Вложение не выбрано.",
+ "Data Bank": "Банк данных",
+ "No files were scraped.": "Не соскраплено ни одного файла.",
+ "Scraped ${0} files from ${1} to ${2}.": "Соскраплено ${0} файлов из ${1} в ${2}.",
+ "Check browser console for details.": "Подробности см. в консоли браузера.",
+ "Scraping failed": "Ошибка скрапинга",
+ "External media has been blocked": "Внешние медиа отключены",
+ "Use the 'Ext. Media' button to allow it. Click on this message to dismiss.": "Разрешить можно с помощью кнопки 'Внешн. медиа'. Нажмите на это сообщение, чтобы его скрыть.",
+ "Couldn't get CSRF token. Please refresh the page.": "Не удалось получить CSRF токен. Попробуйте перезагрузить страницу.",
+ "Error": "Ошибка",
+ "API Error": "Ошибка API",
+ "Please wait until the chat is saved before switching characters.": "Пожалуйста, дождитесь сохранения чата, прежде чем переключать персонажа.",
+ "Your chat is still saving...": "Чат всё ещё сохраняется...",
+ "Character ${0} not found in the list": "Персонаж ${0} не найден в списке",
+ "Streaming is enabled, but the version of Kobold used does not support token streaming.": "Включён стриминг текста, но ваша версия Kobold не поддерживает стриминг токенов.",
+ "Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.": "Для устаревшего API стриминг недоступен. Обновите oobaboga и используйте новый API, чтобы включить стриминг.",
+ "Verify that the server is running and accessible.": "Убедитесь, что сервер запущен и доступен по сети.",
+ "ST Server cannot be reached": "Не удалось соединиться с ST Server",
+ "You must first select a character to duplicate!": "Вы не выбрали персонажа, которого хотите клонировать!",
+ "Character Duplicated": "Персонаж склонирован",
+ "No character name provided.": "Вы не ввели имя персонажа.",
+ "Rename Character": "Переименование",
+ "No character selected.": "Вы не выбрали персонажа.",
+ "New name:": "Новое имя:",
+ "Same character name provided, so name did not change.": "Введено то же самое имя, ничего не изменилось.",
+ "Character renamed and past chats updated!": "Персонаж переименован, а чаты обновлены!",
+ "Character renamed!": "Персонаж переименован!",
+ "Something went wrong. The page will be reloaded.": "Что-то пошло не так. Страница будет перезагружена.",
+ "Past chat could not be updated: ${0}": "Не удалось обновить чат ${0}",
+ "Trying to save group chat with regular saveChat function. Aborting to prevent corruption.": "Произошла попытка сохранения группового чата функцией saveChat. Откатываем изменения, чтобы предотвратить потерю данных.",
+ "Check the server connection and reload the page to prevent data loss.": "Проверьте связь с сервером и перезагрузите страницу, чтобы избежать потери данных.",
+ "Chat could not be saved": "Не удалось сохранить чат",
+ "Settings could not be loaded after multiple attempts. Please try again later.": "Не удалось загрузить настройки за несколько попыток. Попробуйте позднее.",
+ "Settings could not be saved": "Не удалось сохранить настройки",
+ "Could not load chat data. Try reloading the page.": "Не удалось загрузить чат. Попробуйте обновить страницу.",
+ "Invalid process (no 'type')": "Невалидный процесс (нет параметра 'type')",
+ "Character Deleted: ${0}": "Персонаж удалён: ${0}",
+ "Character Created: ${0}": "Персонаж создан: ${0}",
+ "Group Created": "Группа создана",
+ "Group Deleted": "Группа удалена",
+ "Character Imported: ${0}": "Персонаж импортирован: ${0}",
+ "Invalid swipe ID: ${0}": "Некорректный идентификатор свайпа: ${0}",
+ "No messages to delete swipes from.": "Сообщение, из которого требуется удалить свайп, не найдено.",
+ "Can't delete the last swipe.": "Невозможно удалить единственный свайп.",
+ "GUI Settings preset is not supported for Horde. Please select another preset.": "Для Horde не поддерживаются пресеты настроек GUI. Пожалуйста, выберите другой пресет.",
+ "Embedded lorebook will be removed from this character.": "Встроенный лорбук будет удалён из персонажа.",
+ "Name is required": "Введите имя",
+ "Cannot create characters while generating. Stop the request and try again.": "Во время генерации ответа создать персонажа невозможно. Остановите запрос и повторите попытку.",
+ "Creation aborted": "Процесс создания прерван",
+ "Failed to create character": "Не удалось создать персонажа",
+ "Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.": "Что-то пошло не так в процессе сохранения персонажа, либо вы загрузили файл некорректного формата. Проверьте, что изображение точно не в формате webp.",
+ "Context template '${0}' not found": "Шаблон контекста '${0}' не найден",
+ "Instruct template '${0}' not found": "Шаблон Instruct-режима '${0}' не найден",
+ "Error: ${0} is not a valid API": "Ошибка: ${0} не является валидным API",
+ "API set to ${0}, trying to connect..": "Установлено API ${0}, пробуем подключиться...",
+ "Unsupported file type: ": "Неподдерживаемый тип файла: ",
+ "Cannot import characters while generating. Stop the request and try again.": "Во время генерации ответа импорт персонажа невозможен. Остановите запрос и повторите попытку.",
+ "Import aborted": "Процесс импорта прерван",
+ "The file is likely invalid or corrupted.": "Вероятно, файл невалиден или повреждён.",
+ "Could not import character": "Не удалось импортировать персонажа",
+ "Cannot run /impersonate command while the reply is being generated.": "Во время генерации ответа выполнить /impersonate невозможно.",
+ "Name must be provided as an argument to rename this chat.": "Для переименования чата необходимо предоставить новое имя в качестве аргумента.",
+ "No chat selected that can be renamed.": "Чат, который требуется переименовать, не найден.",
+ "Successfully renamed chat to: ${0}": "Чат успешно переименован в: ${0}",
+ "Character ${0} not found. Skipping deletion.": "Персонаж ${0} не найден. Удаление пропускается.",
+ "Failed to delete character": "При удалении персонажа произошло ошибка",
+ "duplicate_confirm_1": "Вы точно хотите клонировать этого персонажа?",
+ "duplicate_confirm_2": "Если вы хотите просто создать новый чат, воспользуйтесь кнопкой \"Начать новый чат\" в меню слева внизу.",
+ "delete_confirm_1": "ОТМЕНИТЬ БУДЕТ НЕВОЗМОЖНО!",
+ "delete_confirm_2": "Также удалить файлы чатов",
+ "Delete the character?": "Удалить персонажа?",
+ "Not a valid number": "Некорректное число",
+ "Author's Note depth updated": "Глубина заметок автора обновлена",
+ "Author's Note frequency updated": "Частота заметок автора обновлена",
+ "Not a valid position": "Некорректная позиция",
+ "Author's Note position updated": "Позиция заметок автора обновлена",
+ "Something went wrong. Could not save character's author's note.": "Что-то пошло не так. Не удалось сохранить заметки автора для этого персонажа.",
+ "Select a character before trying to use Author's Note": "Сначала необходимо выбрать персонажа",
+ "Author's Note text updated": "Текст заметок автора обновлён",
+ "Group Validation": "Валидация группы",
+ "Warning: Listed member ${0} does not exist as a character. It will be removed from the group.": "Предупреждение: персонаж ${0} не существует в виде карточки. Он будет удалён из группы.",
+ "Group Chat could not be saved": "Не удалось сохранить групповой чат",
+ "Deleted group member swiped. To get a reply, add them back to the group.": "Вы пытаетесь свайпнуть удалённого члена группы. Чтобы получить ответ, добавьте этого персонажа обратно в группу.",
+ "Currently no group selected.": "В данный момент не выбрано ни одной группы.",
+ "Not so fast! Wait for the characters to stop typing before deleting the group.": "Чуть помедленнее! Перед удалением группы дождитесь, пока персонаж закончит печатать.",
+ "Delete the group?": "Удалить группу?",
+ "This will also delete all your chats with that group. If you want to delete a single conversation, select a \"View past chats\" option in the lower left menu.": "Вместе с ней будут удалены и все её чаты. Если требуется удалить только один чат, воспользуйтесь кнопкой \"Все чаты\" в меню в левом нижнем углу.",
+ "Can't peek a character while group reply is being generated": "Невозможно открыть карточку персонажа во время генерации ответа"
}
diff --git a/public/script.js b/public/script.js
index 8d085b6d2..139c64a37 100644
--- a/public/script.js
+++ b/public/script.js
@@ -383,8 +383,8 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
if (localStorage.getItem(warningShownKey) === null) {
const warningToast = toastr.warning(
- 'Use the "Ext. Media" button to allow it. Click on this message to dismiss.',
- 'External media has been blocked',
+ t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
+ t`External media has been blocked`,
{
timeOut: 0,
preventDuplicates: true,
@@ -924,7 +924,7 @@ async function firstLoadInit() {
token = tokenData.token;
} catch {
hideLoader();
- toastr.error('Couldn\'t get CSRF token. Please refresh the page.', 'Error', { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
+ toastr.error(t`Couldn't get CSRF token. Please refresh the page.`, t`Error`, { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
throw new Error('Initialization failed');
}
@@ -1140,7 +1140,7 @@ async function getStatusKobold() {
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
- toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
+ toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
console.error('Error getting status', err);
@@ -1226,7 +1226,7 @@ async function getStatusTextgen() {
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
- toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
+ toastr.error(data.response, t`API Error`, { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
if (err instanceof AbortReason) {
@@ -1278,7 +1278,7 @@ export async function selectCharacterById(id) {
}
if (isChatSaving) {
- toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...');
+ toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
}
@@ -1632,7 +1632,7 @@ export async function getOneCharacter(avatarUrl) {
if (indexOf !== -1) {
characters[indexOf] = getData;
} else {
- toastr.error(`Character ${avatarUrl} not found in the list`, 'Error', { timeOut: 5000, preventDuplicates: true });
+ toastr.error(t`Character ${avatarUrl} not found in the list`, t`Error`, { timeOut: 5000, preventDuplicates: true });
}
}
}
@@ -3374,7 +3374,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
await eventSource.emit(event_types.GENERATION_AFTER_COMMANDS, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun);
if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) {
- toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
+ toastr.error(t`Streaming is enabled, but the version of Kobold used does not support token streaming.`, undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration(type);
return Promise.resolve();
}
@@ -3383,7 +3383,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
textgen_settings.streaming &&
textgen_settings.legacy_api &&
textgen_settings.type === OOBA) {
- toastr.error('Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
+ toastr.error(t`Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.`, undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration(type);
return Promise.resolve();
}
@@ -3399,7 +3399,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (!pingResult) {
unblockGeneration(type);
- toastr.error('Verify that the server is running and accessible.', 'ST Server cannot be reached');
+ toastr.error(t`Verify that the server is running and accessible.`, t`ST Server cannot be reached`);
throw new Error('Server unreachable');
}
@@ -4427,7 +4427,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
generatedPromptCache = '';
if (data?.response) {
- toastr.error(data.response, 'API Error', { preventDuplicates: true });
+ toastr.error(data.response, t`API Error`, { preventDuplicates: true });
}
throw new Error(data?.response);
}
@@ -4518,7 +4518,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
function onError(exception) {
if (typeof exception?.error?.message === 'string') {
- toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 });
+ toastr.error(exception.error.message, t`Error`, { timeOut: 10000, extendedTimeOut: 20000 });
}
generatedPromptCache = '';
@@ -4920,7 +4920,7 @@ function addChatsSeparator(mesSendString) {
async function duplicateCharacter() {
if (!this_chid) {
- toastr.warning('You must first select a character to duplicate!');
+ toastr.warning(t`You must first select a character to duplicate!`);
return '';
}
@@ -4939,7 +4939,7 @@ async function duplicateCharacter() {
body: JSON.stringify(body),
});
if (response.ok) {
- toastr.success('Character Duplicated');
+ toastr.success(t`Character Duplicated`);
const data = await response.json();
await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path });
await getCharacters();
@@ -5786,23 +5786,23 @@ export function setSendButtonState(value) {
export async function renameCharacter(name = null, { silent = false, renameChats = null } = {}) {
if (!name && silent) {
- toastr.warning('No character name provided.', 'Rename Character');
+ toastr.warning(t`No character name provided.`, t`Rename Character`);
return false;
}
if (this_chid === undefined) {
- toastr.warning('No character selected.', 'Rename Character');
+ toastr.warning(t`No character selected.`, t`Rename Character`);
return false;
}
const oldAvatar = characters[this_chid].avatar;
- const newValue = name || await callGenericPopup('
', POPUP_TYPE.INPUT, characters[this_chid].name);
if (!newValue) {
- toastr.warning('No character name provided.', 'Rename Character');
+ toastr.warning(t`No character name provided.`, t`Rename Character`);
return false;
}
if (newValue === characters[this_chid].name) {
- toastr.info('Same character name provided, so name did not change.', 'Rename Character');
+ toastr.info(t`Same character name provided, so name did not change.`, t`Rename Character`);
return false;
}
@@ -5849,9 +5849,9 @@ export async function renameCharacter(name = null, { silent = false, renameChats
if (renamePastChatsConfirm) {
await renamePastChats(newAvatar, newValue);
await reloadCurrentChat();
- toastr.success('Character renamed and past chats updated!', 'Rename Character');
+ toastr.success(t`Character renamed and past chats updated!`, t`Rename Character`);
} else {
- toastr.success('Character renamed!', 'Rename Character');
+ toastr.success(t`Character renamed!`, t`Rename Character`);
}
}
else {
@@ -5864,8 +5864,8 @@ export async function renameCharacter(name = null, { silent = false, renameChats
}
catch (error) {
// Reloading to prevent data corruption
- if (!silent) await callPopup('Something went wrong. The page will be reloaded.', 'text');
- else toastr.error('Something went wrong. The page will be reloaded.', 'Rename Character');
+ if (!silent) await callPopup(t`Something went wrong. The page will be reloaded.`, 'text');
+ else toastr.error(t`Something went wrong. The page will be reloaded.`, t`Rename Character`);
console.log('Renaming character error:', error);
location.reload();
@@ -5922,7 +5922,7 @@ async function renamePastChats(newAvatar, newValue) {
}
}
} catch (error) {
- toastr.error(`Past chat could not be updated: ${file_name}`);
+ toastr.error(t`Past chat could not be updated: ${file_name}`);
console.error(error);
}
}
@@ -5971,7 +5971,7 @@ export async function saveChat(chatName, withMetadata, mesId) {
characters[this_chid]['date_last_chat'] = Date.now();
chat.forEach(function (item, i) {
if (item['is_group']) {
- toastr.error('Trying to save group chat with regular saveChat function. Aborting to prevent corruption.');
+ toastr.error(t`Trying to save group chat with regular saveChat function. Aborting to prevent corruption.`);
throw new Error('Group chat saved from saveChat');
}
/*
@@ -6016,7 +6016,7 @@ export async function saveChat(chatName, withMetadata, mesId) {
contentType: 'application/json',
success: function (data) { },
error: function (jqXHR, exception) {
- toastr.error('Check the server connection and reload the page to prevent data loss.', 'Chat could not be saved');
+ toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`);
console.log(exception);
console.log(jqXHR);
},
@@ -6413,7 +6413,7 @@ export async function getSettings() {
if (!response.ok) {
reloadLoop();
- toastr.error('Settings could not be loaded after multiple attempts. Please try again later.');
+ toastr.error(t`Settings could not be loaded after multiple attempts. Please try again later.`);
throw new Error('Error getting settings');
}
@@ -6640,7 +6640,7 @@ export async function saveSettings(type) {
eventSource.emit(event_types.SETTINGS_UPDATED);
},
error: function (jqXHR, exception) {
- toastr.error('Check the server connection and reload the page to prevent data loss.', 'Settings could not be saved');
+ toastr.error(t`Check the server connection and reload the page to prevent data loss.t`, t`Settings could not be saved`);
console.log(exception);
console.log(jqXHR);
},
@@ -6906,7 +6906,7 @@ export async function displayPastChats() {
const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats());
if (!data) {
- toastr.error('Could not load chat data. Try reloading the page.');
+ toastr.error(t`Could not load chat data. Try reloading the page.`);
return;
}
@@ -7039,7 +7039,7 @@ export function selectRightMenuWithAnimation(selectedMenuId) {
export function select_rm_info(type, charId, previousCharId = null) {
if (!type) {
- toastr.error('Invalid process (no \'type\')');
+ toastr.error(t`Invalid process (no 'type')`);
return;
}
if (type !== 'group_create') {
@@ -7047,20 +7047,20 @@ export function select_rm_info(type, charId, previousCharId = null) {
}
if (type === 'char_delete') {
- toastr.warning(`Character Deleted: ${displayName}`);
+ toastr.warning(t`Character Deleted: ${displayName}`);
}
if (type === 'char_create') {
- toastr.success(`Character Created: ${displayName}`);
+ toastr.success(t`Character Created: ${displayName}`);
}
if (type === 'group_create') {
- toastr.success('Group Created');
+ toastr.success(t`Group Created`);
}
if (type === 'group_delete') {
- toastr.warning('Group Deleted');
+ toastr.warning(t`Group Deleted`);
}
if (type === 'char_import') {
- toastr.success(`Character Imported: ${displayName}`);
+ toastr.success(t`Character Imported: ${displayName}`);
}
selectRightMenuWithAnimation('rm_characters_block');
@@ -7515,25 +7515,25 @@ export function hideSwipeButtons() {
*/
export async function deleteSwipe(swipeId = null) {
if (swipeId && (isNaN(swipeId) || swipeId < 0)) {
- toastr.warning(`Invalid swipe ID: ${swipeId + 1}`);
+ toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);
return;
}
const lastMessage = chat[chat.length - 1];
if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) {
- toastr.warning('No messages to delete swipes from.');
+ toastr.warning(t`No messages to delete swipes from.`);
return;
}
if (lastMessage.swipes.length <= 1) {
- toastr.warning('Can\'t delete the last swipe.');
+ toastr.warning(t`Can't delete the last swipe.`);
return;
}
swipeId = swipeId ?? lastMessage.swipe_id;
if (swipeId < 0 || swipeId >= lastMessage.swipes.length) {
- toastr.warning(`Invalid swipe ID: ${swipeId + 1}`);
+ toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`);
return;
}
@@ -7672,7 +7672,7 @@ export function setGenerationProgress(progress) {
function isHordeGenerationNotAllowed() {
if (main_api == 'koboldhorde' && preset_settings == 'gui') {
- toastr.error('GUI Settings preset is not supported for Horde. Please select another preset.');
+ toastr.error(t`GUI Settings preset is not supported for Horde. Please select another preset.`);
return true;
}
@@ -7721,7 +7721,7 @@ function openCharacterWorldPopup() {
}
$('#character_json_data').val(JSON.stringify(data));
- toastr.info('Embedded lorebook will be removed from this character.');
+ toastr.info(t`Embedded lorebook will be removed from this character.`);
} catch {
console.error('Failed to parse character JSON data.');
}
@@ -7912,11 +7912,11 @@ async function createOrEditCharacter(e) {
if ($('#form_create').attr('actiontype') == 'createcharacter') {
if (String($('#character_name_pole').val()).length === 0) {
- toastr.error('Name is required');
+ toastr.error(t`Name is required`);
return;
}
if (is_group_generating || is_send_press) {
- toastr.error('Cannot create characters while generating. Stop the request and try again.', 'Creation aborted');
+ toastr.error(t`Cannot create characters while generating. Stop the request and try again.`, t`Creation aborted`);
return;
}
try {
@@ -8000,7 +8000,7 @@ async function createOrEditCharacter(e) {
} catch (error) {
console.error('Error creating character', error);
- toastr.error('Failed to create character');
+ toastr.error(t`Failed to create character`);
}
} else {
try {
@@ -8059,7 +8059,7 @@ async function createOrEditCharacter(e) {
}
} catch (error) {
console.log(error);
- toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.');
+ toastr.error(t`Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.`);
}
}
}
@@ -8551,7 +8551,7 @@ async function selectContextCallback(args, name) {
const result = fuse.search(name);
if (result.length === 0) {
- !quiet && toastr.warning(`Context template "${name}" not found`);
+ !quiet && toastr.warning(t`Context template '${name}' not found`);
return '';
}
@@ -8571,7 +8571,7 @@ async function selectInstructCallback(args, name) {
const result = fuse.search(name);
if (result.length === 0) {
- !quiet && toastr.warning(`Instruct template "${name}" not found`);
+ !quiet && toastr.warning(t`Instruct template '${name}' not found`);
return '';
}
@@ -8623,7 +8623,7 @@ async function connectAPISlash(args, text) {
const apiConfig = CONNECT_API_MAP[text.toLowerCase()];
if (!apiConfig) {
- toastr.error(`Error: ${text} is not a valid API`);
+ toastr.error(t`Error: ${text} is not a valid API`);
return '';
}
@@ -8652,7 +8652,7 @@ async function connectAPISlash(args, text) {
}
const quiet = isTrueBoolean(args?.quiet);
- const toast = quiet ? jQuery() : toastr.info(`API set to ${text}, trying to connect..`);
+ const toast = quiet ? jQuery() : toastr.info(t`API set to ${text}, trying to connect..`);
try {
await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100);
@@ -8691,7 +8691,7 @@ export async function processDroppedFiles(files, data = new Map()) {
const preservedName = data instanceof Map && data.get(file);
await importCharacter(file, preservedName);
} else {
- toastr.warning('Unsupported file type: ' + file.name);
+ toastr.warning(t`Unsupported file type: ` + file.name);
}
}
}
@@ -8704,7 +8704,7 @@ export async function processDroppedFiles(files, data = new Map()) {
*/
async function importCharacter(file, preserveFileName = '') {
if (is_group_generating || is_send_press) {
- toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted');
+ toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`Import aborted`);
throw new Error('Cannot import character while generating');
}
@@ -8731,7 +8731,7 @@ async function importCharacter(file, preserveFileName = '') {
});
if (data.error) {
- toastr.error('The file is likely invalid or corrupted.', 'Could not import character');
+ toastr.error(t`The file is likely invalid or corrupted.`, t`Could not import character`);
return;
}
@@ -8784,7 +8784,7 @@ async function doImpersonate(args, prompt) {
await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100);
} catch {
console.warn('Timeout waiting for generation unlock');
- toastr.warning('Cannot run /impersonate command while the reply is being generated.');
+ toastr.warning(t`Cannot run /impersonate command while the reply is being generated.`);
return '';
}
@@ -8846,19 +8846,19 @@ async function doDeleteChat() {
async function doRenameChat(_, chatName) {
if (!chatName) {
- toastr.warning('Name must be provided as an argument to rename this chat.');
+ toastr.warning(t`Name must be provided as an argument to rename this chat.`);
return '';
}
const currentChatName = getCurrentChatId();
if (!currentChatName) {
- toastr.warning('No chat selected that can be renamed.');
+ toastr.warning(t`No chat selected that can be renamed.`);
return '';
}
await renameChat(currentChatName, chatName);
- toastr.success(`Successfully renamed chat to: ${chatName}`);
+ toastr.success(t`Successfully renamed chat to: ${chatName}`);
return '';
}
@@ -8973,7 +8973,7 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
for (const key of characterKey) {
const character = characters.find(x => x.avatar == key);
if (!character) {
- toastr.warning(`Character ${key} not found. Skipping deletion.`);
+ toastr.warning(t`Character ${key} not found. Skipping deletion.`);
continue;
}
@@ -8990,7 +8990,7 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
});
if (!response.ok) {
- toastr.error(`${response.status} ${response.statusText}`, 'Failed to delete character');
+ toastr.error(`${response.status} ${response.statusText}`, t`Failed to delete character`);
continue;
}
@@ -9665,12 +9665,7 @@ jQuery(async function () {
let deleteChats = false;
- const confirm = await Popup.show.confirm('Delete the character?', `
- THIS IS PERMANENT!
-
-
- Also delete the chat files
- `, {
+ const confirm = await Popup.show.confirm(t`Delete the character?`, await renderTemplateAsync('deleteConfirm'), {
onClose: () => deleteChats = !!$('#del_char_checkbox').prop('checked'),
});
if (!confirm) {
diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js
index 551b78328..00a34c86b 100644
--- a/public/scripts/PromptManager.js
+++ b/public/scripts/PromptManager.js
@@ -8,6 +8,7 @@ import { debounce, waitUntilCondition, escapeHtml } from './utils.js';
import { debounce_timeout } from './constants.js';
import { renderTemplateAsync } from './templates.js';
import { Popup } from './popup.js';
+import { t } from './i18n.js';
function debouncePromise(func, delay) {
let timeoutId;
@@ -455,7 +456,7 @@ class PromptManager {
// Delete selected prompt from list form and close edit form
this.handleDeletePrompt = async (event) => {
- Popup.show.confirm('Are you sure you want to delete this prompt?', null).then((userChoice) => {
+ Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {
if (!userChoice) return;
const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value;
const prompt = this.getPromptById(promptID);
@@ -531,7 +532,7 @@ class PromptManager {
// Import prompts for the selected character
this.handleImport = () => {
- Popup.show.confirm('Existing prompts with the same ID will be overridden. Do you want to proceed?', null)
+ Popup.show.confirm(t`Existing prompts with the same ID will be overridden. Do you want to proceed?`, null)
.then(userChoice => {
if (!userChoice) return;
@@ -552,7 +553,7 @@ class PromptManager {
const data = JSON.parse(fileContent);
this.import(data);
} catch (err) {
- toastr.error('An error occurred while importing prompts. More info available in console.');
+ toastr.error(t`An error occurred while importing prompts. More info available in console.`);
console.log('An error occurred while importing prompts');
console.log(err.toString());
}
@@ -567,7 +568,7 @@ class PromptManager {
// Restore default state of a characters prompt order
this.handleCharacterReset = () => {
- Popup.show.confirm('This will reset the prompt order for this character. You will not lose any prompts.', null)
+ Popup.show.confirm(t`This will reset the prompt order for this character. You will not lose any prompts.`, null)
.then(userChoice => {
if (!userChoice) return;
@@ -1649,7 +1650,7 @@ class PromptManager {
};
if (false === this.validateObject(controlObj, importData)) {
- toastr.warning('Could not import prompts. Export failed validation.');
+ toastr.warning(t`Could not import prompts. Export failed validation.`);
return;
}
@@ -1672,7 +1673,7 @@ class PromptManager {
throw new Error('Prompt order strategy not supported.');
}
- toastr.success('Prompt import complete.');
+ toastr.success(t`Prompt import complete.`);
this.saveServiceSettings().then(() => this.render());
}
diff --git a/public/scripts/authors-note.js b/public/scripts/authors-note.js
index 31a5719e1..5e1cf44a3 100644
--- a/public/scripts/authors-note.js
+++ b/public/scripts/authors-note.js
@@ -16,6 +16,7 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
export { MODULE_NAME as NOTE_MODULE_NAME };
+import { t } from './i18n.js';
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
@@ -37,7 +38,7 @@ const chara_note_position = {
function setNoteTextCommand(_, text) {
$('#extension_floating_prompt').val(text).trigger('input');
- toastr.success('Author\'s Note text updated');
+ toastr.success(t`Author's Note text updated`);
return '';
}
@@ -45,12 +46,12 @@ function setNoteDepthCommand(_, text) {
const value = Number(text);
if (Number.isNaN(value)) {
- toastr.error('Not a valid number');
+ toastr.error(t`Not a valid number`);
return;
}
$('#extension_floating_depth').val(Math.abs(value)).trigger('input');
- toastr.success('Author\'s Note depth updated');
+ toastr.success(t`Author's Note depth updated`);
return '';
}
@@ -58,12 +59,12 @@ function setNoteIntervalCommand(_, text) {
const value = Number(text);
if (Number.isNaN(value)) {
- toastr.error('Not a valid number');
+ toastr.error(t`Not a valid number`);
return;
}
$('#extension_floating_interval').val(Math.abs(value)).trigger('input');
- toastr.success('Author\'s Note frequency updated');
+ toastr.success(t`Author's Note frequency updated`);
return '';
}
@@ -76,12 +77,12 @@ function setNotePositionCommand(_, text) {
const position = validPositions[text?.trim()];
if (Number.isNaN(position)) {
- toastr.error('Not a valid position');
+ toastr.error(t`Not a valid position`);
return;
}
$(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input');
- toastr.info('Author\'s Note position updated');
+ toastr.info(t`Author's Note position updated`);
return '';
}
@@ -206,7 +207,7 @@ function onExtensionFloatingCharaPromptInput() {
extension_settings.note.chara.push(tempCharaNote);
} else {
console.log('Character author\'s note error: No avatar name key could be found.');
- toastr.error('Something went wrong. Could not save character\'s author\'s note.');
+ toastr.error(t`Something went wrong. Could not save character's author's note.`);
// Don't save settings if something went wrong
return;
@@ -397,7 +398,7 @@ function onANMenuItemClick() {
//because this listener takes priority
$('#options').stop().fadeOut(animation_duration);
} else {
- toastr.warning('Select a character before trying to use Author\'s Note', '', { timeOut: 2000 });
+ toastr.warning(t`Select a character before trying to use Author's Note`, '', { timeOut: 2000 });
}
}
diff --git a/public/scripts/chats.js b/public/scripts/chats.js
index 75ad746b9..d8f1e8dad 100644
--- a/public/scripts/chats.js
+++ b/public/scripts/chats.js
@@ -40,6 +40,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { ScraperManager } from './scrapers.js';
import { DragAndDropHandler } from './dragdrop.js';
import { renderTemplateAsync } from './templates.js';
+import { t } from './i18n.js';
/**
* @typedef {Object} FileAttachment
@@ -206,7 +207,7 @@ export async function populateFileAttachment(message, inputId = 'file_form_input
const fileText = await converter(file);
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
} catch (error) {
- toastr.error(String(error), 'Could not convert file');
+ toastr.error(String(error), t`Could not convert file`);
console.error('Could not convert file', error);
}
}
@@ -257,7 +258,7 @@ export async function uploadFileAttachment(fileName, base64Data) {
const responseData = await result.json();
return responseData.path;
} catch (error) {
- toastr.error(String(error), 'Could not upload file');
+ toastr.error(String(error), t`Could not upload file`);
console.error('Could not upload file', error);
}
}
@@ -283,7 +284,7 @@ export async function getFileAttachment(url) {
const text = await result.text();
return text;
} catch (error) {
- toastr.error(error, 'Could not download file');
+ toastr.error(error, t`Could not download file`);
console.error('Could not download file', error);
}
}
@@ -299,13 +300,13 @@ async function validateFile(file) {
const isBinary = /^[\x00-\x08\x0E-\x1F\x7F-\xFF]*$/.test(fileText);
if (!isImage && file.size > fileSizeLimit) {
- toastr.error(`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`);
+ toastr.error(t`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`);
return false;
}
// If file is binary
if (isBinary && !isImage && !isConvertible(file.type)) {
- toastr.error('Binary files are not supported. Select a text file or image.');
+ toastr.error(t`Binary files are not supported. Select a text file or image.`);
return false;
}
@@ -521,7 +522,7 @@ async function openExternalMediaOverridesDialog() {
const entityId = getCurrentEntityId();
if (!entityId) {
- toastr.info('No character or group selected');
+ toastr.info(t`No character or group selected`);
return;
}
@@ -646,7 +647,7 @@ async function deleteFileFromServer(url, silent = false) {
await eventSource.emit(event_types.FILE_ATTACHMENT_DELETED, url);
return true;
} catch (error) {
- toastr.error(String(error), 'Could not delete file');
+ toastr.error(String(error), t`Could not delete file`);
console.error('Could not delete file', error);
return false;
}
@@ -1054,7 +1055,7 @@ async function openAttachmentManager() {
const selectedAttachments = document.querySelectorAll('.attachmentListItemCheckboxContainer .attachmentListItemCheckbox:checked');
if (selectedAttachments.length === 0) {
- toastr.info('No attachments selected.', 'Data Bank');
+ toastr.info(t`No attachments selected.`, t`Data Bank`);
return;
}
@@ -1168,7 +1169,7 @@ async function runScraper(scraperId, target, callback) {
if (files.length === 0) {
console.warn('Scraping returned no files');
- toastr.info('No files were scraped.', 'Data Bank');
+ toastr.info(t`No files were scraped.`, t`Data Bank`);
return;
}
@@ -1176,12 +1177,12 @@ async function runScraper(scraperId, target, callback) {
await uploadFileAttachmentToServer(file, target);
}
- toastr.success(`Scraped ${files.length} files from ${scraperId} to ${target}.`, 'Data Bank');
+ toastr.success(t`Scraped ${files.length} files from ${scraperId} to ${target}.`, t`Data Bank`);
callback();
}
catch (error) {
console.error('Scraping failed', error);
- toastr.error('Check browser console for details.', 'Scraping failed');
+ toastr.error(t`Check browser console for details.`, t`Scraping failed`);
}
}
@@ -1208,7 +1209,7 @@ export async function uploadFileAttachmentToServer(file, target) {
const fileText = await converter(file);
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
} catch (error) {
- toastr.error(String(error), 'Could not convert file');
+ toastr.error(String(error), t`Could not convert file`);
console.error('Could not convert file', error);
}
} else {
diff --git a/public/scripts/extensions/regex/editor.html b/public/scripts/extensions/regex/editor.html
index 5552504e1..aa207ffd9 100644
--- a/public/scripts/extensions/regex/editor.html
+++ b/public/scripts/extensions/regex/editor.html
@@ -132,7 +132,7 @@
Ephemerality
-
+ Alter Chat Display
diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js
index 3e0b6955a..aebf565f6 100644
--- a/public/scripts/group-chats.js
+++ b/public/scripts/group-chats.js
@@ -189,8 +189,8 @@ async function validateGroup(group) {
group.members = group.members.filter(member => {
const character = characters.find(x => x.avatar === member || x.name === member);
if (!character) {
- const msg = `Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
- toastr.warning(msg, 'Group Validation');
+ const msg = t`Warning: Listed member ${member} does not exist as a character. It will be removed from the group.`;
+ toastr.warning(msg, t`Group Validation`);
console.warn(msg);
dirty = true;
}
@@ -522,7 +522,7 @@ async function saveGroupChat(groupId, shouldSaveGroup) {
});
if (!response.ok) {
- toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group Chat could not be saved');
+ toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group Chat could not be saved`);
console.error('Group chat could not be saved', response);
return;
}
@@ -837,7 +837,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
activatedMembers = activateSwipe(group.members);
if (activatedMembers.length === 0) {
- toastr.warning('Deleted group member swiped. To get a reply, add them back to the group.');
+ toastr.warning(t`Deleted group member swiped. To get a reply, add them back to the group.`);
throw new Error('Deleted group member swiped');
}
}
@@ -1368,15 +1368,15 @@ function isGroupMemberDisabled(avatarId) {
async function onDeleteGroupClick() {
if (!openGroupId) {
- toastr.warning('Currently no group selected.');
+ toastr.warning(t`Currently no group selected.`);
return;
}
if (is_group_generating) {
- toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.');
+ toastr.warning(t`Not so fast! Wait for the characters to stop typing before deleting the group.`);
return;
}
- const confirm = await Popup.show.confirm('Delete the group?', '
This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.
');
+ const confirm = await Popup.show.confirm(t`Delete the group?`, '
' + t`This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.` + '
');
if (confirm) {
deleteGroup(openGroupId);
}
@@ -1630,7 +1630,7 @@ function updateFavButtonState(state) {
export async function openGroupById(groupId) {
if (isChatSaving) {
- toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...');
+ toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
}
@@ -1659,7 +1659,7 @@ export async function openGroupById(groupId) {
function openCharacterDefinition(characterSelect) {
if (is_group_generating) {
- toastr.warning('Can\'t peek a character while group reply is being generated');
+ toastr.warning(t`Can't peek a character while group reply is being generated`);
console.warn('Can\'t peek a character def while group reply is being generated');
return;
}
@@ -1908,7 +1908,7 @@ export async function saveGroupBookmarkChat(groupId, name, metadata, mesId) {
});
if (!response.ok) {
- toastr.error('Check the server connection and reload the page to prevent data loss.', 'Group chat could not be saved');
+ toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group chat could not be saved`);
console.error('Group chat could not be saved', response);
}
}
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index df798132b..33142640d 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -388,7 +388,7 @@ async function validateReverseProxy() {
new URL(oai_settings.reverse_proxy);
}
catch (err) {
- toastr.error('Entered reverse proxy address is not a valid URL');
+ toastr.error(t`Entered reverse proxy address is not a valid URL`);
setOnlineStatus('no_connection');
resultCheckStatus();
throw err;
@@ -399,7 +399,7 @@ async function validateReverseProxy() {
const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
if (!confirmation) {
- toastr.error('Update or remove your reverse proxy settings.');
+ toastr.error(t`Update or remove your reverse proxy settings.`);
setOnlineStatus('no_connection');
resultCheckStatus();
throw new Error('Proxy connection denied.');
@@ -1225,15 +1225,15 @@ export async function prepareOpenAIMessages({
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.');
+ toastr.error(t`An error occurred while counting tokens: Token budget exceeded.`);
chatCompletion.log('Token budget exceeded.');
- promptManager.error = 'Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.';
+ promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.`;
} else if (error instanceof InvalidCharacterNameError) {
- toastr.warning('An error occurred while counting tokens: Invalid character name');
+ toastr.warning(t`An error occurred while counting tokens: Invalid character name`);
chatCompletion.log('Invalid character name');
- promptManager.error = 'The name of at least one character contained whitespaces or special characters. Please check your user and character name.';
+ promptManager.error = t`The name of at least one character contained whitespaces or special characters. Please check your user and character name.`;
} else {
- toastr.error('An unknown error occurred while counting tokens. Further information may be available in console.');
+ toastr.error(t`An unknown error occurred while counting tokens. Further information may be available in console.`);
chatCompletion.log('----- Unexpected error while preparing prompts -----');
chatCompletion.log(error);
chatCompletion.log(error.stack);
@@ -1287,11 +1287,8 @@ function tryParseStreamingError(response, decoded) {
}
}
-function checkQuotaError(data) {
- const errorText = `
Encountered an error while processing your request.
- Check you have credits available on your
- OpenAI account.
- If you have sufficient credits, please try again later.
`;
+async function checkQuotaError(data) {
+ const errorText = await renderTemplateAsync('quotaError');
if (!data) {
return;
@@ -1927,11 +1924,11 @@ async function sendOpenAIRequest(type, messages, signal) {
else {
const data = await response.json();
- checkQuotaError(data);
+ await checkQuotaError(data);
checkModerationError(data);
if (data.error) {
- toastr.error(data.error.message || response.statusText, 'API returned an error');
+ toastr.error(data.error.message || response.statusText, t`API returned an error`);
throw new Error(data);
}
@@ -2178,7 +2175,7 @@ function parseOpenAITextLogprobs(logprobs) {
function handleWindowError(err) {
const text = parseWindowError(err);
- toastr.error(text, 'Window.ai returned an error');
+ toastr.error(text, t`Window.ai returned an error`);
throw err;
}
@@ -3251,7 +3248,7 @@ async function getStatusOpen() {
}
function showWindowExtensionError() {
- toastr.error('Get it here: windowai.io', 'Extension is not installed', {
+ toastr.error(t`Get it here:` + ' windowai.io', t`Extension is not installed`, {
escapeHtml: false,
timeOut: 0,
extendedTimeOut: 0,
@@ -3370,7 +3367,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
if (triggerUi) $('#settings_preset_openai').append(option).trigger('change');
}
} else {
- toastr.error('Failed to save preset');
+ toastr.error(t`Failed to save preset`);
throw new Error('Failed to save preset');
}
}
@@ -3449,7 +3446,7 @@ async function createNewLogitBiasPreset() {
}
if (name in oai_settings.bias_presets) {
- toastr.error('Preset name should be unique.');
+ toastr.error(t`Preset name should be unique.`);
return;
}
@@ -3493,7 +3490,7 @@ async function onPresetImportFileChange(e) {
try {
presetBody = JSON.parse(importedFile);
} catch (err) {
- toastr.error('Invalid file');
+ toastr.error(t`Invalid file`);
return;
}
@@ -3534,7 +3531,7 @@ async function onPresetImportFileChange(e) {
});
if (!savePresetSettings.ok) {
- toastr.error('Failed to save preset');
+ toastr.error(t`Failed to save preset`);
return;
}
@@ -3559,7 +3556,7 @@ async function onPresetImportFileChange(e) {
async function onExportPresetClick() {
if (!oai_settings.preset_settings_openai) {
- toastr.error('No preset selected');
+ toastr.error(t`No preset selected`);
return;
}
@@ -3600,12 +3597,12 @@ async function onLogitBiasPresetImportFileChange(e) {
e.target.value = '';
if (name in oai_settings.bias_presets) {
- toastr.error('Preset name should be unique.');
+ toastr.error(t`Preset name should be unique.`);
return;
}
if (!Array.isArray(importedFile)) {
- toastr.error('Invalid logit bias preset file.');
+ toastr.error(t`Invalid logit bias preset file.`);
return;
}
@@ -3664,16 +3661,16 @@ async function onDeletePresetClick() {
});
if (!response.ok) {
- toastr.warning('Preset was not deleted from server');
+ toastr.warning(t`Preset was not deleted from server`);
} else {
- toastr.success('Preset deleted');
+ toastr.success(t`Preset deleted`);
}
saveSettingsDebounced();
}
async function onLogitBiasPresetDeleteClick() {
- const value = await callPopup('Delete the preset?', 'confirm');
+ const value = await callPopup(t`Delete the preset?`, 'confirm');
if (!value) {
return;
@@ -4808,7 +4805,7 @@ function runProxyCallback(_, value) {
const result = fuse.search(value);
if (result.length === 0) {
- toastr.warning(`Proxy preset "${value}" not found`);
+ toastr.warning(t`Proxy preset '${value}' not found`);
return '';
}
@@ -4982,7 +4979,7 @@ export function initOpenAI() {
$('#update_oai_preset').on('click', async function () {
const name = oai_settings.preset_settings_openai;
await saveOpenAIPreset(name, oai_settings);
- toastr.success('Preset updated');
+ toastr.success(t`Preset updated`);
});
$('#impersonation_prompt_restore').on('click', function () {
diff --git a/public/scripts/templates/assistantNote.html b/public/scripts/templates/assistantNote.html
index 148b945e3..a7a2ad9fb 100644
--- a/public/scripts/templates/assistantNote.html
+++ b/public/scripts/templates/assistantNote.html
@@ -1,3 +1,3 @@
- Note: this chat is temporary and will be deleted as soon as you leave it.
+ Note:this chat is temporary and will be deleted as soon as you leave it.
diff --git a/public/scripts/templates/deleteConfirm.html b/public/scripts/templates/deleteConfirm.html
new file mode 100644
index 000000000..f468fe74d
--- /dev/null
+++ b/public/scripts/templates/deleteConfirm.html
@@ -0,0 +1,5 @@
+THIS IS PERMANENT!
+
+
+ Also delete the chat files
+
\ No newline at end of file
diff --git a/public/scripts/templates/duplicateConfirm.html b/public/scripts/templates/duplicateConfirm.html
index 7cfa8e3c3..9df24f02f 100644
--- a/public/scripts/templates/duplicateConfirm.html
+++ b/public/scripts/templates/duplicateConfirm.html
@@ -1,5 +1,5 @@
-
Are you sure you want to duplicate this character?
- If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.
+
Are you sure you want to duplicate this character?
+ If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.
- Works in the chatbar and textareas marked with this icon:
+ Works in the chatbar and textareas marked with this icon:
-
Ctrl+B = **bold**
-
Ctrl+I = *italic*
-
Ctrl+U = __underline__
-
Ctrl+K = `inline code`
-
Ctrl+Shift+~ = ~~strikethrough~~
+
Ctrl+B = **bold**
+
Ctrl+I = *italic*
+
Ctrl+U = __underline__
+
Ctrl+K = `inline code`
+
Ctrl+Shift+~ = ~~strikethrough~~
diff --git a/public/scripts/templates/quotaError.html b/public/scripts/templates/quotaError.html
new file mode 100644
index 000000000..eae1ba573
--- /dev/null
+++ b/public/scripts/templates/quotaError.html
@@ -0,0 +1,4 @@
+
Encountered an error while processing your request.
+ Check you have credits available on your
+ OpenAI account.
+ If you have sufficient credits, please try again later.
\ No newline at end of file
From 33222191be5782c8503addd3d10a7fc9afecae08 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 19:15:53 +0300
Subject: [PATCH 149/268] Remove confusing toast from listvar
---
public/scripts/variables.js | 3 ---
1 file changed, 3 deletions(-)
diff --git a/public/scripts/variables.js b/public/scripts/variables.js
index 93c2238fd..c768c4f5e 100644
--- a/public/scripts/variables.js
+++ b/public/scripts/variables.js
@@ -313,9 +313,6 @@ async function listVariablesCallback(args) {
if (args.format) {
toastr.warning(`Legacy argument 'format' with value '${args.format}' is deprecated. Please use 'return' instead. Routing to the correct return type...`, 'Deprecation warning');
const type = String(args?.format).toLowerCase().trim();
- if (!chat_metadata.script_injects || !Object.keys(chat_metadata.script_injects).length) {
- type !== 'none' && toastr.info('No script injections for the current chat');
- }
switch (type) {
case 'none':
returnType = 'none';
From cd297f926eb2ec0a289728bbdf8fb113dc87d01f Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 19:38:25 +0300
Subject: [PATCH 150/268] Smaller swipe arrows
---
public/style.css | 31 +++++++++++++++++++------------
1 file changed, 19 insertions(+), 12 deletions(-)
diff --git a/public/style.css b/public/style.css
index 244d371f4..e7cf90cc4 100644
--- a/public/style.css
+++ b/public/style.css
@@ -969,24 +969,29 @@ body .panelControlBar {
/* SWIPE RELATED STYLES*/
+.mes {
+ --swipeCounterHeight: 15px;
+ --swipeCounterMargin: 5px;
+}
+
.swipe_right,
.swipe_left {
- height: 40px;
- width: 40px;
+ width: 25px;
+ height: 25px;
opacity: 0.3;
align-items: center;
justify-content: center;
z-index: 9999;
grid-row-start: 2;
- font-size: 30px;
+ font-size: 20px;
cursor: pointer;
align-self: center;
-
}
-.swipe_left{
-position: absolute;
-bottom: 15px;
-flex-flow: column;
+
+.swipe_left {
+ position: absolute;
+ bottom: calc(var(--swipeCounterHeight) + var(--swipeCounterMargin));
+ flex-flow: column;
}
.swipeRightBlock {
@@ -995,17 +1000,19 @@ flex-flow: column;
bottom: 0;
}
-
.swipes-counter {
color: var(--SmartThemeBodyColor);
font-size: 12px;
- padding: 0;
+ padding: 0 5px;
font-family: var(--mainFontFamily);
font-weight: 400;
align-self: center;
min-width: 40px;
display: flex;
justify-content: center;
+ opacity: 0.7;
+ margin-bottom: var(--swipeCounterMargin);
+ height: var(--swipeCounterHeight);
}
.swipe_left {
@@ -1015,7 +1022,7 @@ flex-flow: column;
.swipe_right {
right: 5px;
- align-self:end;
+ align-self: center;
}
.ui-settings {
@@ -2649,7 +2656,7 @@ select option:not(:checked) {
#instruct_enabled_label .menu_button:not(.toggleEnabled),
#sysprompt_enabled_label .menu_button:not(.toggleEnabled) {
- color: Red;
+ color: Red;
}
.displayBlock {
From d5b845345e2cb03fae086b446aad08eb484cb20e Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 19:48:46 +0300
Subject: [PATCH 151/268] Better opacity handling. Remove debug logs
---
public/script.js | 20 ++++++++++++--------
public/scripts/power-user.js | 2 +-
public/style.css | 5 ++++-
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/public/script.js b/public/script.js
index e5cdb8d64..48ebbdc78 100644
--- a/public/script.js
+++ b/public/script.js
@@ -7493,29 +7493,33 @@ export function showSwipeButtons() {
const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
const swipeId = chat[chat.length - 1].swipe_id;
const swipeCounterText = (`${(swipeId + 1)}\u200B/\u200b${(chat[chat.length - 1].swipes.length)}`);
+ const swipeRight = currentMessage.find('.swipe_right');
+ const swipeLeft = currentMessage.find('.swipe_left');
+ const swipeCounter = currentMessage.find('.swipes-counter');
if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) {
- currentMessage.children('.swipe_left').css('display', 'flex');
+ swipeLeft.css('display', 'flex');
}
//only show right when generate is off, or when next right swipe would not make a generate happen
if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) {
- console.error('showingSwipeButtons: showing.');
- currentMessage.find('.swipe_right').css('display', 'flex');
- currentMessage.find('.swipe_right').css('opacity', '0.3');
+ // console.error('showingSwipeButtons: showing.');
+ swipeRight.css('display', 'flex').css('opacity', '0.3');
+ swipeCounter.css('opacity', '0.3');
}
//console.log((chat[chat.length - 1]));
if ((chat[chat.length - 1].swipes.length - swipeId) === 1) {
- console.error('highlighting R swipe');
+ // console.error('highlighting R swipe');
//chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not
//necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on
- currentMessage.find('.swipe_right').css('opacity', '0.7');
+ swipeRight.css('opacity', '0.7');
+ swipeCounter.css('opacity', '0.7');
}
//console.log(swipesCounterHTML);
//allows for writing individual swipe counters for past messages
- $('.last_mes .swipes-counter').text(swipeCounterText);
- $('.last_mes .swipes-counter').show();
+ const lastSwipeCounter = $('.last_mes .swipes-counter');
+ lastSwipeCounter.text(swipeCounterText).show();
//console.log(swipeId);
//console.log(chat[chat.length - 1].swipes.length);
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index 79c5eb724..bd618bad0 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -472,7 +472,7 @@ function switchCompactInputArea() {
export function switchSwipeNumAllMessages() {
$('#show_swipe_num_all_messages').prop('checked', power_user.show_swipe_num_all_messages);
- $('.mes:not(.last_mes) .swipes-counter').toggle(power_user.show_swipe_num_all_messages);
+ $('.mes:not(.last_mes) .swipes-counter').css('opacity', '').toggle(power_user.show_swipe_num_all_messages);
}
var originalSliderValues = [];
diff --git a/public/style.css b/public/style.css
index e7cf90cc4..01a49ab9f 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1010,11 +1010,14 @@ body .panelControlBar {
min-width: 40px;
display: flex;
justify-content: center;
- opacity: 0.7;
margin-bottom: var(--swipeCounterMargin);
height: var(--swipeCounterHeight);
}
+.mes:not(.last_mes) .swipes-counter {
+ opacity: 0.3;
+}
+
.swipe_left {
right: auto;
left: 20px;
From 80de28d251b293bc3acb9405b53cc75396167939 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 19:50:56 +0300
Subject: [PATCH 152/268] Remove debug log
---
public/script.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/script.js b/public/script.js
index 48ebbdc78..994a0be91 100644
--- a/public/script.js
+++ b/public/script.js
@@ -7528,7 +7528,7 @@ export function showSwipeButtons() {
}
export function hideSwipeButtons() {
- console.error('hideswipebuttons entered');
+ // console.error('hideswipebuttons entered');
$('#chat').find('.swipe_right').hide();
$('#chat').find('.last_mes .swipes-counter').hide();
$('#chat').find('.swipe_left').hide();
From a23c31dd09e396934f4da76750efb509c0e60164 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 20:42:03 +0300
Subject: [PATCH 153/268] Update visuals and labels of CC postfix/prefix
drawers
---
public/index.html | 23 ++++++++++-------------
1 file changed, 10 insertions(+), 13 deletions(-)
diff --git a/public/index.html b/public/index.html
index 741871edd..d272ca96a 100644
--- a/public/index.html
+++ b/public/index.html
@@ -1709,9 +1709,9 @@
-
+
Character Names Behavior
-
+
()
@@ -1720,25 +1720,22 @@
None
-
-
- Never add character names.
+
+ Never add character name prefixes. May behave poorly in groups, choose with caution.
Default
-
-
- Don't add character names unless necessary.
+
+ Add prefixes for groups and past personas. Otherwise, make sure you provide names in the prompt.
Completion Object
-
-
- Add character names to completion objects.
+
+ Add character names to completion objects. Restrictions apply: only Latin alphanumerics and underscores.
@@ -1754,9 +1751,9 @@
-
+
Continue Postfix
-
+
()
From 7d0cc9e372b57efd2d31baa78f1a7160aa128184 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Tue, 1 Oct 2024 20:48:45 +0300
Subject: [PATCH 154/268] Remove commented debug code
---
public/script.js | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/public/script.js b/public/script.js
index 994a0be91..35bfcbab0 100644
--- a/public/script.js
+++ b/public/script.js
@@ -2370,8 +2370,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
addCopyToCodeBlocks(newMessage);
- //const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
-
// Set the swipes counter for past messages, only visible if 'Show Swipes on All Message' is enabled
if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) {
const swipesNum = chat[newMessageId].swipes?.length;
@@ -2379,11 +2377,9 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
newMessage.find('.swipes-counter').text(`${swipeId}\u200B/\u200b${swipesNum}`);
}
-
if (showSwipes) {
$('#chat .mes').last().addClass('last_mes');
$('#chat .mes').eq(-2).removeClass('last_mes');
- //.find('.swipe_right').hide(); //otherwise it stays looking like it did when it was last_mes
hideSwipeButtons();
showSwipeButtons();
}
@@ -7483,7 +7479,6 @@ export function showSwipeButtons() {
//had to add this to make the swipe counter work
//(copied from the onclick functions for swipe buttons..
//don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..)
-
if (chat[chat.length - 1]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array
chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0
chat[chat.length - 1]['swipes'] = []; // empty the array
@@ -7502,33 +7497,24 @@ export function showSwipeButtons() {
}
//only show right when generate is off, or when next right swipe would not make a generate happen
if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) {
- // console.error('showingSwipeButtons: showing.');
swipeRight.css('display', 'flex').css('opacity', '0.3');
swipeCounter.css('opacity', '0.3');
}
- //console.log((chat[chat.length - 1]));
if ((chat[chat.length - 1].swipes.length - swipeId) === 1) {
- // console.error('highlighting R swipe');
-
//chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not
//necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on
swipeRight.css('opacity', '0.7');
swipeCounter.css('opacity', '0.7');
}
- //console.log(swipesCounterHTML);
//allows for writing individual swipe counters for past messages
const lastSwipeCounter = $('.last_mes .swipes-counter');
lastSwipeCounter.text(swipeCounterText).show();
- //console.log(swipeId);
- //console.log(chat[chat.length - 1].swipes.length);
-
switchSwipeNumAllMessages();
}
export function hideSwipeButtons() {
- // console.error('hideswipebuttons entered');
$('#chat').find('.swipe_right').hide();
$('#chat').find('.last_mes .swipes-counter').hide();
$('#chat').find('.swipe_left').hide();
From 3dad029091cf39175c40a44146d621471ab47411 Mon Sep 17 00:00:00 2001
From: ceruleandeep
Date: Wed, 2 Oct 2024 06:55:32 +1000
Subject: [PATCH 155/268] Add Featherless to modelSelectMap for /model
---
public/scripts/slash-commands.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index bc0941968..a34b4e544 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -3612,6 +3612,7 @@ function getModelOptions(quiet) {
{ id: 'aphrodite_model', api: 'textgenerationwebui', type: textgen_types.APHRODITE },
{ id: 'ollama_model', api: 'textgenerationwebui', type: textgen_types.OLLAMA },
{ id: 'tabby_model', api: 'textgenerationwebui', type: textgen_types.TABBY },
+ { id: 'featherless_model', api: 'textgenerationwebui', type: textgen_types.FEATHERLESS },
{ id: 'model_openai_select', api: 'openai', type: chat_completion_sources.OPENAI },
{ id: 'model_claude_select', api: 'openai', type: chat_completion_sources.CLAUDE },
{ id: 'model_windowai_select', api: 'openai', type: chat_completion_sources.WINDOWAI },
From 8006795897c54180823323357ad5a74c8660fe25 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 01:00:48 +0300
Subject: [PATCH 156/268] New tool calling framework
---
public/global.d.ts | 41 --
public/script.js | 35 +-
.../scripts/extensions/expressions/index.js | 52 +--
public/scripts/openai.js | 149 +------
public/scripts/tool-calling.js | 381 ++++++++++++++++++
src/endpoints/backends/chat-completions.js | 22 +-
6 files changed, 427 insertions(+), 253 deletions(-)
create mode 100644 public/scripts/tool-calling.js
diff --git a/public/global.d.ts b/public/global.d.ts
index c8bfe14c2..39ba4ea0c 100644
--- a/public/global.d.ts
+++ b/public/global.d.ts
@@ -1365,44 +1365,3 @@ declare namespace moment {
declare global {
const moment: typeof moment;
}
-
-/**
- * Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool can be registered.
- */
-interface FunctionToolRegister {
- /**
- * The type of generation that is being used
- */
- type?: string;
- /**
- * Generation data, including messages and sampling parameters
- */
- data: Record;
- /**
- * Callback to register an LLM function tool.
- */
- registerFunctionTool: typeof registerFunctionTool;
-}
-
-/**
- * Callback data for the `LLM_FUNCTION_TOOL_REGISTER` event type that is triggered when a function tool is registered.
- * @param name Name of the function tool to register
- * @param description Description of the function tool
- * @param params JSON schema for the parameters of the function tool
- * @param required Whether the function tool should be forced to be used
- */
-declare function registerFunctionTool(name: string, description: string, params: object, required: boolean): Promise;
-
-/**
- * Callback data for the `LLM_FUNCTION_TOOL_CALL` event type that is triggered when a function tool is called.
- */
-interface FunctionToolCall {
- /**
- * Name of the function tool to call
- */
- name: string;
- /**
- * JSON object with the parameters to pass to the function tool
- */
- arguments: string;
-}
diff --git a/public/script.js b/public/script.js
index 35bfcbab0..fac11b8f4 100644
--- a/public/script.js
+++ b/public/script.js
@@ -246,6 +246,7 @@ import { initInputMarkdown } from './scripts/input-md-formatting.js';
import { AbortReason } from './scripts/util/AbortReason.js';
import { initSystemPrompts } from './scripts/sysprompt.js';
import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js';
+import { ToolManager } from './scripts/tool-calling.js';
//exporting functions and vars for mods
export {
@@ -463,8 +464,6 @@ export const event_types = {
FILE_ATTACHMENT_DELETED: 'file_attachment_deleted',
WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate',
OPEN_CHARACTER_LIBRARY: 'open_character_library',
- LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register',
- LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call',
ONLINE_STATUS_CHANGED: 'online_status_changed',
IMAGE_SWIPED: 'image_swiped',
CONNECTION_PROFILE_LOADED: 'connection_profile_loaded',
@@ -2921,6 +2920,7 @@ class StreamingProcessor {
this.swipes = [];
/** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
this.messageLogprobs = [];
+ this.toolCalls = [];
}
#checkDomElements(messageId) {
@@ -3139,7 +3139,7 @@ class StreamingProcessor {
}
/**
- * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs }, void, void>}
+ * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, void, void>}
*/
*nullStreamingGeneration() {
throw new Error('Generation function for streaming is not hooked up');
@@ -3161,12 +3161,13 @@ class StreamingProcessor {
try {
const sw = new Stopwatch(1000 / power_user.streaming_fps);
const timestamps = [];
- for await (const { text, swipes, logprobs } of this.generator()) {
+ for await (const { text, swipes, logprobs, toolCalls } of this.generator()) {
timestamps.push(Date.now());
if (this.isStopped) {
return;
}
+ this.toolCalls = toolCalls;
this.result = text;
this.swipes = Array.from(swipes ?? []);
if (logprobs) {
@@ -4405,6 +4406,20 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
getMessage = continue_mag + getMessage;
}
+ if (ToolManager.isFunctionCallingSupported() && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
+ const invocations = await ToolManager.checkFunctionToolCalls(streamingProcessor.toolCalls);
+ if (invocations.length) {
+ const lastMessage = chat[chat.length - 1];
+ const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
+ if (shouldDeleteMessage) {
+ await deleteLastMessage();
+ streamingProcessor = null;
+ }
+ ToolManager.saveFunctionToolInvocations(invocations);
+ return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
+ }
+ }
+
if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {
await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);
streamingProcessor = null;
@@ -4440,6 +4455,14 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
throw new Error(data?.response);
}
+ if (ToolManager.isFunctionCallingSupported()) {
+ const invocations = await ToolManager.checkFunctionToolCalls(data);
+ if (invocations.length) {
+ ToolManager.saveFunctionToolInvocations(invocations);
+ return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
+ }
+ }
+
//const getData = await response.json();
let getMessage = extractMessageFromData(data);
let title = extractTitleFromData(data);
@@ -7853,7 +7876,7 @@ function openAlternateGreetings() {
if (menu_type !== 'create') {
await createOrEditCharacter();
}
- }
+ },
});
for (let index = 0; index < getArray().length; index++) {
@@ -8130,6 +8153,8 @@ window['SillyTavern'].getContext = function () {
registerHelper: () => { },
registerMacro: MacrosParser.registerMacro.bind(MacrosParser),
unregisterMacro: MacrosParser.unregisterMacro.bind(MacrosParser),
+ registerFunctionTool: ToolManager.registerFunctionTool.bind(ToolManager),
+ unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
registerDebugFunction: registerDebugFunction,
/** @deprecated Use renderExtensionTemplateAsync instead. */
renderExtensionTemplate: renderExtensionTemplate,
diff --git a/public/scripts/extensions/expressions/index.js b/public/scripts/extensions/expressions/index.js
index 4a09e3aeb..078375cf1 100644
--- a/public/scripts/extensions/expressions/index.js
+++ b/public/scripts/extensions/expressions/index.js
@@ -9,7 +9,6 @@ import { debounce_timeout } from '../../constants.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
-import { isFunctionCallingSupported } from '../../openai.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
@@ -21,7 +20,6 @@ const UPDATE_INTERVAL = 2000;
const STREAMING_UPDATE_INTERVAL = 10000;
const TALKINGCHECK_UPDATE_INTERVAL = 500;
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
-const FUNCTION_NAME = 'set_emotion';
const DEFAULT_LLM_PROMPT = 'Ignore previous instructions. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
const DEFAULT_EXPRESSIONS = [
'talkinghead',
@@ -1017,10 +1015,6 @@ async function getLlmPrompt(labels) {
return '';
}
- if (isFunctionCallingSupported()) {
- return '';
- }
-
const labelsString = labels.map(x => `"${x}"`).join(', ');
const prompt = substituteParamsExtended(String(extension_settings.expressions.llmPrompt), { labels: labelsString });
return prompt;
@@ -1056,41 +1050,6 @@ function parseLlmResponse(emotionResponse, labels) {
throw new Error('Could not parse emotion response ' + emotionResponse);
}
-/**
- * Registers the function tool for the LLM API.
- * @param {FunctionToolRegister} args Function tool register arguments.
- */
-function onFunctionToolRegister(args) {
- if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
- // Only trigger on quiet mode
- if (args.type !== 'quiet') {
- return;
- }
-
- const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
- const jsonSchema = {
- $schema: 'http://json-schema.org/draft-04/schema#',
- type: 'object',
- properties: {
- emotion: {
- type: 'string',
- enum: emotions,
- description: `One of the following: ${JSON.stringify(emotions)}`,
- },
- },
- required: [
- 'emotion',
- ],
- };
- args.registerFunctionTool(
- FUNCTION_NAME,
- substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
- jsonSchema,
- true,
- );
- }
-}
-
function onTextGenSettingsReady(args) {
// Only call if inside an API call
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
@@ -1164,18 +1123,9 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
const expressionsList = await getExpressionsList();
const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
- let functionResult = null;
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
- eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
- eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
- if (args.name !== FUNCTION_NAME) {
- return;
- }
-
- functionResult = args?.arguments;
- });
const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
- return parseLlmResponse(functionResult || emotionResponse, expressionsList);
+ return parseLlmResponse(emotionResponse, expressionsList);
}
// Extras
default: {
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 20c141316..a6048dce5 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -70,6 +70,7 @@ import { renderTemplateAsync } from './templates.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { Popup, POPUP_RESULT } from './popup.js';
import { t } from './i18n.js';
+import { ToolManager } from './tool-calling.js';
export {
openai_messages_count,
@@ -1863,8 +1864,8 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['seed'] = oai_settings.seed;
}
- if (isFunctionCallingSupported() && !stream) {
- await registerFunctionTools(type, generate_data);
+ if (!canMultiSwipe && ToolManager.isFunctionCallingSupported()) {
+ await ToolManager.registerFunctionToolsOpenAI(generate_data);
}
if (isOAI && oai_settings.openai_model.startsWith('o1-')) {
@@ -1911,6 +1912,7 @@ async function sendOpenAIRequest(type, messages, signal) {
return async function* streamData() {
let text = '';
const swipes = [];
+ const toolCalls = [];
while (true) {
const { done, value } = await reader.read();
if (done) return;
@@ -1926,7 +1928,9 @@ async function sendOpenAIRequest(type, messages, signal) {
text += getStreamingReply(parsed);
}
- yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed) };
+ ToolManager.parseToolCalls(toolCalls, parsed);
+
+ yield { text, swipes: swipes, logprobs: parseChatCompletionLogprobs(parsed), toolCalls: toolCalls };
}
};
}
@@ -1948,147 +1952,10 @@ async function sendOpenAIRequest(type, messages, signal) {
delay(1).then(() => saveLogprobsForActiveMessage(logprobs, null));
}
- if (isFunctionCallingSupported()) {
- await checkFunctionToolCalls(data);
- }
-
return data;
}
}
-/**
- * Register function tools for the next chat completion request.
- * @param {string} type Generation type
- * @param {object} data Generation data
- */
-async function registerFunctionTools(type, data) {
- let toolChoice = 'auto';
- const tools = [];
-
- /**
- * @type {registerFunctionTool}
- */
- const registerFunctionTool = (name, description, parameters, required) => {
- tools.push({
- type: 'function',
- function: {
- name,
- description,
- parameters,
- },
- });
-
- if (required) {
- toolChoice = 'required';
- }
- };
-
- /**
- * @type {FunctionToolRegister}
- */
- const args = {
- type,
- data,
- registerFunctionTool,
- };
-
- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_REGISTER, args);
-
- if (tools.length) {
- console.log('Registered function tools:', tools);
-
- data['tools'] = tools;
- data['tool_choice'] = toolChoice;
- }
-}
-
-async function checkFunctionToolCalls(data) {
- const oaiCompat = [
- chat_completion_sources.OPENAI,
- chat_completion_sources.CUSTOM,
- chat_completion_sources.MISTRALAI,
- chat_completion_sources.OPENROUTER,
- chat_completion_sources.GROQ,
- ];
- if (oaiCompat.includes(oai_settings.chat_completion_source)) {
- if (!Array.isArray(data?.choices)) {
- return;
- }
-
- // Find a choice with 0-index
- const choice = data.choices.find(choice => choice.index === 0);
-
- if (!choice) {
- return;
- }
-
- const toolCalls = choice.message.tool_calls;
-
- if (!Array.isArray(toolCalls)) {
- return;
- }
-
- for (const toolCall of toolCalls) {
- if (typeof toolCall.function !== 'object') {
- continue;
- }
-
- /** @type {FunctionToolCall} */
- const args = toolCall.function;
- console.log('Function tool call:', toolCall);
- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
- }
- }
-
- if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
- if (!Array.isArray(data?.content)) {
- return;
- }
-
- for (const content of data.content) {
- if (content.type === 'tool_use') {
- /** @type {FunctionToolCall} */
- const args = { name: content.name, arguments: JSON.stringify(content.input) };
- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
- }
- }
- }
-
- if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
- if (!Array.isArray(data?.tool_calls)) {
- return;
- }
-
- for (const toolCall of data.tool_calls) {
- /** @type {FunctionToolCall} */
- const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
- console.log('Function tool call:', toolCall);
- await eventSource.emit(event_types.LLM_FUNCTION_TOOL_CALL, args);
- }
- }
-}
-
-export function isFunctionCallingSupported() {
- if (main_api !== 'openai') {
- return false;
- }
-
- if (!oai_settings.function_calling) {
- return false;
- }
-
- const supportedSources = [
- chat_completion_sources.OPENAI,
- chat_completion_sources.COHERE,
- chat_completion_sources.CUSTOM,
- chat_completion_sources.MISTRALAI,
- chat_completion_sources.CLAUDE,
- chat_completion_sources.OPENROUTER,
- chat_completion_sources.GROQ,
- ];
- return supportedSources.includes(oai_settings.chat_completion_source);
-}
-
function getStreamingReply(data) {
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
return data?.delta?.text || '';
@@ -4019,7 +3886,7 @@ async function onModelChange() {
$('#openai_max_context').attr('max', max_32k);
} else if (value === 'text-bison-001') {
$('#openai_max_context').attr('max', max_8k);
- // The ultra endpoints are possibly dead:
+ // The ultra endpoints are possibly dead:
} else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {
$('#openai_max_context').attr('max', max_32k);
} else {
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
new file mode 100644
index 000000000..c2f6c209f
--- /dev/null
+++ b/public/scripts/tool-calling.js
@@ -0,0 +1,381 @@
+import { chat, main_api } from '../script.js';
+import { chat_completion_sources, oai_settings } from './openai.js';
+
+/**
+ * @typedef {object} ToolInvocation
+ * @property {string} id - A unique identifier for the tool invocation.
+ * @property {string} name - The name of the tool.
+ * @property {string} parameters - The parameters for the tool invocation.
+ * @property {string} result - The result of the tool invocation.
+ */
+
+/**
+ * A class that represents a tool definition.
+ */
+class ToolDefinition {
+ /**
+ * A unique name for the tool.
+ * @type {string}
+ */
+ #name;
+
+ /**
+ * A description of what the tool does.
+ * @type {string}
+ */
+ #description;
+
+ /**
+ * A JSON schema for the parameters that the tool accepts.
+ * @type {object}
+ */
+ #parameters;
+
+ /**
+ * A function that will be called when the tool is executed.
+ * @type {function}
+ */
+ #action;
+
+ /**
+ * Creates a new ToolDefinition.
+ * @param {string} name A unique name for the tool.
+ * @param {string} description A description of what the tool does.
+ * @param {object} parameters A JSON schema for the parameters that the tool accepts.
+ * @param {function} action A function that will be called when the tool is executed.
+ */
+ constructor(name, description, parameters, action) {
+ this.#name = name;
+ this.#description = description;
+ this.#parameters = parameters;
+ this.#action = action;
+ }
+
+ /**
+ * Converts the ToolDefinition to an OpenAI API representation
+ * @returns {object} OpenAI API representation of the tool.
+ */
+ toFunctionOpenAI() {
+ return {
+ type: 'function',
+ function: {
+ name: this.#name,
+ description: this.#description,
+ parameters: this.#parameters,
+ },
+ };
+ }
+
+ /**
+ * Invokes the tool with the given parameters.
+ * @param {object} parameters The parameters to pass to the tool.
+ * @returns {Promise} The result of the tool's action function.
+ */
+ async invoke(parameters) {
+ return await this.#action(parameters);
+ }
+}
+
+/**
+ * A class that manages the registration and invocation of tools.
+ */
+export class ToolManager {
+ /**
+ * A map of tool names to tool definitions.
+ * @type {Map}
+ */
+ static #tools = new Map();
+
+ /**
+ * Returns an Array of all tools that have been registered.
+ * @type {ToolDefinition[]}
+ */
+ static get tools() {
+ return Array.from(this.#tools.values());
+ }
+
+ /**
+ * Registers a new tool with the tool registry.
+ * @param {string} name The name of the tool.
+ * @param {string} description A description of what the tool does.
+ * @param {object} parameters A JSON schema for the parameters that the tool accepts.
+ * @param {function} action A function that will be called when the tool is executed.
+ */
+ static registerFunctionTool(name, description, parameters, action) {
+ if (this.#tools.has(name)) {
+ console.warn(`A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
+ }
+
+ const definition = new ToolDefinition(name, description, parameters, action);
+ this.#tools.set(name, definition);
+ }
+
+ /**
+ * Removes a tool from the tool registry.
+ * @param {string} name The name of the tool to unregister.
+ */
+ static unregisterFunctionTool(name) {
+ if (!this.#tools.has(name)) {
+ console.warn(`No tool with the name "${name}" has been registered.`);
+ return;
+ }
+
+ this.#tools.delete(name);
+ }
+
+ /**
+ * Invokes a tool by name. Returns the result of the tool's action function.
+ * @param {string} name The name of the tool to invoke.
+ * @param {object} parameters Function parameters. For example, if the tool requires a "name" parameter, you would pass {name: "value"}.
+ * @returns {Promise} The result of the tool's action function. If an error occurs, null is returned. Non-string results are JSON-stringified.
+ */
+ static async invokeFunctionTool(name, parameters) {
+ try {
+ if (!this.#tools.has(name)) {
+ throw new Error(`No tool with the name "${name}" has been registered.`);
+ }
+
+ const invokeParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
+ const tool = this.#tools.get(name);
+ const result = await tool.invoke(invokeParameters);
+ return typeof result === 'string' ? result : JSON.stringify(result);
+ } catch (error) {
+ console.error(`An error occurred while invoking the tool "${name}":`, error);
+ return null;
+ }
+ }
+
+ /**
+ * Register function tools for the next chat completion request.
+ * @param {object} data Generation data
+ */
+ static async registerFunctionToolsOpenAI(data) {
+ const tools = [];
+
+ for (const tool of ToolManager.tools) {
+ tools.push(tool.toFunctionOpenAI());
+ }
+
+ if (tools.length) {
+ console.log('Registered function tools:', tools);
+
+ data['tools'] = tools;
+ data['tool_choice'] = 'auto';
+ }
+ }
+
+ /**
+ * Utility function to parse tool calls from a parsed response.
+ * @param {any[]} toolCalls The tool calls to update.
+ * @param {any} parsed The parsed response from the OpenAI API.
+ * @returns {void}
+ */
+ static parseToolCalls(toolCalls, parsed) {
+ if (!Array.isArray(parsed?.choices)) {
+ return;
+ }
+ for (const choice of parsed.choices) {
+ const choiceIndex = (typeof choice.index === 'number') ? choice.index : null;
+ const choiceDelta = choice.delta;
+
+ if (choiceIndex === null || !choiceDelta) {
+ continue;
+ }
+
+ const toolCallDeltas = choiceDelta?.tool_calls;
+
+ if (!Array.isArray(toolCallDeltas)) {
+ continue;
+ }
+
+ if (!Array.isArray(toolCalls[choiceIndex])) {
+ toolCalls[choiceIndex] = [];
+ }
+
+ for (const toolCallDelta of toolCallDeltas) {
+ const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : null;
+
+ if (toolCallIndex === null) {
+ continue;
+ }
+
+ if (toolCalls[choiceIndex][toolCallIndex] === undefined) {
+ toolCalls[choiceIndex][toolCallIndex] = {};
+ }
+
+ const targetToolCall = toolCalls[choiceIndex][toolCallIndex];
+
+ ToolManager.#applyToolCallDelta(targetToolCall, toolCallDelta);
+ }
+ }
+ }
+
+ static #applyToolCallDelta(target, delta) {
+ for (const key in delta) {
+ if (!delta.hasOwnProperty(key)) continue;
+
+ const deltaValue = delta[key];
+ const targetValue = target[key];
+
+ if (deltaValue === null || deltaValue === undefined) {
+ target[key] = deltaValue;
+ continue;
+ }
+
+ if (typeof deltaValue === 'string') {
+ if (typeof targetValue === 'string') {
+ // Concatenate strings
+ target[key] = targetValue + deltaValue;
+ } else {
+ target[key] = deltaValue;
+ }
+ } else if (typeof deltaValue === 'object' && !Array.isArray(deltaValue)) {
+ if (typeof targetValue !== 'object' || targetValue === null || Array.isArray(targetValue)) {
+ target[key] = {};
+ }
+ // Recursively apply deltas to nested objects
+ ToolManager.#applyToolCallDelta(target[key], deltaValue);
+ } else {
+ // Assign other types directly
+ target[key] = deltaValue;
+ }
+ }
+ }
+
+ static isFunctionCallingSupported() {
+ if (main_api !== 'openai') {
+ return false;
+ }
+
+ if (!oai_settings.function_calling) {
+ return false;
+ }
+
+ const supportedSources = [
+ chat_completion_sources.OPENAI,
+ //chat_completion_sources.COHERE,
+ chat_completion_sources.CUSTOM,
+ chat_completion_sources.MISTRALAI,
+ //chat_completion_sources.CLAUDE,
+ chat_completion_sources.OPENROUTER,
+ chat_completion_sources.GROQ,
+ ];
+ return supportedSources.includes(oai_settings.chat_completion_source);
+ }
+
+ static #getToolCallsFromData(data) {
+ // Parsed tool calls from streaming data
+ if (Array.isArray(data) && data.length > 0) {
+ return data[0];
+ }
+
+ // Parsed tool calls from non-streaming data
+ if (!Array.isArray(data?.choices)) {
+ return;
+ }
+
+ // Find a choice with 0-index
+ const choice = data.choices.find(choice => choice.index === 0);
+
+ if (!choice) {
+ return;
+ }
+
+ return choice.message.tool_calls;
+ }
+
+ /**
+ * Check for function tool calls in the response data and invoke them.
+ * @param {any} data Reply data
+ * @returns {Promise} Successful tool invocations
+ */
+ static async checkFunctionToolCalls(data) {
+ if (!ToolManager.isFunctionCallingSupported()) {
+ return [];
+ }
+
+ /** @type {ToolInvocation[]} */
+ const invocations = [];
+ const toolCalls = ToolManager.#getToolCallsFromData(data);
+ const oaiCompat = [
+ chat_completion_sources.OPENAI,
+ chat_completion_sources.CUSTOM,
+ chat_completion_sources.MISTRALAI,
+ chat_completion_sources.OPENROUTER,
+ chat_completion_sources.GROQ,
+ ];
+
+ if (oaiCompat.includes(oai_settings.chat_completion_source)) {
+ if (!Array.isArray(toolCalls)) {
+ return;
+ }
+
+ for (const toolCall of toolCalls) {
+ if (typeof toolCall.function !== 'object') {
+ continue;
+ }
+
+ console.log('Function tool call:', toolCall);
+ const id = toolCall.id;
+ const parameters = toolCall.function.arguments;
+ const name = toolCall.function.name;
+
+ toastr.info('Invoking function tool: ' + name);
+ const result = await ToolManager.invokeFunctionTool(name, parameters);
+ toastr.info('Function tool result: ' + result);
+
+ // Save a successful invocation
+ if (result) {
+ invocations.push({ id, name, result, parameters });
+ }
+ }
+ }
+
+ /*
+ if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
+ if (!Array.isArray(data?.content)) {
+ return;
+ }
+
+ for (const content of data.content) {
+ if (content.type === 'tool_use') {
+ const args = { name: content.name, arguments: JSON.stringify(content.input) };
+ }
+ }
+ }
+ */
+
+ /*
+ if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
+ if (!Array.isArray(data?.tool_calls)) {
+ return;
+ }
+
+ for (const toolCall of data.tool_calls) {
+ const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
+ console.log('Function tool call:', toolCall);
+ }
+ }
+ */
+
+ return invocations;
+ }
+
+ /**
+ * Saves function tool invocations to the last user chat message extra metadata.
+ * @param {ToolInvocation[]} invocations
+ */
+ static saveFunctionToolInvocations(invocations) {
+ for (let index = chat.length - 1; index >= 0; index--) {
+ const message = chat[index];
+ if (message.is_user) {
+ if (!message.extra || typeof message.extra !== 'object') {
+ message.extra = {};
+ }
+ message.extra.tool_invocations = invocations;
+ debugger;
+ break;
+ }
+ }
+ }
+}
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index ca86db051..095a65ceb 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -121,18 +121,20 @@ async function sendClaudeRequest(request, response) {
? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
: convertedPrompt.systemPrompt;
}
+ /*
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
// Claude doesn't do prefills on function calls, and doesn't allow empty messages
if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
convertedPrompt.messages.push({ role: 'user', content: '.' });
}
additionalHeaders['anthropic-beta'] = 'tools-2024-05-16';
- requestBody.tool_choice = { type: request.body.tool_choice === 'required' ? 'any' : 'auto' };
+ requestBody.tool_choice = { type: request.body.tool_choice };
requestBody.tools = request.body.tools
.filter(tool => tool.type === 'function')
.map(tool => tool.function)
.map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
}
+ */
if (enableSystemPromptCache) {
additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
}
@@ -479,7 +481,7 @@ async function sendMistralAIRequest(request, response) {
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
requestBody['tools'] = request.body.tools;
- requestBody['tool_choice'] = request.body.tool_choice === 'required' ? 'any' : 'auto';
+ requestBody['tool_choice'] = request.body.tool_choice;
}
const config = {
@@ -549,11 +551,13 @@ async function sendCohereRequest(request, response) {
});
}
+ /*
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
tools.push(...convertCohereTools(request.body.tools));
// Can't have both connectors and tools in the same request
connectors.splice(0, connectors.length);
}
+ */
// https://docs.cohere.com/reference/chat
const requestBody = {
@@ -910,18 +914,6 @@ router.post('/generate', jsonParser, function (request, response) {
apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ);
headers = {};
bodyParams = {};
-
- // 'required' tool choice is not supported by Groq
- if (request.body.tool_choice === 'required') {
- if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
- request.body.tool_choice = request.body.tools.length > 1
- ? 'auto' :
- { type: 'function', function: { name: request.body.tools[0]?.function?.name } };
-
- } else {
- request.body.tool_choice = 'none';
- }
- }
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ZEROONEAI) {
apiUrl = API_01AI;
apiKey = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
@@ -958,7 +950,7 @@ router.post('/generate', jsonParser, function (request, response) {
controller.abort();
});
- if (!isTextCompletion) {
+ if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {
bodyParams['tools'] = request.body.tools;
bodyParams['tool_choice'] = request.body.tool_choice;
}
From c94c06ed4dac3c4895e4afb5b0e6c1524829e49b Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 01:45:57 +0300
Subject: [PATCH 157/268] Implement function tool calling for OpenAI
---
public/script.js | 4 +--
public/scripts/openai.js | 52 +++++++++++++++++++++++++++++++---
public/scripts/tool-calling.js | 5 ++--
3 files changed, 52 insertions(+), 9 deletions(-)
diff --git a/public/script.js b/public/script.js
index fac11b8f4..3312c556f 100644
--- a/public/script.js
+++ b/public/script.js
@@ -4408,7 +4408,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (ToolManager.isFunctionCallingSupported() && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
const invocations = await ToolManager.checkFunctionToolCalls(streamingProcessor.toolCalls);
- if (invocations.length) {
+ if (Array.isArray(invocations) && invocations.length) {
const lastMessage = chat[chat.length - 1];
const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
if (shouldDeleteMessage) {
@@ -4457,7 +4457,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (ToolManager.isFunctionCallingSupported()) {
const invocations = await ToolManager.checkFunctionToolCalls(data);
- if (invocations.length) {
+ if (Array.isArray(invocations) && invocations.length) {
ToolManager.saveFunctionToolInvocations(invocations);
return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
}
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index a6048dce5..8b9c06048 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -454,7 +454,8 @@ function setOpenAIMessages(chat) {
if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`;
const name = chat[j]['name'];
const image = chat[j]?.extra?.image;
- messages[i] = { 'role': role, 'content': content, name: name, 'image': image };
+ const invocations = chat[j]?.extra?.tool_invocations;
+ messages[i] = { 'role': role, 'content': content, name: name, 'image': image, 'invocations': invocations };
j++;
}
@@ -702,6 +703,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
}
const imageInlining = isImageInliningSupported();
+ const toolCalling = ToolManager.isFunctionCallingSupported();
// Insert chat messages as long as there is budget available
const chatPool = [...messages].reverse();
@@ -723,6 +725,24 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
await chatMessage.addImage(chatPrompt.image);
}
+ if (toolCalling && Array.isArray(chatPrompt.invocations)) {
+ /** @type {import('./tool-calling.js').ToolInvocation[]} */
+ const invocations = chatPrompt.invocations.slice().reverse();
+ const toolCallMessage = new Message('assistant', undefined, 'toolCall-' + chatMessage.identifier);
+ toolCallMessage.setToolCalls(invocations);
+ if (chatCompletion.canAfford(toolCallMessage)) {
+ for (const invocation of invocations) {
+ const toolResultMessage = new Message('tool', invocation.result, invocation.id);
+ const canAfford = chatCompletion.canAfford(toolResultMessage) && chatCompletion.canAfford(toolCallMessage);
+ if (!canAfford) {
+ break;
+ }
+ chatCompletion.insertAtStart(toolResultMessage, 'chatHistory');
+ }
+ chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
+ }
+ }
+
if (chatCompletion.canAfford(chatMessage)) {
if (type === 'continue' && oai_settings.continue_prefill && chatPrompt === firstNonInjected) {
// in case we are using continue_prefill and the latest message is an assistant message, we want to prepend the users assistant prefill on the message
@@ -2193,6 +2213,8 @@ class Message {
content;
/** @type {string} */
name;
+ /** @type {object} */
+ tool_call = null;
/**
* @constructor
@@ -2217,6 +2239,22 @@ class Message {
}
}
+ /**
+ * Reconstruct the message from a tool invocation.
+ * @param {import('./tool-calling.js').ToolInvocation[]} invocations
+ */
+ setToolCalls(invocations) {
+ this.tool_calls = invocations.map(i => ({
+ id: i.id,
+ type: 'function',
+ function: {
+ arguments: i.parameters,
+ name: i.name,
+ },
+ }));
+ this.tokens = tokenHandler.count({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
+ }
+
setName(name) {
this.name = name;
this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name });
@@ -2564,7 +2602,7 @@ export class ChatCompletion {
this.checkTokenBudget(message, message.identifier);
const index = this.findMessageIndex(identifier);
- if (message.content) {
+ if (message.content || message.tool_calls) {
if ('start' === position) this.messages.collection[index].collection.unshift(message);
else if ('end' === position) this.messages.collection[index].collection.push(message);
else if (typeof position === 'number') this.messages.collection[index].collection.splice(position, 0, message);
@@ -2633,8 +2671,14 @@ export class ChatCompletion {
for (let item of this.messages.collection) {
if (item instanceof MessageCollection) {
chat.push(...item.getChat());
- } else if (item instanceof Message && item.content) {
- const message = { role: item.role, content: item.content, ...(item.name ? { name: item.name } : {}) };
+ } else if (item instanceof Message && (item.content || item.tool_calls)) {
+ const message = {
+ role: item.role,
+ content: item.content,
+ ...(item.name ? { name: item.name } : {}),
+ ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
+ ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
+ };
chat.push(message);
} else {
this.log(`Skipping invalid or empty message in collection: ${JSON.stringify(item)}`);
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index c2f6c209f..c88528dd7 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -307,7 +307,7 @@ export class ToolManager {
if (oaiCompat.includes(oai_settings.chat_completion_source)) {
if (!Array.isArray(toolCalls)) {
- return;
+ return [];
}
for (const toolCall of toolCalls) {
@@ -363,7 +363,7 @@ export class ToolManager {
/**
* Saves function tool invocations to the last user chat message extra metadata.
- * @param {ToolInvocation[]} invocations
+ * @param {ToolInvocation[]} invocations Successful tool invocations
*/
static saveFunctionToolInvocations(invocations) {
for (let index = chat.length - 1; index >= 0; index--) {
@@ -373,7 +373,6 @@ export class ToolManager {
message.extra = {};
}
message.extra.tool_invocations = invocations;
- debugger;
break;
}
}
From 68c87f7e7aca14e382d8d7976154629a3f007908 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 01:53:03 +0300
Subject: [PATCH 158/268] Fix code scanning alert no. 231: Prototype-polluting
function
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
---
public/scripts/tool-calling.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index c88528dd7..c19d1ac7a 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -213,6 +213,7 @@ export class ToolManager {
static #applyToolCallDelta(target, delta) {
for (const key in delta) {
if (!delta.hasOwnProperty(key)) continue;
+ if (key === "__proto__" || key === "constructor") continue;
const deltaValue = delta[key];
const targetValue = target[key];
From 63724a2b383aaa2bcaa5ca4009f65062f45c39cd Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 01:54:47 +0300
Subject: [PATCH 159/268] eslint update
---
public/scripts/tool-calling.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index c19d1ac7a..964093bc3 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -213,7 +213,7 @@ export class ToolManager {
static #applyToolCallDelta(target, delta) {
for (const key in delta) {
if (!delta.hasOwnProperty(key)) continue;
- if (key === "__proto__" || key === "constructor") continue;
+ if (key === '__proto__' || key === 'constructor') continue;
const deltaValue = delta[key];
const targetValue = target[key];
From e8b972042553b27fbce73cb9377067ebaeebc965 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 01:56:27 +0300
Subject: [PATCH 160/268] Budgeting fix
---
public/scripts/openai.js | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 8b9c06048..96821eda4 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -727,18 +727,20 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
if (toolCalling && Array.isArray(chatPrompt.invocations)) {
/** @type {import('./tool-calling.js').ToolInvocation[]} */
- const invocations = chatPrompt.invocations.slice().reverse();
+ const invocations = chatPrompt.invocations;
const toolCallMessage = new Message('assistant', undefined, 'toolCall-' + chatMessage.identifier);
toolCallMessage.setToolCalls(invocations);
if (chatCompletion.canAfford(toolCallMessage)) {
- for (const invocation of invocations) {
+ chatCompletion.reserveBudget(toolCallMessage);
+ for (const invocation of invocations.slice().reverse()) {
const toolResultMessage = new Message('tool', invocation.result, invocation.id);
- const canAfford = chatCompletion.canAfford(toolResultMessage) && chatCompletion.canAfford(toolCallMessage);
+ const canAfford = chatCompletion.canAfford(toolResultMessage);
if (!canAfford) {
break;
}
chatCompletion.insertAtStart(toolResultMessage, 'chatHistory');
}
+ chatCompletion.freeBudget(toolCallMessage);
chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
}
}
From 3335dbf1a75dd0ffeaede2239c64fb5b85425f31 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 01:59:53 +0300
Subject: [PATCH 161/268] Add empty tool calls to streaming processors
---
public/scripts/kai-settings.js | 2 +-
public/scripts/nai-settings.js | 2 +-
public/scripts/textgen-settings.js | 3 ++-
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js
index 27a204c42..6efadce87 100644
--- a/public/scripts/kai-settings.js
+++ b/public/scripts/kai-settings.js
@@ -188,7 +188,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) {
if (data?.token) {
text += data.token;
}
- yield { text, swipes: [] };
+ yield { text, swipes: [], toolCalls: [] };
}
};
}
diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js
index fc4a042c5..040c6f203 100644
--- a/public/scripts/nai-settings.js
+++ b/public/scripts/nai-settings.js
@@ -746,7 +746,7 @@ export async function generateNovelWithStreaming(generate_data, signal) {
text += data.token;
}
- yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs) };
+ yield { text, swipes: [], logprobs: parseNovelAILogprobs(data.logprobs), toolCalls: [] };
}
};
}
diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js
index 9403555bd..c021c9682 100644
--- a/public/scripts/textgen-settings.js
+++ b/public/scripts/textgen-settings.js
@@ -916,6 +916,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
/** @type {import('./logprobs.js').TokenLogprobs | null} */
let logprobs = null;
const swipes = [];
+ const toolCalls = [];
while (true) {
const { done, value } = await reader.read();
if (done) return;
@@ -934,7 +935,7 @@ async function generateTextGenWithStreaming(generate_data, signal) {
logprobs = parseTextgenLogprobs(newText, data.choices?.[0]?.logprobs || data?.completion_probabilities);
}
- yield { text, swipes, logprobs };
+ yield { text, swipes, logprobs, toolCalls };
}
};
}
From 0f8c1fa95d7bdfea5d69ffed3c0ee76eef444886 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 22:17:27 +0300
Subject: [PATCH 162/268] Save tool calls to visible chats.
---
public/script.js | 26 ++++++++------
public/scripts/openai.js | 23 +++++++-----
public/scripts/tool-calling.js | 66 +++++++++++++++++++++-------------
3 files changed, 71 insertions(+), 44 deletions(-)
diff --git a/public/script.js b/public/script.js
index 3312c556f..1651d786e 100644
--- a/public/script.js
+++ b/public/script.js
@@ -3571,7 +3571,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
}
// Collect messages with usable content
- let coreChat = chat.filter(x => !x.is_system);
+ const canUseTools = ToolManager.isToolCallingSupported();
+ const canPerformToolCalls = !dryRun && ToolManager.canPerformToolCalls(type);
+ let coreChat = chat.filter(x => !x.is_system || (canUseTools && Array.isArray(x.extra?.tool_invocations)));
if (type === 'swipe') {
coreChat.pop();
}
@@ -4406,8 +4408,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
getMessage = continue_mag + getMessage;
}
- if (ToolManager.isFunctionCallingSupported() && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
- const invocations = await ToolManager.checkFunctionToolCalls(streamingProcessor.toolCalls);
+ if (canPerformToolCalls && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
+ const invocations = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
if (Array.isArray(invocations) && invocations.length) {
const lastMessage = chat[chat.length - 1];
const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
@@ -4455,14 +4457,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
throw new Error(data?.response);
}
- if (ToolManager.isFunctionCallingSupported()) {
- const invocations = await ToolManager.checkFunctionToolCalls(data);
- if (Array.isArray(invocations) && invocations.length) {
- ToolManager.saveFunctionToolInvocations(invocations);
- return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
- }
- }
-
//const getData = await response.json();
let getMessage = extractMessageFromData(data);
let title = extractTitleFromData(data);
@@ -4502,6 +4496,16 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
parseAndSaveLogprobs(data, continue_mag);
}
+ if (canPerformToolCalls) {
+ const invocations = await ToolManager.invokeFunctionTools(data);
+ if (Array.isArray(invocations) && invocations.length) {
+ const shouldDeleteMessage = ['', '...'].includes(getMessage);
+ shouldDeleteMessage && await deleteLastMessage();
+ ToolManager.saveFunctionToolInvocations(invocations);
+ return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
+ }
+ }
+
if (type !== 'quiet') {
playMessageSound();
}
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 96821eda4..e9216eccf 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -703,7 +703,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
}
const imageInlining = isImageInliningSupported();
- const toolCalling = ToolManager.isFunctionCallingSupported();
+ const canUseTools = ToolManager.isToolCallingSupported();
// Insert chat messages as long as there is budget available
const chatPool = [...messages].reverse();
@@ -725,10 +725,10 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
await chatMessage.addImage(chatPrompt.image);
}
- if (toolCalling && Array.isArray(chatPrompt.invocations)) {
+ if (canUseTools && Array.isArray(chatPrompt.invocations)) {
/** @type {import('./tool-calling.js').ToolInvocation[]} */
const invocations = chatPrompt.invocations;
- const toolCallMessage = new Message('assistant', undefined, 'toolCall-' + chatMessage.identifier);
+ const toolCallMessage = new Message(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
toolCallMessage.setToolCalls(invocations);
if (chatCompletion.canAfford(toolCallMessage)) {
chatCompletion.reserveBudget(toolCallMessage);
@@ -1285,7 +1285,7 @@ export async function prepareOpenAIMessages({
const eventData = { chat, dryRun };
await eventSource.emit(event_types.CHAT_COMPLETION_PROMPT_READY, eventData);
- openai_messages_count = chat.filter(x => x?.role === 'user' || x?.role === 'assistant')?.length || 0;
+ openai_messages_count = chat.filter(x => !x?.tool_calls && (x?.role === 'user' || x?.role === 'assistant'))?.length || 0;
return [chat, promptManager.tokenHandler.counts];
}
@@ -1886,7 +1886,7 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['seed'] = oai_settings.seed;
}
- if (!canMultiSwipe && ToolManager.isFunctionCallingSupported()) {
+ if (!canMultiSwipe && ToolManager.canPerformToolCalls(type)) {
await ToolManager.registerFunctionToolsOpenAI(generate_data);
}
@@ -2393,13 +2393,20 @@ class MessageCollection {
}
/**
- * Get chat in the format of {role, name, content}.
+ * Get chat in the format of {role, name, content, tool_calls}.
* @returns {Array} Array of objects with role, name, and content properties.
*/
getChat() {
return this.collection.reduce((acc, message) => {
- const name = message.name;
- if (message.content) acc.push({ role: message.role, ...(name && { name }), content: message.content });
+ if (message.content || message.tool_calls) {
+ acc.push({
+ role: message.role,
+ content: message.content,
+ ...(message.name && { name: message.name }),
+ ...(message.tool_calls && { tool_calls: message.tool_calls }),
+ ...(message.role === 'tool' && { tool_call_id: message.identifier }),
+ });
+ }
return acc;
}, []);
}
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index 964093bc3..def0c078a 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -1,4 +1,4 @@
-import { chat, main_api } from '../script.js';
+import { addOneMessage, chat, main_api, system_avatar, systemUserName } from '../script.js';
import { chat_completion_sources, oai_settings } from './openai.js';
/**
@@ -243,12 +243,12 @@ export class ToolManager {
}
}
- static isFunctionCallingSupported() {
- if (main_api !== 'openai') {
- return false;
- }
-
- if (!oai_settings.function_calling) {
+ /**
+ * Checks if tool calling is supported for the current settings and generation type.
+ * @returns {boolean} Whether tool calling is supported for the given type
+ */
+ static isToolCallingSupported() {
+ if (main_api !== 'openai' || !oai_settings.function_calling) {
return false;
}
@@ -264,6 +264,22 @@ export class ToolManager {
return supportedSources.includes(oai_settings.chat_completion_source);
}
+ /**
+ * Checks if tool calls can be performed for the current settings and generation type.
+ * @param {string} type Generation type
+ * @returns {boolean} Whether tool calls can be performed for the given type
+ */
+ static canPerformToolCalls(type) {
+ const noToolCallTypes = ['swipe', 'impersonate', 'quiet', 'continue'];
+ const isSupported = ToolManager.isToolCallingSupported();
+ return isSupported && !noToolCallTypes.includes(type);
+ }
+
+ /**
+ * Utility function to get tool calls from the response data.
+ * @param {any} data Response data
+ * @returns {any[]} Tool calls from the response data
+ */
static #getToolCallsFromData(data) {
// Parsed tool calls from streaming data
if (Array.isArray(data) && data.length > 0) {
@@ -290,15 +306,11 @@ export class ToolManager {
* @param {any} data Reply data
* @returns {Promise} Successful tool invocations
*/
- static async checkFunctionToolCalls(data) {
- if (!ToolManager.isFunctionCallingSupported()) {
- return [];
- }
-
+ static async invokeFunctionTools(data) {
/** @type {ToolInvocation[]} */
const invocations = [];
const toolCalls = ToolManager.#getToolCallsFromData(data);
- const oaiCompat = [
+ const oaiCompatibleSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.MISTRALAI,
@@ -306,7 +318,7 @@ export class ToolManager {
chat_completion_sources.GROQ,
];
- if (oaiCompat.includes(oai_settings.chat_completion_source)) {
+ if (oaiCompatibleSources.includes(oai_settings.chat_completion_source)) {
if (!Array.isArray(toolCalls)) {
return [];
}
@@ -323,7 +335,7 @@ export class ToolManager {
toastr.info('Invoking function tool: ' + name);
const result = await ToolManager.invokeFunctionTool(name, parameters);
- toastr.info('Function tool result: ' + result);
+ console.log('Function tool result:', result);
// Save a successful invocation
if (result) {
@@ -367,15 +379,19 @@ export class ToolManager {
* @param {ToolInvocation[]} invocations Successful tool invocations
*/
static saveFunctionToolInvocations(invocations) {
- for (let index = chat.length - 1; index >= 0; index--) {
- const message = chat[index];
- if (message.is_user) {
- if (!message.extra || typeof message.extra !== 'object') {
- message.extra = {};
- }
- message.extra.tool_invocations = invocations;
- break;
- }
- }
+ const toolNames = invocations.map(i => i.name).join(', ');
+ const message = {
+ name: systemUserName,
+ force_avatar: system_avatar,
+ is_system: true,
+ is_user: false,
+ mes: `Performed tool calls: ${toolNames}`,
+ extra: {
+ isSmallSys: true,
+ tool_invocations: invocations,
+ },
+ };
+ chat.push(message);
+ addOneMessage(message);
}
}
From 85b0d135f1d709f92405566ba2e801dcf98ff28b Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 22:22:33 +0300
Subject: [PATCH 163/268] Add utility function for formatting swipe counters
---
public/script.js | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/public/script.js b/public/script.js
index 35bfcbab0..1fd4092c6 100644
--- a/public/script.js
+++ b/public/script.js
@@ -2374,7 +2374,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) {
const swipesNum = chat[newMessageId].swipes?.length;
const swipeId = chat[newMessageId].swipe_id + 1;
- newMessage.find('.swipes-counter').text(`${swipeId}\u200B/\u200b${swipesNum}`);
+ newMessage.find('.swipes-counter').text(formatSwipeCounter(swipeId, swipesNum));
}
if (showSwipes) {
@@ -7487,7 +7487,7 @@ export function showSwipeButtons() {
const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`);
const swipeId = chat[chat.length - 1].swipe_id;
- const swipeCounterText = (`${(swipeId + 1)}\u200B/\u200b${(chat[chat.length - 1].swipes.length)}`);
+ const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[chat.length - 1].swipes.length);
const swipeRight = currentMessage.find('.swipe_right');
const swipeLeft = currentMessage.find('.swipe_left');
const swipeCounter = currentMessage.find('.swipes-counter');
@@ -8165,6 +8165,20 @@ window['SillyTavern'].getContext = function () {
};
};
+/**
+ * Formats a counter for a swipe view.
+ * @param {number} current The current number of items.
+ * @param {number} total The total number of items.
+ * @returns {string} The formatted counter.
+ */
+function formatSwipeCounter(current, total) {
+ if (isNaN(current) || isNaN(total)) {
+ return '';
+ }
+
+ return `${current}\u200b/\u200b${total}`;
+}
+
function swipe_left() { // when we swipe left..but no generation.
if (chat.length - 1 === Number(this_edit_mes_id)) {
closeMessageEditor();
From 2dad86e0769f803f674995a3b15ca48369144c36 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 23:12:49 +0300
Subject: [PATCH 164/268] Delete empty message before tool invocations
---
public/script.js | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/public/script.js b/public/script.js
index c3aa593e8..3a38a04a9 100644
--- a/public/script.js
+++ b/public/script.js
@@ -4409,14 +4409,12 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
}
if (canPerformToolCalls && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
+ const lastMessage = chat[chat.length - 1];
+ const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
+ shouldDeleteMessage && await deleteLastMessage();
const invocations = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
if (Array.isArray(invocations) && invocations.length) {
- const lastMessage = chat[chat.length - 1];
- const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
- if (shouldDeleteMessage) {
- await deleteLastMessage();
- streamingProcessor = null;
- }
+ streamingProcessor = null;
ToolManager.saveFunctionToolInvocations(invocations);
return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
}
@@ -4497,10 +4495,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
}
if (canPerformToolCalls) {
+ const shouldDeleteMessage = ['', '...'].includes(getMessage);
+ shouldDeleteMessage && await deleteLastMessage();
const invocations = await ToolManager.invokeFunctionTools(data);
if (Array.isArray(invocations) && invocations.length) {
- const shouldDeleteMessage = ['', '...'].includes(getMessage);
- shouldDeleteMessage && await deleteLastMessage();
ToolManager.saveFunctionToolInvocations(invocations);
return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
}
From 2b7c03f3b0c9662471880b25b1ee83ea90f51860 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 23:13:11 +0300
Subject: [PATCH 165/268] Nicely format message for tool calls
---
public/scripts/tool-calling.js | 26 +++++++++++++++++++++++---
public/style.css | 9 +++++++++
2 files changed, 32 insertions(+), 3 deletions(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index def0c078a..b3c8ac1b1 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -108,6 +108,7 @@ export class ToolManager {
const definition = new ToolDefinition(name, description, parameters, action);
this.#tools.set(name, definition);
+ console.log('[ToolManager] Registered function tool:', definition);
}
/**
@@ -121,6 +122,7 @@ export class ToolManager {
}
this.#tools.delete(name);
+ console.log(`[ToolManager] Unregistered function tool: ${name}`);
}
/**
@@ -333,8 +335,9 @@ export class ToolManager {
const parameters = toolCall.function.arguments;
const name = toolCall.function.name;
- toastr.info('Invoking function tool: ' + name);
+ const toast = toastr.info(`Invoking function tool: ${name}`);
const result = await ToolManager.invokeFunctionTool(name, parameters);
+ toastr.clear(toast);
console.log('Function tool result:', result);
// Save a successful invocation
@@ -374,18 +377,35 @@ export class ToolManager {
return invocations;
}
+ /**
+ * Formats a message with tool invocations.
+ * @param {ToolInvocation[]} invocations Tool invocations.
+ * @returns {string} Formatted message with tool invocations.
+ */
+ static #formatMessage(invocations) {
+ const toolNames = invocations.map(i => i.name).join(', ');
+ const detailsElement = document.createElement('details');
+ const summaryElement = document.createElement('summary');
+ const preElement = document.createElement('pre');
+ const codeElement = document.createElement('code');
+ codeElement.textContent = JSON.stringify(invocations, null, 2);
+ summaryElement.textContent = `Performed tool calls: ${toolNames}`;
+ preElement.append(codeElement);
+ detailsElement.append(summaryElement, preElement);
+ return detailsElement.outerHTML;
+ }
+
/**
* Saves function tool invocations to the last user chat message extra metadata.
* @param {ToolInvocation[]} invocations Successful tool invocations
*/
static saveFunctionToolInvocations(invocations) {
- const toolNames = invocations.map(i => i.name).join(', ');
const message = {
name: systemUserName,
force_avatar: system_avatar,
is_system: true,
is_user: false,
- mes: `Performed tool calls: ${toolNames}`,
+ mes: ToolManager.#formatMessage(invocations),
extra: {
isSmallSys: true,
tool_invocations: invocations,
diff --git a/public/style.css b/public/style.css
index 01a49ab9f..19a9bb738 100644
--- a/public/style.css
+++ b/public/style.css
@@ -418,6 +418,15 @@ small {
text-align: center;
}
+.mes.smallSysMes pre {
+ text-align: initial;
+ word-break: break-all;
+}
+
+.mes.smallSysMes summary {
+ cursor: pointer;
+}
+
.mes.smallSysMes .mes_text p:last-child {
margin: 0;
}
From 1e076a3e4367cc228bb63cadaed3812298897308 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Wed, 2 Oct 2024 23:32:29 +0300
Subject: [PATCH 166/268] Prettify displayed message
---
public/scripts/tool-calling.js | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index b3c8ac1b1..dd1d1d8c4 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -342,7 +342,7 @@ export class ToolManager {
// Save a successful invocation
if (result) {
- invocations.push({ id, name, result, parameters });
+ invocations.push({ id, name, parameters, result });
}
}
}
@@ -383,12 +383,16 @@ export class ToolManager {
* @returns {string} Formatted message with tool invocations.
*/
static #formatMessage(invocations) {
- const toolNames = invocations.map(i => i.name).join(', ');
+ const tryParse = (x) => { try { return JSON.parse(x); } catch { return x; } };
+ const data = structuredClone(invocations);
const detailsElement = document.createElement('details');
const summaryElement = document.createElement('summary');
const preElement = document.createElement('pre');
const codeElement = document.createElement('code');
- codeElement.textContent = JSON.stringify(invocations, null, 2);
+ codeElement.classList.add('language-json');
+ data.forEach(i => i.parameters = tryParse(i.parameters));
+ codeElement.textContent = JSON.stringify(data, null, 2);
+ const toolNames = data.map(i => i.name).join(', ');
summaryElement.textContent = `Performed tool calls: ${toolNames}`;
preElement.append(codeElement);
detailsElement.append(summaryElement, preElement);
From 6706cce10de9cc02e2337d512120ec3a0e8d431c Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Thu, 3 Oct 2024 08:41:45 +0300
Subject: [PATCH 167/268] Groq: Add new models and multimodal captions
---
public/index.html | 35 +++++++++++++------
public/scripts/extensions/caption/index.js | 1 +
.../scripts/extensions/caption/settings.html | 4 +++
public/scripts/extensions/shared.js | 6 +++-
public/scripts/openai.js | 5 ++-
src/endpoints/openai.js | 8 +++++
6 files changed, 47 insertions(+), 12 deletions(-)
diff --git a/public/index.html b/public/index.html
index c6117f665..b10a66d46 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2954,16 +2954,31 @@
Groq Model
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js
index 0f740cc9a..04812cead 100644
--- a/public/scripts/extensions/caption/index.js
+++ b/public/scripts/extensions/caption/index.js
@@ -403,6 +403,7 @@ jQuery(async function () {
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
+ (extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'groq' && secret_state[SECRET_KEYS.GROQ]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'mistral' && (secret_state[SECRET_KEYS.MISTRALAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html
index 318577277..f6a6fc7a3 100644
--- a/public/scripts/extensions/caption/settings.html
+++ b/public/scripts/extensions/caption/settings.html
@@ -21,6 +21,7 @@
+
@@ -60,6 +61,9 @@
+
+
+
diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js
index 950105ce1..444231527 100644
--- a/public/scripts/extensions/shared.js
+++ b/public/scripts/extensions/shared.js
@@ -36,7 +36,7 @@ export async function getMultimodalCaption(base64Img, prompt) {
const isVllm = extension_settings.caption.multimodal_api === 'vllm';
const base64Bytes = base64Img.length * 0.75;
const compressionLimit = 2 * 1024 * 1024;
- if ((['google', 'openrouter', 'mistral'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
+ if ((['google', 'openrouter', 'mistral', 'groq'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
const maxSide = 1024;
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
}
@@ -135,6 +135,10 @@ function throwIfInvalidModel(useReverseProxy) {
throw new Error('01.AI API key is not set.');
}
+ if (extension_settings.caption.multimodal_api === 'groq' && !secret_state[SECRET_KEYS.GROQ]) {
+ throw new Error('Groq API key is not set.');
+ }
+
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
throw new Error('Google AI Studio API key is not set.');
}
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index 20c141316..670fd2968 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -4196,7 +4196,10 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
- else if (oai_settings.groq_model.includes('llama-3.1')) {
+ else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
+ $('#openai_max_context').attr('max', max_8k);
+ }
+ else if (oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
$('#openai_max_context').attr('max', max_128k);
}
else if (oai_settings.groq_model.includes('llama3-groq')) {
diff --git a/src/endpoints/openai.js b/src/endpoints/openai.js
index af1f107e2..e7760a8fb 100644
--- a/src/endpoints/openai.js
+++ b/src/endpoints/openai.js
@@ -55,6 +55,10 @@ router.post('/caption-image', jsonParser, async (request, response) => {
key = readSecret(request.user.directories, SECRET_KEYS.MISTRALAI);
}
+ if (request.body.api === 'groq') {
+ key = readSecret(request.user.directories, SECRET_KEYS.GROQ);
+ }
+
if (!key && !request.body.reverse_proxy && ['custom', 'ooba', 'koboldcpp', 'vllm'].includes(request.body.api) === false) {
console.log('No key found for API', request.body.api);
return response.sendStatus(400);
@@ -111,6 +115,10 @@ router.post('/caption-image', jsonParser, async (request, response) => {
apiUrl = 'https://api.01.ai/v1/chat/completions';
}
+ if (request.body.api === 'groq') {
+ apiUrl = 'https://api.groq.com/openai/v1/chat/completions';
+ }
+
if (request.body.api === 'mistral') {
apiUrl = 'https://api.mistral.ai/v1/chat/completions';
}
From c59afd1b56f11df07bb137a9799ab0c505dd41c1 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Thu, 3 Oct 2024 09:51:38 +0300
Subject: [PATCH 168/268] Claude: console.log system before messages
---
src/endpoints/backends/chat-completions.js | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index ca86db051..cb4b77f65 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -107,6 +107,7 @@ async function sendClaudeRequest(request, response) {
}
const requestBody = {
+ /** @type {any} */ system: '',
messages: convertedPrompt.messages,
model: request.body.model,
max_tokens: request.body.max_tokens,
@@ -120,6 +121,8 @@ async function sendClaudeRequest(request, response) {
requestBody.system = enableSystemPromptCache
? [{ type: 'text', text: convertedPrompt.systemPrompt, cache_control: { type: 'ephemeral' } }]
: convertedPrompt.systemPrompt;
+ } else {
+ delete requestBody.system;
}
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
// Claude doesn't do prefills on function calls, and doesn't allow empty messages
@@ -156,12 +159,13 @@ async function sendClaudeRequest(request, response) {
forwardFetchResponse(generateResponse, response);
} else {
if (!generateResponse.ok) {
- console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${await generateResponse.text()}\n${divider}`));
+ const generateResponseText = await generateResponse.text();
+ console.log(color.red(`Claude API returned error: ${generateResponse.status} ${generateResponse.statusText}\n${generateResponseText}\n${divider}`));
return response.status(generateResponse.status).send({ error: true });
}
const generateResponseJson = await generateResponse.json();
- const responseText = generateResponseJson.content[0].text;
+ const responseText = generateResponseJson?.content?.[0]?.text || '';
console.log('Claude response:', generateResponseJson);
// Wrap it back to OAI format + save the original content
From da9200c82ed39f790b3ff982e796f33aeaec3ae1 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Thu, 3 Oct 2024 12:59:59 +0000
Subject: [PATCH 169/268] Skip adding tool messages as regular chats
---
public/scripts/openai.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index e9216eccf..9ab05b467 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -743,6 +743,8 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
chatCompletion.freeBudget(toolCallMessage);
chatCompletion.insertAtStart(toolCallMessage, 'chatHistory');
}
+
+ continue;
}
if (chatCompletion.canAfford(chatMessage)) {
From 90809852c2a7e1ecc133003b536d237908e1f498 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Thu, 3 Oct 2024 13:23:53 +0000
Subject: [PATCH 170/268] Hide message on streaming tool calls
---
public/script.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/public/script.js b/public/script.js
index 3a38a04a9..e3c7bf1ba 100644
--- a/public/script.js
+++ b/public/script.js
@@ -2932,6 +2932,13 @@ class StreamingProcessor {
}
}
+ #updateMessageBlockVisibility() {
+ if (this.messageDom instanceof HTMLElement && Array.isArray(this.toolCalls) && this.toolCalls.length > 0) {
+ const shouldHide = ['', '...'].includes(this.result);
+ this.messageDom.classList.toggle('displayNone', shouldHide);
+ }
+ }
+
showMessageButtons(messageId) {
if (messageId == -1) {
return;
@@ -2997,6 +3004,7 @@ class StreamingProcessor {
}
else {
this.#checkDomElements(messageId);
+ this.#updateMessageBlockVisibility();
const currentTime = new Date();
// Don't waste time calculating token count for streaming
const currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
From 1278b5c309b30d14f743a4720d453cd76f8f1f64 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Thu, 3 Oct 2024 22:13:37 +0300
Subject: [PATCH 171/268] Fix character attachment content saving into
settings.json
---
.../scripts/extensions/attachments/index.js | 21 +++++++++++++++++++
public/scripts/extensions/vectors/index.js | 4 ++--
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/public/scripts/extensions/attachments/index.js b/public/scripts/extensions/attachments/index.js
index 3ae36e46f..7db88dc4f 100644
--- a/public/scripts/extensions/attachments/index.js
+++ b/public/scripts/extensions/attachments/index.js
@@ -1,3 +1,4 @@
+import { event_types, eventSource, saveSettingsDebounced } from '../../../script.js';
import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
@@ -196,7 +197,27 @@ async function enableDataBankAttachment(args, value) {
return '';
}
+function cleanUpAttachments() {
+ let shouldSaveSettings = false;
+ if (extension_settings.character_attachments) {
+ Object.values(extension_settings.character_attachments).flat().filter(a => a.text).forEach(a => {
+ shouldSaveSettings = true;
+ delete a.text;
+ });
+ }
+ if (Array.isArray(extension_settings.attachments)) {
+ extension_settings.attachments.filter(a => a.text).forEach(a => {
+ shouldSaveSettings = true;
+ delete a.text;
+ });
+ }
+ if (shouldSaveSettings) {
+ saveSettingsDebounced();
+ }
+}
+
jQuery(async () => {
+ eventSource.on(event_types.APP_READY, cleanUpAttachments);
const manageButton = await renderExtensionTemplateAsync('attachments', 'manage-button', {});
const attachButton = await renderExtensionTemplateAsync('attachments', 'attach-button', {});
$('#data_bank_wand_container').append(manageButton);
diff --git a/public/scripts/extensions/vectors/index.js b/public/scripts/extensions/vectors/index.js
index 3d1a7a539..c78004917 100644
--- a/public/scripts/extensions/vectors/index.js
+++ b/public/scripts/extensions/vectors/index.js
@@ -466,13 +466,13 @@ async function ingestDataBankAttachments(source) {
}
// Download and process the file
- file.text = await getFileAttachment(file.url);
+ const fileText = await getFileAttachment(file.url);
console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
// Convert kilobytes to string length
const thresholdLength = settings.size_threshold_db * 1024;
// Use chunk size from settings if file is larger than threshold
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
- await vectorizeFile(file.text, file.name, collectionId, chunkSize, settings.overlap_percent_db);
+ await vectorizeFile(fileText, file.name, collectionId, chunkSize, settings.overlap_percent_db);
}
return dataBankCollectionIds;
From 6558b106754a73faff6ca171254192ed9a1157e9 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 00:11:36 +0300
Subject: [PATCH 172/268] Show an error when all tools fail
---
public/script.js | 27 +++++++++++++----
public/scripts/tool-calling.js | 54 +++++++++++++++++++++++++++-------
public/style.css | 1 +
3 files changed, 66 insertions(+), 16 deletions(-)
diff --git a/public/script.js b/public/script.js
index e3c7bf1ba..4d59bde08 100644
--- a/public/script.js
+++ b/public/script.js
@@ -4420,10 +4420,18 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
const lastMessage = chat[chat.length - 1];
const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
shouldDeleteMessage && await deleteLastMessage();
- const invocations = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
- if (Array.isArray(invocations) && invocations.length) {
+ const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
+ if (invocationResult.hadToolCalls) {
+ if (!invocationResult.invocations.length && shouldDeleteMessage) {
+ ToolManager.showToolCallError(invocationResult.errors);
+ unblockGeneration(type);
+ generatedPromptCache = '';
+ streamingProcessor = null;
+ return;
+ }
+
streamingProcessor = null;
- ToolManager.saveFunctionToolInvocations(invocations);
+ ToolManager.saveFunctionToolInvocations(invocationResult.invocations);
return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
}
}
@@ -4505,9 +4513,16 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
if (canPerformToolCalls) {
const shouldDeleteMessage = ['', '...'].includes(getMessage);
shouldDeleteMessage && await deleteLastMessage();
- const invocations = await ToolManager.invokeFunctionTools(data);
- if (Array.isArray(invocations) && invocations.length) {
- ToolManager.saveFunctionToolInvocations(invocations);
+ const invocationResult = await ToolManager.invokeFunctionTools(data);
+ if (invocationResult.hadToolCalls) {
+ if (!invocationResult.invocations.length && shouldDeleteMessage) {
+ ToolManager.showToolCallError(invocationResult.errors);
+ unblockGeneration(type);
+ generatedPromptCache = '';
+ return;
+ }
+
+ ToolManager.saveFunctionToolInvocations(invocationResult.invocations);
return Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName }, dryRun);
}
}
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index dd1d1d8c4..bd8c572f8 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -1,5 +1,6 @@
import { addOneMessage, chat, main_api, system_avatar, systemUserName } from '../script.js';
import { chat_completion_sources, oai_settings } from './openai.js';
+import { Popup } from './popup.js';
/**
* @typedef {object} ToolInvocation
@@ -9,6 +10,13 @@ import { chat_completion_sources, oai_settings } from './openai.js';
* @property {string} result - The result of the tool invocation.
*/
+/**
+ * @typedef {object} ToolInvocationResult
+ * @property {ToolInvocation[]} invocations Successful tool invocations
+ * @property {boolean} hadToolCalls Whether any tool calls were found
+ * @property {Error[]} errors Errors that occurred during tool invocation
+ */
+
/**
* A class that represents a tool definition.
*/
@@ -129,7 +137,7 @@ export class ToolManager {
* Invokes a tool by name. Returns the result of the tool's action function.
* @param {string} name The name of the tool to invoke.
* @param {object} parameters Function parameters. For example, if the tool requires a "name" parameter, you would pass {name: "value"}.
- * @returns {Promise} The result of the tool's action function. If an error occurs, null is returned. Non-string results are JSON-stringified.
+ * @returns {Promise} The result of the tool's action function. If an error occurs, null is returned. Non-string results are JSON-stringified.
*/
static async invokeFunctionTool(name, parameters) {
try {
@@ -143,7 +151,13 @@ export class ToolManager {
return typeof result === 'string' ? result : JSON.stringify(result);
} catch (error) {
console.error(`An error occurred while invoking the tool "${name}":`, error);
- return null;
+
+ if (error instanceof Error) {
+ error.cause = name;
+ return error;
+ }
+
+ return new Error('Unknown error occurred while invoking the tool.', { cause: name });
}
}
@@ -306,11 +320,15 @@ export class ToolManager {
/**
* Check for function tool calls in the response data and invoke them.
* @param {any} data Reply data
- * @returns {Promise} Successful tool invocations
+ * @returns {Promise} Successful tool invocations
*/
static async invokeFunctionTools(data) {
- /** @type {ToolInvocation[]} */
- const invocations = [];
+ /** @type {ToolInvocationResult} */
+ const result = {
+ invocations: [],
+ hadToolCalls: false,
+ errors: [],
+ };
const toolCalls = ToolManager.#getToolCallsFromData(data);
const oaiCompatibleSources = [
chat_completion_sources.OPENAI,
@@ -322,7 +340,7 @@ export class ToolManager {
if (oaiCompatibleSources.includes(oai_settings.chat_completion_source)) {
if (!Array.isArray(toolCalls)) {
- return [];
+ return result;
}
for (const toolCall of toolCalls) {
@@ -334,16 +352,20 @@ export class ToolManager {
const id = toolCall.id;
const parameters = toolCall.function.arguments;
const name = toolCall.function.name;
+ result.hadToolCalls = true;
const toast = toastr.info(`Invoking function tool: ${name}`);
- const result = await ToolManager.invokeFunctionTool(name, parameters);
+ const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
toastr.clear(toast);
console.log('Function tool result:', result);
// Save a successful invocation
- if (result) {
- invocations.push({ id, name, parameters, result });
+ if (toolResult instanceof Error) {
+ result.errors.push(toolResult);
+ continue;
}
+
+ result.invocations.push({ id, name, parameters, result: toolResult });
}
}
@@ -374,7 +396,7 @@ export class ToolManager {
}
*/
- return invocations;
+ return result;
}
/**
@@ -418,4 +440,16 @@ export class ToolManager {
chat.push(message);
addOneMessage(message);
}
+
+ /**
+ * Shows an error message for tool calls.
+ * @param {Error[]} errors Errors that occurred during tool invocation
+ * @returns {void}
+ */
+ static showToolCallError(errors) {
+ toastr.error('An error occurred while invoking function tools. Click here for more details.', 'Tool Calling', {
+ onclick: () => Popup.show.text('Tool Calling Errors', DOMPurify.sanitize(errors.map(e => `${e.cause}: ${e.message}`).join(' '))),
+ timeOut: 5000,
+ });
+ }
}
diff --git a/public/style.css b/public/style.css
index 19a9bb738..96ae504b7 100644
--- a/public/style.css
+++ b/public/style.css
@@ -421,6 +421,7 @@ small {
.mes.smallSysMes pre {
text-align: initial;
word-break: break-all;
+ margin-top: 5px;
}
.mes.smallSysMes summary {
From 5cf64a2613252cc9ab8557fe145fab7546ea56ec Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 00:39:28 +0300
Subject: [PATCH 173/268] Update tool registration
---
public/scripts/tool-calling.js | 100 +++++++++++++++++++++++++++++----
1 file changed, 89 insertions(+), 11 deletions(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index bd8c572f8..0eaada477 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -5,6 +5,7 @@ import { Popup } from './popup.js';
/**
* @typedef {object} ToolInvocation
* @property {string} id - A unique identifier for the tool invocation.
+ * @property {string} displayName - The display name of the tool.
* @property {string} name - The name of the tool.
* @property {string} parameters - The parameters for the tool invocation.
* @property {string} result - The result of the tool invocation.
@@ -27,6 +28,12 @@ class ToolDefinition {
*/
#name;
+ /**
+ * A user-friendly display name for the tool.
+ * @type {string}
+ */
+ #displayName;
+
/**
* A description of what the tool does.
* @type {string}
@@ -45,18 +52,28 @@ class ToolDefinition {
*/
#action;
+ /**
+ * A function that will be called to format the tool call toast.
+ * @type {function}
+ */
+ #formatMessage;
+
/**
* Creates a new ToolDefinition.
* @param {string} name A unique name for the tool.
+ * @param {string} displayName A user-friendly display name for the tool.
* @param {string} description A description of what the tool does.
* @param {object} parameters A JSON schema for the parameters that the tool accepts.
* @param {function} action A function that will be called when the tool is executed.
+ * @param {function} formatMessage A function that will be called to format the tool call toast.
*/
- constructor(name, description, parameters, action) {
+ constructor(name, displayName, description, parameters, action, formatMessage) {
this.#name = name;
+ this.#displayName = displayName;
this.#description = description;
this.#parameters = parameters;
this.#action = action;
+ this.#formatMessage = formatMessage;
}
/**
@@ -82,6 +99,21 @@ class ToolDefinition {
async invoke(parameters) {
return await this.#action(parameters);
}
+
+ /**
+ * Formats a message with the tool invocation.
+ * @param {object} parameters The parameters to pass to the tool.
+ * @returns {string} The formatted message.
+ */
+ formatMessage(parameters) {
+ return typeof this.#formatMessage === 'function'
+ ? this.#formatMessage(parameters)
+ : `Invoking tool: ${this.#displayName || this.#name}`;
+ }
+
+ get displayName() {
+ return this.#displayName;
+ }
}
/**
@@ -104,17 +136,25 @@ export class ToolManager {
/**
* Registers a new tool with the tool registry.
- * @param {string} name The name of the tool.
- * @param {string} description A description of what the tool does.
- * @param {object} parameters A JSON schema for the parameters that the tool accepts.
- * @param {function} action A function that will be called when the tool is executed.
+ * @param {object} tool The tool to register.
+ * @param {string} tool.name The name of the tool.
+ * @param {string} tool.displayName A user-friendly display name for the tool.
+ * @param {string} tool.description A description of what the tool does.
+ * @param {object} tool.parameters A JSON schema for the parameters that the tool accepts.
+ * @param {function} tool.action A function that will be called when the tool is executed.
+ * @param {function} tool.formatMessage A function that will be called to format the tool call toast.
*/
- static registerFunctionTool(name, description, parameters, action) {
+ static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage }) {
+ // Convert WIP arguments
+ if (typeof arguments[0] !== 'object') {
+ [name, description, parameters, action] = arguments;
+ }
+
if (this.#tools.has(name)) {
console.warn(`A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
}
- const definition = new ToolDefinition(name, description, parameters, action);
+ const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage);
this.#tools.set(name, definition);
console.log('[ToolManager] Registered function tool:', definition);
}
@@ -161,6 +201,35 @@ export class ToolManager {
}
}
+ static formatToolCallMessage(name, parameters) {
+ if (!this.#tools.has(name)) {
+ return `Invoked unknown tool: ${name}`;
+ }
+
+ try {
+ const tool = this.#tools.get(name);
+ const formatParameters = typeof parameters === 'string' ? JSON.parse(parameters) : parameters;
+ return tool.formatMessage(formatParameters);
+ } catch (error) {
+ console.error(`An error occurred while formatting the tool call message for "${name}":`, error);
+ return `Invoking tool: ${name}`;
+ }
+ }
+
+ /**
+ * Gets the display name of a tool by name.
+ * @param {string} name
+ * @returns {string} The display name of the tool.
+ */
+ static getDisplayName(name) {
+ if (!this.#tools.has(name)) {
+ return name;
+ }
+
+ const tool = this.#tools.get(name);
+ return tool.displayName || name;
+ }
+
/**
* Register function tools for the next chat completion request.
* @param {object} data Generation data
@@ -352,9 +421,11 @@ export class ToolManager {
const id = toolCall.id;
const parameters = toolCall.function.arguments;
const name = toolCall.function.name;
+ const displayName = ToolManager.getDisplayName(name);
result.hadToolCalls = true;
- const toast = toastr.info(`Invoking function tool: ${name}`);
+ const message = ToolManager.formatToolCallMessage(name, parameters);
+ const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
toastr.clear(toast);
console.log('Function tool result:', result);
@@ -365,7 +436,14 @@ export class ToolManager {
continue;
}
- result.invocations.push({ id, name, parameters, result: toolResult });
+ const invocation = {
+ id,
+ displayName,
+ name,
+ parameters,
+ result: toolResult,
+ };
+ result.invocations.push(invocation);
}
}
@@ -414,8 +492,8 @@ export class ToolManager {
codeElement.classList.add('language-json');
data.forEach(i => i.parameters = tryParse(i.parameters));
codeElement.textContent = JSON.stringify(data, null, 2);
- const toolNames = data.map(i => i.name).join(', ');
- summaryElement.textContent = `Performed tool calls: ${toolNames}`;
+ const toolNames = data.map(i => i.displayName || i.name).join(', ');
+ summaryElement.textContent = `Tool calls: ${toolNames}`;
preElement.append(codeElement);
detailsElement.append(summaryElement, preElement);
return detailsElement.outerHTML;
From 6cb82fc21e3fc7996b5f3c4a1e480890038e47da Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 00:39:45 +0300
Subject: [PATCH 174/268] Add json param to /transcript
---
src/endpoints/search.js | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/endpoints/search.js b/src/endpoints/search.js
index 457124228..8c8ed7055 100644
--- a/src/endpoints/search.js
+++ b/src/endpoints/search.js
@@ -60,6 +60,7 @@ router.post('/transcript', jsonParser, async (request, response) => {
const RE_XML_TRANSCRIPT = /([^<]*)<\/text>/g;
const id = request.body.id;
const lang = request.body.lang;
+ const json = request.body.json;
if (!id) {
console.log('Id is required for /transcript');
@@ -129,7 +130,9 @@ router.post('/transcript', jsonParser, async (request, response) => {
// The text is double-encoded
const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
- return response.send(transcriptText);
+ return json
+ ? response.json({ transcript: transcriptText, html: videoPageBody })
+ : response.send(transcriptText);
} catch (error) {
console.log(error);
return response.sendStatus(500);
From 6fc8588a8fb65b782d5ec306195009ad9fa333b0 Mon Sep 17 00:00:00 2001
From: FriedCaper
Date: Thu, 3 Oct 2024 17:03:58 -0300
Subject: [PATCH 175/268] On ctrl-enter when editing, focus text input
---
public/scripts/RossAscends-mods.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js
index 1a9e2d7d2..dc4d07238 100644
--- a/public/scripts/RossAscends-mods.js
+++ b/public/scripts/RossAscends-mods.js
@@ -1027,6 +1027,7 @@ export function initRossMods() {
const editMesDone = $('.mes_edit_done:visible');
if (editMesDone.length > 0) {
console.debug('Accepting edits with Ctrl+Enter');
+ $('#send_textarea').focus();
editMesDone.trigger('click');
return;
} else if (is_send_press == false) {
From 1b3db273891c1ba8c781d2f691ad2e41937ca2aa Mon Sep 17 00:00:00 2001
From: FriedCaper
Date: Thu, 3 Oct 2024 18:48:11 -0300
Subject: [PATCH 176/268] Abort Send and Continue while editing a message
---
public/script.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/public/script.js b/public/script.js
index 1fd4092c6..0076269a0 100644
--- a/public/script.js
+++ b/public/script.js
@@ -1885,6 +1885,7 @@ export async function reloadCurrentChat() {
export async function sendTextareaMessage() {
if (is_send_press) return;
if (isExecutingCommandsFromChatInput) return;
+ if (this_edit_mes_id) return; // don't proceed if editing a message
let generateType;
// "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
@@ -10002,6 +10003,8 @@ jQuery(async function () {
}
else if (id == 'option_continue') {
+ if (this_edit_mes_id) return; // don't proceed if editing a message
+
if (is_send_press == false || fromSlashCommand) {
is_send_press = true;
Generate('continue', buildOrFillAdditionalArgs());
From 777b2518bdb0153f194db79fb651c558e196fc3d Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 01:12:12 +0300
Subject: [PATCH 177/268] Allow returning page if transcript extraction failed
---
src/endpoints/search.js | 132 +++++++++++++++++++++++-----------------
1 file changed, 75 insertions(+), 57 deletions(-)
diff --git a/src/endpoints/search.js b/src/endpoints/search.js
index 8c8ed7055..64993589e 100644
--- a/src/endpoints/search.js
+++ b/src/endpoints/search.js
@@ -22,6 +22,72 @@ const visitHeaders = {
'Sec-Fetch-User': '?1',
};
+/**
+ * Extract the transcript of a YouTube video
+ * @param {string} videoPageBody HTML of the video page
+ * @param {string} lang Language code
+ * @returns {Promise} Transcript text
+ */
+async function extractTranscript(videoPageBody, lang) {
+ const he = require('he');
+ const RE_XML_TRANSCRIPT = /([^<]*)<\/text>/g;
+ const splittedHTML = videoPageBody.split('"captions":');
+
+ if (splittedHTML.length <= 1) {
+ if (videoPageBody.includes('class="g-recaptcha"')) {
+ throw new Error('Too many requests');
+ }
+ if (!videoPageBody.includes('"playabilityStatus":')) {
+ throw new Error('Video is not available');
+ }
+ throw new Error('Transcript not available');
+ }
+
+ const captions = (() => {
+ try {
+ return JSON.parse(splittedHTML[1].split(',"videoDetails')[0].replace('\n', ''));
+ } catch (e) {
+ return undefined;
+ }
+ })()?.['playerCaptionsTracklistRenderer'];
+
+ if (!captions) {
+ throw new Error('Transcript disabled');
+ }
+
+ if (!('captionTracks' in captions)) {
+ throw new Error('Transcript not available');
+ }
+
+ if (lang && !captions.captionTracks.some(track => track.languageCode === lang)) {
+ throw new Error('Transcript not available in this language');
+ }
+
+ const transcriptURL = (lang ? captions.captionTracks.find(track => track.languageCode === lang) : captions.captionTracks[0]).baseUrl;
+ const transcriptResponse = await fetch(transcriptURL, {
+ headers: {
+ ...(lang && { 'Accept-Language': lang }),
+ 'User-Agent': visitHeaders['User-Agent'],
+ },
+ });
+
+ if (!transcriptResponse.ok) {
+ throw new Error('Transcript request failed');
+ }
+
+ const transcriptBody = await transcriptResponse.text();
+ const results = [...transcriptBody.matchAll(RE_XML_TRANSCRIPT)];
+ const transcript = results.map((result) => ({
+ text: result[3],
+ duration: parseFloat(result[2]),
+ offset: parseFloat(result[1]),
+ lang: lang ?? captions.captionTracks[0].languageCode,
+ }));
+ // The text is double-encoded
+ const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
+ return transcriptText;
+}
+
router.post('/serpapi', jsonParser, async (request, response) => {
try {
const key = readSecret(request.user.directories, SECRET_KEYS.SERPAPI);
@@ -56,8 +122,6 @@ router.post('/serpapi', jsonParser, async (request, response) => {
*/
router.post('/transcript', jsonParser, async (request, response) => {
try {
- const he = require('he');
- const RE_XML_TRANSCRIPT = /([^<]*)<\/text>/g;
const id = request.body.id;
const lang = request.body.lang;
const json = request.body.json;
@@ -75,64 +139,18 @@ router.post('/transcript', jsonParser, async (request, response) => {
});
const videoPageBody = await videoPageResponse.text();
- const splittedHTML = videoPageBody.split('"captions":');
- if (splittedHTML.length <= 1) {
- if (videoPageBody.includes('class="g-recaptcha"')) {
- throw new Error('Too many requests');
+ try {
+ const transcriptText = await extractTranscript(videoPageBody, lang);
+ return json
+ ? response.json({ transcript: transcriptText, html: videoPageBody })
+ : response.send(transcriptText);
+ } catch (error) {
+ if (json) {
+ return response.json({ html: videoPageBody, transcript: '' });
}
- if (!videoPageBody.includes('"playabilityStatus":')) {
- throw new Error('Video is not available');
- }
- throw new Error('Transcript not available');
+ throw error;
}
-
- const captions = (() => {
- try {
- return JSON.parse(splittedHTML[1].split(',"videoDetails')[0].replace('\n', ''));
- } catch (e) {
- return undefined;
- }
- })()?.['playerCaptionsTracklistRenderer'];
-
- if (!captions) {
- throw new Error('Transcript disabled');
- }
-
- if (!('captionTracks' in captions)) {
- throw new Error('Transcript not available');
- }
-
- if (lang && !captions.captionTracks.some(track => track.languageCode === lang)) {
- throw new Error('Transcript not available in this language');
- }
-
- const transcriptURL = (lang ? captions.captionTracks.find(track => track.languageCode === lang) : captions.captionTracks[0]).baseUrl;
- const transcriptResponse = await fetch(transcriptURL, {
- headers: {
- ...(lang && { 'Accept-Language': lang }),
- 'User-Agent': visitHeaders['User-Agent'],
- },
- });
-
- if (!transcriptResponse.ok) {
- throw new Error('Transcript request failed');
- }
-
- const transcriptBody = await transcriptResponse.text();
- const results = [...transcriptBody.matchAll(RE_XML_TRANSCRIPT)];
- const transcript = results.map((result) => ({
- text: result[3],
- duration: parseFloat(result[2]),
- offset: parseFloat(result[1]),
- lang: lang ?? captions.captionTracks[0].languageCode,
- }));
- // The text is double-encoded
- const transcriptText = transcript.map((line) => he.decode(he.decode(line.text))).join(' ');
-
- return json
- ? response.json({ transcript: transcriptText, html: videoPageBody })
- : response.send(transcriptText);
} catch (error) {
console.log(error);
return response.sendStatus(500);
From 447a7fba68298cdae8becd82acb3b2f97dc785fd Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 01:45:37 +0300
Subject: [PATCH 178/268] Only delete message if had successful tool calls
---
public/script.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/public/script.js b/public/script.js
index 4d59bde08..22ce72641 100644
--- a/public/script.js
+++ b/public/script.js
@@ -4417,11 +4417,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
}
if (canPerformToolCalls && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length) {
- const lastMessage = chat[chat.length - 1];
- const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
- shouldDeleteMessage && await deleteLastMessage();
const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);
if (invocationResult.hadToolCalls) {
+ const lastMessage = chat[chat.length - 1];
+ const shouldDeleteMessage = ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor.result);
+ shouldDeleteMessage && await deleteLastMessage();
if (!invocationResult.invocations.length && shouldDeleteMessage) {
ToolManager.showToolCallError(invocationResult.errors);
unblockGeneration(type);
@@ -4511,10 +4511,10 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
}
if (canPerformToolCalls) {
- const shouldDeleteMessage = ['', '...'].includes(getMessage);
- shouldDeleteMessage && await deleteLastMessage();
const invocationResult = await ToolManager.invokeFunctionTools(data);
if (invocationResult.hadToolCalls) {
+ const shouldDeleteMessage = ['', '...'].includes(getMessage);
+ shouldDeleteMessage && await deleteLastMessage();
if (!invocationResult.invocations.length && shouldDeleteMessage) {
ToolManager.showToolCallError(invocationResult.errors);
unblockGeneration(type);
From 01f03dbf508a00240e3019c517d8f61166af9eb1 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 01:51:41 +0300
Subject: [PATCH 179/268] Support MistralAI streaming tool calls
---
public/scripts/tool-calling.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index 0eaada477..e0e3c53e0 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -278,9 +278,9 @@ export class ToolManager {
}
for (const toolCallDelta of toolCallDeltas) {
- const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : null;
+ const toolCallIndex = (typeof toolCallDelta?.index === 'number') ? toolCallDelta.index : toolCallDeltas.indexOf(toolCallDelta);
- if (toolCallIndex === null) {
+ if (isNaN(toolCallIndex) || toolCallIndex < 0) {
continue;
}
From 559f1b81f79b2c4e2dc68538d29ead31ed738c36 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 02:11:46 +0300
Subject: [PATCH 180/268] Remove tool calling for Cohere v1
---
public/scripts/tool-calling.js | 13 ----
src/endpoints/backends/chat-completions.js | 10 +--
src/prompt-converters.js | 71 ----------------------
3 files changed, 1 insertion(+), 93 deletions(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index e0e3c53e0..2f101d9ba 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -461,19 +461,6 @@ export class ToolManager {
}
*/
- /*
- if ([chat_completion_sources.COHERE].includes(oai_settings.chat_completion_source)) {
- if (!Array.isArray(data?.tool_calls)) {
- return;
- }
-
- for (const toolCall of data.tool_calls) {
- const args = { name: toolCall.name, arguments: JSON.stringify(toolCall.parameters) };
- console.log('Function tool call:', toolCall);
- }
- }
- */
-
return result;
}
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index 55cb08c70..552ebefbd 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -4,7 +4,7 @@ const fetch = require('node-fetch').default;
const { jsonParser } = require('../../express-common');
const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants');
const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util');
-const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertCohereTools, convertAI21Messages } = require('../../prompt-converters');
+const { convertClaudeMessages, convertGooglePrompt, convertTextCompletionPrompt, convertCohereMessages, convertMistralMessages, convertAI21Messages } = require('../../prompt-converters');
const CohereStream = require('../../cohere-stream');
const { readSecret, SECRET_KEYS } = require('../secrets');
@@ -555,14 +555,6 @@ async function sendCohereRequest(request, response) {
});
}
- /*
- if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
- tools.push(...convertCohereTools(request.body.tools));
- // Can't have both connectors and tools in the same request
- connectors.splice(0, connectors.length);
- }
- */
-
// https://docs.cohere.com/reference/chat
const requestBody = {
stream: Boolean(request.body.stream),
diff --git a/src/prompt-converters.js b/src/prompt-converters.js
index 5d52adb6e..2f6aaf960 100644
--- a/src/prompt-converters.js
+++ b/src/prompt-converters.js
@@ -522,76 +522,6 @@ function convertTextCompletionPrompt(messages) {
return messageStrings.join('\n') + '\nassistant:';
}
-/**
- * Convert OpenAI Chat Completion tools to the format used by Cohere.
- * @param {object[]} tools OpenAI Chat Completion tool definitions
- */
-function convertCohereTools(tools) {
- if (!Array.isArray(tools) || tools.length === 0) {
- return [];
- }
-
- const jsonSchemaToPythonTypes = {
- 'string': 'str',
- 'number': 'float',
- 'integer': 'int',
- 'boolean': 'bool',
- 'array': 'list',
- 'object': 'dict',
- };
-
- const cohereTools = [];
-
- for (const tool of tools) {
- if (tool?.type !== 'function') {
- console.log(`Unsupported tool type: ${tool.type}`);
- continue;
- }
-
- const name = tool?.function?.name;
- const description = tool?.function?.description;
- const properties = tool?.function?.parameters?.properties;
- const required = tool?.function?.parameters?.required;
- const parameters = {};
-
- if (!name) {
- console.log('Tool name is missing');
- continue;
- }
-
- if (!description) {
- console.log('Tool description is missing');
- }
-
- if (!properties || typeof properties !== 'object') {
- console.log(`No properties found for tool: ${tool?.function?.name}`);
- continue;
- }
-
- for (const property in properties) {
- const parameterDefinition = properties[property];
- const description = parameterDefinition.description || (parameterDefinition.enum ? JSON.stringify(parameterDefinition.enum) : '');
- const type = jsonSchemaToPythonTypes[parameterDefinition.type] || 'str';
- const isRequired = Array.isArray(required) && required.includes(property);
- parameters[property] = {
- description: description,
- type: type,
- required: isRequired,
- };
- }
-
- const cohereTool = {
- name: tool.function.name,
- description: tool.function.description,
- parameter_definitions: parameters,
- };
-
- cohereTools.push(cohereTool);
- }
-
- return cohereTools;
-}
-
module.exports = {
convertClaudePrompt,
convertClaudeMessages,
@@ -599,6 +529,5 @@ module.exports = {
convertTextCompletionPrompt,
convertCohereMessages,
convertMistralMessages,
- convertCohereTools,
convertAI21Messages,
};
From c3c10a629e000f3d7479f9a50fbb6d28316acaa0 Mon Sep 17 00:00:00 2001
From: Cohee <18619528+Cohee1207@users.noreply.github.com>
Date: Fri, 4 Oct 2024 03:41:25 +0300
Subject: [PATCH 181/268] Claude: new prompt converter + non-streaming tools
---
public/scripts/tool-calling.js | 111 +++++++++---------
src/endpoints/backends/chat-completions.js | 2 -
src/prompt-converters.js | 127 ++++++++++++++-------
3 files changed, 141 insertions(+), 99 deletions(-)
diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js
index 2f101d9ba..15d3db954 100644
--- a/public/scripts/tool-calling.js
+++ b/public/scripts/tool-calling.js
@@ -339,10 +339,9 @@ export class ToolManager {
const supportedSources = [
chat_completion_sources.OPENAI,
- //chat_completion_sources.COHERE,
chat_completion_sources.CUSTOM,
chat_completion_sources.MISTRALAI,
- //chat_completion_sources.CLAUDE,
+ chat_completion_sources.CLAUDE,
chat_completion_sources.OPENROUTER,
chat_completion_sources.GROQ,
];
@@ -372,18 +371,29 @@ export class ToolManager {
}
// Parsed tool calls from non-streaming data
- if (!Array.isArray(data?.choices)) {
- return;
+ if (Array.isArray(data?.choices)) {
+ // Find a choice with 0-index
+ const choice = data.choices.find(choice => choice.index === 0);
+
+ if (choice) {
+ return choice.message.tool_calls;
+ }
}
- // Find a choice with 0-index
- const choice = data.choices.find(choice => choice.index === 0);
+ if (Array.isArray(data?.content)) {
+ // Claude tool calls to OpenAI tool calls
+ const content = data.content.filter(c => c.type === 'tool_use').map(c => {
+ return {
+ id: c.id,
+ function: {
+ name: c.name,
+ arguments: c.input,
+ },
+ };
+ });
- if (!choice) {
- return;
+ return content;
}
-
- return choice.message.tool_calls;
}
/**
@@ -407,59 +417,43 @@ export class ToolManager {
chat_completion_sources.GROQ,
];
- if (oaiCompatibleSources.includes(oai_settings.chat_completion_source)) {
- if (!Array.isArray(toolCalls)) {
- return result;
- }
-
- for (const toolCall of toolCalls) {
- if (typeof toolCall.function !== 'object') {
- continue;
- }
-
- console.log('Function tool call:', toolCall);
- const id = toolCall.id;
- const parameters = toolCall.function.arguments;
- const name = toolCall.function.name;
- const displayName = ToolManager.getDisplayName(name);
- result.hadToolCalls = true;
-
- const message = ToolManager.formatToolCallMessage(name, parameters);
- const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
- const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
- toastr.clear(toast);
- console.log('Function tool result:', result);
-
- // Save a successful invocation
- if (toolResult instanceof Error) {
- result.errors.push(toolResult);
- continue;
- }
-
- const invocation = {
- id,
- displayName,
- name,
- parameters,
- result: toolResult,
- };
- result.invocations.push(invocation);
- }
+ if (!Array.isArray(toolCalls)) {
+ return result;
}
- /*
- if ([chat_completion_sources.CLAUDE].includes(oai_settings.chat_completion_source)) {
- if (!Array.isArray(data?.content)) {
- return;
+ for (const toolCall of toolCalls) {
+ if (typeof toolCall.function !== 'object') {
+ continue;
}
- for (const content of data.content) {
- if (content.type === 'tool_use') {
- const args = { name: content.name, arguments: JSON.stringify(content.input) };
- }
+ console.log('Function tool call:', toolCall);
+ const id = toolCall.id;
+ const parameters = toolCall.function.arguments;
+ const name = toolCall.function.name;
+ const displayName = ToolManager.getDisplayName(name);
+ result.hadToolCalls = true;
+
+ const message = ToolManager.formatToolCallMessage(name, parameters);
+ const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
+ const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
+ toastr.clear(toast);
+ console.log('Function tool result:', result);
+
+ // Save a successful invocation
+ if (toolResult instanceof Error) {
+ result.errors.push(toolResult);
+ continue;
}
+
+ const invocation = {
+ id,
+ displayName,
+ name,
+ parameters,
+ result: toolResult,
+ };
+ result.invocations.push(invocation);
}
- */
return result;
}
@@ -491,6 +485,9 @@ export class ToolManager {
* @param {ToolInvocation[]} invocations Successful tool invocations
*/
static saveFunctionToolInvocations(invocations) {
+ if (!Array.isArray(invocations) || invocations.length === 0) {
+ return;
+ }
const message = {
name: systemUserName,
force_avatar: system_avatar,
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index 552ebefbd..9c29a3b44 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -124,7 +124,6 @@ async function sendClaudeRequest(request, response) {
} else {
delete requestBody.system;
}
- /*
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
// Claude doesn't do prefills on function calls, and doesn't allow empty messages
if (convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
@@ -137,7 +136,6 @@ async function sendClaudeRequest(request, response) {
.map(tool => tool.function)
.map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
}
- */
if (enableSystemPromptCache) {
additionalHeaders['anthropic-beta'] = 'prompt-caching-2024-07-31';
}
diff --git a/src/prompt-converters.js b/src/prompt-converters.js
index 2f6aaf960..b4b3d718b 100644
--- a/src/prompt-converters.js
+++ b/src/prompt-converters.js
@@ -118,8 +118,27 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
});
}
}
+
// Now replace all further messages that have the role 'system' with the role 'user'. (or all if we're not using one)
messages.forEach((message) => {
+ if (message.role === 'assistant' && message.tool_calls) {
+ message.content = message.tool_calls.map((tc) => ({
+ type: 'tool_use',
+ id: tc.id,
+ name: tc.function.name,
+ input: tc.function.arguments,
+ }));
+ }
+
+ if (message.role === 'tool') {
+ message.role = 'user';
+ message.content = [{
+ type: 'tool_result',
+ tool_use_id: message.tool_call_id,
+ content: message.content,
+ }];
+ }
+
if (message.role === 'system') {
if (userName && message.name === 'example_user') {
message.content = `${userName}: ${message.content}`;
@@ -128,13 +147,80 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
message.content = `${charName}: ${message.content}`;
}
message.role = 'user';
+
+ // Delete name here so it doesn't get added later
+ delete message.name;
}
+
+ // Convert everything to an array of it would be easier to work with
+ if (typeof message.content === 'string') {
+ // Take care of name properties since claude messages don't support them
+ if (message.name) {
+ message.content = `${message.name}: ${message.content}`;
+ }
+
+ message.content = [{ type: 'text', text: message.content }];
+ } else if (Array.isArray(message.content)) {
+ message.content = message.content.map((content) => {
+ if (content.type === 'image_url') {
+ const imageEntry = content?.image_url;
+ const imageData = imageEntry?.url;
+ const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
+ const base64Data = imageData?.split(',')?.[1];
+
+ return {
+ type: 'image',
+ source: {
+ type: 'base64',
+ media_type: mimeType,
+ data: base64Data,
+ },
+ };
+ }
+
+ if (content.type === 'text') {
+ if (message.name) {
+ content.text = `${message.name}: ${content.text}`;
+ }
+
+ return content;
+ }
+
+ return content;
+ });
+ }
+
+ // Remove offending properties
+ delete message.name;
+ delete message.tool_calls;
+ delete message.tool_call_id;
});
+ // Images in assistant messages should be moved to the next user message
+ for (let i = 0; i < messages.length; i++) {
+ if (messages[i].role === 'assistant' && messages[i].content.some(c => c.type === 'image')) {
+ // Find the next user message
+ let j = i + 1;
+ while (j < messages.length && messages[j].role !== 'user') {
+ j++;
+ }
+
+ // Move the images
+ if (j >= messages.length) {
+ // If there is no user message after the assistant message, add a new one
+ messages.splice(i + 1, 0, { role: 'user', content: [] });
+ }
+
+ messages[j].content.push(...messages[i].content.filter(c => c.type === 'image'));
+ messages[i].content = messages[i].content.filter(c => c.type !== 'image');
+ }
+ }
+
// Shouldn't be conditional anymore, messages api expects the last role to be user unless we're explicitly prefilling
if (prefillString) {
messages.push({
role: 'assistant',
+ // Dangling whitespace are not allowed for prefilling
content: prefillString.trimEnd(),
});
}
@@ -143,50 +229,11 @@ function convertClaudeMessages(messages, prefillString, useSysPrompt, humanMsgFi
// Also handle multi-modality, holy slop.
let mergedMessages = [];
messages.forEach((message) => {
- const imageEntry = message.content?.[1]?.image_url;
- const imageData = imageEntry?.url;
- const mimeType = imageData?.split(';')?.[0].split(':')?.[1];
- const base64Data = imageData?.split(',')?.[1];
-
- // Take care of name properties since claude messages don't support them
- if (message.name) {
- if (Array.isArray(message.content)) {
- message.content[0].text = `${message.name}: ${message.content[0].text}`;
- } else {
- message.content = `${message.name}: ${message.content}`;
- }
- delete message.name;
- }
-
if (mergedMessages.length > 0 && mergedMessages[mergedMessages.length - 1].role === message.role) {
- if (Array.isArray(message.content)) {
- if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
- mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content[0].text;
- } else {
- mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content[0].text;
- }
- } else {
- if (Array.isArray(mergedMessages[mergedMessages.length - 1].content)) {
- mergedMessages[mergedMessages.length - 1].content[0].text += '\n\n' + message.content;
- } else {
- mergedMessages[mergedMessages.length - 1].content += '\n\n' + message.content;
- }
- }
+ mergedMessages[mergedMessages.length - 1].content.push(...message.content);
} else {
mergedMessages.push(message);
}
- if (imageData) {
- mergedMessages[mergedMessages.length - 1].content = [
- { type: 'text', text: mergedMessages[mergedMessages.length - 1].content[0]?.text || mergedMessages[mergedMessages.length - 1].content },
- {
- type: 'image', source: {
- type: 'base64',
- media_type: mimeType,
- data: base64Data,
- },
- },
- ];
- }
});
return { messages: mergedMessages, systemPrompt: systemPrompt.trim() };
From 3e465d155cb544d5d1d246a032bc0eafd84a9b0e Mon Sep 17 00:00:00 2001
From: QuantumEntangledAndy
Date: Fri, 4 Oct 2024 15:26:48 +0700
Subject: [PATCH 182/268] Include polyfill css
---
public/css/popup.css | 3 +-
public/lib/dialog-polyfill.css | 37 ++
public/lib/dialog-polyfill.esm.js | 858 ++++++++++++++++++++++++++++++
public/scripts/popup.js | 2 +
4 files changed, 899 insertions(+), 1 deletion(-)
create mode 100644 public/lib/dialog-polyfill.css
create mode 100644 public/lib/dialog-polyfill.esm.js
diff --git a/public/css/popup.css b/public/css/popup.css
index d184dc1fb..545f7014a 100644
--- a/public/css/popup.css
+++ b/public/css/popup.css
@@ -1,3 +1,4 @@
+@import url('/lib/dialog-polyfill.css');
@import url('./popup-safari-fix.css');
dialog {
@@ -42,7 +43,7 @@ dialog {
display: flex;
flex-direction: column;
overflow: hidden;
- width: 100%;
+ width: min(100%, 100vw);
height: 100%;
padding: 1px;
}
diff --git a/public/lib/dialog-polyfill.css b/public/lib/dialog-polyfill.css
new file mode 100644
index 000000000..6b38bf08b
--- /dev/null
+++ b/public/lib/dialog-polyfill.css
@@ -0,0 +1,37 @@
+dialog {
+ position: absolute;
+ left: 0; right: 0;
+ width: -moz-fit-content;
+ width: -webkit-fit-content;
+ width: fit-content;
+ height: -moz-fit-content;
+ height: -webkit-fit-content;
+ height: fit-content;
+ margin: auto;
+ border: solid;
+ padding: 1em;
+ background: white;
+ color: black;
+ display: block;
+}
+
+dialog:not([open]) {
+ display: none;
+}
+
+dialog + .backdrop {
+ position: fixed;
+ top: 0; right: 0; bottom: 0; left: 0;
+ background: rgba(0,0,0,0.1);
+}
+
+._dialog_overlay {
+ position: fixed;
+ top: 0; right: 0; bottom: 0; left: 0;
+}
+
+dialog.fixed {
+ position: fixed;
+ top: 50%;
+ transform: translate(0, -50%);
+}
\ No newline at end of file
diff --git a/public/lib/dialog-polyfill.esm.js b/public/lib/dialog-polyfill.esm.js
new file mode 100644
index 000000000..da774bc98
--- /dev/null
+++ b/public/lib/dialog-polyfill.esm.js
@@ -0,0 +1,858 @@
+// nb. This is for IE10 and lower _only_.
+var supportCustomEvent = window.CustomEvent;
+if (!supportCustomEvent || typeof supportCustomEvent === 'object') {
+ supportCustomEvent = function CustomEvent(event, x) {
+ x = x || {};
+ var ev = document.createEvent('CustomEvent');
+ ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
+ return ev;
+ };
+ supportCustomEvent.prototype = window.Event.prototype;
+}
+
+/**
+ * Dispatches the passed event to both an "on" handler as well as via the
+ * normal dispatch operation. Does not bubble.
+ *
+ * @param {!EventTarget} target
+ * @param {!Event} event
+ * @return {boolean}
+ */
+function safeDispatchEvent(target, event) {
+ var check = 'on' + event.type.toLowerCase();
+ if (typeof target[check] === 'function') {
+ target[check](event);
+ }
+ return target.dispatchEvent(event);
+}
+
+/**
+ * @param {Element} el to check for stacking context
+ * @return {boolean} whether this el or its parents creates a stacking context
+ */
+function createsStackingContext(el) {
+ while (el && el !== document.body) {
+ var s = window.getComputedStyle(el);
+ var invalid = function(k, ok) {
+ return !(s[k] === undefined || s[k] === ok);
+ };
+
+ if (s.opacity < 1 ||
+ invalid('zIndex', 'auto') ||
+ invalid('transform', 'none') ||
+ invalid('mixBlendMode', 'normal') ||
+ invalid('filter', 'none') ||
+ invalid('perspective', 'none') ||
+ s['isolation'] === 'isolate' ||
+ s.position === 'fixed' ||
+ s.webkitOverflowScrolling === 'touch') {
+ return true;
+ }
+ el = el.parentElement;
+ }
+ return false;
+}
+
+/**
+ * Finds the nearest