Blame Raw
Cohee · e3f41666 · · 505 lines (19.6 KB)
1 contributor
1import { QuickReply } from '../src/QuickReply.js';
2import { QuickReplyContextLink } from '../src/QuickReplyContextLink.js';
3import { QuickReplySet } from '../src/QuickReplySet.js';
4import { QuickReplySettings } from '../src/QuickReplySettings.js';
5import { SettingsUi } from '../src/ui/SettingsUi.js';
6import { onlyUnique } from '../../../utils.js';
7
8export class QuickReplyApi {
9 /** @type {QuickReplySettings} */ settings;
10 /** @type {SettingsUi} */ settingsUi;
11
12
13 constructor(/** @type {QuickReplySettings} */settings, /** @type {SettingsUi} */settingsUi) {
14 this.settings = settings;
15 this.settingsUi = settingsUi;
16 }
17
18
19 /**
20 * @param {QuickReply} qr
21 * @returns {QuickReplySet}
22 */
23 getSetByQr(qr) {
24 return QuickReplySet.list.find(it => it.qrList.includes(qr));
25 }
26
27 /**
28 * Finds and returns an existing Quick Reply Set by its name.
29 *
30 * @param {string} name name of the quick reply set
31 * @returns the quick reply set, or undefined if not found
32 */
33 getSetByName(name) {
34 return QuickReplySet.get(name);
35 }
36
37 /**
38 * Finds and returns an existing Quick Reply by its set's name and its label.
39 *
40 * @param {string} setName name of the quick reply set
41 * @param {string|number} label label or numeric ID of the quick reply
42 * @returns the quick reply, or undefined if not found
43 */
44 getQrByLabel(setName, label) {
45 const set = this.getSetByName(setName);
46 if (!set) return;
47 if (Number.isInteger(label)) return set.qrList.find(it => it.id == label);
48 return set.qrList.find(it => it.label == label);
49 }
50
51
52 /**
53 * Executes a quick reply by its index and returns the result.
54 *
55 * @param {Number} idx the index (zero-based) of the quick reply to execute
56 * @returns the return value of the quick reply, or undefined if not found
57 */
58 async executeQuickReplyByIndex(idx) {
59 const qr = [...this.settings.config.setList, ...(this.settings.chatConfig?.setList ?? [])]
60 .map(it => it.set.qrList)
61 .flat()[idx]
62 ;
63 if (qr) {
64 return await qr.onExecute();
65 } else {
66 throw new Error(`No quick reply at index "${idx}"`);
67 }
68 }
69
70 /**
71 * Executes an existing quick reply.
72 *
73 * @param {string} setName name of the existing quick reply set
74 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
75 * @param {object} [args] optional arguments
76 * @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options] optional execution options
77 */
78 async executeQuickReply(setName, label, args = {}, options = {}) {
79 const qr = this.getQrByLabel(setName, label);
80 if (!qr) {
81 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
82 }
83 return await qr.execute(args, false, false, options);
84 }
85
86
87 /**
88 * Adds or removes a quick reply set to the list of globally active quick reply sets.
89 *
90 * @param {string} name the name of the set
91 * @param {boolean} isVisible whether to show the set's buttons or not
92 */
93 toggleGlobalSet(name, isVisible = true) {
94 const set = this.getSetByName(name);
95 if (!set) {
96 throw new Error(`No quick reply set with name "${name}" found.`);
97 }
98 if (this.settings.config.hasSet(set)) {
99 this.settings.config.removeSet(set);
100 } else {
101 this.settings.config.addSet(set, isVisible);
102 }
103 }
104
105 /**
106 * Adds a quick reply set to the list of globally active quick reply sets.
107 *
108 * @param {string} name the name of the set
109 * @param {boolean} isVisible whether to show the set's buttons or not
110 */
111 addGlobalSet(name, isVisible = true) {
112 const set = this.getSetByName(name);
113 if (!set) {
114 throw new Error(`No quick reply set with name "${name}" found.`);
115 }
116 this.settings.config.addSet(set, isVisible);
117 }
118
119 /**
120 * Removes a quick reply set from the list of globally active quick reply sets.
121 *
122 * @param {string} name the name of the set
123 */
124 removeGlobalSet(name) {
125 const set = this.getSetByName(name);
126 if (!set) {
127 throw new Error(`No quick reply set with name "${name}" found.`);
128 }
129 this.settings.config.removeSet(set);
130 }
131
132
133 /**
134 * Adds or removes a quick reply set to the list of the current chat's active quick reply sets.
135 *
136 * @param {string} name the name of the set
137 * @param {boolean} isVisible whether to show the set's buttons or not
138 */
139 toggleChatSet(name, isVisible = true) {
140 if (!this.settings.chatConfig) return;
141 const set = this.getSetByName(name);
142 if (!set) {
143 throw new Error(`No quick reply set with name "${name}" found.`);
144 }
145 if (this.settings.chatConfig.hasSet(set)) {
146 this.settings.chatConfig.removeSet(set);
147 } else {
148 this.settings.chatConfig.addSet(set, isVisible);
149 }
150 }
151
152 /**
153 * Adds a quick reply set to the list of the current chat's active quick reply sets.
154 *
155 * @param {string} name the name of the set
156 * @param {boolean} isVisible whether to show the set's buttons or not
157 */
158 addChatSet(name, isVisible = true) {
159 if (!this.settings.chatConfig) return;
160 const set = this.getSetByName(name);
161 if (!set) {
162 throw new Error(`No quick reply set with name "${name}" found.`);
163 }
164 this.settings.chatConfig.addSet(set, isVisible);
165 }
166
167 /**
168 * Removes a quick reply set from the list of the current chat's active quick reply sets.
169 *
170 * @param {string} name the name of the set
171 */
172 removeChatSet(name) {
173 if (!this.settings.chatConfig) return;
174 const set = this.getSetByName(name);
175 if (!set) {
176 throw new Error(`No quick reply set with name "${name}" found.`);
177 }
178 this.settings.chatConfig.removeSet(set);
179 }
180
181
182 /**
183 * Creates a new quick reply in an existing quick reply set.
184 *
185 * @param {string} setName name of the quick reply set to insert the new quick reply into
186 * @param {string} label label for the new quick reply (text on the button)
187 * @param {object} [props]
188 * @param {string} [props.icon] the icon to show on the QR button
189 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
190 * @param {string} [props.message] the message to be sent or slash command to be executed by the new quick reply
191 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
192 * @param {boolean} [props.isHidden] whether to hide or show the button
193 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
194 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
195 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
196 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
197 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
198 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
199 * @param {boolean} [props.executeBeforeGeneration] whether to execute the quick reply before message generation
200 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
201 * @returns {QuickReply} the new quick reply
202 */
203 createQuickReply(setName, label, {
204 icon,
205 showLabel,
206 message,
207 title,
208 isHidden,
209 executeOnStartup,
210 executeOnUser,
211 executeOnAi,
212 executeOnChatChange,
213 executeOnGroupMemberDraft,
214 executeOnNewChat,
215 executeBeforeGeneration,
216 automationId,
217 } = {}) {
218 const set = this.getSetByName(setName);
219 if (!set) {
220 throw new Error(`No quick reply set with named "${setName}" found.`);
221 }
222 const qr = set.addQuickReply();
223 qr.label = label ?? '';
224 qr.icon = icon ?? '';
225 qr.showLabel = showLabel ?? false;
226 qr.message = message ?? '';
227 qr.title = title ?? '';
228 qr.isHidden = isHidden ?? false;
229 qr.executeOnStartup = executeOnStartup ?? false;
230 qr.executeOnUser = executeOnUser ?? false;
231 qr.executeOnAi = executeOnAi ?? false;
232 qr.executeOnChatChange = executeOnChatChange ?? false;
233 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? false;
234 qr.executeOnNewChat = executeOnNewChat ?? false;
235 qr.executeBeforeGeneration = executeBeforeGeneration ?? false;
236 qr.automationId = automationId ?? '';
237 qr.onUpdate();
238 return qr;
239 }
240
241 /**
242 * Updates an existing quick reply.
243 *
244 * @param {string} setName name of the existing quick reply set
245 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
246 * @param {object} [props]
247 * @param {string} [props.icon] the icon to show on the QR button
248 * @param {boolean} [props.showLabel] whether to show the label even when an icon is assigned
249 * @param {string} [props.newLabel] new label for quick reply (text on the button)
250 * @param {string} [props.message] the message to be sent or slash command to be executed by the quick reply
251 * @param {string} [props.title] the title / tooltip to be shown on the quick reply button
252 * @param {boolean} [props.isHidden] whether to hide or show the button
253 * @param {boolean} [props.executeOnStartup] whether to execute the quick reply when SillyTavern starts
254 * @param {boolean} [props.executeOnUser] whether to execute the quick reply after a user has sent a message
255 * @param {boolean} [props.executeOnAi] whether to execute the quick reply after the AI has sent a message
256 * @param {boolean} [props.executeOnChatChange] whether to execute the quick reply when a new chat is loaded
257 * @param {boolean} [props.executeOnGroupMemberDraft] whether to execute the quick reply when a group member is selected
258 * @param {boolean} [props.executeOnNewChat] whether to execute the quick reply when a new chat is created
259 * @param {boolean} [props.executeBeforeGeneration] whether to execute the quick reply before message generation
260 * @param {string} [props.automationId] when not empty, the quick reply will be executed when the WI with the given automation ID is activated
261 * @returns {QuickReply} the altered quick reply
262 */
263 updateQuickReply(setName, label, {
264 icon,
265 showLabel,
266 newLabel,
267 message,
268 title,
269 isHidden,
270 executeOnStartup,
271 executeOnUser,
272 executeOnAi,
273 executeOnChatChange,
274 executeOnGroupMemberDraft,
275 executeOnNewChat,
276 executeBeforeGeneration,
277 automationId,
278 } = {}) {
279 const qr = this.getQrByLabel(setName, label);
280 if (!qr) {
281 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
282 }
283 qr.updateIcon(icon ?? qr.icon);
284 qr.updateShowLabel(showLabel ?? qr.showLabel);
285 qr.updateLabel(newLabel ?? qr.label);
286 qr.updateMessage(message ?? qr.message);
287 qr.updateTitle(title ?? qr.title);
288 qr.isHidden = isHidden ?? qr.isHidden;
289 qr.executeOnStartup = executeOnStartup ?? qr.executeOnStartup;
290 qr.executeOnUser = executeOnUser ?? qr.executeOnUser;
291 qr.executeOnAi = executeOnAi ?? qr.executeOnAi;
292 qr.executeOnChatChange = executeOnChatChange ?? qr.executeOnChatChange;
293 qr.executeOnGroupMemberDraft = executeOnGroupMemberDraft ?? qr.executeOnGroupMemberDraft;
294 qr.executeOnNewChat = executeOnNewChat ?? qr.executeOnNewChat;
295 qr.executeBeforeGeneration = executeBeforeGeneration ?? qr.executeBeforeGeneration;
296 qr.automationId = automationId ?? qr.automationId;
297 qr.onUpdate();
298 return qr;
299 }
300
301 /**
302 * Deletes an existing quick reply.
303 *
304 * @param {string} setName name of the existing quick reply set
305 * @param {string|number} label label of the existing quick reply (text on the button) or its numeric ID
306 */
307 deleteQuickReply(setName, label) {
308 const qr = this.getQrByLabel(setName, label);
309 if (!qr) {
310 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
311 }
312 qr.delete();
313 }
314
315
316 /**
317 * Adds an existing quick reply set as a context menu to an existing quick reply.
318 *
319 * @param {string} setName name of the existing quick reply set containing the quick reply
320 * @param {string|number} label label of the existing quick reply or its numeric ID
321 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
322 * @param {boolean} isChained whether or not to chain the context menu quick replies
323 */
324 createContextItem(setName, label, contextSetName, isChained = false) {
325 const qr = this.getQrByLabel(setName, label);
326 const set = this.getSetByName(contextSetName);
327 if (!qr) {
328 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
329 }
330 if (!set) {
331 throw new Error(`No quick reply set with name "${contextSetName}" found.`);
332 }
333 const cl = new QuickReplyContextLink();
334 cl.set = set;
335 cl.isChained = isChained;
336 qr.addContextLink(cl);
337 }
338
339 /**
340 * Removes a quick reply set from a quick reply's context menu.
341 *
342 * @param {string} setName name of the existing quick reply set containing the quick reply
343 * @param {string|number} label label of the existing quick reply or its numeric ID
344 * @param {string} contextSetName name of the existing quick reply set to be used as a context menu
345 */
346 deleteContextItem(setName, label, contextSetName) {
347 const qr = this.getQrByLabel(setName, label);
348 const set = this.getSetByName(contextSetName);
349 if (!qr) {
350 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
351 }
352 if (!set) {
353 throw new Error(`No quick reply set with name "${contextSetName}" found.`);
354 }
355 qr.removeContextLink(set.name);
356 }
357
358 /**
359 * Removes all entries from a quick reply's context menu.
360 *
361 * @param {string} setName name of the existing quick reply set containing the quick reply
362 * @param {string|number} label label of the existing quick reply or its numeric ID
363 */
364 clearContextMenu(setName, label) {
365 const qr = this.getQrByLabel(setName, label);
366 if (!qr) {
367 throw new Error(`No quick reply with label "${label}" in set "${setName}" found.`);
368 }
369 qr.clearContextLinks();
370 }
371
372
373 /**
374 * Create a new quick reply set.
375 *
376 * @param {string} name name of the new quick reply set
377 * @param {object} [props]
378 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
379 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
380 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
381 * @returns {Promise<QuickReplySet>} the new quick reply set
382 */
383 async createSet(name, {
384 disableSend,
385 placeBeforeInput,
386 injectInput,
387 } = {}) {
388 const set = new QuickReplySet();
389 set.name = name;
390 set.disableSend = disableSend ?? false;
391 set.placeBeforeInput = placeBeforeInput ?? false;
392 set.injectInput = injectInput ?? false;
393 const oldSet = this.getSetByName(name);
394 if (oldSet) {
395 QuickReplySet.list.splice(QuickReplySet.list.indexOf(oldSet), 1, set);
396 } else {
397 const idx = QuickReplySet.list.findIndex(it => it.name.localeCompare(name) == 1);
398 if (idx > -1) {
399 QuickReplySet.list.splice(idx, 0, set);
400 } else {
401 QuickReplySet.list.push(set);
402 }
403 }
404 await set.save();
405 this.settingsUi.rerender();
406 return set;
407 }
408
409 /**
410 * Update an existing quick reply set.
411 *
412 * @param {string} name name of the existing quick reply set
413 * @param {object} [props]
414 * @param {boolean} [props.disableSend] whether or not to send the quick replies or put the message or slash command into the char input box
415 * @param {boolean} [props.placeBeforeInput] whether or not to place the quick reply contents before the existing user input
416 * @param {boolean} [props.injectInput] whether or not to automatically inject the user input at the end of the quick reply
417 * @returns {Promise<QuickReplySet>} the altered quick reply set
418 */
419 async updateSet(name, {
420 disableSend,
421 placeBeforeInput,
422 injectInput,
423 } = {}) {
424 const set = this.getSetByName(name);
425 if (!set) {
426 throw new Error(`No quick reply set with name "${name}" found.`);
427 }
428 set.disableSend = disableSend ?? false;
429 set.placeBeforeInput = placeBeforeInput ?? false;
430 set.injectInput = injectInput ?? false;
431 await set.save();
432 this.settingsUi.rerender();
433 return set;
434 }
435
436 /**
437 * Delete an existing quick reply set.
438 *
439 * @param {string} name name of the existing quick reply set
440 */
441 async deleteSet(name) {
442 const set = this.getSetByName(name);
443 if (!set) {
444 throw new Error(`No quick reply set with name "${name}" found.`);
445 }
446 await set.delete();
447 this.settingsUi.rerender();
448 }
449
450
451 /**
452 * Gets a list of all quick reply sets.
453 *
454 * @returns array with the names of all quick reply sets
455 */
456 listSets() {
457 return QuickReplySet.list.map(it => it.name);
458 }
459 /**
460 * Gets a list of all globally active quick reply sets.
461 *
462 * @returns array with the names of all quick reply sets
463 */
464 listGlobalSets() {
465 return this.settings.config.setList.map(it => it.set.name);
466 }
467 /**
468 * Gets a list of all quick reply sets activated by the current chat.
469 *
470 * @returns array with the names of all quick reply sets
471 */
472 listChatSets() {
473 return this.settings.chatConfig?.setList?.flatMap(it => it.set.name) ?? [];
474 }
475
476 /**
477 * Gets a list of all quick replies in the quick reply set.
478 *
479 * @param {string} setName name of the existing quick reply set
480 * @returns array with the labels of this set's quick replies
481 */
482 listQuickReplies(setName) {
483 const set = this.getSetByName(setName);
484 if (!set) {
485 throw new Error(`No quick reply set with name "${name}" found.`);
486 }
487 return set.qrList.map(it => it.label);
488 }
489
490 /**
491 * Gets a list of all Automation IDs used by quick replies.
492 *
493 * @returns {String[]} array with all automation IDs used by quick replies
494 */
495 listAutomationIds() {
496 return this
497 .listSets()
498 .flatMap(it => ({ set: it, qrs: this.listQuickReplies(it) }))
499 .map(it => it.qrs?.map(qr => this.getQrByLabel(it.set, qr)?.automationId))
500 .flat()
501 .filter(Boolean)
502 .filter(onlyUnique)
503 .map(String);
504 }
505}