2024-01-07 00:11:30 +00:00
|
|
|
import { getRequestHeaders, substituteParams } from '../../../../script.js';
|
2024-07-10 17:34:48 -04:00
|
|
|
import { Popup, POPUP_RESULT, POPUP_TYPE } from '../../../popup.js';
|
2024-05-12 15:15:05 -04:00
|
|
|
import { executeSlashCommands, executeSlashCommandsOnChatInput, executeSlashCommandsWithOptions } from '../../../slash-commands.js';
|
2024-06-18 14:29:29 -04:00
|
|
|
import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
|
2024-05-12 15:15:05 -04:00
|
|
|
import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
|
2024-07-10 17:34:48 -04:00
|
|
|
import { debounceAsync, log, warn } from '../index.js';
|
2023-12-20 13:40:44 +00:00
|
|
|
import { QuickReply } from './QuickReply.js';
|
|
|
|
|
|
|
|
export class QuickReplySet {
|
|
|
|
/**@type {QuickReplySet[]}*/ static list = [];
|
|
|
|
|
|
|
|
|
|
|
|
static from(props) {
|
2023-12-20 21:44:55 +00:00
|
|
|
props.qrList = []; //props.qrList?.map(it=>QuickReply.from(it));
|
2023-12-20 13:40:44 +00:00
|
|
|
const instance = Object.assign(new this(), props);
|
2023-12-20 21:44:55 +00:00
|
|
|
// instance.init();
|
2023-12-20 13:40:44 +00:00
|
|
|
return instance;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-07-13 14:23:49 -04:00
|
|
|
* @param {string} name - name of the QuickReplySet
|
2023-12-20 13:40:44 +00:00
|
|
|
*/
|
|
|
|
static get(name) {
|
|
|
|
return this.list.find(it=>it.name == name);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-07-13 14:23:49 -04:00
|
|
|
/**@type {string}*/ name;
|
|
|
|
/**@type {boolean}*/ disableSend = false;
|
|
|
|
/**@type {boolean}*/ placeBeforeInput = false;
|
|
|
|
/**@type {boolean}*/ injectInput = false;
|
2024-07-13 14:45:35 -04:00
|
|
|
/**@type {string}*/ color = 'transparent';
|
|
|
|
/**@type {boolean}*/ onlyBorderColor = false;
|
2023-12-20 13:40:44 +00:00
|
|
|
/**@type {QuickReply[]}*/ qrList = [];
|
|
|
|
|
2024-07-13 14:23:49 -04:00
|
|
|
/**@type {number}*/ idIndex = 0;
|
2023-12-20 13:40:44 +00:00
|
|
|
|
2024-07-13 14:23:49 -04:00
|
|
|
/**@type {boolean}*/ isDeleted = false;
|
2023-12-20 13:40:44 +00:00
|
|
|
|
2024-07-13 14:23:49 -04:00
|
|
|
/**@type {function}*/ save;
|
2023-12-20 13:40:44 +00:00
|
|
|
|
|
|
|
/**@type {HTMLElement}*/ dom;
|
|
|
|
/**@type {HTMLElement}*/ settingsDom;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor() {
|
2023-12-22 12:56:06 +00:00
|
|
|
this.save = debounceAsync(()=>this.performSave(), 200);
|
2023-12-20 13:40:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
init() {
|
|
|
|
this.qrList.forEach(qr=>this.hookQuickReply(qr));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
unrender() {
|
|
|
|
this.dom?.remove();
|
|
|
|
this.dom = null;
|
|
|
|
}
|
|
|
|
render() {
|
|
|
|
this.unrender();
|
|
|
|
if (!this.dom) {
|
|
|
|
const root = document.createElement('div'); {
|
|
|
|
this.dom = root;
|
|
|
|
root.classList.add('qr--buttons');
|
2024-07-13 14:45:35 -04:00
|
|
|
this.updateColor();
|
2023-12-20 13:40:44 +00:00
|
|
|
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
|
|
|
|
root.append(qr.render());
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this.dom;
|
|
|
|
}
|
|
|
|
rerender() {
|
|
|
|
if (!this.dom) return;
|
|
|
|
this.dom.innerHTML = '';
|
|
|
|
this.qrList.filter(qr=>!qr.isHidden).forEach(qr=>{
|
|
|
|
this.dom.append(qr.render());
|
|
|
|
});
|
|
|
|
}
|
2024-07-13 14:45:35 -04:00
|
|
|
updateColor() {
|
2024-07-14 17:02:24 -04:00
|
|
|
if (!this.dom) return;
|
2024-07-14 14:13:57 -04:00
|
|
|
if (this.color && this.color != 'transparent') {
|
2024-07-13 14:45:35 -04:00
|
|
|
this.dom.style.setProperty('--qr--color', this.color);
|
2024-07-14 14:13:57 -04:00
|
|
|
this.dom.classList.add('qr--color');
|
2024-07-13 14:45:35 -04:00
|
|
|
if (this.onlyBorderColor) {
|
|
|
|
this.dom.classList.add('qr--borderColor');
|
|
|
|
} else {
|
|
|
|
this.dom.classList.remove('qr--borderColor');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this.dom.style.setProperty('--qr--color', 'transparent');
|
2024-07-14 14:13:57 -04:00
|
|
|
this.dom.classList.remove('qr--color');
|
2024-07-13 14:45:35 -04:00
|
|
|
this.dom.classList.remove('qr--borderColor');
|
|
|
|
}
|
|
|
|
}
|
2023-12-20 13:40:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderSettings() {
|
|
|
|
if (!this.settingsDom) {
|
|
|
|
this.settingsDom = document.createElement('div'); {
|
|
|
|
this.settingsDom.classList.add('qr--set-qrListContents');
|
|
|
|
this.qrList.forEach((qr,idx)=>{
|
|
|
|
this.renderSettingsItem(qr, idx);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return this.settingsDom;
|
|
|
|
}
|
2024-07-10 17:34:48 -04:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {QuickReply} qr
|
|
|
|
* @param {number} idx
|
|
|
|
*/
|
2023-12-20 13:40:44 +00:00
|
|
|
renderSettingsItem(qr, idx) {
|
|
|
|
this.settingsDom.append(qr.renderSettings(idx));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-06-18 14:29:29 -04:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {QuickReply} qr
|
|
|
|
*/
|
|
|
|
async debug(qr) {
|
|
|
|
const parser = new SlashCommandParser();
|
|
|
|
const closure = parser.parse(qr.message, true, [], qr.abortController, qr.debugController);
|
2024-07-08 18:07:37 -04:00
|
|
|
closure.source = `${this.name}.${qr.label}`;
|
2024-06-18 14:29:29 -04:00
|
|
|
closure.onProgress = (done, total) => qr.updateEditorProgress(done, total);
|
2024-07-05 19:14:30 -04:00
|
|
|
closure.scope.setMacro('arg::*', '');
|
2024-06-18 14:29:29 -04:00
|
|
|
return (await closure.execute())?.pipe;
|
|
|
|
}
|
2023-12-20 13:40:44 +00:00
|
|
|
/**
|
2024-05-12 15:15:05 -04:00
|
|
|
*
|
|
|
|
* @param {QuickReply} qr The QR to execute.
|
|
|
|
* @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
|
2024-07-08 18:07:37 -04:00
|
|
|
* @param {import('../../../slash-commands.js').ExecuteSlashCommandsOptions} [options.executionOptions] ({}) further execution options
|
2024-05-12 15:15:05 -04:00
|
|
|
* @returns
|
2023-12-20 13:40:44 +00:00
|
|
|
*/
|
2024-05-12 15:15:05 -04:00
|
|
|
async executeWithOptions(qr, options = {}) {
|
|
|
|
options = Object.assign({
|
|
|
|
message:null,
|
|
|
|
isAutoExecute:false,
|
|
|
|
isEditor:false,
|
|
|
|
isRun:false,
|
|
|
|
scope:null,
|
2024-07-08 18:07:37 -04:00
|
|
|
executionOptions:{},
|
2024-05-12 15:15:05 -04:00
|
|
|
}, options);
|
2024-07-08 18:07:37 -04:00
|
|
|
const execOptions = options.executionOptions;
|
2023-12-20 13:40:44 +00:00
|
|
|
/**@type {HTMLTextAreaElement}*/
|
|
|
|
const ta = document.querySelector('#send_textarea');
|
2024-05-12 15:15:05 -04:00
|
|
|
const finalMessage = options.message ?? qr.message;
|
2023-12-20 13:40:44 +00:00
|
|
|
let input = ta.value;
|
2024-05-12 15:15:05 -04:00
|
|
|
if (!options.isAutoExecute && !options.isEditor && !options.isRun && this.injectInput && input.length > 0) {
|
2023-12-20 13:40:44 +00:00
|
|
|
if (this.placeBeforeInput) {
|
2023-12-27 12:28:15 +00:00
|
|
|
input = `${finalMessage} ${input}`;
|
2023-12-20 13:40:44 +00:00
|
|
|
} else {
|
2023-12-27 12:28:15 +00:00
|
|
|
input = `${input} ${finalMessage}`;
|
2023-12-20 13:40:44 +00:00
|
|
|
}
|
|
|
|
} else {
|
2023-12-27 12:28:15 +00:00
|
|
|
input = `${finalMessage} `;
|
2023-12-20 13:40:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (input[0] == '/' && !this.disableSend) {
|
2024-05-12 15:15:05 -04:00
|
|
|
let result;
|
|
|
|
if (options.isAutoExecute || options.isRun) {
|
2024-07-08 18:07:37 -04:00
|
|
|
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
|
2024-05-12 15:15:05 -04:00
|
|
|
handleParserErrors: true,
|
|
|
|
scope: options.scope,
|
2024-07-08 18:07:37 -04:00
|
|
|
source: `${this.name}.${qr.label}`,
|
|
|
|
}));
|
2024-05-12 15:15:05 -04:00
|
|
|
} else if (options.isEditor) {
|
2024-07-08 18:07:37 -04:00
|
|
|
result = await executeSlashCommandsWithOptions(input, Object.assign(execOptions, {
|
2024-05-12 15:15:05 -04:00
|
|
|
handleParserErrors: false,
|
|
|
|
scope: options.scope,
|
|
|
|
abortController: qr.abortController,
|
2024-07-08 18:07:37 -04:00
|
|
|
source: `${this.name}.${qr.label}`,
|
2024-05-12 15:15:05 -04:00
|
|
|
onProgress: (done, total) => qr.updateEditorProgress(done, total),
|
2024-07-08 18:07:37 -04:00
|
|
|
}));
|
2024-05-12 15:15:05 -04:00
|
|
|
} else {
|
2024-07-08 18:07:37 -04:00
|
|
|
result = await executeSlashCommandsOnChatInput(input, Object.assign(execOptions, {
|
2024-05-12 15:15:05 -04:00
|
|
|
scope: options.scope,
|
2024-07-08 18:07:37 -04:00
|
|
|
source: `${this.name}.${qr.label}`,
|
|
|
|
}));
|
2024-05-12 15:15:05 -04:00
|
|
|
}
|
2023-12-20 13:40:44 +00:00
|
|
|
return typeof result === 'object' ? result?.pipe : '';
|
|
|
|
}
|
|
|
|
|
2024-01-07 00:11:30 +00:00
|
|
|
ta.value = substituteParams(input);
|
2023-12-20 13:40:44 +00:00
|
|
|
ta.focus();
|
|
|
|
|
|
|
|
if (!this.disableSend) {
|
|
|
|
// @ts-ignore
|
|
|
|
document.querySelector('#send_but').click();
|
|
|
|
}
|
|
|
|
}
|
2024-05-12 15:15:05 -04:00
|
|
|
/**
|
|
|
|
* @param {QuickReply} qr
|
2024-07-13 14:23:49 -04:00
|
|
|
* @param {string} [message] - optional altered message to be used
|
2024-05-12 15:15:05 -04:00
|
|
|
* @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,
|
|
|
|
});
|
|
|
|
}
|
2023-12-20 13:40:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2024-07-10 17:34:48 -04:00
|
|
|
addQuickReply(data = {}) {
|
2023-12-20 13:40:44 +00:00
|
|
|
const id = Math.max(this.idIndex, this.qrList.reduce((max,qr)=>Math.max(max,qr.id),0)) + 1;
|
2024-07-10 17:34:48 -04:00
|
|
|
data.id =
|
2023-12-20 13:40:44 +00:00
|
|
|
this.idIndex = id + 1;
|
2024-07-10 17:34:48 -04:00
|
|
|
const qr = QuickReply.from(data);
|
2023-12-20 13:40:44 +00:00
|
|
|
this.qrList.push(qr);
|
|
|
|
this.hookQuickReply(qr);
|
|
|
|
if (this.settingsDom) {
|
|
|
|
this.renderSettingsItem(qr, this.qrList.length - 1);
|
|
|
|
}
|
|
|
|
if (this.dom) {
|
|
|
|
this.dom.append(qr.render());
|
|
|
|
}
|
|
|
|
this.save();
|
|
|
|
return qr;
|
|
|
|
}
|
|
|
|
|
2024-06-18 14:29:29 -04:00
|
|
|
/**
|
|
|
|
*
|
|
|
|
* @param {QuickReply} qr
|
|
|
|
*/
|
2023-12-20 13:40:44 +00:00
|
|
|
hookQuickReply(qr) {
|
2024-06-18 14:29:29 -04:00
|
|
|
qr.onDebug = ()=>this.debug(qr);
|
2024-05-12 15:15:05 -04:00
|
|
|
qr.onExecute = (_, options)=>this.executeWithOptions(qr, options);
|
2023-12-20 13:40:44 +00:00
|
|
|
qr.onDelete = ()=>this.removeQuickReply(qr);
|
|
|
|
qr.onUpdate = ()=>this.save();
|
2024-07-10 17:34:48 -04:00
|
|
|
qr.onInsertBefore = (qrJson)=>{
|
|
|
|
const data = JSON.parse(qrJson ?? '{}');
|
|
|
|
delete data.id;
|
|
|
|
log('onInsertBefore', data);
|
|
|
|
const newQr = this.addQuickReply(data);
|
|
|
|
this.qrList.pop();
|
|
|
|
this.qrList.splice(this.qrList.indexOf(qr), 0, newQr);
|
|
|
|
if (qr.settingsDom) {
|
|
|
|
qr.settingsDom.insertAdjacentElement('beforebegin', newQr.settingsDom);
|
|
|
|
}
|
|
|
|
this.save();
|
|
|
|
};
|
|
|
|
qr.onTransfer = async()=>{
|
|
|
|
/**@type {HTMLSelectElement} */
|
|
|
|
let sel;
|
|
|
|
let isCopy = false;
|
|
|
|
const dom = document.createElement('div'); {
|
|
|
|
dom.classList.add('qr--transferModal');
|
|
|
|
const title = document.createElement('h3'); {
|
|
|
|
title.textContent = 'Transfer Quick Reply';
|
|
|
|
dom.append(title);
|
|
|
|
}
|
|
|
|
const subTitle = document.createElement('h4'); {
|
|
|
|
const entryName = qr.label;
|
|
|
|
const bookName = this.name;
|
|
|
|
subTitle.textContent = `${bookName}: ${entryName}`;
|
|
|
|
dom.append(subTitle);
|
|
|
|
}
|
|
|
|
sel = document.createElement('select'); {
|
|
|
|
sel.classList.add('qr--transferSelect');
|
|
|
|
sel.setAttribute('autofocus', '1');
|
|
|
|
const noOpt = document.createElement('option'); {
|
|
|
|
noOpt.value = '';
|
|
|
|
noOpt.textContent = '-- Select QR Set --';
|
|
|
|
sel.append(noOpt);
|
|
|
|
}
|
|
|
|
for (const qrs of QuickReplySet.list) {
|
|
|
|
const opt = document.createElement('option'); {
|
|
|
|
opt.value = qrs.name;
|
|
|
|
opt.textContent = qrs.name;
|
|
|
|
sel.append(opt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sel.addEventListener('keyup', (evt)=>{
|
|
|
|
if (evt.key == 'Shift') {
|
|
|
|
(dlg.dom ?? dlg.dlg).classList.remove('qr--isCopy');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
sel.addEventListener('keydown', (evt)=>{
|
|
|
|
if (evt.key == 'Shift') {
|
|
|
|
(dlg.dom ?? dlg.dlg).classList.add('qr--isCopy');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!evt.ctrlKey && !evt.altKey && evt.key == 'Enter') {
|
|
|
|
evt.preventDefault();
|
|
|
|
if (evt.shiftKey) isCopy = true;
|
|
|
|
dlg.completeAffirmative();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
dom.append(sel);
|
|
|
|
}
|
|
|
|
const hintP = document.createElement('p'); {
|
|
|
|
const hint = document.createElement('small'); {
|
|
|
|
hint.textContent = 'Type or arrows to select QR Set. Enter to transfer. Shift+Enter to copy.';
|
|
|
|
hintP.append(hint);
|
|
|
|
}
|
|
|
|
dom.append(hintP);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const dlg = new Popup(dom, POPUP_TYPE.CONFIRM, null, { okButton:'Transfer', cancelButton:'Cancel' });
|
|
|
|
const copyBtn = document.createElement('div'); {
|
|
|
|
copyBtn.classList.add('qr--copy');
|
|
|
|
copyBtn.classList.add('menu_button');
|
|
|
|
copyBtn.textContent = 'Copy';
|
|
|
|
copyBtn.addEventListener('click', ()=>{
|
|
|
|
isCopy = true;
|
|
|
|
dlg.completeAffirmative();
|
|
|
|
});
|
|
|
|
(dlg.ok ?? dlg.okButton).insertAdjacentElement('afterend', copyBtn);
|
|
|
|
}
|
|
|
|
const prom = dlg.show();
|
|
|
|
sel.focus();
|
|
|
|
await prom;
|
|
|
|
if (dlg.result == POPUP_RESULT.AFFIRMATIVE) {
|
|
|
|
const qrs = QuickReplySet.list.find(it=>it.name == sel.value);
|
|
|
|
qrs.addQuickReply(qr.toJSON());
|
|
|
|
if (!isCopy) {
|
|
|
|
qr.delete();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2023-12-20 13:40:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
removeQuickReply(qr) {
|
|
|
|
this.qrList.splice(this.qrList.indexOf(qr), 1);
|
|
|
|
this.save();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
toJSON() {
|
|
|
|
return {
|
|
|
|
version: 2,
|
|
|
|
name: this.name,
|
|
|
|
disableSend: this.disableSend,
|
|
|
|
placeBeforeInput: this.placeBeforeInput,
|
|
|
|
injectInput: this.injectInput,
|
2024-07-13 14:45:35 -04:00
|
|
|
color: this.color,
|
|
|
|
onlyBorderColor: this.onlyBorderColor,
|
2023-12-20 13:40:44 +00:00
|
|
|
qrList: this.qrList,
|
|
|
|
idIndex: this.idIndex,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async performSave() {
|
2024-03-20 00:46:46 +02:00
|
|
|
const response = await fetch('/api/quick-replies/save', {
|
2023-12-20 13:40:44 +00:00
|
|
|
method: 'POST',
|
|
|
|
headers: getRequestHeaders(),
|
|
|
|
body: JSON.stringify(this),
|
|
|
|
});
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
this.rerender();
|
|
|
|
} else {
|
|
|
|
warn(`Failed to save Quick Reply Set: ${this.name}`);
|
2024-06-28 03:28:16 +02:00
|
|
|
console.error('QR could not be saved', response);
|
2023-12-20 13:40:44 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-20 15:49:27 +00:00
|
|
|
|
|
|
|
async delete() {
|
2024-03-20 00:46:46 +02:00
|
|
|
const response = await fetch('/api/quick-replies/delete', {
|
2023-12-20 15:49:27 +00:00
|
|
|
method: 'POST',
|
|
|
|
headers: getRequestHeaders(),
|
|
|
|
body: JSON.stringify(this),
|
|
|
|
});
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
|
|
this.unrender();
|
|
|
|
const idx = QuickReplySet.list.indexOf(this);
|
|
|
|
QuickReplySet.list.splice(idx, 1);
|
2023-12-20 17:56:08 +00:00
|
|
|
this.isDeleted = true;
|
2023-12-20 15:49:27 +00:00
|
|
|
} else {
|
|
|
|
warn(`Failed to delete Quick Reply Set: ${this.name}`);
|
|
|
|
}
|
|
|
|
}
|
2023-12-20 13:40:44 +00:00
|
|
|
}
|