Move itemized prompts into separate module (#4229) * Move itemized prompts into separate module * Update public/scripts/itemized-prompts.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update public/scripts/itemized-prompts.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

8a6e6503f50f71b8ecf17aa3b4b2f799af7b01a9

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
2 files changed, +367 -344Showing whitespace changes
public/script.js+15 -344
@@ -4,9 +4,7 @@ import {
44 Fuse,
55 DOMPurify,
66 hljs,
7- localforage,
87 Handlebars,
9- DiffMatchPatch,
108 SVGInject,
119 Popper,
1210 initLibraryShims,
@@ -297,6 +295,7 @@ import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, Prompt
297295import { accountStorage } from './scripts/util/AccountStorage.js';
298296import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } from './scripts/welcome-screen.js';
299297import { initDataMaid } from './scripts/data-maid.js';
298+import { clearItemizedPrompts, deleteItemizedPrompts, findItemizedPromptSet, initItemizedPrompts, itemizedParams, itemizedPrompts, loadItemizedPrompts, promptItemize, replaceItemizedPromptText, saveItemizedPrompts } from './scripts/itemized-prompts.js';
300299
301300// API OBJECT FOR EXTERNAL WIRING
302301globalThis.SillyTavern = {
@@ -304,7 +303,9 @@ globalThis.SillyTavern = {
304303 getContext,
305304};
306305
307-//exporting functions and vars for mods
306+/**
307+ * Exports that were once part of this module. For backwards compatibility.
308+ */
308309export {
309310 user_avatar,
310311 setUserAvatar,
@@ -314,6 +315,15 @@ export {
314315 isOdd,
315316 countOccurrences,
316317 renderTemplate,
318+ promptItemize,
319+ itemizedPrompts,
320+ saveItemizedPrompts,
321+ loadItemizedPrompts,
322+ itemizedParams,
323+ clearItemizedPrompts,
324+ replaceItemizedPromptText,
325+ deleteItemizedPrompts,
326+ findItemizedPromptSet,
317327};
318328
319329/**
@@ -577,9 +587,6 @@ export let mesForShowdownParse; //intended to be used as a context to compare sh
577587export let converter;
578588
579589// array for prompt token calculations
580-console.debug('initializing Prompt Itemization Array on Startup');
581-const promptStorage = localforage.createInstance({ name: 'SillyTavern_Prompts' });
582-export let itemizedPrompts = [];
583590
584591export const systemUserName = 'SillyTavern System';
585592export const neutralCharacterName = 'Assistant';
@@ -928,8 +935,6 @@ var kobold_horde_model = '';
928935
929936export let token;
930937
931-var PromptArrayItemForRawPromptDisplay;
932-var priorPromptArrayItemForRawPromptDisplay;
933938
934939/** The tag of the active character. (NOT the id) */
935940export let active_character = '';
@@ -1038,6 +1043,7 @@ async function firstLoadInit() {
10381043 await initScrapers();
10391044 initCustomSelectedSamplers();
10401045 initDataMaid();
1046+ initItemizedPrompts();
10411047 addDebugFunctions();
10421048 doDailyExtensionUpdatesCheck();
10431049 await hideLoader();
@@ -1102,92 +1108,6 @@ export function setActiveGroup(entityOrKey) {
11021108 if (active_group) active_character = null;
11031109}
11041110
1105-/**
1106- * Gets the itemized prompts for a chat.
1107- * @param {string} chatId Chat ID to load
1108- */
1109-export async function loadItemizedPrompts(chatId) {
1110- try {
1111- if (!chatId) {
1112- itemizedPrompts = [];
1113- return;
1114- }
1115-
1116- itemizedPrompts = await promptStorage.getItem(chatId);
1117-
1118- if (!itemizedPrompts) {
1119- itemizedPrompts = [];
1120- }
1121- } catch {
1122- console.log('Error loading itemized prompts for chat', chatId);
1123- itemizedPrompts = [];
1124- }
1125-}
1126-
1127-/**
1128- * Saves the itemized prompts for a chat.
1129- * @param {string} chatId Chat ID to save itemized prompts for
1130- */
1131-export async function saveItemizedPrompts(chatId) {
1132- try {
1133- if (!chatId) {
1134- return;
1135- }
1136-
1137- await promptStorage.setItem(chatId, itemizedPrompts);
1138- } catch {
1139- console.log('Error saving itemized prompts for chat', chatId);
1140- }
1141-}
1142-
1143-/**
1144- * Replaces the itemized prompt text for a message.
1145- * @param {number} mesId Message ID to get itemized prompt for
1146- * @param {string} promptText New raw prompt text
1147- * @returns
1148- */
1149-export async function replaceItemizedPromptText(mesId, promptText) {
1150- if (!Array.isArray(itemizedPrompts)) {
1151- itemizedPrompts = [];
1152- }
1153-
1154- const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId);
1155-
1156- if (!itemizedPrompt) {
1157- return;
1158- }
1159-
1160- itemizedPrompt.rawPrompt = promptText;
1161-}
1162-
1163-/**
1164- * Deletes the itemized prompts for a chat.
1165- * @param {string} chatId Chat ID to delete itemized prompts for
1166- */
1167-export async function deleteItemizedPrompts(chatId) {
1168- try {
1169- if (!chatId) {
1170- return;
1171- }
1172-
1173- await promptStorage.removeItem(chatId);
1174- } catch {
1175- console.log('Error deleting itemized prompts for chat', chatId);
1176- }
1177-}
1178-
1179-/**
1180- * Empties the itemized prompts array and caches.
1181- */
1182-export async function clearItemizedPrompts() {
1183- try {
1184- await promptStorage.clear();
1185- itemizedPrompts = [];
1186- } catch {
1187- console.log('Error clearing itemized prompts');
1188- }
1189-}
1190-
11911111async function getStatusHorde() {
11921112 try {
11931113 const hordeStatus = await checkHordeStatus();
@@ -2089,7 +2009,7 @@ export async function clearChat() {
20892009 } else { console.debug('saw no avatars'); }
20902010
20912011 await saveItemizedPrompts(getCurrentChatId());
20922012 itemizedPrompts.length = []0;
20932013}
20942014
20952015export async function deleteLastMessage() {
@@ -5724,231 +5644,6 @@ async function duplicateCharacter() {
57245644 return '';
57255645}
57265646
5727-export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId) {
5728- const params = {
5729- charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription),
5730- charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality),
5731- scenarioTextTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].scenarioText),
5732- userPersonaStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].userPersona),
5733- worldInfoStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].worldInfoString),
5734- allAnchorsTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].allAnchors),
5735- summarizeStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].summarizeString),
5736- authorsNoteStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].authorsNoteString),
5737- smartContextStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].smartContextString),
5738- beforeScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].beforeScenarioAnchor),
5739- afterScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].afterScenarioAnchor),
5740- zeroDepthAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused
5741- thisPrompt_padding: itemizedPrompts[thisPromptSet].padding,
5742- this_main_api: itemizedPrompts[thisPromptSet].main_api,
5743- chatInjects: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatInjects),
5744- chatVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatVectorsString),
5745- dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
5746- modelUsed: chat[incomingMesId]?.extra?.model,
5747- apiUsed: chat[incomingMesId]?.extra?.api,
5748- presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
5749- messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
5750- examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
5751- };
5752-
5753- const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
5754-
5755- if (params.apiUsed) {
5756- params.apiUsed = getFriendlyName(params.apiUsed);
5757- }
5758-
5759- if (params.this_main_api) {
5760- params.mainApiFriendlyName = getFriendlyName(params.this_main_api);
5761- }
5762-
5763- if (params.chatInjects) {
5764- params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects;
5765- }
5766-
5767- if (params.this_main_api == 'openai') {
5768- //for OAI API
5769- //console.log('-- Counting OAI Tokens');
5770-
5771- //params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
5772- params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
5773- params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
5774- params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
5775- params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
5776- params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens;
5777- params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
5778- params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
5779- params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
5780- params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
5781- params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
5782- params.finalPromptTokens =
5783- params.oaiStartTokens +
5784- params.oaiPromptTokens +
5785- params.oaiMainTokens +
5786- params.oaiNsfwTokens +
5787- params.oaiBiasTokens +
5788- params.oaiImpersonateTokens +
5789- params.oaiJailbreakTokens +
5790- params.oaiNudgeTokens +
5791- params.ActualChatHistoryTokens +
5792- //charDescriptionTokens +
5793- //charPersonalityTokens +
5794- //allAnchorsTokens +
5795- params.worldInfoStringTokens +
5796- params.beforeScenarioAnchorTokens +
5797- params.afterScenarioAnchorTokens;
5798- // Max context size - max completion tokens
5799- params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
5800-
5801- //console.log('-- applying % on OAI tokens');
5802- params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5803- params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2);
5804- params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5805- params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5806- params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5807- params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5808- params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
5809- params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens;
5810- params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2);
5811- } else {
5812- //for non-OAI APIs
5813- //console.log('-- Counting non-OAI Tokens');
5814- params.finalPromptTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].finalPrompt);
5815- params.storyStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens;
5816- params.examplesStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].examplesString);
5817- params.mesSendStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].mesSendString);
5818- params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding;
5819- params.instructionTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].instruction);
5820- params.promptBiasTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].promptBias);
5821-
5822- params.totalTokensInPrompt =
5823- params.storyStringTokens + //chardefs total
5824- params.worldInfoStringTokens +
5825- params.examplesStringTokens + // example messages
5826- params.ActualChatHistoryTokens + //chat history
5827- params.allAnchorsTokens + // AN and/or legacy anchors
5828- //afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
5829- //zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
5830- params.promptBiasTokens; //{{}}
5831- //- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
5832- params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
5833- params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding;
5834-
5835- //console.log('-- applying % on non-OAI tokens');
5836- params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5837- params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5838- params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5839- params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5840- params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
5841- params.selectedTokenizer = itemizedPrompts[thisPromptSet]?.tokenizer || getFriendlyTokenizerName(params.this_main_api).tokenizerName;
5842- }
5843- return params;
5844-}
5845-
5846-export function findItemizedPromptSet(itemizedPrompts, incomingMesId) {
5847- var thisPromptSet = undefined;
5848-
5849- for (var i = 0; i < itemizedPrompts.length; i++) {
5850- console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`);
5851- if (itemizedPrompts[i].mesId === incomingMesId) {
5852- console.log(`found matching mesID ${i}`);
5853- thisPromptSet = i;
5854- PromptArrayItemForRawPromptDisplay = i;
5855- console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
5856- console.log(itemizedPrompts[thisPromptSet]);
5857- break;
5858- } else if (itemizedPrompts[i].rawPrompt) {
5859- priorPromptArrayItemForRawPromptDisplay = i;
5860- }
5861- }
5862- return thisPromptSet;
5863-}
5864-
5865-async function promptItemize(itemizedPrompts, requestedMesId) {
5866- console.log('PROMPT ITEMIZE ENTERED');
5867- var incomingMesId = Number(requestedMesId);
5868- console.debug(`looking for MesId ${incomingMesId}`);
5869- var thisPromptSet = findItemizedPromptSet(itemizedPrompts, incomingMesId);
5870-
5871- if (thisPromptSet === undefined) {
5872- console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
5873- console.log(itemizedPrompts);
5874- return null;
5875- }
5876-
5877- const params = await itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId);
5878- const flatten = (rawPrompt) => Array.isArray(rawPrompt) ? rawPrompt.map(x => x.content).join('\n') : rawPrompt;
5879-
5880- const template = params.this_main_api == 'openai'
5881- ? await renderTemplateAsync('itemizationChat', params)
5882- : await renderTemplateAsync('itemizationText', params);
5883-
5884- const popup = new Popup(template, POPUP_TYPE.TEXT);
5885-
5886- /** @type {HTMLElement} */
5887- const diffPrevPrompt = popup.dlg.querySelector('#diffPrevPrompt');
5888- if (priorPromptArrayItemForRawPromptDisplay) {
5889- diffPrevPrompt.style.display = '';
5890- diffPrevPrompt.addEventListener('click', function () {
5891- const dmp = new DiffMatchPatch();
5892- const text1 = flatten(itemizedPrompts[priorPromptArrayItemForRawPromptDisplay].rawPrompt);
5893- const text2 = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5894-
5895- dmp.Diff_Timeout = 2.0;
5896-
5897- const d = dmp.diff_main(text1, text2);
5898- let ds = dmp.diff_prettyHtml(d);
5899- // make it readable
5900- ds = ds.replaceAll('background:#e6ffe6;', 'background:#b9f3b9; color:black;');
5901- ds = ds.replaceAll('background:#ffe6e6;', 'background:#f5b4b4; color:black;');
5902- ds = ds.replaceAll('&para;', '');
5903- const container = document.createElement('div');
5904- container.innerHTML = DOMPurify.sanitize(ds);
5905- const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5906- rawPromptWrapper.replaceChildren(container);
5907- $('#rawPromptPopup').slideToggle();
5908- });
5909- } else {
5910- diffPrevPrompt.style.display = 'none';
5911- }
5912- popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () {
5913- let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
5914- let rawPromptValues = rawPrompt;
5915-
5916- if (Array.isArray(rawPrompt)) {
5917- rawPromptValues = rawPrompt.map(x => x.content).join('\n');
5918- }
5919-
5920- await copyText(rawPromptValues);
5921- toastr.info(t`Copied!`);
5922- });
5923-
5924- popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
5925- //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5926- console.log(PromptArrayItemForRawPromptDisplay);
5927- console.log(itemizedPrompts);
5928- console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5929-
5930- const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
5931-
5932- // Mobile needs special handholding. The side-view on the popup wouldn't work,
5933- // so we just show an additional popup for this.
5934- if (isMobile()) {
5935- const content = document.createElement('div');
5936- content.classList.add('tokenItemizingMaintext');
5937- content.innerText = rawPrompt;
5938- const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
5939- await popup.show();
5940- return;
5941- }
5942-
5943- //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
5944- const rawPromptWrapper = document.getElementById('rawPromptWrapper');
5945- rawPromptWrapper.innerText = rawPrompt;
5946- $('#rawPromptPopup').slideToggle();
5947- });
5948-
5949- await popup.show();
5950-}
5951-
59525647function setInContextMessages(msgInContextCount, type) {
59535648 $('#chat .mes').removeClass('lastInContext');
59545649
@@ -10474,15 +10169,6 @@ function addDebugFunctions() {
1047410169 const message = await generateRaw(text, null, false, false);
1047510170 alert(message);
1047610171 });
10477-
10478- registerDebugFunction('clearPrompts', 'Delete itemized prompts', 'Deletes all itemized prompts from the local storage.', async () => {
10479- await clearItemizedPrompts();
10480- toastr.info('Itemized prompts deleted.');
10481- if (getCurrentChatId()) {
10482- await reloadCurrentChat();
10483- }
10484- });
10485-
1048610172 registerDebugFunction('toggleEventTracing', 'Toggle event tracing', 'Useful to see what triggered a certain event.', () => {
1048710173 localStorage.setItem('eventTracing', localStorage.getItem('eventTracing') === 'true' ? 'false' : 'true');
1048810174 toastr.info('Event tracing is now ' + (localStorage.getItem('eventTracing') === 'true' ? 'enabled' : 'disabled'));
@@ -11614,14 +11300,6 @@ jQuery(async function () {
1161411300 }
1161511301 });
1161611302
11617- $(document).on('pointerup', '.mes_prompt', async function () {
11618- let mesIdForItemization = $(this).closest('.mes').attr('mesId');
11619- console.log(`looking for mesID: ${mesIdForItemization}`);
11620- if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
11621- await promptItemize(itemizedPrompts, mesIdForItemization);
11622- }
11623- });
11624-
1162511303 //********************
1162611304 //***Message Editor***
1162711305 $(document).on('click', '.mes_edit', async function () {
@@ -12517,13 +12195,6 @@ jQuery(async function () {
1251712195 // Added here to prevent execution before script.js is loaded and get rid of quirky timeouts
1251812196 await firstLoadInit();
1251912197
12520- eventSource.on(event_types.CHAT_DELETED, async (name) => {
12521- await deleteItemizedPrompts(name);
12522- });
12523- eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => {
12524- await deleteItemizedPrompts(name);
12525- });
12526-
1252712198 window.addEventListener('beforeunload', (e) => {
1252812199 if (isChatSaving) {
1252912200 e.preventDefault();
public/scripts/itemized-prompts.js+352 -0
@@ -0,0 +1,352 @@
1+import { DiffMatchPatch, DOMPurify, localforage } from '../lib.js';
2+import { chat, event_types, eventSource, getCurrentChatId, reloadCurrentChat } from '../script.js';
3+import { t } from './i18n.js';
4+import { oai_settings } from './openai.js';
5+import { Popup, POPUP_TYPE } from './popup.js';
6+import { power_user, registerDebugFunction } from './power-user.js';
7+import { isMobile } from './RossAscends-mods.js';
8+import { renderTemplateAsync } from './templates.js';
9+import { getFriendlyTokenizerName, getTokenCountAsync } from './tokenizers.js';
10+import { copyText } from './utils.js';
11+
12+let PromptArrayItemForRawPromptDisplay;
13+let priorPromptArrayItemForRawPromptDisplay;
14+
15+const promptStorage = localforage.createInstance({ name: 'SillyTavern_Prompts' });
16+export let itemizedPrompts = [];
17+
18+/**
19+ * Gets the itemized prompts for a chat.
20+ * @param {string} chatId Chat ID to load
21+ */
22+export async function loadItemizedPrompts(chatId) {
23+ try {
24+ if (!chatId) {
25+ itemizedPrompts = [];
26+ return;
27+ }
28+
29+ itemizedPrompts = await promptStorage.getItem(chatId);
30+
31+ if (!itemizedPrompts) {
32+ itemizedPrompts = [];
33+ }
34+ } catch {
35+ console.log('Error loading itemized prompts for chat', chatId);
36+ itemizedPrompts = [];
37+ }
38+}
39+
40+/**
41+ * Saves the itemized prompts for a chat.
42+ * @param {string} chatId Chat ID to save itemized prompts for
43+ */
44+export async function saveItemizedPrompts(chatId) {
45+ try {
46+ if (!chatId) {
47+ return;
48+ }
49+
50+ await promptStorage.setItem(chatId, itemizedPrompts);
51+ } catch {
52+ console.log('Error saving itemized prompts for chat', chatId);
53+ }
54+}
55+
56+/**
57+ * Replaces the itemized prompt text for a message.
58+ * @param {number} mesId Message ID to get itemized prompt for
59+ * @param {string} promptText New raw prompt text
60+ * @returns
61+ */
62+export async function replaceItemizedPromptText(mesId, promptText) {
63+ if (!Array.isArray(itemizedPrompts)) {
64+ itemizedPrompts = [];
65+ }
66+
67+ const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId);
68+
69+ if (!itemizedPrompt) {
70+ return;
71+ }
72+
73+ itemizedPrompt.rawPrompt = promptText;
74+}
75+
76+/**
77+ * Deletes the itemized prompts for a chat.
78+ * @param {string} chatId Chat ID to delete itemized prompts for
79+ */
80+export async function deleteItemizedPrompts(chatId) {
81+ try {
82+ if (!chatId) {
83+ return;
84+ }
85+
86+ await promptStorage.removeItem(chatId);
87+ } catch {
88+ console.log('Error deleting itemized prompts for chat', chatId);
89+ }
90+}
91+
92+/**
93+ * Empties the itemized prompts array and caches.
94+ */
95+export async function clearItemizedPrompts() {
96+ try {
97+ await promptStorage.clear();
98+ itemizedPrompts = [];
99+ } catch {
100+ console.log('Error clearing itemized prompts');
101+ }
102+}
103+
104+export async function itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId) {
105+ const params = {
106+ charDescriptionTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charDescription),
107+ charPersonalityTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].charPersonality),
108+ scenarioTextTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].scenarioText),
109+ userPersonaStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].userPersona),
110+ worldInfoStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].worldInfoString),
111+ allAnchorsTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].allAnchors),
112+ summarizeStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].summarizeString),
113+ authorsNoteStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].authorsNoteString),
114+ smartContextStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].smartContextString),
115+ beforeScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].beforeScenarioAnchor),
116+ afterScenarioAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].afterScenarioAnchor),
117+ zeroDepthAnchorTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused
118+ thisPrompt_padding: itemizedPrompts[thisPromptSet].padding,
119+ this_main_api: itemizedPrompts[thisPromptSet].main_api,
120+ chatInjects: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatInjects),
121+ chatVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].chatVectorsString),
122+ dataBankVectorsStringTokens: await getTokenCountAsync(itemizedPrompts[thisPromptSet].dataBankVectorsString),
123+ modelUsed: chat[incomingMesId]?.extra?.model,
124+ apiUsed: chat[incomingMesId]?.extra?.api,
125+ presetName: itemizedPrompts[thisPromptSet].presetName || t`(Unknown)`,
126+ messagesCount: String(itemizedPrompts[thisPromptSet].messagesCount ?? ''),
127+ examplesCount: String(itemizedPrompts[thisPromptSet].examplesCount ?? ''),
128+ };
129+
130+ const getFriendlyName = (value) => $(`#rm_api_block select option[value="${value}"]`).first().text() || value;
131+
132+ if (params.apiUsed) {
133+ params.apiUsed = getFriendlyName(params.apiUsed);
134+ }
135+
136+ if (params.this_main_api) {
137+ params.mainApiFriendlyName = getFriendlyName(params.this_main_api);
138+ }
139+
140+ if (params.chatInjects) {
141+ params.ActualChatHistoryTokens = params.ActualChatHistoryTokens - params.chatInjects;
142+ }
143+
144+ if (params.this_main_api == 'openai') {
145+ //for OAI API
146+ //console.log('-- Counting OAI Tokens');
147+
148+ //params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
149+ params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
150+ params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
151+ params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
152+ params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
153+ params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens;
154+ params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
155+ params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
156+ params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
157+ params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
158+ params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
159+ params.finalPromptTokens =
160+ params.oaiStartTokens +
161+ params.oaiPromptTokens +
162+ params.oaiMainTokens +
163+ params.oaiNsfwTokens +
164+ params.oaiBiasTokens +
165+ params.oaiImpersonateTokens +
166+ params.oaiJailbreakTokens +
167+ params.oaiNudgeTokens +
168+ params.ActualChatHistoryTokens +
169+ //charDescriptionTokens +
170+ //charPersonalityTokens +
171+ //allAnchorsTokens +
172+ params.worldInfoStringTokens +
173+ params.beforeScenarioAnchorTokens +
174+ params.afterScenarioAnchorTokens;
175+ // Max context size - max completion tokens
176+ params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
177+
178+ //console.log('-- applying % on OAI tokens');
179+ params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2);
180+ params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2);
181+ params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2);
182+ params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2);
183+ params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2);
184+ params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2);
185+ params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
186+ params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens;
187+ params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2);
188+ } else {
189+ //for non-OAI APIs
190+ //console.log('-- Counting non-OAI Tokens');
191+ params.finalPromptTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].finalPrompt);
192+ params.storyStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens;
193+ params.examplesStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].examplesString);
194+ params.mesSendStringTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].mesSendString);
195+ params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding;
196+ params.instructionTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].instruction);
197+ params.promptBiasTokens = await getTokenCountAsync(itemizedPrompts[thisPromptSet].promptBias);
198+
199+ params.totalTokensInPrompt =
200+ params.storyStringTokens + //chardefs total
201+ params.worldInfoStringTokens +
202+ params.examplesStringTokens + // example messages
203+ params.ActualChatHistoryTokens + //chat history
204+ params.allAnchorsTokens + // AN and/or legacy anchors
205+ //afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
206+ //zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
207+ params.promptBiasTokens; //{{}}
208+ //- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
209+ params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
210+ params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding;
211+
212+ //console.log('-- applying % on non-OAI tokens');
213+ params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
214+ params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
215+ params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
216+ params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
217+ params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
218+ params.selectedTokenizer = itemizedPrompts[thisPromptSet]?.tokenizer || getFriendlyTokenizerName(params.this_main_api).tokenizerName;
219+ }
220+ return params;
221+}
222+
223+export function findItemizedPromptSet(itemizedPrompts, incomingMesId) {
224+ let thisPromptSet = undefined;
225+
226+ for (let i = 0; i < itemizedPrompts.length; i++) {
227+ console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`);
228+ if (itemizedPrompts[i].mesId === incomingMesId) {
229+ console.log(`found matching mesID ${i}`);
230+ thisPromptSet = i;
231+ PromptArrayItemForRawPromptDisplay = i;
232+ console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
233+ console.log(itemizedPrompts[thisPromptSet]);
234+ break;
235+ } else if (itemizedPrompts[i].rawPrompt) {
236+ priorPromptArrayItemForRawPromptDisplay = i;
237+ }
238+ }
239+ return thisPromptSet;
240+}
241+
242+export async function promptItemize(itemizedPrompts, requestedMesId) {
243+ console.log('PROMPT ITEMIZE ENTERED');
244+ var incomingMesId = Number(requestedMesId);
245+ console.debug(`looking for MesId ${incomingMesId}`);
246+ var thisPromptSet = findItemizedPromptSet(itemizedPrompts, incomingMesId);
247+
248+ if (thisPromptSet === undefined) {
249+ console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
250+ console.log(itemizedPrompts);
251+ return null;
252+ }
253+
254+ const params = await itemizedParams(itemizedPrompts, thisPromptSet, incomingMesId);
255+ const flatten = (rawPrompt) => Array.isArray(rawPrompt) ? rawPrompt.map(x => x.content).join('\n') : rawPrompt;
256+
257+ const template = params.this_main_api == 'openai'
258+ ? await renderTemplateAsync('itemizationChat', params)
259+ : await renderTemplateAsync('itemizationText', params);
260+
261+ const popup = new Popup(template, POPUP_TYPE.TEXT);
262+
263+ /** @type {HTMLElement} */
264+ const diffPrevPrompt = popup.dlg.querySelector('#diffPrevPrompt');
265+ if (priorPromptArrayItemForRawPromptDisplay) {
266+ diffPrevPrompt.style.display = '';
267+ diffPrevPrompt.addEventListener('click', function () {
268+ const dmp = new DiffMatchPatch();
269+ const text1 = flatten(itemizedPrompts[priorPromptArrayItemForRawPromptDisplay].rawPrompt);
270+ const text2 = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
271+
272+ dmp.Diff_Timeout = 2.0;
273+
274+ const d = dmp.diff_main(text1, text2);
275+ let ds = dmp.diff_prettyHtml(d);
276+ // make it readable
277+ ds = ds.replaceAll('background:#e6ffe6;', 'background:#b9f3b9; color:black;');
278+ ds = ds.replaceAll('background:#ffe6e6;', 'background:#f5b4b4; color:black;');
279+ ds = ds.replaceAll('&para;', '');
280+ const container = document.createElement('div');
281+ container.innerHTML = DOMPurify.sanitize(ds);
282+ const rawPromptWrapper = document.getElementById('rawPromptWrapper');
283+ rawPromptWrapper.replaceChildren(container);
284+ $('#rawPromptPopup').slideToggle();
285+ });
286+ } else {
287+ diffPrevPrompt.style.display = 'none';
288+ }
289+ popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('pointerup', async function () {
290+ let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt;
291+ let rawPromptValues = rawPrompt;
292+
293+ if (Array.isArray(rawPrompt)) {
294+ rawPromptValues = rawPrompt.map(x => x.content).join('\n');
295+ }
296+
297+ await copyText(rawPromptValues);
298+ toastr.info(t`Copied!`);
299+ });
300+
301+ popup.dlg.querySelector('#showRawPrompt').addEventListener('click', async function () {
302+ //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
303+ console.log(PromptArrayItemForRawPromptDisplay);
304+ console.log(itemizedPrompts);
305+ console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
306+
307+ const rawPrompt = flatten(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt);
308+
309+ // Mobile needs special handholding. The side-view on the popup wouldn't work,
310+ // so we just show an additional popup for this.
311+ if (isMobile()) {
312+ const content = document.createElement('div');
313+ content.classList.add('tokenItemizingMaintext');
314+ content.innerText = rawPrompt;
315+ const popup = new Popup(content, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, leftAlign: true });
316+ await popup.show();
317+ return;
318+ }
319+
320+ //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '<br>');
321+ const rawPromptWrapper = document.getElementById('rawPromptWrapper');
322+ rawPromptWrapper.innerText = rawPrompt;
323+ $('#rawPromptPopup').slideToggle();
324+ });
325+
326+ await popup.show();
327+}
328+
329+export function initItemizedPrompts() {
330+ registerDebugFunction('clearPrompts', 'Delete itemized prompts', 'Deletes all itemized prompts from the local storage.', async () => {
331+ await clearItemizedPrompts();
332+ toastr.info('Itemized prompts deleted.');
333+ if (getCurrentChatId()) {
334+ await reloadCurrentChat();
335+ }
336+ });
337+
338+ $(document).on('pointerup', '.mes_prompt', async function () {
339+ let mesIdForItemization = $(this).closest('.mes').attr('mesId');
340+ console.log(`looking for mesID: ${mesIdForItemization}`);
341+ if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) {
342+ await promptItemize(itemizedPrompts, mesIdForItemization);
343+ }
344+ });
345+
346+ eventSource.on(event_types.CHAT_DELETED, async (name) => {
347+ await deleteItemizedPrompts(name);
348+ });
349+ eventSource.on(event_types.GROUP_CHAT_DELETED, async (name) => {
350+ await deleteItemizedPrompts(name);
351+ });
352+}