diff --git a/public/script.js b/public/script.js
index 722ffbbf8..daea9cc50 100644
--- a/public/script.js
+++ b/public/script.js
@@ -8392,6 +8392,9 @@ const CONNECT_API_MAP = {
},
};
+// Collect all unique API names in an array
+export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
+
// Fill connections map from textgen_types and chat_completion_sources
for (const textGenType of Object.values(textgen_types)) {
if (CONNECT_API_MAP[textGenType]) continue;
@@ -8966,9 +8969,6 @@ jQuery(async function () {
return '';
}
- // Collect all unique API names in an array
- const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))];
-
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'dupe',
callback: duplicateCharacter,
@@ -8977,13 +8977,13 @@ jQuery(async function () {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'api',
callback: connectAPISlash,
+ returns: 'the current API',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'API to connect to',
typeList: [ARGUMENT_TYPE.STRING],
- isRequired: false,
enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) =>
- new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)),
+ new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)),
selected[0].toUpperCase() ?? enumIcons.default)),
}),
],
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js
index 9552a488d..4fa4ff603 100644
--- a/public/scripts/RossAscends-mods.js
+++ b/public/scripts/RossAscends-mods.js
@@ -954,6 +954,11 @@ export function initRossMods() {
* @param {KeyboardEvent} event
*/
async function processHotkeys(event) {
+ // Default hotkeys and shortcuts shouldn't work if any popup is currently open
+ if (Popup.util.isPopupOpen()) {
+ return;
+ }
+
//Enter to send when send_textarea in focus
if (document.activeElement == hotkeyTargets['send_textarea']) {
const sendOnEnter = shouldSendOnEnter();
@@ -1107,10 +1112,6 @@ export function initRossMods() {
}
if (event.key == 'Escape') { //closes various panels
- // Do not close panels if we are currently inside a popup
- if (Popup.util.isPopupOpen())
- return;
-
//dont override Escape hotkey functions from script.js
//"close edit box" and "cancel stream generation".
if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index f9a5877b9..14f74ef28 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -1,7 +1,9 @@
import {
Generate,
+ UNIQUE_APIS,
activateSendButtons,
addOneMessage,
+ api_server,
callPopup,
characters,
chat,
@@ -49,7 +51,7 @@ import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSel
import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
-import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
+import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
@@ -1496,8 +1498,9 @@ export function initDefaultSlashCommands() {
],
helpString: 'Sets the specified prompt manager entry/entries on or off.',
}));
- SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',
- callback: async()=>((await showFontAwesomePicker()) ?? false).toString(),
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'pick-icon',
+ callback: async () => ((await showFontAwesomePicker()) ?? false).toString(),
returns: 'The chosen icon name or false if cancelled.',
helpString: `
Opens a popup with all the available Font Awesome icons and returns the selected icon's name.
@@ -1511,6 +1514,50 @@ export function initDefaultSlashCommands() {
`,
}));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'api-url',
+ callback: setApiUrlCallback,
+ returns: 'the current API url',
+ aliases: ['server'],
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'api',
+ description: 'API to set/get the URL for - if not provided, current API is used',
+ typeList: [ARGUMENT_TYPE.STRING],
+ enumList: [
+ new SlashCommandEnumValue('custom', 'custom OpenAI-compatible', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'openai')), 'O'),
+ new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'),
+ ...Object.values(textgen_types).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')),
+ ],
+ }),
+ SlashCommandNamedArgument.fromProps({
+ name: 'connect',
+ description: 'Whether to auto-connect to the API after setting the URL',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ defaultValue: 'true',
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'API url to connect to',
+ typeList: [ARGUMENT_TYPE.STRING],
+ }),
+ ],
+ helpString: `
+
+ Set the API url / server url for the currently selected API, including the port. If no argument is provided, it will return the current API url.
+
+
+ If a manual API is provided to set the URL, make sure to set connect=false
, as auto-connect only works for the currently selected API,
+ or consider switching to it with /api
first.
+
+
+ This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible for the Chat Completion sources. If unsure which APIs are supported,
+ check the auto-completion of the optional api
argument of this command.
+
+ `,
+ }));
registerVariableCommands();
}
@@ -3418,6 +3465,102 @@ function setPromptEntryCallback(args, targetState) {
return '';
}
+/**
+ * Sets the API URL and triggers the text generation web UI button click.
+ *
+ * @param {object} args - named args
+ * @param {string?} [args.api=null] - the API name to set/get the URL for
+ * @param {string?} [args.connect=true] - whether to connect to the API after setting
+ * @param {string} url - the API URL to set
+ * @returns {Promise}
+ */
+async function setApiUrlCallback({ api = null, connect = 'true' }, url) {
+ const autoConnect = isTrueBoolean(connect);
+
+ // Special handling for Chat Completion Custom OpenAI compatible, that one can also support API url handling
+ const isCurrentlyCustomOpenai = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
+ if (api === chat_completion_sources.CUSTOM || (!api && isCurrentlyCustomOpenai)) {
+ if (!url) {
+ return oai_settings.custom_url ?? '';
+ }
+
+ if (!isCurrentlyCustomOpenai && autoConnect) {
+ toastr.warning('Custom OpenAI API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
+ return '';
+ }
+
+ $('#custom_api_url_text').val(url).trigger('input');
+
+ if (autoConnect) {
+ $('#api_button_openai').trigger('click');
+ }
+
+ return url;
+ }
+
+ // Special handling for Kobold Classic API
+ const isCurrentlyKoboldClassic = main_api === 'kobold';
+ if (api === 'kobold' || (!api && isCurrentlyKoboldClassic)) {
+ if (!url) {
+ return api_server ?? '';
+ }
+
+ if (!isCurrentlyKoboldClassic && autoConnect) {
+ toastr.warning('Kobold Classic API is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.');
+ return '';
+ }
+
+ $('#api_url_text').val(url).trigger('input');
+ // trigger blur debounced, so we hide the autocomplete menu
+ setTimeout(() => $('#api_url_text').trigger('blur'), 1);
+
+ if (autoConnect) {
+ $('#api_button').trigger('click');
+ }
+
+ return api_server ?? '';
+ }
+
+ // Do some checks and get the api type we are targeting with this command
+ if (api && !Object.values(textgen_types).includes(api)) {
+ toastr.warning(`API '${api}' is not a valid text_gen API.`);
+ return '';
+ }
+ if (!api && !Object.values(textgen_types).includes(textgenerationwebui_settings.type)) {
+ toastr.warning(`API '${textgenerationwebui_settings.type}' is not a valid text_gen API.`);
+ return '';
+ }
+ if (api && url && autoConnect && api !== textgenerationwebui_settings.type) {
+ toastr.warning(`API '${api}' is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
+ return '';
+ }
+ const type = api || textgenerationwebui_settings.type;
+
+ const inputSelector = SERVER_INPUTS[type];
+ if (!inputSelector) {
+ toastr.warning(`API '${type}' does not have a server url input.`);
+ return '';
+ }
+
+ // If no url was provided, return the current one
+ if (!url) {
+ return textgenerationwebui_settings.server_urls[type] ?? '';
+ }
+
+ // else, we want to actually set the url
+ $(inputSelector).val(url).trigger('input');
+ // trigger blur debounced, so we hide the autocomplete menu
+ setTimeout(() => $(inputSelector).trigger('blur'), 1);
+
+ // Trigger the auto connect via connect button, if requested
+ if (autoConnect) {
+ $('#api_button_textgenerationwebui').trigger('click');
+ }
+
+ // We still re-acquire the value, as it might have been modified by the validation on connect
+ return textgenerationwebui_settings.server_urls[type] ?? '';
+}
+
export let isExecutingCommandsFromChatInput = false;
export let commandsFromChatInputAbortController;
diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js
index 50f0c3d4d..a2585a1c3 100644
--- a/public/scripts/textgen-settings.js
+++ b/public/scripts/textgen-settings.js
@@ -94,7 +94,7 @@ let DREAMGEN_SERVER = 'https://dreamgen.com';
let OPENROUTER_SERVER = 'https://openrouter.ai/api';
let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1';
-const SERVER_INPUTS = {
+export const SERVER_INPUTS = {
[textgen_types.OOBA]: '#textgenerationwebui_api_url_text',
[textgen_types.VLLM]: '#vllm_api_url_text',
[textgen_types.APHRODITE]: '#aphrodite_api_url_text',