mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into ru-l10n
This commit is contained in:
@@ -702,7 +702,7 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
*/
|
||||
function autoFitSendTextArea() {
|
||||
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
||||
if (sendTextArea.scrollHeight + 3 == sendTextArea.offsetHeight) {
|
||||
if (Math.ceil(sendTextArea.scrollHeight + 3) >= Math.floor(sendTextArea.offsetHeight)) {
|
||||
// Needs to be pulled dynamically because it is affected by font size changes
|
||||
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
|
||||
sendTextArea.style.height = sendTextAreaMinHeight;
|
||||
@@ -1133,6 +1133,11 @@ export function initRossMods() {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('#dialogue_del_mes_cancel').is(':visible')) {
|
||||
$('#dialogue_del_mes_cancel').trigger('click');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($('.drawer-content')
|
||||
.not('#WorldInfo')
|
||||
.not('#left-nav-panel')
|
||||
|
@@ -25,6 +25,8 @@ export class AutoComplete {
|
||||
/**@type {boolean}*/ isReplaceable = false;
|
||||
/**@type {boolean}*/ isShowingDetails = false;
|
||||
/**@type {boolean}*/ wasForced = false;
|
||||
/**@type {boolean}*/ isForceHidden = false;
|
||||
/**@type {boolean}*/ canBeAutoHidden = false;
|
||||
|
||||
/**@type {string}*/ text;
|
||||
/**@type {AutoCompleteNameResult}*/ parserResult;
|
||||
@@ -57,6 +59,10 @@ export class AutoComplete {
|
||||
return power_user.stscript.matching ?? 'fuzzy';
|
||||
}
|
||||
|
||||
get autoHide() {
|
||||
return power_user.stscript.autocomplete.autoHide ?? false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -224,6 +230,16 @@ export class AutoComplete {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
basicAutoHideCheck() {
|
||||
// auto hide only if at least one char has been typed after the name + space
|
||||
return this.textarea.selectionStart > this.parserResult.start
|
||||
+ this.parserResult.name.length
|
||||
+ (this.startQuote ? 1 : 0)
|
||||
+ (this.endQuote ? 1 : 0)
|
||||
+ 1
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the autocomplete.
|
||||
* @param {boolean} isInput Whether triggered by input.
|
||||
@@ -244,6 +260,9 @@ export class AutoComplete {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
// disable force-hide if trigger was forced
|
||||
if (isForced) this.isForceHidden = false;
|
||||
|
||||
// request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for
|
||||
// cursor position
|
||||
this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart);
|
||||
@@ -275,12 +294,16 @@ export class AutoComplete {
|
||||
this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0));
|
||||
this.parserResult.name = this.name;
|
||||
this.isReplaceable = true;
|
||||
this.isForceHidden = false;
|
||||
this.canBeAutoHidden = false;
|
||||
} else {
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
} else {
|
||||
// if not forced and no user input -> just show details
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
|
||||
if (isForced || isInput || isSelect) {
|
||||
@@ -292,8 +315,11 @@ export class AutoComplete {
|
||||
this.secondaryParserResult = result;
|
||||
this.name = this.secondaryParserResult.name;
|
||||
this.isReplaceable = isForced || this.secondaryParserResult.isRequired;
|
||||
this.isForceHidden = false;
|
||||
this.canBeAutoHidden = false;
|
||||
} else {
|
||||
this.isReplaceable = false;
|
||||
this.canBeAutoHidden = this.basicAutoHideCheck();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,7 +340,17 @@ export class AutoComplete {
|
||||
// filter the list of options by the partial name according to the matching type
|
||||
.filter(it => this.isReplaceable || it.name == '' ? matchers[this.matchType](it.name) : it.name.toLowerCase() == this.name)
|
||||
// remove aliases
|
||||
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx)
|
||||
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx);
|
||||
|
||||
if (this.result.length == 0 && this.effectiveParserResult != this.parserResult && isForced) {
|
||||
// no matching secondary results and forced trigger -> show current command details
|
||||
this.secondaryParserResult = null;
|
||||
this.result = [this.effectiveParserResult.optionList.find(it=>it.name == this.effectiveParserResult.name)];
|
||||
this.name = this.effectiveParserResult.name;
|
||||
this.fuzzyRegex = /(.*)(.*)(.*)/;
|
||||
}
|
||||
|
||||
this.result = this.result
|
||||
// update remaining options
|
||||
.map(option => {
|
||||
// build element
|
||||
@@ -336,6 +372,15 @@ export class AutoComplete {
|
||||
;
|
||||
|
||||
|
||||
|
||||
if (this.isForceHidden) {
|
||||
// hidden with escape
|
||||
return this.hide();
|
||||
}
|
||||
if (this.autoHide && this.canBeAutoHidden && !isForced && this.effectiveParserResult == this.parserResult && this.result.length == 1) {
|
||||
// auto hide user setting enabled and somewhere after name part and would usually show command details
|
||||
return this.hide();
|
||||
}
|
||||
if (this.result.length == 0) {
|
||||
if (!isInput) {
|
||||
// no result and no input? hide autocomplete
|
||||
@@ -683,6 +728,8 @@ export class AutoComplete {
|
||||
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.isForceHidden = true;
|
||||
this.wasForced = false;
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@
|
||||
* @property {boolean} group_override - Overrides any existing group assignment for the extension.
|
||||
* @property {number} group_weight - A value used for prioritizing extensions within the same group.
|
||||
* @property {boolean} prevent_recursion - Completely disallows recursive application of the extension.
|
||||
* @property {boolean} delay_until_recursion - Will only be checked during recursion.
|
||||
* @property {number} scan_depth - The maximum depth to search for matches when applying the extension.
|
||||
* @property {boolean} match_whole_words - Specifies if only entire words should be matched during extension application.
|
||||
* @property {boolean} use_group_scoring - Indicates if group weight is considered when selecting extensions.
|
||||
@@ -69,6 +70,8 @@
|
||||
* @property {"system" | "user" | "assistant"} depth_prompt.role - The role the character takes on during the prompted interaction (system, user, or assistant).
|
||||
* // Non-standard extensions added by external tools
|
||||
* @property {string} [pygmalion_id] - The unique identifier assigned to the character by the Pygmalion.chat.
|
||||
* @property {string} [github_repo] - The gitHub repository associated with the character.
|
||||
* @property {string} [source_url] - The source URL associated with the character.
|
||||
* @property {{full_path: string}} [chub] - The Chub-specific data associated with the character.
|
||||
*/
|
||||
|
||||
|
@@ -109,7 +109,7 @@ function downloadAssetsList(url) {
|
||||
</div>`);
|
||||
}
|
||||
|
||||
for (const i in availableAssets[assetType]) {
|
||||
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
|
||||
const asset = availableAssets[assetType][i];
|
||||
const elemId = `assets_install_${assetType}_${i}`;
|
||||
let element = $('<div />', { id: elemId, class: 'asset-download-button right_menu_button' });
|
||||
@@ -200,6 +200,9 @@ function downloadAssetsList(url) {
|
||||
</div>`);
|
||||
|
||||
if (assetType === 'character') {
|
||||
if (asset.highlight) {
|
||||
assetBlock.find('.asset-name').append('<i class="fa-solid fa-sm fa-trophy"></i>');
|
||||
}
|
||||
assetBlock.find('.asset-name').prepend(`<div class="avatar"><img src="${asset['url']}" alt="${displayName}"></div>`);
|
||||
}
|
||||
|
||||
|
@@ -4,7 +4,7 @@
|
||||
Enter a URL or the ID of a Fandom wiki page to scrape:
|
||||
</label>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://harrypotter.fandom.com/</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>harrypotter</code>
|
||||
|
@@ -7,7 +7,7 @@
|
||||
Don't include the page name!
|
||||
</i>
|
||||
<small>
|
||||
<span data-i18n=Examples:">Examples:</span>
|
||||
<span data-i18n="Examples:">Examples:</span>
|
||||
<code>https://streetcat.wiki/index.php</code>
|
||||
<span data-i18n="or">or</span>
|
||||
<code>https://tcrf.net</code>
|
||||
|
@@ -435,12 +435,17 @@ jQuery(function () {
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
|
||||
<option data-type="openai" value="gpt-4o">gpt-4o</option>
|
||||
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-haiku">anthropic/claude-3-haiku</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet">anthropic/claude-3-sonnet</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus">anthropic/claude-3-opus</option>
|
||||
@@ -449,6 +454,8 @@ jQuery(function () {
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
|
||||
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
|
||||
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
|
||||
<option data-type="ollama" value="ollama_current">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
|
||||
<option data-type="ollama" value="llava:latest">llava:latest</option>
|
||||
|
@@ -1020,12 +1020,12 @@ function parseLlmResponse(emotionResponse, labels) {
|
||||
const parsedEmotion = JSON.parse(emotionResponse);
|
||||
return parsedEmotion?.emotion ?? fallbackExpression;
|
||||
} catch {
|
||||
const fuse = new Fuse([emotionResponse]);
|
||||
for (const label of labels) {
|
||||
const result = fuse.search(label);
|
||||
if (result.length > 0) {
|
||||
return label;
|
||||
}
|
||||
const fuse = new Fuse(labels, { includeScore: true });
|
||||
console.debug('Using fuzzy search in labels:', labels);
|
||||
const result = fuse.search(emotionResponse);
|
||||
if (result.length > 0) {
|
||||
console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse);
|
||||
return result[0].item;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -804,10 +804,7 @@ function setMemoryContext(value, saveToMessage, index = null) {
|
||||
const context = getContext();
|
||||
context.setExtensionPrompt(MODULE_NAME, formatMemoryValue(value), extension_settings.memory.position, extension_settings.memory.depth, false, extension_settings.memory.role);
|
||||
$('#memory_contents').val(value);
|
||||
console.log('Summary set to: ' + value);
|
||||
console.debug('Position: ' + extension_settings.memory.position);
|
||||
console.debug('Depth: ' + extension_settings.memory.depth);
|
||||
console.debug('Role: ' + extension_settings.memory.role);
|
||||
console.log('Summary set to: ' + value, 'Position: ' + extension_settings.memory.position, 'Depth: ' + extension_settings.memory.depth, 'Role: ' + extension_settings.memory.role);
|
||||
|
||||
if (saveToMessage && context.chat.length) {
|
||||
const idx = index ?? context.chat.length - 2;
|
||||
|
@@ -342,6 +342,16 @@ export class QuickReply {
|
||||
message.addEventListener('scroll', (evt)=>{
|
||||
updateScrollDebounced();
|
||||
});
|
||||
/** @type {any} */
|
||||
const resizeListener = debounce((evt) => {
|
||||
updateSyntax();
|
||||
updateScrollDebounced(evt);
|
||||
if (document.activeElement == message) {
|
||||
message.blur();
|
||||
message.focus();
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', resizeListener);
|
||||
message.style.color = 'transparent';
|
||||
message.style.background = 'transparent';
|
||||
message.style.setProperty('text-shadow', 'none', 'important');
|
||||
@@ -514,6 +524,8 @@ export class QuickReply {
|
||||
});
|
||||
|
||||
await popupResult;
|
||||
|
||||
window.removeEventListener('resize', resizeListener);
|
||||
} else {
|
||||
warn('failed to fetch qrEditor template');
|
||||
}
|
||||
|
@@ -209,6 +209,10 @@
|
||||
justify-content: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
#qr--qrOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -218,12 +222,17 @@
|
||||
@media screen and (max-width: 750px) {
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
|
||||
flex-direction: column;
|
||||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) {
|
||||
|
@@ -229,6 +229,8 @@
|
||||
|
||||
|
||||
#qr--qrOptions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> #qr--ctxEditor {
|
||||
.qr--ctxItem {
|
||||
display: flex;
|
||||
@@ -244,11 +246,16 @@
|
||||
@media screen and (max-width: 750px) {
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
> #qr--main {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
> #qr--main > .qr--labels {
|
||||
flex-direction: column;
|
||||
}
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
||||
min-height: 90svh;
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -14,7 +14,7 @@
|
||||
<i class="fa-solid fa-file-import"></i>
|
||||
<span data-i18n="ext_regex_import_script">Import Script</span>
|
||||
</div>
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" />
|
||||
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
|
||||
</div>
|
||||
<hr />
|
||||
<label data-i18n="ext_regex_saved_scripts">Saved Scripts</label>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { substituteParams } from '../../../script.js';
|
||||
import { extension_settings } from '../../extensions.js';
|
||||
import { regexFromString } from '../../utils.js';
|
||||
export {
|
||||
regex_placement,
|
||||
getRegexedString,
|
||||
@@ -21,29 +22,6 @@ const regex_placement = {
|
||||
WORLD_INFO: 5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Instantiates a regular expression from a string.
|
||||
* @param {string} input The input string.
|
||||
* @returns {RegExp} The regular expression instance.
|
||||
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
|
||||
*/
|
||||
function regexFromString(input) {
|
||||
try {
|
||||
// Parse input
|
||||
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
|
||||
|
||||
// Invalid flags
|
||||
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
|
||||
return RegExp(input);
|
||||
}
|
||||
|
||||
// Create the regular expression
|
||||
return new RegExp(m[2], m[3]);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parent function to fetch a regexed version of a raw string
|
||||
* @param {string} rawString The raw string to be regexed
|
||||
|
@@ -325,7 +325,9 @@ jQuery(async () => {
|
||||
});
|
||||
$('#import_regex_file').on('change', async function () {
|
||||
const inputElement = this instanceof HTMLInputElement && this;
|
||||
await onRegexImportFileChange(inputElement.files[0]);
|
||||
for (const file of inputElement.files) {
|
||||
await onRegexImportFileChange(file);
|
||||
}
|
||||
inputElement.value = '';
|
||||
});
|
||||
$('#import_regex').on('click', function () {
|
||||
|
@@ -1948,7 +1948,7 @@ async function generatePicture(args, trigger, message, callback) {
|
||||
}
|
||||
|
||||
if (!isValidState()) {
|
||||
toastr.warning('Extensions API is not connected or doesn\'t provide SD module. Enable Stable Horde to generate images.');
|
||||
toastr.warning('Image generation is not available. Check your settings and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -642,9 +642,9 @@ jQuery(() => {
|
||||
|
||||
loadSettings();
|
||||
|
||||
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
|
||||
eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage);
|
||||
eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage);
|
||||
eventSource.on(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage);
|
||||
eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady);
|
||||
eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit);
|
||||
|
||||
|
@@ -261,6 +261,7 @@ async function playAudioData(audioJob) {
|
||||
audioElement.addEventListener('ended', completeCurrentAudioJob);
|
||||
audioElement.addEventListener('canplay', () => {
|
||||
console.debug('Starting TTS playback');
|
||||
audioElement.playbackRate = extension_settings.tts.playback_rate;
|
||||
audioElement.play();
|
||||
});
|
||||
}
|
||||
@@ -529,6 +530,10 @@ function loadSettings() {
|
||||
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
|
||||
$('#tts_skip_codeblocks').prop('checked', extension_settings.tts.skip_codeblocks);
|
||||
$('#tts_skip_tags').prop('checked', extension_settings.tts.skip_tags);
|
||||
$('#playback_rate').val(extension_settings.tts.playback_rate);
|
||||
$('#playback_rate_counter').val(Number(extension_settings.tts.playback_rate).toFixed(2));
|
||||
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
|
||||
|
||||
$('body').toggleClass('tts', extension_settings.tts.enabled);
|
||||
}
|
||||
|
||||
@@ -538,6 +543,7 @@ const defaultSettings = {
|
||||
currentProvider: 'ElevenLabs',
|
||||
auto_generation: true,
|
||||
narrate_user: false,
|
||||
playback_rate: 1,
|
||||
};
|
||||
|
||||
function setTtsStatus(status, success) {
|
||||
@@ -649,6 +655,7 @@ async function loadTtsProvider(provider) {
|
||||
function onTtsProviderChange() {
|
||||
const ttsProviderSelection = $('#tts_provider').val();
|
||||
extension_settings.tts.currentProvider = ttsProviderSelection;
|
||||
$('#playback_rate_block').toggle(extension_settings.tts.currentProvider !== 'System');
|
||||
loadTtsProvider(ttsProviderSelection);
|
||||
}
|
||||
|
||||
@@ -1022,6 +1029,20 @@ $(document).ready(function () {
|
||||
<small>Pass Asterisks to TTS Engine</small>
|
||||
</label>
|
||||
</div>
|
||||
<div id="playback_rate_block" class="range-block">
|
||||
<hr>
|
||||
<div class="range-block-title justifyLeft" data-i18n="Audio Playback Speed">
|
||||
<small>Audio Playback Speed</small>
|
||||
</div>
|
||||
<div class="range-block-range-and-counter">
|
||||
<div class="range-block-range">
|
||||
<input type="range" id="playback_rate" name="volume" min="0" max="3" step="0.05">
|
||||
</div>
|
||||
<div class="range-block-counter">
|
||||
<input type="number" min="0" max="3" step="0.05" data-for="playback_rate" id="playback_rate_counter">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tts_voicemap_block">
|
||||
</div>
|
||||
<hr>
|
||||
@@ -1046,6 +1067,15 @@ $(document).ready(function () {
|
||||
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
|
||||
$('#tts_auto_generation').on('click', onAutoGenerationClick);
|
||||
$('#tts_narrate_user').on('click', onNarrateUserClick);
|
||||
|
||||
$('#playback_rate').on('input', function () {
|
||||
const value = $(this).val();
|
||||
const formattedValue = Number(value).toFixed(2);
|
||||
extension_settings.tts.playback_rate = value;
|
||||
$('#playback_rate_counter').val(formattedValue);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#tts_voices').on('click', onTtsVoicesClick);
|
||||
for (const provider in ttsProviders) {
|
||||
$('#tts_provider').append($('<option />').val(provider).text(provider));
|
||||
@@ -1063,8 +1093,8 @@ $(document).ready(function () {
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
eventSource.on(event_types.MESSAGE_DELETED, onMessageDeleted);
|
||||
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
|
||||
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
|
||||
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent);
|
||||
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
|
||||
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak',
|
||||
callback: onNarrateText,
|
||||
aliases: ['narrate', 'tts'],
|
||||
|
@@ -258,9 +258,8 @@ export class FilterHelper {
|
||||
*/
|
||||
folderFilter(data) {
|
||||
const state = this.filterData[FILTER_TYPES.FOLDER];
|
||||
// Slightly different than the other filters, as a positive folder filter means it doesn't filter anything (folders get "not hidden" at another place),
|
||||
// while a negative state should then filter out all folders.
|
||||
const isFolder = entity => isFilterState(state, FILTER_STATES.SELECTED) ? true : entity.type === 'tag';
|
||||
// Filter directly on folder. Special rules on still displaying characters with active folder filter are implemented in 'getEntitiesList' directly.
|
||||
const isFolder = entity => entity.type === 'tag';
|
||||
|
||||
return this.filterDataByState(data, state, isFolder);
|
||||
}
|
||||
@@ -342,15 +341,40 @@ export class FilterHelper {
|
||||
* Applies all filters to the given data.
|
||||
* @param {any[]} data - The data to filter.
|
||||
* @param {object} options - Optional call parameters
|
||||
* @param {boolean|FilterType} [options.clearScoreCache=true] - Whether the score
|
||||
* @param {boolean} [options.clearScoreCache=true] - Whether the score cache should be cleared.
|
||||
* @param {Object.<FilterType, any>} [options.tempOverrides={}] - Temporarily override specific filters for this filter application
|
||||
* @returns {any[]} The filtered data.
|
||||
*/
|
||||
applyFilters(data, { clearScoreCache = true } = {}) {
|
||||
applyFilters(data, { clearScoreCache = true, tempOverrides = {} } = {}) {
|
||||
if (clearScoreCache) this.clearScoreCache();
|
||||
return Object.values(this.filterFunctions)
|
||||
.reduce((data, fn) => fn(data), data);
|
||||
|
||||
// Save original filter states
|
||||
const originalStates = {};
|
||||
for (const key in tempOverrides) {
|
||||
originalStates[key] = this.filterData[key];
|
||||
this.filterData[key] = tempOverrides[key];
|
||||
}
|
||||
|
||||
try {
|
||||
const result = Object.values(this.filterFunctions)
|
||||
.reduce((data, fn) => fn(data), data);
|
||||
|
||||
// Restore original filter states
|
||||
for (const key in originalStates) {
|
||||
this.filterData[key] = originalStates[key];
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Restore original filter states in case of an error
|
||||
for (const key in originalStates) {
|
||||
this.filterData[key] = originalStates[key];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cache scores for a specific filter type
|
||||
* @param {FilterType} type - The type of data being cached
|
||||
|
@@ -637,7 +637,7 @@ function isValidImageUrl(url) {
|
||||
if (Object.keys(url).length === 0) {
|
||||
return false;
|
||||
}
|
||||
return isDataURL(url) || (url && url.startsWith('user'));
|
||||
return isDataURL(url) || (url && (url.startsWith('user') || url.startsWith('/user')));
|
||||
}
|
||||
|
||||
function getGroupAvatar(group) {
|
||||
@@ -1418,6 +1418,10 @@ function select_group_chats(groupId, skipAnimation) {
|
||||
* @returns {Promise<void>} - A promise that resolves when the processing and upload is complete.
|
||||
*/
|
||||
async function uploadGroupAvatar(event) {
|
||||
if (!(event.target instanceof HTMLInputElement) || !event.target.files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = event.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
|
@@ -5,7 +5,9 @@ const storageKey = 'language';
|
||||
const overrideLanguage = localStorage.getItem(storageKey);
|
||||
const localeFile = String(overrideLanguage || navigator.language || navigator.userLanguage || 'en').toLowerCase();
|
||||
const langs = await fetch('/locales/lang.json').then(response => response.json());
|
||||
const localeData = await getLocaleData(localeFile);
|
||||
// Don't change to let/const! It will break module loading.
|
||||
// eslint-disable-next-line prefer-const
|
||||
var localeData = await getLocaleData(localeFile);
|
||||
|
||||
/**
|
||||
* Fetches the locale data for the given language.
|
||||
|
@@ -7,7 +7,7 @@ import {
|
||||
power_user,
|
||||
context_presets,
|
||||
} from './power-user.js';
|
||||
import { resetScrollHeight } from './utils.js';
|
||||
import { regexFromString, resetScrollHeight } from './utils.js';
|
||||
|
||||
/**
|
||||
* @type {any[]} Instruct mode presets.
|
||||
@@ -189,10 +189,10 @@ export function autoSelectInstructPreset(modelId) {
|
||||
// If activation regex is set, check if it matches the model id
|
||||
if (preset.activation_regex) {
|
||||
try {
|
||||
const regex = new RegExp(preset.activation_regex, 'i');
|
||||
const regex = regexFromString(preset.activation_regex);
|
||||
|
||||
// Stop on first match so it won't cycle back and forth between presets if multiple regexes match
|
||||
if (regex.test(modelId)) {
|
||||
if (regex instanceof RegExp && regex.test(modelId)) {
|
||||
selectInstructPreset(preset.name);
|
||||
|
||||
return true;
|
||||
|
@@ -50,6 +50,7 @@ import {
|
||||
download,
|
||||
getBase64Async,
|
||||
getFileText,
|
||||
getImageSizeFromDataURL,
|
||||
getSortableDelay,
|
||||
isDataURL,
|
||||
parseJsonFile,
|
||||
@@ -265,6 +266,7 @@ const default_settings = {
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
@@ -273,6 +275,7 @@ const default_settings = {
|
||||
use_alt_scale: false,
|
||||
squash_system_messages: false,
|
||||
image_inlining: false,
|
||||
inline_image_quality: 'low',
|
||||
bypass_status_check: false,
|
||||
continue_prefill: false,
|
||||
names_behavior: character_names_behavior.NONE,
|
||||
@@ -340,6 +343,7 @@ const oai_settings = {
|
||||
show_external_models: false,
|
||||
proxy_password: '',
|
||||
assistant_prefill: '',
|
||||
assistant_impersonation: '',
|
||||
human_sysprompt_message: default_claude_human_sysprompt_message,
|
||||
use_ai21_tokenizer: false,
|
||||
use_google_tokenizer: false,
|
||||
@@ -348,6 +352,7 @@ const oai_settings = {
|
||||
use_alt_scale: false,
|
||||
squash_system_messages: false,
|
||||
image_inlining: false,
|
||||
inline_image_quality: 'low',
|
||||
bypass_status_check: false,
|
||||
continue_prefill: false,
|
||||
names_behavior: character_names_behavior.NONE,
|
||||
@@ -1764,7 +1769,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
generate_data['human_sysprompt_message'] = substituteParams(oai_settings.human_sysprompt_message);
|
||||
// Don't add a prefill on quiet gens (summarization)
|
||||
if (!isQuiet) {
|
||||
generate_data['assistant_prefill'] = substituteParams(oai_settings.assistant_prefill);
|
||||
generate_data['assistant_prefill'] = isImpersonate ? substituteParams(oai_settings.assistant_impersonation) : substituteParams(oai_settings.assistant_prefill);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1844,6 +1849,8 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
generate_data['seed'] = oai_settings.seed;
|
||||
}
|
||||
|
||||
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
|
||||
|
||||
const generate_url = '/api/backends/chat-completions/generate';
|
||||
const response = await fetch(generate_url, {
|
||||
method: 'POST',
|
||||
@@ -2188,12 +2195,47 @@ class Message {
|
||||
}
|
||||
}
|
||||
|
||||
const quality = oai_settings.inline_image_quality || default_settings.inline_image_quality;
|
||||
this.content = [
|
||||
{ type: 'text', text: textContent },
|
||||
{ type: 'image_url', image_url: { 'url': image, 'detail': 'low' } },
|
||||
{ type: 'image_url', image_url: { 'url': image, 'detail': quality } },
|
||||
];
|
||||
|
||||
this.tokens += Message.tokensPerImage;
|
||||
const tokens = await this.getImageTokenCost(image, quality);
|
||||
this.tokens += tokens;
|
||||
}
|
||||
|
||||
async getImageTokenCost(dataUrl, quality) {
|
||||
if (quality === 'low') {
|
||||
return Message.tokensPerImage;
|
||||
}
|
||||
|
||||
const size = await getImageSizeFromDataURL(dataUrl);
|
||||
|
||||
// If the image is small enough, we can use the low quality token cost
|
||||
if (quality === 'auto' && size.width <= 512 && size.height <= 512) {
|
||||
return Message.tokensPerImage;
|
||||
}
|
||||
|
||||
/*
|
||||
* Images are first scaled to fit within a 2048 x 2048 square, maintaining their aspect ratio.
|
||||
* Then, they are scaled such that the shortest side of the image is 768px long.
|
||||
* Finally, we count how many 512px squares the image consists of.
|
||||
* Each of those squares costs 170 tokens. Another 85 tokens are always added to the final total.
|
||||
* https://platform.openai.com/docs/guides/vision/calculating-costs
|
||||
*/
|
||||
|
||||
const scale = 2048 / Math.min(size.width, size.height);
|
||||
const scaledWidth = Math.round(size.width * scale);
|
||||
const scaledHeight = Math.round(size.height * scale);
|
||||
|
||||
const finalScale = 768 / Math.min(scaledWidth, scaledHeight);
|
||||
const finalWidth = Math.round(scaledWidth * finalScale);
|
||||
const finalHeight = Math.round(scaledHeight * finalScale);
|
||||
|
||||
const squares = Math.ceil(finalWidth / 512) * Math.ceil(finalHeight / 512);
|
||||
const tokens = squares * 170 + 85;
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2720,8 +2762,10 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
|
||||
oai_settings.proxy_password = settings.proxy_password ?? default_settings.proxy_password;
|
||||
oai_settings.assistant_prefill = settings.assistant_prefill ?? default_settings.assistant_prefill;
|
||||
oai_settings.assistant_impersonation = settings.assistant_impersonation ?? default_settings.assistant_impersonation;
|
||||
oai_settings.human_sysprompt_message = settings.human_sysprompt_message ?? default_settings.human_sysprompt_message;
|
||||
oai_settings.image_inlining = settings.image_inlining ?? default_settings.image_inlining;
|
||||
oai_settings.inline_image_quality = settings.inline_image_quality ?? default_settings.inline_image_quality;
|
||||
oai_settings.bypass_status_check = settings.bypass_status_check ?? default_settings.bypass_status_check;
|
||||
oai_settings.seed = settings.seed ?? default_settings.seed;
|
||||
oai_settings.n = settings.n ?? default_settings.n;
|
||||
@@ -2755,10 +2799,14 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#api_url_scale').val(oai_settings.api_url_scale);
|
||||
$('#openai_proxy_password').val(oai_settings.proxy_password);
|
||||
$('#claude_assistant_prefill').val(oai_settings.assistant_prefill);
|
||||
$('#claude_assistant_impersonation').val(oai_settings.assistant_impersonation);
|
||||
$('#claude_human_sysprompt_textarea').val(oai_settings.human_sysprompt_message);
|
||||
$('#openai_image_inlining').prop('checked', oai_settings.image_inlining);
|
||||
$('#openai_bypass_status_check').prop('checked', oai_settings.bypass_status_check);
|
||||
|
||||
$('#openai_inline_image_quality').val(oai_settings.inline_image_quality);
|
||||
$(`#openai_inline_image_quality option[value="${oai_settings.inline_image_quality}"]`).prop('selected', true);
|
||||
|
||||
$('#model_openai_select').val(oai_settings.openai_model);
|
||||
$(`#model_openai_select option[value="${oai_settings.openai_model}"`).attr('selected', true);
|
||||
$('#model_claude_select').val(oai_settings.claude_model);
|
||||
@@ -3071,6 +3119,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
api_url_scale: settings.api_url_scale,
|
||||
show_external_models: settings.show_external_models,
|
||||
assistant_prefill: settings.assistant_prefill,
|
||||
assistant_impersonation: settings.assistant_impersonation,
|
||||
human_sysprompt_message: settings.human_sysprompt_message,
|
||||
use_ai21_tokenizer: settings.use_ai21_tokenizer,
|
||||
use_google_tokenizer: settings.use_google_tokenizer,
|
||||
@@ -3079,6 +3128,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
use_alt_scale: settings.use_alt_scale,
|
||||
squash_system_messages: settings.squash_system_messages,
|
||||
image_inlining: settings.image_inlining,
|
||||
inline_image_quality: settings.inline_image_quality,
|
||||
bypass_status_check: settings.bypass_status_check,
|
||||
continue_prefill: settings.continue_prefill,
|
||||
continue_postfix: settings.continue_postfix,
|
||||
@@ -3456,6 +3506,7 @@ function onSettingsPresetChange() {
|
||||
show_external_models: ['#openai_show_external_models', 'show_external_models', true],
|
||||
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
|
||||
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
|
||||
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false],
|
||||
human_sysprompt_message: ['#claude_human_sysprompt_textarea', 'human_sysprompt_message', false],
|
||||
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
|
||||
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
|
||||
@@ -3464,6 +3515,7 @@ function onSettingsPresetChange() {
|
||||
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
|
||||
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
|
||||
image_inlining: ['#openai_image_inlining', 'image_inlining', true],
|
||||
inline_image_quality: ['#openai_inline_image_quality', 'inline_image_quality', false],
|
||||
continue_prefill: ['#continue_prefill', 'continue_prefill', true],
|
||||
continue_postfix: ['#continue_postfix', 'continue_postfix', false],
|
||||
seed: ['#seed_openai', 'seed', false],
|
||||
@@ -3480,6 +3532,11 @@ function onSettingsPresetChange() {
|
||||
preset.names_behavior = character_names_behavior.COMPLETION;
|
||||
}
|
||||
|
||||
// Claude: Assistant Impersonation Prefill = Inherit from Assistant Prefill
|
||||
if (preset.assistant_prefill !== undefined && preset.assistant_impersonation === undefined) {
|
||||
preset.assistant_impersonation = preset.assistant_prefill;
|
||||
}
|
||||
|
||||
const updateInput = (selector, value) => $(selector).val(value).trigger('input');
|
||||
const updateCheckbox = (selector, value) => $(selector).prop('checked', value).trigger('input');
|
||||
|
||||
@@ -3515,7 +3572,7 @@ function getMaxContextOpenAI(value) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
return unlocked_max;
|
||||
}
|
||||
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
|
||||
else if (value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
|
||||
return max_128k;
|
||||
}
|
||||
else if (value.includes('gpt-3.5-turbo-1106')) {
|
||||
@@ -3679,7 +3736,7 @@ async function onModelChange() {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', max_1mil);
|
||||
} else if (value === 'gemini-1.5-pro-latest') {
|
||||
} else if (value === 'gemini-1.5-pro-latest' || value.includes('gemini-1.5-flash')) {
|
||||
$('#openai_max_context').attr('max', max_1mil);
|
||||
} else if (value === 'gemini-ultra' || value === 'gemini-1.0-pro-latest' || value === 'gemini-pro' || value === 'gemini-1.0-ultra-latest') {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
@@ -4239,11 +4296,14 @@ export function isImageInliningSupported() {
|
||||
// gultra just isn't being offered as multimodal, thanks google.
|
||||
const visionSupportedModels = [
|
||||
'gpt-4-vision',
|
||||
'gemini-1.5-flash-latest',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.0-pro-vision-latest',
|
||||
'gemini-1.5-pro-latest',
|
||||
'gemini-pro-vision',
|
||||
'claude-3',
|
||||
'gpt-4-turbo',
|
||||
'gpt-4o',
|
||||
];
|
||||
|
||||
switch (oai_settings.chat_completion_source) {
|
||||
@@ -4672,6 +4732,11 @@ $(document).ready(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_assistant_impersonation').on('input', function () {
|
||||
oai_settings.assistant_impersonation = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#claude_human_sysprompt_textarea').on('input', function () {
|
||||
oai_settings.human_sysprompt_message = String($('#claude_human_sysprompt_textarea').val());
|
||||
saveSettingsDebounced();
|
||||
@@ -4707,6 +4772,11 @@ $(document).ready(async function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#openai_inline_image_quality').on('input', function () {
|
||||
oai_settings.inline_image_quality = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#continue_prefill').on('input', function () {
|
||||
oai_settings.continue_prefill = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
@@ -259,6 +259,7 @@ let power_user = {
|
||||
stscript: {
|
||||
matching: 'fuzzy',
|
||||
autocomplete: {
|
||||
autoHide: false,
|
||||
style: 'theme',
|
||||
font: {
|
||||
scale: 0.8,
|
||||
@@ -1618,6 +1619,7 @@ function loadPowerUserSettings(settings, data) {
|
||||
$('#token_padding').val(power_user.token_padding);
|
||||
$('#aux_field').val(power_user.aux_field);
|
||||
|
||||
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
|
||||
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
|
||||
$('#stscript_autocomplete_style').val(power_user.stscript.autocomplete_style ?? 'theme');
|
||||
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
|
||||
@@ -3646,6 +3648,11 @@ $(document).ready(() => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_autocomplete_autoHide').on('input', function () {
|
||||
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#stscript_matching').on('change', function () {
|
||||
const value = $(this).find(':selected').val();
|
||||
power_user.stscript.matching = String(value);
|
||||
|
@@ -114,7 +114,9 @@ class PresetManager {
|
||||
* @returns {any} Preset value
|
||||
*/
|
||||
findPreset(name) {
|
||||
return $(this.select).find(`option:contains(${name})`).val();
|
||||
return $(this.select).find('option').filter(function() {
|
||||
return $(this).text() === name;
|
||||
}).val();
|
||||
}
|
||||
|
||||
/**
|
||||
|
441
public/scripts/samplerSelect.js
Normal file
441
public/scripts/samplerSelect.js
Normal file
@@ -0,0 +1,441 @@
|
||||
import {
|
||||
main_api,
|
||||
saveSettingsDebounced,
|
||||
novelai_setting_names,
|
||||
callPopup,
|
||||
settings,
|
||||
} from '../script.js';
|
||||
import { power_user } from './power-user.js';
|
||||
//import { BIAS_CACHE, displayLogitBias, getLogitBiasListResult } from './logit-bias.js';
|
||||
//import { getEventSourceStream } from './sse-stream.js';
|
||||
//import { getSortableDelay, onlyUnique } from './utils.js';
|
||||
//import { getCfgPrompt } from './cfg-scale.js';
|
||||
import { setting_names } from './textgen-settings.js';
|
||||
|
||||
|
||||
const TGsamplerNames = setting_names;
|
||||
|
||||
const forcedOnColoring = 'filter: sepia(1) hue-rotate(59deg) contrast(1.5) saturate(3.5)';
|
||||
const forcedOffColoring = 'filter: sepia(1) hue-rotate(308deg) contrast(0.7) saturate(10)';
|
||||
|
||||
let userDisabledSamplers, userShownSamplers;
|
||||
|
||||
/*
|
||||
|
||||
for reference purposes:
|
||||
|
||||
//NAI
|
||||
const nai_settings = {
|
||||
temperature: 1.5,
|
||||
repetition_penalty: 2.25,
|
||||
repetition_penalty_range: 2048,
|
||||
repetition_penalty_slope: 0.09,
|
||||
repetition_penalty_frequency: 0,
|
||||
repetition_penalty_presence: 0.005,
|
||||
tail_free_sampling: 0.975,
|
||||
top_k: 10,
|
||||
top_p: 0.75,
|
||||
top_a: 0.08,
|
||||
typical_p: 0.975,
|
||||
min_length: 1,
|
||||
model_novel: 'clio-v1',
|
||||
preset_settings_novel: 'Talker-Chat-Clio',
|
||||
streaming_novel: false,
|
||||
preamble: default_preamble,
|
||||
prefix: '',
|
||||
cfg_uc: '',
|
||||
banned_tokens: '',
|
||||
order: default_order,
|
||||
logit_bias: [],
|
||||
};
|
||||
|
||||
// TG Types
|
||||
export const textgen_types = {
|
||||
OOBA: 'ooba',
|
||||
MANCER: 'mancer',
|
||||
VLLM: 'vllm',
|
||||
APHRODITE: 'aphrodite',
|
||||
TABBY: 'tabby',
|
||||
KOBOLDCPP: 'koboldcpp',
|
||||
TOGETHERAI: 'togetherai',
|
||||
LLAMACPP: 'llamacpp',
|
||||
OLLAMA: 'ollama',
|
||||
INFERMATICAI: 'infermaticai',
|
||||
DREAMGEN: 'dreamgen',
|
||||
OPENROUTER: 'openrouter',
|
||||
};
|
||||
|
||||
//KAI and TextGen
|
||||
const setting_names = [
|
||||
'temp',
|
||||
'temperature_last',
|
||||
'rep_pen',
|
||||
'rep_pen_range',
|
||||
'no_repeat_ngram_size',
|
||||
'top_k',
|
||||
'top_p',
|
||||
'top_a',
|
||||
'tfs',
|
||||
'epsilon_cutoff',
|
||||
'eta_cutoff',
|
||||
'typical_p',
|
||||
'min_p',
|
||||
'penalty_alpha',
|
||||
'num_beams',
|
||||
'length_penalty',
|
||||
'min_length',
|
||||
'dynatemp',
|
||||
'min_temp',
|
||||
'max_temp',
|
||||
'dynatemp_exponent',
|
||||
'smoothing_factor',
|
||||
'smoothing_curve',
|
||||
'max_tokens_second',
|
||||
'encoder_rep_pen',
|
||||
'freq_pen',
|
||||
'presence_pen',
|
||||
'do_sample',
|
||||
'early_stopping',
|
||||
'seed',
|
||||
'add_bos_token',
|
||||
'ban_eos_token',
|
||||
'skip_special_tokens',
|
||||
'streaming',
|
||||
'mirostat_mode',
|
||||
'mirostat_tau',
|
||||
'mirostat_eta',
|
||||
'guidance_scale',
|
||||
'negative_prompt',
|
||||
'grammar_string',
|
||||
'json_schema',
|
||||
'banned_tokens',
|
||||
'legacy_api',
|
||||
//'n_aphrodite',
|
||||
//'best_of_aphrodite',
|
||||
'ignore_eos_token',
|
||||
'spaces_between_special_tokens',
|
||||
//'logits_processors_aphrodite',
|
||||
//'log_probs_aphrodite',
|
||||
//'prompt_log_probs_aphrodite'
|
||||
'sampler_order',
|
||||
'sampler_priority',
|
||||
'samplers',
|
||||
'n',
|
||||
'logit_bias',
|
||||
'custom_model',
|
||||
'bypass_status_check',
|
||||
];
|
||||
|
||||
//OAI settings
|
||||
|
||||
const default_settings = {
|
||||
preset_settings_openai: 'Default',
|
||||
temp_openai: 1.0,
|
||||
freq_pen_openai: 0,
|
||||
pres_pen_openai: 0,
|
||||
count_pen: 0.0,
|
||||
top_p_openai: 1.0,
|
||||
top_k_openai: 0,
|
||||
min_p_openai: 0,
|
||||
top_a_openai: 1,
|
||||
repetition_penalty_openai: 1,
|
||||
stream_openai: false,
|
||||
//...
|
||||
}
|
||||
|
||||
|
||||
*/
|
||||
|
||||
// Goal 1: show popup with all samplers for active API
|
||||
async function showSamplerSelectPopup() {
|
||||
const popup = $('#dialogue_popup');
|
||||
popup.addClass('large_dialogue_popup');
|
||||
const html = $(document.createElement('div'));
|
||||
html.attr('id', 'sampler_view_list')
|
||||
.addClass('flex-container flexFlowColumn');
|
||||
html.append(`
|
||||
<div class="title_restorable flexFlowColumn alignItemsBaseline">
|
||||
<div class="flex-container justifyCenter">
|
||||
<h3>Sampler Select</h3>
|
||||
<div class="flex-container alignItemsBaseline">
|
||||
<div id="resetSelectedSamplers" class="menu_button menu_button_icon tag_view_create" title="Reset custom sampler selection">
|
||||
<i class="fa-solid fa-recycle"></i>
|
||||
</div>
|
||||
</div>
|
||||
<!--<div class="flex-container alignItemsBaseline">
|
||||
<div class="menu_button menu_button_icon tag_view_create" title="Create a new sampler">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span data-i18n="Create">Create</span>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
<small>Here you can toggle the display of individual samplers. (WIP)</small>
|
||||
</div>
|
||||
<hr>`);
|
||||
|
||||
const listContainer = $('<div id="apiSamplersList" class="flex-container flexNoGap"></div>');
|
||||
const APISamplers = await listSamplers(main_api);
|
||||
listContainer.append(APISamplers);
|
||||
html.append(listContainer);
|
||||
|
||||
callPopup(html, 'text', null, { allowVerticalScrolling: true });
|
||||
|
||||
setSamplerListListeners();
|
||||
|
||||
$('#resetSelectedSamplers').off('click').on('click', async function () {
|
||||
console.log('saw sampler select reset click');
|
||||
userDisabledSamplers = [];
|
||||
userShownSamplers = [];
|
||||
power_user.selectSamplers.forceShown = [];
|
||||
power_user.selectSamplers.forceHidden = [];
|
||||
await validateDisabledSamplers(true);
|
||||
});
|
||||
}
|
||||
|
||||
function setSamplerListListeners() {
|
||||
// Goal 2: hide unchecked samplers from DOM
|
||||
let listContainer = $('#apiSamplersList');
|
||||
listContainer.find('input').off('change').on('change', async function () {
|
||||
|
||||
const samplerName = this.name.replace('_checkbox', '');
|
||||
let relatedDOMElement = $(`#${samplerName}_${main_api}`).parent();
|
||||
let targetDisplayType = 'flex';
|
||||
|
||||
if (samplerName === 'json_schema') {
|
||||
relatedDOMElement = $('#json_schema_block');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'grammar_string') {
|
||||
relatedDOMElement = $('#grammar_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'guidance_scale') {
|
||||
relatedDOMElement = $('#cfg_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'mirostat_mode') {
|
||||
relatedDOMElement = $('#mirostat_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'dynatemp') {
|
||||
relatedDOMElement = $('#dynatemp_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'banned_tokens') {
|
||||
relatedDOMElement = $('#banned_tokens_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (samplerName === 'sampler_order') {
|
||||
relatedDOMElement = $('#sampler_order_block');
|
||||
targetDisplayType = 'flex';
|
||||
}
|
||||
|
||||
// Get the current state of the custom data attribute
|
||||
const previousState = relatedDOMElement.data('selectsampler');
|
||||
|
||||
if ($(this).prop('checked') === false) {
|
||||
//console.log('saw clicking checkbox from on to off...');
|
||||
if (previousState === 'shown') {
|
||||
console.log('saw previously custom shown sampler');
|
||||
//console.log('removing from custom force show list');
|
||||
relatedDOMElement.removeData('selectsampler');
|
||||
$(this).parent().find('.sampler_name').removeAttr('style');
|
||||
power_user?.selectSamplers?.forceShown.splice(power_user?.selectSamplers?.forceShown.indexOf(samplerName), 1);
|
||||
console.log(power_user?.selectSamplers?.forceShown);
|
||||
} else {
|
||||
console.log('saw previous untouched sampler');
|
||||
//console.log(`adding ${samplerName} to force hide list`);
|
||||
relatedDOMElement.data('selectsampler', 'hidden');
|
||||
console.log(relatedDOMElement.data('selectsampler'));
|
||||
power_user.selectSamplers.forceHidden.push(samplerName);
|
||||
$(this).parent().find('.sampler_name').attr('style', forcedOffColoring);
|
||||
console.log(power_user.selectSamplers.forceHidden);
|
||||
}
|
||||
} else { // going from unchecked to checked
|
||||
//console.log('saw clicking checkbox from off to on...');
|
||||
if (previousState === 'hidden') {
|
||||
console.log('saw previously custom hidden sampler');
|
||||
//console.log('removing from custom force hide list');
|
||||
relatedDOMElement.removeData('selectsampler');
|
||||
$(this).parent().find('.sampler_name').removeAttr('style');
|
||||
power_user?.selectSamplers?.forceHidden.splice(power_user?.selectSamplers?.forceHidden.indexOf(samplerName), 1);
|
||||
console.log(power_user?.selectSamplers?.forceHidden);
|
||||
} else {
|
||||
console.log('saw previous untouched sampler');
|
||||
//console.log(`adding ${samplerName} to force shown list`);
|
||||
relatedDOMElement.data('selectsampler', 'shown');
|
||||
console.log(relatedDOMElement.data('selectsampler'));
|
||||
power_user.selectSamplers.forceShown.push(samplerName);
|
||||
$(this).parent().find('.sampler_name').attr('style', forcedOnColoring);
|
||||
console.log(power_user.selectSamplers.forceShown);
|
||||
}
|
||||
}
|
||||
await saveSettingsDebounced();
|
||||
|
||||
const shouldDisplay = $(this).prop('checked') ? targetDisplayType : 'none';
|
||||
relatedDOMElement.css('display', shouldDisplay);
|
||||
|
||||
console.log(samplerName, relatedDOMElement.data('selectsampler'), shouldDisplay);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function isElementVisibleInDOM(element) {
|
||||
while (element && element !== document.body) {
|
||||
if (window.getComputedStyle(element).display === 'none') {
|
||||
return false;
|
||||
}
|
||||
element = element.parentElement;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async function listSamplers(main_api, arrayOnly = false) {
|
||||
let availableSamplers;
|
||||
if (main_api === 'textgenerationwebui') {
|
||||
availableSamplers = TGsamplerNames;
|
||||
const valuesToRemove = new Set(['streaming', 'seed', 'bypass_status_check', 'custom_model', 'legacy_api', 'samplers']);
|
||||
availableSamplers = availableSamplers.filter(sampler => !valuesToRemove.has(sampler));
|
||||
availableSamplers.sort();
|
||||
}
|
||||
|
||||
if (arrayOnly) {
|
||||
console.log('returning full samplers array');
|
||||
return availableSamplers;
|
||||
}
|
||||
|
||||
const samplersListHTML = availableSamplers.reduce((html, sampler) => {
|
||||
let customColor;
|
||||
const targetDOMelement = $(`#${sampler}_${main_api}`);
|
||||
|
||||
const isInForceHiddenArray = userDisabledSamplers.includes(sampler);
|
||||
const isInForceShownArray = userShownSamplers.includes(sampler);
|
||||
let isVisibleInDOM = isElementVisibleInDOM(targetDOMelement[0]);
|
||||
const isInDefaultState = () => {
|
||||
if (isVisibleInDOM && isInForceShownArray) { return false; }
|
||||
else if (!isVisibleInDOM && isInForceHiddenArray) { return false; }
|
||||
else { return true; }
|
||||
};
|
||||
|
||||
const shouldBeChecked = () => {
|
||||
if (isInForceHiddenArray) {
|
||||
customColor = forcedOffColoring;
|
||||
return false;
|
||||
}
|
||||
else if (isInForceShownArray) {
|
||||
customColor = forcedOnColoring;
|
||||
return true;
|
||||
}
|
||||
else { return isVisibleInDOM; }
|
||||
};
|
||||
console.log(sampler, isInDefaultState(), isInForceHiddenArray, shouldBeChecked());
|
||||
return html + `
|
||||
<div class="sampler_view_list_item wide50p flex-container">
|
||||
<input type="checkbox" name="${sampler}_checkbox" ${shouldBeChecked() ? 'checked' : ''}>
|
||||
<small class="sampler_name" style="${customColor}">${sampler}</small>
|
||||
</div>
|
||||
`;
|
||||
}, '');
|
||||
|
||||
return samplersListHTML;
|
||||
}
|
||||
|
||||
// Goal 3: make "sampler is hidden/disabled" status persistent (save settings)
|
||||
// this runs on initial getSettings as well as after API changes
|
||||
|
||||
export async function validateDisabledSamplers(redraw = false) {
|
||||
const APISamplers = await listSamplers(main_api, true);
|
||||
|
||||
if (!Array.isArray(APISamplers)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const sampler of APISamplers) {
|
||||
let relatedDOMElement = $(`#${sampler}_${main_api}`).parent();
|
||||
let targetDisplayType = 'flex';
|
||||
|
||||
if (sampler === 'json_schema') {
|
||||
relatedDOMElement = $('#json_schema_block');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'grammar_string') {
|
||||
relatedDOMElement = $('#grammar_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'guidance_scale') {
|
||||
relatedDOMElement = $('#cfg_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'mirostat_mode') {
|
||||
relatedDOMElement = $('#mirostat_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'dynatemp') {
|
||||
relatedDOMElement = $('#dynatemp_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'banned_tokens') {
|
||||
relatedDOMElement = $('#banned_tokens_block_ooba');
|
||||
targetDisplayType = 'block';
|
||||
}
|
||||
|
||||
if (sampler === 'sampler_order') {
|
||||
relatedDOMElement = $('#sampler_order_block');
|
||||
}
|
||||
|
||||
if (power_user?.selectSamplers?.forceHidden.includes(sampler)) {
|
||||
//default handling for standard sliders
|
||||
relatedDOMElement.data('selectsampler', 'hidden');
|
||||
relatedDOMElement.css('display', 'none');
|
||||
} else if (power_user?.selectSamplers?.forceShown.includes(sampler)) {
|
||||
relatedDOMElement.data('selectsampler', 'shown');
|
||||
relatedDOMElement.css('display', targetDisplayType);
|
||||
} else {
|
||||
if (relatedDOMElement.data('selectsampler') === 'hidden') {
|
||||
relatedDOMElement.removeAttr('selectsampler');
|
||||
relatedDOMElement.css('display', targetDisplayType);
|
||||
}
|
||||
if (relatedDOMElement.data('selectsampler') === 'shown') {
|
||||
relatedDOMElement.removeAttr('selectsampler');
|
||||
relatedDOMElement.css('display', 'none');
|
||||
}
|
||||
}
|
||||
if (redraw) {
|
||||
let samplersHTML = await listSamplers(main_api);
|
||||
$('#apiSamplersList').empty().append(samplersHTML);
|
||||
setSamplerListListeners();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function initCustomSelectedSamplers() {
|
||||
|
||||
userDisabledSamplers = power_user?.selectSamplers?.forceHidden || [];
|
||||
userShownSamplers = power_user?.selectSamplers?.forceShown || [];
|
||||
power_user.selectSamplers = {};
|
||||
power_user.selectSamplers.forceHidden = userDisabledSamplers;
|
||||
power_user.selectSamplers.forceShown = userShownSamplers;
|
||||
await saveSettingsDebounced();
|
||||
$('#samplerSelectButton').off('click').on('click', showSamplerSelectPopup);
|
||||
|
||||
}
|
||||
|
||||
// Goal 4: filter hidden samplers from API output
|
||||
|
||||
// Goal 5: allow addition of custom samplers to be displayed
|
||||
// Goal 6: send custom sampler values into prompt
|
@@ -447,8 +447,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unhide',
|
||||
],
|
||||
helpString: 'Unhides a message from the prompt.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'disable',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-disable',
|
||||
callback: disableGroupMemberCallback,
|
||||
aliases: ['disable', 'disablemember', 'memberdisable'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@@ -456,7 +457,8 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'disable',
|
||||
],
|
||||
helpString: 'Disables a group member from being drafted for replies.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'enable',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-enable',
|
||||
aliases: ['enable', 'enablemember', 'memberenable'],
|
||||
callback: enableGroupMemberCallback,
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
@@ -465,9 +467,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'enable',
|
||||
],
|
||||
helpString: 'Enables a group member to be drafted for replies.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberadd',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-add',
|
||||
callback: addGroupMemberCallback,
|
||||
aliases: ['addmember'],
|
||||
aliases: ['addmember', 'memberadd'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'character name', [ARGUMENT_TYPE.STRING], true,
|
||||
@@ -481,15 +483,15 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberadd',
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/memberadd John Doe</code></pre>
|
||||
<pre><code>/member-add John Doe</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberremove',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-remove',
|
||||
callback: removeGroupMemberCallback,
|
||||
aliases: ['removemember'],
|
||||
aliases: ['removemember', 'memberremove'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@@ -503,16 +505,16 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberremove
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/memberremove 2</code></pre>
|
||||
<pre><code>/memberremove John Doe</code></pre>
|
||||
<pre><code>/member-remove 2</code></pre>
|
||||
<pre><code>/member-remove John Doe</code></pre>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-up',
|
||||
callback: moveGroupMemberUpCallback,
|
||||
aliases: ['upmember'],
|
||||
aliases: ['upmember', 'memberup'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@@ -520,9 +522,9 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberup',
|
||||
],
|
||||
helpString: 'Moves a group member up in the group chat list.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'memberdown',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'member-down',
|
||||
callback: moveGroupMemberDownCallback,
|
||||
aliases: ['downmember'],
|
||||
aliases: ['downmember', 'memberdown'],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'member index or name', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING], true,
|
||||
@@ -702,6 +704,19 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'addswipe',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'abort',
|
||||
callback: abortCallback,
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({ name: 'quiet',
|
||||
description: 'Whether to suppress the toast message notifying about the /abort call.',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'true',
|
||||
enumList: ['true', 'false'],
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({ description: 'The reason for aborting command execution. Shown when quiet=false',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
helpString: 'Aborts the slash command batch execution.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'fuzzy',
|
||||
@@ -847,6 +862,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'messages',
|
||||
new SlashCommandNamedArgument(
|
||||
'names', 'show message author names', [ARGUMENT_TYPE.BOOLEAN], false, false, 'off', ['off', 'on'],
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'hidden', 'include hidden messages', [ARGUMENT_TYPE.BOOLEAN], false, false, 'on', ['off', 'on'],
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'role', 'filter messages by role' , [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'assistant', 'user'],
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
@@ -858,6 +879,12 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'messages',
|
||||
<div>
|
||||
Returns the specified message or range of messages as a string.
|
||||
</div>
|
||||
<div>
|
||||
Use the <code>hidden=off</code> argument to exclude hidden messages.
|
||||
</div>
|
||||
<div>
|
||||
Use the <code>role</code> argument to filter messages by role. Possible values are: system, assistant, user.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Examples:</strong>
|
||||
<ul>
|
||||
@@ -1310,13 +1337,37 @@ async function popupCallback(args, value) {
|
||||
|
||||
function getMessagesCallback(args, value) {
|
||||
const includeNames = !isFalseBoolean(args?.names);
|
||||
const includeHidden = isTrueBoolean(args?.hidden);
|
||||
const role = args?.role;
|
||||
const range = stringToRange(value, 0, chat.length - 1);
|
||||
|
||||
if (!range) {
|
||||
console.warn(`WARN: Invalid range provided for /getmessages command: ${value}`);
|
||||
console.warn(`WARN: Invalid range provided for /messages command: ${value}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const filterByRole = (mes) => {
|
||||
if (!role) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isNarrator = mes.extra?.type === system_message_types.NARRATOR;
|
||||
|
||||
if (role === 'system') {
|
||||
return isNarrator && !mes.is_user;
|
||||
}
|
||||
|
||||
if (role === 'assistant') {
|
||||
return !isNarrator && !mes.is_user;
|
||||
}
|
||||
|
||||
if (role === 'user') {
|
||||
return !isNarrator && mes.is_user;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid role provided. Expected one of: system, assistant, user. Got: ${role}`);
|
||||
};
|
||||
|
||||
const messages = [];
|
||||
|
||||
for (let messageId = range.start; messageId <= range.end; messageId++) {
|
||||
@@ -1326,7 +1377,13 @@ function getMessagesCallback(args, value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.is_system) {
|
||||
if (role && !filterByRole(message)) {
|
||||
console.debug(`/messages: Skipping message with ID ${messageId} due to role filter`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!includeHidden && message.is_system) {
|
||||
console.debug(`/messages: Skipping hidden message with ID ${messageId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1377,9 +1434,15 @@ async function runCallback(args, name) {
|
||||
}
|
||||
}
|
||||
|
||||
function abortCallback() {
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
throw new Error('/abort command executed');
|
||||
/**
|
||||
*
|
||||
* @param {object} param0
|
||||
* @param {SlashCommandAbortController} param0._abortController
|
||||
* @param {string} [param0.quiet]
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
function abortCallback({ _abortController, quiet }, reason) {
|
||||
_abortController.abort((reason ?? '').toString().length == 0 ? '/abort command executed' : reason, !isFalseBoolean(quiet ?? 'true'));
|
||||
}
|
||||
|
||||
async function delayCallback(_, amount) {
|
||||
@@ -2645,7 +2708,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
|
||||
* @prop {boolean} [handleParserErrors] (true) Whether to handle parser errors (show toast on error) or throw.
|
||||
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||||
* @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
|
||||
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
|
||||
* @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
|
||||
*/
|
||||
@@ -2653,7 +2716,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
|
||||
/**
|
||||
* @typedef ExecuteSlashCommandsOnChatInputOptions
|
||||
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||||
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
|
||||
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
|
||||
*/
|
||||
|
||||
@@ -2705,10 +2768,12 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
|
||||
}
|
||||
} catch (e) {
|
||||
document.querySelector('#form_sheld').classList.add('script_error');
|
||||
toastr.error(e.message);
|
||||
result = new SlashCommandClosureResult();
|
||||
result.isError = true;
|
||||
result.errorMessage = e.message;
|
||||
if (e.cause !== 'abort') {
|
||||
toastr.error(e.message);
|
||||
}
|
||||
} finally {
|
||||
delay(1000).then(()=>clearCommandProgressDebounced());
|
||||
|
||||
@@ -2740,7 +2805,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
|
||||
|
||||
let closure;
|
||||
try {
|
||||
closure = parser.parse(text, true, options.parserFlags, options.abortController);
|
||||
closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());
|
||||
closure.scope.parent = options.scope;
|
||||
closure.onProgress = options.onProgress;
|
||||
} catch (e) {
|
||||
@@ -2767,8 +2832,9 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
|
||||
|
||||
try {
|
||||
const result = await closure.execute();
|
||||
if (result.isAborted) {
|
||||
if (result.isAborted && !result.isQuietlyAborted) {
|
||||
toastr.warning(result.abortReason, 'Command execution aborted');
|
||||
closure.abortController.signal.isQuiet = true;
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
|
@@ -1,5 +1,25 @@
|
||||
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { PARSER_FLAG } from './SlashCommandParser.js';
|
||||
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* _pipe:string|SlashCommandClosure,
|
||||
* _scope:SlashCommandScope,
|
||||
* _parserFlags:{[id:PARSER_FLAG]:boolean},
|
||||
* _abortController:SlashCommandAbortController,
|
||||
* [id:string]:string|SlashCommandClosure,
|
||||
* }} NamedArguments
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string|SlashCommandClosure|(string|SlashCommandClosure)[]} UnnamedArguments
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +28,7 @@ export class SlashCommand {
|
||||
* Creates a SlashCommand from a properties object.
|
||||
* @param {Object} props
|
||||
* @param {string} [props.name]
|
||||
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @param {(namedArguments:NamedArguments, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @param {string} [props.helpString]
|
||||
* @param {boolean} [props.splitUnnamedArgument]
|
||||
* @param {string[]} [props.aliases]
|
||||
@@ -25,7 +45,7 @@ export class SlashCommand {
|
||||
|
||||
|
||||
/**@type {string}*/ name;
|
||||
/**@type {(namedArguments:Object<string, string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||
/**@type {(namedArguments:{_pipe:string|SlashCommandClosure, _scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||
/**@type {string}*/ helpString;
|
||||
/**@type {boolean}*/ splitUnnamedArgument = false;
|
||||
/**@type {string[]}*/ aliases = [];
|
||||
|
@@ -5,7 +5,8 @@ export class SlashCommandAbortController {
|
||||
constructor() {
|
||||
this.signal = new SlashCommandAbortSignal();
|
||||
}
|
||||
abort(reason = 'No reason.') {
|
||||
abort(reason = 'No reason.', isQuiet = false) {
|
||||
this.signal.isQuiet = isQuiet;
|
||||
this.signal.aborted = true;
|
||||
this.signal.reason = reason;
|
||||
}
|
||||
@@ -20,8 +21,8 @@ export class SlashCommandAbortController {
|
||||
}
|
||||
|
||||
export class SlashCommandAbortSignal {
|
||||
/**@type {boolean}*/ isQuiet = false;
|
||||
/**@type {boolean}*/ paused = false;
|
||||
/**@type {boolean}*/ aborted = false;
|
||||
/**@type {string}*/ reason = null;
|
||||
|
||||
}
|
||||
|
@@ -50,6 +50,17 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
}
|
||||
|
||||
getNamedArgumentAt(text, index, isSelect) {
|
||||
function getSplitRegex() {
|
||||
try {
|
||||
return new RegExp('(?<==)');
|
||||
} catch {
|
||||
// For browsers that don't support lookbehind
|
||||
return new RegExp('=(.*)');
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(this.executor.command?.namedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const notProvidedNamedArguments = this.executor.command.namedArgumentList.filter(arg=>!this.executor.namedArgumentList.find(it=>it.name == arg.name));
|
||||
let name;
|
||||
let value;
|
||||
@@ -62,7 +73,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
// cursor is somewhere within the named arguments (including final space)
|
||||
argAssign = this.executor.namedArgumentList.find(it=>it.start <= index && it.end >= index);
|
||||
if (argAssign) {
|
||||
const [argName, ...v] = text.slice(argAssign.start, index).split(/(?<==)/);
|
||||
const [argName, ...v] = text.slice(argAssign.start, index).split(getSplitRegex());
|
||||
name = argName;
|
||||
value = v.join('');
|
||||
start = argAssign.start;
|
||||
@@ -99,7 +110,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start + name.length,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
true,
|
||||
);
|
||||
result.isRequired = true;
|
||||
@@ -122,6 +133,9 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
}
|
||||
|
||||
getUnnamedArgumentAt(text, index, isSelect) {
|
||||
if (!Array.isArray(this.executor.command?.unnamedArgumentList)) {
|
||||
return null;
|
||||
}
|
||||
const lastArgIsBlank = this.executor.unnamedArgumentList.slice(-1)[0]?.value == '';
|
||||
const notProvidedArguments = this.executor.command.unnamedArgumentList.slice(this.executor.unnamedArgumentList.length - (lastArgIsBlank ? 1 : 0));
|
||||
let value;
|
||||
@@ -154,7 +168,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||
const result = new AutoCompleteSecondaryNameResult(
|
||||
value,
|
||||
start,
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
|
||||
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
|
||||
false,
|
||||
);
|
||||
const isCompleteValue = cmdArg.enumList.find(it=>it.value == value);
|
||||
|
@@ -161,6 +161,7 @@ export class SlashCommandClosure {
|
||||
let args = {
|
||||
_scope: this.scope,
|
||||
_parserFlags: executor.parserFlags,
|
||||
_abortController: this.abortController,
|
||||
};
|
||||
let value;
|
||||
// substitute named arguments
|
||||
@@ -223,6 +224,15 @@ export class SlashCommandClosure {
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}')
|
||||
;
|
||||
} else if (Array.isArray(value)) {
|
||||
value = value.map(v=>{
|
||||
if (typeof v == 'string') {
|
||||
return v
|
||||
?.replace(/\\\{/g, '{')
|
||||
?.replace(/\\\}/g, '}');
|
||||
}
|
||||
return v;
|
||||
});
|
||||
}
|
||||
|
||||
let abortResult = await this.testAbortController();
|
||||
@@ -254,6 +264,7 @@ export class SlashCommandClosure {
|
||||
if (this.abortController?.signal?.aborted) {
|
||||
const result = new SlashCommandClosureResult();
|
||||
result.isAborted = true;
|
||||
result.isQuietlyAborted = this.abortController.signal.isQuiet;
|
||||
result.abortReason = this.abortController.signal.reason.toString();
|
||||
return result;
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ export class SlashCommandClosureResult {
|
||||
/**@type {boolean}*/ interrupt = false;
|
||||
/**@type {string}*/ pipe;
|
||||
/**@type {boolean}*/ isAborted = false;
|
||||
/**@type {boolean}*/ isQuietlyAborted = false;
|
||||
/**@type {string}*/ abortReason;
|
||||
/**@type {boolean}*/ isError = false;
|
||||
/**@type {string}*/ errorMessage;
|
||||
|
@@ -1,16 +1,20 @@
|
||||
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||
import { SlashCommand } from './SlashCommand.js';
|
||||
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||
|
||||
export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
|
||||
/**@type {SlashCommand}*/ cmd;
|
||||
/**@type {SlashCommandEnumValue}*/ enumValue;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param {SlashCommand} cmd
|
||||
* @param {SlashCommandEnumValue} enumValue
|
||||
*/
|
||||
constructor(enumValue) {
|
||||
constructor(cmd, enumValue) {
|
||||
super(enumValue.value, '◊');
|
||||
this.cmd = cmd;
|
||||
this.enumValue = enumValue;
|
||||
}
|
||||
|
||||
@@ -25,22 +29,6 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
|
||||
|
||||
|
||||
renderDetails() {
|
||||
const frag = document.createDocumentFragment();
|
||||
const specs = document.createElement('div'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('div'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = this.name;
|
||||
specs.append(name);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
help.textContent = this.enumValue.description;
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
return this.cmd.renderHelpDetails();
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ export class SlashCommandExecutor {
|
||||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];
|
||||
/**@type {SlashCommandUnnamedArgumentAssignment[]}*/ unnamedArgumentList = [];
|
||||
/**@type {Object<PARSER_FLAG,boolean>} */ parserFlags;
|
||||
/**@type {{[id:PARSER_FLAG]:boolean}} */ parserFlags;
|
||||
|
||||
get commandCount() {
|
||||
return 1
|
||||
|
@@ -27,22 +27,6 @@ export class SlashCommandNamedArgumentAutoCompleteOption extends AutoCompleteOpt
|
||||
|
||||
|
||||
renderDetails() {
|
||||
const frag = document.createDocumentFragment();
|
||||
const specs = document.createElement('div'); {
|
||||
specs.classList.add('specs');
|
||||
const name = document.createElement('div'); {
|
||||
name.classList.add('name');
|
||||
name.classList.add('monospace');
|
||||
name.textContent = this.name;
|
||||
specs.append(name);
|
||||
}
|
||||
frag.append(specs);
|
||||
}
|
||||
const help = document.createElement('span'); {
|
||||
help.classList.add('help');
|
||||
help.innerHTML = `${this.arg.isRequired ? '' : '(optional) '}${this.arg.description ?? ''}`;
|
||||
frag.append(help);
|
||||
}
|
||||
return frag;
|
||||
return this.cmd.renderHelpDetails();
|
||||
}
|
||||
}
|
||||
|
@@ -114,7 +114,6 @@ export class SlashCommandParser {
|
||||
|
||||
|
||||
constructor() {
|
||||
//TODO should not be re-registered from every instance
|
||||
// add dummy commands for help strings / autocomplete
|
||||
if (!Object.keys(this.commands).includes('parser-flag')) {
|
||||
const help = {};
|
||||
@@ -186,6 +185,15 @@ export class SlashCommandParser {
|
||||
relevance: 0,
|
||||
};
|
||||
|
||||
function getQuotedRunRegex() {
|
||||
try {
|
||||
return new RegExp('(".+?(?<!\\\\)")|(\\S+?)');
|
||||
} catch {
|
||||
// fallback for browsers that don't support lookbehind
|
||||
return /(".+?")|(\S+?)/;
|
||||
}
|
||||
}
|
||||
|
||||
const COMMENT = {
|
||||
scope: 'comment',
|
||||
begin: /\/[/#]/,
|
||||
@@ -225,7 +233,7 @@ export class SlashCommandParser {
|
||||
const RUN = {
|
||||
match: [
|
||||
/\/:/,
|
||||
/(".+?(?<!\\)") |(\S+?) /,
|
||||
getQuotedRunRegex(),
|
||||
],
|
||||
className: {
|
||||
1: 'variable.language',
|
||||
@@ -362,7 +370,6 @@ export class SlashCommandParser {
|
||||
;
|
||||
if (childClosure !== null) return null;
|
||||
const macro = this.macroIndex.findLast(it=>it.start <= index && it.end >= index);
|
||||
console.log(macro);
|
||||
if (macro) {
|
||||
const frag = document.createRange().createContextualFragment(await (await fetch('/scripts/templates/macros.html')).text());
|
||||
const options = [...frag.querySelectorAll('ul:nth-of-type(2n+1) > li')].map(li=>new MacroAutoCompleteOption(
|
||||
@@ -821,6 +828,7 @@ export class SlashCommandParser {
|
||||
assignment.start = this.index;
|
||||
value = '';
|
||||
}
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseClosure();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
|
@@ -174,7 +174,7 @@ async function* parseStreamData(json) {
|
||||
else if (Array.isArray(json.choices)) {
|
||||
const isNotPrimary = json?.choices?.[0]?.index > 0;
|
||||
if (isNotPrimary || json.choices.length === 0) {
|
||||
return null;
|
||||
throw new Error('Not a primary swipe');
|
||||
}
|
||||
|
||||
if (typeof json.choices[0].text === 'string' && json.choices[0].text.length > 0) {
|
||||
@@ -271,7 +271,7 @@ export class SmoothEventSourceStream extends EventSourceStream {
|
||||
hasFocus && await eventSource.emit(event_types.SMOOTH_STREAM_TOKEN_RECEIVED, parsed.chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Smooth Streaming parsing error', error);
|
||||
console.debug('Smooth Streaming parsing error', error);
|
||||
controller.enqueue(event);
|
||||
}
|
||||
},
|
||||
|
@@ -14,9 +14,12 @@ import {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
|
||||
|
||||
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
||||
import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
|
||||
export {
|
||||
TAG_FOLDER_TYPES,
|
||||
@@ -63,7 +66,7 @@ export const tag_filter_types = {
|
||||
const ACTIONABLE_TAGS = {
|
||||
FAV: { id: '1', sort_order: 1, name: 'Show only favorites', color: 'rgba(255, 255, 0, 0.5)', action: filterByFav, icon: 'fa-solid fa-star', class: 'filterByFavorites' },
|
||||
GROUP: { id: '0', sort_order: 2, name: 'Show only groups', color: 'rgba(100, 100, 100, 0.5)', action: filterByGroups, icon: 'fa-solid fa-users', class: 'filterByGroups' },
|
||||
FOLDER: { id: '4', sort_order: 3, name: 'Always show folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
|
||||
FOLDER: { id: '4', sort_order: 3, name: 'Show only folders', color: 'rgba(120, 120, 120, 0.5)', action: filterByFolder, icon: 'fa-solid fa-folder-plus', class: 'filterByFolder' },
|
||||
VIEW: { id: '2', sort_order: 4, name: 'Manage tags', color: 'rgba(150, 100, 100, 0.5)', action: onViewTagsListClick, icon: 'fa-solid fa-gear', class: 'manageTags' },
|
||||
HINT: { id: '3', sort_order: 5, name: 'Show Tag List', color: 'rgba(150, 100, 100, 0.5)', action: onTagListHintClick, icon: 'fa-solid fa-tags', class: 'showTagList' },
|
||||
UNFILTER: { id: '5', sort_order: 6, name: 'Clear all filters', action: onClearAllFiltersClick, icon: 'fa-solid fa-filter-circle-xmark', class: 'clearAllFilters' },
|
||||
@@ -174,9 +177,8 @@ function filterByTagState(entities, { globalDisplayFilters = false, subForEntity
|
||||
}
|
||||
|
||||
// Hide folders that have 0 visible sub entities after the first filtering round
|
||||
const alwaysFolder = isFilterState(entitiesFilter.getFilterData(FILTER_TYPES.FOLDER), FILTER_STATES.SELECTED);
|
||||
if (entity.type === 'tag') {
|
||||
return alwaysFolder || entity.entities.length > 0;
|
||||
return entity.entities.length > 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -322,6 +324,13 @@ function filterByGroups(filterHelper) {
|
||||
* @param {FilterHelper} filterHelper Instance of FilterHelper class.
|
||||
*/
|
||||
function filterByFolder(filterHelper) {
|
||||
if (!power_user.bogus_folders) {
|
||||
$('#bogus_folders').prop('checked', true).trigger('input');
|
||||
onViewTagsListClick();
|
||||
flashHighlight($('#dialogue_popup .tag_as_folder, #dialogue_popup .tag_folder_indicator'));
|
||||
return;
|
||||
}
|
||||
|
||||
const state = toggleTagThreeState($(this));
|
||||
ACTIONABLE_TAGS.FOLDER.filter_state = state;
|
||||
filterHelper.setFilterData(FILTER_TYPES.FOLDER, state);
|
||||
@@ -350,7 +359,7 @@ function createTagMapFromList(listElement, key) {
|
||||
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
|
||||
*
|
||||
* @param {string} key - The key for which to get tags via the tag map
|
||||
* @param {boolean} [sort=true] -
|
||||
* @param {boolean} [sort=true] - Whether the tag list should be sorted
|
||||
* @returns {Tag[]} A list of tags
|
||||
*/
|
||||
function getTagsList(key, sort = true) {
|
||||
@@ -456,35 +465,122 @@ export function getTagKeyForEntityElement(element) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag to a given entity
|
||||
* @param {Tag} tag - The tag to add
|
||||
* @param {string|string[]} entityId - The entity to add this tag to. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function)
|
||||
* @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
|
||||
* @returns {boolean} Whether at least one tag was added
|
||||
*/
|
||||
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
|
||||
let result = false;
|
||||
// Add tags to the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = addTagToMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
if (tagListSelector) printTagList(tagListSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given entity
|
||||
* @param {Tag} tag - The tag to remove
|
||||
* @param {string|string[]} entityId - The entity to remove this tag from. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the tag removed too (for example because the add was triggered for that function)
|
||||
* @param {JQuery<HTMLElement>?} [options.tagElement=null] - Optionally a direct html element of the tag to be removed, so it can be removed from the UI
|
||||
* @returns {boolean} Whether at least one tag was removed
|
||||
*/
|
||||
export function removeTagFromEntity(tag, entityId, { tagListSelector = null, tagElement = null } = {}) {
|
||||
let result = false;
|
||||
// Remove tag from the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = removeTagFromMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = removeTagFromMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We don't reprint the lists, we can just remove the html elements from them.
|
||||
if (tagListSelector) {
|
||||
const $selector = (typeof tagListSelector === 'string') ? $(tagListSelector) : tagListSelector;
|
||||
$selector.find(`.tag[id="${tag.id}"]`).remove();
|
||||
}
|
||||
if (tagElement) tagElement.remove();
|
||||
$(`${getInlineListSelector()} .tag[id="${tag.id}"]`).remove();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag from a given character. If no character is provided, adds it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was added or not
|
||||
*/
|
||||
function addTagToMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [tagId];
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (tag_map[key].includes(tagId))
|
||||
return false;
|
||||
|
||||
tag_map[key].push(tagId);
|
||||
tag_map[key] = tag_map[key].filter(onlyUnique);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given character. If no character is provided, removes it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was removed or not
|
||||
*/
|
||||
function removeTagFromMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [];
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
const indexOf = tag_map[key].indexOf(tagId);
|
||||
tag_map[key].splice(indexOf, 1);
|
||||
return indexOf !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,24 +624,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => addTagToMap(tag.id, characterId));
|
||||
} else {
|
||||
addTagToMap(tag.id);
|
||||
}
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
printTagList(listSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
|
||||
|
||||
// need to return false to keep the input clear
|
||||
return false;
|
||||
@@ -628,6 +707,7 @@ function createNewTag(tagName) {
|
||||
create_date: Date.now(),
|
||||
};
|
||||
tags.push(tag);
|
||||
console.debug('Created new tag', tag.name, 'with id', tag.id);
|
||||
return tag;
|
||||
}
|
||||
|
||||
@@ -883,8 +963,9 @@ function printTagFilters(type = tag_filter_types.character) {
|
||||
const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
|
||||
$(FILTER_SELECTOR).empty();
|
||||
|
||||
// Print all action tags. (Exclude folder if that setting isn't chosen)
|
||||
const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id);
|
||||
// Print all action tags. (Rework 'Folder' button to some kind of onboarding if no folders are enabled yet)
|
||||
const actionTags = Object.values(ACTIONABLE_TAGS);
|
||||
actionTags.find(x => x == ACTIONABLE_TAGS.FOLDER).name = power_user.bogus_folders ? 'Show only folders' : 'Enable \'Tags as Folder\'\n\nAllows characters to be grouped in folders by their assigned tags.\nTags have to be explicitly chosen as folder to show up.\n\nClick here to start';
|
||||
printTagList($(FILTER_SELECTOR), { empty: false, sort: false, tags: actionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
|
||||
|
||||
const inListActionTags = Object.values(InListActionable);
|
||||
@@ -924,8 +1005,8 @@ function updateTagFilterIndicator() {
|
||||
|
||||
function onTagRemoveClick(event) {
|
||||
event.stopPropagation();
|
||||
const tag = $(this).closest('.tag');
|
||||
const tagId = tag.attr('id');
|
||||
const tagElement = $(this).closest('.tag');
|
||||
const tagId = tagElement.attr('id');
|
||||
|
||||
// Check if we are inside the drilldown. If so, we call remove on the bogus folder
|
||||
if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) {
|
||||
@@ -934,24 +1015,13 @@ function onTagRemoveClick(event) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = tags.find(t => t.id === tagId);
|
||||
|
||||
// Optional, check for multiple character ids being present.
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
tag.remove();
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
|
||||
} else {
|
||||
removeTagFromMap(tagId);
|
||||
}
|
||||
|
||||
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
|
||||
removeTagFromEntity(tag, characterIds, { tagElement: tagElement });
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
@@ -977,7 +1047,7 @@ function onGroupCreateClick() {
|
||||
|
||||
export function applyTagsOnCharacterSelect() {
|
||||
//clearTagsFilter();
|
||||
const chid = Number($(this).attr('chid'));
|
||||
const chid = Number(this_chid);
|
||||
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
@@ -1453,14 +1523,200 @@ function printViewTagList(empty = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerTagsSlashCommands() {
|
||||
/**
|
||||
* Gets the key for char/group for a slash command. If none can be found, a toastr will be shown and null returned.
|
||||
* @param {string?} [charName] The optionally provided char name
|
||||
* @returns {string?} - The char/group key, or null if none found
|
||||
*/
|
||||
function paraGetCharKey(charName) {
|
||||
const entity = charName
|
||||
? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))
|
||||
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
|
||||
const key = getTagKeyForEntity(entity);
|
||||
if (!key) {
|
||||
toastr.warning(`Character ${charName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
/**
|
||||
* Gets a tag by its name. Optionally can create the tag if it does not exist.
|
||||
* @param {string} tagName - The name of the tag
|
||||
* @param {object} options - Optional arguments
|
||||
* @param {boolean} [options.allowCreate=false] - Whether a new tag should be created if no tag with the name exists
|
||||
* @returns {Tag?} The tag, or null if not found
|
||||
*/
|
||||
function paraGetTag(tagName, { allowCreate = false } = {}) {
|
||||
if (!tagName) {
|
||||
toastr.warning('Tag name must be provided.');
|
||||
return null;
|
||||
}
|
||||
let tag = tags.find(t => t.name === tagName);
|
||||
if (allowCreate && !tag) {
|
||||
tag = createNewTag(tagName);
|
||||
}
|
||||
if (!tag) {
|
||||
toastr.warning(`Tag ${tagName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
function updateTagsList() {
|
||||
switch (menu_type) {
|
||||
case 'characters':
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
break;
|
||||
case 'character_edit':
|
||||
applyTagsOnCharacterSelect();
|
||||
break;
|
||||
case 'group_edit':
|
||||
select_group_chats(selected_group, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-add',
|
||||
returns: 'true/false - Whether the tag was added or was assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName, { allowCreate: true });
|
||||
if (!tag) return 'false';
|
||||
const result = addTagToEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Adds a tag to the character. If no character is provided, it adds it to the current character (<code>{{char}}</code>).
|
||||
If the tag doesn't exist, it is created.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-add name="Chloe" scenario</code></pre>
|
||||
will add the tag "scenario" to the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-remove',
|
||||
returns: 'true/false - Whether the tag was removed or wasn\'t assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
const result = removeTagFromEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Removes a tag from the character. If no character is provided, it removes it from the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-remove name="Chloe" scenario</code></pre>
|
||||
will remove the tag "scenario" from the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-exists',
|
||||
returns: 'true/false - Whether the given tag name is assigned to the character',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
return String(tag_map[key].includes(tag.id));
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Checks whether the given tag is assigned to the character. If no character is provided, it checks the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-exists name="Chloe" scenario</code></pre>
|
||||
will return true if the character named Chloe has the tag "scenario".
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-list',
|
||||
returns: 'Comma-separated list of all assigned tags',
|
||||
/** @param {{name: string}} namedArgs @returns {string} */
|
||||
callback: ({ name }) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return '';
|
||||
const tags = getTagsList(key);
|
||||
return tags.map(x => x.name).join(', ');
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Lists all assigned tags of the character. If no character is provided, it uses the current character (<code>{{char}}</code>).
|
||||
<br />
|
||||
Note that there is no special handling for tags containing commas, they will be printed as-is.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-list name="Chloe"</code></pre>
|
||||
could return something like <code>OC, scenario, edited, funny</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
}
|
||||
|
||||
export function initTags() {
|
||||
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
|
||||
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
|
||||
|
||||
$(document).on('click', '#rm_button_create', onCharacterCreateClick);
|
||||
$(document).on('click', '#rm_button_group_chats', onGroupCreateClick);
|
||||
$(document).on('click', '.character_select', applyTagsOnCharacterSelect);
|
||||
$(document).on('click', '.group_select', applyTagsOnGroupSelect);
|
||||
$(document).on('click', '.tag_remove', onTagRemoveClick);
|
||||
$(document).on('input', '.tag_input', onTagInput);
|
||||
$(document).on('click', '.tags_view', onViewTagsListClick);
|
||||
@@ -1471,6 +1727,7 @@ export function initTags() {
|
||||
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
|
||||
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
|
||||
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
||||
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
|
||||
|
||||
$(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => {
|
||||
const toggle = $(evt.target).is(':checked');
|
||||
@@ -1498,4 +1755,6 @@ export function initTags() {
|
||||
printCharactersDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
registerTagsSlashCommands();
|
||||
}
|
||||
|
@@ -11,9 +11,10 @@
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-plug"></i></code><span data-i18n="and select a"> and select a </span><a href="https://docs.sillytavern.app/usage/api-connections/" target="_blank" data-i18n="Chat API">Chat API</a>.</span>
|
||||
</li>
|
||||
<li>
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character"> and pick a character</span>
|
||||
<span data-i18n="Click ">Click </span><code><i class="fa-solid fa-address-card"></i></code><span data-i18n="and pick a character."> and pick a character.</span>
|
||||
</li>
|
||||
</ol>
|
||||
<span data-i18n="You can browse a list of bundled characters in the Download Extensions & Assets menu within">You can browse a list of bundled characters in the <i>Download Extensions & Assets</i> menu within </span><code><i class="fa-solid fa-cubes"></i></code><span>.</span>
|
||||
<hr>
|
||||
<h3 data-i18n="Confused or lost?">Confused or lost?</h3>
|
||||
<ul>
|
||||
|
@@ -237,7 +237,7 @@ function onMancerModelSelect() {
|
||||
$('#api_button_textgenerationwebui').trigger('click');
|
||||
|
||||
const limits = mancerModels.find(x => x.id === modelId)?.limits;
|
||||
setGenerationParamsFromPreset({ max_length: limits.context, genamt: limits.completion }, true);
|
||||
setGenerationParamsFromPreset({ max_length: limits.context });
|
||||
}
|
||||
|
||||
function onTogetherModelSelect() {
|
||||
|
@@ -166,7 +166,7 @@ export let textgenerationwebui_banned_in_macros = [];
|
||||
export let textgenerationwebui_presets = [];
|
||||
export let textgenerationwebui_preset_names = [];
|
||||
|
||||
const setting_names = [
|
||||
export const setting_names = [
|
||||
'temp',
|
||||
'temperature_last',
|
||||
'rep_pen',
|
||||
@@ -659,7 +659,7 @@ jQuery(function () {
|
||||
'no_repeat_ngram_size_textgenerationwebui': 0,
|
||||
'min_length_textgenerationwebui': 0,
|
||||
'num_beams_textgenerationwebui': 1,
|
||||
'length_penalty_textgenerationwebui': 0,
|
||||
'length_penalty_textgenerationwebui': 1,
|
||||
'penalty_alpha_textgenerationwebui': 0,
|
||||
'typical_p_textgenerationwebui': 1, // Added entry
|
||||
'guidance_scale_textgenerationwebui': 1,
|
||||
@@ -991,7 +991,7 @@ export function getTextGenModel() {
|
||||
}
|
||||
|
||||
export function isJsonSchemaSupported() {
|
||||
return settings.type === TABBY && main_api === 'textgenerationwebui';
|
||||
return [TABBY, LLAMACPP].includes(settings.type) && main_api === 'textgenerationwebui';
|
||||
}
|
||||
|
||||
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
|
||||
@@ -1065,7 +1065,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
|
||||
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
|
||||
'grammar_string': settings.grammar_string,
|
||||
'json_schema': settings.type === TABBY ? settings.json_schema : undefined,
|
||||
'json_schema': [TABBY, LLAMACPP].includes(settings.type) ? settings.json_schema : undefined,
|
||||
// llama.cpp aliases. In case someone wants to use LM Studio as Text Completion API
|
||||
'repeat_penalty': settings.rep_pen,
|
||||
'tfs_z': settings.tfs,
|
||||
@@ -1150,5 +1150,15 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
|
||||
eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
|
||||
|
||||
// Grammar conflicts with with json_schema
|
||||
if (settings.type === LLAMACPP) {
|
||||
if (params.json_schema && Object.keys(params.json_schema).length > 0) {
|
||||
delete params.grammar_string;
|
||||
delete params.grammar;
|
||||
} else {
|
||||
delete params.json_schema;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
@@ -732,6 +732,24 @@ export function isDataURL(str) {
|
||||
return regex.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of an image from a data URL.
|
||||
* @param {string} dataUrl Image data URL
|
||||
* @returns {Promise<{ width: number, height: number }>} Image size
|
||||
*/
|
||||
export function getImageSizeFromDataURL(dataUrl) {
|
||||
const image = new Image();
|
||||
image.src = dataUrl;
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = function () {
|
||||
resolve({ width: image.width, height: image.height });
|
||||
};
|
||||
image.onerror = function () {
|
||||
reject(new Error('Failed to load image'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getCharaFilename(chid) {
|
||||
const context = getContext();
|
||||
const fileName = context.characters[chid ?? context.characterId].avatar;
|
||||
@@ -773,6 +791,29 @@ export function escapeRegex(string) {
|
||||
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a regular expression from a string.
|
||||
* @param {string} input The input string.
|
||||
* @returns {RegExp} The regular expression instance.
|
||||
* @copyright Originally from: https://github.com/IonicaBizau/regex-parser.js/blob/master/lib/index.js
|
||||
*/
|
||||
export function regexFromString(input) {
|
||||
try {
|
||||
// Parse input
|
||||
var m = input.match(/(\/?)(.+)\1([a-z]*)/i);
|
||||
|
||||
// Invalid flags
|
||||
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3])) {
|
||||
return RegExp(input);
|
||||
}
|
||||
|
||||
// Create the regular expression
|
||||
return new RegExp(m[2], m[3]);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export class Stopwatch {
|
||||
/**
|
||||
* Initializes a Stopwatch class.
|
||||
@@ -1449,3 +1490,166 @@ export function includesIgnoreCaseAndAccents(text, searchTerm) {
|
||||
// Check if the normalized text includes the normalized search term
|
||||
return normalizedText.includes(normalizedSearchTerm);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Select2Option The option object for select2 controls
|
||||
* @property {string} id - The unique ID inside this select
|
||||
* @property {string} text - The text for this option
|
||||
* @property {number?} [count] - Optionally show the count how often that option was chosen already
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a unique hash as ID for a select2 option text
|
||||
*
|
||||
* @param {string} option - The option
|
||||
* @returns {string} A hashed version of that option
|
||||
*/
|
||||
export function getSelect2OptionId(option) {
|
||||
return String(getStringHash(option));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the select2 options by adding not existing one and optionally selecting them
|
||||
*
|
||||
* @param {JQuery<HTMLElement>} element - The "select" element to add the options to
|
||||
* @param {string[]|Select2Option[]} items - The option items to build, add or select
|
||||
* @param {object} [options] - Optional arguments
|
||||
* @param {boolean} [options.select=false] - Whether the options should be selected right away
|
||||
* @param {object} [options.changeEventArgs=null] - Optional event args being passed into the "change" event when its triggered because a new options is selected
|
||||
*/
|
||||
export function select2ModifyOptions(element, items, { select = false, changeEventArgs = null } = {}) {
|
||||
if (!items.length) return;
|
||||
/** @type {Select2Option[]} */
|
||||
const dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||
|
||||
const existingValues = [];
|
||||
dataItems.forEach(item => {
|
||||
// Set the value, creating a new option if necessary
|
||||
if (element.find('option[value=\'' + item.id + '\']').length) {
|
||||
if (select) existingValues.push(item.id);
|
||||
} else {
|
||||
// Create a DOM Option and optionally pre-select by default
|
||||
var newOption = new Option(item.text, item.id, select, select);
|
||||
// Append it to the select
|
||||
element.append(newOption);
|
||||
if (select) element.trigger('change', changeEventArgs);
|
||||
}
|
||||
if (existingValues.length) element.val(existingValues).trigger('change', changeEventArgs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ajax settings that can be used on the select2 ajax property to dynamically get the data.
|
||||
* Can be used on a single global array, querying data from the server or anything similar.
|
||||
*
|
||||
* @param {function():Select2Option[]} dataProvider - The provider/function to retrieve the data - can be as simple as "() => myData" for arrays
|
||||
* @return {{transport: (params, success, failure) => any}} The ajax object with the transport function to use on the select2 ajax property
|
||||
*/
|
||||
export function dynamicSelect2DataViaAjax(dataProvider) {
|
||||
function dynamicSelect2DataTransport(params, success, failure) {
|
||||
var items = dataProvider();
|
||||
// fitering if params.data.q available
|
||||
if (params.data && params.data.q) {
|
||||
items = items.filter(function (item) {
|
||||
return includesIgnoreCaseAndAccents(item.text, params.data.q);
|
||||
});
|
||||
}
|
||||
var promise = new Promise(function (resolve, reject) {
|
||||
resolve({ results: items });
|
||||
});
|
||||
promise.then(success);
|
||||
promise.catch(failure);
|
||||
}
|
||||
const ajax = {
|
||||
transport: dynamicSelect2DataTransport,
|
||||
};
|
||||
return ajax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given control is a select2 choice element - meaning one of the results being displayed in the select multi select box
|
||||
* @param {JQuery<HTMLElement>|HTMLElement} element - The element to check
|
||||
* @returns {boolean} Whether this is a choice element
|
||||
*/
|
||||
export function isSelect2ChoiceElement(element) {
|
||||
const $element = $(element);
|
||||
return ($element.hasClass('select2-selection__choice__display') || $element.parents('.select2-selection__choice__display').length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes a 'click' event handler to the choice elements of a select2 multi-select control
|
||||
*
|
||||
* @param {JQuery<HTMLElement>} control The original control the select2 was applied to
|
||||
* @param {function(HTMLElement):void} action - The action to execute when a choice element is clicked
|
||||
* @param {object} options - Optional parameters
|
||||
* @param {boolean} [options.buttonStyle=false] - Whether the choices should be styles as a clickable button with color and hover transition, instead of just changed cursor
|
||||
* @param {boolean} [options.closeDrawer=false] - Whether the drawer should be closed and focus removed after the choice item was clicked
|
||||
* @param {boolean} [options.openDrawer=false] - Whether the drawer should be opened, even if this click would normally close it
|
||||
*/
|
||||
export function select2ChoiceClickSubscribe(control, action, { buttonStyle = false, closeDrawer = false, openDrawer = false } = {}) {
|
||||
// Add class for styling (hover color, changed cursor, etc)
|
||||
control.addClass('select2_choice_clickable');
|
||||
if (buttonStyle) control.addClass('select2_choice_clickable_buttonstyle');
|
||||
|
||||
// Get the real container below and create a click handler on that one
|
||||
const select2Container = control.next('span.select2-container');
|
||||
select2Container.on('click', function (event) {
|
||||
const isChoice = isSelect2ChoiceElement(event.target);
|
||||
if (isChoice) {
|
||||
event.preventDefault();
|
||||
|
||||
// select2 still bubbles the event to open the dropdown. So we close it here and remove focus if we want that
|
||||
if (closeDrawer) {
|
||||
control.select2('close');
|
||||
setTimeout(() => select2Container.find('textarea').trigger('blur'), debounce_timeout.quick);
|
||||
}
|
||||
if (openDrawer) {
|
||||
control.select2('open');
|
||||
}
|
||||
|
||||
// Now execute the actual action that was subscribed
|
||||
action(event.target);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies syntax highlighting to a given regex string by generating HTML with classes
|
||||
*
|
||||
* @param {string} regexStr - The javascript compatible regex string
|
||||
* @returns {string} The html representation of the highlighted regex
|
||||
*/
|
||||
export function highlightRegex(regexStr) {
|
||||
// Function to escape HTML special characters for safety
|
||||
const escapeHtml = (str) => str.replace(/[&<>"']/g, match => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', '\'': ''',
|
||||
})[match]);
|
||||
|
||||
// Replace special characters with their HTML-escaped forms
|
||||
regexStr = escapeHtml(regexStr);
|
||||
|
||||
// Patterns that we want to highlight only if they are not escaped
|
||||
const patterns = {
|
||||
brackets: /(?<!\\)\[.*?\]/g, // Non-escaped squary brackets
|
||||
quantifiers: /(?<!\\)[*+?{}]/g, // Non-escaped quantifiers
|
||||
operators: /(?<!\\)[|.^$()]/g, // Non-escaped operators like | and ()
|
||||
specialChars: /\\./g,
|
||||
flags: /(?<=\/)([gimsuy]*)$/g, // Match trailing flags
|
||||
delimiters: /^\/|(?<![\\<])\//g, // Match leading or trailing delimiters
|
||||
};
|
||||
|
||||
// Function to replace each pattern with a highlighted HTML span
|
||||
const wrapPattern = (pattern, className) => {
|
||||
regexStr = regexStr.replace(pattern, match => `<span class="${className}">${match}</span>`);
|
||||
};
|
||||
|
||||
// Apply highlighting patterns
|
||||
wrapPattern(patterns.brackets, 'regex-brackets');
|
||||
wrapPattern(patterns.quantifiers, 'regex-quantifier');
|
||||
wrapPattern(patterns.operators, 'regex-operator');
|
||||
wrapPattern(patterns.specialChars, 'regex-special');
|
||||
wrapPattern(patterns.flags, 'regex-flags');
|
||||
wrapPattern(patterns.delimiters, 'regex-delimiter');
|
||||
|
||||
return `<span class="regex-highlight">${regexStr}</span>`;
|
||||
}
|
||||
|
@@ -1,11 +1,13 @@
|
||||
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
|
||||
import { extension_settings, saveMetadataDebounced } from './extensions.js';
|
||||
import { executeSlashCommands } from './slash-commands.js';
|
||||
import { executeSlashCommands, executeSlashCommandsWithOptions } from './slash-commands.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
|
||||
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||
import { isFalseBoolean } from './utils.js';
|
||||
|
||||
@@ -314,61 +316,117 @@ function listVariablesCallback() {
|
||||
sendSystemMessage(system_message_types.GENERIC, htmlMessage);
|
||||
}
|
||||
|
||||
async function whileCallback(args, command) {
|
||||
/**
|
||||
*
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args
|
||||
* @param {(string|SlashCommandClosure)[]} value
|
||||
*/
|
||||
async function whileCallback(args, value) {
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
|
||||
/**@type {string|SlashCommandClosure} */
|
||||
let command;
|
||||
if (value) {
|
||||
if (value[0] instanceof SlashCommandClosure) {
|
||||
command = value[0];
|
||||
} else {
|
||||
command = value.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
let commandResult;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const { a, b, rule } = parseBooleanOperands(args);
|
||||
const result = evalBoolean(rule, a, b);
|
||||
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) await command.execute();
|
||||
else await executeSubCommands(command, args._scope, args._parserFlags);
|
||||
if (command instanceof SlashCommandClosure) {
|
||||
commandResult = await command.execute();
|
||||
} else {
|
||||
commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
|
||||
}
|
||||
if (commandResult.isAborted) break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (commandResult) {
|
||||
return commandResult.pipe;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args
|
||||
* @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value
|
||||
* @returns
|
||||
*/
|
||||
async function timesCallback(args, value) {
|
||||
let repeats;
|
||||
let command;
|
||||
if (Array.isArray(value)) {
|
||||
[repeats, command] = value;
|
||||
[repeats, ...command] = value;
|
||||
if (command[0] instanceof SlashCommandClosure) {
|
||||
command = command[0];
|
||||
} else {
|
||||
command = command.join(' ');
|
||||
}
|
||||
} else {
|
||||
[repeats, ...command] = value.split(' ');
|
||||
[repeats, ...command] = /**@type {string}*/(value).split(' ');
|
||||
command = command.join(' ');
|
||||
}
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
|
||||
let result;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
/**@type {SlashCommandClosureResult}*/
|
||||
if (command instanceof SlashCommandClosure) {
|
||||
command.scope.setMacro('timesIndex', i);
|
||||
await command.execute();
|
||||
result = await command.execute();
|
||||
}
|
||||
else {
|
||||
await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i), args._scope, args._parserFlags);
|
||||
result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController);
|
||||
}
|
||||
if (result.isAborted) break;
|
||||
}
|
||||
|
||||
return '';
|
||||
return result?.pipe ?? '';
|
||||
}
|
||||
|
||||
async function ifCallback(args, command) {
|
||||
/**
|
||||
*
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args
|
||||
* @param {(string|SlashCommandClosure)[]} value
|
||||
*/
|
||||
async function ifCallback(args, value) {
|
||||
const { a, b, rule } = parseBooleanOperands(args);
|
||||
const result = evalBoolean(rule, a, b);
|
||||
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) return (await command.execute()).pipe;
|
||||
return await executeSubCommands(command, args._scope, args._parserFlags);
|
||||
} else if (!result && args.else && ((typeof args.else === 'string' && args.else !== '') || args.else instanceof SlashCommandClosure)) {
|
||||
if (args.else instanceof SlashCommandClosure) return (await args.else.execute(args._scope)).pipe;
|
||||
return await executeSubCommands(args.else, args._scope, args._parserFlags);
|
||||
/**@type {string|SlashCommandClosure} */
|
||||
let command;
|
||||
if (value) {
|
||||
if (value[0] instanceof SlashCommandClosure) {
|
||||
command = value[0];
|
||||
} else {
|
||||
command = value.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
let commandResult;
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) return (await command.execute()).pipe;
|
||||
commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
|
||||
} else if (!result && args.else && ((typeof args.else === 'string' && args.else !== '') || args.else instanceof SlashCommandClosure)) {
|
||||
if (args.else instanceof SlashCommandClosure) return (await args.else.execute()).pipe;
|
||||
commandResult = await executeSubCommands(args.else, args._scope, args._parserFlags, args._abortController);
|
||||
}
|
||||
|
||||
if (commandResult) {
|
||||
return commandResult.pipe;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -511,20 +569,25 @@ function evalBoolean(rule, a, b) {
|
||||
/**
|
||||
* Executes a slash command from a string (may be enclosed in quotes) and returns the result.
|
||||
* @param {string} command Command to execute. May contain escaped macro and batch separators.
|
||||
* @returns {Promise<string>} Pipe result
|
||||
* @param {SlashCommandScope} [scope] The scope to use.
|
||||
* @param {{[id:PARSER_FLAG]:boolean}} [parserFlags] The parser flags to use.
|
||||
* @param {SlashCommandAbortController} [abortController] The abort controller to use.
|
||||
* @returns {Promise<SlashCommandClosureResult>} Closure execution result
|
||||
*/
|
||||
async function executeSubCommands(command, scope = null, parserFlags = null) {
|
||||
async function executeSubCommands(command, scope = null, parserFlags = null, abortController = null) {
|
||||
if (command.startsWith('"') && command.endsWith('"')) {
|
||||
command = command.slice(1, -1);
|
||||
}
|
||||
|
||||
const result = await executeSlashCommands(command, true, scope, true, parserFlags);
|
||||
const result = await executeSlashCommandsWithOptions(command, {
|
||||
handleExecutionErrors: false,
|
||||
handleParserErrors: false,
|
||||
parserFlags,
|
||||
scope,
|
||||
abortController: abortController ?? new SlashCommandAbortController(),
|
||||
});
|
||||
|
||||
if (!result || typeof result !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return result?.pipe || '';
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1066,6 +1129,7 @@ export function registerVariableCommands() {
|
||||
'command to execute if true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Compares the value of the left operand <code>a</code> with the value of the right operand <code>b</code>,
|
||||
@@ -1132,6 +1196,7 @@ export function registerVariableCommands() {
|
||||
'command to execute while true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Compares the value of the left operand <code>a</code> with the value of the right operand <code>b</code>,
|
||||
@@ -1158,7 +1223,7 @@ export function registerVariableCommands() {
|
||||
<strong>Examples:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=let "/addvar key=i 1"</code></pre>
|
||||
<pre><code class="language-stscript">/setvar key=i 0 | /while left=i right=10 rule=lte "/addvar key=i 1"</code></pre>
|
||||
adds 1 to the value of "i" until it reaches 10.
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1184,6 +1249,7 @@ export function registerVariableCommands() {
|
||||
true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Execute any valid slash command enclosed in quotes <code>repeats</code> number of times.
|
||||
@@ -1592,7 +1658,7 @@ export function registerVariableCommands() {
|
||||
returns: 'length of the provided value',
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], true
|
||||
'value', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], true,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight } from './utils.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean } from './utils.js';
|
||||
import { extension_settings, getContext } from './extensions.js';
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
@@ -69,7 +69,7 @@ const saveSettingsDebounced = debounce(() => {
|
||||
saveSettings();
|
||||
}, debounce_timeout.relaxed);
|
||||
const sortFn = (a, b) => b.order - a.order;
|
||||
let updateEditor = (navigation) => { console.debug('Triggered WI navigation', navigation); };
|
||||
let updateEditor = (navigation, flashOnNav = true) => { console.debug('Triggered WI navigation', navigation, flashOnNav); };
|
||||
|
||||
// Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.
|
||||
const worldInfoFilter = new FilterHelper(() => updateEditor());
|
||||
@@ -197,6 +197,13 @@ class WorldInfoBuffer {
|
||||
* @returns {boolean} True if the string was found in the buffer
|
||||
*/
|
||||
matchKeys(haystack, needle, entry) {
|
||||
// If the needle is a regex, we do regex pattern matching and override all the other options
|
||||
const keyRegex = parseRegexFromString(needle);
|
||||
if (keyRegex) {
|
||||
return keyRegex.test(haystack);
|
||||
}
|
||||
|
||||
// Otherwise we do normal matching of plaintext with the chosen entry settings
|
||||
const transformedString = this.#transformString(needle, entry);
|
||||
const matchWholeWords = entry.matchWholeWords ?? world_info_match_whole_words;
|
||||
|
||||
@@ -541,6 +548,19 @@ function registerWorldInfoSlashCommands() {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof newEntryTemplate[field] === 'boolean') {
|
||||
const isTrue = isTrueBoolean(value);
|
||||
const isFalse = isFalseBoolean(value);
|
||||
|
||||
if (isTrue) {
|
||||
value = String(true);
|
||||
}
|
||||
|
||||
if (isFalse) {
|
||||
value = String(false);
|
||||
}
|
||||
}
|
||||
|
||||
const fuse = new Fuse(entries, {
|
||||
keys: [{ name: field, weight: 1 }],
|
||||
includeScore: true,
|
||||
@@ -984,8 +1004,39 @@ function nullWorldInfo() {
|
||||
toastr.info('Create or import a new World Info file first.', 'World Info is not set', { timeOut: 10000, preventDuplicates: true });
|
||||
}
|
||||
|
||||
function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
|
||||
/** @type {Select2Option[]} Cache all keys as selectable dropdown option */
|
||||
const worldEntryKeyOptionsCache = [];
|
||||
|
||||
/**
|
||||
* Update the cache and all select options for the keys with new values to display
|
||||
* @param {string[]|Select2Option[]} keyOptions - An array of options to update
|
||||
* @param {object} options - Optional arguments
|
||||
* @param {boolean?} [options.remove=false] - Whether the option was removed, so the count should be reduced - otherwise it'll be increased
|
||||
* @param {boolean?} [options.reset=false] - Whether the cache should be reset. Reset will also not trigger update of the controls, as we expect them to be redrawn anyway
|
||||
*/
|
||||
function updateWorldEntryKeyOptionsCache(keyOptions, { remove = false, reset = false } = {}) {
|
||||
if (!keyOptions.length) return;
|
||||
/** @type {Select2Option[]} */
|
||||
const options = keyOptions.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||
if (reset) worldEntryKeyOptionsCache.length = 0;
|
||||
options.forEach(option => {
|
||||
// Update the cache list
|
||||
let cachedEntry = worldEntryKeyOptionsCache.find(x => x.id == option.id);
|
||||
if (cachedEntry) {
|
||||
cachedEntry.count += !remove ? 1 : -1;
|
||||
} else if (!remove) {
|
||||
worldEntryKeyOptionsCache.push(option);
|
||||
cachedEntry = option;
|
||||
cachedEntry.count = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by count DESC and then alphabetically
|
||||
worldEntryKeyOptionsCache.sort((a, b) => b.count - a.count || a.text.localeCompare(b.text));
|
||||
}
|
||||
|
||||
function displayWorldEntries(name, data, navigation = navigation_option.none, flashOnNav = true) {
|
||||
updateEditor = (navigation, flashOnNav = true) => displayWorldEntries(name, data, navigation, flashOnNav);
|
||||
|
||||
const worldEntriesList = $('#world_popup_entries_list');
|
||||
|
||||
@@ -1020,6 +1071,10 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||||
entriesArray = sortEntries(entriesArray);
|
||||
|
||||
// Cache keys
|
||||
const keys = entriesArray.flatMap(entry => [...entry.key, ...entry.keysecondary]);
|
||||
updateWorldEntryKeyOptionsCache(keys, { reset: true });
|
||||
|
||||
// Run the callback for printing this
|
||||
typeof callback === 'function' && callback(entriesArray);
|
||||
return entriesArray;
|
||||
@@ -1036,7 +1091,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
$('#world_info_pagination').pagination({
|
||||
dataSource: getDataArray,
|
||||
pageSize: Number(localStorage.getItem(storageKey)) || perPageDefault,
|
||||
sizeChangerOptions: [10, 25, 50, 100],
|
||||
sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
|
||||
showSizeChanger: true,
|
||||
pageRange: 1,
|
||||
pageNumber: startPage,
|
||||
@@ -1114,7 +1169,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
const parentOffset = element.parent().offset();
|
||||
const scrollOffset = elementOffset.top - parentOffset.top;
|
||||
$('#WorldInfo').scrollTop(scrollOffset);
|
||||
flashHighlight(element);
|
||||
if (flashOnNav) flashHighlight(element);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1202,9 +1257,10 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
}
|
||||
|
||||
worldEntriesList.sortable({
|
||||
items: '.world_entry',
|
||||
delay: getSortableDelay(),
|
||||
handle: '.drag-handle',
|
||||
stop: async function (event, ui) {
|
||||
stop: async function (_event, _ui) {
|
||||
const firstEntryUid = $('#world_popup_entries_list .world_entry').first().data('uid');
|
||||
const minDisplayIndex = data?.entries[firstEntryUid]?.displayIndex ?? 0;
|
||||
$('#world_popup_entries_list .world_entry').each(function (index) {
|
||||
@@ -1234,6 +1290,7 @@ const originalDataKeyMap = {
|
||||
'displayIndex': 'extensions.display_index',
|
||||
'excludeRecursion': 'extensions.exclude_recursion',
|
||||
'preventRecursion': 'extensions.prevent_recursion',
|
||||
'delayUntilRecursion': 'extensions.delay_until_recursion',
|
||||
'selectiveLogic': 'selectiveLogic',
|
||||
'comment': 'comment',
|
||||
'constant': 'constant',
|
||||
@@ -1299,6 +1356,139 @@ function deleteOriginalDataValue(data, uid) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {import('./utils.js').Select2Option} Select2Option */
|
||||
|
||||
/**
|
||||
* Splits a given input string that contains one or more keywords or regexes, separated by commas.
|
||||
*
|
||||
* Each part can be a valid regex following the pattern `/myregex/flags` with optional flags. Commmas inside the regex are allowed, slashes have to be escaped like this: `\/`
|
||||
* If a regex doesn't stand alone, it is not treated as a regex.
|
||||
*
|
||||
* @param {string} input - One or multiple keywords or regexes, separated by commas
|
||||
* @returns {string[]} An array of keywords and regexes
|
||||
*/
|
||||
function splitKeywordsAndRegexes(input) {
|
||||
/** @type {string[]} */
|
||||
let keywordsAndRegexes = [];
|
||||
|
||||
// We can make this easy. Instead of writing another function to find and parse regexes,
|
||||
// we gonna utilize the custom tokenizer that also handles the input.
|
||||
// No need for validation here
|
||||
const addFindCallback = (/** @type {Select2Option} */ item) => {
|
||||
keywordsAndRegexes.push(item.text);
|
||||
};
|
||||
|
||||
const { term } = customTokenizer({ _type: 'custom_call', term: input }, undefined, addFindCallback);
|
||||
const finalTerm = term.trim();
|
||||
if (finalTerm) {
|
||||
addFindCallback({ id: getSelect2OptionId(finalTerm), text: finalTerm });
|
||||
}
|
||||
|
||||
return keywordsAndRegexes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tokenizer parsing input and splitting it into keywords and regexes
|
||||
*
|
||||
* @param {{_type: string, term: string}} input - The typed input
|
||||
* @param {{options: object}} _selection - The selection even object (?)
|
||||
* @param {function(Select2Option):void} callback - The original callback function to call if an item should be inserted
|
||||
* @returns {{term: string}} - The remaining part that is untokenized in the textbox
|
||||
*/
|
||||
function customTokenizer(input, _selection, callback) {
|
||||
let current = input.term;
|
||||
|
||||
// Go over the input and check the current state, if we can get a token
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
let char = current[i];
|
||||
|
||||
// If a comma is typed, we tokenize the input.
|
||||
// unless we are inside a possible regex, which would allow commas inside
|
||||
if (char === ',') {
|
||||
// We take everything up till now and consider this a token
|
||||
const token = current.slice(0, i).trim();
|
||||
|
||||
// Now how we test if this is a valid regex? And not a finished one, but a half-finished one?
|
||||
// Easy, if someone typed a comma it can't be a delimiter escape.
|
||||
// So we just check if this opening with a slash, and if so, we "close" the regex and try to parse it.
|
||||
// So if we are inside a valid regex, we can't take the token now, we continue processing until the regex is closed,
|
||||
// or this is not a valid regex anymore
|
||||
if (token.startsWith('/') && isValidRegex(token + '/')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// So now the comma really means the token is done.
|
||||
// We take the token up till now, and insert it. Empty will be skipped.
|
||||
if (token) {
|
||||
const isRegex = isValidRegex(token);
|
||||
|
||||
// Last chance to check for valid regex again. Because it might have been valid while typing, but now is not valid anymore and contains commas we need to split.
|
||||
if (token.startsWith('/') && !isRegex) {
|
||||
const tokens = token.split(',').map(x => x.trim());
|
||||
tokens.forEach(x => callback({ id: getSelect2OptionId(x), text: x }));
|
||||
} else {
|
||||
callback({ id: getSelect2OptionId(token), text: token });
|
||||
}
|
||||
}
|
||||
|
||||
// Now remove the token from the current input, and the comma too
|
||||
current = current.slice(i + 1);
|
||||
i = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// At the end, just return the left-over input
|
||||
return { term: current };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string is a valid slash-delimited regex, that can be parsed and executed
|
||||
*
|
||||
* This is a wrapper around `parseRegexFromString`
|
||||
*
|
||||
* @param {string} input - A delimited regex string
|
||||
* @returns {boolean} Whether this would be a valid regex that can be parsed and executed
|
||||
*/
|
||||
function isValidRegex(input) {
|
||||
return parseRegexFromString(input) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a real regex object from a slash-delimited regex string
|
||||
*
|
||||
* This function works with `/` as delimiter, and each occurance of it inside the regex has to be escaped.
|
||||
* Flags are optional, but can only be valid flags supported by JavaScript's `RegExp` (`g`, `i`, `m`, `s`, `u`, `y`).
|
||||
*
|
||||
* @param {string} input - A delimited regex string
|
||||
* @returns {RegExp|null} The regex object, or null if not a valid regex
|
||||
*/
|
||||
function parseRegexFromString(input) {
|
||||
// Extracting the regex pattern and flags
|
||||
let match = input.match(/^\/([\w\W]+?)\/([gimsuy]*)$/);
|
||||
if (!match) {
|
||||
return null; // Not a valid regex format
|
||||
}
|
||||
|
||||
let [, pattern, flags] = match;
|
||||
|
||||
// If we find any unescaped slash delimiter, we also exit out.
|
||||
// JS doesn't care about delimiters inside regex patterns, but for this to be a valid regex outside of our implementation,
|
||||
// we have to make sure that our delimiter is correctly escaped. Or every other engine would fail.
|
||||
if (pattern.match(/(^|[^\\])\//)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Now we need to actually unescape the slash delimiters, because JS doesn't care about delimiters
|
||||
pattern = pattern.replace('\\/', '/');
|
||||
|
||||
// Then we return the regex. If it fails, it was invalid syntax.
|
||||
try {
|
||||
return new RegExp(pattern, flags);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getWorldEntry(name, data, entry) {
|
||||
if (!data.entries[entry.uid]) {
|
||||
return;
|
||||
@@ -1308,28 +1498,125 @@ function getWorldEntry(name, data, entry) {
|
||||
template.data('uid', entry.uid);
|
||||
template.attr('uid', entry.uid);
|
||||
|
||||
// Init default state of WI Key toggle (=> true)
|
||||
if (typeof power_user.wi_key_input_plaintext === 'undefined') power_user.wi_key_input_plaintext = true;
|
||||
|
||||
/** Function to build the keys input controls @param {string} entryPropName @param {string} originalDataValueName */
|
||||
function enableKeysInput(entryPropName, originalDataValueName) {
|
||||
const isFancyInput = !isMobile() && !power_user.wi_key_input_plaintext;
|
||||
const input = isFancyInput ? template.find(`select[name="${entryPropName}"]`) : template.find(`textarea[name="${entryPropName}"]`);
|
||||
input.data('uid', entry.uid);
|
||||
input.on('click', function (event) {
|
||||
// Prevent closing the drawer on clicking the input
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
function templateStyling(/** @type {Select2Option} */ item, { searchStyle = false } = {}) {
|
||||
const content = $('<span>').addClass('item').text(item.text).attr('title', `${item.text}\n\nClick to edit`);
|
||||
const isRegex = isValidRegex(item.text);
|
||||
if (isRegex) {
|
||||
content.html(highlightRegex(item.text));
|
||||
content.addClass('regex_item').prepend($('<span>').addClass('regex_icon').text('•*').attr('title', 'Regex'));
|
||||
}
|
||||
|
||||
if (searchStyle && item.count) {
|
||||
// Build a wrapping element
|
||||
const wrapper = $('<span>').addClass('result_block')
|
||||
.append(content);
|
||||
wrapper.append($('<span>').addClass('item_count').text(item.count).attr('title', `Used as a key ${item.count} ${item.count != 1 ? 'times' : 'time'} in this lorebook`));
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
if (isFancyInput) {
|
||||
input.select2({
|
||||
ajax: dynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
|
||||
tags: true,
|
||||
tokenSeparators: [','],
|
||||
tokenizer: customTokenizer,
|
||||
placeholder: input.attr('placeholder'),
|
||||
templateResult: item => templateStyling(item, { searchStyle: true }),
|
||||
templateSelection: item => templateStyling(item),
|
||||
});
|
||||
input.on('change', function (_, { skipReset, noSave } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
/** @type {string[]} */
|
||||
const keys = ($(this).select2('data')).map(x => x.text);
|
||||
|
||||
!skipReset && resetScrollHeight(this);
|
||||
if (!noSave) {
|
||||
data.entries[uid][entryPropName] = keys;
|
||||
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
|
||||
saveWorldInfo(name, data);
|
||||
}
|
||||
});
|
||||
input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
|
||||
input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
|
||||
|
||||
select2ChoiceClickSubscribe(input, target => {
|
||||
const key = $(target).text();
|
||||
console.debug('Editing WI key', key);
|
||||
|
||||
// Remove the current key from the actual selection
|
||||
const selected = input.val();
|
||||
if (!Array.isArray(selected)) return;
|
||||
var index = selected.indexOf(getSelect2OptionId(key));
|
||||
if (index > -1) selected.splice(index, 1);
|
||||
input.val(selected).trigger('change');
|
||||
// Manually update the cache, that change event is not gonna trigger it
|
||||
updateWorldEntryKeyOptionsCache([key], { remove: true });
|
||||
|
||||
// We need to "hack" the actual text input into the currently open textarea
|
||||
input.next('span.select2-container').find('textarea')
|
||||
.val(key).trigger('input');
|
||||
}, { openDrawer: true });
|
||||
|
||||
select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
|
||||
}
|
||||
else {
|
||||
// Compatibility with mobile devices. On mobile we need a text input field, not a select option control, so we need its own event handlers
|
||||
template.find(`select[name="${entryPropName}"]`).hide();
|
||||
input.show();
|
||||
|
||||
input.on('input', function (_, { skipReset, noSave } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
const value = String($(this).val());
|
||||
!skipReset && resetScrollHeight(this);
|
||||
if (!noSave) {
|
||||
data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
|
||||
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
|
||||
saveWorldInfo(name, data);
|
||||
}
|
||||
});
|
||||
input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
|
||||
}
|
||||
return { isFancy: isFancyInput, control: input };
|
||||
}
|
||||
|
||||
// key
|
||||
const keyInput = template.find('textarea[name="key"]');
|
||||
keyInput.data('uid', entry.uid);
|
||||
keyInput.on('click', function (event) {
|
||||
// Prevent closing the drawer on clicking the input
|
||||
event.stopPropagation();
|
||||
});
|
||||
const keyInput = enableKeysInput('key', 'keys');
|
||||
|
||||
keyInput.on('input', function (_, { skipReset } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
const value = String($(this).val());
|
||||
!skipReset && resetScrollHeight(this);
|
||||
data.entries[uid].key = value
|
||||
.split(',')
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x);
|
||||
// keysecondary
|
||||
const keySecondaryInput = enableKeysInput('keysecondary', 'secondary_keys');
|
||||
|
||||
setOriginalDataValue(data, uid, 'keys', data.entries[uid].key);
|
||||
saveWorldInfo(name, data);
|
||||
// draw key input switch button
|
||||
template.find('.switch_input_type_icon').on('click', function () {
|
||||
power_user.wi_key_input_plaintext = !power_user.wi_key_input_plaintext;
|
||||
saveSettingsDebounced();
|
||||
|
||||
// Just redraw the panel
|
||||
const uid = ($(this).parents('.world_entry')).data('uid');
|
||||
updateEditor(uid, false);
|
||||
|
||||
$(`.world_entry[uid="${uid}"] .inline-drawer-icon`).trigger('click');
|
||||
// setTimeout(() => {
|
||||
// }, debounce_timeout.standard);
|
||||
}).each((_, icon) => {
|
||||
$(icon).attr('title', $(icon).data(power_user.wi_key_input_plaintext ? 'tooltip-on' : 'tooltip-off'));
|
||||
$(icon).text($(icon).data(power_user.wi_key_input_plaintext ? 'icon-on' : 'icon-off'));
|
||||
});
|
||||
keyInput.val(entry.key.join(', ')).trigger('input', { skipReset: true });
|
||||
//initScrollHeight(keyInput);
|
||||
|
||||
// logic AND/NOT
|
||||
const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
|
||||
@@ -1458,25 +1745,6 @@ function getWorldEntry(name, data, entry) {
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
|
||||
// keysecondary
|
||||
const keySecondaryInput = template.find('textarea[name="keysecondary"]');
|
||||
keySecondaryInput.data('uid', entry.uid);
|
||||
keySecondaryInput.on('input', function (_, { skipReset } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
const value = String($(this).val());
|
||||
!skipReset && resetScrollHeight(this);
|
||||
data.entries[uid].keysecondary = value
|
||||
.split(',')
|
||||
.map((x) => x.trim())
|
||||
.filter((x) => x);
|
||||
|
||||
setOriginalDataValue(data, uid, 'secondary_keys', data.entries[uid].keysecondary);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
|
||||
keySecondaryInput.val(entry.keysecondary.join(', ')).trigger('input', { skipReset: true });
|
||||
//initScrollHeight(keySecondaryInput);
|
||||
|
||||
// comment
|
||||
const commentInput = template.find('textarea[name="comment"]');
|
||||
const commentToggle = template.find('input[name="addMemo"]');
|
||||
@@ -1539,8 +1807,8 @@ function getWorldEntry(name, data, entry) {
|
||||
if (counter.data('first-run')) {
|
||||
counter.data('first-run', false);
|
||||
countTokensDebounced(counter, contentInput.val());
|
||||
initScrollHeight(keyInput);
|
||||
initScrollHeight(keySecondaryInput);
|
||||
if (!keyInput.isFancy) initScrollHeight(keyInput.control);
|
||||
if (!keySecondaryInput.isFancy) initScrollHeight(keySecondaryInput.control);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1563,11 +1831,11 @@ function getWorldEntry(name, data, entry) {
|
||||
.closest('.world_entry')
|
||||
.find('.keysecondarytextpole');
|
||||
|
||||
const keyprimarytextpole = $(this)
|
||||
const keyprimaryselect = $(this)
|
||||
.closest('.world_entry')
|
||||
.find('.keyprimarytextpole');
|
||||
.find('.keyprimaryselect');
|
||||
|
||||
const keyprimaryHeight = keyprimarytextpole.outerHeight();
|
||||
const keyprimaryHeight = keyprimaryselect.outerHeight();
|
||||
keysecondarytextpole.css('height', keyprimaryHeight + 'px');
|
||||
|
||||
value ? keysecondary.show() : keysecondary.hide();
|
||||
@@ -1619,7 +1887,7 @@ function getWorldEntry(name, data, entry) {
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
groupInput.val(entry.group ?? '').trigger('input');
|
||||
setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data)), 1);
|
||||
setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data), { allowMultiple: true }), 1);
|
||||
|
||||
// inclusion priority
|
||||
const groupOverrideInput = template.find('input[name="groupOverride"]');
|
||||
@@ -1891,6 +2159,18 @@ function getWorldEntry(name, data, entry) {
|
||||
});
|
||||
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
|
||||
|
||||
// delay until recursion
|
||||
const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]');
|
||||
delayUntilRecursionInput.data('uid', entry.uid);
|
||||
delayUntilRecursionInput.on('input', function () {
|
||||
const uid = $(this).data('uid');
|
||||
const value = $(this).prop('checked');
|
||||
data.entries[uid].delayUntilRecursion = value;
|
||||
setOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
|
||||
saveWorldInfo(name, data);
|
||||
});
|
||||
delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input');
|
||||
|
||||
// duplicate button
|
||||
const duplicateButton = template.find('.duplicate_entry_button');
|
||||
duplicateButton.data('uid', entry.uid);
|
||||
@@ -2029,11 +2309,15 @@ function getWorldEntry(name, data, entry) {
|
||||
* @returns {(input: any, output: any) => any} Callback function for the autocomplete
|
||||
*/
|
||||
function getInclusionGroupCallback(data) {
|
||||
return function (input, output) {
|
||||
return function (control, input, output) {
|
||||
const uid = $(control).data('uid');
|
||||
const thisGroups = String($(control).val()).split(/,\s*/).filter(x => x).map(x => x.toLowerCase());
|
||||
const groups = new Set();
|
||||
for (const entry of Object.values(data.entries)) {
|
||||
// Skip the groups of this entry, because auto-complete should only suggest the ones that are already available on other entries
|
||||
if (entry.uid == uid) continue;
|
||||
if (entry.group) {
|
||||
groups.add(String(entry.group));
|
||||
entry.group.split(/,\s*/).filter(x => x).forEach(x => groups.add(x));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2041,20 +2325,19 @@ function getInclusionGroupCallback(data) {
|
||||
haystack.sort((a, b) => a.localeCompare(b));
|
||||
const needle = input.term.toLowerCase();
|
||||
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
|
||||
const result = haystack.filter(x => x.toLowerCase().includes(needle));
|
||||
|
||||
if (input.term && !hasExactMatch) {
|
||||
result.unshift(input.term);
|
||||
}
|
||||
const result = haystack.filter(x => x.toLowerCase().includes(needle) && (!thisGroups.includes(x) || hasExactMatch && thisGroups.filter(g => g == x).length == 1));
|
||||
|
||||
output(result);
|
||||
};
|
||||
}
|
||||
|
||||
function getAutomationIdCallback(data) {
|
||||
return function (input, output) {
|
||||
return function (control, input, output) {
|
||||
const uid = $(control).data('uid');
|
||||
const ids = new Set();
|
||||
for (const entry of Object.values(data.entries)) {
|
||||
// Skip automation id of this entry, because auto-complete should only suggest the ones that are already available on other entries
|
||||
if (entry.uid == uid) continue;
|
||||
if (entry.automationId) {
|
||||
ids.add(String(entry.automationId));
|
||||
}
|
||||
@@ -2070,36 +2353,53 @@ function getAutomationIdCallback(data) {
|
||||
const haystack = Array.from(ids);
|
||||
haystack.sort((a, b) => a.localeCompare(b));
|
||||
const needle = input.term.toLowerCase();
|
||||
const hasExactMatch = haystack.findIndex(x => x.toLowerCase() == needle) !== -1;
|
||||
const result = haystack.filter(x => x.toLowerCase().includes(needle));
|
||||
|
||||
if (input.term && !hasExactMatch) {
|
||||
result.unshift(input.term);
|
||||
}
|
||||
|
||||
output(result);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an autocomplete for the inclusion group.
|
||||
* @param {JQuery<HTMLElement>} input Input element to attach the autocomplete to
|
||||
* @param {(input: any, output: any) => any} callback Source data callbacks
|
||||
* @param {JQuery<HTMLElement>} input - Input element to attach the autocomplete to
|
||||
* @param {(control: JQuery<HTMLElement>, input: any, output: any) => any} callback - Source data callbacks
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {boolean} [options.allowMultiple=false] - Whether to allow multiple comma-separated values
|
||||
*/
|
||||
function createEntryInputAutocomplete(input, callback) {
|
||||
function createEntryInputAutocomplete(input, callback, { allowMultiple = false } = {}) {
|
||||
const handleSelect = (event, ui) => {
|
||||
// Prevent default autocomplete select, so we can manually set the value
|
||||
event.preventDefault();
|
||||
if (!allowMultiple) {
|
||||
$(input).val(ui.item.value).trigger('input').trigger('blur');
|
||||
} else {
|
||||
var terms = String($(input).val()).split(/,\s*/);
|
||||
terms.pop(); // remove the current input
|
||||
terms.push(ui.item.value); // add the selected item
|
||||
$(input).val(terms.filter(x => x).join(', ')).trigger('input').trigger('blur');
|
||||
}
|
||||
};
|
||||
|
||||
$(input).autocomplete({
|
||||
minLength: 0,
|
||||
source: callback,
|
||||
select: function (event, ui) {
|
||||
$(input).val(ui.item.value).trigger('input').trigger('blur');
|
||||
source: function (request, response) {
|
||||
if (!allowMultiple) {
|
||||
callback(input, request, response);
|
||||
} else {
|
||||
const term = request.term.split(/,\s*/).pop();
|
||||
request.term = term;
|
||||
callback(input, request, response);
|
||||
}
|
||||
},
|
||||
select: handleSelect,
|
||||
});
|
||||
|
||||
$(input).on('focus click', function () {
|
||||
$(input).autocomplete('search', String($(input).val()));
|
||||
$(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : $(input).val());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Duplicated a WI entry by copying all of its properties and assigning a new uid
|
||||
* @param {*} data - The data of the book
|
||||
@@ -2152,6 +2452,8 @@ const newEntryTemplate = {
|
||||
position: 0,
|
||||
disable: false,
|
||||
excludeRecursion: false,
|
||||
preventRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
probability: 100,
|
||||
useProbability: true,
|
||||
depth: DEFAULT_DEPTH,
|
||||
@@ -2166,7 +2468,7 @@ const newEntryTemplate = {
|
||||
role: 0,
|
||||
};
|
||||
|
||||
function createWorldInfoEntry(name, data) {
|
||||
function createWorldInfoEntry(_name, data) {
|
||||
const newUid = getFreeWorldEntryUid(data);
|
||||
|
||||
if (!Number.isInteger(newUid)) {
|
||||
@@ -2519,7 +2821,7 @@ async function checkWorldInfo(chat, maxContext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
|
||||
if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion) || (count == 1 && entry.delayUntilRecursion)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2792,10 +3094,12 @@ function filterGroupsByScoring(groups, buffer, removeEntry) {
|
||||
function filterByInclusionGroups(newEntries, allActivatedEntries, buffer) {
|
||||
console.debug('-- INCLUSION GROUP CHECKS BEGIN --');
|
||||
const grouped = newEntries.filter(x => x.group).reduce((acc, item) => {
|
||||
if (!acc[item.group]) {
|
||||
acc[item.group] = [];
|
||||
}
|
||||
acc[item.group].push(item);
|
||||
item.group.split(/,\s*/).filter(x => x).forEach(group => {
|
||||
if (!acc[group]) {
|
||||
acc[group] = [];
|
||||
}
|
||||
acc[group].push(item);
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@@ -2887,9 +3191,10 @@ function convertAgnaiMemoryBook(inputObj) {
|
||||
disable: !entry.enabled,
|
||||
addMemo: !!entry.name,
|
||||
excludeRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
displayIndex: index,
|
||||
probability: null,
|
||||
useProbability: false,
|
||||
probability: 100,
|
||||
useProbability: true,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: DEFAULT_WEIGHT,
|
||||
@@ -2925,9 +3230,10 @@ function convertRisuLorebook(inputObj) {
|
||||
disable: false,
|
||||
addMemo: true,
|
||||
excludeRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
displayIndex: index,
|
||||
probability: entry.activationPercent ?? null,
|
||||
useProbability: entry.activationPercent ?? false,
|
||||
probability: entry.activationPercent ?? 100,
|
||||
useProbability: entry.activationPercent ?? true,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: DEFAULT_WEIGHT,
|
||||
@@ -2968,9 +3274,10 @@ function convertNovelLorebook(inputObj) {
|
||||
disable: !entry.enabled,
|
||||
addMemo: addMemo,
|
||||
excludeRecursion: false,
|
||||
delayUntilRecursion: false,
|
||||
displayIndex: index,
|
||||
probability: null,
|
||||
useProbability: false,
|
||||
probability: 100,
|
||||
useProbability: true,
|
||||
group: '',
|
||||
groupOverride: false,
|
||||
groupWeight: DEFAULT_WEIGHT,
|
||||
@@ -3008,11 +3315,12 @@ function convertCharacterBook(characterBook) {
|
||||
position: entry.extensions?.position ?? (entry.position === 'before_char' ? world_info_position.before : world_info_position.after),
|
||||
excludeRecursion: entry.extensions?.exclude_recursion ?? false,
|
||||
preventRecursion: entry.extensions?.prevent_recursion ?? false,
|
||||
delayUntilRecursion: entry.extensions?.delay_until_recursion ?? false,
|
||||
disable: !entry.enabled,
|
||||
addMemo: entry.comment ? true : false,
|
||||
displayIndex: entry.extensions?.display_index ?? index,
|
||||
probability: entry.extensions?.probability ?? null,
|
||||
useProbability: entry.extensions?.useProbability ?? false,
|
||||
probability: entry.extensions?.probability ?? 100,
|
||||
useProbability: entry.extensions?.useProbability ?? true,
|
||||
depth: entry.extensions?.depth ?? DEFAULT_DEPTH,
|
||||
selectiveLogic: entry.extensions?.selectiveLogic ?? world_info_logic.AND_ANY,
|
||||
group: entry.extensions?.group ?? '',
|
||||
@@ -3261,7 +3569,7 @@ export async function importWorldInfo(file) {
|
||||
toastr.info(`World Info "${data.name}" imported successfully!`);
|
||||
}
|
||||
},
|
||||
error: (jqXHR, exception) => { },
|
||||
error: (_jqXHR, _exception) => { },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3468,21 +3776,14 @@ jQuery(() => {
|
||||
});
|
||||
|
||||
// Subscribe world loading to the select2 multiselect items (We need to target the specific select2 control)
|
||||
$('#world_info + span.select2-container').on('click', function (event) {
|
||||
if ($(event.target).hasClass('select2-selection__choice__display')) {
|
||||
event.preventDefault();
|
||||
|
||||
// select2 still bubbles the event to open the dropdown. So we close it here
|
||||
$('#world_info').select2('close');
|
||||
|
||||
const name = $(event.target).text();
|
||||
const selectedIndex = world_names.indexOf(name);
|
||||
if (selectedIndex !== -1) {
|
||||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||||
console.log('Quick selection of world', name);
|
||||
}
|
||||
select2ChoiceClickSubscribe($('#world_info'), target => {
|
||||
const name = $(target).text();
|
||||
const selectedIndex = world_names.indexOf(name);
|
||||
if (selectedIndex !== -1) {
|
||||
$('#world_editor_select').val(selectedIndex).trigger('change');
|
||||
console.log('Quick selection of world', name);
|
||||
}
|
||||
});
|
||||
}, { buttonStyle: true, closeDrawer: true });
|
||||
}
|
||||
|
||||
$('#WorldInfo').on('scroll', () => {
|
||||
|
Reference in New Issue
Block a user