import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods, shouldSendOnEnter } from './scripts/RossAscends-mods.js'; import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js'; import { generateKoboldWithStreaming, kai_settings, loadKoboldSettings, formatKoboldUrl, getKoboldGenerationData, kai_flags, setKoboldFlags, } from './scripts/kai-settings.js'; import { textgenerationwebui_settings as textgen_settings, loadTextGenSettings, generateTextGenWithStreaming, getTextGenGenerationData, textgen_types, getTextGenServer, validateTextGenUrl, parseTextgenLogprobs, parseTabbyLogprobs, } from './scripts/textgen-settings.js'; const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types; import { world_info, getWorldInfoPrompt, getWorldInfoSettings, setWorldInfoSettings, world_names, importEmbeddedWorldInfo, checkEmbeddedWorld, setWorldInfoButtonClass, importWorldInfo, wi_anchor_position, } from './scripts/world-info.js'; import { groups, selected_group, saveGroupChat, getGroups, generateGroupWrapper, deleteGroup, is_group_generating, resetSelectedGroup, select_group_chats, regenerateGroup, group_generation_id, getGroupChat, renameGroupMember, createNewGroupChat, getGroupPastChats, getGroupAvatar, openGroupChat, 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, ui_mode, switchSimpleMode, flushEphemeralStoppingStrings, context_presets, resetMovableStyles, forceCharacterEditorTokenize, } from './scripts/power-user.js'; import { setOpenAIMessageExamples, setOpenAIMessages, setupChatCompletionPromptManager, prepareOpenAIMessages, sendOpenAIRequest, loadOpenAISettings, oai_settings, openai_messages_count, chat_completion_sources, getChatCompletionModel, isOpenRouterWithInstruct, proxies, loadProxyPresets, selected_proxy, } from './scripts/openai.js'; import { generateNovelWithStreaming, getNovelGenerationData, getKayraMaxContextTokens, getNovelTier, loadNovelPreset, loadNovelSettings, nai_settings, adjustNovelInstructionPrompt, loadNovelSubscriptionData, parseNovelAILogprobs, } from './scripts/nai-settings.js'; import { createNewBookmark, showBookmarksButtons, createBranch, } from './scripts/bookmarks.js'; import { horde_settings, loadHordeSettings, generateHorde, checkHordeStatus, getHordeModels, adjustHordeGenerationParams, MIN_LENGTH, } 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, isTrueBoolean, } from './scripts/utils.js'; import { debounce_timeout } from './scripts/constants.js'; import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js'; import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } 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, } from './scripts/tags.js'; import { SECRET_KEYS, readSecretState, secret_state, writeSecret, } from './scripts/secrets.js'; import { EventEmitter } from './lib/eventemitter.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, getInstructStoppingSequences, autoSelectInstructPreset, formatInstructModeSystemPrompt, selectInstructPreset, instruct_presets, selectContextPreset, } from './scripts/instruct-mode.js'; import { initLocales } from './scripts/i18n.js'; import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.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'; import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js'; import { initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; import { POPUP_TYPE, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js'; import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; import { ScraperManager } from './scripts/scrapers.js'; import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js'; import { SlashCommand } from './scripts/slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './scripts/slash-commands/SlashCommandArgument.js'; import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.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 { SlashCommandEnumValue, enumTypes } from './scripts/slash-commands/SlashCommandEnumValue.js'; import { enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js'; //exporting functions and vars for mods export { user_avatar, setUserAvatar, getUserAvatars, getUserAvatar, nai_settings, isOdd, countOccurrences, renderTemplate, }; /** * Wait for page to load before continuing the app initialization. */ await new Promise((resolve) => { if (document.readyState === 'complete') { resolve(); } else { window.addEventListener('load', resolve); } }); showLoader(); // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS document.getElementById('preloader').remove(); // Configure toast library: toastr.options.escapeHtml = true; // Prevent raw HTML inserts toastr.options.timeOut = 4000; // How long the toast will display without user interaction toastr.options.extendedTimeOut = 10000; // How long the toast will display after a user hovers over it toastr.options.progressBar = true; // Visually indicate how long before a toast expires. toastr.options.closeButton = true; // enable a close button toastr.options.positionClass = 'toast-top-center'; // Where to position the toast container toastr.options.onHidden = () => { // 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(); }; // Allow target="_blank" in links DOMPurify.addHook('afterSanitizeAttributes', function (node) { if ('target' in node) { node.setAttribute('target', '_blank'); node.setAttribute('rel', 'noopener'); } }); DOMPurify.addHook('uponSanitizeAttribute', (_, data, config) => { if (!config['MESSAGE_SANITIZE']) { return; } switch (data.attrName) { case 'class': { if (data.attrValue) { data.attrValue = data.attrValue.split(' ').map((v) => { if (v.startsWith('fa-') || v.startsWith('note-') || v === 'monospace') { return v; } return 'custom-' + v; }).join(' '); } break; } } }); DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { if (!config['MESSAGE_SANITIZE']) { return; } // Replace line breaks with <br> in unknown elements if (node instanceof HTMLUnknownElement) { node.innerHTML = node.innerHTML.replaceAll('\n', '<br>'); } const isMediaAllowed = isExternalMediaAllowed(); if (isMediaAllowed) { return; } let mediaBlocked = false; switch (node.tagName) { case 'AUDIO': case 'VIDEO': case 'SOURCE': case 'TRACK': case 'EMBED': case 'OBJECT': case 'IMG': { const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin); const src = node.getAttribute('src'); const data = node.getAttribute('data'); const srcset = node.getAttribute('srcset'); if (srcset) { const srcsetUrls = srcset.split(','); for (const srcsetUrl of srcsetUrls) { const [url] = srcsetUrl.trim().split(' '); if (isExternalUrl(url)) { console.warn('External media blocked', url); node.remove(); mediaBlocked = true; break; } } } if (src && isExternalUrl(src)) { console.warn('External media blocked', src); mediaBlocked = true; node.remove(); } if (data && isExternalUrl(data)) { console.warn('External media blocked', data); mediaBlocked = true; node.remove(); } if (mediaBlocked && (node instanceof HTMLMediaElement)) { node.autoplay = false; node.pause(); } } break; } if (mediaBlocked) { const entityId = getCurrentEntityId(); const warningShownKey = `mediaWarningShown:${entityId}`; if (localStorage.getItem(warningShownKey) === null) { const warningToast = toastr.warning( 'Use the "Ext. Media" button to allow it. Click on this message to dismiss.', 'External media has been blocked', { timeOut: 0, preventDuplicates: true, onclick: () => toastr.clear(warningToast), }, ); localStorage.setItem(warningShownKey, 'true'); } } }); // API OBJECT FOR EXTERNAL WIRING window['SillyTavern'] = {}; // Event source init export const event_types = { APP_READY: 'app_ready', EXTRAS_CONNECTED: 'extras_connected', MESSAGE_SWIPED: 'message_swiped', MESSAGE_SENT: 'message_sent', MESSAGE_RECEIVED: 'message_received', MESSAGE_EDITED: 'message_edited', MESSAGE_DELETED: 'message_deleted', MESSAGE_UPDATED: 'message_updated', IMPERSONATE_READY: 'impersonate_ready', CHAT_CHANGED: 'chat_id_changed', GENERATION_STARTED: 'generation_started', GENERATION_STOPPED: 'generation_stopped', GENERATION_ENDED: 'generation_ended', EXTENSIONS_FIRST_LOAD: 'extensions_first_load', SETTINGS_LOADED: 'settings_loaded', SETTINGS_UPDATED: 'settings_updated', GROUP_UPDATED: 'group_updated', MOVABLE_PANELS_RESET: 'movable_panels_reset', SETTINGS_LOADED_BEFORE: 'settings_loaded_before', SETTINGS_LOADED_AFTER: 'settings_loaded_after', CHATCOMPLETION_SOURCE_CHANGED: 'chatcompletion_source_changed', CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed', OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before', OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after', WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated', WORLDINFO_UPDATED: 'worldinfo_updated', CHARACTER_EDITED: 'character_edited', CHARACTER_PAGE_LOADED: 'character_page_loaded', CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE: 'character_group_overlay_state_change_before', CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER: 'character_group_overlay_state_change_after', USER_MESSAGE_RENDERED: 'user_message_rendered', CHARACTER_MESSAGE_RENDERED: 'character_message_rendered', FORCE_SET_BACKGROUND: 'force_set_background', CHAT_DELETED: 'chat_deleted', CHAT_CREATED: 'chat_created', GROUP_CHAT_DELETED: 'group_chat_deleted', GROUP_CHAT_CREATED: 'group_chat_created', GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts', GENERATE_AFTER_COMBINE_PROMPTS: 'generate_after_combine_prompts', GROUP_MEMBER_DRAFTED: 'group_member_drafted', WORLD_INFO_ACTIVATED: 'world_info_activated', TEXT_COMPLETION_SETTINGS_READY: 'text_completion_settings_ready', CHAT_COMPLETION_SETTINGS_READY: 'chat_completion_settings_ready', CHAT_COMPLETION_PROMPT_READY: 'chat_completion_prompt_ready', CHARACTER_FIRST_MESSAGE_SELECTED: 'character_first_message_selected', // TODO: Naming convention is inconsistent with other events CHARACTER_DELETED: 'characterDeleted', CHARACTER_DUPLICATED: 'character_duplicated', SMOOTH_STREAM_TOKEN_RECEIVED: 'smooth_stream_token_received', FILE_ATTACHMENT_DELETED: 'file_attachment_deleted', WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate', OPEN_CHARACTER_LIBRARY: 'open_character_library', LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register', LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call', }; export const eventSource = new EventEmitter(); eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands); 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); // Markdown converter 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' }); export let itemizedPrompts = []; export const systemUserName = 'SillyTavern System'; let default_user_name = 'User'; export let name1 = default_user_name; export let name2 = 'SillyTavern System'; export let chat = []; let safetychat = [ { name: systemUserName, is_user: false, create_date: 0, mes: 'You deleted a character/chat and arrived back here for safety reasons! Pick another character!', }, ]; let chatSaveTimeout; let importFlashTimeout; export let isChatSaving = false; let chat_create_date = ''; let firstRun = false; let settingsReady = false; let currentVersion = '0.0.0'; let displayVersion = 'SillyTavern'; let generatedPromptCache = ''; let generation_started = new Date(); /** @type {import('scripts/char-data.js').v1CharData[]} */ export let characters = []; 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 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 rawPromptPopper = Popper.createPopper(document.getElementById('dialogue_popup'), document.getElementById('rawPromptPopup'), { placement: 'right', }); // Saved here for performance reasons const messageTemplate = $('#message_template .mes'); const chatElement = $('#chat'); let dialogueResolve = null; let dialogueCloseStop = false; 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; export let abortStatusCheck = new AbortController(); let charDragDropHandler = 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(() => saveSettings(), 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 {string} System message types */ export const system_message_types = { HELP: 'help', WELCOME: 'welcome', GROUP: 'group', EMPTY: 'empty', GENERIC: 'generic', BOOKMARK_CREATED: 'bookmark_created', BOOKMARK_BACK: 'bookmark_back', NARRATOR: 'narrator', COMMENT: 'comment', SLASH_COMMANDS: 'slash_commands', FORMATTING: 'formatting', HOTKEYS: 'hotkeys', MACROS: 'macros', }; /** * @enum {number} Extension prompt types */ export const extension_prompt_types = { 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 = 1000; export let system_messages = {}; async function getSystemMessages() { system_messages = { help: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: await renderTemplateAsync('help'), }, slash_commands: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: '', }, hotkeys: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: await renderTemplateAsync('hotkeys'), }, formatting: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: await renderTemplateAsync('formatting'), }, macros: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: await renderTemplateAsync('macros'), }, welcome: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: await renderTemplateAsync('welcome', { displayVersion }), }, group: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, is_group: true, mes: 'Group chat created. Say \'Hi\' to lovely people!', }, empty: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: 'No one hears you. <b>Hint:</b> add more members to the group!', }, generic: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: 'Generic system message. User `text` parameter to override the contents', }, bookmark_created: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: 'Checkpoint created! Click here to open the checkpoint chat: <a class="bookmark_link" file_name="{0}" href="javascript:void(null);">{1}</a>', }, bookmark_back: { name: systemUserName, force_avatar: system_avatar, is_user: false, is_system: true, mes: 'Click here to return to the previous chat: <a class="bookmark_link" file_name="{0}" href="javascript:void(null);">Return</a>', }, }; } // Register configuration migrations registerPromptManagerMigration(); $(document).ajaxError(function myErrorHandler(_, xhr) { // Cohee: CSRF doesn't error out in multiple tabs anymore, so this is unnecessary /* if (xhr.status == 403) { toastr.warning( 'doubleCsrf errors in console are NORMAL in this case. If you want to run ST in multiple tabs, start the server with --disableCsrf option.', 'Looks like you\'ve opened SillyTavern in another browser tab', { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true }, ); } */ }); 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(render_formulas = false) { if (render_formulas) { converter = new showdown.Converter({ emoji: true, underline: true, tables: true, parseImgDimensions: true, simpleLineBreaks: true, strikethrough: true, disableForced4SpacesIndentedSublists: true, extensions: [ showdownKatex( { delimiters: [ { left: '$$', right: '$$', display: true, asciimath: false }, { left: '$', right: '$', display: false, asciimath: true }, ], }, )], }); } else { 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? setTimeout(() => { if (power_user) { converter.addExtension(markdownExclusionExt(), 'exclusion'); } }, 1); 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 let create_save = { name: '', description: '', creator_notes: '', post_history_instructions: '', character_version: '', system_prompt: '', tags: '', creator: '', personality: '', first_message: '', avatar: '', 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: {}, }; //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 api_server = ''; export let is_send_press = false; //Send generation let this_del_mes = -1; //message editing and chat scroll position persistence var this_edit_mes_chname = ''; var this_edit_mes_id; var scroll_holder = 0; var is_use_scroll_holder = false; //settings export let settings; export let koboldai_settings; export let koboldai_setting_names; var preset_settings = 'gui'; export let amount_gen = 80; //default max length of AI generated responses export let max_context = 2048; var swipes = true; let extension_prompts = {}; export let main_api;// = "kobold"; //novel settings export let novelai_settings; export let novelai_setting_names; let abortController; //css var css_send_form_display = $('<div id=send_form></div>').css('display'); const MAX_GENERATION_LOOPS = 5; var kobold_horde_model = ''; export let token; var PromptArrayItemForRawPromptDisplay; /** 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() { return { 'Content-Type': 'application/json', 'X-CSRF-Token': token, }; } $.ajaxPrefilter((options, originalOptions, xhr) => { xhr.setRequestHeader('X-CSRF-Token', token); }); /** * Pings the STserver to check if it is reachable. * @returns {Promise<boolean>} True if the server is reachable, false otherwise. */ export async function pingServer() { try { const result = await fetch('api/ping', { method: 'GET', headers: getRequestHeaders(), }); if (!result.ok) { return false; } return true; } catch (error) { console.error('Error pinging server', error); return false; } } async function firstLoadInit() { try { const tokenResponse = await fetch('/csrf-token'); const tokenData = await tokenResponse.json(); token = tokenData.token; } catch { hideLoader(); toastr.error('Couldn\'t get CSRF token. Please refresh the page.', 'Error', { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true }); throw new Error('Initialization failed'); } await getClientVersion(); await readSecretState(); initLocales(); await getSystemMessages(); sendSystemMessage(system_message_types.WELCOME); await getSettings(); initKeyboard(); initDynamicStyles(); initTags(); await getUserAvatars(true, user_avatar); await getCharacters(); await getBackgrounds(); await initTokenizers(); await initPresetManager(); initBackgrounds(); initAuthorsNote(); initPersonas(); initRossMods(); initStats(); initCfg(); initLogprobs(); doDailyExtensionUpdatesCheck(); hideLoader(); await eventSource.emit(event_types.APP_READY); } function cancelStatusCheck() { abortStatusCheck?.abort(); 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`); } export function setActiveCharacter(entityOrKey) { active_character = getTagKeyForEntity(entityOrKey); } export function setActiveGroup(entityOrKey) { active_group = getTagKeyForEntity(entityOrKey); } /** * Gets the itemized prompts for a chat. * @param {string} chatId Chat ID to load */ export async function loadItemizedPrompts(chatId) { try { if (!chatId) { itemizedPrompts = []; return; } itemizedPrompts = await promptStorage.getItem(chatId); if (!itemizedPrompts) { itemizedPrompts = []; } } catch { console.log('Error loading itemized prompts for chat', chatId); itemizedPrompts = []; } } /** * Saves the itemized prompts for a chat. * @param {string} chatId Chat ID to save itemized prompts for */ export async function saveItemizedPrompts(chatId) { try { if (!chatId) { return; } await promptStorage.setItem(chatId, itemizedPrompts); } catch { console.log('Error saving itemized prompts for chat', chatId); } } /** * Replaces the itemized prompt text for a message. * @param {number} mesId Message ID to get itemized prompt for * @param {string} promptText New raw prompt text * @returns */ export async function replaceItemizedPromptText(mesId, promptText) { if (!Array.isArray(itemizedPrompts)) { itemizedPrompts = []; } const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId); if (!itemizedPrompt) { return; } itemizedPrompt.rawPrompt = promptText; } /** * Deletes the itemized prompts for a chat. * @param {string} chatId Chat ID to delete itemized prompts for */ export async function deleteItemizedPrompts(chatId) { try { if (!chatId) { return; } await promptStorage.removeItem(chatId); } catch { console.log('Error deleting itemized prompts for chat', chatId); } } /** * Empties the itemized prompts array and caches. */ export async function clearItemizedPrompts() { try { await promptStorage.clear(); itemizedPrompts = []; } catch { console.log('Error clearing itemized prompts'); } } async function getStatusHorde() { try { const hordeStatus = await checkHordeStatus(); online_status = hordeStatus ? 'Connected' : 'no_connection'; } catch { online_status = 'no_connection'; } return resultCheckStatus(); } async function getStatusKobold() { let endpoint = api_server; if (!endpoint) { console.warn('No endpoint for status check'); online_status = 'no_connection'; return resultCheckStatus(); } try { const response = await fetch('/api/backends/kobold/status', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ main_api, api_server: endpoint, }), signal: abortStatusCheck.signal, }); const data = await response.json(); online_status = data?.model ?? 'no_connection'; if (!data.koboldUnitedVersion) { throw new Error('Missing mandatory Kobold version in data:', data); } // Determine instruct mode preset autoSelectInstructPreset(online_status); // determine if we can use stop sequence and streaming setKoboldFlags(data.koboldUnitedVersion, data.koboldCppVersion); // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress. if (online_status === 'no_connection' && data.response) { toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true }); } } catch (err) { console.error('Error getting status', err); online_status = 'no_connection'; } return resultCheckStatus(); } async function getStatusTextgen() { const url = '/api/backends/text-completions/status'; const endpoint = getTextGenServer(); if (!endpoint) { console.warn('No endpoint for status check'); online_status = 'no_connection'; return resultCheckStatus(); } if (textgen_settings.type == OOBA && textgen_settings.bypass_status_check) { online_status = 'Status check bypassed'; return resultCheckStatus(); } try { const response = await fetch(url, { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ api_server: endpoint, api_type: textgen_settings.type, legacy_api: textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE), }), signal: abortStatusCheck.signal, }); const data = await response.json(); if (textgen_settings.type === MANCER) { loadMancerModels(data?.data); online_status = textgen_settings.mancer_model; } else if (textgen_settings.type === TOGETHERAI) { loadTogetherAIModels(data?.data); online_status = textgen_settings.togetherai_model; } else if (textgen_settings.type === OLLAMA) { loadOllamaModels(data?.data); online_status = textgen_settings.ollama_model || 'Connected'; } else if (textgen_settings.type === INFERMATICAI) { loadInfermaticAIModels(data?.data); online_status = textgen_settings.infermaticai_model; } else if (textgen_settings.type === DREAMGEN) { loadDreamGenModels(data?.data); online_status = textgen_settings.dreamgen_model; } else if (textgen_settings.type === OPENROUTER) { loadOpenRouterModels(data?.data); online_status = textgen_settings.openrouter_model; } else if (textgen_settings.type === VLLM) { loadVllmModels(data?.data); online_status = textgen_settings.vllm_model; } else if (textgen_settings.type === APHRODITE) { loadAphroditeModels(data?.data); online_status = textgen_settings.aphrodite_model; } else { online_status = data?.result; } if (!online_status) { online_status = 'no_connection'; } // Determine instruct mode preset autoSelectInstructPreset(online_status); // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress. if (online_status === 'no_connection' && data.response) { toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true }); } } catch (err) { console.error('Error getting status', err); online_status = 'no_connection'; } return resultCheckStatus(); } async function getStatusNovel() { try { const result = await loadNovelSubscriptionData(); if (!result) { throw new Error('Could not load subscription data'); } online_status = getNovelTier(); } catch { online_status = 'no_connection'; } resultCheckStatus(); } 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(); } export async function selectCharacterById(id) { if (characters[id] === undefined) { return; } if (isChatSaving) { toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...'); return; } if (selected_group && is_group_generating) { return; } if (selected_group || this_chid !== 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'; this_chid = id; chat.length = 0; chat_metadata = {}; await getChat(); } } else { //if clicked on character that was already selected selected_button = 'character_edit'; select_selected_character(this_chid); } } function getBackBlock() { const template = $('#bogus_folder_back_template .bogus_folder_select').clone(); return template; } function getEmptyBlock() { const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog']; const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it']; const roll = new Date().getMinutes() % icons.length; const emptyBlock = ` <div class="text_block empty_block"> <i class="fa-solid ${icons[roll]} fa-4x"></i> <h1>${texts[roll]}</h1> <p>There are no items to display.</p> </div>`; return $(emptyBlock); } /** * @param {number} hidden Number of hidden characters */ function getHiddenBlock(hidden) { const hiddenBlock = ` <div class="text_block hidden_block"> <small> <p>${hidden} ${hidden > 1 ? 'characters' : 'character'} hidden.</p> <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Characters and groups hidden by filters or closed folders" title="Characters and groups hidden by filters or closed folders"></div> </small> </div>`; 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({ '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 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 }); // 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 }); $('#rm_print_characters_pagination').pagination({ dataSource: entities, pageSize: Number(localStorage.getItem(storageKey)) || per_page_default, sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000], pageRange: 1, pageNumber: saveCharactersPage || 1, position: 'top', showPageNumbers: false, showSizeChanger: true, prevText: '<', nextText: '>', formatNavigator: PAGINATION_TEMPLATE, showNavigator: true, callback: function (/** @type {Entity[]} */ data) { $(listId).empty(); if (power_user.bogus_folders && isBogusFolderOpen()) { $(listId).append(getBackBlock()); } if (!data.length) { $(listId).append(getEmptyBlock()); } 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()) { $(listId).append(getHiddenBlock(hidden)); } eventSource.emit(event_types.CHARACTER_PAGE_LOADED); }, afterSizeSelectorChange: function (e) { localStorage.setItem(storageKey, e.target.value); }, afterPaging: function (e) { saveCharactersPage = e; }, afterRender: function () { $(listId).scrollTop(currentScrollTop); }, }); favsToHotswap(); } /** 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 } }); } if (doSort) { sortEntitiesList(subEntities); } 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); // 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 } }); } } // 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); } 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(`Character ${avatarUrl} not found in the list`, '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}`; } return ''; } export async function getCharacters() { const response = await fetch('/api/characters/all', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ '': '', }), }); if (response.ok === true) { 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 (this_chid !== undefined) { $('#avatar_url_pole').val(characters[this_chid].avatar); } await getGroups(); await printCharacters(true); } } 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); } } 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 function showMoreMessages() { let messageId = Number($('#chat').children('.mes').first().attr('mesid')); let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER; console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); const prevHeight = $('#chat').prop('scrollHeight'); while (messageId > 0 && count > 0) { count--; messageId--; addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId }); } if (messageId == 0) { $('#show_more_messages').remove(); } const newHeight = $('#chat').prop('scrollHeight'); $('#chat').scrollTop(newHeight - prevHeight); } 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; $('#chat').append('<div id="show_more_messages">Show more messages</div>'); } for (let i = startIndex; i < chat.length; i++) { const item = chat[i]; addOneMessage(item, { scroll: false, forceId: i, showSwipes: false }); } // Scroll to bottom when all images are loaded const images = document.querySelectorAll('#chat .mes img'); let imagesLoaded = 0; for (let i = 0; i < images.length; i++) { const image = images[i]; if (image instanceof HTMLImageElement) { if (image.complete) { incrementAndCheck(); } else { image.addEventListener('load', incrementAndCheck); } } } $('#chat .mes').removeClass('last_mes'); $('#chat .mes').last().addClass('last_mes'); hideSwipeButtons(); showSwipeButtons(); scrollChatToBottom(); function incrementAndCheck() { imagesLoaded++; if (imagesLoaded === images.length) { scrollChatToBottom(); } } } export async function clearChat() { closeMessageEditor(); extension_prompts = {}; if (is_delete_mode) { $('#dialogue_del_mes_cancel').trigger('click'); } $('#chat').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 = []; } export async function deleteLastMessage() { chat.length = chat.length - 1; $('#chat').children('.mes').last().remove(); await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); } export async function reloadCurrentChat() { await clearChat(); chat.length = 0; if (selected_group) { await getGroupChat(selected_group, true); } else if (this_chid !== undefined) { await getChat(); } else { resetChatState(); await getCharacters(); await printMessages(); await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); } hideSwipeButtons(); showSwipeButtons(); } /** * Send the message currently typed into the chat box. */ export function sendTextareaMessage() { if (is_send_press) return; if (isExecutingCommandsFromChatInput) return; let generateType; // "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 && !textareaText && !selected_group && chat.length && !chat[chat.length - 1]['is_user'] && !chat[chat.length - 1]['is_system'] ) { generateType = 'continue'; } 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 * @returns {string} HTML string */ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId) { if (!mes) { return ''; } if (Number(messageId) === 0 && !isSystem && !isUser) { mes = substituteParams(mes, undefined, ch_name); } 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 if (!power_user.show_user_prompt_bias && ch_name && !isUser && !isSystem) { mes = mes.replaceAll(substituteParams(power_user.user_prompt_bias), ''); } if (!isSystem) { function getRegexPlacement() { try { 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 = mes.replaceAll('<', '<').replaceAll('>', '>'); } if (this_chid === undefined && !selected_group) { mes = mes.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>'); } else 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(/```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(".+?")|(\u201C.+?\u201D)/gm, function (match, p1, p2) { if (p1) { return '<q>"' + p1.replace(/"/g, '') + '"</q>'; } else if (p2) { return '<q>“' + p2.replace(/\u201C|\u201D/g, '') + '”</q>'; } else { return match; } }); // Restore double quotes in tags if (!power_user.encode_tags) { mes = mes.replace(/\ufffe/g, '"'); } mes = mes.replaceAll('\\begin{align*}', '$$'); mes = mes.replaceAll('\\end{align*}', '$$'); mes = converter.makeHtml(mes); mes = mes.replace(/<code(.*)>[\s\S]*?<\/code>/g, function (match) { // Firefox creates extra newlines from <br>s in code blocks, so we replace them before converting newlines to <br>s. return match.replace(/\n/gm, '\u0000'); }); mes = mes.replace(/\u0000/g, '\n'); // Restore converted newlines mes = mes.trim(); mes = mes.replace(/<code(.*)>[\s\S]*?<\/code>/g, function (match) { return match.replace(/&/g, '&'); }); } /* // Hides bias from empty messages send with slash commands if (isSystem) { mes = mes.replace(/\{\{[\s\S]*?\}\}/gm, ""); } */ if (!power_user.allow_name2_display && ch_name && !isUser && !isSystem) { mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1'); } /** @type {any} */ const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'] }; mes = encodeStyleTags(mes); mes = DOMPurify.sanitize(mes, config); mes = decodeStyleTags(mes); return mes; } /** * Inserts or replaces an SVG icon adjacent to the provided message's timestamp. * * If the `extra.api` is "openai" and `extra.model` contains the substring "claude", * the function fetches the "claude.svg". Otherwise, it fetches the SVG named after * the value in `extra.api`. * * @param {JQuery<HTMLElement>} mes - The message element containing the timestamp where the icon should be inserted or replaced. * @param {Object} extra - Contains the API and model details. * @param {string} extra.api - The name of the API, used to determine which SVG to fetch. * @param {string} extra.model - The model name, used to check for the substring "claude". */ function insertSVGIcon(mes, extra) { // Determine the SVG filename let modelName; // Claude on OpenRouter or Anthropic if (extra.api === 'openai' && extra.model?.toLowerCase().includes('claude')) { modelName = 'claude'; } // OpenAI on OpenRouter else if (extra.api === 'openai' && extra.model?.toLowerCase().includes('openai')) { modelName = 'openai'; } // OpenRouter website model or other models else if (extra.api === 'openai' && (extra.model === null || extra.model?.toLowerCase().includes('/'))) { modelName = 'openrouter'; } // Everything else else { modelName = extra.api; } const image = new Image(); // Add classes for styling and identification image.classList.add('icon-svg', 'timestamp-icon'); image.src = `/img/${modelName}.svg`; image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`; image.onload = async function () { // Check if an SVG already exists adjacent to the timestamp let existingSVG = mes.find('.timestamp').next('.timestamp-icon'); if (existingSVG.length) { // Replace existing SVG existingSVG.replaceWith(image); } else { // Append the new SVG if none exists mes.find('.timestamp').after(image); } await SVGInject(this); }; } function getMessageFromTemplate({ mesId, swipeId, characterName, isUser, avatarImg, bias, isSystem, title, timerValue, timerTitle, bookmarkLink, forceAvatar, timestamp, tokenCount, extra, }) { const mes = messageTemplate.clone(); mes.attr({ 'mesid': mesId, 'swipeid': swipeId, 'ch_name': characterName, 'is_user': isUser, 'is_system': !!isSystem, 'bookmark_link': bookmarkLink, 'force_avatar': !!forceAvatar, 'timestamp': timestamp, }); mes.find('.avatar img').attr('src', avatarImg); mes.find('.ch_name .name_text').text(characterName); mes.find('.mes_bias').html(bias); mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`); mes.find('.mesIDDisplay').text(`#${mesId}`); tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`); title && mes.attr('title', title); timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue); if (power_user.timestamp_model_icon && extra?.api) { insertSVGIcon(mes, extra); } return mes; } export function updateMessageBlock(messageId, message) { const messageElement = $(`#chat [mesid="${messageId}"]`); const text = message?.extra?.display_text ?? message.mes; messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId)); addCopyToCodeBlocks(messageElement); appendMediaToMessage(message, messageElement); } export function appendMediaToMessage(mes, messageElement) { // Add image to message if (mes.extra?.image) { const chatHeight = $('#chat').prop('scrollHeight'); const image = messageElement.find('.mes_img'); const text = messageElement.find('.mes_text'); const isInline = !!mes.extra?.inline_image; image.on('load', function () { const scrollPosition = $('#chat').scrollTop(); const newChatHeight = $('#chat').prop('scrollHeight'); const diff = newChatHeight - chatHeight; $('#chat').scrollTop(scrollPosition + diff); }); image.attr('src', mes.extra?.image); image.attr('title', mes.extra?.title || mes.title || ''); messageElement.find('.mes_img_container').addClass('img_extra'); image.toggleClass('img_inline', isInline); text.toggleClass('displayNone', !isInline); } // Add file to message if (mes.extra?.file) { messageElement.find('.mes_file_container').remove(); const messageId = messageElement.attr('mesid'); const template = $('#message_file_template .mes_file_container').clone(); template.find('.mes_file_name').text(mes.extra.file.name); template.find('.mes_file_size').text(humanFileSize(mes.extra.file.size)); template.find('.mes_file_download').attr('mesid', messageId); template.find('.mes_file_delete').attr('mesid', messageId); messageElement.find('.mes_block').append(template); } else { messageElement.find('.mes_file_container').remove(); } } /** * @deprecated Use appendMediaToMessage instead. */ export function appendImageToMessage(mes, messageElement) { appendMediaToMessage(mes, messageElement); } export function addCopyToCodeBlocks(messageElement) { const codeBlocks = $(messageElement).find('pre code'); for (let i = 0; i < codeBlocks.length; i++) { hljs.highlightElement(codeBlocks.get(i)); if (navigator.clipboard !== undefined) { const copyButton = document.createElement('i'); copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable'); copyButton.title = 'Copy code'; codeBlocks.get(i).appendChild(copyButton); copyButton.addEventListener('pointerup', function (event) { navigator.clipboard.writeText(codeBlocks.get(i).innerText); toastr.info('Copied!', '', { timeOut: 2000 }); }); } } } 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') : ''; if (mes?.extra?.display_text) { messageText = mes.extra.display_text; } // Forbidden black magic // This allows to use "continue" on user messages if (type === 'swipe' && mes.swipe_id === undefined) { mes.swipe_id = 0; mes.swipes = [mes.mes]; } let avatarImg = getUserAvatar(user_avatar); const isSystem = mes.is_system; const title = mes.title; generatedPromptCache = ''; //for non-user mesages if (!mes['is_user']) { if (mes.force_avatar) { avatarImg = mes.force_avatar; } else if (this_chid === undefined) { avatarImg = system_avatar; } else { if (characters[this_chid].avatar != 'none') { avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar); } else { avatarImg = default_avatar; } } //old processing: //if messge is from sytem, use the name provided in the message JSONL to proceed, //if not system message, use name2 (char's name) to proceed //characterName = mes.is_system || mes.force_avatar ? mes.name : name2; } else if (mes['is_user'] && mes['force_avatar']) { // Special case for persona images. avatarImg = mes['force_avatar']; } messageText = messageFormatting( messageText, mes.name, isSystem, mes.is_user, chat.indexOf(mes), ); const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1); let bookmarkLink = mes?.extra?.bookmark_link ?? ''; let params = { mesId: forceId ?? chat.length - 1, swipeId: mes.swipe_id ?? 0, characterName: mes.name, isUser: mes.is_user, avatarImg: avatarImg, bias: bias, isSystem: isSystem, title: title, bookmarkLink: bookmarkLink, forceAvatar: mes.force_avatar, timestamp: timestamp, extra: mes.extra, tokenCount: mes.extra?.token_count ?? 0, ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count), }; const renderedMessage = getMessageFromTemplate(params); if (type !== 'swipe') { if (!insertAfter && !insertBefore) { chatElement.append(renderedMessage); } else if (insertAfter) { const target = chatElement.find(`.mes[mesid="${insertAfter}"]`); $(renderedMessage).insertAfter(target); } else { const target = chatElement.find(`.mes[mesid="${insertBefore}"]`); $(renderedMessage).insertBefore(target); } } // Callers push the new message to chat before calling addOneMessage const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1; const newMessage = $(`#chat [mesid="${newMessageId}"]`); const isSmallSys = mes?.extra?.isSmallSys; if (isSmallSys === true) { newMessage.addClass('smallSysMes'); } //shows or hides the Prompt display button let mesIdToFind = type == 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId')); //if we have itemized messages, and the array isn't null.. if (params.isUser === false && Array.isArray(itemizedPrompts) && itemizedPrompts.length > 0) { const itemizedPrompt = itemizedPrompts.find(x => Number(x.mesId) === Number(mesIdToFind)); if (itemizedPrompt) { newMessage.find('.mes_prompt').show(); } } newMessage.find('.avatar img').on('error', function () { $(this).hide(); $(this).parent().html('<div class="missing-avatar fa-solid fa-user-slash"></div>'); }); if (type === 'swipe') { const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`); swipeMessage.find('.mes_text').html(messageText).attr('title', title); swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`); appendMediaToMessage(mes, swipeMessage); if (power_user.timestamp_model_icon && params.extra?.api) { insertSVGIcon(swipeMessage, params.extra); } if (mes.swipe_id == mes.swipes.length - 1) { swipeMessage.find('.mes_timer').text(params.timerValue).attr('title', params.timerTitle); swipeMessage.find('.tokenCounterDisplay').text(`${params.tokenCount}t`); } else { swipeMessage.find('.mes_timer').empty(); swipeMessage.find('.tokenCounterDisplay').empty(); } } else { const messageId = forceId ?? chat.length - 1; chatElement.find(`[mesid="${messageId}"] .mes_text`).append(messageText); appendMediaToMessage(mes, newMessage); showSwipes && hideSwipeButtons(); } addCopyToCodeBlocks(newMessage); if (showSwipes) { $('#chat .mes').last().addClass('last_mes'); $('#chat .mes').eq(-2).removeClass('last_mes'); hideSwipeButtons(); showSwipeButtons(); } // Don't scroll if not inserting last if (!insertAfter && !insertBefore && scroll) { scrollChatToBottom(); } } /** * Returns the URL of the avatar for the given character Id. * @param {number} characterId Character Id * @returns {string} Avatar URL */ export function getCharacterAvatar(characterId) { const character = characters[characterId]; const avatarImg = character?.avatar; if (!avatarImg || avatarImg === 'none') { return default_avatar; } return formatCharacterAvatar(avatarImg); } export function formatCharacterAvatar(characterAvatar) { return `characters/${characterAvatar}`; } /** * Formats the title for the generation timer. * @param {Date} gen_started Date when generation was started * @param {Date} gen_finished Date when generation was finished * @param {number} tokenCount Number of tokens generated (0 if not available) * @returns {Object} Object containing the formatted timer value and title * @example * const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount); * console.log(timerValue); // 1.2s * console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s */ function formatGenerationTimer(gen_started, gen_finished, tokenCount) { if (!gen_started || !gen_finished) { return {}; } const dateFormat = 'HH:mm:ss D MMM YYYY'; const start = moment(gen_started); const finish = moment(gen_finished); const seconds = finish.diff(start, 'seconds', true); const timerValue = `${seconds.toFixed(1)}s`; const timerTitle = [ `Generation queued: ${start.format(dateFormat)}`, `Reply received: ${finish.format(dateFormat)}`, `Time to generate: ${seconds} seconds`, tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '', ].join('\n'); if (isNaN(seconds) || seconds < 0) { return { timerValue: '', timerTitle }; } return { timerValue, timerTitle }; } export function scrollChatToBottom() { if (power_user.auto_scroll_chat_to_bottom) { let position = chatElement[0].scrollHeight; if (power_user.waifuMode) { const lastMessage = chatElement.find('.mes').last(); if (lastMessage.length) { const lastMessagePosition = lastMessage.position().top; position = chatElement.scrollTop() + lastMessagePosition; } } chatElement.scrollTop(position); } } /** * Substitutes {{macro}} parameters in a string. * @param {string} content - The string to substitute parameters in. * @param {Record<string,any>} additionalMacro - Additional environment variables for substitution. * @returns {string} The string with substituted parameters. */ export function substituteParamsExtended(content, additionalMacro = {}) { return substituteParams(content, undefined, undefined, undefined, undefined, true, additionalMacro); } /** * Substitutes {{macro}} parameters in a string. * @param {string} content - The string to substitute parameters in. * @param {string} [_name1] - The name of the user. Uses global name1 if not provided. * @param {string} [_name2] - The name of the character. Uses global name2 if not provided. * @param {string} [_original] - The original message for {{original}} substitution. * @param {string} [_group] - The group members list for {{group}} substitution. * @param {boolean} [_replaceCharacterCard] - Whether to replace character card macros. * @param {Record<string,any>} [additionalMacro] - Additional environment variables for substitution. * @returns {string} The string with substituted parameters. */ export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}) { if (!content) { return ''; } const environment = {}; if (typeof _original === 'string') { let originalSubstituted = false; environment.original = () => { if (originalSubstituted) { return ''; } originalSubstituted = true; return _original; }; } const getGroupValue = () => { if (typeof _group === 'string') { return _group; } if (selected_group) { const members = groups.find(x => x.id === selected_group)?.members; const names = Array.isArray(members) ? members.map(m => characters.find(c => c.avatar === m)?.name).filter(Boolean).join(', ') : ''; return names; } else { return _name2 ?? name2; } }; if (_replaceCharacterCard) { const fields = getCharacterCardFields(); environment.charPrompt = fields.system || ''; environment.charJailbreak = fields.jailbreak || ''; environment.description = fields.description || ''; environment.personality = fields.personality || ''; environment.scenario = fields.scenario || ''; environment.persona = fields.persona || ''; environment.mesExamples = fields.mesExamples || ''; environment.charVersion = fields.version || ''; environment.char_version = fields.version || ''; } // Must be substituted last so that they're replaced inside {{description}} environment.user = _name1 ?? name1; environment.char = _name2 ?? name2; environment.group = environment.charIfNotGroup = getGroupValue(); environment.model = getGeneratingModel(); if (additionalMacro && typeof additionalMacro === 'object') { Object.assign(environment, additionalMacro); } return evaluateMacros(content, environment); } /** * Gets stopping sequences for the prompt. * @param {boolean} isImpersonate A request is made to impersonate a user * @param {boolean} isContinue A request is made to continue the message * @returns {string[]} Array of stopping strings */ export function getStoppingStrings(isImpersonate, isContinue) { const charString = `\n${name2}:`; const userString = `\n${name1}:`; const result = isImpersonate ? [charString] : [userString]; result.push(userString); if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) { result.push(charString); } // Add other group members as the stopping strings if (selected_group) { const group = groups.find(x => x.id === selected_group); if (group && Array.isArray(group.members)) { const names = group.members .map(x => characters.find(y => y.avatar == x)) .filter(x => x && x.name && x.name !== name2) .map(x => `\n${x.name}:`); result.push(...names); } } result.push(...getInstructStoppingSequences()); result.push(...getCustomStoppingStrings()); if (power_user.single_line) { result.unshift('\n'); } return result.filter(onlyUnique); } /** * Background generation based on the provided prompt. * @param {string} quiet_prompt Instruction prompt for the AI * @param {boolean} quietToLoud Whether the message should be sent in a foreground (loud) or background (quiet) mode * @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt * @param {string} quietImage Image to use for the quiet prompt * @param {string} quietName Name to use for the quiet prompt (defaults to "System:") * @param {number} [responseLength] Maximum response length. If unset, the global default value is used. * @returns */ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null, responseLength = null) { console.log('got into genQuietPrompt'); const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; let originalResponseLength = -1; try { /** @type {GenerateOptions} */ const options = { quiet_prompt, quietToLoud, skipWIAN: skipWIAN, force_name2: true, quietImage: quietImage, quietName: quietName, }; originalResponseLength = responseLengthCustomized ? saveResponseLength(main_api, responseLength) : -1; const generateFinished = await Generate('quiet', options); return generateFinished; } finally { if (responseLengthCustomized) { restoreResponseLength(main_api, originalResponseLength); } } } /** * Executes slash commands and returns the new text and whether the generation was interrupted. * @param {string} message Text to be sent * @returns {Promise<boolean>} Whether the message sending was interrupted */ export async function processCommands(message) { if (!message || !message.trim().startsWith('/')) { return false; } await executeSlashCommandsOnChatInput(message, { clearChatInput: true, }); return true; } export function sendSystemMessage(type, text, extra = {}) { const systemMessage = system_messages[type]; if (!systemMessage) { return; } const newMessage = { ...systemMessage, send_date: getMessageTimeStamp() }; if (text) { newMessage.mes = text; } if (type == system_message_types.SLASH_COMMANDS) { newMessage.mes = getSlashCommandsHelp(); } if (!newMessage.extra) { newMessage.extra = {}; } newMessage.extra = Object.assign(newMessage.extra, extra); newMessage.extra.type = type; chat.push(newMessage); addOneMessage(newMessage); is_send_press = false; if (type == system_message_types.SLASH_COMMANDS) { const browser = new SlashCommandBrowser(); const spinner = document.querySelector('#chat .last_mes .custom-slashHelp'); const parent = spinner.parentElement; spinner.remove(); browser.renderInto(parent); browser.search.focus(); } } /** * Extracts the contents of bias macros from a message. * @param {string} message Message text * @returns {string} Message bias extracted from the message (or an empty string if not found) */ export function extractMessageBias(message) { if (!message) { return ''; } try { const biasHandlebars = Handlebars.create(); const biasMatches = []; biasHandlebars.registerHelper('bias', function (text) { biasMatches.push(text); return ''; }); const template = biasHandlebars.compile(message); template({}); if (biasMatches && biasMatches.length > 0) { return ` ${biasMatches.join(' ')}`; } return ''; } catch { return ''; } } /** * Removes impersonated group member lines from the group member messages. * Doesn't do anything if group reply trimming is disabled. * @param {string} getMessage Group message * @returns Cleaned-up group message */ function cleanGroupMessage(getMessage) { if (power_user.disable_group_trimming) { return getMessage; } const group = groups.find((x) => x.id == selected_group); if (group && Array.isArray(group.members) && group.members) { for (let member of group.members) { const character = characters.find(x => x.avatar == member); if (!character) { continue; } const name = character.name; // Skip current speaker. if (name === name2) { continue; } const regex = new RegExp(`(^|\n)${escapeRegex(name)}:`); const nameMatch = getMessage.match(regex); if (nameMatch) { getMessage = getMessage.substring(0, nameMatch.index); } } } return getMessage; } function addPersonaDescriptionExtensionPrompt() { const INJECT_TAG = 'PERSONA_DESCRIPTION'; setExtensionPrompt(INJECT_TAG, '', extension_prompt_types.IN_PROMPT, 0); if (!power_user.persona_description) { return; } const promptPositions = [persona_description_positions.BOTTOM_AN, persona_description_positions.TOP_AN]; if (promptPositions.includes(power_user.persona_description_position) && shouldWIAddPrompt) { const originalAN = extension_prompts[NOTE_MODULE_NAME].value; const ANWithDesc = power_user.persona_description_position === persona_description_positions.TOP_AN ? `${power_user.persona_description}\n${originalAN}` : `${originalAN}\n${power_user.persona_description}`; setExtensionPrompt(NOTE_MODULE_NAME, ANWithDesc, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan, chat_metadata[metadata_keys.role]); } if (power_user.persona_description_position === persona_description_positions.AT_DEPTH) { setExtensionPrompt(INJECT_TAG, power_user.persona_description, extension_prompt_types.IN_CHAT, power_user.persona_description_depth, true, power_user.persona_description_role); } } function getAllExtensionPrompts() { const value = Object .values(extension_prompts) .filter(x => x.value) .map(x => x.value.trim()) .join('\n'); return value.length ? substituteParams(value) : ''; } // Wrapper to fetch extension prompts by module name export function getExtensionPromptByName(moduleName) { if (moduleName) { return substituteParams(extension_prompts[moduleName]?.value); } else { return; } } /** * Returns the extension prompt for the given position, depth, and role. * If multiple prompts are found, they are joined with a separator. * @param {number} [position] Position of the prompt * @param {number} [depth] Depth of the prompt * @param {string} [separator] Separator for joining multiple prompts * @param {number} [role] Role of the prompt * @param {boolean} [wrap] Wrap start and end with a separator * @returns {string} Extension prompt */ 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]) .filter(x => x.position == position && x.value) .filter(x => depth === undefined || x.depth === undefined || x.depth === depth) .filter(x => role === undefined || x.role === undefined || x.role === role) .map(x => x.value.trim()) .join(separator); if (wrap && extension_prompt.length && !extension_prompt.startsWith(separator)) { extension_prompt = separator + extension_prompt; } if (wrap && extension_prompt.length && !extension_prompt.endsWith(separator)) { extension_prompt = extension_prompt + separator; } if (extension_prompt.length) { extension_prompt = substituteParams(extension_prompt); } return extension_prompt; } export function baseChatReplace(value, name1, name2) { if (value !== undefined && value.length > 0) { const _ = undefined; value = substituteParams(value, name1, name2, _, _, false); if (power_user.collapse_newlines) { value = collapseNewlines(value); } value = value.replace(/\r/g, ''); } return value; } /** * Returns the character card fields for the current character. * @returns {{system: string, mesExamples: string, description: string, personality: string, persona: string, scenario: string, jailbreak: string, version: string}} */ export function getCharacterCardFields() { const result = { system: '', mesExamples: '', description: '', personality: '', persona: '', scenario: '', jailbreak: '', version: '' }; const character = characters[this_chid]; if (!character) { return result; } const scenarioText = chat_metadata['scenario'] || characters[this_chid]?.scenario; result.description = baseChatReplace(characters[this_chid].description?.trim(), name1, name2); result.personality = baseChatReplace(characters[this_chid].personality?.trim(), name1, name2); result.scenario = baseChatReplace(scenarioText.trim(), name1, name2); result.mesExamples = baseChatReplace(characters[this_chid].mes_example?.trim(), name1, name2); result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2); result.system = power_user.prefer_character_prompt ? baseChatReplace(characters[this_chid].data?.system_prompt?.trim(), name1, name2) : ''; result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(characters[this_chid].data?.post_history_instructions?.trim(), name1, name2) : ''; result.version = characters[this_chid].data?.character_version ?? ''; if (selected_group) { const groupCards = getGroupCharacterCards(selected_group, Number(this_chid)); if (groupCards) { result.description = groupCards.description; result.personality = groupCards.personality; result.scenario = groupCards.scenario; result.mesExamples = groupCards.mesExamples; } } return result; } 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) || (main_api == 'novel' && nai_settings.streaming_novel) || (main_api == 'textgenerationwebui' && textgen_settings.streaming)); } function showStopButton() { $('#mes_stop').css({ 'display': 'flex' }); } function hideStopButton() { // prevent NOOP, because hideStopButton() gets called multiple times if ($('#mes_stop').css('display') !== 'none') { $('#mes_stop').css({ 'display': 'none' }); eventSource.emit(event_types.GENERATION_ENDED, chat.length); } } class StreamingProcessor { constructor(type, force_name2, timeStarted, messageAlreadyGenerated) { this.result = ''; this.messageId = -1; this.type = type; this.force_name2 = force_name2; this.isStopped = false; this.isFinished = false; this.generator = this.nullStreamingGeneration; this.abortController = new AbortController(); this.firstMessageText = '...'; this.timeStarted = timeStarted; this.messageAlreadyGenerated = messageAlreadyGenerated; this.swipes = []; /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */ this.messageLogprobs = []; } showMessageButtons(messageId) { if (messageId == -1) { return; } showStopButton(); $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' }); } hideMessageButtons(messageId) { if (messageId == -1) { return; } hideStopButton(); $(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' }); } async onStartStreaming(text) { let messageId = -1; if (this.type == 'impersonate') { $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true })); } else { await saveReply(this.type, text, true); messageId = chat.length - 1; this.showMessageButtons(messageId); } hideSwipeButtons(); scrollChatToBottom(); return messageId; } onProgressStreaming(messageId, text, isFinal) { const isImpersonate = this.type == 'impersonate'; const isContinue = this.type == 'continue'; if (!isImpersonate && !isContinue && Array.isArray(this.swipes) && this.swipes.length > 0) { for (let i = 0; i < this.swipes.length; i++) { this.swipes[i] = cleanUpMessage(this.swipes[i], false, false, true, this.stoppingStrings); } } let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings); // Predict unbalanced asterisks / quotes during streaming const charsToBalance = ['*', '"', '```']; for (const char of charsToBalance) { if (!isFinal && isOdd(countOccurrences(processedText, char))) { // Add character at the end to balance it const separator = char.length > 1 ? '\n' : ''; processedText = processedText.trimEnd() + separator + char; } } if (isImpersonate) { $('#send_textarea').val(processedText)[0].dispatchEvent(new Event('input', { bubbles: true })); } else { let currentTime = new Date(); // Don't waste time calculating token count for streaming let currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0; const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount); chat[messageId]['mes'] = processedText; chat[messageId]['gen_started'] = this.timeStarted; chat[messageId]['gen_finished'] = currentTime; if (currentTokenCount) { if (!chat[messageId]['extra']) { chat[messageId]['extra'] = {}; } chat[messageId]['extra']['token_count'] = currentTokenCount; const tokenCounter = $(`#chat .mes[mesid="${messageId}"] .tokenCounterDisplay`); tokenCounter.text(`${currentTokenCount}t`); } if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['swipes'])) { chat[messageId]['swipes'][chat[messageId]['swipe_id']] = processedText; chat[messageId]['swipe_info'][chat[messageId]['swipe_id']] = { 'send_date': chat[messageId]['send_date'], 'gen_started': chat[messageId]['gen_started'], 'gen_finished': chat[messageId]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])) }; } let formattedText = messageFormatting( processedText, chat[messageId].name, chat[messageId].is_system, chat[messageId].is_user, messageId, ); const mesText = $(`#chat .mes[mesid="${messageId}"] .mes_text`); mesText.html(formattedText); $(`#chat .mes[mesid="${messageId}"] .mes_timer`).text(timePassed.timerValue).attr('title', timePassed.timerTitle); this.setFirstSwipe(messageId); } if (!scrollLock) { scrollChatToBottom(); } } async onFinishStreaming(messageId, text) { this.hideMessageButtons(this.messageId); this.onProgressStreaming(messageId, text, true); addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`)); if (Array.isArray(this.swipes) && this.swipes.length > 0) { const message = chat[messageId]; const swipeInfo = { send_date: message.send_date, gen_started: message.gen_started, gen_finished: message.gen_finished, extra: structuredClone(message.extra), }; const swipeInfoArray = []; swipeInfoArray.length = this.swipes.length; swipeInfoArray.fill(swipeInfo); chat[messageId].swipes.push(...this.swipes); chat[messageId].swipe_info.push(...swipeInfoArray); } if (this.type !== 'impersonate') { await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId); } else { await eventSource.emit(event_types.IMPERSONATE_READY, text); } const continueMsg = this.type === 'continue' ? this.messageAlreadyGenerated : undefined; saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), continueMsg); await saveChatConditional(); unblockGeneration(); generatedPromptCache = ''; //console.log("Generated text size:", text.length, text) if (power_user.auto_swipe) { function containsBlacklistedWords(str, blacklist, threshold) { const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi'); const matches = str.match(regex) || []; return matches.length >= threshold; } const generatedTextFiltered = (text) => { if (text) { if (power_user.auto_swipe_minimum_length) { if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) { console.log('Generated text size too small'); return true; } } if (power_user.auto_swipe_blacklist_threshold) { if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) { console.log('Generated text has blacklisted words'); return true; } } } return false; }; if (generatedTextFiltered(text)) { swipe_right(); return; } } playMessageSound(); } onErrorStreaming() { this.abortController.abort(); this.isStopped = true; this.hideMessageButtons(this.messageId); generatedPromptCache = ''; unblockGeneration(); } setFirstSwipe(messageId) { if (this.type !== 'swipe' && this.type !== 'impersonate') { if (Array.isArray(chat[messageId]['swipes']) && chat[messageId]['swipes'].length === 1 && chat[messageId]['swipe_id'] === 0) { chat[messageId]['swipes'][0] = chat[messageId]['mes']; chat[messageId]['swipe_info'][0] = { 'send_date': chat[messageId]['send_date'], 'gen_started': chat[messageId]['gen_started'], 'gen_finished': chat[messageId]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])) }; } } } onStopStreaming() { this.onErrorStreaming(); } /** * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs }, void, void>} */ *nullStreamingGeneration() { throw new Error('Generation function for streaming is not hooked up'); } async generate() { if (this.messageId == -1) { this.messageId = await this.onStartStreaming(this.firstMessageText); await delay(1); // delay for message to be rendered scrollLock = false; } // Stopping strings are expensive to calculate, especially with macros enabled. To remove stopping strings // when streaming, we cache the result of getStoppingStrings instead of calling it once per token. const isImpersonate = this.type == 'impersonate'; const isContinue = this.type == 'continue'; this.stoppingStrings = getStoppingStrings(isImpersonate, isContinue); try { const sw = new Stopwatch(1000 / power_user.streaming_fps); const timestamps = []; for await (const { text, swipes, logprobs } of this.generator()) { timestamps.push(Date.now()); if (this.isStopped) { return; } this.result = text; this.swipes = Array.from(swipes ?? []); if (logprobs) { this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); } await sw.tick(() => this.onProgressStreaming(this.messageId, this.messageAlreadyGenerated + text)); } const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000; console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`); } catch (err) { console.error(err); this.onErrorStreaming(); return; } this.isFinished = true; return this.result; } } /** * Generates a message using the provided prompt. * @param {string} prompt Prompt to generate a message from * @param {string} api API to use. Main API is used if not specified. * @param {boolean} instructOverride true to override instruct mode, false to use the default value * @param {boolean} quietToLoud true to generate a message in system mode, false to generate a message in character mode * @param {string} [systemPrompt] System prompt to use. Only Instruct mode or OpenAI. * @param {number} [responseLength] Maximum response length. If unset, the global default value is used. * @returns {Promise<string>} Generated message */ export async function generateRaw(prompt, api, instructOverride, quietToLoud, systemPrompt, responseLength) { if (!api) { api = main_api; } const abortController = new AbortController(); const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; let originalResponseLength = -1; const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride; const isQuiet = true; if (systemPrompt) { systemPrompt = substituteParams(systemPrompt); systemPrompt = isInstruct ? formatInstructModeSystemPrompt(systemPrompt) : systemPrompt; prompt = api === 'openai' ? prompt : `${systemPrompt}\n${prompt}`; } prompt = substituteParams(prompt); prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt; prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt; prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2, isQuiet, quietToLoud)) : (prompt + '\n'); try { originalResponseLength = responseLengthCustomized ? saveResponseLength(api, responseLength) : -1; let generateData = {}; switch (api) { case 'kobold': case 'koboldhorde': if (preset_settings === 'gui') { generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server }; } else { const isHorde = api === 'koboldhorde'; const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]]; generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet'); } break; case 'novel': { const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null, 'quiet'); break; } case 'textgenerationwebui': generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet'); break; case 'openai': { generateData = [{ role: 'user', content: prompt.trim() }]; if (systemPrompt) { generateData.unshift({ role: 'system', content: systemPrompt.trim() }); } } break; } let data = {}; if (api == 'koboldhorde') { data = await generateHorde(prompt, generateData, abortController.signal, false); } else if (api == 'openai') { data = await sendOpenAIRequest('quiet', generateData, abortController.signal); } else { const generateUrl = getGenerateUrl(api); const response = await fetch(generateUrl, { method: 'POST', headers: getRequestHeaders(), cache: 'no-cache', body: JSON.stringify(generateData), signal: abortController.signal, }); if (!response.ok) { const error = await response.json(); throw error; } data = await response.json(); } if (data.error) { throw new Error(data.error); } const message = cleanUpMessage(extractMessageFromData(data), false, false, true); if (!message) { throw new Error('No message generated'); } return message; } finally { if (responseLengthCustomized) { restoreResponseLength(api, originalResponseLength); } } } /** * Temporarily change the response length for the specified API. * @param {string} api API to use. * @param {number} responseLength Target response length. * @returns {number} The original response length. */ function saveResponseLength(api, responseLength) { let oldValue = -1; if (api === 'openai') { oldValue = oai_settings.openai_max_tokens; oai_settings.openai_max_tokens = responseLength; } else { oldValue = amount_gen; amount_gen = responseLength; } return oldValue; } /** * Restore the original response length for the specified API. * @param {string} api API to use. * @param {number} responseLength Target response length. * @returns {void} */ function restoreResponseLength(api, responseLength) { if (api === 'openai') { oai_settings.openai_max_tokens = responseLength; } else { amount_gen = responseLength; } } /** * Runs a generation using the current chat context. * @param {string} type Generation type * @param {GenerateOptions} options Generation options * @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt * @returns {Promise<any>} 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 */ 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); generation_started = new Date(); // Don't recreate abort controller if signal is passed if (!(abortController && signal)) { abortController = new AbortController(); } // OpenAI doesn't need instruct mode. Use OAI main prompt instead. const isInstruct = power_user.instruct.enabled && main_api !== 'openai'; const isImpersonate = type == 'impersonate'; let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `; if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) { const interruptedByCommand = await processCommands(String($('#send_textarea').val())); if (interruptedByCommand) { //$("#send_textarea").val('')[0].dispatchEvent(new Event('input', { bubbles:true })); unblockGeneration(type); return Promise.resolve(); } } if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) { toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true }); unblockGeneration(type); return Promise.resolve(); } if (main_api === 'textgenerationwebui' && textgen_settings.streaming && textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE)) { toastr.error('Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.', undefined, { timeOut: 10000, preventDuplicates: true }); unblockGeneration(type); return Promise.resolve(); } if (isHordeGenerationNotAllowed()) { unblockGeneration(type); return Promise.resolve(); } if (!dryRun) { // Ping server to make sure it is still alive const pingResult = await pingServer(); if (!pingResult) { unblockGeneration(type); toastr.error('Verify that the server is running and accessible.', 'ST Server cannot be reached'); throw new Error('Server unreachable'); } // Hide swipes if not in a dry run. hideSwipeButtons(); // If generated any message, set the flag to indicate it can't be recreated again. chat_metadata['tainted'] = true; } if (selected_group && !is_group_generating) { if (!dryRun) { // Returns the promise that generateGroupWrapper returns; resolves when generation is done return generateGroupWrapper(false, type, { quiet_prompt, force_chid, signal: abortController.signal, quietImage, maxLoops }); } const characterIndexMap = new Map(characters.map((char, index) => [char.avatar, index])); const group = groups.find((x) => x.id === selected_group); const enabledMembers = group.members.reduce((acc, member) => { if (!group.disabled_members.includes(member) && !acc.includes(member)) { acc.push(member); } return acc; }, []); const memberIds = enabledMembers .map((member) => characterIndexMap.get(member)) .filter((index) => index !== undefined && index !== null); if (memberIds.length > 0) { setCharacterId(memberIds[0]); setCharacterName(''); } else { console.log('No enabled members found'); unblockGeneration(type); return Promise.resolve(); } } //#########QUIET PROMPT STUFF############## //this function just gives special care to novel quiet instruction prompts if (quiet_prompt) { quiet_prompt = substituteParams(quiet_prompt); quiet_prompt = main_api == 'novel' && !quietToLoud ? adjustNovelInstructionPrompt(quiet_prompt) : quiet_prompt; } const isChatValid = online_status !== 'no_connection' && this_chid !== undefined; // We can't do anything because we're not in a chat right now. (Unless it's a dry run, in which case we need to // assemble the prompt so we can count its tokens regardless of whether a chat is active.) if (!dryRun && !isChatValid) { if (this_chid === undefined) { toastr.warning('Сharacter is not selected'); } is_send_press = false; return Promise.resolve(); } let textareaText; if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) { is_send_press = true; textareaText = String($('#send_textarea').val()); $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true })); } else { textareaText = ''; if (chat.length && chat[chat.length - 1]['is_user']) { //do nothing? why does this check exist? } else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) { chat.length = chat.length - 1; $('#chat').children().last().hide(250, function () { $(this).remove(); }); await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); } } const isContinue = type == 'continue'; // Rewrite the generation timer to account for the time passed for all the continuations. if (isContinue && chat.length) { const prevFinished = chat[chat.length - 1]['gen_finished']; const prevStarted = chat[chat.length - 1]['gen_started']; if (prevFinished && prevStarted) { const timePassed = prevFinished - prevStarted; generation_started = new Date(Date.now() - timePassed); chat[chat.length - 1]['gen_started'] = generation_started; } } if (!dryRun) { deactivateSendButtons(); } let { messageBias, promptBias, isUserPromptBias } = getBiasStrings(textareaText, type); //********************************* //PRE FORMATING STRING //********************************* //for normal messages sent from user.. if ((textareaText != '' || hasPendingFileAttachment()) && !automatic_trigger && type !== 'quiet' && !dryRun) { // If user message contains no text other than bias - send as a system message if (messageBias && !removeMacros(textareaText)) { sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias }); } else { await sendMessageAsUser(textareaText, messageBias); } } else if (textareaText == '' && !automatic_trigger && !dryRun && type === undefined && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0) { // Use send_if_empty if set and the user message is empty. Only when sending messages normally await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias); } let { description, personality, persona, scenario, mesExamples, system, jailbreak, } = getCharacterCardFields(); if (isInstruct) { system = power_user.prefer_character_prompt && system ? system : baseChatReplace(power_user.instruct.system_prompt, name1, name2); system = formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.instruct.system_prompt)); } // Depth prompt (character-specific A/N) removeDepthPrompts(); const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid)); if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) { groupDepthPrompts.forEach((value, index) => { const role = getExtensionPromptRoleByName(value.role); setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan, role); }); } else { const depthPromptText = baseChatReplace(characters[this_chid].data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || ''; const depthPromptDepth = characters[this_chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default; const depthPromptRole = getExtensionPromptRoleByName(characters[this_chid].data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default); setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan, depthPromptRole); } // First message in fresh 1-on-1 chat reacts to user/character settings changes if (chat.length) { chat[0].mes = substituteParams(chat[0].mes); } // Collect messages with usable content let coreChat = chat.filter(x => !x.is_system); if (type === 'swipe') { coreChat.pop(); } coreChat = await Promise.all(coreChat.map(async (chatItem, index) => { let message = chatItem.mes; let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT; let options = { isPrompt: true, depth: (coreChat.length - index - 1) }; let regexedMessage = getRegexedString(message, regexType, options); regexedMessage = await appendFileContent(chatItem, regexedMessage); return { ...chatItem, mes: regexedMessage, index, }; })); // Determine token limit let this_max_context = getMaxContextSize(); if (!dryRun && type !== 'quiet') { console.debug('Running extension interceptors'); const aborted = await runGenerationInterceptors(coreChat, this_max_context); if (aborted) { console.debug('Generation aborted by extension interceptors'); unblockGeneration(type); return Promise.resolve(); } } else { console.debug('Skipping extension interceptors for dry run'); } // Adjust token limit for Horde let adjustedParams; if (main_api == 'koboldhorde' && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) { try { adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen); } catch { unblockGeneration(type); return Promise.resolve(); } if (horde_settings.auto_adjust_context_length) { this_max_context = (adjustedParams.maxContextLength - adjustedParams.maxLength); } } console.log(`Core/all messages: ${coreChat.length}/${chat.length}`); // kingbri MARK: - Make sure the prompt bias isn't the same as the user bias if ((promptBias && !isUserPromptBias) || power_user.always_force_name2 || main_api == 'novel') { force_name2 = true; } if (isImpersonate) { force_name2 = false; } // TODO (kingbri): Migrate to a utility function /** * Parses an examples string. * @param {string} examplesStr * @returns {string[]} Examples array with block heading */ function parseMesExamples(examplesStr) { if (examplesStr.length === 0 || examplesStr === '<START>') { return []; } if (!examplesStr.startsWith('<START>')) { examplesStr = '<START>\n' + examplesStr.trim(); } const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : ''; const blockHeading = main_api === 'openai' ? '<START>\n' : (exampleSeparator || (isInstruct ? '<START>\n' : '')); const splitExamples = examplesStr.split(/<START>/gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`); return splitExamples; } let mesExamplesArray = parseMesExamples(mesExamples); ////////////////////////////////// // Extension added strings // Set non-WI AN setFloatingPrompt(); // Add persona description to prompt addPersonaDescriptionExtensionPrompt(); // Add WI to prompt (and also inject WI to AN value via hijack) // Make quiet prompt available for WIAN setExtensionPrompt('QUIET_PROMPT', quiet_prompt || '', extension_prompt_types.IN_PROMPT, 0, true); const chatForWI = coreChat.map(x => `${x.name}: ${x.mes}`).reverse(); const { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoExamples, worldInfoDepth } = await getWorldInfoPrompt(chatForWI, this_max_context, dryRun); setExtensionPrompt('QUIET_PROMPT', '', extension_prompt_types.IN_PROMPT, 0, true); // Add message example WI for (const example of worldInfoExamples) { const exampleMessage = example.content; if (exampleMessage.length === 0) { continue; } const formattedExample = baseChatReplace(exampleMessage, name1, name2); const cleanedExample = parseMesExamples(formattedExample); // Insert depending on before or after position if (example.position === wi_anchor_position.before) { mesExamplesArray.unshift(...cleanedExample); } else { mesExamplesArray.push(...cleanedExample); } } // At this point, the raw message examples can be created const mesExamplesRawArray = [...mesExamplesArray]; if (mesExamplesArray && isInstruct) { mesExamplesArray = formatInstructModeExamples(mesExamplesArray, name1, name2); } if (skipWIAN !== true) { console.log('skipWIAN not active, adding WIAN'); // Add all depth WI entries to prompt flushWIDepthInjections(); if (Array.isArray(worldInfoDepth)) { worldInfoDepth.forEach((e) => { const joinedEntries = e.entries.join('\n'); setExtensionPrompt(`customDepthWI-${e.depth}-${e.role}`, joinedEntries, extension_prompt_types.IN_CHAT, e.depth, false, e.role); }); } } else { console.log('skipping WIAN'); } // Inject all Depth prompts. Chat Completion does it separately let injectedIndices = []; if (main_api !== 'openai') { injectedIndices = doChatInject(coreChat, isContinue); } // Insert character jailbreak as the last user message (if exists, allowed, preferred, and not using Chat Completion) if (power_user.context.allow_jailbreak && power_user.prefer_character_jailbreak && main_api !== 'openai' && jailbreak) { // Set "original" explicity to empty string since there's no original jailbreak = substituteParams(jailbreak, name1, name2, ''); // When continuing generation of previous output, last user message precedes the message to continue if (isContinue) { coreChat.splice(coreChat.length - 1, 0, { mes: jailbreak, is_user: true }); } else { coreChat.push({ mes: jailbreak, is_user: true }); } } let chat2 = []; let continue_mag = ''; const userMessageIndices = []; for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) { if (main_api == 'openai') { chat2[i] = coreChat[j].mes; if (i === 0 && isContinue) { chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length); continue_mag = coreChat[j].mes; } continue; } chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, false); if (j === 0 && isInstruct) { // Reformat with the first output sequence (if any) chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST); } // Do not suffix the message for continuation if (i === 0 && isContinue) { if (isInstruct) { // Reformat with the last output sequence (if any) chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST); } chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length); continue_mag = coreChat[j].mes; } if (coreChat[j].is_user) { userMessageIndices.push(i); } } let addUserAlignment = isInstruct && power_user.instruct.user_alignment_message; let userAlignmentMessage = ''; if (addUserAlignment) { const alignmentMessage = { name: name1, mes: power_user.instruct.user_alignment_message, is_user: true, }; userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, false); } // Call combined AN into Generate const beforeScenarioAnchor = getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT).trimStart(); const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.IN_PROMPT); const storyStringParams = { description: description, personality: personality, persona: persona, scenario: scenario, system: isInstruct ? system : '', char: name2, user: name1, wiBefore: worldInfoBefore, wiAfter: worldInfoAfter, loreBefore: worldInfoBefore, loreAfter: worldInfoAfter, mesExamples: mesExamplesArray.join(''), mesExamplesRaw: mesExamplesRawArray.join(''), }; const storyString = renderStoryString(storyStringParams); // Story string rendered, safe to remove if (power_user.strip_examples) { mesExamplesArray = []; } let oaiMessages = []; let oaiMessageExamples = []; if (main_api === 'openai') { message_already_generated = ''; oaiMessages = setOpenAIMessages(coreChat); oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray); } // hack for regeneration of the first message if (chat2.length == 0) { chat2.push(''); } let examplesString = ''; let chatString = ''; let cyclePrompt = ''; async function getMessagesTokenCount() { const encodeString = [ beforeScenarioAnchor, storyString, afterScenarioAnchor, examplesString, chatString, quiet_prompt, cyclePrompt, userAlignmentMessage, ].join('').replace(/\r/gm, ''); return getTokenCountAsync(encodeString, power_user.token_padding); } // Force pinned examples into the context let pinExmString; if (power_user.pin_examples) { pinExmString = examplesString = mesExamplesArray.join(''); } // Only add the chat in context if past the greeting message if (isContinue && (chat2.length > 1 || main_api === 'openai')) { cyclePrompt = chat2.shift(); } // Collect enough messages to fill the context let arrMes = new Array(chat2.length); let tokenCount = await getMessagesTokenCount(); let lastAddedIndex = -1; // Pre-allocate all injections first. // If it doesn't fit - user shot himself in the foot for (const index of injectedIndices) { const item = chat2[index]; if (typeof item !== 'string') { continue; } tokenCount += await getTokenCountAsync(item.replace(/\r/gm, '')); chatString = item + chatString; if (tokenCount < this_max_context) { arrMes[index] = item; lastAddedIndex = Math.max(lastAddedIndex, index); } else { break; } } for (let i = 0; i < chat2.length; i++) { // not needed for OAI prompting if (main_api == 'openai') { break; } // Skip already injected messages if (arrMes[i] !== undefined) { continue; } const item = chat2[i]; if (typeof item !== 'string') { continue; } tokenCount += await getTokenCountAsync(item.replace(/\r/gm, '')); chatString = item + chatString; if (tokenCount < this_max_context) { arrMes[i] = item; lastAddedIndex = Math.max(lastAddedIndex, i); } else { break; } } // Add user alignment message if last message is not a user message const stoppedAtUser = userMessageIndices.includes(lastAddedIndex); if (addUserAlignment && !stoppedAtUser) { tokenCount += await getTokenCountAsync(userAlignmentMessage.replace(/\r/gm, '')); chatString = userAlignmentMessage + chatString; arrMes.push(userAlignmentMessage); injectedIndices.push(arrMes.length - 1); } // Unsparse the array. Adjust injected indices const newArrMes = []; const newInjectedIndices = []; for (let i = 0; i < arrMes.length; i++) { if (arrMes[i] !== undefined) { newArrMes.push(arrMes[i]); if (injectedIndices.includes(i)) { newInjectedIndices.push(newArrMes.length - 1); } } } arrMes = newArrMes; injectedIndices = newInjectedIndices; if (main_api !== 'openai') { setInContextMessages(arrMes.length - injectedIndices.length, type); } // Estimate how many unpinned example messages fit in the context tokenCount = await getMessagesTokenCount(); let count_exm_add = 0; if (!power_user.pin_examples) { for (let example of mesExamplesArray) { tokenCount += await getTokenCountAsync(example.replace(/\r/gm, '')); examplesString += example; if (tokenCount < this_max_context) { count_exm_add++; } else { break; } } } let mesSend = []; console.debug('calling runGenerate'); if (isContinue) { // Coping mechanism for OAI spacing const isForceInstruct = isOpenRouterWithInstruct(); if (main_api === 'openai' && !isForceInstruct && !cyclePrompt.endsWith(' ')) { cyclePrompt += oai_settings.continue_postfix; continue_mag += oai_settings.continue_postfix; } message_already_generated = continue_mag; } const originalType = type; if (!dryRun) { is_send_press = true; } generatedPromptCache += cyclePrompt; if (generatedPromptCache.length == 0 || type === 'continue') { console.debug('generating prompt'); chatString = ''; arrMes = arrMes.reverse(); arrMes.forEach(function (item, i, arr) { // OAI doesn't need all of this if (main_api === 'openai') { return; } // Cohee: This removes a newline from the end of the last message in the context // Last prompt line will add a newline if it's not a continuation // In instruct mode it only removes it if wrap is enabled and it's not a quiet generation if (i === arrMes.length - 1 && type !== 'continue') { if (!isInstruct || (power_user.instruct.wrap && type !== 'quiet')) { item = item.replace(/\n?$/, ''); } } mesSend[mesSend.length] = { message: item, extensionPrompts: [] }; }); } let mesExmString = ''; function setPromptString() { if (main_api == 'openai') { return; } console.debug('--setting Prompt string'); mesExmString = pinExmString ?? mesExamplesArray.slice(0, count_exm_add).join(''); if (mesSend.length) { mesSend[mesSend.length - 1].message = modifyLastPromptLine(mesSend[mesSend.length - 1].message); } } function modifyLastPromptLine(lastMesString) { //#########QUIET PROMPT STUFF PT2############## // Add quiet generation prompt at depth 0 if (quiet_prompt && quiet_prompt.length) { // here name1 is forced for all quiet prompts..why? const name = name1; //checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true, '', name1, name2, false) : `\n${quiet_prompt}`; //This begins to fix quietPrompts (particularly /sysgen) for instruct //previously instruct input sequence was being appended to the last chat message w/o '\n' //and no output sequence was added after the input's content. //TODO: respect output_sequence vs last_output_sequence settings //TODO: decide how to prompt this to clarify who is talking 'Narrator', 'System', etc. if (isInstruct) { lastMesString += quietAppend; // + power_user.instruct.output_sequence + '\n'; } else { lastMesString += quietAppend; } // Ross: bailing out early prevents quiet prompts from respecting other instruct prompt toggles // for sysgen, SD, and summary this is desireable as it prevents the AI from responding as char.. // but for idle prompting, we want the flexibility of the other prompt toggles, and to respect them as per settings in the extension // need a detection for what the quiet prompt is being asked for... // Bail out early? if (!isInstruct && !quietToLoud) { return lastMesString; } } // Get instruct mode line if (isInstruct && !isContinue) { const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2); const isQuiet = quiet_prompt && type == 'quiet'; lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, quietToLoud); } // Get non-instruct impersonation line if (!isInstruct && isImpersonate && !isContinue) { const name = name1; if (!lastMesString.endsWith('\n')) { lastMesString += '\n'; } lastMesString += name + ':'; } // Add character's name // Force name append on continue (if not continuing on user message or first message) const isContinuingOnFirstMessage = chat.length === 1 && isContinue; if (!isInstruct && force_name2 && !isContinuingOnFirstMessage) { if (!lastMesString.endsWith('\n')) { lastMesString += '\n'; } if (!isContinue || !(chat[chat.length - 1]?.is_user)) { lastMesString += `${name2}:`; } } return lastMesString; } // Clean up the already generated prompt for seamless addition function cleanupPromptCache(promptCache) { // Remove the first occurrance of character's name if (promptCache.trimStart().startsWith(`${name2}:`)) { promptCache = promptCache.replace(`${name2}:`, '').trimStart(); } // Remove the first occurrance of prompt bias if (promptCache.trimStart().startsWith(promptBias)) { promptCache = promptCache.replace(promptBias, ''); } // Add a space if prompt cache doesn't start with one if (!/^\s/.test(promptCache) && !isInstruct) { promptCache = ' ' + promptCache; } return promptCache; } async function checkPromptSize() { console.debug('---checking Prompt size'); setPromptString(); const prompt = [ beforeScenarioAnchor, storyString, afterScenarioAnchor, mesExmString, mesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''), '\n', generatedPromptCache, quiet_prompt, ].join('').replace(/\r/gm, ''); let thisPromptContextSize = await getTokenCountAsync(prompt, power_user.token_padding); if (thisPromptContextSize > this_max_context) { //if the prepared prompt is larger than the max context size... if (count_exm_add > 0) { // ..and we have example mesages.. count_exm_add--; // remove the example messages... await checkPromptSize(); // and try agin... } else if (mesSend.length > 0) { // if the chat history is longer than 0 mesSend.shift(); // remove the first (oldest) chat entry.. await checkPromptSize(); // and check size again.. } else { //end console.debug(`---mesSend.length = ${mesSend.length}`); } } } if (generatedPromptCache.length > 0 && main_api !== 'openai') { console.debug('---Generated Prompt Cache length: ' + generatedPromptCache.length); await checkPromptSize(); } else { console.debug('---calling setPromptString ' + generatedPromptCache.length); setPromptString(); } // Fetches the combined prompt for both negative and positive prompts const cfgGuidanceScale = getGuidanceScale(); const useCfgPrompt = cfgGuidanceScale && cfgGuidanceScale.value !== 1; // For prompt bit itemization let mesSendString = ''; function getCombinedPrompt(isNegative) { // Only return if the guidance scale doesn't exist or the value is 1 // Also don't return if constructing the neutral prompt if (isNegative && !useCfgPrompt) { return; } // OAI has its own prompt manager. No need to do anything here if (main_api === 'openai') { return ''; } // Deep clone let finalMesSend = structuredClone(mesSend); if (useCfgPrompt) { const cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative); if (cfgPrompt.value) { if (cfgPrompt.depth === 0) { finalMesSend[finalMesSend.length - 1].message += /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) ? cfgPrompt.value : ` ${cfgPrompt.value}`; } else { // TODO: Make all extension prompts use an array/splice method const lengthDiff = mesSend.length - cfgPrompt.depth; const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0; finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`); } } } // Add prompt bias after everything else // Always run with continue if (!isInstruct && !isImpersonate) { if (promptBias.trim().length !== 0) { finalMesSend[finalMesSend.length - 1].message += /\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)) ? promptBias.trimStart() : ` ${promptBias.trimStart()}`; } } // Prune from prompt cache if it exists if (generatedPromptCache.length !== 0) { generatedPromptCache = cleanupPromptCache(generatedPromptCache); } // Flattens the multiple prompt objects to a string. const combine = () => { // Right now, everything is suffixed with a newline mesSendString = finalMesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''); // add a custom dingus (if defined) mesSendString = addChatsSeparator(mesSendString); // add chat preamble mesSendString = addChatsPreamble(mesSendString); let combinedPrompt = beforeScenarioAnchor + storyString + afterScenarioAnchor + mesExmString + mesSendString + generatedPromptCache; combinedPrompt = combinedPrompt.replace(/\r/gm, ''); if (power_user.collapse_newlines) { combinedPrompt = collapseNewlines(combinedPrompt); } return combinedPrompt; }; finalMesSend.forEach((item, i) => { item.injected = injectedIndices.includes(finalMesSend.length - i - 1); }); let data = { api: main_api, combinedPrompt: null, description, personality, persona, scenario, char: name2, user: name1, worldInfoBefore, worldInfoAfter, beforeScenarioAnchor, afterScenarioAnchor, storyString, mesExmString, mesSendString, finalMesSend, generatedPromptCache, main: system, jailbreak, naiPreamble: nai_settings.preamble, }; // Before returning the combined prompt, give available context related information to all subscribers. eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data); // If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context. return !data.combinedPrompt ? combine() : data.combinedPrompt; } let finalPrompt = getCombinedPrompt(false); const eventData = { prompt: finalPrompt, dryRun: dryRun }; await eventSource.emit(event_types.GENERATE_AFTER_COMBINE_PROMPTS, eventData); finalPrompt = eventData.prompt; let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate let thisPromptBits = []; let generate_data; switch (main_api) { case 'koboldhorde': case 'kobold': if (main_api == 'koboldhorde' && horde_settings.auto_adjust_response_length) { maxLength = Math.min(maxLength, adjustedParams.maxLength); maxLength = Math.max(maxLength, MIN_LENGTH); // prevent validation errors } generate_data = { prompt: finalPrompt, gui_settings: true, max_length: maxLength, max_context_length: max_context, api_server, }; if (preset_settings != 'gui') { const isHorde = main_api == 'koboldhorde'; const presetSettings = koboldai_settings[koboldai_setting_names[preset_settings]]; const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context; generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type); } break; case 'textgenerationwebui': { const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale, negativePrompt: getCombinedPrompt(true) } : null; generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type); break; } case 'novel': { const cfgValues = useCfgPrompt ? { guidanceScale: cfgGuidanceScale } : null; const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, isContinue, cfgValues, type); break; } case 'openai': { let [prompt, counts] = await prepareOpenAIMessages({ name2: name2, charDescription: description, charPersonality: personality, Scenario: scenario, worldInfoBefore: worldInfoBefore, worldInfoAfter: worldInfoAfter, extensionPrompts: extension_prompts, bias: promptBias, type: type, quietPrompt: quiet_prompt, quietImage: quietImage, cyclePrompt: cyclePrompt, systemPromptOverride: system, jailbreakPromptOverride: jailbreak, personaDescription: persona, messages: oaiMessages, messageExamples: oaiMessageExamples, }, dryRun); generate_data = { prompt: prompt }; // TODO: move these side-effects somewhere else, so this switch-case solely sets generate_data // counts will return false if the user has not enabled the token breakdown feature if (counts) { parseTokenCounts(counts, thisPromptBits); } if (!dryRun) { setInContextMessages(openai_messages_count, type); } break; } } if (dryRun) { generatedPromptCache = ''; return Promise.resolve(); } async function finishGenerating() { if (power_user.console_log_prompts) { console.log(generate_data.prompt); } console.debug('rungenerate calling API'); showStopButton(); //set array object for prompt token itemization of this message let currentArrayEntry = Number(thisPromptBits.length - 1); let additionalPromptStuff = { ...thisPromptBits[currentArrayEntry], rawPrompt: generate_data.prompt || generate_data.input, mesId: getNextMessageId(type), allAnchors: getAllExtensionPrompts(), chatInjects: injectedIndices?.map(index => arrMes[arrMes.length - index - 1])?.join('') || '', summarizeString: (extension_prompts['1_memory']?.value || ''), authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''), smartContextString: (extension_prompts['chromadb']?.value || ''), worldInfoString: worldInfoString, storyString: storyString, beforeScenarioAnchor: beforeScenarioAnchor, afterScenarioAnchor: afterScenarioAnchor, examplesString: examplesString, mesSendString: mesSendString, generatedPromptCache: generatedPromptCache, promptBias: promptBias, finalPrompt: finalPrompt, charDescription: description, charPersonality: personality, scenarioText: scenario, this_max_context: this_max_context, padding: power_user.token_padding, main_api: main_api, instruction: isInstruct ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.instruct.system_prompt) : '', userPersona: (power_user.persona_description || ''), }; //console.log(additionalPromptStuff); const itemizedIndex = itemizedPrompts.findIndex((item) => item.mesId === additionalPromptStuff.mesId); if (itemizedIndex !== -1) { itemizedPrompts[itemizedIndex] = additionalPromptStuff; } else { itemizedPrompts.push(additionalPromptStuff); } console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`); if (isStreamingEnabled() && type !== 'quiet') { streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generated); if (isContinue) { // Save reply does add cycle text to the prompt, so it's not needed here streamingProcessor.firstMessageText = ''; } streamingProcessor.generator = await sendStreamingRequest(type, generate_data); hideSwipeButtons(); let getMessage = await streamingProcessor.generate(); let messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); if (isContinue) { getMessage = continue_mag + getMessage; } if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) { await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage); streamingProcessor = null; triggerAutoContinue(messageChunk, isImpersonate); return Object.defineProperties(new String(getMessage), { 'messageChunk': { value: messageChunk }, 'fromStream': { value: true }, }); } } else { return await sendGenerationRequest(type, generate_data); } } return finishGenerating().then(onSuccess, onError); async function onSuccess(data) { if (!data) return; if (data?.fromStream) { return data; } let messageChunk = ''; if (data.error) { unblockGeneration(type); generatedPromptCache = ''; if (data?.response) { toastr.error(data.response, 'API Error'); } throw data?.response; } //const getData = await response.json(); let getMessage = extractMessageFromData(data); let title = extractTitleFromData(data); kobold_horde_model = title; const swipes = extractMultiSwipes(data, type); messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); if (isContinue) { getMessage = continue_mag + getMessage; } //Formating const displayIncomplete = type === 'quiet' && !quietToLoud; getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete); if (getMessage.length > 0 || data.allowEmptyResponse) { if (isImpersonate) { $('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles: true })); generatedPromptCache = ''; await eventSource.emit(event_types.IMPERSONATE_READY, getMessage); } else if (type == 'quiet') { unblockGeneration(type); return getMessage; } else { // Without streaming we'll be having a full message on continuation. Treat it as a last chunk. if (originalType !== 'continue') { ({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes)); } else { ({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes)); } // This relies on `saveReply` having been called to add the message to the chat, so it must be last. parseAndSaveLogprobs(data, continue_mag); } if (type !== 'quiet') { playMessageSound(); } } else { // If maxLoops is not passed in (e.g. first time generating), set it to MAX_GENERATION_LOOPS maxLoops ??= MAX_GENERATION_LOOPS; if (maxLoops === 0) { if (type !== 'quiet') { throwCircuitBreakerError(); } throw new Error('Generate circuit breaker interruption'); } // regenerate with character speech reenforced // to make sure we leave on swipe type while also adding the name2 appendage await delay(1000); // A message was already deleted on regeneration, so instead treat is as a normal gen if (type === 'regenerate') { type = 'normal'; } // The first await is for waiting for the generate to start. The second one is waiting for it to finish const result = await await Generate(type, { automatic_trigger, force_name2: true, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, maxLoops: maxLoops - 1 }); return result; } if (power_user.auto_swipe) { console.debug('checking for autoswipeblacklist on non-streaming message'); function containsBlacklistedWords(getMessage, blacklist, threshold) { console.debug('checking blacklisted words'); const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi'); const matches = getMessage.match(regex) || []; return matches.length >= threshold; } const generatedTextFiltered = (getMessage) => { if (power_user.auto_swipe_blacklist_threshold) { if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) { console.debug('Generated text has blacklisted words'); return true; } } return false; }; if (generatedTextFiltered(getMessage)) { console.debug('swiping right automatically'); is_send_press = false; swipe_right(); // TODO: do we want to resolve after an auto-swipe? return; } } console.debug('/api/chats/save called by /Generate'); await saveChatConditional(); unblockGeneration(type); streamingProcessor = null; if (type !== 'quiet') { triggerAutoContinue(messageChunk, isImpersonate); } // Don't break the API chain that expects a single string in return return Object.defineProperty(new String(getMessage), 'messageChunk', { value: messageChunk }); } function onError(exception) { if (typeof exception?.error?.message === 'string') { toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 }); } generatedPromptCache = ''; unblockGeneration(type); console.log(exception); streamingProcessor = null; throw exception; } } /** * Injects extension prompts into chat messages. * @param {object[]} messages Array of chat messages * @param {boolean} isContinue Whether the generation is a continuation. If true, the extension prompts of depth 0 are injected at position 1. * @returns {number[]} Array of indices where the extension prompts were injected */ function doChatInject(messages, isContinue) { const injectedIndices = []; let totalInsertedMessages = 0; messages.reverse(); for (let i = 0; i <= MAX_INJECTION_DEPTH; i++) { // Order of priority (most important go lower) const roles = [extension_prompt_roles.SYSTEM, extension_prompt_roles.USER, extension_prompt_roles.ASSISTANT]; const names = { [extension_prompt_roles.SYSTEM]: '', [extension_prompt_roles.USER]: name1, [extension_prompt_roles.ASSISTANT]: name2, }; const roleMessages = []; const separator = '\n'; const wrap = false; for (const role of roles) { const extensionPrompt = String(getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, role, wrap)).trimStart(); const isNarrator = role === extension_prompt_roles.SYSTEM; const isUser = role === extension_prompt_roles.USER; const name = names[role]; if (extensionPrompt) { roleMessages.push({ name: name, is_user: isUser, mes: extensionPrompt, extra: { type: isNarrator ? system_message_types.NARRATOR : null, }, }); } } if (roleMessages.length) { const depth = isContinue && i === 0 ? 1 : i; const injectIdx = depth + totalInsertedMessages; messages.splice(injectIdx, 0, ...roleMessages); totalInsertedMessages += roleMessages.length; injectedIndices.push(...Array.from({ length: roleMessages.length }, (_, i) => injectIdx + i)); } } messages.reverse(); return injectedIndices; } function flushWIDepthInjections() { //prevent custom depth WI entries (which have unique random key names) from duplicating for (const key of Object.keys(extension_prompts)) { if (key.startsWith('customDepthWI')) { delete extension_prompts[key]; } } } /** * Unblocks the UI after a generation is complete. * @param {string} [type] Generation type (optional) */ function unblockGeneration(type) { // Don't unblock if a parallel stream is still running if (type === 'quiet' && streamingProcessor && !streamingProcessor.isFinished) { return; } is_send_press = false; activateSendButtons(); showSwipeButtons(); setGenerationProgress(0); flushEphemeralStoppingStrings(); flushWIDepthInjections(); } export function getNextMessageId(type) { return type == 'swipe' ? chat.length - 1 : chat.length; } /** * Determines if the message should be auto-continued. * @param {string} messageChunk Current message chunk * @param {boolean} isImpersonate Is the user impersonation * @returns {boolean} Whether the message should be auto-continued */ export function shouldAutoContinue(messageChunk, isImpersonate) { if (!power_user.auto_continue.enabled) { console.debug('Auto-continue is disabled by user.'); return false; } if (typeof messageChunk !== 'string') { console.debug('Not triggering auto-continue because message chunk is not a string'); return false; } if (isImpersonate) { console.log('Continue for impersonation is not implemented yet'); return false; } if (is_send_press) { console.debug('Auto-continue is disabled because a message is currently being sent.'); return false; } if (power_user.auto_continue.target_length <= 0) { console.log('Auto-continue target length is 0, not triggering auto-continue'); return false; } if (main_api === 'openai' && !power_user.auto_continue.allow_chat_completions) { console.log('Auto-continue for OpenAI is disabled by user.'); return false; } const textareaText = String($('#send_textarea').val()); const USABLE_LENGTH = 5; if (textareaText.length > 0) { console.log('Not triggering auto-continue because user input is not empty'); return false; } if (messageChunk.trim().length > USABLE_LENGTH && chat.length) { const lastMessage = chat[chat.length - 1]; const messageLength = getTokenCount(lastMessage.mes); const shouldAutoContinue = messageLength < power_user.auto_continue.target_length; if (shouldAutoContinue) { console.log(`Triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}. Message chunk: ${messageChunk}`); return true; } else { console.log(`Not triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}`); return false; } } else { console.log('Last generated chunk was empty, not triggering auto-continue'); return false; } } /** * Triggers auto-continue if the message meets the criteria. * @param {string} messageChunk Current message chunk * @param {boolean} isImpersonate Is the user impersonation */ export function triggerAutoContinue(messageChunk, isImpersonate) { if (selected_group) { console.debug('Auto-continue is disabled for group chat'); return; } if (shouldAutoContinue(messageChunk, isImpersonate)) { $('#option_continue').trigger('click'); } } export function getBiasStrings(textareaText, type) { if (type == 'impersonate' || type == 'continue') { return { messageBias: '', promptBias: '', isUserPromptBias: false }; } let promptBias = ''; let messageBias = extractMessageBias(textareaText); // If user input is not provided, retrieve the bias of the most recent relevant message if (!textareaText) { for (let i = chat.length - 1; i >= 0; i--) { const mes = chat[i]; if (type === 'swipe' && chat.length - 1 === i) { continue; } if (mes && (mes.is_user || mes.is_system || mes.extra?.type === system_message_types.NARRATOR)) { if (mes.extra?.bias?.trim()?.length > 0) { promptBias = mes.extra.bias; } break; } } } promptBias = messageBias || promptBias || power_user.user_prompt_bias || ''; const isUserPromptBias = promptBias === power_user.user_prompt_bias; // Substitute params for everything messageBias = substituteParams(messageBias); promptBias = substituteParams(promptBias); return { messageBias, promptBias, isUserPromptBias }; } /** * @param {Object} chatItem Message history item. * @param {boolean} isInstruct Whether instruct mode is enabled. * @param {boolean|number} forceOutputSequence Whether to force the first/last output sequence for instruct mode. */ function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) { const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR; const characterName = chatItem?.name ? chatItem.name : name2; const itemName = chatItem.is_user ? chatItem['name'] : characterName; const shouldPrependName = !isNarratorType; // Don't include a name if it's empty let textResult = chatItem?.name && shouldPrependName ? `${itemName}: ${chatItem.mes}\n` : `${chatItem.mes}\n`; if (isInstruct) { textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType, chatItem.force_avatar, name1, name2, forceOutputSequence); } return textResult; } /** * Removes all {{macros}} from a string. * @param {string} str String to remove macros from. * @returns {string} String with macros removed. */ export function removeMacros(str) { return (str ?? '').replace(/\{\{[\s\S]*?\}\}/gm, '').trim(); } /** * Inserts a user message into the chat history. * @param {string} messageText Message text. * @param {string} messageBias Message bias. * @param {number} [insertAt] Optional index to insert the message at. * @param {boolean} [compact] Send as a compact display message. * @param {string} [name] Name of the user sending the message. Defaults to name1. * @param {string} [avatar] Avatar of the user sending the message. Defaults to user_avatar. * @returns {Promise<void>} A promise that resolves when the message is inserted. */ export async function sendMessageAsUser(messageText, messageBias, insertAt = null, compact = false, name = name1, avatar = user_avatar) { messageText = getRegexedString(messageText, regex_placement.USER_INPUT); const message = { name: name, is_user: true, is_system: false, send_date: getMessageTimeStamp(), mes: substituteParams(messageText), extra: { isSmallSys: compact, }, }; if (power_user.message_token_count_enabled) { message.extra.token_count = await getTokenCountAsync(message.mes, 0); } // Lock user avatar to a persona. if (avatar in power_user.personas) { message.force_avatar = getUserAvatar(avatar); } if (messageBias) { message.extra.bias = messageBias; message.mes = removeMacros(message.mes); } await populateFileAttachment(message); statMesProcess(message, 'user', characters, this_chid, ''); if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) { chat.splice(insertAt, 0, message); await saveChatConditional(); await eventSource.emit(event_types.MESSAGE_SENT, insertAt); await reloadCurrentChat(); await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt); } else { chat.push(message); const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_SENT, chat_id); addOneMessage(message); await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id); } } /** * Gets the maximum usable context size for the current API. * @param {number|null} overrideResponseLength Optional override for the response length. * @returns {number} Maximum usable context size. */ export function getMaxContextSize(overrideResponseLength = null) { if (typeof overrideResponseLength !== 'number' || overrideResponseLength <= 0 || isNaN(overrideResponseLength)) { overrideResponseLength = null; } let this_max_context = 1487; if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') { this_max_context = (max_context - (overrideResponseLength || amount_gen)); } if (main_api == 'novel') { this_max_context = Number(max_context); if (nai_settings.model_novel.includes('clio')) { this_max_context = Math.min(max_context, 8192); } if (nai_settings.model_novel.includes('kayra')) { this_max_context = Math.min(max_context, 8192); const subscriptionLimit = getKayraMaxContextTokens(); if (typeof subscriptionLimit === 'number' && this_max_context > subscriptionLimit) { this_max_context = subscriptionLimit; console.log(`NovelAI subscription limit reached. Max context size is now ${this_max_context}`); } } this_max_context = this_max_context - (overrideResponseLength || amount_gen); } if (main_api == 'openai') { this_max_context = oai_settings.openai_max_context - (overrideResponseLength || oai_settings.openai_max_tokens); } return this_max_context; } function parseTokenCounts(counts, thisPromptBits) { /** * @param {any[]} numbers */ function getSum(...numbers) { return numbers.map(x => Number(x)).filter(x => !Number.isNaN(x)).reduce((acc, val) => acc + val, 0); } const total = getSum(Object.values(counts)); thisPromptBits.push({ oaiStartTokens: (counts?.start + counts?.controlPrompts) || 0, oaiPromptTokens: getSum(counts?.prompt, counts?.charDescription, counts?.charPersonality, counts?.scenario) || 0, oaiBiasTokens: counts?.bias || 0, oaiNudgeTokens: counts?.nudge || 0, oaiJailbreakTokens: counts?.jailbreak || 0, oaiImpersonateTokens: counts?.impersonate || 0, oaiExamplesTokens: (counts?.dialogueExamples + counts?.examples) || 0, oaiConversationTokens: (counts?.conversation + counts?.chatHistory) || 0, oaiNsfwTokens: counts?.nsfw || 0, oaiMainTokens: counts?.main || 0, oaiTotalTokens: total, }); } function addChatsPreamble(mesSendString) { return main_api === 'novel' ? substituteParams(nai_settings.preamble) + '\n' + mesSendString : mesSendString; } function addChatsSeparator(mesSendString) { if (power_user.context.chat_start) { return substituteParams(power_user.context.chat_start + '\n') + mesSendString; } else { return mesSendString; } } async function DupeChar() { if (!this_chid) { toastr.warning('You must first select a character to duplicate!'); return ''; } const confirmMessage = ` <h3>Are you sure you want to duplicate this character?</h3> <span>If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.</span><br><br>`; const confirm = await callPopup(confirmMessage, 'confirm'); if (!confirm) { console.log('User cancelled duplication'); return ''; } const body = { avatar_url: characters[this_chid].avatar }; const response = await fetch('/api/characters/duplicate', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify(body), }); if (response.ok) { toastr.success('Character Duplicated'); const data = await response.json(); await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path }); getCharacters(); } return ''; } export async function itemizedParams(itemizedPrompts, thisPromptSet) { const params = { charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription), charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality), scenarioTextTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].scenarioText), userPersonaStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].userPersona), worldInfoStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].worldInfoString), allAnchorsTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].allAnchors), summarizeStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].summarizeString), authorsNoteStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].authorsNoteString), smartContextStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].smartContextString), beforeScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].beforeScenarioAnchor), afterScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].afterScenarioAnchor), zeroDepthAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused thisPrompt_padding: itemizedPrompts[thisPromptSet].padding, this_main_api: itemizedPrompts[thisPromptSet].main_api, chatInjects: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatInjects), }; if (params.chatInjects) { params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects; } if (params.this_main_api == 'openai') { //for OAI API //console.log('-- Counting OAI Tokens'); //params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens; params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens; params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens; params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens; params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens; params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens; params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens; params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens; params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens; params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens; params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens; params.finalPromptTokens = params.oaiStartTokens + params.oaiPromptTokens + params.oaiMainTokens + params.oaiNsfwTokens + params.oaiBiasTokens + params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.ActualChatHistoryTokens + //charDescriptionTokens + //charPersonalityTokens + //allAnchorsTokens + params.worldInfoStringTokens + params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens; // Max context size - max completion tokens params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens); //console.log('-- applying % on OAI tokens'); params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2); params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2); params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2); params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2); params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2); params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2); params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName; params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens; params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2); } else { //for non-OAI APIs //console.log('-- Counting non-OAI Tokens'); params.finalPromptTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].finalPrompt); params.storyStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens; params.examplesStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].examplesString); params.mesSendStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].mesSendString); params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding; params.instructionTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].instruction); params.promptBiasTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].promptBias); params.totalTokensInPrompt = params.storyStringTokens + //chardefs total params.worldInfoStringTokens + params.examplesStringTokens + // example messages params.ActualChatHistoryTokens + //chat history params.allAnchorsTokens + // AN and/or legacy anchors //afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario' //zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth params.promptBiasTokens; //{{}} //- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt' params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context; params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding; //console.log('-- applying % on non-OAI tokens'); params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2); params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2); params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2); params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2); params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2); params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName; } return params; } export function findItemizedPromptSet(itemizedPrompts, incomingMesId) { var thisPromptSet = undefined; for (var i = 0; i < itemizedPrompts.length; i++) { console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`); if (itemizedPrompts[i].mesId === incomingMesId) { console.log(`found matching mesID ${i}`); thisPromptSet = i; PromptArrayItemForRawPromptDisplay = i; console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`); console.log(itemizedPrompts[thisPromptSet]); } } return thisPromptSet; } async function promptItemize(itemizedPrompts, requestedMesId) { console.log('PROMPT ITEMIZE ENTERED'); var incomingMesId = Number(requestedMesId); console.debug(`looking for MesId ${incomingMesId}`); var thisPromptSet = findItemizedPromptSet(itemizedPrompts, incomingMesId); if (thisPromptSet === undefined) { console.log(`couldnt find the right mesId. looked for ${incomingMesId}`); console.log(itemizedPrompts); return null; } const params = await itemizedParams(itemizedPrompts, thisPromptSet); if (params.this_main_api == 'openai') { const template = await renderTemplateAsync('itemizationChat', params); callPopup(template, 'text'); } else { const template = await renderTemplateAsync('itemizationText', params); callPopup(template, 'text'); } } function setInContextMessages(lastmsg, type) { $('#chat .mes').removeClass('lastInContext'); if (type === 'swipe' || type === 'regenerate' || type === 'continue') { lastmsg++; } const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsg); lastMessageBlock.addClass('lastInContext'); if (lastMessageBlock.length === 0) { const firstMessageId = getFirstDisplayedMessageId(); $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); } } /** * Sends a non-streaming request to the API. * @param {string} type Generation type * @param {object} data Generation data * @returns {Promise<object>} Response data from the API */ async function sendGenerationRequest(type, data) { if (main_api === 'openai') { return await sendOpenAIRequest(type, data.prompt, abortController.signal); } if (main_api === 'koboldhorde') { return await generateHorde(data.prompt, data, abortController.signal, true); } const response = await fetch(getGenerateUrl(main_api), { method: 'POST', headers: getRequestHeaders(), cache: 'no-cache', body: JSON.stringify(data), signal: abortController.signal, }); if (!response.ok) { const error = await response.json(); throw error; } const responseData = await response.json(); return responseData; } /** * Sends a streaming request to the API. * @param {string} type Generation type * @param {object} data Generation data * @returns {Promise<any>} Streaming generator */ async function sendStreamingRequest(type, data) { switch (main_api) { case 'openai': return await sendOpenAIRequest(type, data.prompt, streamingProcessor.abortController.signal); case 'textgenerationwebui': return await generateTextGenWithStreaming(data, streamingProcessor.abortController.signal); case 'novel': return await generateNovelWithStreaming(data, streamingProcessor.abortController.signal); case 'kobold': return await generateKoboldWithStreaming(data, streamingProcessor.abortController.signal); default: throw new Error('Streaming is enabled, but the current API does not support streaming.'); } } /** * Gets the generation endpoint URL for the specified API. * @param {string} api API name * @returns {string} Generation URL */ function getGenerateUrl(api) { switch (api) { case 'kobold': return '/api/backends/kobold/generate'; case 'koboldhorde': return '/api/backends/koboldhorde/generate'; case 'textgenerationwebui': return '/api/backends/text-completions/generate'; case 'novel': return '/api/novelai/generate'; default: throw new Error(`Unknown API: ${api}`); } } function throwCircuitBreakerError() { callPopup(`Could not extract reply in ${MAX_GENERATION_LOOPS} attempts. Try generating again`, 'text'); unblockGeneration(); } function extractTitleFromData(data) { if (main_api == 'koboldhorde') { return data.workerName; } return undefined; } /** * parseAndSaveLogprobs receives the full data response for a non-streaming * generation, parses logprobs for all tokens in the message, and saves them * to the currently active message. * @param {object} data - response data containing all tokens/logprobs * @param {string} continueFrom - for 'continue' generations, the prompt * */ function parseAndSaveLogprobs(data, continueFrom) { /** @type {import('./scripts/logprobs.js').TokenLogprobs[] | null} */ let logprobs = null; switch (main_api) { case 'novel': // parser only handles one token/logprob pair at a time logprobs = data.logprobs?.map(parseNovelAILogprobs) || null; break; case 'openai': // OAI and other chat completion APIs must handle this earlier in // `sendOpenAIRequest`. `data` for these APIs is just a string with // the text of the generated message, logprobs are not included. return; case 'textgenerationwebui': switch (textgen_settings.type) { case textgen_types.LLAMACPP: { logprobs = data?.completion_probabilities?.map(x => parseTextgenLogprobs(x.content, [x])) || null; } break; case textgen_types.VLLM: case textgen_types.APHRODITE: case textgen_types.MANCER: case textgen_types.TABBY: { logprobs = parseTabbyLogprobs(data) || null; } break; } break; default: return; } saveLogprobsForActiveMessage(logprobs, continueFrom); } /** * Extracts the message from the response data. * @param {object} data Response data * @returns {string} Extracted message */ function extractMessageFromData(data) { if (typeof data === 'string') { return data; } switch (main_api) { case 'kobold': return data.results[0].text; case 'koboldhorde': return data.text; case 'textgenerationwebui': return data.choices?.[0]?.text ?? data.content ?? data.response ?? ''; case 'novel': return data.output; case 'openai': return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? ''; default: return ''; } } /** * Extracts multiswipe swipes from the response data. * @param {Object} data Response data * @param {string} type Type of generation * @returns {string[]} Array of extra swipes */ function extractMultiSwipes(data, type) { const swipes = []; if (!data) { return swipes; } if (type === 'continue' || type === 'impersonate' || type === 'quiet') { return swipes; } if (main_api === 'openai' || (main_api === 'textgenerationwebui' && [MANCER, VLLM, APHRODITE, TABBY].includes(textgen_settings.type))) { if (!Array.isArray(data.choices)) { return swipes; } const multiSwipeCount = data.choices.length - 1; if (multiSwipeCount <= 0) { return swipes; } for (let i = 1; i < data.choices.length; i++) { const text = data?.choices[i]?.message?.content ?? data?.choices[i]?.text ?? ''; const cleanedText = cleanUpMessage(text, false, false, false); swipes.push(cleanedText); } } return swipes; } export function cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncompleteSentences = false, stoppingStrings = null) { if (!getMessage) { return ''; } // Add the prompt bias before anything else if ( power_user.user_prompt_bias && !isImpersonate && !isContinue && power_user.user_prompt_bias.length !== 0 ) { getMessage = substituteParams(power_user.user_prompt_bias) + getMessage; } // Allow for caching of stopping strings. getStoppingStrings is an expensive function, especially with macros // enabled, so for streaming, we call it once and then pass it into each cleanUpMessage call. if (!stoppingStrings) { stoppingStrings = getStoppingStrings(isImpersonate, isContinue); } for (const stoppingString of stoppingStrings) { if (stoppingString.length) { for (let j = stoppingString.length; j > 0; j--) { if (getMessage.slice(-j) === stoppingString.slice(0, j)) { getMessage = getMessage.slice(0, -j); break; } } } } // Regex uses vars, so add before formatting getMessage = getRegexedString(getMessage, isImpersonate ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT); if (!displayIncompleteSentences && power_user.trim_sentences) { getMessage = trimToEndSentence(getMessage, power_user.include_newline); } if (power_user.collapse_newlines) { getMessage = collapseNewlines(getMessage); } if (power_user.trim_spaces) { getMessage = getMessage.trim(); } // trailing invisible whitespace before every newlines, on a multiline string // "trailing whitespace on newlines \nevery line of the string \n?sample text" -> // "trailing whitespace on newlines\nevery line of the string\nsample text" getMessage = getMessage.replace(/[^\S\r\n]+$/gm, ''); let nameToTrim = isImpersonate ? name2 : name1; if (isImpersonate) { nameToTrim = power_user.allow_name2_display ? '' : name2; } else { nameToTrim = power_user.allow_name1_display ? '' : name1; } if (nameToTrim && getMessage.indexOf(`${nameToTrim}:`) == 0) { getMessage = getMessage.substring(0, getMessage.indexOf(`${nameToTrim}:`)); } if (nameToTrim && getMessage.indexOf(`\n${nameToTrim}:`) >= 0) { getMessage = getMessage.substring(0, getMessage.indexOf(`\n${nameToTrim}:`)); } if (getMessage.indexOf('<|endoftext|>') != -1) { getMessage = getMessage.substring(0, getMessage.indexOf('<|endoftext|>')); } const isInstruct = power_user.instruct.enabled && main_api !== 'openai'; if (isInstruct && power_user.instruct.stop_sequence) { if (getMessage.indexOf(power_user.instruct.stop_sequence) != -1) { getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.stop_sequence)); } } // Hana: Only use the first sequence (should be <|model|>) // of the prompt before <|user|> (as KoboldAI Lite does it). if (isInstruct && power_user.instruct.input_sequence) { if (getMessage.indexOf(power_user.instruct.input_sequence) != -1) { getMessage = getMessage.substring(0, getMessage.indexOf(power_user.instruct.input_sequence)); } } if (isInstruct && power_user.instruct.input_sequence && isImpersonate) { //getMessage = getMessage.replaceAll(power_user.instruct.input_sequence, ''); power_user.instruct.input_sequence.split('\n') .filter(line => line.trim() !== '') .forEach(line => { getMessage = getMessage.replaceAll(line, ''); }); } if (isInstruct && power_user.instruct.output_sequence && !isImpersonate) { //getMessage = getMessage.replaceAll(power_user.instruct.output_sequence, ''); power_user.instruct.output_sequence.split('\n') .filter(line => line.trim() !== '') .forEach(line => { getMessage = getMessage.replaceAll(line, ''); }); } if (isInstruct && power_user.instruct.last_output_sequence && !isImpersonate) { //getMessage = getMessage.replaceAll(power_user.instruct.last_output_sequence, ''); power_user.instruct.last_output_sequence.split('\n') .filter(line => line.trim() !== '') .forEach(line => { getMessage = getMessage.replaceAll(line, ''); }); } // clean-up group message from excessive generations if (selected_group) { getMessage = cleanGroupMessage(getMessage); } if (!power_user.allow_name2_display) { const name2Escaped = escapeRegex(name2); getMessage = getMessage.replace(new RegExp(`(^|\n)${name2Escaped}:\\s*`, 'g'), '$1'); } if (isImpersonate) { getMessage = getMessage.trim(); } if (power_user.auto_fix_generated_markdown) { getMessage = fixMarkdown(getMessage, false); } const nameToTrim2 = isImpersonate ? name1 : name2; if (getMessage.startsWith(nameToTrim2 + ':')) { getMessage = getMessage.replace(nameToTrim2 + ':', ''); getMessage = getMessage.trimStart(); } if (isImpersonate) { getMessage = getMessage.trim(); } return getMessage; } async function saveReply(type, getMessage, fromStreaming, title, swipes) { if (type != 'append' && type != 'continue' && type != 'appendFinal' && chat.length && (chat[chat.length - 1]['swipe_id'] === undefined || chat[chat.length - 1]['is_user'])) { type = 'normal'; } if (chat.length && (!chat[chat.length - 1]['extra'] || typeof chat[chat.length - 1]['extra'] !== 'object')) { chat[chat.length - 1]['extra'] = {}; } let oldMessage = ''; const generationFinished = new Date(); const img = extractImageFromMessage(getMessage); getMessage = img.getMessage; if (type === 'swipe') { oldMessage = chat[chat.length - 1]['mes']; chat[chat.length - 1]['swipes'].length++; if (chat[chat.length - 1]['swipe_id'] === chat[chat.length - 1]['swipes'].length - 1) { chat[chat.length - 1]['title'] = title; chat[chat.length - 1]['mes'] = getMessage; chat[chat.length - 1]['gen_started'] = generation_started; chat[chat.length - 1]['gen_finished'] = generationFinished; chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); addOneMessage(chat[chat_id], { type: 'swipe' }); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id); } else { chat[chat.length - 1]['mes'] = getMessage; } } else if (type === 'append' || type === 'continue') { console.debug('Trying to append.'); oldMessage = chat[chat.length - 1]['mes']; chat[chat.length - 1]['title'] = title; chat[chat.length - 1]['mes'] += getMessage; chat[chat.length - 1]['gen_started'] = generation_started; chat[chat.length - 1]['gen_finished'] = generationFinished; chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); addOneMessage(chat[chat_id], { type: 'swipe' }); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id); } else if (type === 'appendFinal') { oldMessage = chat[chat.length - 1]['mes']; console.debug('Trying to appendFinal.'); chat[chat.length - 1]['title'] = title; chat[chat.length - 1]['mes'] = getMessage; chat[chat.length - 1]['gen_started'] = generation_started; chat[chat.length - 1]['gen_finished'] = generationFinished; chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); addOneMessage(chat[chat_id], { type: 'swipe' }); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id); } else { console.debug('entering chat update routine for non-swipe post'); chat[chat.length] = {}; chat[chat.length - 1]['extra'] = {}; chat[chat.length - 1]['name'] = name2; chat[chat.length - 1]['is_user'] = false; chat[chat.length - 1]['send_date'] = getMessageTimeStamp(); chat[chat.length - 1]['extra']['api'] = getGeneratingApi(); chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); if (power_user.trim_spaces) { getMessage = getMessage.trim(); } chat[chat.length - 1]['mes'] = getMessage; chat[chat.length - 1]['title'] = title; chat[chat.length - 1]['gen_started'] = generation_started; chat[chat.length - 1]['gen_finished'] = generationFinished; if (power_user.message_token_count_enabled) { chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(chat[chat.length - 1]['mes'], 0); } if (selected_group) { console.debug('entering chat update for groups'); let avatarImg = 'img/ai4.png'; if (characters[this_chid].avatar != 'none') { avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar); } chat[chat.length - 1]['force_avatar'] = avatarImg; chat[chat.length - 1]['original_avatar'] = characters[this_chid].avatar; chat[chat.length - 1]['extra']['gen_id'] = group_generation_id; } saveImageToMessage(img, chat[chat.length - 1]); const chat_id = (chat.length - 1); !fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); addOneMessage(chat[chat_id]); !fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id); } const item = chat[chat.length - 1]; if (item['swipe_info'] === undefined) { item['swipe_info'] = []; } if (item['swipe_id'] !== undefined) { const swipeId = item['swipe_id']; item['swipes'][swipeId] = item['mes']; item['swipe_info'][swipeId] = { send_date: item['send_date'], gen_started: item['gen_started'], gen_finished: item['gen_finished'], extra: JSON.parse(JSON.stringify(item['extra'])), }; } else { item['swipe_id'] = 0; item['swipes'] = []; item['swipes'][0] = chat[chat.length - 1]['mes']; item['swipe_info'][0] = { send_date: chat[chat.length - 1]['send_date'], gen_started: chat[chat.length - 1]['gen_started'], gen_finished: chat[chat.length - 1]['gen_finished'], extra: JSON.parse(JSON.stringify(chat[chat.length - 1]['extra'])), }; } if (Array.isArray(swipes) && swipes.length > 0) { const swipeInfo = { send_date: item.send_date, gen_started: item.gen_started, gen_finished: item.gen_finished, extra: structuredClone(item.extra), }; const swipeInfoArray = []; swipeInfoArray.length = swipes.length; swipeInfoArray.fill(swipeInfo, 0, swipes.length); item.swipes.push(...swipes); item.swipe_info.push(...swipeInfoArray); } statMesProcess(chat[chat.length - 1], type, characters, this_chid, oldMessage); return { type, getMessage }; } function saveImageToMessage(img, mes) { if (mes && img.image) { if (!mes.extra || typeof mes.extra !== 'object') { mes.extra = {}; } mes.extra.image = img.image; mes.extra.title = img.title; } } export function getGeneratingApi() { switch (main_api) { case 'openai': return oai_settings.chat_completion_source || 'openai'; case 'textgenerationwebui': return textgen_settings.type === textgen_types.OOBA ? 'textgenerationwebui' : textgen_settings.type; default: return main_api; } } function getGeneratingModel(mes) { let model = ''; switch (main_api) { case 'kobold': model = online_status; break; case 'novel': model = nai_settings.model_novel; break; case 'openai': model = getChatCompletionModel(); break; case 'textgenerationwebui': model = online_status; break; case 'koboldhorde': model = kobold_horde_model; break; } return model; } function extractImageFromMessage(getMessage) { const regex = /<img src="(.*?)".*?alt="(.*?)".*?>/g; const results = regex.exec(getMessage); const image = results ? results[1] : ''; const title = results ? results[2] : ''; getMessage = getMessage.replace(regex, ''); return { getMessage, image, title }; } export function activateSendButtons() { is_send_press = false; $('#send_but').removeClass('displayNone'); $('#mes_continue').removeClass('displayNone'); $('.mes_buttons:last').show(); hideStopButton(); } export function deactivateSendButtons() { $('#send_but').addClass('displayNone'); $('#mes_continue').addClass('displayNone'); showStopButton(); } 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. name2 = systemUserName; // sets up system user to tell user about having deleted a character chat = [...safetychat]; // resets chat metadata chat_metadata = {}; // resets the characters array, forcing getcharacters to reset characters.length = 0; } /** * * @param {'characters' | 'character_edit' | 'create' | 'group_edit' | 'group_create'} value */ export function setMenuType(value) { menu_type = value; // Allow custom CSS to see which menu type is active document.getElementById('right-nav-panel').dataset.menuType = menu_type; } export function setExternalAbortController(controller) { abortController = controller; } export function setCharacterId(value) { this_chid = value; } export function setCharacterName(value) { name2 = value; } export function setOnlineStatus(value) { online_status = value; displayOnlineStatus(); } export function setEditedMessageId(value) { this_edit_mes_id = value; } export function setSendButtonState(value) { is_send_press = value; } export async function renameCharacter(name = null, { silent = false, renameChats = null } = {}) { if (!name && silent) { toastr.warning('No character name provided.', 'Rename Character'); return false; } if (this_chid === undefined) { toastr.warning('No character selected.', 'Rename Character'); return false; } const oldAvatar = characters[this_chid].avatar; const newValue = name || await callGenericPopup('<h3>New name:</h3>', POPUP_TYPE.INPUT, characters[this_chid].name); if (!newValue) { toastr.warning('No character name provided.', 'Rename Character'); return false; } if (newValue === characters[this_chid].name) { toastr.info('Same character name provided, so name did not change.', 'Rename Character'); return false; } const body = JSON.stringify({ avatar_url: oldAvatar, new_name: newValue }); const response = await fetch('/api/characters/rename', { method: 'POST', headers: getRequestHeaders(), body, }); try { if (response.ok) { const data = await response.json(); const newAvatar = data.avatar; // Replace tags list renameTagKey(oldAvatar, newAvatar); // Reload characters list await getCharacters(); // Find newly renamed character const newChId = characters.findIndex(c => c.avatar == data.avatar); if (newChId !== -1) { // Select the character after the renaming this_chid = -1; await selectCharacterById(String(newChId)); // Async delay to update UI await delay(1); if (this_chid === -1) { throw new Error('New character not selected'); } // Also rename as a group member await renameGroupMember(oldAvatar, newAvatar, newValue); const renamePastChatsConfirm = renameChats !== null ? renameChats : silent ? false : await callPopup(`<h3>Character renamed!</h3> <p>Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?</p> <i><b>Sprites folder (if any) should be renamed manually.</b></i>`, 'confirm'); if (renamePastChatsConfirm) { await renamePastChats(newAvatar, newValue); await reloadCurrentChat(); toastr.success('Character renamed and past chats updated!', 'Rename Character'); } else { toastr.success('Character renamed!', 'Rename Character'); } } else { throw new Error('Newly renamed character was lost?'); } } else { throw new Error('Could not rename the character'); } } catch (error) { // Reloading to prevent data corruption if (!silent) await callPopup('Something went wrong. The page will be reloaded.', 'text'); else toastr.error('Something went wrong. The page will be reloaded.', 'Rename Character'); console.log('Renaming character error:', error); location.reload(); return false; } return true; } async function renamePastChats(newAvatar, newValue) { const pastChats = await getPastCharacterChats(); for (const { file_name } of pastChats) { try { const fileNameWithoutExtension = file_name.replace('.jsonl', ''); const getChatResponse = await fetch('/api/chats/get', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ ch_name: newValue, file_name: fileNameWithoutExtension, avatar_url: newAvatar, }), cache: 'no-cache', }); if (getChatResponse.ok) { const currentChat = await getChatResponse.json(); for (const message of currentChat) { if (message.is_user || message.is_system || message.extra?.type == system_message_types.NARRATOR) { continue; } if (message.name !== undefined) { message.name = newValue; } } const saveChatResponse = await fetch('/api/chats/save', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ ch_name: newValue, file_name: fileNameWithoutExtension, chat: currentChat, avatar_url: newAvatar, }), cache: 'no-cache', }); if (!saveChatResponse.ok) { throw new Error('Could not save chat'); } } } catch (error) { toastr.error(`Past chat could not be updated: ${file_name}`); console.error(error); } } } export function saveChatDebounced() { const chid = this_chid; const selectedGroup = selected_group; if (chatSaveTimeout) { console.debug('Clearing chat save timeout'); clearTimeout(chatSaveTimeout); } chatSaveTimeout = setTimeout(async () => { if (selectedGroup !== selected_group) { console.warn('Chat save timeout triggered, but group changed. Aborting.'); return; } if (chid !== this_chid) { console.warn('Chat save timeout triggered, but chid changed. Aborting.'); return; } console.debug('Chat save timeout triggered'); await saveChatConditional(); console.debug('Chat saved'); }, 1000); } export async function saveChat(chat_name, withMetadata, mesId) { const metadata = { ...chat_metadata, ...(withMetadata || {}) }; let file_name = chat_name ?? characters[this_chid]?.chat; if (!file_name) { console.warn('saveChat called without chat_name and no chat file found'); return; } characters[this_chid]['date_last_chat'] = Date.now(); chat.forEach(function (item, i) { if (item['is_group']) { toastr.error('Trying to save group chat with regular saveChat function. Aborting to prevent corruption.'); throw new Error('Group chat saved from saveChat'); } /* if (item.is_user) { //var str = item.mes.replace(`${name1}:`, `${name1}:`); //chat[i].mes = str; //chat[i].name = name1; } else if (i !== chat.length - 1 && chat[i].swipe_id !== undefined) { // delete chat[i].swipes; // delete chat[i].swipe_id; } */ }); const trimmed_chat = (mesId !== undefined && mesId >= 0 && mesId < chat.length) ? chat.slice(0, parseInt(mesId) + 1) : chat; var save_chat = [ { user_name: name1, character_name: name2, create_date: chat_create_date, chat_metadata: metadata, }, ...trimmed_chat, ]; return jQuery.ajax({ type: 'POST', url: '/api/chats/save', data: JSON.stringify({ ch_name: characters[this_chid].name, file_name: file_name, chat: save_chat, avatar_url: characters[this_chid].avatar, }), beforeSend: function () { }, cache: false, dataType: 'json', contentType: 'application/json', success: function (data) { }, error: function (jqXHR, exception) { console.log(exception); console.log(jqXHR); }, }); } async function read_avatar_load(input) { if (input.files && input.files[0]) { if (selected_button == 'create') { create_save.avatar = input.files; } const file = input.files[0]; const fileData = await getBase64Async(file); if (!power_user.never_resize_avatars) { $('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup'); const croppedImage = await callPopup(getCropPopup(fileData), 'avatarToCrop'); if (!croppedImage) { return; } $('#avatar_load_preview').attr('src', croppedImage); } else { $('#avatar_load_preview').attr('src', fileData); } if (menu_type == 'create') { return; } await createOrEditCharacter(); await delay(DEFAULT_SAVE_EDIT_TIMEOUT); const formData = new FormData($('#form_create').get(0)); await fetch(getThumbnailUrl('avatar', formData.get('avatar_url')), { method: 'GET', cache: 'no-cache', headers: { 'pragma': 'no-cache', 'cache-control': 'no-cache', }, }); $('.mes').each(async function () { const nameMatch = $(this).attr('ch_name') == formData.get('ch_name'); if ($(this).attr('is_system') == 'true' && !nameMatch) { return; } if ($(this).attr('is_user') == 'true') { return; } if (nameMatch) { const previewSrc = $('#avatar_load_preview').attr('src'); const avatar = $(this).find('.avatar img'); avatar.attr('src', default_avatar); await delay(1); avatar.attr('src', previewSrc); } }); console.log('Avatar refreshed'); } } export function getCropPopup(src) { return `<h3>Set the crop position of the avatar image and click Accept to confirm.</h3> <div id='avatarCropWrap'> <img id='avatarToCrop' src='${src}'> </div>`; } export function getThumbnailUrl(type, file) { return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`; } export function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, interactable = false, highlightFavs = true } = {}) { if (empty) { block.empty(); } for (const entity of entities) { const id = entity.id; // Populate the template const avatarTemplate = $(`#${templateId} .avatar`).clone(); let this_avatar = default_avatar; if (entity.item.avatar !== undefined && entity.item.avatar != 'none') { this_avatar = getThumbnailUrl('avatar', entity.item.avatar); } avatarTemplate.attr('data-type', entity.type); avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); if (highlightFavs) { avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true'); avatarTemplate.find('.ch_fav').val(entity.item.fav); } // If this is a group, we need to hack slightly. We still want to keep most of the css classes and layout, but use a group avatar instead. if (entity.type === 'group') { const grpTemplate = getGroupAvatar(entity.item); avatarTemplate.addClass(grpTemplate.attr('class')); avatarTemplate.empty(); avatarTemplate.append(grpTemplate.children()); avatarTemplate.attr('title', `[Group] ${entity.item.name}`); } if (interactable) { avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); avatarTemplate.toggleClass('character_select', entity.type === 'character'); avatarTemplate.toggleClass('group_select', entity.type === 'group'); } block.append(avatarTemplate); } } export async function getChat() { //console.log('/api/chats/get -- entered for -- ' + characters[this_chid].name); try { const response = await $.ajax({ type: 'POST', url: '/api/chats/get', data: JSON.stringify({ ch_name: characters[this_chid].name, file_name: characters[this_chid].chat, avatar_url: characters[this_chid].avatar, }), dataType: 'json', contentType: 'application/json', }); if (response[0] !== undefined) { chat.splice(0, chat.length, ...response); chat_create_date = chat[0]['create_date']; chat_metadata = chat[0]['chat_metadata'] ?? {}; chat.shift(); } else { chat_create_date = humanizedDateTime(); } await getChatResult(); eventSource.emit('chatLoaded', { detail: { id: this_chid, character: characters[this_chid] } }); // Focus on the textarea if not already focused on a visible text input setTimeout(function () { if ($(document.activeElement).is('input:visible, textarea:visible')) { return; } $('#send_textarea').trigger('click').trigger('focus'); }, 200); } catch (error) { await getChatResult(); console.log(error); } } async function getChatResult() { name2 = characters[this_chid].name; let freshChat = false; if (chat.length === 0) { const message = getFirstMessage(); if (message.mes) { chat.push(message); await saveChatConditional(); freshChat = true; } } await loadItemizedPrompts(getCurrentChatId()); await printMessages(); select_selected_character(this_chid); await eventSource.emit(event_types.CHAT_CHANGED, (getCurrentChatId())); if (freshChat) await eventSource.emit(event_types.CHAT_CREATED); if (chat.length === 1) { const chat_id = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id); } } function getFirstMessage() { const firstMes = characters[this_chid].first_mes || ''; const alternateGreetings = characters[this_chid]?.data?.alternate_greetings; const message = { name: name2, is_user: false, is_system: false, send_date: getMessageTimeStamp(), mes: getRegexedString(firstMes, regex_placement.AI_OUTPUT), extra: {}, }; if (Array.isArray(alternateGreetings) && alternateGreetings.length > 0) { const swipes = [message.mes, ...(alternateGreetings.map(greeting => getRegexedString(greeting, regex_placement.AI_OUTPUT)))]; if (!message.mes) { swipes.shift(); message.mes = swipes[0]; } message['swipe_id'] = 0; message['swipes'] = swipes; message['swipe_info'] = []; } return message; } export async function openCharacterChat(file_name) { await clearChat(); characters[this_chid]['chat'] = file_name; chat.length = 0; chat_metadata = {}; await getChat(); $('#selected_chat_pole').val(file_name); await createOrEditCharacter(); } ////////// OPTIMZED MAIN API CHANGE FUNCTION //////////// export function changeMainAPI() { const selectedVal = $('#main_api').val(); //console.log(selectedVal); const apiElements = { 'koboldhorde': { apiSettings: $('#kobold_api-settings'), apiConnector: $('#kobold_horde'), apiPresets: $('#kobold_api-presets'), apiRanges: $('#range_block'), maxContextElem: $('#max_context_block'), amountGenElem: $('#amount_gen_block'), }, 'kobold': { apiSettings: $('#kobold_api-settings'), apiConnector: $('#kobold_api'), apiPresets: $('#kobold_api-presets'), apiRanges: $('#range_block'), maxContextElem: $('#max_context_block'), amountGenElem: $('#amount_gen_block'), }, 'textgenerationwebui': { apiSettings: $('#textgenerationwebui_api-settings'), apiConnector: $('#textgenerationwebui_api'), apiPresets: $('#textgenerationwebui_api-presets'), apiRanges: $('#range_block_textgenerationwebui'), maxContextElem: $('#max_context_block'), amountGenElem: $('#amount_gen_block'), }, 'novel': { apiSettings: $('#novel_api-settings'), apiConnector: $('#novel_api'), apiPresets: $('#novel_api-presets'), apiRanges: $('#range_block_novel'), maxContextElem: $('#max_context_block'), amountGenElem: $('#amount_gen_block'), }, 'openai': { apiSettings: $('#openai_settings'), apiConnector: $('#openai_api'), apiPresets: $('#openai_api-presets'), apiRanges: $('#range_block_openai'), maxContextElem: $('#max_context_block'), amountGenElem: $('#amount_gen_block'), }, }; //console.log('--- apiElements--- '); //console.log(apiElements); //first, disable everything so the old elements stop showing for (const apiName in apiElements) { const apiObj = apiElements[apiName]; //do not hide items to then proceed to immediately show them. if (selectedVal === apiName) { continue; } apiObj.apiSettings.css('display', 'none'); apiObj.apiConnector.css('display', 'none'); apiObj.apiRanges.css('display', 'none'); apiObj.apiPresets.css('display', 'none'); } //then, find and enable the active item. //This is split out of the loop so that different apis can share settings divs let activeItem = apiElements[selectedVal]; activeItem.apiSettings.css('display', 'block'); activeItem.apiConnector.css('display', 'block'); activeItem.apiRanges.css('display', 'block'); activeItem.apiPresets.css('display', 'block'); if (selectedVal === 'openai') { activeItem.apiPresets.css('display', 'flex'); } if (selectedVal === 'textgenerationwebui' || selectedVal === 'novel') { console.log('enabling amount_gen for ooba/novel'); activeItem.amountGenElem.find('input').prop('disabled', false); activeItem.amountGenElem.css('opacity', 1.0); } //custom because streaming has been moved up under response tokens, which exists inside common settings block if (selectedVal === 'textgenerationwebui') { $('#streaming_textgenerationwebui_block').css('display', 'block'); } else { $('#streaming_textgenerationwebui_block').css('display', 'none'); } if (selectedVal === 'kobold') { $('#streaming_kobold_block').css('display', 'block'); } else { $('#streaming_kobold_block').css('display', 'none'); } if (selectedVal === 'novel') { $('#ai_module_block_novel').css('display', 'block'); } else { $('#ai_module_block_novel').css('display', 'none'); } // Hide common settings for OpenAI console.debug('value?', selectedVal); if (selectedVal == 'openai') { console.debug('hiding settings?'); $('#common-gen-settings-block').css('display', 'none'); } else { $('#common-gen-settings-block').css('display', 'block'); } main_api = selectedVal; online_status = 'no_connection'; if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI) { $('#api_button_openai').trigger('click'); } if (main_api == 'koboldhorde') { getStatusHorde(); getHordeModels(true); } validateDisabledSamplers(); setupChatCompletionPromptManager(oai_settings); forceCharacterEditorTokenize(); } export function setUserName(value) { name1 = value; if (name1 === undefined || name1 == '') name1 = default_user_name; console.log(`User name changed to ${name1}`); $('#your_name').val(name1); if (power_user.persona_show_notifications) { toastr.success(`Your messages will now be sent as ${name1}`, 'Current persona updated'); } saveSettingsDebounced(); } async function doOnboarding(avatarId) { let simpleUiMode = false; const template = $('#onboarding_template .onboarding'); template.find('input[name="enable_simple_mode"]').on('input', function () { simpleUiMode = $(this).is(':checked'); }); let userName = await callGenericPopup(template, POPUP_TYPE.INPUT, currentUser?.name || name1, { rows: 2 }); if (userName) { userName = userName.replace('\n', ' '); setUserName(userName); console.log(`Binding persona ${avatarId} to name ${userName}`); power_user.personas[avatarId] = userName; power_user.persona_descriptions[avatarId] = { description: '', position: persona_description_positions.IN_PROMPT, }; } if (simpleUiMode) { power_user.ui_mode = ui_mode.SIMPLE; $('#ui_mode_select').val(power_user.ui_mode); switchSimpleMode(); } } function reloadLoop() { const MAX_RELOADS = 5; let reloads = Number(sessionStorage.getItem('reloads') || 0); if (reloads < MAX_RELOADS) { reloads++; sessionStorage.setItem('reloads', String(reloads)); window.location.reload(); } } //***************SETTINGS****************// /////////////////////////////////////////// export async function getSettings() { const response = await fetch('/api/settings/get', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({}), cache: 'no-cache', }); if (!response.ok) { reloadLoop(); toastr.error('Settings could not be loaded after multiple attempts. Please try again later.'); throw new Error('Error getting settings'); } const data = await response.json(); if (data.result != 'file not find' && data.settings) { settings = JSON.parse(data.settings); if (settings.username !== undefined && settings.username !== '') { name1 = settings.username; $('#your_name').val(name1); } await setUserControls(data.enable_accounts); // Allow subscribers to mutate settings eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings); //Load KoboldAI settings koboldai_setting_names = data.koboldai_setting_names; koboldai_settings = data.koboldai_settings; koboldai_settings.forEach(function (item, i, arr) { koboldai_settings[i] = JSON.parse(item); }); let arr_holder = {}; $('#settings_preset').empty(); $('#settings_preset').append( '<option value="gui">GUI KoboldAI Settings</option>', ); //adding in the GUI settings, since it is not loaded dynamically koboldai_setting_names.forEach(function (item, i, arr) { arr_holder[item] = i; $('#settings_preset').append(`<option value=${i}>${item}</option>`); //console.log('loading preset #'+i+' -- '+item); }); koboldai_setting_names = {}; koboldai_setting_names = arr_holder; preset_settings = settings.preset_settings; if (preset_settings == 'gui') { selectKoboldGuiPreset(); } else { if (typeof koboldai_setting_names[preset_settings] !== 'undefined') { $(`#settings_preset option[value=${koboldai_setting_names[preset_settings]}]`) .attr('selected', 'true'); } else { preset_settings = 'gui'; selectKoboldGuiPreset(); } } novelai_setting_names = data.novelai_setting_names; novelai_settings = data.novelai_settings; novelai_settings.forEach(function (item, i, arr) { novelai_settings[i] = JSON.parse(item); }); arr_holder = {}; $('#settings_preset_novel').empty(); novelai_setting_names.forEach(function (item, i, arr) { arr_holder[item] = i; $('#settings_preset_novel').append(`<option value=${i}>${item}</option>`); }); novelai_setting_names = {}; novelai_setting_names = arr_holder; //Load AI model config settings amount_gen = settings.amount_gen; if (settings.max_context !== undefined) max_context = parseInt(settings.max_context); swipes = settings.swipes !== undefined ? !!settings.swipes : true; // enable swipes by default $('#swipes-checkbox').prop('checked', swipes); /// swipecode hideSwipeButtons(); showSwipeButtons(); // Kobold loadKoboldSettings(settings.kai_settings ?? settings); // Novel loadNovelSettings(settings.nai_settings ?? settings); $(`#settings_preset_novel option[value=${novelai_setting_names[nai_settings.preset_settings_novel]}]`).attr('selected', 'true'); // TextGen loadTextGenSettings(data, settings); // OpenAI loadOpenAISettings(data, settings.oai_settings ?? settings); // Horde loadHordeSettings(settings); // Load power user settings loadPowerUserSettings(settings, data); // Load character tags loadTagsSettings(settings); // Load background loadBackgroundSettings(settings); // Load proxy presets loadProxyPresets(settings); // Allow subscribers to mutate settings eventSource.emit(event_types.SETTINGS_LOADED_AFTER, settings); // Set context size after loading power user (may override the max value) $('#max_context').val(max_context); $('#max_context_counter').val(max_context); $('#amount_gen').val(amount_gen); $('#amount_gen_counter').val(amount_gen); //Load which API we are using if (settings.main_api == undefined) { settings.main_api = 'kobold'; } if (settings.main_api == 'poe') { settings.main_api = 'openai'; } main_api = settings.main_api; $('#main_api').val(main_api); $('#main_api option[value=' + main_api + ']').attr( 'selected', 'true', ); changeMainAPI(); //Load User's Name and Avatar initUserAvatar(settings.user_avatar); setPersonaDescription(); //Load the active character and group active_character = settings.active_character; active_group = settings.active_group; //Load the API server URL from settings api_server = settings.api_server; $('#api_url_text').val(api_server); setWorldInfoSettings(settings.world_info_settings ?? settings, data); selected_button = settings.selected_button; if (data.enable_extensions) { const isVersionChanged = settings.currentVersion !== currentVersion; await loadExtensionSettings(settings, isVersionChanged); eventSource.emit(event_types.EXTENSION_SETTINGS_LOADED); } firstRun = !!settings.firstRun; if (firstRun) { hideLoader(); await doOnboarding(user_avatar); firstRun = false; } } await validateDisabledSamplers(); settingsReady = true; eventSource.emit(event_types.SETTINGS_LOADED); } function selectKoboldGuiPreset() { $('#settings_preset option[value=gui]') .attr('selected', 'true') .trigger('change'); } export async function saveSettings(type) { if (!settingsReady) { console.warn('Settings not ready, aborting save'); return; } //console.log('Entering settings with name1 = '+name1); return jQuery.ajax({ type: 'POST', url: '/api/settings/save', data: JSON.stringify({ firstRun: firstRun, currentVersion: currentVersion, username: name1, active_character: active_character, active_group: active_group, api_server: api_server, preset_settings: preset_settings, user_avatar: user_avatar, amount_gen: amount_gen, max_context: max_context, main_api: main_api, world_info_settings: getWorldInfoSettings(), textgenerationwebui_settings: textgen_settings, swipes: swipes, horde_settings: horde_settings, power_user: power_user, extension_settings: extension_settings, tags: tags, tag_map: tag_map, nai_settings: nai_settings, kai_settings: kai_settings, oai_settings: oai_settings, background: background_settings, proxies: proxies, selected_proxy: selected_proxy, }, null, 4), beforeSend: function () { }, cache: false, dataType: 'json', contentType: 'application/json', //processData: false, success: async function (data) { eventSource.emit(event_types.SETTINGS_UPDATED); }, error: function (jqXHR, exception) { toastr.error('Check the server connection and reload the page to prevent data loss.', 'Settings could not be saved'); console.log(exception); console.log(jqXHR); }, }); } export function setGenerationParamsFromPreset(preset) { const needsUnlock = (preset.max_length ?? max_context) > MAX_CONTEXT_DEFAULT || (preset.genamt ?? amount_gen) > MAX_RESPONSE_DEFAULT; $('#max_context_unlocked').prop('checked', needsUnlock).trigger('change'); if (preset.genamt !== undefined) { amount_gen = preset.genamt; $('#amount_gen').val(amount_gen); $('#amount_gen_counter').val(amount_gen); } if (preset.max_length !== undefined) { max_context = preset.max_length; $('#max_context').val(max_context); $('#max_context_counter').val(max_context); } } // Common code for message editor done and auto-save function updateMessage(div) { const mesBlock = div.closest('.mes_block'); let text = mesBlock.find('.edit_textarea').val(); const mes = chat[this_edit_mes_id]; let regexPlacement; if (mes.is_user) { regexPlacement = regex_placement.USER_INPUT; } else if (mes.extra?.type === 'narrator') { regexPlacement = regex_placement.SLASH_COMMAND; } else { regexPlacement = regex_placement.AI_OUTPUT; } // Ignore character override if sent as system text = getRegexedString( text, regexPlacement, { characterOverride: mes.extra?.type === 'narrator' ? undefined : mes.name }, ); if (power_user.trim_spaces) { text = text.trim(); } const bias = substituteParams(extractMessageBias(text)); text = substituteParams(text); if (bias) { text = removeMacros(text); } mes['mes'] = text; if (mes['swipe_id'] !== undefined) { mes['swipes'][mes['swipe_id']] = text; } // editing old messages if (!mes.extra) { mes.extra = {}; } if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) { mes.extra.bias = bias ?? null; } else { mes.extra.bias = null; } chat_metadata['tainted'] = true; return { mesBlock, text, mes, bias }; } function openMessageDelete(fromSlashCommand) { closeMessageEditor(); hideSwipeButtons(); if (fromSlashCommand || (this_chid != undefined && !is_send_press) || (selected_group && !is_group_generating)) { $('#dialogue_del_mes').css('display', 'block'); $('#send_form').css('display', 'none'); $('.del_checkbox').each(function () { $(this).css('display', 'grid'); $(this).parent().children('.for_checkbox').css('display', 'none'); }); } else { console.debug(` ERR -- could not enter del mode this_chid: ${this_chid} is_send_press: ${is_send_press} selected_group: ${selected_group} is_group_generating: ${is_group_generating}`); } this_del_mes = -1; is_delete_mode = true; } function messageEditAuto(div) { const { mesBlock, text, mes, bias } = updateMessage(div); mesBlock.find('.mes_text').val(''); mesBlock.find('.mes_text').val(messageFormatting( text, this_edit_mes_chname, mes.is_system, mes.is_user, this_edit_mes_id, )); mesBlock.find('.mes_bias').empty(); mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1)); saveChatDebounced(); } async function messageEditDone(div) { let { mesBlock, text, mes, bias } = updateMessage(div); if (this_edit_mes_id == 0) { text = substituteParams(text); } await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id); text = chat[this_edit_mes_id]?.mes ?? text; mesBlock.find('.mes_text').empty(); mesBlock.find('.mes_edit_buttons').css('display', 'none'); mesBlock.find('.mes_buttons').css('display', ''); mesBlock.find('.mes_text').append( messageFormatting( text, this_edit_mes_chname, mes.is_system, mes.is_user, this_edit_mes_id, ), ); mesBlock.find('.mes_bias').empty(); mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1)); appendMediaToMessage(mes, div.closest('.mes')); addCopyToCodeBlocks(div.closest('.mes')); await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id); this_edit_mes_id = undefined; await saveChatConditional(); } /** * Fetches the chat content for each chat file from the server and compiles them into a dictionary. * The function iterates over a provided list of chat metadata and requests the actual chat content * for each chat, either as an individual chat or a group chat based on the context. * * @param {Array} data - An array containing metadata about each chat such as file_name. * @param {boolean} isGroupChat - A flag indicating if the chat is a group chat. * @returns {Promise<Object>} chat_dict - A dictionary where each key is a file_name and the value is the * corresponding chat content fetched from the server. */ export async function getChatsFromFiles(data, isGroupChat) { const context = getContext(); let chat_dict = {}; let chat_list = Object.values(data).sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse(); let chat_promise = chat_list.map(({ file_name }) => { return new Promise(async (res, rej) => { try { const endpoint = isGroupChat ? '/api/chats/group/get' : '/api/chats/get'; const requestBody = isGroupChat ? JSON.stringify({ id: file_name }) : JSON.stringify({ ch_name: characters[context.characterId].name, file_name: file_name.replace('.jsonl', ''), avatar_url: characters[context.characterId].avatar, }); const chatResponse = await fetch(endpoint, { method: 'POST', headers: getRequestHeaders(), body: requestBody, cache: 'no-cache', }); if (!chatResponse.ok) { return res(); // continue; } const currentChat = await chatResponse.json(); if (!isGroupChat) { // remove the first message, which is metadata, only for individual chats currentChat.shift(); } chat_dict[file_name] = currentChat; } catch (error) { console.error(error); } return res(); }); }); await Promise.all(chat_promise); return chat_dict; } /** * Fetches the metadata of all past chats related to a specific character based on its avatar URL. * The function sends a POST request to the server to retrieve all chats for the character. It then * processes the received data, sorts it by the file name, and returns the sorted data. * * @param {null|number} [characterId=null] - When set, the function will use this character id instead of this_chid. * * @returns {Promise<Array>} - An array containing metadata of all past chats of the character, sorted * in descending order by file name. Returns an empty array if the fetch request is unsuccessful or the * response is an object with an `error` property set to `true`. */ export async function getPastCharacterChats(characterId = null) { characterId = characterId ?? this_chid; if (!characters[characterId]) return []; const response = await fetch('/api/characters/chats', { method: 'POST', body: JSON.stringify({ avatar_url: characters[characterId].avatar }), headers: getRequestHeaders(), }); if (!response.ok) { return []; } const data = await response.json(); if (typeof data === 'object' && data.error === true) { return []; } const chats = Object.values(data); return chats.sort((a, b) => a['file_name'].localeCompare(b['file_name'])).reverse(); } /** * Helper for `displayPastChats`, to make the same info consistently available for other functions */ function getCurrentChatDetails() { if (!characters[this_chid] && !selected_group) { return { sessionName: '', group: null, characterName: '', avatarImgURL: '' }; } const group = selected_group ? groups.find(x => x.id === selected_group) : null; const currentChat = selected_group ? group?.chat_id : characters[this_chid]['chat']; const displayName = selected_group ? group?.name : characters[this_chid].name; const avatarImg = selected_group ? group?.avatar_url : getThumbnailUrl('avatar', characters[this_chid]['avatar']); return { sessionName: currentChat, group: group, characterName: displayName, avatarImgURL: avatarImg }; } /** * Displays the past chats for a character or a group based on the selected context. * The function first fetches the chats, processes them, and then displays them in * the HTML. It also has a built-in search functionality that allows filtering the * displayed chats based on a search query. */ export async function displayPastChats() { $('#select_chat_div').empty(); $('#select_chat_search').val('').off('input'); const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats()); if (!data) { toastr.error('Could not load chat data. Try reloading the page.'); return; } const chatDetails = getCurrentChatDetails(); const group = chatDetails.group; const currentChat = chatDetails.sessionName; const displayName = chatDetails.characterName; const avatarImg = chatDetails.avatarImgURL; const rawChats = await getChatsFromFiles(data, selected_group); // Sort by last message date descending data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); console.log(data); $('#load_select_chat_div').css('display', 'none'); $('#ChatHistoryCharName').text(`${displayName}'s `); const displayChats = (searchQuery) => { $('#select_chat_div').empty(); // Clear the current chats before appending filtered chats const filteredData = data.filter(chat => { const fileName = chat['file_name']; const chatContent = rawChats[fileName]; // // Uncomment this to return to old behavior (classical full-substring search). // return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase())); // Fragment search a.k.a. swoop (as in `helm-swoop` in the Helm package of Emacs). // Split a `query` {string} into its fragments {string[]}. function makeQueryFragments(query) { let fragments = query.trim().split(/\s+/).map(str => str.trim().toLowerCase()).filter(onlyUnique); // fragments = fragments.filter( function(str) { return str.length >= 3; } ); // Helm does this, but perhaps better if we don't. return fragments; } // Check whether `text` {string} includes all of the `fragments` {string[]}. function matchFragments(fragments, text) { if (!text) { return false; } return fragments.every(item => text.includes(item)); } const fragments = makeQueryFragments(searchQuery); // At least one chat message must match *all* the fragments. // Currently, this doesn't match if the fragment matches are distributed across several chat messages. return chatContent && Object.values(chatContent).some(message => matchFragments(fragments, message?.mes?.toLowerCase())); }); console.debug(filteredData); for (const value of filteredData.values()) { let strlen = 300; let mes = value['mes']; if (mes !== undefined) { if (mes.length > strlen) { mes = '...' + mes.substring(mes.length - strlen); } const fileSize = value['file_size']; const fileName = value['file_name']; const chatItems = rawChats[fileName].length; const timestamp = timestampToMoment(value['last_mes']).format('lll'); const template = $('#past_chat_template .select_chat_block_wrapper').clone(); template.find('.select_chat_block').attr('file_name', fileName); template.find('.avatar img').attr('src', avatarImg); template.find('.select_chat_block_filename').text(fileName); template.find('.chat_file_size').text(`(${fileSize},`); template.find('.chat_messages_num').text(`${chatItems}💬)`); template.find('.select_chat_block_mes').text(mes); template.find('.PastChat_cross').attr('file_name', fileName); template.find('.chat_messages_date').text(timestamp); if (selected_group) { template.find('.avatar img').replaceWith(getGroupAvatar(group)); } $('#select_chat_div').append(template); if (currentChat === fileName.toString().replace('.jsonl', '')) { $('#select_chat_div').find('.select_chat_block:last').attr('highlight', String(true)); } } } }; displayChats(''); // Display all by default const debouncedDisplay = debounce((searchQuery) => { displayChats(searchQuery); }); // Define the search input listener $('#select_chat_search').on('input', function () { const searchQuery = $(this).val(); debouncedDisplay(searchQuery); }); // UX convenience: Focus the search field when the Manage Chat Files view opens. setTimeout(function () { const textSearchElement = $('#select_chat_search'); textSearchElement.click(); textSearchElement.focus(); textSearchElement.select(); // select content (if any) for easy erasing }, 200); } export function selectRightMenuWithAnimation(selectedMenuId) { const displayModes = { 'rm_group_chats_block': 'flex', 'rm_api_block': 'grid', 'rm_characters_block': 'flex', }; $('#result_info').toggle(selectedMenuId === 'rm_ch_create_block'); document.querySelectorAll('#right-nav-panel .right_menu').forEach((menu) => { $(menu).css('display', 'none'); if (selectedMenuId && selectedMenuId.replace('#', '') === menu.id) { const mode = displayModes[menu.id] ?? 'block'; $(menu).css('display', mode); $(menu).css('opacity', 0.0); $(menu).transition({ opacity: 1.0, duration: animation_duration, easing: animation_easing, complete: function () { }, }); } }); } export function select_rm_info(type, charId, previousCharId = null) { if (!type) { toastr.error('Invalid process (no \'type\')'); return; } if (type !== 'group_create') { var displayName = String(charId).replace('.png', ''); } if (type === 'char_delete') { toastr.warning(`Character Deleted: ${displayName}`); } if (type === 'char_create') { toastr.success(`Character Created: ${displayName}`); } if (type === 'group_create') { toastr.success('Group Created'); } if (type === 'group_delete') { toastr.warning('Group Deleted'); } if (type === 'char_import') { toastr.success(`Character Imported: ${displayName}`); } selectRightMenuWithAnimation('rm_characters_block'); // Set a timeout so multiple flashes don't overlap clearTimeout(importFlashTimeout); importFlashTimeout = setTimeout(function () { if (type === 'char_import' || type === 'char_create') { // Find the page at which the character is located const avatarFileName = `${charId}.png`; const charData = getEntitiesList({ doFilter: true }); const charIndex = charData.findIndex((x) => x?.item?.avatar?.startsWith(avatarFileName)); if (charIndex === -1) { console.log(`Could not find character ${charId} in the list`); return; } try { const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default; const page = Math.floor(charIndex / perPage) + 1; const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`; $('#rm_print_characters_pagination').pagination('go', page); waitUntilCondition(() => document.querySelector(selector) !== null).then(() => { const element = $(selector).parent(); if (element.length === 0) { console.log(`Could not find element for character ${charId}`); return; } const scrollOffset = element.offset().top - element.parent().offset().top; element.parent().scrollTop(scrollOffset); flashHighlight(element, 5000); }); } catch (e) { console.error(e); } } if (type === 'group_create') { // Find the page at which the character is located const charData = getEntitiesList({ doFilter: true }); const charIndex = charData.findIndex((x) => String(x?.item?.id) === String(charId)); if (charIndex === -1) { console.log(`Could not find group ${charId} in the list`); return; } const perPage = Number(localStorage.getItem('Characters_PerPage')) || per_page_default; const page = Math.floor(charIndex / perPage) + 1; $('#rm_print_characters_pagination').pagination('go', page); const selector = `#rm_print_characters_block [grid="${charId}"]`; try { waitUntilCondition(() => document.querySelector(selector) !== null).then(() => { const element = $(selector); const scrollOffset = element.offset().top - element.parent().offset().top; element.parent().scrollTop(scrollOffset); flashHighlight(element, 5000); }); } catch (e) { console.error(e); } } }, 250); if (previousCharId) { const newId = characters.findIndex((x) => x.avatar == previousCharId); if (newId >= 0) { this_chid = newId; } } } export function select_selected_character(chid) { //character select //console.log('select_selected_character() -- starting with input of -- ' + chid + ' (name:' + characters[chid].name + ')'); select_rm_create(); setMenuType('character_edit'); $('#delete_button').css('display', 'flex'); $('#export_button').css('display', 'flex'); var display_name = characters[chid].name; //create text poles $('#rm_button_back').css('display', 'none'); //$("#character_import_button").css("display", "none"); $('#create_button').attr('value', 'Save'); // what is the use case for this? $('#dupe_button').show(); $('#create_button_label').css('display', 'none'); // Hide the chat scenario button if we're peeking the group member defs $('#set_chat_scenario').toggle(!selected_group); // Don't update the navbar name if we're peeking the group member defs if (!selected_group) { $('#rm_button_selected_ch').children('h2').text(display_name); } $('#add_avatar_button').val(''); $('#character_popup-button-h3').text(characters[chid].name); $('#character_name_pole').val(characters[chid].name); $('#description_textarea').val(characters[chid].description); $('#character_world').val(characters[chid].data?.extensions?.world || ''); $('#creator_notes_textarea').val(characters[chid].data?.creator_notes || characters[chid].creatorcomment); $('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(substituteParams(characters[chid].data?.creator_notes) || characters[chid].creatorcomment), { MESSAGE_SANITIZE: true })); $('#character_version_textarea').val(characters[chid].data?.character_version || ''); $('#system_prompt_textarea').val(characters[chid].data?.system_prompt || ''); $('#post_history_instructions_textarea').val(characters[chid].data?.post_history_instructions || ''); $('#tags_textarea').val(Array.isArray(characters[chid].data?.tags) ? characters[chid].data.tags.join(', ') : ''); $('#creator_textarea').val(characters[chid].data?.creator); $('#character_version_textarea').val(characters[chid].data?.character_version || ''); $('#personality_textarea').val(characters[chid].personality); $('#firstmessage_textarea').val(characters[chid].first_mes); $('#scenario_pole').val(characters[chid].scenario); $('#depth_prompt_prompt').val(characters[chid].data?.extensions?.depth_prompt?.prompt ?? ''); $('#depth_prompt_depth').val(characters[chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default); $('#depth_prompt_role').val(characters[chid].data?.extensions?.depth_prompt?.role ?? depth_prompt_role_default); $('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default); $('#mes_example_textarea').val(characters[chid].mes_example); $('#selected_chat_pole').val(characters[chid].chat); $('#create_date_pole').val(characters[chid].create_date); $('#avatar_url_pole').val(characters[chid].avatar); $('#chat_import_avatar_url').val(characters[chid].avatar); $('#chat_import_character_name').val(characters[chid].name); $('#character_json_data').val(characters[chid].json_data); let this_avatar = default_avatar; if (characters[chid].avatar != 'none') { this_avatar = getThumbnailUrl('avatar', characters[chid].avatar); } updateFavButtonState(characters[chid].fav || characters[chid].fav == 'true'); $('#avatar_load_preview').attr('src', this_avatar); $('#name_div').removeClass('displayBlock'); $('#name_div').addClass('displayNone'); $('#renameCharButton').css('display', ''); $('.open_alternate_greetings').data('chid', chid); $('#set_character_world').data('chid', chid); setWorldInfoButtonClass(chid); checkEmbeddedWorld(chid); $('#form_create').attr('actiontype', 'editcharacter'); $('.form_create_bottom_buttons_block .chat_lorebook_button').show(); const externalMediaState = isExternalMediaAllowed(); $('#character_open_media_overrides').toggle(!selected_group); $('#character_media_allowed_icon').toggle(externalMediaState); $('#character_media_forbidden_icon').toggle(!externalMediaState); saveSettingsDebounced(); } function select_rm_create() { setMenuType('create'); //console.log('select_rm_Create() -- selected button: '+selected_button); if (selected_button == 'create') { if (create_save.avatar != '') { $('#add_avatar_button').get(0).files = create_save.avatar; read_avatar_load($('#add_avatar_button').get(0)); } } selectRightMenuWithAnimation('rm_ch_create_block'); $('#set_chat_scenario').hide(); $('#delete_button_div').css('display', 'none'); $('#delete_button').css('display', 'none'); $('#export_button').css('display', 'none'); $('#create_button_label').css('display', ''); $('#create_button').attr('value', 'Create'); $('#dupe_button').hide(); //create text poles $('#rm_button_back').css('display', ''); $('#character_import_button').css('display', ''); $('#character_popup-button-h3').text('Create character'); $('#character_name_pole').val(create_save.name); $('#description_textarea').val(create_save.description); $('#character_world').val(create_save.world); $('#creator_notes_textarea').val(create_save.creator_notes); $('#creator_notes_spoiler').html(DOMPurify.sanitize(converter.makeHtml(create_save.creator_notes), { MESSAGE_SANITIZE: true })); $('#post_history_instructions_textarea').val(create_save.post_history_instructions); $('#system_prompt_textarea').val(create_save.system_prompt); $('#tags_textarea').val(create_save.tags); $('#creator_textarea').val(create_save.creator); $('#character_version_textarea').val(create_save.character_version); $('#personality_textarea').val(create_save.personality); $('#firstmessage_textarea').val(create_save.first_message); $('#talkativeness_slider').val(create_save.talkativeness); $('#scenario_pole').val(create_save.scenario); $('#depth_prompt_prompt').val(create_save.depth_prompt_prompt); $('#depth_prompt_depth').val(create_save.depth_prompt_depth); $('#depth_prompt_role').val(create_save.depth_prompt_role); $('#mes_example_textarea').val(create_save.mes_example); $('#character_json_data').val(''); $('#avatar_div').css('display', 'flex'); $('#avatar_load_preview').attr('src', default_avatar); $('#renameCharButton').css('display', 'none'); $('#name_div').removeClass('displayNone'); $('#name_div').addClass('displayBlock'); $('.open_alternate_greetings').data('chid', undefined); $('#set_character_world').data('chid', undefined); setWorldInfoButtonClass(undefined, !!create_save.world); updateFavButtonState(false); checkEmbeddedWorld(); $('#form_create').attr('actiontype', 'createcharacter'); $('.form_create_bottom_buttons_block .chat_lorebook_button').hide(); $('#character_open_media_overrides').hide(); } function select_rm_characters() { const doFullRefresh = menu_type === 'characters'; setMenuType('characters'); selectRightMenuWithAnimation('rm_characters_block'); printCharacters(doFullRefresh); } /** * Sets a prompt injection to insert custom text into any outgoing prompt. For use in UI extensions. * @param {string} key Prompt injection id. * @param {string} value Prompt injection value. * @param {number} position Insertion position. 0 is after story string, 1 is in-chat with custom depth. * @param {number} depth Insertion depth. 0 represets the last message in context. Expected values up to MAX_INJECTION_DEPTH. * @param {number} role Extension prompt role. Defaults to SYSTEM. * @param {boolean} scan Should the prompt be included in the world info scan. */ export function setExtensionPrompt(key, value, position, depth, scan = false, role = extension_prompt_roles.SYSTEM) { extension_prompts[key] = { value: String(value), position: Number(position), depth: Number(depth), scan: !!scan, role: Number(role ?? extension_prompt_roles.SYSTEM), }; } /** * Gets a enum value of the extension prompt role by its name. * @param {string} roleName The name of the extension prompt role. * @returns {number} The role id of the extension prompt. */ export function getExtensionPromptRoleByName(roleName) { // If the role is already a valid number, return it if (typeof roleName === 'number' && Object.values(extension_prompt_roles).includes(roleName)) { return roleName; } switch (roleName) { case 'system': return extension_prompt_roles.SYSTEM; case 'user': return extension_prompt_roles.USER; case 'assistant': return extension_prompt_roles.ASSISTANT; } // Skill issue? return extension_prompt_roles.SYSTEM; } /** * Removes all char A/N prompt injections from the chat. * To clean up when switching from groups to solo and vice versa. */ export function removeDepthPrompts() { for (const key of Object.keys(extension_prompts)) { if (key.startsWith('DEPTH_PROMPT')) { delete extension_prompts[key]; } } } /** * Adds or updates the metadata for the currently active chat. * @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. */ export function updateChatMetadata(newValues, reset) { chat_metadata = reset ? { ...newValues } : { ...chat_metadata, ...newValues }; } function updateFavButtonState(state) { fav_ch_checked = state; $('#fav_checkbox').val(fav_ch_checked); $('#favorite_button').toggleClass('fav_on', fav_ch_checked); $('#favorite_button').toggleClass('fav_off', !fav_ch_checked); } export function setScenarioOverride() { if (!selected_group && !this_chid) { console.warn('setScenarioOverride() -- no selected group or character'); return; } const template = $('#scenario_override_template .scenario_override').clone(); const metadataValue = chat_metadata['scenario'] || ''; const isGroup = !!selected_group; template.find('[data-group="true"]').toggle(isGroup); template.find('[data-character="true"]').toggle(!isGroup); template.find('.chat_scenario').val(metadataValue).on('input', onScenarioOverrideInput); template.find('.remove_scenario_override').on('click', onScenarioOverrideRemoveClick); callPopup(template, 'text'); } function onScenarioOverrideInput() { const value = String($(this).val()); chat_metadata['scenario'] = value; saveMetadataDebounced(); } function onScenarioOverrideRemoveClick() { $(this).closest('.scenario_override').find('.chat_scenario').val('').trigger('input'); } /** * Displays a blocking popup with a given text and type. * @param {JQuery<HTMLElement>|string|Element} text - Text to display in the popup. * @param {string} type * @param {string} inputValue - Value to set the input to. * @param {PopupOptions} options - Options for the popup. * @typedef {{okButton?: string, rows?: number, wide?: boolean, wider?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean, cropAspect?: number }} PopupOptions - Options for the popup. * @returns */ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) { function getOkButtonText() { if (['avatarToCrop'].includes(popup_type)) { return okButton ?? 'Accept'; } else if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) { $dialoguePopupCancel.css('display', 'none'); return okButton ?? 'Ok'; } else if (['delete_extension'].includes(popup_type)) { return okButton ?? 'Ok'; } else if (['new_chat', 'confirm'].includes(popup_type)) { return okButton ?? 'Yes'; } else if (['input'].includes(popup_type)) { return okButton ?? 'Save'; } return okButton ?? 'Delete'; } dialogueCloseStop = true; if (type) { popup_type = type; } const $dialoguePopup = $('#dialogue_popup'); const $dialoguePopupCancel = $('#dialogue_popup_cancel'); const $dialoguePopupOk = $('#dialogue_popup_ok'); const $dialoguePopupInput = $('#dialogue_popup_input'); const $dialoguePopupText = $('#dialogue_popup_text'); const $shadowPopup = $('#shadow_popup'); $dialoguePopup.toggleClass('wide_dialogue_popup', !!wide) .toggleClass('wider_dialogue_popup', !!wider) .toggleClass('large_dialogue_popup', !!large) .toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling) .toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling); $dialoguePopupCancel.css('display', 'inline-block'); $dialoguePopupOk.text(getOkButtonText()); $dialoguePopupInput.toggle(popup_type === 'input').val(inputValue).attr('rows', rows ?? 1); $dialoguePopupText.empty().append(text); $shadowPopup.css('display', 'block'); if (popup_type == 'input') { $dialoguePopupInput.trigger('focus'); } if (popup_type == 'avatarToCrop') { // unset existing data crop_data = undefined; $('#avatarToCrop').cropper({ aspectRatio: cropAspect ?? 2 / 3, autoCropArea: 1, viewMode: 2, rotatable: false, crop: function (event) { crop_data = event.detail; crop_data.want_resize = !power_user.never_resize_avatars; }, }); } $shadowPopup.transition({ opacity: 1, duration: animation_duration, easing: animation_easing, }); return new Promise((resolve) => { dialogueResolve = resolve; }); } export function showSwipeButtons() { if (chat.length === 0) { return; } if ( chat[chat.length - 1].is_system || !swipes || Number($('.mes:last').attr('mesid')) < 0 || chat[chat.length - 1].is_user || chat[chat.length - 1].extra?.image || (selected_group && is_group_generating) ) { return; } // swipe_id should be set if alternate greetings are added if (chat.length == 1 && chat[0].swipe_id === undefined) { return; } //had to add this to make the swipe counter work //(copied from the onclick functions for swipe buttons.. //don't know why the array isn't set for non-swipe messsages in Generate or addOneMessage..) if (chat[chat.length - 1]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0 chat[chat.length - 1]['swipes'] = []; // empty the array chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes']; //assign swipe array with last message from chat } const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`); const swipeId = chat[chat.length - 1].swipe_id; var swipesCounterHTML = (`${(swipeId + 1)}/${(chat[chat.length - 1].swipes.length)}`); if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) { currentMessage.children('.swipe_left').css('display', 'flex'); } //only show right when generate is off, or when next right swipe would not make a generate happen if (is_send_press === false || chat[chat.length - 1].swipes.length >= swipeId) { currentMessage.children('.swipe_right').css('display', 'flex'); currentMessage.children('.swipe_right').css('opacity', '0.3'); } //console.log((chat[chat.length - 1])); if ((chat[chat.length - 1].swipes.length - swipeId) === 1) { //console.log('highlighting R swipe'); currentMessage.children('.swipe_right').css('opacity', '0.7'); } //console.log(swipesCounterHTML); $('.swipes-counter').html(swipesCounterHTML); //console.log(swipeId); //console.log(chat[chat.length - 1].swipes.length); } export function hideSwipeButtons() { //console.log('hideswipebuttons entered'); $('#chat').find('.swipe_right').css('display', 'none'); $('#chat').find('.swipe_left').css('display', 'none'); } export async function saveMetadata() { if (selected_group) { await editGroup(selected_group, true, false); } else { await saveChatConditional(); } } export async function saveChatConditional() { try { await waitUntilCondition(() => !isChatSaving, DEFAULT_SAVE_EDIT_TIMEOUT, 100); } catch { console.warn('Timeout waiting for chat to save'); return; } try { isChatSaving = true; if (selected_group) { await saveGroupChat(selected_group, true); } else { await saveChat(); } // Save token and prompts cache to IndexedDB storage saveTokenCache(); saveItemizedPrompts(getCurrentChatId()); } catch (error) { console.error('Error saving chat', error); } finally { isChatSaving = false; } } async function importCharacterChat(formData) { await jQuery.ajax({ type: 'POST', url: '/api/chats/import', data: formData, beforeSend: function () { }, cache: false, contentType: false, processData: false, success: async function (data) { if (data.res) { await displayPastChats(); } }, error: function () { $('#create_button').removeAttr('disabled'); }, }); } function updateViewMessageIds(startFromZero = false) { const minId = startFromZero ? 0 : getFirstDisplayedMessageId(); $('#chat').find('.mes').each(function (index, element) { $(element).attr('mesid', minId + index); $(element).find('.mesIDDisplay').text(`#${minId + index}`); }); $('#chat .mes').removeClass('last_mes'); $('#chat .mes').last().addClass('last_mes'); updateEditArrowClasses(); } export function getFirstDisplayedMessageId() { const allIds = Array.from(document.querySelectorAll('#chat .mes')).map(el => Number(el.getAttribute('mesid'))).filter(x => !isNaN(x)); const minId = Math.min(...allIds); return minId; } function updateEditArrowClasses() { $('#chat .mes .mes_edit_up').removeClass('disabled'); $('#chat .mes .mes_edit_down').removeClass('disabled'); if (this_edit_mes_id !== undefined) { const down = $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_down`); const up = $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_up`); const lastId = Number($('#chat .mes').last().attr('mesid')); const firstId = Number($('#chat .mes').first().attr('mesid')); if (lastId == Number(this_edit_mes_id)) { down.addClass('disabled'); } if (firstId == Number(this_edit_mes_id)) { up.addClass('disabled'); } } } function closeMessageEditor() { if (this_edit_mes_id) { $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_cancel`).click(); } } export function setGenerationProgress(progress) { if (!progress) { $('#send_textarea').css({ 'background': '', 'transition': '' }); } else { $('#send_textarea').css({ 'background': `linear-gradient(90deg, #008000d6 ${progress}%, transparent ${progress}%)`, 'transition': '0.25s ease-in-out', }); } } function isHordeGenerationNotAllowed() { if (main_api == 'koboldhorde' && preset_settings == 'gui') { toastr.error('GUI Settings preset is not supported for Horde. Please select another preset.'); return true; } return false; } export function cancelTtsPlay() { if ('speechSynthesis' in window) { speechSynthesis.cancel(); } } function updateAlternateGreetingsHintVisibility(root) { const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length; $(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0); } function openCharacterWorldPopup() { const chid = $('#set_character_world').data('chid'); if (menu_type != 'create' && chid == undefined) { toastr.error('Does not have an Id for this character in world select menu.'); return; } async function onSelectCharacterWorld() { const value = $('.character_world_info_selector').find('option:selected').val(); const worldIndex = value !== '' ? Number(value) : NaN; const name = !isNaN(worldIndex) ? world_names[worldIndex] : ''; const previousValue = $('#character_world').val(); $('#character_world').val(name); console.debug('Character world selected:', name); if (menu_type == 'create') { create_save.world = name; } else { if (previousValue && !name) { try { // Dirty hack to remove embedded lorebook from character JSON data. const data = JSON.parse(String($('#character_json_data').val())); if (data?.data?.character_book) { data.data.character_book = undefined; } $('#character_json_data').val(JSON.stringify(data)); toastr.info('Embedded lorebook will be removed from this character.'); } catch { console.error('Failed to parse character JSON data.'); } } await createOrEditCharacter(); } setWorldInfoButtonClass(undefined, !!value); } function onExtraWorldInfoChanged() { const selectedWorlds = $('.character_extra_world_info_selector').val(); let charLore = world_info.charLore ?? []; // TODO: Maybe make this utility function not use the window context? const fileName = getCharaFilename(chid); const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter((e) => e !== undefined); const existingCharIndex = charLore.findIndex((e) => e.name === fileName); if (existingCharIndex === -1) { const newCharLoreEntry = { name: fileName, extraBooks: tempExtraBooks, }; charLore.push(newCharLoreEntry); } else if (tempExtraBooks.length === 0) { charLore.splice(existingCharIndex, 1); } else { charLore[existingCharIndex].extraBooks = tempExtraBooks; } Object.assign(world_info, { charLore: charLore }); saveSettingsDebounced(); } const template = $('#character_world_template .character_world').clone(); const select = template.find('.character_world_info_selector'); const extraSelect = template.find('.character_extra_world_info_selector'); const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless'; const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || ''; template.find('.character_name').text(name); // Not needed on mobile if (!isMobile()) { $(extraSelect).select2({ width: '100%', placeholder: 'No auxillary Lorebooks set. Click here to select.', allowClear: true, closeOnSelect: false, }); } // Apped to base dropdown world_names.forEach((item, i) => { const option = document.createElement('option'); option.value = i; option.innerText = item; option.selected = item === worldId; select.append(option); }); // Append to extras dropdown if (world_names.length > 0) { extraSelect.empty(); } world_names.forEach((item, i) => { const option = document.createElement('option'); option.value = i; option.innerText = item; const existingCharLore = world_info.charLore?.find((e) => e.name === getCharaFilename()); if (existingCharLore) { option.selected = existingCharLore.extraBooks.includes(item); } else { option.selected = false; } extraSelect.append(option); }); select.on('change', onSelectCharacterWorld); extraSelect.on('mousedown change', async function (e) { // If there's no world names, don't do anything if (world_names.length === 0) { e.preventDefault(); return; } onExtraWorldInfoChanged(); }); callPopup(template, 'text'); } function openAlternateGreetings() { const chid = $('.open_alternate_greetings').data('chid'); if (menu_type != 'create' && chid === undefined) { toastr.error('Does not have an Id for this character in editor menu.'); return; } else { // If the character does not have alternate greetings, create an empty array if (chid && Array.isArray(characters[chid].data.alternate_greetings) == false) { characters[chid].data.alternate_greetings = []; } } const template = $('#alternate_greetings_template .alternate_grettings').clone(); const getArray = () => menu_type == 'create' ? create_save.alternate_greetings : characters[chid].data.alternate_greetings; for (let index = 0; index < getArray().length; index++) { addAlternateGreeting(template, getArray()[index], index, getArray); } template.find('.add_alternate_greeting').on('click', function () { const array = getArray(); const index = array.length; array.push(''); addAlternateGreeting(template, '', index, getArray); updateAlternateGreetingsHintVisibility(template); }); updateAlternateGreetingsHintVisibility(template); callPopup(template, 'alternate_greeting', '', { wide: true, large: true }); } function addAlternateGreeting(template, greeting, index, getArray) { const greetingBlock = $('#alternate_greeting_form_template .alternate_greeting').clone(); greetingBlock.find('.alternate_greeting_text').on('input', async function () { const value = $(this).val(); const array = getArray(); array[index] = value; }).val(greeting); greetingBlock.find('.greeting_index').text(index + 1); greetingBlock.find('.delete_alternate_greeting').on('click', async function () { if (confirm('Are you sure you want to delete this alternate greeting?')) { const array = getArray(); array.splice(index, 1); // We need to reopen the popup to update the index numbers openAlternateGreetings(); } }); template.find('.alternate_greetings_list').append(greetingBlock); } /** * Creates or edits a character based on the form data. * @param {Event} [e] Event that triggered the function call. */ async function createOrEditCharacter(e) { $('#rm_info_avatar').html(''); const formData = new FormData($('#form_create').get(0)); formData.set('fav', String(fav_ch_checked)); const isNewChat = e instanceof CustomEvent && e.type === 'newChat'; const rawFile = formData.get('avatar'); if (rawFile instanceof File) { const convertedFile = await ensureImageFormatSupported(rawFile); formData.set('avatar', convertedFile); } if ($('#form_create').attr('actiontype') == 'createcharacter') { if (String($('#character_name_pole').val()).length > 0) { if (is_group_generating || is_send_press) { toastr.error('Cannot create characters while generating. Stop the request and try again.', 'Creation aborted'); throw new Error('Cannot import character while generating'); } //if the character name text area isn't empty (only posible when creating a new character) let url = '/api/characters/create'; if (crop_data != undefined) { url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; } formData.delete('alternate_greetings'); for (const value of create_save.alternate_greetings) { formData.append('alternate_greetings', value); } formData.append('extensions', JSON.stringify(create_save.extensions)); await jQuery.ajax({ type: 'POST', url: url, data: formData, beforeSend: function () { $('#create_button').attr('disabled', String(true)); $('#create_button').attr('value', '⏳'); }, cache: false, contentType: false, processData: false, success: async function (html) { $('#character_cross').trigger('click'); //closes the advanced character editing popup const fields = [ { id: '#character_name_pole', callback: value => create_save.name = value }, { id: '#description_textarea', callback: value => create_save.description = value }, { id: '#creator_notes_textarea', callback: value => create_save.creator_notes = value }, { id: '#character_version_textarea', callback: value => create_save.character_version = value }, { id: '#post_history_instructions_textarea', callback: value => create_save.post_history_instructions = value }, { id: '#system_prompt_textarea', callback: value => create_save.system_prompt = value }, { id: '#tags_textarea', callback: value => create_save.tags = value }, { id: '#creator_textarea', callback: value => create_save.creator = value }, { id: '#personality_textarea', callback: value => create_save.personality = value }, { id: '#firstmessage_textarea', callback: value => create_save.first_message = value }, { id: '#talkativeness_slider', callback: value => create_save.talkativeness = value, defaultValue: talkativeness_default }, { id: '#scenario_pole', callback: value => create_save.scenario = value }, { id: '#depth_prompt_prompt', callback: value => create_save.depth_prompt_prompt = value }, { id: '#depth_prompt_depth', callback: value => create_save.depth_prompt_depth = value, defaultValue: depth_prompt_depth_default }, { id: '#depth_prompt_role', callback: value => create_save.depth_prompt_role = value, defaultValue: depth_prompt_role_default }, { id: '#mes_example_textarea', callback: value => create_save.mes_example = value }, { id: '#character_json_data', callback: () => { } }, { id: '#alternate_greetings_template', callback: value => create_save.alternate_greetings = value, defaultValue: [] }, { id: '#character_world', callback: value => create_save.world = value }, { id: '#_character_extensions_fake', callback: value => create_save.extensions = {} }, ]; fields.forEach(field => { const fieldValue = field.defaultValue !== undefined ? field.defaultValue : ''; $(field.id).val(fieldValue); field.callback && field.callback(fieldValue); }); $('#character_popup-button-h3').text('Create character'); create_save.avatar = ''; $('#create_button').removeAttr('disabled'); $('#add_avatar_button').replaceWith( $('#add_avatar_button').val('').clone(true), ); $('#create_button').attr('value', '✅'); let oldSelectedChar = null; if (this_chid !== undefined) { oldSelectedChar = characters[this_chid].avatar; } console.log(`new avatar id: ${html}`); createTagMapFromList('#tagList', html); await getCharacters(); select_rm_info('char_create', html, oldSelectedChar); crop_data = undefined; }, error: function (jqXHR, exception) { $('#create_button').removeAttr('disabled'); }, }); } else { toastr.error('Name is required'); } } else { let url = '/api/characters/edit'; if (crop_data != undefined) { url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`; } formData.delete('alternate_greetings'); const chid = $('.open_alternate_greetings').data('chid'); if (chid && Array.isArray(characters[chid]?.data?.alternate_greetings)) { for (const value of characters[chid].data.alternate_greetings) { formData.append('alternate_greetings', value); } } await jQuery.ajax({ type: 'POST', url: url, data: formData, beforeSend: function () { $('#create_button').attr('disabled', String(true)); $('#create_button').attr('value', 'Save'); }, cache: false, contentType: false, processData: false, success: async function (html) { $('#create_button').removeAttr('disabled'); await getOneCharacter(formData.get('avatar_url')); favsToHotswap(); // Update fav state $('#add_avatar_button').replaceWith( $('#add_avatar_button').val('').clone(true), ); $('#create_button').attr('value', 'Save'); crop_data = undefined; eventSource.emit(event_types.CHARACTER_EDITED, { detail: { id: this_chid, character: characters[this_chid] } }); // Recreate the chat if it hasn't been used at least once (i.e. with continue). const message = getFirstMessage(); const shouldRegenerateMessage = !isNewChat && message.mes && !selected_group && !chat_metadata['tainted'] && (chat.length === 0 || (chat.length === 1 && !chat[0].is_user && !chat[0].is_system)); if (shouldRegenerateMessage) { chat.splice(0, chat.length, message); const messageId = (chat.length - 1); await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId); await clearChat(); await printMessages(); await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId); await saveChatConditional(); } }, error: function (jqXHR, exception) { $('#create_button').removeAttr('disabled'); console.log('Error! Either a file with the same name already existed, or the image file provided was in an invalid format. Double check that the image is not a webp.'); toastr.error('Something went wrong while saving the character, or the image file provided was in an invalid format. Double check that the image is not a webp.'); }, }); } } window['SillyTavern'].getContext = function () { return { chat: chat, characters: characters, groups: groups, name1: name1, name2: name2, characterId: this_chid, groupId: selected_group, chatId: selected_group ? groups.find(x => x.id == selected_group)?.chat_id : (this_chid && characters[this_chid] && characters[this_chid].chat), getCurrentChatId: getCurrentChatId, getRequestHeaders: getRequestHeaders, reloadCurrentChat: reloadCurrentChat, renameChat: renameChat, saveSettingsDebounced: saveSettingsDebounced, onlineStatus: online_status, maxContext: Number(max_context), chatMetadata: chat_metadata, streamingProcessor, eventSource: eventSource, eventTypes: event_types, addOneMessage: addOneMessage, generate: Generate, getTokenCount: getTokenCount, extensionPrompts: extension_prompts, setExtensionPrompt: setExtensionPrompt, updateChatMetadata: updateChatMetadata, saveChat: saveChatConditional, openCharacterChat: openCharacterChat, openGroupChat: openGroupChat, saveMetadata: saveMetadata, sendSystemMessage: sendSystemMessage, activateSendButtons, deactivateSendButtons, saveReply, substituteParams, substituteParamsExtended, registerSlashCommand: registerSlashCommand, executeSlashCommands: executeSlashCommands, timestampToMoment: timestampToMoment, /** * @deprecated Handlebars for extensions are no longer supported. */ registerHelper: () => { }, registedDebugFunction: registerDebugFunction, /** * @deprecated Use renderExtensionTemplateAsync instead. */ renderExtensionTemplate: renderExtensionTemplate, renderExtensionTemplateAsync: renderExtensionTemplateAsync, registerDataBankScraper: ScraperManager.registerDataBankScraper, callPopup: callPopup, callGenericPopup: callGenericPopup, mainApi: main_api, extensionSettings: extension_settings, ModuleWorkerWrapper: ModuleWorkerWrapper, getTokenizerModel: getTokenizerModel, generateQuietPrompt: generateQuietPrompt, writeExtensionField: writeExtensionField, getThumbnailUrl: getThumbnailUrl, selectCharacterById: selectCharacterById, messageFormatting: messageFormatting, shouldSendOnEnter: shouldSendOnEnter, isMobile: isMobile, tags: tags, tagMap: tag_map, menuType: menu_type, createCharacterData: create_save, /** * @deprecated Legacy snake-case naming, compatibility with old extensions */ event_types: event_types, }; }; function swipe_left() { // when we swipe left..but no generation. if (chat.length - 1 === Number(this_edit_mes_id)) { closeMessageEditor(); } if (isStreamingEnabled() && streamingProcessor) { streamingProcessor.onStopStreaming(); } const swipe_duration = 120; const swipe_range = '700px'; chat[chat.length - 1]['swipe_id']--; if (chat[chat.length - 1]['swipe_id'] < 0) { chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length - 1; } if (chat[chat.length - 1]['swipe_id'] >= 0) { /*$(this).parent().children('swipe_right').css('display', 'flex'); if (chat[chat.length - 1]['swipe_id'] === 0) { $(this).css('display', 'none'); }*/ // Just in case if (!Array.isArray(chat[chat.length - 1]['swipe_info'])) { chat[chat.length - 1]['swipe_info'] = []; } let this_mes_div = $(this).parent(); let this_mes_block = $(this).parent().children('.mes_block').children('.mes_text'); const this_mes_div_height = this_mes_div[0].scrollHeight; this_mes_div.css('height', this_mes_div_height); const this_mes_block_height = this_mes_block[0].scrollHeight; chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']]; chat[chat.length - 1]['send_date'] = chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1].send_date; //load the last mes box with the latest generation chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringify(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra)); if (chat[chat.length - 1].extra) { // if message has memory attached - remove it to allow regen if (chat[chat.length - 1].extra.memory) { delete chat[chat.length - 1].extra.memory; } // ditto for display text if (chat[chat.length - 1].extra.display_text) { delete chat[chat.length - 1].extra.display_text; } } $(this).parent().children('.mes_block').transition({ x: swipe_range, duration: swipe_duration, easing: animation_easing, queue: false, complete: async function () { const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10); //console.log('on left swipe click calling addOneMessage'); addOneMessage(chat[chat.length - 1], { type: 'swipe' }); if (power_user.message_token_count_enabled) { if (!chat[chat.length - 1].extra) { chat[chat.length - 1].extra = {}; } const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`); const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0); chat[chat.length - 1]['extra']['token_count'] = tokenCount; swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`); } let new_height = this_mes_div_height - (this_mes_block_height - this_mes_block[0].scrollHeight); if (new_height < 103) new_height = 103; this_mes_div.animate({ height: new_height + 'px' }, { duration: 0, //used to be 100 queue: false, progress: function () { // Scroll the chat down as the message expands if (is_animation_scroll) $('#chat').scrollTop($('#chat')[0].scrollHeight); }, complete: function () { this_mes_div.css('height', 'auto'); // Scroll the chat down to the bottom once the animation is complete if (is_animation_scroll) $('#chat').scrollTop($('#chat')[0].scrollHeight); }, }); $(this).parent().children('.mes_block').transition({ x: '-' + swipe_range, duration: 0, easing: animation_easing, queue: false, complete: function () { $(this).parent().children('.mes_block').transition({ x: '0px', duration: swipe_duration, easing: animation_easing, queue: false, complete: async function () { await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1)); saveChatDebounced(); }, }); }, }); }, }); $(this).parent().children('.avatar').transition({ x: swipe_range, duration: swipe_duration, easing: animation_easing, queue: false, complete: function () { $(this).parent().children('.avatar').transition({ x: '-' + swipe_range, duration: 0, easing: animation_easing, queue: false, complete: function () { $(this).parent().children('.avatar').transition({ x: '0px', duration: swipe_duration, easing: animation_easing, queue: false, complete: function () { }, }); }, }); }, }); } if (chat[chat.length - 1]['swipe_id'] < 0) { chat[chat.length - 1]['swipe_id'] = 0; } } /** * Creates a new branch from the message with the given ID * @param {number} mesId Message ID * @returns {Promise<string>} Branch file name */ async function branchChat(mesId) { const fileName = await createBranch(mesId); await saveItemizedPrompts(fileName); if (selected_group) { await openGroupChat(selected_group, fileName); } else { await openCharacterChat(fileName); } return fileName; } // when we click swipe right button const swipe_right = () => { if (chat.length - 1 === Number(this_edit_mes_id)) { closeMessageEditor(); } if (isHordeGenerationNotAllowed()) { return unblockGeneration(); } const swipe_duration = 200; const swipe_range = 700; //console.log(swipe_range); let run_generate = false; let run_swipe_right = false; if (chat[chat.length - 1]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array chat[chat.length - 1]['swipe_id'] = 0; // set it to id 0 chat[chat.length - 1]['swipes'] = []; // empty the array chat[chat.length - 1]['swipe_info'] = []; chat[chat.length - 1]['swipes'][0] = chat[chat.length - 1]['mes']; //assign swipe array with last message from chat chat[chat.length - 1]['swipe_info'][0] = { 'send_date': chat[chat.length - 1]['send_date'], 'gen_started': chat[chat.length - 1]['gen_started'], 'gen_finished': chat[chat.length - 1]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[chat.length - 1]['extra'])) }; //assign swipe info array with last message from chat } if (chat.length === 1 && chat[0]['swipe_id'] !== undefined && chat[0]['swipe_id'] === chat[0]['swipes'].length - 1) { // if swipe_right is called on the last alternate greeting, loop back around chat[0]['swipe_id'] = 0; } else { chat[chat.length - 1]['swipe_id']++; // make new slot in array } if (chat[chat.length - 1].extra) { // if message has memory attached - remove it to allow regen if (chat[chat.length - 1].extra.memory) { delete chat[chat.length - 1].extra.memory; } // ditto for display text if (chat[chat.length - 1].extra.display_text) { delete chat[chat.length - 1].extra.display_text; } } if (!Array.isArray(chat[chat.length - 1]['swipe_info'])) { chat[chat.length - 1]['swipe_info'] = []; } //console.log(chat[chat.length-1]['swipes']); if (parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length && chat.length !== 1) { //if swipe id of last message is the same as the length of the 'swipes' array and not the greeting delete chat[chat.length - 1].gen_started; delete chat[chat.length - 1].gen_finished; run_generate = true; } else if (parseInt(chat[chat.length - 1]['swipe_id']) < chat[chat.length - 1]['swipes'].length) { //otherwise, if the id is less than the number of swipes chat[chat.length - 1]['mes'] = chat[chat.length - 1]['swipes'][chat[chat.length - 1]['swipe_id']]; //load the last mes box with the latest generation chat[chat.length - 1]['send_date'] = chat[chat.length - 1]?.swipe_info[chat[chat.length - 1]['swipe_id']]?.send_date || chat[chat.length - 1]['send_date']; //update send date chat[chat.length - 1]['extra'] = JSON.parse(JSON.stringify(chat[chat.length - 1].swipe_info[chat[chat.length - 1]['swipe_id']]?.extra || chat[chat.length - 1].extra || [])); run_swipe_right = true; //then prepare to do normal right swipe to show next message } const currentMessage = $('#chat').children().filter(`[mesid="${chat.length - 1}"]`); let this_div = currentMessage.children('.swipe_right'); let this_mes_div = this_div.parent(); if (chat[chat.length - 1]['swipe_id'] > chat[chat.length - 1]['swipes'].length) { //if we swipe right while generating (the swipe ID is greater than what we are viewing now) chat[chat.length - 1]['swipe_id'] = chat[chat.length - 1]['swipes'].length; //show that message slot (will be '...' while generating) } if (run_generate) { //hide swipe arrows while generating this_div.css('display', 'none'); } // handles animated transitions when swipe right, specifically height transitions between messages if (run_generate || run_swipe_right) { let this_mes_block = this_mes_div.children('.mes_block').children('.mes_text'); const this_mes_div_height = this_mes_div[0].scrollHeight; const this_mes_block_height = this_mes_block[0].scrollHeight; this_mes_div.children('.swipe_left').css('display', 'flex'); this_mes_div.children('.mes_block').transition({ // this moves the div back and forth x: '-' + swipe_range, duration: swipe_duration, easing: animation_easing, queue: false, complete: async function () { /*if (!selected_group) { var typingIndicator = $("#typing_indicator_template .typing_indicator").clone(); typingIndicator.find(".typing_indicator_name").text(characters[this_chid].name); } */ /* $("#chat").append(typingIndicator); */ const is_animation_scroll = ($('#chat').scrollTop() >= ($('#chat').prop('scrollHeight') - $('#chat').outerHeight()) - 10); //console.log(parseInt(chat[chat.length-1]['swipe_id'])); //console.log(chat[chat.length-1]['swipes'].length); const swipeMessage = $('#chat').find('[mesid="' + (chat.length - 1) + '"]'); if (run_generate && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { //shows "..." while generating swipeMessage.find('.mes_text').html('...'); // resets the timer swipeMessage.find('.mes_timer').html(''); swipeMessage.find('.tokenCounterDisplay').text(''); } else { //console.log('showing previously generated swipe candidate, or "..."'); //console.log('onclick right swipe calling addOneMessage'); addOneMessage(chat[chat.length - 1], { type: 'swipe' }); if (power_user.message_token_count_enabled) { if (!chat[chat.length - 1].extra) { chat[chat.length - 1].extra = {}; } const tokenCount = await getTokenCountAsync(chat[chat.length - 1].mes, 0); chat[chat.length - 1]['extra']['token_count'] = tokenCount; swipeMessage.find('.tokenCounterDisplay').text(`${tokenCount}t`); } } let new_height = this_mes_div_height - (this_mes_block_height - this_mes_block[0].scrollHeight); if (new_height < 103) new_height = 103; this_mes_div.animate({ height: new_height + 'px' }, { duration: 0, //used to be 100 queue: false, progress: function () { // Scroll the chat down as the message expands if (is_animation_scroll) $('#chat').scrollTop($('#chat')[0].scrollHeight); }, complete: function () { this_mes_div.css('height', 'auto'); // Scroll the chat down to the bottom once the animation is complete if (is_animation_scroll) $('#chat').scrollTop($('#chat')[0].scrollHeight); }, }); this_mes_div.children('.mes_block').transition({ x: swipe_range, duration: 0, easing: animation_easing, queue: false, complete: function () { this_mes_div.children('.mes_block').transition({ x: '0px', duration: swipe_duration, easing: animation_easing, queue: false, complete: async function () { await eventSource.emit(event_types.MESSAGE_SWIPED, (chat.length - 1)); if (run_generate && !is_send_press && parseInt(chat[chat.length - 1]['swipe_id']) === chat[chat.length - 1]['swipes'].length) { console.debug('caught here 2'); is_send_press = true; $('.mes_buttons:last').hide(); await Generate('swipe'); } else { if (parseInt(chat[chat.length - 1]['swipe_id']) !== chat[chat.length - 1]['swipes'].length) { saveChatDebounced(); } } }, }); }, }); }, }); this_mes_div.children('.avatar').transition({ // moves avatar along with swipe x: '-' + swipe_range, duration: swipe_duration, easing: animation_easing, queue: false, complete: function () { this_mes_div.children('.avatar').transition({ x: swipe_range, duration: 0, easing: animation_easing, queue: false, complete: function () { this_mes_div.children('.avatar').transition({ x: '0px', duration: swipe_duration, easing: animation_easing, queue: false, complete: function () { }, }); }, }); }, }); } }; const CONNECT_API_MAP = { 'kobold': { selected: 'kobold', button: '#api_button', }, 'horde': { selected: 'koboldhorde', }, 'novel': { selected: 'novel', button: '#api_button_novel', }, 'ooba': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.OOBA, }, 'tabby': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.TABBY, }, 'llamacpp': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.LLAMACPP, }, 'ollama': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.OLLAMA, }, 'mancer': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.MANCER, }, 'vllm': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.VLLM, }, 'aphrodite': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.APHRODITE, }, 'koboldcpp': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.KOBOLDCPP, }, 'kcpp': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.KOBOLDCPP, }, 'togetherai': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.TOGETHERAI, }, 'openai': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.OPENAI, }, 'oai': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.OPENAI, }, 'claude': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.CLAUDE, }, 'windowai': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.WINDOWAI, }, 'openrouter': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.OPENROUTER, }, 'scale': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.SCALE, }, 'ai21': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.AI21, }, 'makersuite': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.MAKERSUITE, }, 'mistralai': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.MISTRALAI, }, 'custom': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.CUSTOM, }, 'cohere': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.COHERE, }, 'perplexity': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.PERPLEXITY, }, 'groq': { selected: 'openai', button: '#api_button_openai', source: chat_completion_sources.GROQ, }, 'infermaticai': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.INFERMATICAI, }, 'dreamgen': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.DREAMGEN, }, 'openrouter-text': { selected: 'textgenerationwebui', button: '#api_button_textgenerationwebui', type: textgen_types.OPENROUTER, }, }; async function selectContextCallback(_, name) { if (!name) { return power_user.context.preset; } const contextNames = context_presets.map(preset => preset.name); const fuse = new Fuse(contextNames); const result = fuse.search(name); if (result.length === 0) { toastr.warning(`Context preset "${name}" not found`); return ''; } const foundName = result[0].item; selectContextPreset(foundName); return foundName; } async function selectInstructCallback(_, name) { if (!name) { return power_user.instruct.preset; } const instructNames = instruct_presets.map(preset => preset.name); const fuse = new Fuse(instructNames); const result = fuse.search(name); if (result.length === 0) { toastr.warning(`Instruct preset "${name}" not found`); return ''; } const foundName = result[0].item; selectInstructPreset(foundName); return foundName; } async function enableInstructCallback() { $('#instruct_enabled').prop('checked', true).trigger('change'); return ''; } async function disableInstructCallback() { $('#instruct_enabled').prop('checked', false).trigger('change'); return ''; } /** * @param {string} text API name */ async function connectAPISlash(_, text) { if (!text.trim()) { for (const [key, config] of Object.entries(CONNECT_API_MAP)) { if (config.selected !== main_api) continue; if (config.source) { if (oai_settings.chat_completion_source === config.source) { return key; } else { continue; } } if (config.type) { if (textgen_settings.type === config.type) { return key; } else { continue; } } return key; } } const apiConfig = CONNECT_API_MAP[text.toLowerCase()]; if (!apiConfig) { toastr.error(`Error: ${text} is not a valid API`); return; } $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true); $('#main_api').trigger('change'); if (apiConfig.source) { $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true); $('#chat_completion_source').trigger('change'); } if (apiConfig.type) { $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true); $('#textgen_type').trigger('change'); } if (apiConfig.button) { $(apiConfig.button).trigger('click'); } toastr.info(`API set to ${text}, trying to connect..`); try { await waitUntilCondition(() => online_status !== 'no_connection', 10000, 100); console.log('Connection successful'); } catch { console.log('Could not connect after 5 seconds, skipping.'); } } /** * Imports supported files dropped into the app window. * @param {File[]} files Array of files to process * @param {boolean?} preserveFileNames Whether to preserve original file names * @returns {Promise<void>} */ export async function processDroppedFiles(files, preserveFileNames = false) { const allowedMimeTypes = [ 'application/json', 'image/png', 'application/yaml', 'application/x-yaml', 'text/yaml', 'text/x-yaml', ]; const allowedExtensions = [ 'charx', ]; for (const file of files) { const extension = file.name.split('.').pop().toLowerCase(); if (allowedMimeTypes.includes(file.type) || allowedExtensions.includes(extension)) { await importCharacter(file, preserveFileNames); } else { toastr.warning('Unsupported file type: ' + file.name); } } } /** * Imports a character from a file. * @param {File} file File to import * @param {boolean?} preserveFileName Whether to preserve original file name * @returns {Promise<void>} */ async function importCharacter(file, preserveFileName = false) { if (is_group_generating || is_send_press) { toastr.error('Cannot import characters while generating. Stop the request and try again.', 'Import aborted'); throw new Error('Cannot import character while generating'); } const ext = file.name.match(/\.(\w+)$/); if (!ext || !(['json', 'png', 'yaml', 'yml', 'charx'].includes(ext[1].toLowerCase()))) { return; } const format = ext[1].toLowerCase(); $('#character_import_file_type').val(format); const formData = new FormData(); formData.append('avatar', file); formData.append('file_type', format); formData.append('preserve_file_name', String(preserveFileName)); const data = await jQuery.ajax({ type: 'POST', url: '/api/characters/import', data: formData, async: true, cache: false, contentType: false, processData: false, }); if (data.error) { toastr.error('The file is likely invalid or corrupted.', 'Could not import character'); return; } if (data.file_name !== undefined) { $('#character_search_bar').val('').trigger('input'); let oldSelectedChar = null; if (this_chid !== undefined) { oldSelectedChar = characters[this_chid].avatar; } await getCharacters(); select_rm_info('char_import', data.file_name, oldSelectedChar); if (power_user.tag_import_setting !== tag_import_setting.NONE) { let currentContext = getContext(); let avatarFileName = `${data.file_name}.png`; let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName); await importTags(importedCharacter); } } } async function importFromURL(items, files) { for (const item of items) { if (item.type === 'text/uri-list') { const uriList = await new Promise((resolve) => { item.getAsString((uriList) => { resolve(uriList); }); }); const uris = uriList.split('\n').filter(uri => uri.trim() !== ''); try { for (const uri of uris) { const request = await fetch(uri); const data = await request.blob(); const fileName = request.headers.get('Content-Disposition')?.split('filename=')[1]?.replace(/"/g, '') || uri.split('/').pop() || 'file.png'; const file = new File([data], fileName, { type: data.type }); files.push(file); } } catch (error) { console.error('Failed to import from URL', error); } } } } async function doImpersonate(args, prompt) { const options = prompt?.trim() ? { quiet_prompt: prompt.trim(), quietToLoud: true } : {}; const shouldAwait = isTrueBoolean(args?.await); const outerPromise = new Promise((outerResolve) => setTimeout(async () => { try { await waitUntilCondition(() => !is_send_press && !is_group_generating, 10000, 100); } catch { console.warn('Timeout waiting for generation unlock'); toastr.warning('Cannot run /impersonate command while the reply is being generated.'); return ''; } // Prevent generate recursion $('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles: true })); outerResolve(new Promise(innerResolve => setTimeout(() => innerResolve(Generate('impersonate', options)), 1))); }, 1)); if (shouldAwait) { const innerPromise = await outerPromise; await innerPromise; } return ''; } async function doDeleteChat() { await displayPastChats(); let currentChatDeleteButton = $('.select_chat_block[highlight=\'true\']').parent().find('.PastChat_cross'); $(currentChatDeleteButton).trigger('click'); await delay(1); $('#dialogue_popup_ok').trigger('click', { fromSlashCommand: true }); return ''; } async function doRenameChat(_, chatName) { if (!chatName) { toastr.warning('Name must be provided as an argument to rename this chat.'); return ''; } const currentChatName = getCurrentChatId(); if (!currentChatName) { toastr.warning('No chat selected that can be renamed.'); return ''; } await renameChat(currentChatName, chatName); toastr.success(`Successfully renamed chat to: ${chatName}`); return ''; } /** * Renames the currently selected chat. * @param {string} oldFileName Old name of the chat (no JSONL extension) * @param {string} newName New name for the chat (no JSONL extension) */ export async function renameChat(oldFileName, newName) { const body = { is_group: !!selected_group, avatar_url: characters[this_chid]?.avatar, original_file: `${oldFileName}.jsonl`, renamed_file: `${newName}.jsonl`, }; try { showLoader(); const response = await fetch('/api/chats/rename', { method: 'POST', body: JSON.stringify(body), headers: getRequestHeaders(), }); if (!response.ok) { throw new Error('Unsuccessful request.'); } const data = await response.json(); if (data.error) { throw new Error('Server returned an error.'); } if (selected_group) { await renameGroupChat(selected_group, oldFileName, newName); } else { if (characters[this_chid].chat == oldFileName) { characters[this_chid].chat = newName; $('#selected_chat_pole').val(characters[this_chid].chat); await createOrEditCharacter(); } } await reloadCurrentChat(); } catch { hideLoader(); await delay(500); await callPopup('An error has occurred. Chat was not renamed.', 'text'); } finally { hideLoader(); } } /** * /getchatname` slash command */ async function doGetChatName() { return getCurrentChatDetails().sessionName; } const isPwaMode = window.navigator.standalone; if (isPwaMode) { $('body').addClass('PWA'); } function doCharListDisplaySwitch() { console.debug('toggling body charListGrid state'); $('body').toggleClass('charListGrid'); power_user.charListGrid = $('body').hasClass('charListGrid') ? true : false; saveSettingsDebounced(); } function doCloseChat() { $('#option_close_chat').trigger('click'); return ''; } /** * Function to handle the deletion of a character, given a specific popup type and character ID. * If popup type equals "del_ch", it will proceed with deletion otherwise it will exit the function. * It fetches the delete character route, sending necessary parameters, and in case of success, * it proceeds to delete character from UI and saves settings. * In case of error during the fetch request, it logs the error details. * * @param {string} popup_type - The type of popup currently active. * @param {string} this_chid - The character ID to be deleted. * @param {boolean} delete_chats - Whether to delete chats or not. */ export async function handleDeleteCharacter(popup_type, this_chid, delete_chats) { if (popup_type !== 'del_ch' || !characters[this_chid]) { return; } await deleteCharacter(characters[this_chid].avatar, { deleteChats: delete_chats }); } /** * Deletes a character completely, including associated chats if specified * * @param {string} characterKey - The key (avatar) of the character to be deleted * @param {Object} [options] - Optional parameters for the deletion * @param {boolean} [options.deleteChats=true] - Whether to delete associated chats or not * @return {Promise<void>} - A promise that resolves when the character is successfully deleted */ export async function deleteCharacter(characterKey, { deleteChats = true } = {}) { const character = characters.find(x => x.avatar == characterKey); if (!character) { toastr.warning(`Character ${characterKey} not found. Cannot be deleted.`); return; } const chid = characters.indexOf(character); const pastChats = await getPastCharacterChats(chid); const msg = { avatar_url: character.avatar, delete_chats: deleteChats }; const response = await fetch('/api/characters/delete', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify(msg), cache: 'no-cache', }); if (!response.ok) { throw new Error(`Failed to delete character: ${response.status} ${response.statusText}`); } await removeCharacterFromUI(character.name, character.avatar); if (deleteChats) { for (const chat of pastChats) { const name = chat.file_name.replace('.jsonl', ''); await eventSource.emit(event_types.CHAT_DELETED, name); } } } /** * Function to delete a character from UI after character deletion API success. * It manages necessary UI changes such as closing advanced editing popup, unsetting * character ID, resetting characters array and chat metadata, deselecting character's tab * panel, removing character name from navigation tabs, clearing chat, removing character's * avatar from tag_map, fetching updated list of characters and updating the 'deleted * character' message. * It also ensures to save the settings after all the operations. * * @param {string} name - The name of the character to be deleted. * @param {string} avatar - The avatar URL of the character to be deleted. * @param {boolean} reloadCharacters - Whether the character list should be refreshed after deletion. */ async function removeCharacterFromUI(name, avatar, reloadCharacters = true) { await clearChat(); $('#character_cross').click(); this_chid = undefined; characters.length = 0; name2 = systemUserName; chat = [...safetychat]; chat_metadata = {}; $(document.getElementById('rm_button_selected_ch')).children('h2').text(''); this_chid = undefined; delete tag_map[avatar]; if (reloadCharacters) await getCharacters(); select_rm_info('char_delete', name); await printMessages(); saveSettingsDebounced(); } function doTogglePanels() { $('#option_settings').trigger('click'); return ''; } function addDebugFunctions() { const doBackfill = async () => { for (const message of chat) { // System messages are not counted if (message.is_system) { continue; } if (!message.extra) { message.extra = {}; } message.extra.token_count = await getTokenCountAsync(message.mes, 0); } await saveChatConditional(); await reloadCurrentChat(); }; registerDebugFunction('backfillTokenCounts', 'Backfill token counters', `Recalculates token counts of all messages in the current chat to refresh the counters. Useful when you switch between models that have different tokenizers. This is a visual change only. Your chat will be reloaded.`, doBackfill); registerDebugFunction('generationTest', 'Send a generation request', 'Generates text using the currently selected API.', async () => { const text = prompt('Input text:', 'Hello'); toastr.info('Working on it...'); const message = await generateRaw(text, null, false, false); alert(message); }); registerDebugFunction('clearPrompts', 'Delete itemized prompts', 'Deletes all itemized prompts from the local storage.', async () => { await clearItemizedPrompts(); toastr.info('Itemized prompts deleted.'); if (getCurrentChatId()) { await reloadCurrentChat(); } }); registerDebugFunction('toggleEventTracing', 'Toggle event tracing', 'Useful to see what triggered a certain event.', () => { localStorage.setItem('eventTracing', localStorage.getItem('eventTracing') === 'true' ? 'false' : 'true'); toastr.info('Event tracing is now ' + (localStorage.getItem('eventTracing') === 'true' ? 'enabled' : 'disabled')); }); registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => { const getContextContents = getContext(); const getSettingsContents = settings; //console.log(getSettingsContents); const logMessage = ` \`\`\` API: ${getSettingsContents.main_api} API Type: ${getSettingsContents[getSettingsContents.main_api + '_settings'].type} API server: ${getSettingsContents.api_server} Model: ${getContextContents.onlineStatus} Context Preset: ${power_user.context.preset} Instruct Preset: ${power_user.instruct.preset} API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api + '_settings'], null, 2)} \`\`\` `; //console.log(getSettingsContents) //console.log(logMessage); try { await navigator.clipboard.writeText(logMessage); toastr.info('Your ST API setup data has been copied to the clipboard.'); } catch (error) { toastr.error('Failed to copy ST Setup to clipboard:', error); } }); } jQuery(async function () { if (isMobile()) { console.debug('hiding movingUI and sheldWidth toggles for mobile'); $('#sheldWidthToggleBlock').hide(); $('#movingUIModeCheckBlock').hide(); } async function doForceSave() { await saveSettings(); await saveChatConditional(); toastr.success('Chat and settings saved.'); return ''; } // Collect all unique API names in an array const uniqueAPIs = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))]; SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'dupe', callback: DupeChar, helpString: 'Duplicates the currently selected character.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'api', callback: connectAPISlash, unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'API to connect to', typeList: [ARGUMENT_TYPE.STRING], isRequired: false, enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) => new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(uniqueAPIs.findIndex(x => x === selected)), selected[0].toUpperCase() ?? enumIcons.default)), }), ], helpString: ` <div> Connect to an API. If no argument is provided, it will return the currently connected API. </div> <div> <strong>Available APIs:</strong> <pre><code>${Object.keys(CONNECT_API_MAP).join(', ')}</code></pre> </div> `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'impersonate', callback: doImpersonate, aliases: ['imp'], namedArgumentList: [ new SlashCommandNamedArgument( 'await', 'Whether to await for the triggered generation before continuing', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ), ], unnamedArgumentList: [ new SlashCommandArgument( 'prompt', [ARGUMENT_TYPE.STRING], false, ), ], helpString: ` <div> Calls an impersonation response, with an optional additional prompt. </div> <div> If <code>await=true</code> named argument is passed, the command will wait for the impersonation to end before continuing. </div> <div> <strong>Example:</strong> <ul> <li> <pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre> </li> </ul> </div> `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'delchat', callback: doDeleteChat, helpString: 'Deletes the current chat.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'renamechat', callback: doRenameChat, unnamedArgumentList: [ new SlashCommandArgument( 'new chat name', [ARGUMENT_TYPE.STRING], true, ), ], helpString: 'Renames the current chat.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getchatname', callback: doGetChatName, returns: 'chat file name', helpString: 'Returns the name of the current chat file into the pipe.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'closechat', callback: doCloseChat, helpString: 'Closes the current chat.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'panels', callback: doTogglePanels, aliases: ['togglepanels'], helpString: 'Toggle UI panels on/off', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'forcesave', callback: doForceSave, helpString: 'Forces a save of the current chat and settings', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct', callback: selectInstructCallback, returns: 'current preset', unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'instruct preset name', typeList: [ARGUMENT_TYPE.STRING], enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)), }), ], helpString: ` <div> Selects instruct mode preset by name. Gets the current instruct if no name is provided. </div> <div> <strong>Example:</strong> <ul> <li> <pre><code class="language-stscript">/instruct creative</code></pre> </li> </ul> </div> `, })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-on', callback: enableInstructCallback, helpString: 'Enables instruct mode.', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-off', callback: disableInstructCallback, helpString: 'Disables instruct mode', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'context', callback: selectContextCallback, returns: 'template name', unnamedArgumentList: [ SlashCommandArgument.fromProps({ description: 'context preset name', typeList: [ARGUMENT_TYPE.STRING], enumProvider: () => context_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)), }), ], helpString: 'Selects context template by name. Gets the current template if no name is provided', })); SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'chat-manager', callback: () => { $('#option_select_chat').trigger('click'); return ''; }, aliases: ['chat-history', 'manage-chats'], helpString: 'Opens the chat manager for the current character/group.', })); setTimeout(function () { $('#groupControlsToggle').trigger('click'); $('#groupCurrentMemberListToggle .inline-drawer-icon').trigger('click'); }, 200); $('#chat').on('wheel touchstart', () => { scrollLock = true; }); $(document).on('click', '.api_loading', cancelStatusCheck); //////////INPUT BAR FOCUS-KEEPING LOGIC///////////// let S_TAPreviouslyFocused = false; $('#send_textarea').on('focusin focus click', () => { S_TAPreviouslyFocused = true; }); $('#send_but, #option_regenerate, #option_continue, #mes_continue').on('click', () => { if (S_TAPreviouslyFocused) { $('#send_textarea').focus(); } }); $(document).click(event => { if ($(':focus').attr('id') !== 'send_textarea') { var validIDs = ['options_button', 'send_but', 'mes_continue', 'send_textarea', 'option_regenerate', 'option_continue']; if (!validIDs.includes($(event.target).attr('id'))) { S_TAPreviouslyFocused = false; } } else { S_TAPreviouslyFocused = true; } }); ///////////////// $('#swipes-checkbox').change(function () { swipes = !!$('#swipes-checkbox').prop('checked'); if (swipes) { //console.log('toggle change calling showswipebtns'); showSwipeButtons(); } else { hideSwipeButtons(); } saveSettingsDebounced(); }); ///// SWIPE BUTTON CLICKS /////// $(document).on('click', '.swipe_right', swipe_right); $(document).on('click', '.swipe_left', swipe_left); const debouncedCharacterSearch = debounce((searchQuery) => { entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchQuery); }); $('#character_search_bar').on('input', function () { const searchQuery = String($(this).val()); debouncedCharacterSearch(searchQuery); }); $('#mes_continue').on('click', function () { $('#option_continue').trigger('click'); }); $('#send_but').on('click', function () { sendTextareaMessage(); }); //menu buttons setup $('#rm_button_settings').click(function () { selected_button = 'settings'; selectRightMenuWithAnimation('rm_api_block'); }); $('#rm_button_characters').click(function () { selected_button = 'characters'; select_rm_characters(); }); $('#rm_button_back').click(function () { selected_button = 'characters'; select_rm_characters(); }); $('#rm_button_create').click(function () { selected_button = 'create'; select_rm_create(); }); $('#rm_button_selected_ch').click(function () { if (selected_group) { select_group_chats(selected_group); } else { selected_button = 'character_edit'; select_selected_character(this_chid); } $('#character_search_bar').val('').trigger('input'); }); $(document).on('click', '.character_select', async function () { const id = $(this).attr('chid'); await selectCharacterById(id); }); $(document).on('click', '.bogus_folder_select', function () { const tagId = $(this).attr('tagid'); console.debug('Bogus folder clicked', tagId); chooseBogusFolder($(this), tagId); }); $(document).on('input', '.edit_textarea', function () { scroll_holder = $('#chat').scrollTop(); $(this).height(0).height(this.scrollHeight); is_use_scroll_holder = true; }); $('#chat').on('scroll', function () { if (is_use_scroll_holder) { $('#chat').scrollTop(scroll_holder); is_use_scroll_holder = false; } }); $(document).on('click', '.mes', function () { //when a 'delete message' parent div is clicked // and we are in delete mode and del_checkbox is visible if (!is_delete_mode || !$(this).children('.del_checkbox').is(':visible')) { return; } $('.mes').children('.del_checkbox').each(function () { $(this).prop('checked', false); $(this).parent().removeClass('selected'); }); $(this).addClass('selected'); //sets the bg of the mes selected for deletion var i = Number($(this).attr('mesid')); //checks the message ID in the chat this_del_mes = i; //as long as the current message ID is less than the total chat length while (i < chat.length) { //sets the bg of the all msgs BELOW the selected .mes $(`.mes[mesid="${i}"]`).addClass('selected'); $(`.mes[mesid="${i}"]`).children('.del_checkbox').prop('checked', true); i++; } }); $(document).on('click', '.PastChat_cross', function (e) { e.stopPropagation(); chat_file_for_del = $(this).attr('file_name'); console.debug('detected cross click for' + chat_file_for_del); callPopup('<h3>Delete the Chat File?</h3>', 'del_chat'); }); $('#advanced_div').click(function () { if (!is_advanced_char_open) { is_advanced_char_open = true; $('#character_popup').css({ 'display': 'flex', 'opacity': 0.0 }).addClass('open'); $('#character_popup').transition({ opacity: 1.0, duration: animation_duration, easing: animation_easing, }); } else { is_advanced_char_open = false; $('#character_popup').css('display', 'none').removeClass('open'); } }); $('#character_cross').click(function () { is_advanced_char_open = false; $('#character_popup').transition({ opacity: 0, duration: animation_duration, easing: animation_easing, }); setTimeout(function () { $('#character_popup').css('display', 'none'); }, animation_duration); }); $('#character_popup_ok').click(function () { is_advanced_char_open = false; $('#character_popup').css('display', 'none'); }); $('#dialogue_popup_ok').click(async function (e, customData) { const fromSlashCommand = customData?.fromSlashCommand || false; dialogueCloseStop = false; $('#shadow_popup').transition({ opacity: 0, duration: animation_duration, easing: animation_easing, }); setTimeout(function () { if (dialogueCloseStop) return; $('#shadow_popup').css('display', 'none'); $('#dialogue_popup').removeClass('large_dialogue_popup'); $('#dialogue_popup').removeClass('wide_dialogue_popup'); }, animation_duration); // $("#shadow_popup").css("opacity:", 0.0); if (popup_type == 'avatarToCrop') { dialogueResolve($('#avatarToCrop').data('cropper').getCroppedCanvas().toDataURL('image/jpeg')); } if (popup_type == 'del_chat') { //close past chat popup $('#select_chat_cross').trigger('click'); showLoader(); if (selected_group) { await deleteGroupChat(selected_group, chat_file_for_del); } else { await delChat(chat_file_for_del); } if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view. $('#options').hide(); // hide option popup menu hideLoader(); } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat). setTimeout(function () { $('#option_select_chat').click(); $('#options').hide(); // hide option popup menu hideLoader(); }, 2000); } } if (popup_type == 'del_ch') { const deleteChats = !!$('#del_char_checkbox').prop('checked'); eventSource.emit(event_types.CHARACTER_DELETED, { id: this_chid, character: characters[this_chid] }); await handleDeleteCharacter(popup_type, this_chid, deleteChats); } if (popup_type == 'alternate_greeting' && menu_type !== 'create') { createOrEditCharacter(); } if (popup_type === 'del_group') { const groupId = $('#dialogue_popup').data('group_id'); if (groupId) { deleteGroup(groupId); } } //Make a new chat for selected character if ( popup_type == 'new_chat' && (selected_group || this_chid !== undefined) && menu_type != 'create' ) { //Fix it; New chat doesn't create while open create character menu await clearChat(); chat.length = 0; chat_file_for_del = getCurrentChatDetails()?.sessionName; const isDelChatCheckbox = document.getElementById('del_chat_checkbox')?.checked; // Make it easier to find in backups if (isDelChatCheckbox) { await saveChatConditional(); } if (selected_group) { await createNewGroupChat(selected_group); if (isDelChatCheckbox) await deleteGroupChat(selected_group, chat_file_for_del); } else { //RossAscends: added character name to new chat filenames and replaced Date.now() with humanizedDateTime; chat_metadata = {}; characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`; $('#selected_chat_pole').val(characters[this_chid].chat); await getChat(); await createOrEditCharacter(new CustomEvent('newChat')); if (isDelChatCheckbox) await delChat(chat_file_for_del + '.jsonl'); } } rawPromptPopper.update(); $('#rawPromptPopup').hide(); if (dialogueResolve) { if (popup_type == 'input') { dialogueResolve($('#dialogue_popup_input').val()); $('#dialogue_popup_input').val(''); } else { dialogueResolve(true); } dialogueResolve = null; } }); $('#dialogue_popup_cancel').click(function (e) { dialogueCloseStop = false; $('#shadow_popup').transition({ opacity: 0, duration: animation_duration, easing: animation_easing, }); setTimeout(function () { if (dialogueCloseStop) return; $('#shadow_popup').css('display', 'none'); $('#dialogue_popup').removeClass('large_dialogue_popup'); }, animation_duration); //$("#shadow_popup").css("opacity:", 0.0); popup_type = ''; if (dialogueResolve) { dialogueResolve(false); dialogueResolve = null; } }); $('#add_avatar_button').change(function () { read_avatar_load(this); }); $('#form_create').submit(createOrEditCharacter); $('#delete_button').on('click', function () { callPopup(` <h3>Delete the character?</h3> <b>THIS IS PERMANENT!<br><br> <label for="del_char_checkbox" class="checkbox_label justifyCenter"> <input type="checkbox" id="del_char_checkbox" /> <small>Also delete the chat files</small> </label><br></b>`, 'del_ch', '', ); }); //////// OPTIMIZED ALL CHAR CREATION/EDITING TEXTAREA LISTENERS /////////////// $('#character_name_pole').on('input', function () { if (menu_type == 'create') { create_save.name = String($('#character_name_pole').val()); } }); const elementsToUpdate = { '#description_textarea': function () { create_save.description = String($('#description_textarea').val()); }, '#creator_notes_textarea': function () { create_save.creator_notes = String($('#creator_notes_textarea').val()); }, '#character_version_textarea': function () { create_save.character_version = String($('#character_version_textarea').val()); }, '#system_prompt_textarea': function () { create_save.system_prompt = String($('#system_prompt_textarea').val()); }, '#post_history_instructions_textarea': function () { create_save.post_history_instructions = String($('#post_history_instructions_textarea').val()); }, '#creator_textarea': function () { create_save.creator = String($('#creator_textarea').val()); }, '#tags_textarea': function () { create_save.tags = String($('#tags_textarea').val()); }, '#personality_textarea': function () { create_save.personality = String($('#personality_textarea').val()); }, '#scenario_pole': function () { create_save.scenario = String($('#scenario_pole').val()); }, '#mes_example_textarea': function () { create_save.mes_example = String($('#mes_example_textarea').val()); }, '#firstmessage_textarea': function () { create_save.first_message = String($('#firstmessage_textarea').val()); }, '#talkativeness_slider': function () { create_save.talkativeness = Number($('#talkativeness_slider').val()); }, '#depth_prompt_prompt': function () { create_save.depth_prompt_prompt = String($('#depth_prompt_prompt').val()); }, '#depth_prompt_depth': function () { create_save.depth_prompt_depth = Number($('#depth_prompt_depth').val()); }, '#depth_prompt_role': function () { create_save.depth_prompt_role = String($('#depth_prompt_role').val()); }, }; Object.keys(elementsToUpdate).forEach(function (id) { $(id).on('input', function () { if (menu_type == 'create') { elementsToUpdate[id](); } else { saveCharacterDebounced(); } }); }); $('#favorite_button').on('click', function () { updateFavButtonState(!fav_ch_checked); if (menu_type != 'create') { saveCharacterDebounced(); } }); /* $("#renameCharButton").on('click', renameCharacter); */ $(document).on('click', '.renameChatButton', async function (e) { e.stopPropagation(); const oldFileNameFull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text(); const oldFileName = oldFileNameFull.replace('.jsonl', ''); const popupText = `<h3>Enter the new name for the chat:<h3> <small>!!Using an existing filename will produce an error!!<br> This will break the link between checkpoint chats.<br> No need to add '.jsonl' at the end.<br> </small>`; const newName = await callPopup(popupText, 'input', oldFileName); if (!newName || newName == oldFileName) { console.log('no new name found, aborting'); return; } await renameChat(oldFileName, newName); await delay(250); $('#option_select_chat').trigger('click'); $('#options').hide(); }); $(document).on('click', '.exportChatButton, .exportRawChatButton', async function (e) { e.stopPropagation(); const format = $(this).data('format') || 'txt'; await saveChatConditional(); const filenamefull = $(this).closest('.select_chat_block_wrapper').find('.select_chat_block_filename').text(); console.log(`exporting ${filenamefull} in ${format} format`); const filename = filenamefull.replace('.jsonl', ''); const body = { is_group: !!selected_group, avatar_url: characters[this_chid]?.avatar, file: `${filename}.jsonl`, exportfilename: `${filename}.${format}`, format: format, }; console.log(body); try { const response = await fetch('/api/chats/export', { method: 'POST', body: JSON.stringify(body), headers: getRequestHeaders(), }); const data = await response.json(); if (!response.ok) { // display error message console.log(data.message); await delay(250); toastr.error(`Error: ${data.message}`); return; } else { const mimeType = format == 'txt' ? 'text/plain' : 'application/octet-stream'; // success, handle response data console.log(data); await delay(250); toastr.success(data.message); download(data.result, body.exportfilename, mimeType); } } catch (error) { // display error message console.log(`An error has occurred: ${error.message}`); await delay(250); toastr.error(`Error: ${error.message}`); } }); /////////////////////////////////////////////////////////////////////////////////// $('#api_button').click(function (e) { if ($('#api_url_text').val() != '') { let value = formatKoboldUrl(String($('#api_url_text').val()).trim()); if (!value) { toastr.error('Please enter a valid URL.'); return; } $('#api_url_text').val(value); api_server = value; startStatusLoading(); main_api = 'kobold'; saveSettingsDebounced(); getStatusKobold(); } }); $('#api_button_textgenerationwebui').on('click', async function (e) { const keys = [ { id: 'api_key_mancer', secret: SECRET_KEYS.MANCER }, { id: 'api_key_vllm', secret: SECRET_KEYS.VLLM }, { id: 'api_key_aphrodite', secret: SECRET_KEYS.APHRODITE }, { id: 'api_key_tabby', secret: SECRET_KEYS.TABBY }, { id: 'api_key_togetherai', secret: SECRET_KEYS.TOGETHERAI }, { id: 'api_key_ooba', secret: SECRET_KEYS.OOBA }, { id: 'api_key_infermaticai', secret: SECRET_KEYS.INFERMATICAI }, { id: 'api_key_dreamgen', secret: SECRET_KEYS.DREAMGEN }, { id: 'api_key_openrouter-tg', secret: SECRET_KEYS.OPENROUTER }, { id: 'api_key_koboldcpp', secret: SECRET_KEYS.KOBOLDCPP }, { id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP }, ]; for (const key of keys) { const keyValue = String($(`#${key.id}`).val()).trim(); if (keyValue.length) { await writeSecret(key.secret, keyValue); } } validateTextGenUrl(); startStatusLoading(); main_api = 'textgenerationwebui'; saveSettingsDebounced(); getStatusTextgen(); }); $('#api_button_novel').on('click', async function (e) { e.stopPropagation(); const api_key_novel = String($('#api_key_novel').val()).trim(); if (api_key_novel.length) { await writeSecret(SECRET_KEYS.NOVEL, api_key_novel); } if (!secret_state[SECRET_KEYS.NOVEL]) { console.log('No secret key saved for NovelAI'); return; } startStatusLoading(); // Check near immediately rather than waiting for up to 90s await getStatusNovel(); }); var button = $('#options_button'); var menu = $('#options'); function showMenu() { showBookmarksButtons(); // menu.stop() menu.fadeIn(animation_duration); optionsPopper.update(); } function hideMenu() { // menu.stop(); menu.fadeOut(animation_duration); optionsPopper.update(); } function isMouseOverButtonOrMenu() { return menu.is(':hover, :focus-within') || button.is(':hover, :focus'); } button.on('click', function () { if (menu.is(':visible')) { hideMenu(); } else { showMenu(); } }); button.on('blur', function () { //delay to prevent menu hiding when mouse leaves button into menu setTimeout(() => { if (!isMouseOverButtonOrMenu()) { hideMenu(); } }, 100); }); menu.on('blur', function () { //delay to prevent menu hide when mouseleaves menu into button setTimeout(() => { if (!isMouseOverButtonOrMenu()) { hideMenu(); } }, 100); }); $(document).on('click', function () { if (!isMouseOverButtonOrMenu() && menu.is(':visible')) { hideMenu(); } }); /* $('#set_chat_scenario').on('click', setScenarioOverride); */ ///////////// OPTIMIZED LISTENERS FOR LEFT SIDE OPTIONS POPUP MENU ////////////////////// $('#options [id]').on('click', async function (event, customData) { const fromSlashCommand = customData?.fromSlashCommand || false; var id = $(this).attr('id'); // Check whether a custom prompt was provided via custom data (for example through a slash command) const additionalPrompt = customData?.additionalPrompt?.trim() || undefined; const buildOrFillAdditionalArgs = (args = {}) => ({ ...args, ...(additionalPrompt !== undefined && { quiet_prompt: additionalPrompt, quietToLoud: true }), }); if (id == 'option_select_chat') { if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) { await displayPastChats(); //this is just to avoid the shadow for past chat view when using /delchat //however, the dialog popup still gets one.. if (!fromSlashCommand) { console.log('displaying shadow'); $('#shadow_select_chat_popup').css('display', 'block'); $('#shadow_select_chat_popup').css('opacity', 0.0); $('#shadow_select_chat_popup').transition({ opacity: 1.0, duration: animation_duration, easing: animation_easing, }); } } } else if (id == 'option_start_new_chat') { if ((selected_group || this_chid !== undefined) && !is_send_press) { callPopup(` <h3>Start new chat?</h3><br> <label for="del_chat_checkbox" class="checkbox_label justifyCenter" title="If necessary, you can later restore this chat file from the /backups folder"> <input type="checkbox" id="del_chat_checkbox" /> <small>Also delete the current chat file</small> </label><br> `, 'new_chat', ''); } } else if (id == 'option_regenerate') { closeMessageEditor(); if (is_send_press == false) { //hideSwipeButtons(); if (selected_group) { regenerateGroup(); } else { is_send_press = true; Generate('regenerate', buildOrFillAdditionalArgs()); } } } else if (id == 'option_impersonate') { if (is_send_press == false || fromSlashCommand) { is_send_press = true; Generate('impersonate', buildOrFillAdditionalArgs()); } } else if (id == 'option_continue') { if (is_send_press == false || fromSlashCommand) { is_send_press = true; Generate('continue', buildOrFillAdditionalArgs()); } } else if (id == 'option_delete_mes') { setTimeout(() => openMessageDelete(fromSlashCommand), animation_duration); } else if (id == 'option_close_chat') { if (is_send_press == false) { await clearChat(); chat.length = 0; resetSelectedGroup(); setCharacterId(undefined); setCharacterName(''); setActiveCharacter(null); setActiveGroup(null); this_edit_mes_id = undefined; chat_metadata = {}; selected_button = 'characters'; $('#rm_button_selected_ch').children('h2').text(''); select_rm_characters(); sendSystemMessage(system_message_types.WELCOME); eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); await getClientVersion(); } else { toastr.info('Please stop the message generation first.'); } } else if (id === 'option_settings') { //var checkBox = document.getElementById("waifuMode"); var topBar = document.getElementById('top-bar'); var topSettingsHolder = document.getElementById('top-settings-holder'); var divchat = document.getElementById('chat'); //if (checkBox.checked) { if (topBar.style.display === 'none') { topBar.style.display = ''; // or "inline-block" if that's the original display value topSettingsHolder.style.display = ''; // or "inline-block" if that's the original display value divchat.style.borderRadius = ''; divchat.style.backgroundColor = ''; } else { divchat.style.borderRadius = '10px'; // Adjust the value to control the roundness of the corners divchat.style.backgroundColor = ''; // Set the background color to your preference topBar.style.display = 'none'; topSettingsHolder.style.display = 'none'; } //} } hideMenu(); }); $('#newChatFromManageScreenButton').on('click', function () { setTimeout(() => { $('#option_start_new_chat').trigger('click'); }, 1); setTimeout(() => { $('#dialogue_popup_ok').trigger('click'); }, 1); $('#select_chat_cross').trigger('click'); }); ////////////////////////////////////////////////////////////////////////////////////////////// //functionality for the cancel delete messages button, reverts to normal display of input form $('#dialogue_del_mes_cancel').click(function () { $('#dialogue_del_mes').css('display', 'none'); $('#send_form').css('display', css_send_form_display); $('.del_checkbox').each(function () { $(this).css('display', 'none'); $(this).parent().children('.for_checkbox').css('display', 'block'); $(this).parent().removeClass('selected'); $(this).prop('checked', false); }); showSwipeButtons(); this_del_mes = -1; is_delete_mode = false; }); //confirms message deletion with the "ok" button $('#dialogue_del_mes_ok').click(async function () { $('#dialogue_del_mes').css('display', 'none'); $('#send_form').css('display', css_send_form_display); $('.del_checkbox').each(function () { $(this).css('display', 'none'); $(this).parent().children('.for_checkbox').css('display', 'block'); $(this).parent().removeClass('selected'); $(this).prop('checked', false); }); if (this_del_mes >= 0) { $(`.mes[mesid="${this_del_mes}"]`).nextAll('div').remove(); $(`.mes[mesid="${this_del_mes}"]`).remove(); chat.length = this_del_mes; await saveChatConditional(); chatElement.scrollTop(chatElement[0].scrollHeight); eventSource.emit(event_types.MESSAGE_DELETED, chat.length); $('#chat .mes').removeClass('last_mes'); $('#chat .mes').last().addClass('last_mes'); } else { console.log('this_del_mes is not >= 0, not deleting'); } showSwipeButtons(); this_del_mes = -1; is_delete_mode = false; }); $('#settings_preset').change(function () { if ($('#settings_preset').find(':selected').val() != 'gui') { preset_settings = $('#settings_preset').find(':selected').text(); const preset = koboldai_settings[koboldai_setting_names[preset_settings]]; loadKoboldSettings(preset); setGenerationParamsFromPreset(preset); $('#kobold_api-settings').find('input').prop('disabled', false); $('#kobold_api-settings').css('opacity', 1.0); $('#kobold_order') .css('opacity', 1) .sortable('enable'); } else { //$('.button').disableSelection(); preset_settings = 'gui'; $('#kobold_api-settings').find('input').prop('disabled', true); $('#kobold_api-settings').css('opacity', 0.5); $('#kobold_order') .css('opacity', 0.5) .sortable('disable'); } saveSettingsDebounced(); }); $('#settings_preset_novel').change(function () { nai_settings.preset_settings_novel = $('#settings_preset_novel') .find(':selected') .text(); const preset = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]]; loadNovelPreset(preset); amount_gen = Number($('#amount_gen').val()); max_context = Number($('#max_context').val()); saveSettingsDebounced(); }); $('#main_api').change(function () { cancelStatusCheck(); changeMainAPI(); saveSettingsDebounced(); }); ////////////////// OPTIMIZED RANGE SLIDER LISTENERS//////////////// var sliderLocked = true; var sliderTimer; $('input[type=\'range\']').on('touchstart', function () { // Unlock the slider after 300ms setTimeout(function () { sliderLocked = false; $(this).css('background-color', 'var(--SmartThemeQuoteColor)'); }.bind(this), 300); }); $('input[type=\'range\']').on('touchend', function () { clearTimeout(sliderTimer); $(this).css('background-color', ''); sliderLocked = true; }); $('input[type=\'range\']').on('touchmove', function (event) { if (sliderLocked) { event.preventDefault(); } }); const sliders = [ { sliderId: '#amount_gen', counterId: '#amount_gen_counter', format: (val) => `${val}`, setValue: (val) => { amount_gen = Number(val); }, }, { sliderId: '#max_context', counterId: '#max_context_counter', format: (val) => `${val}`, setValue: (val) => { max_context = Number(val); }, }, ]; sliders.forEach(slider => { $(document).on('input', slider.sliderId, function () { const value = $(this).val(); const formattedValue = slider.format(value); slider.setValue(value); $(slider.counterId).val(formattedValue); saveSettingsDebounced(); }); }); ////////////////////////////////////////////////////////////// $('#select_chat_cross').click(function () { $('#shadow_select_chat_popup').transition({ opacity: 0, duration: animation_duration, easing: animation_easing, }); setTimeout(function () { $('#shadow_select_chat_popup').css('display', 'none'); }, animation_duration); //$("#shadow_select_chat_popup").css("display", "none"); $('#load_select_chat_div').css('display', 'block'); }); if (navigator.clipboard === undefined) { // No clipboard support $('.mes_copy').remove(); } else { $(document).on('pointerup', '.mes_copy', function () { if (this_chid !== undefined || selected_group) { try { const messageId = $(this).closest('.mes').attr('mesid'); const text = chat[messageId]['mes']; navigator.clipboard.writeText(text); toastr.info('Copied!', '', { timeOut: 2000 }); } catch (err) { console.error('Failed to copy: ', err); } } }); } $(document).on('pointerup', '.mes_prompt', function () { let mesIdForItemization = $(this).closest('.mes').attr('mesId'); console.log(`looking for mesID: ${mesIdForItemization}`); if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) { promptItemize(itemizedPrompts, mesIdForItemization); } }); $(document).on('pointerup', '#copyPromptToClipboard', function () { let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; let rawPromptValues = rawPrompt; if (Array.isArray(rawPrompt)) { rawPromptValues = rawPrompt.map(x => x.content).join('\n'); } navigator.clipboard.writeText(rawPromptValues); toastr.info('Copied!', '', { timeOut: 2000 }); }); $(document).on('pointerup', '#showRawPrompt', function () { //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); console.log(PromptArrayItemForRawPromptDisplay); console.log(itemizedPrompts); console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; let rawPromptValues = rawPrompt; if (Array.isArray(rawPrompt)) { rawPromptValues = rawPrompt.map(x => x.content).join('\n'); } //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>'); $('#rawPromptWrapper').text(rawPromptValues); rawPromptPopper.update(); $('#rawPromptPopup').toggle(); }); //******************** //***Message Editor*** $(document).on('click', '.mes_edit', async function () { if (this_chid !== undefined || selected_group) { // Previously system messages we're allowed to be edited /*const message = $(this).closest(".mes"); if (message.data("isSystem")) { return; }*/ let chatScrollPosition = $('#chat').scrollTop(); if (this_edit_mes_id !== undefined) { let mes_edited = $(`#chat [mesid="${this_edit_mes_id}"]`).find('.mes_edit_done'); if (Number(edit_mes_id) == chat.length - 1) { //if the generating swipe (...) let run_edit = true; if (chat[edit_mes_id]['swipe_id'] !== undefined) { if (chat[edit_mes_id]['swipes'].length === chat[edit_mes_id]['swipe_id']) { run_edit = false; } } if (run_edit) { hideSwipeButtons(); } } await messageEditDone(mes_edited); } $(this).closest('.mes_block').find('.mes_text').empty(); $(this).closest('.mes_block').find('.mes_buttons').css('display', 'none'); $(this).closest('.mes_block').find('.mes_edit_buttons').css('display', 'inline-flex'); var edit_mes_id = $(this).closest('.mes').attr('mesid'); this_edit_mes_id = edit_mes_id; var text = chat[edit_mes_id]['mes']; if (chat[edit_mes_id]['is_user']) { this_edit_mes_chname = name1; } else if (chat[edit_mes_id]['force_avatar']) { this_edit_mes_chname = chat[edit_mes_id]['name']; } else { this_edit_mes_chname = name2; } if (power_user.trim_spaces) { text = text.trim(); } $(this) .closest('.mes_block') .find('.mes_text') .append( '<textarea id=\'curEditTextarea\' class=\'edit_textarea\' style=\'max-width:auto;\'></textarea>', ); $('#curEditTextarea').val(text); let edit_textarea = $(this) .closest('.mes_block') .find('.edit_textarea'); edit_textarea.height(0); edit_textarea.height(edit_textarea[0].scrollHeight); edit_textarea.focus(); edit_textarea[0].setSelectionRange( //this sets the cursor at the end of the text String(edit_textarea.val()).length, String(edit_textarea.val()).length, ); if (Number(this_edit_mes_id) === chat.length - 1) { $('#chat').scrollTop(chatScrollPosition); } updateEditArrowClasses(); } }); $(document).on('input', '#curEditTextarea', function () { if (power_user.auto_save_msg_edits === true) { messageEditAuto($(this)); } }); $(document).on('click', '.extraMesButtonsHint', function (e) { const elmnt = e.target; $(elmnt).transition({ opacity: 0, duration: animation_duration, easing: 'ease-in-out', }); setTimeout(function () { $(elmnt).hide(); $(elmnt).siblings('.extraMesButtons').css('opcacity', '0'); $(elmnt).siblings('.extraMesButtons').css('display', 'flex'); $(elmnt).siblings('.extraMesButtons').transition({ opacity: 1, duration: animation_duration, easing: 'ease-in-out', }); }, animation_duration); }); $(document).on('click', function (e) { // Expanded options don't need to be closed if (power_user.expand_message_actions) { return; } // Check if the click was outside the relevant elements if (!$(e.target).closest('.extraMesButtons, .extraMesButtonsHint').length) { // Transition out the .extraMesButtons first $('.extraMesButtons:visible').transition({ opacity: 0, duration: animation_duration, easing: 'ease-in-out', complete: function () { $(this).hide(); // Hide the .extraMesButtons after the transition // Transition the .extraMesButtonsHint back in $('.extraMesButtonsHint:not(:visible)').show().transition({ opacity: .3, duration: animation_duration, easing: 'ease-in-out', complete: function () { $(this).css('opacity', ''); }, }); }, }); } }); $(document).on('click', '.mes_edit_cancel', function () { let text = chat[this_edit_mes_id]['mes']; $(this).closest('.mes_block').find('.mes_text').empty(); $(this).closest('.mes_edit_buttons').css('display', 'none'); $(this).closest('.mes_block').find('.mes_buttons').css('display', ''); $(this) .closest('.mes_block') .find('.mes_text') .append(messageFormatting( text, this_edit_mes_chname, chat[this_edit_mes_id].is_system, chat[this_edit_mes_id].is_user, this_edit_mes_id, )); appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes')); addCopyToCodeBlocks($(this).closest('.mes')); this_edit_mes_id = undefined; }); $(document).on('click', '.mes_edit_up', async function () { if (is_send_press || this_edit_mes_id <= 0) { return; } hideSwipeButtons(); const targetId = Number(this_edit_mes_id) - 1; const target = $(`#chat .mes[mesid="${targetId}"]`); const root = $(this).closest('.mes'); if (root.length === 0 || target.length === 0) { return; } root.insertBefore(target); target.attr('mesid', this_edit_mes_id); root.attr('mesid', targetId); const temp = chat[targetId]; chat[targetId] = chat[this_edit_mes_id]; chat[this_edit_mes_id] = temp; this_edit_mes_id = targetId; updateViewMessageIds(); await saveChatConditional(); showSwipeButtons(); }); $(document).on('click', '.mes_edit_down', async function () { if (is_send_press || this_edit_mes_id >= chat.length - 1) { return; } hideSwipeButtons(); const targetId = Number(this_edit_mes_id) + 1; const target = $(`#chat .mes[mesid="${targetId}"]`); const root = $(this).closest('.mes'); if (root.length === 0 || target.length === 0) { return; } root.insertAfter(target); target.attr('mesid', this_edit_mes_id); root.attr('mesid', targetId); const temp = chat[targetId]; chat[targetId] = chat[this_edit_mes_id]; chat[this_edit_mes_id] = temp; this_edit_mes_id = targetId; updateViewMessageIds(); await saveChatConditional(); showSwipeButtons(); }); $(document).on('click', '.mes_edit_copy', async function () { const confirmation = await callPopup('Create a copy of this message?', 'confirm'); if (!confirmation) { return; } hideSwipeButtons(); const oldScroll = chatElement[0].scrollTop; const clone = structuredClone(chat[this_edit_mes_id]); clone.send_date = Date.now(); clone.mes = $(this).closest('.mes').find('.edit_textarea').val(); if (power_user.trim_spaces) { clone.mes = clone.mes.trim(); } chat.splice(Number(this_edit_mes_id) + 1, 0, clone); addOneMessage(clone, { insertAfter: this_edit_mes_id }); updateViewMessageIds(); await saveChatConditional(); chatElement[0].scrollTop = oldScroll; showSwipeButtons(); }); $(document).on('click', '.mes_edit_delete', async function (event, customData) { const fromSlashCommand = customData?.fromSlashCommand || false; const swipeExists = (!Array.isArray(chat[this_edit_mes_id].swipes) || chat[this_edit_mes_id].swipes.length <= 1 || chat[this_edit_mes_id].is_user || parseInt(this_edit_mes_id) !== chat.length - 1); if (power_user.confirm_message_delete && fromSlashCommand !== true) { const confirmation = swipeExists ? await callPopup('Are you sure you want to delete this message?', 'confirm') : await callPopup('<h3>Delete this...</h3> <select id=\'del_type\'><option value=\'swipe\'>Swipe</option><option value=\'message\'>Message</option></select>', 'confirm'); if (!confirmation) { return; } } const mes = $(this).closest('.mes'); if (!mes) { return; } if ($('#del_type').val() === 'swipe') { const swipe_id = chat[this_edit_mes_id]['swipe_id']; chat[this_edit_mes_id]['swipes'].splice(swipe_id, 1); if (swipe_id > 0) { $('.swipe_left:last').click(); } else { $('.swipe_right:last').click(); } } else { chat.splice(this_edit_mes_id, 1); mes.remove(); } let startFromZero = Number(this_edit_mes_id) === 0; this_edit_mes_id = undefined; updateViewMessageIds(startFromZero); saveChatDebounced(); hideSwipeButtons(); showSwipeButtons(); await eventSource.emit(event_types.MESSAGE_DELETED, chat.length); }); $(document).on('click', '.mes_edit_done', async function () { await messageEditDone($(this)); }); //Select chat //**************************CHARACTER IMPORT EXPORT*************************// $('#character_import_button').click(function () { $('#character_import_file').click(); }); $('#character_import_file').on('change', async function (e) { $('#rm_info_avatar').html(''); if (!(e.target instanceof HTMLInputElement)) { return; } if (!e.target.files.length) { return; } for (const file of e.target.files) { await importCharacter(file); } }); $('#export_button').on('click', function (e) { $('#export_format_popup').toggle(); exportPopper.update(); }); $(document).on('click', '.export_format', async function () { const format = $(this).data('format'); if (!format) { return; } // Save before exporting await createOrEditCharacter(); const body = { format, avatar_url: characters[this_chid].avatar }; const response = await fetch('/api/characters/export', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify(body), }); if (response.ok) { const filename = characters[this_chid].avatar.replace('.png', `.${format}`); const blob = await response.blob(); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.setAttribute('download', filename); document.body.appendChild(a); a.click(); URL.revokeObjectURL(a.href); document.body.removeChild(a); } $('#export_format_popup').hide(); }); //**************************CHAT IMPORT EXPORT*************************// $('#chat_import_button').click(function () { $('#chat_import_file').click(); }); $('#chat_import_file').on('change', async function (e) { var file = e.target.files[0]; if (!file) { return; } var ext = file.name.match(/\.(\w+)$/); if ( !ext || (ext[1].toLowerCase() != 'json' && ext[1].toLowerCase() != 'jsonl') ) { return; } if (selected_group && file.name.endsWith('.json')) { toastr.warning('Only SillyTavern\'s own format is supported for group chat imports. Sorry!'); return; } var format = ext[1].toLowerCase(); $('#chat_import_file_type').val(format); var formData = new FormData($('#form_import_chat').get(0)); formData.append('user_name', name1); $('#select_chat_div').html(''); $('#load_select_chat_div').css('display', 'block'); if (selected_group) { await importGroupChat(formData); } else { await importCharacterChat(formData); } }); $('#rm_button_group_chats').click(function () { selected_button = 'group_chats'; select_group_chats(); }); $('#rm_button_back_from_group').click(function () { selected_button = 'characters'; select_rm_characters(); }); $('#dupe_button').click(async function () { await DupeChar(); }); $(document).on('click', '.select_chat_block, .bookmark_link, .mes_bookmark', async function () { let file_name = $(this).hasClass('mes_bookmark') ? $(this).closest('.mes').attr('bookmark_link') : $(this).attr('file_name').replace('.jsonl', ''); if (!file_name) { return; } try { showLoader(); if (selected_group) { await openGroupChat(selected_group, file_name); } else { await openCharacterChat(file_name); } } finally { hideLoader(); } $('#shadow_select_chat_popup').css('display', 'none'); $('#load_select_chat_div').css('display', 'block'); }); $(document).on('click', '.mes_create_bookmark', async function () { var selected_mes_id = $(this).closest('.mes').attr('mesid'); if (selected_mes_id !== undefined) { createNewBookmark(selected_mes_id); } }); $(document).on('click', '.mes_create_branch', async function () { var selected_mes_id = $(this).closest('.mes').attr('mesid'); if (selected_mes_id !== undefined) { branchChat(Number(selected_mes_id)); } }); $(document).on('click', '.mes_stop', function () { if (streamingProcessor) { streamingProcessor.onStopStreaming(); streamingProcessor = null; } if (abortController) { abortController.abort('Clicked stop button'); hideStopButton(); } eventSource.emit(event_types.GENERATION_STOPPED); }); $(document).on('click', '#form_sheld .stscript_continue', function () { pauseScriptExecution(); }); $(document).on('click', '#form_sheld .stscript_pause', function () { pauseScriptExecution(); }); $(document).on('click', '#form_sheld .stscript_stop', function () { stopScriptExecution(); }); $('.drawer-toggle').on('click', function () { var icon = $(this).find('.drawer-icon'); var drawer = $(this).parent().find('.drawer-content'); if (drawer.hasClass('resizing')) { return; } var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer'); let targetDrawerID = $(this).parent().find('.drawer-content').attr('id'); const pinnedDrawerClicked = drawer.hasClass('pinnedOpen'); if (!drawerWasOpenAlready) { //to open the drawer $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); }); $('.openIcon').toggleClass('closedIcon openIcon'); $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer'); icon.toggleClass('openIcon closedIcon'); drawer.toggleClass('openDrawer closedDrawer'); //console.log(targetDrawerID); if (targetDrawerID === 'right-nav-panel') { $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({ duration: 200, easing: 'swing', start: function () { jQuery(this).css('display', 'flex'); //flex needed to make charlist scroll }, complete: async function () { favsToHotswap(); await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); $('#rm_print_characters_block').trigger('scroll'); }, }); } else { $(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, 'swing', async function () { await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); }); } // Set the height of "autoSetHeight" textareas within the drawer to their scroll height $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(function () { resetScrollHeight($(this)); }); } else if (drawerWasOpenAlready) { //to close manually icon.toggleClass('closedIcon openIcon'); if (pinnedDrawerClicked) { $(drawer).addClass('resizing').slideToggle(200, 'swing', async function () { await delay(50); $(this).removeClass('resizing'); }); } else { $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', async function () { await delay(50); $(this).closest('.drawer-content').removeClass('resizing'); }); } drawer.toggleClass('closedDrawer openDrawer'); } }); $('html').on('touchstart mousedown', function (e) { var clickTarget = $(e.target); if ($('#export_format_popup').is(':visible') && clickTarget.closest('#export_button').length == 0 && clickTarget.closest('#export_format_popup').length == 0) { $('#export_format_popup').hide(); } const forbiddenTargets = [ '#character_cross', '#avatar-and-name-block', '#shadow_popup', '.popup', '#world_popup', '.ui-widget', '.text_pole', '#toast-container', '.select2-results', ]; for (const id of forbiddenTargets) { if (clickTarget.closest(id).length > 0) { return; } } var targetParentHasOpenDrawer = clickTarget.parents('.openDrawer').length; if (clickTarget.hasClass('drawer-icon') == false && !clickTarget.hasClass('openDrawer')) { if ($('.openDrawer').length !== 0) { if (targetParentHasOpenDrawer === 0) { //console.log($('.openDrawer').not('.pinnedOpen').length); $('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, 'swing', function () { $(this).closest('.drawer-content').removeClass('resizing'); }); $('.openIcon').toggleClass('closedIcon openIcon'); $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer'); } } } }); $(document).on('click', '.inline-drawer-toggle', function (e) { if ($(e.target).hasClass('text_pole')) { return; } var icon = $(this).find('.inline-drawer-icon'); icon.toggleClass('down up'); icon.toggleClass('fa-circle-chevron-down fa-circle-chevron-up'); $(this).closest('.inline-drawer').find('.inline-drawer-content').stop().slideToggle(); // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height $(this).closest('.inline-drawer').find('.inline-drawer-content textarea.autoSetHeight').each(function () { resetScrollHeight($(this)); }); }); $(document).on('click', '.inline-drawer-maximize', function () { const icon = $(this).find('.inline-drawer-icon, .floating_panel_maximize'); icon.toggleClass('fa-window-maximize fa-window-restore'); const drawerContent = $(this).closest('.drawer-content'); drawerContent.toggleClass('maximized'); const drawerId = drawerContent.attr('id'); resetMovableStyles(drawerId); }); $(document).on('click', '.mes .avatar', function () { const messageElement = $(this).closest('.mes'); const thumbURL = $(this).children('img').attr('src'); const charsPath = '/characters/'; const targetAvatarImg = thumbURL.substring(thumbURL.lastIndexOf('=') + 1); const charname = targetAvatarImg.replace('.png', ''); const isValidCharacter = characters.some(x => x.avatar === decodeURIComponent(targetAvatarImg)); // Remove existing zoomed avatars for characters that are not the clicked character when moving UI is not enabled if (!power_user.movingUI) { $('.zoomed_avatar').each(function () { const currentForChar = $(this).attr('forChar'); if (currentForChar !== charname && typeof currentForChar !== 'undefined') { console.debug(`Removing zoomed avatar for character: ${currentForChar}`); $(this).remove(); } }); } const avatarSrc = isDataURL(thumbURL) ? thumbURL : charsPath + targetAvatarImg; if ($(`.zoomed_avatar[forChar="${charname}"]`).length) { console.debug('removing container as it already existed'); $(`.zoomed_avatar[forChar="${charname}"]`).fadeOut(animation_duration, () => { $(`.zoomed_avatar[forChar="${charname}"]`).remove(); }); } else { console.debug('making new container from template'); const template = $('#zoomed_avatar_template').html(); const newElement = $(template); newElement.attr('forChar', charname); newElement.attr('id', `zoomFor_${charname}`); newElement.addClass('draggable'); newElement.find('.drag-grabber').attr('id', `zoomFor_${charname}header`); $('body').append(newElement); newElement.fadeIn(animation_duration); const zoomedAvatarImgElement = $(`.zoomed_avatar[forChar="${charname}"] img`); if (messageElement.attr('is_user') == 'true' || (messageElement.attr('is_system') == 'true' && !isValidCharacter)) { //handle user and system avatars zoomedAvatarImgElement.attr('src', thumbURL); zoomedAvatarImgElement.attr('data-izoomify-url', thumbURL); } else if (messageElement.attr('is_user') == 'false') { //handle char avatars zoomedAvatarImgElement.attr('src', avatarSrc); zoomedAvatarImgElement.attr('data-izoomify-url', avatarSrc); } loadMovingUIState(); $(`.zoomed_avatar[forChar="${charname}"]`).css('display', 'flex'); dragElement(newElement); if (power_user.zoomed_avatar_magnification) { $('.zoomed_avatar_container').izoomify(); } $('.zoomed_avatar, .zoomed_avatar .dragClose').on('click touchend', (e) => { if (e.target.closest('.dragClose')) { $(`.zoomed_avatar[forChar="${charname}"]`).fadeOut(animation_duration, () => { $(`.zoomed_avatar[forChar="${charname}"]`).remove(); }); } }); zoomedAvatarImgElement.on('dragstart', (e) => { console.log('saw drag on avatar!'); e.preventDefault(); return false; }); } }); $(document).on('click', '#OpenAllWIEntries', function () { $('#world_popup_entries_list').children().find('.down').click(); }); $(document).on('click', '#CloseAllWIEntries', function () { $('#world_popup_entries_list').children().find('.up').click(); }); $(document).on('click', '.open_alternate_greetings', openAlternateGreetings); /* $('#set_character_world').on('click', openCharacterWorldPopup); */ $(document).keyup(function (e) { if (e.key === 'Escape') { const isEditVisible = $('#curEditTextarea').is(':visible'); if (isEditVisible && power_user.auto_save_msg_edits === false) { closeMessageEditor(); $('#send_textarea').focus(); return; } if (isEditVisible && power_user.auto_save_msg_edits === true) { $(`#chat .mes[mesid="${this_edit_mes_id}"] .mes_edit_done`).click(); $('#send_textarea').focus(); return; } if (!this_edit_mes_id && $('#mes_stop').is(':visible')) { $('#mes_stop').trigger('click'); if (chat.length && Array.isArray(chat[chat.length - 1].swipes) && chat[chat.length - 1].swipe_id == chat[chat.length - 1].swipes.length) { $('.last_mes .swipe_left').trigger('click'); } } } }); $('#char-management-dropdown').on('change', async (e) => { let target = $(e.target.selectedOptions).attr('id'); switch (target) { case 'set_character_world': openCharacterWorldPopup(); break; case 'set_chat_scenario': setScenarioOverride(); break; case 'renameCharButton': renameCharacter(); break; /*case 'dupe_button': DupeChar(); break; case 'export_button': $('#export_format_popup').toggle(); exportPopper.update(); break; */ case 'import_character_info': await importEmbeddedWorldInfo(); saveCharacterDebounced(); break; case 'character_source': { const source = getCharacterSource(this_chid); if (source && isValidUrl(source)) { const url = new URL(source); const confirm = await callPopup(`Open ${url.hostname} in a new tab?`, 'confirm'); if (confirm) { window.open(source, '_blank'); } } else { toastr.info('This character doesn\'t seem to have a source.'); } } break; case 'replace_update': { const confirm = await callPopup('<p><b>Choose a new character card to replace this character with.</b></p><p>All chats, assets and group memberships will be preserved, but local changes to the character data will be lost.</p><p>Proceed?</p>', 'confirm', ''); if (confirm) { async function uploadReplacementCard(e) { const file = e.target.files[0]; if (!file) { return; } try { const cloneFile = new File([file], characters[this_chid].avatar, { type: file.type }); const chatFile = characters[this_chid]['chat']; await processDroppedFiles([cloneFile], true); await openCharacterChat(chatFile); } catch { toastr.error('Failed to replace the character card.', 'Something went wrong'); } } $('#character_replace_file').off('change').on('change', uploadReplacementCard).trigger('click'); } } break; case 'import_tags': { await importTags(characters[this_chid], { forceShow: true }); } break; /*case 'delete_button': popup_type = "del_ch"; callPopup(` <h3>Delete the character?</h3> <b>THIS IS PERMANENT!<br><br> THIS WILL ALSO DELETE ALL<br> OF THE CHARACTER'S CHAT FILES.<br><br></b>` ); break;*/ default: eventSource.emit('charManagementDropdown', target); } $('#char-management-dropdown').prop('selectedIndex', 0); }); $(window).on('beforeunload', () => { cancelTtsPlay(); if (streamingProcessor) { console.log('Page reloaded. Aborting streaming...'); streamingProcessor.onStopStreaming(); } }); var isManualInput = false; var valueBeforeManualInput; $('.range-block-counter input, .neo-range-input').on('click', function () { valueBeforeManualInput = $(this).val(); console.log(valueBeforeManualInput); }) .on('change', function (e) { e.target.focus(); e.target.dispatchEvent(new Event('keyup')); }) .on('keydown', function (e) { const masterSelector = '#' + $(this).data('for'); const masterElement = $(masterSelector); if (e.key === 'Enter') { let manualInput = Number($(this).val()); if (isManualInput) { //disallow manual inputs outside acceptable range if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) { //if value is ok, assign to slider and update handle text and position //newSlider.val(manualInput) //handleSlideEvent.call(newSlider, null, { value: parseFloat(manualInput) }, 'manual'); valueBeforeManualInput = manualInput; $(masterElement).val($(this).val()).trigger('input'); } else { //if value not ok, warn and reset to last known valid value toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`); console.log(valueBeforeManualInput); //newSlider.val(valueBeforeManualInput) $(this).val(valueBeforeManualInput); } } } }) .on('keyup', function () { valueBeforeManualInput = $(this).val(); console.log(valueBeforeManualInput); isManualInput = true; }) //trigger slider changes when user clicks away .on('mouseup blur', function () { const masterSelector = '#' + $(this).data('for'); const masterElement = $(masterSelector); let manualInput = Number($(this).val()); if (isManualInput) { //if value is between correct range for the slider if (manualInput >= Number($(this).attr('min')) && manualInput <= Number($(this).attr('max'))) { valueBeforeManualInput = manualInput; //set the slider value to input value $(masterElement).val($(this).val()).trigger('input'); } else { //if value not ok, warn and reset to last known valid value toastr.warning(`Invalid value. Must be between ${$(this).attr('min')} and ${$(this).attr('max')}`); console.log(valueBeforeManualInput); $(this).val(valueBeforeManualInput); } } isManualInput = false; }); $('.user_stats_button').on('click', function () { userStatsHandler(); }); $(document).on('click', '.external_import_button, #external_import_button', async () => { const html = await renderTemplateAsync('importCharacters'); /** @type {string?} */ const input = await callGenericPopup(html, POPUP_TYPE.INPUT, '', { wider: true, okButton: $('#popup_template').attr('popup-button-import'), rows: 4 }); if (!input) { console.debug('Custom content import cancelled'); return; } // break input into one input per line const inputs = input.split('\n').map(x => x.trim()).filter(x => x.length > 0); for (const url of inputs) { let request; if (isValidUrl(url)) { console.debug('Custom content import started for URL: ', url); request = await fetch('/api/content/importURL', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ url }), }); } else { console.debug('Custom content import started for Char UUID: ', url); request = await fetch('/api/content/importUUID', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ url }), }); } if (!request.ok) { toastr.info(request.statusText, 'Custom content import failed'); console.error('Custom content import failed', request.status, request.statusText); return; } const data = await request.blob(); const customContentType = request.headers.get('X-Custom-Content-Type'); const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, ''); const file = new File([data], fileName, { type: data.type }); switch (customContentType) { case 'character': await processDroppedFiles([file]); break; case 'lorebook': await importWorldInfo(file); break; default: toastr.warning('Unknown content type'); console.error('Unknown content type', customContentType); break; } } }); charDragDropHandler = new DragAndDropHandler('body', async (files, event) => { if (!files.length) { await importFromURL(event.originalEvent.dataTransfer.items, files); } await processDroppedFiles(files); }, { noAnimation: true }); $('#charListGridToggle').on('click', async () => { doCharListDisplaySwitch(); }); $('#hideCharPanelAvatarButton').on('click', () => { $('#avatar-and-name-block').slideToggle(); }); $(document).on('mouseup touchend', '#show_more_messages', () => { showMoreMessages(); }); $(document).on('click', '.open_characters_library', async function () { await getCharacters(); eventSource.emit(event_types.OPEN_CHARACTER_LIBRARY); }); // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts await firstLoadInit(); addDebugFunctions(); eventSource.on(event_types.CHAT_DELETED, async (name) => { await deleteItemizedPrompts(name); }); eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => { await deleteItemizedPrompts(name); }); initCustomSelectedSamplers(); });