mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
STscript Parser Rewrite (#1965)
* set isForced to true on input * make floating auto-complete follow horizontal scrolling * add callable closure vars * changes to /let and /var for callable closures * fix error message * fix scope for closure arguments * if should return the pipe result from closures * use /run to call closures and no arguments on immediate closures * throw exception from QRs window-function if no match * when to show autocomplete vs info only * autocomplete positioning * autocomplete styling * add theming to autocomplete (theme, dark, light) * improve autocomplete show/hide logic and editor selection * use blur tint color instead of chat tint color and use blur setting * cleanup and docs * use scope macros for QR args * add enter to select autocomplete * fix no executor found * cleanup and comment * fix alias list in help string * fallback to empty string piped value if null or undefined * fix typo * blur textarea on ctrl+enter execute (and refocus after) * stop executeSlashCommand if parser throws * move /let and /var callbacks into functions * switch textarea to monospace when value starts with slash * add double pipe a pipe breaker * fix /? slash * remove some logging * add "/:name" as shorthand for "/run name" after all * move shit around * fix error message * use testRunShorthandEnd * use parseQuotedValue and parseValue to determine name for "/:" QR labels and set names can include spaces * add some adjustments to make autocomplete work properly some hint in there about "/:" would still be nice * add autocomplete style selector * only strip quotes from subcommand if they are at both ends * fix JSDoc * escaping * allow open quotes on dry run * throwing shit at the wall for /: autocomplete * escapes only for symbols * clean up autocomplete * improve performance * fix scope macros * remove unescaping of pipes * fix macros in scope copy * fix "/? slash" * don't run parser for getNameAt if text has not changed * fix options filter * re-enable blur listener * restore selection on non-replace select * fix for escaping first character of value * add support for {{pipe}} and {{var::}} closures * add index support to var macro * add scoped var macro to macro help * more escape fixes * reduce autocomplete render debounce * cleanup * restore old escape handling and parser flag for strict escaping * fix "no match" autocomplete message * add dummy commands for comments and parser flag * fix type annotations * somewhat safer macro replacements * fix autocomplete select on blank / "no match" * fix cutting off handled part in substitution * add parser flag REPLACE_GETVAR Replaces all {{getvar::}} and {{getglobalvar::}} macros with {{var::}}. Inserts a series of command executors before the command with the macros that: - save {{pipe}} to a var - call /getvar or /getglobalvar to get the variable used in the macro - call /let to save the retrieved variable - return the saved {{pipe}} value This helps to avoid double-substitutions when the var values contain text that could be interpreted as macros. * remove old parser * fix send on enter when no match * deal with pipes in quoted values (loose escaping) * add default parser flags to user settings * allow quoted values in unnamed argument * set parser flag without explicit state to "on" * add click hint on parser error toast * dirty more detailed cmd defs * remove name from unnamed arg * move autocomplete into class and floating with details * replace jQuery's trigger('input') on #send_textarea with native events because jQuery does not dispatch the native event * fix ctrl+space * fix arrow navigation * add comments * fix pointer block * add static fromProps * fix up dummy commands * migrate all commands to addCommandObject * remove commented comment command * fix alias in details * add range as argument type * switch to addCommandObject * switch to addCommandObject * fix height * fix floating details position on left * re-enable blur event * use auto width for full details on floating autocomplete * auto-size floating full details * fix typo * re-enable blur listener * don't prevent enter when selected item is fully typed out * add autocomplete details tooltips * add language to slash command examples * move makeItem into option and command and fix click select * use autocomplete parts in /? slash * fix alias formatting * add language to slash command examples * fix details position on initial input history * small screen styles * replace registerSlashCommand with detailed declarations * put name on first line * add missing returns * fix missing comma * fix alias display in autocomplete list * remove args from help string * move parser settings to its own section * jsdoc * hljs stscript lang * add hljs to autocomplete help examples * add missing import * apply autocomplete colors to stscript codeblocks (hljs) * add fromProps * cache autocomplete elements * towards generic autocomplete * remove unused imports * fix blanks * add return types * re-enable blur * fix blank check * Caption messages by id * add aborting command execution * fix return type * fix chat input font reset * add slash command progress indicator * add missing return * mark registerSlashCommand deprecated * why?? * separate abort logic for commands * remove parsing of quoted values from unnamed arg * add adjustable autocomplete width * revert stop button pulse * add progress and pause/abort to QR editor * add resize event on autocomplete width change * add key= argument to all get vars * refactoring * introduce NamedArgumentAsignment * add TODOs * refactoring * record start and end of named arg assignment * refactoring * prevent duplicate calls to show * refactoring * remove macro ac * add secondary autocomplete and enum descriptions * add syntax highlighting to QR editor * add enum descriptions to /while * add /let key=... to scope variable names * add unnamed argument assignment class and unnamed argument splitting * fix QR editor style * remove dash before autocomplete help text * add autocomplete for unnamed enums * fix remaining dom after holding backslash * fix for unnamed enums * fix autocomplete for /parser-flag * add parser-flag enum help * fix type annotations * fix autocomplete result for /: * add colored autocomplete type icons * collapse second line autocomplete help if empty * mark optional named args in autocomplete * fix when what * remove duplicate debug buttons * dispatch input on autocomplete select * prevent grow from editor syntax layer * add auto-adjust qr editor caret color * remove text-shadow from autocomplete * join value strings in /let and /var * add /abort syntax highlight * fix attempting secondary result when there is none * rename settings headers and split autocomplete / stscript * add parser flag tooltips * add tooltips to chat width stops * fix typo * return clone of help item * fix enum string * don't make optional notice for autocomplete arguments smaller * avoid scrollbar in chat input * add rudimentary macro autocomplete * strip macro from helptext * finally remove closure delimiters around root * cleanup * fix index stuff for removed closure delimiters * fix type hint * add child commands to progress indicator * include sub-separator in macro autocomplete * remove all mentions of interruptsGeneration and purge * remove unused imports * fix syntax highlight with newline at end of input * cleanup select pointer events * coalesce onProgress call * add regex to STscript syntax highlighting * fix closure end * fix autocomplete type icon alignment * adjustments for small screens * fix removing wrong element * add missing "at=" arg to /sys, /comment, /sendas * add font scale setting for autocomplete * add target=_blank for parser flag links * fix for searching enums * remove REGEXP_MODE from hljs just causes trouble * fix autocomplete in closures * fix typo * fix type hint * Get rid of scroll bar on load * Add type hint for /send name argument. Fix 'at' types * Add 'negative' arg hint to /sd command * reenable blur event * Allow /summarize to process any text * Compact layout of script toggles * Expand CSS by default * fix double ranger indicator and adjust to narrow container * make custom css input fill available vertical space * reduce scroll lag * use default cursor on scrollbar * Clean-up module loading in index.html * fix tab indent with hljs --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
@ -3886,9 +3886,9 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
|
|
||||||
|
|
||||||
<div>
|
<div name="MiscellaneousToggles">
|
||||||
|
<h4><span data-i18n="Miscellaneous">Miscellaneous</span></h4>
|
||||||
<div data-newbie-hidden class="flex-container">
|
<div data-newbie-hidden class="flex-container">
|
||||||
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
|
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="[title]Reload and redraw the currently open chat" title="Reload and redraw the currently open chat.">
|
||||||
<small data-i18n="Reload Chat">Reload Chat</small>
|
<small data-i18n="Reload Chat">Reload Chat</small>
|
||||||
@ -3963,8 +3963,8 @@
|
|||||||
<span data-i18n="Custom CSS">Custom CSS</span>
|
<span data-i18n="Custom CSS">Custom CSS</span>
|
||||||
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="customCSS" title="Expand the editor"></i>
|
<i class="editor_maximize fa-solid fa-maximize right_menu_button" data-for="customCSS" title="Expand the editor"></i>
|
||||||
</h4>
|
</h4>
|
||||||
<div class="flex-container flexnowrap alignitemscenter">
|
<div id="CustomCSS-textAreaBlock" class="flex-container flexnowrap alignitemscenter">
|
||||||
<textarea id="customCSS" class="text_pole margin0 margin-r5 textarea_compact monospace"></textarea>
|
<textarea id="customCSS" class="text_pole margin0 margin-r5 textarea_compact monospace" rows="8"></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -4114,7 +4114,76 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div name="AutoCompleteToggle">
|
||||||
|
<h4 data-i18n="AutoComplete Settings">AutoComplete Settings</h4>
|
||||||
|
<div class="flex-container">
|
||||||
|
<div class="flex1" title="Determines how entries are found for autocomplete." data-i18n="[title]Determines how entries are found for autocomplete.">
|
||||||
|
<label for="stscript_matching" data-i18n="Autocomplete Matching"><small>Matching</small></label>
|
||||||
|
<select id="stscript_matching">
|
||||||
|
<option data-i18n="Starts with" value="strict">Starts with</option>
|
||||||
|
<option data-i18n="Includes" value="includes">Includes</option>
|
||||||
|
<option data-i18n="Fuzzy" value="fuzzy">Fuzzy</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex1" title="Sets the style of the autocomplete." data-i18n="[title]Sets the style of the autocomplete.">
|
||||||
|
<label for="stscript_autocomplete_style" data-i18n="Autocomplete Style"><small>Style</small></label>
|
||||||
|
<div class="flex-container flexFlowRow alignItemsBaseline">
|
||||||
|
<select id="stscript_autocomplete_style">
|
||||||
|
<option data-i18n="Follow Theme" value="theme">Follow Theme</option>
|
||||||
|
<option data-i18n="Dark" value="dark">Dark</option>
|
||||||
|
<option data-i18n="Light" value="light">Light</option>
|
||||||
|
</select>
|
||||||
|
<!-- <div class="menu_button fa-solid fa-pen-to-square" title="Customize colors"></div> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-container flexFlowColumn gap0" title="Sets the font size of the autocomplete." data-i18n="[title]Sets the font size of the autocomplete.">
|
||||||
|
<label for="stscript_autocomplete_font_scale"><small>Font Scale</small></label>
|
||||||
|
<input class="neo-range-slider" type="range" id="stscript_autocomplete_font_scale" min="0.5" max="2" step="0.01">
|
||||||
|
<input class="neo-range-input" type="number" min="0.5" max="2" step="0.01" data-for="stscript_autocomplete_font_scale" id="stscript_autocomplete_font_scale_counter">
|
||||||
|
</div>
|
||||||
|
<div title="Sets the width of the autocomplete." data-i18n="[title]Sets the width of the autocomplete.">
|
||||||
|
<label for="stscript_autocomplete_width" data-i18n="Autocomplete Width"><small>Width</small></label>
|
||||||
|
<div class="doubleRangeContainer">
|
||||||
|
<div class="doubleRangeInputContainer">
|
||||||
|
<input type="range" id="stscript_autocomplete_width_left" min="0" max="2" step="1">
|
||||||
|
<datalist id="stscript_autocomplete_width_left_values">
|
||||||
|
<option value="0" label="input" title="chat input box"></option>
|
||||||
|
<option value="1" label="chat" title="entire chat width"></option>
|
||||||
|
<option value="2" label="full" title="full window width"></option>
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div class="doubleRangeInputContainer">
|
||||||
|
<input type="range" id="stscript_autocomplete_width_right" min="0" max="2" step="1">
|
||||||
|
<datalist id="stscript_autocomplete_width_right_values">
|
||||||
|
<option value="0" label="input" title="chat input box"></option>
|
||||||
|
<option value="1" label="chat" title="entire chat width"></option>
|
||||||
|
<option value="2" label="full" title="full window width"></option>
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div name="STscriptToggles">
|
||||||
|
<h4 data-i18n="STscript Settings">STscript Settings</h4>
|
||||||
|
<div title="Sets default flags for the STscript parser." data-i18n="[title]Sets default flags for the STscript parser.">
|
||||||
|
<label data-i18n="Parser Flags"><small>Parser Flags</small></label>
|
||||||
|
<label class="checkbox_label" title="Switch to stricter escaping, allowing all delimiting characters to be escaped with a backslash, and backslashes to be escaped as well." data-i18n="[title]Switch to stricter escaping, allowing all dellimiting characters to be escaped with a backslash, and backslashes to be escaped as well.">
|
||||||
|
<input id="stscript_parser_flag_strict_escaping" type="checkbox" />
|
||||||
|
<span data-i18n="STRICT_ESCAPING"><small>STRICT_ESCAPING</small></span>
|
||||||
|
<a href="https://docs.sillytavern.app/" target="_blank" class="notes-link">
|
||||||
|
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
<label class="checkbox_label" title="Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution." data-i18n="[title]Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.">
|
||||||
|
<input id="stscript_parser_flag_replace_getvar" type="checkbox" />
|
||||||
|
<span data-i18n="REPLACE_GETVAR"><small>REPLACE_GETVAR</small></span>
|
||||||
|
<a href="https://docs.sillytavern.app/" target="_blank" class="notes-link">
|
||||||
|
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -6045,6 +6114,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea>
|
<textarea id="send_textarea" name="text" data-i18n="[no_connection_text]Not connected to API!;[connected_text]Type a message, or /? for help" placeholder="Not connected to API!" no_connection_text="Not connected to API!" connected_text="Type a message, or /? for help"></textarea>
|
||||||
<div id="rightSendForm" class="alignContentCenter">
|
<div id="rightSendForm" class="alignContentCenter">
|
||||||
|
<div id="stscript_continue" title="Continue script execution" class="stscript_btn stscript_continue" data-i18n="[title]Continue script execution">
|
||||||
|
<i class="fa-solid fa-play"></i>
|
||||||
|
</div>
|
||||||
|
<div id="stscript_pause" title="Pause script execution" class="stscript_btn stscript_pause" data-i18n="[title]Pause script execution">
|
||||||
|
<i class="fa-solid fa-pause"></i>
|
||||||
|
</div>
|
||||||
|
<div id="stscript_stop" title="Abort script execution" class="stscript_btn stscript_stop" data-i18n="[title]Abort script execution">
|
||||||
|
<i class="fa-solid fa-stop"></i>
|
||||||
|
</div>
|
||||||
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
|
<div id="mes_stop" title="Abort request" class="mes_stop" data-i18n="[title]Abort request">
|
||||||
<i class="fa-solid fa-circle-stop"></i>
|
<i class="fa-solid fa-circle-stop"></i>
|
||||||
</div>
|
</div>
|
||||||
@ -6208,31 +6286,8 @@
|
|||||||
<script type="module" src="lib/structured-clone/monkey-patch.js"></script>
|
<script type="module" src="lib/structured-clone/monkey-patch.js"></script>
|
||||||
<script type="module" src="lib/swiped-events.js"></script>
|
<script type="module" src="lib/swiped-events.js"></script>
|
||||||
<script type="module" src="lib/eventemitter.js"></script>
|
<script type="module" src="lib/eventemitter.js"></script>
|
||||||
<script type="module" src="scripts/power-user.js"></script>
|
|
||||||
<script type="module" src="scripts/i18n.js"></script>
|
<script type="module" src="scripts/i18n.js"></script>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
<script type="module" src="scripts/world-info.js"></script>
|
|
||||||
<script type="module" src="scripts/group-chats.js"></script>
|
|
||||||
<script type="module" src="scripts/kai-settings.js"></script>
|
|
||||||
<script type="module" src="scripts/textgen-settings.js"></script>
|
|
||||||
<script type="module" src="scripts/textgen-models.js"></script>
|
|
||||||
<script type="module" src="scripts/bookmarks.js"></script>
|
|
||||||
<script type="module" src="scripts/horde.js"></script>
|
|
||||||
<script type="module" src="scripts/RossAscends-mods.js"></script>
|
|
||||||
<script type="module" src="scripts/slash-commands.js"></script>
|
|
||||||
<script type="module" src="scripts/tags.js"></script>
|
|
||||||
<script type="module" src="scripts/secrets.js"></script>
|
|
||||||
<script type="module" src="scripts/extensions.js"></script>
|
|
||||||
<script type="module" src="scripts/authors-note.js"></script>
|
|
||||||
<script type="module" src="scripts/preset-manager.js"></script>
|
|
||||||
<script type="module" src="scripts/filters.js"></script>
|
|
||||||
<script type="module" src="scripts/personas.js"></script>
|
|
||||||
<script type="module" src="scripts/server-history.js"></script>
|
|
||||||
<script type="module" src="scripts/setting-search.js"></script>
|
|
||||||
<script type="module" src="scripts/bulk-edit.js"></script>
|
|
||||||
<script type="module" src="scripts/cfg-scale.js"></script>
|
|
||||||
<script type="module" src="scripts/chats.js"></script>
|
|
||||||
<script type="module" src="scripts/user.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
// Configure toast library:
|
// Configure toast library:
|
||||||
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
|
toastr.options.escapeHtml = true; // Prevent raw HTML inserts
|
||||||
|
194
public/script.js
194
public/script.js
@ -158,7 +158,7 @@ import {
|
|||||||
import { debounce_timeout } from './scripts/constants.js';
|
import { debounce_timeout } from './scripts/constants.js';
|
||||||
|
|
||||||
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
|
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
|
||||||
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, processChatSlashCommands, registerSlashCommand } from './scripts/slash-commands.js';
|
import { COMMENT_NAME_DEFAULT, executeSlashCommands, executeSlashCommandsOnChatInput, getSlashCommandsHelp, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, registerSlashCommand, stopScriptExecution } from './scripts/slash-commands.js';
|
||||||
import {
|
import {
|
||||||
tag_map,
|
tag_map,
|
||||||
tags,
|
tags,
|
||||||
@ -227,6 +227,10 @@ import { currentUser, setUserControls } from './scripts/user.js';
|
|||||||
import { callGenericPopup } from './scripts/popup.js';
|
import { callGenericPopup } from './scripts/popup.js';
|
||||||
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
|
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
|
||||||
import { ScraperManager } from './scripts/scrapers.js';
|
import { ScraperManager } from './scripts/scrapers.js';
|
||||||
|
import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from './scripts/slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument } from './scripts/slash-commands/SlashCommandArgument.js';
|
||||||
|
import { SlashCommandBrowser } from './scripts/slash-commands/SlashCommandBrowser.js';
|
||||||
|
|
||||||
//exporting functions and vars for mods
|
//exporting functions and vars for mods
|
||||||
export {
|
export {
|
||||||
@ -1716,6 +1720,7 @@ export async function reloadCurrentChat() {
|
|||||||
*/
|
*/
|
||||||
export function sendTextareaMessage() {
|
export function sendTextareaMessage() {
|
||||||
if (is_send_press) return;
|
if (is_send_press) return;
|
||||||
|
if (isExecutingCommandsFromChatInput) return;
|
||||||
|
|
||||||
let generateType;
|
let generateType;
|
||||||
// "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
|
// "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
|
||||||
@ -2397,30 +2402,14 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
|
|||||||
* @param {string} message Text to be sent
|
* @param {string} message Text to be sent
|
||||||
* @returns {Promise<boolean>} Whether the message sending was interrupted
|
* @returns {Promise<boolean>} Whether the message sending was interrupted
|
||||||
*/
|
*/
|
||||||
async function processCommands(message) {
|
export async function processCommands(message) {
|
||||||
if (!message || !message.trim().startsWith('/')) {
|
if (!message || !message.trim().startsWith('/')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
await executeSlashCommandsOnChatInput(message, {
|
||||||
const previousText = String($('#send_textarea').val());
|
clearChatInput: true,
|
||||||
const result = await executeSlashCommands(message);
|
});
|
||||||
|
|
||||||
if (!result || typeof result !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentText = String($('#send_textarea').val());
|
|
||||||
|
|
||||||
if (previousText === currentText) {
|
|
||||||
$('#send_textarea').val(result.newText).trigger('input');
|
|
||||||
}
|
|
||||||
|
|
||||||
// interrupt generation if the input was nothing but a command
|
|
||||||
if (message.length > 0 && result?.newText.length === 0) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
return result?.interrupt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendSystemMessage(type, text, extra = {}) {
|
export function sendSystemMessage(type, text, extra = {}) {
|
||||||
@ -2450,6 +2439,14 @@ export function sendSystemMessage(type, text, extra = {}) {
|
|||||||
chat.push(newMessage);
|
chat.push(newMessage);
|
||||||
addOneMessage(newMessage);
|
addOneMessage(newMessage);
|
||||||
is_send_press = false;
|
is_send_press = false;
|
||||||
|
if (type == system_message_types.SLASH_COMMANDS) {
|
||||||
|
const browser = new SlashCommandBrowser();
|
||||||
|
const spinner = document.querySelector('#chat .last_mes .custom-slashHelp');
|
||||||
|
const parent = spinner.parentElement;
|
||||||
|
spinner.remove();
|
||||||
|
browser.renderInto(parent);
|
||||||
|
browser.search.focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2697,7 +2694,7 @@ class StreamingProcessor {
|
|||||||
let messageId = -1;
|
let messageId = -1;
|
||||||
|
|
||||||
if (this.type == 'impersonate') {
|
if (this.type == 'impersonate') {
|
||||||
$('#send_textarea').val('').trigger('input');
|
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
await saveReply(this.type, text, true);
|
await saveReply(this.type, text, true);
|
||||||
@ -2733,7 +2730,7 @@ class StreamingProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isImpersonate) {
|
if (isImpersonate) {
|
||||||
$('#send_textarea').val(processedText).trigger('input');
|
$('#send_textarea').val(processedText)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
let currentTime = new Date();
|
let currentTime = new Date();
|
||||||
@ -3080,7 +3077,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
|||||||
const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
|
const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
|
||||||
|
|
||||||
if (interruptedByCommand) {
|
if (interruptedByCommand) {
|
||||||
//$("#send_textarea").val('').trigger('input');
|
//$("#send_textarea").val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
unblockGeneration(type);
|
unblockGeneration(type);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -3175,7 +3172,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
|||||||
if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) {
|
if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) {
|
||||||
is_send_press = true;
|
is_send_press = true;
|
||||||
textareaText = String($('#send_textarea').val());
|
textareaText = String($('#send_textarea').val());
|
||||||
$('#send_textarea').val('').trigger('input');
|
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
} else {
|
} else {
|
||||||
textareaText = '';
|
textareaText = '';
|
||||||
if (chat.length && chat[chat.length - 1]['is_user']) {
|
if (chat.length && chat[chat.length - 1]['is_user']) {
|
||||||
@ -4137,7 +4134,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
|||||||
|
|
||||||
if (getMessage.length > 0) {
|
if (getMessage.length > 0) {
|
||||||
if (isImpersonate) {
|
if (isImpersonate) {
|
||||||
$('#send_textarea').val(getMessage).trigger('input');
|
$('#send_textarea').val(getMessage)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
generatedPromptCache = '';
|
generatedPromptCache = '';
|
||||||
await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
|
await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
|
||||||
}
|
}
|
||||||
@ -8477,19 +8474,123 @@ jQuery(async function () {
|
|||||||
toastr.success('Chat and settings saved.');
|
toastr.success('Chat and settings saved.');
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSlashCommand('dupe', DupeChar, [], '– duplicates the currently selected character', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'dupe',
|
||||||
registerSlashCommand('api', connectAPISlash, [], `<span class="monospace">(${Object.keys(CONNECT_API_MAP).join(', ')})</span> – connect to an API`, true, true);
|
callback: DupeChar,
|
||||||
registerSlashCommand('impersonate', doImpersonate, ['imp'], '<span class="monospace">[prompt]</span> – calls an impersonation response, with an optional additional prompt', true, true);
|
helpString: 'Duplicates the currently selected character.',
|
||||||
registerSlashCommand('delchat', doDeleteChat, [], '– deletes the current chat', true, true);
|
}));
|
||||||
registerSlashCommand('getchatname', doGetChatName, [], '– returns the name of the current chat file into the pipe', false, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'api',
|
||||||
registerSlashCommand('closechat', doCloseChat, [], '– closes the current chat', true, true);
|
callback: connectAPISlash,
|
||||||
registerSlashCommand('panels', doTogglePanels, ['togglepanels'], '– toggle UI panels on/off', true, true);
|
namedArgumentList: [],
|
||||||
registerSlashCommand('forcesave', doForceSave, [], '– forces a save of the current chat and settings', true, true);
|
unnamedArgumentList: [
|
||||||
registerSlashCommand('instruct', selectInstructCallback, [], '<span class="monospace">(name)</span> – selects instruct mode preset by name. Gets the current instruct if no name is provided', true, true);
|
new SlashCommandArgument(
|
||||||
registerSlashCommand('instruct-on', enableInstructCallback, [], '– enables instruct mode', true, true);
|
'API to connect to',
|
||||||
registerSlashCommand('instruct-off', disableInstructCallback, [], '– disables instruct mode', true, true);
|
[ARGUMENT_TYPE.STRING],
|
||||||
registerSlashCommand('context', selectContextCallback, [], '<span class="monospace">(name)</span> – selects context template by name. Gets the current template if no name is provided', true, true);
|
true,
|
||||||
registerSlashCommand('chat-manager', () => $('#option_select_chat').trigger('click'), ['chat-history', 'manage-chats'], '– opens the chat manager for the current character/group', true, true);
|
false,
|
||||||
|
null,
|
||||||
|
Object.keys(CONNECT_API_MAP),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Connect to an API.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Available APIs:</strong>
|
||||||
|
<pre><code>${Object.keys(CONNECT_API_MAP).join(', ')}</code></pre>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'impersonate',
|
||||||
|
callback: doImpersonate,
|
||||||
|
aliases: ['imp'],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'prompt', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Calls an impersonation response, with an optional additional prompt.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/impersonate What is the meaning of life?</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'delchat',
|
||||||
|
callback: doDeleteChat,
|
||||||
|
helpString: 'Deletes the current chat.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getchatname',
|
||||||
|
callback: doGetChatName,
|
||||||
|
returns: 'chat file name',
|
||||||
|
helpString: 'Returns the name of the current chat file into the pipe.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'closechat',
|
||||||
|
callback: doCloseChat,
|
||||||
|
helpString: 'Closes the current chat.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'panels',
|
||||||
|
callback: doTogglePanels,
|
||||||
|
aliases: ['togglepanels'],
|
||||||
|
helpString: 'Toggle UI panels on/off',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'forcesave',
|
||||||
|
callback: doForceSave,
|
||||||
|
helpString: 'Forces a save of the current chat and settings',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct',
|
||||||
|
callback: selectInstructCallback,
|
||||||
|
returns: 'current preset',
|
||||||
|
namedArgumentList: [],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Selects instruct mode preset by name. Gets the current instruct if no name is provided.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code class="language-stscript">/instruct creative</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-on',
|
||||||
|
callback: enableInstructCallback,
|
||||||
|
helpString: 'Enables instruct mode.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'instruct-off',
|
||||||
|
callback: disableInstructCallback,
|
||||||
|
helpString: 'Disables instruct mode',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'context',
|
||||||
|
callback: selectContextCallback,
|
||||||
|
returns: 'template name',
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Selects context template by name. Gets the current template if no name is provided',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'chat-manager',
|
||||||
|
callback: () => $('#option_select_chat').trigger('click'),
|
||||||
|
aliases: ['chat-history', 'manage-chats'],
|
||||||
|
helpString: 'Opens the chat manager for the current character/group.',
|
||||||
|
}));
|
||||||
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
$('#groupControlsToggle').trigger('click');
|
$('#groupControlsToggle').trigger('click');
|
||||||
@ -9867,11 +9968,22 @@ jQuery(async function () {
|
|||||||
streamingProcessor = null;
|
streamingProcessor = null;
|
||||||
}
|
}
|
||||||
if (abortController) {
|
if (abortController) {
|
||||||
abortController.abort();
|
abortController.abort('Clicked stop button');
|
||||||
hideStopButton();
|
hideStopButton();
|
||||||
}
|
}
|
||||||
eventSource.emit(event_types.GENERATION_STOPPED);
|
eventSource.emit(event_types.GENERATION_STOPPED);
|
||||||
activateSendButtons();
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#form_sheld .stscript_continue', function () {
|
||||||
|
pauseScriptExecution();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#form_sheld .stscript_pause', function () {
|
||||||
|
pauseScriptExecution();
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '#form_sheld .stscript_stop', function () {
|
||||||
|
stopScriptExecution();
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.drawer-toggle').on('click', function () {
|
$('.drawer-toggle').on('click', function () {
|
||||||
|
@ -424,7 +424,7 @@ function restoreUserInput() {
|
|||||||
|
|
||||||
const userInput = LoadLocal('userInput');
|
const userInput = LoadLocal('userInput');
|
||||||
if (userInput) {
|
if (userInput) {
|
||||||
$('#send_textarea').val(userInput).trigger('input');
|
$('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -702,12 +702,12 @@ const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
|||||||
*/
|
*/
|
||||||
function autoFitSendTextArea() {
|
function autoFitSendTextArea() {
|
||||||
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
const originalScrollBottom = chatBlock.scrollHeight - (chatBlock.scrollTop + chatBlock.offsetHeight);
|
||||||
if (sendTextArea.scrollHeight == sendTextArea.offsetHeight) {
|
if (sendTextArea.scrollHeight + 3 == sendTextArea.offsetHeight) {
|
||||||
// Needs to be pulled dynamically because it is affected by font size changes
|
// Needs to be pulled dynamically because it is affected by font size changes
|
||||||
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
|
const sendTextAreaMinHeight = window.getComputedStyle(sendTextArea).getPropertyValue('min-height');
|
||||||
sendTextArea.style.height = sendTextAreaMinHeight;
|
sendTextArea.style.height = sendTextAreaMinHeight;
|
||||||
}
|
}
|
||||||
sendTextArea.style.height = sendTextArea.scrollHeight + 0.3 + 'px';
|
sendTextArea.style.height = sendTextArea.scrollHeight + 3 + 'px';
|
||||||
|
|
||||||
if (!isFirefox) {
|
if (!isFirefox) {
|
||||||
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
|
const newScrollTop = Math.round(chatBlock.scrollHeight - (chatBlock.offsetHeight + originalScrollBottom));
|
||||||
|
@ -9,10 +9,12 @@ import {
|
|||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
import { selected_group } from './group-chats.js';
|
import { selected_group } from './group-chats.js';
|
||||||
import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
|
import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
|
||||||
import { registerSlashCommand } from './slash-commands.js';
|
|
||||||
import { getCharaFilename, debounce, delay } from './utils.js';
|
import { getCharaFilename, debounce, delay } from './utils.js';
|
||||||
import { getTokenCountAsync } from './tokenizers.js';
|
import { getTokenCountAsync } from './tokenizers.js';
|
||||||
import { debounce_timeout } from './constants.js';
|
import { debounce_timeout } from './constants.js';
|
||||||
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||||
export { MODULE_NAME as NOTE_MODULE_NAME };
|
export { MODULE_NAME as NOTE_MODULE_NAME };
|
||||||
|
|
||||||
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
|
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
|
||||||
@ -455,9 +457,59 @@ export function initAuthorsNote() {
|
|||||||
});
|
});
|
||||||
$('#option_toggle_AN').on('click', onANMenuItemClick);
|
$('#option_toggle_AN').on('click', onANMenuItemClick);
|
||||||
|
|
||||||
registerSlashCommand('note', setNoteTextCommand, [], '<span class=\'monospace\'>(text)</span> – sets an author\'s note for the currently selected chat', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'note',
|
||||||
registerSlashCommand('depth', setNoteDepthCommand, [], '<span class=\'monospace\'>(number)</span> – sets an author\'s note depth for in-chat positioning', true, true);
|
callback: setNoteTextCommand,
|
||||||
registerSlashCommand('freq', setNoteIntervalCommand, ['interval'], '<span class=\'monospace\'>(number)</span> – sets an author\'s note insertion frequency', true, true);
|
unnamedArgumentList: [
|
||||||
registerSlashCommand('pos', setNotePositionCommand, ['position'], '(<span class=\'monospace\'>chat</span> or <span class=\'monospace\'>scenario</span>) – sets an author\'s note position', true, true);
|
new SlashCommandArgument(
|
||||||
|
'text', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Sets an author's note for the currently selected chat.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'depth',
|
||||||
|
callback: setNoteDepthCommand,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Sets an author's note depth for in-chat positioning.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'freq',
|
||||||
|
callback: setNoteIntervalCommand,
|
||||||
|
namedArgumentList: [],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Sets an author's note insertion frequency.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'pos',
|
||||||
|
callback: setNotePositionCommand,
|
||||||
|
namedArgumentList: [],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'position', [ARGUMENT_TYPE.STRING], true, false, null, ['chat', 'scenario'],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Sets an author's note position.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||||
}
|
}
|
||||||
|
745
public/scripts/autocomplete/AutoComplete.js
Normal file
745
public/scripts/autocomplete/AutoComplete.js
Normal file
@ -0,0 +1,745 @@
|
|||||||
|
import { power_user } from '../power-user.js';
|
||||||
|
import { debounce, escapeRegex } from '../utils.js';
|
||||||
|
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||||
|
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
|
||||||
|
import { BlankAutoCompleteOption } from './BlankAutoCompleteOption.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||||
|
import { AutoCompleteSecondaryNameResult } from './AutoCompleteSecondaryNameResult.js';
|
||||||
|
|
||||||
|
/**@readonly*/
|
||||||
|
/**@enum {Number}*/
|
||||||
|
export const AUTOCOMPLETE_WIDTH = {
|
||||||
|
'INPUT': 0,
|
||||||
|
'CHAT': 1,
|
||||||
|
'FULL': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AutoComplete {
|
||||||
|
/**@type {HTMLTextAreaElement}*/ textarea;
|
||||||
|
/**@type {boolean}*/ isFloating = false;
|
||||||
|
/**@type {()=>boolean}*/ checkIfActivate;
|
||||||
|
/**@type {(text:string, index:number) => Promise<AutoCompleteNameResult>}*/ getNameAt;
|
||||||
|
|
||||||
|
/**@type {boolean}*/ isActive = false;
|
||||||
|
/**@type {boolean}*/ isReplaceable = false;
|
||||||
|
/**@type {boolean}*/ isShowingDetails = false;
|
||||||
|
/**@type {boolean}*/ wasForced = false;
|
||||||
|
|
||||||
|
/**@type {string}*/ text;
|
||||||
|
/**@type {AutoCompleteNameResult}*/ parserResult;
|
||||||
|
/**@type {AutoCompleteSecondaryNameResult}*/ secondaryParserResult;
|
||||||
|
get effectiveParserResult() { return this.secondaryParserResult ?? this.parserResult; }
|
||||||
|
/**@type {string}*/ name;
|
||||||
|
|
||||||
|
/**@type {boolean}*/ startQuote;
|
||||||
|
/**@type {boolean}*/ endQuote;
|
||||||
|
/**@type {number}*/ selectionStart;
|
||||||
|
|
||||||
|
/**@type {RegExp}*/ fuzzyRegex;
|
||||||
|
|
||||||
|
/**@type {AutoCompleteOption[]}*/ result = [];
|
||||||
|
/**@type {AutoCompleteOption}*/ selectedItem = null;
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/ clone;
|
||||||
|
/**@type {HTMLElement}*/ domWrap;
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
/**@type {HTMLElement}*/ detailsWrap;
|
||||||
|
/**@type {HTMLElement}*/ detailsDom;
|
||||||
|
|
||||||
|
/**@type {function}*/ renderDebounced;
|
||||||
|
/**@type {function}*/ renderDetailsDebounced;
|
||||||
|
/**@type {function}*/ updatePositionDebounced;
|
||||||
|
/**@type {function}*/ updateDetailsPositionDebounced;
|
||||||
|
/**@type {function}*/ updateFloatingPositionDebounced;
|
||||||
|
|
||||||
|
get matchType() {
|
||||||
|
return power_user.stscript.matching ?? 'fuzzy';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLTextAreaElement} 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.
|
||||||
|
*/
|
||||||
|
constructor(textarea, checkIfActivate, getNameAt, isFloating = false) {
|
||||||
|
this.textarea = textarea;
|
||||||
|
this.checkIfActivate = checkIfActivate;
|
||||||
|
this.getNameAt = getNameAt;
|
||||||
|
this.isFloating = isFloating;
|
||||||
|
|
||||||
|
this.domWrap = document.createElement('div'); {
|
||||||
|
this.domWrap.classList.add('autoComplete-wrap');
|
||||||
|
if (isFloating) this.domWrap.classList.add('isFloating');
|
||||||
|
}
|
||||||
|
this.dom = document.createElement('ul'); {
|
||||||
|
this.dom.classList.add('autoComplete');
|
||||||
|
this.domWrap.append(this.dom);
|
||||||
|
}
|
||||||
|
this.detailsWrap = document.createElement('div'); {
|
||||||
|
this.detailsWrap.classList.add('autoComplete-detailsWrap');
|
||||||
|
if (isFloating) this.detailsWrap.classList.add('isFloating');
|
||||||
|
}
|
||||||
|
this.detailsDom = document.createElement('div'); {
|
||||||
|
this.detailsDom.classList.add('autoComplete-details');
|
||||||
|
this.detailsWrap.append(this.detailsDom);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderDebounced = debounce(this.render.bind(this), 10);
|
||||||
|
this.renderDetailsDebounced = debounce(this.renderDetails.bind(this), 10);
|
||||||
|
this.updatePositionDebounced = debounce(this.updatePosition.bind(this), 10);
|
||||||
|
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('keydown', (evt)=>this.handleKeyDown(evt));
|
||||||
|
textarea.addEventListener('click', ()=>this.isActive ? this.show() : null);
|
||||||
|
textarea.addEventListener('selectionchange', ()=>this.show());
|
||||||
|
textarea.addEventListener('blur', ()=>this.hide());
|
||||||
|
if (isFloating) {
|
||||||
|
textarea.addEventListener('scroll', ()=>this.updateFloatingPositionDebounced());
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', ()=>this.updatePositionDebounced());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AutoCompleteOption} option
|
||||||
|
*/
|
||||||
|
makeItem(option) {
|
||||||
|
const li = option.renderItem();
|
||||||
|
// gotta listen to pointerdown (happens before textarea-blur)
|
||||||
|
li.addEventListener('pointerdown', (evt)=>{
|
||||||
|
evt.preventDefault();
|
||||||
|
this.selectedItem = this.result.find(it=>it.name == li.getAttribute('data-name'));
|
||||||
|
this.select();
|
||||||
|
});
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {AutoCompleteOption} item
|
||||||
|
*/
|
||||||
|
updateName(item) {
|
||||||
|
const chars = Array.from(item.dom.querySelector('.name').children);
|
||||||
|
switch (this.matchType) {
|
||||||
|
case 'strict': {
|
||||||
|
chars.forEach((it, idx)=>{
|
||||||
|
if (idx + item.nameOffset < item.name.length) {
|
||||||
|
it.classList.add('matched');
|
||||||
|
} else {
|
||||||
|
it.classList.remove('matched');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'includes': {
|
||||||
|
const start = item.name.toLowerCase().search(this.name);
|
||||||
|
chars.forEach((it, idx)=>{
|
||||||
|
if (idx + item.nameOffset < start) {
|
||||||
|
it.classList.remove('matched');
|
||||||
|
} else if (idx + item.nameOffset < start + item.name.length) {
|
||||||
|
it.classList.add('matched');
|
||||||
|
} else {
|
||||||
|
it.classList.remove('matched');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'fuzzy': {
|
||||||
|
item.name.replace(this.fuzzyRegex, (_, ...parts)=>{
|
||||||
|
parts.splice(-2, 2);
|
||||||
|
if (parts.length == 2) {
|
||||||
|
chars.forEach(c=>c.classList.remove('matched'));
|
||||||
|
} else {
|
||||||
|
let cIdx = item.nameOffset;
|
||||||
|
parts.forEach((it, idx)=>{
|
||||||
|
if (it === null || it.length == 0) return '';
|
||||||
|
if (idx % 2 == 1) {
|
||||||
|
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.add('matched'));
|
||||||
|
} else {
|
||||||
|
chars.slice(cIdx, cIdx + it.length).forEach(c=>c.classList.remove('matched'));
|
||||||
|
}
|
||||||
|
cIdx += it.length;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate score for the fuzzy match.
|
||||||
|
* @param {AutoCompleteOption} option
|
||||||
|
* @returns The option.
|
||||||
|
*/
|
||||||
|
fuzzyScore(option) {
|
||||||
|
const parts = this.fuzzyRegex.exec(option.name).slice(1, -1);
|
||||||
|
let start = null;
|
||||||
|
let consecutive = [];
|
||||||
|
let current = '';
|
||||||
|
let offset = 0;
|
||||||
|
parts.forEach((part, idx) => {
|
||||||
|
if (idx % 2 == 0) {
|
||||||
|
if (part.length > 0) {
|
||||||
|
if (current.length > 0) {
|
||||||
|
consecutive.push(current);
|
||||||
|
}
|
||||||
|
current = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (start === null) {
|
||||||
|
start = offset;
|
||||||
|
}
|
||||||
|
current += part;
|
||||||
|
}
|
||||||
|
offset += part.length;
|
||||||
|
});
|
||||||
|
if (current.length > 0) {
|
||||||
|
consecutive.push(current);
|
||||||
|
}
|
||||||
|
consecutive.sort((a,b)=>b.length - a.length);
|
||||||
|
option.score = new AutoCompleteFuzzyScore(start, consecutive[0]?.length ?? 0);
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two auto complete options by their fuzzy score.
|
||||||
|
* @param {AutoCompleteOption} a
|
||||||
|
* @param {AutoCompleteOption} b
|
||||||
|
*/
|
||||||
|
fuzzyScoreCompare(a, b) {
|
||||||
|
if (a.score.start < b.score.start) return -1;
|
||||||
|
if (a.score.start > b.score.start) return 1;
|
||||||
|
if (a.score.longestConsecutive > b.score.longestConsecutive) return -1;
|
||||||
|
if (a.score.longestConsecutive < b.score.longestConsecutive) return 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the autocomplete.
|
||||||
|
* @param {boolean} isInput Whether triggered by input.
|
||||||
|
* @param {boolean} isForced Whether force-showing (ctrl+space).
|
||||||
|
* @param {boolean} isSelect Whether an autocomplete option was just selected.
|
||||||
|
*/
|
||||||
|
async show(isInput = false, isForced = false, isSelect = false) {
|
||||||
|
//TODO check if isInput and isForced are both required
|
||||||
|
this.text = this.textarea.value;
|
||||||
|
this.isReplaceable = false;
|
||||||
|
|
||||||
|
if (document.activeElement != this.textarea) {
|
||||||
|
// only show with textarea in focus
|
||||||
|
return this.hide();
|
||||||
|
}
|
||||||
|
if (!this.checkIfActivate()) {
|
||||||
|
// only show if provider wants to
|
||||||
|
return this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// request provider to get name result (potentially "incomplete", i.e. not an actual existing name) for
|
||||||
|
// cursor position
|
||||||
|
this.parserResult = await this.getNameAt(this.text, this.textarea.selectionStart);
|
||||||
|
this.secondaryParserResult = null;
|
||||||
|
|
||||||
|
if (!this.parserResult) {
|
||||||
|
// don't show if no name result found, e.g., cursor's area is not a command
|
||||||
|
return this.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// need to know if name can be inside quotes, and then check if quotes are already there
|
||||||
|
if (this.parserResult.canBeQuoted) {
|
||||||
|
this.startQuote = this.text[this.parserResult.start] == '"';
|
||||||
|
this.endQuote = this.startQuote && this.text[this.parserResult.start + this.parserResult.name.length + 1] == '"';
|
||||||
|
} else {
|
||||||
|
this.startQuote = false;
|
||||||
|
this.endQuote = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use lowercase name for matching
|
||||||
|
this.name = this.parserResult.name.toLowerCase() ?? '';
|
||||||
|
|
||||||
|
const isCursorInNamePart = this.textarea.selectionStart >= this.parserResult.start && this.textarea.selectionStart <= this.parserResult.start + this.parserResult.name.length + (this.startQuote ? 1 : 0);
|
||||||
|
if (isForced || isInput) {
|
||||||
|
// if forced (ctrl+space) or user input...
|
||||||
|
if (isCursorInNamePart) {
|
||||||
|
// ...and cursor is somewhere in the name part (including right behind the final char)
|
||||||
|
// -> show autocomplete for the (partial if cursor in the middle) name
|
||||||
|
this.name = this.name.slice(0, this.textarea.selectionStart - (this.parserResult.start) - (this.startQuote ? 1 : 0));
|
||||||
|
this.parserResult.name = this.name;
|
||||||
|
this.isReplaceable = true;
|
||||||
|
} else {
|
||||||
|
this.isReplaceable = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if not forced and no user input -> just show details
|
||||||
|
this.isReplaceable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isForced || isInput || isSelect) {
|
||||||
|
// is forced or user input or just selected autocomplete option...
|
||||||
|
if (!isCursorInNamePart) {
|
||||||
|
// ...and cursor is not somwehere in the main name part -> check for secondary options (e.g., named arguments)
|
||||||
|
const result = this.parserResult.getSecondaryNameAt(this.text, this.textarea.selectionStart, isSelect);
|
||||||
|
if (result && (isForced || result.isRequired)) {
|
||||||
|
this.secondaryParserResult = result;
|
||||||
|
this.name = this.secondaryParserResult.name;
|
||||||
|
this.isReplaceable = isForced || this.secondaryParserResult.isRequired;
|
||||||
|
} else {
|
||||||
|
this.isReplaceable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.matchType == 'fuzzy') {
|
||||||
|
// only build the fuzzy regex if match type is set to fuzzy
|
||||||
|
this.fuzzyRegex = new RegExp(`^(.*?)${this.name.split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i');
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO maybe move the matchers somewhere else; a single match function? matchType is available as property
|
||||||
|
const matchers = {
|
||||||
|
'strict': (name) => name.toLowerCase().startsWith(this.name),
|
||||||
|
'includes': (name) => name.toLowerCase().includes(this.name),
|
||||||
|
'fuzzy': (name) => this.fuzzyRegex.test(name),
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
// remove aliases
|
||||||
|
.filter((it,idx,list) => list.findIndex(opt=>opt.value == it.value) == idx)
|
||||||
|
// update remaining options
|
||||||
|
.map(option => {
|
||||||
|
// build element
|
||||||
|
option.dom = this.makeItem(option);
|
||||||
|
// update replacer and add quotes if necessary
|
||||||
|
if (this.effectiveParserResult.canBeQuoted) {
|
||||||
|
option.replacer = option.name.includes(' ') || this.startQuote || this.endQuote ? `"${option.name}"` : `${option.name}`;
|
||||||
|
} else {
|
||||||
|
option.replacer = option.name;
|
||||||
|
}
|
||||||
|
// calculate fuzzy score if matching is fuzzy
|
||||||
|
if (this.matchType == 'fuzzy') this.fuzzyScore(option);
|
||||||
|
// update the name to highlight the matched chars
|
||||||
|
this.updateName(option);
|
||||||
|
return option;
|
||||||
|
})
|
||||||
|
// sort by fuzzy score or alphabetical
|
||||||
|
.toSorted(this.matchType == 'fuzzy' ? this.fuzzyScoreCompare : (a, b) => a.name.localeCompare(b.name))
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
if (this.result.length == 0) {
|
||||||
|
if (!isInput) {
|
||||||
|
// no result and no input? hide autocomplete
|
||||||
|
return this.hide();
|
||||||
|
}
|
||||||
|
// otherwise add "no match" notice
|
||||||
|
const option = new BlankAutoCompleteOption(
|
||||||
|
this.name.length ?
|
||||||
|
this.effectiveParserResult.makeNoMatchText()
|
||||||
|
: this.effectiveParserResult.makeNoOptionstext()
|
||||||
|
,
|
||||||
|
);
|
||||||
|
this.result.push(option);
|
||||||
|
} else if (this.result.length == 1 && this.effectiveParserResult && 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;
|
||||||
|
} else if (!this.isReplaceable && this.result.length > 1) {
|
||||||
|
return this.hide();
|
||||||
|
}
|
||||||
|
this.selectedItem = this.result[0];
|
||||||
|
this.isActive = true;
|
||||||
|
this.wasForced = isForced;
|
||||||
|
this.renderDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide autocomplete.
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.domWrap?.remove();
|
||||||
|
this.detailsWrap?.remove();
|
||||||
|
this.isActive = false;
|
||||||
|
this.isShowingDetails = false;
|
||||||
|
this.wasForced = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create updated DOM.
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
if (!this.isActive) return this.domWrap.remove();
|
||||||
|
if (this.isReplaceable) {
|
||||||
|
this.dom.innerHTML = '';
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
for (const item of this.result) {
|
||||||
|
if (item == this.selectedItem) {
|
||||||
|
item.dom.classList.add('selected');
|
||||||
|
} else {
|
||||||
|
item.dom.classList.remove('selected');
|
||||||
|
}
|
||||||
|
frag.append(item.dom);
|
||||||
|
}
|
||||||
|
this.dom.append(frag);
|
||||||
|
this.updatePosition();
|
||||||
|
document.body.append(this.domWrap);
|
||||||
|
} else {
|
||||||
|
this.domWrap.remove();
|
||||||
|
}
|
||||||
|
this.renderDetailsDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create updated DOM for details.
|
||||||
|
*/
|
||||||
|
renderDetails() {
|
||||||
|
if (!this.isActive) return this.detailsWrap.remove();
|
||||||
|
if (!this.isShowingDetails && this.isReplaceable) return this.detailsWrap.remove();
|
||||||
|
this.detailsDom.innerHTML = '';
|
||||||
|
this.detailsDom.append(this.selectedItem?.renderDetails() ?? 'NO ITEM');
|
||||||
|
document.body.append(this.detailsWrap);
|
||||||
|
this.updateDetailsPositionDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update position of DOM.
|
||||||
|
*/
|
||||||
|
updatePosition() {
|
||||||
|
if (this.isFloating) {
|
||||||
|
this.updateFloatingPosition();
|
||||||
|
} else {
|
||||||
|
const rect = {};
|
||||||
|
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
|
||||||
|
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
|
||||||
|
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.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`;
|
||||||
|
if (this.isShowingDetails) {
|
||||||
|
this.domWrap.style.setProperty('--leftOffset', '1vw');
|
||||||
|
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
|
||||||
|
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(${rect[power_user.stscript.autocomplete.width.right].right}px, ${this.isShowingDetails ? 74 : 0}vw)`);
|
||||||
|
} else {
|
||||||
|
this.domWrap.style.setProperty('--leftOffset', `max(1vw, ${rect[power_user.stscript.autocomplete.width.left].left}px)`);
|
||||||
|
this.domWrap.style.setProperty('--rightOffset', `calc(100vw - min(99vw, ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
|
||||||
|
}
|
||||||
|
this.updateDetailsPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update position of details DOM.
|
||||||
|
*/
|
||||||
|
updateDetailsPosition() {
|
||||||
|
if (this.isShowingDetails || !this.isReplaceable) {
|
||||||
|
if (this.isFloating) {
|
||||||
|
this.updateFloatingDetailsPosition();
|
||||||
|
} else {
|
||||||
|
const rect = {};
|
||||||
|
rect[AUTOCOMPLETE_WIDTH.INPUT] = this.textarea.getBoundingClientRect();
|
||||||
|
rect[AUTOCOMPLETE_WIDTH.CHAT] = document.querySelector('#sheld').getBoundingClientRect();
|
||||||
|
rect[AUTOCOMPLETE_WIDTH.FULL] = document.body.getBoundingClientRect();
|
||||||
|
if (this.isReplaceable) {
|
||||||
|
this.detailsWrap.classList.remove('full');
|
||||||
|
const selRect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||||
|
this.detailsWrap.style.setProperty('--targetOffset', `${selRect.top}`);
|
||||||
|
this.detailsWrap.style.setProperty('--rightOffset', '1vw');
|
||||||
|
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
|
||||||
|
this.detailsWrap.style.setProperty('--leftOffset', `calc(100vw - ${this.domWrap.style.getPropertyValue('--rightOffset')}`);
|
||||||
|
} else {
|
||||||
|
this.detailsWrap.classList.add('full');
|
||||||
|
this.detailsWrap.style.setProperty('--targetOffset', `${rect[AUTOCOMPLETE_WIDTH.INPUT].top}`);
|
||||||
|
this.detailsWrap.style.setProperty('--bottomOffset', `calc(100vh - ${rect[AUTOCOMPLETE_WIDTH.INPUT].top}px)`);
|
||||||
|
this.detailsWrap.style.setProperty('--leftOffset', `${rect[power_user.stscript.autocomplete.width.left].left}px`);
|
||||||
|
this.detailsWrap.style.setProperty('--rightOffset', `calc(100vw - ${rect[power_user.stscript.autocomplete.width.right].right}px)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update position of floating autocomplete.
|
||||||
|
*/
|
||||||
|
updateFloatingPosition() {
|
||||||
|
const location = this.getCursorPosition();
|
||||||
|
const rect = this.textarea.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);
|
||||||
|
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.bottom = 'auto';
|
||||||
|
this.domWrap.style.maxHeight = `calc(${location.bottom}px - 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)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFloatingDetailsPosition(location = null) {
|
||||||
|
if (!location) location = this.getCursorPosition();
|
||||||
|
const rect = this.textarea.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);
|
||||||
|
this.detailsWrap.style.setProperty('--targetOffset', `${left}`);
|
||||||
|
if (this.isReplaceable) {
|
||||||
|
this.detailsWrap.classList.remove('full');
|
||||||
|
if (left < window.innerWidth / 4) {
|
||||||
|
// if cursor is in left part of screen, show details on right of list
|
||||||
|
this.detailsWrap.classList.add('right');
|
||||||
|
this.detailsWrap.classList.remove('left');
|
||||||
|
} else {
|
||||||
|
// if cursor is in right part of screen, show details on left of list
|
||||||
|
this.detailsWrap.classList.remove('right');
|
||||||
|
this.detailsWrap.classList.add('left');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.detailsWrap.classList.remove('left');
|
||||||
|
this.detailsWrap.classList.remove('right');
|
||||||
|
this.detailsWrap.classList.add('full');
|
||||||
|
}
|
||||||
|
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.bottom = 'auto';
|
||||||
|
this.detailsWrap.style.maxHeight = `calc(${location.bottom}px - 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)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate (keyboard) cursor coordinates within textarea.
|
||||||
|
* @returns {{left:number, top:number, bottom:number}}
|
||||||
|
*/
|
||||||
|
getCursorPosition() {
|
||||||
|
const inputRect = this.textarea.getBoundingClientRect();
|
||||||
|
const style = window.getComputedStyle(this.textarea);
|
||||||
|
if (!this.clone) {
|
||||||
|
this.clone = document.createElement('div');
|
||||||
|
for (const key of style) {
|
||||||
|
this.clone.style[key] = style[key];
|
||||||
|
}
|
||||||
|
this.clone.style.position = 'fixed';
|
||||||
|
this.clone.style.visibility = 'hidden';
|
||||||
|
document.body.append(this.clone);
|
||||||
|
const mo = new MutationObserver(muts=>{
|
||||||
|
if (muts.find(it=>Array.from(it.removedNodes).includes(this.textarea))) {
|
||||||
|
this.clone.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mo.observe(this.textarea.parentElement, { childList:true });
|
||||||
|
}
|
||||||
|
this.clone.style.height = `${inputRect.height}px`;
|
||||||
|
this.clone.style.left = `${inputRect.left}px`;
|
||||||
|
this.clone.style.top = `${inputRect.top}px`;
|
||||||
|
this.clone.style.whiteSpace = style.whiteSpace;
|
||||||
|
this.clone.style.tabSize = style.tabSize;
|
||||||
|
const text = this.textarea.value;
|
||||||
|
const before = text.slice(0, this.textarea.selectionStart);
|
||||||
|
this.clone.textContent = before;
|
||||||
|
const locator = document.createElement('span');
|
||||||
|
locator.textContent = text[this.textarea.selectionStart];
|
||||||
|
this.clone.append(locator);
|
||||||
|
this.clone.append(text.slice(this.textarea.selectionStart + 1));
|
||||||
|
this.clone.scrollTop = this.textarea.scrollTop;
|
||||||
|
this.clone.scrollLeft = this.textarea.scrollLeft;
|
||||||
|
const locatorRect = locator.getBoundingClientRect();
|
||||||
|
const location = {
|
||||||
|
left: locatorRect.left,
|
||||||
|
top: locatorRect.top,
|
||||||
|
bottom: locatorRect.bottom,
|
||||||
|
};
|
||||||
|
return location;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle details view alongside autocomplete list.
|
||||||
|
*/
|
||||||
|
toggleDetails() {
|
||||||
|
this.isShowingDetails = !this.isShowingDetails;
|
||||||
|
this.renderDetailsDebounced();
|
||||||
|
this.updatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select an item for autocomplete and put text into textarea.
|
||||||
|
*/
|
||||||
|
async select() {
|
||||||
|
if (this.isReplaceable && this.selectedItem.value !== null) {
|
||||||
|
this.textarea.value = `${this.text.slice(0, this.effectiveParserResult.start)}${this.selectedItem.replacer}${this.text.slice(this.effectiveParserResult.start + this.effectiveParserResult.name.length + (this.startQuote ? 1 : 0) + (this.endQuote ? 1 : 0))}`;
|
||||||
|
this.textarea.selectionStart = this.effectiveParserResult.start + this.selectedItem.replacer.length;
|
||||||
|
this.textarea.selectionEnd = this.textarea.selectionStart;
|
||||||
|
this.show(false, false, true);
|
||||||
|
} else {
|
||||||
|
const selectionStart = this.textarea.selectionStart;
|
||||||
|
const selectionEnd = this.textarea.selectionDirection;
|
||||||
|
this.textarea.selectionStart = selectionStart;
|
||||||
|
this.textarea.selectionDirection = selectionEnd;
|
||||||
|
}
|
||||||
|
this.wasForced = false;
|
||||||
|
this.textarea.dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the item at newIdx in the autocomplete list as selected.
|
||||||
|
* @param {number} newIdx
|
||||||
|
*/
|
||||||
|
selectItemAtIndex(newIdx) {
|
||||||
|
this.selectedItem.dom.classList.remove('selected');
|
||||||
|
this.selectedItem = this.result[newIdx];
|
||||||
|
this.selectedItem.dom.classList.add('selected');
|
||||||
|
const rect = this.selectedItem.dom.children[0].getBoundingClientRect();
|
||||||
|
const rectParent = this.dom.getBoundingClientRect();
|
||||||
|
if (rect.top < rectParent.top || rect.bottom > rectParent.bottom ) {
|
||||||
|
this.dom.scrollTop += rect.top < rectParent.top ? rect.top - rectParent.top : rect.bottom - rectParent.bottom;
|
||||||
|
}
|
||||||
|
this.renderDetailsDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle keyboard events.
|
||||||
|
* @param {KeyboardEvent} evt The event.
|
||||||
|
*/
|
||||||
|
async handleKeyDown(evt) {
|
||||||
|
// autocomplete is shown and cursor at end of current command name (or inside name and typed or forced)
|
||||||
|
if (this.isActive && this.isReplaceable) {
|
||||||
|
// actions in the list
|
||||||
|
switch (evt.key) {
|
||||||
|
case 'ArrowUp': {
|
||||||
|
// select previous item
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const idx = this.result.indexOf(this.selectedItem);
|
||||||
|
let newIdx;
|
||||||
|
if (idx == 0) newIdx = this.result.length - 1;
|
||||||
|
else newIdx = idx - 1;
|
||||||
|
this.selectItemAtIndex(newIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'ArrowDown': {
|
||||||
|
// select next item
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
const idx = this.result.indexOf(this.selectedItem);
|
||||||
|
const newIdx = (idx + 1) % this.result.length;
|
||||||
|
this.selectItemAtIndex(newIdx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Enter': {
|
||||||
|
// pick the selected item to autocomplete
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
|
||||||
|
if (this.selectedItem.name == this.name) break;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopImmediatePropagation();
|
||||||
|
this.select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Tab': {
|
||||||
|
// pick the selected item to autocomplete
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey || this.selectedItem.value == '') break;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopImmediatePropagation();
|
||||||
|
this.select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// details are shown, cursor can be anywhere
|
||||||
|
if (this.isActive) {
|
||||||
|
switch (evt.key) {
|
||||||
|
case 'Escape': {
|
||||||
|
// close autocomplete
|
||||||
|
if (evt.ctrlKey || evt.altKey || evt.shiftKey) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 'Enter': {
|
||||||
|
// hide autocomplete on enter (send, execute, ...)
|
||||||
|
if (!evt.shiftKey) {
|
||||||
|
this.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// autocomplete shown or not, cursor anywhere
|
||||||
|
switch (evt.key) {
|
||||||
|
case ' ': {
|
||||||
|
if (evt.ctrlKey) {
|
||||||
|
if (this.isActive && this.isReplaceable) {
|
||||||
|
// ctrl-space to toggle details for selected item
|
||||||
|
this.toggleDetails();
|
||||||
|
} else {
|
||||||
|
// ctrl-space to force show autocomplete
|
||||||
|
this.show(false, true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (['Control', 'Shift', 'Alt'].includes(evt.key)) {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
public/scripts/autocomplete/AutoCompleteFuzzyScore.js
Normal file
16
public/scripts/autocomplete/AutoCompleteFuzzyScore.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class AutoCompleteFuzzyScore {
|
||||||
|
/**@type {number}*/ start;
|
||||||
|
/**@type {number}*/ longestConsecutive;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} longestConsecutive
|
||||||
|
*/
|
||||||
|
constructor(start, longestConsecutive) {
|
||||||
|
this.start = start;
|
||||||
|
this.longestConsecutive = longestConsecutive;
|
||||||
|
}
|
||||||
|
}
|
44
public/scripts/autocomplete/AutoCompleteNameResult.js
Normal file
44
public/scripts/autocomplete/AutoCompleteNameResult.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { SlashCommandNamedArgumentAutoCompleteOption } from '../slash-commands/SlashCommandNamedArgumentAutoCompleteOption.js';
|
||||||
|
import { AutoCompleteOption } from './AutoCompleteOption.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} text The whole text
|
||||||
|
* @param {number} index Cursor index within text
|
||||||
|
* @param {boolean} isSelect Whether autocomplete was triggered by selecting an autocomplete option
|
||||||
|
* @returns {AutoCompleteSecondaryNameResult}
|
||||||
|
*/
|
||||||
|
getSecondaryNameAt(text, index, isSelect) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
206
public/scripts/autocomplete/AutoCompleteOption.js
Normal file
206
public/scripts/autocomplete/AutoCompleteOption.js
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
import { SlashCommand } from '../slash-commands/SlashCommand.js';
|
||||||
|
import { AutoCompleteFuzzyScore } from './AutoCompleteFuzzyScore.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class AutoCompleteOption {
|
||||||
|
/**@type {string}*/ name;
|
||||||
|
/**@type {string}*/ typeIcon;
|
||||||
|
/**@type {number}*/ nameOffset = 0;
|
||||||
|
/**@type {AutoCompleteFuzzyScore}*/ score;
|
||||||
|
/**@type {string}*/ replacer;
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used as a comparison value when removing duplicates (e.g., when a SlashCommand has aliases).
|
||||||
|
* @type {any}
|
||||||
|
* */
|
||||||
|
get value() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
constructor(name, typeIcon = ' ') {
|
||||||
|
this.name = name;
|
||||||
|
this.typeIcon = typeIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
makeItem(key, typeIcon, noSlash, namedArguments = [], unnamedArguments = [], returnType = 'void', helpString = '', aliasList = []) {
|
||||||
|
const li = document.createElement('li'); {
|
||||||
|
li.classList.add('item');
|
||||||
|
const type = document.createElement('span'); {
|
||||||
|
type.classList.add('type');
|
||||||
|
type.classList.add('monospace');
|
||||||
|
type.textContent = typeIcon;
|
||||||
|
li.append(type);
|
||||||
|
}
|
||||||
|
const specs = document.createElement('span'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('span'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = noSlash ? '' : '/';
|
||||||
|
key.split('').forEach(char=>{
|
||||||
|
const span = document.createElement('span'); {
|
||||||
|
span.textContent = char;
|
||||||
|
name.append(span);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
const body = document.createElement('span'); {
|
||||||
|
body.classList.add('body');
|
||||||
|
const args = document.createElement('span'); {
|
||||||
|
args.classList.add('arguments');
|
||||||
|
for (const arg of namedArguments) {
|
||||||
|
const argItem = document.createElement('span'); {
|
||||||
|
argItem.classList.add('argument');
|
||||||
|
argItem.classList.add('namedArgument');
|
||||||
|
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||||
|
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||||
|
const name = document.createElement('span'); {
|
||||||
|
name.classList.add('argument-name');
|
||||||
|
name.textContent = arg.name;
|
||||||
|
argItem.append(name);
|
||||||
|
}
|
||||||
|
if (arg.enumList.length > 0) {
|
||||||
|
const enums = document.createElement('span'); {
|
||||||
|
enums.classList.add('argument-enums');
|
||||||
|
for (const e of arg.enumList) {
|
||||||
|
const enumItem = document.createElement('span'); {
|
||||||
|
enumItem.classList.add('argument-enum');
|
||||||
|
enumItem.textContent = e;
|
||||||
|
enums.append(enumItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argItem.append(enums);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const types = document.createElement('span'); {
|
||||||
|
types.classList.add('argument-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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.append(argItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const arg of unnamedArguments) {
|
||||||
|
const argItem = document.createElement('span'); {
|
||||||
|
argItem.classList.add('argument');
|
||||||
|
argItem.classList.add('unnamedArgument');
|
||||||
|
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');
|
||||||
|
for (const e of arg.enumList) {
|
||||||
|
const enumItem = document.createElement('span'); {
|
||||||
|
enumItem.classList.add('argument-enum');
|
||||||
|
enumItem.textContent = e;
|
||||||
|
enums.append(enumItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
argItem.append(enums);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const types = document.createElement('span'); {
|
||||||
|
types.classList.add('argument-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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.append(argItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.append(args);
|
||||||
|
}
|
||||||
|
const returns = document.createElement('span'); {
|
||||||
|
returns.classList.add('returns');
|
||||||
|
returns.textContent = returnType ?? 'void';
|
||||||
|
// body.append(returns);
|
||||||
|
}
|
||||||
|
specs.append(body);
|
||||||
|
}
|
||||||
|
li.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
const content = document.createElement('span'); {
|
||||||
|
content.classList.add('helpContent');
|
||||||
|
content.innerHTML = helpString;
|
||||||
|
const text = content.textContent;
|
||||||
|
content.innerHTML = '';
|
||||||
|
content.textContent = text;
|
||||||
|
help.append(content);
|
||||||
|
}
|
||||||
|
li.append(help);
|
||||||
|
}
|
||||||
|
if (aliasList.length > 0) {
|
||||||
|
const aliases = document.createElement('span'); {
|
||||||
|
aliases.classList.add('aliases');
|
||||||
|
aliases.append(' (alias: ');
|
||||||
|
for (const aliasName of aliasList) {
|
||||||
|
const alias = document.createElement('span'); {
|
||||||
|
alias.classList.add('monospace');
|
||||||
|
alias.textContent = `/${aliasName}`;
|
||||||
|
aliases.append(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aliases.append(')');
|
||||||
|
// li.append(aliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {HTMLElement}
|
||||||
|
*/
|
||||||
|
renderItem() {
|
||||||
|
// throw new Error(`${this.constructor.name}.renderItem() is not implemented`);
|
||||||
|
let li;
|
||||||
|
li = this.makeItem(this.name, this.typeIcon, true);
|
||||||
|
li.setAttribute('data-name', this.name);
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {DocumentFragment}
|
||||||
|
*/
|
||||||
|
renderDetails() {
|
||||||
|
// throw new Error(`${this.constructor.name}.renderDetails() is not implemented`);
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const specs = document.createElement('div'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('div'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = this.name;
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
frag.append(specs);
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { AutoCompleteNameResult } from './AutoCompleteNameResult.js';
|
||||||
|
|
||||||
|
export class AutoCompleteSecondaryNameResult extends AutoCompleteNameResult {
|
||||||
|
/**@type {boolean}*/ isRequired = false;
|
||||||
|
}
|
29
public/scripts/autocomplete/BlankAutoCompleteOption.js
Normal file
29
public/scripts/autocomplete/BlankAutoCompleteOption.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||||
|
|
||||||
|
export class BlankAutoCompleteOption extends AutoCompleteOption {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
constructor(name) {
|
||||||
|
super(name);
|
||||||
|
this.dom = this.renderItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
get value() { return null; }
|
||||||
|
|
||||||
|
|
||||||
|
renderItem() {
|
||||||
|
const li = document.createElement('li'); {
|
||||||
|
li.classList.add('item');
|
||||||
|
li.classList.add('blank');
|
||||||
|
li.textContent = this.name;
|
||||||
|
}
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderDetails() {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
44
public/scripts/autocomplete/MacroAutoCompleteOption.js
Normal file
44
public/scripts/autocomplete/MacroAutoCompleteOption.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { AutoCompleteOption } from './AutoCompleteOption.js';
|
||||||
|
|
||||||
|
export class MacroAutoCompleteOption extends AutoCompleteOption {
|
||||||
|
/**@type {string}*/ fullName;
|
||||||
|
/**@type {string}*/ description;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(name, fullName, description) {
|
||||||
|
super(name, '{}');
|
||||||
|
this.fullName = fullName;
|
||||||
|
this.description = description;
|
||||||
|
this.nameOffset = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderItem() {
|
||||||
|
let li;
|
||||||
|
li = this.makeItem(`${this.fullName}`, '{}', true, [], [], null, this.description);
|
||||||
|
li.setAttribute('data-name', this.name);
|
||||||
|
li.setAttribute('data-option-type', 'macro');
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderDetails() {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const specs = document.createElement('div'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('div'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = this.fullName;
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
frag.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
help.innerHTML = this.description;
|
||||||
|
frag.append(help);
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
|
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
|
||||||
import { saveMetadataDebounced } from './extensions.js';
|
import { saveMetadataDebounced } from './extensions.js';
|
||||||
import { registerSlashCommand } from './slash-commands.js';
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
import { flashHighlight, stringFormat } from './utils.js';
|
import { flashHighlight, stringFormat } from './utils.js';
|
||||||
|
|
||||||
const BG_METADATA_KEY = 'custom_background';
|
const BG_METADATA_KEY = 'custom_background';
|
||||||
@ -480,7 +481,20 @@ export function initBackgrounds() {
|
|||||||
$('#auto_background').on('click', autoBackgroundCommand);
|
$('#auto_background').on('click', autoBackgroundCommand);
|
||||||
$('#add_bg_button').on('change', onBackgroundUploadSelected);
|
$('#add_bg_button').on('change', onBackgroundUploadSelected);
|
||||||
$('#bg-filter').on('input', onBackgroundFilterInput);
|
$('#bg-filter').on('input', onBackgroundFilterInput);
|
||||||
registerSlashCommand('lockbg', onLockBackgroundClick, ['bglock'], '– locks a background for the currently selected chat', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
|
||||||
registerSlashCommand('unlockbg', onUnlockBackgroundClick, ['bgunlock'], '– unlocks a background for the currently selected chat', true, true);
|
callback: onLockBackgroundClick,
|
||||||
registerSlashCommand('autobg', autoBackgroundCommand, ['bgauto'], '– automatically changes the background based on the chat context using the AI request prompt', true, true);
|
aliases: ['bglock'],
|
||||||
|
helpString: 'Locks a background for the currently selected chat',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
|
||||||
|
callback: onUnlockBackgroundClick,
|
||||||
|
aliases: ['bgunlock'],
|
||||||
|
helpString: 'Unlocks a background for the currently selected chat',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'autobg',
|
||||||
|
callback: autoBackgroundCommand,
|
||||||
|
aliases: ['bgauto'],
|
||||||
|
helpString: 'Automatically changes the background based on the chat context using the AI request prompt',
|
||||||
|
}));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
|
|
||||||
jQuery(async () => {
|
jQuery(async () => {
|
||||||
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||||
$('#extensionsMenu').prepend(buttons);
|
$('#extensionsMenu').prepend(buttons);
|
||||||
|
|
||||||
registerSlashCommand('db', () => document.getElementById('manageAttachments')?.click(), ['databank', 'data-bank'], '– open the data bank', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db',
|
||||||
|
callback: () => document.getElementById('manageAttachments')?.click(),
|
||||||
|
aliases: ['databank', 'data-bank'],
|
||||||
|
helpString: 'Open the data bank',
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,9 @@ import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
|||||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||||
import { getMultimodalCaption } from '../shared.js';
|
import { getMultimodalCaption } from '../shared.js';
|
||||||
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
|
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
const MODULE_NAME = 'caption';
|
const MODULE_NAME = 'caption';
|
||||||
@ -254,6 +256,19 @@ async function onSelectImage(e, prompt, quiet) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const caption = await getCaptionForFile(file, prompt, quiet);
|
||||||
|
form && form.reset();
|
||||||
|
return caption;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a caption for an image file.
|
||||||
|
* @param {File} file Input file
|
||||||
|
* @param {string} prompt Caption prompt
|
||||||
|
* @param {boolean} quiet Suppresses sending a message
|
||||||
|
* @returns {Promise<string>} Generated caption
|
||||||
|
*/
|
||||||
|
async function getCaptionForFile(file, prompt, quiet) {
|
||||||
try {
|
try {
|
||||||
setSpinnerIcon();
|
setSpinnerIcon();
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
@ -273,7 +288,6 @@ async function onSelectImage(e, prompt, quiet) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
form && form.reset();
|
|
||||||
setImageIcon();
|
setImageIcon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -288,9 +302,26 @@ function onRefineModeInput() {
|
|||||||
* @param {object} args Named parameters
|
* @param {object} args Named parameters
|
||||||
* @param {string} prompt Caption prompt
|
* @param {string} prompt Caption prompt
|
||||||
*/
|
*/
|
||||||
function captionCommandCallback(args, prompt) {
|
async function captionCommandCallback(args, prompt) {
|
||||||
return new Promise(resolve => {
|
|
||||||
const quiet = isTrueBoolean(args?.quiet);
|
const quiet = isTrueBoolean(args?.quiet);
|
||||||
|
const id = args?.id;
|
||||||
|
|
||||||
|
if (!isNaN(Number(id))) {
|
||||||
|
const message = getContext().chat[id];
|
||||||
|
if (message?.extra?.image) {
|
||||||
|
try {
|
||||||
|
const fetchResult = await fetch(message.extra.image);
|
||||||
|
const blob = await fetchResult.blob();
|
||||||
|
const file = new File([blob], 'image.jpg', { type: blob.type });
|
||||||
|
return await getCaptionForFile(file, prompt, quiet);
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error('Failed to get image from the message. Make sure the image is accessible.');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = 'image/*';
|
input.accept = 'image/*';
|
||||||
@ -492,5 +523,35 @@ jQuery(function () {
|
|||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSlashCommand('caption', captionCommandCallback, [], '<span class="monospace">quiet=true/false [prompt]</span> - caption an image with an optional prompt and passes the caption down the pipe. Only multimodal sources support custom prompts. Set the "quiet" argument to true to suppress sending a captioned message, default: false.', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
|
||||||
|
callback: captionCommandCallback,
|
||||||
|
returns: 'caption',
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['true', 'false'],
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'id', 'get image from a message with this ID', [ARGUMENT_TYPE.NUMBER], false, false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'prompt', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Caption an image with an optional prompt and passes the caption down the pipe.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Only multimodal sources support custom prompts.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Provide a message ID to get an image from a message instead of uploading one.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Set the "quiet" argument to true to suppress sending a captioned message, default: false.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
@ -2,11 +2,13 @@ import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHea
|
|||||||
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
import { dragElement, isMobile } from '../../RossAscends-mods.js';
|
||||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
|
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||||
import { loadMovingUIState, power_user } from '../../power-user.js';
|
import { loadMovingUIState, power_user } from '../../power-user.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
|
||||||
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
|
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
|
||||||
import { hideMutedSprites } from '../../group-chats.js';
|
import { hideMutedSprites } from '../../group-chats.js';
|
||||||
import { isJsonSchemaSupported } from '../../textgen-settings.js';
|
import { isJsonSchemaSupported } from '../../textgen-settings.js';
|
||||||
import { debounce_timeout } from '../../constants.js';
|
import { debounce_timeout } from '../../constants.js';
|
||||||
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
const MODULE_NAME = 'expressions';
|
const MODULE_NAME = 'expressions';
|
||||||
@ -906,8 +908,10 @@ async function setSpriteSetCommand(_, folder) {
|
|||||||
|
|
||||||
$('#expression_override').val(folder.trim());
|
$('#expression_override').val(folder.trim());
|
||||||
onClickExpressionOverrideButton();
|
onClickExpressionOverrideButton();
|
||||||
removeExpression();
|
// removeExpression();
|
||||||
moduleWorker();
|
// moduleWorker();
|
||||||
|
const vnMode = isVisualNovelMode();
|
||||||
|
await sendExpressionCall(folder, lastExpression, true, vnMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function classifyCommand(_, text) {
|
async function classifyCommand(_, text) {
|
||||||
@ -1967,9 +1971,61 @@ function migrateSettings() {
|
|||||||
});
|
});
|
||||||
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
||||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||||
registerSlashCommand('sprite', setSpriteSlashCommand, ['emote'], '<span class="monospace">(spriteId)</span> – force sets the sprite for the current character', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'sprite',
|
||||||
registerSlashCommand('spriteoverride', setSpriteSetCommand, ['costume'], '<span class="monospace">(optional folder)</span> – sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.', true, true);
|
aliases: ['emote'],
|
||||||
registerSlashCommand('lastsprite', (_, value) => lastExpression[value.trim()] ?? '', [], '<span class="monospace">(charName)</span> – Returns the last set sprite / expression for the named character.', true, true);
|
callback: setSpriteSlashCommand,
|
||||||
registerSlashCommand('th', toggleTalkingHeadCommand, ['talkinghead'], '– Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.', true, true);
|
unnamedArgumentList: [
|
||||||
registerSlashCommand('classify', classifyCommand, [], '<span class="monospace">(text)</span> – performs an emotion classification of the given text and returns a label.', true, true);
|
new SlashCommandArgument(
|
||||||
|
'spriteId', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Force sets the sprite for the current character.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'spriteoverride',
|
||||||
|
aliases: ['costume'],
|
||||||
|
callback: setSpriteSetCommand,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'optional folder', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Sets an override sprite folder for the current character. If the name starts with a slash or a backslash, selects a sub-folder in the character-named folder. Empty value to reset to default.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lastsprite',
|
||||||
|
callback: (_, value) => lastExpression[value.trim()] ?? '',
|
||||||
|
returns: 'sprite',
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'charName', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Returns the last set sprite / expression for the named character.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'th',
|
||||||
|
callback: toggleTalkingHeadCommand,
|
||||||
|
aliases: ['talkinghead'],
|
||||||
|
helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'classify',
|
||||||
|
callback: classifyCommand,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'text', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
returns: 'emotion classification label for the given text',
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Performs an emotion classification of the given text and returns a label.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/classify I am so happy today!</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
})();
|
})();
|
||||||
|
@ -8,7 +8,9 @@ import { groups, selected_group } from '../../group-chats.js';
|
|||||||
import { loadFileToDocument, delay } from '../../utils.js';
|
import { loadFileToDocument, delay } from '../../utils.js';
|
||||||
import { loadMovingUIState } from '../../power-user.js';
|
import { loadMovingUIState } from '../../power-user.js';
|
||||||
import { dragElement } from '../../RossAscends-mods.js';
|
import { dragElement } from '../../RossAscends-mods.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
|
|
||||||
const extensionName = 'gallery';
|
const extensionName = 'gallery';
|
||||||
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
const extensionFolderPath = `scripts/extensions/${extensionName}/`;
|
||||||
@ -415,8 +417,26 @@ function viewWithDragbox(items) {
|
|||||||
|
|
||||||
|
|
||||||
// Registers a simple command for opening the char gallery.
|
// Registers a simple command for opening the char gallery.
|
||||||
registerSlashCommand('show-gallery', showGalleryCommand, ['sg'], '– shows the gallery', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'show-gallery',
|
||||||
registerSlashCommand('list-gallery', listGalleryCommand, ['lg'], '<span class="monospace">[optional char=charName] [optional group=groupName]</span> – list images in the gallery of the current char / group or a specified char / group', true, true);
|
aliases: ['sg'],
|
||||||
|
callback: showGalleryCommand,
|
||||||
|
helpString: 'Shows the gallery.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'list-gallery',
|
||||||
|
aliases: ['lg'],
|
||||||
|
callback: listGalleryCommand,
|
||||||
|
returns: 'list of images',
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'char', 'character name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'group', 'group name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'List images in the gallery of the current char / group or a specified char / group.',
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
function showGalleryCommand(args) {
|
function showGalleryCommand(args) {
|
||||||
showCharGallery();
|
showCharGallery();
|
||||||
|
@ -16,11 +16,14 @@ import {
|
|||||||
getMaxContextSize,
|
getMaxContextSize,
|
||||||
} from '../../../script.js';
|
} from '../../../script.js';
|
||||||
import { is_group_generating, selected_group } from '../../group-chats.js';
|
import { is_group_generating, selected_group } from '../../group-chats.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
|
||||||
import { loadMovingUIState } from '../../power-user.js';
|
import { loadMovingUIState } from '../../power-user.js';
|
||||||
import { dragElement } from '../../RossAscends-mods.js';
|
import { dragElement } from '../../RossAscends-mods.js';
|
||||||
import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
|
import { getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
|
||||||
import { debounce_timeout } from '../../constants.js';
|
import { debounce_timeout } from '../../constants.js';
|
||||||
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
|
import { resolveVariable } from '../../variables.js';
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
const MODULE_NAME = '1_memory';
|
const MODULE_NAME = '1_memory';
|
||||||
@ -416,7 +419,7 @@ async function forceSummarizeChat() {
|
|||||||
console.log(`Skipping WIAN? ${skipWIAN}`);
|
console.log(`Skipping WIAN? ${skipWIAN}`);
|
||||||
if (!context.chatId) {
|
if (!context.chatId) {
|
||||||
toastr.warning('No chat selected');
|
toastr.warning('No chat selected');
|
||||||
return;
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
toastr.info('Summarizing chat...', 'Please wait');
|
toastr.info('Summarizing chat...', 'Please wait');
|
||||||
@ -424,7 +427,42 @@ async function forceSummarizeChat() {
|
|||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
toastr.warning('Failed to summarize chat');
|
toastr.warning('Failed to summarize chat');
|
||||||
return;
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for the summarize command.
|
||||||
|
* @param {object} args Command arguments
|
||||||
|
* @param {string} text Text to summarize
|
||||||
|
*/
|
||||||
|
async function summarizeCallback(args, text) {
|
||||||
|
text = text.trim();
|
||||||
|
|
||||||
|
// Using forceSummarizeChat to summarize the current chat
|
||||||
|
if (!text) {
|
||||||
|
return await forceSummarizeChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = args.source || extension_settings.memory.source;
|
||||||
|
const prompt = substituteParams((resolveVariable(args.prompt) || extension_settings.memory.prompt)?.replace(/{{words}}/gi, extension_settings.memory.promptWords));
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (source) {
|
||||||
|
case summary_sources.extras:
|
||||||
|
return await callExtrasSummarizeAPI(text);
|
||||||
|
case summary_sources.main:
|
||||||
|
return await generateRaw(text, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
|
||||||
|
default:
|
||||||
|
toastr.warning('Invalid summarization source specified');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error(String(error), 'Failed to summarize text');
|
||||||
|
console.log(error);
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -668,25 +706,7 @@ async function summarizeChatExtras(context) {
|
|||||||
// perform the summarization API call
|
// perform the summarization API call
|
||||||
try {
|
try {
|
||||||
inApiCall = true;
|
inApiCall = true;
|
||||||
const url = new URL(getApiUrl());
|
const summary = await callExtrasSummarizeAPI(resultingString);
|
||||||
url.pathname = '/api/summarize';
|
|
||||||
|
|
||||||
const apiResult = await doExtrasFetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Bypass-Tunnel-Reminder': 'bypass',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
text: resultingString,
|
|
||||||
params: {},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (apiResult.ok) {
|
|
||||||
const data = await apiResult.json();
|
|
||||||
const summary = data.summary;
|
|
||||||
|
|
||||||
const newContext = getContext();
|
const newContext = getContext();
|
||||||
|
|
||||||
// something changed during summarization request
|
// something changed during summarization request
|
||||||
@ -699,7 +719,6 @@ async function summarizeChatExtras(context) {
|
|||||||
|
|
||||||
setMemoryContext(summary, true);
|
setMemoryContext(summary, true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
@ -708,6 +727,40 @@ async function summarizeChatExtras(context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call the Extras API to summarize the provided text.
|
||||||
|
* @param {string} text Text to summarize
|
||||||
|
* @returns {Promise<string>} Summarized text
|
||||||
|
*/
|
||||||
|
async function callExtrasSummarizeAPI(text) {
|
||||||
|
if (!modules.includes('summarize')) {
|
||||||
|
throw new Error('Summarize module is not enabled in Extras API');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(getApiUrl());
|
||||||
|
url.pathname = '/api/summarize';
|
||||||
|
|
||||||
|
const apiResult = await doExtrasFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Bypass-Tunnel-Reminder': 'bypass',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
params: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (apiResult.ok) {
|
||||||
|
const data = await apiResult.json();
|
||||||
|
const summary = data.summary;
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Extras API call failed');
|
||||||
|
}
|
||||||
|
|
||||||
function onMemoryRestoreClick() {
|
function onMemoryRestoreClick() {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const content = $('#memory_contents').val();
|
const content = $('#memory_contents').val();
|
||||||
@ -865,5 +918,16 @@ jQuery(async function () {
|
|||||||
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);
|
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);
|
||||||
eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent);
|
eventSource.on(event_types.MESSAGE_SWIPED, onChatEvent);
|
||||||
eventSource.on(event_types.CHAT_CHANGED, onChatEvent);
|
eventSource.on(event_types.CHAT_CHANGED, onChatEvent);
|
||||||
registerSlashCommand('summarize', forceSummarizeChat, [], '– forces the summarization of the current chat using the Main API', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: 'summarize',
|
||||||
|
callback: summarizeCallback,
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument('source', 'API to use for summarization', [ARGUMENT_TYPE.STRING], false, false, '', ['main', 'extras']),
|
||||||
|
new SlashCommandNamedArgument('prompt', 'prompt to use for summarization', [ARGUMENT_TYPE.STRING, ARGUMENT_TYPE.VARIABLE_NAME], false, false, ''),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
|
||||||
|
],
|
||||||
|
helpString: 'Summarizes the given text. If no text is provided, the current chat will be summarized. Can specify the source and the prompt to use.',
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
@ -30,7 +30,10 @@
|
|||||||
<span>Ctrl+Enter to execute</span>
|
<span>Ctrl+Enter to execute</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<textarea class="monospace" id="qr--modal-message"></textarea>
|
<div id="qr--modal-messageHolder">
|
||||||
|
<pre id="qr--modal-messageSyntax"><code id="qr--modal-messageSyntaxInner" class="hljs language-stscript"></code></pre>
|
||||||
|
<textarea class="monospace" id="qr--modal-message" spellcheck="false"></textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -94,14 +97,27 @@
|
|||||||
|
|
||||||
|
|
||||||
<h3>Testing</h3>
|
<h3>Testing</h3>
|
||||||
<div id="qr--modal-execute" class="menu_button" title="Execute the quick reply now">
|
<div id="qr--modal-executeButtons">
|
||||||
|
<div id="qr--modal-execute" class="qr--modal-executeButton menu_button" title="Execute the quick reply now">
|
||||||
<i class="fa-solid fa-play"></i>
|
<i class="fa-solid fa-play"></i>
|
||||||
Execute
|
Execute
|
||||||
</div>
|
</div>
|
||||||
|
<div id="qr--modal-pause" class="qr--modal-executeButton menu_button" title="Pause / continue execution">
|
||||||
|
<span class="qr--modal-executeComboIcon">
|
||||||
|
<i class="fa-solid fa-play"></i>
|
||||||
|
<i class="fa-solid fa-pause"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div id="qr--modal-stop" class="qr--modal-executeButton menu_button" title="Abort execution">
|
||||||
|
<i class="fa-solid fa-stop"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="qr--modal-executeProgress"></div>
|
||||||
<label class="checkbox_label">
|
<label class="checkbox_label">
|
||||||
<input type="checkbox" id="qr--modal-executeHide">
|
<input type="checkbox" id="qr--modal-executeHide">
|
||||||
<span> Hide editor while executing</span>
|
<span> Hide editor while executing</span>
|
||||||
</label>
|
</label>
|
||||||
<div id="qr--modal-executeErrors"></div>
|
<div id="qr--modal-executeErrors"></div>
|
||||||
|
<div id="qr--modal-executeResult"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -183,14 +183,16 @@ const init = async () => {
|
|||||||
;
|
;
|
||||||
if (!qr) {
|
if (!qr) {
|
||||||
let [setName, ...qrName] = name.split('.');
|
let [setName, ...qrName] = name.split('.');
|
||||||
name = qrName.join('.');
|
qrName = qrName.join('.');
|
||||||
let qrs = QuickReplySet.get(setName);
|
let qrs = QuickReplySet.get(setName);
|
||||||
if (qrs) {
|
if (qrs) {
|
||||||
qr = qrs.qrList.find(it=>it.label == name);
|
qr = qrs.qrList.find(it=>it.label == qrName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (qr && qr.onExecute) {
|
if (qr && qr.onExecute) {
|
||||||
return await qr.execute(args);
|
return await qr.execute(args, false, true);
|
||||||
|
} else {
|
||||||
|
throw new Error(`No Quick Reply found for "${name}".`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { POPUP_TYPE, Popup } from '../../../popup.js';
|
import { POPUP_TYPE, Popup } from '../../../popup.js';
|
||||||
import { getSortableDelay } from '../../../utils.js';
|
import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
|
||||||
|
import { SlashCommandAbortController } from '../../../slash-commands/SlashCommandAbortController.js';
|
||||||
|
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
|
||||||
|
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
||||||
|
import { debounce, getSortableDelay } from '../../../utils.js';
|
||||||
import { log, warn } from '../index.js';
|
import { log, warn } from '../index.js';
|
||||||
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
|
||||||
import { QuickReplySet } from './QuickReplySet.js';
|
import { QuickReplySet } from './QuickReplySet.js';
|
||||||
@ -47,9 +51,14 @@ export class QuickReply {
|
|||||||
/**@type {Popup}*/ editorPopup;
|
/**@type {Popup}*/ editorPopup;
|
||||||
|
|
||||||
/**@type {HTMLElement}*/ editorExecuteBtn;
|
/**@type {HTMLElement}*/ editorExecuteBtn;
|
||||||
|
/**@type {HTMLElement}*/ editorExecuteBtnPause;
|
||||||
|
/**@type {HTMLElement}*/ editorExecuteBtnStop;
|
||||||
|
/**@type {HTMLElement}*/ editorExecuteProgress;
|
||||||
/**@type {HTMLElement}*/ editorExecuteErrors;
|
/**@type {HTMLElement}*/ editorExecuteErrors;
|
||||||
|
/**@type {HTMLElement}*/ editorExecuteResult;
|
||||||
/**@type {HTMLInputElement}*/ editorExecuteHide;
|
/**@type {HTMLInputElement}*/ editorExecuteHide;
|
||||||
/**@type {Promise}*/ editorExecutePromise;
|
/**@type {Promise}*/ editorExecutePromise;
|
||||||
|
/**@type {SlashCommandAbortController}*/ abortController;
|
||||||
|
|
||||||
|
|
||||||
get hasContext() {
|
get hasContext() {
|
||||||
@ -225,15 +234,43 @@ export class QuickReply {
|
|||||||
const updateWrap = () => {
|
const updateWrap = () => {
|
||||||
if (wrap.checked) {
|
if (wrap.checked) {
|
||||||
message.style.whiteSpace = 'pre-wrap';
|
message.style.whiteSpace = 'pre-wrap';
|
||||||
|
messageSyntaxInner.style.whiteSpace = 'pre-wrap';
|
||||||
} else {
|
} else {
|
||||||
message.style.whiteSpace = 'pre';
|
message.style.whiteSpace = 'pre';
|
||||||
|
messageSyntaxInner.style.whiteSpace = 'pre';
|
||||||
}
|
}
|
||||||
|
updateScrollDebounced();
|
||||||
|
};
|
||||||
|
const updateScroll = (evt) => {
|
||||||
|
let left = message.scrollLeft;
|
||||||
|
let top = message.scrollTop;
|
||||||
|
if (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
left = message.scrollLeft + evt.deltaX;
|
||||||
|
top = message.scrollTop + evt.deltaY;
|
||||||
|
message.scrollTo({
|
||||||
|
behavior: 'instant',
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
messageSyntaxInner.scrollTo({
|
||||||
|
behavior: 'instant',
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const updateScrollDebounced = updateScroll;
|
||||||
|
const updateSyntax = ()=>{
|
||||||
|
messageSyntaxInner.innerHTML = hljs.highlight(`${message.value}${message.value.slice(-1) == '\n' ? ' ' : ''}`, { language:'stscript', ignoreIllegals:true })?.value;
|
||||||
};
|
};
|
||||||
/**@type {HTMLInputElement}*/
|
/**@type {HTMLInputElement}*/
|
||||||
const tabSize = dom.querySelector('#qr--modal-tabSize');
|
const tabSize = dom.querySelector('#qr--modal-tabSize');
|
||||||
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
|
tabSize.value = JSON.parse(localStorage.getItem('qr--tabSize') ?? '4');
|
||||||
const updateTabSize = () => {
|
const updateTabSize = () => {
|
||||||
message.style.tabSize = tabSize.value;
|
message.style.tabSize = tabSize.value;
|
||||||
|
messageSyntaxInner.style.tabSize = tabSize.value;
|
||||||
|
updateScrollDebounced();
|
||||||
};
|
};
|
||||||
tabSize.addEventListener('change', () => {
|
tabSize.addEventListener('change', () => {
|
||||||
localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
|
localStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
|
||||||
@ -247,14 +284,15 @@ export class QuickReply {
|
|||||||
});
|
});
|
||||||
/**@type {HTMLTextAreaElement}*/
|
/**@type {HTMLTextAreaElement}*/
|
||||||
const message = dom.querySelector('#qr--modal-message');
|
const message = dom.querySelector('#qr--modal-message');
|
||||||
updateWrap();
|
|
||||||
updateTabSize();
|
|
||||||
message.value = this.message;
|
message.value = this.message;
|
||||||
message.addEventListener('input', () => {
|
message.addEventListener('input', () => {
|
||||||
|
updateSyntax();
|
||||||
this.updateMessage(message.value);
|
this.updateMessage(message.value);
|
||||||
|
updateScrollDebounced();
|
||||||
});
|
});
|
||||||
|
setSlashCommandAutoComplete(message, true);
|
||||||
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
|
//TODO move tab support for textarea into its own helper(?) and use for both this and .editor_maximize
|
||||||
message.addEventListener('keydown', (evt) => {
|
message.addEventListener('keydown', async(evt) => {
|
||||||
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const start = message.selectionStart;
|
const start = message.selectionStart;
|
||||||
@ -265,12 +303,12 @@ export class QuickReply {
|
|||||||
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;
|
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${message.value.substring(end)}`;
|
||||||
message.selectionStart = start + 1;
|
message.selectionStart = start + 1;
|
||||||
message.selectionEnd = end + count;
|
message.selectionEnd = end + count;
|
||||||
this.updateMessage(message.value);
|
updateSyntax();
|
||||||
} else {
|
} else {
|
||||||
message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
|
message.value = `${message.value.substring(0, start)}\t${message.value.substring(end)}`;
|
||||||
message.selectionStart = start + 1;
|
message.selectionStart = start + 1;
|
||||||
message.selectionEnd = end + 1;
|
message.selectionEnd = end + 1;
|
||||||
this.updateMessage(message.value);
|
updateSyntax();
|
||||||
}
|
}
|
||||||
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
@ -281,15 +319,37 @@ export class QuickReply {
|
|||||||
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;
|
message.value = `${message.value.substring(0, lineStart)}${message.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${message.value.substring(end)}`;
|
||||||
message.selectionStart = start - 1;
|
message.selectionStart = start - 1;
|
||||||
message.selectionEnd = end - count;
|
message.selectionEnd = end - count;
|
||||||
this.updateMessage(message.value);
|
updateSyntax();
|
||||||
} else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
|
} else if (evt.key == 'Enter' && evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
if (executeShortcut.checked) {
|
if (executeShortcut.checked) {
|
||||||
this.executeFromEditor();
|
const selectionStart = message.selectionStart;
|
||||||
|
const selectionEnd = message.selectionEnd;
|
||||||
|
message.blur();
|
||||||
|
await this.executeFromEditor();
|
||||||
|
if (document.activeElement != message) {
|
||||||
|
message.focus();
|
||||||
|
message.selectionStart = selectionStart;
|
||||||
|
message.selectionEnd = selectionEnd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
message.addEventListener('wheel', (evt)=>{
|
||||||
|
updateScrollDebounced(evt);
|
||||||
|
});
|
||||||
|
message.addEventListener('scroll', (evt)=>{
|
||||||
|
updateScrollDebounced();
|
||||||
|
});
|
||||||
|
message.style.color = 'transparent';
|
||||||
|
message.style.background = 'transparent';
|
||||||
|
message.style.setProperty('text-shadow', 'none', 'important');
|
||||||
|
/**@type {HTMLElement}*/
|
||||||
|
const messageSyntaxInner = dom.querySelector('#qr--modal-messageSyntaxInner');
|
||||||
|
updateSyntax();
|
||||||
|
updateWrap();
|
||||||
|
updateTabSize();
|
||||||
|
|
||||||
// context menu
|
// context menu
|
||||||
/**@type {HTMLTemplateElement}*/
|
/**@type {HTMLTemplateElement}*/
|
||||||
@ -414,9 +474,15 @@ export class QuickReply {
|
|||||||
this.updateContext();
|
this.updateContext();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**@type {HTMLElement}*/
|
||||||
|
const executeProgress = dom.querySelector('#qr--modal-executeProgress');
|
||||||
|
this.editorExecuteProgress = executeProgress;
|
||||||
/**@type {HTMLElement}*/
|
/**@type {HTMLElement}*/
|
||||||
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
|
const executeErrors = dom.querySelector('#qr--modal-executeErrors');
|
||||||
this.editorExecuteErrors = executeErrors;
|
this.editorExecuteErrors = executeErrors;
|
||||||
|
/**@type {HTMLElement}*/
|
||||||
|
const executeResult = dom.querySelector('#qr--modal-executeResult');
|
||||||
|
this.editorExecuteResult = executeResult;
|
||||||
/**@type {HTMLInputElement}*/
|
/**@type {HTMLInputElement}*/
|
||||||
const executeHide = dom.querySelector('#qr--modal-executeHide');
|
const executeHide = dom.querySelector('#qr--modal-executeHide');
|
||||||
this.editorExecuteHide = executeHide;
|
this.editorExecuteHide = executeHide;
|
||||||
@ -426,6 +492,26 @@ export class QuickReply {
|
|||||||
executeBtn.addEventListener('click', async()=>{
|
executeBtn.addEventListener('click', async()=>{
|
||||||
await this.executeFromEditor();
|
await this.executeFromEditor();
|
||||||
});
|
});
|
||||||
|
/**@type {HTMLElement}*/
|
||||||
|
const executeBtnPause = dom.querySelector('#qr--modal-pause');
|
||||||
|
this.editorExecuteBtnPause = executeBtnPause;
|
||||||
|
executeBtnPause.addEventListener('click', async()=>{
|
||||||
|
if (this.abortController) {
|
||||||
|
if (this.abortController.signal.paused) {
|
||||||
|
this.abortController.continue('Continue button clicked');
|
||||||
|
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||||
|
} else {
|
||||||
|
this.abortController.pause('Pause button clicked');
|
||||||
|
this.editorExecuteProgress.classList.add('qr--paused');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/**@type {HTMLElement}*/
|
||||||
|
const executeBtnStop = dom.querySelector('#qr--modal-stop');
|
||||||
|
this.editorExecuteBtnStop = executeBtnStop;
|
||||||
|
executeBtnStop.addEventListener('click', async()=>{
|
||||||
|
this.abortController?.abort('Stop button clicked');
|
||||||
|
});
|
||||||
|
|
||||||
await popupResult;
|
await popupResult;
|
||||||
} else {
|
} else {
|
||||||
@ -436,21 +522,54 @@ export class QuickReply {
|
|||||||
async executeFromEditor() {
|
async executeFromEditor() {
|
||||||
if (this.editorExecutePromise) return;
|
if (this.editorExecutePromise) return;
|
||||||
this.editorExecuteBtn.classList.add('qr--busy');
|
this.editorExecuteBtn.classList.add('qr--busy');
|
||||||
|
this.editorExecuteProgress.style.setProperty('--prog', '0');
|
||||||
|
this.editorExecuteErrors.classList.remove('qr--hasErrors');
|
||||||
|
this.editorExecuteResult.classList.remove('qr--hasResult');
|
||||||
|
this.editorExecuteProgress.classList.remove('qr--error');
|
||||||
|
this.editorExecuteProgress.classList.remove('qr--success');
|
||||||
|
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||||
|
this.editorExecuteProgress.classList.remove('qr--aborted');
|
||||||
this.editorExecuteErrors.innerHTML = '';
|
this.editorExecuteErrors.innerHTML = '';
|
||||||
|
this.editorExecuteResult.innerHTML = '';
|
||||||
if (this.editorExecuteHide.checked) {
|
if (this.editorExecuteHide.checked) {
|
||||||
this.editorPopup.dom.classList.add('qr--hide');
|
this.editorPopup.dom.classList.add('qr--hide');
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.editorExecutePromise = this.execute();
|
this.editorExecutePromise = this.execute({}, true);
|
||||||
await this.editorExecutePromise;
|
const result = await this.editorExecutePromise;
|
||||||
|
if (this.abortController?.signal?.aborted) {
|
||||||
|
this.editorExecuteProgress.classList.add('qr--aborted');
|
||||||
|
} else {
|
||||||
|
this.editorExecuteResult.textContent = result?.toString();
|
||||||
|
this.editorExecuteResult.classList.add('qr--hasResult');
|
||||||
|
this.editorExecuteProgress.classList.add('qr--success');
|
||||||
|
}
|
||||||
|
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
this.editorExecuteErrors.textContent = ex.message;
|
this.editorExecuteErrors.classList.add('qr--hasErrors');
|
||||||
|
this.editorExecuteProgress.classList.add('qr--error');
|
||||||
|
this.editorExecuteProgress.classList.remove('qr--paused');
|
||||||
|
if (ex instanceof SlashCommandParserError) {
|
||||||
|
this.editorExecuteErrors.innerHTML = `
|
||||||
|
<div>${ex.message}</div>
|
||||||
|
<div>Line: ${ex.line} Column: ${ex.column}</div>
|
||||||
|
<pre style="text-align:left;">${ex.hint}</pre>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
this.editorExecuteErrors.innerHTML = `
|
||||||
|
<div>${ex.message}</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.editorExecutePromise = null;
|
this.editorExecutePromise = null;
|
||||||
this.editorExecuteBtn.classList.remove('qr--busy');
|
this.editorExecuteBtn.classList.remove('qr--busy');
|
||||||
this.editorPopup.dom.classList.remove('qr--hide');
|
this.editorPopup.dom.classList.remove('qr--hide');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateEditorProgress(done, total) {
|
||||||
|
this.editorExecuteProgress.style.setProperty('--prog', `${done / total * 100}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -526,12 +645,22 @@ export class QuickReply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async execute(args = {}) {
|
async execute(args = {}, isEditor = false, isRun = false) {
|
||||||
if (this.message?.length > 0 && this.onExecute) {
|
if (this.message?.length > 0 && this.onExecute) {
|
||||||
const message = this.message.replace(/\{\{arg::([^}]+)\}\}/g, (_, key) => {
|
const scope = new SlashCommandScope();
|
||||||
return args[key] ?? '';
|
for (const key of Object.keys(args)) {
|
||||||
|
scope.setMacro(`arg::${key}`, args[key]);
|
||||||
|
}
|
||||||
|
if (isEditor) {
|
||||||
|
this.abortController = new SlashCommandAbortController();
|
||||||
|
}
|
||||||
|
return await this.onExecute(this, {
|
||||||
|
message:this.message,
|
||||||
|
isAutoExecute: args.isAutoExecute ?? false,
|
||||||
|
isEditor,
|
||||||
|
isRun,
|
||||||
|
scope,
|
||||||
});
|
});
|
||||||
return await this.onExecute(this, message, args.isAutoExecute ?? false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { getRequestHeaders, substituteParams } from '../../../../script.js';
|
import { getRequestHeaders, substituteParams } from '../../../../script.js';
|
||||||
import { executeSlashCommands } from '../../../slash-commands.js';
|
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
|
||||||
|
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
||||||
import { debounceAsync, warn } from '../index.js';
|
import { debounceAsync, warn } from '../index.js';
|
||||||
import { QuickReply } from './QuickReply.js';
|
import { QuickReply } from './QuickReply.js';
|
||||||
|
|
||||||
@ -100,15 +101,29 @@ export class QuickReplySet {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {QuickReply} qr
|
*
|
||||||
* @param {String} [message] - optional altered message to be used
|
* @param {QuickReply} qr The QR to execute.
|
||||||
|
* @param {object} options
|
||||||
|
* @param {string} [options.message] (null) altered message to be used
|
||||||
|
* @param {boolean} [options.isAutoExecute] (false) whether the execution is triggered by auto execute
|
||||||
|
* @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
|
||||||
|
* @returns
|
||||||
*/
|
*/
|
||||||
async execute(qr, message = null, isAutoExecute = false) {
|
async executeWithOptions(qr, options = {}) {
|
||||||
|
options = Object.assign({
|
||||||
|
message:null,
|
||||||
|
isAutoExecute:false,
|
||||||
|
isEditor:false,
|
||||||
|
isRun:false,
|
||||||
|
scope:null,
|
||||||
|
}, options);
|
||||||
/**@type {HTMLTextAreaElement}*/
|
/**@type {HTMLTextAreaElement}*/
|
||||||
const ta = document.querySelector('#send_textarea');
|
const ta = document.querySelector('#send_textarea');
|
||||||
const finalMessage = message ?? qr.message;
|
const finalMessage = options.message ?? qr.message;
|
||||||
let input = ta.value;
|
let input = ta.value;
|
||||||
if (!isAutoExecute && this.injectInput && input.length > 0) {
|
if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) {
|
||||||
if (this.placeBeforeInput) {
|
if (this.placeBeforeInput) {
|
||||||
input = `${finalMessage} ${input}`;
|
input = `${finalMessage} ${input}`;
|
||||||
} else {
|
} else {
|
||||||
@ -119,7 +134,24 @@ export class QuickReplySet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input[0] == '/' && !this.disableSend) {
|
if (input[0] == '/' && !this.disableSend) {
|
||||||
const result = await executeSlashCommands(input);
|
let result;
|
||||||
|
if (options.isAutoExecute || options.isRun) {
|
||||||
|
result = await executeSlashCommandsWithOptions(input, {
|
||||||
|
handleParserErrors: true,
|
||||||
|
scope: options.scope,
|
||||||
|
});
|
||||||
|
} else if (options.isEditor) {
|
||||||
|
result = await executeSlashCommandsWithOptions(input, {
|
||||||
|
handleParserErrors: false,
|
||||||
|
scope: options.scope,
|
||||||
|
abortController: qr.abortController,
|
||||||
|
onProgress: (done, total) => qr.updateEditorProgress(done, total),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = await executeSlashCommandsOnChatInput(input, {
|
||||||
|
scope: options.scope,
|
||||||
|
});
|
||||||
|
}
|
||||||
return typeof result === 'object' ? result?.pipe : '';
|
return typeof result === 'object' ? result?.pipe : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,6 +163,18 @@ export class QuickReplySet {
|
|||||||
document.querySelector('#send_but').click();
|
document.querySelector('#send_but').click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @param {QuickReply} qr
|
||||||
|
* @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) {
|
||||||
|
return this.executeWithOptions(qr, {
|
||||||
|
message,
|
||||||
|
isAutoExecute,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -152,7 +196,7 @@ export class QuickReplySet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hookQuickReply(qr) {
|
hookQuickReply(qr) {
|
||||||
qr.onExecute = (_, message, isAutoExecute)=>this.execute(qr, message, isAutoExecute);
|
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
|
||||||
qr.onDelete = ()=>this.removeQuickReply(qr);
|
qr.onDelete = ()=>this.removeQuickReply(qr);
|
||||||
qr.onUpdate = ()=>this.save();
|
qr.onUpdate = ()=>this.save();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { registerSlashCommand } from '../../../slash-commands.js';
|
import { SlashCommand } from '../../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../../slash-commands/SlashCommandArgument.js';
|
||||||
|
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
|
||||||
import { isTrueBoolean } from '../../../utils.js';
|
import { isTrueBoolean } from '../../../utils.js';
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
import { QuickReplyApi } from '../api/QuickReplyApi.js';
|
import { QuickReplyApi } from '../api/QuickReplyApi.js';
|
||||||
@ -17,46 +19,331 @@ export class SlashCommandHandler {
|
|||||||
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
registerSlashCommand('qr', (_, value) => this.executeQuickReplyByIndex(Number(value)), [], '<span class="monospace">(number)</span> – activates the specified Quick Reply', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr',
|
||||||
registerSlashCommand('qrset', ()=>toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'), [], '<strong>DEPRECATED</strong> – The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.', true, true);
|
callback: (_, value) => this.executeQuickReplyByIndex(Number(value)),
|
||||||
registerSlashCommand('qr-set', (args, value)=>this.toggleGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – toggle global QR set', true, true);
|
unnamedArgumentList: [
|
||||||
registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – activate global QR set', true, true);
|
new SlashCommandArgument(
|
||||||
registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '<span class="monospace">(number)</span> – deactivate global QR set', true, true);
|
'number', [ARGUMENT_TYPE.NUMBER], true,
|
||||||
registerSlashCommand('qr-chat-set', (args, value)=>this.toggleChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – toggle chat QR set', true, true);
|
),
|
||||||
registerSlashCommand('qr-chat-set-on', (args, value)=>this.addChatSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> – activate chat QR set', true, true);
|
],
|
||||||
registerSlashCommand('qr-chat-set-off', (_, value)=>this.removeChatSet(value), [], '<span class="monospace">(number)</span> – deactivate chat QR set', true, true);
|
helpString: 'Activates the specified Quick Reply',
|
||||||
registerSlashCommand('qr-set-list', (_, value)=>this.listSets(value ?? 'all'), [], '(all|global|chat) – gets a list of the names of all quick reply sets', true, true);
|
}));
|
||||||
registerSlashCommand('qr-list', (_, value)=>this.listQuickReplies(value), [], '(set name) – gets a list of the names of all quick replies in this quick reply set', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qrset',
|
||||||
|
callback: () => toastr.warning('The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.'),
|
||||||
|
helpString: '<strong>DEPRECATED</strong> – The command /qrset has been deprecated. Use /qr-set, /qr-set-on, and /qr-set-off instead.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set',
|
||||||
|
callback: (args, value) => this.toggleGlobalSet(value, args),
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Toggle global QR set',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-on',
|
||||||
|
callback: (args, value) => this.addGlobalSet(value, args),
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Activate global QR set',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-off',
|
||||||
|
callback: (_, value) => this.removeGlobalSet(value),
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Deactivate global QR set',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set',
|
||||||
|
callback: (args, value) => this.toggleChatSet(value, args),
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'visible', 'set visibility', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Toggle chat QR set',
|
||||||
|
}));
|
||||||
|
|
||||||
const qrArgs = `
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-on',
|
||||||
label - string - text on the button, e.g., label=MyButton
|
callback: (args, value) => this.addChatSet(value, args),
|
||||||
set - string - name of the QR set, e.g., set=PresetName1
|
namedArgumentList: [
|
||||||
hidden - bool - whether the button should be hidden, e.g., hidden=true
|
new SlashCommandNamedArgument(
|
||||||
startup - bool - auto execute on app startup, e.g., startup=true
|
'visible', 'whether the QR set should be visible', [ARGUMENT_TYPE.BOOLEAN], false, false, 'true', ['true', 'false'],
|
||||||
user - bool - auto execute on user message, e.g., user=true
|
),
|
||||||
bot - bool - auto execute on AI message, e.g., bot=true
|
],
|
||||||
load - bool - auto execute on chat load, e.g., load=true
|
unnamedArgumentList: [
|
||||||
group - bool - auto execute on group member selection, e.g., group=true
|
new SlashCommandArgument(
|
||||||
title - string - title / tooltip to be shown on button, e.g., title="My Fancy Button"
|
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||||
`.trim();
|
),
|
||||||
const qrUpdateArgs = `
|
],
|
||||||
newlabel - string - new text for the button, e.g. newlabel=MyRenamedButton
|
helpString: 'Activate chat QR set',
|
||||||
${qrArgs}
|
}));
|
||||||
`.trim();
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-chat-set-off',
|
||||||
registerSlashCommand('qr-create', (args, message)=>this.createQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrArgs}</span> – creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
|
callback: (_, value) => this.removeChatSet(value),
|
||||||
registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `<span class="monospace" style="white-space:pre-line;">[arguments] (message)\n arguments:\n ${qrUpdateArgs}</span> – updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
|
unnamedArgumentList: [
|
||||||
registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '<span class="monospace">set=string [label]</span> – deletes Quick Reply', true, true);
|
new SlashCommandArgument(
|
||||||
registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '<span class="monospace">set=string label=string [chain=false] (preset name)</span> – add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
|
'QR set name', [ARGUMENT_TYPE.STRING], true,
|
||||||
registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '<span class="monospace">set=string label=string (preset name)</span> – remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
|
),
|
||||||
registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '<span class="monospace">set=string (label)</span> – remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
|
],
|
||||||
const presetArgs = `
|
helpString: 'Deactivate chat QR set',
|
||||||
nosend - bool - disable send / insert in user input (invalid for slash commands)
|
}));
|
||||||
before - bool - place QR before user input
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-list',
|
||||||
inject - bool - inject user input automatically (if disabled use {{input}})
|
callback: (_, value) => this.listSets(value ?? 'all'),
|
||||||
`.trim();
|
returns: 'list of QR sets',
|
||||||
registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> – create a new preset (overrides existing ones), example: <tt>/qr-set-add MyNewPreset</tt>`, true, true);
|
namedArgumentList: [],
|
||||||
registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `<span class="monospace" style="white-space:pre-line;">[arguments] (name)\n arguments:\n ${presetArgs}</span> – update an existing preset, example: <tt>/qr-set-update enabled=false MyPreset</tt>`, true, true);
|
unnamedArgumentList: [
|
||||||
registerSlashCommand('qr-set-delete', (args, name)=>this.deleteSet(name), ['qr-presetdelete'], `<span class="monospace" style="white-space:pre-line;">(name)\n arguments:\n ${presetArgs}</span> – delete an existing preset, example: <tt>/qr-set-delete MyPreset</tt>`, true, true);
|
new SlashCommandArgument(
|
||||||
|
'set type', [ARGUMENT_TYPE.STRING], false, false, null, ['all', 'global', 'chat'],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Gets a list of the names of all quick reply sets.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-list',
|
||||||
|
callback: (_, value) => this.listQuickReplies(value),
|
||||||
|
returns: 'list of QRs',
|
||||||
|
namedArgumentList: [],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'set name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Gets a list of the names of all quick replies in this quick reply set.',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const qrArgs = [
|
||||||
|
new SlashCommandNamedArgument('label', 'text on the button, e.g., label=MyButton', [ARGUMENT_TYPE.STRING]),
|
||||||
|
new SlashCommandNamedArgument('set', 'name of the QR set, e.g., set=PresetName1', [ARGUMENT_TYPE.STRING]),
|
||||||
|
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'),
|
||||||
|
new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||||
|
new SlashCommandNamedArgument('bot', 'auto execute on AI message, e.g., bot=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||||
|
new SlashCommandNamedArgument('load', 'auto execute on chat load, e.g., load=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||||
|
new SlashCommandNamedArgument('group', 'auto execute on group member selection, e.g., group=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||||
|
new SlashCommandNamedArgument('title', 'title / tooltip to be shown on button, e.g., title="My Fancy Button"', [ARGUMENT_TYPE.STRING], false),
|
||||||
|
];
|
||||||
|
const qrUpdateArgs = [
|
||||||
|
new SlashCommandNamedArgument('newlabel', 'new text for the button', [ARGUMENT_TYPE.STRING], false),
|
||||||
|
];
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-create',
|
||||||
|
callback: (args, message) => this.createQuickReply(args, message),
|
||||||
|
namedArgumentList: qrArgs,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'command', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>Creates a new Quick Reply.</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/qr-create set=MyPreset label=MyButton /echo 123</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-update',
|
||||||
|
callback: (args, message) => this.updateQuickReply(args, message),
|
||||||
|
returns: 'updated quick reply',
|
||||||
|
namedArgumentList: [...qrUpdateArgs, ...qrArgs],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Updates Quick Reply.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-delete',
|
||||||
|
callback: (args, name) => this.deleteQuickReply(args, name),
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'set', 'Quick Reply set', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'label', 'Quick Reply label', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Deletes a Quick Reply from the specified set. If no label is provided, the entire set is deleted.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextadd',
|
||||||
|
callback: (args, name) => this.createContextItem(args, name),
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'set', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'label', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'chain', 'boolean', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'preset name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Add context menu preset to a QR.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextdel',
|
||||||
|
callback: (args, name) => this.deleteContextItem(args, name),
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'set', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'label', 'string', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'preset name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Remove context menu preset from a QR.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-contextclear',
|
||||||
|
callback: (args, label) => this.clearContextMenu(args, label),
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'set', 'context menu preset name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'label', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Remove all context menu presets from a QR.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/qr-contextclear set=MyPreset MyButton</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const presetArgs = [
|
||||||
|
new SlashCommandNamedArgument('nosend', 'disable send / insert in user input (invalid for slash commands)', [ARGUMENT_TYPE.BOOLEAN], false),
|
||||||
|
new SlashCommandNamedArgument('before', 'place QR before user input', [ARGUMENT_TYPE.BOOLEAN], false),
|
||||||
|
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),
|
||||||
|
aliases: ['qr-presetadd'],
|
||||||
|
namedArgumentList: presetArgs,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Create a new preset (overrides existing ones).
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/qr-set-add MyNewPreset</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-update',
|
||||||
|
callback: (args, name) => this.updateSet(name, args),
|
||||||
|
aliases: ['qr-presetupdate'],
|
||||||
|
namedArgumentList: presetArgs,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Update an existing preset.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<pre><code>/qr-set-update enabled=false MyPreset</code></pre>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'qr-set-delete',
|
||||||
|
callback: (args, name) => this.deleteSet(name),
|
||||||
|
aliases: ['qr-presetdelete'],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument('name', [ARGUMENT_TYPE.STRING], true),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Delete an existing preset.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<pre><code>/qr-set-delete MyPreset</code></pre>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -238,11 +238,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--labels {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@ -268,6 +270,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -283,17 +286,167 @@
|
|||||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input {
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > .qr--modal-editorSettings > .checkbox_label > input {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-message {
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
display: grid;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute {
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-messageSyntax > #qr--modal-messageSyntaxInner {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
caret-color: white;
|
||||||
|
mix-blend-mode: difference;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar,
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder > #qr--modal-message::-webkit-scrollbar-thumb {
|
||||||
|
visibility: hidden;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-message,
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder #qr--modal-messageSyntaxInner {
|
||||||
|
padding: 0.75em;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
line-height: 1.2;
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton {
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
}
|
}
|
||||||
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-execute.qr--busy {
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--modal-executeButton .qr--modal-executeComboIcon {
|
||||||
opacity: 0.5;
|
display: flex;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
|
||||||
|
transition: 200ms;
|
||||||
|
filter: grayscale(0);
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute.qr--busy {
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-execute {
|
||||||
|
border-color: #51a351;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause,
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-pause,
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons .qr--busy ~ #qr--modal-stop {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
filter: grayscale(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-pause {
|
||||||
|
border-color: #92befc;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeButtons #qr--modal-stop {
|
||||||
|
border-color: #d78872;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress {
|
||||||
|
--prog: 0;
|
||||||
|
--progColor: #92befc;
|
||||||
|
--progFlashColor: #d78872;
|
||||||
|
--progSuccessColor: #51a351;
|
||||||
|
--progErrorColor: #bd362f;
|
||||||
|
--progAbortedColor: #d78872;
|
||||||
|
height: 0.5em;
|
||||||
|
background-color: var(--black50a);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress:after {
|
||||||
|
content: '';
|
||||||
|
background-color: var(--progColor);
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
right: calc(100% - var(--prog) * 1%);
|
||||||
|
transition: 200ms;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--paused:after {
|
||||||
|
animation-name: qr--progressPulse;
|
||||||
|
animation-duration: 1500ms;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--aborted:after {
|
||||||
|
background-color: var(--progAbortedColor);
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--success:after {
|
||||||
|
background-color: var(--progSuccessColor);
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeProgress.qr--error:after {
|
||||||
|
background-color: var(--progErrorColor);
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors {
|
||||||
|
display: none;
|
||||||
|
text-align: left;
|
||||||
|
font-size: smaller;
|
||||||
|
background-color: #bd362f;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5em;
|
||||||
|
overflow: auto;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeErrors.qr--hasErrors {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult {
|
||||||
|
display: none;
|
||||||
|
text-align: left;
|
||||||
|
font-size: smaller;
|
||||||
|
background-color: #51a351;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5em;
|
||||||
|
overflow: auto;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult.qr--hasResult {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor #qr--modal-executeResult:before {
|
||||||
|
content: 'Result: ';
|
||||||
|
}
|
||||||
|
@keyframes qr--progressPulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
background-color: var(--progColor);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: var(--progFlashColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.shadow_popup.qr--hide {
|
.shadow_popup.qr--hide {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
|
@ -264,11 +264,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
> #qr--main {
|
> #qr--main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
> .qr--labels {
|
> .qr--labels {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -293,6 +295,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
> .qr--modal-editorSettings {
|
> .qr--modal-editorSettings {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -307,22 +310,167 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> #qr--modal-message {
|
> #qr--modal-messageHolder {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
display: grid;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
> #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 {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
caret-color: white;
|
||||||
|
mix-blend-mode: difference;
|
||||||
|
&::-webkit-scrollbar, &::-webkit-scrollbar-thumb {
|
||||||
|
visibility: hidden;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--modal-message, #qr--modal-messageSyntaxInner {
|
||||||
|
padding: 0.75em;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
resize: none;
|
||||||
|
line-height: 1.2;
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#qr--modal-execute {
|
#qr--modal-executeButtons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
.qr--modal-executeButton {
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
|
padding: 0.5em 0.75em;
|
||||||
|
.qr--modal-executeComboIcon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--modal-execute {
|
||||||
|
transition: 200ms;
|
||||||
|
filter: grayscale(0);
|
||||||
&.qr--busy {
|
&.qr--busy {
|
||||||
opacity: 0.5;
|
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--modal-execute {
|
||||||
|
border-color: rgb(81, 163, 81);
|
||||||
|
}
|
||||||
|
#qr--modal-pause, #qr--modal-stop {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: grayscale(1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.qr--busy {
|
||||||
|
~ #qr--modal-pause, ~ #qr--modal-stop {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
filter: grayscale(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--modal-pause {
|
||||||
|
border-color: rgb(146, 190, 252);
|
||||||
|
}
|
||||||
|
#qr--modal-stop {
|
||||||
|
border-color: rgb(215, 136, 114);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--modal-executeProgress {
|
||||||
|
--prog: 0;
|
||||||
|
--progColor: rgb(146, 190, 252);
|
||||||
|
--progFlashColor: rgb(215, 136, 114);
|
||||||
|
--progSuccessColor: rgb(81, 163, 81);
|
||||||
|
--progErrorColor: rgb(189, 54, 47);
|
||||||
|
--progAbortedColor: rgb(215, 136, 114);
|
||||||
|
height: 0.5em;
|
||||||
|
background-color: var(--black50a);
|
||||||
|
position: relative;
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
background-color: var(--progColor);
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
right: calc(100% - var(--prog) * 1%);
|
||||||
|
transition: 200ms;
|
||||||
|
}
|
||||||
|
&.qr--paused:after {
|
||||||
|
animation-name: qr--progressPulse;
|
||||||
|
animation-duration: 1500ms;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
&.qr--aborted:after {
|
||||||
|
background-color: var(--progAbortedColor);
|
||||||
|
}
|
||||||
|
&.qr--success:after {
|
||||||
|
background-color: var(--progSuccessColor);
|
||||||
|
}
|
||||||
|
&.qr--error:after {
|
||||||
|
background-color: var(--progErrorColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#qr--modal-executeErrors {
|
||||||
|
display: none;
|
||||||
|
&.qr--hasErrors {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
text-align: left;
|
||||||
|
font-size: smaller;
|
||||||
|
background-color: rgb(189, 54, 47);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5em;
|
||||||
|
overflow: auto;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
#qr--modal-executeResult {
|
||||||
|
display: none;
|
||||||
|
&.qr--hasResult {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
&:before { content: 'Result: '; }
|
||||||
|
text-align: left;
|
||||||
|
font-size: smaller;
|
||||||
|
background-color: rgb(81, 163, 81);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5em;
|
||||||
|
overflow: auto;
|
||||||
|
min-width: 100%;
|
||||||
|
width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@keyframes qr--progressPulse {
|
||||||
|
0%, 100% {
|
||||||
|
background-color: var(--progColor);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-color: var(--progFlashColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
|
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
|
||||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
|
import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
|
||||||
import { resolveVariable } from '../../variables.js';
|
import { resolveVariable } from '../../variables.js';
|
||||||
import { regex_placement, runRegexScript } from './engine.js';
|
import { regex_placement, runRegexScript } from './engine.js';
|
||||||
@ -353,5 +355,20 @@ jQuery(async () => {
|
|||||||
await loadRegexScripts();
|
await loadRegexScripts();
|
||||||
$('#saved_regex_scripts').sortable('enable');
|
$('#saved_regex_scripts').sortable('enable');
|
||||||
|
|
||||||
registerSlashCommand('regex', runRegexCallback, [], '(name=scriptName [input]) – runs a Regex extension script by name on the provided string. The script must be enabled.', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'regex',
|
||||||
|
callback: runRegexCallback,
|
||||||
|
returns: 'replaced text',
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'name', 'script name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'input', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,9 @@ import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.j
|
|||||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||||
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
|
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
|
||||||
import { getMultimodalCaption } from '../shared.js';
|
import { getMultimodalCaption } from '../shared.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
import { resolveVariable } from '../../variables.js';
|
import { resolveVariable } from '../../variables.js';
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
@ -3055,8 +3057,43 @@ $('#sd_dropdown [id]').on('click', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
jQuery(async () => {
|
jQuery(async () => {
|
||||||
registerSlashCommand('imagine', generatePicture, ['sd', 'img', 'image'], helpString, true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine',
|
||||||
registerSlashCommand('imagine-comfy-workflow', changeComfyWorkflow, ['icw'], '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <tt>/imagine-comfy-workflow MyWorkflow</tt>');
|
callback: generatePicture,
|
||||||
|
aliases: ['sd', 'img', 'image'],
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'quiet', 'whether to post the generated image to chat', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false', ['false', 'true'],
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'negative', 'negative prompt prefix', [ARGUMENT_TYPE.STRING], false, false, '',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'argument', [ARGUMENT_TYPE.STRING], false, false, null, Object.values(triggerWords).flat(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Requests to generate an image and posts it to chat (unless quiet=true argument is specified). Supported arguments: <code>${Object.values(triggerWords).flat().join(', ')}</code>.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Anything else would trigger a "free mode" to make generate whatever you prompted. Example: <code>/imagine apple tree</code> would generate a picture of an apple tree. Returns a link to the generated image.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine-comfy-workflow',
|
||||||
|
callback: changeComfyWorkflow,
|
||||||
|
aliases: ['icw'],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'workflowName', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: '(workflowName) - change the workflow to be used for image generation with ComfyUI, e.g. <pre><code>/imagine-comfy-workflow MyWorkflow</code></pre>',
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
const template = await renderExtensionTemplateAsync('stable-diffusion', 'settings', defaultSettings);
|
const template = await renderExtensionTemplateAsync('stable-diffusion', 'settings', defaultSettings);
|
||||||
$('#extensions_settings').append(template);
|
$('#extensions_settings').append(template);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { callPopup, main_api } from '../../../script.js';
|
import { callPopup, main_api } from '../../../script.js';
|
||||||
import { getContext } from '../../extensions.js';
|
import { getContext } from '../../extensions.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
|
import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js';
|
||||||
import { resetScrollHeight, debounce } from '../../utils.js';
|
import { resetScrollHeight, debounce } from '../../utils.js';
|
||||||
import { debounce_timeout } from '../../constants.js';
|
import { debounce_timeout } from '../../constants.js';
|
||||||
@ -132,5 +133,10 @@ jQuery(() => {
|
|||||||
</div>`;
|
</div>`;
|
||||||
$('#extensionsMenu').prepend(buttonHtml);
|
$('#extensionsMenu').prepend(buttonHtml);
|
||||||
$('#token_counter').on('click', doTokenCounter);
|
$('#token_counter').on('click', doTokenCounter);
|
||||||
registerSlashCommand('count', doCount, [], '– counts the number of tokens in the current chat', true, false);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'count',
|
||||||
|
callback: doCount,
|
||||||
|
returns: 'number of tokens',
|
||||||
|
helpString: 'Counts the number of tokens in the current chat.',
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -8,11 +8,13 @@ import { CoquiTtsProvider } from './coqui.js';
|
|||||||
import { SystemTtsProvider } from './system.js';
|
import { SystemTtsProvider } from './system.js';
|
||||||
import { NovelTtsProvider } from './novel.js';
|
import { NovelTtsProvider } from './novel.js';
|
||||||
import { power_user } from '../../power-user.js';
|
import { power_user } from '../../power-user.js';
|
||||||
import { registerSlashCommand } from '../../slash-commands.js';
|
|
||||||
import { OpenAITtsProvider } from './openai.js';
|
import { OpenAITtsProvider } from './openai.js';
|
||||||
import { XTTSTtsProvider } from './xtts.js';
|
import { XTTSTtsProvider } from './xtts.js';
|
||||||
import { AllTalkTtsProvider } from './alltalk.js';
|
import { AllTalkTtsProvider } from './alltalk.js';
|
||||||
import { SpeechT5TtsProvider } from './speecht5.js';
|
import { SpeechT5TtsProvider } from './speecht5.js';
|
||||||
|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||||
export { talkingAnimation };
|
export { talkingAnimation };
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 1000;
|
const UPDATE_INTERVAL = 1000;
|
||||||
@ -1063,6 +1065,36 @@ $(document).ready(function () {
|
|||||||
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
|
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
|
||||||
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
|
eventSource.on(event_types.MESSAGE_SENT, onMessageEvent);
|
||||||
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent);
|
eventSource.on(event_types.MESSAGE_RECEIVED, onMessageEvent);
|
||||||
registerSlashCommand('speak', onNarrateText, ['narrate', 'tts'], '<span class="monospace">(text)</span> – narrate any text using currently selected character\'s voice. Use voice="Character Name" argument to set other voice from the voice map, example: <tt>/speak voice="Donald Duck" Quack!</tt>', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'speak',
|
||||||
|
callback: onNarrateText,
|
||||||
|
aliases: ['narrate', 'tts'],
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'voice', 'character voice name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'text', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Narrate any text using currently selected character's voice.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Use <code>voice="Character Name"</code> argument to set other voice from the voice map.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/speak voice="Donald Duck" Quack!</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
document.body.appendChild(audioElement);
|
document.body.appendChild(audioElement);
|
||||||
});
|
});
|
||||||
|
@ -804,7 +804,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
|
|||||||
const bias = getBiasStrings(userInput, type);
|
const bias = getBiasStrings(userInput, type);
|
||||||
await sendMessageAsUser(userInput, bias.messageBias);
|
await sendMessageAsUser(userInput, bias.messageBias);
|
||||||
await saveChatConditional();
|
await saveChatConditional();
|
||||||
$('#send_textarea').val('').trigger('input');
|
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// now the real generation begins: cycle through every activated character
|
// now the real generation begins: cycle through every activated character
|
||||||
|
@ -32,7 +32,6 @@ import {
|
|||||||
this_chid,
|
this_chid,
|
||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
import { selected_group } from './group-chats.js';
|
import { selected_group } from './group-chats.js';
|
||||||
import { registerSlashCommand } from './slash-commands.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
chatCompletionDefaultPrompts,
|
chatCompletionDefaultPrompts,
|
||||||
@ -66,6 +65,9 @@ import {
|
|||||||
} from './instruct-mode.js';
|
} from './instruct-mode.js';
|
||||||
import { isMobile } from './RossAscends-mods.js';
|
import { isMobile } from './RossAscends-mods.js';
|
||||||
import { saveLogprobsForActiveMessage } from './logprobs.js';
|
import { saveLogprobsForActiveMessage } from './logprobs.js';
|
||||||
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
openai_messages_count,
|
openai_messages_count,
|
||||||
@ -4383,7 +4385,18 @@ function runProxyCallback(_, value) {
|
|||||||
return foundName;
|
return foundName;
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSlashCommand('proxy', runProxyCallback, [], '<span class="monospace">(name)</span> – sets a proxy preset by name');
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'proxy',
|
||||||
|
callback: runProxyCallback,
|
||||||
|
returns: 'current proxy',
|
||||||
|
namedArgumentList: [],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Sets a proxy preset by name.',
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
$(document).ready(async function () {
|
$(document).ready(async function () {
|
||||||
$('#test_api_button').on('click', testApiConnection);
|
$('#test_api_button').on('click', testApiConnection);
|
||||||
|
@ -35,7 +35,6 @@ import {
|
|||||||
selectInstructPreset,
|
selectInstructPreset,
|
||||||
} from './instruct-mode.js';
|
} from './instruct-mode.js';
|
||||||
|
|
||||||
import { registerSlashCommand } from './slash-commands.js';
|
|
||||||
import { getTagsList, tag_map, tags } from './tags.js';
|
import { getTagsList, tag_map, tags } from './tags.js';
|
||||||
import { tokenizers } from './tokenizers.js';
|
import { tokenizers } from './tokenizers.js';
|
||||||
import { BIAS_CACHE } from './logit-bias.js';
|
import { BIAS_CACHE } from './logit-bias.js';
|
||||||
@ -43,6 +42,10 @@ import { renderTemplateAsync } from './templates.js';
|
|||||||
|
|
||||||
import { countOccurrences, debounce, delay, download, getFileText, isOdd, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
import { countOccurrences, debounce, delay, download, getFileText, isOdd, onlyUnique, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
|
||||||
import { FILTER_TYPES } from './filters.js';
|
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 } from './slash-commands/SlashCommandArgument.js';
|
||||||
|
import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
loadPowerUserSettings,
|
loadPowerUserSettings,
|
||||||
@ -253,6 +256,23 @@ let power_user = {
|
|||||||
zoomed_avatar_magnification: false,
|
zoomed_avatar_magnification: false,
|
||||||
show_tag_filters: false,
|
show_tag_filters: false,
|
||||||
aux_field: 'character_version',
|
aux_field: 'character_version',
|
||||||
|
stscript: {
|
||||||
|
matching: 'fuzzy',
|
||||||
|
autocomplete: {
|
||||||
|
style: 'theme',
|
||||||
|
font: {
|
||||||
|
scale: 0.8,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
left: AUTOCOMPLETE_WIDTH.CHAT,
|
||||||
|
right: AUTOCOMPLETE_WIDTH.CHAT,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parser: {
|
||||||
|
/**@type {Object.<PARSER_FLAG,boolean>} */
|
||||||
|
flags: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
restore_user_input: true,
|
restore_user_input: true,
|
||||||
reduced_motion: false,
|
reduced_motion: false,
|
||||||
compact_input_area: true,
|
compact_input_area: true,
|
||||||
@ -1430,11 +1450,32 @@ function getExampleMessagesBehavior() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadPowerUserSettings(settings, data) {
|
function loadPowerUserSettings(settings, data) {
|
||||||
|
const defaultStscript = JSON.parse(JSON.stringify(power_user.stscript));
|
||||||
// Load from settings.json
|
// Load from settings.json
|
||||||
if (settings.power_user !== undefined) {
|
if (settings.power_user !== undefined) {
|
||||||
Object.assign(power_user, settings.power_user);
|
Object.assign(power_user, settings.power_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (power_user.stscript === undefined) {
|
||||||
|
power_user.stscript = defaultStscript;
|
||||||
|
} else {
|
||||||
|
if (power_user.stscript.autocomplete === undefined) {
|
||||||
|
power_user.stscript.autocomplete = defaultStscript.autocomplete;
|
||||||
|
} else {
|
||||||
|
if (power_user.stscript.autocomplete.width === undefined) {
|
||||||
|
power_user.stscript.autocomplete.width = defaultStscript.autocomplete.width;
|
||||||
|
}
|
||||||
|
if (power_user.stscript.autocomplete.font === undefined) {
|
||||||
|
power_user.stscript.autocomplete.font = defaultStscript.autocomplete.font;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (power_user.stscript.parser === undefined) {
|
||||||
|
power_user.stscript.parser = defaultStscript.parser;
|
||||||
|
} else if (power_user.stscript.parser.flags === undefined) {
|
||||||
|
power_user.stscript.parser.flags = defaultStscript.parser.flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (data.themes !== undefined) {
|
if (data.themes !== undefined) {
|
||||||
themes = data.themes;
|
themes = data.themes;
|
||||||
}
|
}
|
||||||
@ -1576,6 +1617,20 @@ function loadPowerUserSettings(settings, data) {
|
|||||||
$('#chat_width_slider').val(power_user.chat_width);
|
$('#chat_width_slider').val(power_user.chat_width);
|
||||||
$('#token_padding').val(power_user.token_padding);
|
$('#token_padding').val(power_user.token_padding);
|
||||||
$('#aux_field').val(power_user.aux_field);
|
$('#aux_field').val(power_user.aux_field);
|
||||||
|
|
||||||
|
$('#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_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);
|
||||||
|
$('#stscript_autocomplete_font_scale_counter').val(power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale);
|
||||||
|
document.body.style.setProperty('--ac-font-scale', power_user.stscript.autocomplete.font.scale ?? defaultStscript.autocomplete.font.scale.toString());
|
||||||
|
$('#stscript_autocomplete_width_left').val(power_user.stscript.autocomplete.width.left ?? AUTOCOMPLETE_WIDTH.CHAT);
|
||||||
|
document.querySelector('#stscript_autocomplete_width_left').dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
|
$('#stscript_autocomplete_width_right').val(power_user.stscript.autocomplete.width.right ?? AUTOCOMPLETE_WIDTH.CHAT);
|
||||||
|
document.querySelector('#stscript_autocomplete_width_right').dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
|
|
||||||
$('#restore_user_input').prop('checked', power_user.restore_user_input);
|
$('#restore_user_input').prop('checked', power_user.restore_user_input);
|
||||||
|
|
||||||
$('#chat_truncation').val(power_user.chat_truncation);
|
$('#chat_truncation').val(power_user.chat_truncation);
|
||||||
@ -3591,6 +3646,64 @@ $(document).ready(() => {
|
|||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('#stscript_matching').on('change', function () {
|
||||||
|
const value = $(this).find(':selected').val();
|
||||||
|
power_user.stscript.matching = String(value);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stscript_autocomplete_style').on('change', function () {
|
||||||
|
const value = $(this).find(':selected').val();
|
||||||
|
power_user.stscript.autocomplete_style = String(value);
|
||||||
|
document.body.setAttribute('data-stscript-style', power_user.stscript.autocomplete_style);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stscript_autocomplete_font_scale').on('input', function () {
|
||||||
|
const value = $(this).val();
|
||||||
|
$('#stscript_autocomplete_font_scale_counter').val(value);
|
||||||
|
power_user.stscript.autocomplete.font.scale = Number(value);
|
||||||
|
document.body.style.setProperty('--ac-font-scale', value.toString());
|
||||||
|
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$('#stscript_autocomplete_font_scale_counter').on('input', function () {
|
||||||
|
const value = $(this).val();
|
||||||
|
$('#stscript_autocomplete_font_scale').val(value);
|
||||||
|
power_user.stscript.autocomplete.font.scale = Number(value);
|
||||||
|
document.body.style.setProperty('--ac-font-scale', value.toString());
|
||||||
|
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stscript_autocomplete_width_left').on('input', function () {
|
||||||
|
const value = $(this).val();
|
||||||
|
power_user.stscript.autocomplete.width.left = Number(value);
|
||||||
|
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
|
||||||
|
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stscript_autocomplete_width_right').on('input', function () {
|
||||||
|
const value = $(this).val();
|
||||||
|
power_user.stscript.autocomplete.width.right = Number(value);
|
||||||
|
/**@type {HTMLElement}*/(this.closest('.doubleRangeInputContainer')).style.setProperty('--value', value.toString());
|
||||||
|
window.dispatchEvent(new Event('resize', { bubbles:true }));
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stscript_parser_flag_strict_escaping').on('click', function () {
|
||||||
|
const value = $(this).prop('checked');
|
||||||
|
power_user.stscript.parser.flags[PARSER_FLAG.STRICT_ESCAPING] = value;
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stscript_parser_flag_replace_getvar').on('click', function () {
|
||||||
|
const value = $(this).prop('checked');
|
||||||
|
power_user.stscript.parser.flags[PARSER_FLAG.REPLACE_GETVAR] = value;
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
|
||||||
$('#restore_user_input').on('input', function () {
|
$('#restore_user_input').on('input', function () {
|
||||||
power_user.restore_user_input = !!$(this).prop('checked');
|
power_user.restore_user_input = !!$(this).prop('checked');
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
@ -3669,13 +3782,84 @@ $(document).ready(() => {
|
|||||||
browser_has_focus = false;
|
browser_has_focus = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
registerSlashCommand('vn', toggleWaifu, [], '– swaps Visual Novel Mode On/Off', false, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'vn',
|
||||||
registerSlashCommand('newchat', doNewChat, [], '– start a new chat with current character', true, true);
|
callback: toggleWaifu,
|
||||||
registerSlashCommand('random', doRandomChat, [], '<span class="monospace">(optional tag name)</span> – start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.', true, true);
|
helpString: 'Swaps Visual Novel Mode On/Off',
|
||||||
registerSlashCommand('delmode', doDelMode, ['del'], '<span class="monospace">(optional number)</span> – enter message deletion mode, and auto-deletes last N messages if numeric argument is provided', true, true);
|
}));
|
||||||
registerSlashCommand('cut', doMesCut, [], '<span class="monospace">(number or range)</span> – cuts the specified message or continuous chunk from the chat, e.g. <tt>/cut 0-10</tt>. Ranges are inclusive! Returns the text of cut messages separated by a newline.', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'newchat',
|
||||||
registerSlashCommand('resetpanels', doResetPanels, ['resetui'], '– resets UI panels to original state.', true, true);
|
callback: doNewChat,
|
||||||
registerSlashCommand('bgcol', setAvgBG, [], '– WIP test of auto-bg avg coloring', true, true);
|
helpString: 'Start a new chat with the current character',
|
||||||
registerSlashCommand('theme', setThemeCallback, [], '<span class="monospace">(name)</span> – sets a UI theme by name', true, true);
|
}));
|
||||||
registerSlashCommand('movingui', setmovingUIPreset, [], '<span class="monospace">(name)</span> – activates a movingUI preset by name', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'random',
|
||||||
|
callback: doRandomChat,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'optional tag name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Start a new chat with a random character. If an argument is provided, only considers characters that have the specified tag.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'delmode',
|
||||||
|
callback: doDelMode,
|
||||||
|
aliases: ['del'],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'optional number', [ARGUMENT_TYPE.NUMBER], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'Enter message deletion mode, and auto-deletes last N messages if numeric argument is provided.',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'cut',
|
||||||
|
callback: doMesCut,
|
||||||
|
returns: 'the text of cut messages separated by a newline',
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'number or range', [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.RANGE], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Cuts the specified message or continuous chunk from the chat.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Ranges are inclusive!
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/cut 0-10</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
aliases: [],
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'resetpanels',
|
||||||
|
callback: doResetPanels,
|
||||||
|
helpString: 'resets UI panels to original state',
|
||||||
|
aliases: ['resetui'],
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'bgcol',
|
||||||
|
callback: setAvgBG,
|
||||||
|
helpString: '– WIP test of auto-bg avg coloring',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'theme',
|
||||||
|
callback: setThemeCallback,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'sets a UI theme by name',
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'movingui',
|
||||||
|
callback: setmovingUIPreset,
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: 'activates a movingUI preset by name',
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,9 @@ import { groups, selected_group } from './group-chats.js';
|
|||||||
import { instruct_presets } from './instruct-mode.js';
|
import { instruct_presets } from './instruct-mode.js';
|
||||||
import { kai_settings } from './kai-settings.js';
|
import { kai_settings } from './kai-settings.js';
|
||||||
import { context_presets, getContextSettings, power_user } from './power-user.js';
|
import { context_presets, getContextSettings, power_user } from './power-user.js';
|
||||||
import { registerSlashCommand } from './slash-commands.js';
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
|
||||||
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
import {
|
import {
|
||||||
textgenerationwebui_preset_names,
|
textgenerationwebui_preset_names,
|
||||||
textgenerationwebui_presets,
|
textgenerationwebui_presets,
|
||||||
@ -472,7 +474,33 @@ async function waitForConnection() {
|
|||||||
export async function initPresetManager() {
|
export async function initPresetManager() {
|
||||||
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
|
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
|
||||||
registerPresetManagers();
|
registerPresetManagers();
|
||||||
registerSlashCommand('preset', presetCommandCallback, [], '<span class="monospace">(name)</span> – sets a preset by name for the current API. Gets the current preset if no name is provided', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'preset',
|
||||||
|
callback: presetCommandCallback,
|
||||||
|
returns: 'current preset',
|
||||||
|
namedArgumentList: [],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Sets a preset by name for the current API. Gets the current preset if no name is provided.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/preset myPreset</code></pre>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<pre><code>/preset</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
$(document).on('click', '[data-preset-manager-update]', async function () {
|
$(document).on('click', '[data-preset-manager-update]', async function () {
|
||||||
const apiId = $(this).data('preset-manager-update');
|
const apiId = $(this).data('preset-manager-update');
|
||||||
|
File diff suppressed because it is too large
Load Diff
359
public/scripts/slash-commands/SlashCommand.js
Normal file
359
public/scripts/slash-commands/SlashCommand.js
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommand {
|
||||||
|
/**
|
||||||
|
* Creates a SlashCommand from a properties object.
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} [props.name]
|
||||||
|
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||||
|
* @param {string} [props.helpString]
|
||||||
|
* @param {boolean} [props.splitUnnamedArgument]
|
||||||
|
* @param {string[]} [props.aliases]
|
||||||
|
* @param {string} [props.returns]
|
||||||
|
* @param {SlashCommandNamedArgument[]} [props.namedArgumentList]
|
||||||
|
* @param {SlashCommandArgument[]} [props.unnamedArgumentList]
|
||||||
|
*/
|
||||||
|
static fromProps(props) {
|
||||||
|
const instance = Object.assign(new this(), props);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**@type {string}*/ name;
|
||||||
|
/**@type {(namedArguments:Object<string, string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||||
|
/**@type {string}*/ helpString;
|
||||||
|
/**@type {boolean}*/ splitUnnamedArgument = false;
|
||||||
|
/**@type {string[]}*/ aliases = [];
|
||||||
|
/**@type {string}*/ returns;
|
||||||
|
/**@type {SlashCommandNamedArgument[]}*/ namedArgumentList = [];
|
||||||
|
/**@type {SlashCommandArgument[]}*/ unnamedArgumentList = [];
|
||||||
|
|
||||||
|
/**@type {Object.<string, HTMLElement>}*/ helpCache = {};
|
||||||
|
/**@type {Object.<string, DocumentFragment>}*/ helpDetailsCache = {};
|
||||||
|
|
||||||
|
renderHelpItem(key = null) {
|
||||||
|
key = key ?? this.name;
|
||||||
|
if (!this.helpCache[key]) {
|
||||||
|
const typeIcon = '[/]';
|
||||||
|
const li = document.createElement('li'); {
|
||||||
|
li.classList.add('item');
|
||||||
|
const type = document.createElement('span'); {
|
||||||
|
type.classList.add('type');
|
||||||
|
type.classList.add('monospace');
|
||||||
|
type.textContent = typeIcon;
|
||||||
|
li.append(type);
|
||||||
|
}
|
||||||
|
const specs = document.createElement('span'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('span'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = '/';
|
||||||
|
key.split('').forEach(char=>{
|
||||||
|
const span = document.createElement('span'); {
|
||||||
|
span.textContent = char;
|
||||||
|
name.append(span);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
const body = document.createElement('span'); {
|
||||||
|
body.classList.add('body');
|
||||||
|
const args = document.createElement('span'); {
|
||||||
|
args.classList.add('arguments');
|
||||||
|
for (const arg of this.namedArgumentList) {
|
||||||
|
const argItem = document.createElement('span'); {
|
||||||
|
argItem.classList.add('argument');
|
||||||
|
argItem.classList.add('namedArgument');
|
||||||
|
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||||
|
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||||
|
const name = document.createElement('span'); {
|
||||||
|
name.classList.add('argument-name');
|
||||||
|
name.textContent = arg.name;
|
||||||
|
argItem.append(name);
|
||||||
|
}
|
||||||
|
if (arg.enumList.length > 0) {
|
||||||
|
const enums = document.createElement('span'); {
|
||||||
|
enums.classList.add('argument-enums');
|
||||||
|
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');
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.append(argItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const arg of this.unnamedArgumentList) {
|
||||||
|
const argItem = document.createElement('span'); {
|
||||||
|
argItem.classList.add('argument');
|
||||||
|
argItem.classList.add('unnamedArgument');
|
||||||
|
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');
|
||||||
|
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');
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args.append(argItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.append(args);
|
||||||
|
}
|
||||||
|
const returns = document.createElement('span'); {
|
||||||
|
returns.classList.add('returns');
|
||||||
|
returns.textContent = this.returns ?? 'void';
|
||||||
|
body.append(returns);
|
||||||
|
}
|
||||||
|
specs.append(body);
|
||||||
|
}
|
||||||
|
li.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
const content = document.createElement('span'); {
|
||||||
|
content.classList.add('helpContent');
|
||||||
|
content.innerHTML = this.helpString;
|
||||||
|
const text = content.textContent;
|
||||||
|
content.innerHTML = '';
|
||||||
|
content.textContent = text;
|
||||||
|
help.append(content);
|
||||||
|
}
|
||||||
|
li.append(help);
|
||||||
|
}
|
||||||
|
if (this.aliases.length > 0) {
|
||||||
|
const aliases = document.createElement('span'); {
|
||||||
|
aliases.classList.add('aliases');
|
||||||
|
aliases.append(' (alias: ');
|
||||||
|
for (const aliasName of this.aliases) {
|
||||||
|
const alias = document.createElement('span'); {
|
||||||
|
alias.classList.add('monospace');
|
||||||
|
alias.textContent = `/${aliasName}`;
|
||||||
|
aliases.append(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
aliases.append(')');
|
||||||
|
// li.append(aliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.helpCache[key] = li;
|
||||||
|
}
|
||||||
|
return /**@type {HTMLElement}*/(this.helpCache[key].cloneNode(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHelpDetails(key = null) {
|
||||||
|
key = key ?? this.name;
|
||||||
|
if (!this.helpDetailsCache[key]) {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const cmd = this;
|
||||||
|
const namedArguments = cmd.namedArgumentList ?? [];
|
||||||
|
const unnamedArguments = cmd.unnamedArgumentList ?? [];
|
||||||
|
const returnType = cmd.returns ?? 'void';
|
||||||
|
const helpString = cmd.helpString ?? 'NO DETAILS';
|
||||||
|
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 body = document.createElement('div'); {
|
||||||
|
body.classList.add('body');
|
||||||
|
const args = document.createElement('ul'); {
|
||||||
|
args.classList.add('arguments');
|
||||||
|
for (const arg of namedArguments) {
|
||||||
|
const listItem = document.createElement('li'); {
|
||||||
|
listItem.classList.add('argumentItem');
|
||||||
|
const argSpec = document.createElement('div'); {
|
||||||
|
argSpec.classList.add('argumentSpec');
|
||||||
|
const argItem = document.createElement('div'); {
|
||||||
|
argItem.classList.add('argument');
|
||||||
|
argItem.classList.add('namedArgument');
|
||||||
|
argItem.title = `${arg.isRequired ? '' : 'optional '}named argument`;
|
||||||
|
if (!arg.isRequired || (arg.defaultValue ?? false)) argItem.classList.add('optional');
|
||||||
|
if (arg.acceptsMultiple) argItem.classList.add('multiple');
|
||||||
|
const name = document.createElement('span'); {
|
||||||
|
name.classList.add('argument-name');
|
||||||
|
name.title = `${argItem.title} - name`;
|
||||||
|
name.textContent = arg.name;
|
||||||
|
argItem.append(name);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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(argSpec);
|
||||||
|
}
|
||||||
|
const desc = document.createElement('div'); {
|
||||||
|
desc.classList.add('argument-description');
|
||||||
|
desc.innerHTML = arg.description;
|
||||||
|
listItem.append(desc);
|
||||||
|
}
|
||||||
|
args.append(listItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listItem.append(argItem);
|
||||||
|
}
|
||||||
|
const desc = document.createElement('div'); {
|
||||||
|
desc.classList.add('argument-description');
|
||||||
|
desc.innerHTML = arg.description;
|
||||||
|
listItem.append(desc);
|
||||||
|
}
|
||||||
|
args.append(listItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body.append(args);
|
||||||
|
}
|
||||||
|
const returns = document.createElement('span'); {
|
||||||
|
returns.classList.add('returns');
|
||||||
|
returns.title = [null, undefined, 'void'].includes(returnType) ? 'command does not return anything' : 'return value';
|
||||||
|
returns.textContent = returnType ?? 'void';
|
||||||
|
body.append(returns);
|
||||||
|
}
|
||||||
|
specs.append(body);
|
||||||
|
}
|
||||||
|
frag.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
help.innerHTML = helpString;
|
||||||
|
for (const code of help.querySelectorAll('pre > code')) {
|
||||||
|
code.classList.add('language-stscript');
|
||||||
|
hljs.highlightElement(code);
|
||||||
|
}
|
||||||
|
frag.append(help);
|
||||||
|
}
|
||||||
|
if (aliasList.length > 0) {
|
||||||
|
const aliases = document.createElement('span'); {
|
||||||
|
aliases.classList.add('aliases');
|
||||||
|
for (const aliasName of aliasList) {
|
||||||
|
const alias = document.createElement('span'); {
|
||||||
|
alias.classList.add('alias');
|
||||||
|
alias.textContent = `/${aliasName}`;
|
||||||
|
aliases.append(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
frag.append(aliases);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.helpDetailsCache[key] = frag;
|
||||||
|
}
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
frag.append(this.helpDetailsCache[key].cloneNode(true));
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
27
public/scripts/slash-commands/SlashCommandAbortController.js
Normal file
27
public/scripts/slash-commands/SlashCommandAbortController.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export class SlashCommandAbortController {
|
||||||
|
/**@type {SlashCommandAbortSignal}*/ signal;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.signal = new SlashCommandAbortSignal();
|
||||||
|
}
|
||||||
|
abort(reason = 'No reason.') {
|
||||||
|
this.signal.aborted = true;
|
||||||
|
this.signal.reason = reason;
|
||||||
|
}
|
||||||
|
pause(reason = 'No reason.') {
|
||||||
|
this.signal.paused = true;
|
||||||
|
this.signal.reason = reason;
|
||||||
|
}
|
||||||
|
continue(reason = 'No reason.') {
|
||||||
|
this.signal.paused = false;
|
||||||
|
this.signal.reason = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SlashCommandAbortSignal {
|
||||||
|
/**@type {boolean}*/ paused = false;
|
||||||
|
/**@type {boolean}*/ aborted = false;
|
||||||
|
/**@type {string}*/ reason = null;
|
||||||
|
|
||||||
|
}
|
121
public/scripts/slash-commands/SlashCommandArgument.js
Normal file
121
public/scripts/slash-commands/SlashCommandArgument.js
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**@readonly*/
|
||||||
|
/**@enum {string}*/
|
||||||
|
export const ARGUMENT_TYPE = {
|
||||||
|
'STRING': 'string',
|
||||||
|
'NUMBER': 'number',
|
||||||
|
'RANGE': 'range',
|
||||||
|
'BOOLEAN': 'bool',
|
||||||
|
'VARIABLE_NAME': 'varname',
|
||||||
|
'CLOSURE': 'closure',
|
||||||
|
'SUBCOMMAND': 'subcommand',
|
||||||
|
'LIST': 'list',
|
||||||
|
'DICTIONARY': 'dictionary',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandArgument {
|
||||||
|
/**
|
||||||
|
* Creates an unnamed argument from a poperties object.
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} props.description description of the argument
|
||||||
|
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
|
||||||
|
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
|
||||||
|
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
|
||||||
|
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
|
||||||
|
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
|
||||||
|
*/
|
||||||
|
static fromProps(props) {
|
||||||
|
return new SlashCommandArgument(
|
||||||
|
props.description,
|
||||||
|
props.typeList ?? [ARGUMENT_TYPE.STRING],
|
||||||
|
props.isRequired ?? false,
|
||||||
|
props.acceptsMultiple ?? false,
|
||||||
|
props.defaultValue ?? null,
|
||||||
|
props.enumList ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**@type {string}*/ description;
|
||||||
|
/**@type {ARGUMENT_TYPE[]}*/ typeList = [];
|
||||||
|
/**@type {boolean}*/ isRequired = false;
|
||||||
|
/**@type {boolean}*/ acceptsMultiple = false;
|
||||||
|
/**@type {string|SlashCommandClosure}*/ defaultValue;
|
||||||
|
/**@type {SlashCommandEnumValue[]}*/ enumList = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} description
|
||||||
|
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
|
||||||
|
* @param {string|SlashCommandClosure} defaultValue
|
||||||
|
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
|
||||||
|
*/
|
||||||
|
constructor(description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = []) {
|
||||||
|
this.description = description;
|
||||||
|
this.typeList = types ? Array.isArray(types) ? types : [types] : [];
|
||||||
|
this.isRequired = isRequired ?? false;
|
||||||
|
this.acceptsMultiple = acceptsMultiple ?? false;
|
||||||
|
this.defaultValue = defaultValue;
|
||||||
|
this.enumList = (enums ? Array.isArray(enums) ? enums : [enums] : []).map(it=>{
|
||||||
|
if (it instanceof SlashCommandEnumValue) return it;
|
||||||
|
return new SlashCommandEnumValue(it);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandNamedArgument extends SlashCommandArgument {
|
||||||
|
/**
|
||||||
|
* Creates an unnamed argument from a poperties object.
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} props.name the argument's name
|
||||||
|
* @param {string[]} [props.aliasList] list of aliases
|
||||||
|
* @param {string} props.description description of the argument
|
||||||
|
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} props.typeList default: ARGUMENT_TYPE.STRING - list of accepted types (from ARGUMENT_TYPE)
|
||||||
|
* @param {boolean} [props.isRequired] default: false - whether the argument is required (false = optional argument)
|
||||||
|
* @param {boolean} [props.acceptsMultiple] default: false - whether argument accepts multiple values
|
||||||
|
* @param {string|SlashCommandClosure} [props.defaultValue] default value if no value is provided
|
||||||
|
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} [props.enumList] list of accepted values
|
||||||
|
*/
|
||||||
|
static fromProps(props) {
|
||||||
|
return new SlashCommandNamedArgument(
|
||||||
|
props.name,
|
||||||
|
props.description,
|
||||||
|
props.typeList ?? [ARGUMENT_TYPE.STRING],
|
||||||
|
props.isRequired ?? false,
|
||||||
|
props.acceptsMultiple ?? false,
|
||||||
|
props.defaultValue ?? null,
|
||||||
|
props.enumList ?? [],
|
||||||
|
props.aliasList ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**@type {string}*/ name;
|
||||||
|
/**@type {string[]}*/ aliasList = [];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} description
|
||||||
|
* @param {ARGUMENT_TYPE|ARGUMENT_TYPE[]} types
|
||||||
|
* @param {string|SlashCommandClosure} defaultValue
|
||||||
|
* @param {string|SlashCommandEnumValue|(string|SlashCommandEnumValue)[]} enums
|
||||||
|
*/
|
||||||
|
constructor(name, description, types, isRequired = false, acceptsMultiple = false, defaultValue = null, enums = [], aliases = []) {
|
||||||
|
super(description, types, isRequired, acceptsMultiple, defaultValue, enums);
|
||||||
|
this.name = name;
|
||||||
|
this.aliasList = aliases ? Array.isArray(aliases) ? aliases : [aliases] : [];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,165 @@
|
|||||||
|
import { AutoCompleteNameResult } from '../autocomplete/AutoCompleteNameResult.js';
|
||||||
|
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||||
|
import { AutoCompleteSecondaryNameResult } from '../autocomplete/AutoCompleteSecondaryNameResult.js';
|
||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
import { SlashCommandCommandAutoCompleteOption } from './SlashCommandCommandAutoCompleteOption.js';
|
||||||
|
import { SlashCommandEnumAutoCompleteOption } from './SlashCommandEnumAutoCompleteOption.js';
|
||||||
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||||
|
import { SlashCommandNamedArgumentAutoCompleteOption } from './SlashCommandNamedArgumentAutoCompleteOption.js';
|
||||||
|
|
||||||
|
export class SlashCommandAutoCompleteNameResult extends AutoCompleteNameResult {
|
||||||
|
/**@type {SlashCommandExecutor}*/ executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SlashCommandExecutor} executor
|
||||||
|
* @param {Object.<string,SlashCommand>} commands
|
||||||
|
*/
|
||||||
|
constructor(executor, commands) {
|
||||||
|
super(
|
||||||
|
executor.name,
|
||||||
|
executor.start,
|
||||||
|
Object
|
||||||
|
.keys(commands)
|
||||||
|
.map(key=>new SlashCommandCommandAutoCompleteOption(commands[key], key))
|
||||||
|
,
|
||||||
|
false,
|
||||||
|
()=>`No matching slash commands for "/${this.name}"`,
|
||||||
|
()=>'No slash commands found!',
|
||||||
|
);
|
||||||
|
this.executor = executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSecondaryNameAt(text, index, isSelect) {
|
||||||
|
const namedResult = this.getNamedArgumentAt(text, index, isSelect);
|
||||||
|
if (!namedResult || namedResult.optionList.length == 0 || !namedResult.isRequired) {
|
||||||
|
const unnamedResult = this.getUnnamedArgumentAt(text, index, isSelect);
|
||||||
|
if (!namedResult) return unnamedResult;
|
||||||
|
if (namedResult && unnamedResult) {
|
||||||
|
const combinedResult = new AutoCompleteSecondaryNameResult(
|
||||||
|
namedResult.name,
|
||||||
|
namedResult.start,
|
||||||
|
[...namedResult.optionList, ...unnamedResult.optionList],
|
||||||
|
);
|
||||||
|
combinedResult.isRequired = namedResult.isRequired || unnamedResult.isRequired;
|
||||||
|
return combinedResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return namedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNamedArgumentAt(text, index, isSelect) {
|
||||||
|
const notProvidedNamedArguments = this.executor.command.namedArgumentList.filter(arg=>!this.executor.namedArgumentList.find(it=>it.name == arg.name));
|
||||||
|
let name;
|
||||||
|
let value;
|
||||||
|
let start;
|
||||||
|
let cmdArg;
|
||||||
|
let argAssign;
|
||||||
|
const unamedArgLength = this.executor.endUnnamedArgs - this.executor.startUnnamedArgs;
|
||||||
|
const namedArgsFollowedBySpace = text[this.executor.endNamedArgs] == ' ';
|
||||||
|
if (this.executor.startNamedArgs <= index && this.executor.endNamedArgs + (namedArgsFollowedBySpace ? 1 : 0) >= index) {
|
||||||
|
// cursor is somewhere within the named arguments (including final space)
|
||||||
|
argAssign = this.executor.namedArgumentList.find(it=>it.start <= index && it.end >= index);
|
||||||
|
if (argAssign) {
|
||||||
|
const [argName, ...v] = text.slice(argAssign.start, index).split(/(?<==)/);
|
||||||
|
name = argName;
|
||||||
|
value = v.join('');
|
||||||
|
start = argAssign.start;
|
||||||
|
cmdArg = this.executor.command.namedArgumentList.find(it=>[it.name, `${it.name}=`].includes(argAssign.name));
|
||||||
|
if (cmdArg) notProvidedNamedArguments.push(cmdArg);
|
||||||
|
} else {
|
||||||
|
name = '';
|
||||||
|
start = index;
|
||||||
|
}
|
||||||
|
} 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 <= 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;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.includes('=') && cmdArg) {
|
||||||
|
// if cursor is already behind "=" check for enums
|
||||||
|
/**@type {SlashCommandNamedArgument} */
|
||||||
|
if (cmdArg && cmdArg.enumList?.length) {
|
||||||
|
if (isSelect && cmdArg.enumList.includes(value) && argAssign && argAssign.end == index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const result = new AutoCompleteSecondaryNameResult(
|
||||||
|
value,
|
||||||
|
start + name.length,
|
||||||
|
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
result.isRequired = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notProvidedNamedArguments.length > 0) {
|
||||||
|
const result = new AutoCompleteSecondaryNameResult(
|
||||||
|
name,
|
||||||
|
start,
|
||||||
|
notProvidedNamedArguments.map(it=>new SlashCommandNamedArgumentAutoCompleteOption(it, this.executor.command)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
result.isRequired = notProvidedNamedArguments.find(it=>it.isRequired) != null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUnnamedArgumentAt(text, index, isSelect) {
|
||||||
|
const lastArgIsBlank = this.executor.unnamedArgumentList.slice(-1)[0]?.value == '';
|
||||||
|
const notProvidedArguments = this.executor.command.unnamedArgumentList.slice(this.executor.unnamedArgumentList.length - (lastArgIsBlank ? 1 : 0));
|
||||||
|
let value;
|
||||||
|
let start;
|
||||||
|
let cmdArg;
|
||||||
|
let argAssign;
|
||||||
|
if (this.executor.startUnnamedArgs <= index && this.executor.endUnnamedArgs + 1 >= index) {
|
||||||
|
// cursor is somwehere in the unnamed args
|
||||||
|
const idx = this.executor.unnamedArgumentList.findIndex(it=>it.start <= index && it.end >= index);
|
||||||
|
if (idx > -1) {
|
||||||
|
argAssign = this.executor.unnamedArgumentList[idx];
|
||||||
|
cmdArg = this.executor.command.unnamedArgumentList[idx];
|
||||||
|
if (cmdArg && cmdArg.enumList.length > 0) {
|
||||||
|
value = argAssign.value.toString().slice(0, index - argAssign.start);
|
||||||
|
start = argAssign.start;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = '';
|
||||||
|
start = index;
|
||||||
|
cmdArg = notProvidedArguments[0];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cmdArg == null || cmdArg.enumList.length == 0) return null;
|
||||||
|
|
||||||
|
const result = new AutoCompleteSecondaryNameResult(
|
||||||
|
value,
|
||||||
|
start,
|
||||||
|
cmdArg.enumList.map(it=>new SlashCommandEnumAutoCompleteOption(it)),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
const isCompleteValue = cmdArg.enumList.find(it=>it.value == value);
|
||||||
|
const isSelectedValue = isSelect && isCompleteValue;
|
||||||
|
result.isRequired = cmdArg.isRequired && !isSelectedValue && !isCompleteValue;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
148
public/scripts/slash-commands/SlashCommandBrowser.js
Normal file
148
public/scripts/slash-commands/SlashCommandBrowser.js
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { escapeRegex } from '../utils.js';
|
||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
import { SlashCommandParser } from './SlashCommandParser.js';
|
||||||
|
|
||||||
|
export class SlashCommandBrowser {
|
||||||
|
/**@type {SlashCommand[]}*/ cmdList;
|
||||||
|
/**@type {HTMLElement}*/ dom;
|
||||||
|
/**@type {HTMLElement}*/ search;
|
||||||
|
/**@type {HTMLElement}*/ details;
|
||||||
|
/**@type {Object.<string,HTMLElement>}*/ itemMap = {};
|
||||||
|
/**@type {MutationObserver}*/ mo;
|
||||||
|
|
||||||
|
renderInto(parent) {
|
||||||
|
if (!this.dom) {
|
||||||
|
const queryRegex = /(?:(?:^|\s+)([^\s"][^\s]*?)(?:\s+|$))|(?:(?:^|\s+)"(.*?)(?:"|$)(?:\s+|$))/;
|
||||||
|
const root = document.createElement('div'); {
|
||||||
|
this.dom = root;
|
||||||
|
const search = document.createElement('div'); {
|
||||||
|
search.classList.add('search');
|
||||||
|
const lbl = document.createElement('label'); {
|
||||||
|
lbl.classList.add('searchLabel');
|
||||||
|
lbl.textContent = 'Search: ';
|
||||||
|
const inp = document.createElement('input'); {
|
||||||
|
this.search = inp;
|
||||||
|
inp.classList.add('searchInput');
|
||||||
|
inp.classList.add('text_pole');
|
||||||
|
inp.type = 'search';
|
||||||
|
inp.placeholder = 'Search slash commands - use quotes to search "literal" instead of fuzzy';
|
||||||
|
inp.addEventListener('input', ()=>{
|
||||||
|
this.details?.remove();
|
||||||
|
this.details = null;
|
||||||
|
let query = inp.value.trim();
|
||||||
|
if (query.slice(-1) == '"' && !/(?:^|\s+)"/.test(query)) {
|
||||||
|
query = `"${query}`;
|
||||||
|
}
|
||||||
|
let fuzzyList = [];
|
||||||
|
let quotedList = [];
|
||||||
|
while (query.length > 0) {
|
||||||
|
const match = queryRegex.exec(query);
|
||||||
|
if (!match) break;
|
||||||
|
if (match[1] !== undefined) {
|
||||||
|
fuzzyList.push(new RegExp(`^(.*?)${match[1].split('').map(char=>`(${escapeRegex(char)})`).join('(.*?)')}(.*?)$`, 'i'));
|
||||||
|
} else if (match[2] !== undefined) {
|
||||||
|
quotedList.push(match[2]);
|
||||||
|
}
|
||||||
|
query = query.slice(match.index + match[0].length);
|
||||||
|
}
|
||||||
|
for (const cmd of this.cmdList) {
|
||||||
|
const targets = [
|
||||||
|
cmd.name,
|
||||||
|
...cmd.namedArgumentList.map(it=>it.name),
|
||||||
|
...cmd.namedArgumentList.map(it=>it.description),
|
||||||
|
...cmd.namedArgumentList.map(it=>it.enumList.map(e=>e.value)).flat(),
|
||||||
|
...cmd.namedArgumentList.map(it=>it.typeList).flat(),
|
||||||
|
...cmd.unnamedArgumentList.map(it=>it.description),
|
||||||
|
...cmd.unnamedArgumentList.map(it=>it.enumList.map(e=>e.value)).flat(),
|
||||||
|
...cmd.unnamedArgumentList.map(it=>it.typeList).flat(),
|
||||||
|
...cmd.aliases,
|
||||||
|
cmd.helpString,
|
||||||
|
];
|
||||||
|
const find = ()=>targets.find(t=>(fuzzyList.find(f=>f.test(t)) ?? quotedList.find(q=>t.includes(q))) !== undefined) !== undefined;
|
||||||
|
if (fuzzyList.length + quotedList.length == 0 || find()) {
|
||||||
|
this.itemMap[cmd.name].classList.remove('isFiltered');
|
||||||
|
} else {
|
||||||
|
this.itemMap[cmd.name].classList.add('isFiltered');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
lbl.append(inp);
|
||||||
|
}
|
||||||
|
search.append(lbl);
|
||||||
|
}
|
||||||
|
root.append(search);
|
||||||
|
}
|
||||||
|
const container = document.createElement('div'); {
|
||||||
|
container.classList.add('commandContainer');
|
||||||
|
const list = document.createElement('div'); {
|
||||||
|
list.classList.add('autoComplete');
|
||||||
|
this.cmdList = Object
|
||||||
|
.keys(SlashCommandParser.commands)
|
||||||
|
.filter(key => SlashCommandParser.commands[key].name == key) // exclude aliases
|
||||||
|
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
|
||||||
|
.map(key => SlashCommandParser.commands[key])
|
||||||
|
;
|
||||||
|
for (const cmd of this.cmdList) {
|
||||||
|
const item = cmd.renderHelpItem();
|
||||||
|
this.itemMap[cmd.name] = item;
|
||||||
|
let details;
|
||||||
|
item.addEventListener('click', ()=>{
|
||||||
|
if (!details) {
|
||||||
|
details = document.createElement('div'); {
|
||||||
|
details.classList.add('autoComplete-detailsWrap');
|
||||||
|
const inner = document.createElement('div'); {
|
||||||
|
inner.classList.add('autoComplete-details');
|
||||||
|
inner.append(cmd.renderHelpDetails());
|
||||||
|
details.append(inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.details != details) {
|
||||||
|
Array.from(list.querySelectorAll('.selected')).forEach(it=>it.classList.remove('selected'));
|
||||||
|
item.classList.add('selected');
|
||||||
|
this.details?.remove();
|
||||||
|
container.append(details);
|
||||||
|
this.details = details;
|
||||||
|
const pRect = list.getBoundingClientRect();
|
||||||
|
const rect = item.children[0].getBoundingClientRect();
|
||||||
|
details.style.setProperty('--targetOffset', rect.top - pRect.top);
|
||||||
|
} else {
|
||||||
|
item.classList.remove('selected');
|
||||||
|
details.remove();
|
||||||
|
this.details = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list.append(item);
|
||||||
|
}
|
||||||
|
container.append(list);
|
||||||
|
}
|
||||||
|
root.append(container);
|
||||||
|
}
|
||||||
|
root.classList.add('slashCommandBrowser');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parent.append(this.dom);
|
||||||
|
|
||||||
|
this.mo = new MutationObserver(muts=>{
|
||||||
|
if (muts.find(mut=>Array.from(mut.removedNodes).find(it=>it == this.dom || it.contains(this.dom)))) {
|
||||||
|
this.mo.disconnect();
|
||||||
|
window.removeEventListener('keydown', boundHandler);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.mo.observe(document.querySelector('#chat'), { childList:true, subtree:true });
|
||||||
|
const boundHandler = this.handleKeyDown.bind(this);
|
||||||
|
window.addEventListener('keydown', boundHandler);
|
||||||
|
return this.dom;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown(evt) {
|
||||||
|
if (!evt.shiftKey && !evt.altKey && evt.ctrlKey && evt.key.toLowerCase() == 'f') {
|
||||||
|
if (!this.dom.closest('body')) return;
|
||||||
|
if (this.dom.closest('.mes') && !this.dom.closest('.last_mes')) return;
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
evt.stopImmediatePropagation();
|
||||||
|
this.search.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
261
public/scripts/slash-commands/SlashCommandClosure.js
Normal file
261
public/scripts/slash-commands/SlashCommandClosure.js
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
import { substituteParams } from '../../script.js';
|
||||||
|
import { delay, escapeRegex } from '../utils.js';
|
||||||
|
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||||
|
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
|
||||||
|
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
|
||||||
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||||
|
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||||
|
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||||
|
|
||||||
|
export class SlashCommandClosure {
|
||||||
|
/**@type {SlashCommandScope}*/ scope;
|
||||||
|
/**@type {boolean}*/ executeNow = false;
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {SlashCommandNamedArgumentAssignment[]}*/ argumentList = [];
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
|
||||||
|
/**@type {SlashCommandExecutor[]}*/ executorList = [];
|
||||||
|
/**@type {SlashCommandAbortController}*/ abortController;
|
||||||
|
/**@type {(done:number, total:number)=>void}*/ onProgress;
|
||||||
|
|
||||||
|
/**@type {number}*/
|
||||||
|
get commandCount() {
|
||||||
|
return this.executorList.map(executor=>executor.commandCount).reduce((sum,cur)=>sum + cur, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parent) {
|
||||||
|
this.scope = new SlashCommandScope(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return '[Closure]';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} text
|
||||||
|
* @param {SlashCommandScope} scope
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
substituteParams(text, scope = null) {
|
||||||
|
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})}})`);
|
||||||
|
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;
|
||||||
|
if (replacer instanceof SlashCommandClosure) {
|
||||||
|
isList = true;
|
||||||
|
if (match.index > 0) {
|
||||||
|
listValues.push(before);
|
||||||
|
}
|
||||||
|
listValues.push(replacer);
|
||||||
|
if (match.index + match[0].length + 1 < remaining.length) {
|
||||||
|
const rest = this.substituteParams(after, scope);
|
||||||
|
listValues.push(...(Array.isArray(rest) ? rest : [rest]));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
done = `${done}${before}${replacer}`;
|
||||||
|
remaining = after;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isList) {
|
||||||
|
text = `${done}${substituteParams(remaining)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isList) {
|
||||||
|
if (listValues.length > 1) return listValues;
|
||||||
|
return listValues[0];
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCopy() {
|
||||||
|
const closure = new SlashCommandClosure();
|
||||||
|
closure.scope = this.scope.getCopy();
|
||||||
|
closure.executeNow = this.executeNow;
|
||||||
|
closure.argumentList = this.argumentList;
|
||||||
|
closure.providedArgumentList = this.providedArgumentList;
|
||||||
|
closure.executorList = this.executorList;
|
||||||
|
closure.abortController = this.abortController;
|
||||||
|
closure.onProgress = this.onProgress;
|
||||||
|
return closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns Promise<SlashCommandClosureResult>
|
||||||
|
*/
|
||||||
|
async execute() {
|
||||||
|
const closure = this.getCopy();
|
||||||
|
return await closure.executeDirect();
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeDirect() {
|
||||||
|
// closure arguments
|
||||||
|
for (const arg of this.argumentList) {
|
||||||
|
let v = arg.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);
|
||||||
|
}
|
||||||
|
// unescape value
|
||||||
|
if (typeof v == 'string') {
|
||||||
|
v = v
|
||||||
|
?.replace(/\\\{/g, '{')
|
||||||
|
?.replace(/\\\}/g, '}')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
this.scope.letVariable(arg.name, v);
|
||||||
|
}
|
||||||
|
for (const arg of this.providedArgumentList) {
|
||||||
|
let v = arg.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, this.scope.parent);
|
||||||
|
}
|
||||||
|
// unescape value
|
||||||
|
if (typeof v == 'string') {
|
||||||
|
v = v
|
||||||
|
?.replace(/\\\{/g, '{')
|
||||||
|
?.replace(/\\\}/g, '}')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
this.scope.setVariable(arg.name, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
let done = 0;
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
let args = {
|
||||||
|
_scope: this.scope,
|
||||||
|
_parserFlags: executor.parserFlags,
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} 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, '}')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
let abortResult = await this.testAbortController();
|
||||||
|
if (abortResult) {
|
||||||
|
return abortResult;
|
||||||
|
}
|
||||||
|
executor.onProgress = (subDone, subTotal)=>this.onProgress?.(done + subDone, this.commandCount);
|
||||||
|
this.scope.pipe = await executor.command.callback(args, value ?? '');
|
||||||
|
done += executor.commandCount;
|
||||||
|
this.onProgress?.(done, this.commandCount);
|
||||||
|
abortResult = await this.testAbortController();
|
||||||
|
if (abortResult) {
|
||||||
|
return abortResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**@type {SlashCommandClosureResult} */
|
||||||
|
const result = Object.assign(new SlashCommandClosureResult(), { pipe: this.scope.pipe });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async testPaused() {
|
||||||
|
while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) {
|
||||||
|
await delay(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async testAbortController() {
|
||||||
|
await this.testPaused();
|
||||||
|
if (this.abortController?.signal?.aborted) {
|
||||||
|
const result = new SlashCommandClosureResult();
|
||||||
|
result.isAborted = true;
|
||||||
|
result.abortReason = this.abortController.signal.reason.toString();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||||
|
|
||||||
|
export class SlashCommandClosureExecutor {
|
||||||
|
/**@type {String}*/ name = '';
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {SlashCommandNamedArgumentAssignment[]}*/ providedArgumentList = [];
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
export class SlashCommandClosureResult {
|
||||||
|
/**@type {boolean}*/ interrupt = false;
|
||||||
|
/**@type {string}*/ pipe;
|
||||||
|
/**@type {boolean}*/ isAborted = false;
|
||||||
|
/**@type {string}*/ abortReason;
|
||||||
|
/**@type {boolean}*/ isError = false;
|
||||||
|
/**@type {string}*/ errorMessage;
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||||
|
|
||||||
|
export class SlashCommandCommandAutoCompleteOption extends AutoCompleteOption {
|
||||||
|
/**@type {SlashCommand}*/ command;
|
||||||
|
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SlashCommand} command
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
constructor(command, name) {
|
||||||
|
super(name);
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderItem() {
|
||||||
|
let li;
|
||||||
|
li = this.command.renderHelpItem(this.name);
|
||||||
|
li.setAttribute('data-name', this.name);
|
||||||
|
li.setAttribute('data-option-type', 'command');
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderDetails() {
|
||||||
|
return this.command.renderHelpDetails(this.name);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||||
|
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||||
|
|
||||||
|
export class SlashCommandEnumAutoCompleteOption extends AutoCompleteOption {
|
||||||
|
/**@type {SlashCommandEnumValue}*/ enumValue;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SlashCommandEnumValue} enumValue
|
||||||
|
*/
|
||||||
|
constructor(enumValue) {
|
||||||
|
super(enumValue.value, '◊');
|
||||||
|
this.enumValue = enumValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderItem() {
|
||||||
|
let li;
|
||||||
|
li = this.makeItem(this.name, '◊', true, [], [], null, this.enumValue.description);
|
||||||
|
li.setAttribute('data-name', this.name);
|
||||||
|
li.setAttribute('data-option-type', 'enum');
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderDetails() {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const specs = document.createElement('div'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('div'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = this.name;
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
frag.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
help.textContent = this.enumValue.description;
|
||||||
|
frag.append(help);
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
13
public/scripts/slash-commands/SlashCommandEnumValue.js
Normal file
13
public/scripts/slash-commands/SlashCommandEnumValue.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export class SlashCommandEnumValue {
|
||||||
|
/**@type {string}*/ value;
|
||||||
|
/**@type {string}*/ description;
|
||||||
|
|
||||||
|
constructor(value, description = null) {
|
||||||
|
this.value = value;
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
}
|
45
public/scripts/slash-commands/SlashCommandExecutor.js
Normal file
45
public/scripts/slash-commands/SlashCommandExecutor.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { PARSER_FLAG } from './SlashCommandParser.js';
|
||||||
|
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
|
||||||
|
|
||||||
|
export class SlashCommandExecutor {
|
||||||
|
/**@type {Boolean}*/ injectPipe = true;
|
||||||
|
/**@type {Number}*/ start;
|
||||||
|
/**@type {Number}*/ end;
|
||||||
|
/**@type {Number}*/ startNamedArgs;
|
||||||
|
/**@type {Number}*/ endNamedArgs;
|
||||||
|
/**@type {Number}*/ startUnnamedArgs;
|
||||||
|
/**@type {Number}*/ endUnnamedArgs;
|
||||||
|
/**@type {String}*/ name = '';
|
||||||
|
/**@type {SlashCommand}*/ command;
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];
|
||||||
|
/**@type {SlashCommandUnnamedArgumentAssignment[]}*/ unnamedArgumentList = [];
|
||||||
|
/**@type {Object<PARSER_FLAG,boolean>} */ parserFlags;
|
||||||
|
|
||||||
|
get commandCount() {
|
||||||
|
return 1
|
||||||
|
+ this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>/**@type {SlashCommandClosure}*/(it.value).commandCount).reduce((cur, sum)=>cur + sum, 0)
|
||||||
|
+ this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>/**@type {SlashCommandClosure}*/(it.value).commandCount).reduce((cur, sum)=>cur + sum, 0)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
set onProgress(value) {
|
||||||
|
const closures = /**@type {SlashCommandClosure[]}*/([
|
||||||
|
...this.namedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>it.value),
|
||||||
|
...this.unnamedArgumentList.filter(it=>it.value instanceof SlashCommandClosure).map(it=>it.value),
|
||||||
|
]);
|
||||||
|
for (const closure of closures) {
|
||||||
|
closure.onProgress = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(start) {
|
||||||
|
this.start = start;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
|
||||||
|
export class SlashCommandNamedArgumentAssignment {
|
||||||
|
/**@type {number}*/ start;
|
||||||
|
/**@type {number}*/ end;
|
||||||
|
/**@type {string}*/ name;
|
||||||
|
/**@type {string|SlashCommandClosure}*/ value;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
import { SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||||
|
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||||
|
|
||||||
|
export class SlashCommandNamedArgumentAutoCompleteOption extends AutoCompleteOption {
|
||||||
|
/**@type {SlashCommandNamedArgument}*/ arg;
|
||||||
|
/**@type {SlashCommand}*/ cmd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {SlashCommandNamedArgument} arg
|
||||||
|
*/
|
||||||
|
constructor(arg, cmd) {
|
||||||
|
super(`${arg.name}=`);
|
||||||
|
this.arg = arg;
|
||||||
|
this.cmd = cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderItem() {
|
||||||
|
let li;
|
||||||
|
li = this.makeItem(this.name, '⌗', true, [], [], null, `${this.arg.isRequired ? '' : '(optional) '}${this.arg.description ?? ''}`);
|
||||||
|
li.setAttribute('data-name', this.name);
|
||||||
|
li.setAttribute('data-option-type', 'namedArgument');
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderDetails() {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const specs = document.createElement('div'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('div'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = this.name;
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
frag.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
help.innerHTML = `${this.arg.isRequired ? '' : '(optional) '}${this.arg.description ?? ''}`;
|
||||||
|
frag.append(help);
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
949
public/scripts/slash-commands/SlashCommandParser.js
Normal file
949
public/scripts/slash-commands/SlashCommandParser.js
Normal file
@ -0,0 +1,949 @@
|
|||||||
|
import { power_user } from '../power-user.js';
|
||||||
|
import { isTrueBoolean, uuidv4 } from '../utils.js';
|
||||||
|
import { SlashCommand } from './SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument } from './SlashCommandArgument.js';
|
||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||||
|
import { SlashCommandParserError } from './SlashCommandParserError.js';
|
||||||
|
import { AutoCompleteNameResult } from '../autocomplete/AutoCompleteNameResult.js';
|
||||||
|
import { SlashCommandQuickReplyAutoCompleteOption } from './SlashCommandQuickReplyAutoCompleteOption.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||||
|
import { SlashCommandVariableAutoCompleteOption } from './SlashCommandVariableAutoCompleteOption.js';
|
||||||
|
import { SlashCommandNamedArgumentAssignment } from './SlashCommandNamedArgumentAssignment.js';
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||||
|
import { SlashCommandAutoCompleteNameResult } from './SlashCommandAutoCompleteNameResult.js';
|
||||||
|
import { SlashCommandUnnamedArgumentAssignment } from './SlashCommandUnnamedArgumentAssignment.js';
|
||||||
|
import { SlashCommandEnumValue } from './SlashCommandEnumValue.js';
|
||||||
|
import { MacroAutoCompleteOption } from '../autocomplete/MacroAutoCompleteOption.js';
|
||||||
|
|
||||||
|
/**@readonly*/
|
||||||
|
/**@enum {Number}*/
|
||||||
|
export const PARSER_FLAG = {
|
||||||
|
'STRICT_ESCAPING': 1,
|
||||||
|
'REPLACE_GETVAR': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SlashCommandParser {
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {Object.<string, SlashCommand>}*/ static commands = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use SlashCommandParser.addCommandObject() instead.
|
||||||
|
* @param {string} command Command name
|
||||||
|
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} callback The function to execute when the command is called
|
||||||
|
* @param {string[]} aliases List of alternative command names
|
||||||
|
* @param {string} helpString Help text shown in autocomplete and command browser
|
||||||
|
*/
|
||||||
|
static addCommand(command, callback, aliases, helpString = '') {
|
||||||
|
this.addCommandObject(SlashCommand.fromProps({
|
||||||
|
name: command,
|
||||||
|
callback,
|
||||||
|
aliases,
|
||||||
|
helpString,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SlashCommand} command
|
||||||
|
*/
|
||||||
|
static addCommandObject(command) {
|
||||||
|
const reserved = ['/', '#', ':', 'parser-flag'];
|
||||||
|
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}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.addCommandObjectUnsafe(command);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SlashCommand} command
|
||||||
|
*/
|
||||||
|
static addCommandObjectUnsafe(command) {
|
||||||
|
if ([command.name, ...command.aliases].some(x => Object.hasOwn(this.commands, x))) {
|
||||||
|
console.trace('WARN: Duplicate slash command registered!', [command.name, ...command.aliases]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commands[command.name] = command;
|
||||||
|
|
||||||
|
if (Array.isArray(command.aliases)) {
|
||||||
|
command.aliases.forEach((alias) => {
|
||||||
|
this.commands[alias] = command;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
get commands() {
|
||||||
|
return SlashCommandParser.commands;
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {Object.<string, string>}*/ helpStrings = {};
|
||||||
|
/**@type {boolean}*/ verifyCommandNames = true;
|
||||||
|
/**@type {string}*/ text;
|
||||||
|
/**@type {number}*/ index;
|
||||||
|
/**@type {SlashCommandAbortController}*/ abortController;
|
||||||
|
/**@type {SlashCommandScope}*/ scope;
|
||||||
|
/**@type {SlashCommandClosure}*/ closure;
|
||||||
|
|
||||||
|
/**@type {Object.<PARSER_FLAG,boolean>}*/ flags = {};
|
||||||
|
|
||||||
|
/**@type {boolean}*/ jumpedEscapeSequence = false;
|
||||||
|
|
||||||
|
/**@type {{start:number, end:number}[]}*/ closureIndex;
|
||||||
|
/**@type {{start:number, end:number, name:string}[]}*/ macroIndex;
|
||||||
|
/**@type {SlashCommandExecutor[]}*/ commandIndex;
|
||||||
|
/**@type {SlashCommandScope[]}*/ scopeIndex;
|
||||||
|
|
||||||
|
get userIndex() { return this.index; }
|
||||||
|
|
||||||
|
get ahead() {
|
||||||
|
return this.text.slice(this.index + 1);
|
||||||
|
}
|
||||||
|
get behind() {
|
||||||
|
return this.text.slice(0, this.index);
|
||||||
|
}
|
||||||
|
get char() {
|
||||||
|
return this.text[this.index];
|
||||||
|
}
|
||||||
|
get endOfText() {
|
||||||
|
return this.index >= this.text.length || /^\s+$/.test(this.ahead);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
//TODO should not be re-registered from every instance
|
||||||
|
// add dummy commands for help strings / autocomplete
|
||||||
|
if (!Object.keys(this.commands).includes('parser-flag')) {
|
||||||
|
const help = {};
|
||||||
|
help[PARSER_FLAG.REPLACE_GETVAR] = 'Replace all {{getvar::}} and {{getglobalvar::}} macros with scoped variables to avoid double macro substitution.';
|
||||||
|
help[PARSER_FLAG.STRICT_ESCAPING] = 'Allows to escape all delimiters with backslash, and allows escaping of backslashes.';
|
||||||
|
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: 'parser-flag',
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'The parser flag to modify.',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
isRequired: true,
|
||||||
|
enumList: Object.keys(PARSER_FLAG).map(flag=>new SlashCommandEnumValue(flag, help[PARSER_FLAG[flag]])),
|
||||||
|
}),
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'The state of the parser flag to set.',
|
||||||
|
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||||
|
defaultValue: 'on',
|
||||||
|
enumList: ['on', 'off'],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
splitUnnamedArgument: true,
|
||||||
|
helpString: 'Set a parser flag.',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (!Object.keys(this.commands).includes('/')) {
|
||||||
|
SlashCommandParser.addCommandObjectUnsafe(SlashCommand.fromProps({ name: '/',
|
||||||
|
aliases: ['#'],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
SlashCommandArgument.fromProps({
|
||||||
|
description: 'commentary',
|
||||||
|
typeList: [ARGUMENT_TYPE.STRING],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
helpString: 'Write a comment.',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO should not be re-registered from every instance
|
||||||
|
this.registerLanguage();
|
||||||
|
}
|
||||||
|
registerLanguage() {
|
||||||
|
// NUMBER mode is copied from highlightjs's own implementation for JavaScript
|
||||||
|
// https://tc39.es/ecma262/#sec-literals-numeric-literals
|
||||||
|
const decimalDigits = '[0-9](_?[0-9])*';
|
||||||
|
const frac = `\\.(${decimalDigits})`;
|
||||||
|
// DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral
|
||||||
|
// https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
|
||||||
|
const decimalInteger = '0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*';
|
||||||
|
const NUMBER = {
|
||||||
|
className: 'number',
|
||||||
|
variants: [
|
||||||
|
// DecimalLiteral
|
||||||
|
{ begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` +
|
||||||
|
`[eE][+-]?(${decimalDigits})\\b` },
|
||||||
|
{ begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },
|
||||||
|
|
||||||
|
// DecimalBigIntegerLiteral
|
||||||
|
{ begin: '\\b(0|[1-9](_?[0-9])*)n\\b' },
|
||||||
|
|
||||||
|
// NonDecimalIntegerLiteral
|
||||||
|
{ begin: '\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b' },
|
||||||
|
{ begin: '\\b0[bB][0-1](_?[0-1])*n?\\b' },
|
||||||
|
{ begin: '\\b0[oO][0-7](_?[0-7])*n?\\b' },
|
||||||
|
|
||||||
|
// LegacyOctalIntegerLiteral (does not include underscore separators)
|
||||||
|
// https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
|
||||||
|
{ begin: '\\b0[0-7]+n?\\b' },
|
||||||
|
],
|
||||||
|
relevance: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMMENT = {
|
||||||
|
scope: 'comment',
|
||||||
|
begin: /\/[/#]/,
|
||||||
|
end: /\||$|:}/,
|
||||||
|
contains: [],
|
||||||
|
};
|
||||||
|
const ABORT = {
|
||||||
|
scope: 'abort',
|
||||||
|
begin: /\/abort/,
|
||||||
|
end: /\||$|:}/,
|
||||||
|
contains: [],
|
||||||
|
};
|
||||||
|
const LET = {
|
||||||
|
begin: [
|
||||||
|
/\/(let|var)\s+/,
|
||||||
|
],
|
||||||
|
beginScope: {
|
||||||
|
1: 'variable',
|
||||||
|
},
|
||||||
|
end: /\||$|:}/,
|
||||||
|
contains: [],
|
||||||
|
};
|
||||||
|
const SETVAR = {
|
||||||
|
begin: /\/(setvar|setglobalvar)\s+/,
|
||||||
|
beginScope: 'variable',
|
||||||
|
end: /\||$|:}/,
|
||||||
|
excludeEnd: true,
|
||||||
|
contains: [],
|
||||||
|
};
|
||||||
|
const GETVAR = {
|
||||||
|
begin: /\/(getvar|getglobalvar)\s+/,
|
||||||
|
beginScope: 'variable',
|
||||||
|
end: /\||$|:}/,
|
||||||
|
excludeEnd: true,
|
||||||
|
contains: [],
|
||||||
|
};
|
||||||
|
const RUN = {
|
||||||
|
match: [
|
||||||
|
/\/:/,
|
||||||
|
/(".+?(?<!\\)") |(\S+?) /,
|
||||||
|
],
|
||||||
|
className: {
|
||||||
|
1: 'variable.language',
|
||||||
|
2: 'title.function.invoke',
|
||||||
|
},
|
||||||
|
contains: [], // defined later
|
||||||
|
};
|
||||||
|
const COMMAND = {
|
||||||
|
scope: 'command',
|
||||||
|
begin: /\/\S+/,
|
||||||
|
beginScope: 'title.function',
|
||||||
|
end: /\||$|(?=:})/,
|
||||||
|
excludeEnd: true,
|
||||||
|
contains: [], // defined later
|
||||||
|
};
|
||||||
|
const CLOSURE = {
|
||||||
|
scope: 'closure',
|
||||||
|
begin: /{:/,
|
||||||
|
end: /:}(\(\))?/,
|
||||||
|
beginScope: 'punctuation',
|
||||||
|
endScope: 'punctuation',
|
||||||
|
contains: [], // defined later
|
||||||
|
};
|
||||||
|
const NAMED_ARG = {
|
||||||
|
scope: 'property',
|
||||||
|
begin: /\w+=/,
|
||||||
|
end: '',
|
||||||
|
};
|
||||||
|
const MACRO = {
|
||||||
|
scope: 'variable',
|
||||||
|
begin: /{{/,
|
||||||
|
end: /}}/,
|
||||||
|
};
|
||||||
|
RUN.contains.push(
|
||||||
|
hljs.BACKSLASH_ESCAPE,
|
||||||
|
NAMED_ARG,
|
||||||
|
hljs.QUOTE_STRING_MODE,
|
||||||
|
NUMBER,
|
||||||
|
MACRO,
|
||||||
|
CLOSURE,
|
||||||
|
);
|
||||||
|
LET.contains.push(
|
||||||
|
hljs.BACKSLASH_ESCAPE,
|
||||||
|
NAMED_ARG,
|
||||||
|
NUMBER,
|
||||||
|
MACRO,
|
||||||
|
CLOSURE,
|
||||||
|
hljs.QUOTE_STRING_MODE,
|
||||||
|
);
|
||||||
|
SETVAR.contains.push(
|
||||||
|
hljs.BACKSLASH_ESCAPE,
|
||||||
|
NAMED_ARG,
|
||||||
|
NUMBER,
|
||||||
|
MACRO,
|
||||||
|
CLOSURE,
|
||||||
|
hljs.QUOTE_STRING_MODE,
|
||||||
|
);
|
||||||
|
GETVAR.contains.push(
|
||||||
|
hljs.BACKSLASH_ESCAPE,
|
||||||
|
NAMED_ARG,
|
||||||
|
hljs.QUOTE_STRING_MODE,
|
||||||
|
NUMBER,
|
||||||
|
MACRO,
|
||||||
|
CLOSURE,
|
||||||
|
);
|
||||||
|
COMMAND.contains.push(
|
||||||
|
hljs.BACKSLASH_ESCAPE,
|
||||||
|
NAMED_ARG,
|
||||||
|
NUMBER,
|
||||||
|
MACRO,
|
||||||
|
CLOSURE,
|
||||||
|
hljs.QUOTE_STRING_MODE,
|
||||||
|
);
|
||||||
|
CLOSURE.contains.push(
|
||||||
|
hljs.BACKSLASH_ESCAPE,
|
||||||
|
COMMENT,
|
||||||
|
ABORT,
|
||||||
|
NAMED_ARG,
|
||||||
|
NUMBER,
|
||||||
|
MACRO,
|
||||||
|
RUN,
|
||||||
|
LET,
|
||||||
|
GETVAR,
|
||||||
|
SETVAR,
|
||||||
|
COMMAND,
|
||||||
|
'self',
|
||||||
|
hljs.QUOTE_STRING_MODE,
|
||||||
|
);
|
||||||
|
hljs.registerLanguage('stscript', ()=>({
|
||||||
|
case_insensitive: false,
|
||||||
|
keywords: ['|'],
|
||||||
|
contains: [
|
||||||
|
hljs.BACKSLASH_ESCAPE,
|
||||||
|
COMMENT,
|
||||||
|
ABORT,
|
||||||
|
RUN,
|
||||||
|
LET,
|
||||||
|
GETVAR,
|
||||||
|
SETVAR,
|
||||||
|
COMMAND,
|
||||||
|
CLOSURE,
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
getHelpString() {
|
||||||
|
return '<div class="slashHelp">Loading...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} text The text to parse.
|
||||||
|
* @param {*} index Index to check for names (cursor position).
|
||||||
|
*/
|
||||||
|
async getNameAt(text, index) {
|
||||||
|
if (this.text != text) {
|
||||||
|
try {
|
||||||
|
this.parse(text, false);
|
||||||
|
} catch (e) {
|
||||||
|
// do nothing
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const executor = this.commandIndex
|
||||||
|
.filter(it=>it.start <= index && (it.end >= index || it.end == null))
|
||||||
|
.slice(-1)[0]
|
||||||
|
?? null
|
||||||
|
;
|
||||||
|
|
||||||
|
if (executor) {
|
||||||
|
const childClosure = this.closureIndex
|
||||||
|
.find(it=>it.start <= index && (it.end >= index || it.end == null) && it.start > executor.start)
|
||||||
|
?? null
|
||||||
|
;
|
||||||
|
if (childClosure !== null) return null;
|
||||||
|
const macro = this.macroIndex.findLast(it=>it.start <= index && it.end >= index);
|
||||||
|
console.log(macro);
|
||||||
|
if (macro) {
|
||||||
|
const frag = document.createRange().createContextualFragment(await (await fetch('/scripts/templates/macros.html')).text());
|
||||||
|
const options = [...frag.querySelectorAll('ul:nth-of-type(2n+1) > li')].map(li=>new MacroAutoCompleteOption(
|
||||||
|
li.querySelector('tt').textContent.slice(2, -2).replace(/^([^\s:]+[\s:]+).*$/, '$1'),
|
||||||
|
li.querySelector('tt').textContent,
|
||||||
|
(li.querySelector('tt').remove(),li.innerHTML),
|
||||||
|
));
|
||||||
|
const result = new AutoCompleteNameResult(
|
||||||
|
macro.name,
|
||||||
|
macro.start + 2,
|
||||||
|
options,
|
||||||
|
false,
|
||||||
|
()=>`No matching macros for "{{${result.name}}}"`,
|
||||||
|
()=>'No macros found.',
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (executor.name == ':') {
|
||||||
|
const options = this.scopeIndex[this.commandIndex.indexOf(executor)]
|
||||||
|
?.allVariableNames
|
||||||
|
?.map(it=>new SlashCommandVariableAutoCompleteOption(it))
|
||||||
|
?? []
|
||||||
|
;
|
||||||
|
try {
|
||||||
|
const qrApi = (await import('../extensions/quick-reply/index.js')).quickReplyApi;
|
||||||
|
options.push(...qrApi.listSets()
|
||||||
|
.map(set=>qrApi.listQuickReplies(set).map(qr=>`${set}.${qr}`))
|
||||||
|
.flat()
|
||||||
|
.map(qr=>new SlashCommandQuickReplyAutoCompleteOption(qr)),
|
||||||
|
);
|
||||||
|
} catch { /* empty */ }
|
||||||
|
const result = new AutoCompleteNameResult(
|
||||||
|
executor.unnamedArgumentList[0]?.value.toString(),
|
||||||
|
executor.start,
|
||||||
|
options,
|
||||||
|
true,
|
||||||
|
()=>`No matching variables in scope and no matching Quick Replies for "${result.name}"`,
|
||||||
|
()=>'No variables in scope and no Quick Replies found.',
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const result = new SlashCommandAutoCompleteNameResult(executor, this.commands);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the index <length> number of characters forward and returns the last character taken.
|
||||||
|
* @param {number} length Number of characters to take.
|
||||||
|
* @param {boolean} keep Whether to add the characters to the kept text.
|
||||||
|
* @returns The last character taken.
|
||||||
|
*/
|
||||||
|
take(length = 1) {
|
||||||
|
this.jumpedEscapeSequence = false;
|
||||||
|
let content = this.char;
|
||||||
|
this.index++;
|
||||||
|
if (length > 1) {
|
||||||
|
content = this.take(length - 1);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
discardWhitespace() {
|
||||||
|
while (/\s/.test(this.char)) {
|
||||||
|
this.take(); // discard whitespace
|
||||||
|
this.jumpedEscapeSequence = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Tests if the next characters match a symbol.
|
||||||
|
* Moves the index forward if the next characters are backslashes directly followed by the symbol.
|
||||||
|
* Expects that the current char is taken after testing.
|
||||||
|
* @param {string|RegExp} sequence Sequence of chars or regex character group that is the symbol.
|
||||||
|
* @param {number} offset Offset from the current index (won't move the index if offset != 0).
|
||||||
|
* @returns Whether the next characters are the indicated symbol.
|
||||||
|
*/
|
||||||
|
testSymbol(sequence, offset = 0) {
|
||||||
|
if (!this.flags[PARSER_FLAG.STRICT_ESCAPING]) return this.testSymbolLooseyGoosey(sequence, offset);
|
||||||
|
// /echo abc | /echo def
|
||||||
|
// -> TOAST: abc
|
||||||
|
// -> TOAST: def
|
||||||
|
// /echo abc \| /echo def
|
||||||
|
// -> TOAST: abc | /echo def
|
||||||
|
// /echo abc \\| /echo def
|
||||||
|
// -> TOAST: abc \
|
||||||
|
// -> TOAST: def
|
||||||
|
// /echo abc \\\| /echo def
|
||||||
|
// -> TOAST: abc \| /echo def
|
||||||
|
// /echo abc \\\\| /echo def
|
||||||
|
// -> TOAST: abc \\
|
||||||
|
// -> TOAST: def
|
||||||
|
// /echo title=\:} \{: | /echo title=\{: \:}
|
||||||
|
// -> TOAST: *:}* {:
|
||||||
|
// -> TOAST: *{:* :}
|
||||||
|
const escapeOffset = this.jumpedEscapeSequence ? -1 : 0;
|
||||||
|
const escapes = this.text.slice(this.index + offset + escapeOffset).replace(/^(\\*).*$/s, '$1').length;
|
||||||
|
const test = (sequence instanceof RegExp) ?
|
||||||
|
(text) => new RegExp(`^${sequence.source}`).test(text) :
|
||||||
|
(text) => text.startsWith(sequence)
|
||||||
|
;
|
||||||
|
if (test(this.text.slice(this.index + offset + escapeOffset + escapes))) {
|
||||||
|
// no backslashes before sequence
|
||||||
|
// -> sequence found
|
||||||
|
if (escapes == 0) return true;
|
||||||
|
// uneven number of backslashes before sequence
|
||||||
|
// = the final backslash escapes the sequence
|
||||||
|
// = every preceding pair is one literal backslash
|
||||||
|
// -> move index forward to skip the backslash escaping the first backslash or the symbol
|
||||||
|
// even number of backslashes before sequence
|
||||||
|
// = every pair is one literal backslash
|
||||||
|
// -> move index forward to skip the backslash escaping the first backslash
|
||||||
|
if (!this.jumpedEscapeSequence && offset == 0) {
|
||||||
|
this.index++;
|
||||||
|
this.jumpedEscapeSequence = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testSymbolLooseyGoosey(sequence, offset = 0) {
|
||||||
|
const escapeOffset = this.jumpedEscapeSequence ? -1 : 0;
|
||||||
|
const escapes = this.text[this.index + offset + escapeOffset] == '\\' ? 1 : 0;
|
||||||
|
const test = (sequence instanceof RegExp) ?
|
||||||
|
(text) => new RegExp(`^${sequence.source}`).test(text) :
|
||||||
|
(text) => text.startsWith(sequence)
|
||||||
|
;
|
||||||
|
if (test(this.text.slice(this.index + offset + escapeOffset + escapes))) {
|
||||||
|
// no backslashes before sequence
|
||||||
|
// -> sequence found
|
||||||
|
if (escapes == 0) return true;
|
||||||
|
// otherwise
|
||||||
|
// -> sequence found
|
||||||
|
if (!this.jumpedEscapeSequence && offset == 0) {
|
||||||
|
this.index++;
|
||||||
|
this.jumpedEscapeSequence = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
replaceGetvar(value) {
|
||||||
|
return value.replace(/{{(get(?:global)?var)::([^}]+)}}/gi, (_, cmd, name) => {
|
||||||
|
name = name.trim();
|
||||||
|
// store pipe
|
||||||
|
const pipeName = `_PARSER_${uuidv4()}`;
|
||||||
|
const storePipe = new SlashCommandExecutor(null); {
|
||||||
|
storePipe.command = this.commands['let'];
|
||||||
|
storePipe.name = 'let';
|
||||||
|
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
nameAss.value = pipeName;
|
||||||
|
const valAss = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
valAss.value = '{{pipe}}';
|
||||||
|
storePipe.unnamedArgumentList = [nameAss, valAss];
|
||||||
|
this.closure.executorList.push(storePipe);
|
||||||
|
}
|
||||||
|
// getvar / getglobalvar
|
||||||
|
const getvar = new SlashCommandExecutor(null); {
|
||||||
|
getvar.command = this.commands[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); {
|
||||||
|
setvar.command = this.commands['let'];
|
||||||
|
setvar.name = 'let';
|
||||||
|
const nameAss = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
nameAss.value = varName;
|
||||||
|
const valAss = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
valAss.value = '{{pipe}}';
|
||||||
|
setvar.unnamedArgumentList = [nameAss, valAss];
|
||||||
|
this.closure.executorList.push(setvar);
|
||||||
|
}
|
||||||
|
// return pipe
|
||||||
|
const returnPipe = new SlashCommandExecutor(null); {
|
||||||
|
returnPipe.command = this.commands['return'];
|
||||||
|
returnPipe.name = 'return';
|
||||||
|
const varAss = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
varAss.value = `{{var::${pipeName}}}`;
|
||||||
|
returnPipe.unnamedArgumentList = [varAss];
|
||||||
|
this.closure.executorList.push(returnPipe);
|
||||||
|
}
|
||||||
|
return `{{var::${varName}}}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
parse(text, verifyCommandNames = true, flags = null, abortController = 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.text = text;
|
||||||
|
this.index = 0;
|
||||||
|
this.scope = null;
|
||||||
|
this.closureIndex = [];
|
||||||
|
this.commandIndex = [];
|
||||||
|
this.scopeIndex = [];
|
||||||
|
this.macroIndex = [];
|
||||||
|
const closure = this.parseClosure(true);
|
||||||
|
return closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
testClosure() {
|
||||||
|
return this.testSymbol('{:');
|
||||||
|
}
|
||||||
|
testClosureEnd() {
|
||||||
|
if (!this.scope.parent) {
|
||||||
|
// "root" closure does not have {: and :}
|
||||||
|
if (this.index >= this.text.length) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!this.verifyCommandNames) {
|
||||||
|
if (this.index >= this.text.length) return true;
|
||||||
|
} else {
|
||||||
|
if (this.ahead.length < 1) throw new SlashCommandParserError(`Unclosed closure at position ${this.userIndex}`, this.text, this.index);
|
||||||
|
}
|
||||||
|
return this.testSymbol(':}');
|
||||||
|
}
|
||||||
|
parseClosure(isRoot = false) {
|
||||||
|
const closureIndexEntry = { start:this.index + 1, end:null };
|
||||||
|
this.closureIndex.push(closureIndexEntry);
|
||||||
|
let injectPipe = true;
|
||||||
|
if (!isRoot) this.take(2); // discard opening {:
|
||||||
|
let closure = new SlashCommandClosure(this.scope);
|
||||||
|
closure.abortController = this.abortController;
|
||||||
|
this.scope = closure.scope;
|
||||||
|
this.closure = closure;
|
||||||
|
this.discardWhitespace();
|
||||||
|
while (this.testNamedArgument()) {
|
||||||
|
const arg = this.parseNamedArgument();
|
||||||
|
closure.argumentList.push(arg);
|
||||||
|
this.scope.variableNames.push(arg.name);
|
||||||
|
this.discardWhitespace();
|
||||||
|
}
|
||||||
|
while (!this.testClosureEnd()) {
|
||||||
|
if (this.testComment()) {
|
||||||
|
this.parseComment();
|
||||||
|
} else if (this.testParserFlag()) {
|
||||||
|
this.parseParserFlag();
|
||||||
|
} else if (this.testRunShorthand()) {
|
||||||
|
const cmd = this.parseRunShorthand();
|
||||||
|
closure.executorList.push(cmd);
|
||||||
|
injectPipe = true;
|
||||||
|
} else if (this.testCommand()) {
|
||||||
|
const cmd = this.parseCommand();
|
||||||
|
cmd.injectPipe = injectPipe;
|
||||||
|
closure.executorList.push(cmd);
|
||||||
|
injectPipe = true;
|
||||||
|
} else {
|
||||||
|
while (!this.testCommandEnd()) this.take(); // discard plain text and comments
|
||||||
|
}
|
||||||
|
this.discardWhitespace();
|
||||||
|
// first pipe marks end of command
|
||||||
|
if (this.testSymbol('|')) {
|
||||||
|
this.take(); // discard first pipe
|
||||||
|
// second pipe indicates no pipe injection for the next command
|
||||||
|
if (this.testSymbol('|')) {
|
||||||
|
injectPipe = false;
|
||||||
|
this.take(); // discard second pipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.discardWhitespace(); // discard further whitespace
|
||||||
|
}
|
||||||
|
if (!isRoot) this.take(2); // discard closing :}
|
||||||
|
if (this.testSymbol('()')) {
|
||||||
|
this.take(2); // discard ()
|
||||||
|
closure.executeNow = true;
|
||||||
|
}
|
||||||
|
closureIndexEntry.end = this.index - 1;
|
||||||
|
this.discardWhitespace(); // discard trailing whitespace
|
||||||
|
this.scope = closure.scope.parent;
|
||||||
|
return closure;
|
||||||
|
}
|
||||||
|
|
||||||
|
testComment() {
|
||||||
|
return this.testSymbol(/\/[/#]/);
|
||||||
|
}
|
||||||
|
testCommentEnd() {
|
||||||
|
return this.testCommandEnd();
|
||||||
|
}
|
||||||
|
parseComment() {
|
||||||
|
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 second "/" or "#" as name
|
||||||
|
while (!this.testCommentEnd()) this.take();
|
||||||
|
cmd.end = this.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
testParserFlag() {
|
||||||
|
return this.testSymbol('/parser-flag ');
|
||||||
|
}
|
||||||
|
testParserFlagEnd() {
|
||||||
|
return this.testCommandEnd();
|
||||||
|
}
|
||||||
|
parseParserFlag() {
|
||||||
|
const start = this.index + 1;
|
||||||
|
const cmd = new SlashCommandExecutor(start);
|
||||||
|
cmd.name = 'parser-flag';
|
||||||
|
cmd.unnamedArgumentList = [];
|
||||||
|
cmd.command = this.commands[cmd.name];
|
||||||
|
this.commandIndex.push(cmd);
|
||||||
|
this.scopeIndex.push(this.scope.getCopy());
|
||||||
|
this.take(13); // discard "/parser-flag "
|
||||||
|
cmd.startNamedArgs = -1;
|
||||||
|
cmd.endNamedArgs = -1;
|
||||||
|
cmd.startUnnamedArgs = this.index;
|
||||||
|
cmd.unnamedArgumentList = this.parseUnnamedArgument(true);
|
||||||
|
const [flag, state] = cmd.unnamedArgumentList ?? [null, null];
|
||||||
|
cmd.endUnnamedArgs = this.index;
|
||||||
|
if (Object.keys(PARSER_FLAG).includes(flag.value.toString())) {
|
||||||
|
this.flags[PARSER_FLAG[flag.value.toString()]] = isTrueBoolean(state?.value.toString() ?? 'on');
|
||||||
|
}
|
||||||
|
cmd.end = this.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
testRunShorthand() {
|
||||||
|
return this.testSymbol('/:') && !this.testSymbol(':}', 1);
|
||||||
|
}
|
||||||
|
testRunShorthandEnd() {
|
||||||
|
return this.testCommandEnd();
|
||||||
|
}
|
||||||
|
parseRunShorthand() {
|
||||||
|
const start = this.index + 2;
|
||||||
|
const cmd = new SlashCommandExecutor(start);
|
||||||
|
cmd.name = ':';
|
||||||
|
cmd.unnamedArgumentList = [];
|
||||||
|
cmd.command = this.commands['run'];
|
||||||
|
this.commandIndex.push(cmd);
|
||||||
|
this.scopeIndex.push(this.scope.getCopy());
|
||||||
|
this.take(2); //discard "/:"
|
||||||
|
const assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
if (this.testQuotedValue()) assignment.value = this.parseQuotedValue();
|
||||||
|
else assignment.value = this.parseValue();
|
||||||
|
cmd.unnamedArgumentList = [assignment];
|
||||||
|
this.discardWhitespace();
|
||||||
|
while (this.testNamedArgument()) {
|
||||||
|
const arg = this.parseNamedArgument();
|
||||||
|
cmd.namedArgumentList.push(arg);
|
||||||
|
this.discardWhitespace();
|
||||||
|
}
|
||||||
|
this.discardWhitespace();
|
||||||
|
// /run shorthand does not take unnamed arguments (the command name practically *is* the unnamed argument)
|
||||||
|
if (this.testRunShorthandEnd()) {
|
||||||
|
cmd.end = this.index;
|
||||||
|
return cmd;
|
||||||
|
} else {
|
||||||
|
console.warn(this.behind, this.char, this.ahead);
|
||||||
|
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testCommand() {
|
||||||
|
return this.testSymbol('/');
|
||||||
|
}
|
||||||
|
testCommandEnd() {
|
||||||
|
return this.testClosureEnd() || this.testSymbol('|');
|
||||||
|
}
|
||||||
|
parseCommand() {
|
||||||
|
const start = this.index + 1;
|
||||||
|
const cmd = new SlashCommandExecutor(start);
|
||||||
|
cmd.parserFlags = Object.assign({}, this.flags);
|
||||||
|
this.commandIndex.push(cmd);
|
||||||
|
this.scopeIndex.push(this.scope.getCopy());
|
||||||
|
this.take(); // discard "/"
|
||||||
|
while (!/\s/.test(this.char) && !this.testCommandEnd()) cmd.name += this.take(); // take chars until whitespace or end
|
||||||
|
this.discardWhitespace();
|
||||||
|
if (this.verifyCommandNames && !this.commands[cmd.name]) throw new SlashCommandParserError(`Unknown command at position ${this.index - cmd.name.length}: "/${cmd.name}"`, this.text, this.index - cmd.name.length);
|
||||||
|
cmd.command = this.commands[cmd.name];
|
||||||
|
cmd.startNamedArgs = this.index;
|
||||||
|
cmd.endNamedArgs = this.index;
|
||||||
|
while (this.testNamedArgument()) {
|
||||||
|
const arg = this.parseNamedArgument();
|
||||||
|
cmd.namedArgumentList.push(arg);
|
||||||
|
cmd.endNamedArgs = this.index;
|
||||||
|
this.discardWhitespace();
|
||||||
|
}
|
||||||
|
this.discardWhitespace();
|
||||||
|
cmd.startUnnamedArgs = this.index;
|
||||||
|
cmd.endUnnamedArgs = this.index;
|
||||||
|
if (this.testUnnamedArgument()) {
|
||||||
|
cmd.unnamedArgumentList = this.parseUnnamedArgument(cmd.command?.unnamedArgumentList?.length && cmd?.command?.splitUnnamedArgument);
|
||||||
|
cmd.endUnnamedArgs = this.index;
|
||||||
|
if (cmd.name == 'let') {
|
||||||
|
const keyArg = cmd.namedArgumentList.find(it=>it.name == 'key');
|
||||||
|
if (keyArg) {
|
||||||
|
this.scope.variableNames.push(keyArg.value.toString());
|
||||||
|
} else if (typeof cmd.unnamedArgumentList[0]?.value == 'string') {
|
||||||
|
this.scope.variableNames.push(cmd.unnamedArgumentList[0].value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.testCommandEnd()) {
|
||||||
|
cmd.end = this.index;
|
||||||
|
return cmd;
|
||||||
|
} else {
|
||||||
|
console.warn(this.behind, this.char, this.ahead);
|
||||||
|
throw new SlashCommandParserError(`Unexpected end of command at position ${this.userIndex}: "/${cmd.name}"`, this.text, this.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testNamedArgument() {
|
||||||
|
return /^(\w+)=/.test(`${this.char}${this.ahead}`);
|
||||||
|
}
|
||||||
|
parseNamedArgument() {
|
||||||
|
let assignment = new SlashCommandNamedArgumentAssignment();
|
||||||
|
assignment.start = this.index;
|
||||||
|
let key = '';
|
||||||
|
while (/\w/.test(this.char)) key += this.take(); // take chars
|
||||||
|
this.take(); // discard "="
|
||||||
|
assignment.name = key;
|
||||||
|
if (this.testClosure()) {
|
||||||
|
assignment.value = this.parseClosure();
|
||||||
|
} else if (this.testQuotedValue()) {
|
||||||
|
assignment.value = this.parseQuotedValue();
|
||||||
|
} else if (this.testListValue()) {
|
||||||
|
assignment.value = this.parseListValue();
|
||||||
|
} else if (this.testValue()) {
|
||||||
|
assignment.value = this.parseValue();
|
||||||
|
}
|
||||||
|
assignment.end = this.index;
|
||||||
|
return assignment;
|
||||||
|
}
|
||||||
|
|
||||||
|
testUnnamedArgument() {
|
||||||
|
return !this.testCommandEnd();
|
||||||
|
}
|
||||||
|
testUnnamedArgumentEnd() {
|
||||||
|
return this.testCommandEnd();
|
||||||
|
}
|
||||||
|
parseUnnamedArgument(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 = [];
|
||||||
|
/**@type {SlashCommandUnnamedArgumentAssignment}*/
|
||||||
|
let assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
assignment.start = this.index;
|
||||||
|
while (!this.testUnnamedArgumentEnd()) {
|
||||||
|
if (this.testClosure()) {
|
||||||
|
isList = true;
|
||||||
|
if (value.length > 0) {
|
||||||
|
assignment.end = assignment.end - (value.length - value.trim().length);
|
||||||
|
this.indexMacros(this.index - value.length, value);
|
||||||
|
assignment.value = value.trim();
|
||||||
|
listValues.push(assignment);
|
||||||
|
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
assignment.start = this.index;
|
||||||
|
value = '';
|
||||||
|
}
|
||||||
|
assignment.value = this.parseClosure();
|
||||||
|
assignment.end = this.index;
|
||||||
|
listValues.push(assignment);
|
||||||
|
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
assignment.start = this.index;
|
||||||
|
} else if (split) {
|
||||||
|
if (this.testQuotedValue()) {
|
||||||
|
assignment.start = this.index;
|
||||||
|
assignment.value = this.parseQuotedValue();
|
||||||
|
assignment.end = this.index;
|
||||||
|
listValues.push(assignment);
|
||||||
|
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
} else if (this.testListValue()) {
|
||||||
|
assignment.start = this.index;
|
||||||
|
assignment.value = this.parseListValue();
|
||||||
|
assignment.end = this.index;
|
||||||
|
listValues.push(assignment);
|
||||||
|
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
} else if (this.testValue()) {
|
||||||
|
assignment.start = this.index;
|
||||||
|
assignment.value = this.parseValue();
|
||||||
|
assignment.end = this.index;
|
||||||
|
listValues.push(assignment);
|
||||||
|
assignment = new SlashCommandUnnamedArgumentAssignment();
|
||||||
|
} else {
|
||||||
|
throw new SlashCommandParserError(`Unexpected end of unnamed argument at index ${this.userIndex}.`);
|
||||||
|
}
|
||||||
|
this.discardWhitespace();
|
||||||
|
} else {
|
||||||
|
value += this.take();
|
||||||
|
assignment.end = this.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isList && value.trim().length > 0) {
|
||||||
|
assignment.value = value.trim();
|
||||||
|
listValues.push(assignment);
|
||||||
|
}
|
||||||
|
if (isList) {
|
||||||
|
return listValues;
|
||||||
|
}
|
||||||
|
this.indexMacros(this.index - value.length, value);
|
||||||
|
value = value.trim();
|
||||||
|
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||||
|
value = this.replaceGetvar(value);
|
||||||
|
}
|
||||||
|
assignment.value = value;
|
||||||
|
return [assignment];
|
||||||
|
}
|
||||||
|
|
||||||
|
testQuotedValue() {
|
||||||
|
return this.testSymbol('"');
|
||||||
|
}
|
||||||
|
testQuotedValueEnd() {
|
||||||
|
if (this.endOfText) {
|
||||||
|
if (this.verifyCommandNames) throw new SlashCommandParserError(`Unexpected end of quoted value at position ${this.index}`, this.text, this.index);
|
||||||
|
else return true;
|
||||||
|
}
|
||||||
|
if (!this.verifyCommandNames && this.testClosureEnd()) return true;
|
||||||
|
if (this.verifyCommandNames && !this.flags[PARSER_FLAG.STRICT_ESCAPING] && this.testCommandEnd()) {
|
||||||
|
throw new SlashCommandParserError(`Unexpected end of quoted value at position ${this.index}`, this.text, this.index);
|
||||||
|
}
|
||||||
|
return this.testSymbol('"') || (!this.flags[PARSER_FLAG.STRICT_ESCAPING] && this.testCommandEnd());
|
||||||
|
}
|
||||||
|
parseQuotedValue() {
|
||||||
|
this.take(); // discard opening quote
|
||||||
|
let value = '';
|
||||||
|
while (!this.testQuotedValueEnd()) value += this.take(); // take all chars until closing quote
|
||||||
|
this.take(); // discard closing quote
|
||||||
|
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||||
|
value = this.replaceGetvar(value);
|
||||||
|
}
|
||||||
|
this.indexMacros(this.index - value.length, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
testListValue() {
|
||||||
|
return this.testSymbol('[');
|
||||||
|
}
|
||||||
|
testListValueEnd() {
|
||||||
|
if (this.endOfText) throw new SlashCommandParserError(`Unexpected end of list value at position ${this.index}`, this.text, this.index);
|
||||||
|
return this.testSymbol(']');
|
||||||
|
}
|
||||||
|
parseListValue() {
|
||||||
|
let value = this.take(); // take the already tested opening bracket
|
||||||
|
while (!this.testListValueEnd()) value += this.take(); // take all chars until closing bracket
|
||||||
|
value += this.take(); // take closing bracket
|
||||||
|
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||||
|
value = this.replaceGetvar(value);
|
||||||
|
}
|
||||||
|
this.indexMacros(this.index - value.length, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
testValue() {
|
||||||
|
return !this.testSymbol(/\s/);
|
||||||
|
}
|
||||||
|
testValueEnd() {
|
||||||
|
if (this.testSymbol(/\s/)) return true;
|
||||||
|
return this.testCommandEnd();
|
||||||
|
}
|
||||||
|
parseValue() {
|
||||||
|
let value = this.jumpedEscapeSequence ? this.take() : ''; // take the first, already tested, char if it is an escaped one
|
||||||
|
while (!this.testValueEnd()) value += this.take(); // take all chars until value end
|
||||||
|
if (this.flags[PARSER_FLAG.REPLACE_GETVAR]) {
|
||||||
|
value = this.replaceGetvar(value);
|
||||||
|
}
|
||||||
|
this.indexMacros(this.index - value.length, value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexMacros(offset, text) {
|
||||||
|
const re = /{{(?:((?:(?!}})[^\s:])+[\s:]*)((?:(?!}}).)*)(}}|}$|$))?/s;
|
||||||
|
let remaining = text;
|
||||||
|
let localOffset = 0;
|
||||||
|
while (remaining.length > 0 && re.test(remaining)) {
|
||||||
|
const match = re.exec(remaining);
|
||||||
|
this.macroIndex.push({
|
||||||
|
start: offset + localOffset + match.index,
|
||||||
|
end: offset + localOffset + match.index + (match[0]?.length ?? 0),
|
||||||
|
name: match[1] ?? '',
|
||||||
|
});
|
||||||
|
localOffset += match.index + (match[0]?.length ?? 0);
|
||||||
|
remaining = remaining.slice(match.index + (match[0]?.length ?? 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
50
public/scripts/slash-commands/SlashCommandParserError.js
Normal file
50
public/scripts/slash-commands/SlashCommandParserError.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
export class SlashCommandParserError extends Error {
|
||||||
|
/**@type {String}*/ text;
|
||||||
|
/**@type {Number}*/ index;
|
||||||
|
|
||||||
|
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(message, text, index) {
|
||||||
|
super(message);
|
||||||
|
this.text = text;
|
||||||
|
this.index = index;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||||
|
|
||||||
|
export class SlashCommandQuickReplyAutoCompleteOption extends AutoCompleteOption {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
constructor(name) {
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderItem() {
|
||||||
|
let li;
|
||||||
|
li = this.makeItem(this.name, 'QR', true);
|
||||||
|
li.setAttribute('data-name', this.name);
|
||||||
|
li.setAttribute('data-option-type', 'qr');
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderDetails() {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const specs = document.createElement('div'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('div'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = this.name;
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
frag.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
help.textContent = 'Quick Reply';
|
||||||
|
frag.append(help);
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
114
public/scripts/slash-commands/SlashCommandScope.js
Normal file
114
public/scripts/slash-commands/SlashCommandScope.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
|
||||||
|
export class SlashCommandScope {
|
||||||
|
/**@type {string[]}*/ variableNames = [];
|
||||||
|
get allVariableNames() {
|
||||||
|
const names = [...this.variableNames, ...(this.parent?.allVariableNames ?? [])];
|
||||||
|
return names.filter((it,idx)=>idx == names.indexOf(it));
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {object.<string, string|SlashCommandClosure>}*/ variables = {};
|
||||||
|
// @ts-ignore
|
||||||
|
/**@type {object.<string, string|SlashCommandClosure>}*/ macros = {};
|
||||||
|
/**@type {{key:string, value:string|SlashCommandClosure}[]} */
|
||||||
|
get macroList() {
|
||||||
|
return [...Object.keys(this.macros).map(key=>({ key, value:this.macros[key] })), ...(this.parent?.macroList ?? [])];
|
||||||
|
}
|
||||||
|
/**@type {SlashCommandScope}*/ parent;
|
||||||
|
/**@type {string}*/ #pipe;
|
||||||
|
get pipe() {
|
||||||
|
return this.#pipe ?? this.parent?.pipe;
|
||||||
|
}
|
||||||
|
set pipe(value) {
|
||||||
|
this.#pipe = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
constructor(parent) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCopy() {
|
||||||
|
const scope = new SlashCommandScope(this.parent);
|
||||||
|
scope.variableNames = [...this.variableNames];
|
||||||
|
scope.variables = Object.assign({}, this.variables);
|
||||||
|
scope.macros = Object.assign({}, this.macros);
|
||||||
|
scope.#pipe = this.#pipe;
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
setMacro(key, value) {
|
||||||
|
this.macros[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
existsVariableInScope(key) {
|
||||||
|
return Object.keys(this.variables).includes(key);
|
||||||
|
}
|
||||||
|
existsVariable(key) {
|
||||||
|
return Object.keys(this.variables).includes(key) || this.parent?.existsVariable(key);
|
||||||
|
}
|
||||||
|
letVariable(key, value = undefined) {
|
||||||
|
if (this.existsVariableInScope(key)) throw new SlashCommandScopeVariableExistsError(`Variable named "${key}" already exists.`);
|
||||||
|
this.variables[key] = value;
|
||||||
|
}
|
||||||
|
setVariable(key, value, index = null) {
|
||||||
|
if (this.existsVariableInScope(key)) {
|
||||||
|
if (index !== null && index !== undefined) {
|
||||||
|
let v = this.variables[key];
|
||||||
|
try {
|
||||||
|
v = JSON.parse(v);
|
||||||
|
const numIndex = Number(index);
|
||||||
|
if (Number.isNaN(numIndex)) {
|
||||||
|
v[index] = value;
|
||||||
|
} else {
|
||||||
|
v[numIndex] = value;
|
||||||
|
}
|
||||||
|
v = JSON.stringify(v);
|
||||||
|
} catch {
|
||||||
|
v[index] = value;
|
||||||
|
}
|
||||||
|
this.variables[key] = v;
|
||||||
|
} else {
|
||||||
|
this.variables[key] = value;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (this.parent) {
|
||||||
|
return this.parent.setVariable(key, value, index);
|
||||||
|
}
|
||||||
|
throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
|
||||||
|
}
|
||||||
|
getVariable(key, index = null) {
|
||||||
|
if (this.existsVariableInScope(key)) {
|
||||||
|
if (index !== null && index !== undefined) {
|
||||||
|
let v = this.variables[key];
|
||||||
|
try { v = JSON.parse(v); } catch { /* empty */ }
|
||||||
|
const numIndex = Number(index);
|
||||||
|
if (Number.isNaN(numIndex)) {
|
||||||
|
v = v[index];
|
||||||
|
} else {
|
||||||
|
v = v[numIndex];
|
||||||
|
}
|
||||||
|
if (typeof v == 'object') return JSON.stringify(v);
|
||||||
|
return v;
|
||||||
|
} else {
|
||||||
|
const value = this.variables[key];
|
||||||
|
return (value === '' || isNaN(Number(value))) ? (value || '') : Number(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.parent) {
|
||||||
|
return this.parent.getVariable(key, index);
|
||||||
|
}
|
||||||
|
throw new SlashCommandScopeVariableNotFoundError(`No such variable: "${key}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandScopeVariableExistsError extends Error {}
|
||||||
|
|
||||||
|
|
||||||
|
export class SlashCommandScopeVariableNotFoundError extends Error {}
|
@ -0,0 +1,11 @@
|
|||||||
|
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||||
|
|
||||||
|
export class SlashCommandUnnamedArgumentAssignment {
|
||||||
|
/**@type {number}*/ start;
|
||||||
|
/**@type {number}*/ end;
|
||||||
|
/**@type {string|SlashCommandClosure}*/ value;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import { AutoCompleteOption } from '../autocomplete/AutoCompleteOption.js';
|
||||||
|
|
||||||
|
export class SlashCommandVariableAutoCompleteOption extends AutoCompleteOption {
|
||||||
|
/**
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
constructor(name) {
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderItem() {
|
||||||
|
let li;
|
||||||
|
li = this.makeItem(this.name, '[𝑥]', true);
|
||||||
|
li.setAttribute('data-name', this.name);
|
||||||
|
li.setAttribute('data-option-type', 'variable');
|
||||||
|
return li;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
renderDetails() {
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
const specs = document.createElement('div'); {
|
||||||
|
specs.classList.add('specs');
|
||||||
|
const name = document.createElement('div'); {
|
||||||
|
name.classList.add('name');
|
||||||
|
name.classList.add('monospace');
|
||||||
|
name.textContent = this.name;
|
||||||
|
specs.append(name);
|
||||||
|
}
|
||||||
|
frag.append(specs);
|
||||||
|
}
|
||||||
|
const help = document.createElement('span'); {
|
||||||
|
help.classList.add('help');
|
||||||
|
help.textContent = 'scoped variable';
|
||||||
|
frag.append(help);
|
||||||
|
}
|
||||||
|
return frag;
|
||||||
|
}
|
||||||
|
}
|
@ -74,6 +74,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div><small>Local variables = unique to the current chat</small></div>
|
<div><small>Local variables = unique to the current chat</small></div>
|
||||||
<div><small>Global variables = works in any chat for any character</small></div>
|
<div><small>Global variables = works in any chat for any character</small></div>
|
||||||
|
<div><small>Scoped variables = works in STscript</small></div>
|
||||||
<ul>
|
<ul>
|
||||||
<li><tt>{{getvar::name}}</tt> – replaced with the value of the local variable "name"</li>
|
<li><tt>{{getvar::name}}</tt> – replaced with the value of the local variable "name"</li>
|
||||||
<li><tt>{{setvar::name::value}}</tt> – replaced with empty string, sets the local variable "name" to "value"</li>
|
<li><tt>{{setvar::name::value}}</tt> – replaced with empty string, sets the local variable "name" to "value"</li>
|
||||||
@ -85,4 +86,6 @@
|
|||||||
<li><tt>{{addglobalvar::name::value}}</tt> – replaced with empty string, adds a numeric value of "increment" to the global variable "name"</li>
|
<li><tt>{{addglobalvar::name::value}}</tt> – replaced with empty string, adds a numeric value of "increment" to the global variable "name"</li>
|
||||||
<li><tt>{{incglobalvar::name}}</tt> – replaced with the result of the increment of value of the global variable "name" by 1</li>
|
<li><tt>{{incglobalvar::name}}</tt> – replaced with the result of the increment of value of the global variable "name" by 1</li>
|
||||||
<li><tt>{{decglobalvar::name}}</tt> – replaced with the result of the decrement of value of the global variable "name" by 1</li>
|
<li><tt>{{decglobalvar::name}}</tt> – replaced with the result of the decrement of value of the global variable "name" by 1</li>
|
||||||
|
<li><tt>{{var::name}}</tt> – replaced with the value of the scoped variable "name"</li>
|
||||||
|
<li><tt>{{var::name::index}}</tt> – replaced with the value of item at index (for arrays / lists or objects / dictionaries) of the scoped variable "name"</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,6 @@ import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_meta
|
|||||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight } from './utils.js';
|
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight } from './utils.js';
|
||||||
import { extension_settings, getContext } from './extensions.js';
|
import { extension_settings, getContext } from './extensions.js';
|
||||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||||
import { registerSlashCommand } from './slash-commands.js';
|
|
||||||
import { isMobile } from './RossAscends-mods.js';
|
import { isMobile } from './RossAscends-mods.js';
|
||||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||||
import { getTokenCountAsync } from './tokenizers.js';
|
import { getTokenCountAsync } from './tokenizers.js';
|
||||||
@ -11,6 +10,9 @@ import { getTagKeyForEntity } from './tags.js';
|
|||||||
import { resolveVariable } from './variables.js';
|
import { resolveVariable } from './variables.js';
|
||||||
import { debounce_timeout } from './constants.js';
|
import { debounce_timeout } from './constants.js';
|
||||||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||||
|
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||||
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
world_info,
|
world_info,
|
||||||
@ -674,12 +676,161 @@ function registerWorldInfoSlashCommands() {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
registerSlashCommand('world', onWorldInfoChange, [], '<span class="monospace">[optional state=off|toggle] [optional silent=true] (optional name)</span> – sets active World, or unsets if no args provided, use <code>state=off</code> and <code>state=toggle</code> to deactivate or toggle a World, use <code>silent=true</code> to suppress toast messages', true, true);
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'world',
|
||||||
registerSlashCommand('getchatbook', getChatBookCallback, ['getchatlore', 'getchatwi'], '– get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe', true, true);
|
callback: onWorldInfoChange,
|
||||||
registerSlashCommand('findentry', findBookEntryCallback, ['findlore', 'findwi'], '<span class="monospace">(file=bookName field=field [texts])</span> – find a UID of the record from the specified book using the fuzzy match of a field value (default: key) and pass it down the pipe, e.g. <tt>/findentry file=chatLore field=key Shadowfang</tt>', true, true);
|
namedArgumentList: [
|
||||||
registerSlashCommand('getentryfield', getEntryFieldCallback, ['getlorefield', 'getwifield'], '<span class="monospace">(file=bookName field=field [UID])</span> – get a field value (default: content) of the record with the UID from the specified book and pass it down the pipe, e.g. <tt>/getentryfield file=chatLore field=content 123</tt>', true, true);
|
new SlashCommandNamedArgument(
|
||||||
registerSlashCommand('createentry', createEntryCallback, ['createlore', 'createwi'], '<span class="monospace">(file=bookName key=key [content])</span> – create a new record in the specified book with the key and content (both are optional) and pass the UID down the pipe, e.g. <tt>/createentry file=chatLore key=Shadowfang The sword of the king</tt>', true, true);
|
'state', 'set world state', [ARGUMENT_TYPE.STRING], false, false, null, ['off', 'toggle'],
|
||||||
registerSlashCommand('setentryfield', setEntryFieldCallback, ['setlorefield', 'setwifield'], '<span class="monospace">(file=bookName uid=UID field=field [value])</span> – set a field value (default: content) of the record with the UID from the specified book. To set multiple values for key fields, use comma-delimited list as a value, e.g. <tt>/setentryfield file=chatLore uid=123 field=key Shadowfang,sword,weapon</tt>', true, true);
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'silent', 'suppress toast messages', [ARGUMENT_TYPE.BOOLEAN], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'name', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Sets active World, or unsets if no args provided, use <code>state=off</code> and <code>state=toggle</code> to deactivate or toggle a World, use <code>silent=true</code> to suppress toast messages.
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
aliases: [],
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getchatbook',
|
||||||
|
callback: getChatBookCallback,
|
||||||
|
returns: 'lorebook name',
|
||||||
|
helpString: 'Get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe.',
|
||||||
|
aliases: ['getchatlore', 'getchatwi'],
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'findentry',
|
||||||
|
aliases: ['findlore', 'findwi'],
|
||||||
|
returns: 'UID',
|
||||||
|
callback: findBookEntryCallback,
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'file', 'bookName', ARGUMENT_TYPE.STRING, true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'field', 'field value for fuzzy match (default: key)', ARGUMENT_TYPE.STRING, false, false, 'key',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'texts', ARGUMENT_TYPE.STRING, true, true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Find a UID of the record from the specified book using the fuzzy match of a field value (default: key) and pass it down the pipe.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/findentry file=chatLore field=key Shadowfang</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'getentryfield',
|
||||||
|
aliases: ['getlorefield', 'getwifield'],
|
||||||
|
callback: getEntryFieldCallback,
|
||||||
|
returns: 'field value',
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'file', 'bookName', ARGUMENT_TYPE.STRING, true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'field', 'field to retrieve (default: content)', ARGUMENT_TYPE.STRING, false, false, 'content',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'UID', ARGUMENT_TYPE.STRING, true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Get a field value (default: content) of the record with the UID from the specified book and pass it down the pipe.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/getentryfield file=chatLore field=content 123</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'createentry',
|
||||||
|
callback: createEntryCallback,
|
||||||
|
aliases: ['createlore', 'createwi'],
|
||||||
|
returns: 'UID of the new record',
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'file', 'book name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'key', 'record key', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'content', [ARGUMENT_TYPE.STRING], false,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Create a new record in the specified book with the key and content (both are optional) and pass the UID down the pipe.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/createentry file=chatLore key=Shadowfang The sword of the king</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'setentryfield',
|
||||||
|
callback: setEntryFieldCallback,
|
||||||
|
aliases: ['setlorefield', 'setwifield'],
|
||||||
|
namedArgumentList: [
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'file', 'book name', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'uid', 'record UID', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
new SlashCommandNamedArgument(
|
||||||
|
'field', 'field name', [ARGUMENT_TYPE.STRING], true, false, 'content',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
unnamedArgumentList: [
|
||||||
|
new SlashCommandArgument(
|
||||||
|
'value', [ARGUMENT_TYPE.STRING], true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
helpString: `
|
||||||
|
<div>
|
||||||
|
Set a field value (default: content) of the record with the UID from the specified book. To set multiple values for key fields, use comma-delimited list as a value.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Example:</strong>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<pre><code>/setentryfield file=chatLore uid=123 field=key Shadowfang,sword,weapon</code></pre>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// World Info Editor
|
// World Info Editor
|
||||||
|
843
public/style.css
843
public/style.css
@ -76,6 +76,7 @@
|
|||||||
/*base variable calculated in rems*/
|
/*base variable calculated in rems*/
|
||||||
--fontScale: 1;
|
--fontScale: 1;
|
||||||
--mainFontSize: calc(var(--fontScale) * 15px);
|
--mainFontSize: calc(var(--fontScale) * 15px);
|
||||||
|
--mainFontFamily: "Noto Sans", "Noto Color Emoji", sans-serif;
|
||||||
|
|
||||||
/* base variable for blur strength slider calculations */
|
/* base variable for blur strength slider calculations */
|
||||||
--blurStrength: 10;
|
--blurStrength: 10;
|
||||||
@ -133,7 +134,7 @@ body {
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
|
font-family: var(--mainFontFamily);
|
||||||
font-size: var(--mainFontSize);
|
font-size: var(--mainFontSize);
|
||||||
color: var(--SmartThemeBodyColor);
|
color: var(--SmartThemeBodyColor);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -626,7 +627,6 @@ body .panelControlBar {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
column-gap: 5px;
|
column-gap: 5px;
|
||||||
font-size: var(--bottomFormIconSize);
|
font-size: var(--bottomFormIconSize);
|
||||||
overflow: hidden;
|
|
||||||
order: 25;
|
order: 25;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@ -688,6 +688,64 @@ body .panelControlBar {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#form_sheld.isExecutingCommandsFromChatInput {
|
||||||
|
#send_but {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
#rightSendForm > div:not(.mes_send).stscript_btn {
|
||||||
|
&.stscript_pause, &.stscript_stop {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.paused {
|
||||||
|
#rightSendForm > div:not(.mes_send).stscript_btn {
|
||||||
|
&.stscript_pause {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&.stscript_continue {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#rightSendForm > div:not(.mes_send) {
|
||||||
|
&.stscript_btn {
|
||||||
|
padding-right: 2px;
|
||||||
|
place-self: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.3s;
|
||||||
|
opacity: 1;
|
||||||
|
display: none;
|
||||||
|
&.stscript_pause > .fa-solid {
|
||||||
|
background-color: rgb(146, 190, 252);
|
||||||
|
}
|
||||||
|
&.stscript_continue > .fa-solid {
|
||||||
|
background-color: rgb(146, 190, 252);
|
||||||
|
}
|
||||||
|
&.stscript_stop > .fa-solid {
|
||||||
|
background-color: rgb(215, 136, 114);
|
||||||
|
}
|
||||||
|
> .fa-solid {
|
||||||
|
--toastInfoColor: #2F96B4;
|
||||||
|
--progColor: rgba(0, 128, 0, 0.839);
|
||||||
|
border-radius: 35%;
|
||||||
|
border: 0 solid var(--progColor);
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
display: flex;
|
||||||
|
color: rgb(24 24 24);
|
||||||
|
font-size: 0.5em;
|
||||||
|
height: var(--bottomFormIconSize);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 var(--progColor),
|
||||||
|
0 0 0 var(--progColor)
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#options_button {
|
#options_button {
|
||||||
width: var(--bottomFormBlockSize);
|
width: var(--bottomFormBlockSize);
|
||||||
height: var(--bottomFormBlockSize);
|
height: var(--bottomFormBlockSize);
|
||||||
@ -1054,8 +1112,8 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#send_textarea {
|
#send_textarea {
|
||||||
min-height: var(--bottomFormBlockSize);
|
min-height: calc(var(--bottomFormBlockSize) + 3px);
|
||||||
height: var(--bottomFormBlockSize);
|
height: calc(var(--bottomFormBlockSize) + 3px);
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
max-height: 50svh;
|
max-height: 50svh;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@ -1070,6 +1128,683 @@ select {
|
|||||||
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
|
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
order: 3;
|
order: 3;
|
||||||
|
|
||||||
|
--progColor: rgb(146, 190, 252);
|
||||||
|
--progFlashColor: rgb(215, 136, 114);
|
||||||
|
--progSuccessColor: rgb(81, 163, 81);
|
||||||
|
--progErrorColor: rgb(189, 54, 47);
|
||||||
|
--progAbortedColor: rgb(215, 136, 114);
|
||||||
|
--progWidth: 3px;
|
||||||
|
--progWidthClip: calc(var(--progWidth) + 2px);
|
||||||
|
--prog: 0%;
|
||||||
|
--progDone: 0;
|
||||||
|
border-top: var(--progWidth) solid var(--progColor);
|
||||||
|
clip-path: polygon(
|
||||||
|
0% calc(var(--progDone) * var(--progWidthClip)),
|
||||||
|
var(--prog) calc(var(--progDone) * var(--progWidthClip)),
|
||||||
|
var(--prog) var(--progWidthClip),
|
||||||
|
100% var(--progWidthClip),
|
||||||
|
100% 100%,
|
||||||
|
0% 100%
|
||||||
|
);
|
||||||
|
transition: clip-path 200ms;
|
||||||
|
}
|
||||||
|
@keyframes script_progress_pulse {
|
||||||
|
0%, 100% {
|
||||||
|
border-top-color: var(--progColor);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-top-color: var(--progFlashColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#form_sheld.isExecutingCommandsFromChatInput.script_paused #send_textarea {
|
||||||
|
animation-name: script_progress_pulse;
|
||||||
|
animation-duration: 1500ms;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
animation-delay: 0s;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#form_sheld.script_success #send_textarea {
|
||||||
|
border-top-color: var(--progSuccessColor);
|
||||||
|
}
|
||||||
|
#form_sheld.script_error #send_textarea {
|
||||||
|
border-top-color: var(--progErrorColor);
|
||||||
|
}
|
||||||
|
#form_sheld.script_aborted #send_textarea {
|
||||||
|
border-top-color: var(--progAbortedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoComplete-wrap {
|
||||||
|
--targetOffset: 0;
|
||||||
|
--direction: column;
|
||||||
|
--leftOffset: 1vw;
|
||||||
|
--rightOffset: 1vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: var(--direction);
|
||||||
|
position: absolute;
|
||||||
|
left: var(--leftOffset);
|
||||||
|
right: var(--rightOffset);
|
||||||
|
z-index: 10000;
|
||||||
|
|
||||||
|
|
||||||
|
&.isFloating {
|
||||||
|
--direction: row;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
flex: 0 1 calc(var(--targetOffset) * 1px);
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.autoComplete {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 50vw;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoComplete-detailsWrap {
|
||||||
|
--targetOffset: 0;
|
||||||
|
--rightOffset: 1vw;
|
||||||
|
--bottomOffset: 0;
|
||||||
|
--leftOffset: 74vw;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: absolute;
|
||||||
|
top: 5vh;
|
||||||
|
right: var(--rightOffset);
|
||||||
|
bottom: var(--bottomOffset);
|
||||||
|
left: var(--leftOffset);
|
||||||
|
z-index: 10000;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
flex: 0 1 calc(var(--targetOffset) * 1px - 5vh);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.autoComplete-details {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-height: 80vh;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isFloating {
|
||||||
|
flex-direction: row;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
.autoComplete-details {
|
||||||
|
max-height: unset;
|
||||||
|
width: 25vw;
|
||||||
|
}
|
||||||
|
&.left {
|
||||||
|
&:before {
|
||||||
|
flex: 0 1 calc(var(--targetOffset) * 1px - 25vw);
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
max-width: 50vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
&:before {
|
||||||
|
flex: 0 0 calc(var(--targetOffset) * 1px + 50vw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.full {
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
flex: 0 1 calc(var(--targetOffset) * 1px);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.autoComplete-details {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-width: 50vw;
|
||||||
|
width: unset;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body[data-stscript-style="dark"] {
|
||||||
|
--ac-style-color-border: rgba(69 69 69 / 1);
|
||||||
|
--ac-style-color-background: rgba(32 32 32 / 1);
|
||||||
|
--ac-style-color-text: rgba(204 204 204 / 1);
|
||||||
|
--ac-style-color-matchedBackground: rgba(0 0 0 / 0);
|
||||||
|
--ac-style-color-matchedText: rgba(108 171 251 / 1);
|
||||||
|
--ac-style-color-selectedBackground: rgba(32 57 92 / 1);
|
||||||
|
--ac-style-color-selectedText: rgba(255 255 255 / 1);
|
||||||
|
--ac-style-color-hoveredBackground: rgba(43 45 46 / 1);
|
||||||
|
--ac-style-color-hoveredText: rgba(204 204 204 / 1);
|
||||||
|
--ac-style-color-argName: rgba(171 209 239 / 1);
|
||||||
|
--ac-style-color-type: rgba(131 199 177 / 1);
|
||||||
|
--ac-style-color-cmd: rgba(219 219 173 / 1);
|
||||||
|
--ac-style-color-symbol: rgba(115 156 211 / 1);
|
||||||
|
--ac-style-color-string: rgba(190 146 122 / 1);
|
||||||
|
--ac-style-color-number: rgba(188 205 170 / 1);
|
||||||
|
--ac-style-color-variable: rgba(131 193 252 / 1);
|
||||||
|
--ac-style-color-variableLanguage: rgba(98 160 251 / 1);
|
||||||
|
--ac-style-color-punctuation: rgba(242 214 48 / 1);
|
||||||
|
--ac-style-color-punctuationL1: rgba(195 118 210 / 1);
|
||||||
|
--ac-style-color-punctuationL2: rgba(98 160 251 / 1);
|
||||||
|
--ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
|
||||||
|
--ac-style-color-comment: rgba(122 151 90 / 1);
|
||||||
|
}
|
||||||
|
body[data-stscript-style="light"] {
|
||||||
|
--ac-style-color-border: rgba(200 200 200 / 1);
|
||||||
|
--ac-style-color-background: rgba(248 248 248 / 1);
|
||||||
|
--ac-style-color-text: rgba(59 59 59 / 1);
|
||||||
|
--ac-style-color-matchedBackground: rgba(0 0 0 / 0);
|
||||||
|
--ac-style-color-matchedText: rgba(61 104 188 / 1);
|
||||||
|
--ac-style-color-selectedBackground: rgba(232 232 232 / 1);
|
||||||
|
--ac-style-color-selectedText: rgba(0 0 0 / 1);
|
||||||
|
--ac-style-color-hoveredBackground: rgba(242 242 242 / 1);
|
||||||
|
--ac-style-color-hoveredText: rgba(59 59 59 / 1);
|
||||||
|
--ac-style-color-argName: rgba(16 24 125 / 1);
|
||||||
|
--ac-style-color-type: rgba(80 127 152 / 1);
|
||||||
|
--ac-style-color-cmd: rgba(113 94 43 / 1);
|
||||||
|
--ac-style-color-symbol: rgba(36 37 249 / 1);
|
||||||
|
--ac-style-color-string: rgba(139 31 24 / 1);
|
||||||
|
--ac-style-color-number: rgba(76 132 91 / 1);
|
||||||
|
--ac-style-color-variable: rgba(16 24 125 / 1);
|
||||||
|
--ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
|
||||||
|
--ac-style-color-comment: rgba(70 126 26 / 1);
|
||||||
|
}
|
||||||
|
body[data-stscript-style="theme"] {
|
||||||
|
--ac-style-color-border: var(--SmartThemeBorderColor);
|
||||||
|
--ac-style-color-background: var(--SmartThemeBlurTintColor);
|
||||||
|
--ac-style-color-text: var(--SmartThemeEmColor);
|
||||||
|
--ac-style-color-matchedBackground: rgba(0 0 0 / 0);
|
||||||
|
--ac-style-color-matchedText: var(--SmartThemeQuoteColor);
|
||||||
|
--ac-style-color-selectedBackground: color-mix(in srgb, rgb(128 128 128) 75%, var(--SmartThemeChatTintColor));
|
||||||
|
--ac-style-color-selectedText: var(--SmartThemeBodyColor);
|
||||||
|
--ac-style-color-hoveredBackground: color-mix(in srgb, rgb(128 128 128) 30%, var(--SmartThemeChatTintColor));
|
||||||
|
--ac-style-color-hoveredText: var(--SmartThemeEmColor);
|
||||||
|
--ac-style-color-argName: rgba(171 209 239 / 1);
|
||||||
|
--ac-style-color-type: rgba(131 199 177 / 1);
|
||||||
|
--ac-style-color-cmd: rgba(219 219 173 / 1);
|
||||||
|
--ac-style-color-symbol: rgba(115 156 211 / 1);
|
||||||
|
--ac-style-color-string: rgba(190 146 122 / 1);
|
||||||
|
--ac-style-color-variable: rgba(131 193 252 / 1);
|
||||||
|
--ac-style-color-currentParenthesis: rgba(195 118 210 / 1);
|
||||||
|
--ac-style-color-comment: rgba(122 151 90 / 1);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
--ac-font-scale: 0.8;
|
||||||
|
}
|
||||||
|
.autoComplete, .autoComplete-details {
|
||||||
|
--ac-color-border: var(--ac-style-color-border, rgba(69 69 69 / 1));
|
||||||
|
--ac-color-background: var(--ac-style-color-background, rgba(32 32 32 / 1));
|
||||||
|
--ac-color-text: var(--ac-style-color-text, rgba(204 204 204 / 1));
|
||||||
|
--ac-color-matchedBackground: var(--ac-style-color-matchedBackground, rgba(0 0 0 / 0));
|
||||||
|
--ac-color-matchedText: var(--ac-style-color-matchedText, rgba(108 171 251 / 1));
|
||||||
|
--ac-color-selectedBackground: var(--ac-style-color-selectedBackground, rgba(32 57 92 / 1));
|
||||||
|
--ac-color-selectedText: var(--ac-style-color-selectedText, rgba(255 255 255 / 1));
|
||||||
|
--ac-color-hoveredBackground: var(--ac-style-color-hoveredBackground, rgba(43 45 46 / 1));
|
||||||
|
--ac-color-hoveredText: var(--ac-style-color-hoveredText, rgba(204 204 204 / 1));
|
||||||
|
--ac-color-argName: var(--ac-style-color-argName, rgba(171 209 239 / 1));
|
||||||
|
--ac-color-type: var(--ac-style-color-type, rgba(131 199 177 / 1));
|
||||||
|
--ac-color-cmd: var(--ac-style-color-cmd, rgba(219 219 173 / 1));
|
||||||
|
--ac-color-symbol: var(--ac-style-color-symbol, rgba(115 156 211 / 1));
|
||||||
|
--ac-color-string: var(--ac-style-color-string, rgba(190 146 122 / 1));
|
||||||
|
--ac-color-number: var(--ac-style-color-number, rgba(188 205 170 / 1));
|
||||||
|
--ac-color-variable: var(--ac-style-color-variable, rgba(131 193 252 / 1));
|
||||||
|
--ac-color-variableLanguage: var(--ac-style-color-variableLanguage, rgba(98 160 251 / 1));
|
||||||
|
--ac-color-punctuation: var(--ac-style-color-punctuation, rgba(242 214 48 / 1));
|
||||||
|
--ac-color-punctuationL1: var(--ac-style-color-punctuationL1, rgba(195 118 210 / 1));
|
||||||
|
--ac-color-punctuationL2: var(--ac-style-color-punctuationL2, rgba(98 160 251 / 1));
|
||||||
|
--ac-color-currentParenthesis: var(--ac-style-color-currentParenthesis, rgba(195 118 210 / 1));
|
||||||
|
--ac-color-comment: var(--ac-style-color-comment, rgba(122 151 90 / 1));
|
||||||
|
|
||||||
|
font-size: calc(var(--ac-font-scale) * 1em);
|
||||||
|
|
||||||
|
--bottom: 50vh;
|
||||||
|
background: var(--ac-color-background);
|
||||||
|
backdrop-filter: blur(var(--SmartThemeBlurStrength));
|
||||||
|
border: 1px solid var(--ac-color-border);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
max-height: calc(95vh - var(--bottom));
|
||||||
|
list-style: none;
|
||||||
|
margin: 0px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: left;
|
||||||
|
z-index: 10000;
|
||||||
|
* { text-shadow: none; }
|
||||||
|
}
|
||||||
|
body[data-stscript-style] .autoComplete [data-option-type] {
|
||||||
|
&[data-option-type="enum"] .type { color: var(--ac-color-string); }
|
||||||
|
&[data-option-type="command"] .type { color: var(--ac-color-cmd); }
|
||||||
|
&[data-option-type="namedArgument"] .type { color: var(--ac-color-argName); }
|
||||||
|
&[data-option-type="variable"] .type { color: var(--ac-color-punctuationL1); }
|
||||||
|
&[data-option-type="qr"] .type { color: var(--ac-color-variable); }
|
||||||
|
&[data-option-type="macro"] .type { color: var(--ac-color-variableLanguage); }
|
||||||
|
}
|
||||||
|
body[data-stscript-style] .hljs.language-stscript {
|
||||||
|
* { text-shadow: none !important; }
|
||||||
|
text-shadow: none !important;
|
||||||
|
|
||||||
|
background-color: var(--ac-style-color-background);
|
||||||
|
color: var(--ac-style-color-text);
|
||||||
|
|
||||||
|
.hljs-title.function_ { color: var(--ac-style-color-cmd); }
|
||||||
|
.hljs-title.function_.invoke__ { color: var(--ac-style-color-cmd); }
|
||||||
|
.hljs-string { color: var(--ac-style-color-string); }
|
||||||
|
.hljs-number { color: var(--ac-style-color-number); }
|
||||||
|
.hljs-variable { color: var(--ac-style-color-variable); }
|
||||||
|
.hljs-variable.language_ { color: var(--ac-style-color-variableLanguage); }
|
||||||
|
.hljs-property { color: var(--ac-style-color-argName); }
|
||||||
|
.hljs-punctuation { color: var(--ac-style-color-punctuation); }
|
||||||
|
.hljs-keyword { color: var(--ac-style-color-variableLanguage); }
|
||||||
|
.hljs-comment { color: var(--ac-style-color-comment); }
|
||||||
|
.hljs-abort { color: var(--ac-style-color-abort, #e38e23); }
|
||||||
|
|
||||||
|
.hljs-closure {
|
||||||
|
> .hljs-punctuation { color: var(--ac-style-color-punctuation); }
|
||||||
|
.hljs-closure {
|
||||||
|
> .hljs-punctuation { color: var(--ac-style-color-punctuationL1); }
|
||||||
|
.hljs-closure {
|
||||||
|
> .hljs-punctuation { color: var(--ac-style-color-punctuationL2); }
|
||||||
|
.hljs-closure {
|
||||||
|
> .hljs-punctuation { color: var(--ac-style-color-punctuation); }
|
||||||
|
.hljs-closure {
|
||||||
|
> .hljs-punctuation { color: var(--ac-style-color-punctuationL1); }
|
||||||
|
.hljs-closure {
|
||||||
|
> .hljs-punctuation { color: var(--ac-style-color-punctuationL2); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoComplete {
|
||||||
|
padding-bottom: 1px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0fr auto minmax(50%, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
max-height: calc(95vh - var(--bottom));
|
||||||
|
container-type: inline-size;
|
||||||
|
> .item {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 3px;
|
||||||
|
text-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
display: contents;
|
||||||
|
@container (max-width: 80em) {
|
||||||
|
.specs {
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
}
|
||||||
|
> .help {
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
padding-left: 1em;
|
||||||
|
opacity: 0.75;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.blank {
|
||||||
|
display: block;
|
||||||
|
grid-column: 1 / 4;
|
||||||
|
}
|
||||||
|
&:hover > * {
|
||||||
|
background-color: var(--ac-color-hoveredBackground);
|
||||||
|
color: var(--ac-color-hoveredText);
|
||||||
|
}
|
||||||
|
&.selected > * {
|
||||||
|
background-color: var(--ac-color-selectedBackground);
|
||||||
|
color: var(--ac-color-selectedText);
|
||||||
|
}
|
||||||
|
> * {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
> *+* {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .type {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.25em;
|
||||||
|
font-size: 0.8em;
|
||||||
|
text-align: center;
|
||||||
|
/* opacity: 0.6; */
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: calc(1.2em / 0.8);
|
||||||
|
/* &:before { content: "["; }
|
||||||
|
&:after { content: "]"; } */
|
||||||
|
}
|
||||||
|
> .specs {
|
||||||
|
align-items: flex-start;
|
||||||
|
> .name {
|
||||||
|
> .matched {
|
||||||
|
background-color: var(--ac-color-matchedBackground);
|
||||||
|
color: var(--ac-color-matchedText);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .body {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
column-gap: 0.5em;
|
||||||
|
> .arguments {
|
||||||
|
display: contents;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .help {
|
||||||
|
height: 100%;
|
||||||
|
> .helpContent {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.9em;white-space: nowrap;
|
||||||
|
line-height: 1.2;
|
||||||
|
max-height: 1.2em;
|
||||||
|
display: block;
|
||||||
|
> * {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.autoComplete-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
> .specs {
|
||||||
|
cursor: default;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0.25em 0.25em 0.5em 0.25em;
|
||||||
|
border-bottom: 1px solid var(--ac-color-border);
|
||||||
|
> .name {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
cursor: help;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: 1px dotted underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .body {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
> .arguments {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.25em;
|
||||||
|
> .argumentItem::marker {
|
||||||
|
color: color-mix(in srgb, var(--ac-color-text), var(--ac-style-color-background));
|
||||||
|
}
|
||||||
|
|
||||||
|
.argumentSpec {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
.argument-default {
|
||||||
|
&:before {
|
||||||
|
content: " = ";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
color: var(--ac-color-string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument {
|
||||||
|
cursor: help;
|
||||||
|
&:hover:not(:has(.argument-name:hover, .argument-types:hover, .argument-enums:hover)) {
|
||||||
|
text-decoration: 1px dotted underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.argument-name,
|
||||||
|
.argument-types,
|
||||||
|
.argument-enums,
|
||||||
|
.argument-default
|
||||||
|
{
|
||||||
|
cursor: help;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: 1px dotted underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.argument.optional + .argument-description:before,
|
||||||
|
.argumentSpec:has(.argument.optional) + .argument-description:before
|
||||||
|
{
|
||||||
|
content: "(optional) ";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.argument-description {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
font-family: var(--mainFontFamily);
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.returns {
|
||||||
|
cursor: help;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: 1px dotted underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .help {
|
||||||
|
padding: 0 0.5em 0.5em 0.5em;
|
||||||
|
div {
|
||||||
|
margin-block-end: 1em;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
> code {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .aliases {
|
||||||
|
padding: 0 0.5em 0.5em 0.5em;
|
||||||
|
&:before { content: '(alias: '; }
|
||||||
|
> .alias {
|
||||||
|
font-family: monospace;
|
||||||
|
&+.alias:before { content: ', '; }
|
||||||
|
}
|
||||||
|
&:after { content: ')'; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.autoComplete > .item, .autoComplete-details {
|
||||||
|
> .specs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
> .name {
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
/* color: var(--ac-color-text); */
|
||||||
|
}
|
||||||
|
> .body {
|
||||||
|
display: flex;
|
||||||
|
> .arguments {
|
||||||
|
font-family: monospace;
|
||||||
|
.argument {
|
||||||
|
white-space: nowrap;
|
||||||
|
&.namedArgument {
|
||||||
|
&:before {
|
||||||
|
content: "[";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "]";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
&.optional:after {
|
||||||
|
content: "]?";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
> .argument-name {
|
||||||
|
color: var(--ac-color-argName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.unnamedArgument {
|
||||||
|
&:before {
|
||||||
|
content: "(";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
&.multiple:before {
|
||||||
|
content: "...(";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: ")";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
&.optional:after {
|
||||||
|
content: ")?";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .argument-name + .argument-types:before {
|
||||||
|
content: "=";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
> .argument-types {
|
||||||
|
color: var(--ac-color-type);
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: break-spaces;
|
||||||
|
> .argument-type + .argument-type:before {
|
||||||
|
content: "|";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
> .argument-types + .argument-enums,
|
||||||
|
> .argument-name + .argument-enums
|
||||||
|
{
|
||||||
|
&:before {
|
||||||
|
content: "=";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .argument-enums {
|
||||||
|
color: var(--ac-color-string);
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: break-spaces;
|
||||||
|
> .argument-enum + .argument-enum:before {
|
||||||
|
content: "|";
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .returns {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--ac-color-text);
|
||||||
|
&:before {
|
||||||
|
content: "=> ";
|
||||||
|
color: var(--ac-color-symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
.autoComplete-wrap {
|
||||||
|
left: 1vw;
|
||||||
|
right: 1vw;
|
||||||
|
}
|
||||||
|
.autoComplete-detailsWrap:not(.full) {
|
||||||
|
left: 50vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.slashCommandBrowser {
|
||||||
|
> .search {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
align-items: baseline;
|
||||||
|
white-space: nowrap;
|
||||||
|
> .searchLabel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: baseline;
|
||||||
|
> .searchInput {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .searchOptions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .commandContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
container-type: inline-size;
|
||||||
|
> .autoComplete {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
max-height: unset;
|
||||||
|
> .isFiltered {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.specs {
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
}
|
||||||
|
.help {
|
||||||
|
grid-column: 2 / 4;
|
||||||
|
padding-left: 1em;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> .autoComplete-detailsWrap {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 30%;
|
||||||
|
position: static;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
flex: 0 1 calc(var(--targetOffset) * 1px);
|
||||||
|
}
|
||||||
|
> .autoComplete-details {
|
||||||
|
max-height: 50vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container (max-width: 1000px) {
|
||||||
|
> .autoComplete-detailsWrap {
|
||||||
|
width: 50%;
|
||||||
|
max-width: unset;
|
||||||
|
position: absolute;
|
||||||
|
left: unset;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#character_popup .editor_maximize {
|
#character_popup .editor_maximize {
|
||||||
@ -2625,6 +3360,97 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
background: var(--white100);
|
background: var(--white100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.doubleRangeContainer {
|
||||||
|
display: flex;
|
||||||
|
--markerWidth: 15px;
|
||||||
|
container-type: inline-size;
|
||||||
|
container-name: doubleRangeContainer;
|
||||||
|
> .doubleRangeInputContainer {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
> datalist {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: x-small;
|
||||||
|
> option {
|
||||||
|
flex: 0 0 0;
|
||||||
|
width: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@container doubleRangeContainer (max-width: 200px) {
|
||||||
|
> datalist {
|
||||||
|
height: 2.5em;
|
||||||
|
}
|
||||||
|
&:nth-child(1) > datalist > option {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
transform-origin: bottom right;
|
||||||
|
}
|
||||||
|
&:nth-child(2) > datalist > option {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
transform-origin: bottom left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> input::-webkit-slider-thumb {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
/* shifted to center to hide corners of the inset shadow */
|
||||||
|
--shift: 2px;
|
||||||
|
/* 100% of the input width (1em padding) */
|
||||||
|
--inputWidth: calc(100% - 1em);
|
||||||
|
/* input padding max(20px, 20%) */
|
||||||
|
--inputPadding: calc(max(20px, var(--inputWidth) * 0.2));
|
||||||
|
/* 100% of the variable range width: 100% of input - input padding - marker width) */
|
||||||
|
--rangeWidth: calc(var(--inputWidth) - var(--inputPadding) - var(--markerWidth));
|
||||||
|
/* always visible part of the input range (value = 0) */
|
||||||
|
--zeroOffset: calc(var(--inputPadding) + var(--markerWidth) / 2);
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 11px;
|
||||||
|
width: calc(var(--value) / 2 * var(--rangeWidth) + var(--zeroOffset) + var(--shift));
|
||||||
|
height: 5px;
|
||||||
|
background-color: var(--SmartThemeQuoteColor);
|
||||||
|
box-shadow: inset 0 0 2px black;
|
||||||
|
}
|
||||||
|
&:nth-child(1) {
|
||||||
|
--value: 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
> input {
|
||||||
|
direction: rtl;
|
||||||
|
position: relative;
|
||||||
|
padding-right: max(20px, 20%);
|
||||||
|
}
|
||||||
|
> datalist {
|
||||||
|
direction: rtl;
|
||||||
|
padding-right: calc(var(--markerWidth)/2 + max(20px, 20%));
|
||||||
|
padding-left: calc(var(--markerWidth)/2 - 2px);
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
right: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
--value: 0;
|
||||||
|
padding-right: 1em;
|
||||||
|
> input {
|
||||||
|
position: relative;
|
||||||
|
padding-left: max(20px, 20%);
|
||||||
|
}
|
||||||
|
> datalist {
|
||||||
|
padding-left: calc(var(--markerWidth)/2 + max(20px, 20%));
|
||||||
|
padding-right: calc(var(--markerWidth)/2 - 2px);
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
left: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*Notes '?' links*/
|
/*Notes '?' links*/
|
||||||
|
|
||||||
.note-link-span {
|
.note-link-span {
|
||||||
@ -3441,6 +4267,15 @@ a {
|
|||||||
transition: all 250ms;
|
transition: all 250ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#user-settings-block [name="MiscellaneousToggles"],
|
||||||
|
#CustomCSS-block,
|
||||||
|
#CustomCSS-textAreaBlock {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
#customCSS {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
#ui_language_select {
|
#ui_language_select {
|
||||||
width: 8em;
|
width: 8em;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user