Blame Raw
Cohee · 51ad27fb · · 321 lines (11.3 KB)
2 contributors
1import { chat, chat_metadata, eventSource, event_types, getRequestHeaders, this_chid, characters } from '../../../script.js';
2import { extension_settings } from '../../extensions.js';
3import { QuickReplyApi } from './api/QuickReplyApi.js';
4import { AutoExecuteHandler } from './src/AutoExecuteHandler.js';
5import { QuickReply } from './src/QuickReply.js';
6import { QuickReplyConfig } from './src/QuickReplyConfig.js';
7import { QuickReplySet } from './src/QuickReplySet.js';
8import { QuickReplySettings } from './src/QuickReplySettings.js';
9import { SlashCommandHandler } from './src/SlashCommandHandler.js';
10import { ButtonUi } from './src/ui/ButtonUi.js';
11import { SettingsUi } from './src/ui/SettingsUi.js';
12import { debounceAsync } from '../../utils.js';
13import { selected_group } from '../../group-chats.js';
14export { debounceAsync };
15
16
17const _VERBOSE = true;
18export const debug = (...msg) => _VERBOSE ? console.debug('[QR2]', ...msg) : null;
19export const log = (...msg) => _VERBOSE ? console.log('[QR2]', ...msg) : null;
20export const warn = (...msg) => _VERBOSE ? console.warn('[QR2]', ...msg) : null;
21
22
23const defaultConfig = {
24 setList: [{
25 set: 'Default',
26 isVisible: true,
27 }],
28};
29
30const defaultSettings = {
31 isEnabled: false,
32 isCombined: false,
33 config: defaultConfig,
34};
35
36
37/** @type {Boolean}*/
38let isReady = false;
39/** @type {Function[]}*/
40let executeQueue = [];
41/** @type {string}*/
42let lastCharId;
43/** @type {QuickReplySettings}*/
44let settings;
45/** @type {SettingsUi} */
46let manager;
47/** @type {ButtonUi} */
48let buttons;
49/** @type {AutoExecuteHandler} */
50let autoExec;
51/** @type {QuickReplyApi} */
52export let quickReplyApi;
53
54
55const loadSets = async () => {
56 const response = await fetch('/api/settings/get', {
57 method: 'POST',
58 headers: getRequestHeaders(),
59 body: JSON.stringify({}),
60 });
61
62 if (response.ok) {
63 const setList = (await response.json()).quickReplyPresets ?? [];
64 for (const set of setList) {
65 if (set.version !== 2) {
66 // migrate old QR set
67 set.version = 2;
68 set.disableSend = set.quickActionEnabled ?? false;
69 set.placeBeforeInput = set.placeBeforeInputEnabled ?? false;
70 set.injectInput = set.AutoInputInject ?? false;
71 set.qrList = set.quickReplySlots.map((slot, idx) => {
72 const qr = {};
73 qr.id = idx + 1;
74 qr.label = slot.label ?? '';
75 qr.title = slot.title ?? '';
76 qr.message = slot.mes ?? '';
77 qr.isHidden = slot.hidden ?? false;
78 qr.executeOnStartup = slot.autoExecute_appStartup ?? false;
79 qr.executeOnUser = slot.autoExecute_userMessage ?? false;
80 qr.executeOnAi = slot.autoExecute_botMessage ?? false;
81 qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false;
82 qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false;
83 qr.executeOnNewChat = slot.autoExecute_newChat ?? false;
84 qr.executeBeforeGeneration = slot.autoExecute_beforeGeneration ?? false;
85 qr.automationId = slot.automationId ?? '';
86 qr.contextList = (slot.contextMenu ?? []).map(it => ({
87 set: it.preset,
88 isChained: it.chain,
89 }));
90 return qr;
91 });
92 }
93 if (set.version == 2) {
94 QuickReplySet.list.push(QuickReplySet.from(JSON.parse(JSON.stringify(set))));
95 }
96 }
97 // need to load QR lists after all sets are loaded to be able to resolve context menu entries
98 setList.forEach((set, idx) => {
99 QuickReplySet.list[idx].qrList = set.qrList.map(it => QuickReply.from(it));
100 QuickReplySet.list[idx].init();
101 });
102 log('sets: ', QuickReplySet.list);
103 }
104};
105
106const loadSettings = async () => {
107 if (!extension_settings.quickReplyV2) {
108 if (!extension_settings.quickReply) {
109 extension_settings.quickReplyV2 = defaultSettings;
110 } else {
111 extension_settings.quickReplyV2 = {
112 isEnabled: extension_settings.quickReply.quickReplyEnabled ?? false,
113 isCombined: false,
114 isPopout: false,
115 config: {
116 setList: [{
117 set: extension_settings.quickReply.selectedPreset ?? extension_settings.quickReply.name ?? 'Default',
118 isVisible: true,
119 }],
120 },
121 };
122 }
123 }
124 try {
125 settings = QuickReplySettings.from(extension_settings.quickReplyV2);
126 settings.config.scope = 'global';
127 settings.config.onUpdate = () => settings.save();
128 } catch (ex) {
129 settings = QuickReplySettings.from(defaultSettings);
130 }
131};
132
133const executeIfReadyElseQueue = async (functionToCall, args) => {
134 if (isReady) {
135 log('calling', { functionToCall, args });
136 await functionToCall(...args);
137 } else {
138 log('queueing', { functionToCall, args });
139 executeQueue.push(async () => await functionToCall(...args));
140 }
141};
142
143const handleCharChange = () => {
144 if (lastCharId === this_chid) return;
145
146 // Unload the old character's config and update the character ID cache.
147 settings.charConfig = null;
148 lastCharId = this_chid;
149
150 // If no character is loaded, there's nothing more to do.
151 /** @type {Character} */
152 const character = characters[this_chid];
153 if (!character || selected_group) {
154 return;
155 }
156
157 // Get the character-specific config from the local settings storage.
158 let charConfig = settings.characterConfigs[character.avatar];
159
160 // If no config exists for this character, create a new one.
161 if (!charConfig) {
162 charConfig = QuickReplyConfig.from({ setList: [] });
163 settings.characterConfigs[character.avatar] = charConfig;
164 }
165
166 charConfig.scope = 'character';
167 // The main settings save function will handle persistence.
168 charConfig.onUpdate = () => settings.save();
169 settings.charConfig = charConfig;
170};
171
172export async function init() {
173 await loadSets();
174 await loadSettings();
175 log('settings: ', settings);
176
177 manager = new SettingsUi(settings);
178 document.querySelector('#qr_container').append(await manager.render());
179
180 buttons = new ButtonUi(settings);
181 buttons.show();
182 settings.onSave = () => buttons.refresh();
183
184 globalThis.executeQuickReplyByName = async (name, args = {}, options = {}) => {
185 let qr = [
186 ...settings.config.setList,
187 ...(settings.chatConfig?.setList ?? []),
188 ...(settings.charConfig?.setList ?? []),
189 ]
190 .map(it => it.set.qrList)
191 .flat()
192 .find(it => it.label == name)
193 ;
194 if (!qr) {
195 let [setName, ...qrName] = name.split('.');
196 qrName = qrName.join('.');
197 let qrs = QuickReplySet.get(setName);
198 if (qrs) {
199 qr = qrs.qrList.find(it => it.label == qrName);
200 }
201 }
202 if (qr && qr.onExecute) {
203 return await qr.execute(args, false, true, options);
204 } else {
205 throw new Error(`No Quick Reply found for "${name}".`);
206 }
207 };
208
209 quickReplyApi = new QuickReplyApi(settings, manager);
210 const slash = new SlashCommandHandler(quickReplyApi);
211 slash.init();
212 autoExec = new AutoExecuteHandler(settings);
213
214 eventSource.on(event_types.APP_READY, async () => await finalizeInit());
215
216 globalThis.quickReplyApi = quickReplyApi;
217}
218
219const finalizeInit = async () => {
220 debug('executing startup');
221 await autoExec.handleStartup();
222 debug('/executing startup');
223
224 debug(`executing queue (${executeQueue.length} items)`);
225 while (executeQueue.length > 0) {
226 const func = executeQueue.shift();
227 await func();
228 }
229 debug('/executing queue');
230 isReady = true;
231 debug('READY');
232};
233
234
235const purgeCharacterQuickReplySets = ({ character }) => {
236 // Remove the character's Quick Reply Sets from the settings.
237 const avatar = character?.avatar;
238 if (avatar && avatar in settings.characterConfigs) {
239 log(`Purging Quick Reply Sets for character: ${avatar}`);
240 delete settings.characterConfigs[avatar];
241 settings.save();
242 }
243};
244
245const updateCharacterQuickReplySets = (oldAvatar, newAvatar) => {
246 // Update the character's Quick Reply Sets in the settings.
247 if (oldAvatar && newAvatar && oldAvatar !== newAvatar) {
248 log(`Updating Quick Reply Sets for character: ${oldAvatar} -> ${newAvatar}`);
249 if (settings.characterConfigs[oldAvatar]) {
250 settings.characterConfigs[newAvatar] = settings.characterConfigs[oldAvatar];
251 delete settings.characterConfigs[oldAvatar];
252 settings.save();
253 }
254 }
255};
256
257const onChatChanged = async (chatIdx) => {
258 log('CHAT_CHANGED', chatIdx);
259
260 handleCharChange();
261
262 if (chatIdx) {
263 const chatConfig = QuickReplyConfig.from(chat_metadata.quickReply ?? {});
264 chatConfig.scope = 'chat';
265 chatConfig.onUpdate = () => settings.save();
266 settings.chatConfig = chatConfig;
267 } else {
268 settings.chatConfig = null;
269 }
270 manager.rerender();
271 buttons.refresh();
272
273 await autoExec.handleChatChanged();
274};
275eventSource.on(event_types.CHAT_CHANGED, (...args) => executeIfReadyElseQueue(onChatChanged, args));
276eventSource.on(event_types.CHARACTER_DELETED, purgeCharacterQuickReplySets);
277eventSource.on(event_types.CHARACTER_RENAMED, updateCharacterQuickReplySets);
278
279const onUserMessage = async () => {
280 await autoExec.handleUser();
281};
282eventSource.makeFirst(event_types.USER_MESSAGE_RENDERED, (...args) => executeIfReadyElseQueue(onUserMessage, args));
283
284const onAiMessage = async (messageId) => {
285 if (['...'].includes(chat[messageId]?.mes)) {
286 log('QR auto-execution suppressed for swiped message');
287 return;
288 }
289
290 await autoExec.handleAi();
291};
292eventSource.makeFirst(event_types.CHARACTER_MESSAGE_RENDERED, (...args) => executeIfReadyElseQueue(onAiMessage, args));
293
294const onGroupMemberDraft = async () => {
295 await autoExec.handleGroupMemberDraft();
296};
297eventSource.on(event_types.GROUP_MEMBER_DRAFTED, (...args) => executeIfReadyElseQueue(onGroupMemberDraft, args));
298
299const onWIActivation = async (entries) => {
300 await autoExec.handleWIActivation(entries);
301};
302eventSource.on(event_types.WORLD_INFO_ACTIVATED, (...args) => executeIfReadyElseQueue(onWIActivation, args));
303
304const onNewChat = async () => {
305 await autoExec.handleNewChat();
306};
307eventSource.on(event_types.CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
308eventSource.on(event_types.GROUP_CHAT_CREATED, (...args) => executeIfReadyElseQueue(onNewChat, args));
309
310const onBeforeGeneration = async (_generationType, _options = {}, isDryRun = false) => {
311 if (isDryRun) {
312 log('Before-generation hook skipped due to dryRun.');
313 return;
314 }
315 if (selected_group && this_chid === undefined) {
316 log('Before-generation hook skipped for event before group wrapper.');
317 return;
318 }
319 await autoExec.handleBeforeGeneration();
320};
321eventSource.on(event_types.GENERATION_AFTER_COMMANDS, (...args) => executeIfReadyElseQueue(onBeforeGeneration, args));