import { showdown, moment, DOMPurify, hljs, Handlebars, SVGInject, Popper, initLibraryShims, default as libs, } from './lib.js'; import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods } from './scripts/RossAscends-mods.js'; import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js'; import { generateKoboldWithStreaming, kai_settings, loadKoboldSettings, getKoboldGenerationData, kai_flags, koboldai_settings, koboldai_setting_names, initKoboldSettings, } from './scripts/kai-settings.js'; import { textgenerationwebui_settings as textgen_settings, loadTextGenSettings, generateTextGenWithStreaming, getTextGenGenerationData, textgen_types, parseTextgenLogprobs, parseTabbyLogprobs, initTextGenSettings, } from './scripts/textgen-settings.js'; import { world_info, getWorldInfoPrompt, getWorldInfoSettings, setWorldInfoSettings, world_names, importEmbeddedWorldInfo, checkEmbeddedWorld, setWorldInfoButtonClass, wi_anchor_position, world_info_include_names, initWorldInfo, charUpdatePrimaryWorld, charSetAuxWorlds, } from './scripts/world-info.js'; import { groups, selected_group, saveGroupChat, getGroups, generateGroupWrapper, is_group_generating, resetSelectedGroup, select_group_chats, regenerateGroup, group_generation_id, getGroupChat, renameGroupMember, createNewGroupChat, getGroupAvatar, editGroup, deleteGroupChat, renameGroupChat, importGroupChat, getGroupBlock, getGroupCharacterCards, getGroupDepthPrompts, } from './scripts/group-chats.js'; import { collapseNewlines, loadPowerUserSettings, playMessageSound, fixMarkdown, power_user, persona_description_positions, loadMovingUIState, getCustomStoppingStrings, MAX_CONTEXT_DEFAULT, MAX_RESPONSE_DEFAULT, renderStoryString, sortEntitiesList, registerDebugFunction, flushEphemeralStoppingStrings, resetMovableStyles, forceCharacterEditorTokenize, applyPowerUserSettings, generatedTextFiltered, applyStylePins, } from './scripts/power-user.js'; import { setOpenAIMessageExamples, setOpenAIMessages, setupChatCompletionPromptManager, prepareOpenAIMessages, sendOpenAIRequest, loadOpenAISettings, oai_settings, openai_messages_count, chat_completion_sources, getChatCompletionModel, proxies, loadProxyPresets, selected_proxy, initOpenAI, } from './scripts/openai.js'; import { generateNovelWithStreaming, getNovelGenerationData, getKayraMaxContextTokens, loadNovelSettings, nai_settings, adjustNovelInstructionPrompt, parseNovelAILogprobs, novelai_settings, novelai_setting_names, initNovelAISettings, } from './scripts/nai-settings.js'; import { initBookmarks, showBookmarksButtons, updateBookmarkDisplay, } from './scripts/bookmarks.js'; import { horde_settings, loadHordeSettings, generateHorde, getStatusHorde, getHordeModels, adjustHordeGenerationParams, isHordeGenerationNotAllowed, MIN_LENGTH, initHorde, } from './scripts/horde.js'; import { debounce, delay, trimToEndSentence, countOccurrences, isOdd, sortMoments, timestampToMoment, download, isDataURL, getCharaFilename, PAGINATION_TEMPLATE, waitUntilCondition, escapeRegex, resetScrollHeight, onlyUnique, getBase64Async, humanFileSize, Stopwatch, isValidUrl, ensureImageFormatSupported, flashHighlight, toggleDrawer, isElementInViewport, copyText, escapeHtml, saveBase64AsFile, uuidv4, equalsIgnoreCaseAndAccents, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler, importFromExternalUrl, shiftUpByOne, shiftDownByOne, canUseNegativeLookbehind, trimSpaces, clamp, } from './scripts/utils.js'; import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './scripts/constants.js'; import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js'; import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js'; import { tag_map, tags, filterByTagState, isBogusFolder, isBogusFolderOpen, chooseBogusFolder, getTagBlock, loadTagsSettings, printTagFilters, getTagKeyForEntity, printTagList, createTagMapFromList, renameTagKey, importTags, tag_filter_type, compareTagsForSort, initTags, applyTagsOnCharacterSelect, applyTagsOnGroupSelect, tag_import_setting, applyCharacterTagsToMessageDivs, } from './scripts/tags.js'; import { initSecrets, readSecretState } from './scripts/secrets.js'; import { markdownExclusionExt } from './scripts/showdown-exclusion.js'; import { markdownUnderscoreExt } from './scripts/showdown-underscore.js'; import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js'; import { registerPromptManagerMigration } from './scripts/PromptManager.js'; import { getRegexedString, regex_placement } from './scripts/extensions/regex/engine.js'; import { initLogprobs, saveLogprobsForActiveMessage } from './scripts/logprobs.js'; import { FILTER_STATES, FILTER_TYPES, FilterHelper, isFilterState } from './scripts/filters.js'; import { getCfgPrompt, getGuidanceScale, initCfg } from './scripts/cfg-scale.js'; import { force_output_sequence, formatInstructModeChat, formatInstructModePrompt, formatInstructModeExamples, formatInstructModeStoryString, getInstructStoppingSequences, } from './scripts/instruct-mode.js'; import { initLocales, t } from './scripts/i18n.js'; import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache } from './scripts/tokenizers.js'; import { user_avatar, getUserAvatars, getUserAvatar, setUserAvatar, initPersonas, setPersonaDescription, initUserAvatar, updatePersonaConnectionsAvatarList, isPersonaPanelOpen, } from './scripts/personas.js'; import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay } from './scripts/BulkEditOverlay.js'; import { initTextGenModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js'; import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js'; import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; import { initScrapers } from './scripts/scrapers.js'; import { initCustomSelectedSamplers, validateDisabledSamplers } from './scripts/samplerSelect.js'; import { DragAndDropHandler } from './scripts/dragdrop.js'; import { INTERACTABLE_CONTROL_CLASS, initKeyboard } from './scripts/keyboard.js'; import { initDynamicStyles } from './scripts/dynamic-styles.js'; import { initInputMarkdown } from './scripts/input-md-formatting.js'; import { AbortReason } from './scripts/util/AbortReason.js'; import { initSystemPrompts } from './scripts/sysprompt.js'; import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js'; import { ToolManager } from './scripts/tool-calling.js'; import { addShowdownPatch } from './scripts/util/showdown-patch.js'; import { applyBrowserFixes } from './scripts/browser-fixes.js'; import { initServerHistory } from './scripts/server-history.js'; import { initSettingsSearch } from './scripts/setting-search.js'; import { initBulkEdit } from './scripts/bulk-edit.js'; import { getContext } from './scripts/st-context.js'; import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; import { accountStorage } from './scripts/util/AccountStorage.js'; import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js'; import { initDataMaid } from './scripts/data-maid.js'; import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js'; import { getSystemMessageByType, initSystemMessages, SAFETY_CHAT, sendSystemMessage, system_message_types, system_messages } from './scripts/system-messages.js'; import { event_types, eventSource } from './scripts/events.js'; import { initAccessibility } from './scripts/a11y.js'; import { applyStreamFadeIn } from './scripts/util/stream-fadein.js'; import { initDomHandlers } from './scripts/dom-handlers.js'; import { SimpleMutex } from './scripts/util/SimpleMutex.js'; import { AudioPlayer } from './scripts/audio-player.js'; // API OBJECT FOR EXTERNAL WIRING globalThis.SillyTavern = { libs, getContext, }; export { user_avatar, setUserAvatar, getUserAvatars, getUserAvatar, nai_settings, isOdd, countOccurrences, renderTemplate, promptItemize, itemizedPrompts, saveItemizedPrompts, loadItemizedPrompts, itemizedParams, clearItemizedPrompts, replaceItemizedPromptText, deleteItemizedPrompts, findItemizedPromptSet, koboldai_settings, koboldai_setting_names, novelai_settings, novelai_setting_names, UNIQUE_APIS, CONNECT_API_MAP, system_messages, system_message_types, sendSystemMessage, getSystemMessageByType, event_types, eventSource, /** @deprecated Use setCharacterSettingsOverrides instead. */ setCharacterSettingsOverrides as setScenarioOverride, /** @deprecated Use appendMediaToMessage instead. */ appendMediaToMessage as appendImageToMessage, }; /** * Wait for page to load before continuing the app initialization. */ await new Promise((resolve) => { if (document.readyState === 'complete') { resolve(); } else { window.addEventListener('load', resolve); } }); // Configure toast library: toastr.options = { positionClass: 'toast-top-center', closeButton: false, progressBar: false, showDuration: 250, hideDuration: 250, timeOut: 4000, extendedTimeOut: 10000, showEasing: 'linear', hideEasing: 'linear', showMethod: 'fadeIn', hideMethod: 'fadeOut', escapeHtml: true, onHidden: function () { // If we have any dialog still open, the last "hidden" toastr will remove the toastr-container. We need to keep it alive inside the dialog though // so the toasts still show up inside there. fixToastrForDialogs(); }, onShown: function () { // Set tooltip to the notification message $(this).attr('title', t`Tap to close`); }, }; export const characterGroupOverlay = new BulkEditOverlay(); // Markdown converter export let mesForShowdownParse; //intended to be used as a context to compare showdown strings against /** @type {import('showdown').Converter} */ export let converter; // array for prompt token calculations export const systemUserName = 'SillyTavern System'; export const neutralCharacterName = 'Assistant'; let default_user_name = 'User'; export let name1 = default_user_name; export let name2 = systemUserName; /** @type {ChatMessage[]} */ export let chat = []; export let isSwipingAllowed = true; //false when a swipe is in progress, or swiping is blocked. let chatSaveTimeout; let importFlashTimeout; export let isChatSaving = false; let chat_create_date = ''; let firstRun = false; let settingsReady = false; let currentVersion = '0.0.0'; export let displayVersion = 'SillyTavern'; let generation_started = new Date(); /** @type {import('./scripts/char-data.js').v1CharData[]} */ export let characters = []; /** * Stringified index of a currently chosen entity in the characters array. * @type {string|undefined} Yes, we hate it as much as you do. */ export let this_chid; let saveCharactersPage = 0; export const default_avatar = 'img/ai4.png'; export const system_avatar = 'img/five.png'; export const comment_avatar = 'img/quill.png'; export const default_user_avatar = 'img/user-default.png'; export let CLIENT_VERSION = 'SillyTavern:UNKNOWN:Cohee#1207'; // For Horde header let optionsPopper = Popper.createPopper(document.getElementById('options_button'), document.getElementById('options'), { placement: 'top-start', }); let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), { placement: 'left', }); let isExportPopupOpen = false; // Saved here for performance reasons const messageTemplate = $('#message_template .mes'); export const chatElement = $('#chat'); let dialogueResolve = null; let dialogueCloseStop = false; export let chat_metadata = {}; /** @type {StreamingProcessor} */ export let streamingProcessor = null; let crop_data = undefined; let is_delete_mode = false; let fav_ch_checked = false; let scrollLock = false; export let abortStatusCheck = new AbortController(); export let charDragDropHandler = null; export let chatDragDropHandler = null; /** @type {debounce_timeout} The debounce timeout used for chat/settings save. debounce_timeout.long: 1.000 ms */ export const DEFAULT_SAVE_EDIT_TIMEOUT = debounce_timeout.relaxed; /** @type {debounce_timeout} The debounce timeout used for printing. debounce_timeout.quick: 100 ms */ export const DEFAULT_PRINT_TIMEOUT = debounce_timeout.quick; export const saveSettingsDebounced = debounce((loopCounter = 0) => saveSettings(loopCounter), DEFAULT_SAVE_EDIT_TIMEOUT); export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), DEFAULT_SAVE_EDIT_TIMEOUT); /** * Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds. * Use this function instead of a direct `printCharacters()` whenever the reprinting of the character list is not the primary focus. * * The printing will also always reprint all filter options of the global list, to keep them up to date. */ export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT); /** * @enum {number} Extension prompt types */ export const extension_prompt_types = { NONE: -1, IN_PROMPT: 0, IN_CHAT: 1, BEFORE_PROMPT: 2, }; /** * @enum {number} Extension prompt roles */ export const extension_prompt_roles = { SYSTEM: 0, USER: 1, ASSISTANT: 2, }; export const MAX_INJECTION_DEPTH = 10000; async function getClientVersion() { try { const response = await fetch('/version'); const data = await response.json(); CLIENT_VERSION = data.agent; displayVersion = `SillyTavern ${data.pkgVersion}`; currentVersion = data.pkgVersion; if (data.gitRevision && data.gitBranch) { displayVersion += ` '${data.gitBranch}' (${data.gitRevision})`; } $('#version_display').text(displayVersion); $('#version_display_welcome').text(displayVersion); } catch (err) { console.error('Couldn\'t get client version', err); } } export function reloadMarkdownProcessor() { converter = new showdown.Converter({ emoji: true, literalMidWordUnderscores: true, parseImgDimensions: true, tables: true, underline: true, simpleLineBreaks: true, strikethrough: true, disableForced4SpacesIndentedSublists: true, extensions: [markdownUnderscoreExt()], }); // Inject the dinkus extension after creating the converter // Maybe move this into power_user init? converter.addExtension(markdownExclusionExt(), 'exclusion'); return converter; } export function getCurrentChatId() { if (selected_group) { return groups.find(x => x.id == selected_group)?.chat_id; } else if (this_chid !== undefined) { return characters[this_chid]?.chat; } } 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; /** * The type of the right menu * @typedef {'characters' | 'character_edit' | 'create' | 'group_edit' | 'group_create' | '' } MenuType */ /** * The type of the right menu that is currently open * @type {MenuType} */ export let menu_type = ''; export let selected_button = ''; //which button pressed //create pole save export let create_save = { name: '', description: '', creator_notes: '', post_history_instructions: '', character_version: '', system_prompt: '', tags: '', creator: '', personality: '', first_message: '', /** @type {FileList|null} */ avatar: null, scenario: '', mes_example: '', world: '', talkativeness: talkativeness_default, alternate_greetings: [], depth_prompt_prompt: '', depth_prompt_depth: depth_prompt_depth_default, depth_prompt_role: depth_prompt_role_default, extensions: {}, extra_books: [], }; //animation right menu export const ANIMATION_DURATION_DEFAULT = 125; export let animation_duration = ANIMATION_DURATION_DEFAULT; export let animation_easing = 'ease-in-out'; let popup_type = ''; let chat_file_for_del = ''; export let online_status = 'no_connection'; export let is_send_press = false; //Send generation let this_del_mes = -1; /** @type {string} */ let this_edit_mes_chname = ''; /** @type {number|undefined} */ let this_edit_mes_id = undefined; //settings export let settings; export let amount_gen = 80; //default max length of AI generated responses export let max_context = 2048; var swipes = true; export let extension_prompts = {}; export let main_api;// = "kobold"; /** @type {AbortController} */ let abortController; //css var css_send_form_display = $('
').css('display'); var kobold_horde_model = ''; export let token; /** The tag of the active character. (NOT the id) */ export let active_character = ''; /** The tag of the active group. (Coincidentally also the id) */ export let active_group = ''; export const entitiesFilter = new FilterHelper(printCharactersDebounced); export function getRequestHeaders({ omitContentType = false } = {}) { const headers = { 'Content-Type': 'application/json', 'X-CSRF-Token': token, }; if (omitContentType) { delete headers['Content-Type']; } return headers; } export function getSlideToggleOptions() { return { miliseconds: animation_duration * 1.5, transitionFunction: animation_duration > 0 ? 'ease-in-out' : 'step-start', }; } $.ajaxPrefilter((options, originalOptions, xhr) => { xhr.setRequestHeader('X-CSRF-Token', token); }); /** * Pings the STserver to check if it is reachable. * @returns {Promise} True if the server is reachable, false otherwise. */ export async function pingServer() { try { const result = await fetch('api/ping', { method: 'POST', headers: getRequestHeaders(), }); if (!result.ok) { return false; } return true; } catch (error) { console.error('Error pinging server', error); return false; } } //MARK: firstLoadInit async function firstLoadInit() { try { const tokenResponse = await fetch('/csrf-token'); const tokenData = await tokenResponse.json(); token = tokenData.token; } catch { toastr.error(t`Couldn't get CSRF token. Please refresh the page.`, t`Error`, { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true }); throw new Error('Initialization failed'); } showLoader(); registerPromptManagerMigration(); initDomHandlers(); initStandaloneMode(); initLibraryShims(); addShowdownPatch(showdown); addDOMPurifyHooks(); reloadMarkdownProcessor(); applyBrowserFixes(); await getClientVersion(); await initSecrets(); await readSecretState(); await initLocales(); initChatUtilities(); initDefaultSlashCommands(); initTextGenModels(); initOpenAI(); initTextGenSettings(); initKoboldSettings(); initNovelAISettings(); initSystemPrompts(); initExtensions(); initExtensionSlashCommands(); ToolManager.initToolSlashCommands(); await initPresetManager(); await initSystemMessages(); await getSettings(); initKeyboard(); initDynamicStyles(); initTags(); initBookmarks(); initMacros(); await getUserAvatars(true, user_avatar); await getCharacters(); await getBackgrounds(); await initTokenizers(); initBackgrounds(); initAuthorsNote(); await initPersonas(); initWorldInfo(); initHorde(); initRossMods(); initStats(); initCfg(); initLogprobs(); initInputMarkdown(); initServerHistory(); initSettingsSearch(); initBulkEdit(); initReasoning(); initWelcomeScreen(); await initScrapers(); initCustomSelectedSamplers(); initDataMaid(); initItemizedPrompts(); initAccessibility(); addDebugFunctions(); doDailyExtensionUpdatesCheck(); await hideLoader(); await fixViewport(); await eventSource.emit(event_types.APP_READY); } async function fixViewport() { document.body.style.position = 'absolute'; await delay(1); document.body.style.position = ''; } function initStandaloneMode() { const isPwaMode = window.matchMedia('(display-mode: standalone)').matches; if (isPwaMode) { $('body').addClass('PWA'); } } function cancelStatusCheck(reason = 'Manually cancelled status check') { abortStatusCheck?.abort(new AbortReason(reason)); abortStatusCheck = new AbortController(); setOnlineStatus('no_connection'); } export function displayOnlineStatus() { if (online_status == 'no_connection') { $('.online_status_indicator').removeClass('success'); $('.online_status_text').text($('#API-status-top').attr('no_connection_text')); } else { $('.online_status_indicator').addClass('success'); $('.online_status_text').text(online_status); } } /** * Sets the duration of JS animations. * @param {number} ms Duration in milliseconds. Resets to default if null. */ export function setAnimationDuration(ms = null) { animation_duration = ms ?? ANIMATION_DURATION_DEFAULT; // Set CSS variable to document document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`); } /** * Sets the currently active character * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active character is reset to `null`. */ export function setActiveCharacter(entityOrKey) { active_character = entityOrKey ? getTagKeyForEntity(entityOrKey) : null; if (active_character) active_group = null; } /** * Sets the currently active group. * @param {object|number|string} [entityOrKey] - An entity with id property (character, group, tag), or directly an id or tag key. If not provided, the active group is reset to `null`. */ export function setActiveGroup(entityOrKey) { active_group = entityOrKey ? getTagKeyForEntity(entityOrKey) : null; if (active_group) active_character = null; } export function startStatusLoading() { $('.api_loading').show(); $('.api_button').addClass('disabled'); } export function stopStatusLoading() { $('.api_loading').hide(); $('.api_button').removeClass('disabled'); } export function resultCheckStatus() { displayOnlineStatus(); stopStatusLoading(); } /** * Switches the currently selected character to the one with the given ID. (character index, not the character key!) * * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing. * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group. * @param {number} id The ID of the character to switch to. * @param {object} [options] Options for the switch. * @param {boolean} [options.switchMenu=true] Whether to switch the right menu to the character edit menu if the character is already selected. * @returns {Promise} A promise that resolves when the character is switched. */ export async function selectCharacterById(id, { switchMenu = true } = {}) { if (characters[id] === undefined) { return; } if (isChatSaving) { toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`); return; } if (selected_group && is_group_generating) { return; } if (selected_group || String(this_chid) !== String(id)) { //if clicked on a different character from what was currently selected if (!is_send_press) { await clearChat(); cancelTtsPlay(); resetSelectedGroup(); this_edit_mes_id = undefined; selected_button = 'character_edit'; setCharacterId(id); chat.length = 0; chat_metadata = {}; await getChat(); } } else { //if clicked on character that was already selected switchMenu && (selected_button = 'character_edit'); await unshallowCharacter(this_chid); select_selected_character(this_chid, { switchMenu }); } } function getBackBlock() { const template = $('#bogus_folder_back_template .bogus_folder_select').clone(); return template; } async function getEmptyBlock() { const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog']; const texts = [t`Here be dragons`, t`Otterly empty`, t`Kiwibunga`, t`Pump-a-Rum`, t`Croak it`]; const roll = new Date().getMinutes() % icons.length; const params = { text: texts[roll], icon: icons[roll], }; const emptyBlock = await renderTemplateAsync('emptyBlock', params); return $(emptyBlock); } /** * @param {number} hidden Number of hidden characters */ async function getHiddenBlock(hidden) { const params = { text: (hidden > 1 ? t`${hidden} characters hidden.` : t`${hidden} character hidden.`), }; const hiddenBlock = await renderTemplateAsync('hiddenBlock', params); return $(hiddenBlock); } function getCharacterBlock(item, id) { let this_avatar = default_avatar; if (item.avatar != 'none') { this_avatar = getThumbnailUrl('avatar', item.avatar); } // Populate the template const template = $('#character_template .character_select').clone(); template.attr({ 'data-chid': id, 'id': `CharID${id}` }); template.find('img').attr('src', this_avatar).attr('alt', item.name); template.find('.avatar').attr('title', `[Character] ${item.name}\nFile: ${item.avatar}`); template.find('.ch_name').text(item.name).attr('title', `[Character] ${item.name}`); if (power_user.show_card_avatar_urls) { template.find('.ch_avatar_url').text(item.avatar); } template.find('.ch_fav_icon').css('display', 'none'); template.toggleClass('is_fav', item.fav || item.fav == 'true'); template.find('.ch_fav').val(item.fav); const isAssistant = item.avatar === getPermanentAssistantAvatar(); if (!isAssistant) { template.find('.ch_assistant').remove(); } const description = item.data?.creator_notes || ''; if (description) { template.find('.ch_description').text(description); } else { template.find('.ch_description').hide(); } 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(); } // Display inline tags const tagsElement = template.find('.tags'); printTagList(tagsElement, { forEntityOrKey: id, tagOptions: { isCharacterList: true } }); // Add to the list return template; } /** * Prints the global character list, optionally doing a full refresh of the list * Use this function whenever the reprinting of the character list is the primary focus, otherwise using `printCharactersDebounced` is preferred for a cleaner, non-blocking experience. * * The printing will also always reprint all filter options of the global list, to keep them up to date. * * @param {boolean} fullRefresh - If true, the list is fully refreshed and the navigation is being reset */ export async function printCharacters(fullRefresh = false) { const storageKey = 'Characters_PerPage'; const listId = '#rm_print_characters_block'; let currentScrollTop = $(listId).scrollTop(); if (fullRefresh) { saveCharactersPage = 0; currentScrollTop = 0; await delay(1); } // Before printing the personas, we check if we should enable/disable search sorting verifyCharactersSearchSortRule(); // We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date printTagFilters(tag_filter_type.character); printTagFilters(tag_filter_type.group_member); // We are also always reprinting the lists on character/group edit window, as these ones doesn't get updated otherwise applyTagsOnCharacterSelect(); applyTagsOnGroupSelect(); const entities = getEntitiesList({ doFilter: true }); const pageSize = Number(accountStorage.getItem(storageKey)) || per_page_default; const sizeChangerOptions = [10, 25, 50, 100, 250, 500, 1000]; $('#rm_print_characters_pagination').pagination({ dataSource: entities, pageSize, pageRange: 1, pageNumber: saveCharactersPage || 1, position: 'top', showPageNumbers: false, showSizeChanger: true, prevText: '<', nextText: '>', formatNavigator: PAGINATION_TEMPLATE, formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions), showNavigator: true, callback: async function (/** @type {Entity[]} */ data) { $(listId).empty(); if (power_user.bogus_folders && isBogusFolderOpen()) { $(listId).append(getBackBlock()); } if (!data.length) { const emptyBlock = await getEmptyBlock(); $(listId).append(emptyBlock); } let displayCount = 0; for (const i of data) { switch (i.type) { case 'character': $(listId).append(getCharacterBlock(i.item, i.id)); displayCount++; break; case 'group': $(listId).append(getGroupBlock(i.item)); displayCount++; break; case 'tag': $(listId).append(getTagBlock(i.item, i.entities, i.hidden, i.isUseless)); break; } } const hidden = (characters.length + groups.length) - displayCount; if (hidden > 0 && entitiesFilter.hasAnyFilter()) { const hiddenBlock = await getHiddenBlock(hidden); $(listId).append(hiddenBlock); } localizePagination($('#rm_print_characters_pagination')); eventSource.emit(event_types.CHARACTER_PAGE_LOADED); }, afterSizeSelectorChange: function (e, size) { accountStorage.setItem(storageKey, e.target.value); paginationDropdownChangeHandler(e, size); }, afterPaging: function (e) { saveCharactersPage = e; }, afterRender: function () { $(listId).scrollTop(currentScrollTop); }, }); favsToHotswap(); updatePersonaConnectionsAvatarList(); } /** Checks the state of the current search, and adds/removes the search sorting option accordingly */ function verifyCharactersSearchSortRule() { const searchTerm = entitiesFilter.getFilterData(FILTER_TYPES.SEARCH); const searchOption = $('#character_sort_order option[data-field="search"]'); const selector = $('#character_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'); searchOption.prop('selected', true); 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', ''); $(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true); } } /** @typedef {object} Character - A character */ /** @typedef {object} Group - A group */ /** * @typedef {object} Entity - Object representing a display entity * @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item * @property {string|number} id - The id * @property {'character'|'group'|'tag'} type - The type of this entity (character, group, tag) * @property {Entity[]?} [entities=null] - An optional list of entities relevant for this item * @property {number?} [hidden=null] - An optional number representing how many hidden entities this entity contains * @property {boolean?} [isUseless=null] - Specifies if the entity is useless (not relevant, but should still be displayed for consistency) and should be displayed greyed out */ /** * Converts the given character to its entity representation * * @param {Character} character - The character * @param {string|number} id - The id of this character * @returns {Entity} The entity for this character */ export function characterToEntity(character, id) { return { item: character, id, type: 'character' }; } /** * Converts the given group to its entity representation * * @param {Group} group - The group * @returns {Entity} The entity for this group */ export function groupToEntity(group) { return { item: group, id: group.id, type: 'group' }; } /** * Converts the given tag to its entity representation * * @param {import('./scripts/tags.js').Tag} tag - The tag * @returns {Entity} The entity for this tag */ export function tagToEntity(tag) { return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] }; } /** * Builds the full list of all entities available * * They will be correctly marked and filtered. * * @param {object} param0 - Optional parameters * @param {boolean} [param0.doFilter] - Whether this entity list should already be filtered based on the global filters * @param {boolean} [param0.doSort] - Whether the entity list should be sorted when returned * @returns {Entity[]} All entities */ export function getEntitiesList({ doFilter = false, doSort = true } = {}) { let entities = [ ...characters.map((item, index) => characterToEntity(item, index)), ...groups.map(item => groupToEntity(item)), ...(power_user.bogus_folders ? tags.filter(isBogusFolder).sort(compareTagsForSort).map(item => tagToEntity(item)) : []), ]; // We need to do multiple filter runs in a specific order, otherwise different settings might override each other // and screw up tags and search filter, sub lists or similar. // The specific filters are written inside the "filterByTagState" method and its different parameters. // Generally what we do is the following: // 1. First swipe over the list to remove the most obvious things // 2. Build sub entity lists for all folders, filtering them similarly to the second swipe // 3. We do the last run, where global filters are applied, and the search filters last // First run filters, that will hide what should never be displayed if (doFilter) { entities = filterByTagState(entities); } // Run over all entities between first and second filter to save some states for (const entity of entities) { // For folders, we remember the sub entities so they can be displayed later, even if they might be filtered // Those sub entities should be filtered and have the search filters applied too if (entity.type === 'tag') { let subEntities = filterByTagState(entities, { subForEntity: entity, filterHidden: false }); const subCount = subEntities.length; subEntities = filterByTagState(entities, { subForEntity: entity }); if (doFilter) { // sub entities filter "hacked" because folder filter should not be applied there, so even in "only folders" mode characters show up subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false, tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false }); } if (doSort) { sortEntitiesList(subEntities, false); } entity.entities = subEntities; entity.hidden = subCount - subEntities.length; } } // Second run filters, hiding whatever should be filtered later if (doFilter) { const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true }); entities = entitiesFilter.applyFilters(beforeFinalEntities, { clearFuzzySearchCaches: false }); // Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters. if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) { entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false }); } } // Final step, updating some properties after the last filter run const nonTagEntitiesCount = entities.filter(entity => entity.type !== 'tag').length; for (const entity of entities) { if (entity.type === 'tag') { if (entity.entities?.length == nonTagEntitiesCount) entity.isUseless = true; } } // Sort before returning if requested if (doSort) { sortEntitiesList(entities, false); } entitiesFilter.clearFuzzySearchCaches(); return entities; } export async function getOneCharacter(avatarUrl) { const response = await fetch('/api/characters/get', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ avatar_url: avatarUrl, }), }); if (response.ok) { const getData = await response.json(); getData['name'] = DOMPurify.sanitize(getData['name']); getData['chat'] = String(getData['chat']); const indexOf = characters.findIndex(x => x.avatar === avatarUrl); if (indexOf !== -1) { characters[indexOf] = getData; } else { toastr.error(t`Character ${avatarUrl} not found in the list`, t`Error`, { timeOut: 5000, preventDuplicates: true }); } } } function getCharacterSource(chId = this_chid) { const character = characters[chId]; if (!character) { return ''; } const chubId = characters[chId]?.data?.extensions?.chub?.full_path; if (chubId) { return `https://chub.ai/characters/${chubId}`; } const pygmalionId = characters[chId]?.data?.extensions?.pygmalion_id; if (pygmalionId) { return `https://pygmalion.chat/${pygmalionId}`; } const githubRepo = characters[chId]?.data?.extensions?.github_repo; if (githubRepo) { return `https://github.com/${githubRepo}`; } const sourceUrl = characters[chId]?.data?.extensions?.source_url; if (sourceUrl) { return sourceUrl; } const risuId = characters[chId]?.data?.extensions?.risuai?.source; if (Array.isArray(risuId) && risuId.length && typeof risuId[0] === 'string' && risuId[0].startsWith('risurealm:')) { const realmId = risuId[0].split(':')[1]; return `https://realm.risuai.net/character/${realmId}`; } const perchanceSlug = characters[chId]?.data?.extensions?.perchance_data?.slug; if (perchanceSlug) { return `https://perchance.org/ai-character-chat?data=${perchanceSlug}`; } return ''; } export async function getCharacters() { const response = await fetch('/api/characters/all', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({}), }); if (response.ok) { const previousAvatar = this_chid !== undefined ? characters[this_chid]?.avatar : null; characters.splice(0, characters.length); const getData = await response.json(); for (let i = 0; i < getData.length; i++) { characters[i] = getData[i]; characters[i]['name'] = DOMPurify.sanitize(characters[i]['name']); // For dropped-in cards if (!characters[i]['chat']) { characters[i]['chat'] = `${characters[i]['name']} - ${humanizedDateTime()}`; } characters[i]['chat'] = String(characters[i]['chat']); } if (previousAvatar) { const newCharacterId = characters.findIndex(x => x.avatar === previousAvatar); if (newCharacterId >= 0) { setCharacterId(newCharacterId); await selectCharacterById(newCharacterId, { switchMenu: false }); } else { await Popup.show.text(t`ERROR: The active character is no longer available.`, t`The page will be refreshed to prevent data loss. Press "OK" to continue.`); return location.reload(); } } await getGroups(); await printCharacters(true); } else { console.error('Failed to fetch characters:', response.statusText); const errorData = await response.json(); if (errorData?.overflow) { await Popup.show.text(t`Character data length limit reached`, t`To resolve this, set "performance.lazyLoadCharacters" to "true" in config.yaml and restart the server.`); } } } async function delChat(chatfile) { const response = await fetch('/api/chats/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ chatfile: chatfile, avatar_url: characters[this_chid].avatar, }), }); if (response.ok === true) { // choose another chat if current was deleted const name = chatfile.replace('.jsonl', ''); if (name === characters[this_chid].chat) { chat_metadata = {}; await replaceCurrentChat(); } await eventSource.emit(event_types.CHAT_DELETED, name); } } /** * Deletes a character chat by its name. * @param {string} characterId Character ID to delete chat for * @param {string} fileName Name of the chat file to delete (without .jsonl extension) * @returns {Promise} A promise that resolves when the chat is deleted. */ export async function deleteCharacterChatByName(characterId, fileName) { // Make sure all the data is loaded. await unshallowCharacter(characterId); /** @type {import('./scripts/char-data.js').v1CharData} */ const character = characters[characterId]; if (!character) { console.warn(`Character with ID ${characterId} not found.`); return; } const response = await fetch('/api/chats/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ chatfile: `${fileName}.jsonl`, avatar_url: character.avatar, }), }); if (!response.ok) { console.error('Failed to delete chat for character.'); return; } if (fileName === character.chat) { const chatsResponse = await fetch('/api/characters/chats', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ avatar_url: character.avatar }), }); const chats = Object.values(await chatsResponse.json()); chats.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); const newChatName = chats.length && typeof chats[0] === 'object' ? chats[0].file_name.replace('.jsonl', '') : `${character.name} - ${humanizedDateTime()}`; await updateRemoteChatName(characterId, newChatName); } await eventSource.emit(event_types.CHAT_DELETED, fileName); } export async function replaceCurrentChat() { await clearChat(); chat.length = 0; const chatsResponse = await fetch('/api/characters/chats', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ avatar_url: characters[this_chid].avatar }), }); if (chatsResponse.ok) { const chats = Object.values(await chatsResponse.json()); chats.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); // pick existing chat if (chats.length && typeof chats[0] === 'object') { characters[this_chid].chat = chats[0].file_name.replace('.jsonl', ''); $('#selected_chat_pole').val(characters[this_chid].chat); saveCharacterDebounced(); await getChat(); } // start new chat else { characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`; $('#selected_chat_pole').val(characters[this_chid].chat); saveCharacterDebounced(); await getChat(); } } } export async function showMoreMessages(messagesToLoad = null) { const firstDisplayedMesId = chatElement.children('.mes').first().attr('mesid'); let messageId = Number(firstDisplayedMesId); let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id, // so the first "new" message being shown will be the last available message if (isNaN(messageId)) { messageId = getLastMessageId() + 1; } console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); const prevHeight = chatElement.prop('scrollHeight'); const isButtonInView = isElementInViewport($('#show_more_messages')[0]); while (messageId > 0 && count > 0) { let newMessageId = messageId - 1; addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId }); count--; messageId--; } if (messageId == 0) { $('#show_more_messages').remove(); } if (isButtonInView) { const newHeight = chatElement.prop('scrollHeight'); chatElement.scrollTop(newHeight - prevHeight); } applyStylePins(); await eventSource.emit(event_types.MORE_MESSAGES_LOADED); } export async function printMessages() { let startIndex = 0; let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; if (chat.length > count) { startIndex = chat.length - count; chatElement.append('
Show more messages
'); } for (let i = startIndex; i < chat.length; i++) { const item = chat[i]; addOneMessage(item, { scroll: false, forceId: i, showSwipes: false }); } chatElement.find('.mes').removeClass('last_mes'); chatElement.find('.mes').last().addClass('last_mes'); refreshSwipeButtons(); applyStylePins(); scrollChatToBottom(); delay(debounce_timeout.short).then(() => scrollOnMediaLoad()); } function scrollOnMediaLoad() { const started = Date.now(); const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray(); let mediaLoaded = 0; for (const currentElement of media) { if (currentElement instanceof HTMLImageElement) { if (currentElement.complete) { incrementAndCheck(); } else { currentElement.addEventListener('load', incrementAndCheck); currentElement.addEventListener('error', incrementAndCheck); } } if (currentElement instanceof HTMLMediaElement) { if (currentElement.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) { incrementAndCheck(); } else { currentElement.addEventListener('loadeddata', incrementAndCheck); currentElement.addEventListener('error', incrementAndCheck); } } } function incrementAndCheck() { const MAX_DELAY = 1000; // 1 second if ((Date.now() - started) > MAX_DELAY) { return; } mediaLoaded++; if (mediaLoaded === media.length) { scrollChatToBottom({ waitForFrame: true }); } } } /** * Cancels the debounced chat save if it is currently pending. */ export function cancelDebouncedChatSave() { if (chatSaveTimeout) { console.debug('Debounced chat save cancelled'); clearTimeout(chatSaveTimeout); chatSaveTimeout = null; } } export async function clearChat() { cancelDebouncedChatSave(); cancelDebouncedMetadataSave(); closeMessageEditor(); extension_prompts = {}; if (is_delete_mode) { $('#dialogue_del_mes_cancel').trigger('click'); } chatElement.children().remove(); if ($('.zoomed_avatar[forChar]').length) { console.debug('saw avatars to remove'); $('.zoomed_avatar[forChar]').remove(); } else { console.debug('saw no avatars'); } await saveItemizedPrompts(getCurrentChatId()); itemizedPrompts.length = 0; } export async function deleteLastMessage() { chat.length = chat.length - 1; chatElement.children('.mes').last().remove(); await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); } /** * Deletes a message from the chat by its ID, optionally asking for confirmation. * @param {number} id The ID of the message to delete. * @param {number} [swipeDeletionIndex] Deletes the swipe with that index. * @param {boolean} [askConfirmation=false] Whether to ask for confirmation before deleting. */ export async function deleteMessage(id, swipeDeletionIndex = undefined, askConfirmation = false) { const canDeleteSwipe = swipeDeletionIndex !== undefined && swipeDeletionIndex !== null; if (canDeleteSwipe) { if (swipeDeletionIndex < 0) { throw new Error('Swipe index cannot be negative'); } if (!Array.isArray(chat[id].swipes)) { throw new Error('Message has no swipes to delete'); } if (chat[id].swipes.length <= swipeDeletionIndex) { throw new Error('Swipe index out of bounds'); } } const minId = getFirstDisplayedMessageId(); const messageElement = chatElement.find(`.mes[mesid="${id}"]`); if (messageElement.length === 0) { return; } let deleteOnlySwipe = canDeleteSwipe; if (askConfirmation) { const result = await callGenericPopup(t`Are you sure you want to delete this message?`, POPUP_TYPE.CONFIRM, null, { okButton: canDeleteSwipe ? t`Delete Swipe` : t`Delete Message`, cancelButton: 'Cancel', customButtons: canDeleteSwipe ? [t`Delete Message`] : null, }); if (!result) { return; } deleteOnlySwipe = canDeleteSwipe && result === POPUP_RESULT.AFFIRMATIVE; // Default button, not the custom one } if (deleteOnlySwipe) { await deleteSwipe(swipeDeletionIndex, id); return; } chat.splice(id, 1); messageElement.remove(); chat_metadata['tainted'] = true; const startIndex = [0, minId].includes(id) ? id : null; updateViewMessageIds(startIndex); saveChatDebounced(); if (this_edit_mes_id === id) { this_edit_mes_id = undefined; } refreshSwipeButtons(); await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); } export async function reloadCurrentChat() { preserveNeutralChat(); await clearChat(); chat.length = 0; if (selected_group) { await getGroupChat(selected_group, true); } else if (this_chid !== undefined) { await getChat(); } else { resetChatState(); restoreNeutralChat(); await getCharacters(); await printMessages(); await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); } refreshSwipeButtons(); } /** * Send the message currently typed into the chat box. */ export async function sendTextareaMessage() { if (is_send_press) return; if (isExecutingCommandsFromChatInput) return; let generateType = 'normal'; // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last // message was sent from a character (not the user or the system). const textareaText = String($('#send_textarea').val()); if (power_user.continue_on_send && !hasPendingFileAttachment() && !textareaText && !selected_group && chat.length && !chat[chat.length - 1]['is_user'] && !chat[chat.length - 1]['is_system'] ) { generateType = 'continue'; } if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) { await newAssistantChat({ temporary: false }); } return await Generate(generateType); } /** * Formats the message text into an HTML string using Markdown and other formatting. * @param {string} mes Message text * @param {string} ch_name Character name * @param {boolean} isSystem If the message was sent by the system * @param {boolean} isUser If the message was sent by the user * @param {number} messageId Message index in chat array * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides * @param {boolean} [isReasoning] If the message is reasoning output * @returns {string} HTML string */ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) { if (!mes) { return ''; } if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) { const mesBeforeReplace = mes; const chatMessage = chat[messageId]; mes = substituteParams(mes, undefined, ch_name); if (chatMessage && chatMessage.mes === mesBeforeReplace && chatMessage.extra?.display_text !== mesBeforeReplace) { chatMessage.mes = mes; } } mesForShowdownParse = mes; // Force isSystem = false on comment messages so they get formatted properly if (ch_name === COMMENT_NAME_DEFAULT && isSystem && !isUser) { isSystem = false; } // Let hidden messages have markdown if (isSystem && ch_name !== systemUserName) { isSystem = false; } // Prompt bias replacement should be applied on the raw message const replacedPromptBias = power_user.user_prompt_bias && substituteParams(power_user.user_prompt_bias); if (!power_user.show_user_prompt_bias && ch_name && !isUser && !isSystem && replacedPromptBias && mes.startsWith(replacedPromptBias)) { mes = mes.slice(replacedPromptBias.length); } if (!isSystem) { function getRegexPlacement() { try { if (isReasoning) { return regex_placement.REASONING; } if (isUser) { return regex_placement.USER_INPUT; } else if (chat[messageId]?.extra?.type === 'narrator') { return regex_placement.SLASH_COMMAND; } else { return regex_placement.AI_OUTPUT; } } catch { return regex_placement.AI_OUTPUT; } } const regexPlacement = getRegexPlacement(); const usableMessages = chat.map((x, index) => ({ message: x, index: index })).filter(x => !x.message.is_system); const indexOf = usableMessages.findIndex(x => x.index === Number(messageId)); const depth = messageId >= 0 && indexOf !== -1 ? (usableMessages.length - indexOf - 1) : undefined; // Always override the character name mes = getRegexedString(mes, regexPlacement, { characterOverride: ch_name, isMarkdown: true, depth: depth, }); } if (power_user.auto_fix_generated_markdown) { mes = fixMarkdown(mes, true); } if (!isSystem && power_user.encode_tags) { mes = canUseNegativeLookbehind() ? mes.replaceAll('<', '<').replace(new RegExp('(?', 'g'), '>') : mes.replaceAll('<', '<').replaceAll('>', '>'); } // Make sure reasoning strings are always shown, even if they include "<" or ">" [power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => { if (!reasoningString || !reasoningString.trim().length) { return; } // Only replace the first occurrence of the reasoning string if (mes.includes(reasoningString)) { mes = mes.replace(reasoningString, escapeHtml(reasoningString)); } }); if (!isSystem) { // Save double quotes in tags as a special character to prevent them from being encoded if (!power_user.encode_tags) { mes = mes.replace(/<([^>]+)>/g, function (_, contents) { return '<' + contents.replace(/"/g, '\ufffe') + '>'; }); } mes = mes.replace( /