Bind user names to avatars (create personas) and select personas for chats

This commit is contained in:
Cohee
2023-06-11 18:49:00 +03:00
parent 471bc6cb48
commit fc001b0b05
6 changed files with 250 additions and 23 deletions

View File

@ -1784,7 +1784,7 @@
</div>
<div id="wi-holder">
<h3>
World Info
World Info / Lorebooks
<a href="https://docs.sillytavern.app/usage/guidebook/#world-info" class="notes-link" target="_blank">
<span class="note-link-span">?</span>
</a>
@ -2129,10 +2129,12 @@
<span class="note-link-span">?</span>
</a>
</label>
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="Reload Chat">
Reload Chat
</div>
</div>
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="Reload Chat">
Reload Chat</div>
</div>
<div name="NameAndAvatar" class="flex-container flexFlowColumn drawer25pWidth">
@ -2179,12 +2181,14 @@
<input id="your_name" name="your_name" placeholder="Enter your name" class="text_pole wide100p" maxlength="50" value="" autocomplete="off">
<div id="your_name_button" class="menu_button fa-solid fa-check" title="Click to set a new User Name">
</div>
<div id="lock_user_name" class="menu_button fa-solid fa-user-lock" title="Click to bind your selected persona to the current chat. Click again to remove the binding.">
</div>
<div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages">
</div>
</div>
</div>
<div name="AvatarSelector">
<h4 data-i18n="Your Avatar">Your Avatar</h4>
<h4 data-i18n="Your Avatar">Your Persona</h4>
<div id="user_avatar_block">
<div class="avatar_upload">+</div>
</div>
@ -3100,6 +3104,22 @@
<div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>
</div>
<div id="user_avatar_template" class="template_element">
<div class="avatar-container">
<div imgfile="" class="avatar">
<img src="" alt="User Avatar">
</div>
<div class="avatar-buttons">
<button class="menu_button bind_user_name" title="Bind user name to that avatar">
<i class="fa-solid fa-user-edit"></i>
</button>
<button class="menu_button delete_avatar" title="Delete persona">
<i class="fa-solid fa-trash-alt"></i>
</button>
</div>
</div>
</div>
<script>
// Configure toast library:
toastr.options.escapeHtml = true; // Prevent raw HTML inserts

View File

