diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index 461e918b3..b2c714b07 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -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); diff --git a/public/scripts/extensions/quick-reply/src/QuickReply.js b/public/scripts/extensions/quick-reply/src/QuickReply.js index 8e262cee2..7fbb85422 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReply.js +++ b/public/scripts/extensions/quick-reply/src/QuickReply.js @@ -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, diff --git a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js index c7ca95c5a..872e33e30 100644 --- a/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js +++ b/public/scripts/extensions/quick-reply/src/QuickReplyConfig.js @@ -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))); } diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js new file mode 100644 index 000000000..a522730bd --- /dev/null +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -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)), [], '(number) – 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.'), [], 'DEPRECATED – 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), [], '[visible=true] (number) – toggle global QR set', true, true); + registerSlashCommand('qr-set-on', (args, value)=>this.addGlobalSet(value, args), [], '[visible=true] (number) – activate global QR set', true, true); + registerSlashCommand('qr-set-off', (_, value)=>this.removeGlobalSet(value), [], '(number) – deactivate global QR set', true, true); + registerSlashCommand('qr-chat-set', (args, value)=>this.toggleChatSet(value, args), [], '[visible=true] (number) – toggle chat QR set', true, true); + registerSlashCommand('qr-chat-set-on', (args, value)=>this.addChatSet(value, args), [], '[visible=true] (number) – activate chat QR set', true, true); + registerSlashCommand('qr-chat-set-off', (_, value)=>this.removeChatSet(value), [], '(number) – 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), [], `(arguments [message])\n arguments:\n ${qrArgs} – creates a new Quick Reply, example: /qr-create set=MyPreset label=MyButton /echo 123`, true, true); + registerSlashCommand('qr-update', (args, message)=>this.updateQuickReply(args, message), [], `(arguments [message])\n arguments:\n ${qrUpdateArgs} – updates Quick Reply, example: /qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123`, true, true); + registerSlashCommand('qr-delete', (args, name)=>this.deleteQuickReply(args, name), [], '(set=string [label]) – deletes Quick Reply', true, true); + registerSlashCommand('qr-contextadd', (args, name)=>this.createContextItem(args, name), [], '(set=string label=string chain=bool [preset name]) – add context menu preset to a QR, example: /qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset', true, true); + registerSlashCommand('qr-contextdel', (args, name)=>this.deleteContextItem(args, name), [], '(set=string label=string [preset name]) – remove context menu preset from a QR, example: /qr-contextdel set=MyPreset label=MyButton MyOtherPreset', true, true); + registerSlashCommand('qr-contextclear', (args, label)=>this.clearContextMenu(args, label), [], '(set=string [label]) – remove all context menu presets from a QR, example: /qr-contextclear set=MyPreset MyButton', 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'], `(arguments [label])\n arguments:\n ${presetArgs} – create a new preset (overrides existing ones), example: /qr-presetadd slots=3 MyNewPreset`, true, true); + registerSlashCommand('qr-set-update', (args, name)=>this.updateSet(name, args), ['qr-presetupdate'], `(arguments [label])\n arguments:\n ${presetArgs} – update an existing preset, example: /qr-presetupdate enabled=false MyPreset`, 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 + } +}