Move persona functions from script. Clean-up exports
This commit is contained in:
parent
a5475e7752
commit
620cd6dfc2
619
public/script.js
619
public/script.js
File diff suppressed because it is too large
Load Diff
|
@ -1,33 +1,230 @@
|
|||
import {
|
||||
callPopup,
|
||||
characters,
|
||||
chat,
|
||||
chat_metadata,
|
||||
crop_data,
|
||||
default_avatar,
|
||||
eventSource,
|
||||
event_types,
|
||||
getCropPopup,
|
||||
getRequestHeaders,
|
||||
getThumbnailUrl,
|
||||
getUserAvatars,
|
||||
name1,
|
||||
reloadCurrentChat,
|
||||
saveChatConditional,
|
||||
saveMetadata,
|
||||
saveSettingsDebounced,
|
||||
setUserAvatar,
|
||||
setUserName,
|
||||
this_chid,
|
||||
user_avatar,
|
||||
} from '../script.js';
|
||||
import { persona_description_positions, power_user } from './power-user.js';
|
||||
import { getTokenCountAsync } from './tokenizers.js';
|
||||
import { debounce, delay, download, parseJsonFile } from './utils.js';
|
||||
import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, parseJsonFile } from './utils.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
|
||||
let savePersonasPage = 0;
|
||||
const GRID_STORAGE_KEY = 'Personas_GridView';
|
||||
export let user_avatar = '';
|
||||
export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
|
||||
|
||||
function switchPersonaGridView() {
|
||||
const state = localStorage.getItem(GRID_STORAGE_KEY) === 'true';
|
||||
$('#user_avatar_block').toggleClass('gridView', state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the avatar for the given user avatar Id.
|
||||
* @param {string} avatarImg User avatar Id
|
||||
* @returns {string} User avatar URL
|
||||
*/
|
||||
export function getUserAvatar(avatarImg) {
|
||||
return `User Avatars/${avatarImg}`;
|
||||
}
|
||||
|
||||
export function initUserAvatar(avatar) {
|
||||
user_avatar = avatar;
|
||||
reloadUserAvatar();
|
||||
highlightSelectedAvatar();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
saveSettingsDebounced();
|
||||
$('.zoomed_avatar[forchar]').remove();
|
||||
}
|
||||
|
||||
function highlightSelectedAvatar() {
|
||||
$('#user_avatar_block .avatar-container').removeClass('selected');
|
||||
$(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected');
|
||||
}
|
||||
|
||||
function reloadUserAvatar(force = false) {
|
||||
$('.mes').each(function () {
|
||||
const avatarImg = $(this).find('.avatar img');
|
||||
if (force) {
|
||||
avatarImg.attr('src', avatarImg.attr('src'));
|
||||
}
|
||||
|
||||
if ($(this).attr('is_user') == 'true' && $(this).attr('force_avatar') == 'false') {
|
||||
avatarImg.attr('src', getUserAvatar(user_avatar));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the given personas
|
||||
* @param {string[]} personas - The persona names to sort
|
||||
* @returns {string[]} The sorted persona names arrray, same reference as passed in
|
||||
*/
|
||||
function sortPersonas(personas) {
|
||||
const option = $('#persona_sort_order').find(':selected');
|
||||
if (option.attr('value') === 'search') {
|
||||
personas.sort((a, b) => {
|
||||
const aScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, a);
|
||||
const bScore = personasFilter.getScore(FILTER_TYPES.PERSONA_SEARCH, b);
|
||||
return (aScore - bScore);
|
||||
});
|
||||
} else {
|
||||
personas.sort((a, b) => {
|
||||
const aName = String(power_user.personas[a] || a);
|
||||
const bName = String(power_user.personas[b] || b);
|
||||
return power_user.persona_sort_order === 'asc' ? aName.localeCompare(bName) : bName.localeCompare(aName);
|
||||
});
|
||||
}
|
||||
|
||||
return personas;
|
||||
}
|
||||
|
||||
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
|
||||
function verifyPersonaSearchSortRule() {
|
||||
const searchTerm = personasFilter.getFilterData(FILTER_TYPES.PERSONA_SEARCH);
|
||||
const searchOption = $('#persona_sort_order option[value="search"]');
|
||||
const selector = $('#persona_sort_order');
|
||||
const isHidden = searchOption.attr('hidden') !== undefined;
|
||||
|
||||
// If we have a search term, we are displaying the sorting option for it
|
||||
if (searchTerm && isHidden) {
|
||||
searchOption.removeAttr('hidden');
|
||||
selector.val(searchOption.attr('value'));
|
||||
flashHighlight(selector);
|
||||
}
|
||||
// If search got cleared, we make sure to hide the option and go back to the one before
|
||||
if (!searchTerm && !isHidden) {
|
||||
searchOption.attr('hidden', '');
|
||||
selector.val(power_user.persona_sort_order);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a rendered avatar block.
|
||||
* @param {string} name Avatar file name
|
||||
* @returns {JQuery<HTMLElement>} Avatar block
|
||||
*/
|
||||
function getUserAvatarBlock(name) {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
const template = $('#user_avatar_template .avatar-container').clone();
|
||||
const personaName = power_user.personas[name];
|
||||
const personaDescription = power_user.persona_descriptions[name]?.description;
|
||||
template.find('.ch_name').text(personaName || '[Unnamed Persona]');
|
||||
template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription);
|
||||
template.attr('imgfile', name);
|
||||
template.find('.avatar').attr('imgfile', name).attr('title', name);
|
||||
template.toggleClass('default_persona', name === power_user.default_persona);
|
||||
let avatarUrl = getUserAvatar(name);
|
||||
if (isFirefox) {
|
||||
avatarUrl += '?t=' + Date.now();
|
||||
}
|
||||
template.find('img').attr('src', avatarUrl);
|
||||
$('#user_avatar_block').append(template);
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of user avatars.
|
||||
* @param {boolean} doRender Whether to render the list
|
||||
* @param {string} openPageAt Item to be opened at
|
||||
* @returns {Promise<string[]>} List of avatar file names
|
||||
*/
|
||||
export async function getUserAvatars(doRender = true, openPageAt = '') {
|
||||
const response = await fetch('/api/avatars/get', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
if (response.ok) {
|
||||
const allEntities = await response.json();
|
||||
|
||||
if (!Array.isArray(allEntities)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!doRender) {
|
||||
return allEntities;
|
||||
}
|
||||
|
||||
// Before printing the personas, we check if we should enable/disable search sorting
|
||||
verifyPersonaSearchSortRule();
|
||||
|
||||
let entities = personasFilter.applyFilters(allEntities);
|
||||
entities = sortPersonas(entities);
|
||||
|
||||
const storageKey = 'Personas_PerPage';
|
||||
const listId = '#user_avatar_block';
|
||||
const perPage = Number(localStorage.getItem(storageKey)) || 5;
|
||||
|
||||
$('#persona_pagination_container').pagination({
|
||||
dataSource: entities,
|
||||
pageSize: perPage,
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
if (openPageAt) {
|
||||
const avatarIndex = entities.indexOf(openPageAt);
|
||||
const page = Math.floor(avatarIndex / perPage) + 1;
|
||||
|
||||
if (avatarIndex !== -1) {
|
||||
$('#persona_pagination_container').pagination('go', page);
|
||||
}
|
||||
}
|
||||
|
||||
return allEntities;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an avatar file to the server
|
||||
* @param {string} url URL for the avatar file
|
||||
|
@ -59,6 +256,73 @@ async function uploadUserAvatar(url, name) {
|
|||
});
|
||||
}
|
||||
|
||||
async function changeUserAvatar(e) {
|
||||
const form = document.getElementById('form_upload_avatar');
|
||||
|
||||
if (!(form instanceof HTMLFormElement)) {
|
||||
console.error('Form not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = e.target.files[0];
|
||||
|
||||
if (!file) {
|
||||
form.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData(form);
|
||||
const dataUrl = await getBase64Async(file);
|
||||
let url = '/api/avatars/upload';
|
||||
|
||||
if (!power_user.never_resize_avatars) {
|
||||
const confirmation = await callPopup(getCropPopup(dataUrl), 'avatarToCrop', '', { okButton: 'Crop', large: true, wide: true });
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (crop_data !== undefined) {
|
||||
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
|
||||
}
|
||||
}
|
||||
|
||||
const rawFile = formData.get('avatar');
|
||||
if (rawFile instanceof File) {
|
||||
const convertedFile = await ensureImageFormatSupported(rawFile);
|
||||
formData.set('avatar', convertedFile);
|
||||
}
|
||||
|
||||
jQuery.ajax({
|
||||
type: 'POST',
|
||||
url: url,
|
||||
data: formData,
|
||||
beforeSend: () => { },
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: async function (data) {
|
||||
// If the user uploaded a new avatar, we want to make sure it's not cached
|
||||
const name = formData.get('overwrite_name');
|
||||
if (name) {
|
||||
await fetch(getUserAvatar(String(name)), { cache: 'no-cache' });
|
||||
reloadUserAvatar(true);
|
||||
}
|
||||
|
||||
if (!name && data.path) {
|
||||
await getUserAvatars();
|
||||
await delay(500);
|
||||
await createPersona(data.path);
|
||||
}
|
||||
|
||||
await getUserAvatars(true, name || data.path);
|
||||
},
|
||||
error: (jqXHR, exception) => { },
|
||||
});
|
||||
|
||||
// Will allow to select the same file twice in a row
|
||||
form.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to create a persona for the uploaded avatar.
|
||||
* @param {string} avatarId User avatar id
|
||||
|
@ -206,7 +470,7 @@ export function autoSelectPersona(name) {
|
|||
* @param {string} avatarId User avatar id
|
||||
* @param {string} newName New name for the persona
|
||||
*/
|
||||
export async function updatePersonaNameIfExists(avatarId, newName) {
|
||||
async function updatePersonaNameIfExists(avatarId, newName) {
|
||||
if (avatarId in power_user.personas) {
|
||||
power_user.personas[avatarId] = newName;
|
||||
await getUserAvatars(true, avatarId);
|
||||
|
@ -267,7 +531,7 @@ async function bindUserNameToPersona(e) {
|
|||
setPersonaDescription();
|
||||
}
|
||||
|
||||
export function selectCurrentPersona() {
|
||||
function selectCurrentPersona() {
|
||||
const personaName = power_user.personas[user_avatar];
|
||||
if (personaName) {
|
||||
const lockedPersona = chat_metadata['persona'];
|
||||
|
@ -623,6 +887,30 @@ async function onPersonasRestoreInput(e) {
|
|||
$('#personas_restore_input').val('');
|
||||
}
|
||||
|
||||
async function syncUserNameToPersona() {
|
||||
const confirmation = await callPopup(`<h3>Are you sure?</h3>All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm');
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const mes of chat) {
|
||||
if (mes.is_user) {
|
||||
mes.name = name1;
|
||||
mes.force_avatar = getUserAvatar(user_avatar);
|
||||
}
|
||||
}
|
||||
|
||||
await saveChatConditional();
|
||||
await reloadCurrentChat();
|
||||
}
|
||||
|
||||
export function retriggerFirstMessageOnEmptyChat() {
|
||||
if (this_chid >= 0 && !selected_group && chat.length === 1) {
|
||||
$('#firstmessage_textarea').trigger('input');
|
||||
}
|
||||
}
|
||||
|
||||
export function initPersonas() {
|
||||
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
|
||||
$(document).on('click', '.set_default_persona', setDefaultPersona);
|
||||
|
@ -647,6 +935,51 @@ export function initPersonas() {
|
|||
switchPersonaGridView();
|
||||
});
|
||||
|
||||
const debouncedPersonaSearch = debounce((searchQuery) => {
|
||||
personasFilter.setFilterData(FILTER_TYPES.PERSONA_SEARCH, searchQuery);
|
||||
});
|
||||
|
||||
$('#persona_search_bar').on('input', function () {
|
||||
const searchQuery = String($(this).val());
|
||||
debouncedPersonaSearch(searchQuery);
|
||||
});
|
||||
|
||||
$('#sync_name_button').on('click', syncUserNameToPersona);
|
||||
$('#avatar_upload_file').on('change', changeUserAvatar);
|
||||
|
||||
$(document).on('click', '#user_avatar_block .avatar-container', function () {
|
||||
const imgfile = $(this).attr('imgfile');
|
||||
setUserAvatar(imgfile);
|
||||
|
||||
// force firstMes {{user}} update on persona switch
|
||||
retriggerFirstMessageOnEmptyChat();
|
||||
});
|
||||
|
||||
$('#your_name_button').click(async function () {
|
||||
const userName = String($('#your_name').val()).trim();
|
||||
setUserName(userName);
|
||||
await updatePersonaNameIfExists(user_avatar, userName);
|
||||
retriggerFirstMessageOnEmptyChat();
|
||||
});
|
||||
|
||||
$(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 (e) {
|
||||
e.stopPropagation();
|
||||
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
|
||||
|
||||
if (!avatarId) {
|
||||
console.log('no imgfile');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#avatar_upload_overwrite').val(avatarId);
|
||||
$('#avatar_upload_file').trigger('click');
|
||||
});
|
||||
|
||||
eventSource.on('charManagementDropdown', (target) => {
|
||||
if (target === 'convert_to_persona') {
|
||||
convertCharacterToPersona();
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
name2,
|
||||
reloadCurrentChat,
|
||||
removeMacros,
|
||||
retriggerFirstMessageOnEmptyChat,
|
||||
saveChatConditional,
|
||||
sendMessageAsUser,
|
||||
sendSystemMessage,
|
||||
|
@ -44,7 +43,7 @@ import { getContext, saveMetadataDebounced } from './extensions.js';
|
|||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||
import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';
|
||||
import { chat_completion_sources, oai_settings } from './openai.js';
|
||||
import { autoSelectPersona } from './personas.js';
|
||||
import { autoSelectPersona, retriggerFirstMessageOnEmptyChat } from './personas.js';
|
||||
import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
|
||||
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
|
||||
import { decodeTextTokens, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync } from './tokenizers.js';
|
||||
|
|
|
@ -1443,8 +1443,8 @@ export function includesIgnoreCaseAndAccents(text, searchTerm) {
|
|||
if (!text || !searchTerm) return false; // Return false if either string is empty
|
||||
|
||||
// Normalize and remove diacritics, then convert to lower case
|
||||
const normalizedText = text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
||||
const normalizedSearchTerm = searchTerm.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
||||
const normalizedText = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||
const normalizedSearchTerm = searchTerm.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
|
||||
|
||||
// Check if the normalized text includes the normalized search term
|
||||
return normalizedText.includes(normalizedSearchTerm);
|
||||
|
|
Loading…
Reference in New Issue