mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Multiple expressions per group in waifu mode
This commit is contained in:
@ -425,6 +425,8 @@ export const event_types = {
|
|||||||
IMPERSONATE_READY: 'impersonate_ready',
|
IMPERSONATE_READY: 'impersonate_ready',
|
||||||
CHAT_CHANGED: 'chat_id_changed',
|
CHAT_CHANGED: 'chat_id_changed',
|
||||||
GENERATION_STOPPED: 'generation_stopped',
|
GENERATION_STOPPED: 'generation_stopped',
|
||||||
|
SETTINGS_UPDATED: 'settings_updated',
|
||||||
|
GROUP_UPDATED: 'group_updated',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventSource = new EventEmitter();
|
export const eventSource = new EventEmitter();
|
||||||
@ -3955,6 +3957,7 @@ function selectKoboldGuiPreset() {
|
|||||||
|
|
||||||
async function saveSettings(type) {
|
async function saveSettings(type) {
|
||||||
//console.log('Entering settings with name1 = '+name1);
|
//console.log('Entering settings with name1 = '+name1);
|
||||||
|
eventSource.emit(event_types.SETTINGS_UPDATED);
|
||||||
return jQuery.ajax({
|
return jQuery.ajax({
|
||||||
type: "POST",
|
type: "POST",
|
||||||
url: "/savesettings",
|
url: "/savesettings",
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { callPopup, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
|
import { callPopup, eventSource, event_types, getRequestHeaders, saveSettingsDebounced } from "../../../script.js";
|
||||||
|
import { deviceInfo } from "../../RossAscends-mods.js";
|
||||||
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
|
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch } from "../../extensions.js";
|
||||||
|
import { power_user } from "../../power-user.js";
|
||||||
|
import { onlyUnique, debounce } from "../../utils.js";
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
const MODULE_NAME = 'expressions';
|
const MODULE_NAME = 'expressions';
|
||||||
@ -41,6 +44,201 @@ let lastMessage = null;
|
|||||||
let spriteCache = {};
|
let spriteCache = {};
|
||||||
let inApiCall = false;
|
let inApiCall = false;
|
||||||
|
|
||||||
|
function isVisualNovelMode() {
|
||||||
|
return Boolean(!deviceInfo.isMobile && power_user.waifuMode && getContext().groupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forceUpdateVisualNovelMode() {
|
||||||
|
if (isVisualNovelMode()) {
|
||||||
|
await updateVisualNovelMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateVisualNovelModeDebounced = debounce(forceUpdateVisualNovelMode, 100);
|
||||||
|
|
||||||
|
async function updateVisualNovelMode(name, expression) {
|
||||||
|
const container = $('#visual-novel-wrapper');
|
||||||
|
|
||||||
|
await visualNovelRemoveInactive(container);
|
||||||
|
|
||||||
|
const setSpritePromises = await visualNovelSetCharacterSprites(container, name, expression);
|
||||||
|
|
||||||
|
// calculate layer indices based on recent messages
|
||||||
|
await visualNovelUpdateLayers(container);
|
||||||
|
|
||||||
|
await Promise.allSettled(setSpritePromises);
|
||||||
|
|
||||||
|
// update again based on new sprites
|
||||||
|
if (setSpritePromises.length > 0) {
|
||||||
|
await visualNovelUpdateLayers(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function visualNovelRemoveInactive(container) {
|
||||||
|
const context = getContext();
|
||||||
|
const group = context.groups.find(x => x.id == context.groupId);
|
||||||
|
const members = group.members;
|
||||||
|
const removeInactiveCharactersPromises = [];
|
||||||
|
|
||||||
|
// remove inactive characters after 1 second
|
||||||
|
container.find('.expression-holder').each((_, current) => {
|
||||||
|
const promise = new Promise(resolve => {
|
||||||
|
const element = $(current);
|
||||||
|
const avatar = element.data('avatar');
|
||||||
|
|
||||||
|
if (!members.includes(avatar) || group.disabled_members.includes(avatar)) {
|
||||||
|
element.fadeOut(250, () => {
|
||||||
|
element.remove();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
removeInactiveCharactersPromises.push(promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(removeInactiveCharactersPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function visualNovelSetCharacterSprites(container, name, expression) {
|
||||||
|
const context = getContext();
|
||||||
|
const group = context.groups.find(x => x.id == context.groupId);
|
||||||
|
const members = group.members;
|
||||||
|
const labels = await getExpressionsList();
|
||||||
|
|
||||||
|
const createCharacterPromises = [];
|
||||||
|
const setSpritePromises = [];
|
||||||
|
|
||||||
|
for (const avatar of members) {
|
||||||
|
const isDisabled = group.disabled_members.includes(avatar);
|
||||||
|
|
||||||
|
// skip disabled characters
|
||||||
|
if (isDisabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const character = context.characters.find(x => x.avatar == avatar);
|
||||||
|
|
||||||
|
// download images if not downloaded yet
|
||||||
|
if (spriteCache[character.name] === undefined) {
|
||||||
|
spriteCache[character.name] = await getSpritesList(character.name, character);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sprites = spriteCache[character.name];
|
||||||
|
const expressionImage = container.find(`.expression-holder[data-avatar="${avatar}"]`);
|
||||||
|
const defaultSpritePath = sprites.find(x => x.label === 'joy')?.path;
|
||||||
|
|
||||||
|
if (expressionImage.length > 0) {
|
||||||
|
if (name == character.name) {
|
||||||
|
const currentSpritePath = labels.includes(expression) ? sprites.find(x => x.label === expression)?.path : '';
|
||||||
|
|
||||||
|
const path = currentSpritePath || defaultSpritePath || '';
|
||||||
|
const img = expressionImage.find('img');
|
||||||
|
setImage(img, path);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const template = $('#expression-holder').clone();
|
||||||
|
template.attr('data-avatar', avatar);
|
||||||
|
$('#visual-novel-wrapper').append(template);
|
||||||
|
setImage(template.find('img'), defaultSpritePath || '');
|
||||||
|
const fadeInPromise = new Promise(resolve => {
|
||||||
|
template.fadeIn(250, () => resolve());
|
||||||
|
});
|
||||||
|
createCharacterPromises.push(fadeInPromise);
|
||||||
|
const setSpritePromise = setLastMessageSprite(template.find('img'), avatar, labels);
|
||||||
|
setSpritePromises.push(setSpritePromise);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.allSettled(createCharacterPromises);
|
||||||
|
return setSpritePromises;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function visualNovelUpdateLayers(container) {
|
||||||
|
const context = getContext();
|
||||||
|
const group = context.groups.find(x => x.id == context.groupId);
|
||||||
|
const members = group.members;
|
||||||
|
const recentMessages = context.chat.map(x => x.original_avatar).filter(onlyUnique);
|
||||||
|
const filteredMembers = members.filter(x => !group.disabled_members.includes(x));
|
||||||
|
const layerIndices = filteredMembers.slice().sort((a, b) => recentMessages.indexOf(a) - recentMessages.indexOf(b));
|
||||||
|
|
||||||
|
const setLayerIndicesPromises = [];
|
||||||
|
|
||||||
|
const sortFunction = (a, b) => {
|
||||||
|
const avatarA = $(a).data('avatar');
|
||||||
|
const avatarB = $(b).data('avatar');
|
||||||
|
const indexA = filteredMembers.indexOf(avatarA);
|
||||||
|
const indexB = filteredMembers.indexOf(avatarB);
|
||||||
|
return indexA - indexB;
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerWidth = container.width();
|
||||||
|
const pivotalPoint = containerWidth * 0.5;
|
||||||
|
|
||||||
|
let images = $('.expression-holder');
|
||||||
|
let imagesWidth = [];
|
||||||
|
|
||||||
|
images.sort(sortFunction).each(function () {
|
||||||
|
imagesWidth.push($(this).width());
|
||||||
|
});
|
||||||
|
|
||||||
|
let totalWidth = imagesWidth.reduce((a, b) => a + b, 0);
|
||||||
|
let currentPosition = pivotalPoint - (totalWidth / 2);
|
||||||
|
|
||||||
|
if (totalWidth > containerWidth) {
|
||||||
|
let overlap = (totalWidth - containerWidth) / (imagesWidth.length - 1);
|
||||||
|
imagesWidth = imagesWidth.map((width) => width - overlap);
|
||||||
|
currentPosition = 0; // Reset the initial position to 0
|
||||||
|
}
|
||||||
|
|
||||||
|
images.sort(sortFunction).each((index, current) => {
|
||||||
|
const element = $(current);
|
||||||
|
const avatar = element.data('avatar');
|
||||||
|
const layerIndex = layerIndices.indexOf(avatar);
|
||||||
|
element.css('z-index', layerIndex);
|
||||||
|
element.show();
|
||||||
|
|
||||||
|
const promise = new Promise(resolve => {
|
||||||
|
element.animate({ left: currentPosition + 'px' }, 500, () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
currentPosition += imagesWidth[index];
|
||||||
|
|
||||||
|
setLayerIndicesPromises.push(promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.allSettled(setLayerIndicesPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setLastMessageSprite(img, avatar, labels) {
|
||||||
|
const context = getContext();
|
||||||
|
const lastMessage = context.chat.slice().reverse().find(x => x.original_avatar == avatar || (x.force_avatar && x.force_avatar.includes(encodeURIComponent(avatar))));
|
||||||
|
|
||||||
|
if (lastMessage) {
|
||||||
|
const text = lastMessage.mes || '';
|
||||||
|
const sprites = spriteCache[lastMessage.name] || [];
|
||||||
|
const label = await getExpressionLabel(text);
|
||||||
|
const path = labels.includes(label) ? sprites.find(x => x.label === label)?.path : '';
|
||||||
|
|
||||||
|
if (path) {
|
||||||
|
setImage(img, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImage(img, path) {
|
||||||
|
img.attr('src', path);
|
||||||
|
img.removeClass('default');
|
||||||
|
img.off('error');
|
||||||
|
img.on('error', function () {
|
||||||
|
$(this).attr('src', '');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function onExpressionsShowDefaultInput() {
|
function onExpressionsShowDefaultInput() {
|
||||||
const value = $(this).prop('checked');
|
const value = $(this).prop('checked');
|
||||||
extension_settings.expressions.showDefault = value;
|
extension_settings.expressions.showDefault = value;
|
||||||
@ -73,6 +271,23 @@ async function moduleWorker() {
|
|||||||
spriteCache = {};
|
spriteCache = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const vnMode = isVisualNovelMode();
|
||||||
|
const vnWrapperVisible = $('#visual-novel-wrapper').is(':visible');
|
||||||
|
|
||||||
|
if (vnMode) {
|
||||||
|
$('#expression-wrapper').hide();
|
||||||
|
$('#visual-novel-wrapper').show();
|
||||||
|
} else {
|
||||||
|
$('#expression-wrapper').show();
|
||||||
|
$('#visual-novel-wrapper').hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
const vnStateChanged = vnMode !== vnWrapperVisible;
|
||||||
|
|
||||||
|
if (vnStateChanged) {
|
||||||
|
lastMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
const currentLastMessage = getLastCharacterMessage();
|
const currentLastMessage = getLastCharacterMessage();
|
||||||
|
|
||||||
// character has no expressions or it is not loaded
|
// character has no expressions or it is not loaded
|
||||||
@ -119,29 +334,19 @@ async function moduleWorker() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
inApiCall = true;
|
inApiCall = true;
|
||||||
const url = new URL(getApiUrl());
|
let expression = await getExpressionLabel(currentLastMessage.mes);
|
||||||
url.pathname = '/api/classify';
|
|
||||||
|
|
||||||
const apiResult = await doExtrasFetch(url, {
|
const name = context.groupId ? currentLastMessage.name : context.name2;
|
||||||
method: 'POST',
|
const force = !!context.groupId;
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Bypass-Tunnel-Reminder': 'bypass',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ text: currentLastMessage.mes })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (apiResult.ok) {
|
// Character won't be angry on you for swiping
|
||||||
const name = context.groupId ? currentLastMessage.name : context.name2;
|
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
|
||||||
const force = !!context.groupId;
|
expression = 'joy';
|
||||||
const data = await apiResult.json();
|
}
|
||||||
let expression = data.classification[0].label;
|
|
||||||
|
|
||||||
// Character won't be angry on you for swiping
|
|
||||||
if (currentLastMessage.mes == '...' && expressionsList.includes('joy')) {
|
|
||||||
expression = 'joy';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (vnMode) {
|
||||||
|
await updateVisualNovelMode(name, expression);
|
||||||
|
} else {
|
||||||
setExpression(name, expression, force);
|
setExpression(name, expression, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,6 +361,25 @@ async function moduleWorker() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getExpressionLabel(text) {
|
||||||
|
const url = new URL(getApiUrl());
|
||||||
|
url.pathname = '/api/classify';
|
||||||
|
|
||||||
|
const apiResult = await doExtrasFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Bypass-Tunnel-Reminder': 'bypass',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text: text }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (apiResult.ok) {
|
||||||
|
const data = await apiResult.json();
|
||||||
|
return data.classification[0].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getLastCharacterMessage() {
|
function getLastCharacterMessage() {
|
||||||
const context = getContext();
|
const context = getContext();
|
||||||
const reversedChat = context.chat.slice().reverse();
|
const reversedChat = context.chat.slice().reverse();
|
||||||
@ -450,6 +674,14 @@ async function onClickExpressionDelete(event) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
$('body').append(html);
|
$('body').append(html);
|
||||||
}
|
}
|
||||||
|
function addVisualNovelMode() {
|
||||||
|
const html = `
|
||||||
|
<div id="visual-novel-wrapper">
|
||||||
|
</div>`
|
||||||
|
const element = $(html);
|
||||||
|
element.hide();
|
||||||
|
$('body').append(element);
|
||||||
|
}
|
||||||
function addSettings() {
|
function addSettings() {
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
@ -486,12 +718,17 @@ async function onClickExpressionDelete(event) {
|
|||||||
$(document).on('click', '.expression_list_item', onClickExpressionImage);
|
$(document).on('click', '.expression_list_item', onClickExpressionImage);
|
||||||
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
|
$(document).on('click', '.expression_list_upload', onClickExpressionUpload);
|
||||||
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
|
$(document).on('click', '.expression_list_delete', onClickExpressionDelete);
|
||||||
|
$(window).on("resize", updateVisualNovelModeDebounced);
|
||||||
$('.expression_settings').hide();
|
$('.expression_settings').hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
addExpressionImage();
|
addExpressionImage();
|
||||||
|
addVisualNovelMode();
|
||||||
addSettings();
|
addSettings();
|
||||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL);
|
const updateFunction = wrapper.update.bind(wrapper);
|
||||||
|
setInterval(updateFunction, UPDATE_INTERVAL);
|
||||||
moduleWorker();
|
moduleWorker();
|
||||||
|
eventSource.on(event_types.CHAT_CHANGED, updateFunction);
|
||||||
|
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||||
})();
|
})();
|
||||||
|
@ -10,6 +10,21 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#visual-novel-wrapper {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
width: 100vw;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visual-novel-wrapper .expression-holder {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visual-novel-wrapper img.expression {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
.expression-holder {
|
.expression-holder {
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
min-height: 100px;
|
min-height: 100px;
|
||||||
|
@ -185,6 +185,8 @@ function getFirstCharacterMessage(character) {
|
|||||||
mes["name"] = character.name;
|
mes["name"] = character.name;
|
||||||
mes["is_name"] = true;
|
mes["is_name"] = true;
|
||||||
mes["send_date"] = humanizedDateTime();
|
mes["send_date"] = humanizedDateTime();
|
||||||
|
mes["original_avatar"] = character.avatar;
|
||||||
|
mes["extra"] = { "gen_id": Date.now() * Math.random() * 1000000 };
|
||||||
mes["mes"] = character.first_mes
|
mes["mes"] = character.first_mes
|
||||||
? substituteParams(character.first_mes.trim(), name1, character.name)
|
? substituteParams(character.first_mes.trim(), name1, character.name)
|
||||||
: default_ch_mes;
|
: default_ch_mes;
|
||||||
@ -1084,6 +1086,7 @@ function select_group_chats(groupId, skipAnimation) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortGroupMembers("#rm_group_add_members .group_member");
|
sortGroupMembers("#rm_group_add_members .group_member");
|
||||||
|
await eventSource.emit(event_types.GROUP_UPDATED);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user