diff --git "a/script-1.12.11.js" "b/script-1.12.11.js" new file mode 100644--- /dev/null +++ "b/script-1.12.11.js" @@ -0,0 +1,12767 @@ +import { + showdown, + moment, + Fuse, + DOMPurify, + hljs, + localforage, + Handlebars, + DiffMatchPatch, + SVGInject, + Popper, + initLibraryShims, + slideToggle, + default as libs, +} from './lib.js'; + +import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods } from './scripts/RossAscends-mods.js'; +import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js'; +import { + generateKoboldWithStreaming, + kai_settings, + loadKoboldSettings, + 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'; + +import { + world_info, + getWorldInfoPrompt, + getWorldInfoSettings, + setWorldInfoSettings, + world_names, + importEmbeddedWorldInfo, + checkEmbeddedWorld, + setWorldInfoButtonClass, + importWorldInfo, + wi_anchor_position, + world_info_include_names, +} from './scripts/world-info.js'; + +import { + groups, + selected_group, + saveGroupChat, + getGroups, + generateGroupWrapper, + is_group_generating, + resetSelectedGroup, + select_group_chats, + regenerateGroup, + group_generation_id, + getGroupChat, + renameGroupMember, + createNewGroupChat, + getGroupAvatar, + editGroup, + deleteGroupChat, + renameGroupChat, + importGroupChat, + getGroupBlock, + getGroupCharacterCards, + getGroupDepthPrompts, +} from './scripts/group-chats.js'; + +import { + collapseNewlines, + loadPowerUserSettings, + playMessageSound, + fixMarkdown, + power_user, + persona_description_positions, + loadMovingUIState, + getCustomStoppingStrings, + MAX_CONTEXT_DEFAULT, + MAX_RESPONSE_DEFAULT, + renderStoryString, + sortEntitiesList, + registerDebugFunction, + flushEphemeralStoppingStrings, + context_presets, + resetMovableStyles, + forceCharacterEditorTokenize, + applyPowerUserSettings, +} from './scripts/power-user.js'; + +import { + setOpenAIMessageExamples, + setOpenAIMessages, + setupChatCompletionPromptManager, + prepareOpenAIMessages, + sendOpenAIRequest, + loadOpenAISettings, + oai_settings, + openai_messages_count, + chat_completion_sources, + getChatCompletionModel, + proxies, + loadProxyPresets, + selected_proxy, + initOpenAI, +} from './scripts/openai.js'; + +import { + generateNovelWithStreaming, + getNovelGenerationData, + getKayraMaxContextTokens, + getNovelTier, + loadNovelPreset, + loadNovelSettings, + nai_settings, + adjustNovelInstructionPrompt, + loadNovelSubscriptionData, + parseNovelAILogprobs, +} from './scripts/nai-settings.js'; + +import { + initBookmarks, + showBookmarksButtons, + updateBookmarkDisplay, +} 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, + toggleDrawer, + isElementInViewport, + copyText, +} from './scripts/utils.js'; +import { debounce_timeout } from './scripts/constants.js'; + +import { doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js'; +import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, 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, t } from './scripts/i18n.js'; +import { getFriendlyTokenizerName, getTokenCount, getTokenCountAsync, initTokenizers, saveTokenCache, TOKENIZER_SUPPORTED_KEY } 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 { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js'; +import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js'; +import { getPresetManager, initPresetManager } from './scripts/preset-manager.js'; +import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js'; +import { currentUser, setUserControls } from './scripts/user.js'; +import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js'; +import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; +import { initScrapers } from './scripts/scrapers.js'; +import { 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 { commonEnumProviders, enumIcons } from './scripts/slash-commands/SlashCommandCommonEnumsProvider.js'; +import { initInputMarkdown } from './scripts/input-md-formatting.js'; +import { AbortReason } from './scripts/util/AbortReason.js'; +import { initSystemPrompts } from './scripts/sysprompt.js'; +import { registerExtensionSlashCommands as initExtensionSlashCommands } from './scripts/extensions-slashcommands.js'; +import { ToolManager } from './scripts/tool-calling.js'; +import { addShowdownPatch } from './scripts/util/showdown-patch.js'; +import { applyBrowserFixes } from './scripts/browser-fixes.js'; +import { initServerHistory } from './scripts/server-history.js'; +import { initSettingsSearch } from './scripts/setting-search.js'; +import { initBulkEdit } from './scripts/bulk-edit.js'; +import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; +import { getContext } from './scripts/st-context.js'; + +// API OBJECT FOR EXTERNAL WIRING +globalThis.SillyTavern = { + libs, + getContext, +}; + +//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(); + +// +// DAVE MODS: +// Mod #1 +// set globals #1 +// + +let dave_ACTIVE=1; + +// if "1" ; "dave systems" are active. + +let dave_message_watch=""; +let dave_edit_flag=0; +let dave_edit_phrase=""; +let dave_holdtext=""; +let dave_auto_continue=0; +let dave_edit_start=""; + +let dave_signal=""; +let dave_last_signal=0; +let dave_stop_trigger=0; +let dave_skip_dave_edit=0; + +let dave_generate_data={}; + +// +// END DAVE MOD +// + + +// 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', (node, data, config) => { + if (!config['MESSAGE_SANITIZE']) { + return; + } + + /* Retain the classes on UI elements of messages that interact with the main UI */ + const permittedNodeTypes = ['BUTTON', 'DIV']; + if (config['MESSAGE_ALLOW_SYSTEM_UI'] && node.classList.contains('menu_button') && permittedNodeTypes.includes(node.nodeName)) { + 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
in unknown elements + if (node instanceof HTMLUnknownElement) { + node.innerHTML = node.innerHTML.replaceAll('\n', '
'); + } + + 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( + t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`, + t`External media has been blocked`, + { + timeOut: 0, + preventDuplicates: true, + onclick: () => toastr.clear(warningToast), + }, + ); + + localStorage.setItem(warningShownKey, 'true'); + } + } +}); + +// 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', + MESSAGE_FILE_EMBEDDED: 'message_file_embedded', + IMPERSONATE_READY: 'impersonate_ready', + CHAT_CHANGED: 'chat_id_changed', + GENERATION_AFTER_COMMANDS: 'GENERATION_AFTER_COMMANDS', + GENERATION_STARTED: 'generation_started', + GENERATION_STOPPED: 'generation_stopped', + GENERATION_ENDED: 'generation_ended', + EXTENSIONS_FIRST_LOAD: 'extensions_first_load', + EXTENSION_SETTINGS_LOADED: 'extension_settings_loaded', + 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', + OAI_PRESET_EXPORT_READY: 'oai_preset_export_ready', + OAI_PRESET_IMPORT_READY: 'oai_preset_import_ready', + 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', + GENERATE_AFTER_DATA: 'generate_after_data', + 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', + /** @deprecated The event is aliased to STREAM_TOKEN_RECEIVED. */ + SMOOTH_STREAM_TOKEN_RECEIVED: 'stream_token_received', + STREAM_TOKEN_RECEIVED: 'stream_token_received', + FILE_ATTACHMENT_DELETED: 'file_attachment_deleted', + WORLDINFO_FORCE_ACTIVATE: 'worldinfo_force_activate', + OPEN_CHARACTER_LIBRARY: 'open_character_library', + ONLINE_STATUS_CHANGED: 'online_status_changed', + IMAGE_SWIPED: 'image_swiped', + CONNECTION_PROFILE_LOADED: 'connection_profile_loaded', + TOOL_CALLS_PERFORMED: 'tool_calls_performed', + TOOL_CALLS_RENDERED: 'tool_calls_rendered', +}; + +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 +/** @type {import('showdown').Converter} */ +let converter; + +// array for prompt token calculations +console.debug('initializing Prompt Itemization Array on Startup'); +const promptStorage = localforage.createInstance({ name: 'SillyTavern_Prompts' }); +export let itemizedPrompts = []; + +export const systemUserName = 'SillyTavern System'; +export const neutralCharacterName = 'Assistant'; +let default_user_name = 'User'; +export let name1 = default_user_name; +export let name2 = systemUserName; +export let chat = []; +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 const default_user_avatar = 'img/user-default.png'; +export let CLIENT_VERSION = 'SillyTavern:UNKNOWN:Cohee#1207'; // For Horde header +let optionsPopper = Popper.createPopper(document.getElementById('options_button'), document.getElementById('options'), { + placement: 'top-start', +}); +let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), { + placement: 'left', +}); +let isExportPopupOpen = false; + +// Saved here for performance reasons +const messageTemplate = $('#message_template .mes'); +const chatElement = $('#chat'); + +let dialogueResolve = null; +let dialogueCloseStop = false; +export let chat_metadata = {}; +/** @type {StreamingProcessor} */ +export let streamingProcessor = null; +let crop_data = undefined; +let is_delete_mode = false; +let fav_ch_checked = false; +let scrollLock = false; +export let abortStatusCheck = new AbortController(); +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((loopCounter = 0) => saveSettings(loopCounter), DEFAULT_SAVE_EDIT_TIMEOUT); +export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), DEFAULT_SAVE_EDIT_TIMEOUT); + +/** + * Prints the character list in a debounced fashion without blocking, with a delay of 100 milliseconds. + * Use this function instead of a direct `printCharacters()` whenever the reprinting of the character list is not the primary focus. + * + * The printing will also always reprint all filter options of the global list, to keep them up to date. + */ +export const printCharactersDebounced = debounce(() => { printCharacters(false); }, DEFAULT_PRINT_TIMEOUT); + +/** + * @enum {string} System message types + */ +export const system_message_types = { + HELP: 'help', + WELCOME: 'welcome', + GROUP: 'group', + EMPTY: 'empty', + GENERIC: 'generic', + NARRATOR: 'narrator', + COMMENT: 'comment', + SLASH_COMMANDS: 'slash_commands', + FORMATTING: 'formatting', + HOTKEYS: 'hotkeys', + MACROS: 'macros', + WELCOME_PROMPT: 'welcome_prompt', + ASSISTANT_NOTE: 'assistant_note', +}; + +/** + * @enum {number} Extension prompt types + */ +export const extension_prompt_types = { + NONE: -1, + IN_PROMPT: 0, + IN_CHAT: 1, + BEFORE_PROMPT: 2, +}; + +/** + * @enum {number} Extension prompt roles + */ +export const extension_prompt_roles = { + SYSTEM: 0, + USER: 1, + ASSISTANT: 2, +}; + +export const MAX_INJECTION_DEPTH = 1000; + +const SAFETY_CHAT = [ + { + name: systemUserName, + force_avatar: system_avatar, + is_system: true, + is_user: false, + create_date: 0, + mes: 'You deleted a character/chat and arrived back here for safety reasons! Pick another character!', + }, +]; + +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, + uses_system_ui: 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. Hint: 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', + }, + welcome_prompt: { + name: systemUserName, + force_avatar: system_avatar, + is_user: false, + is_system: true, + mes: await renderTemplateAsync('welcomePrompt'), + extra: { + isSmallSys: true, + }, + }, + assistant_note: { + name: systemUserName, + force_avatar: system_avatar, + is_user: false, + is_system: true, + mes: await renderTemplateAsync('assistantNote'), + extra: { + isSmallSys: true, + }, + }, + }; +} + +// 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() { + converter = new showdown.Converter({ + emoji: true, + literalMidWordUnderscores: true, + parseImgDimensions: true, + tables: true, + underline: true, + simpleLineBreaks: true, + strikethrough: true, + disableForced4SpacesIndentedSublists: true, + extensions: [markdownUnderscoreExt()], + }); + + // Inject the dinkus extension after creating the converter + // Maybe move this into power_user init? + converter.addExtension(markdownExclusionExt(), 'exclusion'); + + return converter; +} + +export function getCurrentChatId() { + if (selected_group) { + return groups.find(x => x.id == selected_group)?.chat_id; + } + else if (this_chid !== undefined) { + return characters[this_chid]?.chat; + } +} + +export const talkativeness_default = 0.5; +export const depth_prompt_depth_default = 4; +export const depth_prompt_role_default = 'system'; +const per_page_default = 50; + +var is_advanced_char_open = false; + +/** + * The type of the right menu + * @typedef {'characters' | 'character_edit' | 'create' | 'group_edit' | 'group_create' | '' } MenuType + */ + +/** + * The type of the right menu that is currently open + * @type {MenuType} + */ +export let menu_type = ''; + +export let selected_button = ''; //which button pressed + +//create pole save +export let create_save = { + name: '', + description: '', + creator_notes: '', + post_history_instructions: '', + character_version: '', + system_prompt: '', + tags: '', + creator: '', + personality: '', + first_message: '', + 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 +var this_edit_mes_chname = ''; +var this_edit_mes_id; + +//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; +export let extension_prompts = {}; + +export let main_api;// = "kobold"; +//novel settings +export let novelai_settings; +export let novelai_setting_names; +/** @type {AbortController} */ +let abortController; + +//css +var css_send_form_display = $('
').css('display'); + +var kobold_horde_model = ''; + +export let token; + +var PromptArrayItemForRawPromptDisplay; +var priorPromptArrayItemForRawPromptDisplay; + +/** 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, + }; +} + +export function getSlideToggleOptions() { + return { + miliseconds: animation_duration * 1.5, + transitionFunction: animation_duration > 0 ? 'ease-in-out' : 'step-start', + }; +} + +$.ajaxPrefilter((options, originalOptions, xhr) => { + xhr.setRequestHeader('X-CSRF-Token', token); +}); + +/** + * Pings the STserver to check if it is reachable. + * @returns {Promise} True if the server is reachable, false otherwise. + */ +export async function pingServer() { + try { + const result = await fetch('api/ping', { + method: '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(t`Couldn't get CSRF token. Please refresh the page.`, t`Error`, { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true }); + throw new Error('Initialization failed'); + } + + initLibraryShims(); + addShowdownPatch(showdown); + reloadMarkdownProcessor(); + applyBrowserFixes(); + await getClientVersion(); + await readSecretState(); + await initLocales(); + initDefaultSlashCommands(); + initTextGenModels(); + initOpenAI(); + initSystemPrompts(); + initExtensions(); + initExtensionSlashCommands(); + ToolManager.initToolSlashCommands(); + await initPresetManager(); + await getSystemMessages(); + sendSystemMessage(system_message_types.WELCOME); + sendSystemMessage(system_message_types.WELCOME_PROMPT); + await getSettings(); + initKeyboard(); + initDynamicStyles(); + initTags(); + initBookmarks(); + initMacros(); + await getUserAvatars(true, user_avatar); + await getCharacters(); + await getBackgrounds(); + await initTokenizers(); + initBackgrounds(); + initAuthorsNote(); + initPersonas(); + initRossMods(); + initStats(); + initCfg(); + initLogprobs(); + initInputMarkdown(); + initServerHistory(); + initSettingsSearch(); + initBulkEdit(); + await initScrapers(); + doDailyExtensionUpdatesCheck(); + await hideLoader(); + await fixViewport(); + await eventSource.emit(event_types.APP_READY); +} + +async function fixViewport() { + document.body.style.position = 'absolute'; + await delay(1); + document.body.style.position = ''; +} + +function cancelStatusCheck(reason = 'Manually cancelled status check') { + abortStatusCheck?.abort(new AbortReason(reason)); + abortStatusCheck = new AbortController(); + setOnlineStatus('no_connection'); +} + +export function displayOnlineStatus() { + if (online_status == 'no_connection') { + $('.online_status_indicator').removeClass('success'); + $('.online_status_text').text($('#API-status-top').attr('no_connection_text')); + } else { + $('.online_status_indicator').addClass('success'); + $('.online_status_text').text(online_status); + } +} + +/** + * Sets the duration of JS animations. + * @param {number} ms Duration in milliseconds. Resets to default if null. + */ +export function setAnimationDuration(ms = null) { + animation_duration = ms ?? ANIMATION_DURATION_DEFAULT; + // Set CSS variable to document + document.documentElement.style.setProperty('--animation-duration', `${animation_duration}ms`); +} + +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(); + setOnlineStatus(hordeStatus ? 'Connected' : 'no_connection'); + } + catch { + setOnlineStatus('no_connection'); + } + + return resultCheckStatus(); +} + +async function getStatusKobold() { + let endpoint = api_server; + + if (!endpoint) { + console.warn('No endpoint for status check'); + setOnlineStatus('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(); + + setOnlineStatus(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, t`API Error`, { timeOut: 5000, preventDuplicates: true }); + } + } catch (err) { + console.error('Error getting status', err); + setOnlineStatus('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'); + setOnlineStatus('no_connection'); + return resultCheckStatus(); + } + + if ([textgen_types.GENERIC, textgen_types.OOBA].includes(textgen_settings.type) && textgen_settings.bypass_status_check) { + setOnlineStatus('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, + }), + signal: abortStatusCheck.signal, + }); + + const data = await response.json(); + + if (textgen_settings.type === textgen_types.MANCER) { + loadMancerModels(data?.data); + setOnlineStatus(textgen_settings.mancer_model); + } else if (textgen_settings.type === textgen_types.TOGETHERAI) { + loadTogetherAIModels(data?.data); + setOnlineStatus(textgen_settings.togetherai_model); + } else if (textgen_settings.type === textgen_types.OLLAMA) { + loadOllamaModels(data?.data); + setOnlineStatus(textgen_settings.ollama_model || 'Connected'); + } else if (textgen_settings.type === textgen_types.INFERMATICAI) { + loadInfermaticAIModels(data?.data); + setOnlineStatus(textgen_settings.infermaticai_model); + } else if (textgen_settings.type === textgen_types.DREAMGEN) { + loadDreamGenModels(data?.data); + setOnlineStatus(textgen_settings.dreamgen_model); + } else if (textgen_settings.type === textgen_types.OPENROUTER) { + loadOpenRouterModels(data?.data); + setOnlineStatus(textgen_settings.openrouter_model); + } else if (textgen_settings.type === textgen_types.VLLM) { + loadVllmModels(data?.data); + setOnlineStatus(textgen_settings.vllm_model); + } else if (textgen_settings.type === textgen_types.APHRODITE) { + loadAphroditeModels(data?.data); + setOnlineStatus(textgen_settings.aphrodite_model); + } else if (textgen_settings.type === textgen_types.FEATHERLESS) { + loadFeatherlessModels(data?.data); + setOnlineStatus(textgen_settings.featherless_model); + } else if (textgen_settings.type === textgen_types.TABBY) { + loadTabbyModels(data?.data); + setOnlineStatus(textgen_settings.tabby_model || data?.result); + } else if (textgen_settings.type === textgen_types.GENERIC) { + loadGenericModels(data?.data); + setOnlineStatus(textgen_settings.generic_model || data?.result || 'Connected'); + } else { + setOnlineStatus(data?.result); + } + + if (!online_status) { + setOnlineStatus('no_connection'); + } + + // Determine instruct mode preset + autoSelectInstructPreset(online_status); + + const supportsTokenization = response.headers.get('x-supports-tokenization') === 'true'; + supportsTokenization ? sessionStorage.setItem(TOKENIZER_SUPPORTED_KEY, 'true') : sessionStorage.removeItem(TOKENIZER_SUPPORTED_KEY); + + const wantsInstructDerivation = (power_user.instruct.enabled && power_user.instruct.derived); + const wantsContextDerivation = power_user.context_derived; + const wantsContextSize = power_user.context_size_derived; + const supportsChatTemplate = [textgen_types.KOBOLDCPP, textgen_types.LLAMACPP].includes(textgen_settings.type); + if (supportsChatTemplate && (wantsInstructDerivation || wantsContextDerivation || wantsContextSize)) { + const response = await fetch('/api/backends/text-completions/props', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + api_server: endpoint, + api_type: textgen_settings.type, + }), + }); + + if (response.ok) { + const data = await response.json(); + if (data) { + const { chat_template, chat_template_hash } = data; + if (wantsContextSize && 'default_generation_settings' in data) { + const backend_max_context = data['default_generation_settings']['n_ctx']; + const old_value = max_context; + if (max_context !== backend_max_context) { + setGenerationParamsFromPreset({ max_length: backend_max_context }); + } + if (old_value !== max_context) { + console.log(`Auto-switched max context from ${old_value} to ${max_context}`); + toastr.info(`${old_value} ⇒ ${max_context}`, 'Context Size Changed'); + } + } + console.log(`We have chat template ${chat_template.split('\n')[0]}...`); + const templates = await deriveTemplatesFromChatTemplate(chat_template, chat_template_hash); + if (templates) { + const { context, instruct } = templates; + if (wantsContextDerivation) { + selectContextPreset(context, { isAuto: true }); + } + if (wantsInstructDerivation) { + selectInstructPreset(instruct, { isAuto: true }); + } + } + } + } + } + + // 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, t`API Error`, { timeOut: 5000, preventDuplicates: true }); + } + } catch (err) { + if (err instanceof AbortReason) { + console.info('Status check aborted.', err.reason); + } else { + console.error('Error getting status', err); + + } + setOnlineStatus('no_connection'); + } + + return resultCheckStatus(); +} + +async function getStatusNovel() { + try { + const result = await loadNovelSubscriptionData(); + + if (!result) { + throw new Error('Could not load subscription data'); + } + + setOnlineStatus(getNovelTier()); + } catch { + setOnlineStatus('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(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`); + return; + } + + if (selected_group && is_group_generating) { + return; + } + + if (selected_group || 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 = ` +
+ +

${texts[roll]}

+

There are no items to display.

+
`; + return $(emptyBlock); +} + +/** + * @param {number} hidden Number of hidden characters + */ +function getHiddenBlock(hidden) { + const hiddenBlock = ` +
+ +

${hidden} ${hidden > 1 ? 'characters' : 'character'} hidden.

+
+
+
`; + 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 }, clearFuzzySearchCaches: false }); + } + if (doSort) { + sortEntitiesList(subEntities, false); + } + entity.entities = subEntities; + entity.hidden = subCount - subEntities.length; + } + } + + // Second run filters, hiding whatever should be filtered later + if (doFilter) { + const beforeFinalEntities = filterByTagState(entities, { globalDisplayFilters: true }); + entities = entitiesFilter.applyFilters(beforeFinalEntities, { clearFuzzySearchCaches: false }); + + // Magic for folder filter. If that one is enabled, and no folders are display anymore, we remove that filter to actually show the characters. + if (isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED) && entities.filter(x => x.type == 'tag').length == 0) { + entities = entitiesFilter.applyFilters(beforeFinalEntities, { tempOverrides: { [FILTER_TYPES.FOLDER]: FILTER_STATES.UNDEFINED }, clearFuzzySearchCaches: false }); + } + } + + // Final step, updating some properties after the last filter run + const nonTagEntitiesCount = entities.filter(entity => entity.type !== 'tag').length; + for (const entity of entities) { + if (entity.type === 'tag') { + if (entity.entities?.length == nonTagEntitiesCount) entity.isUseless = true; + } + } + + // Sort before returning if requested + if (doSort) { + sortEntitiesList(entities, false); + } + entitiesFilter.clearFuzzySearchCaches(); + return entities; +} + +export async function getOneCharacter(avatarUrl) { + const response = await fetch('/api/characters/get', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + avatar_url: avatarUrl, + }), + }); + + if (response.ok) { + const getData = await response.json(); + getData['name'] = DOMPurify.sanitize(getData['name']); + getData['chat'] = String(getData['chat']); + + const indexOf = characters.findIndex(x => x.avatar === avatarUrl); + + if (indexOf !== -1) { + characters[indexOf] = getData; + } else { + toastr.error(t`Character ${avatarUrl} not found in the list`, t`Error`, { timeOut: 5000, preventDuplicates: true }); + } + } +} + +function getCharacterSource(chId = this_chid) { + const character = characters[chId]; + + if (!character) { + return ''; + } + + const chubId = characters[chId]?.data?.extensions?.chub?.full_path; + + if (chubId) { + return `https://chub.ai/characters/${chubId}`; + } + + const pygmalionId = characters[chId]?.data?.extensions?.pygmalion_id; + + if (pygmalionId) { + return `https://pygmalion.chat/${pygmalionId}`; + } + + const githubRepo = characters[chId]?.data?.extensions?.github_repo; + + if (githubRepo) { + return `https://github.com/${githubRepo}`; + } + + const sourceUrl = characters[chId]?.data?.extensions?.source_url; + + if (sourceUrl) { + return sourceUrl; + } + + const risuId = characters[chId]?.data?.extensions?.risuai?.source; + + if (Array.isArray(risuId) && risuId.length && typeof risuId[0] === 'string' && risuId[0].startsWith('risurealm:')) { + const realmId = risuId[0].split(':')[1]; + return `https://realm.risuai.net/character/${realmId}`; + } + + 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(messagesToLoad = null) { + const firstDisplayedMesId = $('#chat').children('.mes').first().attr('mesid'); + let messageId = Number(firstDisplayedMesId); + let count = messagesToLoad || power_user.chat_truncation || Number.MAX_SAFE_INTEGER; + + // If there are no messages displayed, or the message somehow has no mesid, we default to one higher than last message id, + // so the first "new" message being shown will be the last available message + if (isNaN(messageId)) { + messageId = getLastMessageId() + 1; + } + + console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length); + const prevHeight = $('#chat').prop('scrollHeight'); + const isButtonInView = isElementInViewport($('#show_more_messages')[0]); + + while (messageId > 0 && count > 0) { + let newMessageId = messageId - 1; + addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId }); + count--; + messageId--; + } + + if (messageId == 0) { + $('#show_more_messages').remove(); + } + + if (isButtonInView) { + const newHeight = $('#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('
Show more messages
'); + } + + 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() { + preserveNeutralChat(); + await clearChat(); + chat.length = 0; + + if (selected_group) { + await getGroupChat(selected_group, true); + } + else if (this_chid !== undefined) { + await getChat(); + } + else { + resetChatState(); + restoreNeutralChat(); + await getCharacters(); + await printMessages(); + await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); + } + + hideSwipeButtons(); + showSwipeButtons(); +} + +/** + * Send the message currently typed into the chat box. + */ +export async function sendTextareaMessage() { + if (is_send_press) return; + if (isExecutingCommandsFromChatInput) return; + if (this_edit_mes_id) return; // don't proceed if editing a message + + 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 && + !hasPendingFileAttachment() && + !textareaText && + !selected_group && + chat.length && + !chat[chat.length - 1]['is_user'] && + !chat[chat.length - 1]['is_system'] + ) { + generateType = 'continue'; + } + + if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) { + await newAssistantChat(); + } + + Generate(generateType); +} + +/** + * Formats the message text into an HTML string using Markdown and other formatting. + * @param {string} mes Message text + * @param {string} ch_name Character name + * @param {boolean} isSystem If the message was sent by the system + * @param {boolean} isUser If the message was sent by the user + * @param {number} messageId Message index in chat array + * @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides + * @returns {string} HTML string + */ +export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) { + if (!mes) { + return ''; + } + + if (Number(messageId) === 0 && !isSystem && !isUser) { + const mesBeforeReplace = mes; + const chatMessage = chat[messageId]; + mes = substituteParams(mes, undefined, ch_name); + if (chatMessage && chatMessage.mes === mesBeforeReplace && chatMessage.extra?.display_text !== mesBeforeReplace) { + chatMessage.mes = mes; + } + } + + mesForShowdownParse = mes; + + // Force isSystem = false on comment messages so they get formatted properly + if (ch_name === COMMENT_NAME_DEFAULT && isSystem && !isUser) { + isSystem = false; + } + + // Let hidden messages have markdown + if (isSystem && ch_name !== systemUserName) { + isSystem = false; + } + + // Prompt bias replacement should be applied on the raw message + 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 (!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)|(\u00AB.*?\u00BB)|(\u300C.*?\u300D)|(\u300E.*?\u300F)|(\uFF02.*?\uFF02)/gm, + function (match, p1, p2, p3, p4, p5, p6) { + if (p1) { + // English double quotes + return `"${p1.slice(1, -1)}"`; + } else if (p2) { + // Curly double quotes “ ” + return `“${p2.slice(1, -1)}”`; + } else if (p3) { + // Guillemets « » + return `«${p3.slice(1, -1)}»`; + } else if (p4) { + // Corner brackets 「 」 + return `「${p4.slice(1, -1)}」`; + } else if (p5) { + // White corner brackets 『 』 + return `『${p5.slice(1, -1)}』`; + } else if (p6) { + // Fullwidth quotes " " + return `"${p6.slice(1, -1)}"`; + } else { + // Return the original match if no quotes are found + 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(/[\s\S]*?<\/code>/g, function (match) { + // Firefox creates extra newlines from
s in code blocks, so we replace them before converting newlines to
s. + return match.replace(/\n/gm, '\u0000'); + }); + mes = mes.replace(/\u0000/g, '\n'); // Restore converted newlines + mes = mes.trim(); + + mes = mes.replace(/[\s\S]*?<\/code>/g, function (match) { + return match.replace(/&/g, '&'); + }); + } + + if (!power_user.allow_name2_display && ch_name && !isUser && !isSystem) { + mes = mes.replace(new RegExp(`(^|\n)${escapeRegex(ch_name)}:`, 'g'), '$1'); + } + + /** @type {import('dompurify').Config & { RETURN_DOM_FRAGMENT: false; RETURN_DOM: false }} */ + const config = { + RETURN_DOM: false, + RETURN_DOM_FRAGMENT: false, + RETURN_TRUSTED_TYPE: false, + MESSAGE_SANITIZE: true, + ADD_TAGS: ['custom-style'], + ...sanitizerOverrides, + }; + 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} 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(image); + }; +} + + +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); + bookmarkLink && updateBookmarkDisplay(mes); + + 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); +} + +/** + * Appends image or file to the message element. + * @param {object} mes Message object + * @param {JQuery} messageElement Message element + * @param {boolean} [adjustScroll=true] Whether to adjust the scroll position after appending the media + */ +export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { + // Add image to message + if (mes.extra?.image) { + const container = messageElement.find('.mes_img_container'); + const chatHeight = $('#chat').prop('scrollHeight'); + const image = messageElement.find('.mes_img'); + const text = messageElement.find('.mes_text'); + const isInline = !!mes.extra?.inline_image; + image.off('load').on('load', function () { + if (!adjustScroll) { + return; + } + 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 || ''); + container.addClass('img_extra'); + image.toggleClass('img_inline', isInline); + text.toggleClass('displayNone', !isInline); + + const imageSwipes = mes.extra.image_swipes; + if (Array.isArray(imageSwipes) && imageSwipes.length > 0) { + container.addClass('img_swipes'); + const counter = container.find('.mes_img_swipe_counter'); + const currentImage = imageSwipes.indexOf(mes.extra.image) + 1; + counter.text(`${currentImage}/${imageSwipes.length}`); + + const swipeLeft = container.find('.mes_img_swipe_left'); + swipeLeft.off('click').on('click', function () { + eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'left' }); + }); + + const swipeRight = container.find('.mes_img_swipe_right'); + swipeRight.off('click').on('click', function () { + eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' }); + }); + } + } + + // 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)); + 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', async function () { + const text = codeBlocks.get(i).innerText; + await copyText(text); + toastr.info(t`Copied!`, '', { timeOut: 2000 }); + }); + } +} + + +/** + * Adds a single message to the chat. + * @param {object} mes Message object + * @param {object} [options] Options + * @param {string} [options.type='normal'] Message type + * @param {number} [options.insertAfter=null] Message ID to insert the new message after + * @param {boolean} [options.scroll=true] Whether to scroll to the new message + * @param {number} [options.insertBefore=null] Message ID to insert the new message before + * @param {number} [options.forceId=null] Force the message ID + * @param {boolean} [options.showSwipes=true] Whether to show swipe buttons + * @returns {void} + */ +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']; + } + + // if mes.uses_system_ui is true, set an override on the sanitizer options + const sanitizerOverrides = mes.uses_system_ui ? { MESSAGE_ALLOW_SYSTEM_UI: true } : {}; + + messageText = messageFormatting( + messageText, + mes.name, + isSystem, + mes.is_user, + chat.indexOf(mes), + sanitizerOverrides, + ); + 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'); + } + + if (Array.isArray(mes?.extra?.tool_invocations)) { + newMessage.addClass('toolCall'); + } + + //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('
'); + }); + + if (type === 'swipe') { + const swipeMessage = chatElement.find(`[mesid="${chat.length - 1}"]`); + swipeMessage.attr('swipeid', params.swipeId); + 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); + + // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message' is enabled + if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) { + const swipesNum = chat[newMessageId].swipes?.length; + const swipeId = chat[newMessageId].swipe_id + 1; + newMessage.find('.swipes-counter').text(formatSwipeCounter(swipeId, swipesNum)); + } + + 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} additionalMacro - Additional environment variables for substitution. + * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro. + * @returns {string} The string with substituted parameters. + */ +export function substituteParamsExtended(content, additionalMacro = {}, postProcessFn = (x) => x) { + return substituteParams(content, undefined, undefined, undefined, undefined, true, additionalMacro, postProcessFn); +} + +/** + * 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} [additionalMacro] - Additional environment variables for substitution. + * @param {(x: string) => string} [postProcessFn] - Post-processing function for each substituted macro. + * @returns {string} The string with substituted parameters. + */ +export function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true, additionalMacro = {}, postProcessFn = (x) => x) { + if (!content) { + return ''; + } + + const environment = {}; + + if (typeof _original === 'string') { + let originalSubstituted = false; + environment.original = () => { + if (originalSubstituted) { + return ''; + } + + originalSubstituted = true; + return _original; + }; + } + + const getGroupValue = (includeMuted) => { + if (typeof _group === 'string') { + return _group; + } + + if (selected_group) { + const members = groups.find(x => x.id === selected_group)?.members; + /** @type {string[]} */ + const disabledMembers = groups.find(x => x.id === selected_group)?.disabled_members ?? []; + const isMuted = x => includeMuted ? true : !disabledMembers.includes(x); + const names = Array.isArray(members) + ? members.filter(isMuted).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.charInstruction = 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(true); + environment.groupNotMuted = getGroupValue(false); + environment.model = getGeneratingModel(); + + if (additionalMacro && typeof additionalMacro === 'object') { + Object.assign(environment, additionalMacro); + } + + return evaluateMacros(content, environment, postProcessFn); +} + + +/** + * 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 result = []; + + if (power_user.context.names_as_stop_strings) { + const charString = `\n${name2}:`; + const userString = `\n${name1}:`; + result.push(isImpersonate ? charString : userString); + + result.push(userString); + + if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) { + result.push(charString); + } + + // Add group members as stopping strings if generating for a specific group member or user. (Allow slash commands to work around name stopping string restrictions) + if (selected_group && (name2 || isImpersonate)) { + 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(x => x).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. + * @param {number} force_chid Character ID to use for this generation run. Works in groups only. + * @returns + */ +export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null, responseLength = null, force_chid = null) { + console.log('got into genQuietPrompt'); + const responseLengthCustomized = typeof responseLength === 'number' && responseLength > 0; + let eventHook = () => { }; + try { + /** @type {GenerateOptions} */ + const options = { + quiet_prompt, + quietToLoud, + skipWIAN: skipWIAN, + force_name2: true, + quietImage: quietImage, + quietName: quietName, + force_chid: force_chid, + }; + if (responseLengthCustomized) { + TempResponseLength.save(main_api, responseLength); + eventHook = TempResponseLength.setupEventHook(main_api); + } + return await Generate('quiet', options); + } finally { + if (responseLengthCustomized && TempResponseLength.isCustomized()) { + TempResponseLength.restore(main_api); + TempResponseLength.removeEventHook(main_api, eventHook); + } + } +} + +/** + * Executes slash commands and returns the new text and whether the generation was interrupted. + * @param {string} message Text to be sent + * @returns {Promise} 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 || power_user.persona_description_position === persona_description_positions.NONE) { + 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); + } +} + +/** + * Returns all extension prompts combined. + * @returns {Promise} Combined extension prompts + */ +async function getAllExtensionPrompts() { + const values = []; + + for (const prompt of Object.values(extension_prompts)) { + const value = prompt?.value?.trim(); + + if (!value) { + continue; + } + + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) { + continue; + } + + values.push(value); + } + + return substituteParams(values.join('\n')); +} + +/** + * Wrapper to fetch extension prompts by module name + * @param {string} moduleName Module name + * @returns {Promise} Extension prompt + */ +export async function getExtensionPromptByName(moduleName) { + if (!moduleName) { + return ''; + } + + const prompt = extension_prompts[moduleName]; + + if (!prompt) { + return ''; + } + + const hasFilter = typeof prompt.filter === 'function'; + + if (hasFilter && !await prompt.filter()) { + return ''; + } + + return substituteParams(prompt.value); +} + +/** + * 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 {Promise} Extension prompt + */ +export async function getExtensionPrompt(position = extension_prompt_types.IN_PROMPT, depth = undefined, separator = '\n', role = undefined, wrap = true) { + const filterByFunction = async (prompt) => { + const hasFilter = typeof prompt.filter === 'function'; + if (hasFilter && !await prompt.filter()) { + return false; + } + return true; + }; + const promptPromises = 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) + .filter(filterByFunction); + const prompts = await Promise.all(promptPromises); + + let values = prompts.map(x => x.value.trim()).join(separator); + if (wrap && values.length && !values.startsWith(separator)) { + values = separator + values; + } + if (wrap && values.length && !values.endsWith(separator)) { + values = values + separator; + } + if (values.length) { + values = substituteParams(values); + } + return values; +} + +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: '' }; + result.persona = baseChatReplace(power_user.persona_description?.trim(), name1, name2); + + const character = characters[this_chid]; + + if (!character) { + return result; + } + + const scenarioText = chat_metadata['scenario'] || character.scenario || ''; + result.description = baseChatReplace(character.description?.trim(), name1, name2); + result.personality = baseChatReplace(character.personality?.trim(), name1, name2); + result.scenario = baseChatReplace(scenarioText.trim(), name1, name2); + result.mesExamples = baseChatReplace(character.mes_example?.trim(), name1, name2); + result.system = power_user.prefer_character_prompt ? baseChatReplace(character.data?.system_prompt?.trim(), name1, name2) : ''; + result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(character.data?.post_history_instructions?.trim(), name1, name2) : ''; + result.version = character.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]; + return ( + (main_api == 'openai' && + oai_settings.stream_openai && + !noStreamSources.includes(oai_settings.chat_completion_source) && + !(oai_settings.chat_completion_source == chat_completion_sources.OPENAI && oai_settings.openai_model.startsWith('o1-')) && + !(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 { + /** + * Creates a new streaming processor. + * @param {string} type Generation type + * @param {boolean} forceName2 If true, force the use of name2 + * @param {Date} timeStarted Date when generation was started + * @param {string} continueMessage Previous message if the type is 'continue' + */ + constructor(type, forceName2, timeStarted, continueMessage) { + this.result = ''; + this.messageId = -1; + this.messageDom = null; + this.messageTextDom = null; + this.messageTimerDom = null; + this.messageTokenCounterDom = null; + /** @type {HTMLTextAreaElement} */ + this.sendTextarea = document.querySelector('#send_textarea'); + this.type = type; + this.force_name2 = forceName2; + this.isStopped = false; + this.isFinished = false; + this.generator = this.nullStreamingGeneration; + this.abortController = new AbortController(); + this.firstMessageText = '...'; + this.timeStarted = timeStarted; + this.continueMessage = type === 'continue' ? continueMessage : ''; + this.swipes = []; + /** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */ + this.messageLogprobs = []; + this.toolCalls = []; + } + + #checkDomElements(messageId) { + if (this.messageDom === null || this.messageTextDom === null) { + this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`); + this.messageTextDom = this.messageDom?.querySelector('.mes_text'); + this.messageTimerDom = this.messageDom?.querySelector('.mes_timer'); + this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay'); + } + } + + #updateMessageBlockVisibility() { + if (this.messageDom instanceof HTMLElement && Array.isArray(this.toolCalls) && this.toolCalls.length > 0) { + const shouldHide = ['', '...'].includes(this.result); + this.messageDom.classList.toggle('displayNone', shouldHide); + } + } + + 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') { + this.sendTextarea.value = ''; + this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); + } + else { + await saveReply(this.type, text, true); + messageId = chat.length - 1; + this.#checkDomElements(messageId); + 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) { + this.sendTextarea.value = processedText; + this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true })); + } + else { + this.#checkDomElements(messageId); + this.#updateMessageBlockVisibility(); + const currentTime = new Date(); + // Don't waste time calculating token count for streaming + const 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; + if (this.messageTokenCounterDom instanceof HTMLElement) { + this.messageTokenCounterDom.textContent = `${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'])) }; + } + + const formattedText = messageFormatting( + processedText, + chat[messageId].name, + chat[messageId].is_system, + chat[messageId].is_user, + messageId, + ); + if (this.messageTextDom instanceof HTMLElement) { + this.messageTextDom.innerHTML = formattedText; + } + if (this.messageTimerDom instanceof HTMLElement) { + this.messageTimerDom.textContent = timePassed.timerValue; + this.messageTimerDom.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); + } + + saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage); + await saveChatConditional(); + unblockGeneration(); + generatedPromptCache = ''; + + //console.log("Generated text size:", text.length, text) + + const isAborted = this.abortController.signal.aborted; + if (power_user.auto_swipe && !isAborted) { + 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(); + + const noEmitTypes = ['swipe', 'impersonate', 'continue']; + if (!noEmitTypes.includes(this.type)) { + eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId); + eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId); + } + } + + 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.abortController.abort(); + this.isFinished = true; + } + + /** + * @returns {Generator<{ text: string, swipes: string[], logprobs: import('./scripts/logprobs.js').TokenLogprobs, toolCalls: any[] }, 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, toolCalls } of this.generator()) { + timestamps.push(Date.now()); + if (this.isStopped) { + return; + } + + this.toolCalls = toolCalls; + this.result = text; + this.swipes = Array.from(swipes ?? []); + if (logprobs) { + this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs])); + } + await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text); + await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text)); + + // + // DAVE MOD + // Mode #2, Set global for incoming text ONLY. + // + + dave_holdtext=text; + + // + // END DAVE MOD + // + + + } + 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) { + // in the case of a self-inflicted abort, we have already cleaned up + if (!this.isFinished) { + console.error(err); + this.onErrorStreaming(); + } + return this.result; + } + + 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} 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; + const isInstruct = power_user.instruct.enabled && api !== 'openai' && api !== 'novel' && !instructOverride; + const isQuiet = true; + let eventHook = () => { }; + + 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 { + if (responseLengthCustomized) { + TempResponseLength.save(api, responseLength); + } + 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'); + } + TempResponseLength.restore(api); + 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'); + TempResponseLength.restore(api); + break; + } + case 'textgenerationwebui': + generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet'); + TempResponseLength.restore(api); + break; + case 'openai': { + generateData = [{ role: 'user', content: prompt.trim() }]; + if (systemPrompt) { + generateData.unshift({ role: 'system', content: systemPrompt.trim() }); + } + eventHook = TempResponseLength.setupEventHook(api); + } 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) { + throw await response.json(); + } + + data = await response.json(); + } + + // should only happen for text completions + // other frontend paths do not return data if calling the backend fails, + // they throw things instead + if (data.error) { + throw new Error(data.response); + } + + const message = cleanUpMessage(extractMessageFromData(data), false, false, true); + + if (!message) { + throw new Error('No message generated'); + } + + return message; + } finally { + if (responseLengthCustomized && TempResponseLength.isCustomized()) { + TempResponseLength.restore(api); + TempResponseLength.removeEventHook(api, eventHook); + } + } +} + +class TempResponseLength { + static #originalResponseLength = -1; + static #lastApi = null; + + static isCustomized() { + return this.#originalResponseLength > -1; + } + + /** + * Save the current response length for the specified API. + * @param {string} api API identifier + * @param {number} responseLength New response length + */ + static save(api, responseLength) { + if (api === 'openai') { + this.#originalResponseLength = oai_settings.openai_max_tokens; + oai_settings.openai_max_tokens = responseLength; + } else { + this.#originalResponseLength = amount_gen; + amount_gen = responseLength; + } + + this.#lastApi = api; + console.log('[TempResponseLength] Saved original response length:', TempResponseLength.#originalResponseLength); + } + + /** + * Restore the original response length for the specified API. + * @param {string|null} api API identifier + * @returns {void} + */ + static restore(api) { + if (this.#originalResponseLength === -1) { + return; + } + if (!api && this.#lastApi) { + api = this.#lastApi; + } + if (api === 'openai') { + oai_settings.openai_max_tokens = this.#originalResponseLength; + } else { + amount_gen = this.#originalResponseLength; + } + + console.log('[TempResponseLength] Restored original response length:', this.#originalResponseLength); + this.#originalResponseLength = -1; + this.#lastApi = null; + } + + /** + * Sets up an event hook to restore the original response length when the event is emitted. + * @param {string} api API identifier + * @returns {function(): void} Event hook function + */ + static setupEventHook(api) { + const eventHook = () => { + if (this.isCustomized()) { + this.restore(api); + } + }; + + switch (api) { + case 'openai': + eventSource.once(event_types.CHAT_COMPLETION_SETTINGS_READY, eventHook); + break; + default: + eventSource.once(event_types.GENERATE_AFTER_DATA, eventHook); + break; + } + + return eventHook; + } + + /** + * Removes the event hook for the specified API. + * @param {string} api API identifier + * @param {function(): void} eventHook Previously set up event hook + */ + static removeEventHook(api, eventHook) { + switch (api) { + case 'openai': + eventSource.removeListener(event_types.CHAT_COMPLETION_SETTINGS_READY, eventHook); + break; + default: + eventSource.removeListener(event_types.GENERATE_AFTER_DATA, eventHook); + break; + } + } +} + +/** + * Removes last message from the chat DOM. + * @returns {Promise} Resolves when the message is removed. + */ +function removeLastMessage() { + return new Promise((resolve) => { + const lastMes = $('#chat').children('.mes').last(); + if (lastMes.length === 0) { + return resolve(); + } + lastMes.hide(animation_duration, function () { + $(this).remove(); + resolve(); + }); + }); +} + +/** + * 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} 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, quietName?: string, depth?: number }} GenerateOptions + */ +export async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth = 0 } = {}, dryRun = false) { + console.log('Generate entered'); + setGenerationProgress(0); + generation_started = new Date(); + + // Occurs every time, even if the generation is aborted due to slash commands execution + await eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun); + + // 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'; + + 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(); + } + } + + // Occurs only if the generation is not aborted due to slash commands execution + await eventSource.emit(event_types.GENERATION_AFTER_COMMANDS, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage }, dryRun); + + if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) { + toastr.error(t`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 (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(t`Verify that the server is running and accessible.`, t`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 }); + } + + 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) { + if (menu_type != 'character_edit') 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 hasBackendConnection = online_status !== 'no_connection'; + + // 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 && !hasBackendConnection) { + 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; + await removeLastMessage(); + 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 + //********************************* + + // These generation types should not attach pending files to the chat + const noAttachTypes = [ + 'regenerate', + 'swipe', + 'impersonate', + 'quiet', + 'continue', + 'ask_command', + ]; + //for normal messages sent from user.. + if ((textareaText != '' || (hasPendingFileAttachment() && !noAttachTypes.includes(type))) && !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 (main_api !== 'openai') { + if (power_user.sysprompt.enabled) { + system = power_user.prefer_character_prompt && system ? system : baseChatReplace(power_user.sysprompt.content, name1, name2); + system = isInstruct ? formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.sysprompt.content)) : system; + } else { + // Nullify if it's not enabled + system = ''; + } + } + + // 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 + const canUseTools = ToolManager.isToolCallingSupported(); + const canPerformToolCalls = !dryRun && ToolManager.canPerformToolCalls(type) && depth < ToolManager.RECURSE_LIMIT; + let coreChat = chat.filter(x => !x.is_system || (canUseTools && Array.isArray(x.extra?.tool_invocations))); + 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); + + if (chatItem?.extra?.append_title && chatItem?.extra?.title) { + regexedMessage = `${regexedMessage}\n\n${chatItem.extra.title}`; + } + + return { + ...chatItem, + mes: regexedMessage, + index, + }; + })); + + // Determine token limit + let this_max_context = getMaxContextSize(); + + if (!dryRun) { + console.debug('Running extension interceptors'); + const aborted = await runGenerationInterceptors(coreChat, this_max_context, type); + + 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); + } + } + + // Fetches the combined prompt for both negative and positive prompts + const cfgGuidanceScale = getGuidanceScale(); + const useCfgPrompt = cfgGuidanceScale && cfgGuidanceScale.value !== 1; + + // Adjust max context based on CFG prompt to prevent overfitting + if (useCfgPrompt) { + const negativePrompt = getCfgPrompt(cfgGuidanceScale, true, true)?.value || ''; + const positivePrompt = getCfgPrompt(cfgGuidanceScale, false, true)?.value || ''; + if (negativePrompt || positivePrompt) { + const previousMaxContext = this_max_context; + const [negativePromptTokenCount, positivePromptTokenCount] = await Promise.all([getTokenCountAsync(negativePrompt), getTokenCountAsync(positivePrompt)]); + const decrement = Math.max(negativePromptTokenCount, positivePromptTokenCount); + this_max_context -= decrement; + console.log(`Max context reduced by ${decrement} tokens of CFG prompt (${previousMaxContext} -> ${this_max_context})`); + } + } + + 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 || examplesStr.length === 0 || examplesStr === '') { + return []; + } + + if (!examplesStr.startsWith('')) { + examplesStr = '\n' + examplesStr.trim(); + } + + const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : ''; + const blockHeading = main_api === 'openai' ? '\n' : (exampleSeparator || (isInstruct ? '\n' : '')); + const splitExamples = examplesStr.split(//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 => world_info_include_names ? `${x.name}: ${x.mes}` : 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 = await 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 = []; + const lastUserMessageIndex = coreChat.findLastIndex(x => x.is_user); + + 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); + } + + if (lastUserMessageIndex >= 0 && j === lastUserMessageIndex && isInstruct) { + // Reformat with the last input sequence (if any) + chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST); + } + + // 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: substituteParams(power_user.instruct.user_alignment_message), + is_user: true, + }; + userAlignmentMessage = formatMessageHistoryItem(alignmentMessage, isInstruct, force_output_sequence.FIRST); + } + + // Call combined AN into Generate + const beforeScenarioAnchor = (await getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT)).trimStart(); + const afterScenarioAnchor = await getExtensionPrompt(extension_prompt_types.IN_PROMPT); + + const storyStringParams = { + description: description, + personality: personality, + persona: power_user.persona_description_position == persona_description_positions.IN_PROMPT ? persona : '', + scenario: scenario, + system: 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') { + oaiMessages = setOpenAIMessages(coreChat); + oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray); + } + + // hack for regeneration of the first message + if (chat2.length == 0) { + chat2.push(''); + } + + let examplesString = ''; + let chatString = addChatsPreamble(addChatsSeparator('')); + let cyclePrompt = ''; + + async function getMessagesTokenCount() { + const encodeString = [ + beforeScenarioAnchor, + storyString, + afterScenarioAnchor, + examplesString, + userAlignmentMessage, + chatString, + modifyLastPromptLine(''), + cyclePrompt, + ].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, '')); + if (tokenCount < this_max_context) { + chatString = chatString + item; + 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, '')); + if (tokenCount < this_max_context) { + chatString = chatString + item; + 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 + if (main_api === 'openai' && !cyclePrompt.endsWith(' ')) { + cyclePrompt += oai_settings.continue_postfix; + continue_mag += oai_settings.continue_postfix; + } + } + + 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 && !isImpersonate) ? (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 jointMessages = mesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''); + const prompt = [ + beforeScenarioAnchor, + storyString, + afterScenarioAnchor, + mesExmString, + addChatsPreamble(addChatsSeparator(jointMessages)), + '\n', + modifyLastPromptLine(''), + generatedPromptCache, + ].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(); + } + + // 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; + } + } + + await eventSource.emit(event_types.GENERATE_AFTER_DATA, generate_data); + + if (dryRun) { + generatedPromptCache = ''; + return Promise.resolve(); + } + + /** + * Saves itemized prompt bits and calls streaming or non-streaming generation API. + * @returns {Promise|String|{fromStream}|string|undefined|Object>} + * @throws {Error|object} Error with message text, or Error with response JSON (OAI/Horde), or the actual response JSON (novel|textgenerationwebui|kobold) + */ + 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: await 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 || ''), + chatVectorsString: (extension_prompts['3_vectors']?.value || ''), + dataBankVectorsString: (extension_prompts['4_vectors_data_bank']?.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: main_api !== 'openai' && power_user.sysprompt.enabled ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.sysprompt.content) : '', + userPersona: (power_user.persona_description_position == persona_description_positions.IN_PROMPT ? (persona || '') : ''), + tokenizer: getFriendlyTokenizerName(main_api).tokenizerName || '', + presetName: getPresetManager()?.getSelectedPresetName() || '', + }; + + //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, continue_mag); + if (isContinue) { + // Save reply does add cycle text to the prompt, so it's not needed here + streamingProcessor.firstMessageText = ''; + } + + // + // DAVE MOD - Sampler Random + // Truely random TEMP / Other Samplers + // Mod #3 + // + + generate_data=dave_sampler(generate_data); + + // + // END MODE + // + + streamingProcessor.generator = await sendStreamingRequest(type, generate_data); + + hideSwipeButtons(); + let getMessage = await streamingProcessor.generate(); + + // + // DAVE MOD #4 + // Stops double edit + + dave_skip_dave_edit=1; + + // + // + + + let messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); + + if (isContinue) { + getMessage = continue_mag + getMessage; + } + + + // + // DAVE MOD #5 + // getmeassge done it cleanup.... + // + + if (dave_ACTIVE==1) { + dave_skip_dave_edit=0; + if (dave_edit_flag>0 || dave_signal != 0 ) { + if (dave_edit_flag>0) { ({getMessage,continue_mag,messageChunk}=dave_edit(getMessage,continue_mag,messageChunk,1)); } + else {dave_signal_go(1);} + } + dave_message_watch=getMessage; + } + + // + // END DAVE MOD + // + + + const isStreamFinished = streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished; + const isStreamWithToolCalls = streamingProcessor && Array.isArray(streamingProcessor.toolCalls) && streamingProcessor.toolCalls.length; + if (canPerformToolCalls && isStreamFinished && isStreamWithToolCalls) { + const lastMessage = chat[chat.length - 1]; + const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls); + const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && ['', '...'].includes(streamingProcessor?.result); + hasToolCalls && shouldDeleteMessage && await deleteLastMessage(); + const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls); + const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length; + if (hasToolCalls) { + if (shouldStopGeneration) { + if (Array.isArray(invocationResult.errors) && invocationResult.errors.length) { + ToolManager.showToolCallError(invocationResult.errors); + } + unblockGeneration(type); + generatedPromptCache = ''; + streamingProcessor = null; + return; + } + + streamingProcessor = null; + depth = depth + 1; + await ToolManager.saveFunctionToolInvocations(invocationResult.invocations); + return Generate('normal', { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth }, dryRun); + } + } + + if (isStreamFinished) { + 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); + + /** + * Handles the successful response from the generation API. + * @param data + * @returns {Promise|undefined>} + * @throws {Error} Throws an error if the response data contains an error message + */ + async function onSuccess(data) { + if (!data) return; + + if (data?.fromStream) { + return data; + } + + let messageChunk = ''; + + // if an error was returned in data (textgenwebui), show it and throw it + if (data.error) { + unblockGeneration(type); + generatedPromptCache = ''; + + if (data?.response) { + toastr.error(data.response, t`API Error`, { preventDuplicates: true }); + } + throw new Error(data?.response); + } + + //const getData = await response.json(); + let getMessage = extractMessageFromData(data); + let title = extractTitleFromData(data); + kobold_horde_model = title; + + const swipes = extractMultiSwipes(data, type); + + // DAVE MOD #6 + // + + dave_skip_dave_edit=1; + + // + // END DAVE MOD + + messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false); + + if (isContinue) { + getMessage = continue_mag + getMessage; + } + + //Formating + const displayIncomplete = type === 'quiet' && !quietToLoud; + getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete); + + // DAVE MOD #7 + // already did "getMessage" updates from "cleanup..." above. + // + + if (dave_ACTIVE==1) { + dave_skip_dave_edit=0; + if (dave_edit_flag>0 || dave_signal != 0 ) { + if (dave_edit_flag>0 ) { ({getMessage,continue_mag,messageChunk}=dave_edit(getMessage,continue_mag,messageChunk,2)); } + else { dave_signal_go(2); } + } + dave_message_watch=getMessage; + } + + // + // END DAVE MOD + + 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 (canPerformToolCalls) { + const hasToolCalls = ToolManager.hasToolCalls(data); + const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage); + hasToolCalls && shouldDeleteMessage && await deleteLastMessage(); + const invocationResult = await ToolManager.invokeFunctionTools(data); + const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length; + if (hasToolCalls) { + if (shouldStopGeneration) { + if (Array.isArray(invocationResult.errors) && invocationResult.errors.length) { + ToolManager.showToolCallError(invocationResult.errors); + } + unblockGeneration(type); + generatedPromptCache = ''; + return; + } + + depth = depth + 1; + await ToolManager.saveFunctionToolInvocations(invocationResult.invocations); + return Generate('normal', { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, quietName, depth }, dryRun); + } + } + + if (type !== 'quiet') { + playMessageSound(); + } + + const isAborted = abortController && abortController.signal.aborted; + if (power_user.auto_swipe && !isAborted) { + 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 }); + } + + /** + * Exception handler for finishGenerating + * @param {Error|object} exception Error or response JSON + * @throws {Error|object} Re-throws the exception + */ + function onError(exception) { + // if the response JSON was thrown (novel|textgenerationwebui|kobold), show the error message + if (typeof exception?.error?.message === 'string') { + toastr.error(exception.error.message, t`Text generation error`, { timeOut: 10000, extendedTimeOut: 20000 }); + } + + generatedPromptCache = ''; + + unblockGeneration(type); + console.log(exception); + streamingProcessor = null; + throw exception; + } +} + +/** + * Stops the generation and any streaming if it is currently running. + */ +export function stopGeneration() { + let stopped = false; + if (streamingProcessor) { + streamingProcessor.onStopStreaming(); + stopped = true; + } + if (abortController) { + abortController.abort('Clicked stop button'); + hideStopButton(); + stopped = true; + } + eventSource.emit(event_types.GENERATION_STOPPED); + return stopped; +} + +/** + * 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 {Promise} Array of indices where the extension prompts were injected + */ +async 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(await 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) { + + // + // DAVE MOD ; Will reset via separate controller. + // Mod #8 + // + // dave_auto_continue OVERRIDE controller. + // 1=> Active. + // + + if (dave_auto_continue==1) { + + if ( dave_holdtext.length<2 ) { + dave_holdtext_count++; + + if (dave_holdtext_count>4) { + dave_auto_continue=0; + console.debug('DAVE STOP: getnextmeessage function'); + return false; + } + } + + else { + dave_holdtext_count=0; + return true; + } + + } + + // + // END MOD + // + + + 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 (abortController && abortController.signal.aborted) { + console.debug('Auto-continue is not triggered because the generation was stopped.'); + 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} A promise that resolves to the message when it 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); + await saveChatConditional(); + } + + return message; +} + +/** + * 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}`); + } + } + if (nai_settings.model_novel.includes('erato')) { + // subscriber limits coming soon + this_max_context = Math.min(max_context, 8192); + + // Added special tokens and whatnot + this_max_context -= 10; + } + + 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 duplicateCharacter() { + if (!this_chid) { + toastr.warning(t`You must first select a character to duplicate!`); + return ''; + } + + const confirmMessage = $(await renderTemplateAsync('duplicateConfirm')); + const confirm = await callGenericPopup(confirmMessage, POPUP_TYPE.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(t`Character Duplicated`); + const data = await response.json(); + await eventSource.emit(event_types.CHARACTER_DUPLICATED, { oldAvatar: body.avatar_url, newAvatar: data.path }); + await getCharacters(); + } + + return ''; +} + +export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId) { + 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), + chatVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatVectorsString), + dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString), + modelUsed: chat[incomingMesId]?.extra?.model, + apiUsed: chat[incomingMesId]?.extra?.api, + presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`, + }; + + const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value; + + if (params.apiUsed) { + params.apiUsed = getFriendlyName(params.apiUsed); + } + + if (params.this_main_api) { + params.mainApiFriendlyName = getFriendlyName(params.this_main_api); + } + + 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 = itemizedPrompts[thisPromptSet]?.tokenizer || 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]); + break; + } else if (itemizedPrompts[i].rawPrompt) { + priorPromptArrayItemForRawPromptDisplay = i; + } + } + 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, incomingMesId); + const flatten = (rawPrompt) => Array.isArray(rawPrompt) ? rawPrompt.map(x => x.content).join('\n') : rawPrompt; + + const template = params.this_main_api == 'openai' + ? await renderTemplateAsync('itemizationChat', params) + : await renderTemplateAsync('itemizationText', params); + + const popup = new Popup(template, POPUP_TYPE.TEXT); + + /** @type {HTMLElement} */ + const diffPrevPrompt = popup.dlg.querySelector('#diffPrevPrompt'); + if (priorPromptArrayItemForRawPromptDisplay) { + diffPrevPrompt.style.display = ''; + diffPrevPrompt.addEventListener('click', function () { + const dmp = new DiffMatchPatch(); + const text1 = flatten(itemizedPrompts[priorPromptArrayItemForRawPromptDisplay].rawPrompt); + const text2 = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); + + dmp.Diff_Timeout = 2.0; + + const d = dmp.diff_main(text1, text2); + let ds = dmp.diff_prettyHtml(d); + // make it readable + ds = ds.replaceAll('background:#e6ffe6;', 'background:#b9f3b9; color:black;'); + ds = ds.replaceAll('background:#ffe6e6;', 'background:#f5b4b4; color:black;'); + ds = ds.replaceAll('¶', ''); + const container = document.createElement('div'); + container.innerHTML = DOMPurify.sanitize(ds); + const rawPromptWrapper = document.getElementById('rawPromptWrapper'); + rawPromptWrapper.replaceChildren(container); + $('#rawPromptPopup').slideToggle(); + }); + } else { + diffPrevPrompt.style.display = 'none'; + } + popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () { + let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; + let rawPromptValues = rawPrompt; + + if (Array.isArray(rawPrompt)) { + rawPromptValues = rawPrompt.map(x => x.content).join('\n'); + } + + await copyText(rawPromptValues); + toastr.info(t`Copied!`); + }); + + popup.dlg.querySelector('#showRawPrompt').addEventListener('click', function () { + //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); + console.log(PromptArrayItemForRawPromptDisplay); + console.log(itemizedPrompts); + console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); + + const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); + + //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '
'); + const rawPromptWrapper = document.getElementById('rawPromptWrapper'); + rawPromptWrapper.innerText = rawPrompt; + $('#rawPromptPopup').slideToggle(); + }); + + await popup.show(); +} + +function setInContextMessages(msgInContextCount, type) { + $('#chat .mes').removeClass('lastInContext'); + + if (type === 'swipe' || type === 'regenerate' || type === 'continue') { + msgInContextCount++; + } + + const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-msgInContextCount); + lastMessageBlock.addClass('lastInContext'); + + if (lastMessageBlock.length === 0) { + const firstMessageId = getFirstDisplayedMessageId(); + $(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext'); + } + + // Update last id to chat. No metadata save on purpose, gets hopefully saved via another call + const lastMessageId = Math.max(0, chat.length - msgInContextCount); + chat_metadata['lastInContextMessageId'] = lastMessageId; +} + +/** + * Sends a non-streaming request to the API. + * @param {string} type Generation type + * @param {object} data Generation data + * @returns {Promise} Response data from the API + * @throws {Error|object} + */ +export 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) { + throw await response.json(); + } + + return await response.json(); +} + +/** + * Sends a streaming request to the API. + * @param {string} type Generation type + * @param {object} data Generation data + * @returns {Promise} Streaming generator + */ +export async function sendStreamingRequest(type, data) { + if (abortController?.signal?.aborted) { + throw new Error('Generation was aborted.'); + } + + 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 + * @throws {Error} If the API is unknown + */ +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 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.KOBOLDCPP: + case textgen_types.VLLM: + case textgen_types.INFERMATICAI: + 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 ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? ''; + 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' && [textgen_types.MANCER, textgen_types.VLLM, textgen_types.APHRODITE, textgen_types.TABBY, textgen_types.INFERMATICAI].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 (power_user.collapse_newlines) { + getMessage = collapseNewlines(getMessage); + } + + // 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'; + const isNotEmpty = (str) => str && str.trim() !== ''; + 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 && isNotEmpty(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 + ? (!power_user.allow_name1_display ? name1 : '') + : (!power_user.allow_name2_display ? name2 : ''); + + if (nameToTrim2 && getMessage.startsWith(nameToTrim2 + ':')) { + getMessage = getMessage.replace(nameToTrim2 + ':', ''); + getMessage = getMessage.trimStart(); + } + + if (isImpersonate) { + getMessage = getMessage.trim(); + } + + if (!displayIncompleteSentences && power_user.trim_sentences) { + getMessage = trimToEndSentence(getMessage); + } + + if (power_user.trim_spaces) { + getMessage = getMessage.trim(); + } + + // + // DAVE MOD #9 + // + + if (dave_ACTIVE==1) { + if (dave_skip_dave_edit==0 && (dave_edit_flag>0 || dave_signal != 0 )) { + if (dave_edit_flag>0 ) { ({getMessage,continue_mag,messageChunk}=dave_edit(getMessage,"","",3)); } + else { dave_signal_go(3); } + } + dave_message_watch=getMessage; + } + + // + // END DAVE MOD + // + + return getMessage; +} + +export 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 = //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_impersonate').removeClass('displayNone'); + $('.mes_buttons:last').show(); + hideStopButton(); +} + +export function deactivateSendButtons() { + $('#send_but').addClass('displayNone'); + $('#mes_continue').addClass('displayNone'); + $('#mes_impersonate').addClass('displayNone'); + showStopButton(); +} + +export function resetChatState() { + // replaces deleted charcter name with system user since it will be displayed next. + name2 = (this_chid === undefined && neutralCharacterName) ? neutralCharacterName : systemUserName; + //unsets expected chid before reloading (related to getCharacters/printCharacters from using old arrays) + this_chid = undefined; + // sets up system user to tell user about having deleted a character + chat.splice(0, chat.length, ...SAFETY_CHAT); + // 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; +} + +/** + * Sets the API connection status of the application + * @param {string|'no_connection'} value Connection status value + */ +export function setOnlineStatus(value) { + const previousStatus = online_status; + online_status = value; + displayOnlineStatus(); + if (previousStatus !== online_status) { + eventSource.emitAndWait(event_types.ONLINE_STATUS_CHANGED, online_status); + } +} + +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(t`No character name provided.`, t`Rename Character`); + return false; + } + if (this_chid === undefined) { + toastr.warning(t`No character selected.`, t`Rename Character`); + return false; + } + + const oldAvatar = characters[this_chid].avatar; + const newValue = name || await callGenericPopup('

' + t`New name:` + '

', POPUP_TYPE.INPUT, characters[this_chid].name); + + if (!newValue) { + toastr.warning(t`No character name provided.`, t`Rename Character`); + return false; + } + if (newValue === characters[this_chid].name) { + toastr.info(t`Same character name provided, so name did not change.`, t`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(`

Character renamed!

+

Past chats will still contain the old character name. Would you like to update the character name in previous chats as well?

+ Sprites folder (if any) should be renamed manually.`, 'confirm'); + + if (renamePastChatsConfirm) { + await renamePastChats(newAvatar, newValue); + await reloadCurrentChat(); + toastr.success(t`Character renamed and past chats updated!`, t`Rename Character`); + } else { + toastr.success(t`Character renamed!`, t`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(t`Something went wrong. The page will be reloaded.`, 'text'); + else toastr.error(t`Something went wrong. The page will be reloaded.`, t`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(t`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(chatName, withMetadata, mesId) { + const metadata = { ...chat_metadata, ...(withMetadata || {}) }; + const fileName = chatName ?? characters[this_chid]?.chat; + + if (!fileName && name2 === neutralCharacterName) { + // TODO: Do something for a temporary chat with no character. + return; + } + + if (!fileName) { + 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(t`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: fileName, + 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) { + toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`); + 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; + } + + crop_data = undefined; + const file = input.files[0]; + const fileData = await getBase64Async(file); + + if (!power_user.never_resize_avatars) { + const dlg = new Popup('Set the crop position of the avatar image', POPUP_TYPE.CROP, '', { cropImage: fileData }); + const croppedImage = await dlg.show(); + + if (!croppedImage) { + return; + } + + crop_data = dlg.cropData; + $('#avatar_load_preview').attr('src', String(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 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); + freshChat = true; + } + // Make sure the chat appears on the server + await saveChatConditional(); + } + 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(new CustomEvent('newChat')); +} + +////////// OPTIMZED MAIN API CHANGE FUNCTION //////////// + +export function changeMainAPI() { + const selectedVal = $('#main_api').val(); + //console.log(selectedVal); + const apiElements = { + 'koboldhorde': { + apiStreaming: $('#NULL_SELECTOR'), + apiSettings: $('#kobold_api-settings'), + apiConnector: $('#kobold_horde'), + apiPresets: $('#kobold_api-presets'), + apiRanges: $('#range_block'), + maxContextElem: $('#max_context_block'), + amountGenElem: $('#amount_gen_block'), + }, + 'kobold': { + apiStreaming: $('#streaming_kobold_block'), + apiSettings: $('#kobold_api-settings'), + apiConnector: $('#kobold_api'), + apiPresets: $('#kobold_api-presets'), + apiRanges: $('#range_block'), + maxContextElem: $('#max_context_block'), + amountGenElem: $('#amount_gen_block'), + }, + 'textgenerationwebui': { + apiStreaming: $('#streaming_textgenerationwebui_block'), + apiSettings: $('#textgenerationwebui_api-settings'), + apiConnector: $('#textgenerationwebui_api'), + apiPresets: $('#textgenerationwebui_api-presets'), + apiRanges: $('#range_block_textgenerationwebui'), + maxContextElem: $('#max_context_block'), + amountGenElem: $('#amount_gen_block'), + }, + 'novel': { + apiStreaming: $('#streaming_novel_block'), + apiSettings: $('#novel_api-settings'), + apiConnector: $('#novel_api'), + apiPresets: $('#novel_api-presets'), + apiRanges: $('#range_block_novel'), + maxContextElem: $('#max_context_block'), + amountGenElem: $('#amount_gen_block'), + }, + 'openai': { + apiStreaming: $('#NULL_SELECTOR'), + 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'); + apiObj.apiStreaming.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.apiStreaming.css('display', 'block'); + 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.debug('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 === '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; + setOnlineStatus('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(t`Your messages will now be sent as ${name1}`, t`Current persona updated`); + } + saveSettingsDebounced(); +} + +async function doOnboarding(avatarId) { + const template = $('#onboarding_template .onboarding'); + let userName = await callGenericPopup(template, POPUP_TYPE.INPUT, currentUser?.name || name1, { rows: 2, wider: true, cancelButton: false }); + + if (userName) { + userName = String(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, + }; + } +} + +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(t`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( + '', + ); //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(``); + //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(``); + }); + 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 + await loadPowerUserSettings(settings, data); + + // Apply theme toggles from power user settings + applyPowerUserSettings(); + + // 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 enableAutoUpdate = Boolean(data.enable_extensions_auto_update); + const isVersionChanged = settings.currentVersion !== currentVersion; + await loadExtensionSettings(settings, isVersionChanged, enableAutoUpdate); + await 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(loopCounter = 0) { + if (!settingsReady) { + console.warn('Settings not ready, aborting save'); + return; + } + + const MAX_RETRIES = 3; + if (TempResponseLength.isCustomized()) { + if (loopCounter < MAX_RETRIES) { + console.warn('Response length is currently being overridden, scheduling another save'); + saveSettingsDebounced(++loopCounter); + return; + } + console.error('Response length is currently being overridden, but the save loop has reached the maximum number of retries'); + TempResponseLength.restore(null); + } + + //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(t`Check the server connection and reload the page to prevent data loss.`, t`Settings could not be saved`); + console.log(exception); + console.log(jqXHR); + }, + }); +} + +/** + * Sets the generation parameters from a preset object. + * @param {{ genamt?: number, max_length?: number }} preset Preset object + */ +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, + isEdit: true, + }, + ); + + + 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 || (!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} 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} - 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 chatDetails = getCurrentChatDetails(); + const currentChat = chatDetails.sessionName; + const displayName = chatDetails.characterName; + const avatarImg = chatDetails.avatarImgURL; + + await displayChats('', currentChat, displayName, avatarImg, selected_group); + + const debouncedDisplay = debounce((searchQuery) => { + displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group); + }); + + // 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); +} + +async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group) { + try { + const trimExtension = (fileName) => String(fileName).replace('.jsonl', ''); + + const response = await fetch('/api/chats/search', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + query: searchQuery, + avatar_url: selected_group ? null : characters[this_chid].avatar, + group_id: selected_group || null, + }), + }); + + if (!response.ok) { + throw new Error('Search failed'); + } + + const filteredData = await response.json(); + $('#select_chat_div').empty(); + + filteredData.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); + + for (const chat of filteredData) { + const isSelected = trimExtension(currentChat) === trimExtension(chat.file_name); + const template = $('#past_chat_template .select_chat_block_wrapper').clone(); + template.find('.select_chat_block').attr('file_name', chat.file_name); + template.find('.avatar img').attr('src', avatarImg); + template.find('.select_chat_block_filename').text(chat.file_name); + template.find('.chat_file_size').text(`(${chat.file_size},`); + template.find('.chat_messages_num').text(`${chat.message_count} 💬)`); + template.find('.select_chat_block_mes').text(chat.preview_message); + template.find('.PastChat_cross').attr('file_name', chat.file_name); + template.find('.chat_messages_date').text(timestampToMoment(chat.last_mes).format('lll')); + + if (isSelected) { + template.find('.select_chat_block').attr('highlight', String(true)); + } + + $('#select_chat_div').append(template); + } + } catch (error) { + console.error('Error loading chats:', error); + toastr.error('Could not load chat data. Try reloading the page.'); + } +} + +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(t`Invalid process (no 'type')`); + return; + } + if (type !== 'group_create') { + var displayName = String(charId).replace('.png', ''); + } + + if (type === 'char_delete') { + toastr.warning(t`Character Deleted: ${displayName}`); + } + if (type === 'char_create') { + toastr.success(t`Character Created: ${displayName}`); + } + if (type === 'group_create') { + toastr.success(t`Group Created`); + } + if (type === 'group_delete') { + toastr.warning(t`Group Deleted`); + } + + if (type === 'char_import') { + toastr.success(t`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' || type === 'char_import_no_toast') { + // Find the page at which the character is located + const avatarFileName = charId; + 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. + * @param {(function(): Promise|boolean)} filter Filter function to determine if the prompt should be injected. + */ +export function setExtensionPrompt(key, value, position, depth, scan = false, role = extension_prompt_roles.SYSTEM, filter = null) { + extension_prompts[key] = { + value: String(value), + position: Number(position), + depth: Number(depth), + scan: !!scan, + role: Number(role ?? extension_prompt_roles.SYSTEM), + filter: filter, + }; +} + +/** + * 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 async function setScenarioOverride() { + if (!selected_group && !this_chid) { + console.warn('setScenarioOverride() -- no selected group or character'); + return; + } + + const metadataValue = chat_metadata['scenario'] || ''; + const isGroup = !!selected_group; + + const $template = $(await renderTemplateAsync('scenarioOverride')); + $template.find('[data-group="true"]').toggle(isGroup); + $template.find('[data-character="true"]').toggle(!isGroup); + // TODO: Why does this save on every character input? Save on popup close + $template.find('.chat_scenario').val(metadataValue).on('input', onScenarioOverrideInput); + $template.find('.remove_scenario_override').on('click', onScenarioOverrideRemoveClick); + + await callGenericPopup($template, POPUP_TYPE.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|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 {Promise} A promise that resolves when the popup is closed. + * @deprecated Use `callGenericPopup` instead. + */ +export function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) { + function getOkButtonText() { + if (['text', '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 ?? t`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'); + } + + $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; + const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[chat.length - 1].swipes.length); + const swipeRight = currentMessage.find('.swipe_right'); + const swipeLeft = currentMessage.find('.swipe_left'); + const swipeCounter = currentMessage.find('.swipes-counter'); + + if (swipeId !== undefined && (chat[chat.length - 1].swipes.length > 1 || swipeId > 0)) { + swipeLeft.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) { + swipeRight.css('display', 'flex').css('opacity', '0.3'); + swipeCounter.css('opacity', '0.3'); + } + if ((chat[chat.length - 1].swipes.length - swipeId) === 1) { + //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not + //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on + swipeRight.css('opacity', '0.7'); + swipeCounter.css('opacity', '0.7'); + } + + //allows for writing individual swipe counters for past messages + const lastSwipeCounter = $('.last_mes .swipes-counter'); + lastSwipeCounter.text(swipeCounterText).show(); +} + +export function hideSwipeButtons() { + chatElement.find('.swipe_right').hide(); + chatElement.find('.last_mes .swipes-counter').hide(); + chatElement.find('.swipe_left').hide(); +} + +/** + * Deletes a swipe from the chat. + * + * @param {number?} swipeId - The ID of the swipe to delete. If not provided, the current swipe will be deleted. + * @returns {Promise|undefined} - The ID of the new swipe after deletion. + */ +export async function deleteSwipe(swipeId = null) { + if (swipeId && (isNaN(swipeId) || swipeId < 0)) { + toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); + return; + } + + const lastMessage = chat[chat.length - 1]; + if (!lastMessage || !Array.isArray(lastMessage.swipes) || !lastMessage.swipes.length) { + toastr.warning(t`No messages to delete swipes from.`); + return; + } + + if (lastMessage.swipes.length <= 1) { + toastr.warning(t`Can't delete the last swipe.`); + return; + } + + swipeId = swipeId ?? lastMessage.swipe_id; + + if (swipeId < 0 || swipeId >= lastMessage.swipes.length) { + toastr.warning(t`Invalid swipe ID: ${swipeId + 1}`); + return; + } + + lastMessage.swipes.splice(swipeId, 1); + + if (Array.isArray(lastMessage.swipe_info) && lastMessage.swipe_info.length) { + lastMessage.swipe_info.splice(swipeId, 1); + } + + // Select the next swip, or the one before if it was the last one + const newSwipeId = Math.min(swipeId, lastMessage.swipes.length - 1); + lastMessage.swipe_id = newSwipeId; + lastMessage.mes = lastMessage.swipes[newSwipeId]; + + await saveChatConditional(); + await reloadCurrentChat(); + + return newSwipeId; +} + +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; + } +} + +/** + * Saves the chat to the server. + * @param {FormData} formData Form data to send to the server. + * @param {EventTarget} eventTarget Event target to trigger the event on. + */ +async function importCharacterChat(formData, eventTarget) { + const headers = getRequestHeaders(); + delete headers['Content-Type']; + const fetchResult = await fetch('/api/chats/import', { + method: 'POST', + body: formData, + headers: headers, + cache: 'no-cache', + }); + + if (fetchResult.ok) { + const data = await fetchResult.json(); + if (data.res) { + await displayPastChats(); + } + } + + if (eventTarget instanceof HTMLInputElement) { + eventTarget.value = ''; + } +} + +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(t`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(t`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: t`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; + const popup = new Popup(template, POPUP_TYPE.TEXT, '', { + wide: true, + large: true, + allowVerticalScrolling: true, + onClose: async () => { + if (menu_type !== 'create') { + await createOrEditCharacter(); + } + }, + }); + + for (let index = 0; index < getArray().length; index++) { + addAlternateGreeting(template, getArray()[index], index, getArray, popup); + } + + template.find('.add_alternate_greeting').on('click', function () { + const array = getArray(); + const index = array.length; + array.push(''); + addAlternateGreeting(template, '', index, getArray, popup); + updateAlternateGreetingsHintVisibility(template); + }); + + popup.show(); + updateAlternateGreetingsHintVisibility(template); +} + +/** + * Adds an alternate greeting to the template. + * @param {JQuery} template + * @param {string} greeting + * @param {number} index + * @param {() => any[]} getArray + * @param {Popup} popup + */ +function addAlternateGreeting(template, greeting, index, getArray, popup) { + 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 (event) { + event.preventDefault(); + event.stopPropagation(); + + if (confirm(t`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 + await popup.complete(POPUP_RESULT.AFFIRMATIVE); + 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); + } + + const headers = getRequestHeaders(); + delete headers['Content-Type']; + + if ($('#form_create').attr('actiontype') == 'createcharacter') { + if (String($('#character_name_pole').val()).length === 0) { + toastr.error(t`Name is required`); + return; + } + if (is_group_generating || is_send_press) { + toastr.error(t`Cannot create characters while generating. Stop the request and try again.`, t`Creation aborted`); + return; + } + try { + //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)); + + const fetchResult = await fetch(url, { + method: 'POST', + headers: headers, + body: formData, + cache: 'no-cache', + }); + + if (!fetchResult.ok) { + throw new Error('Fetch result is not ok'); + } + + const avatarId = await fetchResult.text(); + + $('#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 = ''; + + $('#add_avatar_button').replaceWith( + $('#add_avatar_button').val('').clone(true), + ); + + let oldSelectedChar = null; + if (this_chid !== undefined) { + oldSelectedChar = characters[this_chid].avatar; + } + + console.log(`new avatar id: ${avatarId}`); + createTagMapFromList('#tagList', avatarId); + await getCharacters(); + + select_rm_info('char_create', avatarId, oldSelectedChar); + + crop_data = undefined; + + } catch (error) { + console.error('Error creating character', error); + toastr.error(t`Failed to create character`); + } + } else { + try { + 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); + } + } + + const fetchResult = await fetch(url, { + method: 'POST', + headers: headers, + body: formData, + cache: 'no-cache', + }); + + if (!fetchResult.ok) { + throw new Error('Fetch result is not ok'); + } + + 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; + await 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(); + } + } catch (error) { + console.log(error); + toastr.error(t`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.`); + } + } +} + +/** + * Formats a counter for a swipe view. + * @param {number} current The current number of items. + * @param {number} total The total number of items. + * @returns {string} The formatted counter. + */ +function formatSwipeCounter(current, total) { + if (isNaN(current) || isNaN(total)) { + return ''; + } + + return `${current}\u200b/\u200b${total}`; +} + +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: animation_duration > 0 ? swipe_duration : 0, + 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: animation_duration > 0 ? swipe_duration : 0, + 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: animation_duration > 0 ? swipe_duration : 0, + 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: animation_duration > 0 ? swipe_duration : 0, + easing: animation_easing, + queue: false, + complete: function () { + + }, + }); + }, + }); + }, + }); + } + if (chat[chat.length - 1]['swipe_id'] < 0) { + chat[chat.length - 1]['swipe_id'] = 0; + } +} + +// 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.find('.swipe_right'); + let this_mes_div = this_div.parent().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.find('.mes_block .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: animation_duration > 0 ? swipe_duration : 0, + 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: animation_duration > 0 ? swipe_duration : 0, + 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: animation_duration > 0 ? swipe_duration : 0, + 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: animation_duration > 0 ? swipe_duration : 0, + easing: animation_easing, + queue: false, + complete: function () { + + }, + }); + }, + }); + }, + }); + } +}; + +const CONNECT_API_MAP = { + // Default APIs not contined inside text gen / chat gen + 'kobold': { + selected: 'kobold', + button: '#api_button', + }, + 'horde': { + selected: 'koboldhorde', + }, + 'novel': { + selected: 'novel', + button: '#api_button_novel', + }, + 'koboldcpp': { + selected: 'textgenerationwebui', + button: '#api_button_textgenerationwebui', + type: textgen_types.KOBOLDCPP, + }, + // KoboldCpp alias + 'kcpp': { + selected: 'textgenerationwebui', + button: '#api_button_textgenerationwebui', + type: textgen_types.KOBOLDCPP, + }, + 'openai': { + selected: 'openai', + button: '#api_button_openai', + source: chat_completion_sources.OPENAI, + }, + // OpenAI alias + 'oai': { + selected: 'openai', + button: '#api_button_openai', + source: chat_completion_sources.OPENAI, + }, + // Google alias + 'google': { + selected: 'openai', + button: '#api_button_openai', + source: chat_completion_sources.MAKERSUITE, + }, + // OpenRouter special naming, to differentiate between chat comp and text comp + 'openrouter': { + selected: 'openai', + button: '#api_button_openai', + source: chat_completion_sources.OPENROUTER, + }, + 'openrouter-text': { + selected: 'textgenerationwebui', + button: '#api_button_textgenerationwebui', + type: textgen_types.OPENROUTER, + }, +}; + +// Collect all unique API names in an array +export const UNIQUE_APIS = [...new Set(Object.values(CONNECT_API_MAP).map(x => x.selected))]; + +// Fill connections map from textgen_types and chat_completion_sources +for (const textGenType of Object.values(textgen_types)) { + if (CONNECT_API_MAP[textGenType]) continue; + CONNECT_API_MAP[textGenType] = { + selected: 'textgenerationwebui', + button: '#api_button_textgenerationwebui', + type: textGenType, + }; +} +for (const chatCompletionSource of Object.values(chat_completion_sources)) { + if (CONNECT_API_MAP[chatCompletionSource]) continue; + CONNECT_API_MAP[chatCompletionSource] = { + selected: 'openai', + button: '#api_button_openai', + source: chatCompletionSource, + }; +} + +async function selectContextCallback(args, name) { + if (!name) { + return power_user.context.preset; + } + + const quiet = isTrueBoolean(args?.quiet); + const contextNames = context_presets.map(preset => preset.name); + const fuse = new Fuse(contextNames); + const result = fuse.search(name); + + if (result.length === 0) { + !quiet && toastr.warning(t`Context template '${name}' not found`); + return ''; + } + + const foundName = result[0].item; + selectContextPreset(foundName, { quiet: quiet }); + return foundName; +} + +async function selectInstructCallback(args, name) { + if (!name) { + return power_user.instruct.enabled || isTrueBoolean(args?.forceGet) ? power_user.instruct.preset : ''; + } + + const quiet = isTrueBoolean(args?.quiet); + const instructNames = instruct_presets.map(preset => preset.name); + const fuse = new Fuse(instructNames); + const result = fuse.search(name); + + if (result.length === 0) { + !quiet && toastr.warning(t`Instruct template '${name}' not found`); + return ''; + } + + const foundName = result[0].item; + selectInstructPreset(foundName, { quiet: quiet }); + return foundName; +} + +async function enableInstructCallback() { + $('#instruct_enabled').prop('checked', true).trigger('input').trigger('change'); + return ''; +} + +async function disableInstructCallback() { + $('#instruct_enabled').prop('checked', false).trigger('input').trigger('change'); + return ''; +} + +/** + * @param {string} text API name + */ +async function connectAPISlash(args, 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; + } + + console.error('FIXME: The current API is not in the API map'); + return ''; + } + + const apiConfig = CONNECT_API_MAP[text.toLowerCase()]; + if (!apiConfig) { + toastr.error(t`Error: ${text} is not a valid API`); + return ''; + } + + let connectionRequired = false; + + if (main_api !== apiConfig.selected) { + $(`#main_api option[value='${apiConfig.selected || text}']`).prop('selected', true); + $('#main_api').trigger('change'); + connectionRequired = true; + } + + if (apiConfig.source && oai_settings.chat_completion_source !== apiConfig.source) { + $(`#chat_completion_source option[value='${apiConfig.source}']`).prop('selected', true); + $('#chat_completion_source').trigger('change'); + connectionRequired = true; + } + + if (apiConfig.type && textgen_settings.type !== apiConfig.type) { + $(`#textgen_type option[value='${apiConfig.type}']`).prop('selected', true); + $('#textgen_type').trigger('change'); + connectionRequired = true; + } + + if (connectionRequired && apiConfig.button) { + $(apiConfig.button).trigger('click'); + } + + const quiet = isTrueBoolean(args?.quiet); + const toast = quiet ? jQuery() : toastr.info(t`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 10 seconds, skipping.'); + } + + toastr.clear(toast); + return text; +} + +/** + * Imports supported files dropped into the app window. + * @param {File[]} files Array of files to process + * @param {Map} [data] Extra data to pass to the import function + * @returns {Promise} + */ +export async function processDroppedFiles(files, data = new Map()) { + const allowedMimeTypes = [ + 'application/json', + 'image/png', + 'application/yaml', + 'application/x-yaml', + 'text/yaml', + 'text/x-yaml', + ]; + + const allowedExtensions = [ + 'charx', + ]; + + const avatarFileNames = []; + for (const file of files) { + const extension = file.name.split('.').pop().toLowerCase(); + if (allowedMimeTypes.some(x => file.type.startsWith(x)) || allowedExtensions.includes(extension)) { + const preservedName = data instanceof Map && data.get(file); + const avatarFileName = await importCharacter(file, { preserveFileName: preservedName }); + if (avatarFileName !== undefined) { + avatarFileNames.push(avatarFileName); + } + } else { + toastr.warning(t`Unsupported file type: ` + file.name); + } + } + + if (avatarFileNames.length > 0) { + await importCharactersTags(avatarFileNames); + selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); + } +} + +/** + * Imports tags for the given characters + * @param {string[]} avatarFileNames character avatar filenames whose tags are to import + */ +async function importCharactersTags(avatarFileNames) { + await getCharacters(); + for (let i = 0; i < avatarFileNames.length; i++) { + if (power_user.tag_import_setting !== tag_import_setting.NONE) { + const importedCharacter = characters.find(character => character.avatar === avatarFileNames[i]); + await importTags(importedCharacter); + } + } +} + +/** + * Selects the given imported char + * @param {string} charId char to select + */ +function selectImportedChar(charId) { + let oldSelectedChar = null; + if (this_chid !== undefined) { + oldSelectedChar = characters[this_chid].avatar; + } + select_rm_info('char_import_no_toast', charId, oldSelectedChar); +} + +/** + * Imports a character from a file. + * @param {File} file File to import + * @param {object} [options] - Options + * @param {string} [options.preserveFileName] Whether to preserve original file name + * @param {Boolean} [options.importTags=false] Whether to import tags + * @returns {Promise} + */ +async function importCharacter(file, { preserveFileName = '', importTags = false } = {}) { + if (is_group_generating || is_send_press) { + toastr.error(t`Cannot import characters while generating. Stop the request and try again.`, t`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); + if (preserveFileName) formData.append('preserved_name', 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(t`The file is likely invalid or corrupted.`, t`Could not import character`); + return; + } + + if (data.file_name !== undefined) { + $('#character_search_bar').val('').trigger('input'); + + toastr.success(t`Character Created: ${String(data.file_name).replace('.png', '')}`); + let avatarFileName = `${data.file_name}.png`; + if (importTags) { + await importCharactersTags([avatarFileName]); + + selectImportedChar(data.file_name); + } + return avatarFileName; + } +} + +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(t`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 ''; +} + +export async function doNewChat({ deleteCurrentChat = false } = {}) { + //Make a new chat for selected character + if ((!selected_group && this_chid == undefined) || menu_type == 'create') { + return; + } + + //Fix it; New chat doesn't create while open create character menu + await clearChat(); + chat.length = 0; + + chat_file_for_del = getCurrentChatDetails()?.sessionName; + + // Make it easier to find in backups + if (deleteCurrentChat) { + await saveChatConditional(); + } + + if (selected_group) { + await createNewGroupChat(selected_group); + if (deleteCurrentChat) 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 (deleteCurrentChat) await delChat(chat_file_for_del + '.jsonl'); + } + +} + +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(t`Name must be provided as an argument to rename this chat.`); + return ''; + } + + const currentChatName = getCurrentChatId(); + if (!currentChatName) { + toastr.warning(t`No chat selected that can be renamed.`); + return ''; + } + + await renameChat(currentChatName, chatName); + + toastr.success(t`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.trim()}.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 (data.sanitizedFileName) { + newName = data.sanitizedFileName; + } + + 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() { + power_user.charListGrid = !power_user.charListGrid; + document.body.classList.toggle('charListGrid', power_user.charListGrid); + 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} this_chid - The character ID to be deleted. + * @param {boolean} delete_chats - Whether to delete chats or not. + */ +export async function handleDeleteCharacter(this_chid, delete_chats) { + if (!characters[this_chid]) { + return; + } + + await deleteCharacter(characters[this_chid].avatar, { deleteChats: delete_chats }); +} + +/** + * Deletes a character completely, including associated chats if specified + * + * @param {string|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} - A promise that resolves when the character is successfully deleted + */ +export async function deleteCharacter(characterKey, { deleteChats = true } = {}) { + if (!Array.isArray(characterKey)) { + characterKey = [characterKey]; + } + + for (const key of characterKey) { + const character = characters.find(x => x.avatar == key); + if (!character) { + toastr.warning(t`Character ${key} not found. Skipping deletion.`); + continue; + } + + 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) { + toastr.error(`${response.status} ${response.statusText}`, t`Failed to delete character`); + continue; + } + + delete tag_map[character.avatar]; + select_rm_info('char_delete', character.name); + + if (deleteChats) { + for (const chat of pastChats) { + const name = chat.file_name.replace('.jsonl', ''); + await eventSource.emit(event_types.CHAT_DELETED, name); + } + } + + await eventSource.emit(event_types.CHARACTER_DELETED, { id: chid, character: character }); + } + + await removeCharacterFromUI(); +} + +/** + * 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, fetching updated list of characters. + * It also ensures to save the settings after all the operations. + */ +async function removeCharacterFromUI() { + preserveNeutralChat(); + await clearChat(); + $('#character_cross').trigger('click'); + resetChatState(); + $(document.getElementById('rm_button_selected_ch')).children('h2').text(''); + restoreNeutralChat(); + await getCharacters(); + await printMessages(); + saveSettingsDebounced(); +} + +async function newAssistantChat() { + await clearChat(); + chat.splice(0, chat.length); + chat_metadata = {}; + setCharacterName(neutralCharacterName); + sendSystemMessage(system_message_types.ASSISTANT_NOTE); +} + +function doTogglePanels() { + $('#option_settings').trigger('click'); + return ''; +} + +/** + * Event handler to open a navbar drawer when a drawer open button is clicked. + * Handles click events on .drawer-opener elements. + * Opens the drawer associated with the clicked button according to the data-target attribute. + * @returns {void} + */ +function doDrawerOpenClick() { + const targetDrawerID = $(this).attr('data-target'); + const drawer = $(`#${targetDrawerID}`); + const drawerToggle = drawer.find('.drawer-toggle'); + const drawerWasOpenAlready = drawerToggle.parent().find('.drawer-content').hasClass('openDrawer'); + if (drawerWasOpenAlready || drawer.hasClass('resizing')) { return; } + doNavbarIconClick.call(drawerToggle); +} + +/** + * Event handler to open or close a navbar drawer when a navbar icon is clicked. + * Handles click events on .drawer-toggle elements. + * @returns {void} + */ +function doNavbarIconClick() { + const icon = $(this).find('.drawer-icon'); + const drawer = $(this).parent().find('.drawer-content'); + if (drawer.hasClass('resizing')) { return; } + const drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer'); + const targetDrawerID = $(this).parent().find('.drawer-content').attr('id'); + const pinnedDrawerClicked = drawer.hasClass('pinnedOpen'); + + if (!drawerWasOpenAlready) { //to open the drawer + $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); + }); + $('.openIcon').not('.drawerPinnedOpen').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').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + elementDisplayStyle: 'flex', + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + favsToHotswap(); + $('#rm_print_characters_block').trigger('scroll'); + }, + }); + }); + } else { + $(this).closest('.drawer').find('.drawer-content').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); + }); + } + + // Set the height of "autoSetHeight" textareas within the drawer to their scroll height + if (!CSS.supports('field-sizing', 'content')) { + $(this).closest('.drawer').find('.drawer-content textarea.autoSetHeight').each(async function () { + await resetScrollHeight($(this)); + return; + }); + } + + } else if (drawerWasOpenAlready) { //to close manually + icon.toggleClass('closedIcon openIcon'); + + if (pinnedDrawerClicked) { + $(drawer).addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.classList.remove('resizing'); + }, + }); + }); + } + else { + $('.openDrawer').not('.pinnedOpen').addClass('resizing').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: function (el) { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); + }); + } + + drawer.toggleClass('closedDrawer openDrawer'); + } +} + +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('forceOnboarding', 'Force onboarding', 'Forces the onboarding process to restart.', async () => { + firstRun = true; + await saveSettings(); + location.reload(); + }); + + 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('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => { + localStorage.setItem('RegenerateWithCtrlEnter', localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true'); + toastr.info('Regenerate warning is now ' + (localStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled')); + }); + + 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 Template: ${power_user.context.preset} +Instruct Template: ${power_user.instruct.preset} +API Settings: ${JSON.stringify(getSettingsContents[getSettingsContents.main_api + '_settings'], null, 2)} +\`\`\` + `; + + //console.log(getSettingsContents) + //console.log(logMessage); + + try { + await copyText(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 () { + async function doForceSave() { + await saveSettings(); + await saveChatConditional(); + toastr.success('Chat and settings saved.'); + return ''; + } + + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'dupe', + callback: duplicateCharacter, + helpString: 'Duplicates the currently selected character.', + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'api', + callback: connectAPISlash, + returns: 'the current API', + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'quiet', + description: 'Suppress the toast message on connection', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'API to connect to', + typeList: [ARGUMENT_TYPE.STRING], + enumList: Object.entries(CONNECT_API_MAP).map(([api, { selected }]) => + new SlashCommandEnumValue(api, selected, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === selected)), + selected[0].toUpperCase() ?? enumIcons.default)), + }), + ], + helpString: ` +
+ Connect to an API. If no argument is provided, it will return the currently connected API. +
+
+ Available APIs: +
${Object.keys(CONNECT_API_MAP).join(', ')}
+
+ `, + })); + 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: ` +
+ Calls an impersonation response, with an optional additional prompt. +
+
+ If await=true named argument is passed, the command will wait for the impersonation to end before continuing. +
+
+ Example: +
    +
  • +
    /impersonate What is the meaning of life?
    +
  • +
+
+ `, + })); + 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: 'tempchat', + callback: () => { + return new Promise((resolve, reject) => { + const eventCallback = async (chatId) => { + if (chatId) { + return reject('Not in a temporary chat'); + } + await newAssistantChat(); + return resolve(''); + }; + eventSource.once(event_types.CHAT_CHANGED, eventCallback); + doCloseChat(); + setTimeout(() => { + reject('Failed to open temporary chat'); + eventSource.removeListener(event_types.CHAT_CHANGED, eventCallback); + }, debounce_timeout.relaxed); + }); + }, + helpString: 'Opens a temporary chat with Assistant.', + })); + 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 template', + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'quiet', + description: 'Suppress the toast message on template change', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + SlashCommandNamedArgument.fromProps({ + name: 'forceGet', + description: 'Force getting a name even if instruct mode is disabled', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'instruct template name', + typeList: [ARGUMENT_TYPE.STRING], + enumProvider: () => instruct_presets.map(preset => new SlashCommandEnumValue(preset.name, null, enumTypes.enum, enumIcons.preset)), + }), + ], + helpString: ` +
+ Selects instruct mode template by name. Enables instruct mode if not already enabled. + Gets the current instruct template if no name is provided and instruct mode is enabled or forceGet=true is passed. +
+
+ Example: +
    +
  • +
    /instruct creative
    +
  • +
+
+ `, + })); + 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: 'instruct-state', + aliases: ['instruct-toggle'], + helpString: 'Gets the current instruct mode state. If an argument is provided, it will set the instruct mode state.', + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'instruct mode state', + typeList: [ARGUMENT_TYPE.BOOLEAN], + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], + callback: async (_args, state) => { + if (!state || typeof state !== 'string') { + return String(power_user.instruct.enabled); + } + + const newState = isTrueBoolean(state); + newState ? enableInstructCallback() : disableInstructCallback(); + return String(power_user.instruct.enabled); + }, + })); + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ + name: 'context', + callback: selectContextCallback, + returns: 'template name', + namedArgumentList: [ + SlashCommandNamedArgument.fromProps({ + name: 'quiet', + description: 'Suppress the toast message on template change', + typeList: [ARGUMENT_TYPE.BOOLEAN], + defaultValue: 'false', + enumList: commonEnumProviders.boolean('trueFalse')(), + }), + ], + unnamedArgumentList: [ + SlashCommandArgument.fromProps({ + description: 'context template 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); + + $(document).on('click', '.api_loading', () => cancelStatusCheck('Canceled because connecting was manually canceled')); + + //////////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, #mes_impersonate').on('click', () => { + if (S_TAPreviouslyFocused) { + $('#send_textarea').focus(); + } + }); + $(document).click(event => { + if ($(':focus').attr('id') !== 'send_textarea') { + var validIDs = ['options_button', 'send_but', 'mes_impersonate', '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 /////// + + //limit swiping to only last message clicks + $(document).on('click', '.last_mes .swipe_right', swipe_right); + $(document).on('click', '.last_mes .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_impersonate').on('click', function () { + $('#option_impersonate').trigger('click'); + }); + + $('#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); + }); + + const cssAutofit = CSS.supports('field-sizing', 'content'); + if (!cssAutofit) { + /** + * Sets the scroll height of the edit textarea to fit the content. + * @param {HTMLTextAreaElement} e Textarea element to auto-fit + */ + function autoFitEditTextArea(e) { + const scrollTop = chatElement.scrollTop(); + e.style.height = '0px'; + const newHeight = e.scrollHeight + 4; + e.style.height = `${newHeight}px`; + chatElement.scrollTop(scrollTop); + } + const autoFitEditTextAreaDebounced = debounce(autoFitEditTextArea, debounce_timeout.short); + document.addEventListener('input', e => { + if (e.target instanceof HTMLTextAreaElement && e.target.classList.contains('edit_textarea')) { + const scrollbarShown = e.target.clientWidth < e.target.offsetWidth && e.target.offsetHeight >= window.innerHeight * 0.75; + const immediately = (e.target.scrollHeight > e.target.offsetHeight && !scrollbarShown) || e.target.value === ''; + immediately ? autoFitEditTextArea(e.target) : autoFitEditTextAreaDebounced(e.target); + } + }); + } + + const chatElementScroll = document.getElementById('chat'); + const chatScrollHandler = function () { + if (power_user.waifuMode) { + scrollLock = true; + return; + } + + const scrollIsAtBottom = Math.abs(chatElementScroll.scrollHeight - chatElementScroll.clientHeight - chatElementScroll.scrollTop) < 1; + + // Resume autoscroll if the user scrolls to the bottom + if (scrollLock && scrollIsAtBottom) { + scrollLock = false; + } + + // Cancel autoscroll if the user scrolls up + if (!scrollLock && !scrollIsAtBottom) { + scrollLock = true; + } + }; + chatElementScroll.addEventListener('wheel', chatScrollHandler, { passive: true }); + chatElementScroll.addEventListener('touchmove', chatScrollHandler, { passive: true }); + + $(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('

Delete the Chat File?

', '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); + + 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 (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', async function () { + if (!this_chid) { + toastr.warning('No character selected.'); + return; + } + + let deleteChats = false; + + const confirm = await Popup.show.confirm(t`Delete the character?`, await renderTemplateAsync('deleteConfirm'), { + onClose: () => deleteChats = !!$('#del_char_checkbox').prop('checked'), + }); + if (!confirm) { + return; + } + + await deleteCharacter(characters[this_chid].avatar, { deleteChats: deleteChats }); + }); + + //////// 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 = await renderTemplateAsync('chatRename'); + 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 }, + { id: 'api_key_featherless', secret: SECRET_KEYS.FEATHERLESS }, + { id: 'api_key_huggingface', secret: SECRET_KEYS.HUGGINGFACE }, + { id: 'api_key_generic', secret: SECRET_KEYS.GENERIC }, + ]; + + 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(); + }); + + const button = $('#options_button'); + const menu = $('#options'); + let isOptionsMenuVisible = false; + + function showMenu() { + showBookmarksButtons(); + menu.fadeIn(animation_duration); + optionsPopper.update(); + isOptionsMenuVisible = true; + } + + function hideMenu() { + menu.fadeOut(animation_duration); + optionsPopper.update(); + isOptionsMenuVisible = false; + } + + function isMouseOverButtonOrMenu() { + return menu.is(':hover, :focus-within') || button.is(':hover, :focus'); + } + + button.on('click', function () { + if (isOptionsMenuVisible) { + hideMenu(); + } else { + showMenu(); + } + }); + $(document).on('click', function () { + if (!isOptionsMenuVisible) return; + if (!isMouseOverButtonOrMenu()) { 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) { + let deleteCurrentChat = false; + const result = await Popup.show.confirm(t`Start new chat?`, await renderTemplateAsync('newChatConfirm'), { + onClose: () => deleteCurrentChat = !!$('#del_chat_checkbox').prop('checked'), + }); + if (!result) { + return; + } + + await doNewChat({ deleteCurrentChat: deleteCurrentChat }); + } + if (!selected_group && this_chid === undefined && !is_send_press) { + await newAssistantChat(); + } + } + + 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 (this_edit_mes_id) return; // don't proceed if editing a message + + 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); + sendSystemMessage(system_message_types.WELCOME_PROMPT); + await getClientVersion(); + await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); + } 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', async function () { + await doNewChat({ deleteCurrentChat: false }); + $('#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').on('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); + await 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('Canceled because main api changed'); + 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); + }); + + $(document).on('pointerup', '.mes_copy', async function () { + if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) { + try { + const messageId = $(this).closest('.mes').attr('mesid'); + const text = chat[messageId]['mes']; + await copyText(text); + toastr.info('Copied!', '', { timeOut: 2000 }); + } catch (err) { + console.error('Failed to copy: ', err); + } + } + }); + + $(document).on('pointerup', '.mes_prompt', async function () { + let mesIdForItemization = $(this).closest('.mes').attr('mesId'); + console.log(`looking for mesID: ${mesIdForItemization}`); + if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) { + await promptItemize(itemizedPrompts, mesIdForItemization); + } + }); + + //******************** + //***Message Editor*** + $(document).on('click', '.mes_edit', async function () { + if (this_chid !== undefined || selected_group || name2 === neutralCharacterName) { + // 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( + '', + ); + $('#curEditTextarea').val(text); + let edit_textarea = $(this) + .closest('.mes_block') + .find('.edit_textarea'); + if (!cssAutofit) { + 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 $hint = $(e.target); + const $buttons = $hint.siblings('.extraMesButtons'); + + $hint.transition({ + opacity: 0, + duration: animation_duration, + easing: animation_easing, + complete: function () { + $hint.hide(); + $buttons + .addClass('visible') + .css({ + opacity: 0, + display: 'flex', + }) + .transition({ + opacity: 1, + duration: animation_duration, + easing: animation_easing, + }); + }, + }); + }); + + $(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) { + const $visibleButtons = $('.extraMesButtons.visible'); + + if (!$visibleButtons.length) { + return; + } + + const $hiddenHints = $('.extraMesButtonsHint:hidden'); + + // Transition out the .extraMesButtons first + $visibleButtons.transition({ + opacity: 0, + duration: animation_duration, + easing: animation_easing, + complete: function () { + // Hide the .extraMesButtons after the transition + $(this) + .hide() + .removeClass('visible'); + + // Transition the .extraMesButtonsHint back in + $hiddenHints + .show() + .transition({ + opacity: 0.3, + duration: animation_duration, + easing: animation_easing, + complete: function () { + $(this).css('opacity', ''); + }, + }); + }, + }); + } + }); + + $(document).on('click', '.mes_edit_cancel', async 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')); + + await eventSource.emit(event_types.MESSAGE_UPDATED, this_edit_mes_id); + 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 callGenericPopup('Create a copy of this message?', POPUP_TYPE.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 canDeleteSwipe = (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); + + let deleteOnlySwipe = false; + if (power_user.confirm_message_delete && fromSlashCommand !== true) { + const result = await callGenericPopup(t`Are you sure you want to delete this message?`, POPUP_TYPE.CONFIRM, null, { + okButton: canDeleteSwipe ? t`Delete Swipe` : t`Delete Message`, + cancelButton: 'Cancel', + customButtons: canDeleteSwipe ? [t`Delete Message`] : null, + }); + if (!result) { + return; + } + deleteOnlySwipe = canDeleteSwipe && result === 1; // Default button, not the custom one + } + + const messageElement = $(this).closest('.mes'); + if (!messageElement) { + return; + } + + if (deleteOnlySwipe) { + const message = chat[this_edit_mes_id]; + const swipe_id = message.swipe_id; + await deleteSwipe(swipe_id); + return; + } + + chat.splice(this_edit_mes_id, 1); + messageElement.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; + } + + const avatarFileNames = []; + for (const file of e.target.files) { + const avatarFileName = await importCharacter(file); + if (avatarFileName !== undefined) { + avatarFileNames.push(avatarFileName); + } + } + + if (avatarFileNames.length > 0) { + await importCharactersTags(avatarFileNames); + selectImportedChar(avatarFileNames[avatarFileNames.length - 1]); + } + }); + + $('#export_button').on('click', function () { + isExportPopupOpen = !isExportPopupOpen; + $('#export_format_popup').toggle(isExportPopupOpen); + exportPopper.update(); + }); + + $(document).on('click', '.export_format', async function () { + const format = $(this).data('format'); + + if (!format) { + return; + } + + $('#export_format_popup').hide(); + isExportPopupOpen = false; + exportPopper.update(); + + // 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); + } + }); + //**************************CHAT IMPORT EXPORT*************************// + $('#chat_import_button').click(function () { + $('#chat_import_file').click(); + }); + + $('#chat_import_file').on('change', async function (e) { + const file = e.target.files[0]; + + if (!file) { + return; + } + + const 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; + } + + const format = ext[1].toLowerCase(); + $('#chat_import_file_type').val(format); + + const formData = new FormData($('#form_import_chat').get(0)); + formData.append('user_name', name1); + $('#select_chat_div').html(''); + + if (selected_group) { + await importGroupChat(formData, e.originalEvent.target); + } else { + await importCharacterChat(formData, e.originalEvent.target); + } + }); + + $('#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 duplicateCharacter(); + }); + + $(document).on('click', '.mes_stop', function () { + stopGeneration(); + }); + + $(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(); + }); + + $(document).on('click', '.drawer-opener', doDrawerOpenClick); + + $('.drawer-toggle').on('click', doNavbarIconClick); + + $('html').on('touchstart mousedown', function (e) { + var clickTarget = $(e.target); + + if (isExportPopupOpen + && clickTarget.closest('#export_button').length == 0 + && clickTarget.closest('#export_format_popup').length == 0) { + $('#export_format_popup').hide(); + isExportPopupOpen = false; + exportPopper.update(); + } + + 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').each((_, el) => { + slideToggle(el, { + ...getSlideToggleOptions(), + onAnimationEnd: (el) => { + el.closest('.drawer-content').classList.remove('resizing'); + }, + }); + }); + $('.openIcon').not('.drawerPinnedOpen').toggleClass('closedIcon openIcon'); + $('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer'); + } + } + } + }); + + $(document).on('click', '.inline-drawer-toggle', function (e) { + if ($(e.target).hasClass('text_pole')) { + return; + } + const drawer = $(this).closest('.inline-drawer'); + const icon = drawer.find('.inline-drawer-icon'); + const drawerContent = drawer.find('.inline-drawer-content'); + icon.toggleClass('down up'); + icon.toggleClass('fa-circle-chevron-down fa-circle-chevron-up'); + drawerContent.stop().slideToggle({ + complete: () => { + $(this).css('height', ''); + }, + }); + + // Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height + if (!CSS.supports('field-sizing', 'content')) { + drawerContent.find('textarea.autoSetHeight').each(async function () { + await resetScrollHeight($(this)); + return; + }); + } + }); + + $(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) || /^\/?img\/(?:.+)/.test(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.addEventListener('click', function (e) { + if (!(e.target instanceof HTMLElement)) return; + if (e.target.matches('#OpenAllWIEntries')) { + document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => { + toggleDrawer(drawer, true); + }); + } else if (e.target.matches('#CloseAllWIEntries')) { + document.querySelectorAll('#world_popup_entries_list .inline-drawer').forEach((/** @type {HTMLElement} */ drawer) => { + toggleDrawer(drawer, false); + }); + } + }); + + $(document).on('click', '.open_alternate_greetings', openAlternateGreetings); + /* $('#set_character_world').on('click', openCharacterWorldPopup); */ + + $(document).on('focus', 'input.auto-select, textarea.auto-select', function () { + if (!power_user.enable_auto_select_input) return; + const control = $(this)[0]; + if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) { + control.select(); + console.debug('Auto-selecting content of input control', control); + } + }); + + $(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': + await setScenarioOverride(); + break; + case 'renameCharButton': + renameCharacter(); + 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 Popup.show.confirm('Open Source', `Do you want to open the link to ${url.hostname} in a new tab?${url}`); + 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 Popup.show.confirm('Replace Character', '

Choose a new character card to replace this character with.

All chats, assets and group memberships will be preserved, but local changes to the character data will be lost.
Proceed?'); + if (confirm) { + async function uploadReplacementCard(e) { + const file = e.target.files[0]; + + if (!file) { + return; + } + + try { + const chatFile = characters[this_chid]['chat']; + const data = new Map(); + data.set(file, characters[this_chid].avatar); + await processDroppedFiles([file], data); + await openCharacterChat(chatFile); + await fetch(getThumbnailUrl('avatar', characters[this_chid].avatar), { cache: 'no-cache' }); + } 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], { importSetting: tag_import_setting.ASK }); + } break; + /*case 'delete_button': + popup_type = "del_ch"; + callPopup(` +

Delete the character?

+ THIS IS PERMANENT!

+ THIS WILL ALSO DELETE ALL
+ OF THE CHARACTER'S CHAT FILES.

` + ); + 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; + + $(document).on('input', '.range-block-counter input, .neo-range-input', function () { + valueBeforeManualInput = $(this).val(); + console.log(valueBeforeManualInput); + }); + + $(document).on('change', '.range-block-counter input, .neo-range-input', function (e) { + e.target.focus(); + e.target.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true })); + }); + + $(document).on('keydown', '.range-block-counter input, .neo-range-input', 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', { forced: true }); + } 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); + } + } + } + }); + + $(document).on('keyup', '.range-block-counter input, .neo-range-input', function () { + valueBeforeManualInput = $(this).val(); + console.log(valueBeforeManualInput); + isManualInput = true; + }); + + //trigger slider changes when user clicks away + $(document).on('mouseup blur', '.range-block-counter input, .neo-range-input', 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', { forced: true }); + } 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(); +}); + + +// +// +// DAVE MOD - MAIN STUFF: +// +// + +// +// VARS +// + +// Streaming Setup/Control. + +var nIntervId_stream=""; clearInterval(nIntervId_stream); + +// +// Main filters / replace || LOWERCASE || processed in order listed. +// + + + +// +// End main arrays +// + +// Activates System role based on hitting a trigger. +// text added to system role. + +var dave_trigger=""; var dave_old_trigger=""; var dave_trigger_count=0; + +var dave_replace_phrase=""; var dave_dup_count=0; var dave_old_replace=""; + +var dave_flag=0; var dave_op=0; + +var dave_oldholdtext=""; var dave_holdtext_count=0; + +var dave_gib_active=0; + +var dave_hard_chop=0; var dave_hard_chop2=0; var dave_soft_chop=0; + + +// +// RESET +// + + +function dave_reset_hard() { +dave_trigger=""; +dave_old_trigger=""; +dave_trigger_count=0; +dave_dup_count=0; +dave_old_replace=""; +dave_oldholdtext=""; +dave_holdtext_count=0; +dave_watch_size=-1; +} + + +// +// Main "watcher" - Streaming +// + +var dave_watch_size=0; +var dave_message_watch_old=""; +var dave_match_index=0; +var dave_trigger_point=0; +var dave_trigger_count_old=0; + +// +// Sampler Controls +// + +var dave_s={}; + +// do not edit. +var dave_error_warn=0; + + + +// ----------------------------------------------- DO NOT EDIT ABOVE THIS LINE +// +// AUTOCORRECT: USER ADJUSTABLE SETTINGS: +// +// Gibbish / Auto-Correct Samplers +// MAIN SYSTEMS/SETTINGS +// +// For "on/off" -> 1=on, 0=off +// + +// Turn autocorrect on/off +// This turns AUTO CORRECT on / off +dave_s['gib_on']=1; + +// last XXX characters to "watch" -> characters. +dave_s['sample_size']=500; + + +// +// WORDS CHECKING: +// +// words to check +dave_s['gib_words']=50; + +// next 5 settings in characters. +dave_s['gib_min']=200; +dave_s['gib_words_repeat_lrg']=5; +dave_s['gib_words_repeat_sml']=15; +dave_s['gib_words_max_size']=24; +dave_s['gib_words_max_size_dash']=48; + + +// +// SENTENCE CHECKING +// + +// sentences (scans entire output) +// "min" and "min_size" are in characters. +dave_s['gib_sent_min']=200; +dave_s['gib_sent_min_size']=20; + +// actual sentences required. +dave_s['gib_sent_min_required']=3; +dave_s['gib_sent_repeat']=2; + + +// +// Paragraph CHECKING +// + +// "min" and "min_size" are in characters. +dave_s['gib_para_min_size']=40; +dave_s['gib_para_min']=200; + +// min # required for checking +dave_s['gib_para_min_required']=2; + + +// +// regex check -> Allowed characters PER word/entire output +// "Gibbishish" Systems F +// +// NOTE: will block "piñata" , "FAÇADE", "clichés" . => must add the "ñ" and "Ç", "é" +// but will not block "pinata", "FACADE" and "cliches" . +// +// Contains ONLY allowed characters / symbols in all words, tokens. +// +// If the letter, symbol is NOT here, the word will be auto-rejected. +// +// Only add/take away between the first and last [ ] s unless you know what your are doing. +// + +const regex_dave = /^[~`!@#$%^&*()_+–…©™’\'\-\. \n=[\]\{}|;':",.\/<>?a-zA-Z0-9-]+$/; + +// turn on/off -> OFF recommended if using other than English (model base and/or language in model) at the moment. +dave_s['gib_regex']=1; + + +// Turn on/off low quant AND Error changes to TEMP/TOP K +// This activates when there is an error "correction". +// If on, it scrambles temp/top k until NEXT correction OR if passive reconsider is ON -> when it activates. +// temp / topk scramble ON/OFF for ERROR correction. +// 0 = OFF +// 1 = MILD [default] => range .05 to .95 Temp ; k => 40 to 70. +// 2 = MED => range .5 to 1.8 Temp ; k => 30 to 90. +// 3 = HIGH => range .8 to 3.25 Temp ; k => 20 to 150. +// 4 = SPICY => range 1 to 5 Temp ; k => 5 to 400. +// 5 = YOUR defaults MAX +// Uses your temp / top k settings but randomizes them to whatever the max temp / top_k you have set. +// IE if you set temp of 1.5, and top_k of 100 -> System will pick values from .01 to 1.5 for temp, and 5 to 105 for top K. + +dave_s['gib_error_scramble']=2; + +// +// Passive "RECONSIDER" Systems +// On/Off +// If both off, then systems will only stop/correct when there is an issue. +// + +// stop and reassess every paragraph. +dave_s['paragraph_signal_on']=1; + +// stop and reassess every sentence. +dave_s['sentence_signal_on']=0; + +// temp / topk scramble ON/OFF each "reconsider" automatically. +// 0 = OFF +// 1 = MILD [default] => range .05 to .95 Temp ; k => 40 to 70. +// 2 = MED => range .5 to 1.8 Temp ; k => 30 to 90. +// 3 = HIGH => range .8 to 3.25 Temp ; k => 20 to 150. +// 4 = SPICY => range 1 to 5 Temp ; k => 5 to 400. +// 5 = YOUR defaults MAX +// Uses your temp / top k settings but randomizes them to whatever the max temp / top_k you have set. +// IE if you set temp of 1.5, and top_k of 100 -> System will pick values from .01 to 1.5 for temp, and 5 to 105 for top K. + +dave_s['reconsider_scramble']=0; + + +// +// --------------------------------- DO NOT EDIT BELOW THIS LINE ----------------------------------------------------------- +// + + + + + +// +// PRE Gen Sampler Settings +// + +// DICE START: + +// If set to 1, sets your settings as MAX upper limit (parameters/samplers) of random changes. +// otherwise system will use max settings below. +dave_s['dice_max_on']=0; + +// Signal + Signal settings +// Activates on Signal on AND signal condition(s) met. + +dave_s['signal_on']=1; + + + + +// +// DICE Samplers +// EXTRA SYSTEMS +// + +// temp and top key are linked. +dave_s['dice_temp_topk_on']=1; dave_s['dice_temp_topk_chance']=0; dave_s['dice_temp_range']=500; dave_s['dice_topk_range']=150; + +dave_s['dice_XTC_on']=1; dave_s['dice_XTC_chance']=5; dave_s['dice_XTC_prob']=100; dave_s['dice_XTC_thre']=100; + + + +// +// MASTER CONTROLS: +// 1=on, 0=off +// EXTRA SYSTEMS +// + +var dave_active_plus=0; +var dave_active_samplers=0; + + + + +// +// Streaming Monitor Function. +// + + +function watch_dave() { + +clearInterval(nIntervId_stream); + +if (dave_message_watch.length !=dave_watch_size || dave_message_watch_old !=dave_message_watch ) { + +// checksum,lengthsum, case sum +dave_message_watch_old=dave_message_watch; dave_watch_size=dave_message_watch.length; var dl=dave_message_watch.toLowerCase(); var dl_all=dl; dave_flag=0; var t=""; var i=0; var j=0; + +// 500 / dave_s['sample_size'] last CHARACTERS / 100 Words => set boundry +if (dave_watch_size>dave_s['sample_size']) {dl=dl.substring(dave_watch_size-dave_s['sample_size']);} + +// start at space. +if (dl.indexOf(" ")>-1) { dl=dl.substring(dl.indexOf(" ")); } + + +// +// Gibberish Filter -> go back to last "\n" / "." -> THEN STOP GEN. +// if it only contains letters => BOOM || if the first two = the last two => BOOM +// only does full scan if "error(s)" tripped. +// + +var ww=new Array(); var www2=new Array(); dave_gib_active=0; + +if (dave_s['gib_on']==1 && dl.length>dave_s['gib_min']) { + +var gib_reason=""; var gib=dl; ww=gib.split(" "); dave_error_warn=0; + +// WORD CATCH +// catch word, letter, and oddball stuff. || no spaces in 500 characters? impossible! || words are over 24 characters in length ? not likely. +// !!! no good for programming code. || start from last ... check 50 words. + +var k=0; var k1=-1; var sw=new Array(); var duw=[]; var tj=""; j=0; + +// Max sample size 50 words. [last 50] +if (ww.length>dave_s['gib_words']) { k=ww.length-dave_s['gib_words']; } + +// BOOM -> not enough words! +if (ww.length<10) { k1=1; dave_gib_active=1; t=tj; gib_reason=gib_reason+"Not enough words: " + ww.length + " "; } + +// 5 or more repeats of words 5 letters or more? || word over 24 characters / or over 48 characters with "-" s. || Found 15 or more of any word (same) less than 5 characters. +// REGEX => find non english word/characters. [will only ZAP word] + +if (k1<0) { + +var regex_count=0; var h=""; var h1=""; var m=0; var k2=0; +for (i=(ww.length-1); i>k; i--) {duw[ww[i]]=0; h1=h1+ww[i]+" "; k2++; if (ww[i].length>m) { m=ww[i].length; } } + +h=h1.replace(new RegExp("-", "gi"), "" ); h=h.replace(new RegExp("—", "gi"), "" ); + +// power Test / reduce cycles: => shortcut -> no duplicates ? shortcut -> no "bad characters" ( whole string) ? +// any over "max length" => Then do "length" check only. + +if ( ((dave_s['gib_regex']==1 && regex_dave.test(h)!=false && regex_dave.test(h1)!=false) || dave_s['gib_regex']==0 ) && Object.keys(duw).length == k2 ) { + +if (m>dave_s['gib_words_max_size'] ) { + +for (i=(ww.length-1); i>k; i--) { +// over X size words and repeats +if (ww[i].length>dave_s['gib_words_repeat_lrg'] && ww[i].length>dave_s['gib_words_max_size'] ) { + if (ww[i].indexOf("-")<0) { k1=1; tj=ww[i]; dave_gib_active=1; t=tj; gib_reason=gib_reason+"LENG: " + ww.length + " OR length word ("+tj+"): " +tj.length ; break; } + if (ww[i].length>dave_s['gib_words_max_size_dash'] && ww[i].indexOf("-")>0) { k1=1; tj=ww[i]; dave_gib_active=1; t=tj; gib_reason=gib_reason+"LENG: " + ww.length + " OR length word ("+tj+"): " +tj.length ; break; } +} +} + +} + +} +else { + +// full TEST!!! + +for (i=(ww.length-1); i>k; i--) { + +// auto duplicate checker. +duw[ww[i]]++; + +// ZAPS only "word" ; standard remove/replace unless overriden BUT more than one -> ZAP it all. +// exception: "-" and "—" and "-WORD" +h=ww[i].replace(new RegExp("-", "gi"), "" ); h=h.replace(new RegExp("—", "gi"), "" ); +if (dave_s['gib_regex']==1 && regex_dave.test(ww[i])==false && regex_dave.test(h)==false) { + if (regex_count==0) { dave_flag=2; t=ww[i]; } + else {k1=1; tj=ww[i]; dave_gib_active=1; t=tj; gib_reason=gib_reason+"Non English Word: " + ww.length + " word ("+tj+"): " +tj.length ; break; } +regex_count++; +} + +// over X size words and repeats +if (ww[i].length>dave_s['gib_words_repeat_lrg']) { + if (duw[ww[i]]>dave_s['gib_words_repeat_lrg']) {k1=0; tj=ww[i]; dave_gib_active=1; t=tj; gib_reason=gib_reason+" Same SAME SAME words... ("+tj+") "; break;} + if (ww[i].length>dave_s['gib_words_max_size']) { + if (ww[i].indexOf("-")<0) { k1=1; tj=ww[i]; dave_gib_active=1; t=tj; gib_reason=gib_reason+"LENG: " + ww.length + " OR length word ("+tj+"): " +tj.length ; break; } + if (ww[i].length>dave_s['gib_words_max_size_dash'] && ww[i].indexOf("-")>0) { k1=1; tj=ww[i]; dave_gib_active=1; t=tj; gib_reason=gib_reason+"LENG: " + ww.length + " OR length word ("+tj+"): " +tj.length ; break; } + } +} +else { +if (duw[ww[i]]>dave_s['gib_words_repeat_sml']) {k1=1; tj=ww[i]; dave_gib_active=1; t=tj; gib_reason=gib_reason+"Same same same word, less than 5 characters X+ times ("+duw[ww[i]]+"): " + tj + " " ; break;} +} +} + +// end full test else loop +} + +duw=[]; +} + +// +// error systems response: +// ... Can escalate... +// + +if (dave_gib_active==1) {dave_error_warn=1;} + + +// SENTENCE CATCH +// Needs to TEST ALL GEN => If we have sentence duplication, likely catch "paragraph" duplication. + +if (dl_all.indexOf(".")>-1 && dl_all.length>dave_s['gib_sent_min'] && dave_gib_active !=1 ) { +var gib2=dl_all; var h=gib2.replace(new RegExp("\n", "gi"), "" ); var www_sen=h.split("."); duw=[]; +if (www_sen.length>dave_s['gib_sent_min_required']) { + +// preset +for (i=0;i-1;i--) { +if (www_sen[i].length>dave_s['gib_sent_min_size']) { duw[www_sen[i]]++; if (duw[www_sen[i]]>dave_s['gib_sent_repeat']) { dave_gib_active=1; gib_reason=gib_reason+"Sentence Repeat..."; t=www_sen[i]+"."; dave_error_warn=2; break; } } +} +} + +} + +duw=[]; +} + + +// PARAGRAPH CATCH +// Needs to TEST ALL GEN => If we have sentence duplication, likely catch "paragraph" duplication. + +var sp="\n\n"; + +// gemma patch. +if (dl_all.indexOf("\r\r")>-1) {sp="\r\r";} + +if (dl_all.indexOf(sp)>-1 && dl_all.length>dave_s['gib_para_min'] && dave_gib_active !=1 ) { +var gib2=dl_all; var www_sen=gib2.split(sp); duw=[]; ; var x=0; + +if (www_sen.length>dave_s['gib_para_min_required']) { + +// preset +for (i=0;i-1;i--) { +if (www_sen[i].length>dave_s['gib_para_min_size']) { duw[www_sen[i]]++; if (duw[www_sen[i]]>1) { dave_gib_active=1; gib_reason=gib_reason+"Paragraph Repeat..."; t=www_sen[i]+sp; dave_error_warn=2; break; } } +} +} + +} +duw=[]; +} + + +// +// FIRE GIBBISH CONTROLLER ! +// +if (dave_gib_active==1) { console.debug('DAVE: !!!! - Bullsh!t generation detected. KABOOM. ::' + gib_reason + ' '); dave_flag=90; } + +} + +// end GIB check + + + +// +// EXTRA SYSTEMS +// + +if (dave_active_plus==1) { + + + + +// end dave_active_plus +// END EXTRA SYSTEMS +} + + + +// +// MAIN SYSTEMS +// + + +// +// PASSIVE Reconsider Systems +// Becomes Active in sampler area. +// Signals - Causes changes in samplers/parameters/system role +// -> Caught -> Stop -> Change -> Continue || Does NOT 'edit' output but activates stop and restart at "dave_signal_go()". +// + +// Paragraph "Reconsider": +if (dave_s['paragraph_signal_on']==1) { +if (dl_all.lastIndexOf("\n\n") >-1 && dl_all.lastIndexOf("\n\n") != dave_last_signal && dave_signal==0 ) { dave_last_signal=0+dl_all.lastIndexOf("\n\n"); dave_signal=1; } +if (dl_all.lastIndexOf("\r\r") >-1 && dl_all.lastIndexOf("\r\r") != dave_last_signal && dave_signal==0 ) { dave_last_signal=0+dl_all.lastIndexOf("\r\r"); dave_signal=1; } +} + +// Sentence "Reconsider": +if (dave_s['sentence_signal_on']==1) { +if (dl_all.lastIndexOf(".") >-1 && dl_all.lastIndexOf(".") != dave_last_signal && dave_signal==0 ) { dave_last_signal=0+dl_all.lastIndexOf("."); dave_signal=1; } +} + + + +// GO +// duplicate / system activation/update. + +if (dave_flag>0 || dave_signal != 0 ) { + +if (dave_flag>0) { + +if (t!="") { +dave_edit_phrase=t; dave_edit_flag=dave_flag; + +if (dave_old_replace==dave_edit_phrase) { +dave_dup_count++; +} +else { +dave_old_replace=dave_edit_phrase; dave_dup_count=0; +} + +dave_stop_trigger=0; + +} +} + +// signal +// end signal + + +} +else { +// Continue +nIntervId_stream = setInterval(watch_dave,20); +} + +} + +else { +nIntervId_stream = setInterval(watch_dave,20); +} + + +} + + + + +// +// Signal Caught ; reset +// + +function dave_signal_go(cl) { + +// auto continue +dave_auto_continue=1; + +// Control signal +dave_stop_trigger=1; + +// HALT and restart (auto-continue). +stopGeneration(); + +// restart watch +nIntervId_stream = setInterval(watch_dave,20); +} + + +// +// Main function to "fix" generation. +// +// "cl" -> Call location 1 [ finishGenerating() ] ,2 [ function onSuccess(data) ] ,3 [ cleanUpMessage function, only "getm" active ] +// +// +// Cutting... => dave_edit_flag +// +// PROCESS #1: +// Start at "dave_edit_start" (and remove) ... (OPTIONAL) OR last index of... +// Paragraph =99 / 90 (gibbish) || Sentence =98 || Phrase =97 || Word =96 || 2 words=95 || 3 words=94 || 4 words=93 || 5 words=92 || +// +// PROCESS #2: +// REQUIRES: dave_edit_phrase [now part of process 1] +// dave_soft_chop=1 => remove one extra word () , MODE: dave_edit_flag ==1 || dave_edit_flag ==2 || dave_edit_flag ==4 || dave_edit_flag ==7 +// +// PROCESS #3: [works after process 1/2 AND builds on edits from 1/2 ... ] +// +// NO "word/phrase" required => just "chops". [but will not activate without "dave_edit_flag !=0"] +// dave_hard_chop2=1 || dave_edit_flag==91 => Hard chop of paragraph, with fallback to sentence. +// dave_hard_chop2=2 => Cut to sentence only. +// + +var lastCL=0; + +function dave_edit(getm,contm,chunk,cl) { + +// Whoa... needs to make some changes... +stopGeneration(); + +// RESET: LOCAL +var dave_message=getm; var dave_continue=contm; var dave_chunk=chunk; +var i=""; var change_replace=""; var soft_mess=""; +var sp="\n"; + +// gemma patch. +if (getm.indexOf("\r\r")>-1) {sp="\r";} + +// stop operation from duplicate deleting. +// issue when 3,3, and 1 => ok, but "3" after 1=issues. + +if (lastCL==1 && cl==3) {dave_edit_flag=0;cl=-1;} +if (lastCL!=cl) {lastCL=cl;} + +// Turned off, faster gen, less chance of "hard stop". However generation stop/starts are higher when "fixing" +// Critcal End code +// var end_code="<|endoftext|>"; + +var end_code=""; + +// replace only. Needs limits. + +if (dave_edit_flag ==3) { +dave_message=getm.replace(new RegExp(dave_edit_phrase, "gi"), dave_replace_phrase ); +dave_continue=contm.replace(new RegExp(dave_edit_phrase, "gi"), dave_replace_phrase ); +dave_chunk=chunk.replace(new RegExp(dave_edit_phrase, "gi"), dave_replace_phrase ); +} + +// +// Main Edit / Controller +// + +if ( dave_edit_flag !=0 && dave_edit_flag !=3 ) { + +// amount of chopping. => 2 runs required for "word"/"phrase" + additional cutting +// if null, then start at 1. + +var chp=new Array(); + +// 1-7 - stop word or stop phrase || reverse course, and STOP. +// REQUIRES "dave_edit_phrase" + +// REQUIRED global : dave_replace_phrase +if (dave_edit_flag ==4) {change_replace=dave_replace_phrase;} + +// word, phrase, "error" word/phrase +if (dave_edit_flag ==1 || dave_edit_flag ==2 || dave_edit_flag ==4 || dave_edit_flag ==7 || dave_edit_flag==90) { +chp.push(dave_edit_phrase); +} + +// end 1-8 + +// add remove another word, " " boundary. +if (dave_soft_chop==1 || dave_edit_flag==90) {chp.push(" ");} + +// paragraph default "99" // "90" [gibbish] +if (dave_edit_flag==99 || dave_edit_flag==90) {chp.push(sp);} + +// sentence / could cross para boundry. => Need boundry check(s). +if (dave_edit_flag==98) {chp.push(".");} + +// phrase boundry / might be a bad idea => Need boundry check(s). +if (dave_edit_flag==97) {chp.push(",");} + +// one word to 5 word boundary +if (dave_edit_flag==96) {chp.push(" ");} +if (dave_edit_flag==95) {chp.push(" "); chp.push(" "); } +if (dave_edit_flag==94) {chp.push(" "); chp.push(" "); chp.push(" ");} +if (dave_edit_flag==93) {chp.push(" "); chp.push(" "); chp.push(" "); chp.push(" "); } +if (dave_edit_flag==92) {chp.push(" "); chp.push(" "); chp.push(" "); chp.push(" ") ; chp.push(" "); } + +// hardchop - should override "array" / nullify. +if (dave_hard_chop2>0 || dave_edit_flag==91) { +if (dave_hard_chop2==2 ) {chp.push(".");} +else {chp.push(sp);} +console.debug('DAVE '+cl+' REPLACE: Hardchop '+dave_hard_chop2+' Done in EDIT AREA ' ); +} + +var s1=0; var s2=0; var s3=0; + +for (var i=0;i-1) {dave_message=getm.substring(0,s1)+change_replace ; dave_message=dave_message.trimEnd();} + +// contm +if (s2>-1) {dave_continue=contm.substring(0,s2)+change_replace ; dave_continue=dave_continue.trimEnd();} + +// chunk +if (s3>-1) {dave_chunk=chunk.substring(0,s3) +change_replace; dave_chunk=dave_chunk.trimEnd(); } + +// end null check. +} + +// update +getm=dave_message; contm=dave_continue; chunk=dave_chunk; change_replace=""; + +// chop loop +} + +// END CODE +getm=dave_message.trimEnd()+end_code; contm=dave_continue.trimEnd()+end_code; chunk=dave_chunk.trimEnd()+end_code; + +} + + +// +// LOGGING +// + +// check phrases +if (dave_edit_flag<10 && dave_edit_flag !=0) {console.debug('DAVE: '+cl+' DELETED (PHRASE) a DAVE word function '+dave_edit_flag+': "'+dave_edit_phrase + '"::'+dave_message.substring(dave_message.length-50));} + +if (soft_mess!="") {console.debug('DAVE: '+cl+' REPLACED (soft-chop) a DAVE word '+dave_edit_flag+': "'+soft_mess+dave_edit_phrase + '"::'+dave_message.substring(dave_message.length-50));} + +if (end_code !="" && dave_message_watch.indexOf(end_code)<0) {dave_message_watch=dave_message_watch.substring(0,dave_message_watch.indexOf(end_code));} + +// +// RESET NOW +// + +// auto continue +dave_auto_continue=1; + +if (cl==1) {dave_edit_phrase=""; dave_edit_flag=0; dave_edit_start=""; dave_hard_chop2=0; dave_soft_chop=0; dave_watch_size=-1; } + +// STOP or Continue? +// Controller in "auto continue" TOO. + +if (dave_auto_continue==1) { + +if ( dave_holdtext.length<5 && dave_holdtext==dave_oldholdtext ) { +dave_holdtext_count++; +if (dave_holdtext_count>4) { dave_auto_continue=0; console.debug('DAVE STOP: Dave reset function'); } +} +else { +if (dave_holdtext!="") { dave_oldholdtext=dave_holdtext; dave_holdtext_count=0; } +} + +} + +// End reset. + +// Who is stopping it? || 0=> edit, 1=> Signal. +dave_stop_trigger=0; + +// HALT and restart (auto-continue). +// stopGeneration(); + +// restart watch +nIntervId_stream = setInterval(watch_dave,20); + +// I'll be back... +return { +getMessage:dave_message, +continue_mag:dave_continue, +messageChunk:dave_chunk +} + +} + + +// +// Sampler System +// +// DAVE MOD - Sampler Random +// Truely random TEMP / Other Samplers +// Mod #3 +// +// called from "generate()" ; +// + +let dave_old_gen_data; +let dave_gen_restart=0; +var gen_data=new Array("temperature","top_k","xtc_probability","xtc_threshold","rep_pen_range","repetition_penalty_range","repeat_last_n","frequency_penalty","presence_penalty","repeat_penalty","rep_pen","repetition_penalty"); + +function dave_sampler(generate_data) { + + // + // master patch - All Systems + // + + if (dave_ACTIVE==1) { + + + // OTHER SYSTEMS + // fresh prompt ? + + if (dave_active_samplers==1) { + if (dave_gen_restart==0) { + dave_old_gen_data=[]; + for (var i=0;i 40 to 70. + //if (dave_s['reconsider_scramble']==1) { + generate_data['temperature']=.05+(Math.floor((Math.random() * 90))/100); + generate_data['top_k']=40+(Math.floor((Math.random() * 30)) ); + //} + + // MED + // range .5 to 1.8 Temp ; k => 30 to 90. + if (dave_s['reconsider_scramble']==2) { + generate_data['temperature']=.5+(Math.floor((Math.random() * 130))/100); + generate_data['top_k']=30+(Math.floor((Math.random() * 60)) ); + } + + // HIGH + // range .8 to 3.25 Temp ; k => 20 to 150. + if (dave_s['reconsider_scramble']==3) { + generate_data['temperature']=.8+(Math.floor((Math.random() * 250))/100); + generate_data['top_k']=20+(Math.floor((Math.random() * 130)) ); + } + + // SPICY + // range 1 to 5 Temp ; k => 5 to 400. + if (dave_s['reconsider_scramble']==4) { + generate_data['temperature']=1+(Math.floor((Math.random() * 400))/100); + generate_data['top_k']=5+(Math.floor((Math.random() * 395)) ); + } + + // YOU ARE IN CONTROL. + // Your Range + if (dave_s['reconsider_scramble']==5) { + generate_data['temperature']=.01+(Math.floor((Math.random() * (generate_data['temperature']*100) ))/100); + generate_data['top_k']=5+(Math.floor((Math.random() * generate_data['top_k'])) ); + } + + } + + + // ON error(s) - > scramble TEMP / TOP K + + if (dave_s['gib_error_scramble']!=0 && dave_error_warn !=0) { + + // MILD =1 [default] + // range .05 to .95 Temp ; k => 40 to 70. + generate_data['temperature']=.05+(Math.floor((Math.random() * 90))/100); + generate_data['top_k']=40+(Math.floor((Math.random() * 30)) ); + + // MED + // range .5 to 1.8 Temp ; k => 30 to 90. + if (dave_s['gib_error_scramble']==2) { + generate_data['temperature']=.5+(Math.floor((Math.random() * 130))/100); + generate_data['top_k']=30+(Math.floor((Math.random() * 60)) ); + } + + // HIGH + // range .8 to 3.25 Temp ; k => 20 to 150. + if (dave_s['gib_error_scramble']==3) { + generate_data['temperature']=.8+(Math.floor((Math.random() * 250))/100); + generate_data['top_k']=20+(Math.floor((Math.random() * 130)) ); + } + + // SPICY + // range 1 to 5 Temp ; k => 5 to 400. + if (dave_s['gib_error_scramble']==4) { + generate_data['temperature']=1+(Math.floor((Math.random() * 400))/100); + generate_data['top_k']=5+(Math.floor((Math.random() * 395)) ); + } + + // YOU ARE IN CONTROL. + // Your Range + if (dave_s['gib_error_scramble']==5) { + generate_data['temperature']=.01+(Math.floor((Math.random() * (generate_data['temperature']*100) ))/100); + generate_data['top_k']=5+(Math.floor((Math.random() * generate_data['top_k'])) ); + } + + // force greater rep pen range => make model work harder. + if (dave_error_warn==2) { + generate_data['rep_pen_range']=1000; + } + + dave_error_warn=0; + } + + + // SIGNAL(s) - Stop without edit(s) + // DO NOT do others if "just signal" => dave_stop_trigger = 1 + // + + if (dave_s['signal_on']==1 && dave_signal!=0) { + + CC=CC+(' SIGNALLED STOP Happened' + ":" + dave_signal + ":" +dave_last_signal +" -- "); + + + + // + // EXTRA Systems + // + + if (dave_active_samplers==1) { + + + + } + + // + // END EXTRA SYSTEMS + // + + + dave_signal=0; + } + + // RESET: + dave_signal=0; + + + // + // EXTRA SYSTEMS + // + // REPEAT Samplers. + // keep between changes -only set if signal, but only changes IF signal activates. + // + + if (dave_generate_data['rep_pen']!=null && dave_active_samplers==1 ) { + + + } + + // + // END SIGNAL(s) + // + + + + // + // EXTRA SYSTEMS + // + + // + // Random Temp/Top K and "RISK" + // NONE activate if risk is > 10 + // + // Need lower temp for SYSTEM ROLE TO work better? + // + + // on/off all + if (dave_active_samplers==1) { + + + + + // + // get banned strings + // push extra to array + // Redefined here + // + + var dave_banned=generate_data['banned_strings']; + + + + // + // Trigger -> System Role + // Can not be used except for "WORD BAN" OR else => need to follow duplicate_remove protocol. + // This will activate as "second" choice ("remove") to remove with reg edits. + // + // NEED to SYNC this better with "other edits" which may/may not be happening. + // Only "ban" works; because others interfer with other edit/detection systems. + // + + if (dave_trigger !="" && dave_stop_trigger==0) { + + generate_data['prompt']="Delve into "+dave_trigger+" in vivid and exact detail. " + generate_data['prompt']; + CC=CC+(' DIRECTIVE: --TRIGGER '+dave_trigger+dave_trigger_count+' --'); + + // ban 1,2 + dave_banned.push(dave_trigger); + CC=CC+(' BANNED Trigger: '+dave_trigger ); + + // soft chop ; take one extra word off. + // !! will transfer to "reg" edit too => EXTRA WORD CHOPPED. + //if (dave_trigger_count==3 || dave_trigger_count==5) { + //CC=CC+(' SoftChop Trigger: '+dave_trigger ); + //dave_soft_chop=1; + //} + + // chop chop... sentence ; will also chop word/phrase first. + // !! will transfer to "reg" edit too -> MAY CUT TOO MUCH + //if (dave_trigger_count==5 || dave_trigger_count==6 ) { + //CC=CC+(' CHOPPED - Sentence (trigger): Word before :'+dave_old_replace ); + //dave_hard_chop2=2; + //} + + // BIG CHOP - paragraph ; will also chop word/phrase first. + // !! will transfer to "reg" edit too -> MAY CUT TOO MUCH + //if (dave_trigger_count>7) { + //CC=CC+(' BIG...BIG -- CHOPPED - Paragraph (trigger): Word before :'+dave_old_replace ); + //dave_hard_chop2=1; + //dave_old_replace=""; + //dave_trigger=""; + //dave_trigger_count=0; + //} + + } + + // end all on/off sampler controls. + } + + + // + // chop the prompt if + // TURNED OFF. + // causes repeat issues sometimes. + // + + //var oldp=generate_data['prompt']; + //oldp=oldp.substring(0,oldp.lastIndexOf(" ")); + //generate_data['prompt']=oldp; + //CC=CC+(' CHOPPED: Word before :'+dave_old_replace ); + + // activate sentence CHOP + //CC=CC+(' CHOPPED - Sentence: Word before :'+dave_old_replace ); + + + // + // END EXTRA + // + + + // + // MAIN SYSTEMS + // + + // 7 Step + // duplicate count / fixing system + // + + if (dave_dup_count>0 && dave_stop_trigger==0) { + + generate_data['prompt']="Don't use this word: "+dave_old_replace+", instead go into detail about it. "+ generate_data['prompt']; + CC=CC+(' DIRECTIVE: --Detail: '+dave_old_replace+' '+dave_dup_count+' --'); + + // increase temp and top_K + // can be switched on/off + if (dave_active_samplers==1) { + generate_data['temperature']=1.5+(Math.floor((Math.random() * 300))/100); + generate_data['top_k']=80+(Math.floor((Math.random() * 150)) ); + } + + // 1,2 -> Try to remove word/phrase. + + // soft chop ; take one extra word off. + if (dave_dup_count==3 || dave_dup_count==4 ) { + CC=CC+(' SoftChop Detail: '+dave_dup_count+"::"+dave_old_replace+":: " ); + dave_soft_chop=1; + } + + // Ban it, 3 or higher. + if (dave_dup_count>4 ) { + if (dave_active_samplers==1) { dave_banned.push(dave_old_replace); } + + CC=CC+(' BANNED: '+dave_old_replace ); + } + + // Chop Sentence ; will also chop word/phrase first. + if (dave_dup_count==6 || dave_dup_count==8) { + CC=CC+(' CHOPPED - Sentence (replace): Word before :'+dave_old_replace ); + dave_hard_chop2=2; + } + + // BIG CHOP Paragraph ; will also chop word/phrase first. + if (dave_dup_count>9) { + CC=CC+(' BIG...BIG -- CHOPPED - Paragraph: Word before :'+dave_old_replace ); + dave_hard_chop2=1; dave_dup_count=0; dave_old_replace=""; + } + + } + + + // + // set banned string(s); + // + + generate_data['banned_strings']=dave_banned; + + + // + // END MAIN SYSTEMS + // + + + // FINAL REPAIRS + // + // OTHER SYSTEMS + // + // Enforce MAX parameters limits or not. + // fix gen data issue(s) -> Null AND/OR if "max dice on" => Only max size of random; + // dave_s['dice_max_on']=1; + + if (dave_active_samplers==1) { + + for (var i=0;idave_old_gen_data[gen_data[i]] )) ) { generate_data[gen_data[i]]=dave_old_gen_data[gen_data[i]]; } + } + + } + + // + // END OTHER SYSTEMS + // + + + // + // MAIN SYSTEMS + // + + // Temp / TOP K + CC=CC+('TEMP/TOPK: '+generate_data['temperature'] + ":" + generate_data['top_k'] ); + + // console update + console.debug('DAVE: '+CC); + + // always reset this: + dave_stop_trigger=0; + + + // end DAVE_ACTIVE on/off + } + + +// +// array/function returned... => ALWAYS ! +// + +return generate_data; +} + + + +// +// Start "DAVE WATCH" +// + +if (dave_ACTIVE==1) { +nIntervId_stream = setInterval(watch_dave,20); +} +