`, POPUP_TYPE.INPUT);
if (key == false) {
return;
@@ -621,7 +621,7 @@ jQuery(async () => {
const secretKey = extension_settings.translate.provider + '_url';
const savedUrl = secret_state[secretKey] ? await findSecret(secretKey) : '';
- const url = await callPopup(popupText, 'input', savedUrl);
+ const url = await callGenericPopup(popupText, POPUP_TYPE.INPUT, savedUrl);
if (url == false || url == '') {
return;
diff --git a/public/scripts/extensions/tts/index.js b/public/scripts/extensions/tts/index.js
index 7878b86de..13d3718ed 100644
--- a/public/scripts/extensions/tts/index.js
+++ b/public/scripts/extensions/tts/index.js
@@ -1,4 +1,4 @@
-import { callPopup, cancelTtsPlay, eventSource, event_types, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
+import { cancelTtsPlay, eventSource, event_types, isStreamingEnabled, name2, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { delay, escapeRegex, getBase64Async, getStringHash, onlyUnique } from '../../utils.js';
import { EdgeTtsProvider } from './edge.js';
@@ -21,6 +21,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
import { debounce_timeout } from '../../constants.js';
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
+import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
export { talkingAnimation };
const UPDATE_INTERVAL = 1000;
@@ -310,7 +311,7 @@ async function onTtsVoicesClick() {
popupText = 'Could not load voices list. Check your API key.';
}
- callPopup(popupText, 'text');
+ callGenericPopup(popupText, POPUP_TYPE.TEXT, '', { allowVerticalScrolling: true });
}
function updateUiAudioPlayState() {
diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js
index 5b7c97953..fdc91c1df 100644
--- a/public/scripts/group-chats.js
+++ b/public/scripts/group-chats.js
@@ -24,7 +24,6 @@ import {
characters,
default_avatar,
addOneMessage,
- callPopup,
clearChat,
Generate,
select_rm_info,
@@ -75,7 +74,7 @@ import {
import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map, applyTagsOnGroupSelect } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
import { isExternalMediaAllowed } from './chats.js';
-import { POPUP_TYPE, callGenericPopup } from './popup.js';
+import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
export {
selected_group,
@@ -1300,14 +1299,20 @@ function isGroupMemberDisabled(avatarId) {
return Boolean(thisGroup && thisGroup.disabled_members.includes(avatarId));
}
-function onDeleteGroupClick() {
+async function onDeleteGroupClick() {
+ if (!openGroupId) {
+ toastr.warning('Currently no group selected.');
+ return;
+ }
if (is_group_generating) {
toastr.warning('Not so fast! Wait for the characters to stop typing before deleting the group.');
return;
}
- $('#dialogue_popup').data('group_id', openGroupId);
- callPopup('
Delete the group?
This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.
This will also delete all your chats with that group. If you want to delete a single conversation, select a "View past chats" option in the lower left menu.
');
+ if (confirm) {
+ deleteGroup(openGroupId);
+ }
}
async function onFavoriteGroupClick() {
@@ -1476,8 +1481,7 @@ async function uploadGroupAvatar(event) {
}
async function restoreGroupAvatar() {
- const confirm = await callPopup('
Are you sure you want to restore the group avatar?
Your custom image will be deleted, and a collage will be used instead.', 'confirm');
-
+ const confirm = await Popup.show.confirm('Are you sure you want to restore the group avatar?', 'Your custom image will be deleted, and a collage will be used instead.');
if (!confirm) {
return;
}
diff --git a/public/scripts/personas.js b/public/scripts/personas.js
index 0ea2d5486..3ed28b95e 100644
--- a/public/scripts/personas.js
+++ b/public/scripts/personas.js
@@ -590,7 +590,37 @@ function selectCurrentPersona() {
}
}
-async function lockUserNameToChat() {
+/**
+ * Checks if the persona is locked for the current chat.
+ * @returns {boolean} Whether the persona is locked
+ */
+function isPersonaLocked() {
+ return !!chat_metadata['persona'];
+}
+
+/**
+ * Locks or unlocks the persona for the current chat.
+ * @param {boolean} state Desired lock state
+ * @returns {Promise}
+ */
+export async function setPersonaLockState(state) {
+ return state ? await lockPersona() : await unlockPersona();
+}
+
+/**
+ * Toggle the persona lock state for the current chat.
+ * @returns {Promise}
+ */
+export async function togglePersonaLock() {
+ return isPersonaLocked()
+ ? await unlockPersona()
+ : await lockPersona();
+}
+
+/**
+ * Unlock the persona for the current chat.
+ */
+async function unlockPersona() {
if (chat_metadata['persona']) {
console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`);
delete chat_metadata['persona'];
@@ -599,9 +629,13 @@ async function lockUserNameToChat() {
toastr.info('User persona is now unlocked for this chat. Click the "Lock" again to revert.', 'Persona unlocked');
}
updateUserLockIcon();
- return;
}
+}
+/**
+ * Lock the persona for the current chat.
+ */
+async function lockPersona() {
if (!(user_avatar in power_user.personas)) {
console.log(`Creating a new persona ${user_avatar}`);
if (power_user.persona_show_notifications) {
@@ -625,6 +659,7 @@ async function lockUserNameToChat() {
updateUserLockIcon();
}
+
async function deleteUserAvatar(e) {
e?.stopPropagation();
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
@@ -973,7 +1008,7 @@ export function initPersonas() {
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
$(document).on('click', '.set_default_persona', setDefaultPersona);
$(document).on('click', '.delete_avatar', deleteUserAvatar);
- $('#lock_user_name').on('click', lockUserNameToChat);
+ $('#lock_user_name').on('click', togglePersonaLock);
$('#create_dummy_persona').on('click', createDummyPersona);
$('#persona_description').on('input', onPersonaDescriptionInput);
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
diff --git a/public/scripts/popup.js b/public/scripts/popup.js
index 92152b4db..ddd7dd5d0 100644
--- a/public/scripts/popup.js
+++ b/public/scripts/popup.js
@@ -74,6 +74,22 @@ const showPopupHelper = {
const value = await popup.show();
return value ? String(value) : null;
},
+
+ /**
+ * Asynchronously displays a confirmation popup with the given header and text, returning the clicked result button value.
+ *
+ * @param {string} header - The header text for the popup.
+ * @param {string} text - The main text for the popup.
+ * @param {PopupOptions} [popupOptions={}] - Options for the popup.
+ * @return {Promise} A Promise that resolves with the result of the user's interaction.
+ */
+ confirm: async (header, text, popupOptions = {}) => {
+ const content = PopupUtils.BuildTextWithHeader(header, text);
+ const popup = new Popup(content, POPUP_TYPE.CONFIRM, null, popupOptions);
+ const result = await popup.show();
+ if (typeof result === 'string' || typeof result === 'boolean') throw new Error(`Invalid popup result. CONFIRM popups only support numbers, or null. Result: ${result}`);
+ return result;
+ }
};
export class Popup {
diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js
index 5803cded1..654e479b2 100644
--- a/public/scripts/power-user.js
+++ b/public/scripts/power-user.js
@@ -2,7 +2,6 @@ import {
saveSettingsDebounced,
scrollChatToBottom,
characters,
- callPopup,
reloadMarkdownProcessor,
reloadCurrentChat,
getRequestHeaders,
@@ -48,6 +47,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashComma
import { AUTOCOMPLETE_WIDTH } from './autocomplete/AutoComplete.js';
import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
+import { POPUP_TYPE, callGenericPopup } from './popup.js';
export {
loadPowerUserSettings,
@@ -1432,7 +1432,7 @@ export function registerDebugFunction(functionId, name, description, func) {
async function showDebugMenu() {
const template = await renderTemplateAsync('debug', { functions: debug_functions });
- callPopup(template, 'text', '', { wide: true, large: true });
+ callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true });
}
switchUiMode();
@@ -2207,7 +2207,8 @@ async function deleteTheme() {
return;
}
- const confirm = await callPopup(`Are you sure you want to delete the theme "${themeName}"?`, 'confirm', '', { okButton: 'Yes' });
+ const template = $(await renderTemplateAsync('themeDelete', { themeName }));
+ const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
if (!confirm) {
return;
@@ -2269,7 +2270,8 @@ async function importTheme(file) {
}
if (typeof parsed.custom_css === 'string' && parsed.custom_css.includes('@import')) {
- const confirm = await callPopup('This theme contains @import lines in the Custom CSS. Press "Yes" to proceed.', 'confirm', '', { okButton: 'Yes' });
+ const template = $(await renderTemplateAsync('themeImportWarning'));
+ const confirm = await callGenericPopup(template, POPUP_TYPE.CONFIRM);
if (!confirm) {
throw new Error('Theme contains @import lines');
}
@@ -2294,11 +2296,13 @@ async function importTheme(file) {
*/
async function saveTheme(name = undefined, theme = undefined) {
if (typeof name !== 'string') {
- name = await callPopup('Enter a theme preset name:', 'input', power_user.theme);
+ const newName = await callGenericPopup('Enter a theme preset name:', POPUP_TYPE.INPUT, power_user.theme);
- if (!name) {
+ if (!newName) {
return;
}
+
+ name = String(newName);
}
if (typeof theme !== 'object') {
@@ -2396,7 +2400,7 @@ function getNewTheme(parsed) {
}
async function saveMovingUI() {
- const name = await callPopup('Enter a name for the MovingUI Preset:', 'input');
+ const name = await callGenericPopup('Enter a name for the MovingUI Preset:', POPUP_TYPE.INPUT);
if (!name) {
return;
diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js
index ebbbd42b1..4c87c469e 100644
--- a/public/scripts/slash-commands.js
+++ b/public/scripts/slash-commands.js
@@ -46,7 +46,7 @@ import { extension_settings, getContext, saveMetadataDebounced } from './extensi
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { findGroupMemberId, getGroupMembers, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
import { chat_completion_sources, oai_settings, setupChatCompletionPromptManager } from './openai.js';
-import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, user_avatar } from './personas.js';
+import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js';
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
@@ -118,9 +118,18 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'lock',
- callback: bindCallback,
+ callback: lockPersonaCallback,
aliases: ['bind'],
helpString: 'Locks/unlocks a persona (name and avatar) to the current chat',
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'state',
+ typeList: [ARGUMENT_TYPE.STRING],
+ isRequired: true,
+ defaultValue: 'toggle',
+ enumProvider: commonEnumProviders.boolean('onOffToggle'),
+ }),
+ ],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'bg',
@@ -1998,6 +2007,9 @@ async function addSwipeCallback(_, arg) {
lastMessage.swipe_info = [{}];
lastMessage.swipe_id = 0;
}
+ if (!Array.isArray(lastMessage.swipe_info)) {
+ lastMessage.swipe_info = lastMessage.swipes.map(() => ({}));
+ }
lastMessage.swipes.push(arg);
lastMessage.swipe_info.push({
@@ -2573,8 +2585,23 @@ function syncCallback() {
return '';
}
-function bindCallback() {
- $('#lock_user_name').trigger('click');
+async function lockPersonaCallback(_args, value) {
+ if (['toggle', 't', ''].includes(value.trim().toLowerCase())) {
+ await togglePersonaLock();
+ return '';
+ }
+
+ if (isTrueBoolean(value)) {
+ await setPersonaLockState(true);
+ return '';
+ }
+
+ if (isFalseBoolean(value)) {
+ await setPersonaLockState(false);
+ return '';
+
+ }
+
return '';
}
@@ -2700,9 +2727,26 @@ export async function sendMessageAs(args, text) {
bias: bias.trim().length ? bias : null,
gen_id: Date.now(),
isSmallSys: compact,
+ api: 'manual',
+ model: 'slash command',
},
};
+ message.swipe_id = 0;
+ message.swipes = [message.mes];
+ message.swipes_info = [{
+ send_date: message.send_date,
+ gen_started: null,
+ gen_finished: null,
+ extra: {
+ bias: message.extra.bias,
+ gen_id: message.extra.gen_id,
+ isSmallSys: compact,
+ api: 'manual',
+ model: 'slash command',
+ },
+ }];
+
const insertAt = Number(resolveVariable(args.at));
if (!isNaN(insertAt) && insertAt >= 0 && insertAt <= chat.length) {
@@ -2745,6 +2789,8 @@ export async function sendNarratorMessage(args, text) {
bias: bias.trim().length ? bias : null,
gen_id: Date.now(),
isSmallSys: compact,
+ api: 'manual',
+ model: 'slash command',
},
};
@@ -2795,6 +2841,8 @@ export async function promptQuietForLoudResponse(who, text) {
extra: {
type: system_message_types.COMMENT,
gen_id: Date.now(),
+ api: 'manual',
+ model: 'slash command',
},
};
@@ -2823,6 +2871,8 @@ async function sendCommentMessage(args, text) {
type: system_message_types.COMMENT,
gen_id: Date.now(),
isSmallSys: compact,
+ api: 'manual',
+ model: 'slash command',
},
};
diff --git a/public/scripts/templates/debug.html b/public/scripts/templates/debug.html
index 3e562cc45..943962854 100644
--- a/public/scripts/templates/debug.html
+++ b/public/scripts/templates/debug.html
@@ -1,25 +1,27 @@
-
Debug Menu
-
- Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.
-
-
- {{#each functions}}
- {{#with this}}
-
-
-
- {{this.name}}
-
-
- {{this.description}}
-
-
-
- Execute
+
+
Debug Menu
+
+ Functions in this category are for advanced users only. Don't click anything if you're not sure about the consequences.
+
+
+ {{#each functions}}
+ {{#with this}}
+
+
+
+ {{this.name}}
-
-
-
- {{/with}}
- {{/each}}
-
+
+ {{this.description}}
+
+
+
+ Execute
+
+
+
+
+ {{/with}}
+ {{/each}}
+
+
diff --git a/public/scripts/templates/itemizationChat.html b/public/scripts/templates/itemizationChat.html
index d5a8de41f..969a31757 100644
--- a/public/scripts/templates/itemizationChat.html
+++ b/public/scripts/templates/itemizationChat.html
@@ -127,3 +127,6 @@ API Used: {{this_main_api}}
+
+
+
diff --git a/public/scripts/templates/itemizationText.html b/public/scripts/templates/itemizationText.html
index efd9f84c1..b3855e027 100644
--- a/public/scripts/templates/itemizationText.html
+++ b/public/scripts/templates/itemizationText.html
@@ -107,3 +107,6 @@ API Used: {{this_main_api}}
+
+
+
diff --git a/public/scripts/templates/themeDelete.html b/public/scripts/templates/themeDelete.html
new file mode 100644
index 000000000..c9939d60a
--- /dev/null
+++ b/public/scripts/templates/themeDelete.html
@@ -0,0 +1,3 @@
+
+ Are you sure you want to delete the theme "{{themeName}}"?
+
diff --git a/public/scripts/templates/themeImportWarning.html b/public/scripts/templates/themeImportWarning.html
new file mode 100644
index 000000000..96cb34b34
--- /dev/null
+++ b/public/scripts/templates/themeImportWarning.html
@@ -0,0 +1,3 @@
+
+ This theme contains @import lines in the Custom CSS. Press "Yes" to proceed.
+
diff --git a/public/scripts/utils.js b/public/scripts/utils.js
index 67203adc6..1e140a6ae 100644
--- a/public/scripts/utils.js
+++ b/public/scripts/utils.js
@@ -1,8 +1,9 @@
import { getContext } from './extensions.js';
-import { callPopup, getRequestHeaders } from '../script.js';
+import { getRequestHeaders } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js';
import { debounce_timeout } from './constants.js';
+import { Popup } from './popup.js';
/**
* Pagination status string template.
@@ -1821,7 +1822,7 @@ export async function checkOverwriteExistingData(type, existingNames, name, { in
return true;
}
- const overwrite = interactive ? await callPopup(`
${type} ${actionName}
A ${type.toLowerCase()} with the same name already exists: ${existing}
Do you want to overwrite it?`, 'confirm') : false;
+ const overwrite = interactive && await Popup.show.confirm(`${type} ${actionName}`, `
A ${type.toLowerCase()} with the same name already exists: ${existing}
Do you want to overwrite it?`);
if (!overwrite) {
toastr.warning(`${type} ${actionName.toLowerCase()} cancelled. A ${type.toLowerCase()} with the same name already exists: ${existing}`, `${type} ${actionName}`, { escapeHtml: false });
return false;
diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js
index 1818d7563..23db6945e 100644
--- a/public/scripts/world-info.js
+++ b/public/scripts/world-info.js
@@ -16,6 +16,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
+import { Popup } from './popup.js';
export {
world_info,
@@ -1684,8 +1685,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
// Regardless of whether success is displayed or not. Make sure the delete button is available.
// Do not put this code behind.
$('#world_popup_delete').off('click').on('click', async () => {
- const confirmation = await callPopup(`
Delete the World/Lorebook: "${name}"?
This action is irreversible!`, 'confirm');
-
+ const confirmation = await Popup.show.confirm(`Delete the World/Lorebook: "${name}"?`, `This action is irreversible!`);
if (!confirmation) {
return;
}
@@ -1862,7 +1862,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
$('#world_duplicate').off('click').on('click', async () => {
const tempName = getFreeWorldName();
- const finalName = await callPopup('
Create a new World Info?
Enter a name for the new file:', 'input', tempName);
+ const finalName = await Popup.show.input('Create a new World Info?', 'Enter a name for the new file:', tempName);
if (finalName) {
await saveWorldInfo(finalName, data, true);
@@ -3190,7 +3190,7 @@ async function saveWorldInfo(name, data, immediately) {
async function renameWorldInfo(name, data) {
const oldName = name;
- const newName = await callPopup('
Rename World Info
Enter a new name:', 'input', oldName);
+ const newName = await Popup.show.input('Rename World Info', 'Enter a new name:', oldName);
if (oldName === newName || !newName) {
console.debug('World info rename cancelled');
@@ -4138,11 +4138,9 @@ export async function importEmbeddedWorldInfo(skipPopup = false) {
}
const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`;
- const confirmationText = (`
Are you sure you want to import "${bookName}"?
`) + (world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');
if (!skipPopup) {
- const confirmation = await callPopup(confirmationText, 'confirm');
-
+ const confirmation = await Popup.show.confirm(`Are you sure you want to import "${bookName}"?`, world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');
if (!confirmation) {
return;
}
@@ -4382,7 +4380,7 @@ jQuery(() => {
$('#world_create_button').on('click', async () => {
const tempName = getFreeWorldName();
- const finalName = await callPopup('
Create a new World Info?
Enter a name for the new file:', 'input', tempName);
+ const finalName = await Popup.show.input('Create a new World Info', 'Enter a name for the new file:', tempName);
if (finalName) {
await createNewWorldInfo(finalName, { interactive: true });
diff --git a/public/style.css b/public/style.css
index da3b29c9f..a93b5160b 100644
--- a/public/style.css
+++ b/public/style.css
@@ -82,7 +82,7 @@
/*base variable calculated in rems*/
--fontScale: 1;
--mainFontSize: calc(var(--fontScale) * 15px);
- --mainFontFamily: "Noto Sans", "Noto Color Emoji", sans-serif;
+ --mainFontFamily: "Noto Sans", sans-serif;
--monoFontFamily: 'Noto Sans Mono', 'Courier New', Consolas, monospace;
/* base variable for blur strength slider calculations */
@@ -256,10 +256,6 @@ input[type='checkbox']:focus-visible {
color: var(--SmartThemeEmColor);
}
-#rawPromptWrapper {
- white-space: pre-wrap;
-}
-
.tokenGraph {
border-radius: 10px;
border: 1px solid var(--SmartThemeBorderColor);
@@ -462,7 +458,7 @@ code {
kbd {
display: inline-block;
padding: 2px 4px;
- font-family: Consolas, monospace;
+ font-family: var(--monoFontFamily);
white-space: nowrap;
/* background-color: #eeeeee; */
background-color: rgba(255, 255, 255, 0.9);
@@ -4367,8 +4363,7 @@ a {
text-decoration: none;
}
-#export_format_popup,
-#rawPromptPopup {
+#export_format_popup {
display: none;
z-index: 9999;
}
@@ -4376,7 +4371,7 @@ a {
#rawPromptPopup {
inset: 0px auto auto 0px;
margin: 0px;
- transform: translate(909px, 47px);
+ transform: translate(500px, 0px);
display: block;
overflow-wrap: break-word;
white-space: normal;
@@ -4395,7 +4390,8 @@ a {
display: none;
}
-#rawPopupWrapper {
+#rawPromptWrapper {
+ white-space: pre-wrap;
word-wrap: break-word;
width: 100%;
text-align: start;