diff --git a/public/css/extensions-panel.css b/public/css/extensions-panel.css index c475660a0..2daff5749 100644 --- a/public/css/extensions-panel.css +++ b/public/css/extensions-panel.css @@ -36,9 +36,6 @@ label[for="extensions_autoconnect"] { .extensions_info { text-align: left; - max-height: 100%; - overflow-y: auto; - padding-right: 1em; } .extensions_info h3 { diff --git a/public/css/popup.css b/public/css/popup.css index bdd530a66..045bfc2e5 100644 --- a/public/css/popup.css +++ b/public/css/popup.css @@ -115,12 +115,10 @@ dialog { background-color: var(--crimson-hover); } -.menu_button.popup-button-custom { - /* Custom buttons should not scale to smallest size, otherwise they will always break to multiline */ - width: unset; -} - .popup-controls .menu_button { + /* Popup buttons should not scale to smallest size, otherwise they will always break to multiline if multiple words */ + width: unset; + /* Fix weird animation issue with fonts on brightness filter */ backface-visibility: hidden; transform: translateZ(0); diff --git a/public/img/manual.svg b/public/img/manual.svg new file mode 100644 index 000000000..e313ab59c --- /dev/null +++ b/public/img/manual.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/index.html b/public/index.html index 15ae9a976..3b619ccaf 100644 --- a/public/index.html +++ b/public/index.html @@ -6400,9 +6400,6 @@ -
-
-
diff --git a/public/script.js b/public/script.js index 70eb2726e..6f2d55236 100644 --- a/public/script.js +++ b/public/script.js @@ -228,7 +228,7 @@ import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, de import { initPresetManager } from './scripts/preset-manager.js'; import { MacrosParser, evaluateMacros } from './scripts/macros.js'; import { currentUser, setUserControls } from './scripts/user.js'; -import { POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js'; +import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup, fixToastrForDialogs } from './scripts/popup.js'; import { renderTemplate, renderTemplateAsync } from './scripts/templates.js'; import { ScraperManager } from './scripts/scrapers.js'; import { SlashCommandParser } from './scripts/slash-commands/SlashCommandParser.js'; @@ -513,9 +513,6 @@ let optionsPopper = Popper.createPopper(document.getElementById('options_button' 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', -}); // Saved here for performance reasons const messageTemplate = $('#message_template .mes'); @@ -4890,14 +4887,44 @@ async function promptItemize(itemizedPrompts, requestedMesId) { const params = await itemizedParams(itemizedPrompts, thisPromptSet); - if (params.this_main_api == 'openai') { - const template = await renderTemplateAsync('itemizationChat', params); - callPopup(template, 'text'); + const template = params.this_main_api == 'openai' + ? await renderTemplateAsync('itemizationChat', params) + : await renderTemplateAsync('itemizationText', params); - } else { - const template = await renderTemplateAsync('itemizationText', params); - callPopup(template, 'text'); - } + const popup = new Popup(template, POPUP_TYPE.TEXT); + + popup.dlg.querySelector('#copyPromptToClipboard').addEventListener('click', function () { + let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; + let rawPromptValues = rawPrompt; + + if (Array.isArray(rawPrompt)) { + rawPromptValues = rawPrompt.map(x => x.content).join('\n'); + } + + navigator.clipboard.writeText(rawPromptValues); + toastr.info('Copied!'); + }); + + popup.dlg.querySelector('#showRawPrompt').addEventListener('click', function () { + //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); + console.log(PromptArrayItemForRawPromptDisplay); + console.log(itemizedPrompts); + console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); + + let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; + let rawPromptValues = rawPrompt; + + if (Array.isArray(rawPrompt)) { + rawPromptValues = rawPrompt.map(x => x.content).join('\n'); + } + + //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '
'); + const rawPromptWrapper = document.getElementById('rawPromptWrapper'); + rawPromptWrapper.innerText = rawPromptValues; + $('#rawPromptPopup').slideToggle(); + }); + + await popup.show(); } function setInContextMessages(lastmsg, type) { @@ -9184,13 +9211,6 @@ jQuery(async function () { if (popup_type == 'alternate_greeting' && menu_type !== 'create') { createOrEditCharacter(); } - if (popup_type === 'del_group') { - const groupId = $('#dialogue_popup').data('group_id'); - - if (groupId) { - deleteGroup(groupId); - } - } //Make a new chat for selected character if ( popup_type == 'new_chat' && @@ -9224,9 +9244,6 @@ jQuery(async function () { } } - rawPromptPopper.update(); - $('#rawPromptPopup').hide(); - if (dialogueResolve) { if (popup_type == 'input') { dialogueResolve($('#dialogue_popup_input').val()); @@ -9817,45 +9834,14 @@ jQuery(async function () { }); } - $(document).on('pointerup', '.mes_prompt', function () { + $(document).on('pointerup', '.mes_prompt', async function () { let mesIdForItemization = $(this).closest('.mes').attr('mesId'); console.log(`looking for mesID: ${mesIdForItemization}`); if (itemizedPrompts.length !== undefined && itemizedPrompts.length !== 0) { - promptItemize(itemizedPrompts, mesIdForItemization); + await promptItemize(itemizedPrompts, mesIdForItemization); } }); - $(document).on('pointerup', '#copyPromptToClipboard', function () { - let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; - let rawPromptValues = rawPrompt; - - if (Array.isArray(rawPrompt)) { - rawPromptValues = rawPrompt.map(x => x.content).join('\n'); - } - - navigator.clipboard.writeText(rawPromptValues); - toastr.info('Copied!', '', { timeOut: 2000 }); - }); - - $(document).on('pointerup', '#showRawPrompt', function () { - //console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); - console.log(PromptArrayItemForRawPromptDisplay); - console.log(itemizedPrompts); - console.log(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt); - - let rawPrompt = itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt; - let rawPromptValues = rawPrompt; - - if (Array.isArray(rawPrompt)) { - rawPromptValues = rawPrompt.map(x => x.content).join('\n'); - } - - //let DisplayStringifiedPrompt = JSON.stringify(itemizedPrompts[PromptArrayItemForRawPromptDisplay].rawPrompt).replace(/\n+/g, '
'); - $('#rawPromptWrapper').text(rawPromptValues); - rawPromptPopper.update(); - $('#rawPromptPopup').toggle(); - }); - //******************** //***Message Editor*** $(document).on('click', '.mes_edit', async function () { @@ -10060,7 +10046,7 @@ jQuery(async function () { }); $(document).on('click', '.mes_edit_copy', async function () { - const confirmation = await callPopup('Create a copy of this message?', 'confirm'); + const confirmation = await callGenericPopup('Create a copy of this message?', POPUP_TYPE.CONFIRM); if (!confirmation) { return; } @@ -10086,24 +10072,33 @@ jQuery(async function () { $(document).on('click', '.mes_edit_delete', async function (event, customData) { const fromSlashCommand = customData?.fromSlashCommand || false; - const swipeExists = (!Array.isArray(chat[this_edit_mes_id].swipes) || chat[this_edit_mes_id].swipes.length <= 1 || chat[this_edit_mes_id].is_user || parseInt(this_edit_mes_id) !== chat.length - 1); + const canDeleteSwipe = (Array.isArray(chat[this_edit_mes_id].swipes) && chat[this_edit_mes_id].swipes.length > 1 && !chat[this_edit_mes_id].is_user && parseInt(this_edit_mes_id) === chat.length - 1); + + let deleteOnlySwipe = false; if (power_user.confirm_message_delete && fromSlashCommand !== true) { - const confirmation = swipeExists ? await callPopup('Are you sure you want to delete this message?', 'confirm') - : await callPopup('

Delete this...

', 'confirm'); - if (!confirmation) { + const result = await callGenericPopup('Are you sure you want to delete this message?', POPUP_TYPE.CONFIRM, null, { + okButton: canDeleteSwipe ? 'Delete Swipe' : 'Delete Message', + cancelButton: 'Cancel', + customButtons: canDeleteSwipe ? ['Delete Message'] : null, + }); + if (!result) { return; } + deleteOnlySwipe = canDeleteSwipe && result === 1; // Default button, not the custom one } - const mes = $(this).closest('.mes'); - - if (!mes) { + const messageElement = $(this).closest('.mes'); + if (!messageElement) { return; } - if ($('#del_type').val() === 'swipe') { - const swipe_id = chat[this_edit_mes_id]['swipe_id']; - chat[this_edit_mes_id]['swipes'].splice(swipe_id, 1); + if (deleteOnlySwipe) { + const message = chat[this_edit_mes_id]; + const swipe_id = message.swipe_id; + message.swipes.splice(swipe_id, 1); + if (Array.isArray(message.swipe_info) && message.swipe_info.length) { + message.swipe_info.splice(swipe_id, 1); + } if (swipe_id > 0) { $('.swipe_left:last').click(); } else { @@ -10111,7 +10106,7 @@ jQuery(async function () { } } else { chat.splice(this_edit_mes_id, 1); - mes.remove(); + messageElement.remove(); } let startFromZero = Number(this_edit_mes_id) === 0; diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index b37cea4e5..b2d97ff97 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -16,7 +16,6 @@ import { eventSource, menu_type, substituteParams, - callPopup, sendTextareaMessage, } from '../script.js'; @@ -1004,20 +1003,21 @@ export function initRossMods() { if (skipConfirm) { doRegenerate(); } else { - const popupText = ` -
Are you sure you want to regenerate the latest message?
- `; - callPopup(popupText, 'confirm').then(result => { - if (!result) { - return; - } - const regenerateWithCtrlEnter = $('#regenerateWithCtrlEnter').prop('checked'); - SaveLocal(skipConfirmKey, regenerateWithCtrlEnter); - doRegenerate(); - }); + Popup.show.confirm('Regenerate Message', ` + Are you sure you want to regenerate the latest message? + `, { + onClose: (popup) => { + if (!popup.result) { + return; + } + const regenerateWithCtrlEnter = $('#regenerateWithCtrlEnter').prop('checked'); + SaveLocal(skipConfirmKey, regenerateWithCtrlEnter); + doRegenerate(); + }, + }) } return; } else { diff --git a/public/scripts/extensions.js b/public/scripts/extensions.js index 4b7ba2d37..3ecf7c893 100644 --- a/public/scripts/extensions.js +++ b/public/scripts/extensions.js @@ -504,7 +504,7 @@ function addExtensionScript(name, manifest) { * @param {boolean} isDisabled - Whether the extension is disabled or not. * @param {boolean} isExternal - Whether the extension is external or not. * @param {string} checkboxClass - The class for the checkbox HTML element. - * @return {string} - The HTML string that represents the extension. + * @return {Promise} - The HTML string that represents the extension. */ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal, checkboxClass) { const displayName = manifest.display_name; @@ -556,8 +556,10 @@ async function generateExtensionHtml(name, manifest, isActive, isDisabled, isExt } else if (!isDisabled) { // Neither active nor disabled const requirements = new Set(manifest.requires); modules.forEach(x => requirements.delete(x)); - const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); - extensionHtml += `

Missing modules: ${requirementsString}

`; + if (requirements.size > 0) { + const requirementsString = DOMPurify.sanitize([...requirements].join(', ')); + extensionHtml += `

Missing modules: ${requirementsString}

`; + } } return extensionHtml; @@ -642,7 +644,7 @@ async function showExtensionsDetails() { popup.complete(POPUP_RESULT.AFFIRMATIVE); }, }; - const popup = new Popup(`
${html}
`, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton] }); + const popup = new Popup(`
${html}
`, POPUP_TYPE.TEXT, '', { okButton: 'Close', wide: true, large: true, customButtons: [updateAllButton], allowVerticalScrolling: true }); popupPromise = popup.show(); } catch (error) { toastr.error('Error loading extensions. See browser console for details.'); diff --git a/public/scripts/extensions/token-counter/index.js b/public/scripts/extensions/token-counter/index.js index 7b2bd36a8..45955e4a3 100644 --- a/public/scripts/extensions/token-counter/index.js +++ b/public/scripts/extensions/token-counter/index.js @@ -1,10 +1,11 @@ -import { callPopup, main_api } from '../../../script.js'; +import { main_api } from '../../../script.js'; import { getContext } from '../../extensions.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js'; import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers } from '../../tokenizers.js'; import { resetScrollHeight, debounce } from '../../utils.js'; import { debounce_timeout } from '../../constants.js'; +import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; function rgb2hex(rgb) { rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); @@ -63,8 +64,7 @@ async function doTokenCounter() { }, debounce_timeout.relaxed); dialog.find('#token_counter_textarea').on('input', () => countDebounced()); - $('#dialogue_popup').addClass('wide_dialogue_popup'); - callPopup(dialog, 'text', '', { wide: true, large: true }); + callGenericPopup(dialog, POPUP_TYPE.TEXT, '', { wide: true, large: true, allowVerticalScrolling: true }); } /** diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js index c6baadd04..6a38459c0 100644 --- a/public/scripts/extensions/translate/index.js +++ b/public/scripts/extensions/translate/index.js @@ -1,7 +1,6 @@ export { translate }; import { - callPopup, eventSource, event_types, getRequestHeaders, @@ -11,6 +10,7 @@ import { updateMessageBlock, } from '../../../script.js'; import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js'; +import { POPUP_TYPE, callGenericPopup } from '../../popup.js'; import { findSecret, secret_state, writeSecret } from '../../secrets.js'; import { SlashCommand } from '../../slash-commands/SlashCommand.js'; import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; @@ -510,7 +510,7 @@ async function onTranslateChatClick() { async function onTranslationsClearClick() { const popupHtml = await renderExtensionTemplateAsync('translate', 'deleteConfirmation'); - const confirm = await callPopup(popupHtml, 'confirm'); + const confirm = await callGenericPopup(popupHtml, POPUP_TYPE.CONFIRM); if (!confirm) { return; @@ -598,7 +598,7 @@ jQuery(async () => { $(document).on('click', '.mes_translate', onMessageTranslateClick); $('#translate_key_button').on('click', async () => { const optionText = $('#translation_provider option:selected').text(); - const key = await callPopup(`

${optionText} API Key

`, 'input'); + const key = await callGenericPopup(`

${optionText} API Key

`, 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.

', 'del_group'); + const confirm = await Popup.show.confirm('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.

'); + 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}} - - + + {{/with}} + {{/each}} +
-
- {{this.name}} -
-
- {{this.description}} -
-
-
+
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;