Add type checking and JSDoc comments to some utils

This commit is contained in:
Cohee 2023-08-22 13:07:24 +03:00
parent 2615eb8532
commit e2bac7ec5f
10 changed files with 409 additions and 138 deletions

64
package-lock.json generated
View File

@ -52,8 +52,12 @@
"sillytavern": "server.js"
},
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@types/moment": "^2.13.0",
"dompurify": "^3.0.5",
"pkg": "^5.8.1",
"pkg-fetch": "^3.5.2",
"showdown": "^2.1.0",
"toastr": "^2.1.4"
}
},
@ -645,11 +649,31 @@
"node": ">= 8"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"dev": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="
},
"node_modules/@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ==",
"deprecated": "This is a stub types definition for Moment (https://github.com/moment/moment). Moment provides its own type definitions, so you don't need @types/moment installed!",
"dev": true,
"dependencies": {
"moment": "*"
}
},
"node_modules/@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
@ -1028,6 +1052,15 @@
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
"integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="
},
"node_modules/commander": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
"dev": true,
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/compressible": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@ -1265,6 +1298,12 @@
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/dompurify": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.5.tgz",
"integrity": "sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==",
"dev": true
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -2157,6 +2196,15 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"node_modules/moment": {
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
"dev": true,
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -3069,6 +3117,22 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
"node_modules/showdown": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz",
"integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==",
"dev": true,
"dependencies": {
"commander": "^9.0.0"
},
"bin": {
"showdown": "bin/showdown.js"
},
"funding": {
"type": "individual",
"url": "https://www.paypal.me/tiviesantos"
}
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",

View File

@ -80,8 +80,12 @@
]
},
"devDependencies": {
"@popperjs/core": "^2.11.8",
"@types/moment": "^2.13.0",
"dompurify": "^3.0.5",
"pkg": "^5.8.1",
"pkg-fetch": "^3.5.2",
"showdown": "^2.1.0",
"toastr": "^2.1.4"
}
}

22
public/jsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"checkJs": true,
"target": "esnext",
"module": "commonjs",
"allowUmdGlobalAccess": true,
"allowSyntheticDefaultImports": true
},
"exclude": [
"node_modules"
],
"typeAcquisition": {
"include": [
"jquery",
"@popperjs/core",
"toastr",
"showdown",
"dompurify",
"@types/moment"
]
}
}

View File

