| 1 | import { DOMPurify, Bowser } from '../lib.js'; |
| 2 | |
| 3 | import { |
| 4 | characters, |
| 5 | online_status, |
| 6 | main_api, |
| 7 | is_send_press, |
| 8 | max_context, |
| 9 | saveSettingsDebounced, |
| 10 | active_group, |
| 11 | active_character, |
| 12 | setActiveGroup, |
| 13 | setActiveCharacter, |
| 14 | getEntitiesList, |
| 15 | buildAvatarList, |
| 16 | selectCharacterById, |
| 17 | eventSource, |
| 18 | menu_type, |
| 19 | substituteParams, |
| 20 | sendTextareaMessage, |
| 21 | doNavbarIconClick, |
| 22 | isSwipingAllowed, |
| 23 | } from '../script.js'; |
| 24 | |
| 25 | import { |
| 26 | power_user, |
| 27 | send_on_enter_options, |
| 28 | } from './power-user.js'; |
| 29 | |
| 30 | import { selected_group, is_group_generating, openGroupById } from './group-chats.js'; |
| 31 | import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js'; |
| 32 | import { |
| 33 | SECRET_KEYS, |
| 34 | secret_state, |
| 35 | } from './secrets.js'; |
| 36 | import { debounce, getStringHash, isValidUrl } from './utils.js'; |
| 37 | import { chat_completion_sources, oai_settings } from './openai.js'; |
| 38 | import { getTokenCountAsync } from './tokenizers.js'; |
| 39 | import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js'; |
| 40 | import { debounce_timeout, SWIPE_SOURCE } from './constants.js'; |
| 41 | |
| 42 | import { Popup } from './popup.js'; |
| 43 | import { accountStorage } from './util/AccountStorage.js'; |
| 44 | import { getCurrentUserHandle } from './user.js'; |
| 45 | import { kai_settings } from './kai-settings.js'; |
| 46 | |
| 47 | var RPanelPin = document.getElementById('rm_button_panel_pin'); |
| 48 | var LPanelPin = document.getElementById('lm_button_panel_pin'); |
| 49 | var WIPanelPin = document.getElementById('WI_panel_pin'); |
| 50 | |
| 51 | var RightNavPanel = document.getElementById('right-nav-panel'); |
| 52 | var RightNavDrawerIcon = document.getElementById('rightNavDrawerIcon'); |
| 53 | var LeftNavPanel = document.getElementById('left-nav-panel'); |
| 54 | var LeftNavDrawerIcon = document.getElementById('leftNavDrawerIcon'); |
| 55 | var WorldInfo = document.getElementById('WorldInfo'); |
| 56 | var WIDrawerIcon = document.getElementById('WIDrawerIcon'); |
| 57 | |
| 58 | var SelectedCharacterTab = document.getElementById('rm_button_selected_ch'); |
| 59 | |
| 60 | var connection_made = false; |
| 61 | var retry_delay = 500; |
| 62 | let counterNonce = Date.now(); |
| 63 | |
| 64 | const observerConfig = { childList: true, subtree: true }; |
| 65 | const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed); |
| 66 | const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short); |
| 67 | const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short); |
| 68 | |
| 69 | const observer = new MutationObserver(function (mutations) { |
| 70 | mutations.forEach(function (mutation) { |
| 71 | if (!(mutation.target instanceof HTMLElement)) { |
| 72 | return; |
| 73 | } |
| 74 | if (mutation.target.classList.contains('online_status_text')) { |
| 75 | checkStatusDebounced(); |
| 76 | } else if (mutation.target.parentNode === SelectedCharacterTab) { |
| 77 | countTokensShortDebounced(); |
| 78 | } else if (mutation.target.classList.contains('mes_text')) { |
| 79 | for (const element of mutation.target.getElementsByTagName('math')) { |
| 80 | element.childNodes.forEach(function (child) { |
| 81 | if (child.nodeType === Node.TEXT_NODE) { |
| 82 | child.textContent = ''; |
| 83 | } |
| 84 | }); |
| 85 | } |
| 86 | } |
| 87 | }); |
| 88 | }); |
| 89 | |
| 90 | observer.observe(document.documentElement, observerConfig); |
| 91 | |
| 92 | |
| 93 | /** |
| 94 | * Converts generation time from milliseconds to a human-readable format. |
| 95 | * |
| 96 | * The function takes total generation time as an input, then converts it to a format |
| 97 | * of "_ Days, _ Hours, _ Minutes, _ Seconds". If the generation time does not exceed a |
| 98 | * particular measure (like days or hours), that measure will not be included in the output. |
| 99 | * |
| 100 | * @param {number} total_gen_time - The total generation time in milliseconds. |
| 101 | * @returns {string} - A human-readable string that represents the time spent generating characters. |
| 102 | */ |
| 103 | export function humanizeGenTime(total_gen_time) { |
| 104 | //convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds |
| 105 | let time_spent = total_gen_time || 0; |
| 106 | time_spent = Math.floor(time_spent / 1000); |
| 107 | let seconds = time_spent % 60; |
| 108 | time_spent = Math.floor(time_spent / 60); |
| 109 | let minutes = time_spent % 60; |
| 110 | time_spent = Math.floor(time_spent / 60); |
| 111 | let hours = time_spent % 24; |
| 112 | time_spent = Math.floor(time_spent / 24); |
| 113 | let days = time_spent; |
| 114 | let result = ''; |
| 115 | if (days > 0) { result += `${days} Days, `; } |
| 116 | if (hours > 0) { result += `${hours} Hours, `; } |
| 117 | if (minutes > 0) { result += `${minutes} Minutes, `; } |
| 118 | result += `${seconds} Seconds`; |
| 119 | return result; |
| 120 | } |
| 121 | |
| 122 | /** |
| 123 | * DON'T OPTIMIZE, don't change this to a const or let, it needs to be a var. |
| 124 | */ |
| 125 | var parsedUA = null; |
| 126 | |
| 127 | export function getParsedUA() { |
| 128 | if (!parsedUA) { |
| 129 | try { |
| 130 | parsedUA = Bowser.parse(navigator.userAgent); |
| 131 | } catch { |
| 132 | // In case the user agent is an empty string or Bowser can't parse it for some other reason |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | return parsedUA; |
| 137 | } |
| 138 | |
| 139 | /** |
| 140 | * Checks if the device is a mobile device. |
| 141 | * @returns {boolean} - True if the device is a mobile device, false otherwise. |
| 142 | */ |
| 143 | export function isMobile() { |
| 144 | const mobileTypes = ['mobile', 'tablet']; |
| 145 | |
| 146 | return mobileTypes.includes(getParsedUA()?.platform?.type); |
| 147 | } |
| 148 | |
| 149 | export function shouldSendOnEnter() { |
| 150 | if (!power_user) { |
| 151 | return false; |
| 152 | } |
| 153 | |
| 154 | switch (power_user.send_on_enter) { |
| 155 | case send_on_enter_options.DISABLED: |
| 156 | return false; |
| 157 | case send_on_enter_options.AUTO: |
| 158 | return !isMobile(); |
| 159 | case send_on_enter_options.ENABLED: |
| 160 | return true; |
| 161 | } |
| 162 | } |
| 163 | |
| 164 | /** |
| 165 | * Gets a humanized date time string from a given timestamp. |
| 166 | * @param {number} timestamp Timestamp in milliseconds |
| 167 | * @returns {string} Humanized date time string in the format `YYYY-MM-DD@HHhMMmSSsMSms` |
| 168 | */ |
| 169 | export function humanizedDateTime(timestamp = Date.now()) { |
| 170 | const date = new Date(timestamp); |
| 171 | const dt = { |
| 172 | year: date.getFullYear(), |
| 173 | month: date.getMonth() + 1, |
| 174 | day: date.getDate(), |
| 175 | hour: date.getHours(), |
| 176 | minute: date.getMinutes(), |
| 177 | second: date.getSeconds(), |
| 178 | millisecond: date.getMilliseconds(), |
| 179 | }; |
| 180 | for (const key in dt) { |
| 181 | const padLength = key === 'millisecond' ? 3 : 2; |
| 182 | dt[key] = dt[key].toString().padStart(padLength, '0'); |
| 183 | } |
| 184 | return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`; |
| 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Gets a timestamp for messages in ISO 8601 format. |
| 189 | * @param {number} timestamp - optional timestamp in milliseconds |
| 190 | * @returns {string} ISO 8601 formatted timestamp |
| 191 | */ |
| 192 | export function getMessageTimeStamp(timestamp = Date.now()) { |
| 193 | const date = new Date(timestamp); |
| 194 | return date.toISOString(); |
| 195 | } |
| 196 | |
| 197 | |
| 198 | // triggers: |
| 199 | $('#rm_button_create').on('click', function () { //when "+New Character" is clicked |
| 200 | $(SelectedCharacterTab).children('h2').html(''); // empty nav's 3rd panel tab |
| 201 | }); |
| 202 | //when any input is made to the create/edit character form textareas |
| 203 | $('#rm_ch_create_block').on('input', function () { countTokensDebounced(); }); |
| 204 | //when any input is made to the advanced editing popup textareas |
| 205 | $('#character_popup').on('input', function () { countTokensDebounced(); }); |
| 206 | //function: |
| 207 | export async function RA_CountCharTokens() { |
| 208 | counterNonce = Date.now(); |
| 209 | const counterNonceLocal = counterNonce; |
| 210 | let total_tokens = 0; |
| 211 | let permanent_tokens = 0; |
| 212 | |
| 213 | const tokenCounters = document.querySelectorAll('[data-token-counter]'); |
| 214 | for (const tokenCounter of tokenCounters) { |
| 215 | if (counterNonceLocal !== counterNonce) { |
| 216 | return; |
| 217 | } |
| 218 | |
| 219 | const counter = $(tokenCounter); |
| 220 | const input = $(document.getElementById(counter.data('token-counter'))); |
| 221 | const isPermanent = counter.data('token-permanent') === true; |
| 222 | const value = String(input.val()); |
| 223 | |
| 224 | if (input.length === 0) { |
| 225 | counter.text('Invalid input reference'); |
| 226 | continue; |
| 227 | } |
| 228 | |
| 229 | if (!value) { |
| 230 | input.data('last-value-hash', ''); |
| 231 | counter.text(0); |
| 232 | continue; |
| 233 | } |
| 234 | |
| 235 | const valueHash = getStringHash(value); |
| 236 | |
| 237 | if (input.data('last-value-hash') === valueHash) { |
| 238 | total_tokens += Number(counter.text()); |
| 239 | permanent_tokens += isPermanent ? Number(counter.text()) : 0; |
| 240 | } else { |
| 241 | // We substitute macro for existing characters, but not for the character being created |
| 242 | const valueToCount = menu_type === 'create' ? value : substituteParams(value); |
| 243 | const tokens = await getTokenCountAsync(valueToCount); |
| 244 | |
| 245 | if (counterNonceLocal !== counterNonce) { |
| 246 | return; |
| 247 | } |
| 248 | |
| 249 | counter.text(tokens); |
| 250 | total_tokens += tokens; |
| 251 | permanent_tokens += isPermanent ? tokens : 0; |
| 252 | input.data('last-value-hash', valueHash); |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | // Warn if total tokens exceeds the limit of half the max context |
| 257 | const tokenLimit = Math.max(((main_api !== 'openai' ? max_context : oai_settings.openai_max_context) / 2), 1024); |
| 258 | const showWarning = (total_tokens > tokenLimit); |
| 259 | $('#result_info_total_tokens').text(total_tokens); |
| 260 | $('#result_info_permanent_tokens').text(permanent_tokens); |
| 261 | $('#result_info_text').toggleClass('neutral_warning', showWarning); |
| 262 | $('#chartokenwarning').toggle(showWarning); |
| 263 | } |
| 264 | /** |
| 265 | * Auto load chat with the last active character or group. |
| 266 | * Fires when active_character is defined and auto_load_chat is true. |
| 267 | * The function first tries to find a character with a specific ID from the global settings. |
| 268 | * If it doesn't exist, it tries to find a group with a specific grid from the global settings. |
| 269 | * If the character list hadn't been loaded yet, it calls itself again after 100ms delay. |
| 270 | * The character or group is selected (clicked) if it is found. |
| 271 | */ |
| 272 | async function RA_autoloadchat() { |
| 273 | // active character is the name, we should look it up in the character list and get the id |
| 274 | if (active_character !== null && active_character !== undefined) { |
| 275 | const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character); |
| 276 | if (active_character_id !== -1) { |
| 277 | await selectCharacterById(active_character_id); |
| 278 | |
| 279 | // Do a little tomfoolery to spoof the tag selector |
| 280 | const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`); |
| 281 | applyTagsOnCharacterSelect.call(selectedCharElement); |
| 282 | } else { |
| 283 | setActiveCharacter(null); |
| 284 | saveSettingsDebounced(); |
| 285 | console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`); |
| 286 | } |
| 287 | } |
| 288 | |
| 289 | if (active_group !== null && active_group !== undefined) { |
| 290 | if (active_character) { |
| 291 | console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.'); |
| 292 | setActiveGroup(null); |
| 293 | saveSettingsDebounced(); |
| 294 | } else { |
| 295 | const result = await openGroupById(String(active_group)); |
| 296 | if (!result) { |
| 297 | setActiveGroup(null); |
| 298 | saveSettingsDebounced(); |
| 299 | console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`); |
| 300 | } |
| 301 | } |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | export async function favsToHotswap() { |
| 306 | const entities = getEntitiesList({ doFilter: false }); |
| 307 | const container = $('#right-nav-panel .hotswap'); |
| 308 | |
| 309 | // Hard limit is required because even if all hotswaps don't fit the screen, their images would still be loaded |
| 310 | // 25 is roughly calculated as the maximum number of favs that can fit an ultrawide monitor with the default theme |
| 311 | const FAVS_LIMIT = 25; |
| 312 | const favs = entities.filter(x => x.item.fav || x.item.fav == 'true').slice(0, FAVS_LIMIT); |
| 313 | |
| 314 | //helpful instruction message if no characters are favorited |
| 315 | if (favs.length == 0) { |
| 316 | container.html(`<small><span><i class="fa-solid fa-star"></i> ${DOMPurify.sanitize(container.attr('no_favs'))}</span></small>`); |
| 317 | return; |
| 318 | } |
| 319 | |
| 320 | buildAvatarList(container, favs, { interactable: true, highlightFavs: false }); |
| 321 | } |
| 322 | |
| 323 | //changes input bar and send button display depending on connection status |
| 324 | function RA_checkOnlineStatus() { |
| 325 | if (online_status == 'no_connection') { |
| 326 | const send_textarea = $('#send_textarea'); |
| 327 | send_textarea.attr('placeholder', send_textarea.attr('no_connection_text')); //Input bar placeholder tells users they are not connected |
| 328 | $('#send_form').addClass('no-connection'); |
| 329 | $('#send_but').addClass('displayNone'); //send button is hidden when not connected; |
| 330 | $('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected; |
| 331 | $('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected; |
| 332 | $('#API-status-top').removeClass('fa-plug'); |
| 333 | $('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow'); |
| 334 | connection_made = false; |
| 335 | } else { |
| 336 | if (online_status !== undefined && online_status !== 'no_connection') { |
| 337 | const send_textarea = $('#send_textarea'); |
| 338 | send_textarea.attr('placeholder', send_textarea.attr('connected_text')); //on connect, placeholder tells user to type message |
| 339 | $('#send_form').removeClass('no-connection'); |
| 340 | $('#API-status-top').removeClass('fa-plug-circle-exclamation redOverlayGlow'); |
| 341 | $('#API-status-top').addClass('fa-plug'); |
| 342 | connection_made = true; |
| 343 | retry_delay = 100; |
| 344 | |
| 345 | if (!is_send_press && !(selected_group && is_group_generating)) { |
| 346 | $('#send_but').removeClass('displayNone'); //on connect, send button shows |
| 347 | $('#mes_continue').removeClass('displayNone'); //continue button is shown when connected |
| 348 | $('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected |
| 349 | } |
| 350 | } |
| 351 | } |
| 352 | } |
| 353 | //Auto-connect to API (when set to kobold, API URL exists, and auto_connect is true) |
| 354 | |
| 355 | function RA_autoconnect(PrevApi) { |
| 356 | // secrets.js or script.js not loaded |
| 357 | if (SECRET_KEYS === undefined || online_status === undefined) { |
| 358 | setTimeout(RA_autoconnect, 100); |
| 359 | return; |
| 360 | } |
| 361 | if (online_status === 'no_connection' && power_user.auto_connect) { |
| 362 | switch (main_api) { |
| 363 | case 'kobold': |
| 364 | if (kai_settings.api_server && isValidUrl(kai_settings.api_server)) { |
| 365 | $('#api_button').trigger('click'); |
| 366 | } |
| 367 | break; |
| 368 | case 'novel': |
| 369 | if (secret_state[SECRET_KEYS.NOVEL]) { |
| 370 | $('#api_button_novel').trigger('click'); |
| 371 | } |
| 372 | break; |
| 373 | case 'textgenerationwebui': |
| 374 | if ((textgen_settings.type === textgen_types.MANCER && secret_state[SECRET_KEYS.MANCER]) |
| 375 | || (textgen_settings.type === textgen_types.TOGETHERAI && secret_state[SECRET_KEYS.TOGETHERAI]) |
| 376 | || (textgen_settings.type === textgen_types.INFERMATICAI && secret_state[SECRET_KEYS.INFERMATICAI]) |
| 377 | || (textgen_settings.type === textgen_types.DREAMGEN && secret_state[SECRET_KEYS.DREAMGEN]) |
| 378 | || (textgen_settings.type === textgen_types.OPENROUTER && secret_state[SECRET_KEYS.OPENROUTER]) |
| 379 | || (textgen_settings.type === textgen_types.FEATHERLESS && secret_state[SECRET_KEYS.FEATHERLESS]) |
| 380 | ) { |
| 381 | $('#api_button_textgenerationwebui').trigger('click'); |
| 382 | } else if (isValidUrl(getTextGenServer())) { |
| 383 | $('#api_button_textgenerationwebui').trigger('click'); |
| 384 | } |
| 385 | break; |
| 386 | case 'openai': |
| 387 | if (((secret_state[SECRET_KEYS.OPENAI] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.OPENAI) |
| 388 | || ((secret_state[SECRET_KEYS.CLAUDE] || oai_settings.reverse_proxy) && oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) |
| 389 | || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) |
| 390 | || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21) |
| 391 | || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) |
| 392 | || (secret_state[SECRET_KEYS.VERTEXAI] && oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI && oai_settings.vertexai_auth_mode === 'express') |
| 393 | || (secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] && oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI && oai_settings.vertexai_auth_mode === 'full') |
| 394 | || (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) |
| 395 | || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE) |
| 396 | || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) |
| 397 | || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ) |
| 398 | || (secret_state[SECRET_KEYS.CHUTES] && oai_settings.chat_completion_source == chat_completion_sources.CHUTES) |
| 399 | || (secret_state[SECRET_KEYS.SILICONFLOW] && oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) |
| 400 | || (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) |
| 401 | || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) |
| 402 | || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) |
| 403 | || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI) |
| 404 | || (secret_state[SECRET_KEYS.AIMLAPI] && oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) |
| 405 | || (secret_state[SECRET_KEYS.MOONSHOT] && oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) |
| 406 | || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS) |
| 407 | || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI) |
| 408 | || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI) |
| 409 | || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) |
| 410 | || (secret_state[SECRET_KEYS.WORKERS_AI] && oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI) |
| 411 | || (secret_state[SECRET_KEYS.MINIMAX] && oai_settings.chat_completion_source == chat_completion_sources.MINIMAX) |
| 412 | || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) |
| 413 | || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) |
| 414 | ) { |
| 415 | $('#api_button_openai').trigger('click'); |
| 416 | } |
| 417 | break; |
| 418 | } |
| 419 | |
| 420 | if (!connection_made) { |
| 421 | retry_delay = Math.min(retry_delay * 2, 30000); // double retry delay up to to 30 secs |
| 422 | // console.log('connection attempts: ' + RA_AC_retries + ' delay: ' + (retry_delay / 1000) + 's'); |
| 423 | // setTimeout(RA_autoconnect, retry_delay); |
| 424 | } |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | function OpenNavPanels() { |
| 429 | if (!isMobile()) { |
| 430 | //auto-open R nav if locked and previously open |
| 431 | if (accountStorage.getItem('NavLockOn') == 'true' && accountStorage.getItem('NavOpened') == 'true') { |
| 432 | //console.log("RA -- clicking right nav to open"); |
| 433 | $('#rightNavDrawerIcon').trigger('click'); |
| 434 | } |
| 435 | |
| 436 | //auto-open L nav if locked and previously open |
| 437 | if (accountStorage.getItem('LNavLockOn') == 'true' && accountStorage.getItem('LNavOpened') == 'true') { |
| 438 | console.debug('RA -- clicking left nav to open'); |
| 439 | $('#leftNavDrawerIcon').trigger('click'); |
| 440 | } |
| 441 | |
| 442 | //auto-open WI if locked and previously open |
| 443 | if (accountStorage.getItem('WINavLockOn') == 'true' && accountStorage.getItem('WINavOpened') == 'true') { |
| 444 | console.debug('RA -- clicking WI to open'); |
| 445 | $('#WIDrawerIcon').trigger('click'); |
| 446 | } |
| 447 | } |
| 448 | } |
| 449 | |
| 450 | const getUserInputKey = () => getCurrentUserHandle() + '_userInput'; |
| 451 | |
| 452 | function restoreUserInput() { |
| 453 | if (!power_user.restore_user_input) { |
| 454 | console.debug('restoreUserInput disabled'); |
| 455 | return; |
| 456 | } |
| 457 | |
| 458 | const userInput = localStorage.getItem(getUserInputKey()); |
| 459 | if (userInput) { |
| 460 | $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true })); |
| 461 | } |
| 462 | } |
| 463 | |
| 464 | function saveUserInput() { |
| 465 | const userInput = String($('#send_textarea').val()); |
| 466 | localStorage.setItem(getUserInputKey(), userInput); |
| 467 | console.debug('User Input -- ', userInput); |
| 468 | } |
| 469 | const saveUserInputDebounced = debounce(saveUserInput); |
| 470 | |
| 471 | // Make the DIV element draggable: |
| 472 | |
| 473 | /** |
| 474 | * Make the given element draggable. This is used for Moving UI. |
| 475 | * @param {JQuery} $elmnt - The element to make draggable. |
| 476 | */ |
| 477 | export function dragElement($elmnt) { |
| 478 | let actionType = null; // "drag" or "resize" |
| 479 | let isMouseDown = false; |
| 480 | |
| 481 | let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; |
| 482 | let height, width, top, left, right, bottom, |
| 483 | maxX, maxY, winHeight, winWidth; |
| 484 | |
| 485 | const elmntName = $elmnt.attr('id'); |
| 486 | const elmntNameEscaped = $.escapeSelector(elmntName); |
| 487 | const $elmntHeader = $(`#${elmntNameEscaped}header`); |
| 488 | |
| 489 | // Helper: Save position/size to state and emit events |
| 490 | function savePositionAndSize() { |
| 491 | if (!power_user.movingUIState[elmntName]) power_user.movingUIState[elmntName] = {}; |
| 492 | power_user.movingUIState[elmntName].top = top; |
| 493 | power_user.movingUIState[elmntName].left = left; |
| 494 | power_user.movingUIState[elmntName].right = right; |
| 495 | power_user.movingUIState[elmntName].bottom = bottom; |
| 496 | power_user.movingUIState[elmntName].margin = 'unset'; |
| 497 | if (actionType === 'resize') { |
| 498 | power_user.movingUIState[elmntName].width = width; |
| 499 | power_user.movingUIState[elmntName].height = height; |
| 500 | eventSource.emit('resizeUI', elmntName); |
| 501 | } |
| 502 | saveSettingsDebounced(); |
| 503 | } |
| 504 | |
| 505 | // Helper: Clamp element within viewport |
| 506 | function clampToViewport() { |
| 507 | if (top <= 0) $elmnt.css('top', '0px'); |
| 508 | else if (maxY >= winHeight) $elmnt.css('top', winHeight - maxY + top - 1 + 'px'); |
| 509 | if (left <= 0) $elmnt.css('left', '0px'); |
| 510 | else if (maxX >= winWidth) $elmnt.css('left', winWidth - maxX + left - 1 + 'px'); |
| 511 | } |
| 512 | |
| 513 | // Observer for style changes (position/size) |
| 514 | const observer = new MutationObserver((mutations) => { |
| 515 | const $target = $(mutations[0].target); |
| 516 | if ( |
| 517 | !$target.is(':visible') || |
| 518 | $target.hasClass('resizing') || |
| 519 | $target.height() < 50 || |
| 520 | $target.width() < 50 || |
| 521 | power_user.movingUI === false || |
| 522 | isMobile() || |
| 523 | !isMouseDown |
| 524 | ) { |
| 525 | observer.disconnect(); |
| 526 | return; |
| 527 | } |
| 528 | |
| 529 | const element = /** @type {HTMLElement} */ ($target[0]); |
| 530 | const style = getComputedStyle(element); |
| 531 | height = parseInt(style.height); |
| 532 | width = parseInt(style.width); |
| 533 | top = parseInt(style.top); |
| 534 | left = parseInt(style.left); |
| 535 | right = parseInt(style.right); |
| 536 | bottom = parseInt(style.bottom); |
| 537 | maxX = width + left; |
| 538 | maxY = height + top; |
| 539 | winWidth = window.innerWidth; |
| 540 | winHeight = window.innerHeight; |
| 541 | |
| 542 | // Prepare state object if missing |
| 543 | if (!power_user.movingUIState[elmntName]) power_user.movingUIState[elmntName] = {}; |
| 544 | |
| 545 | if (actionType === 'resize') { |
| 546 | let containerAspectRatio = height / width; |
| 547 | if ($elmnt.attr('id').startsWith('zoomFor_')) { |
| 548 | const zoomedAvatarImage = $elmnt.find('.zoomed_avatar_img'); |
| 549 | const imgHeight = zoomedAvatarImage.height(); |
| 550 | const imgWidth = zoomedAvatarImage.width(); |
| 551 | const imageAspectRatio = imgHeight / imgWidth; |
| 552 | if (containerAspectRatio !== imageAspectRatio) { |
| 553 | $elmnt.css('width', $elmnt.width()); |
| 554 | $elmnt.css('height', $elmnt.width() * imageAspectRatio); |
| 555 | } |
| 556 | if (top + $elmnt.height() >= winHeight) { |
| 557 | $elmnt.css('height', winHeight - top - 1 + 'px'); |
| 558 | $elmnt.css('width', (winHeight - top - 1) / imageAspectRatio + 'px'); |
| 559 | } |
| 560 | if (left + $elmnt.width() >= winWidth) { |
| 561 | $elmnt.css('width', winWidth - left - 1 + 'px'); |
| 562 | $elmnt.css('height', (winWidth - left - 1) * imageAspectRatio + 'px'); |
| 563 | } |
| 564 | } else { |
| 565 | if (top + $elmnt.height() >= winHeight) $elmnt.css('height', winHeight - top - 1 + 'px'); |
| 566 | if (left + $elmnt.width() >= winWidth) $elmnt.css('width', winWidth - left - 1 + 'px'); |
| 567 | } |
| 568 | //if (top < topBarLastY && maxX >= topBarFirstX && left <= topBarFirstX) { |
| 569 | // $elmnt.css('width', width - 1 + 'px'); |
| 570 | // } |
| 571 | $elmnt.css({ left, top }); |
| 572 | $elmnt.off('mouseup').on('mouseup', () => { |
| 573 | if ( |
| 574 | power_user.movingUIState[elmntName].width === $elmnt.width() && |
| 575 | power_user.movingUIState[elmntName].height === $elmnt.height() |
| 576 | ) return; |
| 577 | savePositionAndSize(); |
| 578 | observer.disconnect(); |
| 579 | }); |
| 580 | } else if (actionType === 'drag') { |
| 581 | clampToViewport(); |
| 582 | } |
| 583 | |
| 584 | // Always update position in state |
| 585 | savePositionAndSize(); |
| 586 | }); |
| 587 | |
| 588 | // Mouse event handlers |
| 589 | function dragMouseDown(e) { |
| 590 | if (e) { |
| 591 | actionType = 'drag'; |
| 592 | isMouseDown = true; |
| 593 | e.preventDefault(); |
| 594 | pos3 = e.clientX; |
| 595 | pos4 = e.clientY; |
| 596 | } |
| 597 | $(document).on('mouseup', closeDragElement); |
| 598 | $(document).on('mousemove', elementDrag); |
| 599 | } |
| 600 | |
| 601 | function elementDrag(e) { |
| 602 | if (!power_user.movingUIState[elmntName]) power_user.movingUIState[elmntName] = {}; |
| 603 | e.preventDefault(); |
| 604 | pos1 = pos3 - e.clientX; |
| 605 | pos2 = pos4 - e.clientY; |
| 606 | pos3 = e.clientX; |
| 607 | pos4 = e.clientY; |
| 608 | $elmnt.attr('data-dragged', 'true'); |
| 609 | $elmnt.css('left', ($elmnt.offset().left - pos1) + 'px'); |
| 610 | $elmnt.css('top', ($elmnt.offset().top - pos2) + 'px'); |
| 611 | $elmnt.css('margin', 'unset'); |
| 612 | $elmnt.css('height', height); |
| 613 | $elmnt.css('width', width); |
| 614 | } |
| 615 | |
| 616 | function closeDragElement() { |
| 617 | isMouseDown = false; |
| 618 | actionType = null; |
| 619 | $(document).off('mouseup', closeDragElement); |
| 620 | $(document).off('mousemove', elementDrag); |
| 621 | $elmnt.attr('data-dragged', 'false'); |
| 622 | observer.disconnect(); |
| 623 | savePositionAndSize(); |
| 624 | } |
| 625 | |
| 626 | // Setup event listeners |
| 627 | if ($elmntHeader.length) { |
| 628 | $elmntHeader.off('mousedown').on('mousedown', (e) => { |
| 629 | if ($(e.target).hasClass('drag-grabber')) { |
| 630 | actionType = 'drag'; |
| 631 | isMouseDown = true; |
| 632 | observer.observe($elmnt[0], { attributes: true, attributeFilter: ['style'] }); |
| 633 | dragMouseDown(e); |
| 634 | } |
| 635 | }); |
| 636 | } |
| 637 | |
| 638 | $elmnt.off('mousedown').on('mousedown', (e) => { |
| 639 | const rect = $elmnt[0].getBoundingClientRect(); |
| 640 | const resizeMargin = 16; |
| 641 | const isNearRight = e.clientX > rect.right - resizeMargin; |
| 642 | const isNearBottom = e.clientY > rect.bottom - resizeMargin; |
| 643 | if (isNearRight && isNearBottom) { |
| 644 | actionType = 'resize'; |
| 645 | isMouseDown = true; |
| 646 | observer.observe($elmnt[0], { attributes: true, attributeFilter: ['style'] }); |
| 647 | } |
| 648 | }); |
| 649 | |
| 650 | $elmnt.off('mouseup').on('mouseup', () => { |
| 651 | isMouseDown = false; |
| 652 | actionType = null; |
| 653 | observer.disconnect(); |
| 654 | }); |
| 655 | } |
| 656 | |
| 657 | export async function initMovingUI() { |
| 658 | if (!isMobile() && power_user.movingUI === true) { |
| 659 | console.debug('START MOVING UI'); |
| 660 | dragElement($('#sheld')); |
| 661 | dragElement($('#left-nav-panel')); |
| 662 | dragElement($('#right-nav-panel')); |
| 663 | dragElement($('#WorldInfo')); |
| 664 | dragElement($('#floatingPrompt')); |
| 665 | dragElement($('#logprobsViewer')); |
| 666 | dragElement($('#cfgConfig')); |
| 667 | } |
| 668 | } |
| 669 | |
| 670 | /**@type {HTMLTextAreaElement} */ |
| 671 | const sendTextArea = document.querySelector('#send_textarea'); |
| 672 | const chatBlock = document.getElementById('chat'); |
| 673 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; |
| 674 | |
| 675 | /** |
| 676 | * this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height) |
| 677 | */ |
| 678 | function autoFitSendTextArea() { |
| 679 | const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight); |
| 680 | |
| 681 | sendTextArea.style.height = '1px'; // Reset height to 1px to force recalculation of scrollHeight |
| 682 | const newHeight = sendTextArea.scrollHeight; |
| 683 | sendTextArea.style.height = `${newHeight}px`; |
| 684 | |
| 685 | if (!isFirefox) { |
| 686 | chatBlock.scrollTop = chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom); |
| 687 | } |
| 688 | } |
| 689 | export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short); |
| 690 | |
| 691 | // --------------------------------------------------- |
| 692 | |
| 693 | export function initRossMods() { |
| 694 | // initial status check |
| 695 | checkStatusDebounced(); |
| 696 | |
| 697 | if (power_user.auto_load_chat) { |
| 698 | RA_autoloadchat(); |
| 699 | } |
| 700 | |
| 701 | if (power_user.auto_connect) { |
| 702 | RA_autoconnect(); |
| 703 | } |
| 704 | |
| 705 | $('#main_api').on('change', function () { |
| 706 | var PrevAPI = main_api; |
| 707 | setTimeout(() => RA_autoconnect(PrevAPI), 100); |
| 708 | }); |
| 709 | |
| 710 | $('#api_button').on('click', () => checkStatusDebounced()); |
| 711 | |
| 712 | //toggle pin class when lock toggle clicked |
| 713 | $(RPanelPin).on('click', function () { |
| 714 | accountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked')); |
| 715 | if ($(RPanelPin).prop('checked') == true) { |
| 716 | //console.log('adding pin class to right nav'); |
| 717 | $(RightNavPanel).addClass('pinnedOpen'); |
| 718 | $(RightNavDrawerIcon).addClass('drawerPinnedOpen'); |
| 719 | } else { |
| 720 | //console.log('removing pin class from right nav'); |
| 721 | $(RightNavPanel).removeClass('pinnedOpen'); |
| 722 | $(RightNavDrawerIcon).removeClass('drawerPinnedOpen'); |
| 723 | |
| 724 | if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) { |
| 725 | const toggle = $('#unimportantYes'); |
| 726 | doNavbarIconClick.call(toggle); |
| 727 | } |
| 728 | } |
| 729 | }); |
| 730 | $(LPanelPin).on('click', function () { |
| 731 | accountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked')); |
| 732 | if ($(LPanelPin).prop('checked') == true) { |
| 733 | //console.log('adding pin class to Left nav'); |
| 734 | $(LeftNavPanel).addClass('pinnedOpen'); |
| 735 | $(LeftNavDrawerIcon).addClass('drawerPinnedOpen'); |
| 736 | } else { |
| 737 | //console.log('removing pin class from Left nav'); |
| 738 | $(LeftNavPanel).removeClass('pinnedOpen'); |
| 739 | $(LeftNavDrawerIcon).removeClass('drawerPinnedOpen'); |
| 740 | |
| 741 | if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) { |
| 742 | const toggle = $('#ai-config-button>.drawer-toggle'); |
| 743 | doNavbarIconClick.call(toggle); |
| 744 | } |
| 745 | } |
| 746 | }); |
| 747 | |
| 748 | $(WIPanelPin).on('click', async function () { |
| 749 | accountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked')); |
| 750 | if ($(WIPanelPin).prop('checked') == true) { |
| 751 | console.debug('adding pin class to WI'); |
| 752 | $(WorldInfo).addClass('pinnedOpen'); |
| 753 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); |
| 754 | } else { |
| 755 | console.debug('removing pin class from WI'); |
| 756 | $(WorldInfo).removeClass('pinnedOpen'); |
| 757 | $(WIDrawerIcon).removeClass('drawerPinnedOpen'); |
| 758 | |
| 759 | if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) { |
| 760 | console.debug('closing WI after lock removal'); |
| 761 | const toggle = $('#WI-SP-button>.drawer-toggle'); |
| 762 | doNavbarIconClick.call(toggle); |
| 763 | } |
| 764 | } |
| 765 | }); |
| 766 | |
| 767 | if (!isMobile()) { //only read/set pin states on non-mobile devices |
| 768 | // read the state of right Nav Lock and apply to rightnav classlist |
| 769 | $(RPanelPin).prop('checked', accountStorage.getItem('NavLockOn') == 'true'); |
| 770 | if (accountStorage.getItem('NavLockOn') == 'true') { |
| 771 | //console.log('setting pin class via local var'); |
| 772 | $(RightNavPanel).addClass('pinnedOpen'); |
| 773 | $(RightNavDrawerIcon).addClass('drawerPinnedOpen'); |
| 774 | } |
| 775 | if ($(RPanelPin).prop('checked')) { |
| 776 | console.debug('setting pin class via checkbox state'); |
| 777 | $(RightNavPanel).addClass('pinnedOpen'); |
| 778 | $(RightNavDrawerIcon).addClass('drawerPinnedOpen'); |
| 779 | } |
| 780 | // read the state of left Nav Lock and apply to leftnav classlist |
| 781 | $(LPanelPin).prop('checked', accountStorage.getItem('LNavLockOn') === 'true'); |
| 782 | if (accountStorage.getItem('LNavLockOn') == 'true') { |
| 783 | //console.log('setting pin class via local var'); |
| 784 | $(LeftNavPanel).addClass('pinnedOpen'); |
| 785 | $(LeftNavDrawerIcon).addClass('drawerPinnedOpen'); |
| 786 | } |
| 787 | if ($(LPanelPin).prop('checked')) { |
| 788 | console.debug('setting pin class via checkbox state'); |
| 789 | $(LeftNavPanel).addClass('pinnedOpen'); |
| 790 | $(LeftNavDrawerIcon).addClass('drawerPinnedOpen'); |
| 791 | } |
| 792 | |
| 793 | // read the state of left Nav Lock and apply to leftnav classlist |
| 794 | $(WIPanelPin).prop('checked', accountStorage.getItem('WINavLockOn') === 'true'); |
| 795 | if (accountStorage.getItem('WINavLockOn') == 'true') { |
| 796 | //console.log('setting pin class via local var'); |
| 797 | $(WorldInfo).addClass('pinnedOpen'); |
| 798 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); |
| 799 | } |
| 800 | |
| 801 | if ($(WIPanelPin).prop('checked')) { |
| 802 | console.debug('setting pin class via checkbox state'); |
| 803 | $(WorldInfo).addClass('pinnedOpen'); |
| 804 | $(WIDrawerIcon).addClass('drawerPinnedOpen'); |
| 805 | } |
| 806 | } |
| 807 | |
| 808 | |
| 809 | //save state of Right nav being open or closed |
| 810 | $('#rightNavDrawerIcon').on('click', function () { |
| 811 | if (!$('#rightNavDrawerIcon').hasClass('openIcon')) { |
| 812 | accountStorage.setItem('NavOpened', 'true'); |
| 813 | } else { accountStorage.setItem('NavOpened', 'false'); } |
| 814 | }); |
| 815 | |
| 816 | //save state of Left nav being open or closed |
| 817 | $('#leftNavDrawerIcon').on('click', function () { |
| 818 | if (!$('#leftNavDrawerIcon').hasClass('openIcon')) { |
| 819 | accountStorage.setItem('LNavOpened', 'true'); |
| 820 | } else { accountStorage.setItem('LNavOpened', 'false'); } |
| 821 | }); |
| 822 | |
| 823 | //save state of Left nav being open or closed |
| 824 | $('#WorldInfo').on('click', function () { |
| 825 | if (!$('#WorldInfo').hasClass('openIcon')) { |
| 826 | accountStorage.setItem('WINavOpened', 'true'); |
| 827 | } else { accountStorage.setItem('WINavOpened', 'false'); } |
| 828 | }); |
| 829 | |
| 830 | var chatbarInFocus = false; |
| 831 | $('#send_textarea').on('focus', function () { |
| 832 | chatbarInFocus = true; |
| 833 | }); |
| 834 | |
| 835 | $('#send_textarea').on('blur', function () { |
| 836 | chatbarInFocus = false; |
| 837 | }); |
| 838 | |
| 839 | setTimeout(() => { |
| 840 | OpenNavPanels(); |
| 841 | }, 300); |
| 842 | |
| 843 | $(SelectedCharacterTab).on('click', function () { accountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); }); |
| 844 | $('#rm_button_characters').on('click', function () { accountStorage.setItem('SelectedNavTab', 'rm_button_characters'); }); |
| 845 | |
| 846 | // when a char is selected from the list, save them as the auto-load character for next page load |
| 847 | |
| 848 | // when a char is selected from the list, save their name as the auto-load character for next page load |
| 849 | $(document).on('click', '.character_select', function () { |
| 850 | const characterId = $(this).attr('data-chid'); |
| 851 | setActiveCharacter(characterId); |
| 852 | setActiveGroup(null); |
| 853 | saveSettingsDebounced(); |
| 854 | }); |
| 855 | |
| 856 | $(document).on('click', '.group_select', function () { |
| 857 | const groupId = $(this).attr('data-chid') || $(this).attr('data-grid'); |
| 858 | setActiveCharacter(null); |
| 859 | setActiveGroup(groupId); |
| 860 | saveSettingsDebounced(); |
| 861 | }); |
| 862 | |
| 863 | const cssAutofit = CSS.supports('field-sizing', 'content'); |
| 864 | |
| 865 | if (cssAutofit) { |
| 866 | let lastHeight = chatBlock.offsetHeight; |
| 867 | const chatBlockResizeObserver = new ResizeObserver((entries) => { |
| 868 | for (const entry of entries) { |
| 869 | if (entry.target !== chatBlock) { |
| 870 | continue; |
| 871 | } |
| 872 | |
| 873 | const threshold = 1; |
| 874 | const newHeight = chatBlock.offsetHeight; |
| 875 | const deltaHeight = newHeight - lastHeight; |
| 876 | const isScrollAtBottom = Math.abs(chatBlock.scrollHeight - chatBlock.scrollTop - newHeight) <= threshold; |
| 877 | |
| 878 | if (!isScrollAtBottom && Math.abs(deltaHeight) > threshold) { |
| 879 | chatBlock.scrollTop -= deltaHeight; |
| 880 | } |
| 881 | lastHeight = newHeight; |
| 882 | } |
| 883 | }); |
| 884 | |
| 885 | chatBlockResizeObserver.observe(chatBlock); |
| 886 | } |
| 887 | |
| 888 | sendTextArea.addEventListener('input', () => { |
| 889 | saveUserInputDebounced(); |
| 890 | |
| 891 | if (cssAutofit) { |
| 892 | // Unset modifications made with a manual resize |
| 893 | sendTextArea.style.height = 'auto'; |
| 894 | return; |
| 895 | } |
| 896 | |
| 897 | const hasContent = sendTextArea.value !== ''; |
| 898 | const fitsCurrentSize = sendTextArea.scrollHeight <= sendTextArea.offsetHeight; |
| 899 | const isScrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth; |
| 900 | const isHalfScreenHeight = sendTextArea.offsetHeight >= window.innerHeight / 2; |
| 901 | const needsDebounce = hasContent && (fitsCurrentSize || (isScrollbarShown && isHalfScreenHeight)); |
| 902 | if (needsDebounce) autoFitSendTextAreaDebounced(); |
| 903 | else autoFitSendTextArea(); |
| 904 | }); |
| 905 | |
| 906 | restoreUserInput(); |
| 907 | |
| 908 | // Swipe gestures (see: https://www.npmjs.com/package/swiped-events) |
| 909 | document.addEventListener('swiped-left', function (e) { |
| 910 | if (power_user.gestures === false) { |
| 911 | return; |
| 912 | } |
| 913 | if (Popup.util.isPopupOpen()) { |
| 914 | return; |
| 915 | } |
| 916 | if (!$(e.target).closest('#sheld').length) { |
| 917 | return; |
| 918 | } |
| 919 | if ($('#curEditTextarea').length) { |
| 920 | // Don't swipe while in text edit mode |
| 921 | // the ios selection gestures get picked up |
| 922 | // as swipe gestures |
| 923 | return; |
| 924 | } |
| 925 | var SwipeButR = $('.swipe_right:last'); |
| 926 | var SwipeTargetMesClassParent = $(e.target).closest('.last_mes'); |
| 927 | if (SwipeTargetMesClassParent !== null) { |
| 928 | if (SwipeButR.is(':visible')) { |
| 929 | SwipeButR.trigger('click'); |
| 930 | } |
| 931 | } |
| 932 | }); |
| 933 | document.addEventListener('swiped-right', function (e) { |
| 934 | if (power_user.gestures === false) { |
| 935 | return; |
| 936 | } |
| 937 | if (Popup.util.isPopupOpen()) { |
| 938 | return; |
| 939 | } |
| 940 | if (!$(e.target).closest('#sheld').length) { |
| 941 | return; |
| 942 | } |
| 943 | if ($('#curEditTextarea').length) { |
| 944 | // Don't swipe while in text edit mode |
| 945 | // the ios selection gestures get picked up |
| 946 | // as swipe gestures |
| 947 | return; |
| 948 | } |
| 949 | var SwipeButL = $('.swipe_left:last'); |
| 950 | var SwipeTargetMesClassParent = $(e.target).closest('.last_mes'); |
| 951 | if (SwipeTargetMesClassParent !== null) { |
| 952 | if (SwipeButL.is(':visible')) { |
| 953 | SwipeButL.trigger('click'); |
| 954 | } |
| 955 | } |
| 956 | }); |
| 957 | |
| 958 | |
| 959 | function isInputElementInFocus() { |
| 960 | //return $(document.activeElement).is(":input"); |
| 961 | var focused = $(':focus'); |
| 962 | if (focused.is('input') || focused.is('textarea') || focused.prop('contenteditable') == 'true') { |
| 963 | if (focused.attr('id') === 'send_textarea') { |
| 964 | return false; |
| 965 | } |
| 966 | return true; |
| 967 | } |
| 968 | return false; |
| 969 | } |
| 970 | |
| 971 | function isModifiedKeyboardEvent(event) { |
| 972 | return (event instanceof KeyboardEvent && |
| 973 | (event.shiftKey || |
| 974 | event.ctrlKey || |
| 975 | event.altKey || |
| 976 | event.metaKey)); |
| 977 | } |
| 978 | |
| 979 | $(document).on('keydown', async function (event) { |
| 980 | await processHotkeys(event.originalEvent); |
| 981 | }); |
| 982 | |
| 983 | const hotkeyTargets = { |
| 984 | 'send_textarea': sendTextArea, |
| 985 | 'dialogue_popup_input': document.querySelector('#dialogue_popup_input'), |
| 986 | }; |
| 987 | |
| 988 | //Additional hotkeys CTRL+ENTER and CTRL+UPARROW |
| 989 | /** |
| 990 | * @param {KeyboardEvent} event |
| 991 | */ |
| 992 | async function processHotkeys(event) { |
| 993 | // Default hotkeys and shortcuts shouldn't work if any popup is currently open |
| 994 | if (Popup.util.isPopupOpen()) { |
| 995 | return; |
| 996 | } |
| 997 | |
| 998 | //Enter to send when send_textarea in focus |
| 999 | if (document.activeElement == hotkeyTargets.send_textarea) { |
| 1000 | const sendOnEnter = shouldSendOnEnter(); |
| 1001 | if (!event.isComposing && !event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) { |
| 1002 | event.preventDefault(); |
| 1003 | sendTextareaMessage(); |
| 1004 | return; |
| 1005 | } |
| 1006 | } |
| 1007 | if (document.activeElement == hotkeyTargets.dialogue_popup_input && !isMobile()) { |
| 1008 | if (!event.shiftKey && !event.ctrlKey && event.key == 'Enter') { |
| 1009 | event.preventDefault(); |
| 1010 | $('#dialogue_popup_ok').trigger('click'); |
| 1011 | return; |
| 1012 | } |
| 1013 | } |
| 1014 | //ctrl+shift+up to scroll to context line |
| 1015 | if (event.shiftKey && event.ctrlKey && event.key == 'ArrowUp') { |
| 1016 | event.preventDefault(); |
| 1017 | let contextLine = $('.lastInContext'); |
| 1018 | if (contextLine.length !== 0) { |
| 1019 | $('#chat').animate({ |
| 1020 | scrollTop: contextLine.offset().top - $('#chat').offset().top + $('#chat').scrollTop(), |
| 1021 | }, 300); |
| 1022 | } else { toastr.warning('Context line not found, send a message first!'); } |
| 1023 | return; |
| 1024 | } |
| 1025 | //ctrl+shift+down to scroll to bottom of chat |
| 1026 | if (event.shiftKey && event.ctrlKey && event.key == 'ArrowDown') { |
| 1027 | event.preventDefault(); |
| 1028 | $('#chat').animate({ |
| 1029 | scrollTop: $('#chat').prop('scrollHeight'), |
| 1030 | }, 300); |
| 1031 | return; |
| 1032 | } |
| 1033 | |
| 1034 | // Alt+Enter or AltGr+Enter to Continue |
| 1035 | if ((event.altKey || (event.altKey && event.ctrlKey)) && event.key == 'Enter') { |
| 1036 | if (is_send_press == false) { |
| 1037 | console.debug('Continuing with Alt+Enter'); |
| 1038 | $('#option_continue').trigger('click'); |
| 1039 | return; |
| 1040 | } |
| 1041 | } |
| 1042 | |
| 1043 | // Ctrl+Enter for Regeneration Last Response. If editing, accept the edits instead |
| 1044 | if (event.ctrlKey && event.key == 'Enter') { |
| 1045 | const editMesDone = $('.mes_edit_done:visible'); |
| 1046 | const reasoningMesDone = $('.mes_reasoning_edit_done:visible'); |
| 1047 | if (editMesDone.length > 0) { |
| 1048 | console.debug('Accepting edits with Ctrl+Enter'); |
| 1049 | $('#send_textarea').trigger('focus'); |
| 1050 | editMesDone.trigger('click'); |
| 1051 | return; |
| 1052 | } else if (reasoningMesDone.length > 0) { |
| 1053 | console.debug('Accepting edits with Ctrl+Enter'); |
| 1054 | $('#send_textarea').trigger('focus'); |
| 1055 | reasoningMesDone.trigger('click'); |
| 1056 | return; |
| 1057 | } else if (is_send_press == false) { |
| 1058 | const skipConfirmKey = 'RegenerateWithCtrlEnter'; |
| 1059 | const skipConfirm = accountStorage.getItem(skipConfirmKey) === 'true'; |
| 1060 | function doRegenerate() { |
| 1061 | console.debug('Regenerating with Ctrl+Enter'); |
| 1062 | $('#option_regenerate').trigger('click'); |
| 1063 | $('#options').hide(); |
| 1064 | } |
| 1065 | |
| 1066 | // If there is input text, we do not trigger a regenerate - we just send it |
| 1067 | if ($('#send_textarea').val() !== '') { |
| 1068 | if (shouldSendOnEnter()) { |
| 1069 | console.debug('Sending with Ctrl+Enter'); |
| 1070 | event.preventDefault(); |
| 1071 | sendTextareaMessage(); |
| 1072 | } else { |
| 1073 | console.debug('Text area is not empty, but send on enter is disabled'); |
| 1074 | } |
| 1075 | return; |
| 1076 | } |
| 1077 | |
| 1078 | if (skipConfirm) { |
| 1079 | doRegenerate(); |
| 1080 | } else { |
| 1081 | let regenerateWithCtrlEnter = false; |
| 1082 | const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', { |
| 1083 | customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }], |
| 1084 | onClose: (popup) => { |
| 1085 | regenerateWithCtrlEnter = Boolean(popup.inputResults.get('regenerateWithCtrlEnter') ?? false); |
| 1086 | }, |
| 1087 | }); |
| 1088 | if (!result) { |
| 1089 | return; |
| 1090 | } |
| 1091 | |
| 1092 | accountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter)); |
| 1093 | doRegenerate(); |
| 1094 | } |
| 1095 | return; |
| 1096 | } else { |
| 1097 | console.debug('Ctrl+Enter ignored'); |
| 1098 | } |
| 1099 | } |
| 1100 | |
| 1101 | // Helper function to check if nanogallery2's lightbox is active |
| 1102 | function isNanogallery2LightboxActive() { |
| 1103 | // Check if the body has the 'nGY2On' class, adjust this based on actual behavior |
| 1104 | return document.body.classList.contains('nGY2_body_scrollbar'); |
| 1105 | } |
| 1106 | |
| 1107 | if (event.key == 'ArrowLeft') { //swipes left |
| 1108 | if ( |
| 1109 | isSwipingAllowed() && |
| 1110 | !isNanogallery2LightboxActive() && // Check if lightbox is NOT active |
| 1111 | $('#send_textarea').val() === '' && |
| 1112 | $('#character_popup').css('display') === 'none' && |
| 1113 | $('#shadow_select_chat_popup').css('display') === 'none' && |
| 1114 | !isInputElementInFocus() && |
| 1115 | !isModifiedKeyboardEvent(event) && |
| 1116 | !(document.activeElement instanceof HTMLVideoElement) |
| 1117 | ) { |
| 1118 | $('.swipe_left:last').trigger('click', { source: SWIPE_SOURCE.KEYBOARD, repeated: event.repeat }); |
| 1119 | return; |
| 1120 | } |
| 1121 | } |
| 1122 | if (event.key == 'ArrowRight') { //swipes right |
| 1123 | if ( |
| 1124 | isSwipingAllowed() && |
| 1125 | !isNanogallery2LightboxActive() && // Check if lightbox is NOT active |
| 1126 | $('#send_textarea').val() === '' && |
| 1127 | $('#character_popup').css('display') === 'none' && |
| 1128 | $('#shadow_select_chat_popup').css('display') === 'none' && |
| 1129 | !isInputElementInFocus() && |
| 1130 | !isModifiedKeyboardEvent(event) && |
| 1131 | !(document.activeElement instanceof HTMLVideoElement) |
| 1132 | ) { |
| 1133 | $('.swipe_right:last').trigger('click', { source: SWIPE_SOURCE.KEYBOARD, repeated: event.repeat }); |
| 1134 | return; |
| 1135 | } |
| 1136 | } |
| 1137 | |
| 1138 | |
| 1139 | if (event.ctrlKey && event.key == 'ArrowUp') { //edits last USER message if chatbar is empty and focused |
| 1140 | if ( |
| 1141 | hotkeyTargets.send_textarea.value === '' && |
| 1142 | chatbarInFocus === true && |
| 1143 | ($('.swipe_right:last').css('display') === 'flex' || $('.last_mes').attr('is_system') === 'true') && |
| 1144 | $('#character_popup').css('display') === 'none' && |
| 1145 | $('#shadow_select_chat_popup').css('display') === 'none' |
| 1146 | ) { |
| 1147 | const isUserMesList = document.querySelectorAll('div[is_user="true"]'); |
| 1148 | const lastIsUserMes = isUserMesList[isUserMesList.length - 1]; |
| 1149 | const editMes = lastIsUserMes.querySelector('.mes_block .mes_edit'); |
| 1150 | if (editMes !== null) { |
| 1151 | $(editMes).trigger('click'); |
| 1152 | return; |
| 1153 | } |
| 1154 | } |
| 1155 | } |
| 1156 | |
| 1157 | if (event.key == 'ArrowUp') { //edits last message if chatbar is empty and focused |
| 1158 | console.log('got uparrow input'); |
| 1159 | if ( |
| 1160 | hotkeyTargets.send_textarea.value === '' && |
| 1161 | chatbarInFocus === true && |
| 1162 | //$('.swipe_right:last').css('display') === 'flex' && |
| 1163 | $('.last_mes .mes_buttons').is(':visible') && |
| 1164 | $('#character_popup').css('display') === 'none' && |
| 1165 | $('#shadow_select_chat_popup').css('display') === 'none' |
| 1166 | ) { |
| 1167 | const lastMes = document.querySelector('.last_mes'); |
| 1168 | const editMes = lastMes.querySelector('.mes_block .mes_edit'); |
| 1169 | if (editMes !== null) { |
| 1170 | $(editMes).trigger('click'); |
| 1171 | return; |
| 1172 | } |
| 1173 | } |
| 1174 | } |
| 1175 | |
| 1176 | if (event.key == 'Escape') { //closes various panels |
| 1177 | //dont override Escape hotkey functions from script.js |
| 1178 | //"close edit box" and "cancel stream generation". |
| 1179 | if ($('#curEditTextarea').is(':visible') || $('#mes_stop').is(':visible')) { |
| 1180 | console.debug('escape key, but deferring to script.js routines'); |
| 1181 | return; |
| 1182 | } |
| 1183 | |
| 1184 | if ($('#dialogue_popup').is(':visible')) { |
| 1185 | if ($('#dialogue_popup_cancel').is(':visible')) { |
| 1186 | $('#dialogue_popup_cancel').trigger('click'); |
| 1187 | return; |
| 1188 | } else { |
| 1189 | $('#dialogue_popup_ok').trigger('click'); |
| 1190 | return; |
| 1191 | } |
| 1192 | } |
| 1193 | |
| 1194 | if ($('#select_chat_popup').is(':visible')) { |
| 1195 | $('#select_chat_cross').trigger('click'); |
| 1196 | return; |
| 1197 | } |
| 1198 | |
| 1199 | if ($('#character_popup').is(':visible')) { |
| 1200 | $('#character_cross').trigger('click'); |
| 1201 | return; |
| 1202 | } |
| 1203 | |
| 1204 | if ($('#dialogue_del_mes_cancel').is(':visible')) { |
| 1205 | $('#dialogue_del_mes_cancel').trigger('click'); |
| 1206 | return; |
| 1207 | } |
| 1208 | |
| 1209 | if ($('.drawer-content') |
| 1210 | .not('#WorldInfo') |
| 1211 | .not('#left-nav-panel') |
| 1212 | .not('#right-nav-panel') |
| 1213 | .not('#floatingPrompt') |
| 1214 | .not('#cfgConfig') |
| 1215 | .not('#logprobsViewer') |
| 1216 | .not('#movingDivs > div') |
| 1217 | .is(':visible')) { |
| 1218 | let visibleDrawerContent = $('.drawer-content:visible') |
| 1219 | .not('#WorldInfo') |
| 1220 | .not('#left-nav-panel') |
| 1221 | .not('#right-nav-panel') |
| 1222 | .not('#floatingPrompt') |
| 1223 | .not('#cfgConfig') |
| 1224 | .not('#logprobsViewer') |
| 1225 | .not('#movingDivs > div'); |
| 1226 | $(visibleDrawerContent).parent().find('.drawer-icon').trigger('click'); |
| 1227 | return; |
| 1228 | } |
| 1229 | |
| 1230 | if ($('#logprobsViewer').is(':visible')) { |
| 1231 | $('#logprobsViewerClose').trigger('click'); |
| 1232 | return; |
| 1233 | } |
| 1234 | |
| 1235 | if ($('#cfgConfig').is(':visible')) { |
| 1236 | $('#CFGClose').trigger('click'); |
| 1237 | return; |
| 1238 | } |
| 1239 | |
| 1240 | if ($('#floatingPrompt').is(':visible')) { |
| 1241 | $('#ANClose').trigger('click'); |
| 1242 | return; |
| 1243 | } |
| 1244 | |
| 1245 | if ($('#WorldInfo').is(':visible')) { |
| 1246 | $('#WIDrawerIcon').trigger('click'); |
| 1247 | return; |
| 1248 | } |
| 1249 | |
| 1250 | const movingDivs = $('#movingDivs > div').toArray().reverse(); |
| 1251 | for (const div of movingDivs) { |
| 1252 | if ($(div).is(':visible')) { |
| 1253 | $(div).find('.floating_panel_close, .dragClose').trigger('click'); |
| 1254 | return; |
| 1255 | } |
| 1256 | } |
| 1257 | |
| 1258 | if ($('#left-nav-panel').is(':visible') && |
| 1259 | $(LPanelPin).prop('checked') === false) { |
| 1260 | $('#leftNavDrawerIcon').trigger('click'); |
| 1261 | return; |
| 1262 | } |
| 1263 | |
| 1264 | if ($('#right-nav-panel').is(':visible') && |
| 1265 | $(RPanelPin).prop('checked') === false) { |
| 1266 | $('#rightNavDrawerIcon').trigger('click'); |
| 1267 | return; |
| 1268 | } |
| 1269 | if ($('.draggable').is(':visible')) { |
| 1270 | // Remove the first matched element |
| 1271 | $('.draggable:first').remove(); |
| 1272 | return; |
| 1273 | } |
| 1274 | } |
| 1275 | |
| 1276 | |
| 1277 | if (event.ctrlKey && /^[1-9]$/.test(event.key)) { |
| 1278 | // This will eventually be to trigger quick replies |
| 1279 | // event.preventDefault(); |
| 1280 | console.log('Ctrl +' + event.key + ' pressed!'); |
| 1281 | } |
| 1282 | } |
| 1283 | } |