+
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);
}