mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into wi-go-brrrrrr-too
This commit is contained in:
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
31
public/scripts/autocomplete/AutoCompleteNameResultBase.js
Normal file
31
public/scripts/autocomplete/AutoCompleteNameResultBase.js
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 = 'What’s 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]) ||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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"> </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>
|
||||
|
@ -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>
|
||||
|
@ -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}".`);
|
||||
}
|
||||
|
769
public/scripts/extensions/quick-reply/lib/morphdom-esm.js
Normal file
769
public/scripts/extensions/quick-reply/lib/morphdom-esm.js
Normal 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;
|
@ -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
@ -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();
|
||||
}
|
||||
|
@ -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}`);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.');
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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() {
|
||||
|
@ -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.',
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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();
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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');
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
7
public/scripts/slash-commands/SlashCommandBreak.js
Normal file
7
public/scripts/slash-commands/SlashCommandBreak.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||
|
||||
export class SlashCommandBreak extends SlashCommandExecutor {
|
||||
get value() {
|
||||
return this.unnamedArgumentList[0]?.value;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
export class SlashCommandBreakController {
|
||||
/**@type {boolean} */ isBreak = false;
|
||||
|
||||
break() {
|
||||
this.isBreak = true;
|
||||
}
|
||||
}
|
3
public/scripts/slash-commands/SlashCommandBreakPoint.js
Normal file
3
public/scripts/slash-commands/SlashCommandBreakPoint.js
Normal file
@ -0,0 +1,3 @@
|
||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||
|
||||
export class SlashCommandBreakPoint extends SlashCommandExecutor {}
|
@ -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) ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||
|
||||
export class SlashCommandClosureExecutor {
|
||||
/**@type {String}*/ name = '';
|
||||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
|
||||
}
|
@ -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;
|
||||
|
@ -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));
|
||||
},
|
||||
|
||||
/**
|
||||
|
83
public/scripts/slash-commands/SlashCommandDebugController.js
Normal file
83
public/scripts/slash-commands/SlashCommandDebugController.js
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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() {
|
||||
|
60
public/scripts/slash-commands/SlashCommandExecutionError.js
Normal file
60
public/scripts/slash-commands/SlashCommandExecutionError.js
Normal 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;
|
||||
}
|
||||
}
|
@ -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 = [];
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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')
|
||||
|
@ -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)),
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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();
|
||||
|
||||
|
Reference in New Issue
Block a user