Merge branch 'staging' into wi-go-brrrrrr-too

This commit is contained in:
Cohee
2024-08-06 00:29:24 +03:00
90 changed files with 6903 additions and 1172 deletions

View File

@ -311,6 +311,7 @@ function RA_checkOnlineStatus() {
$('#send_form').addClass('no-connection'); //entire input form area is red when not connected
$('#send_but').addClass('displayNone'); //send button is hidden when not connected;
$('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
$('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
$('#API-status-top').removeClass('fa-plug');
$('#API-status-top').addClass('fa-plug-circle-exclamation redOverlayGlow');
connection_made = false;
@ -327,6 +328,7 @@ function RA_checkOnlineStatus() {
if (!is_send_press && !(selected_group && is_group_generating)) {
$('#send_but').removeClass('displayNone'); //on connect, send button shows
$('#mes_continue').removeClass('displayNone'); //continue button is shown when connected
$('#mes_impersonate').removeClass('displayNone'); //continue button is shown when connected
}
}
}

View File

@ -16,8 +16,15 @@ export const AUTOCOMPLETE_WIDTH = {
'FULL': 2,
};
/**@readonly*/
/**@enum {Number}*/
export const AUTOCOMPLETE_SELECT_KEY = {
'TAB': 1, // 2^0
'ENTER': 2, // 2^1
};
export class AutoComplete {
/**@type {HTMLTextAreaElement}*/ textarea;
/**@type {HTMLTextAreaElement|HTMLInputElement}*/ textarea;
/**@type {boolean}*/ isFloating = false;
/**@type {()=>boolean}*/ checkIfActivate;
/**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
@ -56,6 +63,8 @@ export class AutoComplete {
/**@type {function}*/ updateDetailsPositionDebounced;
/**@type {function}*/ updateFloatingPositionDebounced;
/**@type {(item:AutoCompleteOption)=>any}*/ onSelect;
get matchType() {
return power_user.stscript.matching ?? 'fuzzy';
}
@ -68,7 +77,7 @@ export class AutoComplete {
/**
* @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete.
* @param {HTMLTextAreaElement|HTMLInputElement} textarea The textarea to receive autocomplete.
* @param {() => boolean} checkIfActivate Function should return true only if under the current conditions, autocomplete should display (e.g., for slash commands: autoComplete.text[0] == '/')
* @param {(text: string, index: number) => Promise<AutoCompleteNameResult>} getNameAt Function should return (unfiltered, matching against input is done in AutoComplete) information about name options at index in text.
* @param {boolean} isFloating Whether autocomplete should float at the keyboard cursor.
@ -102,10 +111,15 @@ export class AutoComplete {
this.updateDetailsPositionDebounced = debounce(this.updateDetailsPosition.bind(this), 10);
this.updateFloatingPositionDebounced = debounce(this.updateFloatingPosition.bind(this), 10);
textarea.addEventListener('input', ()=>this.text != this.textarea.value && this.show(true, this.wasForced));
textarea.addEventListener('input', ()=>{
this.selectionStart = this.textarea.selectionStart;
if (this.text != this.textarea.value) this.show(true, this.wasForced);
});
textarea.addEventListener('keydown', (evt)=>this.handleKeyDown(evt));
textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);
textarea.addEventListener('selectionchange', ()=>this.show());
textarea.addEventListener('click', ()=>{
this.selectionStart = this.textarea.selectionStart;
if (this.isActive) this.show();
});
textarea.addEventListener('blur', ()=>this.hide());
if (isFloating) {
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
@ -189,6 +203,11 @@ export class AutoComplete {
* @returns The option.
*/
fuzzyScore(option) {
// might have been matched by the options matchProvider function instead
if (!this.fuzzyRegex.test(option.name)) {
option.score = new AutoCompleteFuzzyScore(Number.MAX_SAFE_INTEGER, -1);
return option;
}
const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
let start = null;
let consecutive = [];
@ -339,7 +358,7 @@ export class AutoComplete {
this.result = this.effectiveParserResult.optionList
// 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)
.filter(it => this.isReplaceable || it.name == '' ? (it.matchProvider ? it.matchProvider(this.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);
@ -357,10 +376,11 @@ export class AutoComplete {
// build element
option.dom = this.makeItem(option);
// update replacer and add quotes if necessary
const optionName = option.valueProvider ? option.valueProvider(this.name) : option.name;
if (this.effectiveParserResult.canBeQuoted) {
option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;
option.replacer = optionName.includes(' ') || this.startQuote || this.endQuote ? `"${optionName.replace(/"/g, '\\"')}"` : `${optionName}`;
} else {
option.replacer = option.name;
option.replacer = optionName;
}
// calculate fuzzy score if matching is fuzzy
if (this.matchType == 'fuzzy') this.fuzzyScore(option);
@ -399,7 +419,7 @@ export class AutoComplete {
,
);
this.result.push(option);
} else if (this.result.length == 1 && this.effectiveParserResult && this.result[0].name == this.effectiveParserResult.name) {
} else if (this.result.length == 1 && this.effectiveParserResult && this.effectiveParserResult != this.secondaryParserResult && this.result[0].name == this.effectiveParserResult.name) {
// only one result that is exactly the current value? just show hint, no autocomplete
this.isReplaceable = false;
this.isShowingDetails = false;
@ -439,11 +459,14 @@ export class AutoComplete {
} else {
item.dom.classList.remove('selected');
}
if (!item.isSelectable) {
item.dom.classList.add('not-selectable');
}
frag.append(item.dom);
}
this.dom.append(frag);
this.updatePosition();
getTopmostModalLayer().append(this.domWrap);
this.getLayer().append(this.domWrap);
} else {
this.domWrap.remove();
}
@ -458,10 +481,17 @@ export class AutoComplete {
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
this.detailsDom.innerHTML = '';
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
getTopmostModalLayer().append(this.detailsWrap);
this.getLayer().append(this.detailsWrap);
this.updateDetailsPositionDebounced();
}
/**
* @returns {HTMLElement} closest ancestor dialog or body
*/
getLayer() {
return this.textarea.closest('dialog, body');
}
/**
@ -474,7 +504,7 @@ export class AutoComplete {
const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
this.domWrap.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.dom.style.setProperty('--bottom', `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`);
this.domWrap.style.bottom = `${window.innerHeight - rect[AUTOCOMPLETE_WIDTH.INPUT].top}px`;
@ -501,7 +531,7 @@ export class AutoComplete {
const rect = {};
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = getTopmostModalLayer().getBoundingClientRect();
rect[AUTOCOMPLETE_WIDTH.FULL] = this.getLayer().getBoundingClientRect();
if (this.isReplaceable) {
this.detailsWrap.classList.remove('full');
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
@ -527,32 +557,34 @@ export class AutoComplete {
updateFloatingPosition() {
const location = this.getCursorPosition();
const rect = this.textarea.getBoundingClientRect();
const layerRect = this.getLayer().getBoundingClientRect();
// cursor is out of view -> hide
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
return this.hide();
}
const left = Math.max(rect.left, location.left);
const left = Math.max(rect.left, location.left) - layerRect.left;
this.domWrap.style.setProperty('--targetOffset', `${left}`);
if (location.top <= window.innerHeight / 2) {
// if cursor is in lower half of window, show list above line
this.domWrap.style.top = `${location.bottom}px`;
this.domWrap.style.top = `${location.bottom - layerRect.top}px`;
this.domWrap.style.bottom = 'auto';
this.domWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
this.domWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
} else {
// if cursor is in upper half of window, show list below line
this.domWrap.style.top = 'auto';
this.domWrap.style.bottom = `calc(100vh - ${location.top}px)`;
this.domWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
this.domWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
this.domWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
}
}
updateFloatingDetailsPosition(location = null) {
if (!location) location = this.getCursorPosition();
const rect = this.textarea.getBoundingClientRect();
const layerRect = this.getLayer().getBoundingClientRect();
if (location.bottom < rect.top || location.top > rect.bottom || location.left < rect.left || location.left > rect.right) {
return this.hide();
}
const left = Math.max(rect.left, location.left);
const left = Math.max(rect.left, location.left) - layerRect.left;
this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
if (this.isReplaceable) {
this.detailsWrap.classList.remove('full');
@ -572,14 +604,14 @@ export class AutoComplete {
}
if (location.top <= window.innerHeight / 2) {
// if cursor is in lower half of window, show list above line
this.detailsWrap.style.top = `${location.bottom}px`;
this.detailsWrap.style.top = `${location.bottom - layerRect.top}px`;
this.detailsWrap.style.bottom = 'auto';
this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 1vh)`;
this.detailsWrap.style.maxHeight = `calc(${location.bottom - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
} else {
// if cursor is in upper half of window, show list below line
this.detailsWrap.style.top = 'auto';
this.detailsWrap.style.bottom = `calc(100vh - ${location.top}px)`;
this.detailsWrap.style.maxHeight = `calc(${location.top}px - 1vh)`;
this.detailsWrap.style.bottom = `calc(${layerRect.height}px - ${location.top - layerRect.top}px)`;
this.detailsWrap.style.maxHeight = `calc(${location.top - layerRect.top}px - ${this.textarea.closest('dialog') ? '0' : '1vh'})`;
}
}
@ -597,7 +629,7 @@ export class AutoComplete {
}
this.clone.style.position = 'fixed';
this.clone.style.visibility = 'hidden';
getTopmostModalLayer().append(this.clone);
document.body.append(this.clone);
const mo = new MutationObserver(muts=>{
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
this.clone.remove();
@ -656,6 +688,7 @@ export class AutoComplete {
}
this.wasForced = false;
this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
this.onSelect?.(this.selectedItem);
}
@ -708,8 +741,10 @@ export class AutoComplete {
}
case 'Enter': {
// pick the selected item to autocomplete
if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.ENTER) != AUTOCOMPLETE_SELECT_KEY.ENTER) break;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
if (this.selectedItem.name == this.name) break;
if (!this.selectedItem.isSelectable) break;
evt.preventDefault();
evt.stopImmediatePropagation();
this.select();
@ -717,9 +752,11 @@ export class AutoComplete {
}
case 'Tab': {
// pick the selected item to autocomplete
if ((power_user.stscript.autocomplete.select & AUTOCOMPLETE_SELECT_KEY.TAB) != AUTOCOMPLETE_SELECT_KEY.TAB) break;
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
evt.preventDefault();
evt.stopImmediatePropagation();
if (!this.selectedItem.isSelectable) break;
this.select();
return;
}
@ -772,30 +809,16 @@ export class AutoComplete {
// ignore keydown on modifier keys
return;
}
switch (evt.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowRight':
case 'ArrowLeft': {
if (this.isActive) {
// keyboard navigation, wait for keyup to complete cursor move
const oldText = this.textarea.value;
await new Promise(resolve=>{
window.addEventListener('keyup', resolve, { once:true });
});
if (this.selectionStart != this.textarea.selectionStart) {
this.selectionStart = this.textarea.selectionStart;
this.show(this.isReplaceable || oldText != this.textarea.value);
}
}
break;
}
default: {
if (this.isActive) {
this.text != this.textarea.value && this.show(this.isReplaceable);
}
break;
}
// await keyup to see if cursor position or text has changed
const oldText = this.textarea.value;
await new Promise(resolve=>{
window.addEventListener('keyup', resolve, { once:true });
});
if (this.selectionStart != this.textarea.selectionStart) {
this.selectionStart = this.textarea.selectionStart;
this.show(this.isReplaceable || oldText != this.textarea.value);
} else if (this.isActive) {
this.text != this.textarea.value && this.show(this.isReplaceable);
}
}
}

View File

@ -1,36 +1,9 @@
import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
import { AutoCompleteOption } from './AutoCompleteOption.js';
// import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
export class AutoCompleteNameResult {
/**@type {string} */ name;
/**@type {number} */ start;
/**@type {AutoCompleteOption[]} */ optionList = [];
/**@type {boolean} */ canBeQuoted = false;
/**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
/**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
/**
* @param {string} name Name (potentially partial) of the name at the requested index.
* @param {number} start Index where the name starts.
* @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
* @param {boolean} canBeQuoted Whether the name can be inside quotes.
* @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
* @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
*/
constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
this.name = name;
this.start = start;
this.optionList = optionList;
this.canBeQuoted = canBeQuoted;
this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
}
export class AutoCompleteNameResult extends AutoCompleteNameResultBase {
/**
*
* @param {string} text The whole text

View File

@ -0,0 +1,31 @@
import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
import { AutoCompleteOption } from './AutoCompleteOption.js';
export class AutoCompleteNameResultBase {
/**@type {string} */ name;
/**@type {number} */ start;
/**@type {AutoCompleteOption[]} */ optionList = [];
/**@type {boolean} */ canBeQuoted = false;
/**@type {()=>string} */ makeNoMatchText = ()=>`No matches found for "${this.name}"`;
/**@type {()=>string} */ makeNoOptionsText = ()=>'No options';
/**
* @param {string} name Name (potentially partial) of the name at the requested index.
* @param {number} start Index where the name starts.
* @param {AutoCompleteOption[]} optionList A list of autocomplete options found in the current scope.
* @param {boolean} canBeQuoted Whether the name can be inside quotes.
* @param {()=>string} makeNoMatchText Function that returns text to show when no matches where found.
* @param {()=>string} makeNoOptionsText Function that returns text to show when no options are available to match against.
*/
constructor(name, start, optionList = [], canBeQuoted = false, makeNoMatchText = null, makeNoOptionsText = null) {
this.name = name;
this.start = start;
this.optionList = optionList;
this.canBeQuoted = canBeQuoted;
this.noMatchText = makeNoMatchText ?? this.makeNoMatchText;
this.noOptionstext = makeNoOptionsText ?? this.makeNoOptionsText;
}
}

View File

@ -11,6 +11,9 @@ export class AutoCompleteOption {
/**@type {AutoCompleteFuzzyScore}*/ score;
/**@type {string}*/ replacer;
/**@type {HTMLElement}*/ dom;
/**@type {(input:string)=>boolean}*/ matchProvider;
/**@type {(input:string)=>string}*/ valueProvider;
/**@type {boolean}*/ makeSelectable = false;
/**
@ -21,14 +24,21 @@ export class AutoCompleteOption {
return this.name;
}
get isSelectable() {
return this.makeSelectable || !this.valueProvider;
}
/**
* @param {string} name
*/
constructor(name, typeIcon = ' ', type = '') {
constructor(name, typeIcon = ' ', type = '', matchProvider = null, valueProvider = null, makeSelectable = false) {
this.name = name;
this.typeIcon = typeIcon;
this.type = type;
this.matchProvider = matchProvider;
this.valueProvider = valueProvider;
this.makeSelectable = makeSelectable;
}

View File

@ -1,6 +1,6 @@
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
import { AutoCompleteNameResultBase } from './AutoCompleteNameResultBase.js';
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResultBase {
/**@type {boolean}*/ isRequired = false;
/**@type {boolean}*/ forceMatch = true;
}

View File

@ -8,13 +8,12 @@ import { textgen_types, textgenerationwebui_settings } from '../../textgen-setti
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
export { MODULE_NAME };
const MODULE_NAME = 'caption';
const PROMPT_DEFAULT = 'Whats in this image?';
const PROMPT_DEFAULT = 'What\'s in this image?';
const TEMPLATE_DEFAULT = '[{{user}} sends {{char}} a picture that contains: {{caption}}]';
/**
@ -170,7 +169,11 @@ async function sendCaptionedMessage(caption, image) {
},
};
context.chat.push(message);
const messageId = context.chat.length - 1;
await eventSource.emit(event_types.MESSAGE_SENT, messageId);
context.addOneMessage(message);
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
await context.saveChat();
}
/**
@ -334,7 +337,7 @@ async function getCaptionForFile(file, prompt, quiet) {
}
catch (error) {
const errorMessage = error.message || 'Unknown error';
toastr.error(errorMessage, "Failed to caption image.");
toastr.error(errorMessage, 'Failed to caption image.');
console.error(error);
return '';
}
@ -399,6 +402,7 @@ jQuery(async function () {
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||

View File

@ -17,6 +17,7 @@
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_api" data-i18n="API">API</label>
<select id="caption_multimodal_api" class="flex1 text_pole">
<option value="zerooneai">01.AI (Yi)</option>
<option value="anthropic">Anthropic</option>
<option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
<option value="google">Google MakerSuite</option>
@ -32,6 +33,7 @@
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_model" data-i18n="Model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="zerooneai" value="yi-vision">yi-vision</option>
<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>
@ -42,6 +44,8 @@
<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="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
<option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</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>

View File

@ -1,4 +1,4 @@
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';
import { callPopup, eventSource, event_types, generateRaw, getRequestHeaders, main_api, online_status, saveSettingsDebounced, substituteParams, substituteParamsExtended, system_message_types } from '../../../script.js';
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, power_user } from '../../power-user.js';
@ -1156,7 +1156,7 @@ async function getExpressionLabel(text) {
functionResult = args?.arguments;
});
const emotionResponse = await generateQuietPrompt(prompt, false, false);
const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
return parseLlmResponse(functionResult || emotionResponse, expressionsList);
}
// Extras

View File

@ -23,10 +23,18 @@ export class QuickReplyApi {
/**
* @param {QuickReply} qr
* @returns {QuickReplySet}
*/
getSetByQr(qr) {
return QuickReplySet.list.find(it=>it.qrList.includes(qr));
}
/**
* Finds and returns an existing Quick Reply Set by its name.
*
* @param {String} name name of the quick reply set
* @param {string} name name of the quick reply set
* @returns the quick reply set, or undefined if not found
*/
getSetByName(name) {
@ -36,13 +44,14 @@ export class QuickReplyApi {
/**
* Finds and returns an existing Quick Reply by its set's name and its label.
*
* @param {String} setName name of the quick reply set
* @param {String} label label of the quick reply
* @param {string} setName name of the quick reply set
* @param {string|number} label label or numeric ID of the quick reply
* @returns the quick reply, or undefined if not found
*/
getQrByLabel(setName, label) {
const set = this.getSetByName(setName);
if (!set) return;
if (Number.isInteger(label)) return set.qrList.find(it=>it.id == label);
return set.qrList.find(it=>it.label == label);
}
@ -70,24 +79,25 @@ export class QuickReplyApi {
/**
* Executes an existing quick reply.
*
* @param {String} setName name of the existing quick reply set
* @param {String} label label of the existing quick reply (text on the button)
* @param {Object} [args] optional arguments
* @param {string} setName name of the existing quick reply set
* @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
* @param {object} [args] optional arguments
* @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
*/
async executeQuickReply(setName, label, args = {}) {
async executeQuickReply(setName, label, args = {}, options = {}) {
const qr = this.getQrByLabel(setName, label);
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
return await qr.execute(args);
return await qr.execute(args, false, false, options);
}
/**
* Adds or removes a quick reply set to the list of globally active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
* @param {string} name the name of the set
* @param {boolean} isVisible whether to show the set's buttons or not
*/
toggleGlobalSet(name, isVisible = true) {
const set = this.getSetByName(name);
@ -104,8 +114,8 @@ export class QuickReplyApi {
/**
* Adds a quick reply set to the list of globally active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
* @param {string} name the name of the set
* @param {boolean} isVisible whether to show the set's buttons or not
*/
addGlobalSet(name, isVisible = true) {
const set = this.getSetByName(name);
@ -118,7 +128,7 @@ export class QuickReplyApi {
/**
* Removes a quick reply set from the list of globally active quick reply sets.
*
* @param {String} name the name of the set
* @param {string} name the name of the set
*/
removeGlobalSet(name) {
const set = this.getSetByName(name);
@ -132,8 +142,8 @@ export class QuickReplyApi {
/**
* Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
* @param {string} name the name of the set
* @param {boolean} isVisible whether to show the set's buttons or not
*/
toggleChatSet(name, isVisible = true) {
if (!this.settings.chatConfig) return;
@ -151,8 +161,8 @@ export class QuickReplyApi {
/**
* Adds a quick reply set to the list of the current chat's active quick reply sets.
*
* @param {String} name the name of the set
* @param {Boolean} isVisible whether to show the set's buttons or not
* @param {string} name the name of the set
* @param {boolean} isVisible whether to show the set's buttons or not
*/
addChatSet(name, isVisible = true) {
if (!this.settings.chatConfig) return;
@ -166,7 +176,7 @@ export class QuickReplyApi {
/**
* Removes a quick reply set from the list of the current chat's active quick reply sets.
*
* @param {String} name the name of the set
* @param {string} name the name of the set
*/
removeChatSet(name) {
if (!this.settings.chatConfig) return;
@ -181,21 +191,25 @@ export class QuickReplyApi {
/**
* Creates a new quick reply in an existing quick reply set.
*
* @param {String} setName name of the quick reply set to insert the new quick reply into
* @param {String} label label for the new quick reply (text on the button)
* @param {Object} [props]
* @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply
* @param {String} [props.title] the title / tooltip to be shown on the quick reply button
* @param {Boolean} [props.isHidden] whether to hide or show the button
* @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
* @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
* @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
* @param {string} setName name of the quick reply set to insert the new quick reply into
* @param {string} label label for the new quick reply (text on the button)
* @param {object} [props]
* @param {string} [props.icon] the icon to show on the QR button
* @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
* @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply
* @param {string} [props.title] the title / tooltip to be shown on the quick reply button
* @param {boolean} [props.isHidden] whether to hide or show the button
* @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
* @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
* @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
* @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
* @returns {QuickReply} the new quick reply
*/
createQuickReply(setName, label, {
icon,
showLabel,
message,
title,
isHidden,
@ -212,6 +226,8 @@ export class QuickReplyApi {
}
const qr = set.addQuickReply();
qr.label = label ?? '';
qr.icon = icon ?? '';
qr.showLabel = showLabel ?? false;
qr.message = message ?? '';
qr.title = title ?? '';
qr.isHidden = isHidden ?? false;
@ -228,22 +244,26 @@ export class QuickReplyApi {
/**
* Updates an existing quick reply.
*
* @param {String} setName name of the existing quick reply set
* @param {String} label label of the existing quick reply (text on the button)
* @param {Object} [props]
* @param {String} [props.newLabel] new label for quick reply (text on the button)
* @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply
* @param {String} [props.title] the title / tooltip to be shown on the quick reply button
* @param {Boolean} [props.isHidden] whether to hide or show the button
* @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
* @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
* @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @param {Boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
* @param {String} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
* @param {string} setName name of the existing quick reply set
* @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
* @param {object} [props]
* @param {string} [props.icon] the icon to show on the QR button
* @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
* @param {string} [props.newLabel] new label for quick reply (text on the button)
* @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply
* @param {string} [props.title] the title / tooltip to be shown on the quick reply button
* @param {boolean} [props.isHidden] whether to hide or show the button
* @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
* @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
* @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
* @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
* @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
* @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
* @returns {QuickReply} the altered quick reply
*/
updateQuickReply(setName, label, {
icon,
showLabel,
newLabel,
message,
title,
@ -259,6 +279,8 @@ export class QuickReplyApi {
if (!qr) {
throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
}
qr.updateIcon(icon ?? qr.icon);
qr.updateShowLabel(showLabel ?? qr.showLabel);
qr.updateLabel(newLabel ?? qr.label);
qr.updateMessage(message ?? qr.message);
qr.updateTitle(title ?? qr.title);
@ -276,8 +298,8 @@ export class QuickReplyApi {
/**
* Deletes an existing quick reply.
*
* @param {String} setName name of the existing quick reply set
* @param {String} label label of the existing quick reply (text on the button)
* @param {string} setName name of the existing quick reply set
* @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
*/
deleteQuickReply(setName, label) {
const qr = this.getQrByLabel(setName, label);
@ -291,10 +313,10 @@ export class QuickReplyApi {
/**
* Adds an existing quick reply set as a context menu to an existing quick reply.
*
* @param {String} setName name of the existing quick reply set containing the quick reply
* @param {String} label label of the existing quick reply
* @param {String} contextSetName name of the existing quick reply set to be used as a context menu
* @param {Boolean} isChained whether or not to chain the context menu quick replies
* @param {string} setName name of the existing quick reply set containing the quick reply
* @param {string|number} label label of the existing quick reply or its numeric ID
* @param {string} contextSetName name of the existing quick reply set to be used as a context menu
* @param {boolean} isChained whether or not to chain the context menu quick replies
*/
createContextItem(setName, label, contextSetName, isChained = false) {
const qr = this.getQrByLabel(setName, label);
@ -314,9 +336,9 @@ export class QuickReplyApi {
/**
* Removes a quick reply set from a quick reply's context menu.
*
* @param {String} setName name of the existing quick reply set containing the quick reply
* @param {String} label label of the existing quick reply
* @param {String} contextSetName name of the existing quick reply set to be used as a context menu
* @param {string} setName name of the existing quick reply set containing the quick reply
* @param {string|number} label label of the existing quick reply or its numeric ID
* @param {string} contextSetName name of the existing quick reply set to be used as a context menu
*/
deleteContextItem(setName, label, contextSetName) {
const qr = this.getQrByLabel(setName, label);
@ -333,8 +355,8 @@ export class QuickReplyApi {
/**
* Removes all entries from a quick reply's context menu.
*
* @param {String} setName name of the existing quick reply set containing the quick reply
* @param {String} label label of the existing quick reply
* @param {string} setName name of the existing quick reply set containing the quick reply
* @param {string|number} label label of the existing quick reply or its numeric ID
*/
clearContextMenu(setName, label) {
const qr = this.getQrByLabel(setName, label);
@ -348,11 +370,11 @@ export class QuickReplyApi {
/**
* Create a new quick reply set.
*
* @param {String} name name of the new quick reply set
* @param {Object} [props]
* @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
* @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
* @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
* @param {string} name name of the new quick reply set
* @param {object} [props]
* @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
* @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
* @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
* @returns {Promise<QuickReplySet>} the new quick reply set
*/
async createSet(name, {
@ -384,11 +406,11 @@ export class QuickReplyApi {
/**
* Update an existing quick reply set.
*
* @param {String} name name of the existing quick reply set
* @param {Object} [props]
* @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
* @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
* @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
* @param {string} name name of the existing quick reply set
* @param {object} [props]
* @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
* @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
* @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
* @returns {Promise<QuickReplySet>} the altered quick reply set
*/
async updateSet(name, {
@ -411,7 +433,7 @@ export class QuickReplyApi {
/**
* Delete an existing quick reply set.
*
* @param {String} name name of the existing quick reply set
* @param {string} name name of the existing quick reply set
*/
async deleteSet(name) {
const set = this.getSetByName(name);
@ -451,7 +473,7 @@ export class QuickReplyApi {
/**
* Gets a list of all quick replies in the quick reply set.
*
* @param {String} setName name of the existing quick reply set
* @param {string} setName name of the existing quick reply set
* @returns array with the labels of this set's quick replies
*/
listQuickReplies(setName) {

View File

@ -2,10 +2,23 @@
<div id="qr--main">
<h3 data-i18n="Labels and Message">Labels and Message</h3>
<div class="qr--labels">
<label>
<span class="qr--labelText" data-i18n="Label">Label</span>
<input type="text" class="text_pole" id="qr--modal-label">
<label class="qr--fit">
<span class="qr--labelText" data-i18n="Label">Icon</span>
<small class="qr--labelHint">&nbsp;</small>
<div class="menu_button fa-fw" id="qr--modal-icon" title="Click to change icon"></div>
</label>
<div class="label">
<span class="qr--labelText" data-i18n="Label">Label</span>
<small class="qr--labelHint" data-i18n="(label of the button, if no icon is chosen) ">(label of the button, if no icon is chosen)</small>
<div class="qr--inputGroup">
<label class="checkbox_label" title="Show label even if an icon is assigned">
<input type="checkbox" id="qr--modal-showLabel">
Show
</label>
<input type="text" class="text_pole" id="qr--modal-label">
<div class="menu_button fa-fw fa-solid fa-chevron-down" id="qr--modal-switcher" title="Switch to another QR"></div>
</div>
</div>
<label>
<span class="qr--labelText" data-i18n="Title">Title</span>
<small class="qr--labelHint" data-i18n="(tooltip, leave empty to show message or /command)">(tooltip, leave empty to show message or /command)</small>
@ -33,6 +46,8 @@
<input type="checkbox" id="qr--modal-syntax">
<span>Syntax highlight</span>
</label>
<small>Ctrl+Alt+Click (or F9) to set / remove breakpoints</small>
<small>Ctrl+<span id="qr--modal-commentKey"></span> to toggle block comments</small>
</div>
<div id="qr--modal-messageHolder">
<pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
@ -43,6 +58,10 @@
<div id="qr--resizeHandle"></div>
<div id="qr--qrOptions">
<h3 data-i18n="Context Menu">Context Menu</h3>
<div id="qr--ctxEditor">
@ -64,7 +83,7 @@
<h3 data-i18n="Auto-Execute">Auto-Execute</h3>
<div class="flex-container flexFlowColumn">
<div id="qr--autoExec" class="flex-container flexFlowColumn">
<label class="checkbox_label" title="Prevent this quick reply from triggering other auto-executed quick replies while auto-executing (i.e., prevent recursive auto-execution)">
<input type="checkbox" id="qr--preventAutoExecute" >
<span><i class="fa-solid fa-fw fa-plane-slash"></i><span data-i18n="Don't trigger auto-execute">Don't trigger auto-execute</span></span>
@ -117,11 +136,18 @@
</div>
</div>
<div id="qr--modal-executeProgress"></div>
<label class="checkbox_label">
<input type="checkbox" id="qr--modal-executeHide">
<span title="Hide editor while executing"> Hide editor while executing</span>
</label>
<div id="qr--modal-executeErrors"></div>
<div id="qr--modal-executeResult"></div>
<div id="qr--modal-debugButtons">
<div title="Resume" id="qr--modal-resume" class="qr--modal-debugButton menu_button"></div>
<div title="Step Over" id="qr--modal-step" class="qr--modal-debugButton menu_button"></div>
<div title="Step Into" id="qr--modal-stepInto" class="qr--modal-debugButton menu_button"></div>
<div title="Step Out" id="qr--modal-stepOut" class="qr--modal-debugButton menu_button"></div>
<div title="Minimize" id="qr--modal-minimize" class="qr--modal-debugButton menu_button fa-solid fa-minimize"></div>
<div title="Maximize" id="qr--modal-maximize" class="qr--modal-debugButton menu_button fa-solid fa-maximize"></div>
</div>
<textarea rows="1" id="qr--modal-send_textarea" placeholder="Chat input for use with {{input}}" title="Chat input for use with {{input}}"></textarea>
<div id="qr--modal-debugState"></div>
</div>
</div>

View File

@ -60,10 +60,20 @@
<label class="flex-container" id="qr--injectInputContainer">
<input type="checkbox" id="qr--injectInput"> <span><span data-i18n="Inject user input automatically">Inject user input automatically</span> <small><span data-i18n="(if disabled, use ">(if disabled, use</span><code>{{input}}</code> <span data-i18n="macro for manual injection)">macro for manual injection)</span></small></span>
</label>
<div class="flex-container alignItemsCenter">
<toolcool-color-picker id="qr--color"></toolcool-color-picker>
<div class="menu_button" id="qr--colorClear">Clear</div>
<span data-i18n="Color">Color</span>
</div>
<label class="flex-container" id="qr--onlyBorderColorContainer">
<input type="checkbox" id="qr--onlyBorderColor"> <span data-i18n="Only apply color as accent">Only apply color as accent</span>
</label>
</div>
<div id="qr--set-qrList" class="qr--qrList"></div>
<div class="qr--set-qrListActions">
<div class="qr--add menu_button menu_button_icon fa-solid fa-plus" id="qr--set-add" title="Add quick reply"></div>
<div class="qr--paste menu_button menu_button_icon fa-solid fa-paste" id="qr--set-paste" title="Paste quick reply from clipboard"></div>
<div class="qr--import menu_button menu_button_icon fa-solid fa-file-import" id="qr--set-importQr" title="Import quick reply from file"></div>
</div>
</div>
</div>

View File

@ -176,7 +176,7 @@ const init = async () => {
buttons.show();
settings.onSave = ()=>buttons.refresh();
window['executeQuickReplyByName'] = async(name, args = {}) => {
window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => {
let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
.map(it=>it.set.qrList)
.flat()
@ -191,7 +191,7 @@ const init = async () => {
}
}
if (qr && qr.onExecute) {
return await qr.execute(args, false, true);
return await qr.execute(args, false, true, options);
} else {
throw new Error(`No Quick Reply found for "${name}".`);
}

View File

@ -0,0 +1,769 @@
var DOCUMENT_FRAGMENT_NODE = 11;
function morphAttrs(fromNode, toNode) {
var toNodeAttrs = toNode.attributes;
var attr;
var attrName;
var attrNamespaceURI;
var attrValue;
var fromValue;
// document-fragments dont have attributes so lets not do anything
if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE || fromNode.nodeType === DOCUMENT_FRAGMENT_NODE) {
return;
}
// update attributes on original DOM element
for (var i = toNodeAttrs.length - 1; i >= 0; i--) {
attr = toNodeAttrs[i];
attrName = attr.name;
attrNamespaceURI = attr.namespaceURI;
attrValue = attr.value;
if (attrNamespaceURI) {
attrName = attr.localName || attrName;
fromValue = fromNode.getAttributeNS(attrNamespaceURI, attrName);
if (fromValue !== attrValue) {
if (attr.prefix === 'xmlns'){
attrName = attr.name; // It's not allowed to set an attribute with the XMLNS namespace without specifying the `xmlns` prefix
}
fromNode.setAttributeNS(attrNamespaceURI, attrName, attrValue);
}
} else {
fromValue = fromNode.getAttribute(attrName);
if (fromValue !== attrValue) {
fromNode.setAttribute(attrName, attrValue);
}
}
}
// Remove any extra attributes found on the original DOM element that
// weren't found on the target element.
var fromNodeAttrs = fromNode.attributes;
for (var d = fromNodeAttrs.length - 1; d >= 0; d--) {
attr = fromNodeAttrs[d];
attrName = attr.name;
attrNamespaceURI = attr.namespaceURI;
if (attrNamespaceURI) {
attrName = attr.localName || attrName;
if (!toNode.hasAttributeNS(attrNamespaceURI, attrName)) {
fromNode.removeAttributeNS(attrNamespaceURI, attrName);
}
} else {
if (!toNode.hasAttribute(attrName)) {
fromNode.removeAttribute(attrName);
}
}
}
}
var range; // Create a range object for efficently rendering strings to elements.
var NS_XHTML = 'http://www.w3.org/1999/xhtml';
var doc = typeof document === 'undefined' ? undefined : document;
var HAS_TEMPLATE_SUPPORT = !!doc && 'content' in doc.createElement('template');
var HAS_RANGE_SUPPORT = !!doc && doc.createRange && 'createContextualFragment' in doc.createRange();
function createFragmentFromTemplate(str) {
var template = doc.createElement('template');
template.innerHTML = str;
return template.content.childNodes[0];
}
function createFragmentFromRange(str) {
if (!range) {
range = doc.createRange();
range.selectNode(doc.body);
}
var fragment = range.createContextualFragment(str);
return fragment.childNodes[0];
}
function createFragmentFromWrap(str) {
var fragment = doc.createElement('body');
fragment.innerHTML = str;
return fragment.childNodes[0];
}
/**
* This is about the same
* var html = new DOMParser().parseFromString(str, 'text/html');
* return html.body.firstChild;
*
* @method toElement
* @param {String} str
*/
function toElement(str) {
str = str.trim();
if (HAS_TEMPLATE_SUPPORT) {
// avoid restrictions on content for things like `<tr><th>Hi</th></tr>` which
// createContextualFragment doesn't support
// <template> support not available in IE
return createFragmentFromTemplate(str);
} else if (HAS_RANGE_SUPPORT) {
return createFragmentFromRange(str);
}
return createFragmentFromWrap(str);
}
/**
* Returns true if two node's names are the same.
*
* NOTE: We don't bother checking `namespaceURI` because you will never find two HTML elements with the same
* nodeName and different namespace URIs.
*
* @param {Element} a
* @param {Element} b The target element
* @return {boolean}
*/
function compareNodeNames(fromEl, toEl) {
var fromNodeName = fromEl.nodeName;
var toNodeName = toEl.nodeName;
var fromCodeStart, toCodeStart;
if (fromNodeName === toNodeName) {
return true;
}
fromCodeStart = fromNodeName.charCodeAt(0);
toCodeStart = toNodeName.charCodeAt(0);
// If the target element is a virtual DOM node or SVG node then we may
// need to normalize the tag name before comparing. Normal HTML elements that are
// in the "http://www.w3.org/1999/xhtml"
// are converted to upper case
if (fromCodeStart <= 90 && toCodeStart >= 97) { // from is upper and to is lower
return fromNodeName === toNodeName.toUpperCase();
} else if (toCodeStart <= 90 && fromCodeStart >= 97) { // to is upper and from is lower
return toNodeName === fromNodeName.toUpperCase();
} else {
return false;
}
}
/**
* Create an element, optionally with a known namespace URI.
*
* @param {string} name the element name, e.g. 'div' or 'svg'
* @param {string} [namespaceURI] the element's namespace URI, i.e. the value of
* its `xmlns` attribute or its inferred namespace.
*
* @return {Element}
*/
function createElementNS(name, namespaceURI) {
return !namespaceURI || namespaceURI === NS_XHTML ?
doc.createElement(name) :
doc.createElementNS(namespaceURI, name);
}
/**
* Copies the children of one DOM element to another DOM element
*/
function moveChildren(fromEl, toEl) {
var curChild = fromEl.firstChild;
while (curChild) {
var nextChild = curChild.nextSibling;
toEl.appendChild(curChild);
curChild = nextChild;
}
return toEl;
}
function syncBooleanAttrProp(fromEl, toEl, name) {
if (fromEl[name] !== toEl[name]) {
fromEl[name] = toEl[name];
if (fromEl[name]) {
fromEl.setAttribute(name, '');
} else {
fromEl.removeAttribute(name);
}
}
}
var specialElHandlers = {
OPTION: function(fromEl, toEl) {
var parentNode = fromEl.parentNode;
if (parentNode) {
var parentName = parentNode.nodeName.toUpperCase();
if (parentName === 'OPTGROUP') {
parentNode = parentNode.parentNode;
parentName = parentNode && parentNode.nodeName.toUpperCase();
}
if (parentName === 'SELECT' && !parentNode.hasAttribute('multiple')) {
if (fromEl.hasAttribute('selected') && !toEl.selected) {
// Workaround for MS Edge bug where the 'selected' attribute can only be
// removed if set to a non-empty value:
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12087679/
fromEl.setAttribute('selected', 'selected');
fromEl.removeAttribute('selected');
}
// We have to reset select element's selectedIndex to -1, otherwise setting
// fromEl.selected using the syncBooleanAttrProp below has no effect.
// The correct selectedIndex will be set in the SELECT special handler below.
parentNode.selectedIndex = -1;
}
}
syncBooleanAttrProp(fromEl, toEl, 'selected');
},
/**
* The "value" attribute is special for the <input> element since it sets
* the initial value. Changing the "value" attribute without changing the
* "value" property will have no effect since it is only used to the set the
* initial value. Similar for the "checked" attribute, and "disabled".
*/
INPUT: function(fromEl, toEl) {
syncBooleanAttrProp(fromEl, toEl, 'checked');
syncBooleanAttrProp(fromEl, toEl, 'disabled');
if (fromEl.value !== toEl.value) {
fromEl.value = toEl.value;
}
if (!toEl.hasAttribute('value')) {
fromEl.removeAttribute('value');
}
},
TEXTAREA: function(fromEl, toEl) {
var newValue = toEl.value;
if (fromEl.value !== newValue) {
fromEl.value = newValue;
}
var firstChild = fromEl.firstChild;
if (firstChild) {
// Needed for IE. Apparently IE sets the placeholder as the
// node value and vise versa. This ignores an empty update.
var oldValue = firstChild.nodeValue;
if (oldValue == newValue || (!newValue && oldValue == fromEl.placeholder)) {
return;
}
firstChild.nodeValue = newValue;
}
},
SELECT: function(fromEl, toEl) {
if (!toEl.hasAttribute('multiple')) {
var selectedIndex = -1;
var i = 0;
// We have to loop through children of fromEl, not toEl since nodes can be moved
// from toEl to fromEl directly when morphing.
// At the time this special handler is invoked, all children have already been morphed
// and appended to / removed from fromEl, so using fromEl here is safe and correct.
var curChild = fromEl.firstChild;
var optgroup;
var nodeName;
while(curChild) {
nodeName = curChild.nodeName && curChild.nodeName.toUpperCase();
if (nodeName === 'OPTGROUP') {
optgroup = curChild;
curChild = optgroup.firstChild;
} else {
if (nodeName === 'OPTION') {
if (curChild.hasAttribute('selected')) {
selectedIndex = i;
break;
}
i++;
}
curChild = curChild.nextSibling;
if (!curChild && optgroup) {
curChild = optgroup.nextSibling;
optgroup = null;
}
}
}
fromEl.selectedIndex = selectedIndex;
}
}
};
var ELEMENT_NODE = 1;
var DOCUMENT_FRAGMENT_NODE$1 = 11;
var TEXT_NODE = 3;
var COMMENT_NODE = 8;
function noop() {}
function defaultGetNodeKey(node) {
if (node) {
return (node.getAttribute && node.getAttribute('id')) || node.id;
}
}
function morphdomFactory(morphAttrs) {
return function morphdom(fromNode, toNode, options) {
if (!options) {
options = {};
}
if (typeof toNode === 'string') {
if (fromNode.nodeName === '#document' || fromNode.nodeName === 'HTML' || fromNode.nodeName === 'BODY') {
var toNodeHtml = toNode;
toNode = doc.createElement('html');
toNode.innerHTML = toNodeHtml;
} else {
toNode = toElement(toNode);
}
} else if (toNode.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
toNode = toNode.firstElementChild;
}
var getNodeKey = options.getNodeKey || defaultGetNodeKey;
var onBeforeNodeAdded = options.onBeforeNodeAdded || noop;
var onNodeAdded = options.onNodeAdded || noop;
var onBeforeElUpdated = options.onBeforeElUpdated || noop;
var onElUpdated = options.onElUpdated || noop;
var onBeforeNodeDiscarded = options.onBeforeNodeDiscarded || noop;
var onNodeDiscarded = options.onNodeDiscarded || noop;
var onBeforeElChildrenUpdated = options.onBeforeElChildrenUpdated || noop;
var skipFromChildren = options.skipFromChildren || noop;
var addChild = options.addChild || function(parent, child){ return parent.appendChild(child); };
var childrenOnly = options.childrenOnly === true;
// This object is used as a lookup to quickly find all keyed elements in the original DOM tree.
var fromNodesLookup = Object.create(null);
var keyedRemovalList = [];
function addKeyedRemoval(key) {
keyedRemovalList.push(key);
}
function walkDiscardedChildNodes(node, skipKeyedNodes) {
if (node.nodeType === ELEMENT_NODE) {
var curChild = node.firstChild;
while (curChild) {
var key = undefined;
if (skipKeyedNodes && (key = getNodeKey(curChild))) {
// If we are skipping keyed nodes then we add the key
// to a list so that it can be handled at the very end.
addKeyedRemoval(key);
} else {
// Only report the node as discarded if it is not keyed. We do this because
// at the end we loop through all keyed elements that were unmatched
// and then discard them in one final pass.
onNodeDiscarded(curChild);
if (curChild.firstChild) {
walkDiscardedChildNodes(curChild, skipKeyedNodes);
}
}
curChild = curChild.nextSibling;
}
}
}
/**
* Removes a DOM node out of the original DOM
*
* @param {Node} node The node to remove
* @param {Node} parentNode The nodes parent
* @param {Boolean} skipKeyedNodes If true then elements with keys will be skipped and not discarded.
* @return {undefined}
*/
function removeNode(node, parentNode, skipKeyedNodes) {
if (onBeforeNodeDiscarded(node) === false) {
return;
}
if (parentNode) {
parentNode.removeChild(node);
}
onNodeDiscarded(node);
walkDiscardedChildNodes(node, skipKeyedNodes);
}
// // TreeWalker implementation is no faster, but keeping this around in case this changes in the future
// function indexTree(root) {
// var treeWalker = document.createTreeWalker(
// root,
// NodeFilter.SHOW_ELEMENT);
//
// var el;
// while((el = treeWalker.nextNode())) {
// var key = getNodeKey(el);
// if (key) {
// fromNodesLookup[key] = el;
// }
// }
// }
// // NodeIterator implementation is no faster, but keeping this around in case this changes in the future
//
// function indexTree(node) {
// var nodeIterator = document.createNodeIterator(node, NodeFilter.SHOW_ELEMENT);
// var el;
// while((el = nodeIterator.nextNode())) {
// var key = getNodeKey(el);
// if (key) {
// fromNodesLookup[key] = el;
// }
// }
// }
function indexTree(node) {
if (node.nodeType === ELEMENT_NODE || node.nodeType === DOCUMENT_FRAGMENT_NODE$1) {
var curChild = node.firstChild;
while (curChild) {
var key = getNodeKey(curChild);
if (key) {
fromNodesLookup[key] = curChild;
}
// Walk recursively
indexTree(curChild);
curChild = curChild.nextSibling;
}
}
}
indexTree(fromNode);
function handleNodeAdded(el) {
onNodeAdded(el);
var curChild = el.firstChild;
while (curChild) {
var nextSibling = curChild.nextSibling;
var key = getNodeKey(curChild);
if (key) {
var unmatchedFromEl = fromNodesLookup[key];
// if we find a duplicate #id node in cache, replace `el` with cache value
// and morph it to the child node.
if (unmatchedFromEl && compareNodeNames(curChild, unmatchedFromEl)) {
curChild.parentNode.replaceChild(unmatchedFromEl, curChild);
morphEl(unmatchedFromEl, curChild);
} else {
handleNodeAdded(curChild);
}
} else {
// recursively call for curChild and it's children to see if we find something in
// fromNodesLookup
handleNodeAdded(curChild);
}
curChild = nextSibling;
}
}
function cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey) {
// We have processed all of the "to nodes". If curFromNodeChild is
// non-null then we still have some from nodes left over that need
// to be removed
while (curFromNodeChild) {
var fromNextSibling = curFromNodeChild.nextSibling;
if ((curFromNodeKey = getNodeKey(curFromNodeChild))) {
// Since the node is keyed it might be matched up later so we defer
// the actual removal to later
addKeyedRemoval(curFromNodeKey);
} else {
// NOTE: we skip nested keyed nodes from being removed since there is
// still a chance they will be matched up later
removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
}
curFromNodeChild = fromNextSibling;
}
}
function morphEl(fromEl, toEl, childrenOnly) {
var toElKey = getNodeKey(toEl);
if (toElKey) {
// If an element with an ID is being morphed then it will be in the final
// DOM so clear it out of the saved elements collection
delete fromNodesLookup[toElKey];
}
if (!childrenOnly) {
// optional
var beforeUpdateResult = onBeforeElUpdated(fromEl, toEl);
if (beforeUpdateResult === false) {
return;
} else if (beforeUpdateResult instanceof HTMLElement) {
fromEl = beforeUpdateResult;
// reindex the new fromEl in case it's not in the same
// tree as the original fromEl
// (Phoenix LiveView sometimes returns a cloned tree,
// but keyed lookups would still point to the original tree)
indexTree(fromEl);
}
// update attributes on original DOM element first
morphAttrs(fromEl, toEl);
// optional
onElUpdated(fromEl);
if (onBeforeElChildrenUpdated(fromEl, toEl) === false) {
return;
}
}
if (fromEl.nodeName !== 'TEXTAREA') {
morphChildren(fromEl, toEl);
} else {
specialElHandlers.TEXTAREA(fromEl, toEl);
}
}
function morphChildren(fromEl, toEl) {
var skipFrom = skipFromChildren(fromEl, toEl);
var curToNodeChild = toEl.firstChild;
var curFromNodeChild = fromEl.firstChild;
var curToNodeKey;
var curFromNodeKey;
var fromNextSibling;
var toNextSibling;
var matchingFromEl;
// walk the children
outer: while (curToNodeChild) {
toNextSibling = curToNodeChild.nextSibling;
curToNodeKey = getNodeKey(curToNodeChild);
// walk the fromNode children all the way through
while (!skipFrom && curFromNodeChild) {
fromNextSibling = curFromNodeChild.nextSibling;
if (curToNodeChild.isSameNode && curToNodeChild.isSameNode(curFromNodeChild)) {
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
curFromNodeKey = getNodeKey(curFromNodeChild);
var curFromNodeType = curFromNodeChild.nodeType;
// this means if the curFromNodeChild doesnt have a match with the curToNodeChild
var isCompatible = undefined;
if (curFromNodeType === curToNodeChild.nodeType) {
if (curFromNodeType === ELEMENT_NODE) {
// Both nodes being compared are Element nodes
if (curToNodeKey) {
// The target node has a key so we want to match it up with the correct element
// in the original DOM tree
if (curToNodeKey !== curFromNodeKey) {
// The current element in the original DOM tree does not have a matching key so
// let's check our lookup to see if there is a matching element in the original
// DOM tree
if ((matchingFromEl = fromNodesLookup[curToNodeKey])) {
if (fromNextSibling === matchingFromEl) {
// Special case for single element removals. To avoid removing the original
// DOM node out of the tree (since that can break CSS transitions, etc.),
// we will instead discard the current node and wait until the next
// iteration to properly match up the keyed target element with its matching
// element in the original tree
isCompatible = false;
} else {
// We found a matching keyed element somewhere in the original DOM tree.
// Let's move the original DOM node into the current position and morph
// it.
// NOTE: We use insertBefore instead of replaceChild because we want to go through
// the `removeNode()` function for the node that is being discarded so that
// all lifecycle hooks are correctly invoked
fromEl.insertBefore(matchingFromEl, curFromNodeChild);
// fromNextSibling = curFromNodeChild.nextSibling;
if (curFromNodeKey) {
// Since the node is keyed it might be matched up later so we defer
// the actual removal to later
addKeyedRemoval(curFromNodeKey);
} else {
// NOTE: we skip nested keyed nodes from being removed since there is
// still a chance they will be matched up later
removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
}
curFromNodeChild = matchingFromEl;
curFromNodeKey = getNodeKey(curFromNodeChild);
}
} else {
// The nodes are not compatible since the "to" node has a key and there
// is no matching keyed node in the source tree
isCompatible = false;
}
}
} else if (curFromNodeKey) {
// The original has a key
isCompatible = false;
}
isCompatible = isCompatible !== false && compareNodeNames(curFromNodeChild, curToNodeChild);
if (isCompatible) {
// We found compatible DOM elements so transform
// the current "from" node to match the current
// target DOM node.
// MORPH
morphEl(curFromNodeChild, curToNodeChild);
}
} else if (curFromNodeType === TEXT_NODE || curFromNodeType == COMMENT_NODE) {
// Both nodes being compared are Text or Comment nodes
isCompatible = true;
// Simply update nodeValue on the original node to
// change the text value
if (curFromNodeChild.nodeValue !== curToNodeChild.nodeValue) {
curFromNodeChild.nodeValue = curToNodeChild.nodeValue;
}
}
}
if (isCompatible) {
// Advance both the "to" child and the "from" child since we found a match
// Nothing else to do as we already recursively called morphChildren above
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
continue outer;
}
// No compatible match so remove the old node from the DOM and continue trying to find a
// match in the original DOM. However, we only do this if the from node is not keyed
// since it is possible that a keyed node might match up with a node somewhere else in the
// target tree and we don't want to discard it just yet since it still might find a
// home in the final DOM tree. After everything is done we will remove any keyed nodes
// that didn't find a home
if (curFromNodeKey) {
// Since the node is keyed it might be matched up later so we defer
// the actual removal to later
addKeyedRemoval(curFromNodeKey);
} else {
// NOTE: we skip nested keyed nodes from being removed since there is
// still a chance they will be matched up later
removeNode(curFromNodeChild, fromEl, true /* skip keyed nodes */);
}
curFromNodeChild = fromNextSibling;
} // END: while(curFromNodeChild) {}
// If we got this far then we did not find a candidate match for
// our "to node" and we exhausted all of the children "from"
// nodes. Therefore, we will just append the current "to" node
// to the end
if (curToNodeKey && (matchingFromEl = fromNodesLookup[curToNodeKey]) && compareNodeNames(matchingFromEl, curToNodeChild)) {
// MORPH
if(!skipFrom){ addChild(fromEl, matchingFromEl); }
morphEl(matchingFromEl, curToNodeChild);
} else {
var onBeforeNodeAddedResult = onBeforeNodeAdded(curToNodeChild);
if (onBeforeNodeAddedResult !== false) {
if (onBeforeNodeAddedResult) {
curToNodeChild = onBeforeNodeAddedResult;
}
if (curToNodeChild.actualize) {
curToNodeChild = curToNodeChild.actualize(fromEl.ownerDocument || doc);
}
addChild(fromEl, curToNodeChild);
handleNodeAdded(curToNodeChild);
}
}
curToNodeChild = toNextSibling;
curFromNodeChild = fromNextSibling;
}
cleanupFromEl(fromEl, curFromNodeChild, curFromNodeKey);
var specialElHandler = specialElHandlers[fromEl.nodeName];
if (specialElHandler) {
specialElHandler(fromEl, toEl);
}
} // END: morphChildren(...)
var morphedNode = fromNode;
var morphedNodeType = morphedNode.nodeType;
var toNodeType = toNode.nodeType;
if (!childrenOnly) {
// Handle the case where we are given two DOM nodes that are not
// compatible (e.g. <div> --> <span> or <div> --> TEXT)
if (morphedNodeType === ELEMENT_NODE) {
if (toNodeType === ELEMENT_NODE) {
if (!compareNodeNames(fromNode, toNode)) {
onNodeDiscarded(fromNode);
morphedNode = moveChildren(fromNode, createElementNS(toNode.nodeName, toNode.namespaceURI));
}
} else {
// Going from an element node to a text node
morphedNode = toNode;
}
} else if (morphedNodeType === TEXT_NODE || morphedNodeType === COMMENT_NODE) { // Text or comment node
if (toNodeType === morphedNodeType) {
if (morphedNode.nodeValue !== toNode.nodeValue) {
morphedNode.nodeValue = toNode.nodeValue;
}
return morphedNode;
} else {
// Text node to something else
morphedNode = toNode;
}
}
}
if (morphedNode === toNode) {
// The "to node" was not compatible with the "from node" so we had to
// toss out the "from node" and use the "to node"
onNodeDiscarded(fromNode);
} else {
if (toNode.isSameNode && toNode.isSameNode(morphedNode)) {
return;
}
morphEl(morphedNode, toNode, childrenOnly);
// We now need to loop over any keyed nodes that might need to be
// removed. We only do the removal if we know that the keyed node
// never found a match. When a keyed node is matched up we remove
// it out of fromNodesLookup and we use fromNodesLookup to determine
// if a keyed node has been matched up or not
if (keyedRemovalList) {
for (var i=0, len=keyedRemovalList.length; i<len; i++) {
var elToRemove = fromNodesLookup[keyedRemovalList[i]];
if (elToRemove) {
removeNode(elToRemove, elToRemove.parentNode, false);
}
}
}
}
if (!childrenOnly && morphedNode !== fromNode && fromNode.parentNode) {
if (morphedNode.actualize) {
morphedNode = morphedNode.actualize(fromNode.ownerDocument || doc);
}
// If we had to swap out the from node with a new node because the old
// node was not compatible with the target node then we need to
// replace the old DOM node in the original DOM tree. This is only
// possible if the original DOM node was part of a DOM tree which
// we know is the case if it has a parent node.
fromNode.parentNode.replaceChild(morphedNode, fromNode);
}
return morphedNode;
};
}
var morphdom = morphdomFactory(morphAttrs);
export default morphdom;

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Patrick Steele-Idem <pnidem@gmail.com> (psteeleidem.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@ -60,7 +60,12 @@ export class QuickReplyConfig {
/**@type {HTMLElement}*/
this.setListDom = root.querySelector('.qr--setList');
root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
this.addSet(QuickReplySet.list[0]);
const newSet = QuickReplySet.list.find(qr=>!this.setList.find(qrl=>qrl.set == qr));
if (newSet) {
this.addSet(newSet);
} else {
toastr.warning('All existing QR Sets have already been added.');
}
});
this.updateSetListDom();
}

View File

@ -1,7 +1,9 @@
import { getRequestHeaders, substituteParams } from '../../../../script.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { debounceAsync, warn } from '../index.js';
import { debounceAsync, log, warn } from '../index.js';
import { QuickReply } from './QuickReply.js';
export class QuickReplySet {
@ -16,7 +18,7 @@ export class QuickReplySet {
}
/**
* @param {String} name - name of the QuickReplySet
* @param {string} name - name of the QuickReplySet
*/
static get(name) {
return this.list.find(it=>it.name == name);
@ -25,17 +27,19 @@ export class QuickReplySet {
/**@type {String}*/ name;
/**@type {Boolean}*/ disableSend = false;
/**@type {Boolean}*/ placeBeforeInput = false;
/**@type {Boolean}*/ injectInput = false;
/**@type {string}*/ name;
/**@type {boolean}*/ disableSend = false;
/**@type {boolean}*/ placeBeforeInput = false;
/**@type {boolean}*/ injectInput = false;
/**@type {string}*/ color = 'transparent';
/**@type {boolean}*/ onlyBorderColor = false;
/**@type {QuickReply[]}*/ qrList = [];
/**@type {Number}*/ idIndex = 0;
/**@type {number}*/ idIndex = 0;
/**@type {Boolean}*/ isDeleted = false;
/**@type {boolean}*/ isDeleted = false;
/**@type {Function}*/ save;
/**@type {function}*/ save;
/**@type {HTMLElement}*/ dom;
/**@type {HTMLElement}*/ settingsDom;
@ -64,6 +68,7 @@ export class QuickReplySet {
const root = document.createElement('div'); {
this.dom = root;
root.classList.add('qr--buttons');
this.updateColor();
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
root.append(qr.render());
});
@ -78,6 +83,22 @@ export class QuickReplySet {
this.dom.append(qr.render());
});
}
updateColor() {
if (!this.dom) return;
if (this.color && this.color != 'transparent') {
this.dom.style.setProperty('--qr--color', this.color);
this.dom.classList.add('qr--color');
if (this.onlyBorderColor) {
this.dom.classList.add('qr--borderColor');
} else {
this.dom.classList.remove('qr--borderColor');
}
} else {
this.dom.style.setProperty('--qr--color', 'transparent');
this.dom.classList.remove('qr--color');
this.dom.classList.remove('qr--borderColor');
}
}
@ -93,6 +114,11 @@ export class QuickReplySet {
}
return this.settingsDom;
}
/**
*
* @param {QuickReply} qr
* @param {number} idx
*/
renderSettingsItem(qr, idx) {
this.settingsDom.append(qr.renderSettings(idx));
}
@ -100,6 +126,18 @@ export class QuickReplySet {
/**
*
* @param {QuickReply} qr
*/
async debug(qr) {
const parser = new SlashCommandParser();
const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController);
closure.source = `${this.name}.${qr.label}`;
closure.onProgress = (done, total) => qr.updateEditorProgress(done, total);
closure.scope.setMacro('arg::*', '');
return (await closure.execute())?.pipe;
}
/**
*
* @param {QuickReply} qr The QR to execute.
@ -109,6 +147,7 @@ export class QuickReplySet {
* @param {boolean} [options.isEditor] (false) whether the execution is triggered by the QR editor
* @param {boolean} [options.isRun] (false) whether the execution is triggered by /run or /: (window.executeQuickReplyByName)
* @param {SlashCommandScope} [options.scope] (null) scope to be used when running the command
* @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
* @returns
*/
async executeWithOptions(qr, options = {}) {
@ -118,7 +157,9 @@ export class QuickReplySet {
isEditor:false,
isRun:false,
scope:null,
executionOptions:{},
}, options);
const execOptions = options.executionOptions;
/**@type {HTMLTextAreaElement}*/
const ta = document.querySelector('#send_textarea');
const finalMessage = options.message ?? qr.message;
@ -136,21 +177,24 @@ export class QuickReplySet {
if (input[0] == '/' && !this.disableSend) {
let result;
if (options.isAutoExecute || options.isRun) {
result = await executeSlashCommandsWithOptions(input, {
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
handleParserErrors: true,
scope: options.scope,
});
source: `${this.name}.${qr.label}`,
}));
} else if (options.isEditor) {
result = await executeSlashCommandsWithOptions(input, {
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
handleParserErrors: false,
scope: options.scope,
abortController: qr.abortController,
source: `${this.name}.${qr.label}`,
onProgress: (done, total) => qr.updateEditorProgress(done, total),
});
}));
} else {
result = await executeSlashCommandsOnChatInput(input, {
result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
scope: options.scope,
});
source: `${this.name}.${qr.label}`,
}));
}
return typeof result === 'object' ? result?.pipe : '';
}
@ -165,7 +209,7 @@ export class QuickReplySet {
}
/**
* @param {QuickReply} qr
* @param {String} [message] - optional altered message to be used
* @param {string} [message] - optional altered message to be used
* @param {SlashCommandScope} [scope] - optional scope to be used when running the command
*/
async execute(qr, message = null, isAutoExecute = false, scope = null) {
@ -179,10 +223,11 @@ export class QuickReplySet {
addQuickReply() {
addQuickReply(data = {}) {
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
data.id =
this.idIndex = id + 1;
const qr = QuickReply.from({ id });
const qr = QuickReply.from(data);
this.qrList.push(qr);
this.hookQuickReply(qr);
if (this.settingsDom) {
@ -194,11 +239,131 @@ export class QuickReplySet {
this.save();
return qr;
}
addQuickReplyFromText(qrJson) {
let data;
if (qrJson) {
try {
data = JSON.parse(qrJson ?? '{}');
delete data.id;
} catch {
// not JSON data
}
if (data) {
// JSON data
if (data.label === undefined || data.message === undefined) {
// not a QR
toastr.error('Not a QR.');
return;
}
} else {
// no JSON, use plaintext as QR message
data = { message: qrJson };
}
} else {
data = {};
}
const newQr = this.addQuickReply(data);
return newQr;
}
/**
*
* @param {QuickReply} qr
*/
hookQuickReply(qr) {
qr.onDebug = ()=>this.debug(qr);
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
qr.onDelete = ()=>this.removeQuickReply(qr);
qr.onUpdate = ()=>this.save();
qr.onInsertBefore = (qrJson)=>{
this.addQuickReplyFromText(qrJson);
const newQr = this.qrList.pop();
this.qrList.splice(this.qrList.indexOf(qr), 0, newQr);
if (qr.settingsDom) {
qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom);
}
this.save();
};
qr.onTransfer = async()=>{
/**@type {HTMLSelectElement} */
let sel;
let isCopy = false;
const dom = document.createElement('div'); {
dom.classList.add('qr--transferModal');
const title = document.createElement('h3'); {
title.textContent = 'Transfer Quick Reply';
dom.append(title);
}
const subTitle = document.createElement('h4'); {
const entryName = qr.label;
const bookName = this.name;
subTitle.textContent = `${bookName}: ${entryName}`;
dom.append(subTitle);
}
sel = document.createElement('select'); {
sel.classList.add('qr--transferSelect');
sel.setAttribute('autofocus', '1');
const noOpt = document.createElement('option'); {
noOpt.value = '';
noOpt.textContent = '-- Select QR Set --';
sel.append(noOpt);
}
for (const qrs of QuickReplySet.list) {
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;
sel.append(opt);
}
}
sel.addEventListener('keyup', (evt)=>{
if (evt.key == 'Shift') {
(dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
return;
}
});
sel.addEventListener('keydown', (evt)=>{
if (evt.key == 'Shift') {
(dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
return;
}
if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') {
evt.preventDefault();
if (evt.shiftKey) isCopy = true;
dlg.completeAffirmative();
}
});
dom.append(sel);
}
const hintP = document.createElement('p'); {
const hint = document.createElement('small'); {
hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.';
hintP.append(hint);
}
dom.append(hintP);
}
}
const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' });
const copyBtn = document.createElement('div'); {
copyBtn.classList.add('qr--copy');
copyBtn.classList.add('menu_button');
copyBtn.textContent = 'Copy';
copyBtn.addEventListener('click', ()=>{
isCopy = true;
dlg.completeAffirmative();
});
(dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
}
const prom = dlg.show();
sel.focus();
await prom;
if (dlg.result == POPUP_RESULT.AFFIRMATIVE) {
const qrs = QuickReplySet.list.find(it=>it.name == sel.value);
qrs.addQuickReply(qr.toJSON());
if (!isCopy) {
qr.delete();
}
}
};
}
removeQuickReply(qr) {
@ -214,6 +379,8 @@ export class QuickReplySet {
disableSend: this.disableSend,
placeBeforeInput: this.placeBeforeInput,
injectInput: this.injectInput,
color: this.color,
onlyBorderColor: this.onlyBorderColor,
qrList: this.qrList,
idIndex: this.idIndex,
};
@ -245,8 +412,12 @@ export class QuickReplySet {
if (response.ok) {
this.unrender();
const idx = QuickReplySet.list.indexOf(this);
QuickReplySet.list.splice(idx, 1);
this.isDeleted = true;
if (idx > -1) {
QuickReplySet.list.splice(idx, 1);
this.isDeleted = true;
} else {
warn(`Deleted Quick Reply Set was not found in the list of sets: ${this.name}`);
}
} else {
warn(`Failed to delete Quick Reply Set: ${this.name}`);
}

View File

@ -45,7 +45,7 @@ export class QuickReplySetLink {
this.set = QuickReplySet.get(set.value);
this.update();
});
QuickReplySet.list.forEach(qrs=>{
QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;

View File

@ -1,8 +1,12 @@
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 { enumIcons } from '../../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandDebugController } from '../../../slash-commands/SlashCommandDebugController.js';
import { SlashCommandEnumValue, enumTypes } from '../../../slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
import { isTrueBoolean } from '../../../utils.js';
// eslint-disable-next-line no-unused-vars
import { QuickReplyApi } from '../api/QuickReplyApi.js';
@ -47,6 +51,13 @@ export class SlashCommandHandler {
return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr);
}) ?? [],
/** All QRs inside a set, utilizing the "set" named argument, returns the QR's ID */
qrIds: (executor) => QuickReplySet.get(String(executor.namedArgumentList.find(x => x.name == 'set')?.value))?.qrList.map(qr => {
const icons = getExecutionIcons(qr);
const message = `${qr.automationId ? `[${qr.automationId}]` : ''}${icons ? `[auto: ${icons}]` : ''} ${qr.title || qr.message}`.trim();
return new SlashCommandEnumValue(qr.label, message, enumTypes.enum, enumIcons.qr, null, ()=>qr.id.toString(), true);
}) ?? [],
/** All QRs as a set.name string, to be able to execute, for example via the /run command */
qrExecutables: () => {
const globalSetList = this.api.settings.config.setList;
@ -63,7 +74,7 @@ export class SlashCommandHandler {
...otherQrs.map(x => new SlashCommandEnumValue(`${x.set.name}.${x.qr.label}`, `${x.qr.title || x.qr.message}`, enumTypes.qr, enumIcons.qr)),
];
},
}
};
window['qrEnumProviderExecutables'] = localEnumProviders.qrExecutables;
@ -234,8 +245,20 @@ export class SlashCommandHandler {
name: 'label',
description: 'text on the button, e.g., label=MyButton',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrLabels,
isRequired: false,
enumProvider: localEnumProviders.qrEntries,
}),
SlashCommandNamedArgument.fromProps({
name: 'icon',
description: 'icon to show on the button, e.g., icon=fa-pencil',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
}),
SlashCommandNamedArgument.fromProps({
name: 'showlabel',
description: 'whether to show the label even when an icon is assigned, e.g., icon=fa-pencil showlabel=true',
typeList: [ARGUMENT_TYPE.BOOLEAN],
isRequired: false,
}),
new SlashCommandNamedArgument('hidden', 'whether the button should be hidden, e.g., hidden=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
new SlashCommandNamedArgument('startup', 'auto execute on app startup, e.g., startup=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
@ -247,6 +270,13 @@ export class SlashCommandHandler {
];
const qrUpdateArgs = [
new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
SlashCommandNamedArgument.fromProps({
name: 'id',
description: 'numeric ID of the QR, e.g., id=42',
typeList: [ARGUMENT_TYPE.NUMBER],
isRequired: false,
enumProvider: localEnumProviders.qrIds,
}),
];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
@ -272,13 +302,61 @@ export class SlashCommandHandler {
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-get',
callback: (args, _) => {
return this.getQuickReply(args);
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'set',
description: 'name of the QR set, e.g., set=PresetName1',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
SlashCommandNamedArgument.fromProps({
name: 'label',
description: 'text on the button, e.g., label=MyButton',
typeList: [ARGUMENT_TYPE.STRING],
isRequired: false,
enumProvider: localEnumProviders.qrEntries,
}),
SlashCommandNamedArgument.fromProps({
name: 'id',
description: 'numeric ID of the QR, e.g., id=42',
typeList: [ARGUMENT_TYPE.NUMBER],
isRequired: false,
enumProvider: localEnumProviders.qrIds,
}),
],
returns: 'a dictionary with all the QR\'s properties',
helpString: `
<div>Get a Quick Reply's properties.</div>
<div>
<strong>Examples:</strong>
<ul>
<li>
<pre><code>/qr-get set=MyPreset label=MyButton | /echo</code></pre>
<pre><code>/qr-get set=MyPreset id=42 | /echo</code></pre>
</li>
</ul>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
callback: (args, message) => {
this.updateQuickReply(args, message);
return '';
},
returns: 'updated quick reply',
namedArgumentList: [...qrUpdateArgs, ...qrArgs],
namedArgumentList: [...qrUpdateArgs, ...qrArgs.map(it=>{
if (it.name == 'label') {
const clone = SlashCommandNamedArgument.fromProps(it);
clone.isRequired = false;
return clone;
}
return it;
})],
unnamedArgumentList: [
new SlashCommandArgument('command', [ARGUMENT_TYPE.STRING]),
],
@ -315,6 +393,12 @@ export class SlashCommandHandler {
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries,
}),
SlashCommandNamedArgument.fromProps({
name: 'id',
description: 'numeric ID of the QR, e.g., id=42',
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: localEnumProviders.qrIds,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
@ -344,6 +428,12 @@ export class SlashCommandHandler {
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries,
}),
SlashCommandNamedArgument.fromProps({
name: 'id',
description: 'numeric ID of the QR, e.g., id=42',
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: localEnumProviders.qrIds,
}),
new SlashCommandNamedArgument(
'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
),
@ -389,6 +479,12 @@ export class SlashCommandHandler {
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: localEnumProviders.qrEntries,
}),
SlashCommandNamedArgument.fromProps({
name: 'id',
description: 'numeric ID of the QR, e.g., id=42',
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: localEnumProviders.qrIds,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
@ -425,6 +521,12 @@ export class SlashCommandHandler {
isRequired: true,
enumProvider: localEnumProviders.qrSets,
}),
SlashCommandNamedArgument.fromProps({
name: 'id',
description: 'numeric ID of the QR, e.g., id=42',
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: localEnumProviders.qrIds,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
@ -454,8 +556,8 @@ export class SlashCommandHandler {
new SlashCommandNamedArgument('inject', 'inject user input automatically (if disabled use {{input}})', [ARGUMENT_TYPE.BOOLEAN], false),
];
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-create',
callback: (args, name) => {
this.createSet(name, args);
callback: async (args, name) => {
await this.createSet(name, args);
return '';
},
aliases: ['qr-presetadd'],
@ -485,8 +587,8 @@ export class SlashCommandHandler {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
callback: (args, name) => {
this.updateSet(name, args);
callback: async (args, name) => {
await this.updateSet(name, args);
return '';
},
aliases: ['qr-presetupdate'],
@ -510,8 +612,8 @@ export class SlashCommandHandler {
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
callback: (_, name) => {
this.deleteSet(name);
callback: async (_, name) => {
await this.deleteSet(name);
return '';
},
aliases: ['qr-presetdelete'],
@ -533,6 +635,134 @@ export class SlashCommandHandler {
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-arg',
callback: ({ _scope }, [key, value]) => {
_scope.setMacro(`arg::${key}`, value, key.includes('*'));
return '';
},
unnamedArgumentList: [
SlashCommandArgument.fromProps({ description: 'argument name',
typeList: ARGUMENT_TYPE.STRING,
isRequired: true,
}),
SlashCommandArgument.fromProps({ description: 'argument value',
typeList: [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.BOOLEAN, ARGUMENT_TYPE.LIST, ARGUMENT_TYPE.DICTIONARY],
isRequired: true,
}),
],
splitUnnamedArgument: true,
splitUnnamedArgumentCount: 2,
helpString: `
<div>
Set a fallback value for a Quick Reply argument.
</div>
<div>
<strong>Example:</strong>
<pre><code>/qr-arg x foo |\n/echo {{arg::x}}</code></pre>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'import',
/**
*
* @param {{_scope:SlashCommandScope, _abortController:SlashCommandAbortController, _debugController:SlashCommandDebugController, from:string}} args
* @param {string} value
*/
callback: (args, value) => {
if (!args.from) throw new Error('/import requires from= to be set.');
if (!value) throw new Error('/import requires the unnamed argument to be set.');
let qr = [...this.api.listGlobalSets(), ...this.api.listChatSets()]
.map(it=>this.api.getSetByName(it)?.qrList ?? [])
.flat()
.find(it=>it.label == args.from)
;
if (!qr) {
let [setName, ...qrNameParts] = args.from.split('.');
let qrName = qrNameParts.join('.');
let qrs = QuickReplySet.get(setName);
if (qrs) {
qr = qrs.qrList.find(it=>it.label == qrName);
}
}
if (qr) {
const parser = new SlashCommandParser();
const closure = parser.parse(qr.message, true, [], args._abortController, args._debugController);
if (args._debugController) {
closure.source = args.from;
}
const testCandidates = (executor)=>{
return (
executor.namedArgumentList.find(arg=>arg.name == 'key')
&& executor.unnamedArgumentList.length > 0
&& executor.unnamedArgumentList[0].value instanceof SlashCommandClosure
) || (
!executor.namedArgumentList.find(arg=>arg.name == 'key')
&& executor.unnamedArgumentList.length > 1
&& executor.unnamedArgumentList[1].value instanceof SlashCommandClosure
);
};
const candidates = closure.executorList
.filter(executor=>['let', 'var'].includes(executor.command.name))
.filter(testCandidates)
.map(executor=>({
key: executor.namedArgumentList.find(arg=>arg.name == 'key')?.value ?? executor.unnamedArgumentList[0].value,
value: executor.unnamedArgumentList[executor.namedArgumentList.find(arg=>arg.name == 'key') ? 0 : 1].value,
}))
;
for (let i = 0; i < value.length; i++) {
const srcName = value[i];
let dstName = srcName;
if (i + 2 < value.length && value[i + 1] == 'as') {
dstName = value[i + 2];
i += 2;
}
const pick = candidates.find(it=>it.key == srcName);
if (!pick) throw new Error(`No scoped closure named "${srcName}" found in "${args.from}"`);
if (args._scope.existsVariableInScope(dstName)) {
args._scope.setVariable(dstName, pick.value);
} else {
args._scope.letVariable(dstName, pick.value);
}
}
} else {
throw new Error(`No Quick Reply found for "${name}".`);
}
return '';
},
namedArgumentList: [
SlashCommandNamedArgument.fromProps({ name: 'from',
description: 'Quick Reply to import from (QRSet.QRLabel)',
typeList: ARGUMENT_TYPE.STRING,
isRequired: true,
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({ description: 'what to import (x or x as y)',
acceptsMultiple: true,
typeList: ARGUMENT_TYPE.STRING,
isRequired: true,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Import one or more closures from another Quick Reply.
</div>
<div>
Only imports closures that are directly assigned a scoped variable via <code>/let</code> or <code>/var</code>.
</div>
<div>
<strong>Examples:</strong>
<ul>
<li><pre><code>/import from=LibraryQrSet.FooBar foo |\n/:foo</code></pre></li>
<li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo\n\tbar\n|\n/:foo |\n/:bar</code></pre></li>
<li><pre><code>/import from=LibraryQrSet.FooBar\n\tfoo as x\n\tbar as y\n|\n/:x |\n/:y</code></pre></li>
</ul>
</div>
`,
}));
}
@ -618,6 +848,8 @@ export class SlashCommandHandler {
args.set ?? '',
args.label ?? '',
{
icon: args.icon,
showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
message: message ?? '',
title: args.title,
isHidden: isTrueBoolean(args.hidden),
@ -633,12 +865,21 @@ export class SlashCommandHandler {
toastr.error(ex.message);
}
}
getQuickReply(args) {
try {
return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
} catch (ex) {
toastr.error(ex.message);
}
}
updateQuickReply(args, message) {
try {
this.api.updateQuickReply(
args.set ?? '',
args.label ?? '',
args.id !== undefined ? Number(args.id) : (args.label ?? ''),
{
icon: args.icon,
showLabel: args.showlabel === undefined ? undefined : isTrueBoolean(args.showlabel),
newLabel: args.newlabel,
message: (message ?? '').trim().length > 0 ? message : undefined,
title: args.title,
@ -657,7 +898,7 @@ export class SlashCommandHandler {
}
deleteQuickReply(args, label) {
try {
this.api.deleteQuickReply(args.set, args.label ?? label);
this.api.deleteQuickReply(args.set, args.id !== undefined ? Number(args.id) : (args.label ?? label));
} catch (ex) {
toastr.error(ex.message);
}
@ -692,9 +933,9 @@ export class SlashCommandHandler {
}
createSet(name, args) {
async createSet(name, args) {
try {
this.api.createSet(
await this.api.createSet(
args.name ?? name ?? '',
{
disableSend: isTrueBoolean(args.nosend),
@ -706,9 +947,9 @@ export class SlashCommandHandler {
toastr.error(ex.message);
}
}
updateSet(name, args) {
async updateSet(name, args) {
try {
this.api.updateSet(
await this.api.updateSet(
args.name ?? name ?? '',
{
disableSend: args.nosend !== undefined ? isTrueBoolean(args.nosend) : undefined,
@ -720,9 +961,9 @@ export class SlashCommandHandler {
toastr.error(ex.message);
}
}
deleteSet(name) {
async deleteSet(name) {
try {
this.api.deleteSet(name ?? '');
await this.api.deleteSet(name ?? '');
} catch (ex) {
toastr.error(ex.message);
}

View File

@ -23,6 +23,8 @@ export class SettingsUi {
/**@type {HTMLInputElement}*/ disableSend;
/**@type {HTMLInputElement}*/ placeBeforeInput;
/**@type {HTMLInputElement}*/ injectInput;
/**@type {HTMLInputElement}*/ color;
/**@type {HTMLInputElement}*/ onlyBorderColor;
/**@type {HTMLSelectElement}*/ currentSet;
@ -117,10 +119,29 @@ export class SettingsUi {
this.dom.querySelector('#qr--set-add').addEventListener('click', async()=>{
this.currentQrSet.addQuickReply();
});
this.dom.querySelector('#qr--set-paste').addEventListener('click', async()=>{
const text = await navigator.clipboard.readText();
this.currentQrSet.addQuickReplyFromText(text);
});
this.dom.querySelector('#qr--set-importQr').addEventListener('click', async()=>{
const inp = document.createElement('input'); {
inp.type = 'file';
inp.accept = '.json';
inp.addEventListener('change', async()=>{
if (inp.files.length > 0) {
for (const file of inp.files) {
const text = await file.text();
this.currentQrSet.addQuickReply(JSON.parse(text));
}
}
});
inp.click();
}
});
this.qrList = this.dom.querySelector('#qr--set-qrList');
this.currentSet = this.dom.querySelector('#qr--set');
this.currentSet.addEventListener('change', ()=>this.onQrSetChange());
QuickReplySet.list.forEach(qrs=>{
QuickReplySet.list.toSorted((a,b)=>a.name.toLowerCase().localeCompare(b.name.toLowerCase())).forEach(qrs=>{
const opt = document.createElement('option'); {
opt.value = qrs.name;
opt.textContent = qrs.name;
@ -145,6 +166,34 @@ export class SettingsUi {
qrs.injectInput = this.injectInput.checked;
qrs.save();
});
let initialColorChange = true;
this.color = this.dom.querySelector('#qr--color');
this.color.color = this.currentQrSet?.color ?? 'transparent';
this.color.addEventListener('change', (evt)=>{
if (!this.dom.closest('body')) return;
const qrs = this.currentQrSet;
if (initialColorChange) {
initialColorChange = false;
this.color.color = qrs.color;
return;
}
qrs.color = evt.detail.rgb;
qrs.save();
this.currentQrSet.updateColor();
});
this.dom.querySelector('#qr--colorClear').addEventListener('click', (evt)=>{
const qrs = this.currentQrSet;
this.color.color = 'transparent';
qrs.save();
this.currentQrSet.updateColor();
});
this.onlyBorderColor = this.dom.querySelector('#qr--onlyBorderColor');
this.onlyBorderColor.addEventListener('click', ()=>{
const qrs = this.currentQrSet;
qrs.onlyBorderColor = this.onlyBorderColor.checked;
qrs.save();
this.currentQrSet.updateColor();
});
this.onQrSetChange();
}
onQrSetChange() {
@ -152,6 +201,8 @@ export class SettingsUi {
this.disableSend.checked = this.currentQrSet.disableSend;
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
this.injectInput.checked = this.currentQrSet.injectInput;
this.color.color = this.currentQrSet.color ?? 'transparent';
this.onlyBorderColor.checked = this.currentQrSet.onlyBorderColor;
this.qrList.innerHTML = '';
const qrsDom = this.currentQrSet.renderSettings();
this.qrList.append(qrsDom);
@ -265,7 +316,7 @@ export class SettingsUi {
const qrs = new QuickReplySet();
qrs.name = name;
qrs.addQuickReply();
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(name) == 1);
const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(name.toLowerCase()) == 1);
if (idx > -1) {
QuickReplySet.list.splice(idx, 0, qrs);
} else {
@ -321,7 +372,7 @@ export class SettingsUi {
this.prepareChatSetList();
}
} else {
const idx = QuickReplySet.list.findIndex(it=>it.name.localeCompare(qrs.name) == 1);
const idx = QuickReplySet.list.findIndex(it=>it.name.toLowerCase().localeCompare(qrs.name.toLowerCase()) == 1);
if (idx > -1) {
QuickReplySet.list.splice(idx, 0, qrs);
} else {

View File

@ -33,11 +33,14 @@ export class ContextMenu {
*/
build(qr, chainedMessage = null, hierarchy = [], labelHierarchy = []) {
const tree = {
icon: qr.icon,
showLabel: qr.showLabel,
label: qr.label,
message: (chainedMessage && qr.message ? `${chainedMessage} | ` : '') + qr.message,
children: [],
};
qr.contextList.forEach((cl) => {
if (!cl.set) return;
if (!hierarchy.includes(cl.set)) {
const nextHierarchy = [...hierarchy, cl.set];
const nextLabelHierarchy = [...labelHierarchy, tree.label];
@ -45,6 +48,8 @@ export class ContextMenu {
cl.set.qrList.forEach(subQr => {
const subTree = this.build(subQr, cl.isChained ? tree.message : null, nextHierarchy, nextLabelHierarchy);
tree.children.push(new MenuItem(
subTree.icon,
subTree.showLabel,
subTree.label,
subTree.message,
(evt) => {

View File

@ -2,7 +2,7 @@ import { MenuItem } from './MenuItem.js';
export class MenuHeader extends MenuItem {
constructor(/**@type {String}*/label) {
super(label, null, null);
super(null, null, label, null, null);
}

View File

@ -1,21 +1,34 @@
import { SubMenu } from './SubMenu.js';
export class MenuItem {
/**@type {String}*/ label;
/**@type {Object}*/ value;
/**@type {Function}*/ callback;
/**@type {string}*/ icon;
/**@type {boolean}*/ showLabel;
/**@type {string}*/ label;
/**@type {object}*/ value;
/**@type {function}*/ callback;
/**@type {MenuItem[]}*/ childList = [];
/**@type {SubMenu}*/ subMenu;
/**@type {Boolean}*/ isForceExpanded = false;
/**@type {boolean}*/ isForceExpanded = false;
/**@type {HTMLElement}*/ root;
/**@type {Function}*/ onExpand;
/**@type {function}*/ onExpand;
constructor(/**@type {String}*/label, /**@type {Object}*/value, /**@type {function}*/callback, /**@type {MenuItem[]}*/children = []) {
/**
*
* @param {string} icon
* @param {boolean} showLabel
* @param {string} label
* @param {object} value
* @param {function} callback
* @param {MenuItem[]} children
*/
constructor(icon, showLabel, label, value, callback, children = []) {
this.icon = icon;
this.showLabel = showLabel;
this.label = label;
this.value = value;
this.callback = callback;
@ -33,7 +46,21 @@ export class MenuItem {
if (this.callback) {
item.addEventListener('click', (evt) => this.callback(evt, this));
}
item.append(this.label);
const icon = document.createElement('div'); {
this.domIcon = icon;
icon.classList.add('qr--button-icon');
icon.classList.add('fa-solid');
if (!this.icon) icon.classList.add('qr--hidden');
else icon.classList.add(this.icon);
item.append(icon);
}
const lbl = document.createElement('div'); {
this.domLabel = lbl;
lbl.classList.add('qr--button-label');
if (this.icon && !this.showLabel) lbl.classList.add('qr--hidden');
lbl.textContent = this.label;
item.append(lbl);
}
if (this.childList.length > 0) {
item.classList.add('ctx-has-children');
const sub = new SubMenu(this.childList);

View File

@ -1,3 +1,20 @@
@keyframes qr--success {
0%,
100% {
color: var(--SmartThemeBodyColor);
}
25%,
75% {
color: #51a351;
}
}
.qr--success {
animation-name: qr--success;
animation-duration: 3s;
animation-timing-function: linear;
animation-delay: 0s;
animation-iteration-count: 1;
}
#qr--bar {
outline: none;
margin: 0;
@ -41,6 +58,7 @@
}
#qr--bar > .qr--buttons,
#qr--popout > .qr--body > .qr--buttons {
--qr--color: transparent;
margin: 0;
padding: 0;
display: flex;
@ -49,10 +67,44 @@
gap: 5px;
width: 100%;
}
#qr--bar > .qr--buttons.qr--color,
#qr--popout > .qr--body > .qr--buttons.qr--color {
background-color: var(--qr--color);
}
#qr--bar > .qr--buttons.qr--borderColor,
#qr--popout > .qr--body > .qr--buttons.qr--borderColor {
background-color: transparent;
border-left: 5px solid var(--qr--color);
border-right: 5px solid var(--qr--color);
}
#qr--bar > .qr--buttons:has(.qr--buttons.qr--color),
#qr--popout > .qr--body > .qr--buttons:has(.qr--buttons.qr--color) {
margin: 5px;
}
#qr--bar > .qr--buttons > .qr--buttons,
#qr--popout > .qr--body > .qr--buttons > .qr--buttons {
display: contents;
}
#qr--bar > .qr--buttons > .qr--buttons.qr--color .qr--button:before,
#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color .qr--button:before {
content: '';
background-color: var(--qr--color);
position: absolute;
inset: -5px;
z-index: -1;
}
#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before,
#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor .qr--button:before {
display: none;
}
#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:before,
#qr--bar > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after,
#qr--popout > .qr--body > .qr--buttons > .qr--buttons.qr--color.qr--borderColor:after {
content: '';
width: 5px;
background-color: var(--qr--color);
}
#qr--bar > .qr--buttons .qr--button,
#qr--popout > .qr--body > .qr--buttons .qr--button {
color: var(--SmartThemeBodyColor);
@ -66,11 +118,19 @@
align-items: center;
justify-content: center;
text-align: center;
position: relative;
}
#qr--bar > .qr--buttons .qr--button:hover,
#qr--popout > .qr--body > .qr--buttons .qr--button:hover {
opacity: 1;
filter: brightness(1.2);
background-color: #4d4d4d;
}
#qr--bar > .qr--buttons .qr--button .qr--hidden,
#qr--popout > .qr--body > .qr--buttons .qr--button .qr--hidden {
display: none;
}
#qr--bar > .qr--buttons .qr--button .qr--button-icon,
#qr--popout > .qr--body > .qr--buttons .qr--button .qr--button-icon {
margin: 0 0.5em;
}
#qr--bar > .qr--buttons .qr--button > .qr--button-expander,
#qr--popout > .qr--body > .qr--buttons .qr--button > .qr--button-expander {
@ -170,36 +230,80 @@
#qr--settings #qr--set-qrList .qr--set-qrListContents {
padding: 0 0.5em;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder {
display: flex;
align-items: center;
opacity: 0;
transition: 100ms;
margin: -2px 0 -11px 0;
position: relative;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions {
display: flex;
gap: 0.25em;
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder .qr--actions .qr--action {
margin: 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:before,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:after {
content: "";
display: block;
flex: 1 1 auto;
border: 1px solid;
margin: 0 1em;
height: 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:hover,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemAdder:focus-within {
opacity: 1;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0.25em 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(1) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(2) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(2) {
flex: 1 1 25%;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(3) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(3) {
flex: 0 0 auto;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(4) {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(4) {
flex: 1 1 75%;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > :nth-child(5) {
flex: 0 0 auto;
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > :nth-child(5) {
flex: 0 1 auto;
display: flex;
gap: 0.25em;
justify-content: flex-end;
flex-wrap: wrap;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item > .drag-handle {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content > .drag-handle {
padding: 0.75em;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemLabel,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--action {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer {
display: flex;
align-items: center;
gap: 0.5em;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemIcon:not(.fa-solid) {
display: none;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabelContainer .qr--set-itemLabel {
min-width: 24px;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemLabel,
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--action {
margin: 0;
}
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--set-itemMessage {
#qr--settings #qr--set-qrList .qr--set-qrListContents > .qr--set-item .qr--content .qr--set-itemMessage {
font-size: smaller;
}
#qr--settings .qr--set-qrListActions {
@ -212,6 +316,7 @@
#qr--qrOptions {
display: flex;
flex-direction: column;
padding-right: 1px;
}
#qr--qrOptions > #qr--ctxEditor .qr--ctxItem {
display: flex;
@ -219,6 +324,12 @@
gap: 0.5em;
align-items: baseline;
}
#qr--qrOptions > #qr--autoExec .checkbox_label {
text-wrap: nowrap;
}
#qr--qrOptions > #qr--autoExec .checkbox_label .fa-fw {
margin-right: 2px;
}
@media screen and (max-width: 750px) {
body .popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor {
flex-direction: column;
@ -238,6 +349,72 @@
.popup:has(#qr--modalEditor) {
aspect-ratio: unset;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) {
min-width: unset;
min-height: unset;
height: auto !important;
width: min-content !important;
position: absolute;
right: 1em;
top: 1em;
left: unset;
bottom: unset;
margin: unset;
padding: 0;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized)::backdrop {
backdrop-filter: unset;
background-color: transparent;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-body {
flex: 0 0 auto;
height: min-content;
width: min-content;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content {
flex: 0 0 auto;
margin-top: 0;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor {
max-height: 50vh;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--main,
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--resizeHandle,
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > h3,
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeButtons,
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor > #qr--qrOptions > #qr--modal-executeProgress {
display: none;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--qrOptions {
width: auto;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
display: flex;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
display: none;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting.qr--minimized) .popup-content > #qr--modalEditor #qr--modal-debugState {
padding-top: 0;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting) .popup-controls {
display: none;
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight {
position: absolute;
z-index: 50000;
pointer-events: none;
background-color: rgba(47, 150, 180, 0.5);
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight.qr--unresolved {
background-color: rgba(255, 255, 0, 0.5);
}
.popup:has(#qr--modalEditor):has(.qr--isExecuting) .qr--highlight-secondary {
position: absolute;
z-index: 50000;
pointer-events: none;
border: 3px solid red;
}
.popup:has(#qr--modalEditor) .popup-content {
display: flex;
flex-direction: column;
@ -249,6 +426,67 @@
gap: 1em;
overflow: hidden;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > h3:first-child,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--labels,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > h3:first-child,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > #qr--ctxEditor,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions > .qr--ctxEditorActions + h3 + div {
display: none;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
visibility: hidden;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons {
display: flex;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
transition: 200ms;
border-color: transparent;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
cursor: pointer;
opacity: 1;
pointer-events: all;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
animation-name: qr--debugPulse;
animation-duration: 1500ms;
animation-timing-function: ease-in-out;
animation-delay: 0s;
animation-iteration-count: infinite;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-resume {
border-color: #51a351;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-step {
border-color: var(--SmartThemeQuoteColor);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepInto {
border-color: var(--SmartThemeQuoteColor);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting.qr--isPaused #qr--modal-debugButtons .menu_button:not(#qr--modal-minimize, #qr--modal-maximize)#qr--modal-stepOut {
border-color: var(--SmartThemeQuoteColor);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle {
width: 6px;
background-color: var(--SmartThemeBorderColor);
border: 2px solid var(--SmartThemeBlurTintColor);
transition: border-color 200ms, background-color 200ms;
cursor: w-resize;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--resizeHandle:hover {
background-color: var(--SmartThemeQuoteColor);
border-color: var(--SmartThemeQuoteColor);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor.qr--isExecuting #qr--qrOptions {
width: var(--width, auto);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main {
flex: 1 1 auto;
display: flex;
@ -260,20 +498,114 @@
display: flex;
flex-direction: row;
gap: 0.5em;
padding: 1px;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label {
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label {
flex: 1 1 1px;
display: flex;
flex-direction: column;
position: relative;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelText {
flex: 1 1 auto;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > .qr--labelHint {
flex: 1 1 auto;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label > input {
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label.qr--fit,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label.qr--fit {
flex: 0 0 auto;
justify-content: center;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup {
display: flex;
align-items: baseline;
gap: 0.5em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--inputGroup input,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--inputGroup input {
flex: 1 1 auto;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--labelText,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelText {
flex: 1 1 auto;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--labelHint,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--labelHint {
flex: 1 1 auto;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label input,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label input {
flex: 0 0 auto;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList {
background-color: var(--stcdx--bgColor);
border: 1px solid var(--SmartThemeBorderColor);
backdrop-filter: blur(var(--SmartThemeBlurStrength));
border-radius: 10px;
font-size: smaller;
position: absolute;
top: 100%;
left: 0;
right: 0;
overflow: auto;
margin: 0;
padding: 0.5em;
max-height: 50vh;
list-style: none;
z-index: 40000;
max-width: 100%;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem {
display: flex;
gap: 1em;
text-align: left;
opacity: 0.75;
transition: 200ms;
cursor: pointer;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem:hover,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem:hover {
opacity: 1;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current {
opacity: 1;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--label,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--modal-switcherItem.qr--current .qr--id {
font-weight: bold;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label {
white-space: nowrap;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--label .menu_button,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--label .menu_button {
display: inline-block;
height: min-content;
width: min-content;
margin: 0 0.5em 0 0;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id {
opacity: 0.5;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:before,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:before {
content: "[";
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--id:after,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--id:after {
content: "]";
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > label .qr--modal-switcherList .qr--message,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--labels > .label .qr--modal-switcherList .qr--message {
height: 1lh;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 0.5;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer {
flex: 1 1 auto;
@ -283,8 +615,9 @@
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 1em;
column-gap: 1em;
color: var(--grey70);
font-size: smaller;
align-items: baseline;
@ -308,6 +641,11 @@
background-color: var(--ac-style-color-background);
color: var(--ac-style-color-text);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::-webkit-scrollbar-thumb {
visibility: visible;
cursor: unset;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder.qr--noSyntax > #qr--modal-message::selection {
color: unset;
background-color: rgba(108 171 251 / 0.25);
@ -357,11 +695,15 @@
font-family: var(--monoFontFamily);
padding: 0.75em;
margin: 0;
border: none;
resize: none;
line-height: 1.2;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
position: relative;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-icon {
height: 100%;
aspect-ratio: 1 / 1;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons {
display: flex;
@ -410,6 +752,46 @@
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
border-color: #d78872;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons {
display: none;
gap: 1em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton {
aspect-ratio: 1.25 / 1;
width: 2.25em;
position: relative;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid) {
border-width: 1px;
border-style: solid;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton:not(.fa-solid):after {
content: '';
position: absolute;
inset: 3px;
background-color: var(--SmartThemeBodyColor);
mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-resume:after {
mask-image: url('/img/step-resume.svg');
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-step:after {
mask-image: url('/img/step-over.svg');
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepInto:after {
mask-image: url('/img/step-into.svg');
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-stepOut:after {
mask-image: url('/img/step-out.svg');
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
display: none;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-send_textarea {
flex: 0 0 auto;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeProgress {
--prog: 0;
--progColor: #92befc;
@ -417,6 +799,7 @@
--progSuccessColor: #51a351;
--progErrorColor: #bd362f;
--progAbortedColor: #d78872;
flex: 0 0 auto;
height: 0.5em;
background-color: var(--black50a);
position: relative;
@ -469,6 +852,7 @@
overflow: auto;
min-width: 100%;
width: 0;
white-space: pre-wrap;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
display: block;
@ -476,6 +860,150 @@
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-executeResult:before {
content: 'Result: ';
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState {
display: none;
text-align: left;
font-size: smaller;
font-family: var(--monoFontFamily);
color: white;
padding: 0.5em 0;
overflow: auto;
min-width: 100%;
width: 0;
white-space: pre-wrap;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState.qr--active {
display: block;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope {
display: grid;
grid-template-columns: 0fr 1fr 1fr;
column-gap: 0em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--title {
grid-column: 1 / 4;
font-weight: bold;
font-family: var(--mainFontFamily);
background-color: var(--black50a);
padding: 0.25em;
margin-top: 0.5em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe {
display: contents;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--key,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--key,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--key,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:nth-child(2n),
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:nth-child(2n),
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:nth-child(2n) {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n + 1) .qr--val:hover,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n + 1) .qr--val:hover,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n + 1) .qr--val:hover {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:nth-child(2n),
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:nth-child(2n),
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:nth-child(2n) {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var:nth-child(2n) .qr--val:hover,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro:nth-child(2n) .qr--val:hover,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe:nth-child(2n) .qr--val:hover {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--key,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--key,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--key,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var.qr--isHidden .qr--val,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro.qr--isHidden .qr--val,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe.qr--isHidden .qr--val {
opacity: 0.5;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val {
grid-column: 2 / 4;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--singleCol,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--singleCol,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--singleCol {
grid-column: unset;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:before,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:before,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:before,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--simple:after,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--simple:after,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--simple:after {
content: '"';
color: var(--SmartThemeQuoteColor);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--var .qr--val.qr--unresolved:after,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro .qr--val.qr--unresolved:after,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe .qr--val.qr--unresolved:after {
content: '-UNRESOLVED-';
font-style: italic;
color: var(--SmartThemeQuoteColor);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key {
margin-left: 0.5em;
padding-right: 1em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--key:after {
content: ": ";
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:before,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:before {
content: "{{";
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--pipe > .qr--key:after,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--macro > .qr--key:after {
content: "}}: ";
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope {
display: contents;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--key,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--scope .qr--scope .qr--pipe .qr--val {
opacity: 0.5;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack {
display: grid;
grid-template-columns: 1fr 0fr;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--title {
grid-column: 1 / 3;
font-weight: bold;
font-family: var(--mainFontFamily);
background-color: var(--black50a);
padding: 0.25em;
margin-top: 1em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item {
display: contents;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--name,
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item:nth-child(2n + 1) .qr--source {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--name {
margin-left: 0.5em;
}
.popup:has(#qr--modalEditor) .popup-content > #qr--modalEditor #qr--modal-debugState .qr--stack .qr--item .qr--source {
opacity: 0.5;
text-align: right;
white-space: nowrap;
}
@keyframes qr--progressPulse {
0%,
100% {
@ -485,9 +1013,63 @@
background-color: var(--progFlashColor);
}
}
@keyframes qr--debugPulse {
0%,
100% {
border-color: #51a351;
}
50% {
border-color: #92befc;
}
}
.popup.qr--hide {
opacity: 0 !important;
}
.popup.qr--hide::backdrop {
opacity: 0 !important;
}
.popup.qr--hide::backdrop {
opacity: 0 !important;
}
.popup:has(.qr--transferModal) .popup-button-ok {
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
.popup:has(.qr--transferModal) .popup-button-ok:after {
content: 'Transfer';
height: 0;
overflow: hidden;
font-weight: bold;
}
.popup:has(.qr--transferModal) .qr--copy {
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
.popup:has(.qr--transferModal) .qr--copy:after {
content: 'Copy';
height: 0;
overflow: hidden;
font-weight: bold;
}
.popup:has(.qr--transferModal):has(.qr--transferSelect:focus) .popup-button-ok {
font-weight: bold;
box-shadow: 0 0 10px;
}
.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .popup-button-ok {
font-weight: normal;
box-shadow: 0 0 0;
}
.popup:has(.qr--transferModal):has(.qr--transferSelect:focus).qr--isCopy .qr--copy {
font-weight: bold;
box-shadow: 0 0 10px;
}

View File

@ -1,3 +1,18 @@
@keyframes qr--success {
0%, 100% {
color: var(--SmartThemeBodyColor);
}
25%, 75% {
color: rgb(81, 163, 81);
}
}
&.qr--success {
animation-name: qr--success;
animation-duration: 3s;
animation-timing-function: linear;
animation-delay: 0s;
animation-iteration-count: 1;
}
#qr--bar {
outline: none;
margin: 0;
@ -50,6 +65,18 @@
#qr--bar,
#qr--popout>.qr--body {
>.qr--buttons {
--qr--color: transparent;
&.qr--color {
background-color: var(--qr--color);
}
&.qr--borderColor {
background-color: transparent;
border-left: 5px solid var(--qr--color);
border-right: 5px solid var(--qr--color);
}
&:has(.qr--buttons.qr--color) {
margin: 5px;
}
margin: 0;
padding: 0;
display: flex;
@ -60,6 +87,25 @@
>.qr--buttons {
display: contents;
&.qr--color {
.qr--button:before {
content: '';
background-color: var(--qr--color);
position: absolute;
inset: -5px;
z-index: -1;
}
&.qr--borderColor {
.qr--button:before {
display: none;
}
&:before, &:after {
content: '';
width: 5px;
background-color: var(--qr--color);
}
}
}
}
.qr--button {
@ -75,10 +121,17 @@
align-items: center;
justify-content: center;
text-align: center;
position: relative;
&:hover {
opacity: 1;
filter: brightness(1.2);
background-color: rgb(30% 30% 30%);
}
.qr--hidden {
display: none;
}
.qr--button-icon {
margin: 0 0.5em;
}
>.qr--button-expander {
@ -211,14 +264,41 @@
.qr--set-qrListContents> {
padding: 0 0.5em;
>.qr--set-item {
>.qr--set-item .qr--set-itemAdder {
display: flex;
align-items: center;
opacity: 0;
transition: 100ms;
margin: -2px 0 -11px 0;
position: relative;
.qr--actions {
display: flex;
gap: 0.25em;
flex: 0 0 auto;
.qr--action {
margin: 0;
}
}
&:before, &:after {
content: "";
display: block;
flex: 1 1 auto;
border: 1px solid;
margin: 0 1em;
height: 0;
}
&:hover, &:focus-within {
opacity: 1;
}
}
>.qr--set-item .qr--content {
display: flex;
flex-direction: row;
gap: 0.5em;
align-items: baseline;
padding: 0.25em 0;
> :nth-child(1) {
> :nth-child(2) {
flex: 0 0 auto;
}
@ -235,13 +315,29 @@
}
> :nth-child(5) {
flex: 0 0 auto;
flex: 0 1 auto;
display: flex;
gap: 0.25em;
justify-content: flex-end;
flex-wrap: wrap;
}
>.drag-handle {
padding: 0.75em;
}
.qr--set-itemLabelContainer {
display: flex;
align-items: center;
gap: 0.5em;
.qr--set-itemIcon:not(.fa-solid) {
display: none;
}
.qr--set-itemLabel {
min-width: 24px;
}
}
.qr--set-itemLabel,
.qr--action {
margin: 0;
@ -251,6 +347,8 @@
font-size: smaller;
}
}
}
}
@ -270,6 +368,7 @@
#qr--qrOptions {
display: flex;
flex-direction: column;
padding-right: 1px;
>#qr--ctxEditor {
.qr--ctxItem {
@ -279,6 +378,15 @@
align-items: baseline;
}
}
>#qr--autoExec {
.checkbox_label {
text-wrap: nowrap;
.fa-fw {
margin-right: 2px;
}
}
}
}
@ -306,6 +414,78 @@
.popup:has(#qr--modalEditor) {
aspect-ratio: unset;
&:has(.qr--isExecuting.qr--minimized) {
min-width: unset;
min-height: unset;
height: auto !important;
width: min-content !important;
position: absolute;
right: 1em;
top: 1em;
left: unset;
bottom: unset;
margin: unset;
padding: 0;
&::backdrop {
backdrop-filter: unset;
background-color: transparent;
}
.popup-body {
flex: 0 0 auto;
height: min-content;
width: min-content;
}
.popup-content {
flex: 0 0 auto;
margin-top: 0;
> #qr--modalEditor {
max-height: 50vh;
> #qr--main,
> #qr--resizeHandle,
> #qr--qrOptions > h3,
> #qr--qrOptions > #qr--modal-executeButtons,
> #qr--qrOptions > #qr--modal-executeProgress
{
display: none;
}
#qr--qrOptions {
width: auto;
}
#qr--modal-debugButtons .qr--modal-debugButton#qr--modal-maximize {
display: flex;
}
#qr--modal-debugButtons .qr--modal-debugButton#qr--modal-minimize {
display: none;
}
#qr--modal-debugState {
padding-top: 0;
}
}
}
}
&:has(.qr--isExecuting) {
.popup-controls {
display: none;
}
.qr--highlight {
position: absolute;
z-index: 50000;
pointer-events: none;
background-color: rgb(47 150 180 / 0.5);
&.qr--unresolved {
background-color: rgb(255 255 0 / 0.5);
}
}
.qr--highlight-secondary {
position: absolute;
z-index: 50000;
pointer-events: none;
border: 3px solid red;
}
}
.popup-content {
display: flex;
flex-direction: column;
@ -317,140 +497,262 @@
gap: 1em;
overflow: hidden;
>#qr--main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
>.qr--labels {
flex: 0 0 auto;
display: flex;
flex-direction: row;
gap: 0.5em;
>label {
flex: 1 1 1px;
display: flex;
flex-direction: column;
>.qr--labelText {
flex: 1 1 auto;
&.qr--isExecuting {
#qr--main > h3:first-child,
#qr--main > .qr--labels,
#qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings,
#qr--qrOptions > h3:first-child,
#qr--qrOptions > #qr--ctxEditor,
#qr--qrOptions > .qr--ctxEditorActions,
#qr--qrOptions > .qr--ctxEditorActions + h3,
#qr--qrOptions > .qr--ctxEditorActions + h3 + div
{
display: none;
}
#qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
visibility: hidden;
}
#qr--modal-debugButtons {
display: flex;
.menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
cursor: not-allowed;
opacity: 0.5;
pointer-events: none;
transition: 200ms;
border-color: transparent;
}
}
&.qr--isPaused #qr--modal-debugButtons {
.menu_button:not(#qr--modal-minimize, #qr--modal-maximize) {
cursor: pointer;
opacity: 1;
pointer-events: all;
&#qr--modal-resume {
animation-name: qr--debugPulse;
animation-duration: 1500ms;
animation-timing-function: ease-in-out;
animation-delay: 0s;
animation-iteration-count: infinite;
}
>.qr--labelHint {
flex: 1 1 auto;
&#qr--modal-resume {
border-color: rgb(81, 163, 81);
}
>input {
flex: 0 0 auto;
&#qr--modal-step {
border-color: var(--SmartThemeQuoteColor);
}
&#qr--modal-stepInto {
border-color: var(--SmartThemeQuoteColor);
}
&#qr--modal-stepOut {
border-color: var(--SmartThemeQuoteColor);
}
}
}
#qr--resizeHandle {
width: 6px;
background-color: var(--SmartThemeBorderColor);
border: 2px solid var(--SmartThemeBlurTintColor);
transition: border-color 200ms, background-color 200ms;
cursor: w-resize;
&:hover {
background-color: var(--SmartThemeQuoteColor);
border-color: var(--SmartThemeQuoteColor);
}
}
#qr--qrOptions {
width: var(--width, auto);
}
}
>.qr--modal-messageContainer {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
>.qr--modal-editorSettings {
display: flex;
flex-direction: row;
gap: 1em;
color: var(--grey70);
font-size: smaller;
align-items: baseline;
>.checkbox_label {
white-space: nowrap;
>input {
font-size: inherit;
> #qr--main {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
> .qr--labels {
flex: 0 0 auto;
display: flex;
flex-direction: row;
gap: 0.5em;
padding: 1px;
> label, > .label {
flex: 1 1 1px;
display: flex;
flex-direction: column;
position: relative;
&.qr--fit {
flex: 0 0 auto;
justify-content: center;
}
.qr--inputGroup {
display: flex;
align-items: baseline;
gap: 0.5em;
input {
flex: 1 1 auto;
}
}
}
>#qr--modal-messageHolder {
flex: 1 1 auto;
display: grid;
text-align: left;
overflow: hidden;
&.qr--noSyntax {
>#qr--modal-messageSyntax {
display: none;
}
>#qr--modal-message {
background-color: var(--ac-style-color-background);
color: var(--ac-style-color-text);
&::selection {
color: unset;
background-color: rgba(108 171 251 / 0.25);
@supports (color: rgb(from white r g b / 0.25)) {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
.qr--labelText {
flex: 1 1 auto;
}
.qr--labelHint {
flex: 1 1 auto;
}
input {
flex: 0 0 auto;
}
.qr--modal-switcherList {
background-color: var(--stcdx--bgColor);
border: 1px solid var(--SmartThemeBorderColor);
backdrop-filter: blur(var(--SmartThemeBlurStrength));
border-radius: 10px;
font-size: smaller;
position: absolute;
top: 100%;
left: 0;
right: 0;
overflow: auto;
margin: 0;
padding: 0.5em;
max-height: 50vh;
list-style: none;
z-index: 40000;
max-width: 100%;
.qr--modal-switcherItem {
display: flex;
gap: 1em;
text-align: left;
opacity: 0.75;
transition: 200ms;
cursor: pointer;
&:hover {
opacity: 1;
}
&.qr--current {
opacity: 1;
.qr--label, .qr--id {
font-weight: bold;
}
}
}
}
>#qr--modal-messageSyntax {
grid-column: 1;
grid-row: 1;
padding: 0;
margin: 0;
border: none;
overflow: hidden;
min-width: 100%;
width: 0;
>#qr--modal-messageSyntaxInner {
height: 100%;
}
}
>#qr--modal-message {
background-color: transparent;
color: transparent;
grid-column: 1;
grid-row: 1;
caret-color: var(--ac-style-color-text);
overflow: auto;
&::-webkit-scrollbar,
&::-webkit-scrollbar-thumb {
visibility: hidden;
cursor: default;
}
&::selection {
color: transparent;
background-color: rgba(108 171 251 / 0.25);
@supports (color: rgb(from white r g b / 0.25)) {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
.qr--label {
white-space: nowrap;
.menu_button {
display: inline-block;
height: min-content;
width: min-content;
margin: 0 0.5em 0 0;
}
}
.qr--id {
opacity: 0.5;
&:before { content: "["; }
&:after { content: "]"; }
}
.qr--message {
height: 1lh;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 0.5;
}
}
#qr--modal-message,
#qr--modal-messageSyntaxInner {
font-family: var(--monoFontFamily);
padding: 0.75em;
margin: 0;
border: none;
resize: none;
line-height: 1.2;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
}
}
}
}
}
> .qr--modal-messageContainer {
flex: 1 1 auto;
display: flex;
flex-direction: column;
overflow: hidden;
> .qr--modal-editorSettings {
display: flex;
flex-wrap: wrap;
flex-direction: row;
column-gap: 1em;
color: var(--grey70);
font-size: smaller;
align-items: baseline;
> .checkbox_label {
white-space: nowrap;
> input {
font-size: inherit;
}
}
}
> #qr--modal-messageHolder {
flex: 1 1 auto;
display: grid;
text-align: left;
overflow: hidden;
&.qr--noSyntax {
> #qr--modal-messageSyntax {
display: none;
}
> #qr--modal-message {
background-color: var(--ac-style-color-background);
color: var(--ac-style-color-text);
&::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
visibility: visible;
cursor: unset;
}
&::selection {
color: unset;
background-color: rgba(108 171 251 / 0.25);
@supports (color: rgb(from white r g b / 0.25)) {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
}
}
}
}
> #qr--modal-messageSyntax {
grid-column: 1;
grid-row: 1;
padding: 0;
margin: 0;
border: none;
overflow: hidden;
min-width: 100%;
width: 0;
> #qr--modal-messageSyntaxInner {
height: 100%;
}
}
> #qr--modal-message {
background-color: transparent;
color: transparent;
grid-column: 1;
grid-row: 1;
caret-color: var(--ac-style-color-text);
overflow: auto;
&::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
visibility: hidden;
cursor: default;
}
&::selection {
color: transparent;
background-color: rgba(108 171 251 / 0.25);
@supports (color: rgb(from white r g b / 0.25)) {
background-color: rgb(from var(--ac-style-color-matchedText) r g b / 0.25);
}
}
}
#qr--modal-message, #qr--modal-messageSyntaxInner {
font-family: var(--monoFontFamily);
padding: 0.75em;
margin: 0;
resize: none;
line-height: 1.2;
border: 1px solid var(--SmartThemeBorderColor);
border-radius: 5px;
position: relative;
}
}
}
}
#qr--modal-icon {
height: 100%;
aspect-ratio: 1 / 1;
}
#qr--modal-executeButtons {
display: flex;
gap: 1em;
@ -510,6 +812,47 @@
border-color: rgb(215, 136, 114);
}
}
#qr--modal-debugButtons {
display: none;
gap: 1em;
.qr--modal-debugButton {
aspect-ratio: 1.25 / 1;
width: 2.25em;
position: relative;
&:not(.fa-solid) {
border-width: 1px;
border-style: solid;
&:after {
content: '';
position: absolute;
inset: 3px;
background-color: var(--SmartThemeBodyColor);
mask-size: contain;
mask-position: center;
mask-repeat: no-repeat;
}
}
&#qr--modal-resume:after {
mask-image: url('/img/step-resume.svg');
}
&#qr--modal-step:after {
mask-image: url('/img/step-over.svg');
}
&#qr--modal-stepInto:after {
mask-image: url('/img/step-into.svg');
}
&#qr--modal-stepOut:after {
mask-image: url('/img/step-out.svg');
}
&#qr--modal-maximize {
display: none;
}
}
}
#qr--modal-send_textarea {
flex: 0 0 auto;
}
#qr--modal-executeProgress {
--prog: 0;
@ -518,6 +861,7 @@
--progSuccessColor: rgb(81, 163, 81);
--progErrorColor: rgb(189, 54, 47);
--progAbortedColor: rgb(215, 136, 114);
flex: 0 0 auto;
height: 0.5em;
background-color: var(--black50a);
position: relative;
@ -588,6 +932,135 @@
overflow: auto;
min-width: 100%;
width: 0;
white-space: pre-wrap;
}
#qr--modal-debugState {
display: none;
&.qr--active {
display: block;
}
text-align: left;
font-size: smaller;
font-family: var(--monoFontFamily);
// background-color: rgb(146, 190, 252);
color: white;
padding: 0.5em 0;
overflow: auto;
min-width: 100%;
width: 0;
white-space: pre-wrap;
.qr--scope {
display: grid;
grid-template-columns: 0fr 1fr 1fr;
column-gap: 0em;
.qr--title {
grid-column: 1 / 4;
font-weight: bold;
font-family: var(--mainFontFamily);
background-color: var(--black50a);
padding: 0.25em;
margin-top: 0.5em;
}
.qr--var, .qr--macro, .qr--pipe {
display: contents;
&:nth-child(2n + 1) {
.qr--key, .qr--val {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
}
.qr--val {
&:nth-child(2n) {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.125);
}
&:hover {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
}
}
}
&:nth-child(2n) {
.qr--val {
&:nth-child(2n) {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.0625);
}
&:hover {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.5);
}
}
}
&.qr--isHidden {
.qr--key, .qr--val {
opacity: 0.5;
}
}
.qr--val {
grid-column: 2 / 4;
&.qr--singleCol {
grid-column: unset;
}
&.qr--simple {
&:before, &:after {
content: '"';
color: var(--SmartThemeQuoteColor);
}
}
&.qr--unresolved {
&:after {
content: '-UNRESOLVED-';
font-style: italic;
color: var(--SmartThemeQuoteColor);
}
}
}
}
.qr--key {
margin-left: 0.5em;
padding-right: 1em;
&:after { content: ": "; }
}
.qr--pipe, .qr--macro {
> .qr--key {
&:before { content: "{{"; }
&:after { content: "}}: "; }
}
}
.qr--scope {
display: contents;
.qr--pipe {
.qr--key, .qr--val {
opacity: 0.5;
}
}
}
}
.qr--stack {
display: grid;
grid-template-columns: 1fr 0fr;
.qr--title {
grid-column: 1 / 3;
font-weight: bold;
font-family: var(--mainFontFamily);
background-color: var(--black50a);
padding: 0.25em;
margin-top: 1em;
}
.qr--item {
display: contents;
&:nth-child(2n + 1) {
.qr--name, .qr--source {
background-color: rgb(from var(--SmartThemeEmColor) r g b / 0.25);
}
}
.qr--name {
margin-left: 0.5em;
}
.qr--source {
opacity: 0.5;
text-align: right;
white-space: nowrap;
}
}
}
}
}
}
@ -605,10 +1078,75 @@
}
}
@keyframes qr--debugPulse {
0%,
100% {
border-color: rgb(81, 163, 81);
}
50% {
border-color: rgb(146, 190, 252);
}
}
.popup.qr--hide {
opacity: 0 !important;
opacity: 0 !important;
&::backdrop {
opacity: 0 !important;
}
}
.popup.qr--hide::backdrop {
opacity: 0 !important;
}
.popup:has(.qr--transferModal) {
.popup-button-ok {
&:after {
content: 'Transfer';
height: 0;
overflow: hidden;
font-weight: bold;
}
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
.qr--copy {
&:after {
content: 'Copy';
height: 0;
overflow: hidden;
font-weight: bold;
}
display: flex;
align-items: center;
flex-direction: column;
white-space: pre;
font-weight: normal;
box-shadow: 0 0 0;
transition: 200ms;
}
&:has(.qr--transferSelect:focus) {
.popup-button-ok {
font-weight: bold;
box-shadow: 0 0 10px;
}
&.qr--isCopy {
.popup-button-ok {
font-weight: normal;
box-shadow: 0 0 0;
}
.qr--copy {
font-weight: bold;
box-shadow: 0 0 10px;
}
}
}
}

View File

@ -136,6 +136,10 @@ function throwIfInvalidModel(useReverseProxy) {
throw new Error('Anthropic (Claude) API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'zerooneai' && !secret_state[SECRET_KEYS.ZEROONEAI]) {
throw new Error('01.AI API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
throw new Error('MakerSuite API key is not set.');
}

View File

@ -1,4 +1,8 @@
<div id="sd_gen" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion" /></div>
Generate Image
<div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion"></div>
<span>Generate Image</span>
</div>
<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task" data-i18n="[title]Abort current image generation task"></div>
<span>Stop Image Generation</span>
</div>

View File

@ -37,6 +37,7 @@ const MODULE_NAME = 'sd';
const UPDATE_INTERVAL = 1000;
// This is a 1x1 transparent PNG
const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
const CUSTOM_STOP_EVENT = 'sd_stop_generation';
const sources = {
extras: 'extras',
@ -718,29 +719,29 @@ function onChatChanged() {
adjustElementScrollHeight();
}
function adjustElementScrollHeight() {
async function adjustElementScrollHeight() {
if (!$('.sd_settings').is(':visible')) {
return;
}
resetScrollHeight($('#sd_prompt_prefix'));
resetScrollHeight($('#sd_negative_prompt'));
resetScrollHeight($('#sd_character_prompt'));
resetScrollHeight($('#sd_character_negative_prompt'));
await resetScrollHeight($('#sd_prompt_prefix'));
await resetScrollHeight($('#sd_negative_prompt'));
await resetScrollHeight($('#sd_character_prompt'));
await resetScrollHeight($('#sd_character_negative_prompt'));
}
function onCharacterPromptInput() {
async function onCharacterPromptInput() {
const key = getCharaFilename(this_chid);
extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();
resetScrollHeight($(this));
await resetScrollHeight($(this));
saveSettingsDebounced();
writePromptFieldsDebounced(this_chid);
}
function onCharacterNegativePromptInput() {
async function onCharacterNegativePromptInput() {
const key = getCharaFilename(this_chid);
extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();
resetScrollHeight($(this));
await resetScrollHeight($(this));
saveSettingsDebounced();
writePromptFieldsDebounced(this_chid);
}
@ -849,15 +850,15 @@ function onStepsInput() {
saveSettingsDebounced();
}
function onPromptPrefixInput() {
async function onPromptPrefixInput() {
extension_settings.sd.prompt_prefix = $('#sd_prompt_prefix').val();
resetScrollHeight($(this));
await resetScrollHeight($(this));
saveSettingsDebounced();
}
function onNegativePromptInput() {
async function onNegativePromptInput() {
extension_settings.sd.negative_prompt = $('#sd_negative_prompt').val();
resetScrollHeight($(this));
await resetScrollHeight($(this));
saveSettingsDebounced();
}
@ -2290,6 +2291,7 @@ async function generatePicture(initiator, args, trigger, message, callback) {
const dimensions = setTypeSpecificDimensions(generationType);
const abortController = new AbortController();
const stopButton = document.getElementById('sd_stop_gen');
let negativePromptPrefix = args?.negative || '';
let imagePath = '';
@ -2300,9 +2302,8 @@ async function generatePicture(initiator, args, trigger, message, callback) {
const prompt = await getPrompt(generationType, message, trigger, quietPrompt, combineNegatives);
console.log('Processed image prompt:', prompt);
eventSource.once(event_types.GENERATION_STOPPED, stopListener);
context.deactivateSendButtons();
hideSwipeButtons();
$(stopButton).show();
eventSource.once(CUSTOM_STOP_EVENT, stopListener);
if (typeof args?._abortController?.addEventListener === 'function') {
args._abortController.addEventListener('abort', stopListener);
@ -2311,13 +2312,13 @@ async function generatePicture(initiator, args, trigger, message, callback) {
imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiator, abortController.signal);
} catch (err) {
console.trace(err);
throw new Error('SD prompt text generation failed.');
toastr.error('SD prompt text generation failed. Reason: ' + err, 'Image Generation');
throw new Error('SD prompt text generation failed. Reason: ' + err);
}
finally {
$(stopButton).hide();
restoreOriginalDimensions(dimensions);
eventSource.removeListener(event_types.GENERATION_STOPPED, stopListener);
context.activateSendButtons();
showSwipeButtons();
eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener);
}
return imagePath;
@ -3350,8 +3351,11 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
},
};
context.chat.push(message);
const messageId = context.chat.length - 1;
await eventSource.emit(event_types.MESSAGE_RECEIVED, messageId);
context.addOneMessage(message);
context.saveChat();
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId);
await context.saveChat();
}
/**
@ -3395,7 +3399,7 @@ async function addSDGenButtons() {
$(document).on('click touchend', function (e) {
const target = $(e.target);
if (target.is(dropdown) || target.closest(dropdown).length) return;
if (target.is(button) && !dropdown.is(':visible') && $('#send_but').is(':visible')) {
if ((target.is(button) || target.closest(button).length) && !dropdown.is(':visible') && $('#send_but').is(':visible')) {
e.preventDefault();
dropdown.fadeIn(animation_duration);
@ -3425,6 +3429,10 @@ async function addSDGenButtons() {
generatePicture(initiators.wand, {}, param);
}
});
const stopGenButton = $('#sd_stop_gen');
stopGenButton.hide();
stopGenButton.on('click', () => eventSource.emit(CUSTOM_STOP_EVENT));
}
function isValidState() {

View File

@ -59,8 +59,8 @@ async function doTokenCounter() {
$('#tokenized_chunks_display').text('—');
}
resetScrollHeight($('#token_counter_textarea'));
resetScrollHeight($('#token_counter_ids'));
await resetScrollHeight($('#token_counter_textarea'));
await resetScrollHeight($('#token_counter_ids'));
}, debounce_timeout.relaxed);
dialog.find('#token_counter_textarea').on('input', () => countDebounced());
@ -134,7 +134,8 @@ jQuery(() => {
</div>`;
$('#token_counter_wand_container').append(buttonHtml);
$('#token_counter').on('click', doTokenCounter);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count',
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'count',
callback: async () => String(await doCount()),
returns: 'number of tokens',
helpString: 'Counts the number of tokens in the current chat.',

View File

@ -1,8 +1,8 @@
<div id="translate_chat" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-language extensionsMenuExtensionButton" /></div>
<div class="fa-solid fa-language extensionsMenuExtensionButton"></div>
<span data-i18n="ext_translate_btn_chat">Translate Chat</span>
</div>
<div id="translate_input_message" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-keyboard extensionsMenuExtensionButton" /></div>
<div class="fa-solid fa-keyboard extensionsMenuExtensionButton"></div>
<span data-i18n="ext_translate_btn_input">Translate Input</span>
</div>
</div>

View File

@ -94,11 +94,11 @@ export function loadInstructMode(data) {
$element.val(power_user.instruct[control.property]);
}
$element.on('input', function () {
$element.on('input', async function () {
power_user.instruct[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
saveSettingsDebounced();
if (!control.isCheckbox) {
resetScrollHeight($element);
await resetScrollHeight($element);
}
});

View File

@ -127,6 +127,7 @@ const max_128k = 128 * 1000;
const max_200k = 200 * 1000;
const max_256k = 256 * 1000;
const max_1mil = 1000 * 1000;
const max_2mil = 2000 * 1000;
const scale_max = 8191;
const claude_max = 9000; // We have a proper tokenizer, so theoretically could be larger (up to 9k)
const claude_100k_max = 99000;
@ -258,14 +259,14 @@ const default_settings = {
group_nudge_prompt: default_group_nudge_prompt,
scenario_format: default_scenario_format,
personality_format: default_personality_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-2.1',
google_model: 'gemini-pro',
openai_model: 'gpt-4-turbo',
claude_model: 'claude-3-5-sonnet-20240620',
google_model: 'gemini-1.5-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium-latest',
cohere_model: 'command-r',
perplexity_model: 'llama-3-70b-instruct',
groq_model: 'llama3-70b-8192',
mistralai_model: 'mistral-large-latest',
cohere_model: 'command-r-plus',
perplexity_model: 'llama-3.1-70b-instruct',
groq_model: 'llama-3.1-70b-versatile',
zerooneai_model: 'yi-large',
custom_model: '',
custom_url: '',
@ -338,14 +339,14 @@ const oai_settings = {
group_nudge_prompt: default_group_nudge_prompt,
scenario_format: default_scenario_format,
personality_format: default_personality_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-2.1',
google_model: 'gemini-pro',
openai_model: 'gpt-4-turbo',
claude_model: 'claude-3-5-sonnet-20240620',
google_model: 'gemini-1.5-pro',
ai21_model: 'j2-ultra',
mistralai_model: 'mistral-medium-latest',
cohere_model: 'command-r',
perplexity_model: 'llama-3-70b-instruct',
groq_model: 'llama3-70b-8192',
mistralai_model: 'mistral-large-latest',
cohere_model: 'command-r-plus',
perplexity_model: 'llama-3.1-70b-instruct',
groq_model: 'llama-3.1-70b-versatile',
zerooneai_model: 'yi-large',
custom_model: '',
custom_url: '',
@ -4056,18 +4057,26 @@ 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_2mil);
} else if (value.includes('gemini-1.5-pro')) {
$('#openai_max_context').attr('max', max_2mil);
} else if (value.includes('gemini-1.5-flash')) {
$('#openai_max_context').attr('max', max_1mil);
} 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);
} else if (value === 'gemini-1.0-pro-vision-latest' || value === 'gemini-pro-vision') {
} else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') {
$('#openai_max_context').attr('max', max_16k);
} else {
} else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
$('#openai_max_context').attr('max', max_32k);
} else if (value === 'text-bison-001') {
$('#openai_max_context').attr('max', max_8k);
// The ultra endpoints are possibly dead:
} else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {
$('#openai_max_context').attr('max', max_32k);
} else {
$('#openai_max_context').attr('max', max_4k);
}
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
let makersuite_max_temp = (value.includes('vision') || value.includes('ultra')) ? 1.0 : 2.0;
oai_settings.temp_openai = Math.min(makersuite_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', makersuite_max_temp).val(oai_settings.temp_openai).trigger('input');
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
}
@ -4201,6 +4210,11 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (oai_settings.perplexity_model.includes('llama-3.1')) {
const isOnline = oai_settings.perplexity_model.includes('online');
const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
$('#openai_max_context').attr('max', contextSize);
}
else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', max_32k);
}
@ -4292,7 +4306,17 @@ async function onModelChange() {
if (oai_settings.chat_completion_source === chat_completion_sources.ZEROONEAI) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else {
}
else if (['yi-large'].includes(oai_settings.zerooneai_model)) {
$('#openai_max_context').attr('max', max_32k);
}
else if (['yi-vision'].includes(oai_settings.zerooneai_model)) {
$('#openai_max_context').attr('max', max_16k);
}
else if (['yi-large-turbo'].includes(oai_settings.zerooneai_model)) {
$('#openai_max_context').attr('max', max_4k);
}
else {
$('#openai_max_context').attr('max', max_16k);
}
@ -4649,16 +4673,21 @@ 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.5-flash-latest',
'gemini-1.5-flash-001',
'gemini-1.0-pro-vision-latest',
'gemini-1.5-pro',
'gemini-1.5-pro-latest',
'gemini-1.5-pro-001',
'gemini-1.5-pro-exp-0801',
'gemini-pro-vision',
'claude-3',
'claude-3-5',
'gpt-4-turbo',
'gpt-4o',
'gpt-4o-mini',
'yi-vision',
];
switch (oai_settings.chat_completion_source) {
@ -4672,6 +4701,8 @@ export function isImageInliningSupported() {
return !oai_settings.openrouter_force_instruct;
case chat_completion_sources.CUSTOM:
return true;
case chat_completion_sources.ZEROONEAI:
return visionSupportedModels.some(model => oai_settings.zerooneai_model.includes(model));
default:
return false;
}

View File

@ -101,6 +101,21 @@ const showPopupHelper = {
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${result}`);
return result;
},
/**
* Asynchronously displays a text popup with the given header and text, returning the clicked result button value.
*
* @param {string?} header - The header text for the popup.
* @param {string?} text - The main text for the popup.
* @param {PopupOptions} [popupOptions={}] - Options for the popup.
* @return {Promise<POPUP_RESULT>} A Promise that resolves with the result of the user's interaction.
*/
text: async (header, text, popupOptions = {}) => {
const content = PopupUtils.BuildTextWithHeader(header, text);
const popup = new Popup(content, POPUP_TYPE.TEXT, null, popupOptions);
const result = await popup.show();
if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. TEXT popups only support numbers, or null. Result: ${result}`);
return result;
},
};
export class Popup {
@ -511,6 +526,15 @@ export class Popup {
return this.#promise;
}
async completeAffirmative() {
return await this.complete(POPUP_RESULT.AFFIRMATIVE);
}
async completeNegative() {
return await this.complete(POPUP_RESULT.NEGATIVE);
}
async completeCancelled() {
return await this.complete(POPUP_RESULT.CANCELLED);
}
/**
* Hides the popup, using the internal resolver to return the value to the original show promise

View File

@ -45,7 +45,7 @@ import { FILTER_TYPES } from './filters.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
import { AUTOCOMPLETE_SELECT_KEY, AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js';
@ -197,6 +197,7 @@ let power_user = {
prefer_character_prompt: true,
prefer_character_jailbreak: true,
quick_continue: false,
quick_impersonate: false,
continue_on_send: false,
trim_spaces: true,
relaxed_api_urls: false,
@ -242,6 +243,7 @@ let power_user = {
example_separator: defaultExampleSeparator,
use_stop_strings: true,
allow_jailbreak: false,
names_as_stop_strings: true,
},
personas: {},
@ -276,6 +278,7 @@ let power_user = {
left: AUTOCOMPLETE_WIDTH.CHAT,
right: AUTOCOMPLETE_WIDTH.CHAT,
},
select: AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER,
},
parser: {
/**@type {Object.<PARSER_FLAG,boolean>} */
@ -347,6 +350,7 @@ const contextControls = [
{ id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },
{ id: 'context_use_stop_strings', property: 'use_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
{ id: 'context_allow_jailbreak', property: 'allow_jailbreak', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
{ id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },
// Existing power user settings
{ id: 'always-force-name2-checkbox', property: 'always_force_name2', isCheckbox: true, isGlobalSetting: true, defaultValue: true },
@ -1026,6 +1030,12 @@ function switchMovingUI() {
if (power_user.movingUIState) {
loadMovingUIState();
}
} else {
if (Object.keys(power_user.movingUIState).length !== 0) {
power_user.movingUIState = {};
resetMovablePanels();
saveSettingsDebounced();
}
}
}
@ -1470,7 +1480,7 @@ function getExampleMessagesBehavior() {
return 'normal';
}
function loadPowerUserSettings(settings, data) {
async function loadPowerUserSettings(settings, data) {
const defaultStscript = JSON.parse(JSON.stringify(power_user.stscript));
// Load from settings.json
if (settings.power_user !== undefined) {
@ -1492,6 +1502,9 @@ function loadPowerUserSettings(settings, data) {
if (power_user.stscript.autocomplete.style === undefined) {
power_user.stscript.autocomplete.style = power_user.stscript.autocomplete_style || defaultStscript.autocomplete.style;
}
if (power_user.stscript.autocomplete.select === undefined) {
power_user.stscript.autocomplete.select = defaultStscript.autocomplete.select;
}
}
if (power_user.stscript.parser === undefined) {
power_user.stscript.parser = defaultStscript.parser;
@ -1586,7 +1599,9 @@ function loadPowerUserSettings(settings, data) {
$('#trim_spaces').prop('checked', power_user.trim_spaces);
$('#continue_on_send').prop('checked', power_user.continue_on_send);
$('#quick_continue').prop('checked', power_user.quick_continue);
$('#quick_impersonate').prop('checked', power_user.quick_continue);
$('#mes_continue').css('display', power_user.quick_continue ? '' : 'none');
$('#mes_impersonate').css('display', power_user.quick_impersonate ? '' : 'none');
$('#gestures-checkbox').prop('checked', power_user.gestures);
$('#auto_swipe').prop('checked', power_user.auto_swipe);
$('#auto_swipe_minimum_length').val(power_user.auto_swipe_minimum_length);
@ -1656,6 +1671,7 @@ function loadPowerUserSettings(settings, data) {
$('#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);
$('#stscript_autocomplete_select').val(power_user.stscript.autocomplete.select ?? (AUTOCOMPLETE_SELECT_KEY.TAB + AUTOCOMPLETE_SELECT_KEY.ENTER));
$('#stscript_parser_flag_strict_escaping').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] ?? false);
$('#stscript_parser_flag_replace_getvar').prop('checked', power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] ?? false);
$('#stscript_autocomplete_font_scale').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
@ -1724,7 +1740,7 @@ function loadPowerUserSettings(settings, data) {
switchCompactInputArea();
reloadMarkdownProcessor(power_user.render_formulas);
loadInstructMode(data);
loadContextSettings();
await loadContextSettings();
loadMaxContextUnlocked();
switchWaifuMode();
switchSpoilerMode();
@ -1856,7 +1872,7 @@ function getContextSettings() {
// TODO: Maybe add a refresh button to reset settings to preset
// TODO: Add "global state" if a preset doesn't set the power_user checkboxes
function loadContextSettings() {
async function loadContextSettings() {
contextControls.forEach(control => {
const $element = $(`#${control.id}`);
@ -1876,7 +1892,7 @@ function loadContextSettings() {
// If the setting already exists, no need to duplicate it
// TODO: Maybe check the power_user object for the setting instead of a flag?
$element.on('input', function () {
$element.on('input', async function () {
const value = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
if (control.isGlobalSetting) {
power_user[control.property] = value;
@ -1886,7 +1902,7 @@ function loadContextSettings() {
saveSettingsDebounced();
if (!control.isCheckbox) {
resetScrollHeight($element);
await resetScrollHeight($element);
}
});
});
@ -3732,6 +3748,13 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#quick_impersonate').on('input', function () {
const value = !!$(this).prop('checked');
power_user.quick_impersonate = value;
$('#mes_impersonate').css('display', value ? '' : 'none');
saveSettingsDebounced();
});
$('#trim_spaces').on('input', function () {
const value = !!$(this).prop('checked');
power_user.trim_spaces = value;
@ -3846,6 +3869,12 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#stscript_autocomplete_select').on('change', function () {
const value = $(this).find(':selected').val();
power_user.stscript.autocomplete.select = parseInt(String(value));
saveSettingsDebounced();
});
$('#stscript_autocomplete_font_scale').on('input', function () {
const value = $(this).val();
$('#stscript_autocomplete_font_scale_counter').val(value);
@ -4014,6 +4043,7 @@ $(document).ready(() => {
),
],
helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',
returns: 'The text of the deleted messages.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'cut',

View File

@ -1,9 +1,7 @@
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';
@ -20,132 +18,6 @@ const forcedOffColoring = 'filter: sepia(1) hue-rotate(308deg) contrast(0.7) sat
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');
@ -158,12 +30,12 @@ async function showSamplerSelectPopup() {
<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">
<div id="resetSelectedSamplers" class="menu_button menu_button_icon" 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">
<div class="menu_button menu_button_icon" title="Create a new sampler">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Create">Create</span>
</div>
@ -190,6 +62,15 @@ async function showSamplerSelectPopup() {
power_user.selectSamplers.forceHidden = [];
await validateDisabledSamplers(true);
});
$('#textgen_type').on('change', async function () {
console.log('changed TG Type, resetting custom samplers'); //unfortunate, but necessary unless we save custom samplers for each TGTytpe
userDisabledSamplers = [];
userShownSamplers = [];
power_user.selectSamplers.forceShown = [];
power_user.selectSamplers.forceHidden = [];
await validateDisabledSamplers();
});
}
function setSamplerListListeners() {
@ -221,6 +102,11 @@ function setSamplerListListeners() {
targetDisplayType = 'block';
}
if (samplerName === 'dry_multiplier') {
relatedDOMElement = $('#dryBlock');
targetDisplayType = 'block';
}
if (samplerName === 'dynatemp') {
relatedDOMElement = $('#dynatemp_block_ooba');
targetDisplayType = 'block';
@ -231,9 +117,30 @@ function setSamplerListListeners() {
targetDisplayType = 'block';
}
if (samplerName === 'sampler_order') {
relatedDOMElement = $('#sampler_order_block');
targetDisplayType = 'flex';
if (samplerName === 'sampler_order') { //this is for kcpp sampler order
relatedDOMElement = $('#sampler_order_block_kcpp');
}
if (samplerName === 'samplers') { //this is for lcpp sampler order
relatedDOMElement = $('#sampler_order_block_lcpp');
}
if (samplerName === 'sampler_priority') { //this is for ooba's sampler priority
relatedDOMElement = $('#sampler_priority_block_ooba');
}
if (samplerName === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
relatedDOMElement = $('#contrastiveSearchBlock');
}
if (samplerName === 'num_beams') { // num_beams is the killswitch for Beam Search
relatedDOMElement = $('#beamSearchBlock');
targetDisplayType = 'block';
}
if (samplerName === 'smoothing_factor') { // num_beams is the killswitch for Beam Search
relatedDOMElement = $('#smoothingBlock');
targetDisplayType = 'block';
}
// Get the current state of the custom data attribute
@ -301,7 +208,7 @@ 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']);
const valuesToRemove = new Set(['streaming', 'bypass_status_check', 'custom_model', 'legacy_api']);
availableSamplers = availableSamplers.filter(sampler => !valuesToRemove.has(sampler));
availableSamplers.sort();
}
@ -312,8 +219,70 @@ async function listSamplers(main_api, arrayOnly = false) {
}
const samplersListHTML = availableSamplers.reduce((html, sampler) => {
let customColor;
const targetDOMelement = $(`#${sampler}_${main_api}`);
let customColor, displayname;
let targetDOMelement = $(`#${sampler}_${main_api}`);
if (sampler === 'sampler_order') { //this is for kcpp sampler order
targetDOMelement = $('#sampler_order_block_kcpp');
displayname = 'KCPP Sampler Order Block';
}
if (sampler === 'samplers') { //this is for lcpp sampler order
targetDOMelement = $('#sampler_order_block_lcpp');
displayname = 'LCPP Sampler Order Block';
}
if (sampler === 'sampler_priority') { //this is for ooba's sampler priority
targetDOMelement = $('#sampler_priority_block_ooba');
displayname = 'Ooba Sampler Priority Block';
}
if (sampler === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
targetDOMelement = $('#contrastiveSearchBlock');
displayname = 'Contrast Search Block';
}
if (sampler === 'num_beams') { // num_beams is the killswitch for Beam Search
targetDOMelement = $('#beamSearchBlock');
displayname = 'Beam Search Block';
}
if (sampler === 'smoothing_factor') { // num_beams is the killswitch for Beam Search
targetDOMelement = $('#smoothingBlock');
displayname = 'Smoothing Block';
}
if (sampler === 'dry_multiplier') {
targetDOMelement = $('#dryBlock');
displayname = 'DRY Rep Pen Block';
}
if (sampler === 'dynatemp') {
targetDOMelement = $('#dynatemp_block_ooba');
displayname = 'DynaTemp Block';
}
if (sampler === 'json_schema') {
targetDOMelement = $('#json_schema_block');
displayname = 'JSON Schema Block';
}
if (sampler === 'grammar_string') {
targetDOMelement = $('#grammar_block_ooba');
displayname = 'Grammar Block';
}
if (sampler === 'guidance_scale') {
targetDOMelement = $('#cfg_block_ooba');
displayname = 'CFG Block';
}
if (sampler === 'mirostat_mode') {
targetDOMelement = $('#mirostat_block_ooba');
displayname = 'Mirostat Block';
}
const isInForceHiddenArray = userDisabledSamplers.includes(sampler);
const isInForceShownArray = userShownSamplers.includes(sampler);
@ -335,11 +304,12 @@ async function listSamplers(main_api, arrayOnly = false) {
}
else { return isVisibleInDOM; }
};
console.log(sampler, isInDefaultState(), isInForceHiddenArray, shouldBeChecked());
console.log(sampler, targetDOMelement.prop('id'), isInDefaultState(), isInForceShownArray, isInForceHiddenArray, shouldBeChecked());
if (displayname === undefined) { displayname = sampler; }
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>
<small class="sampler_name" style="${customColor}">${displayname}</small>
</div>
`;
}, '');
@ -391,8 +361,33 @@ export async function validateDisabledSamplers(redraw = false) {
targetDisplayType = 'block';
}
if (sampler === 'sampler_order') {
relatedDOMElement = $('#sampler_order_block');
if (sampler === 'sampler_order') { //this is for kcpp sampler order
relatedDOMElement = $('#sampler_order_block_kcpp');
}
if (sampler === 'samplers') { //this is for lcpp sampler order
relatedDOMElement = $('#sampler_order_block_lcpp');
}
if (sampler === 'sampler_priority') { //this is for ooba's sampler priority
relatedDOMElement = $('#sampler_priority_block_ooba');
}
if (sampler === 'dry_multiplier') {
relatedDOMElement = $('#dryBlock');
targetDisplayType = 'block';
}
if (sampler === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
relatedDOMElement = $('#contrastiveSearchBlock');
}
if (sampler === 'num_beams') { // num_beams is the killswitch for Beam Search
relatedDOMElement = $('#beamSearchBlock');
}
if (sampler === 'smoothing_factor') { // num_beams is the killswitch for Beam Search
relatedDOMElement = $('#smoothingBlock');
}
if (power_user?.selectSamplers?.forceHidden.includes(sampler)) {
@ -418,6 +413,7 @@ export async function validateDisabledSamplers(redraw = false) {
setSamplerListListeners();
}
await saveSettingsDebounced();
}
}

View File

@ -51,7 +51,7 @@ import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockStat
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { debounce, delay, isFalseBoolean, isTrueBoolean, showFontAwesomePicker, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
import { registerVariableCommands, resolveVariable } from './variables.js';
import { background_settings } from './backgrounds.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@ -64,6 +64,9 @@ import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashComma
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js';
import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
export {
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
};
@ -1120,10 +1123,10 @@ export function initDefaultSlashCommands() {
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'scoped variable or qr label',
typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING],
typeList: [ARGUMENT_TYPE.VARIABLE_NAME, ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.CLOSURE],
isRequired: true,
enumProvider: () => [
...commonEnumProviders.variables('scope')(),
enumProvider: (executor, scope) => [
...commonEnumProviders.variables('scope')(executor, scope),
...(typeof window['qrEnumProviderExecutables'] === 'function') ? window['qrEnumProviderExecutables']() : [],
],
}),
@ -1477,6 +1480,21 @@ export function initDefaultSlashCommands() {
],
helpString: 'Sets the specified prompt manager entry/entries on or off.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pick-icon',
callback: async()=>((await showFontAwesomePicker()) ?? false).toString(),
returns: 'The chosen icon name or false if cancelled.',
helpString: `
<div>Opens a popup with all the available Font Awesome icons and returns the selected icon's name.</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code>/pick-icon |\n/if left={{pipe}} rule=eq right=false\n\telse={: /echo chosen icon: "{{pipe}}" :}\n\t{: /echo cancelled icon selection :}\n|</code></pre>
</li>
</ul>
</div>
`,
}));
registerVariableCommands();
}
@ -1821,6 +1839,11 @@ async function runCallback(args, name) {
throw new Error('No name provided for /run command');
}
if (name instanceof SlashCommandClosure) {
name.breakController = new SlashCommandBreakController();
return (await name.execute())?.pipe;
}
/**@type {SlashCommandScope} */
const scope = args._scope;
if (scope.existsVariable(name)) {
@ -1829,6 +1852,11 @@ async function runCallback(args, name) {
throw new Error(`"${name}" is not callable.`);
}
closure.scope.parent = scope;
closure.breakController = new SlashCommandBreakController();
if (args._debugController && !closure.debugController) {
closure.debugController = args._debugController;
}
while (closure.providedArgumentList.pop());
closure.argumentList.forEach(arg => {
if (Object.keys(args).includes(arg.name)) {
const providedArg = new SlashCommandNamedArgumentAssignment();
@ -1847,9 +1875,14 @@ async function runCallback(args, name) {
try {
name = name.trim();
return await window['executeQuickReplyByName'](name, args);
/**@type {ExecuteSlashCommandsOptions} */
const options = {
abortController: args._abortController,
debugController: args._debugController,
};
return await window['executeQuickReplyByName'](name, args, options);
} catch (error) {
throw new Error(`Error running Quick Reply "${name}": ${error.message}`, 'Error');
throw new Error(`Error running Quick Reply "${name}": ${error.message}`);
}
}
@ -3428,7 +3461,9 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
* @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
* @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
* @prop {SlashCommandDebugController} [debugController] (null) Controller used to control debug execution
* @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
* @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
*/
/**
@ -3436,6 +3471,7 @@ const clearCommandProgressDebounced = debounce(clearCommandProgress);
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
* @prop {{[id:PARSER_FLAG]:boolean}} [parserFlags] (null) Parser flags to apply
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
* @prop {string} [source] (null) String indicating where the code come from (e.g., QR name)
*/
/**
@ -3451,6 +3487,7 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
scope: null,
parserFlags: null,
clearChatInput: false,
source: null,
}, options);
isExecutingCommandsFromChatInput = true;
@ -3473,13 +3510,21 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
/**@type {SlashCommandClosureResult} */
let result = null;
let currentProgress = 0;
try {
commandsFromChatInputAbortController = new SlashCommandAbortController();
result = await executeSlashCommandsWithOptions(text, {
abortController: commandsFromChatInputAbortController,
onProgress: (done, total) => ta.style.setProperty('--prog', `${done / total * 100}%`),
onProgress: (done, total) => {
const newProgress = done / total;
if (newProgress > currentProgress) {
currentProgress = newProgress;
ta.style.setProperty('--prog', `${newProgress * 100}%`);
}
},
parserFlags: options.parserFlags,
scope: options.scope,
source: options.source,
});
if (commandsFromChatInputAbortController.signal.aborted) {
document.querySelector('#form_sheld').classList.add('script_aborted');
@ -3492,7 +3537,23 @@ export async function executeSlashCommandsOnChatInput(text, options = {}) {
result.isError = true;
result.errorMessage = e.message || 'An unknown error occurred';
if (e.cause !== 'abort') {
toastr.error(result.errorMessage);
if (e instanceof SlashCommandExecutionError) {
/**@type {SlashCommandExecutionError}*/
const ex = e;
const toast = `
<div>${ex.message}</div>
<div>Line: ${ex.line} Column: ${ex.column}</div>
<pre style="text-align:left;">${ex.hint}</pre>
`;
const clickHint = '<p>Click to see details</p>';
toastr.error(
`${toast}${clickHint}`,
'SlashCommandExecutionError',
{ escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') },
);
} else {
toastr.error(result.errorMessage);
}
}
} finally {
delay(1000).then(() => clearCommandProgressDebounced());
@ -3520,7 +3581,9 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
handleExecutionErrors: false,
parserFlags: null,
abortController: null,
debugController: null,
onProgress: null,
source: null,
}, options);
let closure;
@ -3528,6 +3591,8 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());
closure.scope.parent = options.scope;
closure.onProgress = options.onProgress;
closure.debugController = options.debugController;
closure.source = options.source;
} catch (e) {
if (options.handleParserErrors && e instanceof SlashCommandParserError) {
/**@type {SlashCommandParserError}*/
@ -3559,7 +3624,23 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
return result;
} catch (e) {
if (options.handleExecutionErrors) {
toastr.error(e.message);
if (e instanceof SlashCommandExecutionError) {
/**@type {SlashCommandExecutionError}*/
const ex = e;
const toast = `
<div>${ex.message}</div>
<div>Line: ${ex.line} Column: ${ex.column}</div>
<pre style="text-align:left;">${ex.hint}</pre>
`;
const clickHint = '<p>Click to see details</p>';
toastr.error(
`${toast}${clickHint}`,
'SlashCommandExecutionError',
{ escapeHtml: false, timeOut: 10000, onclick: () => callPopup(toast, 'text') },
);
} else {
toastr.error(e.message);
}
const result = new SlashCommandClosureResult();
result.isError = true;
result.errorMessage = e.message;
@ -3596,6 +3677,7 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul
*
* @param {HTMLTextAreaElement} textarea The textarea to receive autocomplete
* @param {Boolean} isFloating Whether to show the auto complete as a floating window (e.g., large QR editor)
* @returns {Promise<AutoComplete>}
*/
export async function setSlashCommandAutoComplete(textarea, isFloating = false) {
function canUseNegativeLookbehind() {
@ -3619,6 +3701,7 @@ export async function setSlashCommandAutoComplete(textarea, isFloating = false)
async (text, index) => await parser.getNameAt(text, index),
isFloating,
);
return ac;
}
/**@type {HTMLTextAreaElement} */
const sendTextarea = document.querySelector('#send_textarea');

View File

@ -1,6 +1,7 @@
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandDebugController } from './SlashCommandDebugController.js';
import { PARSER_FLAG } from './SlashCommandParser.js';
import { SlashCommandScope } from './SlashCommandScope.js';
@ -12,6 +13,7 @@ import { SlashCommandScope } from './SlashCommandScope.js';
* _scope:SlashCommandScope,
* _parserFlags:{[id:PARSER_FLAG]:boolean},
* _abortController:SlashCommandAbortController,
* _debugController:SlashCommandDebugController,
* _hasUnnamedArgument:boolean,
* [id:string]:string|SlashCommandClosure,
* }} NamedArguments
@ -36,6 +38,7 @@ export class SlashCommand {
* @param {(namedArguments:NamedArguments|NamedArgumentsCapture, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>} [props.callback]
* @param {string} [props.helpString]
* @param {boolean} [props.splitUnnamedArgument]
* @param {Number} [props.splitUnnamedArgumentCount]
* @param {string[]} [props.aliases]
* @param {string} [props.returns]
* @param {SlashCommandNamedArgument[]} [props.namedArgumentList]
@ -50,9 +53,10 @@ export class SlashCommand {
/**@type {string}*/ name;
/**@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 {(namedArguments:{_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 {Number}*/ splitUnnamedArgumentCount;
/**@type {string[]}*/ aliases = [];
/**@type {string}*/ returns;
/**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = [];
@ -61,6 +65,10 @@ export class SlashCommand {
/**@type {Object.<string, HTMLElement>}*/ helpCache = {};
/**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {};
/**@type {boolean}*/ isExtension = false;
/**@type {boolean}*/ isThirdParty = false;
/**@type {string}*/ source;
renderHelpItem(key = null) {
key = key ?? this.name;
if (!this.helpCache[key]) {
@ -225,12 +233,35 @@ export class SlashCommand {
const aliasList = [cmd.name, ...(cmd.aliases ?? [])].filter(it=>it != key);
const specs = document.createElement('div'); {
specs.classList.add('specs');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.title = 'command name';
name.textContent = `/${key}`;
specs.append(name);
const head = document.createElement('div'); {
head.classList.add('head');
const name = document.createElement('div'); {
name.classList.add('name');
name.classList.add('monospace');
name.title = 'command name';
name.textContent = `/${key}`;
head.append(name);
}
const src = document.createElement('div'); {
src.classList.add('source');
src.classList.add('fa-solid');
if (this.isExtension) {
src.classList.add('isExtension');
src.classList.add('fa-cubes');
if (this.isThirdParty) src.classList.add('isThirdParty');
else src.classList.add('isCore');
} else {
src.classList.add('isCore');
src.classList.add('fa-star-of-life');
}
src.title = [
this.isExtension ? 'Extension' : 'Core',
this.isThirdParty ? 'Third Party' : (this.isExtension ? 'Core' : null),
this.source,
].filter(it=>it).join('\n');
head.append(src);
}
specs.append(head);
}
const body = document.createElement('div'); {
body.classList.add('body');
@ -303,40 +334,52 @@ export class SlashCommand {
for (const arg of unnamedArguments) {
const listItem = document.createElement('li'); {
listItem.classList.add('argumentItem');
const argItem = document.createElement('div'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
enums.title = `${argItem.title} - accepted values`;
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e.value;
enums.append(enumItem);
const argSpec = document.createElement('div'); {
argSpec.classList.add('argumentSpec');
const argItem = document.createElement('div'); {
argItem.classList.add('argument');
argItem.classList.add('unnamedArgument');
argItem.title = `${arg.isRequired ? '' : 'optional '}unnamed argument`;
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
if (arg.acceptsMultiple) argItem.classList.add('multiple');
if (arg.enumList.length > 0) {
const enums = document.createElement('span'); {
enums.classList.add('argument-enums');
enums.title = `${argItem.title} - accepted values`;
for (const e of arg.enumList) {
const enumItem = document.createElement('span'); {
enumItem.classList.add('argument-enum');
enumItem.textContent = e.value;
enums.append(enumItem);
}
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
types.title = `${argItem.title} - accepted types`;
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
}
argItem.append(enums);
}
} else {
const types = document.createElement('span'); {
types.classList.add('argument-types');
types.title = `${argItem.title} - accepted types`;
for (const t of arg.typeList) {
const type = document.createElement('span'); {
type.classList.add('argument-type');
type.textContent = t;
types.append(type);
}
}
argItem.append(types);
argSpec.append(argItem);
}
if (arg.defaultValue !== null) {
const argDefault = document.createElement('div'); {
argDefault.classList.add('argument-default');
argDefault.title = 'default value';
argDefault.textContent = arg.defaultValue.toString();
argSpec.append(argDefault);
}
}
listItem.append(argItem);
listItem.append(argSpec);
}
const desc = document.createElement('div'); {
desc.classList.add('argument-description');

View File

@ -2,6 +2,9 @@ import { SlashCommandClosure } from './SlashCommandClosure.js';
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandScope } from './SlashCommandScope.js';
/**@readonly*/
/**@enum {string}*/
@ -27,7 +30,7 @@ export class SlashCommandArgument {
* @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
*/
static fromProps(props) {
@ -49,7 +52,7 @@ export class SlashCommandArgument {
/**@type {boolean}*/ acceptsMultiple = false;
/**@type {string|SlashCommandClosure}*/ defaultValue;
/**@type {SlashCommandEnumValue[]}*/ enumList = [];
/**@type {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]}*/ enumProvider = null;
/**@type {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]}*/ enumProvider = null;
/**@type {boolean}*/ forceEnum = false;
/**
@ -57,7 +60,7 @@ export class SlashCommandArgument {
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
* @param {string|SlashCommandClosure} defaultValue
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} enumProvider function that returns auto complete options
*/
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], enumProvider = null, forceEnum = false) {
this.description = description;
@ -89,7 +92,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
* @param {boolean} [props.acceptsMultiple=false] default: false - whether argument accepts multiple values
* @param {string|SlashCommandClosure} [props.defaultValue=null] default value if no value is provided
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList=[]] list of accepted values
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [props.enumProvider=null] function that returns auto complete options
* @param {boolean} [props.forceEnum=false] default: false - whether the input must match one of the enum values
*/
static fromProps(props) {
@ -119,7 +122,7 @@ export class SlashCommandNamedArgument extends SlashCommandArgument {
* @param {string|SlashCommandClosure} [defaultValue=null]
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [enums=[]]
* @param {string[]} [aliases=[]]
* @param {(executor:SlashCommandExecutor)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
* @param {(executor:SlashCommandExecutor, scope:SlashCommandScope)=>SlashCommandEnumValue[]} [enumProvider=null] function that returns auto complete options
* @param {boolean} [forceEnum=false]
*/
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = [], enumProvider = null, forceEnum = false) {

View File

@ -8,15 +8,18 @@ import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAuto
import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandNamedArgumentAutoCompleteOption } from './SlashCommandNamedArgumentAutoCompleteOption.js';
import { SlashCommandScope } from './SlashCommandScope.js';
export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
/**@type {SlashCommandExecutor}*/ executor;
/**@type {SlashCommandScope}*/ scope;
/**
* @param {SlashCommandExecutor} executor
* @param {SlashCommandScope} scope
* @param {Object.<string,SlashCommand>} commands
*/
constructor(executor, commands) {
constructor(executor, scope, commands) {
super(
executor.name,
executor.start,
@ -29,6 +32,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
()=>'No slash commands found!',
);
this.executor = executor;
this.scope = scope;
}
getSecondaryNameAt(text, index, isSelect) {
@ -86,8 +90,8 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
}
} else if (unamedArgLength > 0 && index >= this.executor.startUnnamedArgs && index <= this.executor.endUnnamedArgs) {
// cursor is somewhere within the unnamed arguments
//TODO if index is in first array item and that is a string, treat it as an unfinished named arg
if (typeof this.executor.unnamedArgumentList[0].value == 'string') {
// if index is in first array item and that is a string, treat it as an unfinished named arg
if (typeof this.executor.unnamedArgumentList[0]?.value == 'string') {
if (index <= this.executor.startUnnamedArgs + this.executor.unnamedArgumentList[0].value.length) {
name = this.executor.unnamedArgumentList[0].value.slice(0, index - this.executor.startUnnamedArgs);
start = this.executor.startUnnamedArgs;
@ -103,7 +107,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
if (name.includes('=') && cmdArg) {
// if cursor is already behind "=" check for enums
const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList;
if (cmdArg && enumList?.length) {
if (isSelect && enumList.find(it=>it.value == value) && argAssign && argAssign.end == index) {
return null;
@ -111,7 +115,7 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
const result = new AutoCompleteSecondaryNameResult(
value,
start + name.length,
enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
enumList.map(it=>SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)),
true,
);
result.isRequired = true;
@ -150,7 +154,10 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
if (idx > -1) {
argAssign = this.executor.unnamedArgumentList[idx];
cmdArg = this.executor.command.unnamedArgumentList[idx];
const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) {
cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0];
}
const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList;
if (cmdArg && enumList.length > 0) {
value = argAssign.value.toString().slice(0, index - argAssign.start);
start = argAssign.start;
@ -161,23 +168,26 @@ export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
value = '';
start = index;
cmdArg = notProvidedArguments[0];
if (cmdArg === undefined && this.executor.command.unnamedArgumentList.slice(-1)[0]?.acceptsMultiple) {
cmdArg = this.executor.command.unnamedArgumentList.slice(-1)[0];
}
}
} else {
return null;
}
const enumList = cmdArg?.enumProvider?.(this.executor) ?? cmdArg?.enumList;
const enumList = cmdArg?.enumProvider?.(this.executor, this.scope) ?? cmdArg?.enumList;
if (cmdArg == null || enumList.length == 0) return null;
const result = new AutoCompleteSecondaryNameResult(
value,
start,
enumList.map(it=>new SlashCommandEnumAutoCompleteOption(this.executor.command, it)),
enumList.map(it=>SlashCommandEnumAutoCompleteOption.from(this.executor.command, it)),
false,
);
const isCompleteValue = enumList.find(it=>it.value == value);
const isSelectedValue = isSelect && isCompleteValue;
result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue;
result.isRequired = cmdArg.isRequired && !isSelectedValue;
result.forceMatch = cmdArg.forceEnum;
return result;
}

View File

@ -0,0 +1,7 @@
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
export class SlashCommandBreak extends SlashCommandExecutor {
get value() {
return this.unnamedArgumentList[0]?.value;
}
}

View File

@ -0,0 +1,7 @@
export class SlashCommandBreakController {
/**@type {boolean} */ isBreak = false;
break() {
this.isBreak = true;
}
}

View File

@ -0,0 +1,3 @@
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
export class SlashCommandBreakPoint extends SlashCommandExecutor {}

View File

@ -1,9 +1,13 @@
import { substituteParams } from '../../script.js';
import { delay, escapeRegex } from '../utils.js';
import { delay, escapeRegex, uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
import { SlashCommandBreak } from './SlashCommandBreak.js';
import { SlashCommandBreakController } from './SlashCommandBreakController.js';
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
import { SlashCommandDebugController } from './SlashCommandDebugController.js';
import { SlashCommandExecutionError } from './SlashCommandExecutionError.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
import { SlashCommandScope } from './SlashCommandScope.js';
@ -17,8 +21,20 @@ export class SlashCommandClosure {
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
/**@type {SlashCommandExecutor[]}*/ executorList = [];
/**@type {SlashCommandAbortController}*/ abortController;
/**@type {SlashCommandBreakController}*/ breakController;
/**@type {SlashCommandDebugController}*/ debugController;
/**@type {(done:number, total:number)=>void}*/ onProgress;
/**@type {string}*/ rawText;
/**@type {string}*/ fullText;
/**@type {string}*/ parserContext;
/**@type {string}*/ #source = uuidv4();
get source() { return this.#source; }
set source(value) {
this.#source = value;
for (const executor of this.executorList) {
executor.source = value;
}
}
/**@type {number}*/
get commandCount() {
@ -30,7 +46,7 @@ export class SlashCommandClosure {
}
toString() {
return '[Closure]';
return `[Closure]${this.executeNow ? '()' : ''}`;
}
/**
@ -43,16 +59,37 @@ export class SlashCommandClosure {
let isList = false;
let listValues = [];
scope = scope ?? this.scope;
const macros = scope.macroList.map(it=>escapeRegex(it.key)).join('|');
const re = new RegExp(`({{pipe}})|(?:{{var::([^\\s]+?)(?:::((?!}}).+))?}})|(?:{{(${macros})}})`);
const escapeMacro = (it, isAnchored = false)=>{
const regexText = escapeRegex(it.key.replace(/\*/g, '~~~WILDCARD~~~'))
.replaceAll('~~~WILDCARD~~~', '(?:(?:(?!(?:::|}})).)*)')
;
if (isAnchored) {
return `^${regexText}$`;
}
return regexText;
};
const macroList = scope.macroList.toSorted((a,b)=>{
if (a.key.includes('*') && !b.key.includes('*')) return 1;
if (!a.key.includes('*') && b.key.includes('*')) return -1;
if (a.key.includes('*') && b.key.includes('*')) return b.key.indexOf('*') - a.key.indexOf('*');
return 0;
});
const macros = macroList.map(it=>escapeMacro(it)).join('|');
const re = new RegExp(`(?<pipe>{{pipe}})|(?:{{var::(?<var>[^\\s]+?)(?:::(?<varIndex>(?!}}).+))?}})|(?:{{(?<macro>${macros})}})`);
let done = '';
let remaining = text;
while (re.test(remaining)) {
const match = re.exec(remaining);
const before = substituteParams(remaining.slice(0, match.index));
const after = remaining.slice(match.index + match[0].length);
const replacer = match[1] ? scope.pipe : match[2] ? scope.getVariable(match[2], match[3]) : scope.macroList.find(it=>it.key == match[4])?.value;
const replacer = match.groups.pipe ? scope.pipe : match.groups.var ? scope.getVariable(match.groups.var, match.groups.index) : macroList.find(it=>it.key == match.groups.macro || new RegExp(escapeMacro(it, true)).test(match.groups.macro))?.value;
if (replacer instanceof SlashCommandClosure) {
replacer.abortController = this.abortController;
replacer.breakController = this.breakController;
replacer.scope.parent = this.scope;
if (this.debugController && !replacer.debugController) {
replacer.debugController = this.debugController;
}
isList = true;
if (match.index > 0) {
listValues.push(before);
@ -87,6 +124,12 @@ export class SlashCommandClosure {
closure.providedArgumentList = this.providedArgumentList;
closure.executorList = this.executorList;
closure.abortController = this.abortController;
closure.breakController = this.breakController;
closure.debugController = this.debugController;
closure.rawText = this.rawText;
closure.fullText = this.fullText;
closure.parserContext = this.parserContext;
closure.source = this.source;
closure.onProgress = this.onProgress;
return closure;
}
@ -96,11 +139,22 @@ export class SlashCommandClosure {
* @returns {Promise<SlashCommandClosureResult>}
*/
async execute() {
// execute a copy of the closure to no taint it and its scope with the effects of its execution
// as this would affect the closure being called a second time (e.g., loop, multiple /run calls)
const closure = this.getCopy();
return await closure.executeDirect();
const gen = closure.executeDirect();
let step;
while (!step?.done) {
step = await gen.next(this.debugController?.testStepping(this) ?? false);
if (!(step.value instanceof SlashCommandClosureResult) && this.debugController) {
this.debugController.isStepping = await this.debugController.awaitBreakPoint(step.value.closure, step.value.executor);
}
}
return step.value;
}
async executeDirect() {
async * executeDirect() {
this.debugController?.down(this);
// closure arguments
for (const arg of this.argumentList) {
let v = arg.value;
@ -108,6 +162,7 @@ export class SlashCommandClosure {
/**@type {SlashCommandClosure}*/
const closure = v;
closure.scope.parent = this.scope;
closure.breakController = this.breakController;
if (closure.executeNow) {
v = (await closure.execute())?.pipe;
} else {
@ -131,6 +186,7 @@ export class SlashCommandClosure {
/**@type {SlashCommandClosure}*/
const closure = v;
closure.scope.parent = this.scope;
closure.breakController = this.breakController;
if (closure.executeNow) {
v = (await closure.execute())?.pipe;
} else {
@ -149,106 +205,145 @@ export class SlashCommandClosure {
this.scope.setVariable(arg.name, v);
}
let done = 0;
if (this.executorList.length == 0) {
this.scope.pipe = '';
}
const stepper = this.executeStep();
let step;
while (!step?.done && !this.breakController?.isBreak) {
// get executor before execution
step = await stepper.next();
if (step.value instanceof SlashCommandBreakPoint) {
console.log('encountered SlashCommandBreakPoint');
if (this.debugController) {
// resolve args
step = await stepper.next();
// "execute" breakpoint
step = await stepper.next();
// get next executor
step = await stepper.next();
// breakpoint has to yield before arguments are resolved if one of the
// arguments is an immediate closure, otherwise you cannot step into the
// immediate closure
const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) {
this.debugController.isStepping = yield { closure:this, executor:step.value };
} else {
this.debugController.isStepping = true;
this.debugController.stepStack[this.debugController.stepStack.length - 1] = true;
}
}
} else if (!step.done && this.debugController?.testStepping(this)) {
this.debugController.isSteppingInto = false;
// if stepping, have to yield before arguments are resolved if one of the arguments
// is an immediate closure, otherwise you cannot step into the immediate closure
const hasImmediateClosureInNamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.namedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
const hasImmediateClosureInUnnamedArgs = /**@type {SlashCommandExecutor}*/(step.value)?.unnamedArgumentList?.find(it=>it.value instanceof SlashCommandClosure && it.value.executeNow);
if (hasImmediateClosureInNamedArgs || hasImmediateClosureInUnnamedArgs) {
this.debugController.isStepping = yield { closure:this, executor:step.value };
}
}
// resolve args
step = await stepper.next();
if (step.value instanceof SlashCommandBreak) {
console.log('encountered SlashCommandBreak');
if (this.breakController) {
this.breakController?.break();
break;
}
} else if (!step.done && this.debugController?.testStepping(this)) {
this.debugController.isSteppingInto = false;
this.debugController.isStepping = yield { closure:this, executor:step.value };
}
// execute executor
step = await stepper.next();
}
// if execution has returned a closure result, return that (should only happen on abort)
if (step.value instanceof SlashCommandClosureResult) {
this.debugController?.up();
return step.value;
}
/**@type {SlashCommandClosureResult} */
const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe, isBreak: this.breakController?.isBreak ?? false });
this.debugController?.up();
return result;
}
/**
* Generator that steps through the executor list.
* Every executor is split into three steps:
* - before arguments are resolved
* - after arguments are resolved
* - after execution
*/
async * executeStep() {
let done = 0;
let isFirst = true;
for (const executor of this.executorList) {
this.onProgress?.(done, this.commandCount);
if (executor instanceof SlashCommandClosureExecutor) {
const closure = this.scope.getVariable(executor.name);
if (!closure || !(closure instanceof SlashCommandClosure)) throw new Error(`${executor.name} is not a closure.`);
closure.scope.parent = this.scope;
closure.providedArgumentList = executor.providedArgumentList;
const result = await closure.execute();
this.scope.pipe = result.pipe;
if (this.debugController) {
this.debugController.setExecutor(executor);
this.debugController.namedArguments = undefined;
this.debugController.unnamedArguments = undefined;
}
// yield before doing anything with this executor, the debugger might want to do
// something with it (e.g., breakpoint, immediate closures that need resolving
// or stepping into)
yield executor;
/**@type {import('./SlashCommand.js').NamedArguments} */
// @ts-ignore
let args = {
_scope: this.scope,
_parserFlags: executor.parserFlags,
_abortController: this.abortController,
_debugController: this.debugController,
_hasUnnamedArgument: executor.unnamedArgumentList.length > 0,
};
if (executor instanceof SlashCommandBreakPoint) {
// nothing to do for breakpoints, just raise counter and yield for "before exec"
done++;
yield executor;
isFirst = false;
} else if (executor instanceof SlashCommandBreak) {
// /break need to resolve the unnamed arg and put it into pipe, then yield
// for "before exec"
const value = await this.substituteUnnamedArgument(executor, isFirst, args);
done += this.executorList.length - this.executorList.indexOf(executor);
this.scope.pipe = value ?? this.scope.pipe;
yield executor;
isFirst = false;
} else {
/**@type {import('./SlashCommand.js').NamedArguments} */
let args = {
_scope: this.scope,
_parserFlags: executor.parserFlags,
_abortController: this.abortController,
_hasUnnamedArgument: executor.unnamedArgumentList.length > 0,
};
let value;
// substitute named arguments
for (const arg of executor.namedArgumentList) {
if (arg.value instanceof SlashCommandClosure) {
/**@type {SlashCommandClosure}*/
const closure = arg.value;
closure.scope.parent = this.scope;
if (closure.executeNow) {
args[arg.name] = (await closure.execute())?.pipe;
} else {
args[arg.name] = closure;
}
} else {
args[arg.name] = this.substituteParams(arg.value);
}
// unescape named argument
if (typeof args[arg.name] == 'string') {
args[arg.name] = args[arg.name]
?.replace(/\\\{/g, '{')
?.replace(/\\\}/g, '}')
;
}
}
// substitute unnamed argument
if (executor.unnamedArgumentList.length == 0) {
if (executor.injectPipe) {
value = this.scope.pipe;
args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined;
}
} else {
value = [];
for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
let v = executor.unnamedArgumentList[i].value;
if (v instanceof SlashCommandClosure) {
/**@type {SlashCommandClosure}*/
const closure = v;
closure.scope.parent = this.scope;
if (closure.executeNow) {
v = (await closure.execute())?.pipe;
} else {
v = closure;
}
} else {
v = this.substituteParams(v);
}
value[i] = v;
}
if (!executor.command.splitUnnamedArgument) {
if (value.length == 1) {
value = value[0];
} else if (!value.find(it=>it instanceof SlashCommandClosure)) {
value = value.join('');
}
}
}
// unescape unnamed argument
if (typeof value == 'string') {
value = value
?.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;
});
}
// regular commands do all the argument resolving logic...
await this.substituteNamedArguments(executor, args);
let value = await this.substituteUnnamedArgument(executor, isFirst, args);
let abortResult = await this.testAbortController();
if (abortResult) {
return abortResult;
}
if (this.debugController) {
this.debugController.namedArguments = args;
this.debugController.unnamedArguments = value ?? '';
}
// then yield for "before exec"
yield executor;
// followed by command execution
executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
this.scope.pipe = await executor.command.callback(args, value ?? '');
const isStepping = this.debugController?.testStepping(this);
if (this.debugController) {
this.debugController.isStepping = false || this.debugController.isSteppingInto;
}
try {
this.scope.pipe = await executor.command.callback(args, value ?? '');
} catch (ex) {
throw new SlashCommandExecutionError(ex, ex.message, executor.name, executor.start, executor.end, this.fullText.slice(executor.start, executor.end), this.fullText);
}
if (this.debugController) {
this.debugController.namedArguments = undefined;
this.debugController.unnamedArguments = undefined;
this.debugController.isStepping = isStepping;
}
this.#lintPipe(executor.command);
done += executor.commandCount;
this.onProgress?.(done, this.commandCount);
@ -257,10 +352,10 @@ export class SlashCommandClosure {
return abortResult;
}
}
// finally, yield for "after exec"
yield executor;
isFirst = false;
}
/**@type {SlashCommandClosureResult} */
const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe });
return result;
}
async testPaused() {
@ -279,14 +374,113 @@ export class SlashCommandClosure {
}
}
/**
* @param {SlashCommandExecutor} executor
* @param {import('./SlashCommand.js').NamedArguments} args
*/
async substituteNamedArguments(executor, args) {
// substitute named arguments
for (const arg of executor.namedArgumentList) {
if (arg.value instanceof SlashCommandClosure) {
/**@type {SlashCommandClosure}*/
const closure = arg.value;
closure.scope.parent = this.scope;
closure.breakController = this.breakController;
if (this.debugController && !closure.debugController) {
closure.debugController = this.debugController;
}
if (closure.executeNow) {
args[arg.name] = (await closure.execute())?.pipe;
} else {
args[arg.name] = closure;
}
} else {
args[arg.name] = this.substituteParams(arg.value);
}
// unescape named argument
if (typeof args[arg.name] == 'string') {
args[arg.name] = args[arg.name]
?.replace(/\\\{/g, '{')
?.replace(/\\\}/g, '}')
;
}
}
}
/**
* @param {SlashCommandExecutor} executor
* @param {boolean} isFirst
* @param {import('./SlashCommand.js').NamedArguments} args
* @returns {Promise<string|SlashCommandClosure|(string|SlashCommandClosure)[]>}
*/
async substituteUnnamedArgument(executor, isFirst, args) {
let value;
// substitute unnamed argument
if (executor.unnamedArgumentList.length == 0) {
if (!isFirst && executor.injectPipe) {
value = this.scope.pipe;
args._hasUnnamedArgument = this.scope.pipe !== null && this.scope.pipe !== undefined;
}
} else {
value = [];
for (let i = 0; i < executor.unnamedArgumentList.length; i++) {
let v = executor.unnamedArgumentList[i].value;
if (v instanceof SlashCommandClosure) {
/**@type {SlashCommandClosure}*/
const closure = v;
closure.scope.parent = this.scope;
closure.breakController = this.breakController;
if (this.debugController && !closure.debugController) {
closure.debugController = this.debugController;
}
if (closure.executeNow) {
v = (await closure.execute())?.pipe;
} else {
v = closure;
}
} else {
v = this.substituteParams(v);
}
value[i] = v;
}
if (!executor.command.splitUnnamedArgument) {
if (value.length == 1) {
value = value[0];
} else if (!value.find(it=>it instanceof SlashCommandClosure)) {
value = value.join('');
}
}
}
// unescape unnamed argument
if (typeof value == 'string') {
value = value
?.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;
});
}
return value;
}
/**
* Auto-fixes the pipe if it is not a valid result for STscript.
* @param {SlashCommand} command Command being executed
*/
#lintPipe(command) {
if (this.scope.pipe === undefined || this.scope.pipe === null) {
console.warn(`${command.name} returned undefined or null. Auto-fixing to empty string.`);
console.warn(`/${command.name} returned undefined or null. Auto-fixing to empty string.`);
this.scope.pipe = '';
} else if (!(typeof this.scope.pipe == 'string' || this.scope.pipe instanceof SlashCommandClosure)) {
console.warn(`/${command.name} returned illegal type (${typeof this.scope.pipe} - ${this.scope.pipe.constructor?.name ?? ''}). Auto-fixing to stringified JSON.`);
this.scope.pipe = JSON.stringify(this.scope.pipe) ?? '';
}
}
}

View File

@ -1,7 +0,0 @@
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
export class SlashCommandClosureExecutor {
/**@type {String}*/ name = '';
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
}

View File

@ -1,6 +1,7 @@
export class SlashCommandClosureResult {
/**@type {boolean}*/ interrupt = false;
/**@type {string}*/ pipe;
/**@type {boolean}*/ isBreak = false;
/**@type {boolean}*/ isAborted = false;
/**@type {boolean}*/ isQuietlyAborted = false;
/**@type {string}*/ abortReason;

View File

@ -6,6 +6,7 @@ import { searchCharByName, getTagsList, tags } from '../tags.js';
import { world_names } from '../world-info.js';
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandEnumValue, enumTypes } from './SlashCommandEnumValue.js';
import { SlashCommandScope } from "./SlashCommandScope.js";
/**
* A collection of regularly used enum icons
@ -134,16 +135,16 @@ export const commonEnumProviders = {
* Can be filtered by `type` to only show global or local variables
*
* @param {...('global'|'local'|'scope'|'all')} type - The type of variables to include in the array. Can be 'all', 'global', or 'local'.
* @returns {() => SlashCommandEnumValue[]}
* @returns {(executor:SlashCommandExecutor, scope:SlashCommandScope) => SlashCommandEnumValue[]}
*/
variables: (...type) => () => {
variables: (...type) => (executor, scope) => {
const types = type.flat();
const isAll = types.includes('all');
return [
...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [],
...isAll || types.includes('scope') ? scope.allVariableNames.map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [],
...isAll || types.includes('local') ? Object.keys(chat_metadata.variables ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.name, enumIcons.localVariable)) : [],
...isAll || types.includes('scope') ? [].map(name => new SlashCommandEnumValue(name, null, enumTypes.variable, enumIcons.scopeVariable)) : [], // TODO: Add scoped variables here, Lenny
];
...isAll || types.includes('global') ? Object.keys(extension_settings.variables.global ?? []).map(name => new SlashCommandEnumValue(name, null, enumTypes.macro, enumIcons.globalVariable)) : [],
].filter((item, idx, list)=>idx == list.findIndex(it=>it.value == item.value));
},
/**

View File

@ -0,0 +1,83 @@
import { SlashCommandClosure } from './SlashCommandClosure.js';
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
export class SlashCommandDebugController {
/**@type {SlashCommandClosure[]} */ stack = [];
/**@type {SlashCommandExecutor[]} */ cmdStack = [];
/**@type {boolean[]} */ stepStack = [];
/**@type {boolean} */ isStepping = false;
/**@type {boolean} */ isSteppingInto = false;
/**@type {boolean} */ isSteppingOut = false;
/**@type {object} */ namedArguments;
/**@type {string|SlashCommandClosure|(string|SlashCommandClosure)[]} */ unnamedArguments;
/**@type {Promise<boolean>} */ continuePromise;
/**@type {(boolean)=>void} */ continueResolver;
/**@type {(closure:SlashCommandClosure, executor:SlashCommandExecutor)=>Promise<boolean>} */ onBreakPoint;
testStepping(closure) {
return this.stepStack[this.stack.indexOf(closure)];
}
down(closure) {
this.stack.push(closure);
if (this.stepStack.length < this.stack.length) {
this.stepStack.push(this.isSteppingInto);
}
}
up() {
this.stack.pop();
while (this.cmdStack.length > this.stack.length) this.cmdStack.pop();
this.stepStack.pop();
}
setExecutor(executor) {
this.cmdStack[this.stack.length - 1] = executor;
}
resume() {
this.continueResolver?.(false);
this.continuePromise = null;
this.stepStack.forEach((_,idx)=>this.stepStack[idx] = false);
}
step() {
this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true);
this.continueResolver?.(true);
this.continuePromise = null;
}
stepInto() {
this.isSteppingInto = true;
this.stepStack.forEach((_,idx)=>this.stepStack[idx] = true);
this.continueResolver?.(true);
this.continuePromise = null;
}
stepOut() {
this.isSteppingOut = true;
this.stepStack[this.stepStack.length - 1] = false;
this.continueResolver?.(false);
this.continuePromise = null;
}
async awaitContinue() {
this.continuePromise ??= new Promise(resolve=>{
this.continueResolver = resolve;
});
this.isStepping = await this.continuePromise;
return this.isStepping;
}
async awaitBreakPoint(closure, executor) {
this.isStepping = await this.onBreakPoint(closure, executor);
return this.isStepping;
}
}

View File

@ -3,6 +3,17 @@ import { SlashCommand } from './SlashCommand.js';
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
/**
* @param {SlashCommand} cmd
* @param {SlashCommandEnumValue} enumValue
* @returns {SlashCommandEnumAutoCompleteOption}
*/
static from(cmd, enumValue) {
const mapped = this.valueToOptionMap.find(it=>enumValue instanceof it.value)?.option ?? this;
return new mapped(cmd, enumValue);
}
/**@type {{value:(typeof SlashCommandEnumValue), option:(typeof SlashCommandEnumAutoCompleteOption)}[]} */
static valueToOptionMap = [];
/**@type {SlashCommand}*/ cmd;
/**@type {SlashCommandEnumValue}*/ enumValue;
@ -13,7 +24,7 @@ export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
* @param {SlashCommandEnumValue} enumValue
*/
constructor(cmd, enumValue) {
super(enumValue.value, enumValue.typeIcon, enumValue.type);
super(enumValue.value, enumValue.typeIcon, enumValue.type, enumValue.matchProvider, enumValue.valueProvider, enumValue.makeSelectable);
this.cmd = cmd;
this.enumValue = enumValue;
}

View File

@ -1,3 +1,6 @@
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
import { SlashCommandScope } from './SlashCommandScope.js';
/**
* @typedef {'enum' | 'command' | 'namedArgument' | 'variable' | 'qr' | 'macro' | 'number' | 'name'} EnumType
@ -37,14 +40,17 @@ export const enumTypes = {
getBasedOnIndex(index) {
const keys = Object.keys(this);
return this[keys[(index ?? 0) % keys.length]];
}
}
},
};
export class SlashCommandEnumValue {
/**@type {string}*/ value;
/**@type {string}*/ description;
/**@type {EnumType}*/ type = 'enum';
/**@type {string}*/ typeIcon = '◊';
/**@type {(input:string)=>boolean}*/ matchProvider;
/**@type {(input:string)=>string}*/ valueProvider;
/**@type {boolean}*/ makeSelectable = false;
/**
* A constructor for creating a SlashCommandEnumValue instance.
@ -52,13 +58,19 @@ export class SlashCommandEnumValue {
* @param {string} value - The value
* @param {string?} description - Optional description, displayed in a second line
* @param {EnumType?} type - type of the enum (defining its color)
* @param {string} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones)
* @param {string?} typeIcon - The icon to display (Can be pulled from `enumIcons` for common ones)
* @param {(input:string)=>boolean?} matchProvider - A custom function to match autocomplete input instead of startsWith/includes/fuzzy. Should only be used for generic options like "any number" or "any string". "input" is the part of the text that is getting auto completed.
* @param {(input:string)=>string?} valueProvider - A function returning a value to be used in autocomplete instead of the enum value. "input" is the part of the text that is getting auto completed. By default, values with a valueProvider will not be selectable in the autocomplete (with tab/enter).
* @param {boolean?} makeSelectable - Set to true to make the value selectable (through tab/enter) even though a valueProvider exists.
*/
constructor(value, description = null, type = 'enum', typeIcon = '◊') {
constructor(value, description = null, type = 'enum', typeIcon = '◊', matchProvider = null, valueProvider = null, makeSelectable = false) {
this.value = value;
this.description = description;
this.type = type ?? 'enum';
this.typeIcon = typeIcon;
this.matchProvider = matchProvider;
this.valueProvider = valueProvider;
this.makeSelectable = makeSelectable;
}
toString() {

View File

@ -0,0 +1,60 @@
export class SlashCommandExecutionError extends Error {
/**@type {string} */ commandName;
/**@type {number} */ start;
/**@type {number} */ end;
/**@type {string} */ commandText;
/**@type {string} */ text;
get index() { return this.start; }
get line() {
return this.text.slice(0, this.index).replace(/[^\n]/g, '').length;
}
get column() {
return this.text.slice(0, this.index).split('\n').pop().length;
}
get hint() {
let lineOffset = this.line.toString().length;
let lineStart = this.index;
let start = this.index;
let end = this.index;
let offset = 0;
let lineCount = 0;
while (offset < 10000 && lineCount < 3 && start >= 0) {
if (this.text[start] == '\n') lineCount++;
if (lineCount == 0) lineStart--;
offset++;
start--;
}
if (this.text[start + 1] == '\n') start++;
offset = 0;
while (offset < 10000 && this.text[end] != '\n') {
offset++;
end++;
}
let hint = [];
let lines = this.text.slice(start + 1, end - 1).split('\n');
let lineNum = this.line - lines.length + 1;
let tabOffset = 0;
for (const line of lines) {
const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`;
lineNum++;
const untabbedLine = line.replace(/\t/g, ' '.repeat(4));
tabOffset = untabbedLine.length - line.length;
hint.push(`${num}: ${untabbedLine}`);
}
hint.push(`${' '.repeat(this.index - lineStart + lineOffset + 1 + tabOffset)}^^^^^`);
return hint.join('\n');
}
constructor(cause, message, commandName, start, end, commandText, fullText) {
super(message, { cause });
this.commandName = commandName;
this.start = start;
this.end = end;
this.commandText = commandText;
this.text = fullText;
}
}

View File

@ -1,4 +1,5 @@
// eslint-disable-next-line no-unused-vars
import { uuidv4 } from '../utils.js';
import { SlashCommand } from './SlashCommand.js';
// eslint-disable-next-line no-unused-vars
import { SlashCommandClosure } from './SlashCommandClosure.js';
@ -16,6 +17,17 @@ export class SlashCommandExecutor {
/**@type {Number}*/ startUnnamedArgs;
/**@type {Number}*/ endUnnamedArgs;
/**@type {String}*/ name = '';
/**@type {String}*/ #source = uuidv4();
get source() { return this.#source; }
set source(value) {
this.#source = value;
for (const arg of this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) {
arg.value.source = value;
}
for (const arg of this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure)) {
arg.value.source = value;
}
}
/**@type {SlashCommand}*/ command;
// @ts-ignore
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];

View File

@ -17,7 +17,10 @@ import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNa
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';
import { SlashCommandBreakPoint } from './SlashCommandBreakPoint.js';
import { SlashCommandDebugController } from './SlashCommandDebugController.js';
import { commonEnumProviders } from './SlashCommandCommonEnumsProvider.js';
import { SlashCommandBreak } from './SlashCommandBreak.js';
/** @typedef {import('./SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture */
/** @typedef {import('./SlashCommand.js').NamedArguments} NamedArguments */
@ -53,7 +56,7 @@ export class SlashCommandParser {
* @param {SlashCommand} command
*/
static addCommandObject(command) {
const reserved = ['/', '#', ':', 'parser-flag'];
const reserved = ['/', '#', ':', 'parser-flag', 'breakpoint'];
for (const start of reserved) {
if (command.name.toLowerCase().startsWith(start) || (command.aliases ?? []).find(a=>a.toLowerCase().startsWith(start))) {
throw new Error(`Illegal Name. Slash command name cannot begin with "${start}".`);
@ -70,6 +73,18 @@ export class SlashCommandParser {
console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]);
}
const stack = new Error().stack.split('\n').map(it=>it.trim());
command.isExtension = stack.find(it=>it.includes('/scripts/extensions/')) != null;
command.isThirdParty = stack.find(it=>it.includes('/scripts/extensions/third-party/')) != null;
if (command.isThirdParty) {
command.source = stack.find(it=>it.includes('/scripts/extensions/third-party/')).replace(/^.*?\/scripts\/extensions\/third-party\/([^/]+)\/.*$/, '$1');
} else if (command.isExtension) {
command.source = stack.find(it=>it.includes('/scripts/extensions/')).replace(/^.*?\/scripts\/extensions\/([^/]+)\/.*$/, '$1');
} else {
const idx = stack.findLastIndex(it=>it.includes('at SlashCommandParser.')) + 1;
command.source = stack[idx].replace(/^.*?\/((?:scripts\/)?(?:[^/]+)\.js).*$/, '$1');
}
this.commands[command.name] = command;
if (Array.isArray(command.aliases)) {
@ -89,6 +104,7 @@ export class SlashCommandParser {
/**@type {string}*/ text;
/**@type {number}*/ index;
/**@type {SlashCommandAbortController}*/ abortController;
/**@type {SlashCommandDebugController}*/ debugController;
/**@type {SlashCommandScope}*/ scope;
/**@type {SlashCommandClosure}*/ closure;
@ -101,6 +117,8 @@ export class SlashCommandParser {
/**@type {SlashCommandExecutor[]}*/ commandIndex;
/**@type {SlashCommandScope[]}*/ scopeIndex;
/**@type {string}*/ parserContext;
get userIndex() { return this.index; }
get ahead() {
@ -154,6 +172,21 @@ export class SlashCommandParser {
helpString: 'Write a comment.',
}));
}
if (!Object.keys(this.commands).includes('breakpoint')) {
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'breakpoint',
helpString: 'Set a breakpoint for debugging in the QR Editor.',
}));
}
if (!Object.keys(this.commands).includes('break')) {
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'break',
helpString: 'Break out of a loop or closure executed through /run or /:',
unnamedArgumentList: [
SlashCommandArgument.fromProps({ description: 'value to pass down the pipe instead of the current pipe value',
typeList: Object.values(ARGUMENT_TYPE),
}),
],
}));
}
//TODO should not be re-registered from every instance
this.registerLanguage();
@ -191,13 +224,19 @@ export class SlashCommandParser {
function getQuotedRunRegex() {
try {
return new RegExp('(".+?(?<!\\\\)")|(\\S+?)');
return new RegExp('(".+?(?<!\\\\)")|(\\S+?)(\\||$|\\s)');
} catch {
// fallback for browsers that don't support lookbehind
return /(".+?")|(\S+?)/;
return /(".+?")|(\S+?)(\||$|\s)/;
}
}
const BLOCK_COMMENT = {
scope: 'comment',
begin: /\/\*/,
end: /\*\|/,
contains: [],
};
const COMMENT = {
scope: 'comment',
begin: /\/[/#]/,
@ -205,9 +244,29 @@ export class SlashCommandParser {
contains: [],
};
const ABORT = {
scope: 'abort',
begin: /\/abort/,
end: /\||$|:}/,
begin: /\/(abort|breakpoint)/,
beginScope: 'abort',
end: /\||$|(?=:})/,
excludeEnd: false,
returnEnd: true,
contains: [],
};
const IMPORT = {
scope: 'command',
begin: /\/(import)/,
beginScope: 'keyword',
end: /\||$|(?=:})/,
excludeEnd: false,
returnEnd: true,
contains: [],
};
const BREAK = {
scope: 'command',
begin: /\/(break)/,
beginScope: 'keyword',
end: /\||$|(?=:})/,
excludeEnd: false,
returnEnd: true,
contains: [],
};
const LET = {
@ -218,26 +277,31 @@ export class SlashCommandParser {
1: 'variable',
},
end: /\||$|:}/,
excludeEnd: false,
returnEnd: true,
contains: [],
};
const SETVAR = {
begin: /\/(setvar|setglobalvar)\s+/,
beginScope: 'variable',
end: /\||$|:}/,
excludeEnd: true,
excludeEnd: false,
returnEnd: true,
contains: [],
};
const GETVAR = {
begin: /\/(getvar|getglobalvar)\s+/,
beginScope: 'variable',
end: /\||$|:}/,
excludeEnd: true,
excludeEnd: false,
returnEnd: true,
contains: [],
};
const RUN = {
match: [
/\/:/,
getQuotedRunRegex(),
/\||$|(?=:})/,
],
className: {
1: 'variable.language',
@ -250,7 +314,8 @@ export class SlashCommandParser {
begin: /\/\S+/,
beginScope: 'title.function',
end: /\||$|(?=:})/,
excludeEnd: true,
excludeEnd: false,
returnEnd: true,
contains: [], // defined later
};
const CLOSURE = {
@ -271,6 +336,19 @@ export class SlashCommandParser {
begin: /{{/,
end: /}}/,
};
const PIPEBREAK = {
beginScope: 'pipebreak',
begin: /\|\|/,
end: '',
};
const PIPE = {
beginScope: 'pipe',
begin: /\|/,
end: '',
};
BLOCK_COMMENT.contains.push(
BLOCK_COMMENT,
);
RUN.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
@ -279,6 +357,22 @@ export class SlashCommandParser {
MACRO,
CLOSURE,
);
IMPORT.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
NUMBER,
MACRO,
CLOSURE,
hljs.QUOTE_STRING_MODE,
);
BREAK.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
NUMBER,
MACRO,
CLOSURE,
hljs.QUOTE_STRING_MODE,
);
LET.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
@ -303,6 +397,14 @@ export class SlashCommandParser {
MACRO,
CLOSURE,
);
ABORT.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
NUMBER,
MACRO,
CLOSURE,
hljs.QUOTE_STRING_MODE,
);
COMMAND.contains.push(
hljs.BACKSLASH_ESCAPE,
NAMED_ARG,
@ -313,8 +415,11 @@ export class SlashCommandParser {
);
CLOSURE.contains.push(
hljs.BACKSLASH_ESCAPE,
BLOCK_COMMENT,
COMMENT,
ABORT,
IMPORT,
BREAK,
NAMED_ARG,
NUMBER,
MACRO,
@ -325,20 +430,27 @@ export class SlashCommandParser {
COMMAND,
'self',
hljs.QUOTE_STRING_MODE,
PIPEBREAK,
PIPE,
);
hljs.registerLanguage('stscript', ()=>({
case_insensitive: false,
keywords: ['|'],
keywords: [],
contains: [
hljs.BACKSLASH_ESCAPE,
BLOCK_COMMENT,
COMMENT,
ABORT,
IMPORT,
BREAK,
RUN,
LET,
GETVAR,
SETVAR,
COMMAND,
CLOSURE,
PIPEBREAK,
PIPE,
],
}));
}
@ -415,7 +527,7 @@ export class SlashCommandParser {
);
return result;
}
const result = new SlashCommandAutoCompleteNameResult(executor, this.commands);
const result = new SlashCommandAutoCompleteNameResult(executor, this.scopeIndex[this.commandIndex.indexOf(executor)], this.commands);
return result;
}
return null;
@ -515,11 +627,14 @@ export class SlashCommandParser {
}
replaceGetvar(value) {
return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (_, cmd, name) => {
return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (match, cmd, name, idx) => {
name = name.trim();
const startIdx = this.index - value.length + idx;
const endIdx = this.index - value.length + idx + match.length;
// store pipe
const pipeName = `_PARSER_${uuidv4()}`;
const storePipe = new SlashCommandExecutor(null); {
const pipeName = `_PARSER_PIPE_${uuidv4()}`;
const storePipe = new SlashCommandExecutor(startIdx); {
storePipe.end = endIdx;
storePipe.command = this.commands['let'];
storePipe.name = 'let';
const nameAss = new SlashCommandUnnamedArgumentAssignment();
@ -530,17 +645,19 @@ export class SlashCommandParser {
this.closure.executorList.push(storePipe);
}
// getvar / getglobalvar
const getvar = new SlashCommandExecutor(null); {
const getvar = new SlashCommandExecutor(startIdx); {
getvar.end = endIdx;
getvar.command = this.commands[cmd];
getvar.name = 'cmd';
getvar.name = cmd;
const nameAss = new SlashCommandUnnamedArgumentAssignment();
nameAss.value = name;
getvar.unnamedArgumentList = [nameAss];
this.closure.executorList.push(getvar);
}
// set to temp scoped var
const varName = `_PARSER_${uuidv4()}`;
const setvar = new SlashCommandExecutor(null); {
const varName = `_PARSER_VAR_${uuidv4()}`;
const setvar = new SlashCommandExecutor(startIdx); {
setvar.end = endIdx;
setvar.command = this.commands['let'];
setvar.name = 'let';
const nameAss = new SlashCommandUnnamedArgumentAssignment();
@ -551,7 +668,8 @@ export class SlashCommandParser {
this.closure.executorList.push(setvar);
}
// return pipe
const returnPipe = new SlashCommandExecutor(null); {
const returnPipe = new SlashCommandExecutor(startIdx); {
returnPipe.end = endIdx;
returnPipe.command = this.commands['return'];
returnPipe.name = 'return';
const varAss = new SlashCommandUnnamedArgumentAssignment();
@ -564,12 +682,13 @@ export class SlashCommandParser {
}
parse(text, verifyCommandNames = true, flags = null, abortController = null) {
parse(text, verifyCommandNames = true, flags = null, abortController = null, debugController = null) {
this.verifyCommandNames = verifyCommandNames;
for (const key of Object.keys(PARSER_FLAG)) {
this.flags[PARSER_FLAG[key]] = flags?.[PARSER_FLAG[key]] ?? power_user.stscript.parser.flags[PARSER_FLAG[key]] ?? false;
}
this.abortController = abortController;
this.debugController = debugController;
this.text = text;
this.index = 0;
this.scope = null;
@ -577,6 +696,7 @@ export class SlashCommandParser {
this.commandIndex = [];
this.scopeIndex = [];
this.macroIndex = [];
this.parserContext = uuidv4();
const closure = this.parseClosure(true);
return closure;
}
@ -604,8 +724,12 @@ export class SlashCommandParser {
if (!isRoot) this.take(2); // discard opening {:
const textStart = this.index;
let closure = new SlashCommandClosure(this.scope);
closure.parserContext = this.parserContext;
closure.fullText = this.text;
closure.abortController = this.abortController;
closure.debugController = this.debugController;
this.scope = closure.scope;
const oldClosure = this.closure;
this.closure = closure;
this.discardWhitespace();
while (this.testNamedArgument()) {
@ -615,7 +739,9 @@ export class SlashCommandParser {
this.discardWhitespace();
}
while (!this.testClosureEnd()) {
if (this.testComment()) {
if (this.testBlockComment()) {
this.parseBlockComment();
} else if (this.testComment()) {
this.parseComment();
} else if (this.testParserFlag()) {
this.parseParserFlag();
@ -623,6 +749,14 @@ export class SlashCommandParser {
const cmd = this.parseRunShorthand();
closure.executorList.push(cmd);
injectPipe = true;
} else if (this.testBreakPoint()) {
const bp = this.parseBreakPoint();
if (this.debugController) {
closure.executorList.push(bp);
}
} else if (this.testBreak()) {
const b = this.parseBreak();
closure.executorList.push(b);
} else if (this.testCommand()) {
const cmd = this.parseCommand();
cmd.injectPipe = injectPipe;
@ -651,14 +785,83 @@ export class SlashCommandParser {
}
closureIndexEntry.end = this.index - 1;
this.scope = closure.scope.parent;
this.closure = oldClosure ?? closure;
return closure;
}
testBreakPoint() {
return this.testSymbol(/\/breakpoint\s*\|/);
}
parseBreakPoint() {
const cmd = new SlashCommandBreakPoint();
cmd.name = 'breakpoint';
cmd.command = this.commands['breakpoint'];
cmd.start = this.index + 1;
this.take('/breakpoint'.length);
cmd.end = this.index;
this.commandIndex.push(cmd);
this.scopeIndex.push(this.scope.getCopy());
return cmd;
}
testBreak() {
return this.testSymbol(/\/break(\s|\||$)/);
}
parseBreak() {
const cmd = new SlashCommandBreak();
cmd.name = 'break';
cmd.command = this.commands['break'];
cmd.start = this.index + 1;
this.take('/break'.length);
this.discardWhitespace();
if (this.testUnnamedArgument()) {
cmd.unnamedArgumentList.push(...this.parseUnnamedArgument());
}
cmd.end = this.index;
this.commandIndex.push(cmd);
this.scopeIndex.push(this.scope.getCopy());
return cmd;
}
testBlockComment() {
return this.testSymbol('/*');
}
testBlockCommentEnd() {
if (!this.verifyCommandNames) {
if (this.index >= this.text.length) return true;
} else {
if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed block comment at position ${this.userIndex}`, this.text, this.index);
}
return this.testSymbol('*|');
}
parseBlockComment() {
const start = this.index + 1;
const cmd = new SlashCommandExecutor(start);
cmd.command = this.commands['*'];
this.commandIndex.push(cmd);
this.scopeIndex.push(this.scope.getCopy());
this.take(); // discard "/"
cmd.name = this.take(); //set "*" as name
while (!this.testBlockCommentEnd()) {
if (this.testBlockComment()) {
this.parseBlockComment();
}
this.take();
}
this.take(2); // take closing "*|"
cmd.end = this.index - 1;
}
testComment() {
return this.testSymbol(/\/[/#]/);
}
testCommentEnd() {
return this.testCommandEnd();
if (!this.verifyCommandNames) {
if (this.index >= this.text.length) return true;
} else {
if (this.endOfText) throw new SlashCommandParserError(`Unclosed comment at position ${this.userIndex}`, this.text, this.index);
}
return this.testSymbol('|');
}
parseComment() {
const start = this.index + 1;
@ -719,11 +922,13 @@ export class SlashCommandParser {
else assignment.value = this.parseValue();
cmd.unnamedArgumentList = [assignment];
this.discardWhitespace();
cmd.startNamedArgs = this.index;
while (this.testNamedArgument()) {
const arg = this.parseNamedArgument();
cmd.namedArgumentList.push(arg);
this.discardWhitespace();
}
cmd.endNamedArgs = this.index;
this.discardWhitespace();
// /run shorthand does not take unnamed arguments (the command name practically *is* the unnamed argument)
if (this.testRunShorthandEnd()) {
@ -761,10 +966,10 @@ export class SlashCommandParser {
this.discardWhitespace();
}
this.discardWhitespace();
cmd.startUnnamedArgs = this.index;
cmd.startUnnamedArgs = this.index - (/\s(\s*)$/s.exec(this.behind)?.[1]?.length ?? 0);
cmd.endUnnamedArgs = this.index;
if (this.testUnnamedArgument()) {
cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument);
cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument, cmd?.command?.splitUnnamedArgumentCount);
cmd.endUnnamedArgs = this.index;
if (cmd.name == 'let') {
const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key');
@ -773,6 +978,17 @@ export class SlashCommandParser {
} else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') {
this.scope.variableNames.push(cmd.unnamedArgumentList[0].value);
}
} else if (cmd.name == 'import') {
const value = /**@type {string[]}*/(cmd.unnamedArgumentList.map(it=>it.value));
for (let i = 0; i < value.length; i++) {
const srcName = value[i];
let dstName = srcName;
if (i + 2 < value.length && value[i + 1] == 'as') {
dstName = value[i + 2];
i += 2;
}
this.scope.variableNames.push(dstName);
}
}
}
if (this.testCommandEnd()) {
@ -813,24 +1029,60 @@ export class SlashCommandParser {
testUnnamedArgumentEnd() {
return this.testCommandEnd();
}
parseUnnamedArgument(split) {
parseUnnamedArgument(split, splitCount = null) {
const wasSplit = split;
/**@type {SlashCommandClosure|String}*/
let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one
let isList = split;
let listValues = [];
let listQuoted = []; // keep track of which listValues were quoted
/**@type {SlashCommandUnnamedArgumentAssignment}*/
let assignment = new SlashCommandUnnamedArgumentAssignment();
assignment.start = this.index;
if (!split && this.testQuotedValue()) {
// if the next bit is a quoted value, take the whole value and gather contents as a list
assignment.value = this.parseQuotedValue();
assignment.end = this.index;
isList = true;
listValues.push(assignment);
listQuoted.push(true);
assignment = new SlashCommandUnnamedArgumentAssignment();
assignment.start = this.index;
}
while (!this.testUnnamedArgumentEnd()) {
if (split && splitCount && listValues.length >= splitCount) {
// the split count has just been reached: stop splitting, the rest is one singular value
split = false;
if (this.testQuotedValue()) {
// if the next bit is a quoted value, take the whole value
assignment.value = this.parseQuotedValue();
assignment.end = this.index;
listValues.push(assignment);
listQuoted.push(true);
assignment = new SlashCommandUnnamedArgumentAssignment();
assignment.start = this.index;
}
}
if (this.testClosure()) {
isList = true;
if (value.length > 0) {
this.indexMacros(this.index - value.length, value);
assignment.value = value;
listValues.push(assignment);
listQuoted.push(false);
assignment = new SlashCommandUnnamedArgumentAssignment();
assignment.start = this.index;
value = '';
if (!split && this.testQuotedValue()) {
// if where currently not splitting and the next bit is a quoted value, take the whole value
assignment.value = this.parseQuotedValue();
assignment.end = this.index;
listValues.push(assignment);
listQuoted.push(true);
assignment = new SlashCommandUnnamedArgumentAssignment();
assignment.start = this.index;
} else {
value = '';
}
}
assignment.start = this.index;
assignment.value = this.parseClosure();
@ -845,18 +1097,21 @@ export class SlashCommandParser {
assignment.value = this.parseQuotedValue();
assignment.end = this.index;
listValues.push(assignment);
listQuoted.push(true);
assignment = new SlashCommandUnnamedArgumentAssignment();
} else if (this.testListValue()) {
assignment.start = this.index;
assignment.value = this.parseListValue();
assignment.end = this.index;
listValues.push(assignment);
listQuoted.push(false);
assignment = new SlashCommandUnnamedArgumentAssignment();
} else if (this.testValue()) {
assignment.start = this.index;
assignment.value = this.parseValue();
assignment.end = this.index;
listValues.push(assignment);
listQuoted.push(false);
assignment = new SlashCommandUnnamedArgumentAssignment();
} else {
throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`);
@ -870,8 +1125,48 @@ export class SlashCommandParser {
if (isList && value.length > 0) {
assignment.value = value;
listValues.push(assignment);
listQuoted.push(false);
}
if (isList) {
const firstVal = listValues[0];
if (typeof firstVal?.value == 'string') {
if (!listQuoted[0]) {
// only trim the first part if it wasn't quoted
firstVal.value = firstVal.value.trimStart();
}
if (firstVal.value.length == 0) {
listValues.shift();
listQuoted.shift();
}
}
const lastVal = listValues.slice(-1)[0];
if (typeof lastVal?.value == 'string') {
if (!listQuoted.slice(-1)[0]) {
// only trim the last part if it wasn't quoted
lastVal.value = lastVal.value.trimEnd();
}
if (lastVal.value.length == 0) {
listValues.pop();
listQuoted.pop();
}
}
if (wasSplit && splitCount && splitCount + 1 < listValues.length) {
// if split with a split count and there are more values than expected
// -> should be result of quoting + additional (non-whitespace) text
// -> join the parts into one and restore quotes
const joined = new SlashCommandUnnamedArgumentAssignment();
joined.start = listValues[splitCount].start;
joined.end = listValues.slice(-1)[0].end;
joined.value = '';
for (let i = splitCount; i < listValues.length; i++) {
if (listQuoted[i]) joined.value += `"${listValues[i].value}"`;
else joined.value += listValues[i].value;
}
listValues = [
...listValues.slice(0, splitCount),
joined,
];
}
return listValues;
}
this.indexMacros(this.index - value.length, value);

View File

@ -38,8 +38,10 @@ export class SlashCommandScope {
}
setMacro(key, value) {
this.macros[key] = value;
setMacro(key, value, overwrite = true) {
if (overwrite || !this.macroList.find(it=>it.key == key)) {
this.macros[key] = value;
}
}
@ -95,7 +97,7 @@ export class SlashCommandScope {
return v ?? '';
} else {
const value = this.variables[key];
return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value);
return (value?.trim?.() === '' || isNaN(Number(value))) ? (value || '') : Number(value);
}
}
if (this.parent) {

View File

@ -21,7 +21,7 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { isMobile } from './RossAscends-mods.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { debounce_timeout } from './constants.js';
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
@ -1436,18 +1436,28 @@ async function onTagRestoreFileSelect(e) {
const data = await parseJsonFile(file);
if (!data) {
toastr.warning('Empty file data', 'Tag restore');
toastr.warning('Empty file data', 'Tag Restore');
console.log('Tag restore: File data empty.');
return;
}
if (!data.tags || !data.tag_map || !Array.isArray(data.tags) || typeof data.tag_map !== 'object') {
toastr.warning('Invalid file format', 'Tag restore');
toastr.warning('Invalid file format', 'Tag Restore');
console.log('Tag restore: Invalid file format.');
return;
}
// Prompt user if they want to overwrite existing tags
let overwrite = false;
if (tags.length > 0) {
const result = await Popup.show.confirm('Tag Restore', 'You have existing tags. If the backup contains any of those tags, do you want the backup to overwrite their settings (Name, color, folder state, etc)?',
{ okButton: 'Overwrite', cancelButton: 'Keep Existing' });
overwrite = result === POPUP_RESULT.AFFIRMATIVE;
}
const warnings = [];
/** @type {Map<string, string>} Map import tag ids with existing ids on overwrite */
const idToActualTagIdMap = new Map();
// Import tags
for (const tag of data.tags) {
@ -1456,10 +1466,28 @@ async function onTagRestoreFileSelect(e) {
continue;
}
if (tags.find(x => x.id === tag.id)) {
warnings.push(`Tag with id ${tag.id} already exists.`);
// Check against both existing id (direct match) and tag with the same name, which is not allowed.
let existingTag = tags.find(x => x.id === tag.id);
if (existingTag && !overwrite) {
warnings.push(`Tag '${tag.name}' with id ${tag.id} already exists.`);
continue;
}
existingTag = getTag(tag.name);
if (existingTag && !overwrite) {
warnings.push(`Tag with name '${tag.name}' already exists.`);
// Remember the tag id, so we can still import the tag map entries for this
idToActualTagIdMap.set(tag.id, existingTag.id);
continue;
}
if (existingTag) {
// On overwrite, we remove and re-add the tag
removeFromArray(tags, existingTag);
// And remember the ID if it was different, so we can update the tag map accordingly
if (existingTag.id !== tag.id) {
idToActualTagIdMap.set(existingTag.id, tag.id);
}
}
tags.push(tag);
}
@ -1478,30 +1506,39 @@ async function onTagRestoreFileSelect(e) {
const groupExists = groups.some(x => String(x.id) === String(key));
if (!characterExists && !groupExists) {
warnings.push(`Tag map key ${key} does not exist.`);
warnings.push(`Tag map key ${key} does not exist as character or group.`);
continue;
}
// Get existing tag ids for this key or empty array.
const existingTagIds = tag_map[key] || [];
// Merge existing and new tag ids. Remove duplicates.
tag_map[key] = existingTagIds.concat(tagIds).filter(onlyUnique);
// Merge existing and new tag ids. Replace the ones mapped to a new id. Remove duplicates.
const combinedTags = existingTagIds.concat(tagIds)
.map(tagId => (idToActualTagIdMap.has(tagId)) ? idToActualTagIdMap.get(tagId) : tagId)
.filter(onlyUnique);
// Verify that all tags exist. Remove tags that don't exist.
tag_map[key] = tag_map[key].filter(x => tags.some(y => String(y.id) === String(x)));
tag_map[key] = combinedTags.filter(tagId => tags.some(y => String(y.id) === String(tagId)));
}
if (warnings.length) {
toastr.success('Tags restored with warnings. Check console for details.');
toastr.warning('Tags restored with warnings. Check console or click on this message for details.', 'Tag Restore', {
timeOut: toastr.options.timeOut * 2, // Display double the time
onclick: () => Popup.show.text('Tag Restore Warnings', `<samp class="justifyLeft">${DOMPurify.sanitize(warnings.join('\n'))}<samp>`, { allowVerticalScrolling: true }),
});
console.warn(`TAG RESTORE REPORT\n====================\n${warnings.join('\n')}`);
} else {
toastr.success('Tags restored successfully.');
toastr.success('Tags restored successfully.', 'Tag Restore');
}
$('#tag_view_restore_input').val('');
printCharactersDebounced();
saveSettingsDebounced();
await onViewTagsListClick();
// Reprint the tag management popup, without having it to be opened again
const tagContainer = $('#tag_view_list .tag_view_list_tags');
printViewTagList(tagContainer);
}
function onBackupRestoreClick() {
@ -1558,7 +1595,7 @@ function appendViewTagToList(list, tag, everything) {
const primaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>')
.addClass('tag-color')
.attr({ id: colorPickerId, color: tag.color || 'rgba(0, 0, 0, 0.3)', 'data-default-color': 'rgba(0, 0, 0, 0.3)' });
.attr({ id: colorPickerId, color: tag.color || 'rgba(0, 0, 0, 0.5)', 'data-default-color': 'rgba(0, 0, 0, 0.5)' });
const secondaryColorPicker = $('<toolcool-color-picker></toolcool-color-picker>')
.addClass('tag-color2')

View File

@ -72,6 +72,7 @@ export async function loadTogetherAIModels(data) {
return;
}
data.sort((a, b) => a.name.localeCompare(b.name));
togetherModels = data;
if (!data.find(x => x.name === textgen_settings.togetherai_model)) {
@ -99,6 +100,7 @@ export async function loadInfermaticAIModels(data) {
return;
}
data.sort((a, b) => a.id.localeCompare(b.id));
infermaticAIModels = data;
if (!data.find(x => x.id === textgen_settings.infermaticai_model)) {
@ -151,6 +153,7 @@ export async function loadMancerModels(data) {
return;
}
data.sort((a, b) => a.name.localeCompare(b.name));
mancerModels = data;
if (!data.find(x => x.id === textgen_settings.mancer_model)) {
@ -173,6 +176,7 @@ export async function loadOpenRouterModels(data) {
return;
}
data.sort((a, b) => a.name.localeCompare(b.name));
openRouterModels = data;
if (!data.find(x => x.id === textgen_settings.openrouter_model)) {
@ -242,6 +246,7 @@ export async function loadFeatherlessModels(data) {
return;
}
data.sort((a, b) => a.id.localeCompare(b.id));
featherlessModels = data;
if (!data.find(x => x.id === textgen_settings.featherless_model)) {
@ -262,6 +267,8 @@ function onFeatherlessModelSelect() {
const modelId = String($('#featherless_model').val());
textgen_settings.featherless_model = modelId;
$('#api_button_textgenerationwebui').trigger('click');
const model = featherlessModels.find(x => x.id === modelId);
setGenerationParamsFromPreset({ max_length: model.context_length });
}
@ -431,6 +438,20 @@ function getAphroditeModelTemplate(option) {
`));
}
function getFeatherlessModelTemplate(option) {
const model = featherlessModels.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
return $((`
<div class="flex-container flexFlowColumn">
<div><strong>${DOMPurify.sanitize(model.name)}</strong> | <span>${model.context_length || '???'} tokens</span></div>
</div>
`));
}
async function downloadOllamaModel() {
try {
const serverUrl = textgen_settings.server_urls[textgen_types.OLLAMA];
@ -679,6 +700,7 @@ jQuery(function () {
searchInputPlaceholder: 'Search models...',
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getFeatherlessModelTemplate,
});
providersSelect.select2({
sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)),

View File

@ -326,18 +326,21 @@ async function selectPreset(name) {
function formatTextGenURL(value) {
try {
const noFormatTypes = [MANCER, TOGETHERAI, INFERMATICAI, DREAMGEN, OPENROUTER];
const legacyApiTypes = [OOBA, APHRODITE];
if (noFormatTypes.includes(settings.type)) {
return value;
}
const url = new URL(value);
if (url.pathname === '/api' && !settings.legacy_api) {
toastr.info('Enable Legacy API or start Ooba with the OpenAI extension enabled.', 'Legacy API URL detected. Generation may fail.', { preventDuplicates: true, timeOut: 10000, extendedTimeOut: 20000 });
url.pathname = '';
}
if (legacyApiTypes.includes(settings.type)) {
if (url.pathname === '/api' && !settings.legacy_api) {
toastr.info('Enable Legacy API or start Ooba with the OpenAI extension enabled.', 'Legacy API URL detected. Generation may fail.', { preventDuplicates: true, timeOut: 10000, extendedTimeOut: 20000 });
url.pathname = '';
}
if (!power_user.relaxed_api_urls && settings.legacy_api) {
url.pathname = '/api';
if (!power_user.relaxed_api_urls && settings.legacy_api) {
url.pathname = '/api';
}
}
return url.toString();
} catch {

View File

@ -3,7 +3,7 @@ import { getRequestHeaders } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js';
import { debounce_timeout } from './constants.js';
import { Popup } from './popup.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
/**
* Pagination status string template.
@ -334,12 +334,12 @@ export function debouncedThrottle(func, limit = 300) {
let last, deferTimer;
let db = debounce(func);
return function() {
return function () {
let now = +new Date, args = arguments;
if(!last || (last && now < last + limit)) {
if (!last || (last && now < last + limit)) {
clearTimeout(deferTimer);
db.apply(this, args);
deferTimer = setTimeout(function() {
deferTimer = setTimeout(function () {
last = now;
func.apply(this, args);
}, limit);
@ -1961,3 +1961,75 @@ export function toggleDrawer(drawer, expand = true) {
// Set the height of "autoSetHeight" textareas within the inline-drawer to their scroll height
content.querySelectorAll('textarea.autoSetHeight').forEach(resetScrollHeight);
}
export async function fetchFaFile(name) {
const style = document.createElement('style');
style.innerHTML = await (await fetch(`/css/${name}`)).text();
document.head.append(style);
const sheet = style.sheet;
style.remove();
return [...sheet.cssRules].filter(it => it.style?.content).map(it => it.selectorText.split('::').shift().slice(1));
}
export async function fetchFa() {
return [...new Set((await Promise.all([
fetchFaFile('fontawesome.min.css'),
])).flat())];
}
/**
* Opens a popup with all the available Font Awesome icons and returns the selected icon's name.
* @prop {string[]} customList A custom list of Font Awesome icons to use instead of all available icons.
* @returns {Promise<string>} The icon name (fa-pencil) or null if cancelled.
*/
export async function showFontAwesomePicker(customList = null) {
const faList = customList ?? await fetchFa();
const fas = {};
const dom = document.createElement('div'); {
dom.classList.add('faPicker-container');
const search = document.createElement('div'); {
search.classList.add('faQuery-container');
const qry = document.createElement('input'); {
qry.classList.add('text_pole');
qry.classList.add('faQuery');
qry.type = 'search';
qry.placeholder = 'Filter icons';
qry.autofocus = true;
const qryDebounced = debounce(() => {
const result = faList.filter(it => it.includes(qry.value));
for (const fa of faList) {
if (!result.includes(fa)) {
fas[fa].classList.add('hidden');
} else {
fas[fa].classList.remove('hidden');
}
}
});
qry.addEventListener('input', () => qryDebounced());
search.append(qry);
}
dom.append(search);
}
const grid = document.createElement('div'); {
grid.classList.add('faPicker');
for (const fa of faList) {
const opt = document.createElement('div'); {
fas[fa] = opt;
opt.classList.add('menu_button');
opt.classList.add('fa-solid');
opt.classList.add(fa);
opt.title = fa.slice(3);
opt.dataset.result = POPUP_RESULT.AFFIRMATIVE.toString();
opt.addEventListener('click', () => value = fa);
grid.append(opt);
}
}
dom.append(grid);
}
}
let value = '';
const picker = new Popup(dom, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, okButton: 'No Icon', cancelButton: 'Cancel' });
await picker.show();
if (picker.result == POPUP_RESULT.AFFIRMATIVE) {
return value;
}
return null;
}

View File

@ -4,10 +4,11 @@ import { 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 { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
import { isFalseBoolean } from './utils.js';
@ -40,7 +41,7 @@ function getLocalVariable(name, args = {}) {
}
}
return (localVariable === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
}
function setLocalVariable(name, value, args = {}) {
@ -93,7 +94,7 @@ function getGlobalVariable(name, args = {}) {
}
}
return (globalVariable === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
}
function setGlobalVariable(name, value, args = {}) {
@ -348,11 +349,13 @@ async function whileCallback(args, value) {
if (result && command) {
if (command instanceof SlashCommandClosure) {
command.breakController = new SlashCommandBreakController();
commandResult = await command.execute();
} else {
commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
}
if (commandResult.isAborted) break;
if (commandResult.isBreak) break;
} else {
break;
}
@ -390,8 +393,8 @@ async function timesCallback(args, value) {
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.breakController = new SlashCommandBreakController();
command.scope.setMacro('timesIndex', i);
result = await command.execute();
}
@ -399,6 +402,7 @@ async function timesCallback(args, value) {
result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags, args._abortController);
}
if (result.isAborted) break;
if (result.isBreak) break;
}
return result?.pipe ?? '';
@ -461,7 +465,7 @@ function existsGlobalVariable(name) {
* @param {object} args Command arguments
* @returns {{a: string | number, b: string | number, rule: string}} Boolean operands
*/
function parseBooleanOperands(args) {
export function parseBooleanOperands(args) {
// Resolution order: numeric literal, local variable, global variable, string literal
/**
* @param {string} operand Boolean operand candidate
@ -510,36 +514,15 @@ function parseBooleanOperands(args) {
* @param {string|number} b The right operand
* @returns {boolean} True if the rule yields true, false otherwise
*/
function evalBoolean(rule, a, b) {
export function evalBoolean(rule, a, b) {
if (!rule) {
toastr.warning('The rule must be specified for the boolean comparison.', 'Invalid command');
throw new Error('Invalid command.');
}
let result = false;
if (typeof a === 'string' && typeof b !== 'number') {
const aString = String(a).toLowerCase();
const bString = String(b).toLowerCase();
switch (rule) {
case 'in':
result = aString.includes(bString);
break;
case 'nin':
result = !aString.includes(bString);
break;
case 'eq':
result = aString === bString;
break;
case 'neq':
result = aString !== bString;
break;
default:
toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
throw new Error('Invalid command.');
}
} else if (typeof a === 'number') {
if (typeof a === 'number' && typeof b === 'number') {
// only do numeric comparison if both operands are numbers
const aNumber = Number(a);
const bNumber = Number(b);
@ -569,6 +552,38 @@ function evalBoolean(rule, a, b) {
toastr.error('Unknown boolean comparison rule for type number.', 'Invalid command');
throw new Error('Invalid command.');
}
} else {
// otherwise do case-insensitive string comparsion, stringify non-strings
let aString;
let bString;
if (typeof a == 'string') {
aString = a.toLowerCase();
} else {
aString = JSON.stringify(a).toLowerCase();
}
if (typeof b == 'string') {
bString = b.toLowerCase();
} else {
bString = JSON.stringify(b).toLowerCase();
}
switch (rule) {
case 'in':
result = aString.includes(bString);
break;
case 'nin':
result = !aString.includes(bString);
break;
case 'eq':
result = aString === bString;
break;
case 'neq':
result = aString !== bString;
break;
default:
toastr.error('Unknown boolean comparison rule for type string.', 'Invalid /if command');
throw new Error('Invalid command.');
}
}
return result;
@ -783,24 +798,29 @@ function randValuesCallback(from, to, args) {
* @returns The variable's value
*/
function letCallback(args, value) {
if (Array.isArray(value)) {
args._scope.letVariable(value[0], typeof value[1] == 'string' ? value.slice(1).join(' ') : value[1]);
return value[1];
}
if (!Array.isArray(value)) value = [value];
if (args.key !== undefined) {
const key = args.key;
const val = value;
if (typeof key != 'string') throw new Error('Key must be a string');
if (args._hasUnnamedArgument) {
const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
args._scope.letVariable(key, val);
return val;
} else {
args._scope.letVariable(key);
return '';
}
}
const key = value.shift();
if (typeof key != 'string') throw new Error('Key must be a string');
if (value.length > 0) {
const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
args._scope.letVariable(key, val);
return val;
} else {
args._scope.letVariable(key);
return '';
}
if (value instanceof SlashCommandClosure) throw new Error('/let unnamed argument does not support closures if no key is provided');
if (value.includes(' ')) {
const key = value.split(' ')[0];
const val = value.split(' ').slice(1).join(' ');
args._scope.letVariable(key, val);
return val;
}
args._scope.letVariable(value);
}
/**
@ -813,8 +833,9 @@ function varCallback(args, value) {
if (!Array.isArray(value)) value = [value];
if (args.key !== undefined) {
const key = args.key;
if (typeof key != 'string') throw new Error('Key must be a string');
if (args._hasUnnamedArgument) {
const val = value.join(' ');
const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
args._scope.setVariable(key, val, args.index);
return val;
} else {
@ -822,8 +843,9 @@ function varCallback(args, value) {
}
}
const key = value.shift();
if (typeof key != 'string') throw new Error('Key must be a string');
if (value.length > 0) {
const val = value.join(' ');
const val = typeof value[0] == 'string' ? value.join(' ') : value[0];
args._scope.setVariable(key, val, args.index);
return val;
} else {
@ -1382,6 +1404,7 @@ export function registerVariableCommands() {
),
],
splitUnnamedArgument: true,
splitUnnamedArgumentCount: 1,
helpString: `
<div>
Execute any valid slash command enclosed in quotes <code>repeats</code> number of times.
@ -1459,7 +1482,7 @@ export function registerVariableCommands() {
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'add',
callback: addValuesCallback,
callback: (args, /**@type {string[]}*/value) => addValuesCallback(args, value.join(' ')),
returns: 'sum of the provided values',
unnamedArgumentList: [
SlashCommandArgument.fromProps({
@ -1467,10 +1490,32 @@ export function registerVariableCommands() {
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.VARIABLE_NAME],
isRequired: true,
acceptsMultiple: true,
enumProvider: commonEnumProviders.variables('all'),
enumProvider: (executor, scope)=>{
const vars = commonEnumProviders.variables('all')(executor, scope);
vars.push(
new SlashCommandEnumValue(
'any variable name',
null,
enumTypes.variable,
enumIcons.variable,
(input)=>/^\w*$/.test(input),
(input)=>input,
),
new SlashCommandEnumValue(
'any number',
null,
enumTypes.number,
enumIcons.number,
(input)=>input == '' || !Number.isNaN(Number(input)),
(input)=>input,
),
);
return vars;
},
forceEnum: false,
}),
],
splitUnnamedArgument: true,
helpString: `
<div>
Performs an addition of the set of values and passes the result down the pipe.
@ -2001,6 +2046,7 @@ export function registerVariableCommands() {
),
],
splitUnnamedArgument: true,
splitUnnamedArgumentCount: 1,
helpString: `
<div>
Get or set a variable.
@ -2043,6 +2089,7 @@ export function registerVariableCommands() {
),
],
splitUnnamedArgument: true,
splitUnnamedArgumentCount: 1,
helpString: `
<div>
Declares a new variable in the current scope.

View File

@ -18,33 +18,13 @@ import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
import { StructuredCloneMap } from './util/StructuredCloneMap.js';
export {
world_info,
world_info_budget,
world_info_depth,
world_info_min_activations,
world_info_min_activations_depth_max,
world_info_include_names,
world_info_recursive,
world_info_overflow_alert,
world_info_case_sensitive,
world_info_match_whole_words,
world_info_character_strategy,
world_info_budget_cap,
world_names,
checkWorldInfo,
deleteWorldInfo,
setWorldInfoSettings,
getWorldInfoPrompt,
};
const world_info_insertion_strategy = {
export const world_info_insertion_strategy = {
evenly: 0,
character_first: 1,
global_first: 2,
};
const world_info_logic = {
export const world_info_logic = {
AND_ANY: 0,
NOT_ALL: 1,
NOT_ANY: 2,
@ -54,7 +34,7 @@ const world_info_logic = {
/**
* @enum {number} Possible states of the WI evaluation
*/
const scan_state = {
export const scan_state = {
/**
* The scan will be stopped.
*/
@ -75,23 +55,23 @@ const scan_state = {
const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry');
let world_info = {};
let selected_world_info = [];
export let world_info = {};
export let selected_world_info = [];
/** @type {string[]} */
let world_names;
let world_info_depth = 2;
let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated
let world_info_min_activations_depth_max = 0; // used when (world_info_min_activations > 0)
export let world_names;
export let world_info_depth = 2;
export let world_info_min_activations = 0; // if > 0, will continue seeking chat until minimum world infos are activated
export let world_info_min_activations_depth_max = 0; // used when (world_info_min_activations > 0)
let world_info_budget = 25;
let world_info_include_names = true;
let world_info_recursive = false;
let world_info_overflow_alert = false;
let world_info_case_sensitive = false;
let world_info_match_whole_words = false;
let world_info_use_group_scoring = false;
let world_info_character_strategy = world_info_insertion_strategy.character_first;
let world_info_budget_cap = 0;
export let world_info_budget = 25;
export let world_info_include_names = true;
export let world_info_recursive = false;
export let world_info_overflow_alert = false;
export let world_info_case_sensitive = false;
export let world_info_match_whole_words = false;
export let world_info_use_group_scoring = false;
export let world_info_character_strategy = world_info_insertion_strategy.character_first;
export let world_info_budget_cap = 0;
const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), debounce_timeout.relaxed);
const saveSettingsDebounced = debounce(() => {
Object.assign(world_info, { globalSelect: selected_world_info });
@ -101,13 +81,13 @@ const sortFn = (a, b) => b.order - a.order;
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());
const SORT_ORDER_KEY = 'world_info_sort_order';
const METADATA_KEY = 'world_info';
export const worldInfoFilter = new FilterHelper(() => updateEditor());
export const SORT_ORDER_KEY = 'world_info_sort_order';
export const METADATA_KEY = 'world_info';
const DEFAULT_DEPTH = 4;
const DEFAULT_WEIGHT = 100;
const MAX_SCAN_DEPTH = 1000;
export const DEFAULT_DEPTH = 4;
export const DEFAULT_WEIGHT = 100;
export const MAX_SCAN_DEPTH = 1000;
const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
// Typedef area
@ -732,7 +712,7 @@ export function getWorldInfoSettings() {
};
}
const world_info_position = {
export const world_info_position = {
before: 0,
after: 1,
ANTop: 2,
@ -747,8 +727,18 @@ export const wi_anchor_position = {
after: 1,
};
/** @type {StructuredCloneMap<string,object>} */
const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false });
/**
* The cache of all world info data that was loaded from the backend.
*
* Calling `loadWorldInfo` will fill this cache and utilize this cache, so should be the preferred way to load any world info data.
* Only use the cache directly if you need synchronous access.
*
* This will return a deep clone of the data, so no way to modify the data without actually saving it.
* Should generally be only used for readonly access.
*
* @type {StructuredCloneMap<string,object>}
* */
export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false });
/**
* Gets the world info based on chat messages.
@ -758,7 +748,7 @@ const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: fa
* @typedef {{worldInfoString: string, worldInfoBefore: string, worldInfoAfter: string, worldInfoExamples: any[], worldInfoDepth: any[]}} WIPromptResult
* @returns {Promise<WIPromptResult>} The world info string and depth.
*/
async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';
const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun);
@ -780,7 +770,7 @@ async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
};
}
function setWorldInfoSettings(settings, data) {
export function setWorldInfoSettings(settings, data) {
if (settings.world_info_depth !== undefined)
world_info_depth = Number(settings.world_info_depth);
if (settings.world_info_min_activations !== undefined)
@ -916,7 +906,7 @@ function registerWorldInfoSlashCommands() {
return '';
}
const data = await loadWorldInfoData(file);
const data = await loadWorldInfo(file);
if (!data || !('entries' in data)) {
toastr.warning('World Info file has an invalid format');
@ -965,7 +955,7 @@ function registerWorldInfoSlashCommands() {
return '';
}
if (typeof newEntryTemplate[field] === 'boolean') {
if (typeof newWorldInfoEntryTemplate[field] === 'boolean') {
const isTrue = isTrueBoolean(value);
const isFalse = isFalseBoolean(value);
@ -1016,7 +1006,7 @@ function registerWorldInfoSlashCommands() {
return '';
}
if (newEntryTemplate[field] === undefined) {
if (newWorldInfoEntryTemplate[field] === undefined) {
toastr.warning('Valid field name is required');
return '';
}
@ -1038,7 +1028,7 @@ function registerWorldInfoSlashCommands() {
const file = args.file;
const key = args.key;
const data = await loadWorldInfoData(file);
const data = await loadWorldInfo(file);
if (!data || !('entries' in data)) {
toastr.warning('Valid World Info file name is required');
@ -1075,7 +1065,7 @@ function registerWorldInfoSlashCommands() {
value = value.replace(/\\([{}|])/g, '$1');
const data = await loadWorldInfoData(file);
const data = await loadWorldInfo(file);
if (!data || !('entries' in data)) {
toastr.warning('Valid World Info file name is required');
@ -1089,7 +1079,7 @@ function registerWorldInfoSlashCommands() {
return '';
}
if (newEntryTemplate[field] === undefined) {
if (newWorldInfoEntryTemplate[field] === undefined) {
toastr.warning('Valid field name is required');
return '';
}
@ -1104,8 +1094,8 @@ function registerWorldInfoSlashCommands() {
entry[field] = value;
}
if (originalDataKeyMap[field]) {
setOriginalDataValue(data, uid, originalDataKeyMap[field], entry[field]);
if (originalWIDataKeyMap[field]) {
setWIOriginalDataValue(data, uid, originalWIDataKeyMap[field], entry[field]);
}
await saveWorldInfo(file, data);
@ -1226,7 +1216,7 @@ function registerWorldInfoSlashCommands() {
/** A collection of local enum providers for this context of world info */
const localEnumProviders = {
/** All possible fields that can be set in a WI entry */
wiEntryFields: () => Object.entries(newEntryDefinition).map(([key, value]) =>
wiEntryFields: () => Object.entries(newWorldInfoEntryDefinition).map(([key, value]) =>
new SlashCommandEnumValue(key, `[${value.type}] default: ${(typeof value.default === 'string' ? `'${value.default}'` : value.default)}`,
enumTypes.enum, enumIcons.getDataTypeIcon(value.type))),
@ -1566,18 +1556,32 @@ function registerWorldInfoSlashCommands() {
}));
}
// World Info Editor
async function showWorldEditor(name) {
/**
* Loads the given world into the World Editor.
*
* @param {string} name - The name of the world
* @return {Promise<void>} A promise that resolves when the world editor is loaded
*/
export async function showWorldEditor(name) {
if (!name) {
hideWorldEditor();
return;
}
const wiData = await loadWorldInfoData(name);
const wiData = await loadWorldInfo(name);
displayWorldEntries(name, wiData);
}
async function loadWorldInfoData(name) {
/**
* Loads world info from the backend.
*
* This function will return from `worldInfoCache` if it has already been loaded before.
*
* @param {string} name - The name of the world to load
* @return {Promise<Object|null>} A promise that resolves to the loaded world information, or null if the request fails.
*/
export async function loadWorldInfo(name) {
if (!name) {
return;
}
@ -1635,14 +1639,18 @@ function getWIElement(name) {
}
/**
* Sorts the given data based on the selected sort option
*
* @param {any[]} data WI entries
* @param {object} [options={}] - Optional arguments
* @param {{sortField?: string, sortOrder?: string, sortRule?: string}} [options.customSort={}] - Custom sort options, instead of the chosen UI sort
* @returns {any[]} Sorted data
*/
function sortEntries(data) {
export function sortWorldInfoEntries(data, { customSort = null } = {}) {
const option = $('#world_info_sort_order').find(':selected');
const sortField = option.data('field');
const sortOrder = option.data('order');
const sortRule = option.data('rule');
const sortField = customSort?.sortField ?? option.data('field');
const sortOrder = customSort?.sortOrder ?? option.data('order');
const sortRule = customSort?.sortRule ?? option.data('rule');
const orderSign = sortOrder === 'asc' ? 1 : -1;
if (!data.length) return data;
@ -1801,7 +1809,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
// Apply the filter and do the chosen sorting
entriesArray = worldInfoFilter.applyFilters(entriesArray);
entriesArray = sortEntries(entriesArray);
entriesArray = sortWorldInfoEntries(entriesArray);
// Cache keys
const keys = entriesArray.flatMap(entry => [...entry.key, ...entry.keysecondary]);
@ -1920,7 +1928,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
for (const entry of Object.values(data.entries)) {
if (!entry.comment && Array.isArray(entry.key) && entry.key.length > 0) {
entry.comment = entry.key[0];
setOriginalDataValue(data, entry.uid, 'comment', entry.comment);
setWIOriginalDataValue(data, entry.uid, 'comment', entry.comment);
counter++;
}
}
@ -1955,7 +1963,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
// We need to sort the entries here, as the data source isn't sorted
const entries = Object.values(data.entries);
sortEntries(entries);
sortWorldInfoEntries(entries);
let updated = 0, current = start;
for (const entry of entries) {
@ -1963,7 +1971,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
if (entry.order === newOrder) continue;
entry.order = newOrder;
setOriginalDataValue(data, entry.order, 'order', entry.order);
setWIOriginalDataValue(data, entry.order, 'order', entry.order);
updated++;
}
@ -2026,7 +2034,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
}
item.displayIndex = minDisplayIndex + index;
setOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex);
setWIOriginalDataValue(data, uid, 'extensions.display_index', item.displayIndex);
});
console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex })));
@ -2037,7 +2045,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
//$("#world_popup_entries_list").disableSelection();
}
const originalDataKeyMap = {
export const originalWIDataKeyMap = {
'displayIndex': 'extensions.display_index',
'excludeRecursion': 'extensions.exclude_recursion',
'preventRecursion': 'extensions.prevent_recursion',
@ -2088,7 +2096,17 @@ function verifyWorldInfoSearchSortRule() {
}
}
function setOriginalDataValue(data, uid, key, value) {
/**
* Sets the value of a specific key in the original data entry corresponding to the given uid
* This needs to be called whenever you update JSON data fields.
* Use `originalWIDataKeyMap` to find the correct value to be set.
*
* @param {object} data - The data object containing the original data entries.
* @param {string} uid - The unique identifier of the data entry.
* @param {string} key - The key of the value to be set.
* @param {any} value - The value to be set.
*/
export function setWIOriginalDataValue(data, uid, key, value) {
if (data.originalData && Array.isArray(data.originalData.entries)) {
let originalEntry = data.originalData.entries.find(x => x.uid === uid);
@ -2100,7 +2118,13 @@ function setOriginalDataValue(data, uid, key, value) {
}
}
function deleteOriginalDataValue(data, uid) {
/**
* Deletes the original data entry corresponding to the given uid from the provided data object
*
* @param {object} data - The data object containing the original data entries
* @param {string} uid - The unique identifier of the data entry to be deleted
*/
export function deleteWIOriginalDataValue(data, uid) {
if (data.originalData && Array.isArray(data.originalData.entries)) {
const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid);
@ -2121,7 +2145,7 @@ function deleteOriginalDataValue(data, uid) {
* @param {string} input - One or multiple keywords or regexes, separated by commas
* @returns {string[]} An array of keywords and regexes
*/
function splitKeywordsAndRegexes(input) {
export function splitKeywordsAndRegexes(input) {
/** @type {string[]} */
let keywordsAndRegexes = [];
@ -2225,7 +2249,7 @@ function isValidRegex(input) {
* @param {string} input - A delimited regex string
* @returns {RegExp|null} The regex object, or null if not a valid regex
*/
function parseRegexFromString(input) {
export function parseRegexFromString(input) {
// Extracting the regex pattern and flags
let match = input.match(/^\/([\w\W]+?)\/([gimsuy]*)$/);
if (!match) {
@ -2311,10 +2335,10 @@ async function getWorldEntry(name, data, entry) {
/** @type {string[]} */
const keys = ($(this).select2('data')).map(x => x.text);
!skipReset && resetScrollHeight(this);
!skipReset && await resetScrollHeight(this);
if (!noSave) {
data.entries[uid][entryPropName] = keys;
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
await saveWorldInfo(name, data);
}
});
@ -2347,10 +2371,10 @@ async function getWorldEntry(name, data, entry) {
input.on('input', async function (_, { skipReset, noSave } = {}) {
const uid = $(this).data('uid');
const value = String($(this).val());
!skipReset && resetScrollHeight(this);
!skipReset && await resetScrollHeight(this);
if (!noSave) {
data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
setWIOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
await saveWorldInfo(name, data);
}
});
@ -2394,7 +2418,7 @@ async function getWorldEntry(name, data, entry) {
const uid = $(this).data('uid');
const value = Number($(this).val());
data.entries[uid].selectiveLogic = !isNaN(value) ? value : world_info_logic.AND_ANY;
setOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic);
setWIOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic);
await saveWorldInfo(name, data);
});
@ -2443,7 +2467,7 @@ async function getWorldEntry(name, data, entry) {
}
}
setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
setWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
await saveWorldInfo(name, data);
});
characterExclusionInput.prop('checked', entry.characterFilter?.isExclude ?? false).trigger('input');
@ -2505,7 +2529,7 @@ async function getWorldEntry(name, data, entry) {
},
);
}
setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
setWIOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
await saveWorldInfo(name, data);
});
@ -2516,10 +2540,10 @@ async function getWorldEntry(name, data, entry) {
commentInput.on('input', async function (_, { skipReset } = {}) {
const uid = $(this).data('uid');
const value = $(this).val();
!skipReset && resetScrollHeight(this);
!skipReset && await resetScrollHeight(this);
data.entries[uid].comment = value;
setOriginalDataValue(data, uid, 'comment', data.entries[uid].comment);
setWIOriginalDataValue(data, uid, 'comment', data.entries[uid].comment);
await saveWorldInfo(name, data);
});
commentToggle.data('uid', entry.uid);
@ -2554,7 +2578,7 @@ async function getWorldEntry(name, data, entry) {
const value = $(this).val();
data.entries[uid].content = value;
setOriginalDataValue(data, uid, 'content', data.entries[uid].content);
setWIOriginalDataValue(data, uid, 'content', data.entries[uid].content);
await saveWorldInfo(name, data);
if (skipCount) {
@ -2584,7 +2608,7 @@ async function getWorldEntry(name, data, entry) {
const value = $(this).prop('checked');
data.entries[uid].selective = value;
setOriginalDataValue(data, uid, 'selective', data.entries[uid].selective);
setWIOriginalDataValue(data, uid, 'selective', data.entries[uid].selective);
await saveWorldInfo(name, data);
const keysecondary = $(this)
@ -2633,7 +2657,7 @@ async function getWorldEntry(name, data, entry) {
data.entries[uid].order = !isNaN(value) ? value : 0;
updatePosOrdDisplay(uid);
setOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order);
setWIOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order);
await saveWorldInfo(name, data);
});
orderInput.val(entry.order).trigger('input');
@ -2647,7 +2671,7 @@ async function getWorldEntry(name, data, entry) {
const value = String($(this).val()).trim();
data.entries[uid].group = value;
setOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group);
setWIOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group);
await saveWorldInfo(name, data);
});
groupInput.val(entry.group ?? '').trigger('input');
@ -2660,7 +2684,7 @@ async function getWorldEntry(name, data, entry) {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].groupOverride = value;
setOriginalDataValue(data, uid, 'extensions.group_override', data.entries[uid].groupOverride);
setWIOriginalDataValue(data, uid, 'extensions.group_override', data.entries[uid].groupOverride);
await saveWorldInfo(name, data);
});
groupOverrideInput.prop('checked', entry.groupOverride).trigger('input');
@ -2684,7 +2708,7 @@ async function getWorldEntry(name, data, entry) {
}
data.entries[uid].groupWeight = !isNaN(value) ? Math.abs(value) : 1;
setOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight);
setWIOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight);
await saveWorldInfo(name, data);
});
groupWeightInput.val(entry.groupWeight ?? DEFAULT_WEIGHT).trigger('input');
@ -2697,7 +2721,7 @@ async function getWorldEntry(name, data, entry) {
const value = Number($(this).val());
data.entries[uid].sticky = !isNaN(value) ? value : null;
setOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky);
setWIOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky);
await saveWorldInfo(name, data);
});
sticky.val(entry.sticky > 0 ? entry.sticky : '').trigger('input');
@ -2710,7 +2734,7 @@ async function getWorldEntry(name, data, entry) {
const value = Number($(this).val());
data.entries[uid].cooldown = !isNaN(value) ? value : null;
setOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown);
setWIOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown);
await saveWorldInfo(name, data);
});
cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input');
@ -2723,7 +2747,7 @@ async function getWorldEntry(name, data, entry) {
const value = Number($(this).val());
data.entries[uid].delay = !isNaN(value) ? value : null;
setOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay);
setWIOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay);
await saveWorldInfo(name, data);
});
delay.val(entry.delay > 0 ? entry.delay : '').trigger('input');
@ -2743,7 +2767,7 @@ async function getWorldEntry(name, data, entry) {
data.entries[uid].depth = !isNaN(value) ? value : 0;
updatePosOrdDisplay(uid);
setOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth);
setWIOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth);
await saveWorldInfo(name, data);
});
depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input');
@ -2771,7 +2795,7 @@ async function getWorldEntry(name, data, entry) {
}
}
setOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
setWIOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
await saveWorldInfo(name, data);
});
probabilityInput.val(entry.probability).trigger('input');
@ -2838,10 +2862,10 @@ async function getWorldEntry(name, data, entry) {
}
updatePosOrdDisplay(uid);
// Spec v2 only supports before_char and after_char
setOriginalDataValue(data, uid, 'position', data.entries[uid].position == 0 ? 'before_char' : 'after_char');
setWIOriginalDataValue(data, uid, 'position', data.entries[uid].position == 0 ? 'before_char' : 'after_char');
// Write the original value as extensions field
setOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position);
setOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role);
setWIOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position);
setWIOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role);
await saveWorldInfo(name, data);
});
@ -2883,36 +2907,36 @@ async function getWorldEntry(name, data, entry) {
data.entries[uid].constant = true;
data.entries[uid].disable = false;
data.entries[uid].vectorized = false;
setOriginalDataValue(data, uid, 'enabled', true);
setOriginalDataValue(data, uid, 'constant', true);
setOriginalDataValue(data, uid, 'extensions.vectorized', false);
setWIOriginalDataValue(data, uid, 'enabled', true);
setWIOriginalDataValue(data, uid, 'constant', true);
setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
template.removeClass('disabledWIEntry');
break;
case 'normal':
data.entries[uid].constant = false;
data.entries[uid].disable = false;
data.entries[uid].vectorized = false;
setOriginalDataValue(data, uid, 'enabled', true);
setOriginalDataValue(data, uid, 'constant', false);
setOriginalDataValue(data, uid, 'extensions.vectorized', false);
setWIOriginalDataValue(data, uid, 'enabled', true);
setWIOriginalDataValue(data, uid, 'constant', false);
setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
template.removeClass('disabledWIEntry');
break;
case 'vectorized':
data.entries[uid].constant = false;
data.entries[uid].disable = false;
data.entries[uid].vectorized = true;
setOriginalDataValue(data, uid, 'enabled', true);
setOriginalDataValue(data, uid, 'constant', false);
setOriginalDataValue(data, uid, 'extensions.vectorized', true);
setWIOriginalDataValue(data, uid, 'enabled', true);
setWIOriginalDataValue(data, uid, 'constant', false);
setWIOriginalDataValue(data, uid, 'extensions.vectorized', true);
template.removeClass('disabledWIEntry');
break;
case 'disabled':
data.entries[uid].constant = false;
data.entries[uid].disable = true;
data.entries[uid].vectorized = false;
setOriginalDataValue(data, uid, 'enabled', false);
setOriginalDataValue(data, uid, 'constant', false);
setOriginalDataValue(data, uid, 'extensions.vectorized', false);
setWIOriginalDataValue(data, uid, 'enabled', false);
setWIOriginalDataValue(data, uid, 'constant', false);
setWIOriginalDataValue(data, uid, 'extensions.vectorized', false);
template.addClass('disabledWIEntry');
break;
}
@ -2943,7 +2967,7 @@ async function getWorldEntry(name, data, entry) {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].excludeRecursion = value;
setOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion);
setWIOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion);
await saveWorldInfo(name, data);
});
excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
@ -2955,7 +2979,7 @@ async function getWorldEntry(name, data, entry) {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid].preventRecursion = value;
setOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
setWIOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
await saveWorldInfo(name, data);
});
preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
@ -2967,7 +2991,7 @@ async function getWorldEntry(name, data, entry) {
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);
setWIOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
await saveWorldInfo(name, data);
});
delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input');
@ -2987,10 +3011,12 @@ async function getWorldEntry(name, data, entry) {
// delete button
const deleteButton = template.find('.delete_entry_button');
deleteButton.data('uid', entry.uid);
deleteButton.on('click', async function () {
deleteButton.on('click', async function (e) {
e.stopPropagation();
const uid = $(this).data('uid');
deleteWorldInfoEntry(data, uid);
deleteOriginalDataValue(data, uid);
const deleted = await deleteWorldInfoEntry(data, uid);
if (!deleted) return;
deleteWIOriginalDataValue(data, uid);
await saveWorldInfo(name, data);
updateEditor(navigation_option.previous);
});
@ -3017,7 +3043,7 @@ async function getWorldEntry(name, data, entry) {
}
data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value <= MAX_SCAN_DEPTH ? Math.floor(value) : null;
setOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth);
setWIOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth);
await saveWorldInfo(name, data);
});
scanDepthInput.val(entry.scanDepth ?? null).trigger('input');
@ -3030,7 +3056,7 @@ async function getWorldEntry(name, data, entry) {
const value = $(this).val();
data.entries[uid].caseSensitive = value === 'null' ? null : value === 'true';
setOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive);
setWIOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive);
await saveWorldInfo(name, data);
});
caseSensitiveSelect.val((entry.caseSensitive === null || entry.caseSensitive === undefined) ? 'null' : entry.caseSensitive ? 'true' : 'false').trigger('input');
@ -3043,7 +3069,7 @@ async function getWorldEntry(name, data, entry) {
const value = $(this).val();
data.entries[uid].matchWholeWords = value === 'null' ? null : value === 'true';
setOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords);
setWIOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords);
await saveWorldInfo(name, data);
});
matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input');
@ -3056,7 +3082,7 @@ async function getWorldEntry(name, data, entry) {
const value = $(this).val();
data.entries[uid].useGroupScoring = value === 'null' ? null : value === 'true';
setOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring);
setWIOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring);
await saveWorldInfo(name, data);
});
useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');
@ -3069,7 +3095,7 @@ async function getWorldEntry(name, data, entry) {
const value = $(this).val();
data.entries[uid].automationId = value;
setOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId);
setWIOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId);
await saveWorldInfo(name, data);
});
automationIdInput.val(entry.automationId ?? '').trigger('input');
@ -3202,12 +3228,12 @@ function createEntryInputAutocomplete(input, callback, { allowMultiple = false }
/**
* Duplicated a WI entry by copying all of its properties and assigning a new uid
* Duplicate a WI entry by copying all of its properties and assigning a new uid
* @param {*} data - The data of the book
* @param {number} uid - The uid of the entry to copy in this book
* @returns {*} The new WI duplicated entry
*/
function duplicateWorldInfoEntry(data, uid) {
export function duplicateWorldInfoEntry(data, uid) {
if (!data || !('entries' in data) || !data.entries[uid]) {
return;
}
@ -3227,17 +3253,22 @@ function duplicateWorldInfoEntry(data, uid) {
* Deletes a WI entry, with a user confirmation dialog
* @param {*[]} data - The data of the book
* @param {number} uid - The uid of the entry to copy in this book
* @param {object} [options={}] - Optional arguments
* @param {boolean} [options.silent=false] - Whether to prompt the user for deletion or just do it
* @returns {Promise<boolean>} Whether the entry deletion was successful
*/
function deleteWorldInfoEntry(data, uid) {
export async function deleteWorldInfoEntry(data, uid, { silent = false } = {}) {
if (!data || !('entries' in data)) {
return;
}
if (!confirm(`Delete the entry with UID: ${uid}? This action is irreversible!`)) {
throw new Error('User cancelled deletion');
const confirmation = silent || await Popup.show.confirm(`Delete the entry with UID: ${uid}?`, 'This action is irreversible!');
if (!confirmation) {
return false;
}
delete data.entries[uid];
return true;
}
/**
@ -3247,7 +3278,7 @@ function deleteWorldInfoEntry(data, uid) {
*
* @type {{[key: string]: { default: any, type: string }}}
*/
const newEntryDefinition = {
export const newWorldInfoEntryDefinition = {
key: { default: [], type: 'array' },
keysecondary: { default: [], type: 'array' },
comment: { default: '', type: 'string' },
@ -3280,8 +3311,8 @@ const newEntryDefinition = {
delay: { default: null, type: 'number?' },
};
const newEntryTemplate = Object.fromEntries(
Object.entries(newEntryDefinition).map(([key, value]) => [key, value.default]),
export const newWorldInfoEntryTemplate = Object.fromEntries(
Object.entries(newWorldInfoEntryDefinition).map(([key, value]) => [key, value.default]),
);
/**
@ -3290,7 +3321,7 @@ const newEntryTemplate = Object.fromEntries(
* @param {any} data WI data
* @returns {object | undefined} New entry object or undefined if failed
*/
function createWorldInfoEntry(_name, data) {
export function createWorldInfoEntry(_name, data) {
const newUid = getFreeWorldEntryUid(data);
if (!Number.isInteger(newUid)) {
@ -3298,7 +3329,7 @@ function createWorldInfoEntry(_name, data) {
return;
}
const newEntry = { uid: newUid, ...structuredClone(newEntryTemplate) };
const newEntry = { uid: newUid, ...structuredClone(newWorldInfoEntryTemplate) };
data.entries[newUid] = newEntry;
return newEntry;
@ -3316,7 +3347,21 @@ async function _save(name, data) {
eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
}
async function saveWorldInfo(name, data, immediately = false) {
/**
* Saves the world info
*
* This will also refresh the `worldInfoCache`.
* Note, for performance reasons the saved cache will not make a deep clone of the data.
* It is your responsibility to not modify the saved data object after calling this function, or there will be data inconsistencies.
* Call `loadWorldInfoData` or query directly from cache if you need the object again.
*
* @param {string} name - The name of the world info
* @param {any} data - The data to be saved
* @param {boolean} [immediately=false] - Whether to save immediately or use debouncing
* @return {Promise<void>} A promise that resolves when the world info is saved
*/
export async function saveWorldInfo(name, data, immediately = false) {
if (!name || !data) {
return;
}
@ -3373,7 +3418,7 @@ async function renameWorldInfo(name, data) {
* @param {string} worldInfoName - The name of the world info to delete
* @returns {Promise<boolean>} A promise that resolves to true if the world info was successfully deleted, false otherwise
*/
async function deleteWorldInfo(worldInfoName) {
export async function deleteWorldInfo(worldInfoName) {
if (!world_names.includes(worldInfoName)) {
return false;
}
@ -3408,7 +3453,7 @@ async function deleteWorldInfo(worldInfoName) {
return true;
}
function getFreeWorldEntryUid(data) {
export function getFreeWorldEntryUid(data) {
if (!data || !('entries' in data)) {
return null;
}
@ -3424,7 +3469,7 @@ function getFreeWorldEntryUid(data) {
return null;
}
function getFreeWorldName() {
export function getFreeWorldName() {
const MAX_FREE_NAME = 100_000;
for (let index = 1; index < MAX_FREE_NAME; index++) {
const newName = `New World (${index})`;
@ -3446,7 +3491,7 @@ function getFreeWorldName() {
* @param {boolean} [options.interactive=false] - Whether to show a confirmation dialog when overwriting an existing world
* @returns {Promise<boolean>} - True if the world info was successfully created, false otherwise
*/
async function createNewWorldInfo(worldName, { interactive = false } = {}) {
export async function createNewWorldInfo(worldName, { interactive = false } = {}) {
const worldInfoTemplate = { entries: {} };
if (!worldName) {
@ -3507,7 +3552,7 @@ async function getCharacterLore() {
continue;
}
const data = await loadWorldInfoData(worldName);
const data = await loadWorldInfo(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
entries = entries.concat(newEntries);
@ -3527,7 +3572,7 @@ async function getGlobalLore() {
let entries = [];
for (const worldName of selected_world_info) {
const data = await loadWorldInfoData(worldName);
const data = await loadWorldInfo(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
entries = entries.concat(newEntries);
}
@ -3549,7 +3594,7 @@ async function getChatLore() {
return [];
}
const data = await loadWorldInfoData(chatWorld);
const data = await loadWorldInfo(chatWorld);
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: chatWorld, ...rest })) : [];
console.debug(`[WI] Chat lore has ${entries.length} entries`, [chatWorld]);
@ -3586,7 +3631,7 @@ export async function getSortedEntries() {
// Parse decorators
entries = entries.map((entry) => {
const [decorators, content] = parseDecorators(entry.content);
const [decorators, content] = parseDecorators(entry.content || '');
return { ...entry, decorators, content };
});
@ -3665,7 +3710,7 @@ function parseDecorators(content) {
* @typedef {{ worldInfoBefore: string, worldInfoAfter: string, EMEntries: any[], WIDepthEntries: any[], allActivatedEntries: Set<any> }} WIActivated
* @returns {Promise<WIActivated>} The world info activated.
*/
async function checkWorldInfo(chat, maxContext, isDryRun) {
export async function checkWorldInfo(chat, maxContext, isDryRun) {
const context = getContext();
const buffer = new WorldInfoBuffer(chat);
@ -4235,7 +4280,7 @@ function convertAgnaiMemoryBook(inputObj) {
inputObj.entries.forEach((entry, index) => {
outputObj.entries[index] = {
...newEntryTemplate,
...newWorldInfoEntryTemplate,
uid: index,
key: entry.keywords,
keysecondary: [],
@ -4277,7 +4322,7 @@ function convertRisuLorebook(inputObj) {
inputObj.data.forEach((entry, index) => {
outputObj.entries[index] = {
...newEntryTemplate,
...newWorldInfoEntryTemplate,
uid: index,
key: entry.key.split(',').map(x => x.trim()),
keysecondary: entry.secondkey ? entry.secondkey.split(',').map(x => x.trim()) : [],
@ -4324,7 +4369,7 @@ function convertNovelLorebook(inputObj) {
const addMemo = displayName !== undefined && displayName.trim() !== '';
outputObj.entries[index] = {
...newEntryTemplate,
...newWorldInfoEntryTemplate,
uid: index,
key: entry.keys,
keysecondary: [],
@ -4371,7 +4416,7 @@ function convertCharacterBook(characterBook) {
}
result.entries[entry.id] = {
...newEntryTemplate,
...newWorldInfoEntryTemplate,
uid: entry.id,
key: entry.keys,
keysecondary: entry.secondary_keys || [],
@ -4501,7 +4546,7 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
setWorldInfoButtonClass(chid, true);
}
function onWorldInfoChange(args, text) {
export function onWorldInfoChange(args, text) {
if (args !== '__notSlashCommand__') { // if it's a slash command
const silent = isTrueBoolean(args.silent);
if (text.trim() !== '') { // and args are provided
@ -4652,7 +4697,7 @@ export async function importWorldInfo(file) {
});
}
function assignLorebookToChat() {
export function assignLorebookToChat() {
const selectedName = chat_metadata[METADATA_KEY];
const template = $('#chat_world_template .chat_world').clone();