mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			1974 lines
		
	
	
		
			80 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1974 lines
		
	
	
		
			80 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| import { DOMPurify, Popper } from '../lib.js';
 | |
| 
 | |
| import { event_types, eventSource, is_send_press, main_api, substituteParams } from '../script.js';
 | |
| import { is_group_generating } from './group-chats.js';
 | |
| import { Message, TokenHandler } from './openai.js';
 | |
| import { power_user } from './power-user.js';
 | |
| import { debounce, waitUntilCondition, escapeHtml } from './utils.js';
 | |
| import { debounce_timeout } from './constants.js';
 | |
| import { renderTemplateAsync } from './templates.js';
 | |
| import { Popup } from './popup.js';
 | |
| import { t } from './i18n.js';
 | |
| import { isMobile } from './RossAscends-mods.js';
 | |
| 
 | |
| function debouncePromise(func, delay) {
 | |
|     let timeoutId;
 | |
| 
 | |
|     return (...args) => {
 | |
|         clearTimeout(timeoutId);
 | |
| 
 | |
|         return new Promise((resolve) => {
 | |
|             timeoutId = setTimeout(() => {
 | |
|                 const result = func(...args);
 | |
|                 resolve(result);
 | |
|             }, delay);
 | |
|         });
 | |
|     };
 | |
| }
 | |
| 
 | |
| const DEFAULT_DEPTH = 4;
 | |
| 
 | |
| /**
 | |
|  * @enum {number}
 | |
|  */
 | |
