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

f9324c74cd48703b523ba18c247b4bce0b0eb764

Wolfsblvt <wolfsblvt@gmail.com>

5 files changed, +154 -21Ignore whitespace
public/index.html+2 -1
@@ -5002,7 +5002,7 @@
5002 </div>5002 </div>
50035003
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>
public/script.js+2 -1
@@ -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';
241import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';242import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
242import { hideLoader, showLoader } from './scripts/loader.js';243import { 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();
public/scripts/personas.js+74 -19
@@ -21,7 +21,7 @@ import {
21} from '../script.js';21} from '../script.js';
22import { persona_description_positions, power_user } from './power-user.js';22import { persona_description_positions, power_user } from './power-user.js';
23import { getTokenCountAsync } from './tokenizers.js';23import { getTokenCountAsync } from './tokenizers.js';
24import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, onlyUnique, parseJsonFile } from './utils.js';24import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js';
25import { debounce_timeout } from './constants.js';25import { debounce_timeout } from './constants.js';
26import { FILTER_TYPES, FilterHelper } from './filters.js';26import { FILTER_TYPES, FilterHelper } from './filters.js';
27import { groups, selected_group } from './group-chats.js';27import { groups, selected_group } from './group-chats.js';
@@ -57,6 +57,15 @@ const DEFAULT_ROLE = 0;
57export let user_avatar = '';57export let user_avatar = '';
58export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));58export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
5959
60
61/**
62 * Checks if the Persona Management panel is currently open
63 * @returns {boolean}
64 */
65export function isPersonaPanelOpen() {
66 return document.querySelector('#persona-management-button .drawer-content')?.classList.contains('openDrawer') ?? false;
67}
68
60function switchPersonaGridView() {69function 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 }
796805
797 // As the last step, inform user if the persona is only temporarily chosen806 // 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) {
801810 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 shown1183 * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown
1176 * @returns {Promise<void>}1184 * @returns {Promise<void>}
1177 */1185 */
1178async function toggleDefaultPersona(avatarId, { quiet: quiet = false } = {}) {1186async 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 }
12011209
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 }
12181226
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 */
1322function 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}
12841339
1285async function loadPersonaForCurrentChat({ doRender = false } = {}) {1340async function loadPersonaForCurrentChat({ doRender = false } = {}) {
public/scripts/utils.js+31 -0
@@ -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 */
2231export 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 */
2248export 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}
public/style.css+45 -0
@@ -2961,6 +2961,10 @@ select option:not(:checked) {
2961 color: var(--grey50);2961 color: var(--grey50);
2962}2962}
29632963
2964#persona_connections_buttons {
2965 margin-bottom: 5px;
2966}
2967
2964input[type=search]::-webkit-search-cancel-button {2968input[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}
39143918
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}