@ -3908,11 +3908,14 @@ function highlightSelectedAvatar() {
}
function appendUserAvatar(name) {
$("#user_avatar_block").append(
`<div imgfile="${name}" class="avatar">
<img src="User Avatars/${name}"
</div>`
);
const template = $('#user_avatar_template .avatar-container').clone();
const personaName = power_user.personas[name];
if (personaName) {
template.attr('title', personaName);
}
template.find('.avatar').attr('imgfile', name);
template.find('img').attr('src', `User Avatars/${name}`);
$("#user_avatar_block").append(template);
highlightSelectedAvatar();
}
@ -3926,6 +3929,139 @@ function reloadUserAvatar() {
});
}
export function setUserName(value) {
if (!is_send_press) {
name1 = value;
if (name1 === undefined || name1 == "")
name1 = default_user_name;
console.log(name1);
$("#your_name").val(name1);
toastr.success(`Your messages will now be sent as ${name1}`, 'User Name updated');
saveSettings("change_name");
} else {
toastr.warning('You cannot change your name while sending a message', 'Warning');
}
}
export function autoSelectPersona(name) {
for (const [key, value] of Object.entries(power_user.personas)) {
if (value === name) {
$(`.avatar[imgfile="${key}"]`).trigger('click');
return;
}
}
}
async function bindUserNameToPersona() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
const existingPersona = power_user.personas[avatarId];
const personaName = await callPopup('<h3>Enter a name for this persona:</h3>(If empty name is provided, this will unbind the name from this avatar)', 'input', existingPersona || '');
if (personaName) {
power_user.personas[avatarId] = personaName;
} else {
delete power_user.personas[avatarId];
}
saveSettingsDebounced();
await getUserAvatars();
}
function setUserAvatar() {
user_avatar = $(this).attr("imgfile");
reloadUserAvatar();
saveSettingsDebounced();
highlightSelectedAvatar();
const personaName = power_user.personas[user_avatar];
if (personaName && name1 !== personaName) {
const lockedPersona = chat_metadata['persona'];
if (lockedPersona && lockedPersona !== user_avatar) {
toastr.warning(
'Click the "Lock" to bind again. Otherwise, the selection will be reset when your reload the chat.',
`This chat is locked to a different persona (${power_user.personas[lockedPersona]}).`,
{ timeOut: 10000, extendedTimeOut: 20000 },
);
}
setUserName(personaName);
}
}
async function deleteUserAvatar() {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!avatarId) {
console.warn('No avatar id found');
return;
}
if (avatarId == user_avatar) {
toastr.warning('You cannot delete the avatar you are currently using', 'Warning');
return;
}
const confirm = await callPopup('Are you sure you want to delete this avatar?', 'confirm');
if (!confirm) {
return;
}
const request = await fetch("/deleteuseravatar", {
method: "POST",
headers: getRequestHeaders(),
body: JSON.stringify({
"avatar": avatarId,
}),
});
if (request.ok) {
delete power_user.personas[avatarId];
saveSettingsDebounced();
await getUserAvatars();
}
}
function lockUserNameToChat() {
if (chat_metadata['persona']) {
delete chat_metadata['persona'];
saveMetadata();
toastr.info('User persona is now unlocked for this chat. Click the "Lock" to bind again.', 'Persona unlocked');
return;
}
if (!(user_avatar in power_user.personas)) {
toastr.info('Creating a new persona for currently selected user name and avatar...', 'Persona not set for this avatar');
power_user.personas[user_avatar] = name1;
}
chat_metadata['persona'] = user_avatar;
saveMetadata();
saveSettingsDebounced();
toastr.success(`User persona is locked to ${name1} in this chat`);
}
eventSource.on(event_types.CHAT_CHANGED, () => {
// If persona is locked, select it
if (chat_metadata['persona']) {
// Find the avatar file
const personaAvatar = $(`.avatar[imgfile="${chat_metadata['persona']}"]`).trigger('click');
// Avatar missing (persona deleted)
if (personaAvatar.length == 0) {
console.warn('Persona avatar not found, unlocking persona');
delete chat_metadata['persona'];
return;
}
personaAvatar.trigger('click');
}
});
//***************SETTINGS****************//
///////////////////////////////////////////
async function getSettings(type) {
@ -4872,6 +5008,7 @@ async function deleteMessageImage() {
mesBlock.find('.mes_img_container').removeClass('img_extra');
mesBlock.find('.mes_img').attr('src', '');
saveChatConditional();
updateVisibleDivs('#chat', false);
}
function enlargeMessageImage() {
@ -5720,12 +5857,7 @@ $(document).ready(function () {
}
});
$(document).on("click", "#user_avatar_block .avatar", function () {
user_avatar = $(this).attr("imgfile");
reloadUserAvatar();
saveSettingsDebounced();
highlightSelectedAvatar();
});
$(document).on("click", "#user_avatar_block .avatar", setUserAvatar);
$(document).on("click", "#user_avatar_block .avatar_upload", function () {
$("#avatar_upload_file").click();
});
@ -6771,13 +6903,7 @@ $(document).ready(function () {
});
$("#your_name_button").click(function () {
if (!is_send_press) {
name1 = $("#your_name").val();
if (name1 === undefined || name1 == "") name1 = default_user_name;
console.log(name1);
toastr.success(`Your messages will now be sent as ${name1}`, 'User Name updated');
saveSettings("change_name");
}
setUserName($('#your_name').val());
});
$('#sync_name_button').on('click', async function () {
@ -6819,6 +6945,10 @@ $(document).ready(function () {
setTimeout(getStatusNovel, 10);
});
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
$(document).on('click', '.delete_avatar', deleteUserAvatar);
$('#lock_user_name').on('click', lockUserNameToChat);
//**************************CHARACTER IMPORT EXPORT*************************//
$("#character_import_button").click(function () {
$("#character_import_file").click();

View File

@ -143,7 +143,9 @@ let power_user = {
output_sequence: '### Response:',
preset: 'Alpaca',
separator_sequence: '',
}
},
personas: {},
};
let themes = [];

View File

@ -1,5 +1,6 @@
import {
addOneMessage,
autoSelectPersona,
characters,
chat,
chat_metadata,
@ -11,11 +12,13 @@ import {
replaceBiasMarkup,
saveChatConditional,
sendSystemMessage,
setUserName,
substituteParams,
system_avatar,
system_message_types
} from "../script.js";
import { humanizedDateTime } from "./RossAscends-mods.js";
import { power_user } from "./power-user.js";
export {
executeSlashCommands,
registerSlashCommand,
@ -93,6 +96,9 @@ const registerSlashCommand = parser.addCommand.bind(parser);
const getSlashCommandsHelp = parser.getHelpString.bind(parser);
parser.addCommand('help', helpCommandCallback, ['?'], ' displays this help message', true, true);
parser.addCommand('name', setNameCallback, ['persona'], '<span class="monospace">(name)</span> sets user name and persona avatar (if set)', true, true);
parser.addCommand('sync', syncCallback, [], ' syncs user name in user-attributed messages in the current chat', true, true);
parser.addCommand('bind', bindCallback, [], ' binds/unbinds a persona (name and avatar) to the current chat', true, true);
parser.addCommand('bg', setBackgroundCallback, ['background'], '<span class="monospace">(filename)</span> sets a background according to filename, partial names allowed, will set the first one alphabetically if multiple files begin with the provided argument string', false, true);
parser.addCommand('sendas', sendMessageAs, [], ` sends message as a specific character.<br>Example:<br><pre><code>/sendas Chloe\nHello, guys!</code></pre>will send "Hello, guys!" from "Chloe".<br>Uses character avatar if it exists in the characters list.`, true, true);
parser.addCommand('sys', sendNarratorMessage, [], '<span class="monospace">(text)</span> sends message as a system narrator', false, true);
@ -101,6 +107,31 @@ parser.addCommand('sysname', setNarratorName, [], '<span class="monospace">(name
const NARRATOR_NAME_KEY = 'narrator_name';
const NARRATOR_NAME_DEFAULT = 'System';
function syncCallback() {
$('#sync_name_button').trigger('click');
}
function bindCallback() {
$('#lock_user_name').trigger('click');
}
function setNameCallback(_, name) {
if (!name) {
return;
}
name = name.trim();
// If the name is a persona, auto-select it
if (Object.values(power_user.personas).map(x => x.toLowerCase()).includes(name.toLowerCase())) {
autoSelectPersona(name);
}
// Otherwise, set just the name
else {
setUserName(name);
}
}
function setNarratorName(_, text) {
const name = text || NARRATOR_NAME_DEFAULT;
chat_metadata[NARRATOR_NAME_KEY] = name;

View File

@ -1512,6 +1512,31 @@ input[type=search]:focus::-webkit-search-cancel-button {
transition: 0.2s;
}
.avatar-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.avatar-container:hover .avatar-buttons {
display: flex;
}
.avatar-buttons .menu_button {
pointer-events: all;
}
.avatar-buttons {
pointer-events: none;
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
justify-content: space-between;
}
.avatar_div .avatar {
margin-left: 4px;
margin-right: 10px;

View File

@ -1104,6 +1104,25 @@ app.post("/getuseravatars", jsonParser, function (request, response) {
response.send(JSON.stringify(images));
});
app.post('/deleteuseravatar', jsonParser, function (request, response) {
if (!request.body) return response.sendStatus(400);
if (request.body.avatar !== sanitize(request.body.avatar)) {
console.error('Malicious avatar name prevented');
return response.sendStatus(403);
}
const fileName = path.join(directories.avatars, sanitize(request.body.avatar));
if (fs.existsSync(fileName)) {
fs.rmSync(fileName);
return response.send({ result: 'ok' });
}
return response.sendStatus(404);
});
app.post("/setbackground", jsonParser, function (request, response) {
var bg = "#bg1 {background-image: url('../backgrounds/" + request.body.bg + "');}";
fs.writeFile('public/css/bg_load.css', bg, 'utf8', function (err) {