diff --git a/public/script.js b/public/script.js index ee9d9718a..4d2411549 100644 --- a/public/script.js +++ b/public/script.js @@ -206,7 +206,15 @@ import { } from './scripts/instruct-mode.js'; import { initLocales } from './scripts/i18n.js'; import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.js'; -import { createPersona, initPersonas, selectCurrentPersona, setPersonaDescription, updatePersonaNameIfExists } from './scripts/personas.js'; +import { + user_avatar, + getUserAvatars, + getUserAvatar, + setUserAvatar, + initPersonas, + setPersonaDescription, + initUserAvatar, +} from './scripts/personas.js'; import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; @@ -221,81 +229,14 @@ import { ScraperManager } from './scripts/scrapers.js'; //exporting functions and vars for mods export { - Generate, - cleanUpMessage, - getSettings, - saveSettings, - saveSettingsDebounced, - printMessages, - clearChat, - getChat, - getCharacters, - getGeneratingApi, - callPopup, - substituteParams, - sendSystemMessage, - addOneMessage, - deleteLastMessage, - resetChatState, - select_rm_info, - setCharacterId, - setCharacterName, - replaceCurrentChat, - setOnlineStatus, - displayOnlineStatus, - setEditedMessageId, - setSendButtonState, - selectRightMenuWithAnimation, - openCharacterChat, - saveChat, - messageFormatting, - getExtensionPrompt, - getExtensionPromptByName, - showSwipeButtons, - hideSwipeButtons, - changeMainAPI, - setGenerationProgress, - updateChatMetadata, - scrollChatToBottom, - isStreamingEnabled, - getThumbnailUrl, - buildAvatarList, - getStoppingStrings, - reloadMarkdownProcessor, - getCurrentChatId, - chat, - this_chid, - selected_button, - menu_type, - settings, - characters, - online_status, - main_api, - api_server, - system_messages, + user_avatar, + setUserAvatar, + getUserAvatars, + getUserAvatar, nai_settings, - token, - name1, - name2, - is_send_press, - max_context, - chat_metadata, - streamingProcessor, - default_avatar, - system_message_types, - talkativeness_default, - default_ch_mes, - extension_prompt_types, - mesForShowdownParse, - characterGroupOverlay, - printCharacters, - printCharactersDebounced, isOdd, countOccurrences, renderTemplate, - itemizedPrompts, - findItemizedPromptSet, - itemizedParams, }; /** @@ -477,7 +418,7 @@ export const eventSource = new EventEmitter(); eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands); -const characterGroupOverlay = new BulkEditOverlay(); +export const characterGroupOverlay = new BulkEditOverlay(); const characterContextMenu = new CharacterContextMenu(characterGroupOverlay); eventSource.on(event_types.CHARACTER_PAGE_LOADED, characterGroupOverlay.onPageLoad); console.debug('Character context menu initialized', characterContextMenu); @@ -485,20 +426,20 @@ console.debug('Character context menu initialized', characterContextMenu); hljs.addPlugin({ 'before:highlightElement': ({ el }) => { el.textContent = el.innerText; } }); // Markdown converter -let mesForShowdownParse; //intended to be used as a context to compare showdown strings against +export let mesForShowdownParse; //intended to be used as a context to compare showdown strings against let converter; reloadMarkdownProcessor(); // array for prompt token calculations console.debug('initializing Prompt Itemization Array on Startup'); const promptStorage = new localforage.createInstance({ name: 'SillyTavern_Prompts' }); -let itemizedPrompts = []; +export let itemizedPrompts = []; export const systemUserName = 'SillyTavern System'; let default_user_name = 'User'; -let name1 = default_user_name; -let name2 = 'SillyTavern System'; -let chat = []; +export let name1 = default_user_name; +export let name2 = 'SillyTavern System'; +export let chat = []; let safetychat = [ { name: systemUserName, @@ -515,14 +456,13 @@ let firstRun = false; let settingsReady = false; let currentVersion = '0.0.0'; -const default_ch_mes = 'Hello'; +export const default_ch_mes = 'Hello'; let generatedPromptCache = ''; let generation_started = new Date(); -let characters = []; -let this_chid; +export let characters = []; +export let this_chid; let saveCharactersPage = 0; -let savePersonasPage = 0; -const default_avatar = 'img/ai4.png'; +export const default_avatar = 'img/ai4.png'; export const system_avatar = 'img/five.png'; export const comment_avatar = 'img/quill.png'; export let CLIENT_VERSION = 'SillyTavern:UNKNOWN:Cohee#1207'; // For Horde header @@ -542,9 +482,9 @@ const chatElement = $('#chat'); let dialogueResolve = null; let dialogueCloseStop = false; -let chat_metadata = {}; -let streamingProcessor = null; -let crop_data = undefined; +export let chat_metadata = {}; +export let streamingProcessor = null; +export let crop_data = undefined; let is_delete_mode = false; let fav_ch_checked = false; let scrollLock = false; @@ -552,7 +492,7 @@ export let abortStatusCheck = new AbortController(); /** @type {number} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ const durationSaveEdit = debounce_timeout.relaxed; -const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit); +export const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit); export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit); /** @@ -561,12 +501,12 @@ export const saveCharacterDebounced = debounce(() => $('#create_button').trigger * * The printing will also always reprint all filter options of the global list, to keep them up to date. */ -const printCharactersDebounced = debounce(() => { printCharacters(false); }, debounce_timeout.quick); +export const printCharactersDebounced = debounce(() => { printCharacters(false); }, debounce_timeout.quick); /** * @enum {string} System message types */ -const system_message_types = { +export const system_message_types = { HELP: 'help', WELCOME: 'welcome', GROUP: 'group', @@ -585,7 +525,7 @@ const system_message_types = { /** * @enum {number} Extension prompt types */ -const extension_prompt_types = { +export const extension_prompt_types = { IN_PROMPT: 0, IN_CHAT: 1, BEFORE_PROMPT: 2, @@ -602,7 +542,7 @@ export const extension_prompt_roles = { export const MAX_INJECTION_DEPTH = 1000; -let system_messages = {}; +export let system_messages = {}; async function getSystemMessages() { system_messages = { @@ -722,7 +662,7 @@ async function getClientVersion() { } } -function reloadMarkdownProcessor(render_formulas = false) { +export function reloadMarkdownProcessor(render_formulas = false) { if (render_formulas) { converter = new showdown.Converter({ emoji: true, @@ -764,7 +704,7 @@ function reloadMarkdownProcessor(render_formulas = false) { return converter; } -function getCurrentChatId() { +export function getCurrentChatId() { if (selected_group) { return groups.find(x => x.id == selected_group)?.chat_id; } @@ -773,15 +713,15 @@ function getCurrentChatId() { } } -const talkativeness_default = 0.5; +export const talkativeness_default = 0.5; export const depth_prompt_depth_default = 4; export const depth_prompt_role_default = 'system'; const per_page_default = 50; var is_advanced_char_open = false; -var menu_type = ''; //what is selected in the menu -var selected_button = ''; //which button pressed +export let menu_type = ''; //what is selected in the menu +export let selected_button = ''; //which button pressed //create pole save let create_save = { name: '', @@ -812,11 +752,11 @@ export let animation_duration = ANIMATION_DURATION_DEFAULT; export let animation_easing = 'ease-in-out'; let popup_type = ''; let chat_file_for_del = ''; -let online_status = 'no_connection'; +export let online_status = 'no_connection'; -let api_server = ''; +export let api_server = ''; -let is_send_press = false; //Send generation +export let is_send_press = false; //Send generation let this_del_mes = -1; @@ -827,18 +767,17 @@ var scroll_holder = 0; var is_use_scroll_holder = false; //settings -var settings; +export let settings; export let koboldai_settings; export let koboldai_setting_names; var preset_settings = 'gui'; -export let user_avatar = 'you.png'; -export var amount_gen = 80; //default max length of AI generated responses -var max_context = 2048; +export let amount_gen = 80; //default max length of AI generated responses +export let max_context = 2048; var swipes = true; let extension_prompts = {}; -var main_api;// = "kobold"; +export let main_api;// = "kobold"; //novel settings export let novelai_settings; export let novelai_setting_names; @@ -851,7 +790,7 @@ const MAX_GENERATION_LOOPS = 5; var kobold_horde_model = ''; -let token; +export let token; var PromptArrayItemForRawPromptDisplay; @@ -861,7 +800,6 @@ export let active_character = ''; export let active_group = ''; export const entitiesFilter = new FilterHelper(printCharactersDebounced); -export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); export function getRequestHeaders() { return { @@ -915,7 +853,7 @@ function cancelStatusCheck() { setOnlineStatus('no_connection'); } -function displayOnlineStatus() { +export function displayOnlineStatus() { if (online_status == 'no_connection') { $('.online_status_indicator').removeClass('success'); $('.online_status_text').text('No connection...'); @@ -1307,7 +1245,7 @@ function getCharacterBlock(item, id) { * * @param {boolean} fullRefresh - If true, the list is fully refreshed and the navigation is being reset */ -async function printCharacters(fullRefresh = false) { +export async function printCharacters(fullRefresh = false) { const storageKey = 'Characters_PerPage'; const listId = '#rm_print_characters_block'; @@ -1554,7 +1492,7 @@ function getCharacterSource(chId = this_chid) { return ''; } -async function getCharacters() { +export async function getCharacters() { const response = await fetch('/api/characters/all', { method: 'POST', headers: getRequestHeaders(), @@ -1605,7 +1543,7 @@ async function delChat(chatfile) { } } -async function replaceCurrentChat() { +export async function replaceCurrentChat() { await clearChat(); chat.length = 0; @@ -1658,7 +1596,7 @@ export function showMoreMessages() { $('#chat').scrollTop(newHeight - prevHeight); } -async function printMessages() { +export async function printMessages() { let startIndex = 0; let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; @@ -1700,7 +1638,7 @@ async function printMessages() { } } -async function clearChat() { +export async function clearChat() { closeMessageEditor(); extension_prompts = {}; if (is_delete_mode) { @@ -1716,7 +1654,7 @@ async function clearChat() { itemizedPrompts = []; } -async function deleteLastMessage() { +export async function deleteLastMessage() { chat.length = chat.length - 1; $('#chat').children('.mes').last().remove(); await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); @@ -1775,7 +1713,7 @@ export function sendTextareaMessage() { * @param {number} messageId Message index in chat array * @returns {string} HTML string */ -function messageFormatting(mes, ch_name, isSystem, isUser, messageId) { +export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) { if (!mes) { return ''; } @@ -2066,7 +2004,7 @@ export function addCopyToCodeBlocks(messageElement) { } -function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) { +export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) { let messageText = mes['mes']; const momentDate = timestampToMoment(mes.send_date); const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : ''; @@ -2216,15 +2154,6 @@ function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true } } -/** - * Returns the URL of the avatar for the given user avatar Id. - * @param {string} avatarImg User avatar Id - * @returns {string} User avatar URL - */ -export function getUserAvatar(avatarImg) { - return `User Avatars/${avatarImg}`; -} - /** * Returns the URL of the avatar for the given character Id. * @param {number} characterId Character Id @@ -2280,7 +2209,7 @@ function formatGenerationTimer(gen_started, gen_finished, tokenCount) { return { timerValue, timerTitle }; } -function scrollChatToBottom() { +export function scrollChatToBottom() { if (power_user.auto_scroll_chat_to_bottom) { let position = chatElement[0].scrollHeight; @@ -2306,7 +2235,7 @@ function scrollChatToBottom() { * @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros. * @returns {string} The string with substituted parameters. */ -function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) { +export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) { const environment = {}; if (typeof _original === 'string') { @@ -2364,7 +2293,7 @@ function substituteParams(content, _name1, _name2, _original, _group, _replaceCh * @param {boolean} isContinue A request is made to continue the message * @returns {string[]} Array of stopping strings */ -function getStoppingStrings(isImpersonate, isContinue) { +export function getStoppingStrings(isImpersonate, isContinue) { const charString = `\n${name2}:`; const userString = `\n${name1}:`; const result = isImpersonate ? [charString] : [userString]; @@ -2463,7 +2392,7 @@ async function processCommands(message) { return result?.interrupt; } -function sendSystemMessage(type, text, extra = {}) { +export function sendSystemMessage(type, text, extra = {}) { const systemMessage = system_messages[type]; if (!systemMessage) { @@ -2588,7 +2517,7 @@ function getAllExtensionPrompts() { } // Wrapper to fetch extension prompts by module name -function getExtensionPromptByName(moduleName) { +export function getExtensionPromptByName(moduleName) { if (moduleName) { return substituteParams(extension_prompts[moduleName]?.value); } else { @@ -2606,7 +2535,7 @@ function getExtensionPromptByName(moduleName) { * @param {boolean} [wrap] Wrap start and end with a separator * @returns {string} Extension prompt */ -function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) { +export function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) { let extension_prompt = Object.keys(extension_prompts) .sort() .map((x) => extension_prompts[x]) @@ -2676,7 +2605,7 @@ export function getCharacterCardFields() { return result; } -function isStreamingEnabled() { +export function isStreamingEnabled() { const noStreamSources = [chat_completion_sources.SCALE, chat_completion_sources.AI21]; return ((main_api == 'openai' && oai_settings.stream_openai && !noStreamSources.includes(oai_settings.chat_completion_source) && !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison'))) || (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming) @@ -3098,7 +3027,7 @@ function restoreResponseLength(api, responseLength) { * @returns {Promise} Returns a promise that resolves when the text is done generating. * @typedef {{automatic_trigger?: boolean, force_name2?: boolean, quiet_prompt?: string, quietToLoud?: boolean, skipWIAN?: boolean, force_chid?: number, signal?: AbortSignal, quietImage?: string, maxLoops?: number, quietName?: string }} GenerateOptions */ -async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops, quietName } = {}, dryRun = false) { +export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops, quietName } = {}, dryRun = false) { console.log('Generate entered'); eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops }, dryRun); setGenerationProgress(0); @@ -4632,7 +4561,7 @@ async function DupeChar() { } } -async function itemizedParams(itemizedPrompts, thisPromptSet) { +export async function itemizedParams(itemizedPrompts, thisPromptSet) { const params = { charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription), charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality), @@ -4734,7 +4663,7 @@ async function itemizedParams(itemizedPrompts, thisPromptSet) { return params; } -function findItemizedPromptSet(itemizedPrompts, incomingMesId) { +export function findItemizedPromptSet(itemizedPrompts, incomingMesId) { var thisPromptSet = undefined; for (var i = 0; i < itemizedPrompts.length; i++) { @@ -4979,7 +4908,7 @@ function extractMultiSwipes(data, type) { return swipes; } -function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncompleteSentences = false, stoppingStrings = null) { +export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncompleteSentences = false, stoppingStrings = null) { if (!getMessage) { return ''; } @@ -5280,7 +5209,7 @@ function saveImageToMessage(img, mes) { } } -function getGeneratingApi() { +export function getGeneratingApi() { switch (main_api) { case 'openai': return oai_settings.chat_completion_source || 'openai'; @@ -5337,7 +5266,7 @@ export function deactivateSendButtons() { showStopButton(); } -function resetChatState() { +export function resetChatState() { //unsets expected chid before reloading (related to getCharacters/printCharacters from using old arrays) this_chid = undefined; // replaces deleted charcter name with system user since it will be displayed next. @@ -5358,24 +5287,24 @@ export function setExternalAbortController(controller) { abortController = controller; } -function setCharacterId(value) { +export function setCharacterId(value) { this_chid = value; } -function setCharacterName(value) { +export function setCharacterName(value) { name2 = value; } -function setOnlineStatus(value) { +export function setOnlineStatus(value) { online_status = value; displayOnlineStatus(); } -function setEditedMessageId(value) { +export function setEditedMessageId(value) { this_edit_mes_id = value; } -function setSendButtonState(value) { +export function setSendButtonState(value) { is_send_press = value; } @@ -5524,7 +5453,7 @@ export function saveChatDebounced() { }, 1000); } -async function saveChat(chat_name, withMetadata, mesId) { +export async function saveChat(chat_name, withMetadata, mesId) { const metadata = { ...chat_metadata, ...(withMetadata || {}) }; let file_name = chat_name ?? characters[this_chid]?.chat; @@ -5653,11 +5582,11 @@ export function getCropPopup(src) { `; } -function getThumbnailUrl(type, file) { +export function getThumbnailUrl(type, file) { return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; } -function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) { +export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) { if (empty) { block.empty(); } @@ -5702,7 +5631,7 @@ function buildAvatarList(block, entities, { templateId = 'inline_avatar_template } } -async function getChat() { +export async function getChat() { //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name); try { const response = await $.ajax({ @@ -5783,7 +5712,7 @@ function getFirstMessage() { return message; } -async function openCharacterChat(file_name) { +export async function openCharacterChat(file_name) { await clearChat(); characters[this_chid]['chat'] = file_name; chat.length = 0; @@ -5795,7 +5724,7 @@ async function openCharacterChat(file_name) { ////////// OPTIMZED MAIN API CHANGE FUNCTION //////////// -function changeMainAPI() { +export function changeMainAPI() { const selectedVal = $('#main_api').val(); //console.log(selectedVal); const apiElements = { @@ -5918,170 +5847,6 @@ function changeMainAPI() { forceCharacterEditorTokenize(); } -//////////////////////////////////////////////////// - -/** - * Gets a list of user avatars. - * @param {boolean} doRender Whether to render the list - * @param {string} openPageAt Item to be opened at - * @returns {Promise} List of avatar file names - */ -export async function getUserAvatars(doRender = true, openPageAt = '') { - const response = await fetch('/api/avatars/get', { - method: 'POST', - headers: getRequestHeaders(), - }); - if (response.ok) { - const allEntities = await response.json(); - - if (!Array.isArray(allEntities)) { - return []; - } - - if (!doRender) { - return allEntities; - } - - // Before printing the personas, we check if we should enable/disable search sorting - verifyPersonaSearchSortRule(); - - let entities = personasFilter.applyFilters(allEntities); - entities = sortPersonas(entities); - - const storageKey = 'Personas_PerPage'; - const listId = '#user_avatar_block'; - const perPage = Number(localStorage.getItem(storageKey)) || 5; - - $('#persona_pagination_container').pagination({ - dataSource: entities, - pageSize: perPage, - sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000], - pageRange: 1, - pageNumber: savePersonasPage || 1, - position: 'top', - showPageNumbers: false, - showSizeChanger: true, - prevText: '<', - nextText: '>', - formatNavigator: PAGINATION_TEMPLATE, - showNavigator: true, - callback: function (data) { - $(listId).empty(); - for (const item of data) { - $(listId).append(getUserAvatarBlock(item)); - } - highlightSelectedAvatar(); - }, - afterSizeSelectorChange: function (e) { - localStorage.setItem(storageKey, e.target.value); - }, - afterPaging: function (e) { - savePersonasPage = e; - }, - afterRender: function () { - $(listId).scrollTop(0); - }, - }); - - if (openPageAt) { - const avatarIndex = entities.indexOf(openPageAt); - const page = Math.floor(avatarIndex / perPage) + 1; - - if (avatarIndex !== -1) { - $('#persona_pagination_container').pagination('go', page); - } - } - - return allEntities; - } -} - -function highlightSelectedAvatar() { - $('#user_avatar_block .avatar-container').removeClass('selected'); - $(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected'); -} - -/** - * Sort the given personas - * @param {string[]} personas - The persona names to sort - * @returns {string[]} The sorted persona names arrray, same reference as passed in - */ -function sortPersonas(personas) { - const option = $('#persona_sort_order').find(':selected'); - if (option.attr('value') === 'search') { - personas.sort((a, b) => { - const aScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, a); - const bScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, b); - return (aScore - bScore); - }); - } else { - personas.sort((a, b) => { - const aName = String(power_user.personas[a] || a); - const bName = String(power_user.personas[b] || b); - return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName); - }); - } - - return personas; -} - -/** Checks the state of the current search, and adds/removes the search sorting option accordingly */ -function verifyPersonaSearchSortRule() { - const searchTerm = personasFilter.getFilterData(FILTER_TYPES.PERSONA_SEARCH); - const searchOption = $('#persona_sort_order option[value="search"]'); - const selector = $('#persona_sort_order'); - const isHidden = searchOption.attr('hidden') !== undefined; - - // If we have a search term, we are displaying the sorting option for it - if (searchTerm && isHidden) { - searchOption.removeAttr('hidden'); - selector.val(searchOption.attr('value')); - flashHighlight(selector); - } - // If search got cleared, we make sure to hide the option and go back to the one before - if (!searchTerm && !isHidden) { - searchOption.attr('hidden', ''); - selector.val(power_user.persona_sort_order); - } -} - -/** - * Gets a rendered avatar block. - * @param {string} name Avatar file name - * @returns {JQuery} Avatar block - */ -function getUserAvatarBlock(name) { - const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; - const template = $('#user_avatar_template .avatar-container').clone(); - const personaName = power_user.personas[name]; - const personaDescription = power_user.persona_descriptions[name]?.description; - template.find('.ch_name').text(personaName || '[Unnamed Persona]'); - template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription); - template.attr('imgfile', name); - template.find('.avatar').attr('imgfile', name).attr('title', name); - template.toggleClass('default_persona', name === power_user.default_persona); - let avatarUrl = getUserAvatar(name); - if (isFirefox) { - avatarUrl += '?t=' + Date.now(); - } - template.find('img').attr('src', avatarUrl); - $('#user_avatar_block').append(template); - return template; -} - -function reloadUserAvatar(force = false) { - $('.mes').each(function () { - const avatarImg = $(this).find('.avatar img'); - if (force) { - avatarImg.attr('src', avatarImg.attr('src')); - } - - if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') { - avatarImg.attr('src', getUserAvatar(user_avatar)); - } - }); -} - export function setUserName(value) { name1 = value; if (name1 === undefined || name1 == '') @@ -6094,87 +5859,6 @@ export function setUserName(value) { saveSettingsDebounced(); } -/** - * Sets a user avatar file - * @param {string} imgfile Link to an image file - */ -export function setUserAvatar(imgfile) { - user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('imgfile'); - reloadUserAvatar(); - highlightSelectedAvatar(); - selectCurrentPersona(); - saveSettingsDebounced(); - $('.zoomed_avatar[forchar]').remove(); -} - -export function retriggerFirstMessageOnEmptyChat() { - if (this_chid >= 0 && !selected_group && chat.length === 1) { - $('#firstmessage_textarea').trigger('input'); - } -} - -async function uploadUserAvatar(e) { - const file = e.target.files[0]; - - if (!file) { - $('#form_upload_avatar').trigger('reset'); - return; - } - - const formData = new FormData($('#form_upload_avatar').get(0)); - const dataUrl = await getBase64Async(file); - let url = '/api/avatars/upload'; - - 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))}`; - } - } - - const rawFile = formData.get('avatar'); - if (rawFile instanceof File) { - const convertedFile = await ensureImageFormatSupported(rawFile); - formData.set('avatar', convertedFile); - } - - jQuery.ajax({ - type: 'POST', - url: url, - data: formData, - beforeSend: () => { }, - cache: false, - contentType: false, - processData: false, - 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) { - await fetch(getUserAvatar(name), { cache: 'no-cache' }); - reloadUserAvatar(true); - } - - if (!name && data.path) { - await getUserAvatars(); - await delay(500); - await createPersona(data.path); - } - - crop_data = undefined; - await getUserAvatars(true, name || data.path); - }, - error: (jqXHR, exception) => { }, - }); - - // Will allow to select the same file twice in a row - $('#form_upload_avatar').trigger('reset'); -} - async function doOnboarding(avatarId) { let simpleUiMode = false; const template = $('#onboarding_template .onboarding'); @@ -6213,7 +5897,7 @@ function reloadLoop() { //***************SETTINGS****************// /////////////////////////////////////////// -async function getSettings() { +export async function getSettings() { const response = await fetch('/api/settings/get', { method: 'POST', headers: getRequestHeaders(), @@ -6358,8 +6042,8 @@ async function getSettings() { changeMainAPI(); //Load User's Name and Avatar + initUserAvatar(settings.user_avatar); - user_avatar = settings.user_avatar; firstRun = !!settings.firstRun; if (firstRun) { @@ -6368,8 +6052,6 @@ async function getSettings() { firstRun = false; } - reloadUserAvatar(); - highlightSelectedAvatar(); setPersonaDescription(); //Load the active character and group @@ -6401,7 +6083,7 @@ function selectKoboldGuiPreset() { .trigger('change'); } -async function saveSettings(type) { +export async function saveSettings(type) { if (!settingsReady) { console.warn('Settings not ready, aborting save'); return; @@ -6816,7 +6498,7 @@ export async function displayPastChats() { }, 200); } -function selectRightMenuWithAnimation(selectedMenuId) { +export function selectRightMenuWithAnimation(selectedMenuId) { const displayModes = { 'rm_group_chats_block': 'flex', 'rm_api_block': 'grid', @@ -6840,7 +6522,7 @@ function selectRightMenuWithAnimation(selectedMenuId) { }); } -function select_rm_info(type, charId, previousCharId = null) { +export function select_rm_info(type, charId, previousCharId = null) { if (!type) { toastr.error('Invalid process (no \'type\')'); return; @@ -7146,7 +6828,7 @@ export function removeDepthPrompts() { * @param {Object} newValues An object with collection of new values to be added into the metadata. * @param {boolean} reset Should a metadata be reset by this call. */ -function updateChatMetadata(newValues, reset) { +export function updateChatMetadata(newValues, reset) { chat_metadata = reset ? { ...newValues } : { ...chat_metadata, ...newValues }; } @@ -7192,7 +6874,7 @@ function onScenarioOverrideRemoveClick() { * @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup. * @returns */ -function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) { +export function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) { function getOkButtonText() { if (['avatarToCrop'].includes(popup_type)) { return okButton ?? 'Accept'; @@ -7263,7 +6945,7 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, a }); } -function showSwipeButtons() { +export function showSwipeButtons() { if (chat.length === 0) { return; } @@ -7317,7 +6999,7 @@ function showSwipeButtons() { //console.log(chat[chat.length - 1].swipes.length); } -function hideSwipeButtons() { +export function hideSwipeButtons() { //console.log('hideswipebuttons entered'); $('#chat').find('.swipe_right').css('display', 'none'); $('#chat').find('.swipe_left').css('display', 'none'); @@ -7427,7 +7109,7 @@ function closeMessageEditor() { } } -function setGenerationProgress(progress) { +export function setGenerationProgress(progress) { if (!progress) { $('#send_textarea').css({ 'background': '', 'transition': '' }); } @@ -8761,13 +8443,6 @@ jQuery(async function () { debouncedCharacterSearch(searchQuery); }); - const debouncedPersonaSearch = debounce((searchQuery) => { - personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchQuery); - }); - $('#persona_search_bar').on('input', function () { - const searchQuery = String($(this).val()); - debouncedPersonaSearch(searchQuery); - }); $('#mes_continue').on('click', function () { $('#option_continue').trigger('click'); @@ -8853,31 +8528,6 @@ jQuery(async function () { } }); - $(document).on('click', '#user_avatar_block .avatar-container', function () { - const imgfile = $(this).attr('imgfile'); - setUserAvatar(imgfile); - - // force firstMes {{user}} update on persona switch - retriggerFirstMessageOnEmptyChat(); - }); - $(document).on('click', '#user_avatar_block .avatar_upload', function () { - $('#avatar_upload_overwrite').val(''); - $('#avatar_upload_file').trigger('click'); - }); - $(document).on('click', '#user_avatar_block .set_persona_image', function (e) { - e.stopPropagation(); - const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); - - if (!avatarId) { - console.log('no imgfile'); - return; - } - - $('#avatar_upload_overwrite').val(avatarId); - $('#avatar_upload_file').trigger('click'); - }); - $('#avatar_upload_file').on('change', uploadUserAvatar); - $(document).on('click', '.PastChat_cross', function (e) { e.stopPropagation(); chat_file_for_del = $(this).attr('file_name'); @@ -9963,30 +9613,6 @@ jQuery(async function () { await messageEditDone($(this)); }); - $('#your_name_button').click(async function () { - const userName = String($('#your_name').val()).trim(); - setUserName(userName); - await updatePersonaNameIfExists(user_avatar, userName); - retriggerFirstMessageOnEmptyChat(); - }); - - $('#sync_name_button').on('click', async function () { - const confirmation = await callPopup(`

