'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'; 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} 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 = `
${title}
`; 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 = `
Name: ${escapeHtml(title)}, Role: ${role}, Tokens: ${tokens}
${escapeHtml(content)}
`; 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 = 'This marker does not contain any prompts.'; 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 ? `
${DOMPurify.sanitize(this.error)}
` : ''; 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 + ``, ''); 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 = ` `; } else { detachSpanHtml = ''; } let editSpanHtml = ''; if (this.isPromptEditAllowed(prompt)) { editSpanHtml = ` `; } else { editSpanHtml = ''; } let toggleSpanHtml = ''; if (this.isPromptToggleAllowed(prompt)) { toggleSpanHtml = ` `; } else { toggleSpanHtml = ''; } 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` : ''; listItemHtml += `
  • ${isMarkerPrompt ? '' : ''} ${isSystemPrompt ? '' : ''} ${isImportantPrompt ? '' : ''} ${isUserPrompt ? '' : ''} ${isInjectionPrompt ? '' : ''} ${this.isPromptInspectionAllowed(prompt) ? `${encodedName}` : `${encodedName}`} ${isInjectionPrompt ? `@ ${prompt.injection_depth}` : ''} ${isOverriddenPrompt ? '' : ''} ${detachSpanHtml} ${editSpanHtml} ${toggleSpanHtml} ${calculatedTokens}
  • `; }); 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, 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, };