@ -638,10 +638,10 @@ export function getTextTokens(tokenizerType, str) {
function reloadMarkdownProcessor(render_formulas = false) {
if (render_formulas) {
converter = new showdown.Converter({
emoji: "true",
underline: "true",
tables: "true",
parseImgDimensions: "true",
emoji: true,
underline: true,
tables: true,
parseImgDimensions: true,
extensions: [
showdownKatex(
{
@ -655,10 +655,10 @@ function reloadMarkdownProcessor(render_formulas = false) {
}
else {
converter = new showdown.Converter({
emoji: "true",
literalMidWordUnderscores: "true",
parseImgDimensions: "true",
tables: "true",
emoji: true,
literalMidWordUnderscores: true,
parseImgDimensions: true,
tables: true,
});
}
@ -5571,7 +5571,7 @@ async function messageEditDone(div) {
*
* @param {Array} data - An array containing metadata about each chat such as file_name.
* @param {boolean} isGroupChat - A flag indicating if the chat is a group chat.
* @returns {Object} chat_dict - A dictionary where each key is a file_name and the value is the
* @returns {Promise<Object>} chat_dict - A dictionary where each key is a file_name and the value is the
* corresponding chat content fetched from the server.
*/
export async function getChatsFromFiles(data, isGroupChat) {
@ -5621,7 +5621,7 @@ export async function getChatsFromFiles(data, isGroupChat) {
* The function sends a POST request to the server to retrieve all chats for the character. It then
* processes the received data, sorts it by the file name, and returns the sorted data.
*
* @returns {Array} - An array containing metadata of all past chats of the character, sorted
* @returns {Promise<Array>} - An array containing metadata of all past chats of the character, sorted
* in descending order by file name. Returns `undefined` if the fetch request is unsuccessful.
*/
async function getPastCharacterChats() {
@ -7998,7 +7998,6 @@ $(document).ready(function () {
/* $('#set_chat_scenario').on('click', setScenarioOverride); */
///////////// OPTIMIZED LISTENERS FOR LEFT SIDE OPTIONS POPUP MENU //////////////////////
$("#options [id]").on("click", function (event, customData) {
const fromSlashCommand = customData?.fromSlashCommand || false;
var id = $(this).attr("id");
@ -8500,7 +8499,6 @@ $(document).ready(function () {
showSwipeButtons();
});
$(document).on("click", ".mes_edit_delete", async function (event, customData) {
const fromSlashCommand = customData?.fromSlashCommand || false;
const swipeExists = (!chat[this_edit_mes_id].swipes || chat[this_edit_mes_id].swipes.length <= 1 || chat.is_user || parseInt(this_edit_mes_id) !== chat.length - 1);
@ -9072,7 +9070,7 @@ $(document).ready(function () {
await importWorldInfo(file);
break;
default:
toastr.warn('Unknown content type');
toastr.warning('Unknown content type');
console.error('Unknown content type', customContentType);
break;
}

View File

@ -36,8 +36,6 @@ import {
import { debounce, delay, getStringHash, waitUntilCondition } from "./utils.js";
import { chat_completion_sources, oai_settings } from "./openai.js";
var NavToggle = document.getElementById("nav-toggle");
var RPanelPin = document.getElementById("rm_button_panel_pin");
var LPanelPin = document.getElementById("lm_button_panel_pin");
var WIPanelPin = document.getElementById("WI_panel_pin");
@ -47,20 +45,8 @@ var LeftNavPanel = document.getElementById("left-nav-panel");
var WorldInfo = document.getElementById("WorldInfo");
var SelectedCharacterTab = document.getElementById("rm_button_selected_ch");
var AdvancedCharDefsPopup = document.getElementById("character_popup");
var ConfirmationPopup = document.getElementById("dialogue_popup");
var AutoConnectCheckbox = document.getElementById("auto-connect-checkbox");
var AutoLoadChatCheckbox = document.getElementById("auto-load-chat-checkbox");
var SelectedNavTab = ("#" + LoadLocal('SelectedNavTab'));
var create_save_name;
var create_save_description;
var create_save_personality;
var create_save_first_message;
var create_save_scenario;
var create_save_mes_example;
var count_tokens;
var perm_tokens;
var connection_made = false;
var retry_delay = 500;
@ -83,32 +69,6 @@ const observer = new MutationObserver(function (mutations) {
observer.observe(document.documentElement, observerConfig);
/**
* Wait for an element before resolving a promise
* @param {String} querySelector - Selector of element to wait for
* @param {Integer} timeout - Milliseconds to wait before timing out, or 0 for no timeout
*/
function waitForElement(querySelector, timeout) {
return new Promise((resolve, reject) => {
var timer = false;
if (document.querySelectorAll(querySelector).length) return resolve();
const observer = new MutationObserver(() => {
if (document.querySelectorAll(querySelector).length) {
observer.disconnect();
if (timer !== false) clearTimeout(timer);
return resolve();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
if (timeout) timer = setTimeout(() => {
observer.disconnect();
reject();
}, timeout);
});
}
/**
* Converts generation time from milliseconds to a human-readable format.
@ -225,14 +185,6 @@ export function getMessageTimeStamp() {
// triggers:
$("#rm_button_create").on("click", function () { //when "+New Character" is clicked
$(SelectedCharacterTab).children("h2").html(''); // empty nav's 3rd panel tab
//empty temp vars to store new char data for counting
create_save_name = "";
create_save_description = "";
create_save_personality = "";
create_save_first_message = "";
create_save_scenario = "";
create_save_mes_example = "";
});
//when any input is made to the create/edit character form textareas
$("#rm_ch_create_block").on("input", function () { countTokensDebounced(); });
@ -804,7 +756,7 @@ jQuery(async function () {
//console.log('setting pin class via local var');
$(RightNavPanel).addClass('pinnedOpen');
}
if ($(RPanelPin).prop('checked' == true)) {
if (!!$(RPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state');
$(RightNavPanel).addClass('pinnedOpen');
}
@ -814,7 +766,7 @@ jQuery(async function () {
//console.log('setting pin class via local var');
$(LeftNavPanel).addClass('pinnedOpen');
}
if ($(LPanelPin).prop('checked' == true)) {
if (!!$(LPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state');
$(LeftNavPanel).addClass('pinnedOpen');
}
@ -826,7 +778,7 @@ jQuery(async function () {
$(WorldInfo).addClass('pinnedOpen');
}
if ($(WIPanelPin).prop('checked' == true)) {
if (!!$(WIPanelPin).prop('checked')) {
console.debug('setting pin class via checkbox state');
$(WorldInfo).addClass('pinnedOpen');
}
@ -889,8 +841,6 @@ jQuery(async function () {
saveSettingsDebounced();
});
//this makes the chat input text area resize vertically to match the text size (limited by CSS at 50% window height)
$('#send_textarea').on('input', function () {
this.style.height = '40px';
@ -901,7 +851,7 @@ jQuery(async function () {
document.addEventListener('swiped-left', function (e) {
var SwipeButR = $('.swipe_right:last');
var SwipeTargetMesClassParent = e.target.closest('.last_mes');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {
if (SwipeButR.css('display') === 'flex') {
SwipeButR.click();
@ -910,7 +860,7 @@ jQuery(async function () {
});
document.addEventListener('swiped-right', function (e) {
var SwipeButL = $('.swipe_left:last');
var SwipeTargetMesClassParent = e.target.closest('.last_mes');
var SwipeTargetMesClassParent = $(e.target).closest('.last_mes');
if (SwipeTargetMesClassParent !== null) {
if (SwipeButL.css('display') === 'flex') {
SwipeButL.click();

View File

@ -246,29 +246,42 @@ function playMessageSound() {
}
const audio = document.getElementById('audio_message_sound');
audio.volume = 0.8;
audio.pause();
audio.currentTime = 0;
audio.play();
if (audio instanceof HTMLAudioElement) {
audio.volume = 0.8;
audio.pause();
audio.currentTime = 0;
audio.play();
}
}
/**
* Replaces consecutive newlines with a single newline.
* @param {string} x String to be processed.
* @returns {string} Processed string.
* @example
* collapseNewlines("\n\n\n"); // "\n"
*/
function collapseNewlines(x) {
return x.replaceAll(/\n+/g, "\n");
}
/**
* Fix formatting problems in markdown.
* @param {string} text Text to be processed.
* @returns {string} Processed text.
* @example
* "^example * text*\n" // "^example *text*\n"
* "^*example * text\n"// "^*example* text\n"
* "^example *text *\n" // "^example *text*\n"
* "^* example * text\n" // "^*example* text\n"
* // take note that the side you move the asterisk depends on where its pairing is
* // i.e. both of the following strings have the same broken asterisk ' * ',
* // but you move the first to the left and the second to the right, to match the non-broken asterisk
* "^example * text*\n" // "^*example * text\n"
* // and you HAVE to handle the cases where multiple pairs of asterisks exist in the same line
* "^example * text* * harder problem *\n" // "^example *text* *harder problem*\n"
*/
function fixMarkdown(text) {
// fix formatting problems in markdown
// e.g.:
// "^example * text*\n" -> "^example *text*\n"
// "^*example * text\n" -> "^*example* text\n"
// "^example *text *\n" -> "^example *text*\n"
// "^* example * text\n" -> "^*example* text\n"
// take note that the side you move the asterisk depends on where its pairing is
// i.e. both of the following strings have the same broken asterisk ' * ',
// but you move the first to the left and the second to the right, to match the non-broken asterisk "^example * text*\n" "^*example * text\n"
// and you HAVE to handle the cases where multiple pairs of asterisks exist in the same line
// i.e. "^example * text* * harder problem *\n" -> "^example *text* *harder problem*\n"
// Find pairs of formatting characters and capture the text in between them
const format = /([\*_]{1,2})([\s\S]*?)\1/gm;
let matches = [];

View File

@ -25,13 +25,12 @@ function createStatBlock(statName, statValue) {
* @returns {number} - The stat value if it is a number, otherwise 0.
*/
function verifyStatValue(stat) {
return isNaN(stat) ? 0 : stat;
return isNaN(Number(stat)) ? 0 : Number(stat);
}
/**
* Calculates total stats from character statistics.
*
* @param {Object} charStats - Object containing character statistics.
* @returns {Object} - Object containing total statistics.
*/
function calculateTotalStats() {

View File

@ -7,7 +7,7 @@ import {
getCharacters,
entitiesFilter,
} from "../script.js";
import { FILTER_TYPES } from "./filters.js";
import { FILTER_TYPES, FilterHelper } from "./filters.js";
import { groupCandidatesFilter, selected_group } from "./group-chats.js";
import { uuidv4 } from "./utils.js";
@ -24,7 +24,6 @@ export {
importTags,
};
const random_id = () => uuidv4();
const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter';
const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter';
@ -49,17 +48,21 @@ const InListActionable = {
}
const DEFAULT_TAGS = [
{ id: random_id(), name: "Plain Text" },
{ id: random_id(), name: "OpenAI" },
{ id: random_id(), name: "W++" },
{ id: random_id(), name: "Boostyle" },
{ id: random_id(), name: "PList" },
{ id: random_id(), name: "AliChat" },
{ id: uuidv4(), name: "Plain Text" },
{ id: uuidv4(), name: "OpenAI" },
{ id: uuidv4(), name: "W++" },
{ id: uuidv4(), name: "Boostyle" },
{ id: uuidv4(), name: "PList" },
{ id: uuidv4(), name: "AliChat" },
];
let tags = [];
let tag_map = {};
/**
* Applies the favorite filter to the character list.
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
*/
function applyFavFilter(filterHelper) {
const isSelected = $(this).hasClass('selected');
const displayFavoritesOnly = !isSelected;
@ -68,6 +71,10 @@ function applyFavFilter(filterHelper) {
filterHelper.setFilterData(FILTER_TYPES.FAV, displayFavoritesOnly);
}
/**
* Applies the "is group" filter to the character list.
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
*/
function filterByGroups(filterHelper) {
const isSelected = $(this).hasClass('selected');
const displayGroupsOnly = !isSelected;
@ -253,7 +260,7 @@ async function importTags(imported_char) {
function createNewTag(tagName) {
const tag = {
id: random_id(),
id: uuidv4(),
name: tagName,
color: '',
};

View File

@ -7,6 +7,14 @@ export function onlyUnique(value, index, array) {
return array.indexOf(value) === index;
}
/**
* Checks if a string only contains digits.
* @param {string} str The string to check.
* @returns {boolean} True if the string only contains digits, false otherwise.
* @example
* isDigitsOnly('123'); // true
* isDigitsOnly('abc'); // false
*/
export function isDigitsOnly(str) {
return /^\d+$/.test(str);
}
@ -16,6 +24,13 @@ export function getSortableDelay() {
return navigator.maxTouchPoints > 0 ? 750 : 100;
}
/**
* Rearranges an array in a random order.
* @param {any[]} array The array to shuffle.
* @returns {any[]} The shuffled array.
* @example
* shuffle([1, 2, 3]); // [2, 3, 1]
*/
export function shuffle(array) {
let currentIndex = array.length,
randomIndex;
@ -31,6 +46,12 @@ export function shuffle(array) {
return array;
}
/**
* Downloads a file to the user's devices.
* @param {BlobPart} content File content to download.
* @param {string} fileName File name.
* @param {string} contentType File content type.
*/
export function download(content, fileName, contentType) {
const a = document.createElement("a");
const file = new Blob([content], { type: contentType });
@ -49,12 +70,17 @@ export async function urlContentToDataUri(url, params) {
});
}
/**
* Returns a promise that resolves to the file's text.
* @param {Blob} file The file to read.
* @returns {Promise<string>} A promise that resolves to the file's text.
*/
export function getFileText(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsText(file);
reader.onload = function () {
resolve(reader.result);
resolve(String(reader.result));
};
reader.onerror = function (error) {
reject(error);
@ -62,6 +88,10 @@ export function getFileText(file) {
});
}
/**
* Returns a promise that resolves to the file's array buffer.
* @param {Blob} file The file to read.
*/
export function getFileBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@ -75,12 +105,17 @@ export function getFileBuffer(file) {
});
}
/**
* Returns a promise that resolves to the base64 encoded string of a file.
* @param {Blob} file The file to read.
* @returns {Promise<string>} A promise that resolves to the base64 encoded string.
*/
export function getBase64Async(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = function () {
resolve(reader.result);
resolve(String(reader.result));
};
reader.onerror = function (error) {
reject(error);
@ -88,15 +123,26 @@ export function getBase64Async(file) {
});
}
/**
* Parses a file blob as a JSON object.
* @param {Blob} file The file to read.
* @returns {Promise<any>} A promise that resolves to the parsed JSON object.
*/
export async function parseJsonFile(file) {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = event => resolve(JSON.parse(event.target.result));
fileReader.onerror = error => reject(error);
fileReader.readAsText(file);
fileReader.onload = event => resolve(JSON.parse(String(event.target.result)));
fileReader.onerror = error => reject(error);
});
}
/**
* Calculates a hash code for a string.
* @param {string} str The string to hash.
* @param {number} [seed=0] The seed to use for the hash.
* @returns {number} The hash code.
*/
export function getStringHash(str, seed = 0) {
if (typeof str !== 'string') {
return 0;
@ -116,6 +162,12 @@ export function getStringHash(str, seed = 0) {
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
};
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
* @param {function} func The function to debounce.
* @param {number} [timeout=300] The timeout in milliseconds.
* @returns {function} The debounced function.
*/
export function debounce(func, timeout = 300) {
let timer;
return (...args) => {
@ -124,6 +176,12 @@ export function debounce(func, timeout = 300) {
};
}
/**
* Creates a throttled function that only invokes func at most once per every limit milliseconds.
* @param {function} func The function to throttle.
* @param {number} [limit=300] The limit in milliseconds.
* @returns {function} The throttled function.
*/
export function throttle(func, limit = 300) {
let lastCall;
return (...args) => {
@ -135,6 +193,11 @@ export function throttle(func, limit = 300) {
};
}
/**
* Checks if an element is in the viewport.
* @param {any[]} el The element to check.
* @returns {boolean} True if the element is in the viewport, false otherwise.
*/
export function isElementInViewport(el) {
if (typeof jQuery === "function" && el instanceof jQuery) {
el = el[0];
@ -148,6 +211,12 @@ export function isElementInViewport(el) {
);
}
/**
* Returns a name that is unique among the names that exist.
* @param {string} name The name to check.
* @param {{ (y: any): boolean; }} exists Function to check if name exists.
* @returns {string} A unique name.
*/
export function getUniqueName(name, exists) {
let i = 1;
let baseName = name;
@ -158,18 +227,48 @@ export function getUniqueName(name, exists) {
return name;
}
export const delay = (ms) => new Promise((res) => setTimeout(res, ms));
export const isSubsetOf = (a, b) => (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;
/**
* Returns a promise that resolves after the specified number of milliseconds.
* @param {number} ms The number of milliseconds to wait.
* @returns {Promise<void>} A promise that resolves after the specified number of milliseconds.
*/
export function delay(ms) {
return new Promise((res) => setTimeout(res, ms));
}
/**
* Checks if an array is a subset of another array.
* @param {any[]} a Array A
* @param {any[]} b Array B
* @returns {boolean} True if B is a subset of A, false otherwise.
*/
export function isSubsetOf(a, b) {
return (Array.isArray(a) && Array.isArray(b)) ? b.every(val => a.includes(val)) : false;
}
/**
* Increments the trailing number in a string.
* @param {string} str The string to process.
* @returns {string} The string with the trailing number incremented by 1.
* @example
* incrementString('Hello, world! 1'); // 'Hello, world! 2'
*/
export function incrementString(str) {
// Find the trailing number or it will match the empty string
const count = str.match(/\d*$/);
// Take the substring up until where the integer was matched
// Concatenate it to the matched count incremented by 1
return str.substr(0, count.index) + (++count[0]);
return str.substring(0, count.index) + (Number(count[0]) + 1);
};
/**
* Formats a string using the specified arguments.
* @param {string} format The format string.
* @returns {string} The formatted string.
* @example
* stringFormat('Hello, {0}!', 'world'); // 'Hello, world!'
*/
export function stringFormat(format) {
const args = Array.prototype.slice.call(arguments, 1);
return format.replace(/{(\d+)}/g, function (match, number) {
@ -180,7 +279,11 @@ export function stringFormat(format) {
});
};
// Save the caret position in a contenteditable element
/**
* Save the caret position in a contenteditable element.
* @param {Element} element The element to save the caret position of.
* @returns {{ start: number, end: number }} An object with the start and end offsets of the caret.
*/
export function saveCaretPosition(element) {
// Get the current selection
const selection = window.getSelection();
@ -209,7 +312,11 @@ export function saveCaretPosition(element) {
return position;
}
// Restore the caret position in a contenteditable element
/**
* Restore the caret position in a contenteditable element.
* @param {Element} element The element to restore the caret position of.
* @param {{ start: any; end: any; }} position An object with the start and end offsets of the caret.
*/
export function restoreCaretPosition(element, position) {
// If the position is null, do nothing
if (!position) {
@ -236,6 +343,11 @@ export async function resetScrollHeight(element) {
$(element).css('height', $(element).prop('scrollHeight') + 3 + 'px');
}
/**
* Sets the height of an element to its scroll height.
* @param {JQuery<HTMLElement>} element The element to initialize the scroll height of.
* @returns {Promise<void>} A promise that resolves when the scroll height has been initialized.
*/
export async function initScrollHeight(element) {
await delay(1);
@ -252,15 +364,27 @@ export async function initScrollHeight(element) {
//resetScrollHeight(element);
}
/**
* Compares elements by their CSS order property. Used for sorting.
* @param {any} a The first element.
* @param {any} b The second element.
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
*/
export function sortByCssOrder(a, b) {
const _a = Number($(a).css('order'));
const _b = Number($(b).css('order'));
return _a - _b;
}
/**
* Trims a string to the end of a nearest sentence.
* @param {string} input The string to trim.
* @param {boolean} include_newline Whether to include a newline character in the trimmed string.
* @returns {string} The trimmed string.
* @example
* end_trim_to_sentence('Hello, world! I am from'); // 'Hello, world!'
*/
export function end_trim_to_sentence(input, include_newline = false) {
// inspired from https://github.com/kaihordewebui/kaihordewebui.github.io/blob/06b95e6b7720eb85177fbaf1a7f52955d7cdbc02/index.html#L4853-L4867
const punctuation = new Set(['.', '!', '?', '*', '"', ')', '}', '`', ']', '$', '。', '', '', '”', '', '】', '】', '', '」', '】']); // extend this as you see fit
let last = -1;
@ -285,6 +409,15 @@ export function end_trim_to_sentence(input, include_newline = false) {
return input.substring(0, last + 1).trimEnd();
}
/**
* Counts the number of occurrences of a character in a string.
* @param {string} string The string to count occurrences in.
* @param {string} character The character to count occurrences of.
* @returns {number} The number of occurrences of the character in the string.
* @example
* countOccurrences('Hello, world!', 'l'); // 3
* countOccurrences('Hello, world!', 'x'); // 0
*/
export function countOccurrences(string, character) {
let count = 0;
@ -297,6 +430,14 @@ export function countOccurrences(string, character) {
return count;
}
/**
* Checks if a number is odd.
* @param {number} number The number to check.
* @returns {boolean} True if the number is odd, false otherwise.
* @example
* isOdd(3); // true
* isOdd(4); // false
*/
export function isOdd(number) {
return number % 2 !== 0;
}
@ -337,6 +478,12 @@ export function timestampToMoment(timestamp) {
return moment.invalid();
}
/**
* Compare two moment objects for sorting.
* @param {*} a The first moment object.
* @param {*} b The second moment object.
* @returns {number} A negative number if a is before b, a positive number if a is after b, or 0 if they are equal.
*/
export function sortMoments(a, b) {
if (a.isBefore(b)) {
return 1;
@ -347,14 +494,21 @@ export function sortMoments(a, b) {
}
}
/** Split string to parts no more than length in size */
export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ', '']) {
const delim = delimitiers[0] ?? '';
/** Split string to parts no more than length in size.
* @param {string} input The string to split.
* @param {number} length The maximum length of each part.
* @param {string[]} delimiters The delimiters to use when splitting the string.
* @returns {string[]} The split string.
* @example
* splitRecursive('Hello, world!', 3); // ['Hel', 'lo,', 'wor', 'ld!']
*/
export function splitRecursive(input, length, delimiters = ['\n\n', '\n', ' ', '']) {
const delim = delimiters[0] ?? '';
const parts = input.split(delim);
const flatParts = parts.flatMap(p => {
if (p.length < length) return p;
return splitRecursive(input, length, delimitiers.slice(1));
return splitRecursive(input, length, delimiters.slice(1));
});
// Merge short chunks
@ -378,6 +532,13 @@ export function splitRecursive(input, length, delimitiers = ['\n\n', '\n', ' ',
return result;
}
/**
* Checks if a string is a valid data URL.
* @param {string} str The string to check.
* @returns {boolean} True if the string is a valid data URL, false otherwise.
* @example
* isDataURL('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA...'); // true
*/
export function isDataURL(str) {
const regex = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)*;?)?(base64)?,([a-z0-9!$&',()*+;=\-_%.~:@\/?#]+)?$/i;
return regex.test(str);
@ -392,6 +553,13 @@ export function getCharaFilename(chid) {
}
}
/**
* Extracts words from a string.
* @param {string} value The string to extract words from.
* @returns {string[]} The extracted words.
* @example
* extractAllWords('Hello, world!'); // ['hello', 'world']
*/
export function extractAllWords(value) {
const words = [];
@ -406,21 +574,45 @@ export function extractAllWords(value) {
return words;
}
/**
* Escapes a string for use in a regular expression.
* @param {string} string The string to escape.
* @returns {string} The escaped string.
* @example
* escapeRegex('^Hello$'); // '\\^Hello\\$'
*/
export function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
}
/**
* Provides an interface for rate limiting function calls.
*/
export class RateLimiter {
constructor(intervalMillis) {
this._intervalMillis = intervalMillis;
this._lastResolveTime = 0;
this._pendingResolve = Promise.resolve();
/**
* Creates a new RateLimiter.
* @param {number} interval The interval in milliseconds.
* @example
* const rateLimiter = new RateLimiter(1000);
* rateLimiter.waitForResolve().then(() => {
* console.log('Waited 1000ms');
* });
*/
constructor(interval) {
this.interval = interval;
this.lastResolveTime = 0;
this.pendingResolve = Promise.resolve();
}
/**
* Waits for the remaining time in the interval.
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
* @returns {Promise<void>} A promise that resolves when the remaining time has elapsed.
*/
_waitRemainingTime(abortSignal) {
const currentTime = Date.now();
const elapsedTime = currentTime - this._lastResolveTime;
const remainingTime = Math.max(0, this._intervalMillis - elapsedTime);
const elapsedTime = currentTime - this.lastResolveTime;
const remainingTime = Math.max(0, this.interval - elapsedTime);
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
@ -436,19 +628,29 @@ export class RateLimiter {
});
}
/**
* Waits for the next interval to elapse.
* @param {AbortSignal} abortSignal An optional AbortSignal to abort the wait.
* @returns {Promise<void>} A promise that resolves when the next interval has elapsed.
*/
async waitForResolve(abortSignal) {
await this._pendingResolve;
this._pendingResolve = this._waitRemainingTime(abortSignal);
await this.pendingResolve;
this.pendingResolve = this._waitRemainingTime(abortSignal);
// Update the last resolve time
this._lastResolveTime = Date.now() + this._intervalMillis;
console.debug(`RateLimiter.waitForResolve() ${this._lastResolveTime}`);
this.lastResolveTime = Date.now() + this.interval;
console.debug(`RateLimiter.waitForResolve() ${this.lastResolveTime}`);
}
}
// Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html
//import tavern png data. adapted from png-chunks-extract under MIT license
//accepts png input data, and returns the extracted JSON
/**
* Extracts a JSON object from a PNG file.
* Taken from https://github.com/LostRuins/lite.koboldai.net/blob/main/index.html
* Adapted from png-chunks-extract under MIT license
* @param {Uint8Array} data The PNG data to extract the JSON from.
* @param {string} identifier The identifier to look for in the PNG tEXT data.
* @returns {object} The extracted JSON object.
*/
export function extractDataFromPng(data, identifier = 'chara') {
console.log("Attempting PNG import...");
let uint8 = new Uint8Array(4);
@ -599,6 +801,13 @@ export async function saveBase64AsFile(base64Data, characterName, filename = "",
}
}
/**
* Creates a thumbnail from a data URL.
* @param {string} dataUrl The data URL encoded data of the image.
* @param {number} maxWidth The maximum width of the thumbnail.
* @param {number} maxHeight The maximum height of the thumbnail.
* @returns {Promise<string>} A promise that resolves to the thumbnail data URL.
*/
export function createThumbnail(dataUrl, maxWidth, maxHeight) {
return new Promise((resolve, reject) => {
const img = new Image();
@ -634,6 +843,13 @@ export function createThumbnail(dataUrl, maxWidth, maxHeight) {
});
}
/**
* Waits for a condition to be true. Throws an error if the condition is not true within the timeout.
* @param {{ (): boolean; }} condition The condition to wait for.
* @param {number} [timeout=1000] The timeout in milliseconds.
* @param {number} [interval=100] The interval in milliseconds.
* @returns {Promise<void>} A promise that resolves when the condition is true.
*/
export async function waitUntilCondition(condition, timeout = 1000, interval = 100) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
@ -651,6 +867,12 @@ export async function waitUntilCondition(condition, timeout = 1000, interval = 1
});
}
/**
* Returns a UUID v4 string.
* @returns {string} A UUID v4 string.
* @example
* uuidv4(); // '3e2fd9e1-0a7a-4f6d-9aaf-8a7a4babe7eb'
*/
export function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
@ -659,6 +881,11 @@ export function uuidv4() {
});
}
/**
* Clones an object using JSON serialization.
* @param {any} obj The object to clone.
* @returns {any} A deep clone of the object.
*/
export function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}

View File

@ -418,7 +418,7 @@ function getWorldEntry(name, data, entry) {
keyInput.on("input", function () {
const uid = $(this).data("uid");
const value = $(this).val();
const value = String($(this).val());
resetScrollHeight(this);
data.entries[uid].key = value
.split(",")
@ -454,7 +454,7 @@ function getWorldEntry(name, data, entry) {
keySecondaryInput.data("uid", entry.uid);
keySecondaryInput.on("input", function () {
const uid = $(this).data("uid");
const value = $(this).val();
const value = String($(this).val());
resetScrollHeight(this);
data.entries[uid].keysecondary = value
.split(",")
@ -1506,19 +1506,6 @@ jQuery(() => {
return;
}
/*
if (deviceInfo.device.type === 'desktop') {
let selectScrollTop = null;
e.preventDefault();
const option = $(e.target);
const selectElement = $(this)[0];
selectScrollTop = selectElement.scrollTop;
option.prop('selected', !option.prop('selected'));
await delay(1);
selectElement.scrollTop = selectScrollTop;
}
*/
onWorldInfoChange('__notSlashCommand__');
});