Are you sure?

All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm'); - - if (!confirmation) { - return; - } - - for (const mes of chat) { - if (mes.is_user) { - mes.name = name1; - mes.force_avatar = getUserAvatar(user_avatar); - } - } - - await saveChatConditional(); - await reloadCurrentChat(); - }); //Select chat //**************************CHARACTER IMPORT EXPORT*************************// @@ -10524,77 +10150,6 @@ jQuery(async function () { } isManualInput = false; }); - /* - let manualInputTimeout; - .on('input', '.range-block-counter input, .neo-range-input', function () { - clearTimeout(manualInputTimeout); - manualInputTimeout = setTimeout(() => { - const caretPosition = saveCaretPosition($(this).get(0)); - const myText = $(this).val().trim(); - $(this).val(myText); // trim line breaks and spaces - const masterSelector = $(this).data('for'); - const masterElement = document.getElementById(masterSelector); - - if (masterElement == null) { - console.error('Master input element not found for the editable label', masterSelector); - return; - } - - const myValue = Number(myText); - const masterStep = Number(masterElement.getAttribute('step')) - const masterMin = Number($(masterElement).attr('min')); - const masterMax = Number($(masterElement).attr('max')); - const rawStepCompare = myValue / masterStep - const closestStep = Math.round(rawStepCompare) - const closestStepRaw = (closestStep) * masterStep - - //yolo anything for Lab Mode - if (power_user.enableLabMode) { - //console.log($(masterElement).attr('id'), myValue) - $(masterElement).val(myValue).trigger('input') - return - } - - //if text box val is not a number, reset slider val to its previous and wait for better input - if (Number.isNaN(myValue)) { - console.warn('Label input is not a valid number. Resetting the value to match slider', myText); - $(masterElement).trigger('input'); - restoreCaretPosition($(this).get(0), caretPosition); - return; - } - - //if textbox val is less than min, set slider to min - //PROBLEM: the moment slider gets set to min, textbox also auto-sets to min. - //if min = 0, this prevents further typing and locks input at 0 unless users pastes - //a multi-character number which is between min and max. adding delay was necessary. - if (myValue < masterMin) { - console.warn('Label input is less than minimum.', myText, '<', masterMin); - $(masterElement).val(masterMin).trigger('input').trigger('mouseup'); - $(masterElement).val(myValue) - restoreCaretPosition($(this).get(0), caretPosition); - return; - } - //Same as above but in reverse. Not a problem because max value has multiple - //characters which can be edited. - if (myValue > masterMax) { - console.warn('Label input is more than maximum.', myText, '>', masterMax); - $(masterElement).val(masterMax).trigger('input').trigger('mouseup'); - $(masterElement).val(myValue) - restoreCaretPosition($(this).get(0), caretPosition); - return; - } - - //round input value to nearest step if between min and max - if (!(myValue < masterMin) && !(myValue > masterMax)) { - console.debug(`Label value ${myText} is OK, setting slider to closest step (${closestStepRaw})`); - $(masterElement).val(closestStepRaw).trigger('input').trigger('mouseup'); - restoreCaretPosition($(this).get(0), caretPosition); - return; - } - - restoreCaretPosition($(this).get(0), caretPosition); - }, 2000); */ - //}); $('.user_stats_button').on('click', function () { userStatsHandler(); diff --git a/public/scripts/personas.js b/public/scripts/personas.js index bb5ab7209..4b9f7ac70 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -1,33 +1,230 @@ import { callPopup, characters, + chat, chat_metadata, + crop_data, default_avatar, eventSource, event_types, + getCropPopup, getRequestHeaders, getThumbnailUrl, - getUserAvatars, name1, + reloadCurrentChat, + saveChatConditional, saveMetadata, saveSettingsDebounced, - setUserAvatar, setUserName, this_chid, - user_avatar, } from '../script.js'; import { persona_description_positions, power_user } from './power-user.js'; import { getTokenCountAsync } from './tokenizers.js'; -import { debounce, delay, download, parseJsonFile } from './utils.js'; +import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, parseJsonFile } from './utils.js'; import { debounce_timeout } from './constants.js'; +import { FILTER_TYPES, FilterHelper } from './filters.js'; +import { selected_group } from './group-chats.js'; +let savePersonasPage = 0; const GRID_STORAGE_KEY = 'Personas_GridView'; +export let user_avatar = ''; +export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); function switchPersonaGridView() { const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true'; $('#user_avatar_block').toggleClass('gridView', state); } +/** + * Returns the URL of the avatar for the given user avatar Id. + * @param {string} avatarImg User avatar Id + * @returns {string} User avatar URL + */ +export function getUserAvatar(avatarImg) { + return `User Avatars/${avatarImg}`; +} + +export function initUserAvatar(avatar) { + user_avatar = avatar; + reloadUserAvatar(); + highlightSelectedAvatar(); +} + +/** + * Sets a user avatar file + * @param {string} imgfile Link to an image file + */ +export function setUserAvatar(imgfile) { + user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('imgfile'); + reloadUserAvatar(); + highlightSelectedAvatar(); + selectCurrentPersona(); + saveSettingsDebounced(); + $('.zoomed_avatar[forchar]').remove(); +} + +function highlightSelectedAvatar() { + $('#user_avatar_block .avatar-container').removeClass('selected'); + $(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected'); +} + +function reloadUserAvatar(force = false) { + $('.mes').each(function () { + const avatarImg = $(this).find('.avatar img'); + if (force) { + avatarImg.attr('src', avatarImg.attr('src')); + } + + if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') { + avatarImg.attr('src', getUserAvatar(user_avatar)); + } + }); +} + +/** + * Sort the given personas + * @param {string[]} personas - The persona names to sort + * @returns {string[]} The sorted persona names arrray, same reference as passed in + */ +function sortPersonas(personas) { + const option = $('#persona_sort_order').find(':selected'); + if (option.attr('value') === 'search') { + personas.sort((a, b) => { + const aScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, a); + const bScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, b); + return (aScore - bScore); + }); + } else { + personas.sort((a, b) => { + const aName = String(power_user.personas[a] || a); + const bName = String(power_user.personas[b] || b); + return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName); + }); + } + + return personas; +} + +/** Checks the state of the current search, and adds/removes the search sorting option accordingly */ +function verifyPersonaSearchSortRule() { + const searchTerm = personasFilter.getFilterData(FILTER_TYPES.PERSONA_SEARCH); + const searchOption = $('#persona_sort_order option[value="search"]'); + const selector = $('#persona_sort_order'); + const isHidden = searchOption.attr('hidden') !== undefined; + + // If we have a search term, we are displaying the sorting option for it + if (searchTerm && isHidden) { + searchOption.removeAttr('hidden'); + selector.val(searchOption.attr('value')); + flashHighlight(selector); + } + // If search got cleared, we make sure to hide the option and go back to the one before + if (!searchTerm && !isHidden) { + searchOption.attr('hidden', ''); + selector.val(power_user.persona_sort_order); + } +} + +/** + * Gets a rendered avatar block. + * @param {string} name Avatar file name + * @returns {JQuery} Avatar block + */ +function getUserAvatarBlock(name) { + const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; + const template = $('#user_avatar_template .avatar-container').clone(); + const personaName = power_user.personas[name]; + const personaDescription = power_user.persona_descriptions[name]?.description; + template.find('.ch_name').text(personaName || '[Unnamed Persona]'); + template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription); + template.attr('imgfile', name); + template.find('.avatar').attr('imgfile', name).attr('title', name); + template.toggleClass('default_persona', name === power_user.default_persona); + let avatarUrl = getUserAvatar(name); + if (isFirefox) { + avatarUrl += '?t=' + Date.now(); + } + template.find('img').attr('src', avatarUrl); + $('#user_avatar_block').append(template); + return template; +} + +/** + * Gets a list of user avatars. + * @param {boolean} doRender Whether to render the list + * @param {string} openPageAt Item to be opened at + * @returns {Promise} List of avatar file names + */ +export async function getUserAvatars(doRender = true, openPageAt = '') { + const response = await fetch('/api/avatars/get', { + method: 'POST', + headers: getRequestHeaders(), + }); + if (response.ok) { + const allEntities = await response.json(); + + if (!Array.isArray(allEntities)) { + return []; + } + + if (!doRender) { + return allEntities; + } + + // Before printing the personas, we check if we should enable/disable search sorting + verifyPersonaSearchSortRule(); + + let entities = personasFilter.applyFilters(allEntities); + entities = sortPersonas(entities); + + const storageKey = 'Personas_PerPage'; + const listId = '#user_avatar_block'; + const perPage = Number(localStorage.getItem(storageKey)) || 5; + + $('#persona_pagination_container').pagination({ + dataSource: entities, + pageSize: perPage, + sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000], + pageRange: 1, + pageNumber: savePersonasPage || 1, + position: 'top', + showPageNumbers: false, + showSizeChanger: true, + prevText: '<', + nextText: '>', + formatNavigator: PAGINATION_TEMPLATE, + showNavigator: true, + callback: function (data) { + $(listId).empty(); + for (const item of data) { + $(listId).append(getUserAvatarBlock(item)); + } + highlightSelectedAvatar(); + }, + afterSizeSelectorChange: function (e) { + localStorage.setItem(storageKey, e.target.value); + }, + afterPaging: function (e) { + savePersonasPage = e; + }, + afterRender: function () { + $(listId).scrollTop(0); + }, + }); + + if (openPageAt) { + const avatarIndex = entities.indexOf(openPageAt); + const page = Math.floor(avatarIndex / perPage) + 1; + + if (avatarIndex !== -1) { + $('#persona_pagination_container').pagination('go', page); + } + } + + return allEntities; + } +} + /** * Uploads an avatar file to the server * @param {string} url URL for the avatar file @@ -59,6 +256,73 @@ async function uploadUserAvatar(url, name) { }); } +async function changeUserAvatar(e) { + const form = document.getElementById('form_upload_avatar'); + + if (!(form instanceof HTMLFormElement)) { + console.error('Form not found'); + return; + } + + const file = e.target.files[0]; + + if (!file) { + form.reset(); + return; + } + + const formData = new FormData(form); + const dataUrl = await getBase64Async(file); + let url = '/api/avatars/upload'; + + if (!power_user.never_resize_avatars) { + const confirmation = await callPopup(getCropPopup(dataUrl), 'avatarToCrop', '', { okButton: 'Crop', large: true, wide: true }); + if (!confirmation) { + return; + } + + if (crop_data !== undefined) { + url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; + } + } + + const rawFile = formData.get('avatar'); + if (rawFile instanceof File) { + const convertedFile = await ensureImageFormatSupported(rawFile); + formData.set('avatar', convertedFile); + } + + jQuery.ajax({ + type: 'POST', + url: url, + data: formData, + beforeSend: () => { }, + cache: false, + contentType: false, + processData: false, + 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) { + await fetch(getUserAvatar(String(name)), { cache: 'no-cache' }); + reloadUserAvatar(true); + } + + if (!name && data.path) { + await getUserAvatars(); + await delay(500); + await createPersona(data.path); + } + + await getUserAvatars(true, name || data.path); + }, + error: (jqXHR, exception) => { }, + }); + + // Will allow to select the same file twice in a row + form.reset(); +} + /** * Prompts the user to create a persona for the uploaded avatar. * @param {string} avatarId User avatar id @@ -206,7 +470,7 @@ export function autoSelectPersona(name) { * @param {string} avatarId User avatar id * @param {string} newName New name for the persona */ -export async function updatePersonaNameIfExists(avatarId, newName) { +async function updatePersonaNameIfExists(avatarId, newName) { if (avatarId in power_user.personas) { power_user.personas[avatarId] = newName; await getUserAvatars(true, avatarId); @@ -267,7 +531,7 @@ async function bindUserNameToPersona(e) { setPersonaDescription(); } -export function selectCurrentPersona() { +function selectCurrentPersona() { const personaName = power_user.personas[user_avatar]; if (personaName) { const lockedPersona = chat_metadata['persona']; @@ -623,6 +887,30 @@ async function onPersonasRestoreInput(e) { $('#personas_restore_input').val(''); } +async function syncUserNameToPersona() { + const confirmation = await callPopup(`

Are you sure?

All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm'); + + if (!confirmation) { + return; + } + + for (const mes of chat) { + if (mes.is_user) { + mes.name = name1; + mes.force_avatar = getUserAvatar(user_avatar); + } + } + + await saveChatConditional(); + await reloadCurrentChat(); +} + +export function retriggerFirstMessageOnEmptyChat() { + if (this_chid >= 0 && !selected_group && chat.length === 1) { + $('#firstmessage_textarea').trigger('input'); + } +} + export function initPersonas() { $(document).on('click', '.bind_user_name', bindUserNameToPersona); $(document).on('click', '.set_default_persona', setDefaultPersona); @@ -647,6 +935,51 @@ export function initPersonas() { switchPersonaGridView(); }); + const debouncedPersonaSearch = debounce((searchQuery) => { + personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchQuery); + }); + + $('#persona_search_bar').on('input', function () { + const searchQuery = String($(this).val()); + debouncedPersonaSearch(searchQuery); + }); + + $('#sync_name_button').on('click', syncUserNameToPersona); + $('#avatar_upload_file').on('change', changeUserAvatar); + + $(document).on('click', '#user_avatar_block .avatar-container', function () { + const imgfile = $(this).attr('imgfile'); + setUserAvatar(imgfile); + + // force firstMes {{user}} update on persona switch + retriggerFirstMessageOnEmptyChat(); + }); + + $('#your_name_button').click(async function () { + const userName = String($('#your_name').val()).trim(); + setUserName(userName); + await updatePersonaNameIfExists(user_avatar, userName); + retriggerFirstMessageOnEmptyChat(); + }); + + $(document).on('click', '#user_avatar_block .avatar_upload', function () { + $('#avatar_upload_overwrite').val(''); + $('#avatar_upload_file').trigger('click'); + }); + + $(document).on('click', '#user_avatar_block .set_persona_image', function (e) { + e.stopPropagation(); + const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); + + if (!avatarId) { + console.log('no imgfile'); + return; + } + + $('#avatar_upload_overwrite').val(avatarId); + $('#avatar_upload_file').trigger('click'); + }); + eventSource.on('charManagementDropdown', (target) => { if (target === 'convert_to_persona') { convertCharacterToPersona(); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 53f259507..0e49b2757 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -23,7 +23,6 @@ import { name2, reloadCurrentChat, removeMacros, - retriggerFirstMessageOnEmptyChat, saveChatConditional, sendMessageAsUser, sendSystemMessage, @@ -44,7 +43,7 @@ import { getContext, saveMetadataDebounced } from './extensions.js'; import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; import { chat_completion_sources, oai_settings } from './openai.js'; -import { autoSelectPersona } from './personas.js'; +import { autoSelectPersona, retriggerFirstMessageOnEmptyChat } from './personas.js'; import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js'; diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 387ca5c84..33b51ee42 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -1443,8 +1443,8 @@ export function includesIgnoreCaseAndAccents(text, searchTerm) { if (!text || !searchTerm) return false; // Return false if either string is empty // Normalize and remove diacritics, then convert to lower case - const normalizedText = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); - const normalizedSearchTerm = searchTerm.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); + const normalizedText = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); + const normalizedSearchTerm = searchTerm.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase(); // Check if the normalized text includes the normalized search term return normalizedText.includes(normalizedSearchTerm);