diff --git a/package-lock.json b/package-lock.json index 64f3f1cae6..666982fbc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.10.8", + "version": "1.10.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.10.8", + "version": "1.10.9", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 39f731ae6f..ab117a4a75 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "type": "git", "url": "https://github.com/SillyTavern/SillyTavern.git" }, - "version": "1.10.8", + "version": "1.10.9", "scripts": { "start": "node server.js", "start-multi": "node server.js --disableCsrf", diff --git a/public/css/st-tailwind.css b/public/css/st-tailwind.css index 701d038900..d86d53f49e 100644 --- a/public/css/st-tailwind.css +++ b/public/css/st-tailwind.css @@ -229,6 +229,10 @@ display: flex; } +.flexBasis100p { + flex-basis: 100%; +} + .flexBasis50p { flex-basis: 50% } @@ -263,6 +267,10 @@ flex-shrink: 1 } +.flexWrap { + flex-wrap: wrap; +} + .flexnowrap { flex-wrap: nowrap; } diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 4bf01922ea..692ebd2339 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -358,3 +358,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtons { body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint { display: none !important; } + +#openai_image_inlining:not(:checked) ~ #image_inlining_hint { + display: none; +} + +#openai_image_inlining:checked ~ #image_inlining_hint { + display: block; +} diff --git a/public/index.html b/public/index.html index 205ae46917..ac6eda88f3 100644 --- a/public/index.html +++ b/public/index.html @@ -121,7 +121,7 @@
Click slider numbers to input manually.
-
MAD LAB MODE ON
+
MAD LAB MODE ON
@@ -213,8 +213,8 @@

Text Gen WebUI pres
-
- Response (tokens) +
+ Response (tokens)
@@ -236,8 +236,8 @@

Text Gen WebUI pres

-
- Context (tokens) +
+ Context (tokens)
@@ -245,7 +245,7 @@

Text Gen WebUI pres
@@ -735,12 +735,12 @@

Text Gen WebUI pres

- - Typical Sampling -
+ + Typical P +
- - + +
@@ -867,7 +867,7 @@

GBNF Grammar 3

- Typical P Sampling + Typical P 4
@@ -1077,7 +1077,7 @@

GBNF Grammar

- Typical Sampling + Typical P 5
@@ -1097,83 +1097,83 @@

GBNF Grammar

-
- Temperature +
+ Temperature
-
- Top K +
+ Top K
-
- Top P +
+ Top P
-
+
Typical P
-
- Min P +
+ Min P
-
- Top A +
+ Top A
-
- Tail Free Sampling +
+ Tail Free Sampling
-
- Epsilon Cutoff +
+ Epsilon Cutoff
-
- Eta Cutoff +
+ Eta Cutoff
-
- Repetition Penalty +
+ Repetition Penalty
-
- Repetition Penalty Range +
+ Repetition Penalty Range
-
- Encoder Penalty +
+ Encoder Penalty
-
- Frequency Penalty +
+ Frequency Penalty
-
- Presence Penalty +
+ Presence Penalty
-
- No Repeat Ngram Size +
+ No Repeat Ngram Size
-
- Min Length +
+ Min Length
@@ -1204,18 +1204,18 @@

-
- Mode +
+ Mode
-
- Tau +
+ Tau
-
- Eta +
+ Eta
@@ -1226,8 +1226,8 @@

Beam Search

-
- # of Beams +
+ # of Beams
@@ -1723,6 +1723,7 @@

OpenAI Model

+ @@ -1741,10 +1742,25 @@

OpenAI Model

+ +
@@ -1815,6 +1831,36 @@

OpenRouter Model

+
+
+
+ OpenRouter Model Sorting +
+
+
+
+ +
+
+ +
+ + Put OpenAI models in one group, Anthropic models in other group, etc. Can be combined with sorting. + +
+
+
+
+

- +
@@ -3854,7 +3905,7 @@

- +

@@ -4049,6 +4100,7 @@

Edit

