Blame Raw
Cohee · e3f41666 · · 2144 lines (88.6 KB)
1 contributor
1'use strict';
2
3import { DOMPurify } from '../lib.js';
4
5import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js';
6import { is_group_generating } from './group-chats.js';
7import { Message, MessageCollection, TokenHandler } from './openai.js';
8import { power_user } from './power-user.js';
9import { debounce, waitUntilCondition, escapeHtml, uuidv4 } from './utils.js';
10import { debounce_timeout } from './constants.js';
11import { renderTemplateAsync } from './templates.js';
12import { Popup } from './popup.js';
13import { t } from './i18n.js';
14import { isMobile } from './RossAscends-mods.js';
15
16function debouncePromise(func, delay) {
17 let timeoutId;
18
19 return (...args) => {
20 clearTimeout(timeoutId);
21
22 return new Promise((resolve) => {
23 timeoutId = setTimeout(() => {
24 const result = func(...args);
25 resolve(result);
26 }, delay);
27 });
28 };
29}
30
31const DEFAULT_DEPTH = 4;
32const DEFAULT_ORDER = 100;
33
34/**
35 * @enum {number}
36 */
37export const INJECTION_POSITION = {
38 RELATIVE: 0,
39 ABSOLUTE: 1,
40};
41
42/**
43 * Register migrations for the prompt manager when settings are loaded or an Open AI preset is loaded.
44 */
45const registerPromptManagerMigration = () => {
46 const migrate = (settings, savePreset = null, presetName = null) => {
47 if ('Default' === presetName) return;
48
49 if (settings.main_prompt || settings.nsfw_prompt || settings.jailbreak_prompt) {
50 console.log('Running prompt manager configuration migration');
51 if (settings.prompts === undefined || settings.prompts.length === 0) settings.prompts = structuredClone(chatCompletionDefaultPrompts.prompts);
52
53 const findPrompt = (identifier) => settings.prompts.find(prompt => identifier === prompt.identifier);
54 if (settings.main_prompt) {
55 findPrompt('main').content = settings.main_prompt;
56 delete settings.main_prompt;
57 }
58
59 if (settings.nsfw_prompt) {
60 findPrompt('nsfw').content = settings.nsfw_prompt;
61 delete settings.nsfw_prompt;
62 }
63
64 if (settings.jailbreak_prompt) {
65 findPrompt('jailbreak').content = settings.jailbreak_prompt;
66 delete settings.jailbreak_prompt;
67 }
68
69 if (savePreset && presetName) savePreset(presetName, settings, false);
70 }
71 };
72
73 eventSource.on(event_types.SETTINGS_LOADED_BEFORE, settings => migrate(settings));
74 eventSource.on(event_types.OAI_PRESET_CHANGED_BEFORE, event => migrate(event.preset, event.savePreset, event.presetName));
75};
76
77/**
78 * Represents a prompt.
79 */
80class Prompt {
81 /**
82 * Indicates if the prompt is enabled.
83 * @type {boolean}
84 */
85 enabled;
86
87 /**
88 * Unique identifier for the prompt.
89 * @type {string}
90 */
91 identifier;
92
93 /**
94 * Role of the prompt, e.g., 'system', 'user', etc.
95 * @type {string}
96 */
97 role;
98
99 /**
100 * Content of the prompt.
101 * @type {string}
102 */
103 content;
104
105 /**
106 * Display name of the prompt.
107 * @type {string}
108 */
109 name;
110
111 /**
112 * Indicates if the prompt is a system prompt.
113 * @type {boolean}
114 */
115 system_prompt;
116
117 /**
118 * Position of the prompt in the prompt list.
119 * @type {string|number}
120 */
121 position;
122
123 /**
124 * Inject position of the prompt (relative = 0 or in-chat = 1)
125 * @type {number}
126 */
127 injection_position;
128
129 /**
130 * Depth of the prompt in the chat.
131 * @type {number}
132 */
133 injection_depth;
134
135 /**
136 * Order of the prompt in the chat.
137 * @type {number}
138 */
139 injection_order;
140
141 /**
142 * Indicates if the prompt should not be overridden.
143 * @type {boolean}
144 */
145 forbid_overrides;
146
147 /**
148 * Prompt is added by an extension.
149 * @type {boolean}
150 */
151 extension;
152
153 /**
154 * A list of generation type triggers for the prompt injection.
155 * @type {string[]}
156 */
157 injection_trigger;
158
159 /**
160 * Indicates if the prompt is a marker prompt.
161 * @type {boolean}
162 */
163 marker;
164
165 /**
166 * Create a new Prompt instance.
167 *
168 * @param {Object} [param0] - Object containing the properties of the prompt.
169 * @param {string} [param0.identifier] - The unique identifier of the prompt.
170 * @param {string} [param0.role] - The role associated with the prompt.
171 * @param {string} [param0.content] - The content of the prompt.
172 * @param {string} [param0.name] - The name of the prompt.
173 * @param {boolean} [param0.system_prompt] - Indicates if the prompt is a system prompt.
174 * @param {string|number} [param0.position] - The position of the prompt in the prompt list.
175 * @param {number} [param0.injection_position] - The insert position of the prompt.
176 * @param {number} [param0.injection_depth] - The depth of the prompt in the chat.
177 * @param {number} [param0.injection_order] - The order of the prompt in the chat.
178 * @param {string[]} [param0.injection_trigger] - The generation type trigger for the prompt injection.
179 * @param {boolean} [param0.forbid_overrides] - Indicates if the prompt should not be overridden.
180 * @param {boolean} [param0.extension] - Prompt is added by an extension.
181 */
182 constructor({ identifier, role, content, name, system_prompt, position, injection_depth, injection_position, forbid_overrides, extension, injection_order, injection_trigger } = {}) {
183 this.identifier = identifier;
184 this.role = role;
185 this.content = content;
186 this.name = name;
187 this.system_prompt = system_prompt;
188 this.position = position;
189 this.injection_depth = injection_depth;
190 this.injection_position = injection_position;
191 this.forbid_overrides = forbid_overrides;
192 this.extension = extension ?? false;
193 this.injection_order = injection_order ?? DEFAULT_ORDER;
194 this.injection_trigger = injection_trigger ?? [];
195 }
196}
197
198/**
199 * Representing a collection of prompts.
200 */
201export class PromptCollection {
202 /**
203 * List of Prompts in the collection.
204 * @type {Prompt[]}
205 */
206 collection = [];
207
208 /**
209 * List of identifiers of prompts that have been overridden.
210 * @type {string[]}
211 */
212 overriddenPrompts = [];
213
214 /**
215 * Create a new PromptCollection instance.
216 *
217 * @param {...Prompt} prompts - An array of Prompt instances.
218 */
219 constructor(...prompts) {
220 this.add(...prompts);
221 }
222
223 /**
224 * Checks if the provided instances are of the Prompt class.
225 *
226 * @param {...Prompt} prompts - Instances to check.
227 * @throws Will throw an error if one or more instances are not of the Prompt class.
228 */
229 checkPromptInstance(...prompts) {
230 for (let prompt of prompts) {
231 if (!(prompt instanceof Prompt)) {
232 throw new Error('Only Prompt instances can be added to PromptCollection');
233 }
234 }
235 }
236
237 /**
238 * Adds new Prompt instances to the collection.
239 *
240 * @param {...Prompt} prompts - An array of Prompt instances.
241 */
242 add(...prompts) {
243 this.checkPromptInstance(...prompts);
244 this.collection.push(...prompts);
245 }
246
247 /**
248 * Sets a Prompt instance at a specific position in the collection.
249 *
250 * @param {Prompt} prompt - The Prompt instance to set.
251 * @param {number} position - The position in the collection to set the Prompt instance.
252 */
253 set(prompt, position) {
254 this.checkPromptInstance(prompt);
255 this.collection[position] = prompt;
256 }
257
258 /**
259 * Retrieves a Prompt instance from the collection by its identifier.
260 *
261 * @param {string} identifier - The identifier of the Prompt instance to retrieve.
262 * @returns {Prompt} The Prompt instance with the provided identifier, or undefined if not found.
263 */
264 get(identifier) {
265 return this.collection.find(prompt => prompt.identifier === identifier);
266 }
267
268 /**
269 * Retrieves the index of a Prompt instance in the collection by its identifier.
270 *
271 * @param {string} identifier - The identifier of the Prompt instance to find.
272 * @returns {number} The index of the Prompt instance in the collection, or -1 if not found.
273 */
274 index(identifier) {
275 return this.collection.findIndex(prompt => prompt.identifier === identifier);
276 }
277
278 /**
279 * Checks if a Prompt instance exists in the collection by its identifier.
280 *
281 * @param {string} identifier - The identifier of the Prompt instance to check.
282 * @returns {boolean} true if the Prompt instance exists in the collection, false otherwise.
283 */
284 has(identifier) {
285 return this.index(identifier) !== -1;
286 }
287
288 /**
289 * Overrides a prompt at a specific position in the collection.
290 *
291 * @param {Prompt} prompt - The Prompt instance to override.
292 * @param {number} position - The position in the collection to override the Prompt instance.
293 */
294 override(prompt, position) {
295 this.set(prompt, position);
296 this.overriddenPrompts.push(prompt.identifier);
297 }
298}
299
300class PromptManager {
301 get promptSources() {
302 return {
303 charDescription: t`Character Description`,
304 charPersonality: t`Character Personality`,
305 scenario: t`Character Scenario`,
306 personaDescription: t`Persona Description`,
307 worldInfoBefore: t`World Info (↑Char)`,
308 worldInfoAfter: t`World Info (↓Char)`,
309 };
310 }
311
312 constructor() {
313 this.systemPrompts = [
314 'main',
315 'nsfw',
316 'jailbreak',
317 'enhanceDefinitions',
318 ];
319
320 this.overridablePrompts = [
321 'main',
322 'jailbreak',
323 ];
324
325 this.overriddenPrompts = [];
326
327 this.configuration = {
328 version: 1,
329 prefix: '',
330 containerIdentifier: '',
331 listIdentifier: '',
332 listItemTemplateIdentifier: '',
333 toggleDisabled: [],
334 promptOrder: {
335 strategy: 'global',
336 dummyId: 100000,
337 },
338 sortableDelay: 30,
339 warningTokenThreshold: 1500,
340 dangerTokenThreshold: 500,
341 defaultPrompts: {
342 main: '',
343 nsfw: '',
344 jailbreak: '',
345 enhanceDefinitions: '',
346 },
347 };
348
349 // Chatcompletion configuration object
350 this.serviceSettings = null;
351
352 // DOM element containing the prompt manager
353 this.containerElement = null;
354
355 // DOM element containing the prompt list
356 this.listElement = null;
357
358 // Currently selected character
359 this.activeCharacter = null;
360
361 // Message collection of the most recent chatcompletion
362 this.messages = null;
363
364 // The current token handler instance
365 this.tokenHandler = null;
366
367 // Token usage of last dry run
368 this.tokenUsage = 0;
369
370 // Error state, contains error message.
371 this.error = null;
372
373 /** Dry-run for generate, must return a promise */
374 this.tryGenerate = async () => { };
375
376 /** Called to persist the configuration, must return a promise */
377 this.saveServiceSettings = () => { return Promise.resolve(); };
378
379 /** Toggle prompt button click */
380 this.handleToggle = () => { };
381
382 /** Prompt name click */
383 this.handleInspect = () => { };
384
385 /** Edit prompt button click */
386 this.handleEdit = () => { };
387
388 /** Detach prompt button click */
389 this.handleDetach = () => { };
390
391 /** Save prompt button click */
392 this.handleSavePrompt = () => { };
393
394 /** Reset prompt button click */
395 this.handleResetPrompt = () => { };
396
397 /** New prompt button click */
398 this.handleNewPrompt = () => { };
399
400 /** Delete prompt button click */
401 this.handleDeletePrompt = () => { };
402
403 /** Append prompt button click */
404 this.handleAppendPrompt = () => { };
405
406 /** Import button click */
407 this.handleImport = () => { };
408
409 /** Full export click */
410 this.handleFullExport = () => { };
411
412 /** Character export click */
413 this.handleCharacterExport = () => { };
414
415 /** Character reset button click*/
416 this.handleCharacterReset = () => { };
417
418 /** Debounced version of render */
419 this.renderDebounced = debounce(this.render.bind(this), debounce_timeout.relaxed);
420 }
421
422
423 /**
424 * Initializes the PromptManager with provided configuration and service settings.
425 *
426 * Sets up various handlers for user interactions, event listeners and initial rendering of prompts.
427 * It is also responsible for preparing prompt edit form buttons, managing popup form close and clear actions.
428 *
429 * @param {Object} moduleConfiguration - Configuration object for the PromptManager.
430 * @param {Object} serviceSettings - Service settings object for the PromptManager.
431 */
432 init(moduleConfiguration, serviceSettings) {
433 this.configuration = Object.assign(this.configuration, moduleConfiguration);
434 this.tokenHandler = this.tokenHandler || new TokenHandler(() => { throw new Error('Token handler not set'); });
435 this.serviceSettings = serviceSettings;
436 this.containerElement = document.getElementById(this.configuration.containerIdentifier);
437
438 if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
439
440 this.sanitizeServiceSettings();
441
442 // Enable and disable prompts
443 this.handleToggle = (event) => {
444 const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
445 const promptOrderEntry = this.getPromptOrderEntry(this.activeCharacter, promptID);
446 const counts = this.tokenHandler.getCounts();
447
448 counts[promptID] = null;
449 promptOrderEntry.enabled = !promptOrderEntry.enabled;
450 this.render();
451 this.saveServiceSettings();
452 };
453
454 // Open edit form and load selected prompt
455 this.handleEdit = (event) => {
456 this.clearEditForm();
457 this.clearInspectForm();
458
459 const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
460 const prompt = this.getPromptById(promptID);
461
462 this.loadPromptIntoEditForm(prompt);
463
464 this.showPopup();
465 };
466
467 // Open edit form and load selected prompt
468 this.handleInspect = (event) => {
469 this.clearEditForm();
470 this.clearInspectForm();
471
472 const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
473 if (true === this.messages.hasItemWithIdentifier(promptID)) {
474 const messages = this.messages.getItemByIdentifier(promptID);
475
476 this.loadMessagesIntoInspectForm(messages);
477
478 this.showPopup('inspect');
479 }
480 };
481
482 // Detach selected prompt from list form and close edit form
483 this.handleDetach = (event) => {
484 if (null === this.activeCharacter) return;
485 const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
486 const prompt = this.getPromptById(promptID);
487
488 this.detachPrompt(prompt, this.activeCharacter);
489 this.hidePopup();
490 this.clearEditForm();
491 this.render();
492 this.saveServiceSettings();
493 };
494
495 // Save prompt edit form to settings and close form.
496 this.handleSavePrompt = (event) => {
497 const promptId = event.target.dataset.pmPrompt;
498 const prompt = this.getPromptById(promptId);
499
500 if (null === prompt) {
501 const newPrompt = {};
502 this.updatePromptWithPromptEditForm(newPrompt);
503 this.addPrompt(newPrompt, promptId);
504 } else {
505 this.updatePromptWithPromptEditForm(prompt);
506 }
507
508 if ('main' === promptId) this.updateQuickEdit('main', prompt);
509 if ('nsfw' === promptId) this.updateQuickEdit('nsfw', prompt);
510 if ('jailbreak' === promptId) this.updateQuickEdit('jailbreak', prompt);
511
512 this.log('Saved prompt: ' + promptId);
513
514 this.hidePopup();
515 this.clearEditForm();
516 this.render();
517 this.saveServiceSettings();
518 };
519
520 // Reset prompt should it be a system prompt
521 this.handleResetPrompt = (event) => {
522 const promptId = event.target.dataset.pmPrompt;
523 const prompt = this.getPromptById(promptId);
524 const isPulledPrompt = Object.keys(this.promptSources).includes(promptId);
525
526 switch (promptId) {
527 case 'main':
528 prompt.name = 'Main Prompt';
529 prompt.content = this.configuration.defaultPrompts.main;
530 prompt.forbid_overrides = false;
531 break;
532 case 'nsfw':
533 prompt.name = 'Nsfw Prompt';
534 prompt.content = this.configuration.defaultPrompts.nsfw;
535 break;
536 case 'jailbreak':
537 prompt.name = 'Jailbreak Prompt';
538 prompt.content = this.configuration.defaultPrompts.jailbreak;
539 prompt.forbid_overrides = false;
540 break;
541 case 'enhanceDefinitions':
542 prompt.name = 'Enhance Definitions';
543 prompt.content = this.configuration.defaultPrompts.enhanceDefinitions;
544 break;
545 }
546
547 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
548 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
549 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
550 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
551 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
552 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
553 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
554 const depthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block'));
555 const orderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_block'));
556 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
557 const forbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block'));
558 const entrySourceBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block'));
559 const entrySource = /** @type {HTMLSpanElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source'));
560
561 nameField.value = prompt.name;
562 roleField.value = 'system';
563 promptField.value = prompt.content ?? '';
564 injectionPositionField.value = (prompt.injection_position ?? 0).toString();
565 injectionDepthField.value = (prompt.injection_depth ?? DEFAULT_DEPTH).toString();
566 injectionOrderField.value = (prompt.injection_order ?? DEFAULT_ORDER).toString();
567 Array.from(injectionTriggerField.options).forEach(option => {
568 option.selected = false;
569 });
570 injectionTriggerField.dispatchEvent(new Event('change', { bubbles: true }));
571 depthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
572 orderBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
573 forbidOverridesField.checked = prompt.forbid_overrides ?? false;
574 forbidOverridesBlock.style.visibility = this.overridablePrompts.includes(prompt.identifier) ? 'visible' : 'hidden';
575 promptField.disabled = prompt.marker ?? false;
576 entrySourceBlock.style.display = isPulledPrompt ? '' : 'none';
577
578 if (isPulledPrompt) {
579 const sourceName = this.promptSources[promptId];
580 entrySource.textContent = sourceName;
581 }
582 };
583
584 // Append prompt to selected character
585 this.handleAppendPrompt = (event) => {
586 const appendPromptFooter = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt'));
587 const promptID = appendPromptFooter.value;
588 const prompt = this.getPromptById(promptID);
589
590 if (prompt) {
591 this.appendPrompt(prompt, this.activeCharacter);
592 this.render();
593 this.saveServiceSettings();
594 }
595 };
596
597 // Delete selected prompt from list form and close edit form
598 this.handleDeletePrompt = async (event) => {
599 Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {
600 if (!userChoice) return;
601 const appendPromptFooter = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt'));
602 const promptID = appendPromptFooter.value;
603 const prompt = this.getPromptById(promptID);
604
605 if (prompt && true === this.isPromptDeletionAllowed(prompt)) {
606 const promptIndex = this.getPromptIndexById(promptID);
607 this.serviceSettings.prompts.splice(Number(promptIndex), 1);
608
609 this.log('Deleted prompt: ' + prompt.identifier);
610
611 this.hidePopup();
612 this.clearEditForm();
613 this.render();
614 this.saveServiceSettings();
615 }
616 });
617 };
618
619 // Create new prompt, then save it to settings and close form.
620 this.handleNewPrompt = (event) => {
621 const prompt = {
622 identifier: this.getUuidv4(),
623 name: '',
624 role: 'system',
625 content: '',
626 };
627
628 this.loadPromptIntoEditForm(prompt);
629 this.showPopup();
630 };
631
632 // Export all user prompts
633 this.handleFullExport = () => {
634 const prompts = this.serviceSettings.prompts.reduce((userPrompts, prompt) => {
635 if (false === prompt.system_prompt && false === prompt.marker) userPrompts.push(prompt);
636 return userPrompts;
637 }, []);
638
639 let promptOrder = [];
640 if ('global' === this.configuration.promptOrder.strategy) {
641 promptOrder = this.getPromptOrderForCharacter({ id: this.configuration.promptOrder.dummyId });
642 } else if ('character' === this.configuration.promptOrder.strategy) {
643 promptOrder = [];
644 } else {
645 throw new Error('Prompt order strategy not supported.');
646 }
647
648 const exportPrompts = {
649 prompts: prompts,
650 prompt_order: promptOrder,
651 };
652
653 this.export(exportPrompts, 'full', 'st-prompts');
654 };
655
656 // Export user prompts and order for this character
657 this.handleCharacterExport = () => {
658 const characterPrompts = this.getPromptsForCharacter(this.activeCharacter).reduce((userPrompts, prompt) => {
659 if (false === prompt.system_prompt && !prompt.marker) userPrompts.push(prompt);
660 return userPrompts;
661 }, []);
662
663 const characterList = this.getPromptOrderForCharacter(this.activeCharacter);
664
665 const exportPrompts = {
666 prompts: characterPrompts,
667 prompt_order: characterList,
668 };
669
670 const name = this.activeCharacter.name + '-prompts';
671 this.export(exportPrompts, 'character', name);
672 };
673
674 // Import prompts for the selected character
675 this.handleImport = () => {
676 Popup.show.confirm(t`Existing prompts with the same ID will be overridden. Do you want to proceed?`, null)
677 .then(userChoice => {
678 if (!userChoice) return;
679
680 const fileOpener = document.createElement('input');
681 fileOpener.type = 'file';
682 fileOpener.accept = '.json';
683
684 fileOpener.addEventListener('change', (event) => {
685 if (!(event.target instanceof HTMLInputElement)) return;
686 const file = event.target.files[0];
687 if (!file) return;
688
689 const reader = new FileReader();
690
691 reader.onload = (event) => {
692 const fileContent = event.target.result;
693
694 try {
695 const data = JSON.parse(fileContent.toString());
696 this.import(data);
697 } catch (err) {
698 toastr.error(t`An error occurred while importing prompts. More info available in console.`);
699 console.log('An error occurred while importing prompts');
700 console.log(err.toString());
701 }
702 };
703
704 reader.readAsText(file);
705 });
706
707 fileOpener.click();
708 });
709 };
710
711 // Restore default state of a characters prompt order
712 this.handleCharacterReset = () => {
713 Popup.show.confirm(t`This will reset the prompt order for this character. You will not lose any prompts.`, null)
714 .then(userChoice => {
715 if (!userChoice) return;
716
717 this.removePromptOrderForCharacter(this.activeCharacter);
718 this.addPromptOrderForCharacter(this.activeCharacter, promptManagerDefaultPromptOrder);
719
720 this.render();
721 this.saveServiceSettings();
722 });
723 };
724
725 // Fill quick edit fields for the first time
726 if ('global' === this.configuration.promptOrder.strategy) {
727 const handleQuickEditSave = (event) => {
728 const promptId = event.target.dataset.pmPrompt;
729 const prompt = this.getPromptById(promptId);
730
731 prompt.content = event.target.value;
732
733 // Update edit form if present
734 // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
735 const popupEditFormPrompt = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
736 if (popupEditFormPrompt.offsetParent) {
737 popupEditFormPrompt.value = prompt.content;
738 }
739
740 this.log('Saved prompt: ' + promptId);
741 this.saveServiceSettings().then(() => this.render());
742 };
743
744 const mainPrompt = this.getPromptById('main');
745 const mainElementId = this.updateQuickEdit('main', mainPrompt);
746 document.getElementById(mainElementId).addEventListener('blur', handleQuickEditSave);
747
748 const nsfwPrompt = this.getPromptById('nsfw');
749 const nsfwElementId = this.updateQuickEdit('nsfw', nsfwPrompt);
750 document.getElementById(nsfwElementId).addEventListener('blur', handleQuickEditSave);
751
752 const jailbreakPrompt = this.getPromptById('jailbreak');
753 const jailbreakElementId = this.updateQuickEdit('jailbreak', jailbreakPrompt);
754 document.getElementById(jailbreakElementId).addEventListener('blur', handleQuickEditSave);
755 }
756
757 // Re-render when chat history changes.
758 eventSource.on(event_types.MESSAGE_DELETED, () => this.renderDebounced());
759 eventSource.on(event_types.MESSAGE_EDITED, () => this.renderDebounced());
760 eventSource.on(event_types.MESSAGE_RECEIVED, () => this.renderDebounced());
761
762 // Re-render when chatcompletion settings change
763 eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, () => this.renderDebounced());
764
765 eventSource.on(event_types.CHATCOMPLETION_MODEL_CHANGED, () => this.renderDebounced());
766
767 // Re-render when the character changes.
768 eventSource.on(event_types.CHAT_LOADED, (event) => {
769 this.handleCharacterSelected(event);
770 this.saveServiceSettings().then(() => this.renderDebounced());
771 });
772
773 // Re-render when the character gets edited.
774 eventSource.on(event_types.CHARACTER_EDITED, (event) => {
775 this.handleCharacterUpdated(event);
776 this.saveServiceSettings().then(() => this.renderDebounced());
777 });
778
779 // Re-render when the group changes.
780 eventSource.on('groupSelected', (event) => {
781 this.handleGroupSelected(event);
782 this.saveServiceSettings().then(() => this.renderDebounced());
783 });
784
785 // Sanitize settings after character has been deleted.
786 eventSource.on(event_types.CHARACTER_DELETED, (event) => {
787 this.handleCharacterDeleted(event);
788 this.saveServiceSettings().then(() => this.renderDebounced());
789 });
790
791 // Trigger re-render when token settings are changed
792 document.getElementById('openai_max_context').addEventListener('change', (event) => {
793 if (!(event.target instanceof HTMLInputElement)) return;
794 this.serviceSettings.openai_max_context = event.target.value;
795 if (this.activeCharacter) this.renderDebounced();
796 });
797
798 document.getElementById('openai_max_tokens').addEventListener('change', (event) => {
799 if (this.activeCharacter) this.renderDebounced();
800 });
801
802 // Prepare prompt edit form buttons
803 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_save').addEventListener('click', this.handleSavePrompt);
804 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset').addEventListener('click', this.handleResetPrompt);
805
806 const closeAndClearPopup = () => {
807 this.hidePopup();
808 this.clearEditForm();
809 this.clearInspectForm();
810 };
811
812 // Clear forms on closing the popup
813 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_close').addEventListener('click', closeAndClearPopup);
814 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_close_button').addEventListener('click', closeAndClearPopup);
815 closeAndClearPopup();
816
817 // Re-render prompt manager on openai preset change
818 eventSource.on(event_types.OAI_PRESET_CHANGED_AFTER, () => {
819 this.sanitizeServiceSettings();
820 const mainPrompt = this.getPromptById('main');
821 this.updateQuickEdit('main', mainPrompt);
822
823 const nsfwPrompt = this.getPromptById('nsfw');
824 this.updateQuickEdit('nsfw', nsfwPrompt);
825
826 const jailbreakPrompt = this.getPromptById('jailbreak');
827 this.updateQuickEdit('jailbreak', jailbreakPrompt);
828
829 this.hidePopup();
830 this.clearEditForm();
831 this.renderDebounced();
832 });
833
834 // Re-render prompt manager on world settings update
835 eventSource.on(event_types.WORLDINFO_SETTINGS_UPDATED, () => this.renderDebounced());
836
837 this.log('Initialized');
838 }
839
840 /**
841 * Get the scroll position of the prompt manager
842 * @returns {number} - Scroll position of the prompt manager
843 */
844 #getScrollPosition() {
845 return document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTop;
846 }
847
848 /**
849 * Set the scroll position of the prompt manager
850 * @param {number} scrollPosition - The scroll position to set
851 */
852 #setScrollPosition(scrollPosition) {
853 if (scrollPosition === undefined || scrollPosition === null) return;
854 document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTo(0, scrollPosition);
855 }
856
857 /**
858 * Main rendering function
859 *
860 * @param afterTryGenerate - Whether a dry run should be attempted before rendering
861 */
862 render(afterTryGenerate = true) {
863 if (main_api !== 'openai') return;
864
865 if ('character' === this.configuration.promptOrder.strategy && null === this.activeCharacter) return;
866 this.error = null;
867
868 waitUntilCondition(() => !is_send_press && !is_group_generating, 1024 * 1024, 100).then(async () => {
869 if (true === afterTryGenerate) {
870 // Executed during dry-run for determining context composition
871 this.profileStart('filling context');
872 this.tryGenerate().finally(async () => {
873 this.profileEnd('filling context');
874 this.profileStart('render');
875 const scrollPosition = this.#getScrollPosition();
876 await this.renderPromptManager();
877 await this.renderPromptManagerListItems();
878 this.makeDraggable();
879 this.#setScrollPosition(scrollPosition);
880 this.profileEnd('render');
881 });
882 } else {
883 // Executed during live communication
884 this.profileStart('render');
885 const scrollPosition = this.#getScrollPosition();
886 await this.renderPromptManager();
887 await this.renderPromptManagerListItems();
888 this.makeDraggable();
889 this.#setScrollPosition(scrollPosition);
890 this.profileEnd('render');
891 }
892 }).catch(() => {
893 console.log('Timeout while waiting for send press to be false');
894 });
895 }
896
897 /**
898 * Update a prompt with the values from the HTML form.
899 * @param {Partial<Prompt>} prompt - The prompt to be updated.
900 * @returns {void}
901 */
902 updatePromptWithPromptEditForm(prompt) {
903 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
904 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
905 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
906 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
907 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
908 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
909 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
910 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
911
912 prompt.name = nameField.value;
913 prompt.role = roleField.value;
914 prompt.content = promptField.value;
915 prompt.injection_position = Number(injectionPositionField.value);
916 prompt.injection_depth = Number(injectionDepthField.value);
917 prompt.injection_order = Number(injectionOrderField.value);
918 prompt.injection_trigger = Array.from(injectionTriggerField.selectedOptions).map(option => option.value);
919 prompt.forbid_overrides = forbidOverridesField.checked;
920 }
921
922 /**
923 * Find a prompt by its identifier and update it with the provided object.
924 * @param {string} identifier - The identifier of the prompt.
925 * @param {Prompt} updatePrompt - An object with properties to be updated in the prompt.
926 * @returns {void}
927 */
928 updatePromptByIdentifier(identifier, updatePrompt) {
929 let prompt = this.serviceSettings.prompts.find((item) => identifier === item.identifier);
930 if (prompt) prompt = Object.assign(prompt, updatePrompt);
931 }
932
933 /**
934 * Iterate over an array of prompts, find each one by its identifier, and update them with the provided data.
935 * @param {Prompt[]} prompts - An array of prompt updates.
936 * @returns {void}
937 */
938 updatePrompts(prompts) {
939 prompts.forEach((update) => {
940 let prompt = this.getPromptById(update.identifier);
941 if (prompt) Object.assign(prompt, update);
942 });
943 }
944
945 getTokenHandler() {
946 return this.tokenHandler;
947 }
948
949 isPromptDisabledForActiveCharacter(identifier) {
950 const promptOrderEntry = this.getPromptOrderEntry(this.activeCharacter, identifier);
951 if (promptOrderEntry) return !promptOrderEntry.enabled;
952 return false;
953 }
954
955 /**
956 * Add a prompt to the current character's prompt list.
957 * @param {Prompt} prompt - The prompt to be added.
958 * @param {object} character - The character whose prompt list will be updated.
959 * @returns {void}
960 */
961 appendPrompt(prompt, character) {
962 const promptOrder = this.getPromptOrderForCharacter(character);
963 const index = promptOrder.findIndex(entry => entry.identifier === prompt.identifier);
964
965 if (-1 === index) promptOrder.unshift({ identifier: prompt.identifier, enabled: false });
966 }
967
968 /**
969 * Remove a prompt from the current character's prompt list.
970 * @param {Prompt} prompt - The prompt to be removed.
971 * @param {object} character - The character whose prompt list will be updated.
972 * @returns {void}
973 */
974 // Remove a prompt from the current characters prompt list
975 detachPrompt(prompt, character) {
976 const promptOrder = this.getPromptOrderForCharacter(character);
977 const index = promptOrder.findIndex(entry => entry.identifier === prompt.identifier);
978 if (-1 === index) return;
979 promptOrder.splice(index, 1);
980 }
981
982 /**
983 * Create a new prompt and add it to the list of prompts.
984 * @param {Partial<Prompt>} prompt - The prompt to be added.
985 * @param {string} identifier - The identifier for the new prompt.
986 * @returns {void}
987 */
988 addPrompt(prompt, identifier) {
989 if (typeof prompt !== 'object' || prompt === null) throw new Error('Object is not a prompt');
990
991 const newPrompt = {
992 identifier: identifier,
993 system_prompt: false,
994 enabled: false,
995 marker: false,
996 ...prompt,
997 };
998
999 this.serviceSettings.prompts.push(newPrompt);
1000 }
1001
1002 /**
1003 * Sanitize the service settings, ensuring each prompt has a unique identifier.
1004 * @returns {void}
1005 */
1006 sanitizeServiceSettings() {
1007 this.serviceSettings.prompts = this.serviceSettings.prompts ?? [];
1008 this.serviceSettings.prompt_order = this.serviceSettings.prompt_order ?? [];
1009
1010 if ('global' === this.configuration.promptOrder.strategy) {
1011 const dummyCharacter = { id: this.configuration.promptOrder.dummyId };
1012 const promptOrder = this.getPromptOrderForCharacter(dummyCharacter);
1013
1014 if (0 === promptOrder.length) this.addPromptOrderForCharacter(dummyCharacter, promptManagerDefaultPromptOrder);
1015 }
1016
1017 // Check whether the referenced prompts are present.
1018 this.serviceSettings.prompts.length === 0
1019 ? this.setPrompts(chatCompletionDefaultPrompts.prompts)
1020 : this.checkForMissingPrompts(this.serviceSettings.prompts);
1021
1022 // Add identifiers if there are none assigned to a prompt
1023 this.serviceSettings.prompts.forEach(prompt => prompt && (prompt.identifier = prompt.identifier ?? this.getUuidv4()));
1024
1025 if (this.activeCharacter) {
1026 const promptReferences = this.getPromptOrderForCharacter(this.activeCharacter);
1027 for (let i = promptReferences.length - 1; i >= 0; i--) {
1028 const reference = promptReferences[i];
1029 if (reference && -1 === this.serviceSettings.prompts.findIndex(prompt => prompt.identifier === reference.identifier)) {
1030 promptReferences.splice(i, 1);
1031 this.log('Removed unused reference: ' + reference.identifier);
1032 }
1033 }
1034 }
1035 }
1036
1037 /**
1038 * Checks whether entries of a characters prompt order are orphaned
1039 * and if all mandatory system prompts for a character are present.
1040 *
1041 * @param prompts
1042 */
1043 checkForMissingPrompts(prompts) {
1044 const defaultPromptIdentifiers = chatCompletionDefaultPrompts.prompts.reduce((list, prompt) => { list.push(prompt.identifier); return list; }, []);
1045
1046 const missingIdentifiers = defaultPromptIdentifiers.filter(identifier =>
1047 !prompts.some(prompt => prompt.identifier === identifier),
1048 );
1049
1050 missingIdentifiers.forEach(identifier => {
1051 const defaultPrompt = chatCompletionDefaultPrompts.prompts.find(prompt => prompt?.identifier === identifier);
1052 if (defaultPrompt) {
1053 prompts.push(defaultPrompt);
1054 this.log(`Missing system prompt: ${defaultPrompt.identifier}. Added default.`);
1055 }
1056 });
1057 }
1058
1059 /**
1060 * Check whether a prompt can be inspected.
1061 * @param {Prompt} prompt - The prompt to check.
1062 * @returns {boolean} True if the prompt is a marker, false otherwise.
1063 */
1064 isPromptInspectionAllowed(prompt) {
1065 return true;
1066 }
1067
1068 /**
1069 * Check whether a prompt can be deleted. System prompts cannot be deleted.
1070 * @param {Prompt} prompt - The prompt to check.
1071 * @returns {boolean} True if the prompt can be deleted, false otherwise.
1072 */
1073 isPromptDeletionAllowed(prompt) {
1074 return false === prompt.system_prompt;
1075 }
1076
1077 /**
1078 * Check whether a prompt can be edited.
1079 * @param {Prompt} prompt - The prompt to check.
1080 * @returns {boolean} True if the prompt can be edited, false otherwise.
1081 */
1082 isPromptEditAllowed(prompt) {
1083 const forceEditPrompts = [
1084 'charDescription',
1085 'charPersonality',
1086 'scenario',
1087 'personaDescription',
1088 'worldInfoBefore',
1089 'worldInfoAfter',
1090 ];
1091 return forceEditPrompts.includes(prompt.identifier) || !prompt.marker;
1092 }
1093
1094 /**
1095 * Check whether a prompt can be toggled on or off.
1096 * @param {Prompt} prompt - The prompt to check.
1097 * @returns {boolean} True if the prompt can be deleted, false otherwise.
1098 */
1099 isPromptToggleAllowed(prompt) {
1100 const forceTogglePrompts = [
1101 'charDescription',
1102 'charPersonality',
1103 'scenario',
1104 'personaDescription',
1105 'worldInfoBefore',
1106 'worldInfoAfter',
1107 'main',
1108 'chatHistory',
1109 'dialogueExamples',
1110 ];
1111 return prompt.marker && !forceTogglePrompts.includes(prompt.identifier) ? false : !this.configuration.toggleDisabled.includes(prompt.identifier);
1112 }
1113
1114 /**
1115 * Handle the deletion of a character by removing their prompt list and nullifying the active character if it was the one deleted.
1116 * @param {object} event - The event object containing the character's ID.
1117 * @returns void
1118 */
1119 handleCharacterDeleted(event) {
1120 if ('global' === this.configuration.promptOrder.strategy) return;
1121 this.removePromptOrderForCharacter(this.activeCharacter);
1122 if (this.activeCharacter.id === event.detail.id) this.activeCharacter = null;
1123 }
1124
1125 /**
1126 * Handle the selection of a character by setting them as the active character and setting up their prompt list if necessary.
1127 * @param {object} event - The event object containing the character's ID and character data.
1128 * @returns {void}
1129 */
1130 handleCharacterSelected(event) {
1131 if ('global' === this.configuration.promptOrder.strategy) {
1132 this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
1133 } else if ('character' === this.configuration.promptOrder.strategy) {
1134 console.log('FOO');
1135 this.activeCharacter = { id: event.detail.id, ...event.detail.character };
1136 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
1137
1138 // ToDo: These should be passed as parameter or attached to the manager as a set of default options.
1139 // Set default prompts and order for character.
1140 if (0 === promptOrder.length) this.addPromptOrderForCharacter(this.activeCharacter, promptManagerDefaultPromptOrder);
1141 } else {
1142 throw new Error('Unsupported prompt order mode.');
1143 }
1144 }
1145
1146 /**
1147 * Set the most recently selected character
1148 *
1149 * @param event
1150 */
1151 handleCharacterUpdated(event) {
1152 if ('global' === this.configuration.promptOrder.strategy) {
1153 this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
1154 } else if ('character' === this.configuration.promptOrder.strategy) {
1155 this.activeCharacter = { id: event.detail.id, ...event.detail.character };
1156 } else {
1157 throw new Error('Prompt order strategy not supported.');
1158 }
1159 }
1160
1161 /**
1162 * Set the most recently selected character group
1163 *
1164 * @param event
1165 */
1166 handleGroupSelected(event) {
1167 if ('global' === this.configuration.promptOrder.strategy) {
1168 this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
1169 } else if ('character' === this.configuration.promptOrder.strategy) {
1170 const characterDummy = { id: event.detail.id, group: event.detail.group };
1171 this.activeCharacter = characterDummy;
1172 const promptOrder = this.getPromptOrderForCharacter(characterDummy);
1173
1174 if (0 === promptOrder.length) this.addPromptOrderForCharacter(characterDummy, promptManagerDefaultPromptOrder);
1175 } else {
1176 throw new Error('Prompt order strategy not supported.');
1177 }
1178 }
1179
1180 /**
1181 * Get a list of group characters, regardless of whether they are active or not.
1182 *
1183 * @returns {string[]}
1184 */
1185 getActiveGroupCharacters() {
1186 // ToDo: Ideally, this should return the actual characters.
1187 return (this.activeCharacter?.group?.members || []).map(member => member && member.substring(0, member.lastIndexOf('.')));
1188 }
1189
1190 /**
1191 * Get the prompts for a specific character. Can be filtered to only include enabled prompts.
1192 * @returns {Prompt[]} The prompts for the character.
1193 * @param character
1194 * @param onlyEnabled
1195 */
1196 getPromptsForCharacter(character, onlyEnabled = false) {
1197 return this.getPromptOrderForCharacter(character)
1198 .map(item => true === onlyEnabled ? (true === item.enabled ? this.getPromptById(item.identifier) : null) : this.getPromptById(item.identifier))
1199 .filter(prompt => null !== prompt);
1200 }
1201
1202 /**
1203 * Get the order of prompts for a specific character. If no character is specified or the character doesn't have a prompt list, an empty array is returned.
1204 * @param {object|null} character - The character to get the prompt list for.
1205 * @returns {Partial<Prompt>[]} The prompt list for the character, or an empty array.
1206 */
1207 getPromptOrderForCharacter(character) {
1208 return !character ? [] : (this.serviceSettings.prompt_order.find(list => String(list.character_id) === String(character.id))?.order ?? []);
1209 }
1210
1211 /**
1212 * Set the prompts for the manager.
1213 * @param {Partial<Prompt>[]} prompts - The prompts to be set.
1214 * @returns {void}
1215 */
1216 setPrompts(prompts) {
1217 this.serviceSettings.prompts = prompts;
1218 }
1219
1220 /**
1221 * Remove the prompt list for a specific character.
1222 * @param {object} character - The character whose prompt list will be removed.
1223 * @returns {void}
1224 */
1225 removePromptOrderForCharacter(character) {
1226 const index = this.serviceSettings.prompt_order.findIndex(list => String(list.character_id) === String(character.id));
1227 if (-1 !== index) this.serviceSettings.prompt_order.splice(index, 1);
1228 }
1229
1230 /**
1231 * Adds a new prompt list for a specific character.
1232 * @param {Object} character - Object with at least an `id` property
1233 * @param {Array<Object>} promptOrder - Array of prompt objects
1234 */
1235 addPromptOrderForCharacter(character, promptOrder) {
1236 this.serviceSettings.prompt_order.push({
1237 character_id: character.id,
1238 order: JSON.parse(JSON.stringify(promptOrder)),
1239 });
1240 }
1241
1242 /**
1243 * Searches for a prompt list entry for a given character and identifier.
1244 * @param {Object} character - Character object
1245 * @param {string} identifier - Identifier of the prompt list entry
1246 * @returns {Object|null} The prompt list entry object, or null if not found
1247 */
1248 getPromptOrderEntry(character, identifier) {
1249 return this.getPromptOrderForCharacter(character).find(entry => entry.identifier === identifier) ?? null;
1250 }
1251
1252 /**
1253 * Finds and returns a prompt by its identifier.
1254 * @param {string} identifier - Identifier of the prompt
1255 * @returns {Prompt|null} The prompt object, or null if not found
1256 */
1257 getPromptById(identifier) {
1258 return this.serviceSettings.prompts.find(item => item && item.identifier === identifier) ?? null;
1259 }
1260
1261 /**
1262 * Finds and returns the index of a prompt by its identifier.
1263 * @param {string} identifier - Identifier of the prompt
1264 * @returns {number|null} Index of the prompt, or null if not found
1265 */
1266 getPromptIndexById(identifier) {
1267 return this.serviceSettings.prompts.findIndex(item => item.identifier === identifier) ?? null;
1268 }
1269
1270 /**
1271 * Enriches a generic object, creating a new prompt object in the process
1272 *
1273 * @param {Partial<Prompt>} prompt - Prompt object
1274 * @param original
1275 * @returns {Prompt} An object with "role" and "content" properties
1276 */
1277 preparePrompt(prompt, original = null) {
1278 const groupMembers = this.getActiveGroupCharacters();
1279 const preparedPrompt = new Prompt(prompt);
1280
1281 if (typeof original === 'string') {
1282 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', { original, groupOverride: groupMembers.join(', ') });
1283 else preparedPrompt.content = substituteParams(prompt.content, { original });
1284 } else {
1285 if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', { groupOverride: groupMembers.join(', ') });
1286 else preparedPrompt.content = substituteParams(prompt.content);
1287 }
1288
1289 return preparedPrompt;
1290 }
1291
1292 /**
1293 * Factory function for creating a QuickEdit object associated with a prompt element.
1294 *
1295 * The QuickEdit object provides methods to synchronize an input element's value with a prompt's content
1296 * and handle input events to update the prompt content.
1297 *
1298 */
1299 createQuickEdit(identifier, title) {
1300 const prompt = this.getPromptById(identifier);
1301 const textareaIdentifier = `${identifier}_prompt_quick_edit_textarea`;
1302 const html = `<div class="range-block m-t-1">
1303 <div class="justifyLeft">${title}</div>
1304 <div class="wide100p">
1305 <textarea id="${textareaIdentifier}" class="text_pole textarea_compact" rows="6" placeholder="">${prompt.content}</textarea>
1306 </div>
1307 </div>`;
1308
1309 const quickEditContainer = document.getElementById('quick-edit-container');
1310 quickEditContainer.insertAdjacentHTML('afterbegin', html);
1311
1312 const debouncedSaveServiceSettings = debouncePromise(() => this.saveServiceSettings(), 300);
1313
1314 const textarea = /** @type {HTMLTextAreaElement} */(document.getElementById(textareaIdentifier));
1315 textarea.addEventListener('blur', () => {
1316 prompt.content = textarea.value;
1317 this.updatePromptByIdentifier(identifier, prompt);
1318 debouncedSaveServiceSettings().then(() => this.render());
1319 });
1320 }
1321
1322 /**
1323 * Updates the quick edit textarea for a specific prompt.
1324 * @param {string} identifier - The identifier of the prompt.
1325 * @param {Prompt} prompt - The updated prompt object.
1326 * @returns {string} The ID of the updated textarea element.
1327 */
1328 updateQuickEdit(identifier, prompt) {
1329 const elementId = `${identifier}_prompt_quick_edit_textarea`;
1330 const textarea = /** @type {HTMLTextAreaElement} */(document.getElementById(elementId));
1331 textarea.value = prompt.content;
1332
1333 return elementId;
1334 }
1335
1336 /**
1337 * Checks if a given name is accepted by OpenAi API
1338 * @link https://platform.openai.com/docs/api-reference/chat/create
1339 *
1340 * @param name
1341 * @returns {boolean}
1342 */
1343 isValidName(name) {
1344 const regex = /^[a-zA-Z0-9_]{1,64}$/;
1345
1346 return regex.test(name);
1347 }
1348
1349 sanitizeName(name) {
1350 return name.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 64);
1351 }
1352
1353 /**
1354 * Loads a given prompt into the edit form fields.
1355 * @param {Partial<Prompt>} prompt - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
1356 */
1357 loadPromptIntoEditForm(prompt) {
1358 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
1359 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
1360 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
1361 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
1362 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
1363 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
1364 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
1365 const injectionDepthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block'));
1366 const injectionOrderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_block'));
1367 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
1368 const forbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block'));
1369 const entrySourceBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block'));
1370 const entrySource = /** @type {HTMLSpanElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source'));
1371 const isPulledPrompt = Object.keys(this.promptSources).includes(prompt.identifier);
1372
1373 nameField.value = prompt.name ?? '';
1374 roleField.value = prompt.role || 'system';
1375 promptField.value = prompt.content ?? '';
1376 promptField.disabled = prompt.marker ?? false;
1377 injectionPositionField.value = (prompt.injection_position ?? INJECTION_POSITION.RELATIVE).toString();
1378 injectionDepthField.value = (prompt.injection_depth ?? DEFAULT_DEPTH).toString();
1379 injectionOrderField.value = (prompt.injection_order ?? DEFAULT_ORDER).toString();
1380 Array.from(injectionTriggerField.options).forEach(option => {
1381 option.selected = Array.isArray(prompt.injection_trigger) && prompt.injection_trigger.includes(option.value);
1382 });
1383 injectionTriggerField.dispatchEvent(new Event('change', { bubbles: true }));
1384 injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
1385 injectionOrderBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
1386 injectionPositionField.removeAttribute('disabled');
1387 forbidOverridesField.checked = prompt.forbid_overrides ?? false;
1388 forbidOverridesBlock.style.visibility = this.overridablePrompts.includes(prompt.identifier) ? 'visible' : 'hidden';
1389 entrySourceBlock.style.display = isPulledPrompt ? '' : 'none';
1390
1391 if (isPulledPrompt) {
1392 const sourceName = this.promptSources[prompt.identifier];
1393 entrySource.textContent = sourceName;
1394 }
1395
1396 const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset');
1397 if (true === prompt.system_prompt) {
1398 resetPromptButton.style.display = 'block';
1399 resetPromptButton.dataset.pmPrompt = prompt.identifier;
1400 } else {
1401 resetPromptButton.style.display = 'none';
1402 }
1403
1404 injectionPositionField.removeEventListener('change', (e) => this.handleInjectionPositionChange(e));
1405 injectionPositionField.addEventListener('change', (e) => this.handleInjectionPositionChange(e));
1406
1407 const savePromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_save');
1408 savePromptButton.dataset.pmPrompt = prompt.identifier;
1409 }
1410
1411 handleInjectionPositionChange(event) {
1412 const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
1413 const injectionOrderBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_order_block');
1414 const injectionPosition = Number(event.target.value);
1415 if (injectionPosition === INJECTION_POSITION.ABSOLUTE) {
1416 injectionDepthBlock.style.visibility = 'visible';
1417 injectionOrderBlock.style.visibility = 'visible';
1418 } else {
1419 injectionDepthBlock.style.visibility = 'hidden';
1420 injectionOrderBlock.style.visibility = 'hidden';
1421 }
1422 }
1423
1424 /**
1425 * Loads a given prompt into the inspect form
1426 * @param {MessageCollection} messages - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
1427 */
1428 loadMessagesIntoInspectForm(messages) {
1429 if (!messages) return;
1430
1431 const createInlineDrawer = (message) => {
1432 const truncatedTitle = message.content.length > 32 ? message.content.slice(0, 32) + '...' : message.content;
1433 const title = message.identifier || truncatedTitle;
1434 const role = message.role;
1435 const content = message.content || 'No Content';
1436 const tokens = message.getTokens();
1437
1438 let drawerHTML = `
1439 <div class="inline-drawer ${this.configuration.prefix}prompt_manager_prompt">
1440 <div class="inline-drawer-toggle inline-drawer-header">
1441 <span>Name: ${escapeHtml(title)}, Role: ${role}, Tokens: ${tokens}</span>
1442 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
1443 </div>
1444 <div class="inline-drawer-content" style="white-space: pre-wrap;">${escapeHtml(content)}</div>
1445 </div>
1446 `;
1447
1448 let template = document.createElement('template');
1449 template.innerHTML = drawerHTML.trim();
1450 return template.content.firstChild;
1451 };
1452
1453 const messageList = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_inspect_list');
1454
1455 const messagesCollection = messages instanceof Message ? [messages] : messages.getCollection();
1456
1457 if (0 === messagesCollection.length) messageList.innerHTML = '<span>This marker does not contain any prompts.</span>';
1458
1459 messagesCollection.forEach(message => {
1460 messageList.append(createInlineDrawer(message));
1461 });
1462 }
1463
1464 /**
1465 * Clears all input fields in the edit form.
1466 */
1467 clearEditForm() {
1468 const editArea = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_edit');
1469 editArea.style.display = 'none';
1470
1471 const nameField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name'));
1472 const roleField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role'));
1473 const promptField = /** @type {HTMLTextAreaElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt'));
1474 const injectionPositionField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position'));
1475 const injectionDepthField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth'));
1476 const injectionDepthBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block'));
1477 const injectionOrderBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_order_block'));
1478 const injectionOrderField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_order'));
1479 const injectionTriggerField = /** @type {HTMLSelectElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_trigger'));
1480 const forbidOverridesField = /** @type {HTMLInputElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides'));
1481 const forbidOverridesBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block'));
1482 const entrySourceBlock = /** @type {HTMLDivElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source_block'));
1483 const entrySource = /** @type {HTMLSpanElement} */(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_source'));
1484
1485 nameField.value = '';
1486 roleField.selectedIndex = 0;
1487 promptField.value = '';
1488 promptField.disabled = false;
1489 injectionPositionField.selectedIndex = 0;
1490 injectionPositionField.removeAttribute('disabled');
1491 injectionDepthField.value = DEFAULT_DEPTH.toString();
1492 injectionOrderField.value = DEFAULT_ORDER.toString();
1493 injectionTriggerField.value = '';
1494 injectionDepthBlock.style.visibility = 'unset';
1495 injectionOrderBlock.style.visibility = 'unset';
1496 forbidOverridesBlock.style.visibility = 'unset';
1497 forbidOverridesField.checked = false;
1498 entrySourceBlock.style.display = 'none';
1499 entrySource.textContent = '';
1500
1501 roleField.disabled = false;
1502 }
1503
1504 clearInspectForm() {
1505 const inspectArea = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_inspect');
1506 inspectArea.style.display = 'none';
1507 const messageList = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_inspect_list');
1508 messageList.innerHTML = '';
1509 }
1510
1511 /**
1512 * Returns a full list of prompts whose content markers have been substituted.
1513 * @param {string} generationType - The type of generation, e.g., 'continue' or 'quiet'.
1514 * @returns {PromptCollection} A PromptCollection object
1515 */
1516 getPromptCollection(generationType) {
1517 generationType = String(generationType || 'normal').toLowerCase().trim();
1518 const promptCollection = new PromptCollection();
1519 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
1520
1521 promptOrder.forEach(entry => {
1522 const prompt = this.getPromptById(entry.identifier);
1523 const allowedTrigger = entry.enabled && this.shouldTrigger(prompt, generationType);
1524
1525 if (!prompt) {
1526 return;
1527 }
1528
1529 if (allowedTrigger) {
1530 promptCollection.add(this.preparePrompt(prompt));
1531 } else if (entry.identifier === 'main') {
1532 // Some extensions require main prompt to be present for relative inserts.
1533 // So we make a GMO-free vegan replacement.
1534 const replacementPrompt = structuredClone(prompt);
1535 replacementPrompt.content = '';
1536 promptCollection.add(this.preparePrompt(replacementPrompt));
1537 }
1538 });
1539
1540 return promptCollection;
1541 }
1542
1543 /**
1544 * Checks if a prompt should be triggered based on its injection triggers.
1545 * @param {Prompt} prompt - The prompt to check.
1546 * @param {string} generationType - The type of generation to check against.
1547 * @returns {boolean} True if the prompt should be triggered, false otherwise.
1548 */
1549 shouldTrigger(prompt, generationType) {
1550 if (!Array.isArray(prompt?.injection_trigger)) return true;
1551 if (!prompt.injection_trigger.length) return true;
1552 return prompt.injection_trigger.includes(generationType);
1553 }
1554
1555 /**
1556 * Setter for messages property
1557 *
1558 * @param {import('./openai.js').MessageCollection} messages
1559 */
1560 setMessages(messages) {
1561 this.messages = messages;
1562 }
1563
1564 /**
1565 * Set and process a finished chat completion object
1566 *
1567 * @param {import('./openai.js').ChatCompletion} chatCompletion
1568 */
1569 setChatCompletion(chatCompletion) {
1570 const messages = chatCompletion.getMessages();
1571
1572 this.setMessages(messages);
1573 this.populateTokenCounts(messages);
1574 this.overriddenPrompts = chatCompletion.getOverriddenPrompts();
1575 }
1576
1577 /**
1578 * Populates the token handler
1579 *
1580 * @param {import('./openai.js').MessageCollection} messages
1581 */
1582 populateTokenCounts(messages) {
1583 this.tokenHandler.resetCounts();
1584 const counts = this.tokenHandler.getCounts();
1585 messages.getCollection().forEach(message => {
1586 counts[message.identifier] = message.getTokens();
1587 });
1588
1589 this.tokenUsage = this.tokenHandler.getTotal();
1590
1591 this.log('Updated token usage with ' + this.tokenUsage);
1592 }
1593
1594 /**
1595 * Empties, then re-assembles the container containing the prompt list.
1596 */
1597 async renderPromptManager() {
1598 let selectedPromptIndex = 0;
1599 const existingAppendSelect = document.getElementById(`${this.configuration.prefix}prompt_manager_footer_append_prompt`);
1600 if (existingAppendSelect instanceof HTMLSelectElement) {
1601 selectedPromptIndex = existingAppendSelect.selectedIndex;
1602 }
1603 const promptManagerDiv = this.containerElement;
1604 promptManagerDiv.innerHTML = '';
1605
1606 const errorDiv = this.error ? `
1607 <div class="${this.configuration.prefix}prompt_manager_error">
1608 <span class="fa-solid tooltip fa-triangle-exclamation text_danger"></span> ${DOMPurify.sanitize(this.error)}
1609 </div>
1610 ` : '';
1611
1612 const totalActiveTokens = this.tokenUsage;
1613
1614 const headerHtml = await renderTemplateAsync('promptManagerHeader', { error: this.error, errorDiv, prefix: this.configuration.prefix, totalActiveTokens });
1615 promptManagerDiv.insertAdjacentHTML('beforeend', headerHtml);
1616
1617 this.listElement = promptManagerDiv.querySelector(`#${this.configuration.prefix}prompt_manager_list`);
1618
1619 if (null !== this.activeCharacter) {
1620 const prompts = [...this.serviceSettings.prompts]
1621 .filter(prompt => prompt && !prompt?.system_prompt)
1622 .sort((promptA, promptB) => promptA.name.localeCompare(promptB.name));
1623 const promptsHtml = prompts.reduce((acc, prompt) => acc + `<option value="${prompt.identifier}">${escapeHtml(prompt.name)}</option>`, '');
1624
1625 if (selectedPromptIndex > 0) {
1626 selectedPromptIndex = Math.min(selectedPromptIndex, prompts.length - 1);
1627 }
1628
1629 if (selectedPromptIndex === -1 && prompts.length) {
1630 selectedPromptIndex = 0;
1631 }
1632
1633 const rangeBlockDiv = promptManagerDiv.querySelector('.range-block');
1634 const headerDiv = promptManagerDiv.querySelector('.completion_prompt_manager_header');
1635 const footerHtml = await renderTemplateAsync('promptManagerFooter', { promptsHtml, prefix: this.configuration.prefix });
1636 headerDiv.insertAdjacentHTML('afterend', footerHtml);
1637 rangeBlockDiv.querySelector('#prompt-manager-reset-character').addEventListener('click', this.handleCharacterReset);
1638
1639 const footerDiv = rangeBlockDiv.querySelector(`.${this.configuration.prefix}prompt_manager_footer`);
1640 footerDiv.querySelector('.menu_button:nth-child(2)').addEventListener('click', this.handleAppendPrompt);
1641 footerDiv.querySelector('.caution').addEventListener('click', this.handleDeletePrompt);
1642 footerDiv.querySelector('.menu_button:last-child').addEventListener('click', this.handleNewPrompt);
1643 footerDiv.querySelector('select').selectedIndex = selectedPromptIndex;
1644
1645 // Add prompt export dialogue and options
1646 footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport);
1647 footerDiv.querySelector('#prompt-manager-export').addEventListener('click', this.handleFullExport);
1648 }
1649 }
1650
1651 /**
1652 * Empties, then re-assembles the prompt list
1653 */
1654 async renderPromptManagerListItems() {
1655 if (!this.serviceSettings.prompts) return;
1656
1657 const promptManagerList = this.listElement;
1658 promptManagerList.innerHTML = '';
1659
1660 const { prefix } = this.configuration;
1661
1662 let listItemHtml = await renderTemplateAsync('promptManagerListHeader', { prefix });
1663
1664 this.getPromptsForCharacter(this.activeCharacter).forEach(prompt => {
1665 if (!prompt) return;
1666
1667 const listEntry = this.getPromptOrderEntry(this.activeCharacter, prompt.identifier);
1668 const enabledClass = listEntry.enabled ? '' : `${prefix}prompt_manager_prompt_disabled`;
1669 const draggableClass = `${prefix}prompt_manager_prompt_draggable`;
1670 const markerClass = prompt.marker ? `${prefix}prompt_manager_marker` : '';
1671 const tokens = this.tokenHandler?.getCounts()[prompt.identifier] ?? 0;
1672
1673 // Warn the user if the chat history goes below certain token thresholds.
1674 let warningClass = '';
1675 let warningTitle = '';
1676
1677 const tokenBudget = this.serviceSettings.openai_max_context - this.serviceSettings.openai_max_tokens;
1678 if (this.tokenUsage > tokenBudget * 0.8 &&
1679 'chatHistory' === prompt.identifier) {
1680 const warningThreshold = this.configuration.warningTokenThreshold;
1681 const dangerThreshold = this.configuration.dangerTokenThreshold;
1682
1683 if (tokens <= dangerThreshold) {
1684 warningClass = 'fa-solid tooltip fa-triangle-exclamation text_danger';
1685 warningTitle = 'Very little of your chat history is being sent, consider deactivating some other prompts.';
1686 } else if (tokens <= warningThreshold) {
1687 warningClass = 'fa-solid tooltip fa-triangle-exclamation text_warning';
1688 warningTitle = 'Only a few messages worth chat history are being sent.';
1689 }
1690 }
1691
1692 const calculatedTokens = tokens ? tokens : '-';
1693
1694 let detachSpanHtml = '';
1695 if (this.isPromptDeletionAllowed(prompt)) {
1696 detachSpanHtml = `
1697 <span title="Remove" class="prompt-manager-detach-action caution fa-solid fa-chain-broken fa-xs"></span>
1698 `;
1699 } else {
1700 detachSpanHtml = '<span class="fa-solid"></span>';
1701 }
1702
1703 let editSpanHtml = '';
1704 if (this.isPromptEditAllowed(prompt)) {
1705 editSpanHtml = `
1706 <span title="edit" class="prompt-manager-edit-action fa-solid fa-pencil fa-xs"></span>
1707 `;
1708 } else {
1709 editSpanHtml = '<span class="fa-solid"></span>';
1710 }
1711
1712 let toggleSpanHtml = '';
1713 if (this.isPromptToggleAllowed(prompt)) {
1714 toggleSpanHtml = `
1715 <span class="prompt-manager-toggle-action ${listEntry.enabled ? 'fa-solid fa-toggle-on' : 'fa-solid fa-toggle-off'}"></span>
1716 `;
1717 } else {
1718 toggleSpanHtml = '<span class="fa-solid"></span>';
1719 }
1720
1721 const encodedName = escapeHtml(prompt.name);
1722 const isMarkerPrompt = prompt.marker && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
1723 const isSystemPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE && !prompt.forbid_overrides;
1724 const isImportantPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE && prompt.forbid_overrides;
1725 const isUserPrompt = !prompt.marker && !prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
1726 const isInjectionPrompt = prompt.injection_position === INJECTION_POSITION.ABSOLUTE;
1727 const isOverriddenPrompt = Array.isArray(this.overriddenPrompts) && this.overriddenPrompts.includes(prompt.identifier);
1728 const importantClass = isImportantPrompt ? `${prefix}prompt_manager_important` : '';
1729 const iconLookup = prompt.role === 'system' && (prompt.marker || prompt.system_prompt) ? '' : prompt.role;
1730
1731 //add role icons to the right of prompt name
1732 const promptRoles = {
1733 assistant: { roleIcon: 'fa-robot', roleTitle: 'Prompt will be sent as Assistant' },
1734 user: { roleIcon: 'fa-user', roleTitle: 'Prompt will be sent as User' },
1735 };
1736 const roleIcon = promptRoles[iconLookup]?.roleIcon || '';
1737 const roleTitle = promptRoles[iconLookup]?.roleTitle || '';
1738
1739 listItemHtml += `
1740 <li class="${prefix}prompt_manager_prompt ${draggableClass} ${enabledClass} ${markerClass} ${importantClass}" data-pm-identifier="${escapeHtml(prompt.identifier)}">
1741 <span class="drag-handle">☰</span>
1742 <span class="${prefix}prompt_manager_prompt_name" data-pm-name="${encodedName}">
1743 ${isMarkerPrompt ? '<span class="fa-fw fa-solid fa-thumb-tack" title="Marker"></span>' : ''}
1744 ${isSystemPrompt ? '<span class="fa-fw fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''}
1745 ${isImportantPrompt ? '<span class="fa-fw fa-solid fa-star" title="Important Prompt"></span>' : ''}
1746 ${isUserPrompt ? '<span class="fa-fw fa-solid fa-asterisk" title="Preset Prompt"></span>' : ''}
1747 ${isInjectionPrompt ? '<span class="fa-fw fa-solid fa-syringe" title="In-Chat Injection"></span>' : ''}
1748 ${this.isPromptInspectionAllowed(prompt) ? `<a title="${encodedName}" class="prompt-manager-inspect-action">${encodedName}</a>` : `<span title="${encodedName}">${encodedName}</span>`}
1749 ${roleIcon ? `<span data-role="${escapeHtml(prompt.role)}" class="fa-xs fa-solid ${roleIcon}" title="${roleTitle}"></span>` : ''}
1750 ${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${escapeHtml(prompt.injection_depth.toString())}</small>` : ''}
1751 ${isOverriddenPrompt ? '<small class="fa-solid fa-address-card prompt-manager-overridden" title="Pulled from a character card"></small>' : ''}
1752 </span>
1753 <span>
1754 <span class="prompt_manager_prompt_controls">
1755 ${detachSpanHtml}
1756 ${editSpanHtml}
1757 ${toggleSpanHtml}
1758 </span>
1759 </span>
1760
1761 <span class="prompt_manager_prompt_tokens" data-pm-tokens="${calculatedTokens}"><span class="${warningClass}" title="${warningTitle}"> </span>${calculatedTokens}</span>
1762 </li>
1763 `;
1764 });
1765
1766 promptManagerList.insertAdjacentHTML('beforeend', listItemHtml);
1767
1768 // Now that the new elements are in the DOM, you can add the event listeners.
1769 Array.from(promptManagerList.getElementsByClassName('prompt-manager-detach-action')).forEach(el => {
1770 el.addEventListener('click', this.handleDetach);
1771 });
1772
1773 Array.from(promptManagerList.getElementsByClassName('prompt-manager-inspect-action')).forEach(el => {
1774 el.addEventListener('click', this.handleInspect);
1775 });
1776
1777 Array.from(promptManagerList.getElementsByClassName('prompt-manager-edit-action')).forEach(el => {
1778 el.addEventListener('click', this.handleEdit);
1779 });
1780
1781 Array.from(promptManagerList.querySelectorAll('.prompt-manager-toggle-action')).forEach(el => {
1782 el.addEventListener('click', this.handleToggle);
1783 });
1784 }
1785
1786 /**
1787 * Writes the passed data to a json file
1788 *
1789 * @param data
1790 * @param type
1791 * @param name
1792 */
1793 export(data, type, name = 'export') {
1794 const promptExport = {
1795 version: this.configuration.version,
1796 type: type,
1797 data: data,
1798 };
1799
1800 const serializedObject = JSON.stringify(promptExport, null, 4);
1801 const blob = new Blob([serializedObject], { type: 'application/json' });
1802 const url = URL.createObjectURL(blob);
1803 const downloadLink = document.createElement('a');
1804 downloadLink.href = url;
1805
1806 const dateString = this.getFormattedDate();
1807 downloadLink.download = `${name}-${dateString}.json`;
1808
1809 downloadLink.click();
1810
1811 URL.revokeObjectURL(url);
1812 }
1813
1814 /**
1815 * Imports a json file with prompts and an optional prompt list for the active character
1816 *
1817 * @param importData
1818 */
1819 import(importData) {
1820 const mergeKeepNewer = (prompts, newPrompts) => {
1821 let merged = [...prompts, ...newPrompts];
1822
1823 let map = new Map();
1824 for (let obj of merged) {
1825 map.set(obj.identifier, obj);
1826 }
1827
1828 merged = Array.from(map.values());
1829
1830 return merged;
1831 };
1832
1833 const controlObj = {
1834 version: 1,
1835 type: '',
1836 data: {
1837 prompts: [],
1838 prompt_order: null,
1839 },
1840 };
1841
1842 if (false === this.validateObject(controlObj, importData)) {
1843 toastr.warning(t`Could not import prompts. Export failed validation.`);
1844 return;
1845 }
1846
1847 const prompts = mergeKeepNewer(this.serviceSettings.prompts, importData.data.prompts);
1848
1849 this.setPrompts(prompts);
1850 this.log('Prompt import succeeded');
1851
1852 if ('global' === this.configuration.promptOrder.strategy) {
1853 const promptOrder = this.getPromptOrderForCharacter({ id: this.configuration.promptOrder.dummyId });
1854 Object.assign(promptOrder, importData.data.prompt_order);
1855 this.log('Prompt order import succeeded');
1856 } else if ('character' === this.configuration.promptOrder.strategy) {
1857 if ('character' === importData.type) {
1858 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
1859 Object.assign(promptOrder, importData.data.prompt_order);
1860 this.log(`Prompt order import for character ${this.activeCharacter.name} succeeded`);
1861 }
1862 } else {
1863 throw new Error('Prompt order strategy not supported.');
1864 }
1865
1866 toastr.success(t`Prompt import complete.`);
1867 this.saveServiceSettings().then(() => this.render());
1868 }
1869
1870 /**
1871 * Helper function to check whether the structure of object matches controlObj
1872 *
1873 * @param controlObj
1874 * @param object
1875 * @returns {boolean}
1876 */
1877 validateObject(controlObj, object) {
1878 for (let key in controlObj) {
1879 if (!Object.hasOwn(object, key)) {
1880 if (controlObj[key] === null) continue;
1881 else return false;
1882 }
1883
1884 if (typeof controlObj[key] === 'object' && controlObj[key] !== null) {
1885 if (typeof object[key] !== 'object') return false;
1886 if (!this.validateObject(controlObj[key], object[key])) return false;
1887 } else {
1888 if (typeof object[key] !== typeof controlObj[key]) return false;
1889 }
1890 }
1891
1892 return true;
1893 }
1894
1895 /**
1896 * Get current date as mm/dd/YYYY
1897 *
1898 * @returns {`${string}_${string}_${string}`}
1899 */
1900 getFormattedDate() {
1901 const date = new Date();
1902 let month = String(date.getMonth() + 1);
1903 let day = String(date.getDate());
1904 const year = String(date.getFullYear());
1905
1906 if (month.length < 2) month = '0' + month;
1907 if (day.length < 2) day = '0' + day;
1908
1909 return `${month}_${day}_${year}`;
1910 }
1911
1912 /**
1913 * Makes the prompt list draggable and handles swapping of two entries in the list.
1914 * @typedef {Object} Entry
1915 * @property {string} identifier
1916 * @returns {void}
1917 */
1918 makeDraggable() {
1919 $(`#${this.configuration.prefix}prompt_manager_list`).sortable({
1920 delay: this.configuration.sortableDelay,
1921 handle: isMobile() ? '.drag-handle' : null,
1922 items: `.${this.configuration.prefix}prompt_manager_prompt_draggable`,
1923 update: (event, ui) => {
1924 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
1925 const promptListElement = $(`#${this.configuration.prefix}prompt_manager_list`).sortable('toArray', { attribute: 'data-pm-identifier' });
1926 const idToObjectMap = new Map(promptOrder.map(prompt => [prompt.identifier, prompt]));
1927 const updatedPromptOrder = promptListElement.map(identifier => idToObjectMap.get(identifier));
1928
1929 this.removePromptOrderForCharacter(this.activeCharacter);
1930 this.addPromptOrderForCharacter(this.activeCharacter, updatedPromptOrder);
1931
1932 this.log(`Prompt order updated for ${this.activeCharacter.name}.`);
1933
1934 this.saveServiceSettings();
1935 },
1936 });
1937 }
1938
1939 /**
1940 * Slides down the edit form and adds the class 'openDrawer' to the first element of '#openai_prompt_manager_popup'.
1941 * @returns {void}
1942 */
1943 showPopup(area = 'edit') {
1944 const areaElement = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_' + area);
1945 areaElement.style.display = 'flex';
1946
1947 $('#' + this.configuration.prefix + 'prompt_manager_popup').first()
1948 .slideDown(200, 'swing')
1949 .addClass('openDrawer');
1950 }
1951
1952 /**
1953 * Slides up the edit form and removes the class 'openDrawer' from the first element of '#openai_prompt_manager_popup'.
1954 * @returns {void}
1955 */
1956 hidePopup() {
1957 $('#' + this.configuration.prefix + 'prompt_manager_popup').first()
1958 .slideUp(200, 'swing')
1959 .removeClass('openDrawer');
1960 }
1961
1962 /**
1963 * Quick uuid4 implementation
1964 * @returns {string} A string representation of an uuid4
1965 */
1966 getUuidv4() {
1967 return uuidv4();
1968 }
1969
1970 /**
1971 * Write to console with prefix
1972 *
1973 * @param output
1974 */
1975 log(output) {
1976 if (power_user.console_log_prompts) console.log('[PromptManager] ' + output);
1977 }
1978
1979 /**
1980 * Start a profiling task
1981 *
1982 * @param identifier
1983 */
1984 profileStart(identifier) {
1985 if (power_user.console_log_prompts) console.time(identifier);
1986 }
1987
1988 /**
1989 * End a profiling task
1990 *
1991 * @param identifier
1992 */
1993 profileEnd(identifier) {
1994 if (power_user.console_log_prompts) {
1995 this.log('Profiling of "' + identifier + '" finished. Result below.');
1996 console.timeEnd(identifier);
1997 }
1998 }
1999}
2000
2001const chatCompletionDefaultPrompts = {
2002 'prompts': [
2003 {
2004 'name': 'Main Prompt',
2005 'system_prompt': true,
2006 'role': 'system',
2007 'content': 'Write {{char}}\'s next reply in a fictional chat between {{charIfNotGroup}} and {{user}}.',
2008 'identifier': 'main',
2009 },
2010 {
2011 'name': 'Auxiliary Prompt',
2012 'system_prompt': true,
2013 'role': 'system',
2014 'content': '',
2015 'identifier': 'nsfw',
2016 },
2017 {
2018 'identifier': 'dialogueExamples',
2019 'name': 'Chat Examples',
2020 'system_prompt': true,
2021 'marker': true,
2022 },
2023 {
2024 'name': 'Post-History Instructions',
2025 'system_prompt': true,
2026 'role': 'system',
2027 'content': '',
2028 'identifier': 'jailbreak',
2029 },
2030 {
2031 'identifier': 'chatHistory',
2032 'name': 'Chat History',
2033 'system_prompt': true,
2034 'marker': true,
2035 },
2036 {
2037 'identifier': 'worldInfoAfter',
2038 'name': 'World Info (after)',
2039 'system_prompt': true,
2040 'marker': true,
2041 },
2042 {
2043 'identifier': 'worldInfoBefore',
2044 'name': 'World Info (before)',
2045 'system_prompt': true,
2046 'marker': true,
2047 },
2048 {
2049 'identifier': 'enhanceDefinitions',
2050 'role': 'system',
2051 'name': 'Enhance Definitions',
2052 'content': 'If you have more knowledge of {{char}}, add to the character\'s lore and personality to enhance them but keep the Character Sheet\'s definitions absolute.',
2053 'system_prompt': true,
2054 'marker': false,
2055 },
2056 {
2057 'identifier': 'charDescription',
2058 'name': 'Char Description',
2059 'system_prompt': true,
2060 'marker': true,
2061 },
2062 {
2063 'identifier': 'charPersonality',
2064 'name': 'Char Personality',
2065 'system_prompt': true,
2066 'marker': true,
2067 },
2068 {
2069 'identifier': 'scenario',
2070 'name': 'Scenario',
2071 'system_prompt': true,
2072 'marker': true,
2073 },
2074 {
2075 'identifier': 'personaDescription',
2076 'name': 'Persona Description',
2077 'system_prompt': true,
2078 'marker': true,
2079 },
2080 ],
2081};
2082
2083const promptManagerDefaultPromptOrders = {
2084 'prompt_order': [],
2085};
2086
2087const promptManagerDefaultPromptOrder = [
2088 {
2089 'identifier': 'main',
2090 'enabled': true,
2091 },
2092 {
2093 'identifier': 'worldInfoBefore',
2094 'enabled': true,
2095 },
2096 {
2097 'identifier': 'personaDescription',
2098 'enabled': true,
2099 },
2100 {
2101 'identifier': 'charDescription',
2102 'enabled': true,
2103 },
2104 {
2105 'identifier': 'charPersonality',
2106 'enabled': true,
2107 },
2108 {
2109 'identifier': 'scenario',
2110 'enabled': true,
2111 },
2112 {
2113 'identifier': 'enhanceDefinitions',
2114 'enabled': false,
2115 },
2116 {
2117 'identifier': 'nsfw',
2118 'enabled': true,
2119 },
2120 {
2121 'identifier': 'worldInfoAfter',
2122 'enabled': true,
2123 },
2124 {
2125 'identifier': 'dialogueExamples',
2126 'enabled': true,
2127 },
2128 {
2129 'identifier': 'chatHistory',
2130 'enabled': true,
2131 },
2132 {
2133 'identifier': 'jailbreak',
2134 'enabled': true,
2135 },
2136];
2137
2138export {
2139 PromptManager,
2140 registerPromptManagerMigration,
2141 chatCompletionDefaultPrompts,
2142 promptManagerDefaultPromptOrders,
2143 Prompt,
2144};