#1761 Persona management overhaul

This commit is contained in:
Cohee
2024-01-30 19:12:56 +02:00
parent 079ab7db07
commit 4542c66664
7 changed files with 193 additions and 91 deletions

View File

@@ -148,7 +148,8 @@ body.big-avatars .avatar_collage {
aspect-ratio: 2 / 3; aspect-ratio: 2 / 3;
} }
body.big-avatars .ch_description { body.big-avatars .ch_description,
.avatar-container .ch_description {
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 3;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;

View File

@@ -3659,7 +3659,20 @@
<input id="personas_restore_input" type="file" accept=".json" hidden> <input id="personas_restore_input" type="file" accept=".json" hidden>
</div> </div>
</div> </div>
<div id="persona-management-block" class="flex-container wide100p"> <div id="persona-management-block" class="flex-container wide100p flexGap10">
<div class="flex1">
<div class="flex-container marginBot10 alignitemscenter">
<input id="persona_search_bar" class="text_pole width100p flex1" type="search" data-i18n="[placeholder]Search..." placeholder="Search..." maxlength="100">
<div id="persona_pagination_container" class="flex1"></div>
</div>
<div id="user_avatar_block">
<div class="avatar_upload">+</div>
</div>
<form id="form_upload_avatar" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<input type="file" id="avatar_upload_file" accept="image/*" name="avatar">
<input type="hidden" id="avatar_upload_overwrite" name="overwrite_name" value="">
</form>
</div>
<div class="flex1"> <div class="flex1">
<h4 data-i18n="Name">Name</h4> <h4 data-i18n="Name">Name</h4>
<div class="change_name"> <div class="change_name">
@@ -3695,18 +3708,6 @@
</label> </label>
</div> </div>
</div> </div>
<div class="flex1">
<h4 class="title_restorable">
<span data-i18n="Your Persona">Your Persona</span>
</h4>
<div id="user_avatar_block">
<div class="avatar_upload">+</div>
</div>
<form id="form_upload_avatar" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<input type="file" id="avatar_upload_file" accept="image/*" name="avatar">
<input type="hidden" id="avatar_upload_overwrite" name="overwrite_name" value="">
</form>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -5319,27 +5320,31 @@
<div id="rawPromptWrapper" class="tokenItemizingSubclass"></div> <div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>
</div> </div>
<div id="user_avatar_template" class="template_element"> <div id="user_avatar_template" class="template_element">
<div class="avatar-container"> <div class="avatar-container wide100p">
<div imgfile="" class="avatar"> <div imgfile="" class="avatar">
<img src="" alt="User Avatar"> <img src="" alt="User Avatar">
</div> </div>
<div class="avatar-buttons avatar-buttons-top"> <div class="flex-container wide100pLess70px character_select_container">
<div class="wide100p character_name_block">
<span class="ch_name">Kagari</span>
<div class="avatar-buttons">
<button class="menu_button bind_user_name" title="Bind user name to that avatar" data-i18n="[title]Bind user name to that avatar"> <button class="menu_button bind_user_name" title="Bind user name to that avatar" data-i18n="[title]Bind user name to that avatar">
<i class="fa-solid fa-user-edit"></i> <i class="fa-fw fa-solid fa-user-edit"></i>
</button>
<button class="menu_button set_persona_image" title="Change persona image" data-i18n="[title]Change persona image">
<i class="fa-fw fa-solid fa-image"></i>
</button> </button>
<button class="menu_button set_default_persona" title="Select this as default persona for the new chats." data-i18n="[title]Select this as default persona for the new chats."> <button class="menu_button set_default_persona" title="Select this as default persona for the new chats." data-i18n="[title]Select this as default persona for the new chats.">
<i class="fa-solid fa-crown"></i> <i class="fa-fw fa-solid fa-crown"></i>
</button>
</div>
<div class="avatar-buttons avatar-buttons-bottom">
<button class="menu_button set_persona_image" title="Change persona image" data-i18n="[title]Change persona image">
<i class="fa-solid fa-image"></i>
</button> </button>
<button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona"> <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona">
<i class="fa-solid fa-trash-alt"></i> <i class="fa-fw fa-solid fa-trash-alt"></i>
</button> </button>
</div> </div>
</div> </div>
<div class="ch_description">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 </div>
</div>
</div>
</div> </div>
<script> <script>
// Configure toast library: // Configure toast library:

View File

@@ -444,6 +444,7 @@ let generation_started = new Date();
let characters = []; let characters = [];
let this_chid; let this_chid;
let saveCharactersPage = 0; let saveCharactersPage = 0;
let savePersonasPage = 0;
const default_avatar = 'img/ai4.png'; const default_avatar = 'img/ai4.png';
export const system_avatar = 'img/five.png'; export const system_avatar = 'img/five.png';
export const comment_avatar = 'img/quill.png'; export const comment_avatar = 'img/quill.png';
@@ -790,6 +791,7 @@ var PromptArrayItemForRawPromptDisplay;
export let active_character = ''; export let active_character = '';
export let active_group = ''; export let active_group = '';
export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100)); export const entitiesFilter = new FilterHelper(debounce(printCharacters, 100));
export const personasFilter = new FilterHelper(debounce(getUserAvatars, 100));
export function getRequestHeaders() { export function getRequestHeaders() {
return { 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<string[]>} List of avatar file names
*/
export async function getUserAvatars(doRender = true) {
const response = await fetch('/getuseravatars', { const response = await fetch('/getuseravatars', {
method: 'POST', method: 'POST',
headers: getRequestHeaders(), headers: getRequestHeaders(),
body: JSON.stringify({
'': '',
}),
}); });
if (response.ok === true) { if (response.ok) {
const getData = await response.json(); const allEntities = await response.json();
$('#user_avatar_block').html(''); //RossAscends: necessary to avoid doubling avatars each refresh.
$('#user_avatar_block').append('<div class="avatar_upload">+</div>');
for (var i = 0; i < getData.length; i++) { if (!doRender) {
appendUserAvatar(getData[i]); 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() { function highlightSelectedAvatar() {
$('#user_avatar_block').find('.avatar').removeClass('selected'); $('#user_avatar_block .avatar-container').removeClass('selected');
$('#user_avatar_block') $('#user_avatar_block').find(`.avatar-container[imgfile='${user_avatar}']`).addClass('selected');
.find(`.avatar[imgfile='${user_avatar}']`)
.addClass('selected');
} }
function appendUserAvatar(name) { /**
* Gets a rendered avatar block.
* @param {string} name Avatar file name
* @returns {JQuery<HTMLElement>} Avatar block
*/
function getUserAvatarBlock(name) {
const template = $('#user_avatar_template .avatar-container').clone(); const template = $('#user_avatar_template .avatar-container').clone();
const personaName = power_user.personas[name]; const personaName = power_user.personas[name];
if (personaName) { const personaDescription = power_user.persona_descriptions[name]?.description;
template.attr('title', personaName); template.find('.ch_name').text(personaName || '[Unnamed Persona]');
} else { template.find('.ch_description').text(personaDescription || '[No description]').toggleClass('text_muted', !personaDescription);
template.attr('title', '[Unnamed Persona]'); template.attr('imgfile', name);
}
template.find('.avatar').attr('imgfile', name); template.find('.avatar').attr('imgfile', name);
template.toggleClass('default_persona', name === power_user.default_persona); template.toggleClass('default_persona', name === power_user.default_persona);
template.find('img').attr('src', getUserAvatar(name)); template.find('img').attr('src', getUserAvatar(name));
$('#user_avatar_block').append(template); $('#user_avatar_block').append(template);
highlightSelectedAvatar(); return template;
} }
function reloadUserAvatar(force = false) { function reloadUserAvatar(force = false) {
@@ -5463,8 +5503,12 @@ export function setUserName(value) {
saveSettingsDebounced(); 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(); reloadUserAvatar();
highlightSelectedAvatar(); highlightSelectedAvatar();
selectCurrentPersona(); selectCurrentPersona();
@@ -7966,6 +8010,11 @@ jQuery(async function () {
entitiesFilter.setFilterData(FILTER_TYPES.SEARCH, searchValue); 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 () { $('#mes_continue').on('click', function () {
$('#option_continue').trigger('click'); $('#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 () { $(document).on('click', '#user_avatar_block .avatar_upload', function () {
$('#avatar_upload_overwrite').val(''); $('#avatar_upload_overwrite').val('');
$('#avatar_upload_file').trigger('click'); $('#avatar_upload_file').trigger('click');

View File

@@ -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'; import { tag_map } from './tags.js';
/** /**
@@ -11,6 +11,7 @@ export const FILTER_TYPES = {
FAV: 'fav', FAV: 'fav',
GROUP: 'group', GROUP: 'group',
WORLD_INFO_SEARCH: 'world_info_search', 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.FAV]: this.favFilter.bind(this),
[FILTER_TYPES.TAG]: this.tagFilter.bind(this), [FILTER_TYPES.TAG]: this.tagFilter.bind(this),
[FILTER_TYPES.WORLD_INFO_SEARCH]: this.wiSearchFilter.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.FAV]: false,
[FILTER_TYPES.TAG]: { excluded: [], selected: [] }, [FILTER_TYPES.TAG]: { excluded: [], selected: [] },
[FILTER_TYPES.WORLD_INFO_SEARCH]: '', [FILTER_TYPES.WORLD_INFO_SEARCH]: '',
[FILTER_TYPES.PERSONA_SEARCH]: '',
}; };
/** /**
@@ -69,6 +72,22 @@ export class FilterHelper {
return data.filter(entity => fuzzySearchResults.includes(entity.uid)); 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. * Checks if the given entity is tagged with the given tag ID.
* @param {object} entity Searchable entity * @param {object} entity Searchable entity

View File

@@ -11,6 +11,7 @@ import {
name1, name1,
saveMetadata, saveMetadata,
saveSettingsDebounced, saveSettingsDebounced,
setUserAvatar,
setUserName, setUserName,
this_chid, this_chid,
user_avatar, user_avatar,
@@ -187,7 +188,7 @@ export function autoSelectPersona(name) {
for (const [key, value] of Object.entries(power_user.personas)) { for (const [key, value] of Object.entries(power_user.personas)) {
if (value === name) { if (value === name) {
console.log(`Auto-selecting persona ${key} for name ${name}`); console.log(`Auto-selecting persona ${key} for name ${name}`);
$(`.avatar[imgfile="${key}"]`).trigger('click'); setUserAvatar(key);
return; return;
} }
} }
@@ -400,6 +401,9 @@ function onPersonaDescriptionInput() {
object.description = power_user.persona_description; 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(); saveSettingsDebounced();
} }
@@ -481,7 +485,7 @@ function updateUserLockIcon() {
$('#lock_user_name').toggleClass('fa-lock', hasLock); $('#lock_user_name').toggleClass('fa-lock', hasLock);
} }
function setChatLockedPersona() { async function setChatLockedPersona() {
// Define a persona for this chat // Define a persona for this chat
let chatPersona = ''; let chatPersona = '';
@@ -502,10 +506,10 @@ function setChatLockedPersona() {
} }
// Find the avatar file // Find the avatar file
const personaAvatar = $(`.avatar[imgfile="${chatPersona}"]`).trigger('click'); const userAvatars = await getUserAvatars(false);
// Avatar missing (persona deleted) // 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'); console.warn('Persona avatar not found, unlocking persona');
delete chat_metadata['persona']; delete chat_metadata['persona'];
updateUserLockIcon(); updateUserLockIcon();
@@ -513,7 +517,7 @@ function setChatLockedPersona() {
} }
// Default persona missing // 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'); console.warn('Default persona avatar not found, clearing default persona');
power_user.default_persona = null; power_user.default_persona = null;
saveSettingsDebounced(); saveSettingsDebounced();
@@ -521,7 +525,7 @@ function setChatLockedPersona() {
} }
// Persona avatar found, select it // Persona avatar found, select it
personaAvatar.trigger('click'); setUserAvatar(chatPersona);
updateUserLockIcon(); updateUserLockIcon();
} }
@@ -560,7 +564,7 @@ async function onPersonasRestoreInput(e) {
return; return;
} }
const avatarsList = await getUserAvatars(); const avatarsList = await getUserAvatars(false);
const warnings = []; const warnings = [];
// Merge personas with existing ones // Merge personas with existing ones

View File

@@ -1828,6 +1828,23 @@ export function fuzzySearchWorldInfo(data, searchValue) {
return results.map(x => x.item?.uid); 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) { export function fuzzySearchTags(searchValue) {
const fuse = new Fuse(tags, { const fuse = new Fuse(tags, {
keys: [ keys: [

View File

@@ -1596,7 +1596,8 @@ input[type=search]:focus::-webkit-search-cancel-button {
} }
.bogus_folder_select:hover, .bogus_folder_select:hover,
.character_select:hover { .character_select:hover,
.avatar-container:hover {
background-color: var(--white30a); background-color: var(--white30a);
} }
@@ -1821,45 +1822,44 @@ input[type=search]:focus::-webkit-search-cancel-button {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: flex-start;
gap: 5px;
padding: 5px;
border-radius: 10px;
cursor: pointer;
margin-bottom: 1px;
} }
grammarly-extension { grammarly-extension {
z-index: 35; z-index: 35;
} }
.avatar-container:hover .avatar-buttons { .avatar-container .ch_name {
flex: 1;
}
.avatar-container .avatar-buttons {
display: flex; 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 { .avatar-buttons .menu_button {
pointer-events: all; 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 */ /* Ross should be able to handle this later */
/*.big-avatars .avatar-buttons{ /*.big-avatars .avatar-buttons{
justify-content: center; justify-content: center;
width: fit-content; width: fit-content;
}*/ }*/
.avatar-buttons {
pointer-events: none;
display: none;
position: absolute;
width: 100%;
justify-content: space-between;
}
.avatar_div .avatar { .avatar_div .avatar {
/* margin-left: 4px; /* margin-left: 4px;
margin-right: 10px; margin-right: 10px;
@@ -2279,7 +2279,6 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
#user_avatar_block { #user_avatar_block {
display: flex; display: flex;
grid-gap: 10px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-evenly; justify-content: space-evenly;
} }
@@ -2288,20 +2287,28 @@ input[type="checkbox"]:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
cursor: pointer; cursor: pointer;
width: 64px; width: 64px;
height: 64px; height: 64px;
outline: 2px solid rgba(255, 255, 255, 0.7);
border-radius: 50%; 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; outline: 2px solid transparent;
border: 2px solid transparent;
} }
#user_avatar_block .default_persona .avatar { .avatar-container.selected {
border: 2px solid var(--golden); border: 2px solid rgba(255, 255, 255, 0.7);
box-sizing: content-box;
} }
#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); color: var(--golden);
} }