diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 1dce66958..9277fe9fa 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -148,7 +148,8 @@ body.big-avatars .avatar_collage { aspect-ratio: 2 / 3; } -body.big-avatars .ch_description { +body.big-avatars .ch_description, +.avatar-container .ch_description { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; diff --git a/public/index.html b/public/index.html index 727c002a0..14526f41f 100644 --- a/public/index.html +++ b/public/index.html @@ -2117,7 +2117,7 @@
- +
@@ -3652,7 +3652,7 @@
-
+

Persona Management

@@ -3673,14 +3673,27 @@ Restore
-
-
+
+
+
+ + +
+
+
+
+
+
+
+ + +
+

Name

@@ -3716,18 +3729,6 @@
-
-

- Your Persona -

-
-
+
-
-
- - -
-
@@ -3958,7 +3959,8 @@
@@ -4245,9 +4247,8 @@
-
+
-
- - + + +
@@ -5339,25 +5347,29 @@
-
+
User Avatar
-
- - -
-
- - +
+
+ +
+ + + + +
+
+
diff --git a/public/script.js b/public/script.js index 470b9e3d9..c205d5d10 100644 --- a/public/script.js +++ b/public/script.js @@ -444,6 +444,7 @@ let generation_started = new Date(); let characters = []; let this_chid; let saveCharactersPage = 0; +let savePersonasPage = 0; const default_avatar = 'img/ai4.png'; export const system_avatar = 'img/five.png'; export const comment_avatar = 'img/quill.png'; @@ -790,6 +791,7 @@ var PromptArrayItemForRawPromptDisplay; export let active_character = ''; export let active_group = ''; export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100)); +export const personasFilter = new FilterHelper(debounce(getUserAvatars, 100)); export function getRequestHeaders() { return { @@ -5395,47 +5397,85 @@ function changeMainAPI() { //////////////////////////////////////////////////// -export async function getUserAvatars() { +/** + * Gets a list of user avatars. + * @param {boolean} doRender Whether to render the list + * @returns {Promise} List of avatar file names + */ +export async function getUserAvatars(doRender = true) { const response = await fetch('/getuseravatars', { method: 'POST', headers: getRequestHeaders(), - body: JSON.stringify({ - '': '', - }), }); - if (response.ok === true) { - const getData = await response.json(); - $('#user_avatar_block').html(''); //RossAscends: necessary to avoid doubling avatars each refresh. - $('#user_avatar_block').append('
+
'); + if (response.ok) { + const allEntities = await response.json(); - for (var i = 0; i < getData.length; i++) { - appendUserAvatar(getData[i]); + if (!doRender) { + return allEntities; } - return getData; + const entities = personasFilter.applyFilters(allEntities); + + const storageKey = 'Personas_PerPage'; + const listId = '#user_avatar_block'; + + $('#persona_pagination_container').pagination({ + dataSource: entities, + pageSize: Number(localStorage.getItem(storageKey)) || 5, + sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000], + pageRange: 1, + pageNumber: savePersonasPage || 1, + position: 'top', + showPageNumbers: false, + showSizeChanger: true, + prevText: '<', + nextText: '>', + formatNavigator: PAGINATION_TEMPLATE, + showNavigator: true, + callback: function (data) { + $(listId).empty(); + for (const item of data) { + $(listId).append(getUserAvatarBlock(item)); + } + highlightSelectedAvatar(); + }, + afterSizeSelectorChange: function (e) { + localStorage.setItem(storageKey, e.target.value); + }, + afterPaging: function (e) { + savePersonasPage = e; + }, + afterRender: function () { + $(listId).scrollTop(0); + }, + }); + + return allEntities; } } function highlightSelectedAvatar() { - $('#user_avatar_block').find('.avatar').removeClass('selected'); - $('#user_avatar_block') - .find(`.avatar[imgfile='${user_avatar}']`) - .addClass('selected'); + $('#user_avatar_block .avatar-container').removeClass('selected'); + $(`#user_avatar_block .avatar-container[imgfile='${user_avatar}']`).addClass('selected'); } -function appendUserAvatar(name) { +/** + * Gets a rendered avatar block. + * @param {string} name Avatar file name + * @returns {JQuery} Avatar block + */ +function getUserAvatarBlock(name) { const template = $('#user_avatar_template .avatar-container').clone(); const personaName = power_user.personas[name]; - if (personaName) { - template.attr('title', personaName); - } else { - template.attr('title', '[Unnamed Persona]'); - } + const personaDescription = power_user.persona_descriptions[name]?.description; + template.find('.ch_name').text(personaName || '[Unnamed Persona]'); + template.find('.ch_description').text(personaDescription || '[No description]').toggleClass('text_muted', !personaDescription); + template.attr('imgfile', name); template.find('.avatar').attr('imgfile', name); template.toggleClass('default_persona', name === power_user.default_persona); template.find('img').attr('src', getUserAvatar(name)); $('#user_avatar_block').append(template); - highlightSelectedAvatar(); + return template; } function reloadUserAvatar(force = false) { @@ -5463,8 +5503,12 @@ export function setUserName(value) { saveSettingsDebounced(); } -function setUserAvatar() { - user_avatar = $(this).attr('imgfile'); +/** + * Sets a user avatar file + * @param {string} imgfile Link to an image file + */ +export function setUserAvatar(imgfile) { + user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('imgfile'); reloadUserAvatar(); highlightSelectedAvatar(); selectCurrentPersona(); @@ -7966,6 +8010,11 @@ jQuery(async function () { entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchValue); }); + $('#persona_search_bar').on('input', function () { + const searchValue = String($(this).val()).toLowerCase(); + personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchValue); + }); + $('#mes_continue').on('click', function () { $('#option_continue').trigger('click'); }); @@ -8067,12 +8116,13 @@ jQuery(async function () { } }); - $(document).on('click', '#user_avatar_block .avatar', setUserAvatar); + $(document).on('click', '#user_avatar_block .avatar-container', setUserAvatar); $(document).on('click', '#user_avatar_block .avatar_upload', function () { $('#avatar_upload_overwrite').val(''); $('#avatar_upload_file').trigger('click'); }); - $(document).on('click', '#user_avatar_block .set_persona_image', function () { + $(document).on('click', '#user_avatar_block .set_persona_image', function (e) { + e.stopPropagation(); const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); if (!avatarId) { diff --git a/public/scripts/filters.js b/public/scripts/filters.js index 9f461a401..4a0efc3fc 100644 --- a/public/scripts/filters.js +++ b/public/scripts/filters.js @@ -1,4 +1,4 @@ -import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js'; +import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js'; import { tag_map } from './tags.js'; /** @@ -11,6 +11,7 @@ export const FILTER_TYPES = { FAV: 'fav', GROUP: 'group', WORLD_INFO_SEARCH: 'world_info_search', + PERSONA_SEARCH: 'persona_search', }; /** @@ -39,6 +40,7 @@ export class FilterHelper { [FILTER_TYPES.FAV]: this.favFilter.bind(this), [FILTER_TYPES.TAG]: this.tagFilter.bind(this), [FILTER_TYPES.WORLD_INFO_SEARCH]: this.wiSearchFilter.bind(this), + [FILTER_TYPES.PERSONA_SEARCH]: this.personaSearchFilter.bind(this), }; /** @@ -51,6 +53,7 @@ export class FilterHelper { [FILTER_TYPES.FAV]: false, [FILTER_TYPES.TAG]: { excluded: [], selected: [] }, [FILTER_TYPES.WORLD_INFO_SEARCH]: '', + [FILTER_TYPES.PERSONA_SEARCH]: '', }; /** @@ -69,6 +72,22 @@ export class FilterHelper { return data.filter(entity => fuzzySearchResults.includes(entity.uid)); } + /** + * Applies a search filter to Persona data. + * @param {string[]} data The data to filter. + * @returns {string[]} The filtered data. + */ + personaSearchFilter(data) { + const term = this.filterData[FILTER_TYPES.PERSONA_SEARCH]; + + if (!term) { + return data; + } + + const fuzzySearchResults = fuzzySearchPersonas(data, term); + return data.filter(entity => fuzzySearchResults.includes(entity)); + } + /** * Checks if the given entity is tagged with the given tag ID. * @param {object} entity Searchable entity diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index d43322e0b..81a368087 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -108,6 +108,7 @@ export const group_activation_strategy = { export const group_generation_mode = { SWAP: 0, APPEND: 1, + APPEND_DISABLED: 2, }; const DEFAULT_AUTO_MODE_DELAY = 5; @@ -325,7 +326,7 @@ export function getGroupDepthPrompts(groupId, characterId) { } /** - * Combines group members info a single string. Only for groups with generation mode set to APPEND. + * Combines group members cards into a single string. Only for groups with generation mode set to APPEND or APPEND_DISABLED. * @param {string} groupId Group ID * @param {number} characterId Current Character ID * @returns {{description: string, personality: string, scenario: string, mesExamples: string}} Group character cards combined @@ -334,7 +335,7 @@ export function getGroupCharacterCards(groupId, characterId) { console.debug('getGroupCharacterCards entered for group: ', groupId); const group = groups.find(x => x.id === groupId); - if (!group || group?.generation_mode !== group_generation_mode.APPEND || !Array.isArray(group.members) || !group.members.length) { + if (!group || !group?.generation_mode || !Array.isArray(group.members) || !group.members.length) { return null; } @@ -354,7 +355,7 @@ export function getGroupCharacterCards(groupId, characterId) { continue; } - if (group.disabled_members.includes(member) && characterId !== index) { + if (group.disabled_members.includes(member) && characterId !== index && group.generation_mode !== group_generation_mode.APPEND_DISABLED) { console.debug(`Skipping disabled group member: ${member}`); continue; } diff --git a/public/scripts/personas.js b/public/scripts/personas.js index 310928c8c..2c4dda296 100644 --- a/public/scripts/personas.js +++ b/public/scripts/personas.js @@ -11,6 +11,7 @@ import { name1, saveMetadata, saveSettingsDebounced, + setUserAvatar, setUserName, this_chid, user_avatar, @@ -187,7 +188,7 @@ export function autoSelectPersona(name) { for (const [key, value] of Object.entries(power_user.personas)) { if (value === name) { console.log(`Auto-selecting persona ${key} for name ${name}`); - $(`.avatar[imgfile="${key}"]`).trigger('click'); + setUserAvatar(key); return; } } @@ -209,7 +210,8 @@ export async function updatePersonaNameIfExists(avatarId, newName) { } } -async function bindUserNameToPersona() { +async function bindUserNameToPersona(e) { + e?.stopPropagation(); const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); if (!avatarId) { @@ -331,7 +333,8 @@ async function lockUserNameToChat() { updateUserLockIcon(); } -async function deleteUserAvatar() { +async function deleteUserAvatar(e) { + e?.stopPropagation(); const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); if (!avatarId) { @@ -400,6 +403,9 @@ function onPersonaDescriptionInput() { object.description = power_user.persona_description; } + $(`.avatar-container[imgfile="${user_avatar}"] .ch_description`) + .text(power_user.persona_description || '[No description]') + .toggleClass('text_muted', !power_user.persona_description); saveSettingsDebounced(); } @@ -425,7 +431,8 @@ function onPersonaDescriptionPositionInput() { saveSettingsDebounced(); } -async function setDefaultPersona() { +async function setDefaultPersona(e) { + e?.stopPropagation(); const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); if (!avatarId) { @@ -481,7 +488,7 @@ function updateUserLockIcon() { $('#lock_user_name').toggleClass('fa-lock', hasLock); } -function setChatLockedPersona() { +async function setChatLockedPersona() { // Define a persona for this chat let chatPersona = ''; @@ -502,10 +509,10 @@ function setChatLockedPersona() { } // Find the avatar file - const personaAvatar = $(`.avatar[imgfile="${chatPersona}"]`).trigger('click'); + const userAvatars = await getUserAvatars(false); // Avatar missing (persona deleted) - if (chat_metadata['persona'] && personaAvatar.length == 0) { + if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) { console.warn('Persona avatar not found, unlocking persona'); delete chat_metadata['persona']; updateUserLockIcon(); @@ -513,7 +520,7 @@ function setChatLockedPersona() { } // Default persona missing - if (power_user.default_persona && personaAvatar.length == 0) { + if (power_user.default_persona && !userAvatars.includes(power_user.default_persona)) { console.warn('Default persona avatar not found, clearing default persona'); power_user.default_persona = null; saveSettingsDebounced(); @@ -521,7 +528,7 @@ function setChatLockedPersona() { } // Persona avatar found, select it - personaAvatar.trigger('click'); + setUserAvatar(chatPersona); updateUserLockIcon(); } @@ -560,7 +567,7 @@ async function onPersonasRestoreInput(e) { return; } - const avatarsList = await getUserAvatars(); + const avatarsList = await getUserAvatars(false); const warnings = []; // Merge personas with existing ones diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 563e3e72d..c608c7db7 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -1831,6 +1831,23 @@ export function fuzzySearchWorldInfo(data, searchValue) { return results.map(x => x.item?.uid); } +export function fuzzySearchPersonas(data, searchValue) { + data = data.map(x => ({ key: x, description: power_user.persona_descriptions[x]?.description ?? '', name: power_user.personas[x] ?? '' })); + const fuse = new Fuse(data, { + keys: [ + { name: 'name', weight: 4 }, + { name: 'description', weight: 1 }, + ], + includeScore: true, + ignoreLocation: true, + threshold: 0.2, + }); + + const results = fuse.search(searchValue); + console.debug('Personas fuzzy search results for ' + searchValue, results); + return results.map(x => x.item?.key); +} + export function fuzzySearchTags(searchValue) { const fuse = new Fuse(tags, { keys: [ diff --git a/public/style.css b/public/style.css index 533c69245..9c9339d28 100644 --- a/public/style.css +++ b/public/style.css @@ -1596,7 +1596,8 @@ input[type=search]:focus::-webkit-search-cancel-button { } .bogus_folder_select:hover, -.character_select:hover { +.character_select:hover, +.avatar-container:hover { background-color: var(--white30a); } @@ -1821,29 +1822,28 @@ input[type=search]:focus::-webkit-search-cancel-button { position: relative; display: flex; flex-direction: row; - align-items: center; + align-items: flex-start; + gap: 5px; + padding: 5px; + border-radius: 10px; + cursor: pointer; + margin-bottom: 1px; } grammarly-extension { z-index: 35; } -.avatar-container:hover .avatar-buttons { +.avatar-container .avatar-buttons { display: flex; + flex-direction: row; + gap: 5px; + opacity: 0.3; + transition: opacity 0.25s ease-in-out; } -.avatar-buttons .menu_button { - pointer-events: all; -} - -.avatar-buttons-bottom { - bottom: 0; - left: 0; -} - -.avatar-buttons-top { - top: 0; - left: 0; +.avatar-container .avatar-buttons:hover { + opacity: 1; } /* Ross should be able to handle this later */ @@ -1852,14 +1852,6 @@ grammarly-extension { width: fit-content; }*/ -.avatar-buttons { - pointer-events: none; - display: none; - position: absolute; - width: 100%; - justify-content: space-between; -} - .avatar_div .avatar { /* margin-left: 4px; margin-right: 10px; @@ -2279,29 +2271,33 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button #user_avatar_block { display: flex; - grid-gap: 10px; flex-wrap: wrap; justify-content: space-evenly; } -#user_avatar_block .avatar { +.avatar-container .avatar { cursor: pointer; width: 64px; height: 64px; - outline: 2px solid rgba(255, 255, 255, 0.7); border-radius: 50%; -} - -#user_avatar_block .avatar:not(.selected) { + align-self: center; outline: 2px solid transparent; } -#user_avatar_block .default_persona .avatar { - border: 2px solid var(--golden); - box-sizing: content-box; +.avatar-container { + outline: 2px solid transparent; + border: 2px solid transparent; } -#user_avatar_block .default_persona .set_default_persona { +.avatar-container.selected { + border: 2px solid rgba(255, 255, 255, 0.7); +} + +.avatar-container.default_persona .avatar { + outline: 2px solid var(--golden); +} + +.avatar-container.default_persona .set_default_persona { color: var(--golden); }