Streamline persona toasts, infos and states - Added "info" block to persona description panel to show when a temporary persona is in use. Hide the long text behind a tooltip - Reduce toast spam even further, not showing toasts when persona panel is open - Restyle connection state buttons a bit - Designed common 'info-block' utility to show markdown-like info blocks, with CSS styling
| @@ -5002,7 +5002,7 @@ | |||
| 5002 | </div> | 5002 | </div> |
| 5003 | 5003 | ||
| 5004 | <h4 data-i18n="Connections">Connections</h4> | 5004 | <h4 data-i18n="Connections">Connections</h4> |
| 5005 | <div id="persona_connections_buttons" class="flex-container marginBot10"> | 5005 | <div id="persona_connections_buttons" class="flex-container"> |
| 5006 | <div id="lock_persona_default" class="menu_button menu_button_icon" title="Click to select this as default persona for the new chats. Click again to remove it." data-i18n="[title]Click to select this as default persona for the new chats. Click again to remove it."> | 5006 | <div id="lock_persona_default" class="menu_button menu_button_icon" title="Click to select this as default persona for the new chats. Click again to remove it." data-i18n="[title]Click to select this as default persona for the new chats. Click again to remove it."> |
| 5007 | <i class="icon fa-solid fa-crown fa-fw"></i> | 5007 | <i class="icon fa-solid fa-crown fa-fw"></i> |
| 5008 | <div data-i18n="Default">Default</div> | 5008 | <div data-i18n="Default">Default</div> |
| @@ -5016,6 +5016,7 @@ | |||
| 5016 | <div data-i18n="Character">Character</div> | 5016 | <div data-i18n="Character">Character</div> |
| 5017 | </div> | 5017 | </div> |
| 5018 | </div> | 5018 | </div> |
| 5019 | <div id="persona_connections_info_block"></div> | ||
| 5019 | <div id="persona_connections_list" class="text_muted m-b-1 avatars_inline avatars_multiline scroll-reset-container expander"> | 5020 | <div id="persona_connections_list" class="text_muted m-b-1 avatars_inline avatars_multiline scroll-reset-container expander"> |
| 5020 | </div> | 5021 | </div> |
| 5021 | </div> | 5022 | </div> |
| @@ -237,6 +237,7 @@ import { | |||
| 237 | getConnectedPersonas, | 237 | getConnectedPersonas, |
| 238 | askForPersonaSelection, | 238 | askForPersonaSelection, |
| 239 | getCurrentConnectionObj, | 239 | getCurrentConnectionObj, |
| 240 | isPersonaPanelOpen, | ||
| 240 | } from './scripts/personas.js'; | 241 | } from './scripts/personas.js'; |
| 241 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; | 242 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 242 | import { hideLoader, showLoader } from './scripts/loader.js'; | 243 | import { hideLoader, showLoader } from './scripts/loader.js'; |
| @@ -6811,7 +6812,7 @@ export function setUserName(value, { toastPersonaNameChange = true } = {}) { | |||
| 6811 | name1 = default_user_name; | 6812 | name1 = default_user_name; |
| 6812 | console.log(`User name changed to ${name1}`); | 6813 | console.log(`User name changed to ${name1}`); |
| 6813 | $('#your_name').text(name1); | 6814 | $('#your_name').text(name1); |
| 6814 | if (toastPersonaNameChange && power_user.persona_show_notifications) { | 6815 | if (toastPersonaNameChange && power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 6815 | toastr.success(t`Your messages will now be sent as ${name1}`, t`Persona Changed`); | 6816 | toastr.success(t`Your messages will now be sent as ${name1}`, t`Persona Changed`); |
| 6816 | } | 6817 | } |
| 6817 | saveSettingsDebounced(); | 6818 | saveSettingsDebounced(); |
| @@ -21,7 +21,7 @@ import { | |||
| 21 | } from '../script.js'; | 21 | } from '../script.js'; |
| 22 | import { persona_description_positions, power_user } from './power-user.js'; | 22 | import { persona_description_positions, power_user } from './power-user.js'; |
| 23 | import { getTokenCountAsync } from './tokenizers.js'; | 23 | import { getTokenCountAsync } from './tokenizers.js'; |
| 24 | import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, onlyUnique, parseJsonFile } from './utils.js'; | 24 | import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js'; |
| 25 | import { debounce_timeout } from './constants.js'; | 25 | import { debounce_timeout } from './constants.js'; |
| 26 | import { FILTER_TYPES, FilterHelper } from './filters.js'; | 26 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 27 | import { groups, selected_group } from './group-chats.js'; | 27 | import { groups, selected_group } from './group-chats.js'; |
| @@ -57,6 +57,15 @@ const DEFAULT_ROLE = 0; | |||
| 57 | export let user_avatar = ''; | 57 | export let user_avatar = ''; |
| 58 | export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); | 58 | export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); |
| 59 | 59 | ||
| 60 | |||
| 61 | /** | ||
| 62 | * Checks if the Persona Management panel is currently open | ||
| 63 | * @returns {boolean} | ||
| 64 | */ | ||
| 65 | export function isPersonaPanelOpen() { | ||
| 66 | return document.querySelector('#persona-management-button .drawer-content')?.classList.contains('openDrawer') ?? false; | ||
| 67 | } | ||
| 68 | |||
| 60 | function switchPersonaGridView() { | 69 | function switchPersonaGridView() { |
| 61 | const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true'; | 70 | const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true'; |
| 62 | $('#user_avatar_block').toggleClass('gridView', state); | 71 | $('#user_avatar_block').toggleClass('gridView', state); |
| @@ -795,17 +804,14 @@ function selectCurrentPersona({ toastPersonaNameChange = true } = {}) { | |||
| 795 | } | 804 | } |
| 796 | 805 | ||
| 797 | // As the last step, inform user if the persona is only temporarily chosen | 806 | // As the last step, inform user if the persona is only temporarily chosen |
| 798 | if (power_user.persona_show_notifications) { | 807 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 799 | const hasDifferentChatLock = !!chat_metadata['persona'] && chat_metadata['persona'] !== user_avatar; | 808 | const temporary = getPersonaTemporaryLockInfo(); |
| 800 | const hasDifferentDefaultLock = power_user.default_persona && power_user.default_persona !== user_avatar; | 809 | if (temporary.isTemporary) { |
| 801 | 810 | toastr.info(t`This persona is only temporarily chosen. Click for more info.`, t`Temporary Persona`, { | |
| 802 | if (hasDifferentChatLock || (!chat_metadata['persona'] && hasDifferentDefaultLock)) { | 811 | preventDuplicates: true, onclick: () => { |
| 803 | const message = t`A different persona is locked to this chat, or you have a different default persona set. The currently selected persona will only be temporary, and resets on reload. Consider locking this persona to the chat if you want to permanently use it.` | 812 | toastr.info(temporary.info.replaceAll('\n', '<br />'), t`Temporary Persona`, { escapeHtml: false }); |
| 804 | + '<br /><br />' | 813 | }, |
| 805 | + t`Current Persona: ${power_user.personas[user_avatar]}` | 814 | }); |
| 806 | + (hasDifferentChatLock ? '<br />' + t`Chat persona: ${power_user.personas[chat_metadata['persona']]}` : '') | ||
| 807 | + (hasDifferentDefaultLock ? '<br />' + t`Default persona: ${power_user.personas[power_user.default_persona]}` : ''); | ||
| 808 | toastr.info(message, t`Temporary Persona`, { escapeHtml: false, preventDuplicates: true }); | ||
| 809 | } | 815 | } |
| 810 | } | 816 | } |
| 811 | } | 817 | } |
| @@ -881,7 +887,7 @@ async function unlockPersona(type = 'chat') { | |||
| 881 | console.log(`Unlocking persona ${user_avatar} from this chat`); | 887 | console.log(`Unlocking persona ${user_avatar} from this chat`); |
| 882 | delete chat_metadata['persona']; | 888 | delete chat_metadata['persona']; |
| 883 | await saveMetadata(); | 889 | await saveMetadata(); |
| 884 | if (power_user.persona_show_notifications) { | 890 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 885 | toastr.info(t`Persona ${name1} is now unlocked from this chat.`, t`Persona Unlocked`); | 891 | toastr.info(t`Persona ${name1} is now unlocked from this chat.`, t`Persona Unlocked`); |
| 886 | } | 892 | } |
| 887 | } | 893 | } |
| @@ -895,7 +901,7 @@ async function unlockPersona(type = 'chat') { | |||
| 895 | power_user.persona_descriptions[user_avatar].connections = connections.filter(c => !isPersonaConnectionLocked(c)); | 901 | power_user.persona_descriptions[user_avatar].connections = connections.filter(c => !isPersonaConnectionLocked(c)); |
| 896 | saveSettingsDebounced(); | 902 | saveSettingsDebounced(); |
| 897 | updatePersonaConnectionsAvatarList(); | 903 | updatePersonaConnectionsAvatarList(); |
| 898 | if (power_user.persona_show_notifications) { | 904 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 899 | toastr.info(t`Persona ${name1} is now unlocked from character ${name2}.`, t`Persona Unlocked`); | 905 | toastr.info(t`Persona ${name1} is now unlocked from character ${name2}.`, t`Persona Unlocked`); |
| 900 | } | 906 | } |
| 901 | } | 907 | } |
| @@ -939,7 +945,7 @@ async function lockPersona(type = 'chat') { | |||
| 939 | console.log(`Locking persona ${user_avatar} to this chat`); | 945 | console.log(`Locking persona ${user_avatar} to this chat`); |
| 940 | chat_metadata['persona'] = user_avatar; | 946 | chat_metadata['persona'] = user_avatar; |
| 941 | saveMetadataDebounced(); | 947 | saveMetadataDebounced(); |
| 942 | if (power_user.persona_show_notifications) { | 948 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 943 | toastr.success(t`User persona ${name1} is locked to ${name2} in this chat`, t`Persona Locked`); | 949 | toastr.success(t`User persona ${name1} is locked to ${name2} in this chat`, t`Persona Locked`); |
| 944 | } | 950 | } |
| 945 | break; | 951 | break; |
| @@ -971,7 +977,9 @@ async function lockPersona(type = 'chat') { | |||
| 971 | let additional = ''; | 977 | let additional = ''; |
| 972 | if (unlinkedCharacters.length) | 978 | if (unlinkedCharacters.length) |
| 973 | additional += `<br /><br />${t`Unlinked existing persona${unlinkedCharacters.length > 1 ? 's' : ''}: ${unlinkedCharacters.join(', ')}`}`; | 979 | additional += `<br /><br />${t`Unlinked existing persona${unlinkedCharacters.length > 1 ? 's' : ''}: ${unlinkedCharacters.join(', ')}`}`; |
| 974 | toastr.success(t`User persona ${name1} is locked to character ${name2}${additional}`, t`Persona Locked`, { escapeHtml: false }); | 980 | if (additional || !isPersonaPanelOpen()) { |
| 981 | toastr.success(t`User persona ${name1} is locked to character ${name2}${additional}`, t`Persona Locked`, { escapeHtml: false }); | ||
| 982 | } | ||
| 975 | } | 983 | } |
| 976 | } | 984 | } |
| 977 | break; | 985 | break; |
| @@ -1175,7 +1183,7 @@ async function toggleDefaultPersonaClicked(e) { | |||
| 1175 | * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown | 1183 | * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown |
| 1176 | * @returns {Promise<void>} | 1184 | * @returns {Promise<void>} |
| 1177 | */ | 1185 | */ |
| 1178 | async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) { | 1186 | async function toggleDefaultPersona(avatarId, { quiet = false } = {}) { |
| 1179 | if (!avatarId) { | 1187 | if (!avatarId) { |
| 1180 | console.warn('No avatar id found'); | 1188 | console.warn('No avatar id found'); |
| 1181 | return; | 1189 | return; |
| @@ -1200,7 +1208,7 @@ async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) { | |||
| 1200 | } | 1208 | } |
| 1201 | 1209 | ||
| 1202 | console.log(`Removing default persona ${avatarId}`); | 1210 | console.log(`Removing default persona ${avatarId}`); |
| 1203 | if (power_user.persona_show_notifications) { | 1211 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 1204 | toastr.info(t`This persona will no longer be used by default when you open a new chat.`, t`Default Persona Removed`); | 1212 | toastr.info(t`This persona will no longer be used by default when you open a new chat.`, t`Default Persona Removed`); |
| 1205 | } | 1213 | } |
| 1206 | delete power_user.default_persona; | 1214 | delete power_user.default_persona; |
| @@ -1217,7 +1225,7 @@ async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) { | |||
| 1217 | } | 1225 | } |
| 1218 | 1226 | ||
| 1219 | power_user.default_persona = avatarId; | 1227 | power_user.default_persona = avatarId; |
| 1220 | if (power_user.persona_show_notifications) { | 1228 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 1221 | toastr.success(t`Set to ${power_user.personas[avatarId]}.This persona will be used by default when you open a new chat.`, t`Default Persona`); | 1229 | toastr.success(t`Set to ${power_user.personas[avatarId]}.This persona will be used by default when you open a new chat.`, t`Default Persona`); |
| 1222 | } | 1230 | } |
| 1223 | } | 1231 | } |
| @@ -1280,6 +1288,53 @@ function updatePersonaUIStates() { | |||
| 1280 | $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character); | 1288 | $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character); |
| 1281 | $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character); | 1289 | $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character); |
| 1282 | $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !personaStates.locked.character); | 1290 | $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !personaStates.locked.character); |
| 1291 | |||
| 1292 | // Persona panel info block | ||
| 1293 | const { isTemporary, info } = getPersonaTemporaryLockInfo(); | ||
| 1294 | if (isTemporary) { | ||
| 1295 | const messageContainer = document.createElement('div'); | ||
| 1296 | messageContainer.innerHTML = t`Temporary persona in use.`; | ||
| 1297 | |||
| 1298 | const infoIcon = document.createElement('i'); | ||
| 1299 | infoIcon.classList.add('fa-solid', 'fa-circle-info', 'opacity50p', 'marginLeft5'); | ||
| 1300 | infoIcon.title = info; | ||
| 1301 | messageContainer.appendChild(infoIcon); | ||
| 1302 | |||
| 1303 | // Set the info block content | ||
| 1304 | setInfoBlock('#persona_connections_info_block', messageContainer, 'hint'); | ||
| 1305 | } else { | ||
| 1306 | // Clear the info block if no condition applies | ||
| 1307 | clearInfoBlock('#persona_connections_info_block'); | ||
| 1308 | } | ||
| 1309 | } | ||
| 1310 | |||
| 1311 | /** | ||
| 1312 | * Checks if the currently selected persona is temporary due to either a different default persona | ||
| 1313 | * or a different persona being locked to the current chat. If so, it also returns a string that | ||
| 1314 | * can be used to describe this situation to the user. | ||
| 1315 | * | ||
| 1316 | * @returns {{isTemporary: boolean, hasDifferentChatLock: boolean, hasDifferentDefaultLock: boolean, info: string?}} An object containing 4 properties: | ||
| 1317 | * - isTemporary: A boolean indicating if the current persona is temporary | ||
| 1318 | * - hasDifferentChatLock: A boolean indicating if the current chat has a different persona locked to it | ||
| 1319 | * - hasDifferentDefaultLock: A boolean indicating if there is a different default persona set | ||
| 1320 | * - info: A string describing the situation, or an empty if not temporary | ||
| 1321 | */ | ||
| 1322 | function getPersonaTemporaryLockInfo() { | ||
| 1323 | const hasDifferentChatLock = !!chat_metadata['persona'] && chat_metadata['persona'] !== user_avatar; | ||
| 1324 | const hasDifferentDefaultLock = power_user.default_persona && power_user.default_persona !== user_avatar; | ||
| 1325 | const isTemporary = hasDifferentChatLock || (!chat_metadata['persona'] && hasDifferentDefaultLock); | ||
| 1326 | const info = isTemporary ? t`A different persona is locked to this chat, or you have a different default persona set. The currently selected persona will only be temporary, and resets on reload. Consider locking this persona to the chat if you want to permanently use it.` | ||
| 1327 | + '\n\n' | ||
| 1328 | + t`Current Persona: ${power_user.personas[user_avatar]}` | ||
| 1329 | + (hasDifferentChatLock ? '\n' + t`Chat persona: ${power_user.personas[chat_metadata['persona']]}` : '') | ||
| 1330 | + (hasDifferentDefaultLock ? '\n' + t`Default persona: ${power_user.personas[power_user.default_persona]}` : '') : ''; | ||
| 1331 | |||
| 1332 | return { | ||
| 1333 | isTemporary: isTemporary, | ||
| 1334 | hasDifferentChatLock: hasDifferentChatLock, | ||
| 1335 | hasDifferentDefaultLock: hasDifferentDefaultLock, | ||
| 1336 | info: info, | ||
| 1337 | }; | ||
| 1283 | } | 1338 | } |
| 1284 | 1339 | ||
| 1285 | async function loadPersonaForCurrentChat({ doRender = false } = {}) { | 1340 | async function loadPersonaForCurrentChat({ doRender = false } = {}) { |
| @@ -2221,3 +2221,34 @@ export function arraysEqual(a, b) { | |||
| 2221 | } | 2221 | } |
| 2222 | return true; | 2222 | return true; |
| 2223 | } | 2223 | } |
| 2224 | |||
| 2225 | /** | ||
| 2226 | * Updates the content and style of an information block | ||
| 2227 | * @param {string | HTMLElement} target - The CSS selector or the HTML element of the information block | ||
| 2228 | * @param {string | HTMLElement} content - The message to display inside the information block (supports HTML) or an HTML element | ||
| 2229 | * @param {'hint' | 'info' | 'warning' | 'error'} [type='info'] - The type of message, which determines the styling of the information block | ||
| 2230 | */ | ||
| 2231 | export function setInfoBlock(target, content, type = 'info') { | ||
| 2232 | const infoBlock = typeof target === 'string' ? document.querySelector(target) : target; | ||
| 2233 | if (infoBlock) { | ||
| 2234 | infoBlock.className = `info-block ${type}`; | ||
| 2235 | if (typeof content === 'string') { | ||
| 2236 | infoBlock.innerHTML = content; | ||
| 2237 | } else { | ||
| 2238 | infoBlock.innerHTML = ''; | ||
| 2239 | infoBlock.appendChild(content); | ||
| 2240 | } | ||
| 2241 | } | ||
| 2242 | } | ||
| 2243 | |||
| 2244 | /** | ||
| 2245 | * Clears the content and style of an information block. | ||
| 2246 | * @param {string | HTMLElement} target - The CSS selector or the HTML element of the information block | ||
| 2247 | */ | ||
| 2248 | export function clearInfoBlock(target) { | ||
| 2249 | const infoBlock = typeof target === 'string' ? document.querySelector(target) : target; | ||
| 2250 | if (infoBlock && infoBlock.classList.contains('info-block')) { | ||
| 2251 | infoBlock.className = ''; | ||
| 2252 | infoBlock.innerHTML = ''; | ||
| 2253 | } | ||
| 2254 | } | ||
| @@ -2961,6 +2961,10 @@ select option:not(:checked) { | |||
| 2961 | color: var(--grey50); | 2961 | color: var(--grey50); |
| 2962 | } | 2962 | } |
| 2963 | 2963 | ||
| 2964 | #persona_connections_buttons { | ||
| 2965 | margin-bottom: 5px; | ||
| 2966 | } | ||
| 2967 | |||
| 2964 | input[type=search]::-webkit-search-cancel-button { | 2968 | input[type=search]::-webkit-search-cancel-button { |
| 2965 | -webkit-appearance: none; | 2969 | -webkit-appearance: none; |
| 2966 | height: 1em; | 2970 | height: 1em; |
| @@ -3912,6 +3916,14 @@ input[type='checkbox'].del_checkbox { | |||
| 3912 | color: var(--active); | 3916 | color: var(--active); |
| 3913 | } | 3917 | } |
| 3914 | 3918 | ||
| 3919 | #lock_user_name.locked { | ||
| 3920 | border-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 50%, var(--SmartThemeBorderColor));; | ||
| 3921 | } | ||
| 3922 | |||
| 3923 | #lock_persona_to_char.locked { | ||
| 3924 | border-color: color-mix(in srgb, var(--active) 50%, var(--SmartThemeBorderColor)); | ||
| 3925 | } | ||
| 3926 | |||
| 3915 | #user_avatar_block .avatar_upload { | 3927 | #user_avatar_block .avatar_upload { |
| 3916 | cursor: pointer; | 3928 | cursor: pointer; |
| 3917 | width: 60px; | 3929 | width: 60px; |
| @@ -5922,3 +5934,36 @@ body:not(.movingUI) .drawer-content.maximized { | |||
| 5922 | .multiline { | 5934 | .multiline { |
| 5923 | white-space: pre-wrap; | 5935 | white-space: pre-wrap; |
| 5924 | } | 5936 | } |
| 5937 | |||
| 5938 | .info-block { | ||
| 5939 | padding: 10px 1em; | ||
| 5940 | margin: 1em 0; | ||
| 5941 | border-radius: 10px; | ||
| 5942 | border-left: 5px solid; | ||
| 5943 | background-color: var(--SmartThemeBlurTintColor); | ||
| 5944 | color: var(--SmartThemeBodyColor); | ||
| 5945 | backdrop-filter: blur(calc(var(--SmartThemeBlurStrength))); | ||
| 5946 | -webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength))); | ||
| 5947 | } | ||
| 5948 | |||
| 5949 | .info-block.hint { | ||
| 5950 | border-color: var(--info-color, #7fb3e8); | ||
| 5951 | background-color: rgba(163, 201, 241, 0.2); | ||
| 5952 | } | ||
| 5953 | |||
| 5954 | .info-block.info { | ||
| 5955 | border-color: var(--default-color, #e8e07f); | ||
| 5956 | background-color: rgba(255, 255, 224, 0.2); | ||
| 5957 | } | ||
| 5958 | |||
| 5959 | .info-block.warning { | ||
| 5960 | border-color: var(--warning-color, #e8a97f); | ||
| 5961 | background-color: rgba(241, 198, 163, 0.2); | ||
| 5962 | /* Reset the font weight from that main warning class */ | ||
| 5963 | font-weight: unset; | ||
| 5964 | } | ||
| 5965 | |||
| 5966 | .info-block.error { | ||
| 5967 | border-color: var(--error-color, #e87f7f); | ||
| 5968 | background-color: rgba(241, 163, 163, 0.2); | ||
| 5969 | } | ||