Blame Raw
Cohee · 51ad27fb · · 1283 lines (52.7 KB)
3 contributors
1import { DOMPurify, Bowser } from '../lib.js';
2
3import {
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
25import {
26 power_user,
27 send_on_enter_options,
28} from './power-user.js';
29
30import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
31import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
32import {
33 SECRET_KEYS,
34 secret_state,
35} from './secrets.js';
36import { debounce, getStringHash, isValidUrl } from './utils.js';
37import { chat_completion_sources, oai_settings } from './openai.js';
38import { getTokenCountAsync } from './tokenizers.js';
39import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
40import { debounce_timeout, SWIPE_SOURCE } from './constants.js';
41
42import { Popup } from './popup.js';
43import { accountStorage } from './util/AccountStorage.js';
44import { getCurrentUserHandle } from './user.js';
45import { kai_settings } from './kai-settings.js';
46
47var RPanelPin = document.getElementById('rm_button_panel_pin');
48var LPanelPin = document.getElementById('lm_button_panel_pin');
49var WIPanelPin = document.getElementById('WI_panel_pin');
50
51var RightNavPanel = document.getElementById('right-nav-panel');
52var RightNavDrawerIcon = document.getElementById('rightNavDrawerIcon');
53var LeftNavPanel = document.getElementById('left-nav-panel');
54var LeftNavDrawerIcon = document.getElementById('leftNavDrawerIcon');
55var WorldInfo = document.getElementById('WorldInfo');
56var WIDrawerIcon = document.getElementById('WIDrawerIcon');
57
58var SelectedCharacterTab = document.getElementById('rm_button_selected_ch');
59
60var connection_made = false;
61var retry_delay = 500;
62let counterNonce = Date.now();
63
64const observerConfig = { childList: true, subtree: true };
65const countTokensDebounced = debounce(RA_CountCharTokens, debounce_timeout.relaxed);
66const countTokensShortDebounced = debounce(RA_CountCharTokens, debounce_timeout.short);
67const checkStatusDebounced = debounce(RA_checkOnlineStatus, debounce_timeout.short);
68
69const 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
90observer.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 */
103export 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 */
125var parsedUA = null;
126
127export 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 */
143export function isMobile() {
144 const mobileTypes = ['mobile', 'tablet'];
145
146 return mobileTypes.includes(getParsedUA()?.platform?.type);
147}
148
149export 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 */
169export 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 */
192export 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:
207export 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 */
272async 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
305export 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>&nbsp;${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
324function 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
355function 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
428function 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
450const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
451
452function 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
464function saveUserInput() {
465 const userInput = String($('#send_textarea').val());
466 localStorage.setItem(getUserInputKey(), userInput);
467 console.debug('User Input -- ', userInput);
468}
469const 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 */
477export 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
657export 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} */
671const sendTextArea = document.querySelector('#send_textarea');
672const chatBlock = document.getElementById('chat');
673const 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 */
678function 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}
689export const autoFitSendTextAreaDebounced = debounce(autoFitSendTextArea, debounce_timeout.short);
690
691// ---------------------------------------------------
692
693export 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}