mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
separate abort logic for commands
This commit is contained in:
@ -155,7 +155,7 @@ import {
|
|||||||
} from './scripts/utils.js';
|
} from './scripts/utils.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, executeSlashCommandsWithOptions, 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,
|
||||||
@ -1704,6 +1704,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
|
||||||
@ -2395,37 +2396,16 @@ export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, q
|
|||||||
* Executes slash commands and returns the new text and whether the generation was interrupted.
|
* Executes slash commands and returns the new text and whether the generation was interrupted.
|
||||||
* @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
|
||||||
* @param {AbortController} abortController
|
|
||||||
*/
|
*/
|
||||||
async function processCommands(message, abortController) {
|
export async function processCommands(message) {
|
||||||
if (!message || !message.trim().startsWith('/')) {
|
if (!message || !message.trim().startsWith('/')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
await executeSlashCommandsOnChatInput(message, {
|
||||||
deactivateSendButtons();
|
clearChatInput: true,
|
||||||
is_send_press = true;
|
|
||||||
|
|
||||||
/**@type {HTMLTextAreaElement}*/
|
|
||||||
const ta = document.querySelector('#send_textarea');
|
|
||||||
ta.value = '';
|
|
||||||
ta.dispatchEvent(new Event('input', { bubbles:true }));
|
|
||||||
|
|
||||||
await executeSlashCommandsWithOptions(message, {
|
|
||||||
abortController: abortController,
|
|
||||||
onProgress: (done, total)=>ta.style.setProperty('--prog', `${done / total * 100}%`),
|
|
||||||
});
|
});
|
||||||
delay(1000).then(()=>clearCommandProgressDebounced());
|
|
||||||
|
|
||||||
is_send_press = false;
|
|
||||||
activateSendButtons();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function clearCommandProgress() {
|
|
||||||
if (is_send_press) return;
|
|
||||||
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
|
|
||||||
}
|
|
||||||
const clearCommandProgressDebounced = debounce(clearCommandProgress);
|
|
||||||
|
|
||||||
function sendSystemMessage(type, text, extra = {}) {
|
function sendSystemMessage(type, text, extra = {}) {
|
||||||
const systemMessage = system_messages[type];
|
const systemMessage = system_messages[type];
|
||||||
@ -3088,7 +3068,7 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu
|
|||||||
let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
|
let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
|
||||||
|
|
||||||
if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
|
if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
|
||||||
const interruptedByCommand = await processCommands(String($('#send_textarea').val()), abortController);
|
const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
|
||||||
|
|
||||||
if (interruptedByCommand) {
|
if (interruptedByCommand) {
|
||||||
//$("#send_textarea").val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
//$("#send_textarea").val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
@ -10188,6 +10168,18 @@ jQuery(async function () {
|
|||||||
eventSource.emit(event_types.GENERATION_STOPPED);
|
eventSource.emit(event_types.GENERATION_STOPPED);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(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 () {
|
||||||
var icon = $(this).find('.drawer-icon');
|
var icon = $(this).find('.drawer-icon');
|
||||||
var drawer = $(this).parent().find('.drawer-content');
|
var drawer = $(this).parent().find('.drawer-content');
|
||||||
|
@ -190,7 +190,7 @@ const init = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (qr && qr.onExecute) {
|
if (qr && qr.onExecute) {
|
||||||
return await qr.execute(args);
|
return await qr.execute(args, false, true);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`No Quick Reply found for "${name}".`);
|
throw new Error(`No Quick Reply found for "${name}".`);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { POPUP_TYPE, Popup } from '../../../popup.js';
|
import { POPUP_TYPE, Popup } from '../../../popup.js';
|
||||||
import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
|
import { setSlashCommandAutoComplete } from '../../../slash-commands.js';
|
||||||
|
import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
|
||||||
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
||||||
import { getSortableDelay } from '../../../utils.js';
|
import { getSortableDelay } from '../../../utils.js';
|
||||||
import { log, warn } from '../index.js';
|
import { log, warn } from '../index.js';
|
||||||
@ -452,10 +453,23 @@ export class QuickReply {
|
|||||||
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;
|
await this.editorExecutePromise;
|
||||||
|
this.editorExecuteErrors.classList.remove('qr--hasErrors');
|
||||||
|
this.editorExecuteErrors.innerHTML = '';
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
this.editorExecuteErrors.textContent = ex.message;
|
this.editorExecuteErrors.classList.add('qr--hasErrors');
|
||||||
|
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');
|
||||||
@ -537,13 +551,19 @@ 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 scope = new SlashCommandScope();
|
const scope = new SlashCommandScope();
|
||||||
for (const key of Object.keys(args)) {
|
for (const key of Object.keys(args)) {
|
||||||
scope.setMacro(`arg::${key}`, args[key]);
|
scope.setMacro(`arg::${key}`, args[key]);
|
||||||
}
|
}
|
||||||
return await this.onExecute(this, this.message, args.isAutoExecute ?? false, scope);
|
return await this.onExecute(this, {
|
||||||
|
message:this.message,
|
||||||
|
isAutoExecute: args.isAutoExecute ?? false,
|
||||||
|
isEditor,
|
||||||
|
isRun,
|
||||||
|
scope,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
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 { 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';
|
||||||
@ -101,16 +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 {SlashCommandScope} [scope] - optional scope to be used when running the command
|
* @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, scope = null) {
|
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 {
|
||||||
@ -121,7 +134,22 @@ export class QuickReplySet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input[0] == '/' && !this.disableSend) {
|
if (input[0] == '/' && !this.disableSend) {
|
||||||
const result = await executeSlashCommands(input, true, scope);
|
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,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = await executeSlashCommandsOnChatInput(input, {
|
||||||
|
scope: options.scope,
|
||||||
|
});
|
||||||
|
}
|
||||||
return typeof result === 'object' ? result?.pipe : '';
|
return typeof result === 'object' ? result?.pipe : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +161,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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -154,7 +194,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();
|
||||||
}
|
}
|
||||||
|
@ -295,6 +295,20 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
.shadow_popup.qr--hide {
|
.shadow_popup.qr--hide {
|
||||||
opacity: 0 !important;
|
opacity: 0 !important;
|
||||||
}
|
}
|
||||||
|
@ -322,6 +322,20 @@
|
|||||||
cursor: wait;
|
cursor: wait;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ import { autoSelectPersona } from './personas.js';
|
|||||||
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
||||||
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
||||||
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
|
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
|
||||||
import { delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
|
import { debounce, delay, isFalseBoolean, isTrueBoolean, stringToRange, trimToEndSentence, trimToStartSentence, waitUntilCondition } from './utils.js';
|
||||||
import { registerVariableCommands, resolveVariable } from './variables.js';
|
import { registerVariableCommands, resolveVariable } from './variables.js';
|
||||||
import { background_settings } from './backgrounds.js';
|
import { background_settings } from './backgrounds.js';
|
||||||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||||
@ -60,6 +60,7 @@ import { SlashCommandAutoCompleteOption } from './slash-commands/SlashCommandAut
|
|||||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||||
import { SlashCommandAutoComplete } from './slash-commands/SlashCommandAutoComplete.js';
|
import { SlashCommandAutoComplete } from './slash-commands/SlashCommandAutoComplete.js';
|
||||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||||
|
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||||||
export {
|
export {
|
||||||
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
|
||||||
};
|
};
|
||||||
@ -2551,51 +2552,176 @@ function modelCallback(_, model) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export let isExecutingCommandsFromChatInput = false;
|
||||||
|
export let commandsFromChatInputAbortController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show command execution pause/stop buttons next to chat input.
|
||||||
|
*/
|
||||||
|
export function activateScriptButtons() {
|
||||||
|
document.querySelector('#form_sheld').classList.add('isExecutingCommandsFromChatInput');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide command execution pause/stop buttons next to chat input.
|
||||||
|
*/
|
||||||
|
export function deactivateScriptButtons() {
|
||||||
|
document.querySelector('#form_sheld').classList.remove('isExecutingCommandsFromChatInput');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle pause/continue command execution. Only for commands executed via chat input.
|
||||||
|
*/
|
||||||
|
export function pauseScriptExecution() {
|
||||||
|
if (commandsFromChatInputAbortController) {
|
||||||
|
if (commandsFromChatInputAbortController.signal.paused) {
|
||||||
|
commandsFromChatInputAbortController.continue('Clicked pause button');
|
||||||
|
document.querySelector('#form_sheld').classList.remove('script_paused');
|
||||||
|
} else {
|
||||||
|
commandsFromChatInputAbortController.pause('Clicked pause button');
|
||||||
|
document.querySelector('#form_sheld').classList.add('script_paused');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop command execution. Only for commands executed via chat input.
|
||||||
|
*/
|
||||||
|
export function stopScriptExecution() {
|
||||||
|
commandsFromChatInputAbortController?.abort('Clicked stop button');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear up command execution progress bar above chat input.
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
async function clearCommandProgress() {
|
||||||
|
if (isExecutingCommandsFromChatInput) return;
|
||||||
|
document.querySelector('#send_textarea').style.setProperty('--progDone', '1');
|
||||||
|
await delay(250);
|
||||||
|
if (isExecutingCommandsFromChatInput) return;
|
||||||
|
document.querySelector('#send_textarea').style.transition = 'none';
|
||||||
|
await delay(1);
|
||||||
|
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
|
||||||
|
document.querySelector('#send_textarea').style.setProperty('--progDone', '0');
|
||||||
|
document.querySelector('#form_sheld').classList.remove('script_success');
|
||||||
|
document.querySelector('#form_sheld').classList.remove('script_error');
|
||||||
|
document.querySelector('#form_sheld').classList.remove('script_aborted');
|
||||||
|
await delay(1);
|
||||||
|
document.querySelector('#send_textarea').style.transition = null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Debounced version of clearCommandProgress.
|
||||||
|
*/
|
||||||
|
const clearCommandProgressDebounced = debounce(clearCommandProgress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ExecuteSlashCommandsOptions
|
||||||
|
* @prop {boolean} [handleParserErrors] (true) Whether to handle parser errors (show toast on error) or throw.
|
||||||
|
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||||||
|
* @prop {boolean} [handleExecutionErrors] (false) Whether to handle execution errors (show toast on error) or throw
|
||||||
|
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
|
||||||
|
* @prop {SlashCommandAbortController} [abortController] (null) Controller used to abort or pause command execution
|
||||||
|
* @prop {(done:number, total:number)=>void} [onProgress] (null) Callback to handle progress events
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef ExecuteSlashCommandsOnChatInputOptions
|
||||||
|
* @prop {SlashCommandScope} [scope] (null) The scope to be used when executing the commands.
|
||||||
|
* @prop {PARSER_FLAG[]} [parserFlags] (null) Parser flags to apply
|
||||||
|
* @prop {boolean} [clearChatInput] (false) Whether to clear the chat input textarea
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute slash commands while showing progress indicator and pause/stop buttons on
|
||||||
|
* chat input.
|
||||||
|
* @param {string} text Slash command text
|
||||||
|
* @param {ExecuteSlashCommandsOnChatInputOptions} options
|
||||||
|
*/
|
||||||
|
export async function executeSlashCommandsOnChatInput(text, options = {}) {
|
||||||
|
if (isExecutingCommandsFromChatInput) return null;
|
||||||
|
|
||||||
|
options = Object.assign({
|
||||||
|
scope: null,
|
||||||
|
parserFlags: null,
|
||||||
|
clearChatInput: false,
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
isExecutingCommandsFromChatInput = true;
|
||||||
|
commandsFromChatInputAbortController?.abort('processCommands was called');
|
||||||
|
activateScriptButtons();
|
||||||
|
|
||||||
|
/**@type {HTMLTextAreaElement}*/
|
||||||
|
const ta = document.querySelector('#send_textarea');
|
||||||
|
|
||||||
|
if (options.clearChatInput) {
|
||||||
|
ta.value = '';
|
||||||
|
ta.dispatchEvent(new Event('input', { bubbles:true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#send_textarea').style.setProperty('--prog', '0%');
|
||||||
|
document.querySelector('#send_textarea').style.setProperty('--progDone', '0');
|
||||||
|
document.querySelector('#form_sheld').classList.remove('script_success');
|
||||||
|
document.querySelector('#form_sheld').classList.remove('script_error');
|
||||||
|
document.querySelector('#form_sheld').classList.remove('script_aborted');
|
||||||
|
|
||||||
|
/**@type {SlashCommandClosureResult} */
|
||||||
|
let result = null;
|
||||||
|
try {
|
||||||
|
commandsFromChatInputAbortController = new SlashCommandAbortController();
|
||||||
|
result = await executeSlashCommandsWithOptions(text, {
|
||||||
|
abortController: commandsFromChatInputAbortController,
|
||||||
|
onProgress: (done, total)=>ta.style.setProperty('--prog', `${done / total * 100}%`),
|
||||||
|
});
|
||||||
|
if (commandsFromChatInputAbortController.signal.aborted) {
|
||||||
|
document.querySelector('#form_sheld').classList.add('script_aborted');
|
||||||
|
} else {
|
||||||
|
document.querySelector('#form_sheld').classList.add('script_success');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
document.querySelector('#form_sheld').classList.add('script_error');
|
||||||
|
toastr.error(e.message);
|
||||||
|
result = new SlashCommandClosureResult();
|
||||||
|
result.interrupt = true;
|
||||||
|
result.isError = true;
|
||||||
|
result.errorMessage = e.message;
|
||||||
|
} finally {
|
||||||
|
delay(1000).then(()=>clearCommandProgressDebounced());
|
||||||
|
|
||||||
|
commandsFromChatInputAbortController = null;
|
||||||
|
deactivateScriptButtons();
|
||||||
|
isExecutingCommandsFromChatInput = false;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} text Slash command txt
|
* @param {string} text Slash command text
|
||||||
* @param {object} [options]
|
* @param {ExecuteSlashCommandsOptions} [options]
|
||||||
* @param {boolean} [options.handleParserErrors]
|
|
||||||
* @param {SlashCommandScope} [options.scope]
|
|
||||||
* @param {boolean} [options.handleExecutionErrors]
|
|
||||||
* @param {PARSER_FLAG[]} [options.parserFlags]
|
|
||||||
* @param {AbortController} [options.abortController]
|
|
||||||
* @param {(done:number, total:number)=>void} [options.onProgress]
|
|
||||||
* @returns {Promise<SlashCommandClosureResult>}
|
* @returns {Promise<SlashCommandClosureResult>}
|
||||||
*/
|
*/
|
||||||
async function executeSlashCommandsWithOptions(text, options = {}) {
|
async function executeSlashCommandsWithOptions(text, options = {}) {
|
||||||
return await executeSlashCommands(
|
|
||||||
text,
|
|
||||||
options.handleParserErrors ?? true,
|
|
||||||
options.scope ?? null,
|
|
||||||
options.handleExecutionErrors ?? false,
|
|
||||||
options.parserFlags ?? null,
|
|
||||||
options.abortController ?? null,
|
|
||||||
options.onProgress ?? null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Executes slash commands in the provided text
|
|
||||||
* @param {string} text Slash command text
|
|
||||||
* @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw
|
|
||||||
* @param {SlashCommandScope} scope The scope to be used when executing the commands.
|
|
||||||
* @param {boolean} handleExecutionErrors
|
|
||||||
* @param {PARSER_FLAG[]} parserFlags
|
|
||||||
* @param {AbortController} abortController
|
|
||||||
* @returns {Promise<SlashCommandClosureResult>}
|
|
||||||
*/
|
|
||||||
async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false, parserFlags = null, abortController = null, onProgress = null) {
|
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
options = Object.assign({
|
||||||
|
handleParserErrors: true,
|
||||||
|
scope: null,
|
||||||
|
handleExecutionErrors: false,
|
||||||
|
parserFlags: null,
|
||||||
|
abortController: null,
|
||||||
|
onProgress: null,
|
||||||
|
}, options);
|
||||||
|
|
||||||
let closure;
|
let closure;
|
||||||
try {
|
try {
|
||||||
closure = parser.parse(text, true, parserFlags, abortController);
|
closure = parser.parse(text, true, options.parserFlags, options.abortController);
|
||||||
closure.scope.parent = scope;
|
closure.scope.parent = options.scope;
|
||||||
closure.onProgress = onProgress;
|
closure.onProgress = options.onProgress;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (handleParserErrors && e instanceof SlashCommandParserError) {
|
if (options.handleParserErrors && e instanceof SlashCommandParserError) {
|
||||||
/**@type {SlashCommandParserError}*/
|
/**@type {SlashCommandParserError}*/
|
||||||
const ex = e;
|
const ex = e;
|
||||||
const toast = `
|
const toast = `
|
||||||
@ -2624,16 +2750,40 @@ async function executeSlashCommands(text, handleParserErrors = true, scope = nul
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (handleExecutionErrors) {
|
if (options.handleExecutionErrors) {
|
||||||
toastr.error(e.message);
|
toastr.error(e.message);
|
||||||
const result = new SlashCommandClosureResult();
|
const result = new SlashCommandClosureResult();
|
||||||
result.interrupt = true;
|
result.interrupt = true;
|
||||||
|
result.isError = true;
|
||||||
|
result.errorMessage = e.message;
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Executes slash commands in the provided text
|
||||||
|
* @deprecated Use executeSlashCommandWithOptions instead
|
||||||
|
* @param {string} text Slash command text
|
||||||
|
* @param {boolean} handleParserErrors Whether to handle parser errors (show toast on error) or throw
|
||||||
|
* @param {SlashCommandScope} scope The scope to be used when executing the commands.
|
||||||
|
* @param {boolean} handleExecutionErrors Whether to handle execution errors (show toast on error) or throw
|
||||||
|
* @param {PARSER_FLAG[]} parserFlags Parser flags to apply
|
||||||
|
* @param {SlashCommandAbortController} abortController Controller used to abort or pause command execution
|
||||||
|
* @param {(done:number, total:number)=>void} onProgress Callback to handle progress events
|
||||||
|
* @returns {Promise<SlashCommandClosureResult>}
|
||||||
|
*/
|
||||||
|
async function executeSlashCommands(text, handleParserErrors = true, scope = null, handleExecutionErrors = false, parserFlags = null, abortController = null, onProgress = null) {
|
||||||
|
return executeSlashCommandsWithOptions(text, {
|
||||||
|
handleParserErrors,
|
||||||
|
scope,
|
||||||
|
handleExecutionErrors,
|
||||||
|
parserFlags,
|
||||||
|
abortController,
|
||||||
|
onProgress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { substituteParams } from '../../script.js';
|
import { substituteParams } from '../../script.js';
|
||||||
import { escapeRegex } from '../utils.js';
|
import { delay, escapeRegex } from '../utils.js';
|
||||||
|
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||||
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
|
import { SlashCommandClosureExecutor } from './SlashCommandClosureExecutor.js';
|
||||||
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
|
import { SlashCommandClosureResult } from './SlashCommandClosureResult.js';
|
||||||
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
import { SlashCommandExecutor } from './SlashCommandExecutor.js';
|
||||||
@ -14,7 +15,7 @@ export class SlashCommandClosure {
|
|||||||
/**@type {Object.<string,string|SlashCommandClosure>}*/ providedArguments = {};
|
/**@type {Object.<string,string|SlashCommandClosure>}*/ providedArguments = {};
|
||||||
/**@type {SlashCommandExecutor[]}*/ executorList = [];
|
/**@type {SlashCommandExecutor[]}*/ executorList = [];
|
||||||
/**@type {string}*/ keptText;
|
/**@type {string}*/ keptText;
|
||||||
/**@type {AbortController}*/ abortController;
|
/**@type {SlashCommandAbortController}*/ abortController;
|
||||||
/**@type {(done:number, total:number)=>void}*/ onProgress;
|
/**@type {(done:number, total:number)=>void}*/ onProgress;
|
||||||
|
|
||||||
constructor(parent) {
|
constructor(parent) {
|
||||||
@ -232,15 +233,15 @@ export class SlashCommandClosure {
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
let abortResult = this.testAbortController();
|
let abortResult = await this.testAbortController();
|
||||||
if (abortResult) {
|
if (abortResult) {
|
||||||
return abortResult;
|
return abortResult;
|
||||||
}
|
}
|
||||||
this.scope.pipe = await executor.command.callback(args, value ?? '');
|
this.scope.pipe = await executor.command.callback(args, value ?? '');
|
||||||
done += 0.5;
|
done += 0.5;
|
||||||
this.onProgress?.(done, this.executorList.length);
|
this.onProgress?.(done, this.executorList.length);
|
||||||
// eslint-disable-next-line no-cond-assign
|
abortResult = await this.testAbortController();
|
||||||
if (abortResult = this.testAbortController()) {
|
if (abortResult) {
|
||||||
return abortResult;
|
return abortResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -250,7 +251,13 @@ export class SlashCommandClosure {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
testAbortController() {
|
async testPaused() {
|
||||||
|
while (!this.abortController?.signal?.aborted && this.abortController?.signal?.paused) {
|
||||||
|
await delay(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async testAbortController() {
|
||||||
|
await this.testPaused();
|
||||||
if (this.abortController?.signal?.aborted) {
|
if (this.abortController?.signal?.aborted) {
|
||||||
const result = new SlashCommandClosureResult();
|
const result = new SlashCommandClosureResult();
|
||||||
result.isAborted = true;
|
result.isAborted = true;
|
||||||
|
@ -4,4 +4,6 @@ export class SlashCommandClosureResult {
|
|||||||
/**@type {string}*/ pipe;
|
/**@type {string}*/ pipe;
|
||||||
/**@type {boolean}*/ isAborted = false;
|
/**@type {boolean}*/ isAborted = false;
|
||||||
/**@type {string}*/ abortReason;
|
/**@type {string}*/ abortReason;
|
||||||
|
/**@type {boolean}*/ isError = false;
|
||||||
|
/**@type {string}*/ errorMessage;
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,15 @@ export class SlashCommandParserError extends Error {
|
|||||||
let hint = [];
|
let hint = [];
|
||||||
let lines = this.text.slice(start + 1, end - 1).split('\n');
|
let lines = this.text.slice(start + 1, end - 1).split('\n');
|
||||||
let lineNum = this.line - lines.length + 1;
|
let lineNum = this.line - lines.length + 1;
|
||||||
|
let tabOffset = 0;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`;
|
const num = `${' '.repeat(lineOffset - lineNum.toString().length)}${lineNum}`;
|
||||||
lineNum++;
|
lineNum++;
|
||||||
hint.push(`${num}: ${line}`);
|
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)}^^^^^`);
|
hint.push(`${' '.repeat(this.index - 2 - lineStart + lineOffset + 1 + tabOffset)}^^^^^`);
|
||||||
return hint.join('\n');
|
return hint.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
100
public/style.css
100
public/style.css
@ -685,6 +685,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);
|
||||||
@ -1063,13 +1121,51 @@ select {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
order: 3;
|
order: 3;
|
||||||
|
|
||||||
--progColor: rgba(0, 128, 0, 0.839);
|
--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;
|
--progWidth: 3px;
|
||||||
|
--progWidthClip: calc(var(--progWidth) + 2px);
|
||||||
--prog: 0%;
|
--prog: 0%;
|
||||||
|
--progDone: 0;
|
||||||
border-top: var(--progWidth) solid var(--progColor);
|
border-top: var(--progWidth) solid var(--progColor);
|
||||||
clip-path: polygon(0% 0%, var(--prog) 0%, var(--prog) 5px, 100% 5px, 100% 100%, 0% 100%);
|
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;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
.slashCommandAutoComplete-wrap {
|
.slashCommandAutoComplete-wrap {
|
||||||
--targetOffset: 0;
|
--targetOffset: 0;
|
||||||
|
Reference in New Issue
Block a user