implement slash commands

This commit is contained in:
LenAnderson 2023-12-21 18:44:58 +00:00
parent 8959c0d380
commit 5125eaf1dc
4 changed files with 309 additions and 48 deletions

View File

@ -5,6 +5,7 @@ import { QuickReplyConfig } from './src/QuickReplyConfig.js';
import { QuickReplyContextLink } from './src/QuickReplyContextLink.js';
import { QuickReplySet } from './src/QuickReplySet.js';
import { QuickReplySettings } from './src/QuickReplySettings.js';
import { SlashCommandHandler } from './src/SlashCommandHandler.js';
import { ButtonUi } from './src/ui/ButtonUi.js';
import { SettingsUi } from './src/ui/SettingsUi.js';
@ -12,7 +13,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js';
//TODO move advanced QR options into own UI class
//TODO slash commands
//TODO easy way to CRUD QRs and sets
//TODO easy way to set global and chat sets
@ -151,6 +151,9 @@ const init = async () => {
await qr.onExecute();
}
}
const slash = new SlashCommandHandler(settings);
slash.init();
};
eventSource.on(event_types.APP_READY, init);

View File

@ -108,9 +108,6 @@ export class QuickReply {
/**
* @param {any} idx
*/
renderSettings(idx) {
if (!this.settingsDom) {
const item = document.createElement('div'); {
@ -194,44 +191,6 @@ export class QuickReply {
this.settingsDom?.remove();
}
delete() {
if (this.onDelete) {
this.unrender();
this.unrenderSettings();
this.onDelete(this);
}
}
/**
* @param {string} value
*/
updateMessage(value) {
if (this.onUpdate) {
this.message = value;
this.updateRender();
this.onUpdate(this);
}
}
/**
* @param {string} value
*/
updateLabel(value) {
if (this.onUpdate) {
this.label = value;
this.updateRender();
this.onUpdate(this);
}
}
updateContext() {
if (this.onUpdate) {
this.updateRender();
this.onUpdate(this);
}
}
async showOptions() {
const response = await fetch('/scripts/extensions/quick-reply/html/qrOptions.html', { cache: 'no-store' });
if (response.ok) {
@ -359,6 +318,63 @@ export class QuickReply {
delete() {
if (this.onDelete) {
this.unrender();
this.unrenderSettings();
this.onDelete(this);
}
}
/**
* @param {string} value
*/
updateMessage(value) {
if (this.onUpdate) {
this.message = value;
this.updateRender();
this.onUpdate(this);
}
}
/**
* @param {string} value
*/
updateLabel(value) {
if (this.onUpdate) {
this.label = value;
this.updateRender();
this.onUpdate(this);
}
}
updateContext() {
if (this.onUpdate) {
this.updateRender();
this.onUpdate(this);
}
}
addContextLink(cl) {
this.contextList.push(cl);
this.updateContext();
}
removeContextLink(setName) {
const idx = this.contextList.findIndex(it=>it.set.name == setName);
if (idx > -1) {
this.contextList.splice(idx, 1);
this.updateContext();
}
}
clearContextLinks() {
if (this.contextList.length) {
this.contextList.splice(0, this.contextList.length);
this.updateContext();
}
}
toJSON() {
return {
id: this.id,

View File

@ -30,27 +30,52 @@ export class QuickReplyConfig {
}
hasSet(qrs) {
return this.setList.find(it=>it.set == qrs) != null;
}
addSet(qrs, isVisible = true) {
if (!this.hasSet(qrs)) {
const qrl = new QuickReplySetLink();
qrl.set = qrs;
qrl.isVisible = isVisible;
this.setList.push(qrl);
this.update();
this.updateSetListDom();
}
}
removeSet(qrs) {
const idx = this.setList.findIndex(it=>it.set == qrs);
if (idx > -1) {
this.setList.splice(idx, 1);
this.update();
this.updateSetListDom();
}
}
renderSettingsInto(/**@type {HTMLElement}*/root) {
/**@type {HTMLElement}*/
const setList = root.querySelector('.qr--setList');
this.setListDom = setList;
setList.innerHTML = '';
this.setListDom = root.querySelector('.qr--setList');
root.querySelector('.qr--setListAdd').addEventListener('click', ()=>{
const qrl = new QuickReplySetLink();
qrl.set = QuickReplySet.list[0];
this.hookQuickReplyLink(qrl);
this.setList.push(qrl);
setList.append(qrl.renderSettings(this.setList.length - 1));
this.setListDom.append(qrl.renderSettings(this.setList.length - 1));
this.update();
});
this.updateSetListDom();
}
updateSetListDom() {
this.setListDom.innerHTML = '';
// @ts-ignore
$(setList).sortable({
$(this.setListDom).sortable({
delay: getSortableDelay(),
stop: ()=>this.onSetListSort(),
});
this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>setList.append(qrl.renderSettings(idx)));
this.setList.filter(it=>!it.set.isDeleted).forEach((qrl,idx)=>this.setListDom.append(qrl.renderSettings(idx)));
}

View File

@ -0,0 +1,217 @@
import { registerSlashCommand } from '../../../slash-commands.js';
import { QuickReplyContextLink } from './QuickReplyContextLink.js';
import { QuickReplySet } from './QuickReplySet.js';
import { QuickReplySettings } from './QuickReplySettings.js';
export class SlashCommandHandler {
/**@type {QuickReplySettings}*/ settings;
constructor(/**@type {QuickReplySettings}*/settings) {
this.settings = settings;
}
init() {
registerSlashCommand('qr', (_, value) => this.executeQuickReplyByIndex(Number(value)), [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
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);
registerSlashCommand('qr-set', (args, value)=>this.toggleGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> toggle global QR set', true, true);
registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '<span class="monospace">[visible=true] (number)</span> activate global QR set', true, true);
registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '<span class="monospace">(number)</span> deactivate global QR set', true, 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);
const qrArgs = `
label - string - text on the button, e.g., label=MyButton
set - string - name of the QR set, e.g., set=PresetName1
hidden - bool - whether the button should be hidden, e.g., hidden=true
startup - bool - auto execute on app startup, e.g., startup=true
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
title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button"
`.trim();
const qrUpdateArgs = `
newlabel - string - new text fort the button, e.g. newlabel=MyRenamedButton
${qrArgs}
`.trim();
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);
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);
registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '<span class="monospace">(set=string [label])</span> deletes Quick Reply', true, true);
registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '<span class="monospace">(set=string label=string chain=bool [preset name])</span> add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, 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 = `
enabled - bool - enable or disable the preset
nosend - bool - disable send / insert in user input (invalid for slash commands)
before - bool - place QR before user input
slots - int - number of slots
inject - bool - inject user input automatically (if disabled use {{input}})
`.trim();
registerSlashCommand('qr-set-create', (args, name)=>this.createSet(name, args), ['qr-presetadd'], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> create a new preset (overrides existing ones), example: <tt>/qr-presetadd slots=3 MyNewPreset</tt>`, true, true);
registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> update an existing preset, example: <tt>/qr-presetupdate enabled=false MyPreset</tt>`, true, true);
}
getSetByName(name) {
const set = QuickReplySet.get(name);
if (!set) {
toastr.error(`No Quick Reply Set with the name "${name}" could be found.`);
}
return set;
}
getQrByLabel(setName, label) {
const set = this.getSetByName(setName);
if (!set) return;
const qr = set.qrList.find(it=>it.label == label);
if (!qr) {
toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${set.name}"`);
}
return qr;
}
async executeQuickReplyByIndex(idx) {
const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
.map(it=>it.set.qrList)
.flat()[idx]
;
if (qr) {
return await qr.onExecute();
} else {
toastr.error(`No Quick Reply at index "${idx}"`);
}
}
toggleGlobalSet(name, args = {}) {
const set = this.getSetByName(name);
if (!set) return;
if (this.settings.config.hasSet(set)) {
this.settings.config.removeSet(set);
} else {
this.settings.config.addSet(set, JSON.parse(args.visible ?? 'true'));
}
}
addGlobalSet(name, args = {}) {
const set = this.getSetByName(name);
if (!set) return;
this.settings.config.addSet(set, JSON.parse(args.visible ?? 'true'));
}
removeGlobalSet(name) {
const set = this.getSetByName(name);
if (!set) return;
this.settings.config.removeSet(set);
}
toggleChatSet(name, args = {}) {
if (!this.settings.chatConfig) return;
const set = this.getSetByName(name);
if (!set) return;
if (this.settings.chatConfig.hasSet(set)) {
this.settings.chatConfig.removeSet(set);
} else {
this.settings.chatConfig.addSet(set, JSON.parse(args.visible ?? 'true'));
}
}
addChatSet(name, args = {}) {
if (!this.settings.chatConfig) return;
const set = this.getSetByName(name);
if (!set) return;
this.settings.chatConfig.addSet(set, JSON.parse(args.visible ?? 'true'));
}
removeChatSet(name) {
if (!this.settings.chatConfig) return;
const set = this.getSetByName(name);
if (!set) return;
this.settings.chatConfig.removeSet(set);
}
createQuickReply(args, message) {
const set = this.getSetByName(args.set);
if (!set) return;
const qr = set.addQuickReply();
qr.label = args.label ?? '';
qr.message = message ?? '';
qr.title = args.title ?? '';
qr.isHidden = JSON.parse(args.hidden ?? 'false') === true;
qr.executeOnStartup = JSON.parse(args.startup ?? 'false') === true;
qr.executeOnUser = JSON.parse(args.user ?? 'false') === true;
qr.executeOnAi = JSON.parse(args.bot ?? 'false') === true;
qr.executeOnChatChange = JSON.parse(args.load ?? 'false') === true;
qr.onUpdate();
}
updateQuickReply(args, message) {
const qr = this.getQrByLabel(args.set, args.label);
if (!qr) return;
qr.message = (message ?? '').trim().length > 0 ? message : qr.message;
qr.label = args.newlabel !== undefined ? (args.newlabel ?? '') : qr.label;
qr.title = args.title !== undefined ? (args.title ?? '') : qr.title;
qr.isHidden = args.hidden !== undefined ? (JSON.parse(args.hidden ?? 'false') === true) : qr.isHidden;
qr.executeOnStartup = args.startup !== undefined ? (JSON.parse(args.startup ?? 'false') === true) : qr.executeOnStartup;
qr.executeOnUser = args.user !== undefined ? (JSON.parse(args.user ?? 'false') === true) : qr.executeOnUser;
qr.executeOnAi = args.bot !== undefined ? (JSON.parse(args.bot ?? 'false') === true) : qr.executeOnAi;
qr.executeOnChatChange = args.load !== undefined ? (JSON.parse(args.load ?? 'false') === true) : qr.executeOnChatChange;
qr.onUpdate();
}
deleteQuickReply(args, label) {
const qr = this.getQrByLabel(args.set, args.label ?? label);
if (!qr) return;
qr.delete();
}
createContextItem(args, name) {
const qr = this.getQrByLabel(args.set, args.label);
const set = this.getSetByName(name);
if (!qr || !set) return;
const cl = new QuickReplyContextLink();
cl.set = set;
cl.isChained = JSON.parse(args.chain ?? 'false') ?? false;
qr.addContextLink(cl);
}
deleteContextItem(args, name) {
const qr = this.getQrByLabel(args.set, args.label);
const set = this.getSetByName(name);
if (!qr || !set) return;
qr.removeContextLink(set.name);
}
clearContextMenu(args, label) {
const qr = this.getQrByLabel(args.set, args.label ?? label);
if (!qr) return;
qr.clearContextLinks();
}
createSet(name, args) {
const set = new QuickReplySet();
set.name = args.name ?? name;
set.disableSend = JSON.parse(args.nosend ?? 'false') === true;
set.placeBeforeInput = JSON.parse(args.before ?? 'false') === true;
set.injectInput = JSON.parse(args.inject ?? 'false') === true;
QuickReplySet.list.push(set);
set.save();
//TODO settings UI must be updated
}
updateSet(name, args) {
const set = this.getSetByName(args.name ?? name);
if (!set) return;
set.disableSend = args.nosend !== undefined ? (JSON.parse(args.nosend ?? 'false') === true) : set.disableSend;
set.placeBeforeInput = args.before !== undefined ? (JSON.parse(args.before ?? 'false') === true) : set.placeBeforeInput;
set.injectInput = args.inject !== undefined ? (JSON.parse(args.inject ?? 'false') === true) : set.injectInput;
set.save();
//TODO settings UI must be updated
}
}