From a0918a3f5c92e40b7e2dfa2eea4fcaba9b00648c Mon Sep 17 00:00:00 2001 From: LenAnderson Date: Thu, 21 Dec 2023 20:05:42 +0000 Subject: [PATCH] add QR API --- .../quick-reply/api/QuickReplyApi.js | 357 ++++++++++++++++++ .../scripts/extensions/quick-reply/index.js | 8 +- .../quick-reply/src/SlashCommandHandler.js | 188 +++++---- 3 files changed, 450 insertions(+), 103 deletions(-) create mode 100644 public/scripts/extensions/quick-reply/api/QuickReplyApi.js diff --git a/public/scripts/extensions/quick-reply/api/QuickReplyApi.js b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js new file mode 100644 index 000000000..33486b5c0 --- /dev/null +++ b/public/scripts/extensions/quick-reply/api/QuickReplyApi.js @@ -0,0 +1,357 @@ +import { QuickReply } from '../src/QuickReply.js'; +import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js'; +import { QuickReplySet } from '../src/QuickReplySet.js'; +import { QuickReplySettings } from '../src/QuickReplySettings.js'; + + + + +export class QuickReplyApi { + /**@type {QuickReplySettings}*/ settings; + + + + + constructor(/**@type {QuickReplySettings}*/settings) { + this.settings = settings; + } + + + + + /** + * Finds and returns an existing Quick Reply Set by its name. + * + * @param {String} name name of the quick reply set + * @returns the quick reply set, or undefined if not found + */ + getSetByName(name) { + return QuickReplySet.get(name); + } + + /** + * Finds and returns an existing Quick Reply by its set's name and its label. + * + * @param {String} setName name of the quick reply set + * @param {String} label label of the quick reply + * @returns the quick reply, or undefined if not found + */ + getQrByLabel(setName, label) { + const set = this.getSetByName(setName); + if (!set) return; + return set.qrList.find(it=>it.label == label); + } + + + + + /** + * Executes a quick reply by its index and returns the result. + * + * @param {Number} idx the index (zero-based) of the quick reply to execute + * @returns the return value of the quick reply, or undefined if not found + */ + 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 { + throw new Error(`No quick reply at index "${idx}"`); + } + } + + + /** + * Adds or removes a quick reply set to the list of globally active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + toggleGlobalSet(name, isVisible = true) { + 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, isVisible); + } + } + + /** + * Adds a quick reply set to the list of globally active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + addGlobalSet(name, isVisible = true) { + const set = this.getSetByName(name); + if (!set) return; + this.settings.config.addSet(set, isVisible); + } + + /** + * Removes a quick reply set from the list of globally active quick reply sets. + * + * @param {String} name the name of the set + */ + removeGlobalSet(name) { + const set = this.getSetByName(name); + if (!set) return; + this.settings.config.removeSet(set); + } + + + /** + * Adds or removes a quick reply set to the list of the current chat's active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + toggleChatSet(name, isVisible = true) { + 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, isVisible); + } + } + + /** + * Adds a quick reply set to the list of the current chat's active quick reply sets. + * + * @param {String} name the name of the set + * @param {Boolean} isVisible whether to show the set's buttons or not + */ + addChatSet(name, isVisible = true) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + this.settings.chatConfig.addSet(set, isVisible); + } + + /** + * Removes a quick reply set from the list of the current chat's active quick reply sets. + * + * @param {String} name the name of the set + */ + removeChatSet(name) { + if (!this.settings.chatConfig) return; + const set = this.getSetByName(name); + if (!set) return; + this.settings.chatConfig.removeSet(set); + } + + + /** + * Creates a new quick reply in an existing quick reply set. + * + * @param {String} setName name of the quick reply set to insert the new quick reply into + * @param {String} label label for the new quick reply (text on the button) + * @param {Object} [props] + * @param {String} [props.message] the message to be sent or slash command to be executed by the new quick reply + * @param {String} [props.title] the title / tooltip to be shown on the quick reply button + * @param {Boolean} [props.isHidden] whether to hide or show the button + * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @returns {QuickReply} the new quick reply + */ + createQuickReply(setName, label, { + message, + title, + isHidden, + executeOnStartup, + executeOnUser, + executeOnAi, + executeOnChatChange, + } = {}) { + const set = this.getSetByName(setName); + if (!set) { + throw new Error(`No quick reply set with named "${setName}" found.`); + } + const qr = set.addQuickReply(); + qr.label = label ?? ''; + qr.message = message ?? ''; + qr.title = title ?? ''; + qr.isHidden = isHidden ?? false; + qr.executeOnStartup = executeOnStartup ?? false; + qr.executeOnUser = executeOnUser ?? false; + qr.executeOnAi = executeOnAi ?? false; + qr.executeOnChatChange = executeOnChatChange ?? false; + qr.onUpdate(); + return qr; + } + + /** + * Updates an existing quick reply. + * + * @param {String} setName name of the existing quick reply set + * @param {String} label label of the existing quick reply (text on the button) + * @param {Object} [props] + * @param {String} [props.newLabel] new label for quick reply (text on the button) + * @param {String} [props.message] the message to be sent or slash command to be executed by the quick reply + * @param {String} [props.title] the title / tooltip to be shown on the quick reply button + * @param {Boolean} [props.isHidden] whether to hide or show the button + * @param {Boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts + * @param {Boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message + * @param {Boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message + * @param {Boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded + * @returns {QuickReply} the altered quick reply + */ + updateQuickReply(setName, label, { + newLabel, + message, + title, + isHidden, + executeOnStartup, + executeOnUser, + executeOnAi, + executeOnChatChange, + } = {}) { + const qr = this.getQrByLabel(setName, label); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + qr.label = newLabel ?? qr.label; + qr.message = message ?? qr.message; + qr.title = title ?? qr.title; + qr.isHidden = isHidden ?? qr.isHidden; + qr.executeOnStartup = executeOnStartup ?? qr.executeOnStartup; + qr.executeOnUser = executeOnUser ?? qr.executeOnUser; + qr.executeOnAi = executeOnAi ?? qr.executeOnAi; + qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange; + qr.onUpdate(); + return qr; + } + + /** + * Deletes an existing quick reply. + * + * @param {String} setName name of the existing quick reply set + * @param {String} label label of the existing quick reply (text on the button) + */ + deleteQuickReply(setName, label) { + const qr = this.getQrByLabel(setName, label); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + qr.delete(); + } + + + /** + * Adds an existing quick reply set as a context menu to an existing quick reply. + * + * @param {String} setName name of the existing quick reply set containing the quick reply + * @param {String} label label of the existing quick reply + * @param {String} contextSetName name of the existing quick reply set to be used as a context menu + * @param {Boolean} isChained whether or not to chain the context menu quick replies + */ + createContextItem(setName, label, contextSetName, isChained = false) { + const qr = this.getQrByLabel(setName, label); + const set = this.getSetByName(contextSetName); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + if (!set) { + throw new Error(`No quick reply set with name "${contextSetName}" found.`); + } + const cl = new QuickReplyContextLink(); + cl.set = set; + cl.isChained = isChained; + qr.addContextLink(cl); + } + + /** + * Removes a quick reply set from a quick reply's context menu. + * + * @param {String} setName name of the existing quick reply set containing the quick reply + * @param {String} label label of the existing quick reply + * @param {String} contextSetName name of the existing quick reply set to be used as a context menu + */ + deleteContextItem(setName, label, contextSetName) { + const qr = this.getQrByLabel(setName, label); + const set = this.getSetByName(contextSetName); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + if (!set) { + throw new Error(`No quick reply set with name "${contextSetName}" found.`); + } + qr.removeContextLink(set.name); + } + + /** + * Removes all entries from a quick reply's context menu. + * + * @param {String} setName name of the existing quick reply set containing the quick reply + * @param {String} label label of the existing quick reply + */ + clearContextMenu(setName, label) { + const qr = this.getQrByLabel(setName, label); + if (!qr) { + throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`); + } + qr.clearContextLinks(); + } + + + /** + * Create a new quick reply set. + * + * @param {String} name name of the new quick reply set + * @param {Object} [props] + * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @returns {QuickReplySet} the new quick reply set + */ + createSet(name, { + disableSend, + placeBeforeInput, + injectInput, + } = {}) { + const set = new QuickReplySet(); + set.name = name; + set.disableSend = disableSend ?? false; + set.placeBeforeInput = placeBeforeInput ?? false; + set.injectInput = injectInput ?? false; + QuickReplySet.list.push(set); + set.save(); + //TODO settings UI must be updated + return set; + } + + /** + * Update an existing quick reply set. + * + * @param {String} name name of the existing quick reply set + * @param {Object} [props] + * @param {Boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box + * @param {Boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input + * @param {Boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply + * @returns {QuickReplySet} the altered quick reply set + */ + updateSet(name, { + disableSend, + placeBeforeInput, + injectInput, + } = {}) { + const set = this.getSetByName(name); + if (!set) { + throw new Error(`No quick reply set with name "${name}" found.`); + } + set.disableSend = disableSend ?? false; + set.placeBeforeInput = placeBeforeInput ?? false; + set.injectInput = injectInput ?? false; + set.save(); + //TODO settings UI must be updated + return set; + } +} diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index b2c714b07..3c45392a9 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -1,5 +1,6 @@ import { chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js'; import { extension_settings } from '../../extensions.js'; +import { QuickReplyApi } from './api/QuickReplyApi.js'; import { QuickReply } from './src/QuickReply.js'; import { QuickReplyConfig } from './src/QuickReplyConfig.js'; import { QuickReplyContextLink } from './src/QuickReplyContextLink.js'; @@ -13,8 +14,6 @@ import { SettingsUi } from './src/ui/SettingsUi.js'; //TODO move advanced QR options into own UI class -//TODO easy way to CRUD QRs and sets -//TODO easy way to set global and chat sets @@ -44,6 +43,8 @@ let settings; let manager; /** @type {ButtonUi} */ let buttons; +/** @type {QuickReplyApi} */ +export let api; @@ -152,7 +153,8 @@ const init = async () => { } } - const slash = new SlashCommandHandler(settings); + api = new QuickReplyApi(settings); + const slash = new SlashCommandHandler(api); slash.init(); }; eventSource.on(event_types.APP_READY, init); diff --git a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js index a522730bd..37f054616 100644 --- a/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js +++ b/public/scripts/extensions/quick-reply/src/SlashCommandHandler.js @@ -1,16 +1,14 @@ import { registerSlashCommand } from '../../../slash-commands.js'; -import { QuickReplyContextLink } from './QuickReplyContextLink.js'; -import { QuickReplySet } from './QuickReplySet.js'; -import { QuickReplySettings } from './QuickReplySettings.js'; +import { QuickReplyApi } from '../api/QuickReplyApi.js'; export class SlashCommandHandler { - /**@type {QuickReplySettings}*/ settings; + /**@type {QuickReplyApi}*/ api; - constructor(/**@type {QuickReplySettings}*/settings) { - this.settings = settings; + constructor(/**@type {QuickReplyApi}*/api) { + this.api = api; } @@ -61,7 +59,7 @@ export class SlashCommandHandler { getSetByName(name) { - const set = QuickReplySet.get(name); + const set = this.api.getSetByName(name); if (!set) { toastr.error(`No Quick Reply Set with the name "${name}" could be found.`); } @@ -69,11 +67,9 @@ export class SlashCommandHandler { } getQrByLabel(setName, label) { - const set = this.getSetByName(setName); - if (!set) return; - const qr = set.qrList.find(it=>it.label == label); + const qr = this.api.getQrByLabel(setName, label); if (!qr) { - toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${set.name}"`); + toastr.error(`No Quick Reply with the label "${label}" could be found in the set "${setName}"`); } return qr; } @@ -82,111 +78,102 @@ export class SlashCommandHandler { 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}"`); + try { + return await this.api.executeQuickReplyByIndex(idx); + } catch (ex) { + toastr.error(ex.message); } } 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')); - } + this.api.toggleGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); } addGlobalSet(name, args = {}) { - const set = this.getSetByName(name); - if (!set) return; - this.settings.config.addSet(set, JSON.parse(args.visible ?? 'true')); + this.api.addGlobalSet(name, JSON.parse(args.visible ?? 'true') === true); } removeGlobalSet(name) { - const set = this.getSetByName(name); - if (!set) return; - this.settings.config.removeSet(set); + this.api.removeGlobalSet(name); } 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')); - } + this.api.toggleChatSet(name, JSON.parse(args.visible ?? 'true') === 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')); + this.api.addChatSet(name, JSON.parse(args.visible ?? 'true') === true); } removeChatSet(name) { - if (!this.settings.chatConfig) return; - const set = this.getSetByName(name); - if (!set) return; - this.settings.chatConfig.removeSet(set); + this.api.removeChatSet(name); } 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(); + try { + this.api.createQuickReply( + args.set ?? '', + args.label ?? '', + { + message: message ?? '', + title: args.title, + isHidden: JSON.parse(args.hidden ?? 'false') === true, + executeOnStartup: JSON.parse(args.startup ?? 'false') === true, + executeOnUser: JSON.parse(args.user ?? 'false') === true, + executeOnAi: JSON.parse(args.bot ?? 'false') === true, + executeOnChatChange: JSON.parse(args.load ?? 'false') === true, + }, + ); + } catch (ex) { + toastr.error(ex.message); + } } 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(); + try { + this.api.updateQuickReply( + args.set ?? '', + args.label ?? '', + { + newLabel: args.newlabel, + message: (message ?? '').trim().length > 0 ? message : undefined, + title: args.title, + isHidden: args.hidden, + executeOnStartup: args.startup, + executeOnUser: args.user, + executeOnAi: args.bot, + executeOnChatChange: args.load, + }, + ); + } catch (ex) { + toastr.error(ex.message); + } } deleteQuickReply(args, label) { - const qr = this.getQrByLabel(args.set, args.label ?? label); - if (!qr) return; - qr.delete(); + try { + this.api.deleteQuickReply(args.set, label); + } catch (ex) { + toastr.error(ex.message); + } } 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); + try { + this.api.createContextItem( + args.set, + args.label, + name, + JSON.parse(args.chain ?? 'false') === true, + ); + } catch (ex) { + toastr.error(ex.message); + } } deleteContextItem(args, name) { - const qr = this.getQrByLabel(args.set, args.label); - const set = this.getSetByName(name); - if (!qr || !set) return; - qr.removeContextLink(set.name); + try { + this.api.deleteContextItem(args.set, args.label, name); + } catch (ex) { + toastr.error(ex.message); + } } clearContextMenu(args, label) { const qr = this.getQrByLabel(args.set, args.label ?? label); @@ -196,22 +183,23 @@ export class SlashCommandHandler { 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 + this.api.createSet( + args.name ?? name ?? '', + { + disableSend: JSON.parse(args.nosend ?? 'false') === true, + placeBeforeInput: JSON.parse(args.before ?? 'false') === true, + injectInput: JSON.parse(args.inject ?? 'false') === true, + }, + ); } 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 + this.api.updateSet( + args.name ?? name ?? '', + { + disableSend: args.nosend !== undefined ? JSON.parse(args.nosend ?? 'false') === true : undefined, + placeBeforeInput: args.before !== undefined ? JSON.parse(args.before ?? 'false') === true : undefined, + injectInput: args.inject !== undefined ? JSON.parse(args.inject ?? 'false') === true : undefined, + }, + ); } }