Persona stats with avatar, more doc improvements

This commit is contained in:
Wolfsblvt
2024-04-25 03:53:06 +02:00
parent b9f31d5066
commit 3adb955a14
6 changed files with 140 additions and 48 deletions

View File

@ -1,3 +1,7 @@
.rm_stat_popup_header {
margin-bottom: 0px;
}
.rm_stats_button { .rm_stats_button {
cursor: pointer; cursor: pointer;
} }
@ -82,10 +86,10 @@
.rm_stat_avatar_block { .rm_stat_avatar_block {
position: absolute; position: absolute;
top: calc(1.17em + 7px + 20px); top: calc(10px + 1.17em + 12.5px + 2* 7px);
right: 0px; right: 0px;
height: calc(8px + calc(calc(var(--mainFontSize) * 1.33333333333) * 7) + calc(12px * 3)); height: calc(8px + calc(calc(var(--mainFontSize) * 1.33333333333) * 7) + calc(12px * 3));
width: 33.33333333333%; width: calc(33.33333333333% - 10px);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;

View File

@ -1359,14 +1359,16 @@ async function printCharacters(fullRefresh = false) {
favsToHotswap(); favsToHotswap();
} }
/** @typedef {object} Character - A character */ /** @typedef {{name: string, avatar: string, fav: boolean}} Character - A character */
/** @typedef {object} Group - A group */ /** @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 * @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|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 {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 * @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 * 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 * @returns {Entity} The entity for this tag
*/ */
export function tagToEntity(tag) { export function tagToEntity(tag) {
return { item: structuredClone(tag), id: tag.id, type: 'tag', entities: [] }; 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 * Builds the full list of all entities available
* *
@ -5619,6 +5631,17 @@ function getThumbnailUrl(type, file) {
return `/thumbnail?type=${type}&file=${encodeURIComponent(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 } = {}) { function buildAvatarList(block, entities, { templateId = 'inline_avatar_template', empty = true, selectable = false, highlightFavs = true } = {}) {
if (empty) { if (empty) {
block.empty(); block.empty();
@ -5626,34 +5649,39 @@ function buildAvatarList(block, entities, { templateId = 'inline_avatar_template
for (const entity of entities) { for (const entity of entities) {
const id = entity.id; const id = entity.id;
const item = entity.item;
// Populate the template // Populate the template
const avatarTemplate = $(`#${templateId} .avatar`).clone(); 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); avatarTemplate.attr('data-type', entity.type);
avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` });
avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); switch (entity.type) {
avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); case 'character':
if (highlightFavs) { avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` });
avatarTemplate.toggleClass('is_fav', entity.item.fav || entity.item.fav == 'true'); const charAvatar = item.avatar && item.avatar != 'none' ? getThumbnailUrl('avatar', item.avatar) : default_avatar;
avatarTemplate.find('.ch_fav').val(entity.item.fav); 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] ${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 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 (highlightFavs && 'fav' in item) {
if (entity.type === 'group') { avatarTemplate.toggleClass('is_fav', item.fav);
const grpTemplate = getGroupAvatar(entity.item); avatarTemplate.find('.ch_fav').val(item.fav ? 'true' : 'false');
avatarTemplate.addClass(grpTemplate.attr('class'));
avatarTemplate.empty();
avatarTemplate.append(grpTemplate.children());
avatarTemplate.attr('title', `[Group] ${entity.item.name}`);
} }
if (selectable) { if (selectable) {
avatarTemplate.addClass('selectable'); avatarTemplate.addClass('selectable');
avatarTemplate.toggleClass('character_select', entity.type === 'character'); avatarTemplate.toggleClass('character_select', entity.type === 'character');

View File

@ -27,6 +27,31 @@ function switchPersonaGridView() {
$('#user_avatar_block').toggleClass('gridView', state); $('#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 * Uploads an avatar file to the server
* @param {string} url URL for the avatar file * @param {string} url URL for the avatar file

View File

@ -1,6 +1,7 @@
// statsHelper.js // 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 { humanizeTimespan } from './RossAscends-mods.js';
import { getPersona } from './personas.js';
import { registerDebugFunction } from './power-user.js'; import { registerDebugFunction } from './power-user.js';
import { humanFileSize, humanizedDuration, parseJson, sensibleRound, smartTruncate } from './utils.js'; import { humanFileSize, humanizedDuration, parseJson, sensibleRound, smartTruncate } from './utils.js';
@ -106,6 +107,14 @@ function createStatBlock(name, ...values) {
</div>`; </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. * 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. * 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"). * 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 * @param {CharacterStats} stats - The stats data
* @returns {string} The html
*/ */
function createHtml(statsType, stats) { function createCharStatsHtml(statsType, stats) {
const NOW = Date.now(); const NOW = Date.now();
const character = getCharacter(stats.characterKey); const isChar = statsType === 'character';
const HMTL_STAT_SPACER = '<div class="rm_stat_spacer"></div>'; const HMTL_STAT_SPACER = '<div class="rm_stat_spacer"></div>';
const VAL_RIGHT_SPACING = { value: null, classes: ['rm_stat_right_spacing'] }; const VAL_RIGHT_SPACING = { value: null, classes: ['rm_stat_right_spacing'] };
@ -128,11 +138,21 @@ function createHtml(statsType, stats) {
// some pre calculations // some pre calculations
const mostUsedModel = findHighestModel(stats.genModels); 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 // 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 += 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.` }, 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); 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.' }, 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; html += HMTL_STAT_SPACER;
// Hijack avatar list function to draw the user avatar // Hijack avatar list function to draw the user avatar
if (character) { let entity = null;
switch (statsType) {
case 'character':
const character = getCharacter(stats.characterKey);
const cid = characters.indexOf(x => x === character);
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>'); const placeHolder = $('<div class="rm_stat_avatar_block"></div>');
const cid = characters.indexOf(x => x === character);
const entity = characterToEntity(character, cid);
buildAvatarList(placeHolder, [entity]); buildAvatarList(placeHolder, [entity]);
html = placeHolder.prop('outerHTML') + html; html = placeHolder.prop('outerHTML') + html;
} }
callPopup(html, 'text', '', { wider: true, allowVerticalScrolling: true }); return html;
return;
/** @param {AggregateStat} agg1 @param {AggregateStat} agg2 @param {AggBuildOptions} options @returns {StatField[]} */ /** @param {AggregateStat} agg1 @param {AggregateStat} agg2 @param {AggBuildOptions} options @returns {StatField[]} */
function buildBarsFromAggregates(agg1, agg2, options = DEFAULT_AGG_BUILD_OPTIONS) { function buildBarsFromAggregates(agg1, agg2, options = DEFAULT_AGG_BUILD_OPTIONS) {
@ -285,7 +313,8 @@ async function showUserStatsPopup() {
const globalStats = await getGlobalStats(); const globalStats = await getGlobalStats();
// Create HTML with stats // 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 // Create HTML with stats
createHtml('Character', charStats); const html = createCharStatsHtml('character', charStats);
showStatsPopup(html);
} }

View File

@ -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} [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} [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} [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
*/ */
/** /**

View File

@ -17,7 +17,7 @@ const MAX_TIMESTAMP = new Date('9999-12-31T23:59:59.999Z').getTime();
const MIN_DATE = new Date(MIN_TIMESTAMP); const MIN_DATE = new Date(MIN_TIMESTAMP);
const MAX_DATE = new Date(MAX_TIMESTAMP); const MAX_DATE = new Date(MAX_TIMESTAMP);
const STATS_FILE = 'stats.json'; 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 */ /** @type {Map<string, UserStatsCollection>} The stats collections for each user, accessable via their key - gets set/built on init */
const STATS = new Map(); const STATS = new Map();
@ -415,9 +415,8 @@ function triggerChatUpdate(userHandle, characterKey, chatName) {
updateCharStatsWithChat(userStats.stats[characterKey], chatStats); updateCharStatsWithChat(userStats.stats[characterKey], chatStats);
updateCharStatsWithChat(userStats.global, chatStats); updateCharStatsWithChat(userStats.global, chatStats);
// Update name (if it might have changed) // For global chats, we always overwrite the char name with a default one
userStats.stats[characterKey].charName = chatStats.charName; userStats.global.charName = 'Character';
userStats.stats[characterKey].userName = chatStats.userName;
userStats._calculated = now(); userStats._calculated = now();
return chatStats; 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)); 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(); 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; return true;
} }
@ -685,7 +688,7 @@ function countWordsInString(str) {
function newCharacterStats(characterKey = '', charName = '') { function newCharacterStats(characterKey = '', charName = '') {
return { return {
charName: charName, charName: charName,
userName: 'User', userName: '',
characterKey: characterKey, characterKey: characterKey,
chats: 0, chats: 0,
chatSize: 0, chatSize: 0,