mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1308 lines
		
	
	
		
			53 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1308 lines
		
	
	
		
			53 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { DOMPurify, Bowser, slideToggle } from '../lib.js';
 | |
| 
 | |
| import {
 | |
|     characters,
 | |
|     online_status,
 | |
|     main_api,
 | |
|     api_server,
 | |
|     is_send_press,
 | |
|     max_context,
 | |
|     saveSettingsDebounced,
 | |
|     active_group,
 | |
|     active_character,
 | |
|     setActiveGroup,
 | |
|     setActiveCharacter,
 | |
|     getEntitiesList,
 | |
|     buildAvatarList,
 | |
|     selectCharacterById,
 | |
|     eventSource,
 | |
|     menu_type,
 | |
|     substituteParams,
 | |
|     sendTextareaMessage,
 | |
|     getSlideToggleOptions,
 | |
| } from '../script.js';
 | |
| 
 | |
| import {
 | |
|     power_user,
 | |
|     send_on_enter_options,
 | |
| } from './power-user.js';
 | |
| 
 | |
| import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
 | |
| import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
 | |
| import {
 | |
|     SECRET_KEYS,
 | |
|     secret_state,
 | |
| } from './secrets.js';
 | |
| import { debounce, getStringHash, isValidUrl } from './utils.js';
 | |
| import { chat_completion_sources, oai_settings } from './openai.js';
 | |
| import { getTokenCountAsync } from './tokenizers.js';
 | |
| import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
 | |
| import { debounce_timeout } from './constants.js';
 | |
| 
 | |
| import { Popup } from './popup.js';
 | |
| import { accountStorage } from './util/AccountStorage.js';
 | |
| import { getCurrentUserHandle } from './user.js';
 | |
| 
 | |
| var RPanelPin = document.getElementById('rm_button_panel_pin');
 | |
| var LPanelPin = document.getElementById('lm_button_panel_pin');
 | |
| var WIPanelPin = document.getElementById('WI_panel_pin');
 | |
| 
 | |
| var RightNavPanel = document.getElementById('right-nav-panel');
 | |
| var RightNavDrawerIcon = document.getElementById('rightNavDrawerIcon');
 | |
| var LeftNavPanel = document.getElementById('left-nav-panel');
 | |
| var LeftNavDrawerIcon = document.getElementById('leftNavDrawerIcon');
 | |
| var WorldInfo = document.getElementById('WorldInfo');
 | |
| var WIDrawerIcon = document.getElementById('WIDrawerIcon');
 | |
| 
 | |
| var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');
 | |
| 
 | |
| var connection_made = false;
 | |
| var retry_delay = 500;
 | |
| let counterNonce = Date.now();
 | |
| 
 | |
| const observerConfig = { childList: true, subtree: true };
 | |
| const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
 | |
| const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
 | |
| const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
 | |
| 
 | |
| const observer = new MutationObserver(function (mutations) {
 | |
|     mutations.forEach(function (mutation) {
 | |
|         if (!(mutation.target instanceof HTMLElement)) {
 | |
|             return;
 | |
|         }
 | |
|         if (mutation.target.classList.contains('online_status_text')) {
 | |
|             checkStatusDebounced();
 | |
|         } else if (mutation.target.parentNode === SelectedCharacterTab) {
 | |
|             countTokensShortDebounced();
 | |
|         } else if (mutation.target.classList.contains('mes_text')) {
 | |
|             for (const element of mutation.target.getElementsByTagName('math')) {
 | |
|                 element.childNodes.forEach(function (child) {
 | |
|                     if (child.nodeType === Node.TEXT_NODE) {
 | |
|                         child.textContent = '';
 | |
|                     }
 | |
|                 });
 | |
|             }
 | |
|         }
 | |
|     });
 | |
| });
 | |
| 
 | |
| observer.observe(document.documentElement, observerConfig);
 | |
| 
 | |
| 
 | |
| /**
 | |
|  * Converts generation time from milliseconds to a human-readable format.
 | |
|  *
 | |
|  * The function takes total generation time as an input, then converts it to a format
 | |
|  * of "_ Days, _ Hours, _ Minutes, _ Seconds". If the generation time does not exceed a
 | |
|  * particular measure (like days or hours), that measure will not be included in the output.
 | |
|  *
 | |
|  * @param {number} total_gen_time - The total generation time in milliseconds.
 | |
|  * @returns {string} - A human-readable string that represents the time spent generating characters.
 | |
|  */
 | |
