Add async template renderer

This commit is contained in:
Cohee 2024-04-11 22:36:23 +03:00
parent 6290dff3d9
commit 369c3512c0
4 changed files with 170 additions and 63 deletions

View File

@ -153,7 +153,7 @@ import {
ensureImageFormatSupported, ensureImageFormatSupported,
} from './scripts/utils.js'; } from './scripts/utils.js';
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js'; import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, processChatSlashCommands, registerSlashCommand } from './scripts/slash-commands.js'; import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, processChatSlashCommands, registerSlashCommand } from './scripts/slash-commands.js';
import { import {
tag_map, tag_map,
@ -212,6 +212,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de
import { initPresetManager } from './scripts/preset-manager.js'; import { initPresetManager } from './scripts/preset-manager.js';
import { evaluateMacros } from './scripts/macros.js'; import { evaluateMacros } from './scripts/macros.js';
import { callGenericPopup } from './scripts/popup.js'; import { callGenericPopup } from './scripts/popup.js';
import { renderTemplate, renderTemplateAsync } from './scripts/templates.js';
//exporting functions and vars for mods //exporting functions and vars for mods
export { export {
@ -286,6 +287,7 @@ export {
printCharactersDebounced, printCharactersDebounced,
isOdd, isOdd,
countOccurrences, countOccurrences,
renderTemplate,
}; };
showLoader(); showLoader();
@ -575,14 +577,14 @@ export const MAX_INJECTION_DEPTH = 1000;
let system_messages = {}; let system_messages = {};
function getSystemMessages() { async function getSystemMessages() {
system_messages = { system_messages = {
help: { help: {
name: systemUserName, name: systemUserName,
force_avatar: system_avatar, force_avatar: system_avatar,
is_user: false, is_user: false,
is_system: true, is_system: true,
mes: renderTemplate('help'), mes: await renderTemplateAsync('help'),
}, },
slash_commands: { slash_commands: {
name: systemUserName, name: systemUserName,
@ -596,21 +598,21 @@ function getSystemMessages() {
force_avatar: system_avatar, force_avatar: system_avatar,
is_user: false, is_user: false,
is_system: true, is_system: true,
mes: renderTemplate('hotkeys'), mes: await renderTemplateAsync('hotkeys'),
}, },
formatting: { formatting: {
name: systemUserName, name: systemUserName,
force_avatar: system_avatar, force_avatar: system_avatar,
is_user: false, is_user: false,
is_system: true, is_system: true,
mes: renderTemplate('formatting'), mes: await renderTemplateAsync('formatting'),
}, },
macros: { macros: {
name: systemUserName, name: systemUserName,
force_avatar: system_avatar, force_avatar: system_avatar,
is_user: false, is_user: false,
is_system: true, is_system: true,
mes: renderTemplate('macros'), mes: await renderTemplateAsync('macros'),
}, },
welcome: welcome:
{ {
@ -618,7 +620,7 @@ function getSystemMessages() {
force_avatar: system_avatar, force_avatar: system_avatar,
is_user: false, is_user: false,
is_system: true, is_system: true,
mes: renderTemplate('welcome'), mes: await renderTemplateAsync('welcome'),
}, },
group: { group: {
name: systemUserName, name: systemUserName,
@ -672,52 +674,6 @@ $(document).ajaxError(function myErrorHandler(_, xhr) {
} }
}); });
/**
* Loads a URL content using XMLHttpRequest synchronously.
* @param {string} url URL to load synchronously
* @returns {string} Response text
*/
function getUrlSync(url) {
console.debug('Loading URL synchronously', url);
const request = new XMLHttpRequest();
request.open('GET', url, false); // `false` makes the request synchronous
request.send();
if (request.status >= 200 && request.status < 300) {
return request.responseText;
}
throw new Error(`Error loading ${url}: ${request.status} ${request.statusText}`);
}
const templateCache = new Map();
export function renderTemplate(templateId, templateData = {}, sanitize = true, localize = true, fullPath = false) {
try {
const pathToTemplate = fullPath ? templateId : `/scripts/templates/${templateId}.html`;
let template = templateCache.get(pathToTemplate);
if (!template) {
const templateContent = getUrlSync(pathToTemplate);
template = Handlebars.compile(templateContent);
templateCache.set(pathToTemplate, template);
}
let result = template(templateData);
if (sanitize) {
result = DOMPurify.sanitize(result);
}
if (localize) {
result = applyLocale(result);
}
return result;
} catch (err) {
console.error('Error rendering template', templateId, templateData, err);
toastr.error('Check the DevTools console for more information.', 'Error rendering template');
}
}
async function getClientVersion() { async function getClientVersion() {
try { try {
const response = await fetch('/version'); const response = await fetch('/version');
@ -901,7 +857,7 @@ async function firstLoadInit() {
await getClientVersion(); await getClientVersion();
await readSecretState(); await readSecretState();
await getSettings(); await getSettings();
getSystemMessages(); await getSystemMessages();
sendSystemMessage(system_message_types.WELCOME); sendSystemMessage(system_message_types.WELCOME);
initLocales(); initLocales();
initTags(); initTags();
@ -4614,7 +4570,7 @@ async function DupeChar() {
} }
} }
function promptItemize(itemizedPrompts, requestedMesId) { async function promptItemize(itemizedPrompts, requestedMesId) {
console.log('PROMPT ITEMIZE ENTERED'); console.log('PROMPT ITEMIZE ENTERED');
var incomingMesId = Number(requestedMesId); var incomingMesId = Number(requestedMesId);
console.debug(`looking for MesId ${incomingMesId}`); console.debug(`looking for MesId ${incomingMesId}`);
@ -4655,7 +4611,7 @@ function promptItemize(itemizedPrompts, requestedMesId) {
chatInjects: getTokenCount(itemizedPrompts[thisPromptSet].chatInjects), chatInjects: getTokenCount(itemizedPrompts[thisPromptSet].chatInjects),
}; };
if (params.chatInjects){ if (params.chatInjects) {
params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects; params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects;
} }
@ -4737,10 +4693,12 @@ function promptItemize(itemizedPrompts, requestedMesId) {
} }
if (params.this_main_api == 'openai') { if (params.this_main_api == 'openai') {
callPopup(renderTemplate('itemizationChat', params), 'text'); const template = await renderTemplateAsync('itemizationChat', params);
callPopup(template, 'text');
} else { } else {
callPopup(renderTemplate('itemizationText', params), 'text'); const template = await renderTemplateAsync('itemizationText', params);
callPopup(template, 'text');
} }
} }
@ -7814,7 +7772,11 @@ window['SillyTavern'].getContext = function () {
*/ */
registerHelper: () => { }, registerHelper: () => { },
registedDebugFunction: registerDebugFunction, registedDebugFunction: registerDebugFunction,
/**
* @deprecated Use renderExtensionTemplateAsync instead.
*/
renderExtensionTemplate: renderExtensionTemplate, renderExtensionTemplate: renderExtensionTemplate,
renderExtensionTemplateAsync: renderExtensionTemplateAsync,
callPopup: callPopup, callPopup: callPopup,
callGenericPopup: callGenericPopup, callGenericPopup: callGenericPopup,
mainApi: main_api, mainApi: main_api,

View File

@ -1,5 +1,6 @@
import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, substituteParams, renderTemplate, animation_duration } from '../script.js'; import { callPopup, eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration } from '../script.js';
import { hideLoader, showLoader } from './loader.js'; import { hideLoader, showLoader } from './loader.js';
import { renderTemplate, renderTemplateAsync } from '../script.js';
import { isSubsetOf, setValueByPath } from './utils.js'; import { isSubsetOf, setValueByPath } from './utils.js';
export { export {
getContext, getContext,
@ -50,17 +51,31 @@ export function saveMetadataDebounced() {
} }
/** /**
* Provides an ability for extensions to render HTML templates. * Provides an ability for extensions to render HTML templates synchronously.
* Templates sanitation and localization is forced. * Templates sanitation and localization is forced.
* @param {string} extensionName Extension name * @param {string} extensionName Extension name
* @param {string} templateId Template ID * @param {string} templateId Template ID
* @param {object} templateData Additional data to pass to the template * @param {object} templateData Additional data to pass to the template
* @returns {string} Rendered HTML * @returns {string} Rendered HTML
*
* @deprecated Use renderExtensionTemplateAsync instead.
*/ */
export function renderExtensionTemplate(extensionName, templateId, templateData = {}, sanitize = true, localize = true) { export function renderExtensionTemplate(extensionName, templateId, templateData = {}, sanitize = true, localize = true) {
return renderTemplate(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true); return renderTemplate(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true);
} }
/**
* Provides an ability for extensions to render HTML templates asynchronously.
* Templates sanitation and localization is forced.
* @param {string} extensionName Extension name
* @param {string} templateId Template ID
* @param {object} templateData Additional data to pass to the template
* @returns {Promise<string>} Rendered HTML
*/
export function renderExtensionTemplateAsync(extensionName, templateId, templateData = {}, sanitize = true, localize = true) {
return renderTemplateAsync(`scripts/extensions/${extensionName}/${templateId}.html`, templateData, sanitize, localize, true);
}
// Disables parallel updates // Disables parallel updates
class ModuleWorkerWrapper { class ModuleWorkerWrapper {
constructor(callback) { constructor(callback) {

View File

@ -13,7 +13,6 @@ import {
printCharactersDebounced, printCharactersDebounced,
setCharacterId, setCharacterId,
setEditedMessageId, setEditedMessageId,
renderTemplate,
chat, chat,
getFirstDisplayedMessageId, getFirstDisplayedMessageId,
showMoreMessages, showMoreMessages,
@ -23,6 +22,7 @@ import {
ANIMATION_DURATION_DEFAULT, ANIMATION_DURATION_DEFAULT,
setActiveGroup, setActiveGroup,
setActiveCharacter, setActiveCharacter,
renderTemplateAsync,
} from '../script.js'; } from '../script.js';
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js'; import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
import { import {
@ -1363,8 +1363,8 @@ export function registerDebugFunction(functionId, name, description, func) {
debug_functions.push({ functionId, name, description, func }); debug_functions.push({ functionId, name, description, func });
} }
function showDebugMenu() { async function showDebugMenu() {
const template = renderTemplate('debug', { functions: debug_functions }); const template = await renderTemplateAsync('debug', { functions: debug_functions });
callPopup(template, 'text', '', { wide: true, large: true }); callPopup(template, 'text', '', { wide: true, large: true });
} }

130
public/scripts/templates.js Normal file
View File

@ -0,0 +1,130 @@
import { applyLocale } from './i18n.js';
/**
* @type {Map<string, function>}
* @description Cache for Handlebars templates.
*/
const TEMPLATE_CACHE = new Map();
/**
* Loads a URL content using XMLHttpRequest synchronously.
* @param {string} url URL to load synchronously
* @returns {string} Response text
*/
function getUrlSync(url) {
console.debug('Loading URL synchronously', url);
const request = new XMLHttpRequest();
request.open('GET', url, false); // `false` makes the request synchronous
request.send();
if (request.status >= 200 && request.status < 300) {
return request.responseText;
}
throw new Error(`Error loading ${url}: ${request.status} ${request.statusText}`);
}
/**
* Loads a URL content using XMLHttpRequest asynchronously.
* @param {string} url URL to load asynchronously
* @returns {Promise<string>} Response text
*/
function getUrlAsync(url) {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = () => {
if (request.status >= 200 && request.status < 300) {
resolve(request.responseText);
} else {
reject(new Error(`Error loading ${url}: ${request.status} ${request.statusText}`));
}
};
request.onerror = () => {
reject(new Error(`Error loading ${url}: ${request.status} ${request.statusText}`));
};
request.send();
});
}
/**
* Renders a Handlebars template asynchronously.
* @param {string} templateId ID of the template to render
* @param {Record<string, any>} templateData The data to pass to the template
* @param {boolean} sanitize Should the template be sanitized with DOMPurify
* @param {boolean} localize Should the template be localized
* @param {boolean} fullPath Should the template ID be treated as a full path or a relative path
* @returns {Promise<string>} Rendered template
*/
export async function renderTemplateAsync(templateId, templateData = {}, sanitize = true, localize = true, fullPath = false) {
async function fetchTemplateAsync(pathToTemplate) {
let template = TEMPLATE_CACHE.get(pathToTemplate);
if (!template) {
const templateContent = await getUrlAsync(pathToTemplate);
template = Handlebars.compile(templateContent);
TEMPLATE_CACHE.set(pathToTemplate, template);
}
return template;
}
try {
const pathToTemplate = fullPath ? templateId : `/scripts/templates/${templateId}.html`;
const template = await fetchTemplateAsync(pathToTemplate);
let result = template(templateData);
if (sanitize) {
result = DOMPurify.sanitize(result);
}
if (localize) {
result = applyLocale(result);
}
return result;
} catch (err) {
console.error('Error rendering template', templateId, templateData, err);
toastr.error('Check the DevTools console for more information.', 'Error rendering template');
}
}
/**
* Renders a Handlebars template synchronously.
* @param {string} templateId ID of the template to render
* @param {Record<string, any>} templateData The data to pass to the template
* @param {boolean} sanitize Should the template be sanitized with DOMPurify
* @param {boolean} localize Should the template be localized
* @param {boolean} fullPath Should the template ID be treated as a full path or a relative path
* @returns {string} Rendered template
*
* @deprecated Use renderTemplateAsync instead.
*/
export function renderTemplate(templateId, templateData = {}, sanitize = true, localize = true, fullPath = false) {
function fetchTemplateSync(pathToTemplate) {
let template = TEMPLATE_CACHE.get(pathToTemplate);
if (!template) {
const templateContent = getUrlSync(pathToTemplate);
template = Handlebars.compile(templateContent);
TEMPLATE_CACHE.set(pathToTemplate, template);
}
return template;
}
try {
const pathToTemplate = fullPath ? templateId : `/scripts/templates/${templateId}.html`;
const template = fetchTemplateSync(pathToTemplate);
let result = template(templateData);
if (sanitize) {
result = DOMPurify.sanitize(result);
}
if (localize) {
result = applyLocale(result);
}
return result;
} catch (err) {
console.error('Error rendering template', templateId, templateData, err);
toastr.error('Check the DevTools console for more information.', 'Error rendering template');
}
}