Blame Raw
Cohee · e3f41666 · · 619 lines (22.8 KB)
1 contributor
1import {
2 MAX_INJECTION_DEPTH,
3 animation_duration,
4 chat_metadata,
5 eventSource,
6 event_types,
7 extension_prompt_roles,
8 extension_prompt_types,
9 saveSettingsDebounced,
10 this_chid,
11} from '../script.js';
12import { selected_group } from './group-chats.js';
13import { extension_settings, getContext, saveMetadataDebounced } from './extensions.js';
14import { getCharaFilename, debounce, delay } from './utils.js';
15import { getTokenCountAsync } from './tokenizers.js';
16import { debounce_timeout } from './constants.js';
17import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
18import { SlashCommand } from './slash-commands/SlashCommand.js';
19import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js';
20export { MODULE_NAME as NOTE_MODULE_NAME };
21import { t } from './i18n.js';
22import { macros, MacroCategory } from './macros/macro-system.js';
23import { MacrosParser } from './macros.js';
24import { power_user } from './power-user.js';
25
26const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
27
28export var shouldWIAddPrompt = false;
29
30export const metadata_keys = {
31 prompt: 'note_prompt',
32 interval: 'note_interval',
33 depth: 'note_depth',
34 position: 'note_position',
35 role: 'note_role',
36};
37
38const chara_note_position = {
39 replace: 0,
40 before: 1,
41 after: 2,
42};
43
44function setNoteTextCommand(_, text) {
45 if (text) {
46 $('#extension_floating_prompt').val(text).trigger('input');
47 toastr.success(t`Author's Note text updated`);
48 }
49 return chat_metadata[metadata_keys.prompt];
50}
51
52function setNoteDepthCommand(_, text) {
53 if (text) {
54 const value = Number(text);
55
56 if (Number.isNaN(value)) {
57 toastr.error(t`Not a valid number`);
58 return;
59 }
60
61 $('#extension_floating_depth').val(Math.abs(value)).trigger('input');
62 toastr.success(t`Author's Note depth updated`);
63 }
64 return chat_metadata[metadata_keys.depth];
65}
66
67function setNoteIntervalCommand(_, text) {
68 if (text) {
69 const value = Number(text);
70
71 if (Number.isNaN(value)) {
72 toastr.error(t`Not a valid number`);
73 return;
74 }
75
76 $('#extension_floating_interval').val(Math.abs(value)).trigger('input');
77 toastr.success(t`Author's Note frequency updated`);
78 }
79 return chat_metadata[metadata_keys.interval];
80}
81
82function setNotePositionCommand(_, text) {
83 const validPositions = {
84 'after': 0,
85 'scenario': 0,
86 'chat': 1,
87 'before_scenario': 2,
88 'before': 2,
89 };
90
91 if (text) {
92 const position = validPositions[text?.trim()?.toLowerCase()];
93
94 if (typeof position === 'undefined') {
95 toastr.error(t`Not a valid position`);
96 return;
97 }
98
99 $(`input[name="extension_floating_position"][value="${position}"]`).prop('checked', true).trigger('input');
100 toastr.info(t`Author's Note position updated`);
101 }
102 return Object.keys(validPositions).find(key => validPositions[key] == chat_metadata[metadata_keys.position]);
103}
104
105function setNoteRoleCommand(_, text) {
106 const validRoles = {
107 'system': 0,
108 'user': 1,
109 'assistant': 2,
110 };
111
112 if (text) {
113 const role = validRoles[text?.trim()?.toLowerCase()];
114
115 if (typeof role === 'undefined') {
116 toastr.error(t`Not a valid role`);
117 return;
118 }
119
120 $('#extension_floating_role').val(Math.abs(role)).trigger('input');
121 toastr.info(t`Author's Note role updated`);
122 }
123 return Object.keys(validRoles).find(key => validRoles[key] == chat_metadata[metadata_keys.role]);
124}
125
126function updateSettings() {
127 saveSettingsDebounced();
128 loadSettings();
129 setFloatingPrompt();
130}
131
132const setMainPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_prompt_token_counter').text(await getTokenCountAsync(value)), debounce_timeout.relaxed);
133const setCharaPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_chara_token_counter').text(await getTokenCountAsync(value)), debounce_timeout.relaxed);
134const setDefaultPromptTokenCounterDebounced = debounce(async (value) => $('#extension_floating_default_token_counter').text(await getTokenCountAsync(value)), debounce_timeout.relaxed);
135
136async function onExtensionFloatingPromptInput() {
137 chat_metadata[metadata_keys.prompt] = $(this).val();
138 setMainPromptTokenCounterDebounced(chat_metadata[metadata_keys.prompt]);
139 updateSettings();
140 saveMetadataDebounced();
141}
142
143async function onExtensionFloatingIntervalInput() {
144 chat_metadata[metadata_keys.interval] = Number($(this).val());
145 updateSettings();
146 saveMetadataDebounced();
147}
148
149async function onExtensionFloatingDepthInput() {
150 let value = Number($(this).val());
151
152 if (value < 0) {
153 value = Math.abs(value);
154 $(this).val(value);
155 }
156
157 chat_metadata[metadata_keys.depth] = value;
158 updateSettings();
159 saveMetadataDebounced();
160}
161
162async function onExtensionFloatingPositionInput(e) {
163 chat_metadata[metadata_keys.position] = Number(e.target.value);
164 updateSettings();
165 saveMetadataDebounced();
166}
167
168async function onDefaultPositionInput(e) {
169 extension_settings.note.defaultPosition = Number(e.target.value);
170 saveSettingsDebounced();
171}
172
173async function onDefaultDepthInput() {
174 let value = Number($(this).val());
175
176 if (value < 0) {
177 value = Math.abs(value);
178 $(this).val(value);
179 }
180
181 extension_settings.note.defaultDepth = value;
182 saveSettingsDebounced();
183}
184
185async function onDefaultIntervalInput() {
186 extension_settings.note.defaultInterval = Number($(this).val());
187 saveSettingsDebounced();
188}
189
190function onExtensionFloatingRoleInput(e) {
191 chat_metadata[metadata_keys.role] = Number(e.target.value);
192 updateSettings();
193}
194
195function onExtensionDefaultRoleInput(e) {
196 extension_settings.note.defaultRole = Number(e.target.value);
197 saveSettingsDebounced();
198}
199
200async function onExtensionFloatingCharPositionInput(e) {
201 const value = e.target.value;
202 const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
203
204 if (charaNote) {
205 charaNote.position = Number(value);
206 updateSettings();
207 }
208}
209
210function onExtensionFloatingCharaPromptInput() {
211 const tempPrompt = $(this).val();
212 const avatarName = getCharaFilename();
213 let tempCharaNote = {
214 name: avatarName,
215 prompt: tempPrompt,
216 };
217
218 setCharaPromptTokenCounterDebounced(tempPrompt);
219
220 let existingCharaNoteIndex;
221 let existingCharaNote;
222
223 if (extension_settings.note.chara) {
224 existingCharaNoteIndex = extension_settings.note.chara.findIndex((e) => e.name === avatarName);
225 existingCharaNote = extension_settings.note.chara[existingCharaNoteIndex];
226 }
227
228 if (tempPrompt.length === 0 &&
229 extension_settings.note.chara &&
230 existingCharaNote &&
231 !existingCharaNote.useChara
232 ) {
233 extension_settings.note.chara.splice(existingCharaNoteIndex, 1);
234 } else if (extension_settings.note.chara && existingCharaNote) {
235 Object.assign(existingCharaNote, tempCharaNote);
236 } else if (avatarName && tempPrompt.length > 0) {
237 if (!extension_settings.note.chara) {
238 extension_settings.note.chara = [];
239 }
240 Object.assign(tempCharaNote, { useChara: false, position: chara_note_position.replace });
241
242 extension_settings.note.chara.push(tempCharaNote);
243 } else {
244 console.log('Character author\'s note error: No avatar name key could be found.');
245 toastr.error(t`Something went wrong. Could not save character's author's note.`);
246
247 // Don't save settings if something went wrong
248 return;
249 }
250
251 updateSettings();
252}
253
254function onExtensionFloatingCharaCheckboxChanged() {
255 const value = !!$(this).prop('checked');
256 const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
257
258 if (charaNote) {
259 charaNote.useChara = value;
260
261 updateSettings();
262 }
263}
264
265function onExtensionFloatingDefaultInput() {
266 extension_settings.note.default = $(this).val();
267 setDefaultPromptTokenCounterDebounced(extension_settings.note.default);
268 updateSettings();
269}
270
271function loadSettings() {
272 const DEFAULT_DEPTH = 4;
273 const DEFAULT_POSITION = 1;
274 const DEFAULT_INTERVAL = 1;
275 const DEFAULT_ROLE = extension_prompt_roles.SYSTEM;
276
277 if (extension_settings.note.defaultPosition === undefined) {
278 extension_settings.note.defaultPosition = DEFAULT_POSITION;
279 }
280
281 if (extension_settings.note.defaultDepth === undefined) {
282 extension_settings.note.defaultDepth = DEFAULT_DEPTH;
283 }
284
285 if (extension_settings.note.defaultInterval === undefined) {
286 extension_settings.note.defaultInterval = DEFAULT_INTERVAL;
287 }
288
289 if (extension_settings.note.defaultRole === undefined) {
290 extension_settings.note.defaultRole = DEFAULT_ROLE;
291 }
292
293 chat_metadata[metadata_keys.prompt] = chat_metadata[metadata_keys.prompt] ?? extension_settings.note.default ?? '';
294 chat_metadata[metadata_keys.interval] = chat_metadata[metadata_keys.interval] ?? extension_settings.note.defaultInterval ?? DEFAULT_INTERVAL;
295 chat_metadata[metadata_keys.position] = chat_metadata[metadata_keys.position] ?? extension_settings.note.defaultPosition ?? DEFAULT_POSITION;
296 chat_metadata[metadata_keys.depth] = chat_metadata[metadata_keys.depth] ?? extension_settings.note.defaultDepth ?? DEFAULT_DEPTH;
297 chat_metadata[metadata_keys.role] = chat_metadata[metadata_keys.role] ?? extension_settings.note.defaultRole ?? DEFAULT_ROLE;
298 $('#extension_floating_prompt').val(chat_metadata[metadata_keys.prompt]);
299 $('#extension_floating_interval').val(chat_metadata[metadata_keys.interval]);
300 $('#extension_floating_allow_wi_scan').prop('checked', extension_settings.note.allowWIScan ?? false);
301 $('#extension_floating_depth').val(chat_metadata[metadata_keys.depth]);
302 $('#extension_floating_role').val(chat_metadata[metadata_keys.role]);
303 $(`input[name="extension_floating_position"][value="${chat_metadata[metadata_keys.position]}"]`).prop('checked', true);
304
305 if (extension_settings.note.chara && getContext().characterId !== undefined) {
306 const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
307
308 $('#extension_floating_chara').val(charaNote ? charaNote.prompt : '');
309 $('#extension_use_floating_chara').prop('checked', charaNote ? charaNote.useChara : false);
310 $(`input[name="extension_floating_char_position"][value="${charaNote?.position ?? chara_note_position.replace}"]`).prop('checked', true);
311 } else {
312 $('#extension_floating_chara').val('');
313 $('#extension_use_floating_chara').prop('checked', false);
314 $(`input[name="extension_floating_char_position"][value="${chara_note_position.replace}"]`).prop('checked', true);
315 }
316
317 $('#extension_floating_default').val(extension_settings.note.default);
318 $('#extension_default_depth').val(extension_settings.note.defaultDepth);
319 $('#extension_default_interval').val(extension_settings.note.defaultInterval);
320 $('#extension_default_role').val(extension_settings.note.defaultRole);
321 $(`input[name="extension_default_position"][value="${extension_settings.note.defaultPosition}"]`).prop('checked', true);
322}
323
324export function setFloatingPrompt() {
325 const context = getContext();
326 if (!context.groupId && context.characterId === undefined) {
327 console.debug('setFloatingPrompt: Not in a chat. Skipping.');
328 shouldWIAddPrompt = false;
329 return;
330 }
331
332 // take the count of messages
333 let lastMessageNumber = Array.isArray(context.chat) && context.chat.length ? context.chat.filter(m => m.is_user).length : 0;
334
335 console.debug(`
336 setFloatingPrompt entered
337 ------
338 lastMessageNumber = ${lastMessageNumber}
339 metadata_keys.interval = ${chat_metadata[metadata_keys.interval]}
340 metadata_keys.position = ${chat_metadata[metadata_keys.position]}
341 metadata_keys.depth = ${chat_metadata[metadata_keys.depth]}
342 metadata_keys.role = ${chat_metadata[metadata_keys.role]}
343 ------
344 `);
345
346 // interval 1 should be inserted no matter what
347 if (chat_metadata[metadata_keys.interval] === 1) {
348 lastMessageNumber = 1;
349 }
350
351 if (lastMessageNumber <= 0 || chat_metadata[metadata_keys.interval] <= 0) {
352 context.setExtensionPrompt(MODULE_NAME, '', extension_prompt_types.NONE, MAX_INJECTION_DEPTH);
353 $('#extension_floating_counter').text('(disabled)');
354 shouldWIAddPrompt = false;
355 return;
356 }
357
358 const messagesTillInsertion = lastMessageNumber >= chat_metadata[metadata_keys.interval]
359 ? (lastMessageNumber % chat_metadata[metadata_keys.interval])
360 : (chat_metadata[metadata_keys.interval] - lastMessageNumber);
361 const shouldAddPrompt = messagesTillInsertion == 0;
362 shouldWIAddPrompt = shouldAddPrompt;
363
364 let prompt = shouldAddPrompt ? $('#extension_floating_prompt').val() : '';
365 if (shouldAddPrompt && extension_settings.note.chara && getContext().characterId !== undefined) {
366 const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
367
368 // Only replace with the chara note if the user checked the box
369 if (charaNote && charaNote.useChara) {
370 switch (charaNote.position) {
371 case chara_note_position.before:
372 prompt = charaNote.prompt + '\n' + prompt;
373 break;
374 case chara_note_position.after:
375 prompt = prompt + '\n' + charaNote.prompt;
376 break;
377 default:
378 prompt = charaNote.prompt;
379 break;
380 }
381 }
382 }
383 context.setExtensionPrompt(
384 MODULE_NAME,
385 String(prompt),
386 chat_metadata[metadata_keys.position],
387 chat_metadata[metadata_keys.depth],
388 extension_settings.note.allowWIScan,
389 chat_metadata[metadata_keys.role],
390 );
391 $('#extension_floating_counter').text(shouldAddPrompt ? '0' : messagesTillInsertion);
392}
393
394function onANMenuItemClick() {
395 if (!selected_group && this_chid === undefined) {
396 toastr.warning(t`Select a character before trying to use Author's Note`, '', { timeOut: 2000 });
397 return;
398 }
399
400 //show AN if it's hidden
401 const $ANcontainer = $('#floatingPrompt');
402 if ($ANcontainer.css('display') !== 'flex') {
403 $ANcontainer.addClass('resizing');
404 $ANcontainer.css('display', 'flex');
405 $ANcontainer.css('opacity', 0.0);
406 $ANcontainer.transition({
407 opacity: 1.0,
408 duration: animation_duration,
409 }, async function () {
410 await delay(50);
411 $ANcontainer.removeClass('resizing');
412 });
413
414 //auto-open the main AN inline drawer
415 if ($('#ANBlockToggle')
416 .siblings('.inline-drawer-content')
417 .css('display') !== 'block') {
418 $ANcontainer.addClass('resizing');
419 $('#ANBlockToggle').trigger('click');
420 }
421 } else {
422 //hide AN if it's already displayed
423 $ANcontainer.addClass('resizing');
424 $ANcontainer.transition({
425 opacity: 0.0,
426 duration: animation_duration,
427 }, async function () {
428 await delay(50);
429 $ANcontainer.removeClass('resizing');
430 });
431 setTimeout(function () {
432 $ANcontainer.hide();
433 }, animation_duration);
434 }
435
436 //duplicate options menu close handler from script.js
437 //because this listener takes priority
438 $('#options').stop().fadeOut(animation_duration);
439}
440
441async function onChatChanged() {
442 loadSettings();
443 setFloatingPrompt();
444 const context = getContext();
445
446 // Disable the chara note if in a group
447 $('#extension_floating_chara').prop('disabled', !!context.groupId);
448
449 const tokenCounter1 = chat_metadata[metadata_keys.prompt] ? await getTokenCountAsync(chat_metadata[metadata_keys.prompt]) : 0;
450 $('#extension_floating_prompt_token_counter').text(tokenCounter1);
451
452 let tokenCounter2;
453 if (extension_settings.note.chara && context.characterId !== undefined) {
454 const charaNote = extension_settings.note.chara.find((e) => e.name === getCharaFilename());
455
456 if (charaNote) {
457 tokenCounter2 = await getTokenCountAsync(charaNote.prompt);
458 }
459 }
460
461 $('#extension_floating_chara_token_counter').text(tokenCounter2 || 0);
462
463 const tokenCounter3 = extension_settings.note.default ? await getTokenCountAsync(extension_settings.note.default) : 0;
464 $('#extension_floating_default_token_counter').text(tokenCounter3);
465}
466
467function onAllowWIScanCheckboxChanged() {
468 extension_settings.note.allowWIScan = !!$(this).prop('checked');
469 updateSettings();
470}
471
472/**
473 * Inject author's note options and setup event listeners.
474 */
475// Inserts the extension first since it's statically imported
476export function initAuthorsNote() {
477 $('#extension_floating_prompt').on('input', onExtensionFloatingPromptInput);
478 $('#extension_floating_interval').on('input', onExtensionFloatingIntervalInput);
479 $('#extension_floating_depth').on('input', onExtensionFloatingDepthInput);
480 $('#extension_floating_chara').on('input', onExtensionFloatingCharaPromptInput);
481 $('#extension_use_floating_chara').on('input', onExtensionFloatingCharaCheckboxChanged);
482 $('#extension_floating_default').on('input', onExtensionFloatingDefaultInput);
483 $('#extension_default_depth').on('input', onDefaultDepthInput);
484 $('#extension_default_interval').on('input', onDefaultIntervalInput);
485 $('#extension_floating_allow_wi_scan').on('input', onAllowWIScanCheckboxChanged);
486 $('#extension_floating_role').on('input', onExtensionFloatingRoleInput);
487 $('#extension_default_role').on('input', onExtensionDefaultRoleInput);
488 $('input[name="extension_floating_position"]').on('change', onExtensionFloatingPositionInput);
489 $('input[name="extension_default_position"]').on('change', onDefaultPositionInput);
490 $('input[name="extension_floating_char_position"]').on('change', onExtensionFloatingCharPositionInput);
491 $('#ANClose').on('click', function () {
492 $('#floatingPrompt').transition({
493 opacity: 0,
494 duration: animation_duration,
495 easing: 'ease-in-out',
496 });
497 setTimeout(function () { $('#floatingPrompt').hide(); }, animation_duration);
498 });
499 $('#option_toggle_AN').on('click', onANMenuItemClick);
500
501 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
502 name: 'note',
503 callback: setNoteTextCommand,
504 returns: 'current author\'s note',
505 unnamedArgumentList: [
506 new SlashCommandArgument(
507 'text', [ARGUMENT_TYPE.STRING], false,
508 ),
509 ],
510 helpString: `
511 <div>
512 Sets an author's note for the currently selected chat if specified and returns the current note.
513 </div>
514 `,
515 }));
516 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
517 name: 'note-depth',
518 aliases: ['depth'],
519 callback: setNoteDepthCommand,
520 returns: 'current author\'s note depth',
521 unnamedArgumentList: [
522 new SlashCommandArgument(
523 'number', [ARGUMENT_TYPE.NUMBER], false,
524 ),
525 ],
526 helpString: `
527 <div>
528 Sets an author's note depth for in-chat positioning if specified and returns the current depth.
529 </div>
530 `,
531 }));
532 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
533 name: 'note-frequency',
534 aliases: ['freq', 'note-freq'],
535 callback: setNoteIntervalCommand,
536 returns: 'current author\'s note insertion frequency',
537 namedArgumentList: [],
538 unnamedArgumentList: [
539 new SlashCommandArgument(
540 'number', [ARGUMENT_TYPE.NUMBER], false,
541 ),
542 ],
543 helpString: `
544 <div>
545 Sets an author's note insertion frequency if specified and returns the current frequency.
546 </div>
547 `,
548 }));
549 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
550 name: 'note-position',
551 callback: setNotePositionCommand,
552 aliases: ['pos', 'note-pos'],
553 returns: 'current author\'s note insertion position',
554 namedArgumentList: [],
555 unnamedArgumentList: [
556 new SlashCommandArgument(
557 'position', [ARGUMENT_TYPE.STRING], false, false, null, ['before', 'after', 'chat'],
558 ),
559 ],
560 helpString: `
561 <div>
562 Sets an author's note position if specified and returns the current position.
563 </div>
564 `,
565 }));
566 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
567 name: 'note-role',
568 callback: setNoteRoleCommand,
569 returns: 'current author\'s note chat insertion role',
570 namedArgumentList: [],
571 unnamedArgumentList: [
572 new SlashCommandArgument(
573 'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
574 ),
575 ],
576 helpString: `
577 <div>
578 Sets an author's note chat insertion role if specified and returns the current role.
579 </div>
580 `,
581 }));
582 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
583
584 registerAuthorsNoteMacros();
585}
586
587function registerAuthorsNoteMacros() {
588 if (power_user.experimental_macro_engine) {
589 macros.register('authorsNote', {
590 category: MacroCategory.PROMPTS,
591 description: t`The contents of the Author's Note`,
592 handler: () => chat_metadata[metadata_keys.prompt] ?? '',
593 });
594 macros.register('charAuthorsNote', {
595 category: MacroCategory.PROMPTS,
596 description: t`The contents of the Character Author's Note`,
597 handler: () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '',
598 });
599 macros.register('defaultAuthorsNote', {
600 category: MacroCategory.PROMPTS,
601 description: t`The contents of the Default Author's Note`,
602 handler: () => extension_settings.note.default ?? '',
603 });
604 } else {
605 // TODO: Remove this when the experimental macro engine is replacing the old macro engine
606 MacrosParser.registerMacro('authorsNote',
607 () => chat_metadata[metadata_keys.prompt] ?? '',
608 t`The contents of the Author's Note`,
609 );
610 MacrosParser.registerMacro('charAuthorsNote',
611 () => this_chid !== undefined ? (extension_settings.note.chara.find((e) => e.name === getCharaFilename())?.prompt ?? '') : '',
612 t`The contents of the Character Author's Note`,
613 );
614 MacrosParser.registerMacro('defaultAuthorsNote',
615 () => extension_settings.note.default ?? '',
616 t`The contents of the Default Author's Note`,
617 );
618 }
619}