| export function humanizeGenTime(total_gen_time) {
 | |
| 
 | |
|     //convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds
 | |
|     let time_spent = total_gen_time || 0;
 | |
|     time_spent = Math.floor(time_spent / 1000);
 | |
|     let seconds = time_spent % 60;
 | |
|     time_spent = Math.floor(time_spent / 60);
 | |
|     let minutes = time_spent % 60;
 | |
|     time_spent = Math.floor(time_spent / 60);
 | |
|     let hours = time_spent % 24;
 | |
|     time_spent = Math.floor(time_spent / 24);
 | |
|     let days = time_spent;
 | |
|     time_spent = '';
 | |
|     if (days > 0) { time_spent += `${days} Days, `; }
 | |
|     if (hours > 0) { time_spent += `${hours} Hours, `; }
 | |
|     if (minutes > 0) { time_spent += `${minutes} Minutes, `; }
 | |
|     time_spent += `${seconds} Seconds`;
 | |
|     return time_spent;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * DON'T OPTIMIZE, don't change this to a const or let, it needs to be a var.
 | |
|  */
 | |
| var parsedUA = null;
 | |
| 
 | |
| export function getParsedUA() {
 | |
|     if (!parsedUA) {
 | |
|         try {
 | |
|             parsedUA = Bowser.parse(navigator.userAgent);
 | |
|         } catch {
 | |
|             // In case the user agent is an empty string or Bowser can't parse it for some other reason
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return parsedUA;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Checks if the device is a mobile device.
 | |
|  * @returns {boolean} - True if the device is a mobile device, false otherwise.
 | |
|  */
 | |
| export function isMobile() {
 | |
|     const mobileTypes = ['mobile', 'tablet'];
 | |
| 
 | |
|     return mobileTypes.includes(getParsedUA()?.platform?.type);
 | |
| }
 | |
| 
 | |
| export function shouldSendOnEnter() {
 | |
|     if (!power_user) {
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     switch (power_user.send_on_enter) {
 | |
|         case send_on_enter_options.DISABLED:
 | |
|             return false;
 | |
|         case send_on_enter_options.AUTO:
 | |
|             return !isMobile();
 | |
|         case send_on_enter_options.ENABLED:
 | |
|             return true;
 | |
|     }
 | |
| }
 | |
| 
 | |
| //RossAscends: Added function to format dates used in files and chat timestamps to a humanized format.
 | |
| //Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected.
 | |
| //Does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.
 | |
| //New chats made with characters will use this new formatting.
 | |
| export function humanizedDateTime() {
 | |
|     const now = new Date(Date.now());
 | |
|     const dt = {
 | |
|         year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
 | |
|         hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
 | |
|     };
 | |
|     for (const key in dt) {
 | |
|         dt[key] = dt[key].toString().padStart(2, '0');
 | |
|     }
 | |
|     return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s`;
 | |
| }
 | |
| 
 | |
| //this is a common format version to display a timestamp on each chat message
 | |
| //returns something like: June 19, 2023 2:20pm
 | |
| export function getMessageTimeStamp() {
 | |
|     const date = Date.now();
 | |
|     const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
 | |
|     const d = new Date(date);
 | |
|     const month = months[d.getMonth()];
 | |
|     const day = d.getDate();
 | |
|     const year = d.getFullYear();
 | |
|     let hours = d.getHours();
 | |
|     const minutes = ('0' + d.getMinutes()).slice(-2);
 | |
|     let meridiem = 'am';
 | |
|     if (hours >= 12) {
 | |
|         meridiem = 'pm';
 | |
|         hours -= 12;
 | |
|     }
 | |
|     if (hours === 0) {
 | |
|         hours = 12;
 | |
|     }
 | |
|     const formattedDate = month + ' ' + day + ', ' + year + ' ' + hours + ':' + minutes + meridiem;
 | |
|     return formattedDate;
 | |
| }
 | |
| 
 | |
| 
 | |
| // triggers:
 | |
| $('#rm_button_create').on('click', function () {                 //when "+New Character" is clicked
 | |
|     $(SelectedCharacterTab).children('h2').html('');        // empty nav's 3rd panel tab
 | |
| });
 | |
| //when any input is made to the create/edit character form textareas
 | |
| $('#rm_ch_create_block').on('input', function () { countTokensDebounced(); });
 | |
| //when any input is made to the advanced editing popup textareas
 | |
| $('#character_popup').on('input', function () { countTokensDebounced(); });
 | |
| //function:
 | |
| export async function RA_CountCharTokens() {
 | |
|     counterNonce = Date.now();
 | |
|     const counterNonceLocal = counterNonce;
 | |
|     let total_tokens = 0;
 | |
|     let permanent_tokens = 0;
 | |
| 
 | |
|     const tokenCounters = document.querySelectorAll('[data-token-counter]');
 | |
|     for (const tokenCounter of tokenCounters) {
 | |
|         if (counterNonceLocal !== counterNonce) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const counter = $(tokenCounter);
 | |
|         const input = $(document.getElementById(counter.data('token-counter')));
 | |
|         const isPermanent = counter.data('token-permanent') === true;
 | |
|         const value = String(input.val());
 | |
| 
 | |
|         if (input.length === 0) {
 | |
|             counter.text('Invalid input reference');
 | |
|             continue;
 | |
|         }
 | |
| 
 | |
|         if (!value) {
 | |
|             input.data('last-value-hash', '');
 | |
|             counter.text(0);
 | |
|             continue;
 | |
|         }
 | |
| 
 | |
|         const valueHash = getStringHash(value);
 | |
| 
 | |
|         if (input.data('last-value-hash') === valueHash) {
 | |
|             total_tokens += Number(counter.text());
 | |
|             permanent_tokens += isPermanent ? Number(counter.text()) : 0;
 | |
|         } else {
 | |
|             // We substitute macro for existing characters, but not for the character being created
 | |
|             const valueToCount = menu_type === 'create' ? value : substituteParams(value);
 | |
|             const tokens = await getTokenCountAsync(valueToCount);
 | |
| 
 | |
|             if (counterNonceLocal !== counterNonce) {
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             counter.text(tokens);
 | |
|             total_tokens += tokens;
 | |
|             permanent_tokens += isPermanent ? tokens : 0;
 | |
|             input.data('last-value-hash', valueHash);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     // Warn if total tokens exceeds the limit of half the max context
 | |
|     const tokenLimit = Math.max(((main_api !== 'openai' ? max_context : oai_settings.openai_max_context) / 2), 1024);
 | |
|     const showWarning = (total_tokens > tokenLimit);
 | |
|     $('#result_info_total_tokens').text(total_tokens);
 | |
|     $('#result_info_permanent_tokens').text(permanent_tokens);
 | |
|     $('#result_info_text').toggleClass('neutral_warning', showWarning);
 | |
|     $('#chartokenwarning').toggle(showWarning);
 | |
| }
 | |
| /**
 | |
|  * Auto load chat with the last active character or group.
 | |
|  * Fires when active_character is defined and auto_load_chat is true.
 | |
|  * The function first tries to find a character with a specific ID from the global settings.
 | |
|  * If it doesn't exist, it tries to find a group with a specific grid from the global settings.
 | |
|  * If the character list hadn't been loaded yet, it calls itself again after 100ms delay.
 | |
|  * The character or group is selected (clicked) if it is found.
 | |
|  */
 | |
| async function RA_autoloadchat() {
 | |
|     if (document.querySelector('#rm_print_characters_block .character_select') !== null) {
 | |
|         // active character is the name, we should look it up in the character list and get the id
 | |
|         if (active_character !== null && active_character !== undefined) {
 | |
|             const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
 | |
|             if (active_character_id !== -1) {
 | |
|                 await selectCharacterById(active_character_id);
 | |
| 
 | |
|                 // Do a little tomfoolery to spoof the tag selector
 | |
|                 const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
 | |
|                 applyTagsOnCharacterSelect.call(selectedCharElement);
 | |
|             } else {
 | |
|                 setActiveCharacter(null);
 | |
|                 saveSettingsDebounced();
 | |
|                 console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (active_group !== null && active_group !== undefined) {
 | |
|             if (active_character) {
 | |
|                 console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
 | |
|                 setActiveGroup(null);
 | |
|                 saveSettingsDebounced();
 | |
|             } else {
 | |
|                 const result = await openGroupById(String(active_group));
 | |
|                 if (!result) {
 | |
|                     setActiveGroup(null);
 | |
|                     saveSettingsDebounced();
 | |
|                     console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // if the character list hadn't been loaded yet, try again.
 | |
|     } else { setTimeout(RA_autoloadchat, 100); }
 | |
| }
 | |
| 
 | |
| export async function favsToHotswap() {
 | |
|     const entities = getEntitiesList({ doFilter: false });
 | |
|     const container = $('#right-nav-panel .hotswap');
 | |
| 
 | |
|     const favs = entities.filter(x => x.item.fav || x.item.fav == 'true');
 | |
| 
 | |
|     //helpful instruction message if no characters are favorited
 | |
|     if (favs.length == 0) {
 | |
|         container.html(`<small><span><i class="fa-solid fa-star"></i> ${DOMPurify.sanitize(container.attr('no_favs'))}</span></small>`);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     buildAvatarList(container, favs, { interactable: true, highlightFavs: false });
 | |
| }
 | |
| 
 | |
| //changes input bar and send button display depending on connection status
 | |
| function RA_checkOnlineStatus() {
 | |
|     if (online_status == 'no_connection') {
 | |
|         const send_textarea = $('#send_textarea');
 | |
|         send_textarea.attr('placeholder', send_textarea.attr('no_connection_text')); //Input bar placeholder tells users they are not connected
 | |
|         $('#send_form').addClass('no-connection');
 | |
|         $('#send_but').addClass('displayNone'); //send button is hidden when not connected;
 | |
|         $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
 | |
|         $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
 | |
|         $('#API-status-top').removeClass('fa-plug');
 | |
|         $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');
 | |
|         connection_made = false;
 | |
|     } else {
 | |
|         if (online_status !== undefined && online_status !== 'no_connection') {
 | |
|             const send_textarea = $('#send_textarea');
 | |
|             send_textarea.attr('placeholder', send_textarea.attr('connected_text')); //on connect, placeholder tells user to type message
 | |
|             $('#send_form').removeClass('no-connection');
 | |
|             $('#API-status-top').removeClass('fa-plug-circle-exclamation redOverlayGlow');
 | |
|             $('#API-status-top').addClass('fa-plug');
 | |
|             connection_made = true;
 | |
|             retry_delay = 100;
 | |
| 
 | |
|             if (!is_send_press && !(selected_group && is_group_generating)) {
 | |
|                 $('#send_but').removeClass('displayNone'); //on connect, send button shows
 | |
|                 $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected
 | |
|                 $('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| //Auto-connect to API (when set to kobold, API URL exists, and auto_connect is true)
 | |
| 
 | |
| function RA_autoconnect(PrevApi) {
 | |
|     // secrets.js or script.js not loaded
 | |
|     if (SECRET_KEYS === undefined || online_status === undefined) {
 | |
|         setTimeout(RA_autoconnect, 100);
 | |
|         return;
 | |
|     }
 | |
|     if (online_status === 'no_connection' && power_user.auto_connect) {
 | |
|         switch (main_api) {
 | |
|             case 'kobold':
 | |
|                 if (api_server && isValidUrl(api_server)) {
 | |
|                     $('#api_button').trigger('click');
 | |
|                 }
 | |
|                 break;
 | |
|             case 'novel':
 | |
|                 if (secret_state[SECRET_KEYS.NOVEL]) {
 | |
|                     $('#api_button_novel').trigger('click');
 | |
|                 }
 | |
|                 break;
 | |
|             case 'textgenerationwebui':
 | |
|                 if ((textgen_settings.type === textgen_types.MANCER && secret_state[SECRET_KEYS.MANCER])
 | |
|                     || (textgen_settings.type === textgen_types.TOGETHERAI && secret_state[SECRET_KEYS.TOGETHERAI])
 | |
|                     || (textgen_settings.type === textgen_types.INFERMATICAI && secret_state[SECRET_KEYS.INFERMATICAI])
 | |
|                     || (textgen_settings.type === textgen_types.DREAMGEN && secret_state[SECRET_KEYS.DREAMGEN])
 | |
|                     || (textgen_settings.type === textgen_types.OPENROUTER && secret_state[SECRET_KEYS.OPENROUTER])
 | |
|                     || (textgen_settings.type === textgen_types.FEATHERLESS && secret_state[SECRET_KEYS.FEATHERLESS])
 | |
|                 ) {
 | |
|                     $('#api_button_textgenerationwebui').trigger('click');
 | |
|                 }
 | |
|                 else if (isValidUrl(getTextGenServer())) {
 | |
|                     $('#api_button_textgenerationwebui').trigger('click');
 | |
|                 }
 | |
|                 break;
 | |
|             case 'openai':
 | |
|                 if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI)
 | |
|                     || ((secret_state[SECRET_KEYS.CLAUDE] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE)
 | |
|                     || ((secret_state[SECRET_KEYS.SCALE] || secret_state[SECRET_KEYS.SCALE_COOKIE]) && oai_settings.chat_completion_source == chat_completion_sources.SCALE)
 | |
|                     || (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI)
 | |
|                     || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
 | |
|                     || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
 | |
|                     || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)
 | |
|                     || (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)
 | |
|                     || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
 | |
|                     || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
 | |
|                     || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
 | |
|                     || (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
 | |
|                     || (secret_state[SECRET_KEYS.BLOCKENTROPY] && oai_settings.chat_completion_source == chat_completion_sources.BLOCKENTROPY)
 | |
|                     || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
 | |
|                     || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
 | |
|                     || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
 | |
|                 ) {
 | |
|                     $('#api_button_openai').trigger('click');
 | |
|                 }
 | |
|                 break;
 | |
|         }
 | |
| 
 | |
|         if (!connection_made) {
 | |
|             retry_delay = Math.min(retry_delay * 2, 30000); // double retry delay up to to 30 secs
 | |
|             // console.log('connection attempts: ' + RA_AC_retries + ' delay: ' + (retry_delay / 1000) + 's');
 | |
|             // setTimeout(RA_autoconnect, retry_delay);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| function OpenNavPanels() {
 | |
|     if (!isMobile()) {
 | |
|         //auto-open R nav if locked and previously open
 | |
|         if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') {
 | |
|             //console.log("RA -- clicking right nav to open");
 | |
|             $('#rightNavDrawerIcon').click();
 | |
|         }
 | |
| 
 | |
|         //auto-open L nav if locked and previously open
 | |
|         if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') {
 | |
|             console.debug('RA -- clicking left nav to open');
 | |
|             $('#leftNavDrawerIcon').click();
 | |
|         }
 | |
| 
 | |
|         //auto-open WI if locked and previously open
 | |
|         if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') {
 | |
|             console.debug('RA -- clicking WI to open');
 | |
|             $('#WIDrawerIcon').click();
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
 | |
| 
 | |
| function restoreUserInput() {
 | |
|     if (!power_user.restore_user_input) {
 | |
|         console.debug('restoreUserInput disabled');
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     const userInput = localStorage.getItem(getUserInputKey());
 | |
|     if (userInput) {
 | |
|         $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
 | |
|     }
 | |
| }
 | |
| 
 | |
| function saveUserInput() {
 | |
|     const userInput = String($('#send_textarea').val());
 | |
|     localStorage.setItem(getUserInputKey(), userInput);
 | |
|     console.debug('User Input -- ', userInput);
 | |
| }
 | |
| const saveUserInputDebounced = debounce(saveUserInput);
 | |
| 
 | |
| // Make the DIV element draggable:
 | |
| 
 | |
| export function dragElement(elmnt) {
 | |
|     var isHeaderBeingDragged = false;
 | |
|     var isMouseDown = false;
 | |
| 
 | |
|     var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
 | |
|     var height, width, top, left, right, bottom,
 | |
|         maxX, maxY, winHeight, winWidth,
 | |
|         topbar, topBarFirstX, topBarLastY;
 | |
| 
 | |
|     var elmntName = elmnt.attr('id');
 | |
|     console.debug(`dragElement called for ${elmntName}`);
 | |
|     const elmntNameEscaped = $.escapeSelector(elmntName);
 | |
|     const elmntHeader = $(`#${elmntNameEscaped}header`);
 | |
| 
 | |
|     if (elmntHeader.length) {
 | |
|         elmntHeader.off('mousedown').on('mousedown', (e) => { //listener for drag handle repositioning
 | |
|             isHeaderBeingDragged = true;
 | |
|             isMouseDown = true;
 | |
|             observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
 | |
|             dragMouseDown(e);
 | |
|         });
 | |
|         $(elmnt).off('mousedown').on('mousedown', () => { //listener for resize
 | |
|             isMouseDown = true;
 | |
|             observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     const observer = new MutationObserver((mutations) => {
 | |
|         const target = mutations[0].target;
 | |
|         if (!$(target).is(':visible') //abort if element is invisible
 | |
|             || $(target).hasClass('resizing') //being auto-resized by other JS code
 | |
|             || Number((String(target.height).replace('px', ''))) < 50 //too short
 | |
|             || Number((String(target.width).replace('px', ''))) < 50 //too narrow
 | |
|             || power_user.movingUI === false // if MUI is not turned on
 | |
|             || isMobile() // if it's a mobile screen
 | |
|         ) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const style = getComputedStyle(target);
 | |
|         height = parseInt(style.height);
 | |
|         width = parseInt(style.width);
 | |
|         top = parseInt(style.top);
 | |
|         left = parseInt(style.left);
 | |
|         right = parseInt(style.right);
 | |
|         bottom = parseInt(style.bottom);
 | |
|         maxX = parseInt(width + left);
 | |
|         maxY = parseInt(height + top);
 | |
|         winWidth = window.innerWidth;
 | |
|         winHeight = window.innerHeight;
 | |
| 
 | |
|         topbar = document.getElementById('top-bar');
 | |
|         const topbarstyle = getComputedStyle(topbar);
 | |
|         topBarFirstX = parseInt(topbarstyle.marginInline);
 | |
|         topBarLastY = parseInt(topbarstyle.height);
 | |
| 
 | |
|         //prepare an empty poweruser object for the item being altered if we don't have one already
 | |
|         if (!power_user.movingUIState[elmntName]) {
 | |
|             console.debug(`adding config property for ${elmntName}`);
 | |
|             power_user.movingUIState[elmntName] = {};
 | |
|         }
 | |
| 
 | |
|         //handle resizing
 | |
|         if (!isHeaderBeingDragged && isMouseDown) { //if user is dragging the resize handle (not in header)
 | |
|             let imgHeight, imgWidth, imageAspectRatio;
 | |
|             let containerAspectRatio = height / width;
 | |
| 
 | |
|             //force aspect ratio for zoomed avatars
 | |
|             if ($(elmnt).attr('id').startsWith('zoomFor_')) {
 | |
|                 let zoomedAvatarImage = $(elmnt).find('.zoomed_avatar_img');
 | |
|                 imgHeight = zoomedAvatarImage.height();
 | |
|                 imgWidth = zoomedAvatarImage.width();
 | |
|                 imageAspectRatio = imgHeight / imgWidth;
 | |
| 
 | |
|                 // Maintain aspect ratio
 | |
|                 if (containerAspectRatio !== imageAspectRatio) {
 | |
|                     elmnt.css('width', elmnt.width());
 | |
|                     elmnt.css('height', elmnt.width() * imageAspectRatio);
 | |
|                 }
 | |
| 
 | |
|                 // Prevent resizing offscreen
 | |
|                 if (top + elmnt.height() >= winHeight) {
 | |
|                     elmnt.css('height', winHeight - top - 1 + 'px');
 | |
|                     elmnt.css('width', (winHeight - top - 1) / imageAspectRatio + 'px');
 | |
|                 }
 | |
| 
 | |
|                 if (left + elmnt.width() >= winWidth) {
 | |
|                     elmnt.css('width', winWidth - left - 1 + 'px');
 | |
|                     elmnt.css('height', (winWidth - left - 1) * imageAspectRatio + 'px');
 | |
|                 }
 | |
|             } else { //prevent divs that are not zoomedAvatars from resizing offscreen
 | |
| 
 | |
|                 if (top + elmnt.height() >= winHeight) {
 | |
|                     elmnt.css('height', winHeight - top - 1 + 'px');
 | |
|                 }
 | |
| 
 | |
|                 if (left + elmnt.width() >= winWidth) {
 | |
|                     elmnt.css('width', winWidth - left - 1 + 'px');
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             //prevent resizing from top left into the top bar
 | |
|             if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX) {
 | |
|                 elmnt.css('width', width - 1 + 'px');
 | |
|             }
 | |
| 
 | |
|             //set css to prevent weird resize behavior (does not save)
 | |
|             elmnt.css('left', left);
 | |
|             elmnt.css('top', top);
 | |
| 
 | |
|             //set a listener for mouseup to save new width/height
 | |
|             $(window).off('mouseup').on('mouseup', () => {
 | |
|                 console.log(`Saving ${elmntName} Height/Width`);
 | |
|                 // check if the height or width actually changed
 | |
|                 if (power_user.movingUIState[elmntName].width === elmnt.width() && power_user.movingUIState[elmntName].height === elmnt.height()) {
 | |
|                     console.log('no change detected, aborting save');
 | |
|                     return;
 | |
|                 }
 | |
| 
 | |
|                 power_user.movingUIState[elmntName].width = width;
 | |
|                 power_user.movingUIState[elmntName].height = height;
 | |
|                 eventSource.emit('resizeUI', elmntName);
 | |
|                 saveSettingsDebounced();
 | |
|                 imgHeight = null;
 | |
|                 imgWidth = null;
 | |
|                 height = null;
 | |
|                 width = null;
 | |
| 
 | |
|                 containerAspectRatio = null;
 | |
|                 imageAspectRatio = null;
 | |
|                 $(window).off('mouseup');
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         //only record position changes if header is being dragged
 | |
|         power_user.movingUIState[elmntName].top = top;
 | |
|         power_user.movingUIState[elmntName].left = left;
 | |
|         power_user.movingUIState[elmntName].right = right;
 | |
|         power_user.movingUIState[elmntName].bottom = bottom;
 | |
|         power_user.movingUIState[elmntName].margin = 'unset';
 | |
| 
 | |
|         //handle dragging hit detection to prevent dragging offscreen
 | |
|         if (isHeaderBeingDragged && isMouseDown) {
 | |
| 
 | |
|             if (top <= 0) {
 | |
|                 elmnt.css('top', '0px');
 | |
|             } else if (maxY >= winHeight) {
 | |
|                 elmnt.css('top', winHeight - maxY + top - 1 + 'px');
 | |
|             }
 | |
| 
 | |
|             if (left <= 0) {
 | |
|                 elmnt.css('left', '0px');
 | |
|             } else if (maxX >= winWidth) {
 | |
|                 elmnt.css('left', winWidth - maxX + left - 1 + 'px');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Check if the element header exists and set the reposition listener on the grabber in the header
 | |
|         if (elmntHeader.length) {
 | |
|             elmntHeader.off('mousedown').on('mousedown', (e) => {
 | |
|                 dragMouseDown(e);
 | |
|             });
 | |
|         } else { //if no header, put the listener on the elmnt itself.
 | |
|             elmnt.off('mousedown').on('mousedown', dragMouseDown);
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     function dragMouseDown(e) {
 | |
| 
 | |
|         if (e) {
 | |
|             isHeaderBeingDragged = true;
 | |
|             e.preventDefault();
 | |
|             pos3 = e.clientX; //mouse X at click
 | |
|             pos4 = e.clientY; //mouse Y at click
 | |
|         }
 | |
|         $(document).on('mouseup', closeDragElement);
 | |
|         $(document).on('mousemove', elementDrag);
 | |
|     }
 | |
| 
 | |
|     function elementDrag(e) {
 | |
|         if (!power_user.movingUIState[elmntName]) {
 | |
|             power_user.movingUIState[elmntName] = {};
 | |
|         }
 | |
| 
 | |
|         e = e || window.event;
 | |
|         e.preventDefault();
 | |
| 
 | |
|         pos1 = pos3 - e.clientX;    //X change amt (-1 or 1)
 | |
|         pos2 = pos4 - e.clientY;    //Y change amt (-1 or 1)
 | |
|         pos3 = e.clientX;   //new mouse X
 | |
|         pos4 = e.clientY;   //new mouse Y
 | |
| 
 | |
|         elmnt.attr('data-dragged', 'true');
 | |
| 
 | |
|         //first set css to computed values to avoid CSS NaN results from 'auto', etc
 | |
|         elmnt.css('left', (elmnt.offset().left) + 'px');
 | |
|         elmnt.css('top', (elmnt.offset().top) + 'px');
 | |
| 
 | |
|         //then update element position styles to account for drag changes
 | |
|         elmnt.css('margin', 'unset');
 | |
|         elmnt.css('left', (elmnt.offset().left - pos1) + 'px');
 | |
|         elmnt.css('top', (elmnt.offset().top - pos2) + 'px');
 | |
|         /*         elmnt.css('right', ((winWidth - maxX) + 'px'));
 | |
|                 elmnt.css('bottom', ((winHeight - maxY) + 'px')); */
 | |
| 
 | |
|         // Height/Width here are for visuals only, and are not saved to settings.
 | |
|         // This is required because some divs do hot have a set width/height
 | |
|         // and will default to shrink to min value of 100px set in CSS file
 | |
|         elmnt.css('height', height);
 | |
|         elmnt.css('width', width);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     function closeDragElement() {
 | |
|         console.debug('drag finished');
 | |
|         isHeaderBeingDragged = false;
 | |
|         isMouseDown = false;
 | |
|         $(document).off('mouseup', closeDragElement);
 | |
|         $(document).off('mousemove', elementDrag);
 | |
|         $('body').css('overflow', '');
 | |
|         // Clear the "data-dragged" attribute
 | |
|         elmnt.attr('data-dragged', 'false');
 | |
|         observer.disconnect();
 | |
|         console.debug(`Saving ${elmntName} UI position`);
 | |
|         saveSettingsDebounced();
 | |
|         top = null;
 | |
|         left = null;
 | |
|         right = null;
 | |
|         bottom = null;
 | |
|         maxX = null;
 | |
|         maxY = null;
 | |
|     }
 | |
| }
 | |
| 
 | |
| export async function initMovingUI() {
 | |
|     if (!isMobile() && power_user.movingUI === true) {
 | |
|         console.debug('START MOVING UI');
 | |
|         dragElement($('#sheld'));
 | |
|         dragElement($('#left-nav-panel'));
 | |
|         dragElement($('#right-nav-panel'));
 | |
|         dragElement($('#WorldInfo'));
 | |
|         dragElement($('#floatingPrompt'));
 | |
|         dragElement($('#logprobsViewer'));
 | |
|         dragElement($('#cfgConfig'));
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**@type {HTMLTextAreaElement} */
 | |
| const sendTextArea = document.querySelector('#send_textarea');
 | |
| const chatBlock = document.getElementById('chat');
 | |
| const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
 | |
| 
 | |
| /**
 | |
|  * this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
 | |
|  */
 | |
| function autoFitSendTextArea() {
 | |
|     const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
 | |
| 
 | |
|     sendTextArea.style.height = '1px'; // Reset height to 1px to force recalculation of scrollHeight
 | |
|     const newHeight = sendTextArea.scrollHeight;
 | |
|     sendTextArea.style.height = `${newHeight}px`;
 | |
| 
 | |
|     if (!isFirefox) {
 | |
|         chatBlock.scrollTop = chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom);
 | |
|     }
 | |
| }
 | |
| export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short);
 | |
| 
 | |
| // ---------------------------------------------------
 | |
| 
 | |
| export function initRossMods() {
 | |
|     // initial status check
 | |
|     checkStatusDebounced();
 | |
| 
 | |
|     if (power_user.auto_load_chat) {
 | |
|         RA_autoloadchat();
 | |
|     }
 | |
| 
 | |
|     if (power_user.auto_connect) {
 | |
|         RA_autoconnect();
 | |
|     }
 | |
| 
 | |
|     $('#main_api').change(function () {
 | |
|         var PrevAPI = main_api;
 | |
|         setTimeout(() => RA_autoconnect(PrevAPI), 100);
 | |
|     });
 | |
| 
 | |
|     $('#api_button').on('click', () => checkStatusDebounced());
 | |
| 
 | |
|     //toggle pin class when lock toggle clicked
 | |
|     $(RPanelPin).on('click', function () {
 | |
|         accountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
 | |
|         if ($(RPanelPin).prop('checked') == true) {
 | |
|             //console.log('adding pin class to right nav');
 | |
|             $(RightNavPanel).addClass('pinnedOpen');
 | |
|             $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
 | |
|         } else {
 | |
|             //console.log('removing pin class from right nav');
 | |
|             $(RightNavPanel).removeClass('pinnedOpen');
 | |
|             $(RightNavDrawerIcon).removeClass('drawerPinnedOpen');
 | |
| 
 | |
|             if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
 | |
|                 slideToggle(RightNavPanel, getSlideToggleOptions());
 | |
|                 $(RightNavDrawerIcon).toggleClass('closedIcon openIcon');
 | |
|                 $(RightNavPanel).toggleClass('openDrawer closedDrawer');
 | |
|             }
 | |
|         }
 | |
|     });
 | |
|     $(LPanelPin).on('click', function () {
 | |
|         accountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
 | |
|         if ($(LPanelPin).prop('checked') == true) {
 | |
|             //console.log('adding pin class to Left nav');
 | |
|             $(LeftNavPanel).addClass('pinnedOpen');
 | |
|             $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
 | |
|         } else {
 | |
|             //console.log('removing pin class from Left nav');
 | |
|             $(LeftNavPanel).removeClass('pinnedOpen');
 | |
|             $(LeftNavDrawerIcon).removeClass('drawerPinnedOpen');
 | |
| 
 | |
|             if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
 | |
|                 slideToggle(LeftNavPanel, getSlideToggleOptions());
 | |
|                 $(LeftNavDrawerIcon).toggleClass('closedIcon openIcon');
 | |
|                 $(LeftNavPanel).toggleClass('openDrawer closedDrawer');
 | |
|             }
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     $(WIPanelPin).on('click', function () {
 | |
|         accountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
 | |
|         if ($(WIPanelPin).prop('checked') == true) {
 | |
|             console.debug('adding pin class to WI');
 | |
|             $(WorldInfo).addClass('pinnedOpen');
 | |
|             $(WIDrawerIcon).addClass('drawerPinnedOpen');
 | |
|         } else {
 | |
|             console.debug('removing pin class from WI');
 | |
|             $(WorldInfo).removeClass('pinnedOpen');
 | |
|             $(WIDrawerIcon).removeClass('drawerPinnedOpen');
 | |
| 
 | |
|             if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
 | |
|                 console.debug('closing WI after lock removal');
 | |
|                 slideToggle(WorldInfo, getSlideToggleOptions());
 | |
|                 $(WIDrawerIcon).toggleClass('closedIcon openIcon');
 | |
|                 $(WorldInfo).toggleClass('openDrawer closedDrawer');
 | |
|             }
 | |
|         }
 | |
|     });
 | |
| 
 | |
|     // read the state of right Nav Lock and apply to rightnav classlist
 | |
|     $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true');
 | |
|     if (accountStorage.getItem('NavLockOn') == 'true') {
 | |
|         //console.log('setting pin class via local var');
 | |
|         $(RightNavPanel).addClass('pinnedOpen');
 | |
|         $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
 | |
|     }
 | |
|     if ($(RPanelPin).prop('checked')) {
 | |
|         console.debug('setting pin class via checkbox state');
 | |
|         $(RightNavPanel).addClass('pinnedOpen');
 | |
|         $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
 | |
|     }
 | |
|     // read the state of left Nav Lock and apply to leftnav classlist
 | |
|     $(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true');
 | |
|     if (accountStorage.getItem('LNavLockOn') == 'true') {
 | |
|         //console.log('setting pin class via local var');
 | |
|         $(LeftNavPanel).addClass('pinnedOpen');
 | |
|         $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
 | |
|     }
 | |
|     if ($(LPanelPin).prop('checked')) {
 | |
|         console.debug('setting pin class via checkbox state');
 | |
|         $(LeftNavPanel).addClass('pinnedOpen');
 | |
|         $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
 | |
|     }
 | |
| 
 | |
|     // read the state of left Nav Lock and apply to leftnav classlist
 | |
|     $(WIPanelPin).prop('checked', accountStorage.getItem('WINavLockOn') === 'true');
 | |
|     if (accountStorage.getItem('WINavLockOn') == 'true') {
 | |
|         //console.log('setting pin class via local var');
 | |
|         $(WorldInfo).addClass('pinnedOpen');
 | |
|         $(WIDrawerIcon).addClass('drawerPinnedOpen');
 | |
|     }
 | |
| 
 | |
|     if ($(WIPanelPin).prop('checked')) {
 | |
|         console.debug('setting pin class via checkbox state');
 | |
|         $(WorldInfo).addClass('pinnedOpen');
 | |
|         $(WIDrawerIcon).addClass('drawerPinnedOpen');
 | |
|     }
 | |
| 
 | |
|     //save state of Right nav being open or closed
 | |
|     $('#rightNavDrawerIcon').on('click', function () {
 | |
|         if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
 | |
|             accountStorage.setItem('NavOpened', 'true');
 | |
|         } else { accountStorage.setItem('NavOpened', 'false'); }
 | |
|     });
 | |
| 
 | |
|     //save state of Left nav being open or closed
 | |
|     $('#leftNavDrawerIcon').on('click', function () {
 | |
|         if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
 | |
|             accountStorage.setItem('LNavOpened', 'true');
 | |
|         } else { accountStorage.setItem('LNavOpened', 'false'); }
 | |
|     });
 | |
| 
 | |
|     //save state of Left nav being open or closed
 | |
|     $('#WorldInfo').on('click', function () {
 | |
|         if (!$('#WorldInfo').hasClass('openIcon')) {
 | |
|             accountStorage.setItem('WINavOpened', 'true');
 | |
|         } else { accountStorage.setItem('WINavOpened', 'false'); }
 | |
|     });
 | |
| 
 | |
|     var chatbarInFocus = false;
 | |
|     $('#send_textarea').focus(function () {
 | |
|         chatbarInFocus = true;
 | |
|     });
 | |
| 
 | |
|     $('#send_textarea').blur(function () {
 | |
|         chatbarInFocus = false;
 | |
|     });
 | |
| 
 | |
|     setTimeout(() => {
 | |
|         OpenNavPanels();
 | |
|     }, 300);
 | |
| 
 | |
|     $(SelectedCharacterTab).click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
 | |
|     $('#rm_button_characters').click(function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
 | |
| 
 | |
|     // when a char is selected from the list, save them as the auto-load character for next page load
 | |
| 
 | |
|     // when a char is selected from the list, save their name as the auto-load character for next page load
 | |
|     $(document).on('click', '.character_select', function () {
 | |
|         const characterId = $(this).attr('data-chid');
 | |
|         setActiveCharacter(characterId);
 | |
|         setActiveGroup(null);
 | |
|         saveSettingsDebounced();
 | |
|     });
 | |
| 
 | |
|     $(document).on('click', '.group_select', function () {
 | |
|         const groupId = $(this).attr('data-chid') || $(this).attr('data-grid');
 | |
|         setActiveCharacter(null);
 | |
|         setActiveGroup(groupId);
 | |
|         saveSettingsDebounced();
 | |
|     });
 | |
| 
 | |
|     const cssAutofit = CSS.supports('field-sizing', 'content');
 | |
| 
 | |
|     if (cssAutofit) {
 | |
|         let lastHeight = chatBlock.offsetHeight;
 | |
|         const chatBlockResizeObserver = new ResizeObserver((entries) => {
 | |
|             for (const entry of entries) {
 | |
|                 if (entry.target !== chatBlock) {
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 const threshold = 1;
 | |
|                 const newHeight = chatBlock.offsetHeight;
 | |
|                 const deltaHeight = newHeight - lastHeight;
 | |
|                 const isScrollAtBottom = Math.abs(chatBlock.scrollHeight - chatBlock.scrollTop - newHeight) <= threshold;
 | |
| 
 | |
|                 if (!isScrollAtBottom && Math.abs(deltaHeight) > threshold) {
 | |
|                     chatBlock.scrollTop -= deltaHeight;
 | |
|                 }
 | |
|                 lastHeight = newHeight;
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         chatBlockResizeObserver.observe(chatBlock);
 | |
|     }
 | |
| 
 | |
|     sendTextArea.addEventListener('input', () => {
 | |
|         saveUserInputDebounced();
 | |
| 
 | |
|         if (cssAutofit) {
 | |
|             // Unset modifications made with a manual resize
 | |
|             sendTextArea.style.height = 'auto';
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const hasContent = sendTextArea.value !== '';
 | |
|         const fitsCurrentSize = sendTextArea.scrollHeight <= sendTextArea.offsetHeight;
 | |
|         const isScrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth;
 | |
|         const isHalfScreenHeight = sendTextArea.offsetHeight >= window.innerHeight / 2;
 | |
|         const needsDebounce = hasContent && (fitsCurrentSize || (isScrollbarShown && isHalfScreenHeight));
 | |
|         if (needsDebounce) autoFitSendTextAreaDebounced();
 | |
|         else autoFitSendTextArea();
 | |
|     });
 | |
| 
 | |
|     restoreUserInput();
 | |
| 
 | |
|     // Swipe gestures (see: https://www.npmjs.com/package/swiped-events)
 | |
|     document.addEventListener('swiped-left', function (e) {
 | |
|         if (power_user.gestures === false) {
 | |
|             return;
 | |
|         }
 | |
|         if (Popup.util.isPopupOpen()) {
 | |
|             return;
 | |
|         }
 | |
|         if (!$(e.target).closest('#sheld').length) {
 | |
|             return;
 | |
|         }
 | |
|         if ($('#curEditTextarea').length) {
 | |
|             // Don't swipe while in text edit mode
 | |
|             // the ios selection gestures get picked up
 | |
|             // as swipe gestures
 | |
|             return;
 | |
|         }
 | |
|         var SwipeButR = $('.swipe_right:last');
 | |
|         var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
 | |
|         if (SwipeTargetMesClassParent !== null) {
 | |
|             if (SwipeButR.css('display') === 'flex') {
 | |
|                 SwipeButR.click();
 | |
|             }
 | |
|         }
 | |
|     });
 | |
|     document.addEventListener('swiped-right', function (e) {
 | |
|         if (power_user.gestures === false) {
 | |
|             return;
 | |
|         }
 | |
|         if (Popup.util.isPopupOpen()) {
 | |
|             return;
 | |
|         }
 | |
|         if (!$(e.target).closest('#sheld').length) {
 | |
|             return;
 | |
|         }
 | |
|         if ($('#curEditTextarea').length) {
 | |
|             // Don't swipe while in text edit mode
 | |
|             // the ios selection gestures get picked up
 | |
|             // as swipe gestures
 | |
|             return;
 | |
|         }
 | |
|         var SwipeButL = $('.swipe_left:last');
 | |
|         var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
 | |
|         if (SwipeTargetMesClassParent !== null) {
 | |
|             if (SwipeButL.css('display') === 'flex') {
 | |
|                 SwipeButL.click();
 | |
|             }
 | |
|         }
 | |
|     });
 | |
| 
 | |
| 
 | |
|     function isInputElementInFocus() {
 | |
|         //return $(document.activeElement).is(":input");
 | |
|         var focused = $(':focus');
 | |
|         if (focused.is('input') || focused.is('textarea') || focused.prop('contenteditable') == 'true') {
 | |
|             if (focused.attr('id') === 'send_textarea') {
 | |
|                 return false;
 | |
|             }
 | |
|             return true;
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     $(document).on('keydown', async function (event) {
 | |
|         await processHotkeys(event.originalEvent);
 | |
|     });
 | |
| 
 | |
|     const hotkeyTargets = {
 | |
|         'send_textarea': sendTextArea,
 | |
|         'dialogue_popup_input': document.querySelector('#dialogue_popup_input'),
 | |
|     };
 | |
| 
 | |
|     //Additional hotkeys CTRL+ENTER and CTRL+UPARROW
 | |
|     /**
 | |
|      * @param {KeyboardEvent} event
 | |
|      */
 | |
|     async function processHotkeys(event) {
 | |
|         // Default hotkeys and shortcuts shouldn't work if any popup is currently open
 | |
|         if (Popup.util.isPopupOpen()) {
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         //Enter to send when send_textarea in focus
 | |
|         if (document.activeElement == hotkeyTargets['send_textarea']) {
 | |
|             const sendOnEnter = shouldSendOnEnter();
 | |
|             if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
 | |
|                 event.preventDefault();
 | |
|                 sendTextareaMessage();
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         if (document.activeElement == hotkeyTargets['dialogue_popup_input'] && !isMobile()) {
 | |
|             if (!event.shiftKey && !event.ctrlKey && event.key == 'Enter') {
 | |
|                 event.preventDefault();
 | |
|                 $('#dialogue_popup_ok').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         //ctrl+shift+up to scroll to context line
 | |
|         if (event.shiftKey && event.ctrlKey && event.key == 'ArrowUp') {
 | |
|             event.preventDefault();
 | |
|             let contextLine = $('.lastInContext');
 | |
|             if (contextLine.length !== 0) {
 | |
|                 $('#chat').animate({
 | |
|                     scrollTop: contextLine.offset().top - $('#chat').offset().top + $('#chat').scrollTop(),
 | |
|                 }, 300);
 | |
|             } else { toastr.warning('Context line not found, send a message first!'); }
 | |
|             return;
 | |
|         }
 | |
|         //ctrl+shift+down to scroll to bottom of chat
 | |
|         if (event.shiftKey && event.ctrlKey && event.key == 'ArrowDown') {
 | |
|             event.preventDefault();
 | |
|             $('#chat').animate({
 | |
|                 scrollTop: $('#chat').prop('scrollHeight'),
 | |
|             }, 300);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         // Alt+Enter or AltGr+Enter to Continue
 | |
|         if ((event.altKey || (event.altKey && event.ctrlKey)) && event.key == 'Enter') {
 | |
|             if (is_send_press == false) {
 | |
|                 console.debug('Continuing with Alt+Enter');
 | |
|                 $('#option_continue').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead
 | |
|         if (event.ctrlKey && event.key == 'Enter') {
 | |
|             const editMesDone = $('.mes_edit_done:visible');
 | |
|             const reasoningMesDone = $('.mes_reasoning_edit_done:visible');
 | |
|             if (editMesDone.length > 0) {
 | |
|                 console.debug('Accepting edits with Ctrl+Enter');
 | |
|                 $('#send_textarea').trigger('focus');
 | |
|                 editMesDone.trigger('click');
 | |
|                 return;
 | |
|             } else if (reasoningMesDone.length > 0) {
 | |
|                 console.debug('Accepting edits with Ctrl+Enter');
 | |
|                 $('#send_textarea').trigger('focus');
 | |
|                 reasoningMesDone.trigger('click');
 | |
|                 return;
 | |
|             }
 | |
|             else if (is_send_press == false) {
 | |
|                 const skipConfirmKey = 'RegenerateWithCtrlEnter';
 | |
|                 const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true';
 | |
|                 function doRegenerate() {
 | |
|                     console.debug('Regenerating with Ctrl+Enter');
 | |
|                     $('#option_regenerate').trigger('click');
 | |
|                     $('#options').hide();
 | |
|                 }
 | |
|                 if (skipConfirm) {
 | |
|                     doRegenerate();
 | |
|                 } else {
 | |
|                     let regenerateWithCtrlEnter = false;
 | |
|                     const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
 | |
|                         customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
 | |
|                         onClose: (popup) => {
 | |
|                             regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
 | |
|                         },
 | |
|                     });
 | |
|                     if (!result) {
 | |
|                         return;
 | |
|                     }
 | |
| 
 | |
|                     accountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
 | |
|                     doRegenerate();
 | |
|                 }
 | |
|                 return;
 | |
|             } else {
 | |
|                 console.debug('Ctrl+Enter ignored');
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // Helper function to check if nanogallery2's lightbox is active
 | |
|         function isNanogallery2LightboxActive() {
 | |
|             // Check if the body has the 'nGY2On' class, adjust this based on actual behavior
 | |
|             return document.body.classList.contains('nGY2_body_scrollbar');
 | |
|         }
 | |
| 
 | |
|         if (event.key == 'ArrowLeft') {        //swipes left
 | |
|             if (
 | |
|                 !isNanogallery2LightboxActive() &&  // Check if lightbox is NOT active
 | |
|                 $('.swipe_left:last').css('display') === 'flex' &&
 | |
|                 $('#send_textarea').val() === '' &&
 | |
|                 $('#character_popup').css('display') === 'none' &&
 | |
|                 $('#shadow_select_chat_popup').css('display') === 'none' &&
 | |
|                 !isInputElementInFocus()
 | |
|             ) {
 | |
|                 $('.swipe_left:last').click();
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
|         if (event.key == 'ArrowRight') { //swipes right
 | |
|             if (
 | |
|                 !isNanogallery2LightboxActive() &&  // Check if lightbox is NOT active
 | |
|                 $('.swipe_right:last').css('display') === 'flex' &&
 | |
|                 $('#send_textarea').val() === '' &&
 | |
|                 $('#character_popup').css('display') === 'none' &&
 | |
|                 $('#shadow_select_chat_popup').css('display') === 'none' &&
 | |
|                 !isInputElementInFocus()
 | |
|             ) {
 | |
|                 $('.swipe_right:last').click();
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
|         if (event.ctrlKey && event.key == 'ArrowUp') { //edits last USER message if chatbar is empty and focused
 | |
|             if (
 | |
|                 hotkeyTargets['send_textarea'].value === '' &&
 | |
|                 chatbarInFocus === true &&
 | |
|                 ($('.swipe_right:last').css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') &&
 | |
|                 $('#character_popup').css('display') === 'none' &&
 | |
|                 $('#shadow_select_chat_popup').css('display') === 'none'
 | |
|             ) {
 | |
|                 const isUserMesList = document.querySelectorAll('div[is_user="true"]');
 | |
|                 const lastIsUserMes = isUserMesList[isUserMesList.length - 1];
 | |
|                 const editMes = lastIsUserMes.querySelector('.mes_block .mes_edit');
 | |
|                 if (editMes !== null) {
 | |
|                     $(editMes).trigger('click');
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (event.key == 'ArrowUp') { //edits last message if chatbar is empty and focused
 | |
|             console.log('got uparrow input');
 | |
|             if (
 | |
|                 hotkeyTargets['send_textarea'].value === '' &&
 | |
|                 chatbarInFocus === true &&
 | |
|                 //$('.swipe_right:last').css('display') === 'flex' &&
 | |
|                 $('.last_mes .mes_buttons').is(':visible') &&
 | |
|                 $('#character_popup').css('display') === 'none' &&
 | |
|                 $('#shadow_select_chat_popup').css('display') === 'none'
 | |
|             ) {
 | |
|                 const lastMes = document.querySelector('.last_mes');
 | |
|                 const editMes = lastMes.querySelector('.mes_block .mes_edit');
 | |
|                 if (editMes !== null) {
 | |
|                     $(editMes).click();
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (event.key == 'Escape') { //closes various panels
 | |
|             //dont override Escape hotkey functions from script.js
 | |
|             //"close edit box" and "cancel stream generation".
 | |
|             if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) {
 | |
|                 console.debug('escape key, but deferring to script.js routines');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#dialogue_popup').is(':visible')) {
 | |
|                 if ($('#dialogue_popup_cancel').is(':visible')) {
 | |
|                     $('#dialogue_popup_cancel').trigger('click');
 | |
|                     return;
 | |
|                 } else {
 | |
|                     $('#dialogue_popup_ok').trigger('click');
 | |
|                     return;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if ($('#select_chat_popup').is(':visible')) {
 | |
|                 $('#select_chat_cross').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#character_popup').is(':visible')) {
 | |
|                 $('#character_cross').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#dialogue_del_mes_cancel').is(':visible')) {
 | |
|                 $('#dialogue_del_mes_cancel').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('.drawer-content')
 | |
|                 .not('#WorldInfo')
 | |
|                 .not('#left-nav-panel')
 | |
|                 .not('#right-nav-panel')
 | |
|                 .not('#floatingPrompt')
 | |
|                 .not('#cfgConfig')
 | |
|                 .not('#logprobsViewer')
 | |
|                 .not('#movingDivs > div')
 | |
|                 .is(':visible')) {
 | |
|                 let visibleDrawerContent = $('.drawer-content:visible')
 | |
|                     .not('#WorldInfo')
 | |
|                     .not('#left-nav-panel')
 | |
|                     .not('#right-nav-panel')
 | |
|                     .not('#floatingPrompt')
 | |
|                     .not('#cfgConfig')
 | |
|                     .not('#logprobsViewer')
 | |
|                     .not('#movingDivs > div');
 | |
|                 $(visibleDrawerContent).parent().find('.drawer-icon').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#floatingPrompt').is(':visible')) {
 | |
|                 $('#ANClose').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#WorldInfo').is(':visible')) {
 | |
|                 $('#WIDrawerIcon').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#cfgConfig').is(':visible')) {
 | |
|                 $('#CFGClose').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#logprobsViewer').is(':visible')) {
 | |
|                 $('#logprobsViewerClose').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             $('#movingDivs > div').each(function () {
 | |
|                 if ($(this).is(':visible')) {
 | |
|                     $('#movingDivs > div .floating_panel_close').trigger('click');
 | |
|                     return;
 | |
|                 }
 | |
|             });
 | |
| 
 | |
|             if ($('#left-nav-panel').is(':visible') &&
 | |
|                 $(LPanelPin).prop('checked') === false) {
 | |
|                 $('#leftNavDrawerIcon').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             if ($('#right-nav-panel').is(':visible') &&
 | |
|                 $(RPanelPin).prop('checked') === false) {
 | |
|                 $('#rightNavDrawerIcon').trigger('click');
 | |
|                 return;
 | |
|             }
 | |
|             if ($('.draggable').is(':visible')) {
 | |
|                 // Remove the first matched element
 | |
|                 $('.draggable:first').remove();
 | |
|                 return;
 | |
|             }
 | |
|         }
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
|         if (event.ctrlKey && /^[1-9]$/.test(event.key)) {
 | |
|             // This will eventually be to trigger quick replies
 | |
|             // event.preventDefault();
 | |
|             console.log('Ctrl +' + event.key + ' pressed!');
 | |
|         }
 | |
|     }
 | |
| }
 |