mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Bind user names to avatars (create personas) and select personas for chats
This commit is contained in:
@ -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>
|
||||
|
||||
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="Reload Chat">
|
||||
Reload Chat</div>
|
||||
Reload Chat
|
||||
</div>
|
||||
</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
|
||||
|
166
public/script.js
166
public/script.js
@ -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();
|
||||
|
@ -143,7 +143,9 @@ let power_user = {
|
||||
output_sequence: '### Response:',
|
||||
preset: 'Alpaca',
|
||||
separator_sequence: '',
|
||||
}
|
||||
},
|
||||
|
||||
personas: {},
|
||||
};
|
||||
|
||||
let themes = [];
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
19
server.js
19
server.js
@ -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) {
|
||||
|
Reference in New Issue
Block a user