diff --git a/public/scripts/PromptManager.js b/public/scripts/PromptManager.js index 52faeb873..e3f4db253 100644 --- a/public/scripts/PromptManager.js +++ b/public/scripts/PromptManager.js @@ -178,1591 +178,1594 @@ class PromptCollection { } } -function PromptManagerModule() { - this.systemPrompts = [ - 'main', - 'nsfw', - 'jailbreak', - 'enhanceDefinitions', - ]; +class PromptManager { + constructor() { + this.systemPrompts = [ + 'main', + 'nsfw', + 'jailbreak', + 'enhanceDefinitions', + ]; - 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: '', - }, - }; + 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; + // Chatcompletion configuration object + this.serviceSettings = null; - // DOM element containing the prompt manager - this.containerElement = null; + // DOM element containing the prompt manager + this.containerElement = null; - // DOM element containing the prompt list - this.listElement = null; + // DOM element containing the prompt list + this.listElement = null; - // Currently selected character - this.activeCharacter = null; + // Currently selected character + this.activeCharacter = null; - // Message collection of the most recent chatcompletion - this.messages = null; + // Message collection of the most recent chatcompletion + this.messages = null; - // The current token handler instance - this.tokenHandler = null; + // The current token handler instance + this.tokenHandler = null; - // Token usage of last dry run - this.tokenUsage = 0; + // Token usage of last dry run + this.tokenUsage = 0; - // Error state, contains error message. - this.error = null; + // Error state, contains error message. + this.error = null; - /** Dry-run for generate, must return a promise */ - this.tryGenerate = () => { }; + /** Dry-run for generate, must return a promise */ + this.tryGenerate = () => { }; - /** Called to persist the configuration, must return a promise */ - this.saveServiceSettings = () => { }; + /** Called to persist the configuration, must return a promise */ + this.saveServiceSettings = () => { }; - /** Toggle prompt button click */ - this.handleToggle = () => { }; + /** Toggle prompt button click */ + this.handleToggle = () => { }; - /** Prompt name click */ - this.handleInspect = () => { }; + /** Prompt name click */ + this.handleInspect = () => { }; - /** Edit prompt button click */ - this.handleEdit = () => { }; + /** Edit prompt button click */ + this.handleEdit = () => { }; - /** Detach prompt button click */ - this.handleDetach = () => { }; + /** Detach prompt button click */ + this.handleDetach = () => { }; - /** Save prompt button click */ - this.handleSavePrompt = () => { }; + /** Save prompt button click */ + this.handleSavePrompt = () => { }; - /** Reset prompt button click */ - this.handleResetPrompt = () => { }; + /** Reset prompt button click */ + this.handleResetPrompt = () => { }; - /** New prompt button click */ - this.handleNewPrompt = () => { }; + /** New prompt button click */ + this.handleNewPrompt = () => { }; - /** Delete prompt button click */ - this.handleDeletePrompt = () => { }; + /** Delete prompt button click */ + this.handleDeletePrompt = () => { }; - /** Append prompt button click */ - this.handleAppendPrompt = () => { }; + /** Append prompt button click */ + this.handleAppendPrompt = () => { }; - /** Import button click */ - this.handleImport = () => { }; + /** Import button click */ + this.handleImport = () => { }; - /** Full export click */ - this.handleFullExport = () => { }; + /** Full export click */ + this.handleFullExport = () => { }; - /** Character export click */ - this.handleCharacterExport = () => { }; + /** Character export click */ + this.handleCharacterExport = () => { }; - /** Character reset button click*/ - this.handleCharacterReset = () => { }; + /** Character reset button click*/ + this.handleCharacterReset = () => { }; - /** Debounced version of render */ - this.renderDebounced = debounce(this.render.bind(this), 1000); -} + /** Debounced version of render */ + this.renderDebounced = debounce(this.render.bind(this), 1000); + } -/** - * Initializes the PromptManagerModule 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 PromptManagerModule. - * @param {Object} serviceSettings - Service settings object for the PromptManagerModule. - */ -PromptManagerModule.prototype.init = function (moduleConfiguration, serviceSettings) { - this.configuration = Object.assign(this.configuration, moduleConfiguration); - this.tokenHandler = this.tokenHandler || new TokenHandler(); - this.serviceSettings = serviceSettings; - this.containerElement = document.getElementById(this.configuration.containerIdentifier); - if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = { id: this.configuration.promptOrder.dummyId }; + /** + * 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(); + this.serviceSettings = serviceSettings; + this.containerElement = document.getElementById(this.configuration.containerIdentifier); - this.sanitizeServiceSettings(); + if ('global' === this.configuration.promptOrder.strategy) this.activeCharacter = { id: this.configuration.promptOrder.dummyId }; - // 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(); + this.sanitizeServiceSettings(); - counts[promptID] = null; - promptOrderEntry.enabled = !promptOrderEntry.enabled; - this.saveServiceSettings().then(() => this.render()); - }; + // 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(); - // 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.saveServiceSettings().then(() => this.render()); - }; - - // 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.saveServiceSettings().then(() => this.render()); - }; - - // 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; - 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; - 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'; - - 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); + counts[promptID] = null; + promptOrderEntry.enabled = !promptOrderEntry.enabled; this.saveServiceSettings().then(() => this.render()); - } - }; + }; - // Delete selected prompt from list form and close edit form - this.handleDeletePrompt = (event) => { - const promptID = document.getElementById(this.configuration.prefix + 'prompt_manager_footer_append_prompt').value; - const prompt = this.getPromptById(promptID); + // Open edit form and load selected prompt + this.handleEdit = (event) => { + this.clearEditForm(); + this.clearInspectForm(); - if (prompt && true === this.isPromptDeletionAllowed(prompt)) { - const promptIndex = this.getPromptIndexById(promptID); - this.serviceSettings.prompts.splice(Number(promptIndex), 1); + const promptID = event.target.closest('.' + this.configuration.prefix + 'prompt_manager_prompt').dataset.pmIdentifier; + const prompt = this.getPromptById(promptID); - this.log('Deleted prompt: ' + prompt.identifier); + 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.saveServiceSettings().then(() => this.render()); + }; + + // 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.saveServiceSettings().then(() => this.render()); - } - }; - - // 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 = () => { - callPopup('Existing prompts with the same ID will be overridden. Do you want to proceed?', 'confirm') - .then(userChoice => { - if (false === 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('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 = () => { - callPopup('This will reset the prompt order for this character. You will not lose any prompts.', 'confirm') - .then(userChoice => { - if (false === userChoice) return; - - this.removePromptOrderForCharacter(this.activeCharacter); - this.addPromptOrderForCharacter(this.activeCharacter, promptManagerDefaultPromptOrder); - - this.saveServiceSettings().then(() => this.render()); - }); - }; - - // Fill quick edit fields for the first time - if ('global' === this.configuration.promptOrder.strategy) { - const handleQuickEditSave = (event) => { + // Reset prompt should it be a system prompt + this.handleResetPrompt = (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; + switch (promptId) { + case 'main': + prompt.name = 'Main Prompt'; + prompt.content = this.configuration.defaultPrompts.main; + 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; + break; + case 'enhanceDefinitions': + prompt.name = 'Enhance Definitions'; + prompt.content = this.configuration.defaultPrompts.enhanceDefinitions; + break; } - this.log('Saved prompt: ' + promptId); - this.saveServiceSettings().then(() => this.render()); + 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'; + + if (!this.systemPrompts.includes(promptId)) { + document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_injection_position').removeAttribute('disabled'); + } }; - const mainPrompt = this.getPromptById('main'); - const mainElementId = this.updateQuickEdit('main', mainPrompt); - document.getElementById(mainElementId).addEventListener('blur', handleQuickEditSave); + // 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); - const nsfwPrompt = this.getPromptById('nsfw'); - const nsfwElementId = this.updateQuickEdit('nsfw', nsfwPrompt); - document.getElementById(nsfwElementId).addEventListener('blur', handleQuickEditSave); + if (prompt) { + this.appendPrompt(prompt, this.activeCharacter); + this.saveServiceSettings().then(() => this.render()); + } + }; - const jailbreakPrompt = this.getPromptById('jailbreak'); - const jailbreakElementId = this.updateQuickEdit('jailbreak', jailbreakPrompt); - document.getElementById(jailbreakElementId).addEventListener('blur', handleQuickEditSave); + // Delete selected prompt from list form and close edit form + this.handleDeletePrompt = (event) => { + 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.saveServiceSettings().then(() => this.render()); + } + }; + + // 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 = () => { + callPopup('Existing prompts with the same ID will be overridden. Do you want to proceed?', 'confirm') + .then(userChoice => { + if (false === 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('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 = () => { + callPopup('This will reset the prompt order for this character. You will not lose any prompts.', 'confirm') + .then(userChoice => { + if (false === userChoice) return; + + this.removePromptOrderForCharacter(this.activeCharacter); + this.addPromptOrderForCharacter(this.activeCharacter, promptManagerDefaultPromptOrder); + + this.saveServiceSettings().then(() => this.render()); + }); + }; + + // 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('characterDeleted', (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'); } - // 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()); + /** + * Main rendering function + * + * @param afterTryGenerate - Whether a dry run should be attempted before rendering + */ + render(afterTryGenerate = true) { + if (main_api !== 'openai') return; - // Re-render when chatcompletion settings change - eventSource.on(event_types.CHATCOMPLETION_SOURCE_CHANGED, () => this.renderDebounced()); + if ('character' === this.configuration.promptOrder.strategy && null === this.activeCharacter) return; + this.error = null; - 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('characterDeleted', (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'); -}; - -/** - * Main rendering function - * - * @param afterTryGenerate - Whether a dry run should be attempted before rendering - */ -PromptManagerModule.prototype.render = function (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(() => { - if (true === afterTryGenerate) { - // Executed during dry-run for determining context composition - this.profileStart('filling context'); - this.tryGenerate().finally(() => { - this.profileEnd('filling context'); + waitUntilCondition(() => !is_send_press && !is_group_generating, 1024 * 1024, 100).then(() => { + if (true === afterTryGenerate) { + // Executed during dry-run for determining context composition + this.profileStart('filling context'); + this.tryGenerate().finally(() => { + this.profileEnd('filling context'); + this.profileStart('render'); + this.renderPromptManager(); + this.renderPromptManagerListItems(); + this.makeDraggable(); + this.profileEnd('render'); + }); + } else { + // Executed during live communication this.profileStart('render'); this.renderPromptManager(); this.renderPromptManagerListItems(); this.makeDraggable(); this.profileEnd('render'); - }); - } else { - // Executed during live communication - this.profileStart('render'); - this.renderPromptManager(); - this.renderPromptManagerListItems(); - this.makeDraggable(); - 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} - */ -PromptManagerModule.prototype.updatePromptWithPromptEditForm = function (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); -}; - -/** - * 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} - */ -PromptManagerModule.prototype.updatePromptByIdentifier = function (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} - */ -PromptManagerModule.prototype.updatePrompts = function (prompts) { - prompts.forEach((update) => { - let prompt = this.getPromptById(update.identifier); - if (prompt) Object.assign(prompt, update); - }); -}; - -PromptManagerModule.prototype.getTokenHandler = function () { - return this.tokenHandler; -}; - -PromptManagerModule.prototype.isPromptDisabledForActiveCharacter = function (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} - */ -PromptManagerModule.prototype.appendPrompt = function (prompt, character) { - const promptOrder = this.getPromptOrderForCharacter(character); - const index = promptOrder.findIndex(entry => entry.identifier === prompt.identifier); - - if (-1 === index) promptOrder.push({ 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 -PromptManagerModule.prototype.detachPrompt = function (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} - */ -PromptManagerModule.prototype.addPrompt = function (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} - */ -PromptManagerModule.prototype.sanitizeServiceSettings = function () { - 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 (-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 - */ -PromptManagerModule.prototype.checkForMissingPrompts = function (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. - */ -PromptManagerModule.prototype.isPromptInspectionAllowed = function (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. - */ -PromptManagerModule.prototype.isPromptDeletionAllowed = function (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. - */ -PromptManagerModule.prototype.isPromptEditAllowed = function (prompt) { - return !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. - */ -PromptManagerModule.prototype.isPromptToggleAllowed = function (prompt) { - const forceTogglePrompts = ['charDescription', 'charPersonality', 'scenario', 'personaDescription', 'worldInfoBefore', 'worldInfoAfter']; - 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 - */ -PromptManagerModule.prototype.handleCharacterDeleted = function (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} - */ -PromptManagerModule.prototype.handleCharacterSelected = function (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 - */ -PromptManagerModule.prototype.handleCharacterUpdated = function (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 - */ -PromptManagerModule.prototype.handleGroupSelected = function (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[]} - */ -PromptManagerModule.prototype.getActiveGroupCharacters = function () { - // 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 - */ -PromptManagerModule.prototype.getPromptsForCharacter = function (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. - */ -PromptManagerModule.prototype.getPromptOrderForCharacter = function (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} - */ -PromptManagerModule.prototype.setPrompts = function (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} - */ -PromptManagerModule.prototype.removePromptOrderForCharacter = function (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 - */ -PromptManagerModule.prototype.addPromptOrderForCharacter = function (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 - */ -PromptManagerModule.prototype.getPromptOrderEntry = function (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 - */ -PromptManagerModule.prototype.getPromptById = function (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 - */ -PromptManagerModule.prototype.getPromptIndexById = function (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 - */ -PromptManagerModule.prototype.preparePrompt = function (prompt, original = null) { - const groupMembers = this.getActiveGroupCharacters(); - const preparedPrompt = new Prompt(prompt); - - if (original) { - 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); + }).catch(() => { + console.log('Timeout while waiting for send press to be false'); + }); } - 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. - * - */ -PromptManagerModule.prototype.createQuickEdit = function (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()); - }); - -}; - -PromptManagerModule.prototype.updateQuickEdit = function (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} - */ -PromptManagerModule.prototype.isValidName = function (name) { - const regex = /^[a-zA-Z0-9_]{1,64}$/; - - return regex.test(name); -}; - -PromptManagerModule.prototype.sanitizeName = function (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' - */ -PromptManagerModule.prototype.loadPromptIntoEditForm = function (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'); - - nameField.value = prompt.name ?? ''; - roleField.value = prompt.role ?? ''; - promptField.value = prompt.content ?? ''; - 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'); - - if (this.systemPrompts.includes(prompt.identifier)) { - injectionPositionField.setAttribute('disabled', 'disabled'); + /** + * 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); } - 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'; + /** + * 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); } - 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; -}; - -PromptManagerModule.prototype.handleInjectionPositionChange = function (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'; + /** + * 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); + }); } -}; -/** - * Loads a given prompt into the inspect form - * @param {MessageCollection} messages - Prompt object with properties 'name', 'role', 'content', and 'system_prompt' - */ -PromptManagerModule.prototype.loadMessagesIntoInspectForm = function (messages) { - if (!messages) return; + getTokenHandler() { + return this.tokenHandler; + } - 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(); + isPromptDisabledForActiveCharacter(identifier) { + const promptOrderEntry = this.getPromptOrderEntry(this.activeCharacter, identifier); + if (promptOrderEntry) return !promptOrderEntry.enabled; + return false; + } - let drawerHTML = ` -
-
- Name: ${escapeHtml(title)}, Role: ${role}, Tokens: ${tokens} -
-
-
${escapeHtml(content)}
-
- `; + /** + * 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); - let template = document.createElement('template'); - template.innerHTML = drawerHTML.trim(); - return template.content.firstChild; - }; + if (-1 === index) promptOrder.push({ identifier: prompt.identifier, enabled: false }); + } - const messageList = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_entry_form_inspect_list'); + /** + * 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); + } - const messagesCollection = messages instanceof Message ? [messages] : messages.getCollection(); + /** + * 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 (0 === messagesCollection.length) messageList.innerHTML = 'This marker does not contain any prompts.'; + if (typeof prompt !== 'object' || prompt === null) throw new Error('Object is not a prompt'); - messagesCollection.forEach(message => { - messageList.append(createInlineDrawer(message)); - }); -}; - -/** - * Clears all input fields in the edit form. - */ -PromptManagerModule.prototype.clearEditForm = function () { - 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'); - - nameField.value = ''; - roleField.selectedIndex = 0; - promptField.value = ''; - injectionPositionField.selectedIndex = 0; - injectionPositionField.removeAttribute('disabled'); - injectionDepthField.value = DEFAULT_DEPTH; - injectionDepthBlock.style.visibility = 'unset'; - - roleField.disabled = false; -}; - -PromptManagerModule.prototype.clearInspectForm = function () { - 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 - */ -PromptManagerModule.prototype.getPromptCollection = function () { - 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)); - } - }); - - return promptCollection; -}; - -/** - * Setter for messages property - * - * @param {MessageCollection} messages - */ -PromptManagerModule.prototype.setMessages = function (messages) { - this.messages = messages; -}; - -/** - * Set and process a finished chat completion object - * - * @param {ChatCompletion} chatCompletion - */ -PromptManagerModule.prototype.setChatCompletion = function (chatCompletion) { - const messages = chatCompletion.getMessages(); - - this.setMessages(messages); - this.populateTokenCounts(messages); -}; - -/** - * Populates the token handler - * - * @param {MessageCollection} messages - */ -PromptManagerModule.prototype.populateTokenCounts = function (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); -}; - -/** - * Populates legacy token counts - * - * @deprecated This might serve no purpose and should be evaluated for removal - * - * @param {MessageCollection} messages - */ -PromptManagerModule.prototype.populateLegacyTokenCounts = function (messages) { - // Update general token counts - const chatHistory = messages.getItemByIdentifier('chatHistory'); - const startChat = chatHistory?.getCollection()[0]?.getTokens() || 0; - const continueNudge = chatHistory?.getCollection().find(message => message.identifier === 'continueNudge')?.getTokens() || 0; - - this.tokenHandler.counts = { - ...this.tokenHandler.counts, - ...{ - 'start_chat': startChat, - 'prompt': 0, - 'bias': this.tokenHandler.counts.bias ?? 0, - 'nudge': continueNudge, - 'jailbreak': this.tokenHandler.counts.jailbreak ?? 0, - 'impersonate': 0, - 'examples': this.tokenHandler.counts.dialogueExamples ?? 0, - 'conversation': this.tokenHandler.counts.chatHistory ?? 0, - }, - }; -}; - -/** - * Empties, then re-assembles the container containing the prompt list. - */ -PromptManagerModule.prototype.renderPromptManager = function () { - const promptManagerDiv = this.containerElement; - promptManagerDiv.innerHTML = ''; - - const errorDiv = ` -
- ${this.error} -
- `; - - const totalActiveTokens = this.tokenUsage; - - promptManagerDiv.insertAdjacentHTML('beforeend', ` -
- ${this.error ? errorDiv : ''} -
-
- Prompts -
-
Total Tokens: ${totalActiveTokens}
-
- -
- `); - - 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)) - .reduce((acc, prompt) => acc + ``, ''); - - const footerHtml = ` - - `; - - const rangeBlockDiv = promptManagerDiv.querySelector('.range-block'); - rangeBlockDiv.insertAdjacentHTML('beforeend', 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); - - // Add prompt export dialogue and options - const exportForCharacter =` -
- Export for character - -
`; - const exportPopup = ` -
-
-
- Export all - -
- ${'global' === this.configuration.promptOrder.strategy ? '' : exportForCharacter } -
-
- `; - - rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup); - - let 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', ''); - - exportPopper.update(); + const newPrompt = { + identifier: identifier, + system_prompt: false, + enabled: false, + marker: false, + ...prompt, }; - 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); + this.serviceSettings.prompts.push(newPrompt); } -}; -/** - * Empties, then re-assembles the prompt list - */ -PromptManagerModule.prototype.renderPromptManagerListItems = function () { - if (!this.serviceSettings.prompts) return; + /** + * 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 ?? []; - const promptManagerList = this.listElement; - promptManagerList.innerHTML = ''; + if ('global' === this.configuration.promptOrder.strategy) { + const dummyCharacter = { id: this.configuration.promptOrder.dummyId }; + const promptOrder = this.getPromptOrderForCharacter(dummyCharacter); - const { prefix } = this.configuration; + if (0 === promptOrder.length) this.addPromptOrderForCharacter(dummyCharacter, promptManagerDefaultPromptOrder); + } - let listItemHtml = ` -
  • - Name - - Tokens -
  • -
  • -
    -
  • - `; + // Check whether the referenced prompts are present. + this.serviceSettings.prompts.length === 0 + ? this.setPrompts(chatCompletionDefaultPrompts.prompts) + : this.checkForMissingPrompts(this.serviceSettings.prompts); - this.getPromptsForCharacter(this.activeCharacter).forEach(prompt => { - if (!prompt) return; + // Add identifiers if there are none assigned to a prompt + this.serviceSettings.prompts.forEach(prompt => prompt && (prompt.identifier = prompt.identifier ?? this.getUuidv4())); - 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; + if (this.activeCharacter) { + const promptReferences = this.getPromptOrderForCharacter(this.activeCharacter); + for (let i = promptReferences.length - 1; i >= 0; i--) { + const reference = promptReferences[i]; + if (-1 === this.serviceSettings.prompts.findIndex(prompt => prompt.identifier === reference.identifier)) { + promptReferences.splice(i, 1); + this.log('Removed unused reference: ' + reference.identifier); + } + } + } + } - // Warn the user if the chat history goes below certain token thresholds. - let warningClass = ''; - let warningTitle = ''; + /** + * 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 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; + const missingIdentifiers = defaultPromptIdentifiers.filter(identifier => + !prompts.some(prompt => prompt.identifier === identifier), + ); - 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.'; + 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) { + return !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']; + 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 (original) { + 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'); + + nameField.value = prompt.name ?? ''; + roleField.value = prompt.role ?? ''; + promptField.value = prompt.content ?? ''; + 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'); + + 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'); + + nameField.value = ''; + roleField.selectedIndex = 0; + promptField.value = ''; + injectionPositionField.selectedIndex = 0; + injectionPositionField.removeAttribute('disabled'); + injectionDepthField.value = DEFAULT_DEPTH; + injectionDepthBlock.style.visibility = 'unset'; + + 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)); + } + }); + + return promptCollection; + } + + /** + * Setter for messages property + * + * @param {MessageCollection} messages + */ + setMessages(messages) { + this.messages = messages; + } + + /** + * Set and process a finished chat completion object + * + * @param {ChatCompletion} chatCompletion + */ + setChatCompletion(chatCompletion) { + const messages = chatCompletion.getMessages(); + + this.setMessages(messages); + this.populateTokenCounts(messages); + } + + /** + * Populates the token handler + * + * @param {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); + } + + /** + * Populates legacy token counts + * + * @deprecated This might serve no purpose and should be evaluated for removal + * + * @param {MessageCollection} messages + */ + populateLegacyTokenCounts(messages) { + // Update general token counts + const chatHistory = messages.getItemByIdentifier('chatHistory'); + const startChat = chatHistory?.getCollection()[0]?.getTokens() || 0; + const continueNudge = chatHistory?.getCollection().find(message => message.identifier === 'continueNudge')?.getTokens() || 0; + + this.tokenHandler.counts = { + ...this.tokenHandler.counts, + ...{ + 'start_chat': startChat, + 'prompt': 0, + 'bias': this.tokenHandler.counts.bias ?? 0, + 'nudge': continueNudge, + 'jailbreak': this.tokenHandler.counts.jailbreak ?? 0, + 'impersonate': 0, + 'examples': this.tokenHandler.counts.dialogueExamples ?? 0, + 'conversation': this.tokenHandler.counts.chatHistory ?? 0, + }, + }; + } + + /** + * Empties, then re-assembles the container containing the prompt list. + */ + renderPromptManager() { + const promptManagerDiv = this.containerElement; + promptManagerDiv.innerHTML = ''; + + const errorDiv = ` +
    + ${this.error} +
    + `; + + const totalActiveTokens = this.tokenUsage; + + promptManagerDiv.insertAdjacentHTML('beforeend', ` +
    + ${this.error ? errorDiv : ''} +
    +
    + Prompts +
    +
    Total Tokens: ${totalActiveTokens}
    +
    +
      +
      + `); + + 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)) + .reduce((acc, prompt) => acc + ``, ''); + + const footerHtml = ` + + `; + + const rangeBlockDiv = promptManagerDiv.querySelector('.range-block'); + rangeBlockDiv.insertAdjacentHTML('beforeend', 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); + + // Add prompt export dialogue and options + const exportForCharacter =` + `; + const exportPopup = ` +
      +
      +
      + Export all + +
      + ${'global' === this.configuration.promptOrder.strategy ? '' : exportForCharacter } +
      +
      + `; + + rangeBlockDiv.insertAdjacentHTML('beforeend', exportPopup); + + let 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', ''); + + 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 + */ + renderPromptManagerListItems() { + if (!this.serviceSettings.prompts) return; + + const promptManagerList = this.listElement; + promptManagerList.innerHTML = ''; + + const { prefix } = this.configuration; + + let listItemHtml = ` +
    • + Name + + Tokens +
    • +
    • +
      +
    • + `; + + 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 isSystemPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE; + const isUserPrompt = !prompt.marker && !prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE; + const isInjectionPrompt = !prompt.marker && prompt.injection_position === INJECTION_POSITION.ABSOLUTE; + listItemHtml += ` +
    • + + ${prompt.marker ? '' : ''} + ${isSystemPrompt ? '' : ''} + ${isUserPrompt ? '' : ''} + ${isInjectionPrompt ? '' : ''} + ${this.isPromptInspectionAllowed(prompt) ? `${encodedName}` : encodedName} + ${isInjectionPrompt ? `@ ${prompt.injection_depth}` : ''} + + + + ${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); + 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('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('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; } } - 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 isSystemPrompt = !prompt.marker && prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE; - const isUserPrompt = !prompt.marker && !prompt.system_prompt && prompt.injection_position !== INJECTION_POSITION.ABSOLUTE; - const isInjectionPrompt = !prompt.marker && prompt.injection_position === INJECTION_POSITION.ABSOLUTE; - listItemHtml += ` -
    • - - ${prompt.marker ? '' : ''} - ${isSystemPrompt ? '' : ''} - ${isUserPrompt ? '' : ''} - ${isInjectionPrompt ? '' : ''} - ${this.isPromptInspectionAllowed(prompt) ? `${encodedName}` : encodedName} - ${isInjectionPrompt ? `@ ${prompt.injection_depth}` : ''} - - - - ${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 - */ -PromptManagerModule.prototype.export = function (data, type, name = 'export') { - const promptExport = { - version: this.configuration.version, - type: type, - data: data, - }; - - const serializedObject = JSON.stringify(promptExport); - 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 - */ -PromptManagerModule.prototype.import = function (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('Could not import prompts. Export failed validation.'); - return; + return true; } - const prompts = mergeKeepNewer(this.serviceSettings.prompts, importData.data.prompts); + /** + * 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()); - this.setPrompts(prompts); - this.log('Prompt import succeeded'); + if (month.length < 2) month = '0' + month; + if (day.length < 2) day = '0' + day; - 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.'); + return `${month}_${day}_${year}`; } - toastr.success('Prompt import complete.'); - this.saveServiceSettings().then(() => this.render()); -}; + /** + * 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)); -/** - * Helper function to check whether the structure of object matches controlObj - * - * @param controlObj - * @param object - * @returns {boolean} - */ -PromptManagerModule.prototype.validateObject = function (controlObj, object) { - for (let key in controlObj) { - if (!Object.hasOwn(object, key)) { - if (controlObj[key] === null) continue; - else return false; - } + this.removePromptOrderForCharacter(this.activeCharacter); + this.addPromptOrderForCharacter(this.activeCharacter, updatedPromptOrder); - 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; - } + this.log(`Prompt order updated for ${this.activeCharacter.name}.`); + + this.saveServiceSettings(); + }, + }); } - return true; -}; + /** + * 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 = 'block'; -/** - * Get current date as mm/dd/YYYY - * - * @returns {`${string}_${string}_${string}`} - */ -PromptManagerModule.prototype.getFormattedDate = function () { - 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} - */ -PromptManagerModule.prototype.makeDraggable = function () { - $(`#${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} - */ -PromptManagerModule.prototype.showPopup = function (area = 'edit') { - const areaElement = document.getElementById(this.configuration.prefix + 'prompt_manager_popup_' + area); - areaElement.style.display = 'block'; - - $('#' + 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} - */ -PromptManagerModule.prototype.hidePopup = function () { - $('#' + this.configuration.prefix + 'prompt_manager_popup').first() - .slideUp(200, 'swing') - .removeClass('openDrawer'); -}; - -/** - * Quick uuid4 implementation - * @returns {string} A string representation of an uuid4 - */ -PromptManagerModule.prototype.getUuidv4 = function () { - 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 - */ -PromptManagerModule.prototype.log = function (output) { - if (power_user.console_log_prompts) console.log('[PromptManager] ' + output); -}; - -/** - * Start a profiling task - * - * @param identifier - */ -PromptManagerModule.prototype.profileStart = function (identifier) { - if (power_user.console_log_prompts) console.time(identifier); -}; - -/** - * End a profiling task - * - * @param identifier - */ -PromptManagerModule.prototype.profileEnd = function (identifier) { - if (power_user.console_log_prompts) { - this.log('Profiling of "' + identifier + '" finished. Result below.'); - console.timeEnd(identifier); + $('#' + 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': [ @@ -1902,7 +1905,7 @@ const promptManagerDefaultPromptOrder = [ ]; export { - PromptManagerModule, + PromptManager, registerPromptManagerMigration, chatCompletionDefaultPrompts, promptManagerDefaultPromptOrders, diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 25e0c9754..22c4ae7a1 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -38,7 +38,7 @@ import { INJECTION_POSITION, Prompt, promptManagerDefaultPromptOrders, - PromptManagerModule as PromptManager, + PromptManager, } from './PromptManager.js'; import { getCustomStoppingStrings, persona_description_positions, power_user } from './power-user.js'; @@ -458,7 +458,7 @@ function setOpenAIMessageExamples(mesExamplesArray) { * One-time setup for prompt manager module. * * @param openAiSettings - * @returns {PromptManagerModule|null} + * @returns {PromptManager|null} */ function setupChatCompletionPromptManager(openAiSettings) { // Do not set up prompt manager more than once