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 | 5002 | </div> |
| 5003 | 5003 | |
| 5004 | 5004 | <h4 data-i18n="Connections">Connections</h4> |
| 5005 | 5005 | <div id="persona_connections_buttons" class="flex-container marginBot10"> |
| 5006 | 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 | 5007 | <i class="icon fa-solid fa-crown fa-fw"></i> |
| 5008 | 5008 | <div data-i18n="Default">Default</div> |
| @@ -5016,6 +5016,7 @@ | ||
| 5016 | 5016 | <div data-i18n="Character">Character</div> |
| 5017 | 5017 | </div> |
| 5018 | 5018 | </div> |
| 5019 | + <div id="persona_connections_info_block"></div> | |
| 5019 | 5020 | <div id="persona_connections_list" class="text_muted m-b-1 avatars_inline avatars_multiline scroll-reset-container expander"> |
| 5020 | 5021 | </div> |
| 5021 | 5022 | </div> |
| @@ -237,6 +237,7 @@ import { | ||
| 237 | 237 | getConnectedPersonas, |
| 238 | 238 | askForPersonaSelection, |
| 239 | 239 | getCurrentConnectionObj, |
| 240 | + isPersonaPanelOpen, | |
| 240 | 241 | } from './scripts/personas.js'; |
| 241 | 242 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 242 | 243 | import { hideLoader, showLoader } from './scripts/loader.js'; |
| @@ -6811,7 +6812,7 @@ export function setUserName(value, { toastPersonaNameChange = true } = {}) { | ||
| 6811 | 6812 | name1 = default_user_name; |
| 6812 | 6813 | console.log(`User name changed to ${name1}`); |
| 6813 | 6814 | $('#your_name').text(name1); |
| 6814 | 6815 | if (toastPersonaNameChange && power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 6815 | 6816 | toastr.success(t`Your messages will now be sent as ${name1}`, t`Persona Changed`); |
| 6816 | 6817 | } |
| 6817 | 6818 | saveSettingsDebounced(); |
| @@ -21,7 +21,7 @@ import { | ||
| 21 | 21 | } from '../script.js'; |
| 22 | 22 | import { persona_description_positions, power_user } from './power-user.js'; |
| 23 | 23 | import { getTokenCountAsync } from './tokenizers.js'; |
| 24 | 24 | import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js'; |
| 25 | 25 | import { debounce_timeout } from './constants.js'; |
| 26 | 26 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 27 | 27 | import { groups, selected_group } from './group-chats.js'; |
| @@ -57,6 +57,15 @@ const DEFAULT_ROLE = 0; | ||
| 57 | 57 | export let user_avatar = ''; |
| 58 | 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 | 69 | function switchPersonaGridView() { |
| 61 | 70 | const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true'; |
| 62 | 71 | $('#user_avatar_block').toggleClass('gridView', state); |
| @@ -795,17 +804,14 @@ function selectCurrentPersona({ toastPersonaNameChange = true } = {}) { | ||
| 795 | 804 | } |
| 796 | 805 | |
| 797 | 806 | // As the last step, inform user if the persona is only temporarily chosen |
| 798 | 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 | 887 | console.log(`Unlocking persona ${user_avatar} from this chat`); |
| 882 | 888 | delete chat_metadata['persona']; |
| 883 | 889 | await saveMetadata(); |
| 884 | 890 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 885 | 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 | 901 | power_user.persona_descriptions[user_avatar].connections = connections.filter(c => !isPersonaConnectionLocked(c)); |
| 896 | 902 | saveSettingsDebounced(); |
| 897 | 903 | updatePersonaConnectionsAvatarList(); |
| 898 | 904 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 899 | 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 | 945 | console.log(`Locking persona ${user_avatar} to this chat`); |
| 940 | 946 | chat_metadata['persona'] = user_avatar; |
| 941 | 947 | saveMetadataDebounced(); |
| 942 | 948 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 943 | 949 | toastr.success(t`User persona ${name1} is locked to ${name2} in this chat`, t`Persona Locked`); |
| 944 | 950 | } |
| 945 | 951 | break; |
| @@ -971,7 +977,9 @@ async function lockPersona(type = 'chat') { | ||
| 971 | 977 | let additional = ''; |
| 972 | 978 | if (unlinkedCharacters.length) |
| 973 | 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 | 985 | break; |
| @@ -1175,7 +1183,7 @@ async function toggleDefaultPersonaClicked(e) { | ||
| 1175 | 1183 | * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown |
| 1176 | 1184 | * @returns {Promise<void>} |
| 1177 | 1185 | */ |
| 1178 | 1186 | async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) { |
| 1179 | 1187 | if (!avatarId) { |
| 1180 | 1188 | console.warn('No avatar id found'); |
| 1181 | 1189 | return; |
| @@ -1200,7 +1208,7 @@ async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) { | ||
| 1200 | 1208 | } |
| 1201 | 1209 | |
| 1202 | 1210 | console.log(`Removing default persona ${avatarId}`); |
| 1203 | 1211 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 1204 | 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 | 1214 | delete power_user.default_persona; |
| @@ -1217,7 +1225,7 @@ async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) { | ||
| 1217 | 1225 | } |
| 1218 | 1226 | |
| 1219 | 1227 | power_user.default_persona = avatarId; |
| 1220 | 1228 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 1221 | 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 | 1288 | $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character); |
| 1281 | 1289 | $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character); |
| 1282 | 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 | 1340 | async function loadPersonaForCurrentChat({ doRender = false } = {}) { |
| @@ -2221,3 +2221,34 @@ export function arraysEqual(a, b) { | ||
| 2221 | 2221 | } |
| 2222 | 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 | 2961 | color: var(--grey50); |
| 2962 | 2962 | } |
| 2963 | 2963 | |
| 2964 | +#persona_connections_buttons { | |
| 2965 | + margin-bottom: 5px; | |
| 2966 | +} | |
| 2967 | + | |
| 2964 | 2968 | input[type=search]::-webkit-search-cancel-button { |
| 2965 | 2969 | -webkit-appearance: none; |
| 2966 | 2970 | height: 1em; |
| @@ -3912,6 +3916,14 @@ input[type='checkbox'].del_checkbox { | ||
| 3912 | 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 | 3927 | #user_avatar_block .avatar_upload { |
| 3916 | 3928 | cursor: pointer; |
| 3917 | 3929 | width: 60px; |
| @@ -5922,3 +5934,36 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5922 | 5934 | .multiline { |
| 5923 | 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 | +} | |