Spaces:
Paused
Paused
| import { chat, chat_metadata, eventSource, event_types, getRequestHeaders } from '../../../script.js'; | |
| import { extension_settings } from '../../extensions.js'; | |
| import { QuickReplyApi } from './api/QuickReplyApi.js'; | |
| import { AutoExecuteHandler } from './src/AutoExecuteHandler.js'; | |
| import { QuickReply } from './src/QuickReply.js'; | |
| import { QuickReplyConfig } from './src/QuickReplyConfig.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'; | |
| const _VERBOSE = true; | |
| export const debug = (...msg) => _VERBOSE ? console.debug('[QR2]', ...msg) : null; | |
| export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null; | |
| export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null; | |
| /** | |
| * 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; | |
| }; | |
| } | |
| const defaultConfig = { | |
| setList: [{ | |
| set: 'Default', | |
| isVisible: true, | |
| }], | |
| }; | |
| const defaultSettings = { | |
| isEnabled: false, | |
| isCombined: false, | |
| config: defaultConfig, | |
| }; | |
| /** @type {Boolean}*/ | |
| let isReady = false; | |
| /** @type {Function[]}*/ | |
| let executeQueue = []; | |
| /** @type {QuickReplySettings}*/ | |
| let settings; | |
| /** @type {SettingsUi} */ | |
| let manager; | |
| /** @type {ButtonUi} */ | |
| let buttons; | |
| /** @type {AutoExecuteHandler} */ | |
| let autoExec; | |
| /** @type {QuickReplyApi} */ | |
| export let quickReplyApi; | |
| 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) { | |
| 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 = {}; | |
| qr.id = idx + 1; | |
| qr.label = slot.label ?? ''; | |
| qr.title = slot.title ?? ''; | |
| qr.message = slot.mes ?? ''; | |
| 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; | |
| qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false; | |
| qr.executeOnNewChat = slot.autoExecute_newChat ?? false; | |
| qr.automationId = slot.automationId ?? ''; | |
| qr.contextList = (slot.contextMenu ?? []).map(it=>({ | |
| set: it.preset, | |
| isChained: it.chain, | |
| })); | |
| return qr; | |
| }); | |
| } | |
| if (set.version == 2) { | |
| QuickReplySet.list.push(QuickReplySet.from(JSON.parse(JSON.stringify(set)))); | |
| } | |
| } | |
| // need to load QR lists after all sets are loaded to be able to resolve context menu entries | |
| setList.forEach((set, idx)=>{ | |
| QuickReplySet.list[idx].qrList = set.qrList.map(it=>QuickReply.from(it)); | |
| QuickReplySet.list[idx].init(); | |
| }); | |
| log('sets: ', QuickReplySet.list); | |
| } | |
| }; | |
| const loadSettings = async () => { | |
| if (!extension_settings.quickReplyV2) { | |
| if (!extension_settings.quickReply) { | |
| extension_settings.quickReplyV2 = defaultSettings; | |
| } else { | |
| extension_settings.quickReplyV2 = { | |
| isEnabled: extension_settings.quickReply.quickReplyEnabled ?? false, | |
| isCombined: false, | |
| isPopout: false, | |
| config: { | |
| setList: [{ | |
| set: extension_settings.quickReply.selectedPreset ?? extension_settings.quickReply.name ?? 'Default', | |
| isVisible: true, | |
| }], | |
| }, | |
| }; | |
| } | |
| } | |
| try { | |
| settings = QuickReplySettings.from(extension_settings.quickReplyV2); | |
| } catch (ex) { | |
| 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)); | |
| } | |
| }; | |
| const init = async () => { | |
| await loadSets(); | |
| await loadSettings(); | |
| log('settings: ', settings); | |
| manager = new SettingsUi(settings); | |
| document.querySelector('#qr_container').append(await manager.render()); | |
| buttons = new ButtonUi(settings); | |
| buttons.show(); | |
| settings.onSave = ()=>buttons.refresh(); | |
| window['executeQuickReplyByName'] = async(name, args = {}, options = {}) => { | |
| 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('.'); | |
| qrName = qrName.join('.'); | |
| let qrs = QuickReplySet.get(setName); | |
| if (qrs) { | |
| qr = qrs.qrList.find(it=>it.label == qrName); | |
| } | |
| } | |
| if (qr && qr.onExecute) { | |
| return await qr.execute(args, false, true, options); | |
| } else { | |
| throw new Error(`No Quick Reply found for "${name}".`); | |
| } | |
| }; | |
| quickReplyApi = new QuickReplyApi(settings, manager); | |
| const slash = new SlashCommandHandler(quickReplyApi); | |
| slash.init(); | |
| autoExec = new AutoExecuteHandler(settings); | |
| eventSource.on(event_types.APP_READY, async()=>await finalizeInit()); | |
| window['quickReplyApi'] = quickReplyApi; | |
| }; | |
| const finalizeInit = async () => { | |
| debug('executing startup'); | |
| await autoExec.handleStartup(); | |
| debug('/executing startup'); | |
| debug(`executing queue (${executeQueue.length} items)`); | |
| while (executeQueue.length > 0) { | |
| const func = executeQueue.shift(); | |
| await func(); | |
| } | |
| debug('/executing queue'); | |
| isReady = true; | |
| debug('READY'); | |
| }; | |
| await init(); | |
| 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(); | |
| await autoExec.handleChatChanged(); | |
| }; | |
| eventSource.on(event_types.CHAT_CHANGED, (...args)=>executeIfReadyElseQueue(onChatChanged, args)); | |
| const onUserMessage = async () => { | |
| await autoExec.handleUser(); | |
| }; | |
| eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onUserMessage, args)); | |
| const onAiMessage = async (messageId) => { | |
| if (['...'].includes(chat[messageId]?.mes)) { | |
| log('QR auto-execution suppressed for swiped message'); | |
| return; | |
| } | |
| await autoExec.handleAi(); | |
| }; | |
| eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, (...args)=>executeIfReadyElseQueue(onAiMessage, args)); | |
| 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)); | |
| const onNewChat = async () => { | |
| await autoExec.handleNewChat(); | |
| }; | |
| eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args)); | |