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 b276b47d8..3ffe610bc 100644 --- a/public/index.html +++ b/public/index.html @@ -3659,7 +3659,20 @@ -
+
+
+
+ +
+
+
+
+
+
+
+ + +
+

Name

@@ -3695,18 +3708,6 @@
-
-

- Your Persona -

-
-
+
-
-
- - -
-
@@ -5319,25 +5320,29 @@
-
+
User Avatar
-
- - -
-
- - +
+
+ Kagari +
+ + + + +
+
+
Kagari From Rewrite Visual Novel. This is meant to be played for the user be named as Kotarou or Kotarou Tennouji. World Info/Lorebook is included. Enabling 'Prefer Char. Prompt is recommeded
diff --git a/public/script.js b/public/script.js index 470b9e3d9..6c07c807b 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)) || per_page_default, + sizeChangerOptions: [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').find(`.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,7 +8116,7 @@ jQuery(async function () { } }); - $(document).on('click', '#user_avatar_block .avatar', setUserAvatar); + $(document).on('click', '#user_avatar_block .avatar, #user_avatar_block .avatar-container', setUserAvatar); $(document).on('click', '#user_avatar_block .avatar_upload', function () { $('#avatar_upload_overwrite').val(''); $('#avatar_upload_file').trigger('click'); 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/personas.js b/public/scripts/personas.js index 310928c8c..8a0937202 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; } } @@ -400,6 +401,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(); } @@ -481,7 +485,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 +506,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 +517,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 +525,7 @@ function setChatLockedPersona() { } // Persona avatar found, select it - personaAvatar.trigger('click'); + setUserAvatar(chatPersona); updateUserLockIcon(); } @@ -560,7 +564,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 41938ea23..6f90b4909 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -1828,6 +1828,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..c209e3887 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,45 +1822,44 @@ 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 .ch_name { + flex: 1; +} + +.avatar-container .avatar-buttons { display: flex; + flex-direction: row; + gap: 5px; + opacity: 0.3; + transition: opacity 0.25s ease-in-out; +} + +.avatar-container .avatar-buttons:hover { + opacity: 1; } .avatar-buttons .menu_button { pointer-events: all; } -.avatar-buttons-bottom { - bottom: 0; - left: 0; -} - -.avatar-buttons-top { - top: 0; - left: 0; -} - /* Ross should be able to handle this later */ /*.big-avatars .avatar-buttons{ justify-content: center; 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,7 +2279,6 @@ 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; } @@ -2288,20 +2287,28 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button cursor: pointer; width: 64px; height: 64px; - outline: 2px solid rgba(255, 255, 255, 0.7); border-radius: 50%; + align-self: center; } -#user_avatar_block .avatar:not(.selected) { +#user_avatar_block .ch_description { + white-space: unset; +} + +.avatar-container { outline: 2px solid transparent; + border: 2px solid transparent; } -#user_avatar_block .default_persona .avatar { - border: 2px solid var(--golden); - box-sizing: content-box; +.avatar-container.selected { + border: 2px solid rgba(255, 255, 255, 0.7); } -#user_avatar_block .default_persona .set_default_persona { +.avatar-container.default_persona { + outline: 2px solid var(--golden); +} + +.avatar-container.default_persona .set_default_persona { color: var(--golden); }