+
diff --git a/public/script.js b/public/script.js index 59aba505ac..1e1f825a23 100644 --- a/public/script.js +++ b/public/script.js @@ -143,6 +143,7 @@ import { escapeRegex, resetScrollHeight, onlyUnique, + getBase64Async, } from "./scripts/utils.js"; import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js"; @@ -184,7 +185,7 @@ import { } from "./scripts/instruct-mode.js"; import { applyLocale } from "./scripts/i18n.js"; import { getFriendlyTokenizerName, getTokenCount, getTokenizerModel, initTokenizers, saveTokenCache } from "./scripts/tokenizers.js"; -import { initPersonas, selectCurrentPersona, setPersonaDescription } from "./scripts/personas.js"; +import { createPersona, initPersonas, selectCurrentPersona, setPersonaDescription } from "./scripts/personas.js"; import { getBackgrounds, initBackgrounds } from "./scripts/backgrounds.js"; import { hideLoader, showLoader } from "./scripts/loader.js"; import { CharacterContextMenu, BulkEditOverlay } from "./scripts/BulkEditOverlay.js"; @@ -313,11 +314,6 @@ export const event_types = { export const eventSource = new EventEmitter(); -// Check for override warnings every 5 seconds... -setInterval(displayOverrideWarnings, 5000); -// ...or when the chat changes -eventSource.on(event_types.SETTINGS_LOADED, () => { settingsReady = true; }); -eventSource.on(event_types.CHAT_CHANGED, displayOverrideWarnings); eventSource.on(event_types.MESSAGE_RECEIVED, processExtensionHelpers); eventSource.on(event_types.MESSAGE_SENT, processExtensionHelpers); @@ -729,7 +725,7 @@ async function firstLoadInit() { sendSystemMessage(system_message_types.WELCOME); await readSecretState(); await getClientVersion(); - await getSettings("def"); + await getSettings(); await getUserAvatars(); await getCharacters(); await getBackgrounds(); @@ -926,12 +922,12 @@ async function getStatus() { export function startStatusLoading() { $(".api_loading").show(); - $(".api_button").attr("disabled", "disabled").addClass("disabled"); + $(".api_button").addClass("disabled"); } export function stopStatusLoading() { $(".api_loading").hide(); - $(".api_button").removeAttr("disabled").removeClass("disabled"); + $(".api_button").removeClass("disabled"); } export function resultCheckStatus() { @@ -1014,7 +1010,7 @@ function getBackBlock() { function getEmptyBlock() { const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog']; const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it']; - const roll = Math.floor(Math.random() * icons.length); + const roll = new Date().getMinutes() % icons.length; const emptyBlock = `
@@ -1050,9 +1046,10 @@ function getCharacterBlock(item, id) { template.find('.ch_description').hide(); } - const version = item.data?.character_version || ''; - if (version) { - template.find('.character_version').text(version); + const auxFieldName = power_user.aux_field || 'character_version'; + const auxFieldValue = (item.data && item.data[auxFieldName]) || ''; + if (auxFieldValue) { + template.find('.character_version').text(auxFieldValue); } else { template.find('.character_version').hide(); @@ -1079,7 +1076,6 @@ async function printCharacters(fullRefresh = false) { } await delay(1); - displayOverrideWarnings(); } const storageKey = 'Characters_PerPage'; @@ -3408,7 +3404,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, cfgValues); } else if (main_api == 'openai') { - let [prompt, counts] = prepareOpenAIMessages({ + let [prompt, counts] = await prepareOpenAIMessages({ name2: name2, charDescription: description, charPersonality: personality, @@ -4784,22 +4780,21 @@ async function read_avatar_load(input) { create_save.avatar = input.files; } - const e = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = resolve; - reader.onerror = reject; - reader.readAsDataURL(input.files[0]); - }) + const file = input.files[0]; + const fileData = await getBase64Async(file); - $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); + if (!power_user.never_resize_avatars) { + $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); + const croppedImage = await callPopup(getCropPopup(fileData), 'avatarToCrop'); + if (!croppedImage) { + return; + } - const croppedImage = await callPopup(getCropPopup(e.target.result), 'avatarToCrop'); - if (!croppedImage) { - return; + $("#avatar_load_preview").attr("src", croppedImage); + } else { + $("#avatar_load_preview").attr("src", fileData); } - $("#avatar_load_preview").attr("src", croppedImage || e.target.result); - if (menu_type == "create") { return; } @@ -5161,24 +5156,19 @@ async function uploadUserAvatar(e) { } const formData = new FormData($("#form_upload_avatar").get(0)); - - const dataUrl = await new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = resolve; - reader.onerror = reject; - reader.readAsDataURL(file); - }); - - $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); - const confirmation = await callPopup(getCropPopup(dataUrl.target.result), 'avatarToCrop'); - if (!confirmation) { - return; - } - + const dataUrl = await getBase64Async(file); let url = "/uploaduseravatar"; - if (crop_data !== undefined) { - url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; + if (!power_user.never_resize_avatars) { + $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); + const confirmation = await callPopup(getCropPopup(dataUrl), 'avatarToCrop'); + if (!confirmation) { + return; + } + + if (crop_data !== undefined) { + url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; + } } jQuery.ajax({ @@ -5189,7 +5179,7 @@ async function uploadUserAvatar(e) { cache: false, contentType: false, processData: false, - success: async function () { + success: async function (data) { // If the user uploaded a new avatar, we want to make sure it's not cached const name = formData.get("overwrite_name"); if (name) { @@ -5197,6 +5187,12 @@ async function uploadUserAvatar(e) { reloadUserAvatar(true); } + if (data.path) { + await getUserAvatars(); + await delay(500); + await createPersona(data.path); + } + crop_data = undefined; await getUserAvatars(); }, @@ -5234,7 +5230,7 @@ async function doOnboarding(avatarId) { //***************SETTINGS****************// /////////////////////////////////////////// -async function getSettings(type) { +async function getSettings() { const response = await fetch("/getsettings", { method: "POST", headers: getRequestHeaders(), @@ -5407,6 +5403,7 @@ async function getSettings(type) { } } + settingsReady = true; eventSource.emit(event_types.SETTINGS_LOADED); } @@ -7131,19 +7128,6 @@ const swipe_right = () => { } } - - -function displayOverrideWarnings() { - if (!this_chid || !selected_group) { - $('.prompt_overridden').hide(); - $('.jailbreak_overridden').hide(); - return; - } - - $('.prompt_overridden').toggle(!!(characters[this_chid]?.data?.system_prompt)); - $('.jailbreak_overridden').toggle(!!(characters[this_chid]?.data?.post_history_instructions)); -} - function connectAPISlash(_, text) { if (!text) return; @@ -8694,7 +8678,7 @@ jQuery(async function () { startStatusLoading(); // Check near immediately rather than waiting for up to 90s - setTimeout(getStatusNovel, 10); + await getStatusNovel(); }); //**************************CHARACTER IMPORT EXPORT*************************// @@ -9104,8 +9088,11 @@ jQuery(async function () { } }); + let manualInputTimeout; + $(document).on('input', '.range-block-counter input, .neo-range-input', function () { - setTimeout(() => { + clearTimeout(manualInputTimeout); + manualInputTimeout = setTimeout(() => { const caretPosition = saveCaretPosition($(this).get(0)); const myText = $(this).val().trim(); $(this).val(myText); // trim line breaks and spaces @@ -9127,7 +9114,7 @@ jQuery(async function () { //yolo anything for Lab Mode if (power_user.enableLabMode) { - console.log($(masterElement).attr('id'), myValue) + //console.log($(masterElement).attr('id'), myValue) $(masterElement).val(myValue).trigger('input') return } @@ -9171,7 +9158,7 @@ jQuery(async function () { restoreCaretPosition($(this).get(0), caretPosition); }, 2000); - }) + }); $(".user_stats_button").on('click', function () { userStatsHandler(); diff --git a/public/scripts/BulkEditOverlay.js b/public/scripts/BulkEditOverlay.js index 3a213073ef..ae0a60a048 100644 --- a/public/scripts/BulkEditOverlay.js +++ b/public/scripts/BulkEditOverlay.js @@ -13,6 +13,7 @@ import { } from "../script.js"; import { favsToHotswap } from "./RossAscends-mods.js"; +import { hideLoader, showLoader } from "./loader.js"; import { convertCharacterToPersona } from "./personas.js"; import { createTagInput, getTagKeyForCharacter, tag_map } from "./tags.js"; @@ -614,9 +615,12 @@ class BulkEditOverlay { const deleteChats = document.getElementById('del_char_checkbox').checked ?? false; + showLoader(); + toastr.info("We're deleting your characters, please wait...", 'Working on it'); Promise.all(this.selectedCharacters.map(async characterId => CharacterContextMenu.delete(characterId, deleteChats))) .then(() => getCharacters()) .then(() => this.browseState()) + .finally(() => hideLoader()); } ); } diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 05f70dff40..4d83d1419e 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -1,8 +1,9 @@ import { getBase64Async, saveBase64AsFile } from "../../utils.js"; import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js"; -import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js"; +import { appendImageToMessage, callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js"; import { getMessageTimeStamp } from "../../RossAscends-mods.js"; import { SECRET_KEYS, secret_state } from "../../secrets.js"; +import { isImageInliningSupported } from "../../openai.js"; export { MODULE_NAME }; const MODULE_NAME = 'caption'; @@ -223,6 +224,83 @@ function onRefineModeInput() { saveSettingsDebounced(); } +async function sendEmbeddedImage(e) { + const file = e.target.files[0]; + + if (!file || !(file instanceof File)) { + return; + } + + try { + const context = getContext(); + const fileData = await getBase64Async(file); + const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1]; + const base64Data = fileData.split(',')[1]; + const caption = await callPopup('

Enter a comment or question (optional)

', 'input', 'What is this?', { okButton: 'Send', rows: 2 }); + const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format); + const message = { + name: context.name1, + is_user: true, + send_date: getMessageTimeStamp(), + mes: caption || `[${context.name1} sends ${context.name2} a picture]`, + extra: { + image: imagePath, + inline_image: !!caption, + title: caption || '', + }, + }; + context.chat.push(message); + context.addOneMessage(message); + await context.generate('caption'); + } + catch (error) { + console.log(error); + } + finally { + e.target.form.reset(); + setImageIcon(); + } +} + +function onImageEmbedClicked() { + const context = getContext(); + const messageElement = $(this).closest('.mes'); + const messageId = messageElement.attr('mesid'); + const message = context.chat[messageId]; + + if (!message) { + console.warn('Failed to find message with id', messageId); + return; + } + + $('#embed_img_file') + .off('change') + .on('change', parseAndUploadEmbed) + .trigger('click'); + + async function parseAndUploadEmbed(e) { + const file = e.target.files[0]; + + if (!file || !(file instanceof File)) { + return; + } + const fileData = await getBase64Async(file); + const base64Data = fileData.split(',')[1]; + const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1]; + const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format); + + if (!message.extra) { + message.extra = {}; + } + + message.extra.image = imagePath; + message.extra.inline_image = true; + message.extra.title = ''; + appendImageToMessage(message, messageElement); + await context.saveChat(); + } +} + jQuery(function () { function addSendPictureButton() { const sendButton = $(` @@ -234,6 +312,12 @@ jQuery(function () { $('#extensionsMenu').prepend(sendButton); $(sendButton).hide(); $(sendButton).on('click', () => { + if (isImageInliningSupported()) { + console.log('Native image inlining is supported. Skipping captioning.'); + $('#embed_img_file').off('change').on('change', sendEmbeddedImage).trigger('click'); + return; + } + const hasCaptionModule = (modules.includes('caption') && extension_settings.caption.source === 'extras') || (extension_settings.caption.source === 'openai' && secret_state[SECRET_KEYS.OPENAI]) || @@ -249,10 +333,12 @@ jQuery(function () { }); } function addPictureSendForm() { - const inputHtml = ``; + const inputHtml = ``; + const embedInputHtml = ``; const imgForm = document.createElement('form'); imgForm.id = 'img_form'; $(imgForm).append(inputHtml); + $(imgForm).append(embedInputHtml); $(imgForm).hide(); $('#form_sheld').append(imgForm); $('#img_file').on('change', onSelectImage); @@ -312,5 +398,6 @@ jQuery(function () { extension_settings.caption.template = String($('#caption_template').val()); saveSettingsDebounced(); }); + $(document).on('click', '.mes_embed', onImageEmbedClicked); setInterval(moduleWorker, UPDATE_INTERVAL); }); diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index ff72640710..83c8086cdb 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -21,7 +21,7 @@ const defaultSettings = { //method from worldinfo async function updateQuickReplyPresetList() { - var result = await fetch("/getsettings", { + const result = await fetch("/getsettings", { method: "POST", headers: getRequestHeaders(), body: JSON.stringify({}), diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index dd964b55c6..c859fc2502 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -71,7 +71,7 @@ const triggerWords = { } const messageTrigger = { - activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.*\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the)?)?(.+)/i, + activationRegex: /\b(send|mail|imagine|generate|make|create|draw|paint|render)\b.*\b(pic|picture|image|drawing|painting|photo|photograph)\b(?:\s+of)?(?:\s+(?:a|an|the|this|that|those)?)?(.+)/i, specialCases: { [generationMode.CHARACTER]: ['you', 'yourself'], [generationMode.USER]: ['me', 'myself'], @@ -251,12 +251,12 @@ function processTriggers(chat, _, abort) { console.log(`SD: Triggered by "${message}", detected subject: ${subject}"`); - for (const [specialMode, triggers] of Object.entries(messageTrigger.specialCases)) { + outer: for (const [specialMode, triggers] of Object.entries(messageTrigger.specialCases)) { for (const trigger of triggers) { if (subject === trigger) { subject = triggerWords[specialMode][0]; console.log(`SD: Detected special case "${trigger}", switching to mode ${specialMode}`); - break; + break outer; } } } diff --git a/public/scripts/extensions/tts/elevenlabs.js b/public/scripts/extensions/tts/elevenlabs.js index 3e0c90fe2a..4cb7813fe4 100644 --- a/public/scripts/extensions/tts/elevenlabs.js +++ b/public/scripts/extensions/tts/elevenlabs.js @@ -45,6 +45,8 @@ class ElevenLabsTtsProvider { this.settings.stability = $('#elevenlabs_tts_stability').val() this.settings.similarity_boost = $('#elevenlabs_tts_similarity_boost').val() this.settings.model = $('#elevenlabs_tts_model').find(':selected').val() + $('#elevenlabs_tts_stability_output').text(this.settings.stability); + $('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost); saveTtsProviderSettings() } @@ -79,6 +81,8 @@ class ElevenLabsTtsProvider { $('#elevenlabs_tts_similarity_boost').on('input', this.onSettingsChange.bind(this)) $('#elevenlabs_tts_stability').on('input', this.onSettingsChange.bind(this)) $('#elevenlabs_tts_model').on('change', this.onSettingsChange.bind(this)) + $('#elevenlabs_tts_stability_output').text(this.settings.stability); + $('#elevenlabs_tts_similarity_boost_output').text(this.settings.similarity_boost); try { await this.checkReady() diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js index 0b22753d16..2ff01190af 100644 --- a/public/scripts/extensions/tts/index.js +++ b/public/scripts/extensions/tts/index.js @@ -9,6 +9,7 @@ import { SystemTtsProvider } from './system.js' import { NovelTtsProvider } from './novel.js' import { power_user } from '../../power-user.js' import { registerSlashCommand } from '../../slash-commands.js' +import { OpenAITtsProvider } from './openai.js' export { talkingAnimation }; const UPDATE_INTERVAL = 1000 @@ -73,6 +74,7 @@ let ttsProviders = { Coqui: CoquiTtsProvider, Edge: EdgeTtsProvider, Novel: NovelTtsProvider, + OpenAI: OpenAITtsProvider, } let ttsProvider let ttsProviderName diff --git a/public/scripts/extensions/tts/openai.js b/public/scripts/extensions/tts/openai.js new file mode 100644 index 0000000000..393a829402 --- /dev/null +++ b/public/scripts/extensions/tts/openai.js @@ -0,0 +1,148 @@ +import { getRequestHeaders } from "../../../script.js" +import { saveTtsProviderSettings } from "./index.js"; + +export { OpenAITtsProvider } + +class OpenAITtsProvider { + static voices = [ + { name: 'Alloy', voice_id: 'alloy', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/alloy.wav' }, + { name: 'Echo', voice_id: 'echo', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/echo.wav' }, + { name: 'Fable', voice_id: 'fable', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/fable.wav' }, + { name: 'Onyx', voice_id: 'onyx', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/onyx.wav' }, + { name: 'Nova', voice_id: 'nova', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/nova.wav' }, + { name: 'Shimmer', voice_id: 'shimmer', lang: 'en-US', preview_url: 'https://cdn.openai.com/API/docs/audio/shimmer.wav' }, + ]; + + settings + voices = [] + separator = ' . ' + audioElement = document.createElement('audio') + + defaultSettings = { + voiceMap: {}, + customVoices: [], + model: 'tts-1', + speed: 1, + } + + get settingsHtml() { + let html = ` +
Use OpenAI's TTS engine.
+ Hint: Save an API key in the OpenAI API settings to use it here. +
+ + +
+
+ + +
`; + return html; + } + + async loadSettings(settings) { + // Populate Provider UI given input settings + if (Object.keys(settings).length == 0) { + console.info("Using default TTS Provider settings") + } + + // Only accept keys defined in defaultSettings + this.settings = this.defaultSettings; + + for (const key in settings) { + if (key in this.settings) { + this.settings[key] = settings[key]; + } else { + throw `Invalid setting passed to TTS Provider: ${key}`; + } + } + + $('#openai-tts-model').val(this.settings.model); + $('#openai-tts-model').on('change', () => { + this.onSettingsChange(); + }); + + $('#openai-tts-speed').val(this.settings.speed); + $('#openai-tts-speed').on('input', () => { + this.onSettingsChange(); + }); + + $('#openai-tts-speed-output').text(this.settings.speed); + + await this.checkReady(); + console.debug("OpenAI TTS: Settings loaded"); + } + + onSettingsChange() { + // Update dynamically + this.settings.model = String($('#openai-tts-model').find(':selected').val()); + this.settings.speed = Number($('#openai-tts-speed').val()); + $('#openai-tts-speed-output').text(this.settings.speed); + saveTtsProviderSettings(); + } + + async checkReady() { + await this.fetchTtsVoiceObjects(); + } + + async onRefreshClick() { + return; + } + + async getVoice(voiceName) { + if (!voiceName) { + throw `TTS Voice name not provided` + } + + const voice = OpenAITtsProvider.voices.find(voice => voice.voice_id === voiceName || voice.name === voiceName); + + if (!voice) { + throw `TTS Voice not found: ${voiceName}` + } + + return voice; + } + + async generateTts(text, voiceId) { + const response = await this.fetchTtsGeneration(text, voiceId) + return response + } + + async fetchTtsVoiceObjects() { + return OpenAITtsProvider.voices; + } + + async previewTtsVoice(_) { + return; + } + + async fetchTtsGeneration(inputText, voiceId) { + console.info(`Generating new TTS for voice_id ${voiceId}`) + const response = await fetch(`/api/openai/generate-voice`, { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + "text": inputText, + "voice": voiceId, + "model": this.settings.model, + "speed": this.settings.speed, + }), + }); + + if (!response.ok) { + toastr.error(response.statusText, 'TTS Generation Failed'); + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + + return response; + } +} diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js index f2bc7ef739..8cab041fdd 100644 --- a/public/scripts/kai-settings.js +++ b/public/scripts/kai-settings.js @@ -31,6 +31,11 @@ export const kai_settings = { seed: -1, }; +/** + * Stable version of KoboldAI has a nasty payload validation. + * It will reject any payload that has a key that is not in the whitelist. + * @typedef {Object.} kai_flags + */ export const kai_flags = { can_use_tokenization: false, can_use_stop_sequence: false, @@ -38,6 +43,7 @@ export const kai_flags = { can_use_default_badwordsids: false, can_use_mirostat: false, can_use_grammar: false, + can_use_min_p: false, }; const defaultValues = Object.freeze(structuredClone(kai_settings)); @@ -48,6 +54,7 @@ const MIN_STREAMING_KCPPVERSION = '1.30'; const MIN_TOKENIZATION_KCPPVERSION = '1.41'; const MIN_MIROSTAT_KCPPVERSION = '1.35'; const MIN_GRAMMAR_KCPPVERSION = '1.44'; +const MIN_MIN_P_KCPPVERSION = '1.48'; const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5]; export function formatKoboldUrl(value) { @@ -114,7 +121,7 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon top_a: kai_settings.top_a, top_k: kai_settings.top_k, top_p: kai_settings.top_p, - min_p: kai_settings.min_p, + min_p: (kai_flags.can_use_min_p || isHorde) ? kai_settings.min_p : undefined, typical: kai_settings.typical, s1: sampler_order[0], s2: sampler_order[1], @@ -128,11 +135,11 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate) : undefined, streaming: kai_settings.streaming_kobold && kai_flags.can_use_streaming && type !== 'quiet', can_abort: kai_flags.can_use_streaming, - mirostat: kai_flags.can_use_mirostat ? kai_settings.mirostat : undefined, - mirostat_tau: kai_flags.can_use_mirostat ? kai_settings.mirostat_tau : undefined, - mirostat_eta: kai_flags.can_use_mirostat ? kai_settings.mirostat_eta : undefined, - use_default_badwordsids: kai_flags.can_use_default_badwordsids ? kai_settings.use_default_badwordsids : undefined, - grammar: kai_flags.can_use_grammar ? substituteParams(kai_settings.grammar) : undefined, + mirostat: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat : undefined, + mirostat_tau: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_tau : undefined, + mirostat_eta: (kai_flags.can_use_mirostat || isHorde) ? kai_settings.mirostat_eta : undefined, + use_default_badwordsids: (kai_flags.can_use_default_badwordsids || isHorde) ? kai_settings.use_default_badwordsids : undefined, + grammar: (kai_flags.can_use_grammar || isHorde) ? substituteParams(kai_settings.grammar) : undefined, sampler_seed: kai_settings.seed >= 0 ? kai_settings.seed : undefined, }; return generate_data; @@ -302,6 +309,7 @@ export function setKoboldFlags(version, koboldVersion) { kai_flags.can_use_default_badwordsids = canUseDefaultBadwordIds(version); kai_flags.can_use_mirostat = canUseMirostat(koboldVersion); kai_flags.can_use_grammar = canUseGrammar(koboldVersion); + kai_flags.can_use_min_p = canUseMinP(koboldVersion); } /** @@ -366,6 +374,17 @@ function canUseGrammar(koboldVersion) { } else return false; } +/** + * Determines if the Kobold min_p can be used with the given version. + * @param {{result:string, version:string;}} koboldVersion KoboldAI version object. + * @returns {boolean} True if the Kobold min_p can be used, false otherwise. + */ +function canUseMinP(koboldVersion) { + if (koboldVersion && koboldVersion.result == 'KoboldCpp') { + return (koboldVersion.version || '0.0').localeCompare(MIN_MIN_P_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1; + } else return false; +} + /** * Sorts the sampler items by the given order. * @param {any[]} orderArray Sampler order array. diff --git a/public/scripts/openai.js b/public/scripts/openai.js index ec5e712bf7..897850df99 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -5,62 +5,63 @@ */ import { - saveSettingsDebounced, - setOnlineStatus, - getExtensionPrompt, - name1, - name2, - extension_prompt_types, - characters, - this_chid, + abortStatusCheck, callPopup, + characters, + event_types, + eventSource, + extension_prompt_types, + Generate, + getExtensionPrompt, + getNextMessageId, getRequestHeaders, - system_message_types, - replaceBiasMarkup, + getStoppingStrings, is_send_press, - Generate, main_api, - eventSource, - event_types, - substituteParams, MAX_INJECTION_DEPTH, - getStoppingStrings, - getNextMessageId, + name1, + name2, + replaceBiasMarkup, replaceItemizedPromptText, - startStatusLoading, resultCheckStatus, - abortStatusCheck, + saveSettingsDebounced, + setOnlineStatus, + startStatusLoading, + substituteParams, + system_message_types, + this_chid, } from "../script.js"; import { groups, selected_group } from "./group-chats.js"; import { + chatCompletionDefaultPrompts, + INJECTION_POSITION, + Prompt, promptManagerDefaultPromptOrders, - chatCompletionDefaultPrompts, Prompt, PromptManagerModule as PromptManager, - INJECTION_POSITION, } from "./PromptManager.js"; -import { - getCustomStoppingStrings, - persona_description_positions, - power_user, -} from "./power-user.js"; -import { - SECRET_KEYS, - secret_state, - writeSecret, -} from "./secrets.js"; +import { getCustomStoppingStrings, persona_description_positions, power_user, } from "./power-user.js"; +import { SECRET_KEYS, secret_state, writeSecret, } from "./secrets.js"; import { delay, download, - getFileText, getSortableDelay, + getBase64Async, + getFileText, + getSortableDelay, + isDataURL, parseJsonFile, resetScrollHeight, stringFormat, } from "./utils.js"; import { countTokensOpenAI, getTokenizerModel } from "./tokenizers.js"; -import { formatInstructModeChat, formatInstructModeExamples, formatInstructModePrompt, formatInstructModeSystemPrompt } from "./instruct-mode.js"; +import { + formatInstructModeChat, + formatInstructModeExamples, + formatInstructModePrompt, + formatInstructModeSystemPrompt +} from "./instruct-mode.js"; export { openai_msgs, @@ -70,7 +71,6 @@ export { setOpenAIMessages, setOpenAIMessageExamples, setupChatCompletionPromptManager, - prepareOpenAIMessages, sendOpenAIRequest, getChatCompletionModel, TokenHandler, @@ -208,6 +208,8 @@ const default_settings = { openrouter_model: openrouter_website_model, openrouter_use_fallback: false, openrouter_force_instruct: false, + openrouter_group_models: false, + openrouter_sort_models: 'alphabetically', jailbreak_system: false, reverse_proxy: '', legacy_streaming: false, @@ -221,6 +223,8 @@ const default_settings = { exclude_assistant: false, use_alt_scale: false, squash_system_messages: false, + image_inlining: false, + bypass_status_check: false, }; const oai_settings = { @@ -254,6 +258,8 @@ const oai_settings = { openrouter_model: openrouter_website_model, openrouter_use_fallback: false, openrouter_force_instruct: false, + openrouter_group_models: false, + openrouter_sort_models: 'alphabetically', jailbreak_system: false, reverse_proxy: '', legacy_streaming: false, @@ -267,6 +273,8 @@ const oai_settings = { exclude_assistant: false, use_alt_scale: false, squash_system_messages: false, + image_inlining: false, + bypass_status_check: false, }; let openai_setting_names; @@ -409,7 +417,8 @@ function setOpenAIMessages(chat) { // Apply the "wrap in quotes" option if (role == 'user' && oai_settings.wrap_in_quotes) content = `"${content}"`; const name = chat[j]['name']; - openai_msgs[i] = { "role": role, "content": content, name: name }; + const image = chat[j]?.extra?.image; + openai_msgs[i] = { "role": role, "content": content, name: name, "image": image }; j++; } } @@ -592,7 +601,7 @@ export function isOpenRouterWithInstruct() { * @param type * @param cyclePrompt */ -function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = null) { +async function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = null) { chatCompletion.add(new MessageCollection('chatHistory'), prompts.index('chatHistory')); let names = (selected_group && groups.find(x => x.id === selected_group)?.members.map(member => characters.find(c => c.avatar === member)?.name).filter(Boolean).join(', ')) || ''; @@ -629,8 +638,13 @@ function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = chatCompletion.insert(message, 'chatHistory'); } + const imageInlining = isImageInliningSupported(); + // Insert chat messages as long as there is budget available - [...openai_msgs].reverse().every((chatPrompt, index) => { + const chatPool = [...openai_msgs].reverse(); + for (let index = 0; index < chatPool.length; index++) { + const chatPrompt = chatPool[index]; + // We do not want to mutate the prompt const prompt = new Prompt(chatPrompt); prompt.identifier = `chatHistory-${openai_msgs.length - index}`; @@ -641,10 +655,16 @@ function populateChatHistory(prompts, chatCompletion, type = null, cyclePrompt = chatMessage.setName(messageName); } - if (chatCompletion.canAfford(chatMessage)) chatCompletion.insertAtStart(chatMessage, 'chatHistory'); - else return false; - return true; - }); + if (imageInlining && chatPrompt.image) { + await chatMessage.addImage(chatPrompt.image); + } + + if (chatCompletion.canAfford(chatMessage)) { + chatCompletion.insertAtStart(chatMessage, 'chatHistory'); + } else { + break; + } + } // Insert and free new chat chatCompletion.freeBudget(newChatMessage); @@ -724,7 +744,7 @@ function getPromptPosition(position) { * @param {string} options.quietPrompt - Instruction prompt for extras * @param {string} options.type - The type of the chat, can be 'impersonate'. */ -function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt } = {}) { +async function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt } = {}) { // Helper function for preparing a prompt, that already exists within the prompt collection, for completion const addToChatCompletion = (source, target = null) => { // We need the prompts array to determine a position for the source. @@ -825,9 +845,9 @@ function populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, ty // Decide whether dialogue examples should always be added if (power_user.pin_examples) { populateDialogueExamples(prompts, chatCompletion); - populateChatHistory(prompts, chatCompletion, type, cyclePrompt); + await populateChatHistory(prompts, chatCompletion, type, cyclePrompt); } else { - populateChatHistory(prompts, chatCompletion, type, cyclePrompt); + await populateChatHistory(prompts, chatCompletion, type, cyclePrompt); populateDialogueExamples(prompts, chatCompletion); } @@ -969,7 +989,7 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor * @param dryRun - Whether this is a live call or not. * @returns {(*[]|boolean)[]} An array where the first element is the prepared chat and the second element is a boolean flag. */ -function prepareOpenAIMessages({ +export async function prepareOpenAIMessages({ name2, charDescription, charPersonality, @@ -1012,7 +1032,7 @@ function prepareOpenAIMessages({ }); // Fill the chat completion with as much context as the budget allows - populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt }); + await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, type, cyclePrompt }); } catch (error) { if (error instanceof TokenBudgetExceededError) { toastr.error('An error occurred while counting tokens: Token budget exceeded.') @@ -1224,18 +1244,16 @@ function saveModelList(data) { model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id)); if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { + model_list = openRouterSortBy(model_list, oai_settings.openrouter_sort_models); + $('#model_openrouter_select').empty(); - $('#model_openrouter_select').append($('`); + + models.forEach((model) => { + appendOption(model, optgroup); + }); + + $('#model_openrouter_select').append(optgroup); + }); + } else { + model_list.forEach((model) => { + appendOption(model); + }); + } +} + +const openRouterSortBy = (data, property = 'alphabetically') => { + return data.sort((a, b) => { + if (property === 'context_length') { + return b.context_length - a.context_length; + } else if (property === 'pricing.prompt') { + return parseFloat(a.pricing.prompt) - parseFloat(b.pricing.prompt); + } else { + // Alphabetically + return a?.id && b?.id && a.id.localeCompare(b.id); + } + }); +}; + +function openRouterGroupByVendor(array) { + return array.reduce((acc, curr) => { + const vendor = curr.id.split('/')[0]; + + if (!acc.has(vendor)) { + acc.set(vendor, []); + } + + acc.get(vendor).push(curr); + + return acc; + }, new Map()); +} + async function sendAltScaleRequest(openai_msgs_tosend, logit_bias, signal, type) { const generate_url = '/generate_altscale'; @@ -1372,6 +1451,16 @@ async function sendOpenAIRequest(type, openai_msgs_tosend, signal) { "stop": getCustomStoppingStrings(openai_max_stop_strings), }; + // Empty array will produce a validation error + if (!Array.isArray(generate_data.stop) || !generate_data.stop.length) { + delete generate_data.stop; + } + + // Vision models don't support logit bias + if (isImageInliningSupported()) { + delete generate_data.logit_bias; + } + // Proxy is only supported for Claude and OpenAI if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI].includes(oai_settings.chat_completion_source)) { validateReverseProxy(); @@ -1640,7 +1729,18 @@ class InvalidCharacterNameError extends Error { * Used for creating, managing, and interacting with a specific message object. */ class Message { - tokens; identifier; role; content; name; + static tokensPerImage = 85; + + /** @type {number} */ + tokens; + /** @type {string} */ + identifier; + /** @type {string} */ + role; + /** @type {string|any[]} */ + content; + /** @type {string} */ + name; /** * @constructor @@ -1665,6 +1765,30 @@ class Message { this.tokens = tokenHandler.count({ role: this.role, content: this.content, name: this.name }); } + async addImage(image) { + const textContent = this.content; + const isDataUrl = isDataURL(image); + + if (!isDataUrl) { + try { + const response = await fetch(image, { method: 'GET', cache: 'force-cache' }); + if (!response.ok) throw new Error('Failed to fetch image'); + const blob = await response.blob(); + image = await getBase64Async(blob); + } catch (error) { + console.error('Image adding skipped', error); + return; + } + } + + this.content = [ + { type: "text", text: textContent }, + { type: "image_url", image_url: { "url": image, "detail": "low" } }, + ]; + + this.tokens += Message.tokensPerImage; + } + /** * Create a new Message instance from a prompt. * @static @@ -2140,6 +2264,8 @@ function loadOpenAISettings(data, settings) { oai_settings.claude_model = settings.claude_model ?? default_settings.claude_model; oai_settings.windowai_model = settings.windowai_model ?? default_settings.windowai_model; oai_settings.openrouter_model = settings.openrouter_model ?? default_settings.openrouter_model; + oai_settings.openrouter_group_models = settings.openrouter_group_models ?? default_settings.openrouter_group_models; + oai_settings.openrouter_sort_models = settings.openrouter_sort_models ?? default_settings.openrouter_sort_models; oai_settings.openrouter_use_fallback = settings.openrouter_use_fallback ?? default_settings.openrouter_use_fallback; oai_settings.openrouter_force_instruct = settings.openrouter_force_instruct ?? default_settings.openrouter_force_instruct; oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model; @@ -2148,6 +2274,8 @@ function loadOpenAISettings(data, settings) { oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models; oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password; oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill; + oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining; + oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check; oai_settings.prompts = settings.prompts ?? default_settings.prompts; oai_settings.prompt_order = settings.prompt_order ?? default_settings.prompt_order; @@ -2168,6 +2296,8 @@ function loadOpenAISettings(data, settings) { $('#api_url_scale').val(oai_settings.api_url_scale); $('#openai_proxy_password').val(oai_settings.proxy_password); $('#claude_assistant_prefill').val(oai_settings.assistant_prefill); + $('#openai_image_inlining').prop('checked', oai_settings.image_inlining); + $('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check); $('#model_openai_select').val(oai_settings.openai_model); $(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true); @@ -2180,6 +2310,7 @@ function loadOpenAISettings(data, settings) { $('#openai_max_context').val(oai_settings.openai_max_context); $('#openai_max_context_counter').val(`${oai_settings.openai_max_context}`); $('#model_openrouter_select').val(oai_settings.openrouter_model); + $('#openrouter_sort_models').val(oai_settings.openrouter_sort_models); $('#openai_max_tokens').val(oai_settings.openai_max_tokens); @@ -2194,6 +2325,7 @@ function loadOpenAISettings(data, settings) { $('#scale-alt').prop('checked', oai_settings.use_alt_scale); $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback); $('#openrouter_force_instruct').prop('checked', oai_settings.openrouter_force_instruct); + $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models); $('#squash_system_messages').prop('checked', oai_settings.squash_system_messages); if (settings.impersonation_prompt !== undefined) oai_settings.impersonation_prompt = settings.impersonation_prompt; @@ -2277,6 +2409,11 @@ async function getStatusOpen() { validateReverseProxy(); } + const canBypass = oai_settings.chat_completion_source === chat_completion_sources.OPENAI && oai_settings.bypass_status_check; + if (canBypass) { + setOnlineStatus('Status check bypassed'); + } + try { const response = await fetch('/getstatus_openai', { method: 'POST', @@ -2292,14 +2429,18 @@ async function getStatusOpen() { const responseData = await response.json(); - if (!('error' in responseData)) + if (!('error' in responseData)) { setOnlineStatus('Valid'); + } if ('data' in responseData && Array.isArray(responseData.data)) { saveModelList(responseData.data); } } catch (error) { console.error(error); - setOnlineStatus('no_connection'); + + if (!canBypass) { + setOnlineStatus('no_connection'); + } } return resultCheckStatus(); @@ -2353,6 +2494,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { openrouter_model: settings.openrouter_model, openrouter_use_fallback: settings.openrouter_use_fallback, openrouter_force_instruct: settings.openrouter_force_instruct, + openrouter_group_models: settings.openrouter_group_models, + openrouter_sort_models: settings.openrouter_sort_models, ai21_model: settings.ai21_model, temperature: settings.temp_openai, frequency_penalty: settings.freq_pen_openai, @@ -2388,6 +2531,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { exclude_assistant: settings.exclude_assistant, use_alt_scale: settings.use_alt_scale, squash_system_messages: settings.squash_system_messages, + image_inlining: settings.image_inlining, }; const savePresetSettings = await fetch(`/api/presets/save-openai?name=${name}`, { @@ -2715,6 +2859,8 @@ function onSettingsPresetChange() { openrouter_model: ['#model_openrouter_select', 'openrouter_model', false], openrouter_use_fallback: ['#openrouter_use_fallback', 'openrouter_use_fallback', true], openrouter_force_instruct: ['#openrouter_force_instruct', 'openrouter_force_instruct', true], + openrouter_group_models: ['#openrouter_group_models', 'openrouter_group_models', false], + openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false], ai21_model: ['#model_ai21_select', 'ai21_model', false], openai_max_context: ['#openai_max_context', 'openai_max_context', false], openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false], @@ -2741,6 +2887,7 @@ function onSettingsPresetChange() { exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true], use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true], squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true], + image_inlining: ['#openai_image_inlining', 'image_inlining', true], }; const presetName = $('#settings_preset_openai').find(":selected").text(); @@ -2785,6 +2932,9 @@ function getMaxContextOpenAI(value) { else if (value.includes('gpt-4-1106')) { return max_128k; } + else if (value.includes('gpt-4-vision')) { + return max_128k; + } else if (value.includes('gpt-3.5-turbo-1106')) { return max_16k; } @@ -2831,6 +2981,9 @@ function getMaxContextWindowAI(value) { else if (value.includes('gpt-4-1106')) { return max_128k; } + else if (value.includes('gpt-4-vision')) { + return max_128k; + } else if (value.includes('gpt-4-32k')) { return max_32k; } @@ -3016,6 +3169,10 @@ async function onModelChange() { eventSource.emit(event_types.CHATCOMPLETION_MODEL_CHANGED, value); } +async function onOpenrouterModelSortChange() { + await getStatusOpen(); +} + async function onNewPresetClick() { const popupText = `

Preset name:

@@ -3217,6 +3374,31 @@ function updateScaleForm() { } } +/** + * Check if the model supports image inlining + * @returns {boolean} True if the model supports image inlining + */ +export function isImageInliningSupported() { + if (main_api !== 'openai') { + return false; + } + + const modelId = 'gpt-4-vision'; + + if (!oai_settings.image_inlining) { + return false; + } + + switch (oai_settings.chat_completion_source) { + case chat_completion_sources.OPENAI: + return oai_settings.openai_model.includes(modelId); + case chat_completion_sources.OPENROUTER: + return oai_settings.openrouter_model.includes(modelId); + default: + return false; + } +} + $(document).ready(async function () { $('#test_api_button').on('click', testApiConnection); @@ -3409,6 +3591,12 @@ $(document).ready(async function () { saveSettingsDebounced(); }); + $('#openai_bypass_status_check').on('input', function () { + oai_settings.bypass_status_check = !!$(this).prop('checked'); + getStatusOpen(); + saveSettingsDebounced(); + }) + $('#chat_completion_source').on('change', function () { oai_settings.chat_completion_source = String($(this).find(":selected").val()); toggleChatCompletionForms(); @@ -3458,11 +3646,26 @@ $(document).ready(async function () { saveSettingsDebounced(); }); + $('#openrouter_group_models').on('input', function () { + oai_settings.openrouter_group_models = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $('#openrouter_sort_models').on('input', function () { + oai_settings.openrouter_sort_models = String($(this).val()); + saveSettingsDebounced(); + }); + $('#squash_system_messages').on('input', function () { oai_settings.squash_system_messages = !!$(this).prop('checked'); saveSettingsDebounced(); }); + $('#openai_image_inlining').on('input', function () { + oai_settings.image_inlining = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + $(document).on('input', '#openai_settings .autoSetHeight', function () { resetScrollHeight($(this)); }); @@ -3475,6 +3678,8 @@ $(document).ready(async function () { $("#model_scale_select").on("change", onModelChange); $("#model_palm_select").on("change", onModelChange); $("#model_openrouter_select").on("change", onModelChange); + $("#openrouter_group_models").on("change", onOpenrouterModelSortChange); + $("#openrouter_sort_models").on("change", onOpenrouterModelSortChange); $("#model_ai21_select").on("change", onModelChange); $("#settings_preset_openai").on("change", onSettingsPresetChange); $("#new_oai_preset").on("click", onNewPresetClick); diff --git a/public/scripts/personas.js b/public/scripts/personas.js index 8a3f0341d0..86cbc78260 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -1,7 +1,3 @@ -/** - * This is a placeholder file for all the Persona Management code. Will be refactored into a separate file soon. - */ - import { callPopup, characters, chat_metadata, default_avatar, eventSource, event_types, getRequestHeaders, getThumbnailUrl, getUserAvatars, name1, saveMetadata, saveSettingsDebounced, setUserName, this_chid, user_avatar } from "../script.js"; import { persona_description_positions, power_user } from "./power-user.js"; import { getTokenCount } from "./tokenizers.js"; @@ -38,6 +34,28 @@ async function uploadUserAvatar(url, name) { }); } +/** + * Prompts the user to create a persona for the uploaded avatar. + * @param {string} avatarId User avatar id + * @returns {Promise} Promise that resolves when the persona is set + */ +export async function createPersona(avatarId) { + const personaName = await callPopup('

Enter a name for this persona:

Cancel if you\'re just uploading an avatar.', 'input', ''); + + if (!personaName) { + console.debug('User cancelled creating a persona'); + return; + } + + await delay(500); + const personaDescription = await callPopup('

Enter a description for this persona:

You can always add or change it later.', 'input', '', { rows: 4 }); + + initPersona(avatarId, personaName, personaDescription); + if (power_user.persona_show_notifications) { + toastr.success(`You can now pick ${personaName} as a persona in the Persona Management menu.`, 'Persona Created'); + } +} + async function createDummyPersona() { const personaName = await callPopup('

Enter a name for this persona:

', 'input', ''); @@ -48,18 +66,28 @@ async function createDummyPersona() { // Date + name (only ASCII) to make it unique const avatarId = `${Date.now()}-${personaName.replace(/[^a-zA-Z0-9]/g, '')}.png`; + initPersona(avatarId, personaName, ''); + await uploadUserAvatar(default_avatar, avatarId); +} + +/** + * Initializes a persona for the given avatar id. + * @param {string} avatarId User avatar id + * @param {string} personaName Name for the persona + * @param {string} personaDescription Optional description for the persona + * @returns {void} + */ +export function initPersona(avatarId, personaName, personaDescription) { power_user.personas[avatarId] = personaName; power_user.persona_descriptions[avatarId] = { - description: '', + description: personaDescription || '', position: persona_description_positions.IN_PROMPT, }; - await uploadUserAvatar(default_avatar, avatarId); saveSettingsDebounced(); } export async function convertCharacterToPersona(characterId = null) { - if (null === characterId) characterId = this_chid; const avatarUrl = characters[characterId]?.avatar; diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 51f1e1c091..238b8cbbda 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -220,6 +220,7 @@ let power_user = { encode_tags: false, servers: [], bogus_folders: false, + aux_field: 'character_version', }; let themes = []; @@ -432,10 +433,12 @@ var originalSliderValues = [] async function switchLabMode() { - if (power_user.enableZenSliders) { - //force disable ZenSliders for Lab Mode - $("#enableZenSliders").trigger('click') - } + /* if (power_user.enableZenSliders && power_user.enableLabMode) { + toastr.warning("Can't start Lab Mode while Zen Sliders are active") + return + //$("#enableZenSliders").trigger('click') + } + */ await delay(100) const value = localStorage.getItem(storage_keys.enableLabMode); power_user.enableLabMode = value === null ? false : value == "true"; @@ -457,7 +460,7 @@ async function switchLabMode() { .attr('min', '-99999') .attr('max', '99999') .attr('step', '0.001') - $("#labModeWarning").show() + $("#labModeWarning").removeClass('displayNone') //$("#advanced-ai-config-block input[type='range']").hide() } else { @@ -470,20 +473,17 @@ async function switchLabMode() { .trigger('input') }); $("#advanced-ai-config-block input[type='range']").show() - $("#labModeWarning").hide() + $("#labModeWarning").addClass('displayNone') } } async function switchZenSliders() { - await delay(100) const value = localStorage.getItem(storage_keys.enableZenSliders); power_user.enableZenSliders = value === null ? false : value == "true"; $("body").toggleClass("enableZenSliders", power_user.enableZenSliders); $("#enableZenSliders").prop("checked", power_user.enableZenSliders); - - if (power_user.enableZenSliders) { $("#clickSlidersTips").hide() $("#pro-settings-block input[type='number']").hide(); @@ -526,6 +526,7 @@ async function switchZenSliders() { var sliderRange = sliderMax - sliderMin var numSteps = 10 var decimals = 2 + var offVal if (sliderID == 'amount_gen') { decimals = 0 @@ -537,58 +538,107 @@ async function switchZenSliders() { sliderValue = steps.indexOf(Number(sliderValue)) if (sliderValue === -1) { sliderValue = 4 } // default to '200' if origSlider has value we can't use } - if (sliderID == 'max_context') { - numSteps = 15 + //customize decimals + if (sliderID == 'max_context' || + sliderID == 'mirostat_mode_textgenerationwebui' || + sliderID == 'mirostat_tau_textgenerationwebui' || + sliderID == 'top_k_textgenerationwebui' || + sliderID == 'num_beams_textgenerationwebui' || + sliderID == 'no_repeat_ngram_size_textgenerationwebui' || + sliderID == 'min_length_textgenerationwebui' || + sliderID == 'top_k' || + sliderID == 'mirostat_mode_kobold' || + sliderID == 'rep_pen_range') { decimals = 0 } + if (sliderID == 'eta_cutoff_textgenerationwebui' || + sliderID == 'epsilon_cutoff_textgenerationwebui') { + numSteps = 50 + decimals = 1 + } - if (sliderID == 'rep_pen_range_textgenerationwebui') { - numSteps = 16 - decimals = 0 + //customize steps + if (sliderID == 'mirostat_mode_textgenerationwebui' || + sliderID == 'mirostat_mode_kobold') { + numSteps = 2 } if (sliderID == 'encoder_rep_pen_textgenerationwebui') { numSteps = 14 } - if (sliderID == 'mirostat_mode_textgenerationwebui') { - numSteps = 2 - decimals = 0 + if (sliderID == 'max_context') { + numSteps = 15 + } + if (sliderID == 'rep_pen_range_textgenerationwebui') { + numSteps = 16 } if (sliderID == 'mirostat_tau_textgenerationwebui' || sliderID == 'top_k_textgenerationwebui' || sliderID == 'num_beams_textgenerationwebui' || - sliderID == 'no_repeat_ngram_size_textgenerationwebui') { + sliderID == 'no_repeat_ngram_size_textgenerationwebui' || + sliderID == 'epsilon_cutoff_textgenerationwebui' || + sliderID == 'tfs_textgenerationwebui' || + sliderID == 'min_p_textgenerationwebui' || + sliderID == 'temp_textgenerationwebui' || + sliderID == 'temp') { numSteps = 20 - decimals = 0 } - if (sliderID == 'epsilon_cutoff_textgenerationwebui') { - numSteps = 20 - decimals = 1 - } - if (sliderID == 'tfs_textgenerationwebui' || - sliderID == 'min_p_textgenerationwebui') { - numSteps = 20 - decimals = 2 - } - if (sliderID == 'mirostat_eta_textgenerationwebui' || sliderID == 'penalty_alpha_textgenerationwebui' || sliderID == 'length_penalty_textgenerationwebui') { numSteps = 50 } - if (sliderID == 'eta_cutoff_textgenerationwebui') { - numSteps = 50 - decimals = 1 + + //customize off values + if (sliderID == 'presence_pen_textgenerationwebui' || + sliderID == 'freq_pen_textgenerationwebui' || + sliderID == 'mirostat_mode_textgenerationwebui' || + sliderID == 'mirostat_mode_kobold' || + sliderID == 'mirostat_tau_textgenerationwebui' || + sliderID == 'mirostat_tau_kobold' || + sliderID == 'mirostat_eta_textgenerationwebui' || + sliderID == 'mirostat_eta_kobold' || + sliderID == 'min_p_textgenerationwebui' || + sliderID == 'min_p' || + sliderID == 'no_repeat_ngram_size_textgenerationwebui' || + sliderID == 'penalty_alpha_textgenerationwebui' || + sliderID == 'length_penalty_textgenerationwebui' || + sliderID == 'epsilon_cutoff_textgenerationwebui' || + sliderID == 'rep_pen_range_textgenerationwebui' || + sliderID == 'rep_pen_range' || + sliderID == 'eta_cutoff_textgenerationwebui' || + sliderID == 'top_a_textgenerationwebui' || + sliderID == 'top_a' || + sliderID == 'top_k_textgenerationwebui' || + sliderID == 'top_k' || + sliderID == 'rep_pen_slope' || + sliderID == 'min_length_textgenerationwebui') { + offVal = 0 } + + if (sliderID == 'rep_pen_textgenerationwebui' || + sliderID == 'rep_pen' || + sliderID == 'tfs_textgenerationwebui' || + sliderID == 'tfs' || + sliderID == 'top_p_textgenerationwebui' || + sliderID == 'top_p' || + sliderID == 'num_beams_textgenerationwebui' || + sliderID == 'typical_p_textgenerationwebui' || + sliderID == 'typical_p' || + sliderID == 'encoder_rep_pen_textgenerationwebui' || + sliderID == 'temp_textgenerationwebui' || + sliderID == 'temp' || + sliderID == 'guidance_scale_textgenerationwebui' || + sliderID == 'guidance_scale') { + offVal = 1 + } + + + if (sliderID == 'guidance_scale_textgenerationwebui') { numSteps = 78 } - if (sliderID == 'min_length_textgenerationwebui') { - decimals = 0 - } - if (sliderID == 'temp_textgenerationwebui') { - numSteps = 20 - } + //customize amt gen steps if (sliderID !== 'amount_gen') { var stepScale = sliderRange / numSteps } @@ -605,60 +655,124 @@ async function switchZenSliders() { max: sliderMax, create: function () { var handle = $(this).find(".ui-slider-handle"); + //handling creaetion of amt_gen if (newSlider.attr('id') == 'amount_gen_zenslider') { - //console.log(sliderValue, steps.indexOf(Number(sliderValue))) var handleText = steps[sliderValue] - handle.text(handleText); - //console.log(handleText) var stepNumber = sliderValue var leftMargin = ((stepNumber) / numSteps) * 50 * -1 - //console.log(`initial value:${handleText}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`) - handle.css('margin-left', `${leftMargin}px`) + handle.text(handleText) + .css('margin-left', `${leftMargin}px`) + console.log(`initial value:${handleText}, stepNum:${stepNumber}, numSteps:${numSteps}, left-margin:${leftMargin}`) } else { - - var handleText = Number(sliderValue).toFixed(decimals) - handle.text(handleText); + //handling creation for all other sliders + var numVal = Number(sliderValue).toFixed(decimals) + offVal = Number(offVal).toFixed(decimals) + console.log(`${sliderID} ON LOAD OFFVAL ${offVal}`) + if (numVal === offVal) { + handle.text('Off').css('color', 'rgba(128,128,128,0.5'); + } else { + handle.text(numVal).css('color', ''); + } var stepNumber = ((sliderValue - sliderMin) / stepScale) var leftMargin = (stepNumber / numSteps) * 50 * -1 + var isManualInput = false + var valueBeforeManualInput handle.css('margin-left', `${leftMargin}px`) + .attr('contenteditable', 'true') + .on('click', function () { + //this just selects all the text in the handle so user can overwrite easily + //needed because JQUery UI uses left/right arrow keys as well as home/end to move the slider.. + valueBeforeManualInput = newSlider.val() + console.log(valueBeforeManualInput) + let handleElement = handle.get(0); + let range = document.createRange(); + range.selectNodeContents(handleElement); + let selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }) + .on('keyup', function () { + valueBeforeManualInput = newSlider.val() + console.log(valueBeforeManualInput) + isManualInput = true + }) + //trigger slider changes when user clicks away + .on('mouseup blur', function () { + let manualInput = parseFloat(handle.text()).toFixed(decimals) + if (isManualInput) { + //disallow manual inputs outside acceptable range + if (manualInput >= sliderMin && manualInput <= sliderMax) { + //if value is ok, assign to slider and update handle text and position + newSlider.val(manualInput) + handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual'); + valueBeforeManualInput = manualInput + } else { + //if value not ok, warn and reset to last known valid value + toastr.warning(`Invalid value. Must be between ${sliderMin} and ${sliderMax}`) + console.log(valueBeforeManualInput) + newSlider.val(valueBeforeManualInput) + handle.text(valueBeforeManualInput) + } + } + isManualInput = false + }) console.debug(sliderID, sliderValue, handleText, stepNumber, stepScale) } }, - slide: function (event, ui) { - var handle = $(this).find(".ui-slider-handle"); - if (newSlider.attr('id') == 'amount_gen_zenslider') { - //console.log(`stepScale${stepScale}, UIvalue:${ui.value}, mappedValue:${steps[ui.value]}`) - $(this).val(steps[ui.value]) - let handleText = steps[ui.value].toFixed(decimals) - handle.text(handleText); - var stepNumber = steps.indexOf(Number(handleText)) - var leftMargin = (stepNumber / numSteps) * 50 * -1 - //console.log(`handleText:${handleText},stepNum:${stepNumber}, numSteps:${numSteps},LeftMargin:${leftMargin}`) - handle.css('margin-left', `${leftMargin}px`) - originalSlider.val(handleText); - originalSlider.trigger('input') - originalSlider.trigger('change') - } else { - handle.text(ui.value.toFixed(decimals)); - var stepNumber = ((ui.value - sliderMin) / stepScale) - var leftMargin = (stepNumber / numSteps) * 50 * -1 - handle.css('margin-left', `${leftMargin}px`) - let handleText = (ui.value) - originalSlider.val(handleText); - originalSlider.trigger('input') - originalSlider.trigger('change') - } + slide: handleSlideEvent + }); + function handleSlideEvent(event, ui, type) { + var handle = $(this).find(".ui-slider-handle"); + var numVal = Number(ui.value).toFixed(decimals); + offVal = Number(offVal).toFixed(decimals); + var stepNumber = ((ui.value - sliderMin) / stepScale); + var handleText = (ui.value); + var leftMargin = (stepNumber / numSteps) * 50 * -1; + var percentOfMax = Number((ui.value / sliderMax)) //what % our value is of the max + var perStepPercent = 1 / numSteps //how far in % each step should be on the slider + var leftPos = newSlider.width() * (stepNumber * perStepPercent) //how big of a left margin to give the slider for manual inputs + + console.log(` + numVal: ${numVal}, + offVal: ${offVal}, + initial value: ${handleText}, + stepNum: ${stepNumber}, + numSteps: ${numSteps}, + left-margin: ${leftMargin}, + width: ${newSlider.width()} + percent of max: ${percentOfMax} + left: ${leftPos}`) + + //special handling for response length slider, pulls text aliases for step values from an array + if (newSlider.attr('id') == 'amount_gen_zenslider') { + handleText = steps[stepNumber] + handle.text(handleText); + newSlider.val(stepNumber) } - - }); + //everything else uses the flat slider value + else { + //show 'off' if disabled value is set + if (numVal === offVal) { handle.text('Off').css('color', 'rgba(128,128,128,0.5'); } + else { handle.text(ui.value.toFixed(decimals)).css('color', ''); } + newSlider.val(handleText) + } + //for manually typed-in values we must adjust left position because JQUI doesn't do it for us + if (type === 'manual') { handle.css('left', leftPos) } + //adjust a negative left margin to avoid overflowing right side of slider body + handle.css('margin-left', `${leftMargin}px`); + + originalSlider.val(handleText); + originalSlider.trigger('input'); + originalSlider.trigger('change'); + } originalSlider.data("newSlider", newSlider); + await delay(1) originalSlider.hide(); }; } - function switchUiMode() { const fastUi = localStorage.getItem(storage_keys.fast_ui_mode); power_user.fast_ui_mode = fastUi === null ? true : fastUi == "true"; @@ -1255,6 +1369,7 @@ function loadPowerUserSettings(settings, data) { $(`#chat_display option[value=${power_user.chat_display}]`).attr("selected", true).trigger('change'); $('#chat_width_slider').val(power_user.chat_width); $("#token_padding").val(power_user.token_padding); + $("#aux_field").val(power_user.aux_field); $("#font_scale").val(power_user.font_scale); $("#font_scale_counter").val(power_user.font_scale); @@ -1355,16 +1470,17 @@ function loadMaxContextUnlocked() { } function switchMaxContextSize() { - const elements = [$('#max_context'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')]; + const elements = [$('#max_context'), $('#max_context_counter'), $('#rep_pen_range'), $('#rep_pen_range_textgenerationwebui')]; const maxValue = power_user.max_context_unlocked ? MAX_CONTEXT_UNLOCKED : MAX_CONTEXT_DEFAULT; const minValue = power_user.max_context_unlocked ? maxContextMin : maxContextMin; const steps = power_user.max_context_unlocked ? unlockedMaxContextStep : maxContextStep; for (const element of elements) { + const id = element.attr('id'); element.attr('max', maxValue); element.attr('step', steps); - if (element.attr('id') == 'max_context') { + if (typeof id === 'string' && id?.indexOf('max_context') !== -1) { element.attr('min', minValue); } const value = Number(element.val()); @@ -1550,7 +1666,7 @@ export function fuzzySearchWorldInfo(data, searchValue) { export function fuzzySearchTags(searchValue) { const fuse = new Fuse(tags, { keys: [ - { name: 'name', weight: 1}, + { name: 'name', weight: 1 }, ], includeScore: true, ignoreLocation: true, @@ -2690,28 +2806,31 @@ $(document).ready(() => { }); $("#enableZenSliders").on("input", function () { - if (power_user.enableLabMode) { + const value = !!$(this).prop('checked'); + if (power_user.enableLabMode === true && value === true) { //disallow zenSliders while Lab Mode is active - toastr.warning('ZenSliders not allowed in Mad Lab Mode') - $(this).prop('checked', false); + toastr.warning('Disable Mad Lab Mode before enabling Zen Sliders') + $(this).prop('checked', false).trigger('input'); return } - const value = !!$(this).prop('checked'); power_user.enableZenSliders = value; localStorage.setItem(storage_keys.enableZenSliders, Boolean(power_user.enableZenSliders)); + saveSettingsDebounced(); switchZenSliders(); }); $("#enableLabMode").on("input", function () { - if (power_user.enableZenSliders) { + const value = !!$(this).prop('checked'); + if (power_user.enableZenSliders === true && value === true) { //disallow Lab Mode if ZenSliders are active - toastr.warning('Mad Lab Mode not allowed while ZenSliders are active') - $(this).prop('checked', false); + toastr.warning('Disable Zen Sliders before enabling Mad Lab Mode') + $(this).prop('checked', false).trigger('input');; return } - const value = !!$(this).prop('checked'); + power_user.enableLabMode = value; localStorage.setItem(storage_keys.enableLabMode, Boolean(power_user.enableLabMode)); + saveSettingsDebounced(); switchLabMode(); }); @@ -2825,13 +2944,20 @@ $(document).ready(() => { switchSimpleMode(); }); - $('#bogus_folders').on('input', function() { + $('#bogus_folders').on('input', function () { const value = !!$(this).prop('checked'); power_user.bogus_folders = value; saveSettingsDebounced(); printCharacters(true); }); + $('#aux_field').on('change', function () { + const value = $(this).find(':selected').val(); + power_user.aux_field = String(value); + saveSettingsDebounced(); + printCharacters(false); + }); + $(document).on('click', '#debug_table [data-debug-function]', function () { const functionId = $(this).data('debug-function'); const functionRecord = debug_functions.find(f => f.functionId === functionId); diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 7eee1fc8c4..7909e542c5 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -227,7 +227,7 @@ async function loadWorldInfoData(name) { } async function updateWorldInfoList() { - var result = await fetch("/getsettings", { + const result = await fetch("/getsettings", { method: "POST", headers: getRequestHeaders(), body: JSON.stringify({}), @@ -269,7 +269,15 @@ function sortEntries(data) { const sortRule = option.data('rule'); const orderSign = sortOrder === 'asc' ? 1 : -1; - if (sortRule === 'priority') { + if (sortRule === 'custom') { + // First by display index, then by order, then by uid + data.sort((a, b) => { + const aValue = a.displayIndex; + const bValue = b.displayIndex; + + return (aValue - bValue || b.order - a.order || a.uid - b.uid); + }); + } else if (sortRule === 'priority') { // First constant, then normal, then disabled. Then sort by order data.sort((a, b) => { const aValue = a.constant ? 0 : a.disable ? 2 : 1; @@ -375,7 +383,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { nextText: '>', formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, - callback: function (page) { + callback: function (/** @type {object[]} */ page) { $("#world_popup_entries_list").empty(); const keywordHeaders = `
@@ -399,6 +407,12 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
` const blocks = page.map(entry => getWorldEntry(name, data, entry)).filter(x => x); + const isCustomOrder = $('#world_info_sort_order').find(':selected').data('rule') === 'custom'; + if (!isCustomOrder) { + blocks.forEach(block => { + block.find('.drag-handle').remove(); + }); + } $("#world_popup_entries_list").append(keywordHeaders); $("#world_popup_entries_list").append(blocks); }, @@ -500,6 +514,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { delay: getSortableDelay(), handle: ".drag-handle", stop: async function (event, ui) { + const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid'); + const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0; $('#world_popup_entries_list .world_entry').each(function (index) { const uid = $(this).data('uid'); @@ -511,8 +527,8 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { return; } - item.displayIndex = index; - setOriginalDataValue(data, uid, 'extensions.display_index', index); + item.displayIndex = minDisplayIndex + index; + setOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex); }); console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex }))); @@ -587,7 +603,7 @@ function getWorldEntry(name, data, entry) { setOriginalDataValue(data, uid, "keys", data.entries[uid].key); saveWorldInfo(name, data); }); - keyInput.val(entry.key.join(",")).trigger("input"); + keyInput.val(entry.key.join(", ")).trigger("input"); //initScrollHeight(keyInput); // logic AND/NOT @@ -708,7 +724,7 @@ function getWorldEntry(name, data, entry) { saveWorldInfo(name, data); }); - keySecondaryInput.val(entry.keysecondary.join(",")).trigger("input"); + keySecondaryInput.val(entry.keysecondary.join(", ")).trigger("input"); initScrollHeight(keySecondaryInput); // comment @@ -1582,9 +1598,7 @@ async function checkWorldInfo(chat, maxContext) { over_max = ( world_info_min_activations_depth_max > 0 && minActivationMsgIndex > world_info_min_activations_depth_max - ) || ( - minActivationMsgIndex >= chat.length - ) + ) || (minActivationMsgIndex >= chat.length) if (!over_max) { needsToScan = true textToScan = transformString(chat.slice(minActivationMsgIndex, minActivationMsgIndex + 1).join("")); @@ -2167,11 +2181,9 @@ jQuery(() => { updateEditor(navigation_option.previous); }); - $('#world_info_sort_order').on('change', function (e) { - if (e.target instanceof HTMLOptionElement) { - localStorage.setItem(SORT_ORDER_KEY, e.target.value); - } - + $('#world_info_sort_order').on('change', function () { + const value = String($(this).find(":selected").val()); + localStorage.setItem(SORT_ORDER_KEY, value); updateEditor(navigation_option.none); }) diff --git a/public/style.css b/public/style.css index e63db8100b..3a8b1bcef7 100644 --- a/public/style.css +++ b/public/style.css @@ -1383,7 +1383,8 @@ select option:not(:checked) { .menu_button.disabled { filter: brightness(75%) grayscale(1); - cursor: not-allowed; + opacity: 0.5; + pointer-events: none; } .fav_on { @@ -3491,14 +3492,6 @@ a { align-items: center; } -.prompt_overridden, -.jailbreak_overridden { - color: var(--SmartThemeQuoteColor); - font-weight: bold; - font-style: italic; - font-size: 0.8em; -} - .openai_restorable .right_menu_button img { height: 20px; } diff --git a/server.js b/server.js index 8e8b71ce9a..5f4683cca9 100644 --- a/server.js +++ b/server.js @@ -2743,7 +2743,7 @@ app.post("/getstatus_openai", jsonParser, async function (request, response_gets data.data.forEach(model => { const context_length = model.context_length; - const tokens_dollar = Number(1 / (1000 * model.pricing.prompt)); + const tokens_dollar = Number(1 / (1000 * model.pricing?.prompt)); const tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0); models[model.id] = { tokens_per_dollar: tokens_rounded + 'k', @@ -2764,8 +2764,8 @@ app.post("/getstatus_openai", jsonParser, async function (request, response_gets } } else { - console.log('Access Token is incorrect.'); - response_getstatus_openai.send({ error: true }); + console.log('OpenAI status check failed. Either Access Token is incorrect or API endpoint is down.'); + response_getstatus_openai.send({ error: true, can_bypass: true, data: { data: [] } }); } } catch (e) { console.error(e); diff --git a/src/assets.js b/src/assets.js index b969f62b31..a489c5dfb5 100644 --- a/src/assets.js +++ b/src/assets.js @@ -37,6 +37,25 @@ function checkAssetFileName(inputFilename) { return path.normalize(inputFilename).replace(/^(\.\.(\/|\\|$))+/, '');; } +// Recursive function to get files +function getFiles(dir, files = []) { + // Get an array of all files and directories in the passed directory using fs.readdirSync + const fileList = fs.readdirSync(dir); + // Create the full path of the file/directory by concatenating the passed directory and file/directory name + for (const file of fileList) { + const name = `${dir}/${file}`; + // Check if the current file/directory is a directory using fs.statSync + if (fs.statSync(name).isDirectory()) { + // If it is a directory, recursively call the getFiles function with the directory path and the files array + getFiles(name, files); + } else { + // If it is a file, push the full path to the files array + files.push(name); + } + } + return files; +} + /** * Registers the endpoints for the asset management. * @param {import('express').Express} app Express app @@ -70,16 +89,14 @@ function registerEndpoints(app, jsonParser) { // Live2d assets if (folder == "live2d") { output[folder] = []; - const live2d_folders = fs.readdirSync(path.join(folderPath, folder)); - for (let model_folder of live2d_folders) { - const live2d_model_path = path.join(folderPath, folder, model_folder); - if (fs.statSync(live2d_model_path).isDirectory()) { - for (let file of fs.readdirSync(live2d_model_path)) { - if (file.includes("model")) { - //console.debug("Asset live2d model found:",file) - output[folder].push([`${model_folder}`, path.join("assets", folder, model_folder, file)]); - } - } + const live2d_folder = path.normalize(path.join(folderPath, folder)); + const files = getFiles(live2d_folder); + //console.debug("FILE FOUND:",files) + for (let file of files) { + file = path.normalize(file.replace('public' + path.sep, '')); + if (file.endsWith("model3.json")) { + //console.debug("Asset live2d model found:",file) + output[folder].push(path.normalize(path.join(file))); } } continue; @@ -257,7 +274,7 @@ function registerEndpoints(app, jsonParser) { for (let file of fs.readdirSync(live2dModelPath)) { //console.debug("Character live2d model found:", file) if (file.includes("model")) - output.push([`${modelFolder}`, path.join("characters", name, category, modelFolder, file)]); + output.push(path.join("characters", name, category, modelFolder, file)); } } } diff --git a/src/openai.js b/src/openai.js index aa2445ada1..857c0198f8 100644 --- a/src/openai.js +++ b/src/openai.js @@ -63,6 +63,45 @@ function registerEndpoints(app, jsonParser) { } }); + app.post('/api/openai/generate-voice', jsonParser, async (request, response) => { + try { + const key = readSecret(SECRET_KEYS.OPENAI); + + if (!key) { + console.log('No OpenAI key found'); + return response.sendStatus(401); + } + + const result = await fetch('https://api.openai.com/v1/audio/speech', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${key}`, + }, + body: JSON.stringify({ + input: request.body.text, + response_format: 'mp3', + voice: request.body.voice ?? 'alloy', + speed: request.body.speed ?? 1, + model: request.body.model ?? 'tts-1', + }), + }); + + if (!result.ok) { + const text = await result.text(); + console.log('OpenAI request failed', result.statusText, text); + return response.status(500).send(text); + } + + const buffer = await result.arrayBuffer(); + response.setHeader('Content-Type', 'audio/mpeg'); + return response.send(Buffer.from(buffer)); + } catch (error) { + console.error('OpenAI TTS generation failed', error); + response.status(500).send('Internal server error'); + } + }); + app.post('/api/openai/generate-image', jsonParser, async (request, response) => { try { const key = readSecret(SECRET_KEYS.OPENAI);