mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Persona stats with avatar, more doc improvements
This commit is contained in:
@ -1,3 +1,7 @@
|
||||
.rm_stat_popup_header {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.rm_stats_button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -82,10 +86,10 @@
|
||||
|
||||
.rm_stat_avatar_block {
|
||||
position: absolute;
|
||||
top: calc(1.17em + 7px + 20px);
|
||||
top: calc(10px + 1.17em + 12.5px + 2* 7px);
|
||||
right: 0px;
|
||||
height: calc(8px + calc(calc(var(--mainFontSize) * 1.33333333333) * 7) + calc(12px * 3));
|
||||
width: 33.33333333333%;
|
||||
width: calc(33.33333333333% - 10px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -1359,14 +1359,16 @@ async function printCharacters(fullRefresh = false) {
|
||||
favsToHotswap();
|
||||
}
|
||||
|
||||
/** @typedef {object} Character - A character */
|
||||
/** @typedef {object} Group - A group */
|
||||
/** @typedef {{name: string, avatar: string, fav: boolean}} Character - A character */
|
||||
/** @typedef {{id: string, name: string, avatar: string, fav: boolean}} Group - A group */
|
||||
/** @typedef {import('./scripts/tags.js').Tag} Tag */
|
||||
/** @typedef {import('./scripts/personas.js').Persona} Persona - A persona */
|
||||
|
||||
/**
|
||||
* @typedef {object} Entity - Object representing a display entity
|
||||
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
|
||||
* @property {Character|Group|Tag|Persona} item - The item
|
||||
* @property {string|number} id - The id
|
||||
* @property {string} type - The type of this entity (character, group, tag)
|
||||
* @property {'character'|'group'|'tag'|'persona'} type - The type of this entity (character, group, tag, persona)
|
||||
* @property {Entity[]} [entities] - An optional list of entities relevant for this item
|
||||
* @property {number} [hidden] - An optional number representing how many hidden entities this entity contains
|
||||
*/
|
||||
@ -1395,13 +1397,23 @@ export function groupToEntity(group) {
|
||||
/**
|
||||
* Converts the given tag to its entity representation
|
||||
*
|
||||
* @param {import('./scripts/tags.js').Tag} tag - The tag
|
||||
* @param {Tag} tag - The tag
|
||||
* @returns {Entity} The entity for this tag
|
||||
*/
|
||||
export function tagToEntity(tag) {
|
||||
return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given persona based to its entity representation
|
||||
*
|
||||
* @param {Persona} persona - The avatar id for the persona
|
||||
* @returns {Entity} The entity for this persona
|
||||
*/
|
||||
export function personaToEntity(persona) {
|
||||
return { item: persona, id: persona.avatar, type: 'persona' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the full list of all entities available
|
||||
*
|
||||
@ -5619,6 +5631,17 @@ function getThumbnailUrl(type, file) {
|
||||
return `/thumbnail?type=${type}&file=${encodeURIComponent(file)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an avatar list for entities inside a given html block element
|
||||
*
|
||||
* @param {JQuery<HTMLElement>} block - The block to build the avatar list in
|
||||
* @param {Entity[]} entities - A list of entities for which to build the avatar for
|
||||
* @param {object} options - Optional options
|
||||
* @param {string} [options.templateId='inline_avatar_template'] - The template from which the inline avatars are built off
|
||||
* @param {boolean} [options.empty=true] - Whether the block will be emptied before drawing the avatars
|
||||
* @param {boolean} [options.selectable=false] - Whether the avatars should be selectable/clickable. If set, the click handler has to be implemented externally on the classes/items
|
||||
* @param {boolean} [options.highlightFavs=true] - Whether favorites should be highlighted
|
||||
*/
|
||||
function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) {
|
||||
if (empty) {
|
||||
block.empty();
|
||||
@ -5626,34 +5649,39 @@ function buildAvatarList(block, entities, { templateId = 'inline_avatar_template
|
||||
|
||||
for (const entity of entities) {
|
||||
const id = entity.id;
|
||||
const item = entity.item;
|
||||
|
||||
// Populate the template
|
||||
const avatarTemplate = $(`#${templateId} .avatar`).clone();
|
||||
|
||||
let this_avatar = default_avatar;
|
||||
if (entity.item.avatar !== undefined && entity.item.avatar != 'none') {
|
||||
this_avatar = getThumbnailUrl('avatar', entity.item.avatar);
|
||||
}
|
||||
|
||||
avatarTemplate.attr('data-type', entity.type);
|
||||
|
||||
switch (entity.type) {
|
||||
case 'character':
|
||||
avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` });
|
||||
avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name);
|
||||
avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`);
|
||||
if (highlightFavs) {
|
||||
avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true');
|
||||
avatarTemplate.find('.ch_fav').val(entity.item.fav);
|
||||
}
|
||||
|
||||
// If this is a group, we need to hack slightly. We still want to keep most of the css classes and layout, but use a group avatar instead.
|
||||
if (entity.type === 'group') {
|
||||
const grpTemplate = getGroupAvatar(entity.item);
|
||||
|
||||
const charAvatar = item.avatar && item.avatar != 'none' ? getThumbnailUrl('avatar', item.avatar) : default_avatar;
|
||||
avatarTemplate.find('img').attr('src', charAvatar).attr('alt', item.name);
|
||||
avatarTemplate.attr('title', `[Character] ${item.name}\nFile: ${item.avatar}`);
|
||||
break;
|
||||
case 'group':
|
||||
const grpTemplate = getGroupAvatar(item);
|
||||
avatarTemplate.attr({ 'gid': id, 'id': `GroupID${id}` });
|
||||
avatarTemplate.addClass(grpTemplate.attr('class'));
|
||||
avatarTemplate.empty();
|
||||
avatarTemplate.append(grpTemplate.children());
|
||||
avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
|
||||
avatarTemplate.attr('title', `[Group] ${item.name}`);
|
||||
break;
|
||||
case 'persona':
|
||||
avatarTemplate.attr({ 'pid': id, 'id': `PersonaID${id}` });
|
||||
const personaAvatar = getUserAvatar(item.avatar)
|
||||
avatarTemplate.find('img').attr('src', personaAvatar).attr('alt', item.name);
|
||||
avatarTemplate.attr('title', `[Persona] ${item.name}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (highlightFavs && 'fav' in item) {
|
||||
avatarTemplate.toggleClass('is_fav', item.fav);
|
||||
avatarTemplate.find('.ch_fav').val(item.fav ? 'true' : 'false');
|
||||
}
|
||||
if (selectable) {
|
||||
avatarTemplate.addClass('selectable');
|
||||
avatarTemplate.toggleClass('character_select', entity.type === 'character');
|
||||
|
@ -27,6 +27,31 @@ function switchPersonaGridView() {
|
||||
$('#user_avatar_block').toggleClass('gridView', state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Persona - A persona
|
||||
* @property {string} id - The id of the persona - currently same as avatar
|
||||
* @property {string} name - The name of the persona
|
||||
* @property {string} avatar - the avatar / avatar id representing the persona
|
||||
* @property {boolean} fav - Whether this persona is favorited
|
||||
* @property {{description: string, position: number}} description - The persona description, containing its text and the position where its placed
|
||||
* */
|
||||
|
||||
/**
|
||||
* Builds an object represting the given persona
|
||||
* @param {string} avatar - The avatar id
|
||||
* @returns {Persona} The persona object, wit all its data
|
||||
*/
|
||||
export function getPersona(avatar) {
|
||||
const persona = {
|
||||
id: avatar,
|
||||
name: power_user.personas[avatar],
|
||||
avatar: avatar,
|
||||
fav: false,
|
||||
description: power_user.persona_descriptions[avatar]
|
||||
};
|
||||
return persona;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an avatar file to the server
|
||||
* @param {string} url URL for the avatar file
|
||||
|
@ -1,6 +1,7 @@
|
||||
// statsHelper.js
|
||||
import { getRequestHeaders, callPopup, characters, this_chid, buildAvatarList, characterToEntity, getOneCharacter, getCharacter } from '../script.js';
|
||||
import { getRequestHeaders, callPopup, characters, this_chid, buildAvatarList, characterToEntity, getOneCharacter, getCharacter, user_avatar, personaToEntity } from '../script.js';
|
||||
import { humanizeTimespan } from './RossAscends-mods.js';
|
||||
import { getPersona } from './personas.js';
|
||||
import { registerDebugFunction } from './power-user.js';
|
||||
import { humanFileSize, humanizedDuration, parseJson, sensibleRound, smartTruncate } from './utils.js';
|
||||
|
||||
@ -106,6 +107,14 @@ function createStatBlock(name, ...values) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the stats popup for a given stats report
|
||||
* @param {string} html - The html report that should be shown in the popup
|
||||
*/
|
||||
function showStatsPopup(html) {
|
||||
callPopup(html, 'text', '', { wider: true, allowVerticalScrolling: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an HTML report of stats.
|
||||
*
|
||||
@ -113,12 +122,13 @@ function createStatBlock(name, ...values) {
|
||||
* chat time, number of user messages and character messages, word count, and swipe count.
|
||||
* The stat blocks are tailored depending on the stats type ("User" or "Character").
|
||||
*
|
||||
* @param {'User'|'Character'} statsType - The type of stats (e.g., "User", "Character")
|
||||
* @param {'user'|'character'} statsType - The type of stats (e.g., "User", "Character")
|
||||
* @param {CharacterStats} stats - The stats data
|
||||
* @returns {string} The html
|
||||
*/
|
||||
function createHtml(statsType, stats) {
|
||||
function createCharStatsHtml(statsType, stats) {
|
||||
const NOW = Date.now();
|
||||
const character = getCharacter(stats.characterKey);
|
||||
const isChar = statsType === 'character';
|
||||
|
||||
const HMTL_STAT_SPACER = '<div class="rm_stat_spacer"></div>';
|
||||
const VAL_RIGHT_SPACING = { value: null, classes: ['rm_stat_right_spacing'] };
|
||||
@ -128,11 +138,21 @@ function createHtml(statsType, stats) {
|
||||
|
||||
// some pre calculations
|
||||
const mostUsedModel = findHighestModel(stats.genModels);
|
||||
const charactersCount = !isChar ? (new Set(stats.chatsStats.map(x => x.charName))).size : null;
|
||||
|
||||
let subHeader = (() => {
|
||||
switch (statsType) {
|
||||
case 'character': return `Overall character stats based on all chats for ${stats.charName}`;
|
||||
case 'user': return `Global stats based on all chats of ${charactersCount} characters`;
|
||||
default: return '';
|
||||
};
|
||||
})();
|
||||
|
||||
// Create popup HTML with stats
|
||||
let html = `<h3>${statsType} Stats - ${stats.charName}</h3>`;
|
||||
let html = `<h3 class="rm_stat_popup_header">${isChar ? 'Character' : 'User'} Stats - ${isChar ? stats.charName : stats.userName}</h3>`;
|
||||
html += `<small>${subHeader}</small>`;
|
||||
html += HMTL_STAT_SPACER;
|
||||
html += createStatBlock({ value: 'Character Overview', isHeader: true });
|
||||
html += createStatBlock({ value: isChar ? 'Character Overview' : 'Overview', isHeader: true });
|
||||
html += createStatBlock({ value: 'Chats', info: `The number of existing chats with ${stats.charName}.\nFor the sake of statistics, Branches count as chats and all their messages will be included.` },
|
||||
stats.chats, VAL_RIGHT_SPACING);
|
||||
html += createStatBlock({ value: 'File Size', info: 'The chat file sizes on disk calculated and summed.\nThis value might not represent the exact same value your operating system uses.' },
|
||||
@ -222,17 +242,25 @@ function createHtml(statsType, stats) {
|
||||
html += HMTL_STAT_SPACER;
|
||||
|
||||
// Hijack avatar list function to draw the user avatar
|
||||
if (character) {
|
||||
const placeHolder = $('<div class="rm_stat_avatar_block"></div>');
|
||||
let entity = null;
|
||||
switch (statsType) {
|
||||
case 'character':
|
||||
const character = getCharacter(stats.characterKey);
|
||||
const cid = characters.indexOf(x => x === character);
|
||||
const entity = characterToEntity(character, cid);
|
||||
entity = characterToEntity(character, cid);
|
||||
break;
|
||||
case 'user':
|
||||
const persona = getPersona(user_avatar);
|
||||
entity = personaToEntity(persona);
|
||||
break;
|
||||
}
|
||||
if (entity) {
|
||||
const placeHolder = $('<div class="rm_stat_avatar_block"></div>');
|
||||
buildAvatarList(placeHolder, [entity]);
|
||||
html = placeHolder.prop('outerHTML') + html;
|
||||
}
|
||||
|
||||
callPopup(html, 'text', '', { wider: true, allowVerticalScrolling: true });
|
||||
|
||||
return;
|
||||
return html;
|
||||
|
||||
/** @param {AggregateStat} agg1 @param {AggregateStat} agg2 @param {AggBuildOptions} options @returns {StatField[]} */
|
||||
function buildBarsFromAggregates(agg1, agg2, options = DEFAULT_AGG_BUILD_OPTIONS) {
|
||||
@ -285,7 +313,8 @@ async function showUserStatsPopup() {
|
||||
const globalStats = await getGlobalStats();
|
||||
|
||||
// Create HTML with stats
|
||||
createHtml('User', globalStats);
|
||||
const html = createCharStatsHtml('user', globalStats);
|
||||
showStatsPopup(html);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -302,7 +331,8 @@ async function showCharacterStatsPopup(characterKey) {
|
||||
}
|
||||
|
||||
// Create HTML with stats
|
||||
createHtml('Character', charStats);
|
||||
const html = createCharStatsHtml('character', charStats);
|
||||
showStatsPopup(html);
|
||||
}
|
||||
|
||||
|
||||
|
@ -119,6 +119,8 @@ const TAG_FOLDER_DEFAULT_TYPE = 'NONE';
|
||||
* @property {string} [class] - An optional css class added to the control representing this tag when printed. Used for custom tags in the filters.
|
||||
* @property {string} [icon] - An optional css class of an icon representing this tag when printed. This will replace the tag name with the icon. Used for custom tags in the filters.
|
||||
* @property {string} [title] - An optional title for the tooltip of this tag. If there is no tooltip specified, and "icon" is chosen, the tooltip will be the "name" property.
|
||||
*
|
||||
* @property {string} [avatar=undefined] - For compatibility with other items, tags have an 'avatar' property, which is not used or filled currently
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -17,7 +17,7 @@ const MAX_TIMESTAMP = new Date('9999-12-31T23:59:59.999Z').getTime();
|
||||
const MIN_DATE = new Date(MIN_TIMESTAMP);
|
||||
const MAX_DATE = new Date(MAX_TIMESTAMP);
|
||||
const STATS_FILE = 'stats.json';
|
||||
const CURRENT_STATS_VERSION = '1.2';
|
||||
const CURRENT_STATS_VERSION = '1.3';
|
||||
|
||||
/** @type {Map<string, UserStatsCollection>} The stats collections for each user, accessable via their key - gets set/built on init */
|
||||
const STATS = new Map();
|
||||
@ -415,9 +415,8 @@ function triggerChatUpdate(userHandle, characterKey, chatName) {
|
||||
updateCharStatsWithChat(userStats.stats[characterKey], chatStats);
|
||||
updateCharStatsWithChat(userStats.global, chatStats);
|
||||
|
||||
// Update name (if it might have changed)
|
||||
userStats.stats[characterKey].charName = chatStats.charName;
|
||||
userStats.stats[characterKey].userName = chatStats.userName;
|
||||
// For global chats, we always overwrite the char name with a default one
|
||||
userStats.global.charName = 'Character';
|
||||
|
||||
userStats._calculated = now();
|
||||
return chatStats;
|
||||
@ -470,8 +469,12 @@ function updateCharStatsWithChat(stats, chatStats) {
|
||||
|
||||
Object.entries(chatStats.genModels).forEach(([model, data]) => addModelUsage(stats.genModels, model, data.tokens, data.count));
|
||||
|
||||
// Update name (if it might have changed)
|
||||
stats.charName = chatStats.charName || stats.charName;
|
||||
stats.userName = chatStats.userName || stats.userName;
|
||||
|
||||
stats._calculated = now();
|
||||
console.debug(`Successfully updated ${stats.charName}'s stats with chat ${chatStats.chatName}`);
|
||||
console.debug(`Successfully updated ${stats.charName}'s stats with chat '${chatStats.chatName}'`);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -685,7 +688,7 @@ function countWordsInString(str) {
|
||||
function newCharacterStats(characterKey = '', charName = '') {
|
||||
return {
|
||||
charName: charName,
|
||||
userName: 'User',
|
||||
userName: '',
|
||||
characterKey: characterKey,
|
||||
chats: 0,
|
||||
chatSize: 0,
|
||||
|
Reference in New Issue
Block a user