| export const INJECTION_POSITION = {
 | |
|     RELATIVE: 0,
 | |
|     ABSOLUTE: 1,
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Register migrations for the prompt manager when settings are loaded or an Open AI preset is loaded.
 | |
|  */
 | |
| const registerPromptManagerMigration = () => {
 | |
|     const migrate = (settings, savePreset = null, presetName = null) => {
 | |
|         if ('Default' === presetName) return;
 | |
| 
 | |
|         if (settings.main_prompt || settings.nsfw_prompt || settings.jailbreak_prompt) {
 | |
|             console.log('Running prompt manager configuration migration');
 | |
|             if (settings.prompts === undefined || settings.prompts.length === 0) settings.prompts = structuredClone(chatCompletionDefaultPrompts.prompts);
 | |
| 
 | |
|             const findPrompt = (identifier) => settings.prompts.find(prompt => identifier === prompt.identifier);
 | |
|             if (settings.main_prompt) {
 | |
|                 findPrompt('main').content = settings.main_prompt;
 | |
|                 delete settings.main_prompt;
 | |
|             }
 | |
| 
 | |
|             if (settings.nsfw_prompt) {
 | |
|                 findPrompt('nsfw').content = settings.nsfw_prompt;
 | |
|                 delete settings.nsfw_prompt;
 | |
|             }
 | |
| 
 | |
|             if (settings.jailbreak_prompt) {
 | |
|                 findPrompt('jailbreak').content = settings.jailbreak_prompt;
 | |
|                 delete settings.jailbreak_prompt;
 | |
|             }
 | |
| 
 | |
|             if (savePreset && presetName) savePreset(presetName, settings, false);
 | |
|         }
 | |
|     };
 | |
| 
 | |
|     eventSource.on(event_types.SETTINGS_LOADED_BEFORE, settings => migrate(settings));
 | |
|     eventSource.on(event_types.OAI_PRESET_CHANGED_BEFORE, event => migrate(event.preset, event.savePreset, event.presetName));
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Represents a prompt.
 | |
|  */
 | |
| class Prompt {
 | |
|     identifier; role; content; name; system_prompt; position; injection_position; injection_depth; forbid_overrides; extension;
 | |
| 
 | |
|     /**
 | |
|      * Create a new Prompt instance.
 | |
|      *
 | |
|      * @param {Object} param0 - Object containing the properties of the prompt.
 | |
|      * @param {string} param0.identifier - The unique identifier of the prompt.
 | |
|      * @param {string} param0.role - The role associated with the prompt.
 | |
|      * @param {string} param0.content - The content of the prompt.
 | |
|      * @param {string} param0.name - The name of the prompt.
 | |
|      * @param {boolean} param0.system_prompt - Indicates if the prompt is a system prompt.
 | |
|      * @param {string} param0.position - The position of the prompt in the prompt list.
 | |
|      * @param {number} param0.injection_position - The insert position of the prompt.
 | |
|      * @param {number} param0.injection_depth - The depth of the prompt in the chat.
 | |
|      * @param {boolean} param0.forbid_overrides - Indicates if the prompt should not be overridden.
 | |
|      * @param {boolean} param0.extension - Prompt is added by an extension.
 | |
|      */
 | |
|     constructor({ identifier, role, content, name, system_prompt, position, injection_depth, injection_position, forbid_overrides, extension } = {}) {
 | |
|         this.identifier = identifier;
 | |
|         this.role = role;
 | |
|         this.content = content;
 | |
|         this.name = name;
 | |
|         this.system_prompt = system_prompt;
 | |
|         this.position = position;
 | |
|         this.injection_depth = injection_depth;
 | |
|         this.injection_position = injection_position;
 | |
|         this.forbid_overrides = forbid_overrides;
 | |
|         this.extension = extension ?? false;
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Representing a collection of prompts.
 | |
|  */
 | |
| export class PromptCollection {
 | |
|     collection = [];
 | |
|     overriddenPrompts = [];
 | |
| 
 | |
|     /**
 | |
|      * Create a new PromptCollection instance.
 | |
|      *
 | |
|      * @param {...Prompt} prompts - An array of Prompt instances.
 | |
|      */
 | |
|     constructor(...prompts) {
 | |
|         this.add(...prompts);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks if the provided instances are of the Prompt class.
 | |
|      *
 | |
|      * @param {...any} prompts - Instances to check.
 | |
|      * @throws Will throw an error if one or more instances are not of the Prompt class.
 | |
|      */
 | |
|     checkPromptInstance(...prompts) {
 | |
|         for (let prompt of prompts) {
 | |
|             if (!(prompt instanceof Prompt)) {
 | |
|                 throw new Error('Only Prompt instances can be added to PromptCollection');
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Adds new Prompt instances to the collection.
 | |
|      *
 | |
|      * @param {...Prompt} prompts - An array of Prompt instances.
 | |
|      */
 | |
|     add(...prompts) {
 | |
|         this.checkPromptInstance(...prompts);
 | |
|         this.collection.push(...prompts);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets a Prompt instance at a specific position in the collection.
 | |
|      *
 | |
|      * @param {Prompt} prompt - The Prompt instance to set.
 | |
|      * @param {number} position - The position in the collection to set the Prompt instance.
 | |
|      */
 | |
|     set(prompt, position) {
 | |
|         this.checkPromptInstance(prompt);
 | |
|         this.collection[position] = prompt;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Retrieves a Prompt instance from the collection by its identifier.
 | |
|      *
 | |
|      * @param {string} identifier - The identifier of the Prompt instance to retrieve.
 | |
|      * @returns {Prompt} The Prompt instance with the provided identifier, or undefined if not found.
 | |
|      */
 | |
|     get(identifier) {
 | |
|         return this.collection.find(prompt => prompt.identifier === identifier);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Retrieves the index of a Prompt instance in the collection by its identifier.
 | |
|      *
 | |
|      * @param {string} identifier - The identifier of the Prompt instance to find.
 | |
|      * @returns {number} The index of the Prompt instance in the collection, or -1 if not found.
 | |
|      */
 | |
|     index(identifier) {
 | |
|         return this.collection.findIndex(prompt => prompt.identifier === identifier);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks if a Prompt instance exists in the collection by its identifier.
 | |
|      *
 | |
|      * @param {string} identifier - The identifier of the Prompt instance to check.
 | |
|      * @returns {boolean} true if the Prompt instance exists in the collection, false otherwise.
 | |
|      */
 | |
|     has(identifier) {
 | |
|         return this.index(identifier) !== -1;
 | |
|     }
 | |
| 
 | |
|     override(prompt, position) {
 | |
|         this.set(prompt, position);
 | |
|         this.overriddenPrompts.push(prompt.identifier);
 | |
|     }
 | |
| }
 | |
| 
 | |
| class PromptManager {
 | |
|     constructor() {
 | |
|         this.systemPrompts = [
 | |
|             'main',
 | |
|             'nsfw',
 | |
|             'jailbreak',
 | |
|             'enhanceDefinitions',
 | |
|         ];
 | |
| 
 | |
|         this.overridablePrompts = [
 | |
|             'main',
 | |
|             'jailbreak',
 | |
|         ];
 | |
| 
 | |
|         this.overriddenPrompts = [];
 | |
| 
 | |
|         this.configuration = {
 | |
|             version: 1,
 | |
|             prefix: '',
 | |
|             containerIdentifier: '',
 | |
|             listIdentifier: '',
 | |
|             listItemTemplateIdentifier: '',
 | |
|             toggleDisabled: [],
 | |
|             promptOrder: {
 | |
|                 strategy: 'global',
 | |
|                 dummyId: 100000,
 | |
|             },
 | |
|             sortableDelay: 30,
 | |
|             warningTokenThreshold: 1500,
 | |
|             dangerTokenThreshold: 500,
 | |
|             defaultPrompts: {
 | |
|                 main: '',
 | |
|                 nsfw: '',
 | |
|                 jailbreak: '',
 | |
|                 enhanceDefinitions: '',
 | |
|             },
 | |
|         };
 | |
| 
 | |
|         // Chatcompletion configuration object
 | |
|         this.serviceSettings = null;
 | |
| 
 | |
|         // DOM element containing the prompt manager
 | |
|         this.containerElement = null;
 | |
| 
 | |
|         // DOM element containing the prompt list
 | |
|         this.listElement = null;
 | |
| 
 | |
|         // Currently selected character
 | |
|         this.activeCharacter = null;
 | |
| 
 | |
|         // Message collection of the most recent chatcompletion
 | |
|         this.messages = null;
 | |
| 
 | |
|         // The current token handler instance
 | |
|         this.tokenHandler = null;
 | |
| 
 | |
|         // Token usage of last dry run
 | |
|         this.tokenUsage = 0;
 | |
| 
 | |
|         // Error state, contains error message.
 | |
|         this.error = null;
 | |
| 
 | |
|         /** Dry-run for generate, must return a promise  */
 | |
|         this.tryGenerate = async () => { };
 | |
| 
 | |
|         /** Called to persist the configuration, must return a promise */
 | |
|         this.saveServiceSettings = () => { };
 | |
| 
 | |
|         /** Toggle prompt button click */
 | |
|         this.handleToggle = () => { };
 | |
| 
 | |
|         /** Prompt name click */
 | |
|         this.handleInspect = () => { };
 | |
| 
 | |
|         /** Edit prompt button click */
 | |
|         this.handleEdit = () => { };
 | |
| 
 | |
|         /** Detach prompt button click */
 | |
|         this.handleDetach = () => { };
 | |
| 
 | |
|         /** Save prompt button click */
 | |
|         this.handleSavePrompt = () => { };
 | |
| 
 | |
|         /** Reset prompt button click */
 | |
|         this.handleResetPrompt = () => { };
 | |
| 
 | |
|         /** New prompt button click */
 | |
|         this.handleNewPrompt = () => { };
 | |
| 
 | |
|         /** Delete prompt button click */
 | |
|         this.handleDeletePrompt = () => { };
 | |
| 
 | |
|         /** Append prompt button click */
 | |
|         this.handleAppendPrompt = () => { };
 | |
| 
 | |
|         /** Import button click */
 | |
|         this.handleImport = () => { };
 | |
| 
 | |
|         /** Full export click */
 | |
|         this.handleFullExport = () => { };
 | |
| 
 | |
|         /** Character export click */
 | |
|         this.handleCharacterExport = () => { };
 | |
| 
 | |
|         /** Character reset button click*/
 | |
|         this.handleCharacterReset = () => { };
 | |
| 
 | |
|         /** Debounced version of render */
 | |
|         this.renderDebounced = debounce(this.render.bind(this), debounce_timeout.relaxed);
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Initializes the PromptManager with provided configuration and service settings.
 | |
|      *
 | |
|      * Sets up various handlers for user interactions, event listeners and initial rendering of prompts.
 | |
|      * It is also responsible for preparing prompt edit form buttons, managing popup form close and clear actions.
 | |
|      *
 | |
|      * @param {Object} moduleConfiguration - Configuration object for the PromptManager.
 | |
|      * @param {Object} serviceSettings - Service settings object for the PromptManager.
 | |
|      */
 | |
|     init(moduleConfiguration, serviceSettings) {
 | |
|         this.configuration = Object.assign(this.configuration, moduleConfiguration);
 | |
|         this.tokenHandler = this.tokenHandler || new TokenHandler(() => { throw new Error('Token handler not set'); });
 | |
|         this.serviceSettings = serviceSettings;
 | |
|         this.containerElement = document.getElementById(this.configuration.containerIdentifier);
 | |
| 
 | |
|         if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
 | |
| 
 | |
|         this.sanitizeServiceSettings();
 | |
| 
 | |
|         // Enable and disable prompts
 | |
|         this.handleToggle = (event) => {
 | |
|             const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
 | |
|             const promptOrderEntry = this.getPromptOrderEntry(this.activeCharacter, promptID);
 | |
|             const counts = this.tokenHandler.getCounts();
 | |
| 
 | |
|             counts[promptID] = null;
 | |
|             promptOrderEntry.enabled = !promptOrderEntry.enabled;
 | |
|             this.render();
 | |
|             this.saveServiceSettings();
 | |
|         };
 | |
| 
 | |
|         // Open edit form and load selected prompt
 | |
|         this.handleEdit = (event) => {
 | |
|             this.clearEditForm();
 | |
|             this.clearInspectForm();
 | |
| 
 | |
|             const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
 | |
|             const prompt = this.getPromptById(promptID);
 | |
| 
 | |
|             this.loadPromptIntoEditForm(prompt);
 | |
| 
 | |
|             this.showPopup();
 | |
|         };
 | |
| 
 | |
|         // Open edit form and load selected prompt
 | |
|         this.handleInspect = (event) => {
 | |
|             this.clearEditForm();
 | |
|             this.clearInspectForm();
 | |
| 
 | |
|             const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
 | |
|             if (true === this.messages.hasItemWithIdentifier(promptID)) {
 | |
|                 const messages = this.messages.getItemByIdentifier(promptID);
 | |
| 
 | |
|                 this.loadMessagesIntoInspectForm(messages);
 | |
| 
 | |
|                 this.showPopup('inspect');
 | |
|             }
 | |
|         };
 | |
| 
 | |
|         // Detach selected prompt from list form and close edit form
 | |
|         this.handleDetach = (event) => {
 | |
|             if (null === this.activeCharacter) return;
 | |
|             const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier;
 | |
|             const prompt = this.getPromptById(promptID);
 | |
| 
 | |
|             this.detachPrompt(prompt, this.activeCharacter);
 | |
|             this.hidePopup();
 | |
|             this.clearEditForm();
 | |
|             this.render();
 | |
|             this.saveServiceSettings();
 | |
|         };
 | |
| 
 | |
|         // Save prompt edit form to settings and close form.
 | |
|         this.handleSavePrompt = (event) => {
 | |
|             const promptId = event.target.dataset.pmPrompt;
 | |
|             const prompt = this.getPromptById(promptId);
 | |
| 
 | |
|             if (null === prompt) {
 | |
|                 const newPrompt = {};
 | |
|                 this.updatePromptWithPromptEditForm(newPrompt);
 | |
|                 this.addPrompt(newPrompt, promptId);
 | |
|             } else {
 | |
|                 this.updatePromptWithPromptEditForm(prompt);
 | |
|             }
 | |
| 
 | |
|             if ('main' === promptId) this.updateQuickEdit('main', prompt);
 | |
|             if ('nsfw' === promptId) this.updateQuickEdit('nsfw', prompt);
 | |
|             if ('jailbreak' === promptId) this.updateQuickEdit('jailbreak', prompt);
 | |
| 
 | |
|             this.log('Saved prompt: ' + promptId);
 | |
| 
 | |
|             this.hidePopup();
 | |
|             this.clearEditForm();
 | |
|             this.render();
 | |
|             this.saveServiceSettings();
 | |
|         };
 | |
| 
 | |
|         // Reset prompt should it be a system prompt
 | |
|         this.handleResetPrompt = (event) => {
 | |
|             const promptId = event.target.dataset.pmPrompt;
 | |
|             const prompt = this.getPromptById(promptId);
 | |
| 
 | |
|             switch (promptId) {
 | |
|                 case 'main':
 | |
|                     prompt.name = 'Main Prompt';
 | |
|                     prompt.content = this.configuration.defaultPrompts.main;
 | |
|                     prompt.forbid_overrides = false;
 | |
|                     break;
 | |
|                 case 'nsfw':
 | |
|                     prompt.name = 'Nsfw Prompt';
 | |
|                     prompt.content = this.configuration.defaultPrompts.nsfw;
 | |
|                     break;
 | |
|                 case 'jailbreak':
 | |
|                     prompt.name = 'Jailbreak Prompt';
 | |
|                     prompt.content = this.configuration.defaultPrompts.jailbreak;
 | |
|                     prompt.forbid_overrides = false;
 | |
|                     break;
 | |
|                 case 'enhanceDefinitions':
 | |
|                     prompt.name = 'Enhance Definitions';
 | |
|                     prompt.content = this.configuration.defaultPrompts.enhanceDefinitions;
 | |
|                     break;
 | |
|             }
 | |
| 
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value = prompt.name;
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value = 'system';
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value = prompt.content ?? '';
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value = prompt.injection_position ?? 0;
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value = prompt.injection_depth ?? DEFAULT_DEPTH;
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block').style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides').checked = prompt.forbid_overrides ?? false;
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block').style.visibility = this.overridablePrompts.includes(prompt.identifier) ? 'visible' : 'hidden';
 | |
|             document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').disabled = prompt.marker ?? false;
 | |
| 
 | |
|             if (!this.systemPrompts.includes(promptId)) {
 | |
|                 document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').removeAttribute('disabled');
 | |
|             }
 | |
|         };
 | |
| 
 | |
|         // Append prompt to selected character
 | |
|         this.handleAppendPrompt = (event) => {
 | |
|             const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value;
 | |
|             const prompt = this.getPromptById(promptID);
 | |
| 
 | |
|             if (prompt) {
 | |
|                 this.appendPrompt(prompt, this.activeCharacter);
 | |
|                 this.render();
 | |
|                 this.saveServiceSettings();
 | |
|             }
 | |
|         };
 | |
| 
 | |
|         // Delete selected prompt from list form and close edit form
 | |
|         this.handleDeletePrompt = async (event) => {
 | |
|             Popup.show.confirm(t`Are you sure you want to delete this prompt?`, null).then((userChoice) => {
 | |
|                 if (!userChoice) return;
 | |
|                 const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value;
 | |
|                 const prompt = this.getPromptById(promptID);
 | |
| 
 | |
|                 if (prompt && true === this.isPromptDeletionAllowed(prompt)) {
 | |
|                     const promptIndex = this.getPromptIndexById(promptID);
 | |
|                     this.serviceSettings.prompts.splice(Number(promptIndex), 1);
 | |
| 
 | |
|                     this.log('Deleted prompt: ' + prompt.identifier);
 | |
| 
 | |
|                     this.hidePopup();
 | |
|                     this.clearEditForm();
 | |
|                     this.render();
 | |
|                     this.saveServiceSettings();
 | |
|                 }
 | |
|             });
 | |
|         };
 | |
| 
 | |
|         // Create new prompt, then save it to settings and close form.
 | |
|         this.handleNewPrompt = (event) => {
 | |
|             const prompt = {
 | |
|                 identifier: this.getUuidv4(),
 | |
|                 name: '',
 | |
|                 role: 'system',
 | |
|                 content: '',
 | |
|             };
 | |
| 
 | |
|             this.loadPromptIntoEditForm(prompt);
 | |
|             this.showPopup();
 | |
|         };
 | |
| 
 | |
|         // Export all user prompts
 | |
|         this.handleFullExport = () => {
 | |
|             const prompts = this.serviceSettings.prompts.reduce((userPrompts, prompt) => {
 | |
|                 if (false === prompt.system_prompt && false === prompt.marker) userPrompts.push(prompt);
 | |
|                 return userPrompts;
 | |
|             }, []);
 | |
| 
 | |
|             let promptOrder = [];
 | |
|             if ('global' === this.configuration.promptOrder.strategy) {
 | |
|                 promptOrder = this.getPromptOrderForCharacter({ id: this.configuration.promptOrder.dummyId });
 | |
|             } else if ('character' === this.configuration.promptOrder.strategy) {
 | |
|                 promptOrder = [];
 | |
|             } else {
 | |
|                 throw new Error('Prompt order strategy not supported.');
 | |
|             }
 | |
| 
 | |
|             const exportPrompts = {
 | |
|                 prompts: prompts,
 | |
|                 prompt_order: promptOrder,
 | |
|             };
 | |
| 
 | |
|             this.export(exportPrompts, 'full', 'st-prompts');
 | |
|         };
 | |
| 
 | |
|         // Export user prompts and order for this character
 | |
|         this.handleCharacterExport = () => {
 | |
|             const characterPrompts = this.getPromptsForCharacter(this.activeCharacter).reduce((userPrompts, prompt) => {
 | |
|                 if (false === prompt.system_prompt && !prompt.marker) userPrompts.push(prompt);
 | |
|                 return userPrompts;
 | |
|             }, []);
 | |
| 
 | |
|             const characterList = this.getPromptOrderForCharacter(this.activeCharacter);
 | |
| 
 | |
|             const exportPrompts = {
 | |
|                 prompts: characterPrompts,
 | |
|                 prompt_order: characterList,
 | |
|             };
 | |
| 
 | |
|             const name = this.activeCharacter.name + '-prompts';
 | |
|             this.export(exportPrompts, 'character', name);
 | |
|         };
 | |
| 
 | |
|         // Import prompts for the selected character
 | |
|         this.handleImport = () => {
 | |
|             Popup.show.confirm(t`Existing prompts with the same ID will be overridden. Do you want to proceed?`, null)
 | |
|                 .then(userChoice => {
 | |
|                     if (!userChoice) return;
 | |
| 
 | |
|                     const fileOpener = document.createElement('input');
 | |
|                     fileOpener.type = 'file';
 | |
|                     fileOpener.accept = '.json';
 | |
| 
 | |
|                     fileOpener.addEventListener('change', (event) => {
 | |
|                         const file = event.target.files[0];
 | |
|                         if (!file) return;
 | |
| 
 | |
|                         const reader = new FileReader();
 | |
| 
 | |
|                         reader.onload = (event) => {
 | |
|                             const fileContent = event.target.result;
 | |
| 
 | |
|                             try {
 | |
|                                 const data = JSON.parse(fileContent);
 | |
|                                 this.import(data);
 | |
|                             } catch (err) {
 | |
|                                 toastr.error(t`An error occurred while importing prompts. More info available in console.`);
 | |
|                                 console.log('An error occurred while importing prompts');
 | |
|                                 console.log(err.toString());
 | |
|                             }
 | |
|                         };
 | |
| 
 | |
|                         reader.readAsText(file);
 | |
|                     });
 | |
| 
 | |
|                     fileOpener.click();
 | |
|                 });
 | |
|         };
 | |
| 
 | |
|         // Restore default state of a characters prompt order
 | |
|         this.handleCharacterReset = () => {
 | |
|             Popup.show.confirm(t`This will reset the prompt order for this character. You will not lose any prompts.`, null)
 | |
|                 .then(userChoice => {
 | |
|                     if (!userChoice) return;
 | |
| 
 | |
|                     this.removePromptOrderForCharacter(this.activeCharacter);
 | |
|                     this.addPromptOrderForCharacter(this.activeCharacter, promptManagerDefaultPromptOrder);
 | |
| 
 | |
|                     this.render();
 | |
|                     this.saveServiceSettings();
 | |
|                 });
 | |
|         };
 | |
| 
 | |
|         // Fill quick edit fields for the first time
 | |
|         if ('global' === this.configuration.promptOrder.strategy) {
 | |
|             const handleQuickEditSave = (event) => {
 | |
|                 const promptId = event.target.dataset.pmPrompt;
 | |
|                 const prompt = this.getPromptById(promptId);
 | |
| 
 | |
|                 prompt.content = event.target.value;
 | |
| 
 | |
|                 // Update edit form if present
 | |
|                 // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
 | |
|                 const popupEditFormPrompt = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
 | |
|                 if (popupEditFormPrompt.offsetParent) {
 | |
|                     popupEditFormPrompt.value = prompt.content;
 | |
|                 }
 | |
| 
 | |
|                 this.log('Saved prompt: ' + promptId);
 | |
|                 this.saveServiceSettings().then(() => this.render());
 | |
|             };
 | |
| 
 | |
|             const mainPrompt = this.getPromptById('main');
 | |
|             const mainElementId = this.updateQuickEdit('main', mainPrompt);
 | |
|             document.getElementById(mainElementId).addEventListener('blur', handleQuickEditSave);
 | |
| 
 | |
|             const nsfwPrompt = this.getPromptById('nsfw');
 | |
|             const nsfwElementId = this.updateQuickEdit('nsfw', nsfwPrompt);
 | |
|             document.getElementById(nsfwElementId).addEventListener('blur', handleQuickEditSave);
 | |
| 
 | |
|             const jailbreakPrompt = this.getPromptById('jailbreak');
 | |
|             const jailbreakElementId = this.updateQuickEdit('jailbreak', jailbreakPrompt);
 | |
|             document.getElementById(jailbreakElementId).addEventListener('blur', handleQuickEditSave);
 | |
|         }
 | |
| 
 | |
|         // Re-render when chat history changes.
 | |
|         eventSource.on(event_types.MESSAGE_DELETED, () => this.renderDebounced());
 | |
|         eventSource.on(event_types.MESSAGE_EDITED, () => this.renderDebounced());
 | |
|         eventSource.on(event_types.MESSAGE_RECEIVED, () => this.renderDebounced());
 | |
| 
 | |
|         // Re-render when chatcompletion settings change
 | |
|         eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, () => this.renderDebounced());
 | |
| 
 | |
|         eventSource.on(event_types.CHATCOMPLETION_MODEL_CHANGED, () => this.renderDebounced());
 | |
| 
 | |
|         // Re-render when the character changes.
 | |
|         eventSource.on('chatLoaded', (event) => {
 | |
|             this.handleCharacterSelected(event);
 | |
|             this.saveServiceSettings().then(() => this.renderDebounced());
 | |
|         });
 | |
| 
 | |
|         // Re-render when the character gets edited.
 | |
|         eventSource.on(event_types.CHARACTER_EDITED, (event) => {
 | |
|             this.handleCharacterUpdated(event);
 | |
|             this.saveServiceSettings().then(() => this.renderDebounced());
 | |
|         });
 | |
| 
 | |
|         // Re-render when the group changes.
 | |
|         eventSource.on('groupSelected', (event) => {
 | |
|             this.handleGroupSelected(event);
 | |
|             this.saveServiceSettings().then(() => this.renderDebounced());
 | |
|         });
 | |
| 
 | |
|         // Sanitize settings after character has been deleted.
 | |
|         eventSource.on(event_types.CHARACTER_DELETED, (event) => {
 | |
|             this.handleCharacterDeleted(event);
 | |
|             this.saveServiceSettings().then(() => this.renderDebounced());
 | |
|         });
 | |
| 
 | |
|         // Trigger re-render when token settings are changed
 | |
|         document.getElementById('openai_max_context').addEventListener('change', (event) => {
 | |
|             this.serviceSettings.openai_max_context = event.target.value;
 | |
|             if (this.activeCharacter) this.renderDebounced();
 | |
|         });
 | |
| 
 | |
|         document.getElementById('openai_max_tokens').addEventListener('change', (event) => {
 | |
|             if (this.activeCharacter) this.renderDebounced();
 | |
|         });
 | |
| 
 | |
|         // Prepare prompt edit form buttons
 | |
|         document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_save').addEventListener('click', this.handleSavePrompt);
 | |
|         document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset').addEventListener('click', this.handleResetPrompt);
 | |
| 
 | |
|         const closeAndClearPopup = () => {
 | |
|             this.hidePopup();
 | |
|             this.clearEditForm();
 | |
|             this.clearInspectForm();
 | |
|         };
 | |
| 
 | |
|         // Clear forms on closing the popup
 | |
|         document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_close').addEventListener('click', closeAndClearPopup);
 | |
|         document.getElementById(this.configuration.prefix + 'prompt_manager_popup_close_button').addEventListener('click', closeAndClearPopup);
 | |
| 
 | |
|         // Re-render prompt manager on openai preset change
 | |
|         eventSource.on(event_types.OAI_PRESET_CHANGED_AFTER, () => {
 | |
|             this.sanitizeServiceSettings();
 | |
|             const mainPrompt = this.getPromptById('main');
 | |
|             this.updateQuickEdit('main', mainPrompt);
 | |
| 
 | |
|             const nsfwPrompt = this.getPromptById('nsfw');
 | |
|             this.updateQuickEdit('nsfw', nsfwPrompt);
 | |
| 
 | |
|             const jailbreakPrompt = this.getPromptById('jailbreak');
 | |
|             this.updateQuickEdit('jailbreak', jailbreakPrompt);
 | |
| 
 | |
|             this.hidePopup();
 | |
|             this.clearEditForm();
 | |
|             this.renderDebounced();
 | |
|         });
 | |
| 
 | |
|         // Re-render prompt manager on world settings update
 | |
|         eventSource.on(event_types.WORLDINFO_SETTINGS_UPDATED, () => this.renderDebounced());
 | |
| 
 | |
|         this.log('Initialized');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get the scroll position of the prompt manager
 | |
|      * @returns {number} - Scroll position of the prompt manager
 | |
|      */
 | |
|     #getScrollPosition() {
 | |
|         return document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTop;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set the scroll position of the prompt manager
 | |
|      * @param {number} scrollPosition - The scroll position to set
 | |
|      */
 | |
|     #setScrollPosition(scrollPosition) {
 | |
|         if (scrollPosition === undefined || scrollPosition === null) return;
 | |
|         document.getElementById(this.configuration.prefix + 'prompt_manager')?.closest('.scrollableInner')?.scrollTo(0, scrollPosition);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Main rendering function
 | |
|      *
 | |
|      * @param afterTryGenerate - Whether a dry run should be attempted before rendering
 | |
|      */
 | |
|     render(afterTryGenerate = true) {
 | |
|         if (main_api !== 'openai') return;
 | |
| 
 | |
|         if ('character' === this.configuration.promptOrder.strategy && null === this.activeCharacter) return;
 | |
|         this.error = null;
 | |
| 
 | |
|         waitUntilCondition(() => !is_send_press && !is_group_generating, 1024 * 1024, 100).then(async () => {
 | |
|             if (true === afterTryGenerate) {
 | |
|                 // Executed during dry-run for determining context composition
 | |
|                 this.profileStart('filling context');
 | |
|                 this.tryGenerate().finally(async () => {
 | |
|                     this.profileEnd('filling context');
 | |
|                     this.profileStart('render');
 | |
|                     const scrollPosition = this.#getScrollPosition();
 | |
|                     await this.renderPromptManager();
 | |
|                     await this.renderPromptManagerListItems();
 | |
|                     this.makeDraggable();
 | |
|                     this.#setScrollPosition(scrollPosition);
 | |
|                     this.profileEnd('render');
 | |
|                 });
 | |
|             } else {
 | |
|                 // Executed during live communication
 | |
|                 this.profileStart('render');
 | |
|                 const scrollPosition = this.#getScrollPosition();
 | |
|                 await this.renderPromptManager();
 | |
|                 await this.renderPromptManagerListItems();
 | |
|                 this.makeDraggable();
 | |
|                 this.#setScrollPosition(scrollPosition);
 | |
|                 this.profileEnd('render');
 | |
|             }
 | |
|         }).catch(() => {
 | |
|             console.log('Timeout while waiting for send press to be false');
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Update a prompt with the values from the HTML form.
 | |
|      * @param {object} prompt - The prompt to be updated.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     updatePromptWithPromptEditForm(prompt) {
 | |
|         prompt.name = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name').value;
 | |
|         prompt.role = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role').value;
 | |
|         prompt.content = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt').value;
 | |
|         prompt.injection_position = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').value);
 | |
|         prompt.injection_depth = Number(document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth').value);
 | |
|         prompt.forbid_overrides = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides').checked;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Find a prompt by its identifier and update it with the provided object.
 | |
|      * @param {string} identifier - The identifier of the prompt.
 | |
|      * @param {object} updatePrompt - An object with properties to be updated in the prompt.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     updatePromptByIdentifier(identifier, updatePrompt) {
 | |
|         let prompt = this.serviceSettings.prompts.find((item) => identifier === item.identifier);
 | |
|         if (prompt) prompt = Object.assign(prompt, updatePrompt);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Iterate over an array of prompts, find each one by its identifier, and update them with the provided data.
 | |
|      * @param {object[]} prompts - An array of prompt updates.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     updatePrompts(prompts) {
 | |
|         prompts.forEach((update) => {
 | |
|             let prompt = this.getPromptById(update.identifier);
 | |
|             if (prompt) Object.assign(prompt, update);
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     getTokenHandler() {
 | |
|         return this.tokenHandler;
 | |
|     }
 | |
| 
 | |
|     isPromptDisabledForActiveCharacter(identifier) {
 | |
|         const promptOrderEntry = this.getPromptOrderEntry(this.activeCharacter, identifier);
 | |
|         if (promptOrderEntry) return !promptOrderEntry.enabled;
 | |
|         return false;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Add a prompt to the current character's prompt list.
 | |
|      * @param {object} prompt - The prompt to be added.
 | |
|      * @param {object} character - The character whose prompt list will be updated.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     appendPrompt(prompt, character) {
 | |
|         const promptOrder = this.getPromptOrderForCharacter(character);
 | |
|         const index = promptOrder.findIndex(entry => entry.identifier === prompt.identifier);
 | |
| 
 | |
|         if (-1 === index) promptOrder.unshift({ identifier: prompt.identifier, enabled: false });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Remove a prompt from the current character's prompt list.
 | |
|      * @param {object} prompt - The prompt to be removed.
 | |
|      * @param {object} character - The character whose prompt list will be updated.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     // Remove a prompt from the current characters prompt list
 | |
|     detachPrompt(prompt, character) {
 | |
|         const promptOrder = this.getPromptOrderForCharacter(character);
 | |
|         const index = promptOrder.findIndex(entry => entry.identifier === prompt.identifier);
 | |
|         if (-1 === index) return;
 | |
|         promptOrder.splice(index, 1);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Create a new prompt and add it to the list of prompts.
 | |
|      * @param {object} prompt - The prompt to be added.
 | |
|      * @param {string} identifier - The identifier for the new prompt.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     addPrompt(prompt, identifier) {
 | |
| 
 | |
|         if (typeof prompt !== 'object' || prompt === null) throw new Error('Object is not a prompt');
 | |
| 
 | |
|         const newPrompt = {
 | |
|             identifier: identifier,
 | |
|             system_prompt: false,
 | |
|             enabled: false,
 | |
|             marker: false,
 | |
|             ...prompt,
 | |
|         };
 | |
| 
 | |
|         this.serviceSettings.prompts.push(newPrompt);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sanitize the service settings, ensuring each prompt has a unique identifier.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     sanitizeServiceSettings() {
 | |
|         this.serviceSettings.prompts = this.serviceSettings.prompts ?? [];
 | |
|         this.serviceSettings.prompt_order = this.serviceSettings.prompt_order ?? [];
 | |
| 
 | |
|         if ('global' === this.configuration.promptOrder.strategy) {
 | |
|             const dummyCharacter = { id: this.configuration.promptOrder.dummyId };
 | |
|             const promptOrder = this.getPromptOrderForCharacter(dummyCharacter);
 | |
| 
 | |
|             if (0 === promptOrder.length) this.addPromptOrderForCharacter(dummyCharacter, promptManagerDefaultPromptOrder);
 | |
|         }
 | |
| 
 | |
|         // Check whether the referenced prompts are present.
 | |
|         this.serviceSettings.prompts.length === 0
 | |
|             ? this.setPrompts(chatCompletionDefaultPrompts.prompts)
 | |
|             : this.checkForMissingPrompts(this.serviceSettings.prompts);
 | |
| 
 | |
|         // Add identifiers if there are none assigned to a prompt
 | |
|         this.serviceSettings.prompts.forEach(prompt => prompt && (prompt.identifier = prompt.identifier ?? this.getUuidv4()));
 | |
| 
 | |
|         if (this.activeCharacter) {
 | |
|             const promptReferences = this.getPromptOrderForCharacter(this.activeCharacter);
 | |
|             for (let i = promptReferences.length - 1; i >= 0; i--) {
 | |
|                 const reference = promptReferences[i];
 | |
|                 if (reference && -1 === this.serviceSettings.prompts.findIndex(prompt => prompt.identifier === reference.identifier)) {
 | |
|                     promptReferences.splice(i, 1);
 | |
|                     this.log('Removed unused reference: ' + reference.identifier);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks whether entries of a characters prompt order are orphaned
 | |
|      * and if all mandatory system prompts for a character are present.
 | |
|      *
 | |
|      * @param prompts
 | |
|      */
 | |
|     checkForMissingPrompts(prompts) {
 | |
|         const defaultPromptIdentifiers = chatCompletionDefaultPrompts.prompts.reduce((list, prompt) => { list.push(prompt.identifier); return list; }, []);
 | |
| 
 | |
|         const missingIdentifiers = defaultPromptIdentifiers.filter(identifier =>
 | |
|             !prompts.some(prompt => prompt.identifier === identifier),
 | |
|         );
 | |
| 
 | |
|         missingIdentifiers.forEach(identifier => {
 | |
|             const defaultPrompt = chatCompletionDefaultPrompts.prompts.find(prompt => prompt?.identifier === identifier);
 | |
|             if (defaultPrompt) {
 | |
|                 prompts.push(defaultPrompt);
 | |
|                 this.log(`Missing system prompt: ${defaultPrompt.identifier}. Added default.`);
 | |
|             }
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check whether a prompt can be inspected.
 | |
|      * @param {object} prompt - The prompt to check.
 | |
|      * @returns {boolean} True if the prompt is a marker, false otherwise.
 | |
|      */
 | |
|     isPromptInspectionAllowed(prompt) {
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check whether a prompt can be deleted. System prompts cannot be deleted.
 | |
|      * @param {object} prompt - The prompt to check.
 | |
|      * @returns {boolean} True if the prompt can be deleted, false otherwise.
 | |
|      */
 | |
|     isPromptDeletionAllowed(prompt) {
 | |
|         return false === prompt.system_prompt;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check whether a prompt can be edited.
 | |
|      * @param {object} prompt - The prompt to check.
 | |
|      * @returns {boolean} True if the prompt can be edited, false otherwise.
 | |
|      */
 | |
|     isPromptEditAllowed(prompt) {
 | |
|         const forceEditPrompts = [
 | |
|             'charDescription',
 | |
|             'charPersonality',
 | |
|             'scenario',
 | |
|             'personaDescription',
 | |
|             'worldInfoBefore',
 | |
|             'worldInfoAfter',
 | |
|         ];
 | |
|         return forceEditPrompts.includes(prompt.identifier) || !prompt.marker;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Check whether a prompt can be toggled on or off.
 | |
|      * @param {object} prompt - The prompt to check.
 | |
|      * @returns {boolean} True if the prompt can be deleted, false otherwise.
 | |
|      */
 | |
|     isPromptToggleAllowed(prompt) {
 | |
|         const forceTogglePrompts = [
 | |
|             'charDescription',
 | |
|             'charPersonality',
 | |
|             'scenario',
 | |
|             'personaDescription',
 | |
|             'worldInfoBefore',
 | |
|             'worldInfoAfter',
 | |
|             'main',
 | |
|             'chatHistory',
 | |
|             'dialogueExamples',
 | |
|         ];
 | |
|         return prompt.marker && !forceTogglePrompts.includes(prompt.identifier) ? false : !this.configuration.toggleDisabled.includes(prompt.identifier);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Handle the deletion of a character by removing their prompt list and nullifying the active character if it was the one deleted.
 | |
|      * @param {object} event - The event object containing the character's ID.
 | |
|      * @returns void
 | |
|      */
 | |
|     handleCharacterDeleted(event) {
 | |
|         if ('global' === this.configuration.promptOrder.strategy) return;
 | |
|         this.removePromptOrderForCharacter(this.activeCharacter);
 | |
|         if (this.activeCharacter.id === event.detail.id) this.activeCharacter = null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Handle the selection of a character by setting them as the active character and setting up their prompt list if necessary.
 | |
|      * @param {object} event - The event object containing the character's ID and character data.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     handleCharacterSelected(event) {
 | |
|         if ('global' === this.configuration.promptOrder.strategy) {
 | |
|             this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
 | |
|         } else if ('character' === this.configuration.promptOrder.strategy) {
 | |
|             console.log('FOO');
 | |
|             this.activeCharacter = { id: event.detail.id, ...event.detail.character };
 | |
|             const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
 | |
| 
 | |
|             // ToDo: These should be passed as parameter or attached to the manager as a set of default options.
 | |
|             // Set default prompts and order for character.
 | |
|             if (0 === promptOrder.length) this.addPromptOrderForCharacter(this.activeCharacter, promptManagerDefaultPromptOrder);
 | |
|         } else {
 | |
|             throw new Error('Unsupported prompt order mode.');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set the most recently selected character
 | |
|      *
 | |
|      * @param event
 | |
|      */
 | |
|     handleCharacterUpdated(event) {
 | |
|         if ('global' === this.configuration.promptOrder.strategy) {
 | |
|             this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
 | |
|         } else if ('character' === this.configuration.promptOrder.strategy) {
 | |
|             this.activeCharacter = { id: event.detail.id, ...event.detail.character };
 | |
|         } else {
 | |
|             throw new Error('Prompt order strategy not supported.');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set the most recently selected character group
 | |
|      *
 | |
|      * @param event
 | |
|      */
 | |
|     handleGroupSelected(event) {
 | |
|         if ('global' === this.configuration.promptOrder.strategy) {
 | |
|             this.activeCharacter = { id: this.configuration.promptOrder.dummyId };
 | |
|         } else if ('character' === this.configuration.promptOrder.strategy) {
 | |
|             const characterDummy = { id: event.detail.id, group: event.detail.group };
 | |
|             this.activeCharacter = characterDummy;
 | |
|             const promptOrder = this.getPromptOrderForCharacter(characterDummy);
 | |
| 
 | |
|             if (0 === promptOrder.length) this.addPromptOrderForCharacter(characterDummy, promptManagerDefaultPromptOrder);
 | |
|         } else {
 | |
|             throw new Error('Prompt order strategy not supported.');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get a list of group characters, regardless of whether they are active or not.
 | |
|      *
 | |
|      * @returns {string[]}
 | |
|      */
 | |
|     getActiveGroupCharacters() {
 | |
|         // ToDo: Ideally, this should return the actual characters.
 | |
|         return (this.activeCharacter?.group?.members || []).map(member => member && member.substring(0, member.lastIndexOf('.')));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get the prompts for a specific character. Can be filtered to only include enabled prompts.
 | |
|      * @returns {object[]} The prompts for the character.
 | |
|      * @param character
 | |
|      * @param onlyEnabled
 | |
|      */
 | |
|     getPromptsForCharacter(character, onlyEnabled = false) {
 | |
|         return this.getPromptOrderForCharacter(character)
 | |
|             .map(item => true === onlyEnabled ? (true === item.enabled ? this.getPromptById(item.identifier) : null) : this.getPromptById(item.identifier))
 | |
|             .filter(prompt => null !== prompt);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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.
 | |
|      * @param {object|null} character - The character to get the prompt list for.
 | |
|      * @returns {object[]} The prompt list for the character, or an empty array.
 | |
|      */
 | |
|     getPromptOrderForCharacter(character) {
 | |
|         return !character ? [] : (this.serviceSettings.prompt_order.find(list => String(list.character_id) === String(character.id))?.order ?? []);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set the prompts for the manager.
 | |
|      * @param {object[]} prompts - The prompts to be set.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     setPrompts(prompts) {
 | |
|         this.serviceSettings.prompts = prompts;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Remove the prompt list for a specific character.
 | |
|      * @param {object} character - The character whose prompt list will be removed.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     removePromptOrderForCharacter(character) {
 | |
|         const index = this.serviceSettings.prompt_order.findIndex(list => String(list.character_id) === String(character.id));
 | |
|         if (-1 !== index) this.serviceSettings.prompt_order.splice(index, 1);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Adds a new prompt list for a specific character.
 | |
|      * @param {Object} character - Object with at least an `id` property
 | |
|      * @param {Array<Object>} promptOrder - Array of prompt objects
 | |
|      */
 | |
|     addPromptOrderForCharacter(character, promptOrder) {
 | |
|         this.serviceSettings.prompt_order.push({
 | |
|             character_id: character.id,
 | |
|             order: JSON.parse(JSON.stringify(promptOrder)),
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Searches for a prompt list entry for a given character and identifier.
 | |
|      * @param {Object} character - Character object
 | |
|      * @param {string} identifier - Identifier of the prompt list entry
 | |
|      * @returns {Object|null} The prompt list entry object, or null if not found
 | |
|      */
 | |
|     getPromptOrderEntry(character, identifier) {
 | |
|         return this.getPromptOrderForCharacter(character).find(entry => entry.identifier === identifier) ?? null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Finds and returns a prompt by its identifier.
 | |
|      * @param {string} identifier - Identifier of the prompt
 | |
|      * @returns {Object|null} The prompt object, or null if not found
 | |
|      */
 | |
|     getPromptById(identifier) {
 | |
|         return this.serviceSettings.prompts.find(item => item && item.identifier === identifier) ?? null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Finds and returns the index of a prompt by its identifier.
 | |
|      * @param {string} identifier - Identifier of the prompt
 | |
|      * @returns {number|null} Index of the prompt, or null if not found
 | |
|      */
 | |
|     getPromptIndexById(identifier) {
 | |
|         return this.serviceSettings.prompts.findIndex(item => item.identifier === identifier) ?? null;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Enriches a generic object, creating a new prompt object in the process
 | |
|      *
 | |
|      * @param {Object} prompt - Prompt object
 | |
|      * @param original
 | |
|      * @returns {Object} An object with "role" and "content" properties
 | |
|      */
 | |
|     preparePrompt(prompt, original = null) {
 | |
|         const groupMembers = this.getActiveGroupCharacters();
 | |
|         const preparedPrompt = new Prompt(prompt);
 | |
| 
 | |
|         if (typeof original === 'string') {
 | |
|             if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null, original, groupMembers.join(', '));
 | |
|             else preparedPrompt.content = substituteParams(prompt.content, null, null, original);
 | |
|         } else {
 | |
|             if (0 < groupMembers.length) preparedPrompt.content = substituteParams(prompt.content ?? '', null, null, null, groupMembers.join(', '));
 | |
|             else preparedPrompt.content = substituteParams(prompt.content);
 | |
|         }
 | |
| 
 | |
|         return preparedPrompt;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Factory function for creating a QuickEdit object associated with a prompt element.
 | |
|      *
 | |
|      * The QuickEdit object provides methods to synchronize an input element's value with a prompt's content
 | |
|      * and handle input events to update the prompt content.
 | |
|      *
 | |
|      */
 | |
|     createQuickEdit(identifier, title) {
 | |
|         const prompt = this.getPromptById(identifier);
 | |
|         const textareaIdentifier = `${identifier}_prompt_quick_edit_textarea`;
 | |
|         const html = `<div class="range-block m-t-1">
 | |
|                         <div class="justifyLeft" data-i18n="${title}">${title}</div>
 | |
|                         <div class="wide100p">
 | |
|                             <textarea id="${textareaIdentifier}" class="text_pole textarea_compact" rows="6" placeholder="">${prompt.content}</textarea>
 | |
|                         </div>
 | |
|                     </div>`;
 | |
| 
 | |
|         const quickEditContainer = document.getElementById('quick-edit-container');
 | |
|         quickEditContainer.insertAdjacentHTML('afterbegin', html);
 | |
| 
 | |
|         const debouncedSaveServiceSettings = debouncePromise(() => this.saveServiceSettings(), 300);
 | |
| 
 | |
|         const textarea = document.getElementById(textareaIdentifier);
 | |
|         textarea.addEventListener('blur', () => {
 | |
|             prompt.content = textarea.value;
 | |
|             this.updatePromptByIdentifier(identifier, prompt);
 | |
|             debouncedSaveServiceSettings().then(() => this.render());
 | |
|         });
 | |
| 
 | |
|     }
 | |
| 
 | |
|     updateQuickEdit(identifier, prompt) {
 | |
|         const elementId = `${identifier}_prompt_quick_edit_textarea`;
 | |
|         const textarea = document.getElementById(elementId);
 | |
|         textarea.value = prompt.content;
 | |
| 
 | |
|         return elementId;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Checks if a given name is accepted by OpenAi API
 | |
|      * @link https://platform.openai.com/docs/api-reference/chat/create
 | |
|      *
 | |
|      * @param name
 | |
|      * @returns {boolean}
 | |
|      */
 | |
|     isValidName(name) {
 | |
|         const regex = /^[a-zA-Z0-9_]{1,64}$/;
 | |
| 
 | |
|         return regex.test(name);
 | |
|     }
 | |
| 
 | |
|     sanitizeName(name) {
 | |
|         return name.replace(/[^a-zA-Z0-9_]/g, '_').substring(0, 64);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Loads a given prompt into the edit form fields.
 | |
|      * @param {Object} prompt - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
 | |
|      */
 | |
|     loadPromptIntoEditForm(prompt) {
 | |
|         const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');
 | |
|         const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');
 | |
|         const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
 | |
|         const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
 | |
|         const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
 | |
|         const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
 | |
|         const forbidOverridesField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides');
 | |
|         const forbidOverridesBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block');
 | |
| 
 | |
|         nameField.value = prompt.name ?? '';
 | |
|         roleField.value = prompt.role || 'system';
 | |
|         promptField.value = prompt.content ?? '';
 | |
|         promptField.disabled = prompt.marker ?? false;
 | |
|         injectionPositionField.value = prompt.injection_position ?? INJECTION_POSITION.RELATIVE;
 | |
|         injectionDepthField.value = prompt.injection_depth ?? DEFAULT_DEPTH;
 | |
|         injectionDepthBlock.style.visibility = prompt.injection_position === INJECTION_POSITION.ABSOLUTE ? 'visible' : 'hidden';
 | |
|         injectionPositionField.removeAttribute('disabled');
 | |
|         forbidOverridesField.checked = prompt.forbid_overrides ?? false;
 | |
|         forbidOverridesBlock.style.visibility = this.overridablePrompts.includes(prompt.identifier) ? 'visible' : 'hidden';
 | |
| 
 | |
|         if (this.systemPrompts.includes(prompt.identifier)) {
 | |
|             injectionPositionField.setAttribute('disabled', 'disabled');
 | |
|         }
 | |
| 
 | |
|         const resetPromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_reset');
 | |
|         if (true === prompt.system_prompt) {
 | |
|             resetPromptButton.style.display = 'block';
 | |
|             resetPromptButton.dataset.pmPrompt = prompt.identifier;
 | |
|         } else {
 | |
|             resetPromptButton.style.display = 'none';
 | |
|         }
 | |
| 
 | |
|         injectionPositionField.removeEventListener('change', (e) => this.handleInjectionPositionChange(e));
 | |
|         injectionPositionField.addEventListener('change', (e) => this.handleInjectionPositionChange(e));
 | |
| 
 | |
|         const savePromptButton = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_save');
 | |
|         savePromptButton.dataset.pmPrompt = prompt.identifier;
 | |
|     }
 | |
| 
 | |
|     handleInjectionPositionChange(event) {
 | |
|         const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
 | |
|         const injectionPosition = Number(event.target.value);
 | |
|         if (injectionPosition === INJECTION_POSITION.ABSOLUTE) {
 | |
|             injectionDepthBlock.style.visibility = 'visible';
 | |
|         } else {
 | |
|             injectionDepthBlock.style.visibility = 'hidden';
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Loads a given prompt into the inspect form
 | |
|      * @param {MessageCollection} messages - Prompt object with properties 'name', 'role', 'content', and 'system_prompt'
 | |
|      */
 | |
|     loadMessagesIntoInspectForm(messages) {
 | |
|         if (!messages) return;
 | |
| 
 | |
|         const createInlineDrawer = (message) => {
 | |
|             const truncatedTitle = message.content.length > 32 ? message.content.slice(0, 32) + '...' : message.content;
 | |
|             const title = message.identifier || truncatedTitle;
 | |
|             const role = message.role;
 | |
|             const content = message.content || 'No Content';
 | |
|             const tokens = message.getTokens();
 | |
| 
 | |
|             let drawerHTML = `
 | |
|         <div class="inline-drawer ${this.configuration.prefix}prompt_manager_prompt">
 | |
|             <div class="inline-drawer-toggle inline-drawer-header">
 | |
|                 <span>Name: ${escapeHtml(title)}, Role: ${role}, Tokens: ${tokens}</span>
 | |
|                 <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
 | |
|             </div>
 | |
|             <div class="inline-drawer-content" style="white-space: pre-wrap;">${escapeHtml(content)}</div>
 | |
|         </div>
 | |
|         `;
 | |
| 
 | |
|             let template = document.createElement('template');
 | |
|             template.innerHTML = drawerHTML.trim();
 | |
|             return template.content.firstChild;
 | |
|         };
 | |
| 
 | |
|         const messageList = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_inspect_list');
 | |
| 
 | |
|         const messagesCollection = messages instanceof Message ? [messages] : messages.getCollection();
 | |
| 
 | |
|         if (0 === messagesCollection.length) messageList.innerHTML = '<span>This marker does not contain any prompts.</span>';
 | |
| 
 | |
|         messagesCollection.forEach(message => {
 | |
|             messageList.append(createInlineDrawer(message));
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Clears all input fields in the edit form.
 | |
|      */
 | |
|     clearEditForm() {
 | |
|         const editArea = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_edit');
 | |
|         editArea.style.display = 'none';
 | |
| 
 | |
|         const nameField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_name');
 | |
|         const roleField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_role');
 | |
|         const promptField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_prompt');
 | |
|         const injectionPositionField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position');
 | |
|         const injectionDepthField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_depth');
 | |
|         const injectionDepthBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_depth_block');
 | |
|         const forbidOverridesField = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_forbid_overrides');
 | |
|         const forbidOverridesBlock = document.getElementById(this.configuration.prefix + 'prompt_manager_forbid_overrides_block');
 | |
| 
 | |
|         nameField.value = '';
 | |
|         roleField.selectedIndex = 0;
 | |
|         promptField.value = '';
 | |
|         promptField.disabled = false;
 | |
|         injectionPositionField.selectedIndex = 0;
 | |
|         injectionPositionField.removeAttribute('disabled');
 | |
|         injectionDepthField.value = DEFAULT_DEPTH;
 | |
|         injectionDepthBlock.style.visibility = 'unset';
 | |
|         forbidOverridesBlock.style.visibility = 'unset';
 | |
|         forbidOverridesField.checked = false;
 | |
| 
 | |
|         roleField.disabled = false;
 | |
|     }
 | |
| 
 | |
|     clearInspectForm() {
 | |
|         const inspectArea = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_inspect');
 | |
|         inspectArea.style.display = 'none';
 | |
|         const messageList = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_inspect_list');
 | |
|         messageList.innerHTML = '';
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns a full list of prompts whose content markers have been substituted.
 | |
|      * @returns {PromptCollection} A PromptCollection object
 | |
|      */
 | |
|     getPromptCollection() {
 | |
|         const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
 | |
| 
 | |
|         const promptCollection = new PromptCollection();
 | |
|         promptOrder.forEach(entry => {
 | |
|             if (true === entry.enabled) {
 | |
|                 const prompt = this.getPromptById(entry.identifier);
 | |
|                 if (prompt) promptCollection.add(this.preparePrompt(prompt));
 | |
|             } else if (!entry.enabled && entry.identifier === 'main') {
 | |
|                 // Some extensions require main prompt to be present for relative inserts.
 | |
|                 // So we make a GMO-free vegan replacement.
 | |
|                 const prompt = structuredClone(this.getPromptById(entry.identifier));
 | |
|                 prompt.content = '';
 | |
|                 if (prompt) promptCollection.add(this.preparePrompt(prompt));
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         return promptCollection;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Setter for messages property
 | |
|      *
 | |
|      * @param {import('./openai.js').MessageCollection} messages
 | |
|      */
 | |
|     setMessages(messages) {
 | |
|         this.messages = messages;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set and process a finished chat completion object
 | |
|      *
 | |
|      * @param {import('./openai.js').ChatCompletion} chatCompletion
 | |
|      */
 | |
|     setChatCompletion(chatCompletion) {
 | |
|         const messages = chatCompletion.getMessages();
 | |
| 
 | |
|         this.setMessages(messages);
 | |
|         this.populateTokenCounts(messages);
 | |
|         this.overriddenPrompts = chatCompletion.getOverriddenPrompts();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Populates the token handler
 | |
|      *
 | |
|      * @param {import('./openai.js').MessageCollection} messages
 | |
|      */
 | |
|     populateTokenCounts(messages) {
 | |
|         this.tokenHandler.resetCounts();
 | |
|         const counts = this.tokenHandler.getCounts();
 | |
|         messages.getCollection().forEach(message => {
 | |
|             counts[message.identifier] = message.getTokens();
 | |
|         });
 | |
| 
 | |
|         this.tokenUsage = this.tokenHandler.getTotal();
 | |
| 
 | |
|         this.log('Updated token usage with ' + this.tokenUsage);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Empties, then re-assembles the container containing the prompt list.
 | |
|      */
 | |
|     async renderPromptManager() {
 | |
|         let selectedPromptIndex = 0;
 | |
|         const existingAppendSelect = document.getElementById(`${this.configuration.prefix}prompt_manager_footer_append_prompt`);
 | |
|         if (existingAppendSelect instanceof HTMLSelectElement) {
 | |
|             selectedPromptIndex = existingAppendSelect.selectedIndex;
 | |
|         }
 | |
|         const promptManagerDiv = this.containerElement;
 | |
|         promptManagerDiv.innerHTML = '';
 | |
| 
 | |
|         const errorDiv = this.error ? `
 | |
|                 <div class="${this.configuration.prefix}prompt_manager_error">
 | |
|                     <span class="fa-solid tooltip fa-triangle-exclamation text_danger"></span> ${DOMPurify.sanitize(this.error)}
 | |
|                 </div>
 | |
|         ` : '';
 | |
| 
 | |
|         const totalActiveTokens = this.tokenUsage;
 | |
| 
 | |
|         const headerHtml = await renderTemplateAsync('promptManagerHeader', { error: this.error, errorDiv, prefix: this.configuration.prefix, totalActiveTokens });
 | |
|         promptManagerDiv.insertAdjacentHTML('beforeend', headerHtml);
 | |
| 
 | |
|         this.listElement = promptManagerDiv.querySelector(`#${this.configuration.prefix}prompt_manager_list`);
 | |
| 
 | |
|         if (null !== this.activeCharacter) {
 | |
|             const prompts = [...this.serviceSettings.prompts]
 | |
|                 .filter(prompt => prompt && !prompt?.system_prompt)
 | |
|                 .sort((promptA, promptB) => promptA.name.localeCompare(promptB.name));
 | |
|             const promptsHtml = prompts.reduce((acc, prompt) => acc + `<option value="${prompt.identifier}">${escapeHtml(prompt.name)}</option>`, '');
 | |
| 
 | |
|             if (selectedPromptIndex > 0) {
 | |
|                 selectedPromptIndex = Math.min(selectedPromptIndex, prompts.length - 1);
 | |
|             }
 | |
| 
 | |
|             if (selectedPromptIndex === -1 && prompts.length) {
 | |
|                 selectedPromptIndex = 0;
 | |
|             }
 | |
| 
 | |
|             const rangeBlockDiv = promptManagerDiv.querySelector('.range-block');
 | |
|             const headerDiv = promptManagerDiv.querySelector('.completion_prompt_manager_header');
 | |
|             const footerHtml = await renderTemplateAsync('promptManagerFooter', { promptsHtml, prefix: this.configuration.prefix });
 | |
|             headerDiv.insertAdjacentHTML('afterend', footerHtml);
 | |
|             rangeBlockDiv.querySelector('#prompt-manager-reset-character').addEventListener('click', this.handleCharacterReset);
 | |
| 
 | |
|             const footerDiv = rangeBlockDiv.querySelector(`.${this.configuration.prefix}prompt_manager_footer`);
 | |
|             footerDiv.querySelector('.menu_button:nth-child(2)').addEventListener('click', this.handleAppendPrompt);
 | |
|             footerDiv.querySelector('.caution').addEventListener('click', this.handleDeletePrompt);
 | |
|             footerDiv.querySelector('.menu_button:last-child').addEventListener('click', this.handleNewPrompt);
 | |
|             footerDiv.querySelector('select').selectedIndex = selectedPromptIndex;
 | |
| 
 | |
|             // Add prompt export dialogue and options
 | |
| 
 | |
|             const exportForCharacter = await renderTemplateAsync('promptManagerExportForCharacter');
 | |
|             const exportPopup = await renderTemplateAsync('promptManagerExportPopup', { isGlobalStrategy: 'global' === this.configuration.promptOrder.strategy, exportForCharacter });
 | |
|             rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup);
 | |
| 
 | |
|             // Destroy previous popper instance if it exists
 | |
|             if (this.exportPopper) {
 | |
|                 this.exportPopper.destroy();
 | |
|             }
 | |
| 
 | |
|             this.exportPopper = Popper.createPopper(
 | |
|                 document.getElementById('prompt-manager-export'),
 | |
|                 document.getElementById('prompt-manager-export-format-popup'),
 | |
|                 { placement: 'bottom' },
 | |
|             );
 | |
| 
 | |
|             const showExportSelection = () => {
 | |
|                 const popup = document.getElementById('prompt-manager-export-format-popup');
 | |
|                 const show = popup.hasAttribute('data-show');
 | |
| 
 | |
|                 if (show) popup.removeAttribute('data-show');
 | |
|                 else popup.setAttribute('data-show', '');
 | |
| 
 | |
|                 this.exportPopper.update();
 | |
|             };
 | |
| 
 | |
|             footerDiv.querySelector('#prompt-manager-import').addEventListener('click', this.handleImport);
 | |
|             footerDiv.querySelector('#prompt-manager-export').addEventListener('click', showExportSelection);
 | |
|             rangeBlockDiv.querySelector('.export-promptmanager-prompts-full').addEventListener('click', this.handleFullExport);
 | |
|             rangeBlockDiv.querySelector('.export-promptmanager-prompts-character')?.addEventListener('click', this.handleCharacterExport);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Empties, then re-assembles the prompt list
 | |
|      */
 | |
|     async renderPromptManagerListItems() {
 | |
|         if (!this.serviceSettings.prompts) return;
 | |
| 
 | |
|         const promptManagerList = this.listElement;
 | |
|         promptManagerList.innerHTML = '';
 | |
| 
 | |
|         const { prefix } = this.configuration;
 | |
| 
 | |
|         let listItemHtml = await renderTemplateAsync('promptManagerListHeader', { prefix });
 | |
| 
 | |
|         this.getPromptsForCharacter(this.activeCharacter).forEach(prompt => {
 | |
|             if (!prompt) return;
 | |
| 
 | |
|             const listEntry = this.getPromptOrderEntry(this.activeCharacter, prompt.identifier);
 | |
|             const enabledClass = listEntry.enabled ? '' : `${prefix}prompt_manager_prompt_disabled`;
 | |
|             const draggableClass = `${prefix}prompt_manager_prompt_draggable`;
 | |
|             const markerClass = prompt.marker ? `${prefix}prompt_manager_marker` : '';
 | |
|             const tokens = this.tokenHandler?.getCounts()[prompt.identifier] ?? 0;
 | |
| 
 | |
|             // Warn the user if the chat history goes below certain token thresholds.
 | |
|             let warningClass = '';
 | |
|             let warningTitle = '';
 | |
| 
 | |
|             const tokenBudget = this.serviceSettings.openai_max_context - this.serviceSettings.openai_max_tokens;
 | |
|             if (this.tokenUsage > tokenBudget * 0.8 &&
 | |
|                 'chatHistory' === prompt.identifier) {
 | |
|                 const warningThreshold = this.configuration.warningTokenThreshold;
 | |
|                 const dangerThreshold = this.configuration.dangerTokenThreshold;
 | |
| 
 | |
|                 if (tokens <= dangerThreshold) {
 | |
|                     warningClass = 'fa-solid tooltip fa-triangle-exclamation text_danger';
 | |
|                     warningTitle = 'Very little of your chat history is being sent, consider deactivating some other prompts.';
 | |
|                 } else if (tokens <= warningThreshold) {
 | |
|                     warningClass = 'fa-solid tooltip fa-triangle-exclamation text_warning';
 | |
|                     warningTitle = 'Only a few messages worth chat history are being sent.';
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             const calculatedTokens = tokens ? tokens : '-';
 | |
| 
 | |
|             let detachSpanHtml = '';
 | |
|             if (this.isPromptDeletionAllowed(prompt)) {
 | |
|                 detachSpanHtml = `
 | |
|                     <span title="Remove" class="prompt-manager-detach-action caution fa-solid fa-chain-broken fa-xs"></span>
 | |
|                 `;
 | |
|             } else {
 | |
|                 detachSpanHtml = '<span class="fa-solid"></span>';
 | |
|             }
 | |
| 
 | |
|             let editSpanHtml = '';
 | |
|             if (this.isPromptEditAllowed(prompt)) {
 | |
|                 editSpanHtml = `
 | |
|                     <span title="edit" class="prompt-manager-edit-action fa-solid fa-pencil fa-xs"></span>
 | |
|                 `;
 | |
|             } else {
 | |
|                 editSpanHtml = '<span class="fa-solid"></span>';
 | |
|             }
 | |
| 
 | |
|             let toggleSpanHtml = '';
 | |
|             if (this.isPromptToggleAllowed(prompt)) {
 | |
|                 toggleSpanHtml = `
 | |
|                     <span class="prompt-manager-toggle-action ${listEntry.enabled ? 'fa-solid fa-toggle-on' : 'fa-solid fa-toggle-off'}"></span>
 | |
|                 `;
 | |
|             } else {
 | |
|                 toggleSpanHtml = '<span class="fa-solid"></span>';
 | |
|             }
 | |
| 
 | |
|             const encodedName = escapeHtml(prompt.name);
 | |
|             const isMarkerPrompt = prompt.marker && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
 | |
|             const isSystemPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE && !prompt.forbid_overrides;
 | |
|             const isImportantPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE && prompt.forbid_overrides;
 | |
|             const isUserPrompt = !prompt.marker && !prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE;
 | |
|             const isInjectionPrompt = prompt.injection_position === INJECTION_POSITION.ABSOLUTE;
 | |
|             const isOverriddenPrompt = Array.isArray(this.overriddenPrompts) && this.overriddenPrompts.includes(prompt.identifier);
 | |
|             const importantClass = isImportantPrompt ? `${prefix}prompt_manager_important` : '';
 | |
|             const iconLookup = prompt.role === 'system' && (prompt.marker || prompt.system_prompt) ? '' : prompt.role;
 | |
| 
 | |
|             //add role icons to the right of prompt name
 | |
|             const promptRoles = {
 | |
|                 assistant: { roleIcon: 'fa-robot', roleTitle: 'Prompt will be sent as Assistant' },
 | |
|                 user: { roleIcon: 'fa-user', roleTitle: 'Prompt will be sent as User' },
 | |
|             };
 | |
|             const roleIcon = promptRoles[iconLookup]?.roleIcon || '';
 | |
|             const roleTitle = promptRoles[iconLookup]?.roleTitle || '';
 | |
| 
 | |
|             listItemHtml += `
 | |
|                 <li class="${prefix}prompt_manager_prompt ${draggableClass} ${enabledClass} ${markerClass} ${importantClass}" data-pm-identifier="${escapeHtml(prompt.identifier)}">
 | |
|                     <span class="drag-handle">☰</span>
 | |
|                     <span class="${prefix}prompt_manager_prompt_name" data-pm-name="${encodedName}">
 | |
|                         ${isMarkerPrompt ? '<span class="fa-fw fa-solid fa-thumb-tack" title="Marker"></span>' : ''}
 | |
|                         ${isSystemPrompt ? '<span class="fa-fw fa-solid fa-square-poll-horizontal" title="Global Prompt"></span>' : ''}
 | |
|                         ${isImportantPrompt ? '<span class="fa-fw fa-solid fa-star" title="Important Prompt"></span>' : ''}
 | |
|                         ${isUserPrompt ? '<span class="fa-fw fa-solid fa-asterisk" title="Preset Prompt"></span>' : ''}
 | |
|                         ${isInjectionPrompt ? '<span class="fa-fw fa-solid fa-syringe" title="In-Chat Injection"></span>' : ''}
 | |
|                         ${this.isPromptInspectionAllowed(prompt) ? `<a title="${encodedName}" class="prompt-manager-inspect-action">${encodedName}</a>` : `<span title="${encodedName}">${encodedName}</span>`}
 | |
|                         ${roleIcon ? `<span data-role="${escapeHtml(prompt.role)}" class="fa-xs fa-solid ${roleIcon}" title="${roleTitle}"></span>` : ''}
 | |
|                         ${isInjectionPrompt ? `<small class="prompt-manager-injection-depth">@ ${escapeHtml(prompt.injection_depth)}</small>` : ''}
 | |
|                         ${isOverriddenPrompt ? '<small class="fa-solid fa-address-card prompt-manager-overridden" title="Pulled from a character card"></small>' : ''}
 | |
|                     </span>
 | |
|                     <span>
 | |
|                             <span class="prompt_manager_prompt_controls">
 | |
|                                 ${detachSpanHtml}
 | |
|                                 ${editSpanHtml}
 | |
|                                 ${toggleSpanHtml}
 | |
|                             </span>
 | |
|                     </span>
 | |
| 
 | |
|                     <span class="prompt_manager_prompt_tokens" data-pm-tokens="${calculatedTokens}"><span class="${warningClass}" title="${warningTitle}"> </span>${calculatedTokens}</span>
 | |
|                 </li>
 | |
|             `;
 | |
|         });
 | |
| 
 | |
|         promptManagerList.insertAdjacentHTML('beforeend', listItemHtml);
 | |
| 
 | |
|         // Now that the new elements are in the DOM, you can add the event listeners.
 | |
|         Array.from(promptManagerList.getElementsByClassName('prompt-manager-detach-action')).forEach(el => {
 | |
|             el.addEventListener('click', this.handleDetach);
 | |
|         });
 | |
| 
 | |
|         Array.from(promptManagerList.getElementsByClassName('prompt-manager-inspect-action')).forEach(el => {
 | |
|             el.addEventListener('click', this.handleInspect);
 | |
|         });
 | |
| 
 | |
|         Array.from(promptManagerList.getElementsByClassName('prompt-manager-edit-action')).forEach(el => {
 | |
|             el.addEventListener('click', this.handleEdit);
 | |
|         });
 | |
| 
 | |
|         Array.from(promptManagerList.querySelectorAll('.prompt-manager-toggle-action')).forEach(el => {
 | |
|             el.addEventListener('click', this.handleToggle);
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Writes the passed data to a json file
 | |
|      *
 | |
|      * @param data
 | |
|      * @param type
 | |
|      * @param name
 | |
|      */
 | |
|     export(data, type, name = 'export') {
 | |
|         const promptExport = {
 | |
|             version: this.configuration.version,
 | |
|             type: type,
 | |
|             data: data,
 | |
|         };
 | |
| 
 | |
|         const serializedObject = JSON.stringify(promptExport, null, 4);
 | |
|         const blob = new Blob([serializedObject], { type: 'application/json' });
 | |
|         const url = URL.createObjectURL(blob);
 | |
|         const downloadLink = document.createElement('a');
 | |
|         downloadLink.href = url;
 | |
| 
 | |
|         const dateString = this.getFormattedDate();
 | |
|         downloadLink.download = `${name}-${dateString}.json`;
 | |
| 
 | |
|         downloadLink.click();
 | |
| 
 | |
|         URL.revokeObjectURL(url);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Imports a json file with prompts and an optional prompt list for the active character
 | |
|      *
 | |
|      * @param importData
 | |
|      */
 | |
|     import(importData) {
 | |
|         const mergeKeepNewer = (prompts, newPrompts) => {
 | |
|             let merged = [...prompts, ...newPrompts];
 | |
| 
 | |
|             let map = new Map();
 | |
|             for (let obj of merged) {
 | |
|                 map.set(obj.identifier, obj);
 | |
|             }
 | |
| 
 | |
|             merged = Array.from(map.values());
 | |
| 
 | |
|             return merged;
 | |
|         };
 | |
| 
 | |
|         const controlObj = {
 | |
|             version: 1,
 | |
|             type: '',
 | |
|             data: {
 | |
|                 prompts: [],
 | |
|                 prompt_order: null,
 | |
|             },
 | |
|         };
 | |
| 
 | |
|         if (false === this.validateObject(controlObj, importData)) {
 | |
|             toastr.warning(t`Could not import prompts. Export failed validation.`);
 | |
|             return;
 | |
|         }
 | |
| 
 | |
|         const prompts = mergeKeepNewer(this.serviceSettings.prompts, importData.data.prompts);
 | |
| 
 | |
|         this.setPrompts(prompts);
 | |
|         this.log('Prompt import succeeded');
 | |
| 
 | |
|         if ('global' === this.configuration.promptOrder.strategy) {
 | |
|             const promptOrder = this.getPromptOrderForCharacter({ id: this.configuration.promptOrder.dummyId });
 | |
|             Object.assign(promptOrder, importData.data.prompt_order);
 | |
|             this.log('Prompt order import succeeded');
 | |
|         } else if ('character' === this.configuration.promptOrder.strategy) {
 | |
|             if ('character' === importData.type) {
 | |
|                 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
 | |
|                 Object.assign(promptOrder, importData.data.prompt_order);
 | |
|                 this.log(`Prompt order import for character ${this.activeCharacter.name} succeeded`);
 | |
|             }
 | |
|         } else {
 | |
|             throw new Error('Prompt order strategy not supported.');
 | |
|         }
 | |
| 
 | |
|         toastr.success(t`Prompt import complete.`);
 | |
|         this.saveServiceSettings().then(() => this.render());
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Helper function to check whether the structure of object matches controlObj
 | |
|      *
 | |
|      * @param controlObj
 | |
|      * @param object
 | |
|      * @returns {boolean}
 | |
|      */
 | |
|     validateObject(controlObj, object) {
 | |
|         for (let key in controlObj) {
 | |
|             if (!Object.hasOwn(object, key)) {
 | |
|                 if (controlObj[key] === null) continue;
 | |
|                 else return false;
 | |
|             }
 | |
| 
 | |
|             if (typeof controlObj[key] === 'object' && controlObj[key] !== null) {
 | |
|                 if (typeof object[key] !== 'object') return false;
 | |
|                 if (!this.validateObject(controlObj[key], object[key])) return false;
 | |
|             } else {
 | |
|                 if (typeof object[key] !== typeof controlObj[key]) return false;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get current date as mm/dd/YYYY
 | |
|      *
 | |
|      * @returns {`${string}_${string}_${string}`}
 | |
|      */
 | |
|     getFormattedDate() {
 | |
|         const date = new Date();
 | |
|         let month = String(date.getMonth() + 1);
 | |
|         let day = String(date.getDate());
 | |
|         const year = String(date.getFullYear());
 | |
| 
 | |
|         if (month.length < 2) month = '0' + month;
 | |
|         if (day.length < 2) day = '0' + day;
 | |
| 
 | |
|         return `${month}_${day}_${year}`;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Makes the prompt list draggable and handles swapping of two entries in the list.
 | |
|      * @typedef {Object} Entry
 | |
|      * @property {string} identifier
 | |
|      * @returns {void}
 | |
|      */
 | |
|     makeDraggable() {
 | |
|         $(`#${this.configuration.prefix}prompt_manager_list`).sortable({
 | |
|             delay: this.configuration.sortableDelay,
 | |
|             handle: isMobile() ? '.drag-handle' : null,
 | |
|             items: `.${this.configuration.prefix}prompt_manager_prompt_draggable`,
 | |
|             update: (event, ui) => {
 | |
|                 const promptOrder = this.getPromptOrderForCharacter(this.activeCharacter);
 | |
|                 const promptListElement = $(`#${this.configuration.prefix}prompt_manager_list`).sortable('toArray', { attribute: 'data-pm-identifier' });
 | |
|                 const idToObjectMap = new Map(promptOrder.map(prompt => [prompt.identifier, prompt]));
 | |
|                 const updatedPromptOrder = promptListElement.map(identifier => idToObjectMap.get(identifier));
 | |
| 
 | |
|                 this.removePromptOrderForCharacter(this.activeCharacter);
 | |
|                 this.addPromptOrderForCharacter(this.activeCharacter, updatedPromptOrder);
 | |
| 
 | |
|                 this.log(`Prompt order updated for ${this.activeCharacter.name}.`);
 | |
| 
 | |
|                 this.saveServiceSettings();
 | |
|             },
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Slides down the edit form and adds the class 'openDrawer' to the first element of '#openai_prompt_manager_popup'.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     showPopup(area = 'edit') {
 | |
|         const areaElement = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_' + area);
 | |
|         areaElement.style.display = 'flex';
 | |
| 
 | |
|         $('#' + this.configuration.prefix + 'prompt_manager_popup').first()
 | |
|             .slideDown(200, 'swing')
 | |
|             .addClass('openDrawer');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Slides up the edit form and removes the class 'openDrawer' from the first element of '#openai_prompt_manager_popup'.
 | |
|      * @returns {void}
 | |
|      */
 | |
|     hidePopup() {
 | |
|         $('#' + this.configuration.prefix + 'prompt_manager_popup').first()
 | |
|             .slideUp(200, 'swing')
 | |
|             .removeClass('openDrawer');
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Quick uuid4 implementation
 | |
|      * @returns {string} A string representation of an uuid4
 | |
|      */
 | |
|     getUuidv4() {
 | |
|         return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
 | |
|             let r = Math.random() * 16 | 0,
 | |
|                 v = c === 'x' ? r : (r & 0x3 | 0x8);
 | |
|             return v.toString(16);
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Write to console with prefix
 | |
|      *
 | |
|      * @param output
 | |
|      */
 | |
|     log(output) {
 | |
|         if (power_user.console_log_prompts) console.log('[PromptManager] ' + output);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Start a profiling task
 | |
|      *
 | |
|      * @param identifier
 | |
|      */
 | |
|     profileStart(identifier) {
 | |
|         if (power_user.console_log_prompts) console.time(identifier);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * End a profiling task
 | |
|      *
 | |
|      * @param identifier
 | |
|      */
 | |
|     profileEnd(identifier) {
 | |
|         if (power_user.console_log_prompts) {
 | |
|             this.log('Profiling of "' + identifier + '" finished. Result below.');
 | |
|             console.timeEnd(identifier);
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| const chatCompletionDefaultPrompts = {
 | |
|     'prompts': [
 | |
|         {
 | |
|             'name': 'Main Prompt',
 | |
|             'system_prompt': true,
 | |
|             'role': 'system',
 | |
|             'content': 'Write {{char}}\'s next reply in a fictional chat between {{charIfNotGroup}} and {{user}}.',
 | |
|             'identifier': 'main',
 | |
|         },
 | |
|         {
 | |
|             'name': 'Auxiliary Prompt',
 | |
|             'system_prompt': true,
 | |
|             'role': 'system',
 | |
|             'content': '',
 | |
|             'identifier': 'nsfw',
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'dialogueExamples',
 | |
|             'name': 'Chat Examples',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|         {
 | |
|             'name': 'Post-History Instructions',
 | |
|             'system_prompt': true,
 | |
|             'role': 'system',
 | |
|             'content': '',
 | |
|             'identifier': 'jailbreak',
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'chatHistory',
 | |
|             'name': 'Chat History',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'worldInfoAfter',
 | |
|             'name': 'World Info (after)',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'worldInfoBefore',
 | |
|             'name': 'World Info (before)',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'enhanceDefinitions',
 | |
|             'role': 'system',
 | |
|             'name': 'Enhance Definitions',
 | |
|             '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.',
 | |
|             'system_prompt': true,
 | |
|             'marker': false,
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'charDescription',
 | |
|             'name': 'Char Description',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'charPersonality',
 | |
|             'name': 'Char Personality',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'scenario',
 | |
|             'name': 'Scenario',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|         {
 | |
|             'identifier': 'personaDescription',
 | |
|             'name': 'Persona Description',
 | |
|             'system_prompt': true,
 | |
|             'marker': true,
 | |
|         },
 | |
|     ],
 | |
| };
 | |
| 
 | |
| const promptManagerDefaultPromptOrders = {
 | |
|     'prompt_order': [],
 | |
| };
 | |
| 
 | |
| const promptManagerDefaultPromptOrder = [
 | |
|     {
 | |
|         'identifier': 'main',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'worldInfoBefore',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'personaDescription',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'charDescription',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'charPersonality',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'scenario',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'enhanceDefinitions',
 | |
|         'enabled': false,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'nsfw',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'worldInfoAfter',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'dialogueExamples',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'chatHistory',
 | |
|         'enabled': true,
 | |
|     },
 | |
|     {
 | |
|         'identifier': 'jailbreak',
 | |
|         'enabled': true,
 | |
|     },
 | |
| ];
 | |
| 
 | |
| export {
 | |
|     PromptManager,
 | |
|     registerPromptManagerMigration,
 | |
|     chatCompletionDefaultPrompts,
 | |
|     promptManagerDefaultPromptOrders,
 | |
|     Prompt,
 | |
| };
 |