diff --git a/public/script.js b/public/script.js index 4f67580f1..7ffcef682 100644 --- a/public/script.js +++ b/public/script.js @@ -136,7 +136,7 @@ import { PAGINATION_TEMPLATE, } from "./scripts/utils.js"; -import { extension_settings, getContext, loadExtensionSettings, registerExtensionHelper, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js"; +import { extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js"; import { executeSlashCommands, getSlashCommandsHelp, registerSlashCommand } from "./scripts/slash-commands.js"; import { tag_map, @@ -278,10 +278,20 @@ export const event_types = { OAI_PRESET_CHANGED: 'oai_preset_changed', WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated', CHARACTER_EDITED: 'character_edited', + USER_MESSAGE_RENDERED: 'user_message_rendered', + CHARACTER_MESSAGE_RENDERED: 'character_message_rendered', } export const eventSource = new EventEmitter(); +// Check for override warnings every 5 seconds... +setInterval(displayOverrideWarnings, 5000); +// ...or when the chat changes +eventSource.on(event_types.CHAT_CHANGED, displayOverrideWarnings); +eventSource.on(event_types.CHAT_CHANGED, setChatLockedPersona); +eventSource.on(event_types.MESSAGE_RECEIVED, processExtensionHelpers); +eventSource.on(event_types.MESSAGE_SENT, processExtensionHelpers); + const gpt3 = new GPT3BrowserTokenizer({ type: 'gpt3' }); hljs.addPlugin({ "before:highlightElement": ({ el }) => { el.textContent = el.innerText } }); @@ -2179,14 +2189,17 @@ class StreamingProcessor { async onFinishStreaming(messageId, text) { this.hideMessageButtons(this.messageId); - - const eventType = this.type !== 'impersonate' ? event_types.MESSAGE_RECEIVED : event_types.IMPERSONATE_READY; - const eventData = this.type !== 'impersonate' ? this.messageId : text; - await eventSource.emit(eventType, eventData); - this.onProgressStreaming(messageId, text, true); addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`)); - saveChatConditional(); + + if (this.type !== 'impersonate') { + await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId); + } else { + await eventSource.emit(event_types.IMPERSONATE_READY, text); + } + + await saveChatConditional(); activateSendButtons(); showSwipeButtons(); setGenerationProgress(0); @@ -3360,6 +3373,7 @@ export async function sendMessageAsUser(textareaText, messageBias) { // Wait for all handlers to finish before continuing with the prompt await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(chat[chat.length - 1]); + await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); console.debug('message sent as user'); } @@ -3913,6 +3927,7 @@ async function saveReply(type, getMessage, this_mes_is_name, title) { chat[chat.length - 1]['extra']['model'] = getGeneratingModel(); await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); addOneMessage(chat[chat.length - 1], { type: 'swipe' }); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); } else { chat[chat.length - 1]['mes'] = getMessage; } @@ -3928,6 +3943,7 @@ async function saveReply(type, getMessage, this_mes_is_name, title) { chat[chat.length - 1]["extra"]["model"] = getGeneratingModel(); await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); addOneMessage(chat[chat.length - 1], { type: 'swipe' }); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); } else if (type === 'appendFinal') { oldMessage = chat[chat.length - 1]['mes']; console.debug("Trying to appendFinal.") @@ -3940,6 +3956,7 @@ async function saveReply(type, getMessage, this_mes_is_name, title) { chat[chat.length - 1]["extra"]["model"] = getGeneratingModel(); await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); addOneMessage(chat[chat.length - 1], { type: 'swipe' }); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); } else { console.debug('entering chat update routine for non-swipe post'); @@ -3974,6 +3991,7 @@ async function saveReply(type, getMessage, this_mes_is_name, title) { saveImageToMessage(img, chat[chat.length - 1]); await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); addOneMessage(chat[chat.length - 1]); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); } const item = chat[chat.length - 1]; @@ -4428,6 +4446,7 @@ async function getChatResult() { if (chat.length === 1) { await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); } } @@ -5009,7 +5028,7 @@ function lockUserNameToChat() { updateUserLockIcon(); } -eventSource.on(event_types.CHAT_CHANGED, () => { +function setChatLockedPersona() { // Define a persona for this chat let chatPersona = ''; @@ -5051,7 +5070,7 @@ eventSource.on(event_types.CHAT_CHANGED, () => { // Persona avatar found, select it personaAvatar.trigger('click'); updateUserLockIcon(); -}); +} async function doOnboarding(avatarId) { const template = $('#onboarding_template .onboarding'); @@ -6593,6 +6612,7 @@ async function createOrEditCharacter(e) { //console.log('form create submission calling addOneMessage'); await eventSource.emit(event_types.MESSAGE_RECEIVED, (chat.length - 1)); addOneMessage(chat[0]); + await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, (chat.length - 1)); } } $("#create_button").removeAttr("disabled"); @@ -7020,12 +7040,6 @@ function connectAPISlash(_, text) { toastr.info(`API set to ${text}, trying to connect..`); } - -// Check for override warnings every 5 seconds... -setInterval(displayOverrideWarnings, 5000); -// ...or when the chat changes -eventSource.on(event_types.CHAT_CHANGED, displayOverrideWarnings); - function importCharacter(file) { const ext = file.name.match(/\.(\w+)$/); if ( diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 70be2dc44..07d16cbc2 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -1,4 +1,4 @@ -import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders } from "../script.js"; +import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams } from "../script.js"; import { isSubsetOf, debounce, waitUntilCondition } from "./utils.js"; export { getContext, @@ -12,7 +12,7 @@ export { }; let extensionNames = []; -let manifests = []; +let manifests = {}; const defaultUrl = "http://localhost:5100"; export const saveMetadataDebounced = debounce(async () => await getContext().saveMetadata(), 1000); @@ -31,7 +31,7 @@ export function registerExtensionHelper(name, helper) { * Applies handlebars extension helpers to a message. * @param {number} messageId Message index in the chat. */ -function processExtensionHelpers(messageId) { +export function processExtensionHelpers(messageId) { const context = getContext(); const message = context.chat[messageId]; @@ -40,12 +40,12 @@ function processExtensionHelpers(messageId) { } // Don't waste time if there are no mustaches - if (!message.mes.includes('{{')) { + if (!substituteParams(message.mes).includes('{{')) { return; } try { - const template = extensionsHandlebars.compile(message.mes, { noEscape: true }); + const template = extensionsHandlebars.compile(substituteParams(message.mes), { noEscape: true }); message.mes = template({}); } catch { // Ignore @@ -211,7 +211,10 @@ async function getManifests(names) { } else { reject(); } - }).catch(err => reject() && console.log('Could not load manifest.json for ' + name, err)); + }).catch(err => { + reject(); + console.log('Could not load manifest.json for ' + name, err); + }); }); promises.push(promise); @@ -268,9 +271,9 @@ async function activateExtensions() { async function connectClickHandler() { const baseUrl = $("#extensions_url").val(); - extension_settings.apiUrl = baseUrl; + extension_settings.apiUrl = String(baseUrl); const testApiKey = $("#extensions_api_key").val(); - extension_settings.apiKey = testApiKey; + extension_settings.apiKey = String(testApiKey); saveSettingsDebounced(); await connectToApi(baseUrl); } @@ -495,7 +498,7 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt * Gets extension data and generates the corresponding HTML for displaying the extension. * * @param {Array} extension - An array where the first element is the extension name and the second element is the extension manifest. - * @return {object} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. + * @return {Promise} - An object with 'isExternal' indicating whether the extension is external, and 'extensionHtml' for the extension's HTML string. */ async function getExtensionData(extension) { const name = extension[0]; @@ -612,7 +615,7 @@ async function onDeleteClick() { * Fetches the version details of a specific extension. * * @param {string} extensionName - The name of the extension. - * @return {object} - An object containing the extension's version details. + * @return {Promise} - An object containing the extension's version details. * This object includes the currentBranchName, currentCommitHash, isUpToDate, and remoteUrl. * @throws {error} - If there is an error during the fetch operation, it logs the error to the console. */ @@ -669,9 +672,6 @@ jQuery(function () { setTimeout(async function () { addExtensionsButtonAndMenu(); $("#extensionsMenuButton").css("display", "flex"); - await waitUntilCondition(() => eventSource !== undefined, 1000, 100); - eventSource.on(event_types.MESSAGE_RECEIVED, processExtensionHelpers); - eventSource.on(event_types.MESSAGE_SENT, processExtensionHelpers); }, 100) $("#extensions_connect").on('click', connectClickHandler); diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js index bb8b1ddb7..ac477ffe3 100644 --- a/public/scripts/extensions/translate/index.js +++ b/public/scripts/extensions/translate/index.js @@ -421,9 +421,9 @@ jQuery(() => { loadSettings(); - eventSource.on(event_types.MESSAGE_RECEIVED, handleIncomingMessage); + eventSource.on(event_types.CHARACTER_MESSAGE_RENDERED, handleIncomingMessage); eventSource.on(event_types.MESSAGE_SWIPED, handleIncomingMessage); - eventSource.on(event_types.MESSAGE_SENT, handleOutgoingMessage); + eventSource.on(event_types.USER_MESSAGE_RENDERED, handleOutgoingMessage); eventSource.on(event_types.IMPERSONATE_READY, handleImpersonateReady); eventSource.on(event_types.MESSAGE_EDITED, handleMessageEdit); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index eb2c2e15d..294a61fb9 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -340,6 +340,7 @@ async function sendMessageAs(_, text) { chat.push(message); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(message); + await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); saveChatConditional(); } @@ -371,6 +372,7 @@ async function sendNarratorMessage(_, text) { chat.push(message); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(message); + await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); saveChatConditional(); } @@ -396,6 +398,7 @@ async function sendCommentMessage(_, text) { chat.push(message); await eventSource.emit(event_types.MESSAGE_SENT, (chat.length - 1)); addOneMessage(message); + await eventSource.emit(event_types.USER_MESSAGE_RENDERED, (chat.length - 1)); saveChatConditional(); } diff --git a/public/scripts/utils.js b/public/scripts/utils.js index 874552998..48140cbe6 100644 --- a/public/scripts/utils.js +++ b/public/scripts/utils.js @@ -1,8 +1,25 @@ import { getContext } from "./extensions.js"; import { getRequestHeaders } from "../script.js"; +/** + * Pagination status string template. + * @type {string} + */ export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>'; +/** + * Navigation options for pagination. + * @enum {number} + */ +export const navigation_option = { none: 0, previous: 1, last: 2, }; + +/** + * Determines if a value is unique in an array. + * @param {any} value Current value. + * @param {number} index Current index. + * @param {any} array The array being processed. + * @returns {boolean} True if the value is unique, false otherwise. + */ export function onlyUnique(value, index, array) { return array.indexOf(value) === index; } @@ -19,7 +36,10 @@ export function isDigitsOnly(str) { return /^\d+$/.test(str); } -// Increase delay on touch screens +/** + * Gets a drag delay for sortable elements. This is to prevent accidental drags when scrolling. + * @returns {number} The delay in milliseconds. 100ms for desktop, 750ms for mobile. + */ export function getSortableDelay() { return navigator.maxTouchPoints > 0 ? 750 : 100; } @@ -60,12 +80,23 @@ export function download(content, fileName, contentType) { a.click(); } +/** + * Fetches a file by URL and parses its contents as data URI. + * @param {string} url The URL to fetch. + * @param {any} params Fetch parameters. + * @returns {Promise} A promise that resolves to the data URI. + */ export async function urlContentToDataUri(url, params) { const response = await fetch(url, params); const blob = await response.blob(); - return await new Promise(callback => { - let reader = new FileReader(); - reader.onload = function () { callback(this.result); }; + return await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = function () { + resolve(String(reader.result)); + }; + reader.onerror = function (error) { + reject(error); + }; reader.readAsDataURL(blob); }); } @@ -195,7 +226,7 @@ export function throttle(func, limit = 300) { /** * Checks if an element is in the viewport. - * @param {any[]} el The element to check. + * @param {Element} el The element to check. * @returns {boolean} True if the element is in the viewport, false otherwise. */ export function isElementInViewport(el) { diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 9c4118687..630dc735b 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1,5 +1,5 @@ import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types } from "../script.js"; -import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, deepClone, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE } from "./utils.js"; +import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, deepClone, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option } from "./utils.js"; import { getContext } from "./extensions.js"; import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js"; import { registerSlashCommand } from "./slash-commands.js"; @@ -46,7 +46,6 @@ const saveSettingsDebounced = debounce(() => { saveSettings() }, 1000); const sortFn = (a, b) => b.order - a.order; -const navigation_option = { none: 0, previous: 1, last: 2, }; let updateEditor = (navigation) => { navigation; }; // Do not optimize. updateEditor is a function that is updated by the displayWorldEntries with new data.