SillyTavern/public/scripts/extensions/quick-reply/index.js

255 lines
8.3 KiB
JavaScript
Raw Normal View History

2023-12-20 14:40:44 +01:00
import { chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js';
import { extension_settings } from '../../extensions.js';
2023-12-21 21:05:42 +01:00
import { QuickReplyApi } from './api/QuickReplyApi.js';
import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';
2023-12-20 14:40:44 +01:00
import { QuickReply } from './src/QuickReply.js';
import { QuickReplyConfig } from './src/QuickReplyConfig.js';
import { QuickReplySet } from './src/QuickReplySet.js';
import { QuickReplySettings } from './src/QuickReplySettings.js';
2023-12-21 19:44:58 +01:00
import { SlashCommandHandler } from './src/SlashCommandHandler.js';
2023-12-20 14:40:44 +01:00
import { ButtonUi } from './src/ui/ButtonUi.js';
import { SettingsUi } from './src/ui/SettingsUi.js';
const _VERBOSE = true;
export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null;
2023-12-22 13:56:06 +01:00
/**
* Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
* @param {Function} func The function to debounce.
* @param {Number} [timeout=300] The timeout in milliseconds.
* @returns {Function} The debounced function.
*/
export function debounceAsync(func, timeout = 300) {
let timer;
/**@type {Promise}*/
let debouncePromise;
/**@type {Function}*/
let debounceResolver;
return (...args) => {
clearTimeout(timer);
if (!debouncePromise) {
debouncePromise = new Promise(resolve => {
debounceResolver = resolve;
});
}
timer = setTimeout(() => {
debounceResolver(func.apply(this, args));
debouncePromise = null;
}, timeout);
return debouncePromise;
};
}
2023-12-20 14:40:44 +01:00
const defaultConfig = {
setList: [{
set: 'Default',
isVisible: true,
}],
};
const defaultSettings = {
2024-01-01 19:12:28 +01:00
isEnabled: false,
2023-12-20 14:40:44 +01:00
isCombined: false,
config: defaultConfig,
};
/** @type {Boolean}*/
let isReady = false;
/** @type {Function[]}*/
let executeQueue = [];
2023-12-20 14:40:44 +01:00
/** @type {QuickReplySettings}*/
let settings;
/** @type {SettingsUi} */
let manager;
/** @type {ButtonUi} */
let buttons;
/** @type {AutoExecuteHandler} */
let autoExec;
2023-12-21 21:05:42 +01:00
/** @type {QuickReplyApi} */
2023-12-21 21:15:32 +01:00
export let quickReplyApi;
2023-12-20 14:40:44 +01:00
const loadSets = async () => {
const response = await fetch('/api/settings/get', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({}),
});
if (response.ok) {
const setList = (await response.json()).quickReplyPresets ?? [];
for (const set of setList) {
2024-01-01 18:01:16 +01:00
if (set.version !== 2) {
// migrate old QR set
set.version = 2;
set.disableSend = set.quickActionEnabled ?? false;
set.placeBeforeInput = set.placeBeforeInputEnabled ?? false;
set.injectInput = set.AutoInputInject ?? false;
set.qrList = set.quickReplySlots.map((slot,idx)=>{
const qr = {};
2023-12-20 14:40:44 +01:00
qr.id = idx + 1;
2024-01-05 17:45:36 +01:00
qr.label = slot.label ?? '';
qr.title = slot.title ?? '';
qr.message = slot.mes ?? '';
2023-12-20 14:40:44 +01:00
qr.isHidden = slot.hidden ?? false;
qr.executeOnStartup = slot.autoExecute_appStartup ?? false;
qr.executeOnUser = slot.autoExecute_userMessage ?? false;
qr.executeOnAi = slot.autoExecute_botMessage ?? false;
qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
2024-01-18 17:08:38 +01:00
qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
2024-04-05 16:27:08 +02:00
qr.automationId = slot.automationId ?? '';
2024-01-01 18:01:16 +01:00
qr.contextList = (slot.contextMenu ?? []).map(it=>({
2023-12-20 14:40:44 +01:00
set: it.preset,
isChained: it.chain,
2024-01-01 18:01:16 +01:00
}));
2023-12-20 14:40:44 +01:00
return qr;
});
2024-01-01 18:01:16 +01:00
}
if (set.version == 2) {
QuickReplySet.list.push(QuickReplySet.from(JSON.parse(JSON.stringify(set))));
2023-12-20 14:40:44 +01:00
}
}
2024-01-01 18:01:16 +01:00
// need to load QR lists after all sets are loaded to be able to resolve context menu entries
2023-12-20 22:44:55 +01:00
setList.forEach((set, idx)=>{
QuickReplySet.list[idx].qrList = set.qrList.map(it=>QuickReply.from(it));
QuickReplySet.list[idx].init();
});
2023-12-20 14:40:44 +01:00
log('sets: ', QuickReplySet.list);
}
};
const loadSettings = async () => {
if (!extension_settings.quickReplyV2) {
2023-12-22 14:00:02 +01:00
if (!extension_settings.quickReply) {
extension_settings.quickReplyV2 = defaultSettings;
} else {
extension_settings.quickReplyV2 = {
isEnabled: extension_settings.quickReply.quickReplyEnabled ?? false,
isCombined: false,
isPopout: false,
2024-01-01 19:12:28 +01:00
config: {
setList: [{
set: extension_settings.quickReply.selectedPreset ?? extension_settings.quickReply.name ?? 'Default',
isVisible: true,
}],
},
2023-12-22 14:00:02 +01:00
};
}
2023-12-20 14:40:44 +01:00
}
try {
settings = QuickReplySettings.from(extension_settings.quickReplyV2);
2023-12-20 18:56:08 +01:00
} catch (ex) {
2023-12-20 14:40:44 +01:00
settings = QuickReplySettings.from(defaultSettings);
}
};
const executeIfReadyElseQueue = async (functionToCall, args) => {
if (isReady) {
log('calling', { functionToCall, args });
await functionToCall(...args);
} else {
log('queueing', { functionToCall, args });
executeQueue.push(async()=>await functionToCall(...args));
}
};
2023-12-20 14:40:44 +01:00
const init = async () => {
await loadSets();
await loadSettings();
log('settings: ', settings);
manager = new SettingsUi(settings);
document.querySelector('#extensions_settings2').append(await manager.render());
buttons = new ButtonUi(settings);
buttons.show();
settings.onSave = ()=>buttons.refresh();
window['executeQuickReplyByName'] = async(name, args = {}) => {
2023-12-20 14:40:44 +01:00
let qr = [...settings.config.setList, ...(settings.chatConfig?.setList ?? [])]
.map(it=>it.set.qrList)
.flat()
.find(it=>it.label == name)
;
if (!qr) {
let [setName, ...qrName] = name.split('.');
name = qrName.join('.');
let qrs = QuickReplySet.get(setName);
if (qrs) {
qr = qrs.qrList.find(it=>it.label == name);
}
}
if (qr && qr.onExecute) {
return await qr.execute(args);
2023-12-20 14:40:44 +01:00
}
};
2023-12-26 12:37:23 +01:00
quickReplyApi = new QuickReplyApi(settings, manager);
const slash = new SlashCommandHandler(quickReplyApi);
slash.init();
autoExec = new AutoExecuteHandler(settings);
2023-12-26 12:37:23 +01:00
eventSource.on(event_types.APP_READY, async()=>await finalizeInit());
2024-02-25 02:54:40 +01:00
window['quickReplyApi'] = quickReplyApi;
2024-01-18 21:43:31 +01:00
};
const finalizeInit = async () => {
log('executing startup');
2024-01-15 18:30:14 +01:00
await autoExec.handleStartup();
log('/executing startup');
log(`executing queue (${executeQueue.length} items)`);
while (executeQueue.length > 0) {
const func = executeQueue.shift();
await func();
}
log('/executing queue');
isReady = true;
log('READY');
2023-12-20 14:40:44 +01:00
};
2024-01-18 21:43:31 +01:00
await init();
2023-12-20 14:40:44 +01:00
const onChatChanged = async (chatIdx) => {
log('CHAT_CHANGED', chatIdx);
if (chatIdx) {
settings.chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {});
} else {
settings.chatConfig = null;
}
manager.rerender();
buttons.refresh();
2024-01-15 18:30:14 +01:00
await autoExec.handleChatChanged();
2023-12-20 14:40:44 +01:00
};
eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args));
2023-12-20 14:40:44 +01:00
const onUserMessage = async () => {
2024-01-15 18:30:14 +01:00
await autoExec.handleUser();
2023-12-20 14:40:44 +01:00
};
eventSource.on(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args));
2023-12-20 14:40:44 +01:00
const onAiMessage = async () => {
2024-01-15 18:30:14 +01:00
await autoExec.handleAi();
2023-12-20 14:40:44 +01:00
};
eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args));
2024-01-18 17:08:38 +01:00
const onGroupMemberDraft = async () => {
await autoExec.handleGroupMemberDraft();
};
eventSource.on(event_types.GROUP_MEMBER_DRAFTED, (...args) => executeIfReadyElseQueue(onGroupMemberDraft, args));
const onWIActivation = async (entries) => {
await autoExec.handleWIActivation(entries);
};
eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));