mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into update-git-workflows
This commit is contained in:
@@ -75,6 +75,7 @@ module.exports = {
|
||||
'plugins/**',
|
||||
'**/*.min.js',
|
||||
'public/scripts/extensions/quick-reply/lib/**',
|
||||
'public/scripts/extensions/tts/lib/**',
|
||||
],
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
|
1
.github/readme.md
vendored
1
.github/readme.md
vendored
@@ -393,6 +393,7 @@ GNU Affero General Public License for more details.**
|
||||
* Icon theme by Font Awesome <https://fontawesome.com> (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
|
||||
* Default content by @OtisAlejandro (Seraphina character and lorebook) and @kallmeflocc (10K Discord Users Celebratory Background)
|
||||
* Docker guide by [@mrguymiah](https://github.com/mrguymiah) and [@Bronya-Rand](https://github.com/Bronya-Rand)
|
||||
* kokoro-js library by [@hexgrad](https://github.com/hexgrad) (Apache-2.0 License)
|
||||
|
||||
## Top Contributors
|
||||
|
||||
|
31
package-lock.json
generated
31
package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@iconfu/svg-inject": "^1.2.3",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zeldafan0225/ai_horde": "^5.2.0",
|
||||
"archiver": "^7.0.1",
|
||||
@@ -95,11 +95,11 @@
|
||||
"@types/jquery": "^3.5.32",
|
||||
"@types/jquery-cropper": "^1.0.4",
|
||||
"@types/jquery.transit": "^0.9.33",
|
||||
"@types/jqueryui": "^1.12.23",
|
||||
"@types/jqueryui": "^1.12.24",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^18.19.78",
|
||||
"@types/node": "^18.19.80",
|
||||
"@types/node-persist": "^3.1.8",
|
||||
"@types/png-chunk-text": "^1.0.3",
|
||||
"@types/png-chunks-encode": "^1.0.2",
|
||||
@@ -915,9 +915,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mozilla/readability": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.5.0.tgz",
|
||||
"integrity": "sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz",
|
||||
"integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -1285,9 +1285,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/jqueryui": {
|
||||
"version": "1.12.23",
|
||||
"resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.23.tgz",
|
||||
"integrity": "sha512-pm1yVNVI29B9IGw41anCEzA5eR2r1pYc7flqD471ZT7B0yUXIY7YNe/zq7LGpihIGXNzWyG+Q4YQSzv2AF3fNA==",
|
||||
"version": "1.12.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/jqueryui/-/jqueryui-1.12.24.tgz",
|
||||
"integrity": "sha512-E2sGULwzMhg4kAeOV+gYcXjg988RuPkviWCt09jLe6GGK9sHM7dTqS8H7JMuUWoZQBucIBzBAgM5o/ezKUFkeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1354,9 +1354,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.78",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.78.tgz",
|
||||
"integrity": "sha512-m1ilZCTwKLkk9rruBJXFeYN0Bc5SbjirwYX/Td3MqPfioYbgun3IvK/m8dQxMCnrPGZPg1kvXjp3SIekCN/ynw==",
|
||||
"version": "18.19.80",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.80.tgz",
|
||||
"integrity": "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
@@ -2136,9 +2136,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
|
@@ -4,7 +4,7 @@
|
||||
"@agnai/sentencepiece-js": "^1.1.1",
|
||||
"@agnai/web-tokenizers": "^0.1.3",
|
||||
"@iconfu/svg-inject": "^1.2.3",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zeldafan0225/ai_horde": "^5.2.0",
|
||||
"archiver": "^7.0.1",
|
||||
@@ -125,11 +125,11 @@
|
||||
"@types/jquery": "^3.5.32",
|
||||
"@types/jquery-cropper": "^1.0.4",
|
||||
"@types/jquery.transit": "^0.9.33",
|
||||
"@types/jqueryui": "^1.12.23",
|
||||
"@types/jqueryui": "^1.12.24",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^18.19.78",
|
||||
"@types/node": "^18.19.80",
|
||||
"@types/node-persist": "^3.1.8",
|
||||
"@types/png-chunk-text": "^1.0.3",
|
||||
"@types/png-chunks-encode": "^1.0.2",
|
||||
|
25
public/global.d.ts
vendored
25
public/global.d.ts
vendored
@@ -1,18 +1,21 @@
|
||||
import libs from './lib';
|
||||
import getContext from './scripts/st-context';
|
||||
|
||||
// Global namespace modules
|
||||
declare var ai;
|
||||
declare var pdfjsLib;
|
||||
declare var ePub;
|
||||
|
||||
declare var SillyTavern: {
|
||||
getContext(): typeof getContext;
|
||||
llm: any;
|
||||
libs: typeof libs;
|
||||
};
|
||||
|
||||
declare global {
|
||||
// Global namespace modules
|
||||
interface Window {
|
||||
ai: any;
|
||||
}
|
||||
|
||||
declare var pdfjsLib;
|
||||
declare var ePub;
|
||||
|
||||
declare var SillyTavern: {
|
||||
getContext(): typeof getContext;
|
||||
llm: any;
|
||||
libs: typeof libs;
|
||||
};
|
||||
|
||||
// Jquery plugins
|
||||
interface JQuery {
|
||||
nanogallery2(options?: any): JQuery;
|
||||
|
@@ -5382,6 +5382,7 @@
|
||||
<option value="2" data-i18n="Manual">Manual</option>
|
||||
<option value="0" data-i18n="Natural order">Natural order</option>
|
||||
<option value="1" data-i18n="List order">List order</option>
|
||||
<option value="3" data-i18n="Pooled order">Pooled order</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex1 flexGap5">
|
||||
|
@@ -14,7 +14,8 @@
|
||||
"**/.git/**",
|
||||
"lib/**",
|
||||
"**/*.min.js",
|
||||
"scripts/extensions/quick-reply/lib/**"
|
||||
"scripts/extensions/quick-reply/lib/**",
|
||||
"scripts/extensions/tts/lib/**"
|
||||
],
|
||||
"typeAcquisition": {
|
||||
"include": []
|
||||
|
@@ -1400,6 +1400,7 @@ export async function selectCharacterById(id) {
|
||||
} else {
|
||||
//if clicked on character that was already selected
|
||||
selected_button = 'character_edit';
|
||||
await unshallowCharacter(this_chid);
|
||||
select_selected_character(this_chid);
|
||||
}
|
||||
}
|
||||
@@ -3372,8 +3373,8 @@ class StreamingProcessor {
|
||||
}
|
||||
|
||||
if (this.type !== 'impersonate') {
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
|
||||
} else {
|
||||
await eventSource.emit(event_types.IMPERSONATE_READY, text);
|
||||
}
|
||||
@@ -3402,8 +3403,8 @@ class StreamingProcessor {
|
||||
|
||||
const noEmitTypes = ['swipe', 'impersonate', 'continue'];
|
||||
if (!noEmitTypes.includes(this.type)) {
|
||||
eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId);
|
||||
eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId);
|
||||
eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
|
||||
eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3939,7 +3940,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
|
||||
let message = chatItem.mes;
|
||||
let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
|
||||
let options = { isPrompt: true, depth: (coreChat.length - index - 1) };
|
||||
let options = { isPrompt: true, depth: (coreChat.length - index - (isContinue ? 2 : 1)) };
|
||||
|
||||
let regexedMessage = getRegexedString(message, regexType, options);
|
||||
regexedMessage = await appendFileContent(chatItem, regexedMessage);
|
||||
@@ -3957,7 +3958,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
|
||||
const promptReasoning = new PromptReasoning();
|
||||
for (let i = coreChat.length - 1; i >= 0; i--) {
|
||||
const depth = coreChat.length - i - 1;
|
||||
const depth = coreChat.length - i - (isContinue ? 2 : 1);
|
||||
const isPrefix = isContinue && i === coreChat.length - 1;
|
||||
coreChat[i] = {
|
||||
...coreChat[i],
|
||||
@@ -6014,9 +6015,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
|
||||
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
|
||||
}
|
||||
const chat_id = (chat.length - 1);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
|
||||
addOneMessage(chat[chat_id], { type: 'swipe' });
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
|
||||
} else {
|
||||
chat[chat.length - 1]['mes'] = getMessage;
|
||||
}
|
||||
@@ -6037,9 +6038,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
|
||||
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
|
||||
}
|
||||
const chat_id = (chat.length - 1);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
|
||||
addOneMessage(chat[chat_id], { type: 'swipe' });
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
|
||||
} else if (type === 'appendFinal') {
|
||||
oldMessage = chat[chat.length - 1]['mes'];
|
||||
console.debug('Trying to appendFinal.');
|
||||
@@ -6057,9 +6058,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
|
||||
chat[chat.length - 1]['extra']['token_count'] = await getTokenCountAsync(tokenCountText, 0);
|
||||
}
|
||||
const chat_id = (chat.length - 1);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
|
||||
addOneMessage(chat[chat_id], { type: 'swipe' });
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
|
||||
|
||||
} else {
|
||||
console.debug('entering chat update routine for non-swipe post');
|
||||
@@ -6099,9 +6100,9 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
|
||||
saveImageToMessage(img, chat[chat.length - 1]);
|
||||
const chat_id = (chat.length - 1);
|
||||
|
||||
!fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
|
||||
!fromStreaming && await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, type);
|
||||
addOneMessage(chat[chat_id]);
|
||||
!fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
|
||||
!fromStreaming && await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, type);
|
||||
}
|
||||
|
||||
const item = chat[chat.length - 1];
|
||||
@@ -6849,8 +6850,8 @@ async function getChatResult() {
|
||||
|
||||
if (chat.length === 1) {
|
||||
const chat_id = (chat.length - 1);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, chat_id, 'first_message');
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, chat_id, 'first_message');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8728,10 +8729,10 @@ async function createOrEditCharacter(e) {
|
||||
if (shouldRegenerateMessage) {
|
||||
chat.splice(0, chat.length, message);
|
||||
const messageId = (chat.length - 1);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'first_message');
|
||||
await clearChat();
|
||||
await printMessages();
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'first_message');
|
||||
await saveChatConditional();
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -130,9 +130,10 @@ function getConverter(type) {
|
||||
* @param {number} start Starting message ID
|
||||
* @param {number} end Ending message ID (inclusive)
|
||||
* @param {boolean} unhide If true, unhide the messages instead.
|
||||
* @param {string} nameFitler Optional name filter
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function hideChatMessageRange(start, end, unhide) {
|
||||
export async function hideChatMessageRange(start, end, unhide, nameFitler = null) {
|
||||
if (isNaN(start)) return;
|
||||
if (!end) end = start;
|
||||
const hide = !unhide;
|
||||
@@ -140,6 +141,7 @@ export async function hideChatMessageRange(start, end, unhide) {
|
||||
for (let messageId = start; messageId <= end; messageId++) {
|
||||
const message = chat[messageId];
|
||||
if (!message) continue;
|
||||
if (nameFitler && message.name !== nameFitler) continue;
|
||||
|
||||
message.is_system = hide;
|
||||
|
||||
@@ -1506,7 +1508,7 @@ jQuery(function () {
|
||||
embedMessageFile(messageId, messageBlock);
|
||||
});
|
||||
|
||||
$(document).on('click', '.editor_maximize', function () {
|
||||
$(document).on('click', '.editor_maximize', async function () {
|
||||
const broId = $(this).attr('data-for');
|
||||
const bro = $(`#${broId}`);
|
||||
const contentEditable = bro.is('[contenteditable]');
|
||||
@@ -1525,6 +1527,7 @@ jQuery(function () {
|
||||
textarea.value = String(contentEditable ? bro[0].innerText : bro.val());
|
||||
textarea.classList.add('height100p', 'wide100p', 'maximized_textarea');
|
||||
bro.hasClass('monospace') && textarea.classList.add('monospace');
|
||||
bro.hasClass('mdHotkeys') && textarea.classList.add('mdHotkeys');
|
||||
textarea.addEventListener('input', function () {
|
||||
if (contentEditable) {
|
||||
bro[0].innerText = textarea.value;
|
||||
@@ -1565,7 +1568,7 @@ jQuery(function () {
|
||||
});
|
||||
}
|
||||
|
||||
callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
await callGenericPopup(wrapper, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
});
|
||||
|
||||
$(document).on('click', 'body.documentstyle .mes .mes_text', function () {
|
||||
|
@@ -9,6 +9,8 @@ import { QuickReplySettings } from './src/QuickReplySettings.js';
|
||||
import { SlashCommandHandler } from './src/SlashCommandHandler.js';
|
||||
import { ButtonUi } from './src/ui/ButtonUi.js';
|
||||
import { SettingsUi } from './src/ui/SettingsUi.js';
|
||||
import { debounceAsync } from '../../utils.js';
|
||||
export { debounceAsync };
|
||||
|
||||
|
||||
|
||||
@@ -17,32 +19,6 @@ const _VERBOSE = true;
|
||||
export const debug = (...msg) => _VERBOSE ? console.debug('[QR2]', ...msg) : null;
|
||||
export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
|
||||
export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null;
|
||||
/**
|
||||
* 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 debounceAsync(func, timeout = 300) {
|
||||
let timer;
|
||||
/**@type {Promise}*/
|
||||
let debouncePromise;
|
||||
/**@type {Function}*/
|
||||
let debounceResolver;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
if (!debouncePromise) {
|
||||
debouncePromise = new Promise(resolve => {
|
||||
debounceResolver = resolve;
|
||||
});
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
debounceResolver(func.apply(this, args));
|
||||
debouncePromise = null;
|
||||
}, timeout);
|
||||
return debouncePromise;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const defaultConfig = {
|
||||
|
@@ -110,14 +110,14 @@
|
||||
</div>
|
||||
<div class="flex-container wide100p marginTop5">
|
||||
<div class="flex1 flex-container flexNoGap">
|
||||
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
|
||||
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. When blank / 'Unlimited' or -1, also affect message to continue on Continue.">
|
||||
<span data-i18n="Min Depth">Min Depth</span>
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</small>
|
||||
<input name="min_depth" class="text_pole textarea_compact" type="number" min="0" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
|
||||
<input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
|
||||
</div>
|
||||
<div class="flex1 flex-container flexNoGap">
|
||||
<small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
|
||||
<small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. Max must be greater than Min for regex to apply.">
|
||||
<span data-i18n="Max Depth">Max Depth</span>
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</small>
|
||||
|
@@ -103,8 +103,8 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
|
||||
}
|
||||
|
||||
// Check if the depth is within the min/max depth
|
||||
if (typeof depth === 'number' && depth >= 0) {
|
||||
if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= 0 && depth < script.minDepth) {
|
||||
if (typeof depth === 'number') {
|
||||
if (!isNaN(script.minDepth) && script.minDepth !== null && script.minDepth >= -1 && depth < script.minDepth) {
|
||||
console.debug(`getRegexedString: Skipping script ${script.scriptName} because depth ${depth} is less than minDepth ${script.minDepth}`);
|
||||
return;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
|
||||
}
|
||||
|
||||
const getRegexString = () => {
|
||||
switch(Number(regexScript.substituteRegex)) {
|
||||
switch (Number(regexScript.substituteRegex)) {
|
||||
case substitute_find_regex.NONE:
|
||||
return regexScript.findRegex;
|
||||
case substitute_find_regex.RAW:
|
||||
|
@@ -3739,9 +3739,9 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
|
||||
};
|
||||
context.chat.push(message);
|
||||
const messageId = context.chat.length - 1;
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId, 'extension');
|
||||
context.addOneMessage(message);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
|
||||
await context.saveChat();
|
||||
}
|
||||
|
||||
|
@@ -27,6 +27,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
|
||||
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { GoogleTranslateTtsProvider } from './google-translate.js';
|
||||
import { KokoroTtsProvider } from './kokoro.js';
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
@@ -94,6 +95,7 @@ const ttsProviders = {
|
||||
'Google Translate': GoogleTranslateTtsProvider,
|
||||
GSVI: GSVITtsProvider,
|
||||
'GPT-SoVITS-V2 (Unofficial)': GptSovitsV2Provider,
|
||||
Kokoro: KokoroTtsProvider,
|
||||
Novel: NovelTtsProvider,
|
||||
OpenAI: OpenAITtsProvider,
|
||||
'OpenAI Compatible': OpenAICompatibleTtsProvider,
|
||||
@@ -716,6 +718,9 @@ async function loadTtsProvider(provider) {
|
||||
}
|
||||
|
||||
function onTtsProviderChange() {
|
||||
if (typeof ttsProvider?.dispose === 'function') {
|
||||
ttsProvider.dispose();
|
||||
}
|
||||
const ttsProviderSelection = $('#tts_provider').val();
|
||||
extension_settings.tts.currentProvider = ttsProviderSelection;
|
||||
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
|
||||
|
113
public/scripts/extensions/tts/kokoro-worker.js
Normal file
113
public/scripts/extensions/tts/kokoro-worker.js
Normal file
@@ -0,0 +1,113 @@
|
||||
// kokoro-worker.js
|
||||
/** @type {import('./lib/kokoro.web.js').KokoroTTS} */
|
||||
let tts = null;
|
||||
/** @type {boolean} */
|
||||
let ready = false;
|
||||
/** @type {string[]} */
|
||||
let voices = [];
|
||||
|
||||
// Handle messages from the main thread
|
||||
self.onmessage = async function(e) {
|
||||
const { action, data } = e.data;
|
||||
|
||||
switch (action) {
|
||||
case 'initialize':
|
||||
try {
|
||||
const result = await initializeTts(data);
|
||||
self.postMessage({
|
||||
action: 'initialized',
|
||||
success: result,
|
||||
voices,
|
||||
});
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
action: 'initialized',
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'generateTts':
|
||||
try {
|
||||
const audioBlob = await generateTts(data.text, data.voice, data.speakingRate);
|
||||
const blobUrl = URL.createObjectURL(audioBlob);
|
||||
self.postMessage({
|
||||
action: 'generatedTts',
|
||||
success: true,
|
||||
blobUrl,
|
||||
requestId: data.requestId,
|
||||
});
|
||||
} catch (error) {
|
||||
self.postMessage({
|
||||
action: 'generatedTts',
|
||||
success: false,
|
||||
error: error.message,
|
||||
requestId: data.requestId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'checkReady':
|
||||
self.postMessage({ action: 'readyStatus', ready });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize the TTS engine
|
||||
async function initializeTts(settings) {
|
||||
try {
|
||||
const { KokoroTTS } = await import('./lib/kokoro.web.js');
|
||||
|
||||
console.log('Worker: Initializing Kokoro TTS with settings:', {
|
||||
modelId: settings.modelId,
|
||||
dtype: settings.dtype,
|
||||
device: settings.device,
|
||||
});
|
||||
|
||||
// Create TTS instance
|
||||
tts = await KokoroTTS.from_pretrained(settings.modelId, {
|
||||
dtype: settings.dtype,
|
||||
device: settings.device,
|
||||
});
|
||||
|
||||
// Get available voices
|
||||
voices = Object.keys(tts.voices);
|
||||
|
||||
// Check if generate method exists
|
||||
if (typeof tts.generate !== 'function') {
|
||||
throw new Error('TTS instance does not have generate method');
|
||||
}
|
||||
|
||||
console.log('Worker: TTS initialized successfully');
|
||||
ready = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Worker: Kokoro TTS initialization failed:', error);
|
||||
ready = false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate TTS audio
|
||||
async function generateTts(text, voiceId, speakingRate) {
|
||||
if (!ready || !tts) {
|
||||
throw new Error('TTS engine not initialized');
|
||||
}
|
||||
|
||||
if (text.trim().length === 0) {
|
||||
throw new Error('Empty text');
|
||||
}
|
||||
|
||||
try {
|
||||
const audio = await tts.generate(text, {
|
||||
voice: voiceId,
|
||||
speed: speakingRate || 1.0,
|
||||
});
|
||||
|
||||
return audio.toBlob();
|
||||
} catch (error) {
|
||||
console.error('Worker: TTS generation failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
352
public/scripts/extensions/tts/kokoro.js
Normal file
352
public/scripts/extensions/tts/kokoro.js
Normal file
@@ -0,0 +1,352 @@
|
||||
import { debounce_timeout } from '../../constants.js';
|
||||
import { debounceAsync, splitRecursive } from '../../utils.js';
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export class KokoroTtsProvider {
|
||||
constructor() {
|
||||
this.settings = {
|
||||
modelId: 'onnx-community/Kokoro-82M-v1.0-ONNX',
|
||||
dtype: 'q8',
|
||||
device: 'wasm',
|
||||
voiceMap: {},
|
||||
defaultVoice: 'af_heart',
|
||||
speakingRate: 1.0,
|
||||
};
|
||||
this.ready = false;
|
||||
this.voices = [
|
||||
'af_heart',
|
||||
'af_alloy',
|
||||
'af_aoede',
|
||||
'af_bella',
|
||||
'af_jessica',
|
||||
'af_kore',
|
||||
'af_nicole',
|
||||
'af_nova',
|
||||
'af_river',
|
||||
'af_sarah',
|
||||
'af_sky',
|
||||
'am_adam',
|
||||
'am_echo',
|
||||
'am_eric',
|
||||
'am_fenrir',
|
||||
'am_liam',
|
||||
'am_michael',
|
||||
'am_onyx',
|
||||
'am_puck',
|
||||
'am_santa',
|
||||
'bf_emma',
|
||||
'bf_isabella',
|
||||
'bm_george',
|
||||
'bm_lewis',
|
||||
'bf_alice',
|
||||
'bf_lily',
|
||||
'bm_daniel',
|
||||
'bm_fable',
|
||||
];
|
||||
this.worker = null;
|
||||
this.separator = ' ... ... ... ';
|
||||
this.pendingRequests = new Map();
|
||||
this.nextRequestId = 1;
|
||||
|
||||
// Update display values immediately but only reinitialize TTS after a delay
|
||||
this.initTtsDebounced = debounceAsync(this.initializeWorker.bind(this), debounce_timeout.relaxed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any text processing before passing to TTS engine.
|
||||
* @param {string} text Input text
|
||||
* @returns {string} Processed text
|
||||
*/
|
||||
processText(text) {
|
||||
// TILDE!
|
||||
text = text.replace(/~/g, '.');
|
||||
return text;
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
if (settings.modelId !== undefined) this.settings.modelId = settings.modelId;
|
||||
if (settings.dtype !== undefined) this.settings.dtype = settings.dtype;
|
||||
if (settings.device !== undefined) this.settings.device = settings.device;
|
||||
if (settings.voiceMap !== undefined) this.settings.voiceMap = settings.voiceMap;
|
||||
if (settings.defaultVoice !== undefined) this.settings.defaultVoice = settings.defaultVoice;
|
||||
if (settings.speakingRate !== undefined) this.settings.speakingRate = settings.speakingRate;
|
||||
|
||||
$('#kokoro_model_id').val(this.settings.modelId).on('input', this.onSettingsChange.bind(this));
|
||||
$('#kokoro_dtype').val(this.settings.dtype).on('change', this.onSettingsChange.bind(this));
|
||||
$('#kokoro_device').val(this.settings.device).on('change', this.onSettingsChange.bind(this));
|
||||
$('#kokoro_speaking_rate').val(this.settings.speakingRate).on('input', this.onSettingsChange.bind(this));
|
||||
$('#kokoro_speaking_rate_output').text(this.settings.speakingRate + 'x');
|
||||
}
|
||||
|
||||
initializeWorker() {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
// Terminate the existing worker if it exists
|
||||
if (this.worker) {
|
||||
this.worker.terminate();
|
||||
$('#kokoro_status_text').text('Initializing...').removeAttr('style');
|
||||
}
|
||||
|
||||
// Create a new worker
|
||||
this.worker = new Worker(new URL('./kokoro-worker.js', import.meta.url), { type: 'module' });
|
||||
|
||||
// Set up message handling
|
||||
this.worker.onmessage = this.handleWorkerMessage.bind(this);
|
||||
|
||||
// Initialize the worker with the current settings
|
||||
this.worker.postMessage({
|
||||
action: 'initialize',
|
||||
data: {
|
||||
modelId: this.settings.modelId,
|
||||
dtype: this.settings.dtype,
|
||||
device: this.settings.device,
|
||||
},
|
||||
});
|
||||
|
||||
// Create a promise that will resolve when initialization completes
|
||||
const initPromise = new Promise((initResolve, initReject) => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
initReject(new Error('Worker initialization timed out'));
|
||||
}, 600000); // 600 second timeout
|
||||
|
||||
this.pendingRequests.set('initialization', {
|
||||
resolve: (result) => {
|
||||
clearTimeout(timeoutId);
|
||||
initResolve(result);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeoutId);
|
||||
initReject(error);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Resolve the outer promise when initialization completes
|
||||
initPromise.then(success => {
|
||||
this.ready = success;
|
||||
this.updateStatusDisplay();
|
||||
resolve(success);
|
||||
}).catch(error => {
|
||||
console.error('Worker initialization failed:', error);
|
||||
this.ready = false;
|
||||
this.updateStatusDisplay();
|
||||
reject(error);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create worker:', error);
|
||||
this.ready = false;
|
||||
this.updateStatusDisplay();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleWorkerMessage(event) {
|
||||
const { action, success, ready, error, requestId, blobUrl } = event.data;
|
||||
|
||||
switch (action) {
|
||||
case 'initialized': {
|
||||
const initRequest = this.pendingRequests.get('initialization');
|
||||
if (initRequest) {
|
||||
if (success) {
|
||||
initRequest.resolve(true);
|
||||
} else {
|
||||
initRequest.reject(new Error(error || 'Initialization failed'));
|
||||
}
|
||||
this.pendingRequests.delete('initialization');
|
||||
}
|
||||
} break;
|
||||
case 'generatedTts': {
|
||||
const request = this.pendingRequests.get(requestId);
|
||||
if (request) {
|
||||
if (success) {
|
||||
fetch(blobUrl).then(response => response.blob()).then(audioBlob => {
|
||||
// Clean up the blob URL
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
|
||||
request.resolve(new Response(audioBlob, {
|
||||
headers: {
|
||||
'Content-Type': 'audio/wav',
|
||||
},
|
||||
}));
|
||||
}).catch(error => {
|
||||
request.reject(new Error('Failed to fetch TTS audio blob: ' + error));
|
||||
});
|
||||
} else {
|
||||
request.reject(new Error(error || 'TTS generation failed'));
|
||||
}
|
||||
this.pendingRequests.delete(requestId);
|
||||
}
|
||||
} break;
|
||||
case 'readyStatus':
|
||||
this.ready = ready;
|
||||
this.updateStatusDisplay();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusDisplay() {
|
||||
const statusText = this.ready ? 'Ready' : 'Failed';
|
||||
const statusColor = this.ready ? 'green' : 'red';
|
||||
$('#kokoro_status_text').text(statusText).css('color', statusColor);
|
||||
}
|
||||
|
||||
async checkReady() {
|
||||
if (!this.worker) {
|
||||
return await this.initializeWorker();
|
||||
}
|
||||
|
||||
this.worker.postMessage({ action: 'checkReady' });
|
||||
return this.ready;
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return await this.initializeWorker();
|
||||
}
|
||||
|
||||
get settingsHtml() {
|
||||
return `
|
||||
<div class="kokoro_tts_settings">
|
||||
<label for="kokoro_model_id">Model ID:</label>
|
||||
<input id="kokoro_model_id" type="text" class="text_pole" value="${this.settings.modelId}" />
|
||||
|
||||
<label for="kokoro_dtype">Data Type:</label>
|
||||
<select id="kokoro_dtype" class="text_pole">
|
||||
<option value="q8" ${this.settings.dtype === 'q8' ? 'selected' : ''}>q8 (Recommended)</option>
|
||||
<option value="fp32" ${this.settings.dtype === 'fp32' ? 'selected' : ''}>fp32 (High Precision)</option>
|
||||
<option value="fp16" ${this.settings.dtype === 'fp16' ? 'selected' : ''}>fp16</option>
|
||||
<option value="q4" ${this.settings.dtype === 'q4' ? 'selected' : ''}>q4 (Low Memory)</option>
|
||||
<option value="q4f16" ${this.settings.dtype === 'q4f16' ? 'selected' : ''}>q4f16</option>
|
||||
</select>
|
||||
|
||||
<label for="kokoro_device">Device:</label>
|
||||
<select id="kokoro_device" class="text_pole">
|
||||
<option value="wasm" ${this.settings.device === 'wasm' ? 'selected' : ''}>WebAssembly (CPU)</option>
|
||||
<option value="webgpu" ${this.settings.device === 'webgpu' ? 'selected' : ''}>WebGPU (GPU Acceleration)</option>
|
||||
</select>
|
||||
|
||||
<label for="kokoro_speaking_rate">Speaking Rate: <span id="kokoro_speaking_rate_output">${this.settings.speakingRate}x</span></label>
|
||||
<input id="kokoro_speaking_rate" type="range" value="${this.settings.speakingRate}" min="0.5" max="2.0" step="0.1" />
|
||||
|
||||
<hr>
|
||||
<div>
|
||||
Status: <span id="kokoro_status_text">Initializing...</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async onSettingsChange() {
|
||||
this.settings.modelId = $('#kokoro_model_id').val().toString();
|
||||
this.settings.dtype = $('#kokoro_dtype').val().toString();
|
||||
this.settings.device = $('#kokoro_device').val().toString();
|
||||
this.settings.speakingRate = parseFloat($('#kokoro_speaking_rate').val().toString());
|
||||
|
||||
// Update UI display
|
||||
$('#kokoro_speaking_rate_output').text(this.settings.speakingRate + 'x');
|
||||
|
||||
// Reinitialize TTS engine with debounce
|
||||
this.initTtsDebounced();
|
||||
saveTtsProviderSettings();
|
||||
}
|
||||
|
||||
async fetchTtsVoiceObjects() {
|
||||
if (!this.ready) {
|
||||
await this.checkReady();
|
||||
}
|
||||
return this.voices.map(voice => ({
|
||||
name: voice,
|
||||
voice_id: voice,
|
||||
preview_url: null,
|
||||
lang: voice.startsWith('b') ? 'en-GB' : 'en-US',
|
||||
}));
|
||||
}
|
||||
|
||||
async previewTtsVoice(voiceId) {
|
||||
if (!this.ready) {
|
||||
await this.checkReady();
|
||||
}
|
||||
|
||||
const voice = this.getVoice(voiceId);
|
||||
const previewText = getPreviewString(voice.lang);
|
||||
for await (const response of this.generateTts(previewText, voiceId)) {
|
||||
const audio = await response.blob();
|
||||
const url = URL.createObjectURL(audio);
|
||||
await new Promise(resolve => {
|
||||
const audioElement = new Audio();
|
||||
audioElement.src = url;
|
||||
audioElement.play();
|
||||
audioElement.onended = () => resolve();
|
||||
});
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
getVoiceDisplayName(voiceId) {
|
||||
return voiceId;
|
||||
}
|
||||
|
||||
getVoice(voiceName) {
|
||||
const defaultVoice = this.settings.defaultVoice || 'af_heart';
|
||||
const actualVoiceName = this.voices.includes(voiceName) ? voiceName : defaultVoice;
|
||||
return {
|
||||
name: actualVoiceName,
|
||||
voice_id: actualVoiceName,
|
||||
preview_url: null,
|
||||
lang: actualVoiceName.startsWith('b') ? 'en-GB' : 'en-US',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate TTS audio for the given text using the specified voice.
|
||||
* @param {string} text Text to generate
|
||||
* @param {string} voiceId Voice ID
|
||||
* @returns {AsyncGenerator<Response>} Audio response generator
|
||||
*/
|
||||
async* generateTts(text, voiceId) {
|
||||
if (!this.ready || !this.worker) {
|
||||
console.log('TTS not ready, initializing...');
|
||||
await this.initializeWorker();
|
||||
}
|
||||
|
||||
if (!this.ready || !this.worker) {
|
||||
throw new Error('Failed to initialize TTS engine');
|
||||
}
|
||||
|
||||
if (text.trim().length === 0) {
|
||||
throw new Error('Empty text');
|
||||
}
|
||||
|
||||
const voice = this.getVoice(voiceId);
|
||||
const requestId = this.nextRequestId++;
|
||||
|
||||
const chunkSize = 400;
|
||||
const chunks = splitRecursive(text, chunkSize, ['\n\n', '\n', '.', '?', '!', ',', ' ', '']);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
yield await new Promise((resolve, reject) => {
|
||||
// Store the promise callbacks
|
||||
this.pendingRequests.set(requestId, { resolve, reject });
|
||||
|
||||
// Send the request to the worker
|
||||
this.worker.postMessage({
|
||||
action: 'generateTts',
|
||||
data: {
|
||||
text: chunk,
|
||||
voice: voice.voice_id,
|
||||
speakingRate: this.settings.speakingRate || 1.0,
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// Clean up the worker when the provider is disposed
|
||||
if (this.worker) {
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
}
|
||||
}
|
||||
}
|
8
public/scripts/extensions/tts/lib/README.md
Normal file
8
public/scripts/extensions/tts/lib/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# kokoro-js
|
||||
|
||||
* Author: hexgrad
|
||||
* NPM: <https://www.npmjs.com/package/kokoro-js>
|
||||
* Version: 1.2.0
|
||||
* License: Apache-2.0
|
||||
|
||||
Last updated: 2025-03-10
|
1
public/scripts/extensions/tts/lib/kokoro.web.js
Normal file
1
public/scripts/extensions/tts/lib/kokoro.web.js
Normal file
File diff suppressed because one or more lines are too long
@@ -16,6 +16,7 @@ Exported for use in extension index.js, and added to providers list in index.js
|
||||
1. previewTtsVoice()
|
||||
2. separator field
|
||||
3. processText(text)
|
||||
4. dispose()
|
||||
|
||||
# Requirement Descriptions
|
||||
### generateTts(text, voiceId)
|
||||
@@ -74,3 +75,7 @@ Defines the string of characters used to introduce separation between between th
|
||||
### processText(text)
|
||||
Optional.
|
||||
A function applied to the input text before passing it to the TTS generator. Can be async.
|
||||
|
||||
### dispose()
|
||||
Optional.
|
||||
Function to handle cleanup of provider resources when the provider is switched.
|
||||
|
@@ -114,6 +114,7 @@ export const group_activation_strategy = {
|
||||
NATURAL: 0,
|
||||
LIST: 1,
|
||||
MANUAL: 2,
|
||||
POOLED: 3,
|
||||
};
|
||||
|
||||
export const group_generation_mode = {
|
||||
@@ -245,9 +246,9 @@ export async function getGroupChat(groupId, reload = false) {
|
||||
}
|
||||
|
||||
chat.push(mes);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'first_message');
|
||||
addOneMessage(mes);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'first_message');
|
||||
}
|
||||
}
|
||||
await saveGroupChat(groupId, false);
|
||||
@@ -880,6 +881,9 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
||||
else if (activationStrategy === group_activation_strategy.LIST) {
|
||||
activatedMembers = activateListOrder(enabledMembers);
|
||||
}
|
||||
else if (activationStrategy === group_activation_strategy.POOLED) {
|
||||
activatedMembers = activatePooledOrder(enabledMembers, lastMessage);
|
||||
}
|
||||
else if (activationStrategy === group_activation_strategy.MANUAL && !isUserInput) {
|
||||
activatedMembers = shuffle(enabledMembers).slice(0, 1).map(x => characters.findIndex(y => y.avatar === x)).filter(x => x !== -1);
|
||||
}
|
||||
@@ -1020,6 +1024,48 @@ function activateListOrder(members) {
|
||||
return memberIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate group members based on the last message.
|
||||
* @param {string[]} members List of member avatars
|
||||
* @param {Object} lastMessage Last message
|
||||
* @returns {number[]} List of character ids
|
||||
*/
|
||||
function activatePooledOrder(members, lastMessage) {
|
||||
/** @type {string} */
|
||||
let activatedMember = null;
|
||||
/** @type {string[]} */
|
||||
const spokenSinceUser = [];
|
||||
|
||||
for (const message of chat.slice().reverse()) {
|
||||
if (message.is_user) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (message.is_system || message.extra?.type === system_message_types.NARRATOR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.original_avatar) {
|
||||
spokenSinceUser.push(message.original_avatar);
|
||||
}
|
||||
}
|
||||
|
||||
const haveNotSpoken = members.filter(x => !spokenSinceUser.includes(x));
|
||||
|
||||
if (haveNotSpoken.length) {
|
||||
activatedMember = haveNotSpoken[Math.floor(Math.random() * haveNotSpoken.length)];
|
||||
}
|
||||
|
||||
if (activatedMember === null) {
|
||||
const lastMessageAvatar = members.length > 1 && lastMessage && !lastMessage.is_user && lastMessage.original_avatar;
|
||||
const randomPool = lastMessageAvatar ? members.filter(x => x !== lastMessage.original_avatar) : members;
|
||||
activatedMember = randomPool[Math.floor(Math.random() * randomPool.length)];
|
||||
}
|
||||
|
||||
const memberId = characters.findIndex(y => y.avatar === activatedMember);
|
||||
return memberId !== -1 ? [memberId] : [];
|
||||
}
|
||||
|
||||
function activateNaturalOrder(members, input, lastMessage, allowSelfResponses, isUserInput) {
|
||||
let activatedMembers = [];
|
||||
|
||||
|
@@ -23,6 +23,7 @@ export const names_behavior_types = {
|
||||
const controls = [
|
||||
{ id: 'instruct_enabled', property: 'enabled', isCheckbox: true },
|
||||
{ id: 'instruct_wrap', property: 'wrap', isCheckbox: true },
|
||||
{ id: 'instruct_macro', property: 'macro', isCheckbox: true },
|
||||
{ id: 'instruct_system_sequence_prefix', property: 'system_sequence_prefix', isCheckbox: false },
|
||||
{ id: 'instruct_system_sequence_suffix', property: 'system_sequence_suffix', isCheckbox: false },
|
||||
{ id: 'instruct_input_sequence', property: 'input_sequence', isCheckbox: false },
|
||||
|
@@ -71,7 +71,7 @@ export {
|
||||
export const MAX_CONTEXT_DEFAULT = 8192;
|
||||
export const MAX_RESPONSE_DEFAULT = 2048;
|
||||
const MAX_CONTEXT_UNLOCKED = 200 * 1024;
|
||||
const MAX_RESPONSE_UNLOCKED = 16 * 1024;
|
||||
const MAX_RESPONSE_UNLOCKED = 32 * 1024;
|
||||
const unlockedMaxContextStep = 512;
|
||||
const maxContextMin = 512;
|
||||
const maxContextStep = 64;
|
||||
@@ -492,7 +492,7 @@ function switchSwipeNumAllMessages() {
|
||||
|
||||
var originalSliderValues = [];
|
||||
|
||||
async function switchLabMode() {
|
||||
async function switchLabMode({ noReset = false } = {}) {
|
||||
|
||||
/* if (power_user.enableZenSliders && power_user.enableLabMode) {
|
||||
toastr.warning("Can't start Lab Mode while Zen Sliders are active")
|
||||
@@ -522,12 +522,15 @@ async function switchLabMode() {
|
||||
$('#labModeWarning').removeClass('displayNone');
|
||||
//$("#advanced-ai-config-block input[type='range']").hide()
|
||||
|
||||
$('#amount_gen_counter').attr('min', '1')
|
||||
.attr('max', '99999')
|
||||
.attr('step', '1');
|
||||
$('#amount_gen').attr('min', '1')
|
||||
.attr('max', '99999')
|
||||
.attr('step', '1');
|
||||
|
||||
|
||||
} else {
|
||||
} else if (!noReset) {
|
||||
//re apply the original sliders values to each input
|
||||
originalSliderValues.forEach(function (slider) {
|
||||
$('#' + slider.id)
|
||||
@@ -539,9 +542,8 @@ async function switchLabMode() {
|
||||
$('#advanced-ai-config-block input[type=\'range\']').show();
|
||||
$('#labModeWarning').addClass('displayNone');
|
||||
|
||||
$('#amount_gen').attr('min', '16')
|
||||
.attr('max', '2048')
|
||||
.attr('step', '1');
|
||||
// To set the correct amount_gen back, we just call the function calculating it correctly
|
||||
switchMaxContextSize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1538,7 +1540,7 @@ async function loadPowerUserSettings(settings, data) {
|
||||
$('#prefer_character_prompt').prop('checked', power_user.prefer_character_prompt);
|
||||
$('#prefer_character_jailbreak').prop('checked', power_user.prefer_character_jailbreak);
|
||||
$('#enableZenSliders').prop('checked', power_user.enableZenSliders).trigger('input');
|
||||
$('#enableLabMode').prop('checked', power_user.enableLabMode).trigger('input');
|
||||
$('#enableLabMode').prop('checked', power_user.enableLabMode).trigger('input', { fromInit: true });
|
||||
$(`input[name="avatar_style"][value="${power_user.avatar_style}"]`).prop('checked', true);
|
||||
$(`#chat_display option[value=${power_user.chat_display}]`).attr('selected', true).trigger('change');
|
||||
$('#chat_width_slider').val(power_user.chat_width);
|
||||
@@ -3577,7 +3579,7 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#enableLabMode').on('input', function () {
|
||||
$('#enableLabMode').on('input', function (event, { fromInit = false } = {}) {
|
||||
const value = !!$(this).prop('checked');
|
||||
if (power_user.enableZenSliders === true && value === true) {
|
||||
//disallow Lab Mode if ZenSliders are active
|
||||
@@ -3587,7 +3589,7 @@ $(document).ready(() => {
|
||||
}
|
||||
|
||||
power_user.enableLabMode = value;
|
||||
switchLabMode();
|
||||
switchLabMode({ noReset: fromInit });
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
|
@@ -1075,7 +1075,7 @@ export function removeReasoningFromString(str) {
|
||||
* @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
|
||||
* @returns {ParsedReasoning|null} Parsed reasoning block and message content
|
||||
*/
|
||||
function parseReasoningFromString(str, { strict = true } = {}) {
|
||||
export function parseReasoningFromString(str, { strict = true } = {}) {
|
||||
// Both prefix and suffix must be defined
|
||||
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
|
||||
return null;
|
||||
|
@@ -667,11 +667,21 @@ export function initDefaultSlashCommands() {
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'hide',
|
||||
callback: hideMessageCallback,
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'name',
|
||||
description: 'only hide messages from a certain character or persona',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
enumProvider: commonEnumProviders.messageNames,
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'message index (starts with 0) or range',
|
||||
description: 'message index (starts with 0) or range, defaults to the last message index if not provided',
|
||||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
enumProvider: commonEnumProviders.messages(),
|
||||
}),
|
||||
],
|
||||
@@ -680,11 +690,21 @@ export function initDefaultSlashCommands() {
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'unhide',
|
||||
callback: unhideMessageCallback,
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'name',
|
||||
description: 'only unhide messages from a certain character or persona',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
enumProvider: commonEnumProviders.messageNames,
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'message index (starts with 0) or range',
|
||||
description: 'message index (starts with 0) or range, defaults to the last message index if not provided',
|
||||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE],
|
||||
isRequired: true,
|
||||
isRequired: false,
|
||||
enumProvider: commonEnumProviders.messages(),
|
||||
}),
|
||||
],
|
||||
@@ -3034,37 +3054,29 @@ async function askCharacter(args, text) {
|
||||
return await slashCommandReturnHelper.doReturn(args.return ?? 'pipe', message, { objectToStringFunc: x => x.mes });
|
||||
}
|
||||
|
||||
async function hideMessageCallback(_, arg) {
|
||||
if (!arg) {
|
||||
console.warn('WARN: No argument provided for /hide command');
|
||||
return '';
|
||||
}
|
||||
|
||||
const range = stringToRange(arg, 0, chat.length - 1);
|
||||
async function hideMessageCallback(args, value) {
|
||||
const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
|
||||
|
||||
if (!range) {
|
||||
console.warn(`WARN: Invalid range provided for /hide command: ${arg}`);
|
||||
console.warn(`WARN: Invalid range provided for /hide command: ${value}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
await hideChatMessageRange(range.start, range.end, false);
|
||||
const nameFilter = String(args.name ?? '').trim();
|
||||
await hideChatMessageRange(range.start, range.end, false, nameFilter);
|
||||
return '';
|
||||
}
|
||||
|
||||
async function unhideMessageCallback(_, arg) {
|
||||
if (!arg) {
|
||||
console.warn('WARN: No argument provided for /unhide command');
|
||||
return '';
|
||||
}
|
||||
|
||||
const range = stringToRange(arg, 0, chat.length - 1);
|
||||
async function unhideMessageCallback(args, value) {
|
||||
const range = value ? stringToRange(value, 0, chat.length - 1) : { start: chat.length - 1, end: chat.length - 1 };
|
||||
|
||||
if (!range) {
|
||||
console.warn(`WARN: Invalid range provided for /unhide command: ${arg}`);
|
||||
console.warn(`WARN: Invalid range provided for /unhide command: ${value}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
await hideChatMessageRange(range.start, range.end, true);
|
||||
const nameFilter = String(args.name ?? '').trim();
|
||||
await hideChatMessageRange(range.start, range.end, true, nameFilter);
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -3616,14 +3628,14 @@ export async function sendMessageAs(args, text) {
|
||||
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
|
||||
chat.splice(insertAt, 0, message);
|
||||
await saveChatConditional();
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, insertAt);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, insertAt, 'command');
|
||||
await reloadCurrentChat();
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, insertAt);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, insertAt, 'command');
|
||||
} else {
|
||||
chat.push(message);
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1));
|
||||
await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1), 'command');
|
||||
addOneMessage(message);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1));
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1), 'command');
|
||||
await saveChatConditional();
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import { extension_settings } from '../extensions.js';
|
||||
import { getGroupMembers, groups } from '../group-chats.js';
|
||||
import { power_user } from '../power-user.js';
|
||||
import { searchCharByName, getTagsList, tags, tag_map } from '../tags.js';
|
||||
import { onlyUniqueJson, sortIgnoreCaseAndAccents } from '../utils.js';
|
||||
import { world_names } from '../world-info.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
|
||||
@@ -251,14 +252,29 @@ export const commonEnumProviders = {
|
||||
* @param {boolean} [options.allowVars=false] - Whether to add enum option for variable names
|
||||
* @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]}
|
||||
*/
|
||||
messages: ({ allowIdAfter = false, allowVars = false } = {}) => (_, scope) => {
|
||||
messages: ({ allowIdAfter = false, allowVars = false } = {}) => (executor, scope) => {
|
||||
const nameFilter = executor.namedArgumentList.find(it => it.name == 'name')?.value || '';
|
||||
return [
|
||||
...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)),
|
||||
...chat.map((message, index) => new SlashCommandEnumValue(String(index), `${message.name}: ${message.mes}`, enumTypes.number, message.is_user ? enumIcons.user : message.is_system ? enumIcons.system : enumIcons.assistant)).filter(value => !nameFilter || value.description.startsWith(`${nameFilter}:`)),
|
||||
...allowIdAfter ? [new SlashCommandEnumValue(String(chat.length), '>> After Last Message >>', enumTypes.enum, '➕')] : [],
|
||||
...allowVars ? commonEnumProviders.variables('all')(_, scope) : [],
|
||||
...allowVars ? commonEnumProviders.variables('all')(executor, scope) : [],
|
||||
];
|
||||
},
|
||||
|
||||
/**
|
||||
* All names used in the current chat.
|
||||
*
|
||||
* @returns {SlashCommandEnumValue[]}
|
||||
*/
|
||||
messageNames: () => chat
|
||||
.map(message => ({
|
||||
name: message.name,
|
||||
icon: message.is_user ? enumIcons.user : enumIcons.assistant,
|
||||
}))
|
||||
.filter(onlyUniqueJson)
|
||||
.sort((a, b) => sortIgnoreCaseAndAccents(a.name, b.name))
|
||||
.map(name => new SlashCommandEnumValue(name.name, null, null, name.icon)),
|
||||
|
||||
/**
|
||||
* All existing worlds / lorebooks
|
||||
*
|
||||
|
@@ -48,6 +48,7 @@ import {
|
||||
printMessages,
|
||||
clearChat,
|
||||
unshallowCharacter,
|
||||
deleteLastMessage,
|
||||
} from '../script.js';
|
||||
import {
|
||||
extension_settings,
|
||||
@@ -79,7 +80,7 @@ import { timestampToMoment, uuidv4 } from './utils.js';
|
||||
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
|
||||
import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
|
||||
import { ChatCompletionService, TextCompletionService } from './custom-request.js';
|
||||
import { updateReasoningUI } from './reasoning.js';
|
||||
import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
|
||||
|
||||
export function getContext() {
|
||||
return {
|
||||
@@ -106,6 +107,7 @@ export function getContext() {
|
||||
eventSource,
|
||||
eventTypes: event_types,
|
||||
addOneMessage,
|
||||
deleteLastMessage,
|
||||
generate: Generate,
|
||||
sendStreamingRequest,
|
||||
sendGenerationRequest,
|
||||
@@ -147,6 +149,7 @@ export function getContext() {
|
||||
unregisterFunctionTool: ToolManager.unregisterFunctionTool.bind(ToolManager),
|
||||
isToolCallingSupported: ToolManager.isToolCallingSupported.bind(ToolManager),
|
||||
canPerformToolCalls: ToolManager.canPerformToolCalls.bind(ToolManager),
|
||||
ToolManager,
|
||||
registerDebugFunction,
|
||||
/** @deprecated Use renderExtensionTemplateAsync instead. */
|
||||
renderExtensionTemplate,
|
||||
@@ -213,6 +216,7 @@ export function getContext() {
|
||||
ChatCompletionService,
|
||||
TextCompletionService,
|
||||
updateReasoningUI,
|
||||
parseReasoningFromString,
|
||||
unshallowCharacter,
|
||||
unshallowGroupMembers,
|
||||
};
|
||||
|
@@ -1251,7 +1251,7 @@ function onCharacterCreateClick() {
|
||||
}
|
||||
|
||||
function onGroupCreateClick() {
|
||||
// Nothing to do here at the moment. Tags in group interface get automatically redrawn.
|
||||
$('#groupTagList').empty();
|
||||
}
|
||||
|
||||
export function applyTagsOnCharacterSelect(chid = null) {
|
||||
@@ -1259,11 +1259,11 @@ export function applyTagsOnCharacterSelect(chid = null) {
|
||||
if (menu_type === 'create') {
|
||||
const currentTagIds = $('#tagList').find('.tag').map((_, el) => $(el).attr('id')).get();
|
||||
const currentTags = tags.filter(x => currentTagIds.includes(x.id));
|
||||
printTagList($('#tagList'), { forEntityOrKey: null, tags: currentTags, tagOptions: { removable: true } });
|
||||
printTagList($('#tagList'), { forEntityOrKey: undefined, tags: currentTags, tagOptions: { removable: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
chid = chid ?? Number(this_chid);
|
||||
chid = chid ?? (this_chid !== undefined ? Number(this_chid) : undefined);
|
||||
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
@@ -1272,11 +1272,11 @@ export function applyTagsOnGroupSelect(groupId = null) {
|
||||
if (menu_type === 'group_create') {
|
||||
const currentTagIds = $('#groupTagList').find('.tag').map((_, el) => $(el).attr('id')).get();
|
||||
const currentTags = tags.filter(x => currentTagIds.includes(x.id));
|
||||
printTagList($('#groupTagList'), { forEntityOrKey: null, tags: currentTags, tagOptions: { removable: true } });
|
||||
printTagList($('#groupTagList'), { forEntityOrKey: undefined, tags: currentTags, tagOptions: { removable: true } });
|
||||
return;
|
||||
}
|
||||
|
||||
groupId = groupId ?? Number(selected_group);
|
||||
groupId = groupId ?? (selected_group ? Number(selected_group) : undefined);
|
||||
printTagList($('#groupTagList'), { forEntityOrKey: groupId, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
} from '../lib.js';
|
||||
|
||||
import { getContext } from './extensions.js';
|
||||
import { characters, getRequestHeaders, this_chid } from '../script.js';
|
||||
import { characters, getRequestHeaders, this_chid, user_avatar } from '../script.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
import { collapseNewlines, power_user } from './power-user.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
@@ -14,7 +14,7 @@ import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
|
||||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||
import { getTagsList } from './tags.js';
|
||||
import { groups, selected_group } from './group-chats.js';
|
||||
import { getCurrentLocale } from './i18n.js';
|
||||
import { getCurrentLocale, t } from './i18n.js';
|
||||
|
||||
/**
|
||||
* Pagination status string template.
|
||||
@@ -196,6 +196,17 @@ export function onlyUnique(value, index, array) {
|
||||
return array.indexOf(value) === index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a value is unique in an array of objects.
|
||||
* @param {any} value Current value.
|
||||
* @param {number} index Current index.
|
||||
* @param {any[]} array The array being processed.
|
||||
* @returns {boolean} True if the value is unique, false otherwise.
|
||||
*/
|
||||
export function onlyUniqueJson(value, index, array) {
|
||||
return array.map(v => JSON.stringify(v)).indexOf(JSON.stringify(value)) === index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first occurrence of a specified item from an array
|
||||
*
|
||||
@@ -436,6 +447,33 @@ export function debounce(func, timeout = debounce_timeout.standard) {
|
||||
return fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 debounceAsync(func, timeout = debounce_timeout.standard) {
|
||||
let timer;
|
||||
/**@type {Promise}*/
|
||||
let debouncePromise;
|
||||
/**@type {Function}*/
|
||||
let debounceResolver;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
if (!debouncePromise) {
|
||||
debouncePromise = new Promise(resolve => {
|
||||
debounceResolver = resolve;
|
||||
});
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
debounceResolver(func.apply(this, args));
|
||||
debouncePromise = null;
|
||||
}, timeout);
|
||||
return debouncePromise;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels a scheduled debounced function.
|
||||
* Does nothing if the function is not debounced or not scheduled.
|
||||
@@ -1772,8 +1810,9 @@ export function runAfterAnimation(control, callback, timeout = 500) {
|
||||
*
|
||||
* @param {string} a - The first string to compare.
|
||||
* @param {string} b - The second string to compare.
|
||||
* @param {(a:string,b:string)=>boolean} comparisonFunction - The function to use for the comparison.
|
||||
* @returns {*} - The result of the comparison.
|
||||
* @param {(a:string,b:string)=>T} comparisonFunction - The function to use for the comparison.
|
||||
* @returns {T} - The result of the comparison.
|
||||
* @template T
|
||||
*/
|
||||
export function compareIgnoreCaseAndAccents(a, b, comparisonFunction) {
|
||||
if (!a || !b) return comparisonFunction(a, b); // Return the comparison result if either string is empty
|
||||
@@ -1810,6 +1849,16 @@ export function equalsIgnoreCaseAndAccents(a, b) {
|
||||
return compareIgnoreCaseAndAccents(a, b, (a, b) => a === b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a case-insensitive and accent-insensitive sort.
|
||||
* @param {string} a - The first string to compare
|
||||
* @param {string} b - The second string to compare
|
||||
* @returns {number} -1 if a < b, 1 if a > b, 0 if a === b
|
||||
*/
|
||||
export function sortIgnoreCaseAndAccents(a, b) {
|
||||
return compareIgnoreCaseAndAccents(a, b, (a, b) => a?.localeCompare(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Select2Option The option object for select2 controls
|
||||
* @property {string} id - The unique ID inside this select
|
||||
@@ -2169,6 +2218,48 @@ export async function showFontAwesomePicker(customList = null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a persona by name, with optional filtering and precedence for avatars
|
||||
* @param {object} [options={}] - The options for the search
|
||||
* @param {string?} [options.name=null] - The name to search for
|
||||
* @param {boolean} [options.allowAvatar=true] - Whether to allow searching by avatar
|
||||
* @param {boolean} [options.insensitive=true] - Whether the search should be case insensitive
|
||||
* @param {boolean} [options.preferCurrentPersona=true] - Whether to prefer the current persona(s)
|
||||
* @param {boolean} [options.quiet=false] - Whether to suppress warnings
|
||||
* @returns {PersonaViewModel} The persona object
|
||||
* @typedef {object} PersonaViewModel
|
||||
* @property {string} avatar - The avatar of the persona
|
||||
* @property {string} name - The name of the persona
|
||||
*/
|
||||
export function findPersona({ name = null, allowAvatar = true, insensitive = true, preferCurrentPersona = true, quiet = false } = {}) {
|
||||
/** @type {PersonaViewModel[]} */
|
||||
const personas = Object.entries(power_user.personas).map(([avatar, name]) => ({ avatar, name }));
|
||||
const matches = (/** @type {PersonaViewModel} */ persona) => !name || (allowAvatar && persona.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(persona.name, name) : persona.name === name);
|
||||
|
||||
// If we have a current persona and prefer it, return that if it matches
|
||||
const currentPersona = personas.find(a => a.avatar === user_avatar);
|
||||
if (preferCurrentPersona && currentPersona && matches(currentPersona)) {
|
||||
return currentPersona;
|
||||
}
|
||||
|
||||
// If allowAvatar is true, search by avatar first
|
||||
if (allowAvatar && name) {
|
||||
const personaByAvatar = personas.find(a => a.avatar === name);
|
||||
if (personaByAvatar && matches(personaByAvatar)) {
|
||||
return personaByAvatar;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for matching personas by name
|
||||
const matchingPersonas = personas.filter(a => matches(a));
|
||||
if (matchingPersonas.length > 1) {
|
||||
if (!quiet) toastr.warning(t`Multiple personas found for given conditions.`);
|
||||
else console.warn(t`Multiple personas found for given conditions. Returning the first match.`);
|
||||
}
|
||||
|
||||
return matchingPersonas[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a character by name, with optional filtering and precedence for avatars
|
||||
* @param {object} [options={}] - The options for the search
|
||||
@@ -2201,8 +2292,8 @@ export function findChar({ name = null, allowAvatar = true, insensitive = true,
|
||||
if (preferCurrentChar) {
|
||||
const preferredCharSearch = currentChars.filter(matches);
|
||||
if (preferredCharSearch.length > 1) {
|
||||
if (!quiet) toastr.warning('Multiple characters found for given conditions.');
|
||||
else console.warn('Multiple characters found for given conditions. Returning the first match.');
|
||||
if (!quiet) toastr.warning(t`Multiple characters found for given conditions.`);
|
||||
else console.warn(t`Multiple characters found for given conditions. Returning the first match.`);
|
||||
}
|
||||
if (preferredCharSearch.length) {
|
||||
return preferredCharSearch[0];
|
||||
|
@@ -3542,6 +3542,10 @@ export async function deleteWorldInfo(worldInfoName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (worldInfoCache.has(worldInfoName)) {
|
||||
worldInfoCache.delete(worldInfoName);
|
||||
}
|
||||
|
||||
const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
|
||||
if (existingWorldIndex !== -1) {
|
||||
selected_world_info.splice(existingWorldIndex, 1);
|
||||
|
165
tests/package-lock.json
generated
165
tests/package-lock.json
generated
@@ -28,12 +28,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
|
||||
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.24.7",
|
||||
"@babel/helper-validator-identifier": "^7.25.9",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -214,18 +215,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
|
||||
"integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
|
||||
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -241,109 +242,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz",
|
||||
"integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz",
|
||||
"integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.24.7",
|
||||
"@babel/types": "^7.24.7"
|
||||
"@babel/template": "^7.26.9",
|
||||
"@babel/types": "^7.26.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
|
||||
"integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.24.7",
|
||||
"chalk": "^2.4.2",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "1.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight/node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
|
||||
"integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz",
|
||||
"integrity": "sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.10"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -529,14 +447,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
|
||||
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
|
||||
"version": "7.26.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz",
|
||||
"integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@babel/types": "^7.24.7"
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
"@babel/parser": "^7.26.9",
|
||||
"@babel/types": "^7.26.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -564,14 +482,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz",
|
||||
"integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==",
|
||||
"version": "7.26.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz",
|
||||
"integrity": "sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.7",
|
||||
"@babel/helper-validator-identifier": "^7.24.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1653,9 +1570,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
|
||||
"integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -5604,15 +5522,6 @@
|
||||
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
"integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
Reference in New Issue
Block a user