import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods } from './scripts/RossAscends-mods.js';
import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js';
import {
generateKoboldWithStreaming,
kai_settings,
loadKoboldSettings,
formatKoboldUrl,
getKoboldGenerationData,
kai_flags,
setKoboldFlags,
} from './scripts/kai-settings.js';
import {
textgenerationwebui_settings as textgen_settings,
loadTextGenSettings,
generateTextGenWithStreaming,
getTextGenGenerationData,
textgen_types,
getTextGenServer,
validateTextGenUrl,
} from './scripts/textgen-settings.js';
const { MANCER, TOGETHERAI, OOBA, APHRODITE, OLLAMA } = textgen_types;
import {
world_info,
getWorldInfoPrompt,
getWorldInfoSettings,
setWorldInfoSettings,
world_names,
importEmbeddedWorldInfo,
checkEmbeddedWorld,
setWorldInfoButtonClass,
importWorldInfo,
} from './scripts/world-info.js';
import {
groups,
selected_group,
saveGroupChat,
getGroups,
generateGroupWrapper,
deleteGroup,
is_group_generating,
resetSelectedGroup,
select_group_chats,
regenerateGroup,
group_generation_id,
getGroupChat,
renameGroupMember,
createNewGroupChat,
getGroupPastChats,
getGroupAvatar,
openGroupChat,
editGroup,
deleteGroupChat,
renameGroupChat,
importGroupChat,
getGroupBlock,
getGroupCharacterCards,
getGroupDepthPrompts,
} from './scripts/group-chats.js';
import {
collapseNewlines,
loadPowerUserSettings,
playMessageSound,
fixMarkdown,
power_user,
persona_description_positions,
loadMovingUIState,
getCustomStoppingStrings,
MAX_CONTEXT_DEFAULT,
MAX_RESPONSE_DEFAULT,
renderStoryString,
sortEntitiesList,
registerDebugFunction,
ui_mode,
switchSimpleMode,
flushEphemeralStoppingStrings,
context_presets,
} from './scripts/power-user.js';
import {
setOpenAIMessageExamples,
setOpenAIMessages,
setupChatCompletionPromptManager,
prepareOpenAIMessages,
sendOpenAIRequest,
loadOpenAISettings,
oai_settings,
openai_messages_count,
chat_completion_sources,
getChatCompletionModel,
isOpenRouterWithInstruct,
proxies,
loadProxyPresets,
selected_proxy,
} from './scripts/openai.js';
import {
generateNovelWithStreaming,
getNovelGenerationData,
getKayraMaxContextTokens,
getNovelTier,
loadNovelPreset,
loadNovelSettings,
nai_settings,
adjustNovelInstructionPrompt,
loadNovelSubscriptionData,
parseNovelAILogprobs,
} from './scripts/nai-settings.js';
import {
createNewBookmark,
showBookmarksButtons,
createBranch,
} from './scripts/bookmarks.js';
import {
horde_settings,
loadHordeSettings,
generateHorde,
checkHordeStatus,
getHordeModels,
adjustHordeGenerationParams,
MIN_LENGTH,
} from './scripts/horde.js';
import {
debounce,
delay,
trimToEndSentence,
countOccurrences,
isOdd,
sortMoments,
timestampToMoment,
download,
isDataURL,
getCharaFilename,
PAGINATION_TEMPLATE,
waitUntilCondition,
escapeRegex,
resetScrollHeight,
onlyUnique,
getBase64Async,
humanFileSize,
Stopwatch,
} from './scripts/utils.js';
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
import { COMMENT_NAME_DEFAULT, executeSlashCommands, getSlashCommandsHelp, processChatSlashCommands, registerSlashCommand } from './scripts/slash-commands.js';
import {
tag_map,
tags,
loadTagsSettings,
printTagFilters,
getTagsList,
appendTagToList,
createTagMapFromList,
renameTagKey,
importTags,
tag_filter_types,
} from './scripts/tags.js';
import {
SECRET_KEYS,
readSecretState,
secret_state,
writeSecret,
} from './scripts/secrets.js';
import { EventEmitter } from './lib/eventemitter.js';
import { markdownExclusionExt } from './scripts/showdown-exclusion.js';
import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js';
import { registerPromptManagerMigration } from './scripts/PromptManager.js';
import { getRegexedString, regex_placement } from './scripts/extensions/regex/engine.js';
import { initLogprobs, saveLogprobsForActiveMessage } from './scripts/logprobs.js';
import { FILTER_TYPES, FilterHelper } from './scripts/filters.js';
import { getCfgPrompt, getGuidanceScale, initCfg } from './scripts/cfg-scale.js';
import {
force_output_sequence,
formatInstructModeChat,
formatInstructModePrompt,
formatInstructModeExamples,
getInstructStoppingSequences,
autoSelectInstructPreset,
formatInstructModeSystemPrompt,
selectInstructPreset,
instruct_presets,
selectContextPreset,
} from './scripts/instruct-mode.js';
import { applyLocale, initLocales } from './scripts/i18n.js';
import { getFriendlyTokenizerName, getTokenCount, getTokenizerModel, initTokenizers, saveTokenCache } from './scripts/tokenizers.js';
import { createPersona, initPersonas, selectCurrentPersona, setPersonaDescription, updatePersonaNameIfExists } from './scripts/personas.js';
import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
import { hideLoader, showLoader } from './scripts/loader.js';
import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
import { loadMancerModels, loadOllamaModels, loadTogetherAIModels } from './scripts/textgen-models.js';
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags } from './scripts/chats.js';
import { initPresetManager } from './scripts/preset-manager.js';
import { evaluateMacros } from './scripts/macros.js';
//exporting functions and vars for mods
export {
Generate,
cleanUpMessage,
getSettings,
saveSettings,
saveSettingsDebounced,
printMessages,
clearChat,
getChat,
getCharacters,
getGeneratingApi,
callPopup,
substituteParams,
sendSystemMessage,
addOneMessage,
deleteLastMessage,
resetChatState,
select_rm_info,
setCharacterId,
setCharacterName,
replaceCurrentChat,
setOnlineStatus,
displayOnlineStatus,
setEditedMessageId,
setSendButtonState,
selectRightMenuWithAnimation,
openCharacterChat,
saveChat,
messageFormatting,
getExtensionPrompt,
getExtensionPromptByName,
showSwipeButtons,
hideSwipeButtons,
changeMainAPI,
setGenerationProgress,
updateChatMetadata,
scrollChatToBottom,
isStreamingEnabled,
getThumbnailUrl,
getStoppingStrings,
reloadMarkdownProcessor,
getCurrentChatId,
chat,
this_chid,
selected_button,
menu_type,
settings,
characters,
online_status,
main_api,
api_server,
system_messages,
nai_settings,
token,
name1,
name2,
is_send_press,
max_context,
chat_metadata,
streamingProcessor,
default_avatar,
system_message_types,
talkativeness_default,
default_ch_mes,
extension_prompt_types,
mesForShowdownParse,
printCharacters,
isOdd,
countOccurrences,
};
showLoader();
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
document.getElementById('preloader').remove();
// Allow target="_blank" in links
DOMPurify.addHook('afterSanitizeAttributes', function (node) {
if ('target' in node) {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener');
}
});
DOMPurify.addHook('uponSanitizeAttribute', (_, data, config) => {
if (!config['MESSAGE_SANITIZE']) {
return;
}
switch (data.attrName) {
case 'class': {
if (data.attrValue) {
data.attrValue = data.attrValue.split(' ').map((v) => {
if (v.startsWith('fa-') || v.startsWith('note-') || v === 'monospace') {
return v;
}
return 'custom-' + v;
}).join(' ');
}
break;
}
}
});
DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
if (!config['MESSAGE_SANITIZE']) {
return;
}
if (!power_user.forbid_external_images) {
return;
}
switch (node.tagName) {
case 'AUDIO':
case 'VIDEO':
case 'SOURCE':
case 'TRACK':
case 'EMBED':
case 'OBJECT':
case 'IMG': {
const isExternalUrl = (url) => (url.indexOf('://') > 0 || url.indexOf('//') === 0) && !url.startsWith(window.location.origin);
const src = node.getAttribute('src');
const data = node.getAttribute('data');
const srcset = node.getAttribute('srcset');
if (srcset) {
const srcsetUrls = srcset.split(',');
for (const srcsetUrl of srcsetUrls) {
const [url] = srcsetUrl.trim().split(' ');
if (isExternalUrl(url)) {
console.warn('External media blocked', url);
node.remove();
break;
}
}
}
if (src && isExternalUrl(src)) {
console.warn('External media blocked', src);
node.remove();
}
if (data && isExternalUrl(data)) {
console.warn('External media blocked', data);
node.remove();
}
}
break;
}
});
// API OBJECT FOR EXTERNAL WIRING
window['SillyTavern'] = {};
// Event source init
export const event_types = {
APP_READY: 'app_ready',
EXTRAS_CONNECTED: 'extras_connected',
MESSAGE_SWIPED: 'message_swiped',
MESSAGE_SENT: 'message_sent',
MESSAGE_RECEIVED: 'message_received',
MESSAGE_EDITED: 'message_edited',
MESSAGE_DELETED: 'message_deleted',
IMPERSONATE_READY: 'impersonate_ready',
CHAT_CHANGED: 'chat_id_changed',
GENERATION_STARTED: 'generation_started',
GENERATION_STOPPED: 'generation_stopped',
GENERATION_ENDED: 'generation_ended',
EXTENSIONS_FIRST_LOAD: 'extensions_first_load',
SETTINGS_LOADED: 'settings_loaded',
SETTINGS_UPDATED: 'settings_updated',
GROUP_UPDATED: 'group_updated',
MOVABLE_PANELS_RESET: 'movable_panels_reset',
SETTINGS_LOADED_BEFORE: 'settings_loaded_before',
SETTINGS_LOADED_AFTER: 'settings_loaded_after',
CHATCOMPLETION_SOURCE_CHANGED: 'chatcompletion_source_changed',
CHATCOMPLETION_MODEL_CHANGED: 'chatcompletion_model_changed',
OAI_PRESET_CHANGED_BEFORE: 'oai_preset_changed_before',
OAI_PRESET_CHANGED_AFTER: 'oai_preset_changed_after',
WORLDINFO_SETTINGS_UPDATED: 'worldinfo_settings_updated',
WORLDINFO_UPDATED: 'worldinfo_updated',
CHARACTER_EDITED: 'character_edited',
CHARACTER_PAGE_LOADED: 'character_page_loaded',
CHARACTER_GROUP_OVERLAY_STATE_CHANGE_BEFORE: 'character_group_overlay_state_change_before',
CHARACTER_GROUP_OVERLAY_STATE_CHANGE_AFTER: 'character_group_overlay_state_change_after',
USER_MESSAGE_RENDERED: 'user_message_rendered',
CHARACTER_MESSAGE_RENDERED: 'character_message_rendered',
FORCE_SET_BACKGROUND: 'force_set_background',
CHAT_DELETED: 'chat_deleted',
GROUP_CHAT_DELETED: 'group_chat_deleted',
GENERATE_BEFORE_COMBINE_PROMPTS: 'generate_before_combine_prompts',
GROUP_MEMBER_DRAFTED: 'group_member_drafted',
};
export const eventSource = new EventEmitter();
eventSource.on(event_types.CHAT_CHANGED, processChatSlashCommands);
const characterGroupOverlay = new BulkEditOverlay();
const characterContextMenu = new CharacterContextMenu(characterGroupOverlay);
eventSource.on(event_types.CHARACTER_PAGE_LOADED, characterGroupOverlay.onPageLoad);
console.debug('Character context menu initialized', characterContextMenu);
hljs.addPlugin({ 'before:highlightElement': ({ el }) => { el.textContent = el.innerText; } });
// Markdown converter
let mesForShowdownParse; //intended to be used as a context to compare showdown strings against
let converter;
reloadMarkdownProcessor();
// array for prompt token calculations
console.debug('initializing Prompt Itemization Array on Startup');
const promptStorage = new localforage.createInstance({ name: 'SillyTavern_Prompts' });
let itemizedPrompts = [];
export const systemUserName = 'SillyTavern System';
let default_user_name = 'User';
let name1 = default_user_name;
let name2 = 'SillyTavern System';
let chat = [];
let safetychat = [
{
name: systemUserName,
is_user: false,
create_date: 0,
mes: 'You deleted a character/chat and arrived back here for safety reasons! Pick another character!',
},
];
let chatSaveTimeout;
let importFlashTimeout;
export let isChatSaving = false;
let chat_create_date = 0;
let firstRun = false;
let settingsReady = false;
let currentVersion = '0.0.0';
const default_ch_mes = 'Hello';
let generatedPromptCache = '';
let generation_started = new Date();
let characters = [];
let this_chid;
let saveCharactersPage = 0;
let savePersonasPage = 0;
const default_avatar = 'img/ai4.png';
export const system_avatar = 'img/five.png';
export const comment_avatar = 'img/quill.png';
export let CLIENT_VERSION = 'SillyTavern:UNKNOWN:Cohee#1207'; // For Horde header
let optionsPopper = Popper.createPopper(document.getElementById('options_button'), document.getElementById('options'), {
placement: 'top-start',
});
let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), {
placement: 'left',
});
let rawPromptPopper = Popper.createPopper(document.getElementById('dialogue_popup'), document.getElementById('rawPromptPopup'), {
placement: 'right',
});
let dialogueResolve = null;
let dialogueCloseStop = false;
let chat_metadata = {};
let streamingProcessor = null;
let crop_data = undefined;
let is_delete_mode = false;
let fav_ch_checked = false;
let scrollLock = false;
export let abortStatusCheck = new AbortController();
const durationSaveEdit = 1000;
const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
export const saveCharacterDebounced = debounce(() => $('#create_button').trigger('click'), durationSaveEdit);
const system_message_types = {
HELP: 'help',
WELCOME: 'welcome',
GROUP: 'group',
EMPTY: 'empty',
GENERIC: 'generic',
BOOKMARK_CREATED: 'bookmark_created',
BOOKMARK_BACK: 'bookmark_back',
NARRATOR: 'narrator',
COMMENT: 'comment',
SLASH_COMMANDS: 'slash_commands',
FORMATTING: 'formatting',
HOTKEYS: 'hotkeys',
MACROS: 'macros',
};
const extension_prompt_types = {
IN_PROMPT: 0,
IN_CHAT: 1,
BEFORE_PROMPT: 2,
};
export const MAX_INJECTION_DEPTH = 1000;
let system_messages = {};
function getSystemMessages() {
system_messages = {
help: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: renderTemplate('help'),
},
slash_commands: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: '',
},
hotkeys: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: renderTemplate('hotkeys'),
},
formatting: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: renderTemplate('formatting'),
},
macros: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: renderTemplate('macros'),
},
welcome:
{
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: renderTemplate('welcome'),
},
group: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
is_group: true,
mes: 'Group chat created. Say \'Hi\' to lovely people!',
},
empty: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: 'No one hears you. Hint: add more members to the group!',
},
generic: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: 'Generic system message. User `text` parameter to override the contents',
},
bookmark_created: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: 'Checkpoint created! Click here to open the checkpoint chat: {1}',
},
bookmark_back: {
name: systemUserName,
force_avatar: system_avatar,
is_user: false,
is_system: true,
mes: 'Click here to return to the previous chat: Return',
},
};
}
// Register configuration migrations
registerPromptManagerMigration();
$(document).ajaxError(function myErrorHandler(_, xhr) {
if (xhr.status == 403) {
toastr.warning(
'doubleCsrf errors in console are NORMAL in this case. If you want to run ST in multiple tabs, start the server with --disableCsrf option.',
'Looks like you\'ve opened SillyTavern in another browser tab',
{ timeOut: 0, extendedTimeOut: 0, preventDuplicates: true },
);
}
});
/**
* 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() {
try {
const response = await fetch('/version');
const data = await response.json();
CLIENT_VERSION = data.agent;
let displayVersion = `SillyTavern ${data.pkgVersion}`;
currentVersion = data.pkgVersion;
if (data.gitRevision && data.gitBranch) {
displayVersion += ` '${data.gitBranch}' (${data.gitRevision})`;
}
$('#version_display').text(displayVersion);
$('#version_display_welcome').text(displayVersion);
} catch (err) {
console.error('Couldn\'t get client version', err);
}
}
function reloadMarkdownProcessor(render_formulas = false) {
if (render_formulas) {
converter = new showdown.Converter({
emoji: true,
underline: true,
tables: true,
parseImgDimensions: true,
extensions: [
showdownKatex(
{
delimiters: [
{ left: '$$', right: '$$', display: true, asciimath: false },
{ left: '$', right: '$', display: false, asciimath: true },
],
},
)],
});
}
else {
converter = new showdown.Converter({
emoji: true,
literalMidWordUnderscores: true,
parseImgDimensions: true,
tables: true,
});
}
// Inject the dinkus extension after creating the converter
// Maybe move this into power_user init?
setTimeout(() => {
if (power_user) {
converter.addExtension(markdownExclusionExt(), 'exclusion');
}
}, 1);
return converter;
}
function getCurrentChatId() {
console.debug(`selectedGroup:${selected_group}, this_chid:${this_chid}`);
if (selected_group) {
return groups.find(x => x.id == selected_group)?.chat_id;
}
else if (this_chid) {
return characters[this_chid]?.chat;
}
}
const talkativeness_default = 0.5;
export const depth_prompt_depth_default = 4;
const per_page_default = 50;
var is_advanced_char_open = false;
var menu_type = ''; //what is selected in the menu
var selected_button = ''; //which button pressed
//create pole save
let create_save = {
name: '',
description: '',
creator_notes: '',
post_history_instructions: '',
character_version: '',
system_prompt: '',
tags: '',
creator: '',
personality: '',
first_message: '',
avatar: '',
scenario: '',
mes_example: '',
world: '',
talkativeness: talkativeness_default,
alternate_greetings: [],
depth_prompt_prompt: '',
depth_prompt_depth: depth_prompt_depth_default,
};
//animation right menu
export const ANIMATION_DURATION_DEFAULT = 125;
export let animation_duration = ANIMATION_DURATION_DEFAULT;
let animation_easing = 'ease-in-out';
let popup_type = '';
let chat_file_for_del = '';
let online_status = 'no_connection';
let api_server = '';
let is_send_press = false; //Send generation
let this_del_mes = -1;
//message editing and chat scroll position persistence
var this_edit_mes_chname = '';
var this_edit_mes_id;
var scroll_holder = 0;
var is_use_scroll_holder = false;
//settings
var settings;
export let koboldai_settings;
export let koboldai_setting_names;
var preset_settings = 'gui';
export let user_avatar = 'you.png';
export var amount_gen = 80; //default max length of AI generated responses
var max_context = 2048;
var swipes = true;
let extension_prompts = {};
var main_api;// = "kobold";
//novel settings
export let novelai_settings;
export let novelai_setting_names;
let abortController;
//css
var css_mes_bg = $('
').css('background');
var css_send_form_display = $('').css('display');
const MAX_GENERATION_LOOPS = 5;
var kobold_horde_model = '';
let token;
var PromptArrayItemForRawPromptDisplay;
export let active_character = '';
export let active_group = '';
export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100));
export const personasFilter = new FilterHelper(debounce(getUserAvatars, 100));
export function getRequestHeaders() {
return {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
};
}
$.ajaxPrefilter((options, originalOptions, xhr) => {
xhr.setRequestHeader('X-CSRF-Token', token);
});
async function firstLoadInit() {
try {
const tokenResponse = await fetch('/csrf-token');
const tokenData = await tokenResponse.json();
token = tokenData.token;
} catch {
hideLoader();
toastr.error('Couldn\'t get CSRF token. Please refresh the page.', 'Error', { timeOut: 0, extendedTimeOut: 0, preventDuplicates: true });
throw new Error('Initialization failed');
}
getSystemMessages();
sendSystemMessage(system_message_types.WELCOME);
initLocales();
await readSecretState();
await getClientVersion();
await getSettings();
await getUserAvatars(true, user_avatar);
await getCharacters();
await getBackgrounds();
await initTokenizers();
await initPresetManager();
initBackgrounds();
initAuthorsNote();
initPersonas();
initRossMods();
initStats();
initCfg();
initLogprobs();
doDailyExtensionUpdatesCheck();
hideLoader();
await eventSource.emit(event_types.APP_READY);
}
function cancelStatusCheck() {
abortStatusCheck?.abort();
abortStatusCheck = new AbortController();
setOnlineStatus('no_connection');
}
function displayOnlineStatus() {
if (online_status == 'no_connection') {
$('.online_status_indicator').removeClass('success');
$('.online_status_text').text('No connection...');
} else {
$('.online_status_indicator').addClass('success');
$('.online_status_text').text(online_status);
}
}
/**
* Sets the duration of JS animations.
* @param {number} ms Duration in milliseconds. Resets to default if null.
*/
export function setAnimationDuration(ms = null) {
animation_duration = ms ?? ANIMATION_DURATION_DEFAULT;
}
export function setActiveCharacter(character) {
active_character = character;
}
export function setActiveGroup(group) {
active_group = group;
}
/**
* Gets the itemized prompts for a chat.
* @param {string} chatId Chat ID to load
*/
export async function loadItemizedPrompts(chatId) {
try {
if (!chatId) {
itemizedPrompts = [];
return;
}
itemizedPrompts = await promptStorage.getItem(chatId);
if (!itemizedPrompts) {
itemizedPrompts = [];
}
} catch {
console.log('Error loading itemized prompts for chat', chatId);
itemizedPrompts = [];
}
}
/**
* Saves the itemized prompts for a chat.
* @param {string} chatId Chat ID to save itemized prompts for
*/
export async function saveItemizedPrompts(chatId) {
try {
if (!chatId) {
return;
}
await promptStorage.setItem(chatId, itemizedPrompts);
} catch {
console.log('Error saving itemized prompts for chat', chatId);
}
}
/**
* Replaces the itemized prompt text for a message.
* @param {number} mesId Message ID to get itemized prompt for
* @param {string} promptText New raw prompt text
* @returns
*/
export async function replaceItemizedPromptText(mesId, promptText) {
if (!Array.isArray(itemizedPrompts)) {
itemizedPrompts = [];
}
const itemizedPrompt = itemizedPrompts.find(x => x.mesId === mesId);
if (!itemizedPrompt) {
return;
}
itemizedPrompt.rawPrompt = promptText;
}
/**
* Deletes the itemized prompts for a chat.
* @param {string} chatId Chat ID to delete itemized prompts for
*/
export async function deleteItemizedPrompts(chatId) {
try {
if (!chatId) {
return;
}
await promptStorage.removeItem(chatId);
} catch {
console.log('Error deleting itemized prompts for chat', chatId);
}
}
/**
* Empties the itemized prompts array and caches.
*/
export async function clearItemizedPrompts() {
try {
await promptStorage.clear();
itemizedPrompts = [];
} catch {
console.log('Error clearing itemized prompts');
}
}
async function getStatusHorde() {
try {
const hordeStatus = await checkHordeStatus();
online_status = hordeStatus ? 'Connected' : 'no_connection';
}
catch {
online_status = 'no_connection';
}
return resultCheckStatus();
}
async function getStatusKobold() {
let endpoint = api_server;
if (!endpoint) {
console.warn('No endpoint for status check');
online_status = 'no_connection';
return resultCheckStatus();
}
try {
const response = await fetch('/api/backends/kobold/status', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
main_api,
api_server: endpoint,
}),
signal: abortStatusCheck.signal,
});
const data = await response.json();
online_status = data?.model ?? 'no_connection';
if (!data.koboldUnitedVersion) {
throw new Error('Missing mandatory Kobold version in data:', data);
}
// Determine instruct mode preset
autoSelectInstructPreset(online_status);
// determine if we can use stop sequence and streaming
setKoboldFlags(data.koboldUnitedVersion, data.koboldCppVersion);
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
console.error('Error getting status', err);
online_status = 'no_connection';
}
return resultCheckStatus();
}
async function getStatusTextgen() {
const url = '/api/backends/text-completions/status';
const endpoint = getTextGenServer();
if (!endpoint) {
console.warn('No endpoint for status check');
online_status = 'no_connection';
return resultCheckStatus();
}
if (textgen_settings.type == OOBA && textgen_settings.bypass_status_check) {
online_status = 'Status check bypassed';
return resultCheckStatus();
}
try {
const response = await fetch(url, {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
api_server: endpoint,
api_type: textgen_settings.type,
legacy_api: textgen_settings.legacy_api && (textgen_settings.type === OOBA || textgen_settings.type === APHRODITE),
}),
signal: abortStatusCheck.signal,
});
const data = await response.json();
if (textgen_settings.type === MANCER) {
loadMancerModels(data?.data);
online_status = textgen_settings.mancer_model;
} else if (textgen_settings.type === TOGETHERAI) {
loadTogetherAIModels(data?.data);
online_status = textgen_settings.togetherai_model;
} else if (textgen_settings.type === OLLAMA) {
loadOllamaModels(data?.data);
online_status = textgen_settings.ollama_model || 'Connected';
} else {
online_status = data?.result;
}
if (!online_status) {
online_status = 'no_connection';
}
// Determine instruct mode preset
autoSelectInstructPreset(online_status);
// We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress.
if (online_status === 'no_connection' && data.response) {
toastr.error(data.response, 'API Error', { timeOut: 5000, preventDuplicates: true });
}
} catch (err) {
console.error('Error getting status', err);
online_status = 'no_connection';
}
return resultCheckStatus();
}
async function getStatusNovel() {
try {
const result = await loadNovelSubscriptionData();
if (!result) {
throw new Error('Could not load subscription data');
}
online_status = getNovelTier();
} catch {
online_status = 'no_connection';
}
resultCheckStatus();
}
export function startStatusLoading() {
$('.api_loading').show();
$('.api_button').addClass('disabled');
}
export function stopStatusLoading() {
$('.api_loading').hide();
$('.api_button').removeClass('disabled');
}
export function resultCheckStatus() {
displayOnlineStatus();
stopStatusLoading();
}
export async function selectCharacterById(id) {
if (characters[id] == undefined) {
return;
}
if (isChatSaving) {
toastr.info('Please wait until the chat is saved before switching characters.', 'Your chat is still saving...');
return;
}
if (selected_group && is_group_generating) {
return;
}
if (selected_group || this_chid !== id) {
//if clicked on a different character from what was currently selected
if (!is_send_press) {
await clearChat();
cancelTtsPlay();
resetSelectedGroup();
this_edit_mes_id = undefined;
selected_button = 'character_edit';
this_chid = id;
chat.length = 0;
chat_metadata = {};
await getChat();
}
} else {
//if clicked on character that was already selected
selected_button = 'character_edit';
select_selected_character(this_chid);
}
}
function getTagBlock(item, entities) {
let count = 0;
for (const entity of entities) {
if (entitiesFilter.isElementTagged(entity, item.id)) {
count++;
}
}
const template = $('#bogus_folder_template .bogus_folder_select').clone();
template.attr({ 'tagid': item.id, 'id': `BogusFolder${item.id}` });
template.find('.avatar').css({ 'background-color': item.color, 'color': item.color2 });
template.find('.ch_name').text(item.name);
template.find('.bogus_folder_counter').text(count);
return template;
}
function getBackBlock() {
const template = $('#bogus_folder_back_template .bogus_folder_select').clone();
return template;
}
function getEmptyBlock() {
const icons = ['fa-dragon', 'fa-otter', 'fa-kiwi-bird', 'fa-crow', 'fa-frog'];
const texts = ['Here be dragons', 'Otterly empty', 'Kiwibunga', 'Pump-a-Rum', 'Croak it'];
const roll = new Date().getMinutes() % icons.length;
const emptyBlock = `
${texts[roll]}
There are no items to display.
`;
return $(emptyBlock);
}
function getCharacterBlock(item, id) {
let this_avatar = default_avatar;
if (item.avatar != 'none') {
this_avatar = getThumbnailUrl('avatar', item.avatar);
}
// Populate the template
const template = $('#character_template .character_select').clone();
template.attr({ 'chid': id, 'id': `CharID${id}` });
template.find('img').attr('src', this_avatar);
template.find('.avatar').attr('title', item.avatar);
template.find('.ch_name').text(item.name);
if (power_user.show_card_avatar_urls) {
template.find('.ch_avatar_url').text(item.avatar);
}
template.find('.ch_fav_icon').css('display', 'none');
template.toggleClass('is_fav', item.fav || item.fav == 'true');
template.find('.ch_fav').val(item.fav);
const description = item.data?.creator_notes?.split('\n', 1)[0] || '';
if (description) {
template.find('.ch_description').text(description);
}
else {
template.find('.ch_description').hide();
}
const auxFieldName = power_user.aux_field || 'character_version';
const auxFieldValue = (item.data && item.data[auxFieldName]) || '';
if (auxFieldValue) {
template.find('.character_version').text(auxFieldValue);
}
else {
template.find('.character_version').hide();
}
// Display inline tags
const tags = getTagsList(item.avatar);
const tagsElement = template.find('.tags');
tags.forEach(tag => appendTagToList(tagsElement, tag, {}));
// Add to the list
return template;
}
async function printCharacters(fullRefresh = false) {
if (fullRefresh) {
saveCharactersPage = 0;
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
// Return to main list
if (isBogusFolderOpen()) {
entitiesFilter.setFilterData(FILTER_TYPES.TAG, { excluded: [], selected: [] });
}
await delay(1);
}
const storageKey = 'Characters_PerPage';
const listId = '#rm_print_characters_block';
const entities = getEntitiesList({ doFilter: true });
$('#rm_print_characters_pagination').pagination({
dataSource: entities,
pageSize: Number(localStorage.getItem(storageKey)) || per_page_default,
sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
pageRange: 1,
pageNumber: saveCharactersPage || 1,
position: 'top',
showPageNumbers: false,
showSizeChanger: true,
prevText: '<',
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
callback: function (data) {
$(listId).empty();
if (isBogusFolderOpen()) {
$(listId).append(getBackBlock());
}
if (!data.length) {
$(listId).append(getEmptyBlock());
}
for (const i of data) {
switch (i.type) {
case 'character':
$(listId).append(getCharacterBlock(i.item, i.id));
break;
case 'group':
$(listId).append(getGroupBlock(i.item));
break;
case 'tag':
$(listId).append(getTagBlock(i.item, entities));
break;
}
}
eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
},
afterSizeSelectorChange: function (e) {
localStorage.setItem(storageKey, e.target.value);
},
afterPaging: function (e) {
saveCharactersPage = e;
},
afterRender: function () {
$(listId).scrollTop(0);
},
});
favsToHotswap();
}
/**
* Indicates whether a user is currently in a bogus folder.
* @returns {boolean} If currently viewing a folder
*/
function isBogusFolderOpen() {
return !!entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.bogus;
}
export function getEntitiesList({ doFilter } = {}) {
function characterToEntity(character, id) {
return { item: character, id, type: 'character' };
}
function groupToEntity(group) {
return { item: group, id: group.id, type: 'group' };
}
function tagToEntity(tag) {
return { item: structuredClone(tag), id: tag.id, type: 'tag' };
}
let entities = [
...characters.map((item, index) => characterToEntity(item, index)),
...groups.map(item => groupToEntity(item)),
...(power_user.bogus_folders ? tags.map(item => tagToEntity(item)) : []),
];
if (doFilter) {
entities = entitiesFilter.applyFilters(entities);
}
if (isBogusFolderOpen()) {
// Get tags of entities within the bogus folder
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
entities = entities.filter(x => x.type !== 'tag');
const otherTags = tags.filter(x => !filterData.selected.includes(x.id));
const bogusTags = [];
for (const entity of entities) {
for (const tag of otherTags) {
if (!bogusTags.includes(tag) && entitiesFilter.isElementTagged(entity, tag.id)) {
bogusTags.push(tag);
}
}
}
entities.push(...bogusTags.map(item => tagToEntity(item)));
}
sortEntitiesList(entities);
return entities;
}
export async function getOneCharacter(avatarUrl) {
const response = await fetch('/api/characters/get', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
avatar_url: avatarUrl,
}),
});
if (response.ok) {
const getData = await response.json();
getData['name'] = DOMPurify.sanitize(getData['name']);
getData['chat'] = String(getData['chat']);
const indexOf = characters.findIndex(x => x.avatar === avatarUrl);
if (indexOf !== -1) {
characters[indexOf] = getData;
} else {
toastr.error(`Character ${avatarUrl} not found in the list`, 'Error', { timeOut: 5000, preventDuplicates: true });
}
}
}
async function getCharacters() {
var response = await fetch('/api/characters/all', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
'': '',
}),
});
if (response.ok === true) {
var getData = ''; //RossAscends: reset to force array to update to account for deleted character.
getData = await response.json();
const load_ch_count = Object.getOwnPropertyNames(getData);
for (var i = 0; i < load_ch_count.length; i++) {
characters[i] = [];
characters[i] = getData[i];
characters[i]['name'] = DOMPurify.sanitize(characters[i]['name']);
// For dropped-in cards
if (!characters[i]['chat']) {
characters[i]['chat'] = `${characters[i]['name']} - ${humanizedDateTime()}`;
}
characters[i]['chat'] = String(characters[i]['chat']);
}
if (this_chid != undefined && this_chid != 'invalid-safety-id') {
$('#avatar_url_pole').val(characters[this_chid].avatar);
}
await getGroups();
await printCharacters(true);
}
}
async function delChat(chatfile) {
const response = await fetch('/api/chats/delete', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
chatfile: chatfile,
avatar_url: characters[this_chid].avatar,
}),
});
if (response.ok === true) {
// choose another chat if current was deleted
const name = chatfile.replace('.jsonl', '');
if (name === characters[this_chid].chat) {
chat_metadata = {};
await replaceCurrentChat();
}
await eventSource.emit(event_types.CHAT_DELETED, name);
}
}
async function replaceCurrentChat() {
await clearChat();
chat.length = 0;
const chatsResponse = await fetch('/api/characters/chats', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ avatar_url: characters[this_chid].avatar }),
});
if (chatsResponse.ok) {
const chats = Object.values(await chatsResponse.json());
chats.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));
// pick existing chat
if (chats.length && typeof chats[0] === 'object') {
characters[this_chid].chat = chats[0].file_name.replace('.jsonl', '');
$('#selected_chat_pole').val(characters[this_chid].chat);
saveCharacterDebounced();
await getChat();
}
// start new chat
else {
characters[this_chid].chat = `${name2} - ${humanizedDateTime()}`;
$('#selected_chat_pole').val(characters[this_chid].chat);
saveCharacterDebounced();
await getChat();
}
}
}
export function showMoreMessages() {
let messageId = Number($('#chat').children('.mes').first().attr('mesid'));
let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
console.debug('Inserting messages before', messageId, 'count', count, 'chat length', chat.length);
const prevHeight = $('#chat').prop('scrollHeight');
while (messageId > 0 && count > 0) {
count--;
messageId--;
addOneMessage(chat[messageId], { insertBefore: messageId + 1, scroll: false, forceId: messageId });
}
if (messageId == 0) {
$('#show_more_messages').remove();
}
const newHeight = $('#chat').prop('scrollHeight');
$('#chat').scrollTop(newHeight - prevHeight);
}
async function printMessages() {
let startIndex = 0;
let count = power_user.chat_truncation || Number.MAX_SAFE_INTEGER;
if (chat.length > count) {
startIndex = chat.length - count;
$('#chat').append('Show more messages
');
}
for (let i = startIndex; i < chat.length; i++) {
const item = chat[i];
addOneMessage(item, { scroll: i === chat.length - 1, forceId: i });
}
// Scroll to bottom when all images are loaded
const images = document.querySelectorAll('#chat .mes img');
let imagesLoaded = 0;
for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image instanceof HTMLImageElement) {
if (image.complete) {
incrementAndCheck();
} else {
image.addEventListener('load', incrementAndCheck);
}
}
}
function incrementAndCheck() {
imagesLoaded++;
if (imagesLoaded === images.length) {
scrollChatToBottom();
}
}
}
async function clearChat() {
closeMessageEditor();
extension_prompts = {};
if (is_delete_mode) {
$('#dialogue_del_mes_cancel').trigger('click');
}
$('#chat').children().remove();
if ($('.zoomed_avatar[forChar]').length) {
console.debug('saw avatars to remove');
$('.zoomed_avatar[forChar]').remove();
} else { console.debug('saw no avatars'); }
await saveItemizedPrompts(getCurrentChatId());
itemizedPrompts = [];
}
async function deleteLastMessage() {
chat.length = chat.length - 1;
$('#chat').children('.mes').last().remove();
await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
}
export async function reloadCurrentChat() {
await clearChat();
chat.length = 0;
if (selected_group) {
await getGroupChat(selected_group);
}
else if (this_chid) {
await getChat();
}
else {
resetChatState();
await printMessages();
await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
}
hideSwipeButtons();
showSwipeButtons();
}
/**
* Send the message currently typed into the chat box.
*/
export function sendTextareaMessage() {
if (is_send_press) return;
let generateType;
// "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
// message was sent from a character (not the user or the system).
const textareaText = String($('#send_textarea').val());
if (power_user.continue_on_send &&
!textareaText &&
!selected_group &&
chat.length &&
!chat[chat.length - 1]['is_user'] &&
!chat[chat.length - 1]['is_system']
) {
generateType = 'continue';
}
Generate(generateType);
}
/**
* Formats the message text into an HTML string using Markdown and other formatting.
* @param {string} mes Message text
* @param {string} ch_name Character name
* @param {boolean} isSystem If the message was sent by the system
* @param {boolean} isUser If the message was sent by the user
* @param {number} messageId Message index in chat array
* @returns {string} HTML string
*/
function messageFormatting(mes, ch_name, isSystem, isUser, messageId) {
if (!mes) {
return '';
}
mesForShowdownParse = mes;
// Force isSystem = false on comment messages so they get formatted properly
if (ch_name === COMMENT_NAME_DEFAULT && isSystem && !isUser) {
isSystem = false;
}
// Let hidden messages have markdown
if (isSystem && ch_name !== systemUserName) {
isSystem = false;
}
// Prompt bias replacement should be applied on the raw message
if (!power_user.show_user_prompt_bias && ch_name && !isUser && !isSystem) {
mes = mes.replaceAll(substituteParams(power_user.user_prompt_bias), '');
}
if (!isSystem) {
function getRegexPlacement() {
try {
if (isUser) {
return regex_placement.USER_INPUT;
} else if (chat[messageId]?.extra?.type === 'narrator') {
return regex_placement.SLASH_COMMAND;
} else {
return regex_placement.AI_OUTPUT;
}
} catch {
return regex_placement.AI_OUTPUT;
}
}
const regexPlacement = getRegexPlacement();
const usableMessages = chat.map((x, index) => ({ message: x, index: index })).filter(x => !x.message.is_system);
const indexOf = usableMessages.findIndex(x => x.index === Number(messageId));
const depth = messageId >= 0 && indexOf !== -1 ? (usableMessages.length - indexOf - 1) : undefined;
// Always override the character name
mes = getRegexedString(mes, regexPlacement, {
characterOverride: ch_name,
isMarkdown: true,
depth: depth,
});
}
if (power_user.auto_fix_generated_markdown) {
mes = fixMarkdown(mes, true);
}
if (!isSystem && power_user.encode_tags) {
mes = mes.replaceAll('<', '<').replaceAll('>', '>');
}
if ((this_chid === undefined || this_chid === 'invalid-safety-id') && !selected_group) {
mes = mes
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\n/g, '
');
} else if (!isSystem) {
// Save double quotes in tags as a special character to prevent them from being encoded
if (!power_user.encode_tags) {
mes = mes.replace(/<([^>]+)>/g, function (_, contents) {
return '<' + contents.replace(/"/g, '\ufffe') + '>';
});
}
mes = mes.replace(/```[\s\S]*?```|``[\s\S]*?``|`[\s\S]*?`|(".+?")|(\u201C.+?\u201D)/gm, function (match, p1, p2) {
if (p1) {
return '"' + p1.replace(/"/g, '') + '"
';
} else if (p2) {
return '“' + p2.replace(/\u201C|\u201D/g, '') + '”
';
} else {
return match;
}
});
// Restore double quotes in tags
if (!power_user.encode_tags) {
mes = mes.replace(/\ufffe/g, '"');
}
mes = mes.replaceAll('\\begin{align*}', '$$');
mes = mes.replaceAll('\\end{align*}', '$$');
mes = converter.makeHtml(mes);
mes = mes.replace(/[\s\S]*?<\/code>/g, function (match) {
// Firefox creates extra newlines from
s in code blocks, so we replace them before converting newlines to
s.
return match.replace(/\n/gm, '\u0000');
});
mes = mes.replace(/\n/g, '
');
mes = mes.replace(/\u0000/g, '\n'); // Restore converted newlines
mes = mes.trim();
mes = mes.replace(/[\s\S]*?<\/code>/g, function (match) {
return match.replace(/&/g, '&');
});
}
/*
// Hides bias from empty messages send with slash commands
if (isSystem) {
mes = mes.replace(/\{\{[\s\S]*?\}\}/gm, "");
}
*/
if (!power_user.allow_name2_display && ch_name && !isUser && !isSystem) {
mes = mes.replace(new RegExp(`(^|\n)${ch_name}:`, 'g'), '$1');
}
/** @type {any} */
const config = { MESSAGE_SANITIZE: true, ADD_TAGS: ['custom-style'] };
mes = encodeStyleTags(mes);
mes = DOMPurify.sanitize(mes, config);
mes = decodeStyleTags(mes);
return mes;
}
/**
* Inserts or replaces an SVG icon adjacent to the provided message's timestamp.
*
* If the `extra.api` is "openai" and `extra.model` contains the substring "claude",
* the function fetches the "claude.svg". Otherwise, it fetches the SVG named after
* the value in `extra.api`.
*
* @param {JQuery} mes - The message element containing the timestamp where the icon should be inserted or replaced.
* @param {Object} extra - Contains the API and model details.
* @param {string} extra.api - The name of the API, used to determine which SVG to fetch.
* @param {string} extra.model - The model name, used to check for the substring "claude".
*/
function insertSVGIcon(mes, extra) {
// Determine the SVG filename
let modelName;
// Claude on OpenRouter or Anthropic
if (extra.api === 'openai' && extra.model?.toLowerCase().includes('claude')) {
modelName = 'claude';
}
// OpenAI on OpenRouter
else if (extra.api === 'openai' && extra.model?.toLowerCase().includes('openai')) {
modelName = 'openai';
}
// OpenRouter website model or other models
else if (extra.api === 'openai' && (extra.model === null || extra.model?.toLowerCase().includes('/'))) {
modelName = 'openrouter';
}
// Everything else
else {
modelName = extra.api;
}
const image = new Image();
// Add classes for styling and identification
image.classList.add('icon-svg', 'timestamp-icon');
image.src = `/img/${modelName}.svg`;
image.title = `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`;
image.onload = async function () {
// Check if an SVG already exists adjacent to the timestamp
let existingSVG = mes.find('.timestamp').next('.timestamp-icon');
if (existingSVG.length) {
// Replace existing SVG
existingSVG.replaceWith(image);
} else {
// Append the new SVG if none exists
mes.find('.timestamp').after(image);
}
await SVGInject(this);
};
}
function getMessageFromTemplate({
mesId,
characterName,
isUser,
avatarImg,
bias,
isSystem,
title,
timerValue,
timerTitle,
bookmarkLink,
forceAvatar,
timestamp,
tokenCount,
extra,
} = {}) {
const mes = $('#message_template .mes').clone();
mes.attr({
'mesid': mesId,
'ch_name': characterName,
'is_user': isUser,
'is_system': !!isSystem,
'bookmark_link': bookmarkLink,
'force_avatar': !!forceAvatar,
'timestamp': timestamp,
});
mes.find('.avatar img').attr('src', avatarImg);
mes.find('.ch_name .name_text').text(characterName);
mes.find('.mes_bias').html(bias);
mes.find('.timestamp').text(timestamp).attr('title', `${extra?.api ? extra.api + ' - ' : ''}${extra?.model ?? ''}`);
mes.find('.mesIDDisplay').text(`#${mesId}`);
tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
title && mes.attr('title', title);
timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
if (power_user.timestamp_model_icon && extra?.api) {
insertSVGIcon(mes, extra);
}
return mes;
}
export function updateMessageBlock(messageId, message) {
const messageElement = $(`#chat [mesid="${messageId}"]`);
const text = message?.extra?.display_text ?? message.mes;
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
addCopyToCodeBlocks(messageElement);
appendMediaToMessage(message, messageElement);
}
export function appendMediaToMessage(mes, messageElement) {
// Add image to message
if (mes.extra?.image) {
const chatHeight = $('#chat').prop('scrollHeight');
const image = messageElement.find('.mes_img');
const text = messageElement.find('.mes_text');
const isInline = !!mes.extra?.inline_image;
image.on('load', function () {
const scrollPosition = $('#chat').scrollTop();
const newChatHeight = $('#chat').prop('scrollHeight');
const diff = newChatHeight - chatHeight;
$('#chat').scrollTop(scrollPosition + diff);
});
image.attr('src', mes.extra?.image);
image.attr('title', mes.extra?.title || mes.title || '');
messageElement.find('.mes_img_container').addClass('img_extra');
image.toggleClass('img_inline', isInline);
text.toggleClass('displayNone', !isInline);
}
// Add file to message
if (mes.extra?.file) {
messageElement.find('.mes_file_container').remove();
const messageId = messageElement.attr('mesid');
const template = $('#message_file_template .mes_file_container').clone();
template.find('.mes_file_name').text(mes.extra.file.name);
template.find('.mes_file_size').text(humanFileSize(mes.extra.file.size));
template.find('.mes_file_download').attr('mesid', messageId);
template.find('.mes_file_delete').attr('mesid', messageId);
messageElement.find('.mes_block').append(template);
} else {
messageElement.find('.mes_file_container').remove();
}
}
/**
* @deprecated Use appendMediaToMessage instead.
*/
export function appendImageToMessage(mes, messageElement) {
appendMediaToMessage(mes, messageElement);
}
export function addCopyToCodeBlocks(messageElement) {
const codeBlocks = $(messageElement).find('pre code');
for (let i = 0; i < codeBlocks.length; i++) {
hljs.highlightElement(codeBlocks.get(i));
if (navigator.clipboard !== undefined) {
const copyButton = document.createElement('i');
copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy');
copyButton.title = 'Copy code';
codeBlocks.get(i).appendChild(copyButton);
copyButton.addEventListener('pointerup', function (event) {
navigator.clipboard.writeText(codeBlocks.get(i).innerText);
toastr.info('Copied!', '', { timeOut: 2000 });
});
}
}
}
function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null } = {}) {
var messageText = mes['mes'];
const momentDate = timestampToMoment(mes.send_date);
const timestamp = momentDate.isValid() ? momentDate.format('LL LT') : '';
if (mes?.extra?.display_text) {
messageText = mes.extra.display_text;
}
// Forbidden black magic
// This allows to use "continue" on user messages
if (type === 'swipe' && mes.swipe_id === undefined) {
mes.swipe_id = 0;
mes.swipes = [mes.mes];
}
var avatarImg = getUserAvatar(user_avatar);
const isSystem = mes.is_system;
const title = mes.title;
generatedPromptCache = '';
//for non-user mesages
if (!mes['is_user']) {
if (mes.force_avatar) {
avatarImg = mes.force_avatar;
} else if (this_chid === undefined || this_chid === 'invalid-safety-id') {
avatarImg = system_avatar;
} else {
if (characters[this_chid].avatar != 'none') {
avatarImg = getThumbnailUrl('avatar', characters[this_chid].avatar);
} else {
avatarImg = default_avatar;
}
}
//old processing:
//if messge is from sytem, use the name provided in the message JSONL to proceed,
//if not system message, use name2 (char's name) to proceed
//characterName = mes.is_system || mes.force_avatar ? mes.name : name2;
} else if (mes['is_user'] && mes['force_avatar']) {
// Special case for persona images.
avatarImg = mes['force_avatar'];
}
messageText = messageFormatting(
messageText,
mes.name,
isSystem,
mes.is_user,
chat.indexOf(mes),
);
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
// Verify bookmarked chat still exists
// Cohee: Commented out for now. I'm worried of performance issues.
/*if (bookmarkLink !== '') {
let chat_names = selected_group
? getGroupChatNames(selected_group)
: Object.values(getPastCharacterChats()).map(({ file_name }) => file_name);
if (!chat_names.includes(bookmarkLink)) {
bookmarkLink = ''
}
}*/
let params = {
mesId: forceId ?? chat.length - 1,
characterName: mes.name,
isUser: mes.is_user,
avatarImg: avatarImg,
bias: bias,
isSystem: isSystem,
title: title,
bookmarkLink: bookmarkLink,
forceAvatar: mes.force_avatar,
timestamp: timestamp,
extra: mes.extra,
tokenCount: mes.extra?.token_count,
...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count),
};
const HTMLForEachMes = getMessageFromTemplate(params);
if (type !== 'swipe') {
if (!insertAfter && !insertBefore) {
$('#chat').append(HTMLForEachMes);
}
else if (insertAfter) {
const target = $('#chat').find(`.mes[mesid="${insertAfter}"]`);
$(HTMLForEachMes).insertAfter(target);
$(HTMLForEachMes).find('.swipe_left').css('display', 'none');
$(HTMLForEachMes).find('.swipe_right').css('display', 'none');
} else {
const target = $('#chat').find(`.mes[mesid="${insertBefore}"]`);
$(HTMLForEachMes).insertBefore(target);
$(HTMLForEachMes).find('.swipe_left').css('display', 'none');
$(HTMLForEachMes).find('.swipe_right').css('display', 'none');
}
}
// Callers push the new message to chat before calling addOneMessage
const newMessageId = typeof forceId == 'number' ? forceId : chat.length - 1;
const newMessage = $(`#chat [mesid="${newMessageId}"]`);
const isSmallSys = mes?.extra?.isSmallSys;
newMessage.data('isSystem', isSystem);
if (isSystem) {
// newMessage.find(".mes_edit").hide();
newMessage.find('.mes_prompt').hide(); //don't need prompt button for sys
}
if (isSmallSys === true) {
newMessage.addClass('smallSysMes');
}
// don't need prompt button for user
if (params.isUser === true) {
newMessage.find('.mes_prompt').hide();
//console.log(`hiding prompt for user mesID ${params.mesId}`);
}
//shows or hides the Prompt display button
let mesIdToFind = type == 'swipe' ? params.mesId - 1 : params.mesId; //Number(newMessage.attr('mesId'));
//if we have itemized messages, and the array isn't null..
if (params.isUser === false && itemizedPrompts.length !== 0 && itemizedPrompts.length !== null) {
// console.log('looking through itemized prompts...');
//console.log(`mesIdToFind = ${mesIdToFind} from ${params.avatarImg}`);
//console.log(`itemizedPrompts.length = ${itemizedPrompts.length}`)
//console.log(itemizedPrompts);
for (var i = 0; i < itemizedPrompts.length; i++) {
//console.log(`itemized array item ${i} is MesID ${Number(itemizedPrompts[i].mesId)}, does it match ${Number(mesIdToFind)}?`);
if (Number(itemizedPrompts[i].mesId) === Number(mesIdToFind)) {
newMessage.find('.mes_prompt').show();
//console.log(`showing button for mesID ${params.mesId} from ${params.characterName}`);
break;
} /*else {
console.log(`no cache obj for mesID ${mesIdToFind}, hiding this prompt button`);
newMessage.find(".mes_prompt").hide();
console.log(itemizedPrompts);
} */
}
} else {
//console.log('itemizedprompt array empty null, or user, hiding this prompt buttons');
//$(".mes_prompt").hide();
newMessage.find('.mes_prompt').hide();
//console.log(itemizedPrompts);
}
newMessage.find('.avatar img').on('error', function () {
$(this).hide();
$(this).parent().html('');
});
if (type === 'swipe') {
const swipeMessage = $('#chat').find(`[mesid="${chat.length - 1}"]`);
swipeMessage.find('.mes_text').html('');
swipeMessage.find('.mes_text').append(messageText);
appendMediaToMessage(mes, swipeMessage);
swipeMessage.attr('title', title);
swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
if (power_user.timestamp_model_icon && params.extra?.api) {
insertSVGIcon(swipeMessage, params.extra);
}
if (mes.swipe_id == mes.swipes.length - 1) {
swipeMessage.find('.mes_timer').text(params.timerValue);
swipeMessage.find('.mes_timer').attr('title', params.timerTitle);
swipeMessage.find('.tokenCounterDisplay').text(`${params.tokenCount}t`);
} else {
swipeMessage.find('.mes_timer').html('');
swipeMessage.find('.tokenCounterDisplay').html('');
}
} else {
const messageId = forceId ?? chat.length - 1;
$('#chat').find(`[mesid="${messageId}"]`).find('.mes_text').append(messageText);
appendMediaToMessage(mes, newMessage);
hideSwipeButtons();
}
addCopyToCodeBlocks(newMessage);
$('#chat .mes').last().addClass('last_mes');
$('#chat .mes').eq(-2).removeClass('last_mes');
hideSwipeButtons();
showSwipeButtons();
// Don't scroll if not inserting last
if (!insertAfter && !insertBefore && scroll) {
scrollChatToBottom();
}
}
/**
* Returns the URL of the avatar for the given user avatar Id.
* @param {string} avatarImg User avatar Id
* @returns {string} User avatar URL
*/
export function getUserAvatar(avatarImg) {
return `User Avatars/${avatarImg}`;
}
/**
* Returns the URL of the avatar for the given character Id.
* @param {number} characterId Character Id
* @returns {string} Avatar URL
*/
export function getCharacterAvatar(characterId) {
const character = characters[characterId];
const avatarImg = character?.avatar;
if (!avatarImg || avatarImg === 'none') {
return default_avatar;
}
return formatCharacterAvatar(avatarImg);
}
export function formatCharacterAvatar(characterAvatar) {
return `characters/${characterAvatar}`;
}
/**
* Formats the title for the generation timer.
* @param {Date} gen_started Date when generation was started
* @param {Date} gen_finished Date when generation was finished
* @param {number} tokenCount Number of tokens generated (0 if not available)
* @returns {Object} Object containing the formatted timer value and title
* @example
* const { timerValue, timerTitle } = formatGenerationTimer(gen_started, gen_finished, tokenCount);
* console.log(timerValue); // 1.2s
* console.log(timerTitle); // Generation queued: 12:34:56 7 Jan 2021\nReply received: 12:34:57 7 Jan 2021\nTime to generate: 1.2 seconds\nToken rate: 5 t/s
*/
function formatGenerationTimer(gen_started, gen_finished, tokenCount) {
if (!gen_started || !gen_finished) {
return {};
}
const dateFormat = 'HH:mm:ss D MMM YYYY';
const start = moment(gen_started);
const finish = moment(gen_finished);
const seconds = finish.diff(start, 'seconds', true);
const timerValue = `${seconds.toFixed(1)}s`;
const timerTitle = [
`Generation queued: ${start.format(dateFormat)}`,
`Reply received: ${finish.format(dateFormat)}`,
`Time to generate: ${seconds} seconds`,
tokenCount > 0 ? `Token rate: ${Number(tokenCount / seconds).toFixed(1)} t/s` : '',
].join('\n');
if (isNaN(seconds) || seconds < 0) {
return { timerValue: '', timerTitle };
}
return { timerValue, timerTitle };
}
function scrollChatToBottom() {
if (power_user.auto_scroll_chat_to_bottom) {
const chatElement = $('#chat');
let position = chatElement[0].scrollHeight;
if (power_user.waifuMode) {
const lastMessage = chatElement.find('.mes').last();
if (lastMessage.length) {
const lastMessagePosition = lastMessage.position().top;
position = chatElement.scrollTop() + lastMessagePosition;
}
}
chatElement.scrollTop(position);
}
}
/**
* Substitutes {{macro}} parameters in a string.
* @param {string} content - The string to substitute parameters in.
* @param {*} _name1 - The name of the user. Uses global name1 if not provided.
* @param {*} _name2 - The name of the character. Uses global name2 if not provided.
* @param {*} _original - The original message for {{original}} substitution.
* @param {*} _group - The group members list for {{group}} substitution.
* @param {boolean} _replaceCharacterCard - Whether to replace character card macros.
* @returns {string} The string with substituted parameters.
*/
function substituteParams(content, _name1, _name2, _original, _group, _replaceCharacterCard = true) {
const environment = {};
if (typeof _original === 'string') {
let originalSubstituted = false;
environment.original = () => {
if (originalSubstituted) {
return '';
}
originalSubstituted = true;
return _original;
};
}
const getGroupValue = () => {
if (typeof _group === 'string') {
return _group;
}
if (selected_group) {
const members = groups.find(x => x.id === selected_group)?.members;
const names = Array.isArray(members)
? members.map(m => characters.find(c => c.avatar === m)?.name).filter(Boolean).join(', ')
: '';
return names;
} else {
return _name2 ?? name2;
}
};
if (_replaceCharacterCard) {
const fields = getCharacterCardFields();
environment.charPrompt = fields.system || '';
environment.charJailbreak = fields.jailbreak || '';
environment.description = fields.description || '';
environment.personality = fields.personality || '';
environment.scenario = fields.scenario || '';
environment.persona = fields.persona || '';
environment.mesExamples = fields.mesExamples || '';
}
// Must be substituted last so that they're replaced inside {{description}}
environment.user = _name1 ?? name1;
environment.char = _name2 ?? name2;
environment.group = environment.charIfNotGroup = getGroupValue();
environment.model = getGeneratingModel();
return evaluateMacros(content, environment);
}
/**
* Gets stopping sequences for the prompt.
* @param {boolean} isImpersonate A request is made to impersonate a user
* @param {boolean} isContinue A request is made to continue the message
* @returns {string[]} Array of stopping strings
*/
function getStoppingStrings(isImpersonate, isContinue) {
const charString = `\n${name2}:`;
const userString = `\n${name1}:`;
const result = isImpersonate ? [charString] : [userString];
result.push(userString);
if (isContinue && Array.isArray(chat) && chat[chat.length - 1]?.is_user) {
result.push(charString);
}
// Add other group members as the stopping strings
if (selected_group) {
const group = groups.find(x => x.id === selected_group);
if (group && Array.isArray(group.members)) {
const names = group.members
.map(x => characters.find(y => y.avatar == x))
.filter(x => x && x.name && x.name !== name2)
.map(x => `\n${x.name}:`);
result.push(...names);
}
}
result.push(...getInstructStoppingSequences());
result.push(...getCustomStoppingStrings());
if (power_user.single_line) {
result.unshift('\n');
}
return result.filter(onlyUnique);
}
/**
* Background generation based on the provided prompt.
* @param {string} quiet_prompt Instruction prompt for the AI
* @param {boolean} quietToLoud Whether the message should be sent in a foreground (loud) or background (quiet) mode
* @param {boolean} skipWIAN whether to skip addition of World Info and Author's Note into the prompt
* @param {string} quietImage Image to use for the quiet prompt
* @param {string} quietName Name to use for the quiet prompt (defaults to "System:")
* @returns
*/
export async function generateQuietPrompt(quiet_prompt, quietToLoud, skipWIAN, quietImage = null, quietName = null) {
console.log('got into genQuietPrompt');
/** @type {GenerateOptions} */
const options = {
quiet_prompt,
quietToLoud,
skipWIAN: skipWIAN,
force_name2: true,
quietImage: quietImage,
quietName: quietName,
};
const generateFinished = await Generate('quiet', options);
return generateFinished;
}
/**
* Executes slash commands and returns the new text and whether the generation was interrupted.
* @param {string} message Text to be sent
* @returns {Promise} Whether the message sending was interrupted
*/
async function processCommands(message) {
if (!message || !message.trim().startsWith('/')) {
return false;
}
const previousText = String($('#send_textarea').val());
const result = await executeSlashCommands(message);
if (!result || typeof result !== 'object') {
return false;
}
const currentText = String($('#send_textarea').val());
if (previousText === currentText) {
$('#send_textarea').val(result.newText).trigger('input');
}
// interrupt generation if the input was nothing but a command
if (message.length > 0 && result?.newText.length === 0) {
return true;
}
return result?.interrupt;
}
function sendSystemMessage(type, text, extra = {}) {
const systemMessage = system_messages[type];
if (!systemMessage) {
return;
}
const newMessage = { ...systemMessage, send_date: getMessageTimeStamp() };
if (text) {
newMessage.mes = text;
}
if (type == system_message_types.SLASH_COMMANDS) {
newMessage.mes = getSlashCommandsHelp();
}
if (!newMessage.extra) {
newMessage.extra = {};
}
newMessage.extra = Object.assign(newMessage.extra, extra);
newMessage.extra.type = type;
chat.push(newMessage);
addOneMessage(newMessage);
is_send_press = false;
}
export function extractMessageBias(message) {
if (!message) {
return null;
}
try {
const biasHandlebars = Handlebars.create();
const biasMatches = [];
biasHandlebars.registerHelper('bias', function (text) {
biasMatches.push(text);
return '';
});
const template = biasHandlebars.compile(message);
template({});
if (biasMatches && biasMatches.length > 0) {
return ` ${biasMatches.join(' ')}`;
}
return '';
} catch {
return '';
}
}
/**
* Removes impersonated group member lines from the group member messages.
* Doesn't do anything if group reply trimming is disabled.
* @param {string} getMessage Group message
* @returns Cleaned-up group message
*/
function cleanGroupMessage(getMessage) {
if (power_user.disable_group_trimming) {
return getMessage;
}
const group = groups.find((x) => x.id == selected_group);
if (group && Array.isArray(group.members) && group.members) {
for (let member of group.members) {
const character = characters.find(x => x.avatar == member);
if (!character) {
continue;
}
const name = character.name;
// Skip current speaker.
if (name === name2) {
continue;
}
const regex = new RegExp(`(^|\n)${escapeRegex(name)}:`);
const nameMatch = getMessage.match(regex);
if (nameMatch) {
getMessage = getMessage.substring(0, nameMatch.index);
}
}
}
return getMessage;
}
function addPersonaDescriptionExtensionPrompt() {
if (!power_user.persona_description) {
return;
}
const promptPositions = [persona_description_positions.BOTTOM_AN, persona_description_positions.TOP_AN];
if (promptPositions.includes(power_user.persona_description_position) && shouldWIAddPrompt) {
const originalAN = extension_prompts[NOTE_MODULE_NAME].value;
const ANWithDesc = power_user.persona_description_position === persona_description_positions.TOP_AN
? `${power_user.persona_description}\n${originalAN}`
: `${originalAN}\n${power_user.persona_description}`;
setExtensionPrompt(NOTE_MODULE_NAME, ANWithDesc, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan);
}
}
function getAllExtensionPrompts() {
const value = Object
.values(extension_prompts)
.filter(x => x.value)
.map(x => x.value.trim())
.join('\n');
return value.length ? substituteParams(value) : '';
}
// Wrapper to fetch extension prompts by module name
function getExtensionPromptByName(moduleName) {
if (moduleName) {
return substituteParams(extension_prompts[moduleName]?.value);
} else {
return;
}
}
function getExtensionPrompt(position = 0, depth = undefined, separator = '\n') {
let extension_prompt = Object.keys(extension_prompts)
.sort()
.map((x) => extension_prompts[x])
.filter(x => x.position == position && x.value && (depth === undefined || x.depth == depth))
.map(x => x.value.trim())
.join(separator);
if (extension_prompt.length && !extension_prompt.startsWith(separator)) {
extension_prompt = separator + extension_prompt;
}
if (extension_prompt.length && !extension_prompt.endsWith(separator)) {
extension_prompt = extension_prompt + separator;
}
if (extension_prompt.length) {
extension_prompt = substituteParams(extension_prompt);
}
return extension_prompt;
}
export function baseChatReplace(value, name1, name2) {
if (value !== undefined && value.length > 0) {
const _ = undefined;
value = substituteParams(value, name1, name2, _, _, false);
if (power_user.collapse_newlines) {
value = collapseNewlines(value);
}
value = value.replace(/\r/g, '');
}
return value;
}
/**
* Returns the character card fields for the current character.
* @returns {{system: string, mesExamples: string, description: string, personality: string, persona: string, scenario: string, jailbreak: string}}
*/
export function getCharacterCardFields() {
const result = { system: '', mesExamples: '', description: '', personality: '', persona: '', scenario: '', jailbreak: '' };
const character = characters[this_chid];
if (!character) {
return result;
}
const scenarioText = chat_metadata['scenario'] || characters[this_chid].scenario;
result.description = baseChatReplace(characters[this_chid].description.trim(), name1, name2);
result.personality = baseChatReplace(characters[this_chid].personality.trim(), name1, name2);
result.scenario = baseChatReplace(scenarioText.trim(), name1, name2);
result.mesExamples = baseChatReplace(characters[this_chid].mes_example.trim(), name1, name2);
result.persona = baseChatReplace(power_user.persona_description.trim(), name1, name2);
result.system = power_user.prefer_character_prompt ? baseChatReplace(characters[this_chid].data?.system_prompt?.trim(), name1, name2) : '';
result.jailbreak = power_user.prefer_character_jailbreak ? baseChatReplace(characters[this_chid].data?.post_history_instructions?.trim(), name1, name2) : '';
if (selected_group) {
const groupCards = getGroupCharacterCards(selected_group, Number(this_chid));
if (groupCards) {
result.description = groupCards.description;
result.personality = groupCards.personality;
result.scenario = groupCards.scenario;
result.mesExamples = groupCards.mesExamples;
}
}
return result;
}
function isStreamingEnabled() {
const noStreamSources = [chat_completion_sources.SCALE, chat_completion_sources.AI21];
return ((main_api == 'openai' && oai_settings.stream_openai && !noStreamSources.includes(oai_settings.chat_completion_source) && !(oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE && oai_settings.google_model.includes('bison')))
|| (main_api == 'kobold' && kai_settings.streaming_kobold && kai_flags.can_use_streaming)
|| (main_api == 'novel' && nai_settings.streaming_novel)
|| (main_api == 'textgenerationwebui' && textgen_settings.streaming));
}
function showStopButton() {
$('#mes_stop').css({ 'display': 'flex' });
}
function hideStopButton() {
// prevent NOOP, because hideStopButton() gets called multiple times
if ($('#mes_stop').css('display') !== 'none') {
$('#mes_stop').css({ 'display': 'none' });
eventSource.emit(event_types.GENERATION_ENDED, chat.length);
}
}
class StreamingProcessor {
constructor(type, force_name2, timeStarted, messageAlreadyGenerated) {
this.result = '';
this.messageId = -1;
this.type = type;
this.force_name2 = force_name2;
this.isStopped = false;
this.isFinished = false;
this.generator = this.nullStreamingGeneration;
this.abortController = new AbortController();
this.firstMessageText = '...';
this.timeStarted = timeStarted;
this.messageAlreadyGenerated = messageAlreadyGenerated;
this.swipes = [];
/** @type {import('./scripts/logprobs.js').TokenLogprobs[]} */
this.messageLogprobs = [];
}
showMessageButtons(messageId) {
if (messageId == -1) {
return;
}
showStopButton();
$(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'none' });
}
hideMessageButtons(messageId) {
if (messageId == -1) {
return;
}
hideStopButton();
$(`#chat .mes[mesid="${messageId}"] .mes_buttons`).css({ 'display': 'flex' });
}
async onStartStreaming(text) {
let messageId = -1;
if (this.type == 'impersonate') {
$('#send_textarea').val('').trigger('input');
}
else {
await saveReply(this.type, text, true);
messageId = chat.length - 1;
this.showMessageButtons(messageId);
}
hideSwipeButtons();
scrollChatToBottom();
return messageId;
}
onProgressStreaming(messageId, text, isFinal) {
const isImpersonate = this.type == 'impersonate';
const isContinue = this.type == 'continue';
if (!isImpersonate && !isContinue && Array.isArray(this.swipes) && this.swipes.length > 0) {
for (let i = 0; i < this.swipes.length; i++) {
this.swipes[i] = cleanUpMessage(this.swipes[i], false, false, true, this.stoppingStrings);
}
}
let processedText = cleanUpMessage(text, isImpersonate, isContinue, !isFinal, this.stoppingStrings);
// Predict unbalanced asterisks / quotes during streaming
const charsToBalance = ['*', '"', '```'];
for (const char of charsToBalance) {
if (!isFinal && isOdd(countOccurrences(processedText, char))) {
// Add character at the end to balance it
const separator = char.length > 1 ? '\n' : '';
processedText = processedText.trimEnd() + separator + char;
}
}
if (isImpersonate) {
$('#send_textarea').val(processedText).trigger('input');
}
else {
let currentTime = new Date();
// Don't waste time calculating token count for streaming
let currentTokenCount = isFinal && power_user.message_token_count_enabled ? getTokenCount(processedText, 0) : 0;
const timePassed = formatGenerationTimer(this.timeStarted, currentTime, currentTokenCount);
chat[messageId]['mes'] = processedText;
chat[messageId]['gen_started'] = this.timeStarted;
chat[messageId]['gen_finished'] = currentTime;
if (currentTokenCount) {
if (!chat[messageId]['extra']) {
chat[messageId]['extra'] = {};
}
chat[messageId]['extra']['token_count'] = currentTokenCount;
const tokenCounter = $(`#chat .mes[mesid="${messageId}"] .tokenCounterDisplay`);
tokenCounter.text(`${currentTokenCount}t`);
}
if ((this.type == 'swipe' || this.type === 'continue') && Array.isArray(chat[messageId]['swipes'])) {
chat[messageId]['swipes'][chat[messageId]['swipe_id']] = processedText;
chat[messageId]['swipe_info'][chat[messageId]['swipe_id']] = { 'send_date': chat[messageId]['send_date'], 'gen_started': chat[messageId]['gen_started'], 'gen_finished': chat[messageId]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])) };
}
let formattedText = messageFormatting(
processedText,
chat[messageId].name,
chat[messageId].is_system,
chat[messageId].is_user,
messageId,
);
const mesText = $(`#chat .mes[mesid="${messageId}"] .mes_text`);
mesText.html(formattedText);
$(`#chat .mes[mesid="${messageId}"] .mes_timer`).text(timePassed.timerValue).attr('title', timePassed.timerTitle);
this.setFirstSwipe(messageId);
}
if (!scrollLock) {
scrollChatToBottom();
}
}
async onFinishStreaming(messageId, text) {
this.hideMessageButtons(this.messageId);
this.onProgressStreaming(messageId, text, true);
addCopyToCodeBlocks($(`#chat .mes[mesid="${messageId}"]`));
if (Array.isArray(this.swipes) && this.swipes.length > 0) {
const message = chat[messageId];
const swipeInfo = {
send_date: message.send_date,
gen_started: message.gen_started,
gen_finished: message.gen_finished,
extra: structuredClone(message.extra),
};
const swipeInfoArray = [];
swipeInfoArray.length = this.swipes.length;
swipeInfoArray.fill(swipeInfo);
chat[messageId].swipes.push(...this.swipes);
chat[messageId].swipe_info.push(...swipeInfoArray);
}
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);
}
const continueMsg = this.type === 'continue' ? this.messageAlreadyGenerated : undefined;
await saveChatConditional();
saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), continueMsg);
activateSendButtons();
showSwipeButtons();
setGenerationProgress(0);
generatedPromptCache = '';
//console.log("Generated text size:", text.length, text)
if (power_user.auto_swipe) {
function containsBlacklistedWords(str, blacklist, threshold) {
const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
const matches = str.match(regex) || [];
return matches.length >= threshold;
}
const generatedTextFiltered = (text) => {
if (text) {
if (power_user.auto_swipe_minimum_length) {
if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
console.log('Generated text size too small');
return true;
}
}
if (power_user.auto_swipe_blacklist_threshold) {
if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
console.log('Generated text has blacklisted words');
return true;
}
}
}
return false;
};
if (generatedTextFiltered(text)) {
swipe_right();
return;
}
}
playMessageSound();
}
onErrorStreaming() {
this.abortController.abort();
this.isStopped = true;
this.hideMessageButtons(this.messageId);
$('#send_textarea').removeAttr('disabled');
is_send_press = false;
activateSendButtons();
setGenerationProgress(0);
showSwipeButtons();
}
setFirstSwipe(messageId) {
if (this.type !== 'swipe' && this.type !== 'impersonate') {
if (Array.isArray(chat[messageId]['swipes']) && chat[messageId]['swipes'].length === 1 && chat[messageId]['swipe_id'] === 0) {
chat[messageId]['swipes'][0] = chat[messageId]['mes'];
chat[messageId]['swipe_info'][0] = { 'send_date': chat[messageId]['send_date'], 'gen_started': chat[messageId]['gen_started'], 'gen_finished': chat[messageId]['gen_finished'], 'extra': JSON.parse(JSON.stringify(chat[messageId]['extra'])) };
}
}
}
onStopStreaming() {
this.onErrorStreaming();
}
*nullStreamingGeneration() {
throw new Error('Generation function for streaming is not hooked up');
}
async generate() {
if (this.messageId == -1) {
this.messageId = await this.onStartStreaming(this.firstMessageText);
await delay(1); // delay for message to be rendered
scrollLock = false;
}
// Stopping strings are expensive to calculate, especially with macros enabled. To remove stopping strings
// when streaming, we cache the result of getStoppingStrings instead of calling it once per token.
const isImpersonate = this.type == 'impersonate';
const isContinue = this.type == 'continue';
this.stoppingStrings = getStoppingStrings(isImpersonate, isContinue);
try {
const sw = new Stopwatch(1000 / power_user.streaming_fps);
const timestamps = [];
for await (const { text, swipes, logprobs } of this.generator()) {
timestamps.push(Date.now());
if (this.isStopped) {
return;
}
this.result = text;
this.swipes = swipes;
if (logprobs) {
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
}
await sw.tick(() => this.onProgressStreaming(this.messageId, this.messageAlreadyGenerated + text));
}
const seconds = (timestamps[timestamps.length - 1] - timestamps[0]) / 1000;
console.warn(`Stream stats: ${timestamps.length} tokens, ${seconds.toFixed(2)} seconds, rate: ${Number(timestamps.length / seconds).toFixed(2)} TPS`);
}
catch (err) {
console.error(err);
this.onErrorStreaming();
return;
}
this.isFinished = true;
return this.result;
}
}
/**
* Generates a message using the provided prompt.
* @param {string} prompt Prompt to generate a message from
* @param {string} api API to use. Main API is used if not specified.
* @param {boolean} instructOverride true to override instruct mode, false to use the default value
* @returns {Promise} Generated message
*/
export async function generateRaw(prompt, api, instructOverride) {
if (!api) {
api = main_api;
}
const abortController = new AbortController();
const isInstruct = power_user.instruct.enabled && main_api !== 'openai' && main_api !== 'novel' && !instructOverride;
prompt = substituteParams(prompt);
prompt = api == 'novel' ? adjustNovelInstructionPrompt(prompt) : prompt;
prompt = isInstruct ? formatInstructModeChat(name1, prompt, false, true, '', name1, name2, false) : prompt;
prompt = isInstruct ? (prompt + formatInstructModePrompt(name2, false, '', name1, name2)) : (prompt + '\n');
let generateData = {};
switch (api) {
case 'kobold':
case 'koboldhorde':
if (preset_settings === 'gui') {
generateData = { prompt: prompt, gui_settings: true, max_length: amount_gen, max_context_length: max_context, api_server };
} else {
const isHorde = api === 'koboldhorde';
const koboldSettings = koboldai_settings[koboldai_setting_names[preset_settings]];
generateData = getKoboldGenerationData(prompt, koboldSettings, amount_gen, max_context, isHorde, 'quiet');
}
break;
case 'novel': {
const novelSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
generateData = getNovelGenerationData(prompt, novelSettings, amount_gen, false, false, null, 'quiet');
break;
}
case 'textgenerationwebui':
generateData = getTextGenGenerationData(prompt, amount_gen, false, false, null, 'quiet');
break;
case 'openai':
generateData = [{ role: 'user', content: prompt.trim() }];
}
let data = {};
if (api == 'koboldhorde') {
data = await generateHorde(prompt, generateData, abortController.signal, false);
} else if (api == 'openai') {
data = await sendOpenAIRequest('quiet', generateData, abortController.signal);
} else {
const generateUrl = getGenerateUrl(api);
const response = await fetch(generateUrl, {
method: 'POST',
headers: getRequestHeaders(),
cache: 'no-cache',
body: JSON.stringify(generateData),
signal: abortController.signal,
});
if (!response.ok) {
const error = await response.json();
throw error;
}
data = await response.json();
}
if (data.error) {
throw new Error(data.error);
}
const message = cleanUpMessage(extractMessageFromData(data), false, false, true);
if (!message) {
throw new Error('No message generated');
}
return message;
}
/**
* Runs a generation using the current chat context.
* @param {string} type Generation type
* @param {GenerateOptions} options Generation options
* @param {boolean} dryRun Whether to actually generate a message or just assemble the prompt
* @returns {Promise} Returns a promise that resolves when the text is done generating.
* @typedef {{automatic_trigger?: boolean, force_name2?: boolean, quiet_prompt?: string, quietToLoud?: boolean, skipWIAN?: boolean, force_chid?: number, signal?: AbortSignal, quietImage?: string, maxLoops?: number, quietName?: string }} GenerateOptions
*/
async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops, quietName } = {}, dryRun = false) {
console.log('Generate entered');
eventSource.emit(event_types.GENERATION_STARTED, type, { automatic_trigger, force_name2, quiet_prompt, quietToLoud, skipWIAN, force_chid, signal, quietImage, maxLoops }, dryRun);
setGenerationProgress(0);
generation_started = new Date();
// Don't recreate abort controller if signal is passed
if (!(abortController && signal)) {
abortController = new AbortController();
}
// OpenAI doesn't need instruct mode. Use OAI main prompt instead.
const isInstruct = power_user.instruct.enabled && main_api !== 'openai';
const isImpersonate = type == 'impersonate';
let message_already_generated = isImpersonate ? `${name1}: ` : `${name2}: `;
if (!(dryRun || type == 'regenerate' || type == 'swipe' || type == 'quiet')) {
const interruptedByCommand = await processCommands(String($('#send_textarea').val()));
if (interruptedByCommand) {
//$("#send_textarea").val('').trigger('input');
unblockGeneration();
return Promise.resolve();
}
}
if (main_api == 'kobold' && kai_settings.streaming_kobold && !kai_flags.can_use_streaming) {
toastr.error('Streaming is enabled, but the version of Kobold used does not support token streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration();
return Promise.resolve();
}
if (main_api === 'textgenerationwebui' &&
textgen_settings.streaming &&
textgen_settings.legacy_api &&
(textgen_settings.type === OOBA || textgen_settings.type === APHRODITE)) {
toastr.error('Streaming is not supported for the Legacy API. Update Ooba and use new API to enable streaming.', undefined, { timeOut: 10000, preventDuplicates: true });
unblockGeneration();
return Promise.resolve();
}
if (isHordeGenerationNotAllowed()) {
unblockGeneration();
return Promise.resolve();
}
if (!dryRun) {
// Hide swipes if not in a dry run.
hideSwipeButtons();
// If generated any message, set the flag to indicate it can't be recreated again.
chat_metadata['tainted'] = true;
}
if (selected_group && !is_group_generating) {
if (!dryRun) {
// Returns the promise that generateGroupWrapper returns; resolves when generation is done
return generateGroupWrapper(false, type, { quiet_prompt, force_chid, signal: abortController.signal, quietImage, maxLoops });
}
const characterIndexMap = new Map(characters.map((char, index) => [char.avatar, index]));
const group = groups.find((x) => x.id === selected_group);
const enabledMembers = group.members.reduce((acc, member) => {
if (!group.disabled_members.includes(member) && !acc.includes(member)) {
acc.push(member);
}
return acc;
}, []);
const memberIds = enabledMembers
.map((member) => characterIndexMap.get(member))
.filter((index) => index !== undefined && index !== null);
if (memberIds.length > 0) {
setCharacterId(memberIds[0]);
setCharacterName('');
} else {
console.log('No enabled members found');
unblockGeneration();
return Promise.resolve();
}
}
//#########QUIET PROMPT STUFF##############
//this function just gives special care to novel quiet instruction prompts
if (quiet_prompt) {
quiet_prompt = substituteParams(quiet_prompt);
quiet_prompt = main_api == 'novel' && !quietToLoud ? adjustNovelInstructionPrompt(quiet_prompt) : quiet_prompt;
}
const isChatValid = online_status != 'no_connection' && this_chid != undefined && this_chid !== 'invalid-safety-id';
// We can't do anything because we're not in a chat right now. (Unless it's a dry run, in which case we need to
// assemble the prompt so we can count its tokens regardless of whether a chat is active.)
if (!dryRun && !isChatValid) {
if (this_chid === undefined || this_chid === 'invalid-safety-id') {
toastr.warning('Сharacter is not selected');
}
is_send_press = false;
return Promise.resolve();
}
let textareaText;
if (type !== 'regenerate' && type !== 'swipe' && type !== 'quiet' && !isImpersonate && !dryRun) {
is_send_press = true;
textareaText = String($('#send_textarea').val());
$('#send_textarea').val('').trigger('input');
} else {
textareaText = '';
if (chat.length && chat[chat.length - 1]['is_user']) {
//do nothing? why does this check exist?
}
else if (type !== 'quiet' && type !== 'swipe' && !isImpersonate && !dryRun && chat.length) {
chat.length = chat.length - 1;
$('#chat').children().last().hide(250, function () {
$(this).remove();
});
await eventSource.emit(event_types.MESSAGE_DELETED, chat.length);
}
}
const isContinue = type == 'continue';
// Rewrite the generation timer to account for the time passed for all the continuations.
if (isContinue && chat.length) {
const prevFinished = chat[chat.length - 1]['gen_finished'];
const prevStarted = chat[chat.length - 1]['gen_started'];
if (prevFinished && prevStarted) {
const timePassed = prevFinished - prevStarted;
generation_started = new Date(Date.now() - timePassed);
chat[chat.length - 1]['gen_started'] = generation_started;
}
}
if (!dryRun) {
deactivateSendButtons();
}
let { messageBias, promptBias, isUserPromptBias } = getBiasStrings(textareaText, type);
//*********************************
//PRE FORMATING STRING
//*********************************
//for normal messages sent from user..
if ((textareaText != '' || hasPendingFileAttachment()) && !automatic_trigger && type !== 'quiet' && !dryRun) {
// If user message contains no text other than bias - send as a system message
if (messageBias && !removeMacros(textareaText)) {
sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
}
else {
await sendMessageAsUser(textareaText, messageBias);
}
}
else if (textareaText == '' && !automatic_trigger && !dryRun && type === undefined && main_api == 'openai' && oai_settings.send_if_empty.trim().length > 0) {
// Use send_if_empty if set and the user message is empty. Only when sending messages normally
await sendMessageAsUser(oai_settings.send_if_empty.trim(), messageBias);
}
let {
description,
personality,
persona,
scenario,
mesExamples,
system,
jailbreak,
} = getCharacterCardFields();
if (isInstruct) {
system = power_user.prefer_character_prompt && system ? system : baseChatReplace(power_user.instruct.system_prompt, name1, name2);
system = formatInstructModeSystemPrompt(substituteParams(system, name1, name2, power_user.instruct.system_prompt));
}
// Depth prompt (character-specific A/N)
removeDepthPrompts();
const groupDepthPrompts = getGroupDepthPrompts(selected_group, Number(this_chid));
if (selected_group && Array.isArray(groupDepthPrompts) && groupDepthPrompts.length > 0) {
groupDepthPrompts.forEach((value, index) => {
setExtensionPrompt('DEPTH_PROMPT_' + index, value.text, extension_prompt_types.IN_CHAT, value.depth, extension_settings.note.allowWIScan);
});
} else {
const depthPromptText = baseChatReplace(characters[this_chid].data?.extensions?.depth_prompt?.prompt?.trim(), name1, name2) || '';
const depthPromptDepth = characters[this_chid].data?.extensions?.depth_prompt?.depth ?? depth_prompt_depth_default;
setExtensionPrompt('DEPTH_PROMPT', depthPromptText, extension_prompt_types.IN_CHAT, depthPromptDepth, extension_settings.note.allowWIScan);
}
// Parse example messages
if (!mesExamples.startsWith('')) {
mesExamples = '\n' + mesExamples.trim();
}
if (mesExamples.replace(//gi, '').trim().length === 0) {
mesExamples = '';
}
const mesExamplesRaw = mesExamples;
if (mesExamples && isInstruct) {
mesExamples = formatInstructModeExamples(mesExamples, name1, name2);
}
/**
* Adds a block heading to the examples string.
* @param {string} examplesStr
* @returns {string[]} Examples array with block heading
*/
function addBlockHeading(examplesStr) {
const exampleSeparator = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
const blockHeading = main_api === 'openai' ? '\n' : exampleSeparator;
return examplesStr.split(//gi).slice(1).map(block => `${blockHeading}${block.trim()}\n`);
}
let mesExamplesArray = addBlockHeading(mesExamples);
let mesExamplesRawArray = addBlockHeading(mesExamplesRaw);
// First message in fresh 1-on-1 chat reacts to user/character settings changes
if (chat.length) {
chat[0].mes = substituteParams(chat[0].mes);
}
// Collect messages with usable content
let coreChat = chat.filter(x => !x.is_system);
if (type === 'swipe') {
coreChat.pop();
}
coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
let message = chatItem.mes;
let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
let options = { isPrompt: true, depth: (coreChat.length - index - 1) };
let regexedMessage = getRegexedString(message, regexType, options);
regexedMessage = await appendFileContent(chatItem, regexedMessage);
return {
...chatItem,
mes: regexedMessage,
index,
};
}));
// Determine token limit
let this_max_context = getMaxContextSize();
if (!dryRun && type !== 'quiet') {
console.debug('Running extension interceptors');
const aborted = await runGenerationInterceptors(coreChat, this_max_context);
if (aborted) {
console.debug('Generation aborted by extension interceptors');
unblockGeneration();
return Promise.resolve();
}
} else {
console.debug('Skipping extension interceptors for dry run');
}
console.log(`Core/all messages: ${coreChat.length}/${chat.length}`);
// kingbri MARK: - Make sure the prompt bias isn't the same as the user bias
if ((promptBias && !isUserPromptBias) || power_user.always_force_name2 || main_api == 'novel') {
force_name2 = true;
}
if (isImpersonate) {
force_name2 = false;
}
//////////////////////////////////
let chat2 = [];
let continue_mag = '';
for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {
// For OpenAI it's only used in WI
if (main_api == 'openai' && (!world_info || world_info.length === 0)) {
console.debug('No WI, skipping chat2 for OAI');
break;
}
chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, false);
if (j === 0 && isInstruct) {
// Reformat with the first output sequence (if any)
chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.FIRST);
}
// Do not suffix the message for continuation
if (i === 0 && isContinue) {
if (isInstruct) {
// Reformat with the last output sequence (if any)
chat2[i] = formatMessageHistoryItem(coreChat[j], isInstruct, force_output_sequence.LAST);
}
chat2[i] = chat2[i].slice(0, chat2[i].lastIndexOf(coreChat[j].mes) + coreChat[j].mes.length);
continue_mag = coreChat[j].mes;
}
}
// Adjust token limit for Horde
let adjustedParams;
if (main_api == 'koboldhorde' && (horde_settings.auto_adjust_context_length || horde_settings.auto_adjust_response_length)) {
try {
adjustedParams = await adjustHordeGenerationParams(max_context, amount_gen);
}
catch {
unblockGeneration();
return Promise.resolve();
}
if (horde_settings.auto_adjust_context_length) {
this_max_context = (adjustedParams.maxContextLength - adjustedParams.maxLength);
}
}
// Extension added strings
// Set non-WI AN
setFloatingPrompt();
// Add WI to prompt (and also inject WI to AN value via hijack)
let { worldInfoString, worldInfoBefore, worldInfoAfter, worldInfoDepth } = await getWorldInfoPrompt(chat2, this_max_context);
if (skipWIAN !== true) {
console.log('skipWIAN not active, adding WIAN');
// Add all depth WI entries to prompt
flushWIDepthInjections();
if (Array.isArray(worldInfoDepth)) {
worldInfoDepth.forEach((e) => {
const joinedEntries = e.entries.join('\n');
setExtensionPrompt(`customDepthWI-${e.depth}`, joinedEntries, extension_prompt_types.IN_CHAT, e.depth);
});
}
} else {
console.log('skipping WIAN');
}
// Add persona description to prompt
addPersonaDescriptionExtensionPrompt();
// Call combined AN into Generate
let allAnchors = getAllExtensionPrompts();
const beforeScenarioAnchor = getExtensionPrompt(extension_prompt_types.BEFORE_PROMPT).trimStart();
const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.IN_PROMPT);
let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' ');
const storyStringParams = {
description: description,
personality: personality,
persona: persona,
scenario: scenario,
system: isInstruct ? system : '',
char: name2,
user: name1,
wiBefore: worldInfoBefore,
wiAfter: worldInfoAfter,
loreBefore: worldInfoBefore,
loreAfter: worldInfoAfter,
mesExamples: mesExamplesArray.join(''),
mesExamplesRaw: mesExamplesRawArray.join(''),
};
const storyString = renderStoryString(storyStringParams);
// Story string rendered, safe to remove
if (power_user.strip_examples) {
mesExamplesArray = [];
}
let oaiMessages = [];
let oaiMessageExamples = [];
if (main_api === 'openai') {
message_already_generated = '';
oaiMessages = setOpenAIMessages(coreChat);
oaiMessageExamples = setOpenAIMessageExamples(mesExamplesArray);
}
// hack for regeneration of the first message
if (chat2.length == 0) {
chat2.push('');
}
let examplesString = '';
let chatString = '';
let cyclePrompt = '';
function getMessagesTokenCount() {
const encodeString = [
storyString,
examplesString,
chatString,
allAnchors,
quiet_prompt,
cyclePrompt,
].join('').replace(/\r/gm, '');
return getTokenCount(encodeString, power_user.token_padding);
}
// Force pinned examples into the context
let pinExmString;
if (power_user.pin_examples) {
pinExmString = examplesString = mesExamplesArray.join('');
}
// Only add the chat in context if past the greeting message
if (isContinue && (chat2.length > 1 || main_api === 'openai')) {
cyclePrompt = chat2.shift();
}
// Collect enough messages to fill the context
let arrMes = [];
let tokenCount = getMessagesTokenCount();
for (let item of chat2) {
// not needed for OAI prompting
if (main_api == 'openai') {
break;
}
tokenCount += getTokenCount(item.replace(/\r/gm, ''));
chatString = item + chatString;
if (tokenCount < this_max_context) {
arrMes[arrMes.length] = item;
} else {
break;
}
// Prevent UI thread lock on tokenization
await delay(1);
}
if (main_api !== 'openai') {
setInContextMessages(arrMes.length, type);
}
// Estimate how many unpinned example messages fit in the context
tokenCount = getMessagesTokenCount();
let count_exm_add = 0;
if (!power_user.pin_examples) {
for (let example of mesExamplesArray) {
tokenCount += getTokenCount(example.replace(/\r/gm, ''));
examplesString += example;
if (tokenCount < this_max_context) {
count_exm_add++;
} else {
break;
}
await delay(1);
}
}
let mesSend = [];
console.debug('calling runGenerate');
if (isContinue) {
// Coping mechanism for OAI spacing
const isForceInstruct = isOpenRouterWithInstruct();
if (main_api === 'openai' && !isForceInstruct && !cyclePrompt.endsWith(' ')) {
cyclePrompt += ' ';
continue_mag += ' ';
}
message_already_generated = continue_mag;
}
const originalType = type;
if (!dryRun) {
is_send_press = true;
}
generatedPromptCache += cyclePrompt;
if (generatedPromptCache.length == 0 || type === 'continue') {
console.debug('generating prompt');
chatString = '';
arrMes = arrMes.reverse();
arrMes.forEach(function (item, i, arr) {// For added anchors and others
// OAI doesn't need all of this
if (main_api === 'openai') {
return;
}
// Cohee: I'm not even sure what this is for anymore
if (i === arrMes.length - 1 && type !== 'continue') {
item = item.replace(/\n?$/, '');
}
mesSend[mesSend.length] = { message: item, extensionPrompts: [] };
});
}
let mesExmString = '';
function setPromptString() {
if (main_api == 'openai') {
return;
}
console.debug('--setting Prompt string');
mesExmString = pinExmString ?? mesExamplesArray.slice(0, count_exm_add).join('');
if (mesSend.length) {
mesSend[mesSend.length - 1].message = modifyLastPromptLine(mesSend[mesSend.length - 1].message);
}
}
function modifyLastPromptLine(lastMesString) {
//#########QUIET PROMPT STUFF PT2##############
// Add quiet generation prompt at depth 0
if (quiet_prompt && quiet_prompt.length) {
// here name1 is forced for all quiet prompts..why?
const name = name1;
//checks if we are in instruct, if so, formats the chat as such, otherwise just adds the quiet prompt
const quietAppend = isInstruct ? formatInstructModeChat(name, quiet_prompt, false, true, '', name1, name2, false) : `\n${quiet_prompt}`;
//This begins to fix quietPrompts (particularly /sysgen) for instruct
//previously instruct input sequence was being appended to the last chat message w/o '\n'
//and no output sequence was added after the input's content.
//TODO: respect output_sequence vs last_output_sequence settings
//TODO: decide how to prompt this to clarify who is talking 'Narrator', 'System', etc.
if (isInstruct) {
lastMesString += '\n' + quietAppend; // + power_user.instruct.output_sequence + '\n';
} else {
lastMesString += quietAppend;
}
// Ross: bailing out early prevents quiet prompts from respecting other instruct prompt toggles
// for sysgen, SD, and summary this is desireable as it prevents the AI from responding as char..
// but for idle prompting, we want the flexibility of the other prompt toggles, and to respect them as per settings in the extension
// need a detection for what the quiet prompt is being asked for...
// Bail out early?
if (!isInstruct && !quietToLoud) {
return lastMesString;
}
}
// Get instruct mode line
if (isInstruct && !isContinue) {
const name = (quiet_prompt && !quietToLoud) ? (quietName ?? 'System') : (isImpersonate ? name1 : name2);
lastMesString += formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2);
}
// Get non-instruct impersonation line
if (!isInstruct && isImpersonate && !isContinue) {
const name = name1;
if (!lastMesString.endsWith('\n')) {
lastMesString += '\n';
}
lastMesString += name + ':';
}
// Add character's name
// Force name append on continue (if not continuing on user message or first message)
const isContinuingOnFirstMessage = chat.length === 1 && isContinue;
if (!isInstruct && force_name2 && !isContinuingOnFirstMessage) {
if (!lastMesString.endsWith('\n')) {
lastMesString += '\n';
}
if (!isContinue || !(chat[chat.length - 1]?.is_user)) {
lastMesString += `${name2}:`;
}
}
return lastMesString;
}
// Clean up the already generated prompt for seamless addition
function cleanupPromptCache(promptCache) {
// Remove the first occurrance of character's name
if (promptCache.trimStart().startsWith(`${name2}:`)) {
promptCache = promptCache.replace(`${name2}:`, '').trimStart();
}
// Remove the first occurrance of prompt bias
if (promptCache.trimStart().startsWith(promptBias)) {
promptCache = promptCache.replace(promptBias, '');
}
// Add a space if prompt cache doesn't start with one
if (!/^\s/.test(promptCache) && !isInstruct && !isContinue) {
promptCache = ' ' + promptCache;
}
return promptCache;
}
function checkPromptSize() {
console.debug('---checking Prompt size');
setPromptString();
const prompt = [
storyString,
mesExmString,
mesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join(''),
'\n',
generatedPromptCache,
allAnchors,
quiet_prompt,
].join('').replace(/\r/gm, '');
let thisPromptContextSize = getTokenCount(prompt, power_user.token_padding);
if (thisPromptContextSize > this_max_context) { //if the prepared prompt is larger than the max context size...
if (count_exm_add > 0) { // ..and we have example mesages..
count_exm_add--; // remove the example messages...
checkPromptSize(); // and try agin...
} else if (mesSend.length > 0) { // if the chat history is longer than 0
mesSend.shift(); // remove the first (oldest) chat entry..
checkPromptSize(); // and check size again..
} else {
//end
console.debug(`---mesSend.length = ${mesSend.length}`);
}
}
}
if (generatedPromptCache.length > 0 && main_api !== 'openai') {
console.debug('---Generated Prompt Cache length: ' + generatedPromptCache.length);
checkPromptSize();
} else {
console.debug('---calling setPromptString ' + generatedPromptCache.length);
setPromptString();
}
// Fetches the combined prompt for both negative and positive prompts
const cfgGuidanceScale = getGuidanceScale();
// For prompt bit itemization
let mesSendString = '';
function getCombinedPrompt(isNegative) {
// Only return if the guidance scale doesn't exist or the value is 1
// Also don't return if constructing the neutral prompt
if (isNegative && (!cfgGuidanceScale || cfgGuidanceScale?.value === 1)) {
return;
}
// OAI has its own prompt manager. No need to do anything here
if (main_api === 'openai') {
return '';
}
// Deep clone
let finalMesSend = structuredClone(mesSend);
// TODO: Rewrite getExtensionPrompt to not require multiple for loops
// Set all extension prompts where insertion depth > mesSend length
if (finalMesSend.length) {
for (let upperDepth = MAX_INJECTION_DEPTH; upperDepth >= finalMesSend.length; upperDepth--) {
const upperAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, upperDepth);
if (upperAnchor && upperAnchor.length) {
finalMesSend[0].extensionPrompts.push(upperAnchor);
}
}
}
finalMesSend.forEach((mesItem, index) => {
if (index === 0) {
return;
}
const anchorDepth = Math.abs(index - finalMesSend.length);
// NOTE: Depth injected here!
const extensionAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, anchorDepth);
if (anchorDepth >= 0 && extensionAnchor && extensionAnchor.length) {
mesItem.extensionPrompts.push(extensionAnchor);
}
});
// TODO: Move zero-depth anchor append to work like CFG and bias appends
if (zeroDepthAnchor?.length && !isContinue) {
console.debug(/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1)));
finalMesSend[finalMesSend.length - 1].message +=
/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
? zeroDepthAnchor
: `${zeroDepthAnchor}`;
}
let cfgPrompt = {};
if (cfgGuidanceScale && cfgGuidanceScale?.value !== 1) {
cfgPrompt = getCfgPrompt(cfgGuidanceScale, isNegative);
}
if (cfgPrompt && cfgPrompt?.value) {
if (cfgPrompt?.depth === 0) {
finalMesSend[finalMesSend.length - 1].message +=
/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
? cfgPrompt.value
: ` ${cfgPrompt.value}`;
} else {
// TODO: Make all extension prompts use an array/splice method
const lengthDiff = mesSend.length - cfgPrompt.depth;
const cfgDepth = lengthDiff >= 0 ? lengthDiff : 0;
finalMesSend[cfgDepth].extensionPrompts.push(`${cfgPrompt.value}\n`);
}
}
// Add prompt bias after everything else
// Always run with continue
if (!isInstruct && !isImpersonate) {
if (promptBias.trim().length !== 0) {
finalMesSend[finalMesSend.length - 1].message +=
/\s/.test(finalMesSend[finalMesSend.length - 1].message.slice(-1))
? promptBias.trimStart()
: ` ${promptBias.trimStart()}`;
}
}
// Prune from prompt cache if it exists
if (generatedPromptCache.length !== 0) {
generatedPromptCache = cleanupPromptCache(generatedPromptCache);
}
// Flattens the multiple prompt objects to a string.
const combine = () => {
// Right now, everything is suffixed with a newline
mesSendString = finalMesSend.map((e) => `${e.extensionPrompts.join('')}${e.message}`).join('');
// add a custom dingus (if defined)
mesSendString = addChatsSeparator(mesSendString);
// add chat preamble
mesSendString = addChatsPreamble(mesSendString);
let combinedPrompt = beforeScenarioAnchor +
storyString +
afterScenarioAnchor +
mesExmString +
mesSendString +
generatedPromptCache;
combinedPrompt = combinedPrompt.replace(/\r/gm, '');
if (power_user.collapse_newlines) {
combinedPrompt = collapseNewlines(combinedPrompt);
}
return combinedPrompt;
};
let data = {
api: main_api,
combinedPrompt: null,
description,
personality,
persona,
scenario,
char: name2,
user: name1,
worldInfoBefore,
worldInfoAfter,
beforeScenarioAnchor,
afterScenarioAnchor,
storyString,
mesExmString,
mesSendString,
finalMesSend,
generatedPromptCache,
main: system,
jailbreak,
naiPreamble: nai_settings.preamble,
};
// Before returning the combined prompt, give available context related information to all subscribers.
eventSource.emitAndWait(event_types.GENERATE_BEFORE_COMBINE_PROMPTS, data);
// If one or multiple subscribers return a value, forfeit the responsibillity of flattening the context.
return !data.combinedPrompt ? combine() : data.combinedPrompt;
}
// Get the negative prompt first since it has the unmodified mesSend array
let negativePrompt = main_api == 'textgenerationwebui' ? getCombinedPrompt(true) : undefined;
let finalPrompt = getCombinedPrompt(false);
// Include the entire guidance scale object
const cfgValues = cfgGuidanceScale && cfgGuidanceScale?.value !== 1 ? ({ guidanceScale: cfgGuidanceScale, negativePrompt: negativePrompt }) : null;
let maxLength = Number(amount_gen); // how many tokens the AI will be requested to generate
let thisPromptBits = [];
// TODO: Make this a switch
if (main_api == 'koboldhorde' && horde_settings.auto_adjust_response_length) {
maxLength = Math.min(maxLength, adjustedParams.maxLength);
maxLength = Math.max(maxLength, MIN_LENGTH); // prevent validation errors
}
let generate_data;
if (main_api == 'koboldhorde' || main_api == 'kobold') {
generate_data = {
prompt: finalPrompt,
gui_settings: true,
max_length: maxLength,
max_context_length: max_context,
api_server,
};
if (preset_settings != 'gui') {
const isHorde = main_api == 'koboldhorde';
const presetSettings = koboldai_settings[koboldai_setting_names[preset_settings]];
const maxContext = (adjustedParams && horde_settings.auto_adjust_context_length) ? adjustedParams.maxContextLength : max_context;
generate_data = getKoboldGenerationData(finalPrompt, presetSettings, maxLength, maxContext, isHorde, type);
}
}
else if (main_api == 'textgenerationwebui') {
generate_data = getTextGenGenerationData(finalPrompt, maxLength, isImpersonate, isContinue, cfgValues, type);
}
else if (main_api == 'novel') {
const presetSettings = novelai_settings[novelai_setting_names[nai_settings.preset_settings_novel]];
generate_data = getNovelGenerationData(finalPrompt, presetSettings, maxLength, isImpersonate, isContinue, cfgValues, type);
}
else if (main_api == 'openai') {
let [prompt, counts] = await prepareOpenAIMessages({
name2: name2,
charDescription: description,
charPersonality: personality,
Scenario: scenario,
worldInfoBefore: worldInfoBefore,
worldInfoAfter: worldInfoAfter,
extensionPrompts: extension_prompts,
bias: promptBias,
type: type,
quietPrompt: quiet_prompt,
quietImage: quietImage,
cyclePrompt: cyclePrompt,
systemPromptOverride: system,
jailbreakPromptOverride: jailbreak,
personaDescription: persona,
messages: oaiMessages,
messageExamples: oaiMessageExamples,
}, dryRun);
generate_data = { prompt: prompt };
// counts will return false if the user has not enabled the token breakdown feature
if (counts) {
parseTokenCounts(counts, thisPromptBits);
}
if (!dryRun) {
setInContextMessages(openai_messages_count, type);
}
}
if (dryRun) {
generatedPromptCache = '';
return Promise.resolve();
}
async function finishGenerating() {
if (power_user.console_log_prompts) {
console.log(generate_data.prompt);
}
console.debug('rungenerate calling API');
showStopButton();
//set array object for prompt token itemization of this message
let currentArrayEntry = Number(thisPromptBits.length - 1);
let additionalPromptStuff = {
...thisPromptBits[currentArrayEntry],
rawPrompt: generate_data.prompt || generate_data.input,
mesId: getNextMessageId(type),
allAnchors: allAnchors,
summarizeString: (extension_prompts['1_memory']?.value || ''),
authorsNoteString: (extension_prompts['2_floating_prompt']?.value || ''),
smartContextString: (extension_prompts['chromadb']?.value || ''),
worldInfoString: worldInfoString,
storyString: storyString,
beforeScenarioAnchor: beforeScenarioAnchor,
afterScenarioAnchor: afterScenarioAnchor,
examplesString: examplesString,
mesSendString: mesSendString,
generatedPromptCache: generatedPromptCache,
promptBias: promptBias,
finalPrompt: finalPrompt,
charDescription: description,
charPersonality: personality,
scenarioText: scenario,
this_max_context: this_max_context,
padding: power_user.token_padding,
main_api: main_api,
instruction: isInstruct ? substituteParams(power_user.prefer_character_prompt && system ? system : power_user.instruct.system_prompt) : '',
userPersona: (power_user.persona_description || ''),
};
thisPromptBits = additionalPromptStuff;
//console.log(thisPromptBits);
const itemizedIndex = itemizedPrompts.findIndex((item) => item.mesId === thisPromptBits['mesId']);
if (itemizedIndex !== -1) {
itemizedPrompts[itemizedIndex] = thisPromptBits;
}
else {
itemizedPrompts.push(thisPromptBits);
}
console.debug(`pushed prompt bits to itemizedPrompts array. Length is now: ${itemizedPrompts.length}`);
if (isStreamingEnabled() && type !== 'quiet') {
streamingProcessor = new StreamingProcessor(type, force_name2, generation_started, message_already_generated);
if (isContinue) {
// Save reply does add cycle text to the prompt, so it's not needed here
streamingProcessor.firstMessageText = '';
}
streamingProcessor.generator = await sendStreamingRequest(type, generate_data);
hideSwipeButtons();
let getMessage = await streamingProcessor.generate();
let messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
if (isContinue) {
getMessage = continue_mag + getMessage;
}
if (streamingProcessor && !streamingProcessor.isStopped && streamingProcessor.isFinished) {
await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage);
streamingProcessor = null;
triggerAutoContinue(messageChunk, isImpersonate);
}
} else {
return await sendGenerationRequest(type, generate_data);
}
}
return finishGenerating().then(onSuccess, onError);
async function onSuccess(data) {
if (!data) return;
let messageChunk = '';
if (data.error) {
generatedPromptCache = '';
if (data?.response) {
toastr.error(data.response, 'API Error');
}
throw data?.response;
}
//const getData = await response.json();
let getMessage = extractMessageFromData(data);
let title = extractTitleFromData(data);
kobold_horde_model = title;
const swipes = extractMultiSwipes(data, type);
messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
if (isContinue) {
getMessage = continue_mag + getMessage;
}
//Formating
const displayIncomplete = type === 'quiet' && !quietToLoud;
getMessage = cleanUpMessage(getMessage, isImpersonate, isContinue, displayIncomplete);
if (getMessage.length > 0) {
if (isImpersonate) {
$('#send_textarea').val(getMessage).trigger('input');
generatedPromptCache = '';
await eventSource.emit(event_types.IMPERSONATE_READY, getMessage);
}
else if (type == 'quiet') {
unblockGeneration();
return getMessage;
}
else {
// Without streaming we'll be having a full message on continuation. Treat it as a last chunk.
if (originalType !== 'continue') {
({ type, getMessage } = await saveReply(type, getMessage, false, title, swipes));
}
else {
({ type, getMessage } = await saveReply('appendFinal', getMessage, false, title, swipes));
}
// This relies on `saveReply` having been called to add the message to the chat, so it must be last.
parseAndSaveLogprobs(data, continue_mag);
}
if (type !== 'quiet') {
playMessageSound();
}
} else {
// If maxLoops is not passed in (e.g. first time generating), set it to MAX_GENERATION_LOOPS
maxLoops ??= MAX_GENERATION_LOOPS;
if (maxLoops === 0) {
if (type !== 'quiet') {
throwCircuitBreakerError();
}
throw new Error('Generate circuit breaker interruption');
}
// regenerate with character speech reenforced
// to make sure we leave on swipe type while also adding the name2 appendage
await delay(1000);
// The first await is for waiting for the generate to start. The second one is waiting for it to finish
const result = await await Generate(type, { automatic_trigger, force_name2: true, quiet_prompt, skipWIAN, force_chid, maxLoops: maxLoops - 1 });
return result;
}
if (power_user.auto_swipe) {
console.debug('checking for autoswipeblacklist on non-streaming message');
function containsBlacklistedWords(getMessage, blacklist, threshold) {
console.debug('checking blacklisted words');
const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
const matches = getMessage.match(regex) || [];
return matches.length >= threshold;
}
const generatedTextFiltered = (getMessage) => {
if (power_user.auto_swipe_blacklist_threshold) {
if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
console.debug('Generated text has blacklisted words');
return true;
}
}
return false;
};
if (generatedTextFiltered(getMessage)) {
console.debug('swiping right automatically');
is_send_press = false;
swipe_right();
// TODO: do we want to resolve after an auto-swipe?
return;
}
}
console.debug('/api/chats/save called by /Generate');
await saveChatConditional();
unblockGeneration();
streamingProcessor = null;
if (type !== 'quiet') {
triggerAutoContinue(messageChunk, isImpersonate);
}
}
function onError(exception) {
if (typeof exception?.error?.message === 'string') {
toastr.error(exception.error.message, 'Error', { timeOut: 10000, extendedTimeOut: 20000 });
}
unblockGeneration();
console.log(exception);
streamingProcessor = null;
throw exception;
}
}
function flushWIDepthInjections() {
//prevent custom depth WI entries (which have unique random key names) from duplicating
for (const key of Object.keys(extension_prompts)) {
if (key.startsWith('customDepthWI')) {
delete extension_prompts[key];
}
}
}
function unblockGeneration() {
is_send_press = false;
activateSendButtons();
showSwipeButtons();
setGenerationProgress(0);
flushEphemeralStoppingStrings();
flushWIDepthInjections();
$('#send_textarea').removeAttr('disabled');
}
export function getNextMessageId(type) {
return type == 'swipe' ? chat.length - 1 : chat.length;
}
/**
*
* @param {string} messageChunk
* @param {boolean} isImpersonate
* @returns {void}
*/
export function triggerAutoContinue(messageChunk, isImpersonate) {
if (selected_group) {
console.log('Auto-continue is disabled for group chat');
return;
}
if (power_user.auto_continue.enabled && !is_send_press) {
if (power_user.auto_continue.target_length <= 0) {
console.log('Auto-continue target length is 0, not triggering auto-continue');
return;
}
if (main_api === 'openai' && !power_user.auto_continue.allow_chat_completions) {
console.log('Auto-continue for OpenAI is disabled by user.');
return;
}
if (isImpersonate) {
console.log('Continue for impersonation is not implemented yet');
return;
}
const textareaText = String($('#send_textarea').val());
const USABLE_LENGTH = 5;
if (textareaText.length > 0) {
console.log('Not triggering auto-continue because user input is not empty');
return;
}
if (messageChunk.trim().length > USABLE_LENGTH && chat.length) {
const lastMessage = chat[chat.length - 1];
const messageLength = getTokenCount(lastMessage.mes);
const shouldAutoContinue = messageLength < power_user.auto_continue.target_length;
if (shouldAutoContinue) {
console.log(`Triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}. Message chunk: ${messageChunk}`);
$('#option_continue').trigger('click');
} else {
console.log(`Not triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}`);
return;
}
} else {
console.log('Last generated chunk was empty, not triggering auto-continue');
return;
}
}
}
export function getBiasStrings(textareaText, type) {
if (type == 'impersonate' || type == 'continue') {
return { messageBias: '', promptBias: '', isUserPromptBias: false };
}
let promptBias = '';
let messageBias = extractMessageBias(textareaText);
// If user input is not provided, retrieve the bias of the most recent relevant message
if (!textareaText) {
for (let i = chat.length - 1; i >= 0; i--) {
const mes = chat[i];
if (type === 'swipe' && chat.length - 1 === i) {
continue;
}
if (mes && (mes.is_user || mes.is_system || mes.extra?.type === system_message_types.NARRATOR)) {
if (mes.extra?.bias?.trim()?.length > 0) {
promptBias = mes.extra.bias;
}
break;
}
}
}
promptBias = messageBias || promptBias || power_user.user_prompt_bias || '';
const isUserPromptBias = promptBias === power_user.user_prompt_bias;
// Substitute params for everything
messageBias = substituteParams(messageBias);
promptBias = substituteParams(promptBias);
return { messageBias, promptBias, isUserPromptBias };
}
/**
* @param {Object} chatItem Message history item.
* @param {boolean} isInstruct Whether instruct mode is enabled.
* @param {boolean|number} forceOutputSequence Whether to force the first/last output sequence for instruct mode.
*/
function formatMessageHistoryItem(chatItem, isInstruct, forceOutputSequence) {
const isNarratorType = chatItem?.extra?.type === system_message_types.NARRATOR;
const characterName = chatItem?.name ? chatItem.name : name2;
const itemName = chatItem.is_user ? chatItem['name'] : characterName;
const shouldPrependName = !isNarratorType;
let textResult = shouldPrependName ? `${itemName}: ${chatItem.mes}\n` : `${chatItem.mes}\n`;
if (isInstruct) {
textResult = formatInstructModeChat(itemName, chatItem.mes, chatItem.is_user, isNarratorType, chatItem.force_avatar, name1, name2, forceOutputSequence);
}
return textResult;
}
/**
* Removes all {{macros}} from a string.
* @param {string} str String to remove macros from.
* @returns {string} String with macros removed.
*/
export function removeMacros(str) {
return (str ?? '').replace(/\{\{[\s\S]*?\}\}/gm, '').trim();
}
/**
* Inserts a user message into the chat history.
* @param {string} messageText Message text.
* @param {string} messageBias Message bias.
* @param {number} [insertAt] Optional index to insert the message at.
* @returns {Promise} A promise that resolves when the message is inserted.
*/
export async function sendMessageAsUser(messageText, messageBias, insertAt = null) {
messageText = getRegexedString(messageText, regex_placement.USER_INPUT);
const message = {
name: name1,
is_user: true,
is_system: false,
send_date: getMessageTimeStamp(),
mes: substituteParams(messageText),
extra: {},
};
if (power_user.message_token_count_enabled) {
message.extra.token_count = getTokenCount(message.mes, 0);
}
// Lock user avatar to a persona.
if (user_avatar in power_user.personas) {
message.force_avatar = getUserAvatar(user_avatar);
}
if (messageBias) {
message.extra.bias = messageBias;
message.mes = removeMacros(message.mes);
}
await populateFileAttachment(message);
statMesProcess(message, 'user', characters, this_chid, '');
if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) {
chat.splice(insertAt, 0, message);
await saveChatConditional();
await eventSource.emit(event_types.MESSAGE_SENT, insertAt);
await reloadCurrentChat();
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, insertAt);
} else {
chat.push(message);
const chat_id = (chat.length - 1);
await eventSource.emit(event_types.MESSAGE_SENT, chat_id);
addOneMessage(message);
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, chat_id);
}
}
export function getMaxContextSize() {
let this_max_context = 1487;
if (main_api == 'kobold' || main_api == 'koboldhorde' || main_api == 'textgenerationwebui') {
this_max_context = (max_context - amount_gen);
}
if (main_api == 'novel') {
this_max_context = Number(max_context);
if (nai_settings.model_novel.includes('clio')) {
this_max_context = Math.min(max_context, 8192);
}
if (nai_settings.model_novel.includes('kayra')) {
this_max_context = Math.min(max_context, 8192);
const subscriptionLimit = getKayraMaxContextTokens();
if (typeof subscriptionLimit === 'number' && this_max_context > subscriptionLimit) {
this_max_context = subscriptionLimit;
console.log(`NovelAI subscription limit reached. Max context size is now ${this_max_context}`);
}
}
this_max_context = this_max_context - amount_gen;
}
if (main_api == 'openai') {
this_max_context = oai_settings.openai_max_context - oai_settings.openai_max_tokens;
}
return this_max_context;
}
function parseTokenCounts(counts, thisPromptBits) {
/**
* @param {any[]} numbers
*/
function getSum(...numbers) {
return numbers.map(x => Number(x)).filter(x => !Number.isNaN(x)).reduce((acc, val) => acc + val, 0);
}
const total = getSum(Object.values(counts));
thisPromptBits.push({
oaiStartTokens: (counts?.start + counts?.controlPrompts) || 0,
oaiPromptTokens: getSum(counts?.prompt, counts?.charDescription, counts?.charPersonality, counts?.scenario) || 0,
oaiBiasTokens: counts?.bias || 0,
oaiNudgeTokens: counts?.nudge || 0,
oaiJailbreakTokens: counts?.jailbreak || 0,
oaiImpersonateTokens: counts?.impersonate || 0,
oaiExamplesTokens: (counts?.dialogueExamples + counts?.examples) || 0,
oaiConversationTokens: (counts?.conversation + counts?.chatHistory) || 0,
oaiNsfwTokens: counts?.nsfw || 0,
oaiMainTokens: counts?.main || 0,
oaiTotalTokens: total,
});
}
function addChatsPreamble(mesSendString) {
return main_api === 'novel'
? substituteParams(nai_settings.preamble) + '\n' + mesSendString
: mesSendString;
}
function addChatsSeparator(mesSendString) {
if (power_user.context.chat_start) {
return substituteParams(power_user.context.chat_start) + '\n' + mesSendString;
}
else {
return mesSendString;
}
}
// There's a TODO related to zero-depth anchors; not removing this function until that's resolved
// eslint-disable-next-line no-unused-vars
function appendZeroDepthAnchor(force_name2, zeroDepthAnchor, finalPrompt) {
const trimBothEnds = !force_name2;
let trimmedPrompt = (trimBothEnds ? zeroDepthAnchor.trim() : zeroDepthAnchor.trimEnd());
if (trimBothEnds && !finalPrompt.endsWith('\n')) {
finalPrompt += '\n';
}
finalPrompt += trimmedPrompt;
if (force_name2) {
finalPrompt += ' ';
}
return finalPrompt;
}
async function DupeChar() {
if (!this_chid) {
toastr.warning('You must first select a character to duplicate!');
return;
}
const confirmMessage = `
Are you sure you want to duplicate this character?
If you just want to start a new chat with the same character, use "Start new chat" option in the bottom-left options menu.
`;
const confirm = await callPopup(confirmMessage, 'confirm');
if (!confirm) {
console.log('User cancelled duplication');
return;
}
const body = { avatar_url: characters[this_chid].avatar };
const response = await fetch('/api/characters/duplicate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(body),
});
if (response.ok) {
toastr.success('Character Duplicated');
getCharacters();
}
}
function promptItemize(itemizedPrompts, requestedMesId) {
console.log('PROMPT ITEMIZE ENTERED');
var incomingMesId = Number(requestedMesId);
console.debug(`looking for MesId ${incomingMesId}`);
var thisPromptSet = undefined;
for (var i = 0; i < itemizedPrompts.length; i++) {
console.log(`looking for ${incomingMesId} vs ${itemizedPrompts[i].mesId}`);
if (itemizedPrompts[i].mesId === incomingMesId) {
console.log(`found matching mesID ${i}`);
thisPromptSet = i;
PromptArrayItemForRawPromptDisplay = i;
console.log(`wanting to raw display of ArrayItem: ${PromptArrayItemForRawPromptDisplay} which is mesID ${incomingMesId}`);
console.log(itemizedPrompts[thisPromptSet]);
}
}
if (thisPromptSet === undefined) {
console.log(`couldnt find the right mesId. looked for ${incomingMesId}`);
console.log(itemizedPrompts);
return null;
}
const params = {
charDescriptionTokens: getTokenCount(itemizedPrompts[thisPromptSet].charDescription),
charPersonalityTokens: getTokenCount(itemizedPrompts[thisPromptSet].charPersonality),
scenarioTextTokens: getTokenCount(itemizedPrompts[thisPromptSet].scenarioText),
userPersonaStringTokens: getTokenCount(itemizedPrompts[thisPromptSet].userPersona),
worldInfoStringTokens: getTokenCount(itemizedPrompts[thisPromptSet].worldInfoString),
allAnchorsTokens: getTokenCount(itemizedPrompts[thisPromptSet].allAnchors),
summarizeStringTokens: getTokenCount(itemizedPrompts[thisPromptSet].summarizeString),
authorsNoteStringTokens: getTokenCount(itemizedPrompts[thisPromptSet].authorsNoteString),
smartContextStringTokens: getTokenCount(itemizedPrompts[thisPromptSet].smartContextString),
beforeScenarioAnchorTokens: getTokenCount(itemizedPrompts[thisPromptSet].beforeScenarioAnchor),
afterScenarioAnchorTokens: getTokenCount(itemizedPrompts[thisPromptSet].afterScenarioAnchor),
zeroDepthAnchorTokens: getTokenCount(itemizedPrompts[thisPromptSet].zeroDepthAnchor), // TODO: unused
thisPrompt_padding: itemizedPrompts[thisPromptSet].padding,
this_main_api: itemizedPrompts[thisPromptSet].main_api,
};
if (params.this_main_api == 'openai') {
//for OAI API
//console.log('-- Counting OAI Tokens');
//params.finalPromptTokens = itemizedPrompts[thisPromptSet].oaiTotalTokens;
params.oaiMainTokens = itemizedPrompts[thisPromptSet].oaiMainTokens;
params.oaiStartTokens = itemizedPrompts[thisPromptSet].oaiStartTokens;
params.ActualChatHistoryTokens = itemizedPrompts[thisPromptSet].oaiConversationTokens;
params.examplesStringTokens = itemizedPrompts[thisPromptSet].oaiExamplesTokens;
params.oaiPromptTokens = itemizedPrompts[thisPromptSet].oaiPromptTokens - (params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens) + params.examplesStringTokens;
params.oaiBiasTokens = itemizedPrompts[thisPromptSet].oaiBiasTokens;
params.oaiJailbreakTokens = itemizedPrompts[thisPromptSet].oaiJailbreakTokens;
params.oaiNudgeTokens = itemizedPrompts[thisPromptSet].oaiNudgeTokens;
params.oaiImpersonateTokens = itemizedPrompts[thisPromptSet].oaiImpersonateTokens;
params.oaiNsfwTokens = itemizedPrompts[thisPromptSet].oaiNsfwTokens;
params.finalPromptTokens =
params.oaiStartTokens +
params.oaiPromptTokens +
params.oaiMainTokens +
params.oaiNsfwTokens +
params.oaiBiasTokens +
params.oaiImpersonateTokens +
params.oaiJailbreakTokens +
params.oaiNudgeTokens +
params.ActualChatHistoryTokens +
//charDescriptionTokens +
//charPersonalityTokens +
//allAnchorsTokens +
params.worldInfoStringTokens +
params.beforeScenarioAnchorTokens +
params.afterScenarioAnchorTokens;
// Max context size - max completion tokens
params.thisPrompt_max_context = (oai_settings.openai_max_context - oai_settings.openai_max_tokens);
//console.log('-- applying % on OAI tokens');
params.oaiStartTokensPercentage = ((params.oaiStartTokens / (params.finalPromptTokens)) * 100).toFixed(2);
params.storyStringTokensPercentage = (((params.afterScenarioAnchorTokens + params.beforeScenarioAnchorTokens + params.oaiPromptTokens) / (params.finalPromptTokens)) * 100).toFixed(2);
params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.finalPromptTokens)) * 100).toFixed(2);
params.promptBiasTokensPercentage = ((params.oaiBiasTokens / (params.finalPromptTokens)) * 100).toFixed(2);
params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.finalPromptTokens)) * 100).toFixed(2);
params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.finalPromptTokens)) * 100).toFixed(2);
params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
params.oaiSystemTokens = params.oaiImpersonateTokens + params.oaiJailbreakTokens + params.oaiNudgeTokens + params.oaiStartTokens + params.oaiNsfwTokens + params.oaiMainTokens;
params.oaiSystemTokensPercentage = ((params.oaiSystemTokens / (params.finalPromptTokens)) * 100).toFixed(2);
} else {
//for non-OAI APIs
//console.log('-- Counting non-OAI Tokens');
params.finalPromptTokens = getTokenCount(itemizedPrompts[thisPromptSet].finalPrompt);
params.storyStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].storyString) - params.worldInfoStringTokens;
params.examplesStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].examplesString);
params.mesSendStringTokens = getTokenCount(itemizedPrompts[thisPromptSet].mesSendString);
params.ActualChatHistoryTokens = params.mesSendStringTokens - (params.allAnchorsTokens - (params.beforeScenarioAnchorTokens + params.afterScenarioAnchorTokens)) + power_user.token_padding;
params.instructionTokens = getTokenCount(itemizedPrompts[thisPromptSet].instruction);
params.promptBiasTokens = getTokenCount(itemizedPrompts[thisPromptSet].promptBias);
params.totalTokensInPrompt =
params.storyStringTokens + //chardefs total
params.worldInfoStringTokens +
params.examplesStringTokens + // example messages
params.ActualChatHistoryTokens + //chat history
params.allAnchorsTokens + // AN and/or legacy anchors
//afterScenarioAnchorTokens + //only counts if AN is set to 'after scenario'
//zeroDepthAnchorTokens + //same as above, even if AN not on 0 depth
params.promptBiasTokens; //{{}}
//- thisPrompt_padding; //not sure this way of calculating is correct, but the math results in same value as 'finalPrompt'
params.thisPrompt_max_context = itemizedPrompts[thisPromptSet].this_max_context;
params.thisPrompt_actual = params.thisPrompt_max_context - params.thisPrompt_padding;
//console.log('-- applying % on non-OAI tokens');
params.storyStringTokensPercentage = ((params.storyStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
params.ActualChatHistoryTokensPercentage = ((params.ActualChatHistoryTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
params.promptBiasTokensPercentage = ((params.promptBiasTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
params.worldInfoStringTokensPercentage = ((params.worldInfoStringTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
params.allAnchorsTokensPercentage = ((params.allAnchorsTokens / (params.totalTokensInPrompt)) * 100).toFixed(2);
params.selectedTokenizer = getFriendlyTokenizerName(params.this_main_api).tokenizerName;
}
if (params.this_main_api == 'openai') {
callPopup(renderTemplate('itemizationChat', params), 'text');
} else {
callPopup(renderTemplate('itemizationText', params), 'text');
}
}
function setInContextMessages(lastmsg, type) {
$('#chat .mes').removeClass('lastInContext');
if (type === 'swipe' || type === 'regenerate' || type === 'continue') {
lastmsg++;
}
const lastMessageBlock = $('#chat .mes:not([is_system="true"])').eq(-lastmsg);
lastMessageBlock.addClass('lastInContext');
if (lastMessageBlock.length === 0) {
const firstMessageId = getFirstDisplayedMessageId();
$(`#chat .mes[mesid="${firstMessageId}"`).addClass('lastInContext');
}
}
/**
* Sends a non-streaming request to the API.
* @param {string} type Generation type
* @param {object} data Generation data
* @returns {Promise