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',