Temp commit

- Fixed "old" popup resizing and scroll bars (now actually respecting the chosen setting)
This commit is contained in:
Wolfsblvt
2024-04-11 20:43:20 +02:00
parent 63117653bb
commit 9cef0d8346
10 changed files with 1215 additions and 498 deletions

View File

@ -1,22 +1,68 @@
// statsHelper.js
import { getRequestHeaders, callPopup, characters, this_chid } from '../script.js';
import { getRequestHeaders, callPopup, characters, this_chid, buildAvatarList, characterToEntity } from '../script.js';
import { humanizeGenTime } from './RossAscends-mods.js';
import { registerDebugFunction } from './power-user.js';
import { timestampToMoment } from './utils.js';
let charStats = {};
const MIN_TIMESTAMP = 0;
const MAX_TIMESTAMP = new Date('9999-12-31T23:59:59.999Z').getTime();
const CURRENT_STATS_VERSION = '1.0';
/** @typedef {import('../script.js').Character} Character */
/**
* Creates an HTML stat block.
*
* @param {string} statName - The name of the stat to be displayed.
* @param {number|string} statValue - The value of the stat to be displayed.
* @returns {string} - An HTML string representing the stat block.
* @typedef {object} AggregateStat
* @property {number[]} list - The values stored, so they can be recalculated
* @property {number} total - Total / Sum
* @property {number} min - Minimum value
* @property {number} max - Maximum value
* @property {number} avg - Average value
*/
function createStatBlock(statName, statValue) {
return `<div class="rm_stat_block">
<div class="rm_stat_name">${statName}:</div>
<div class="rm_stat_value">${statValue}</div>
</div>`;
/**
* @typedef {object} Stats - Stats for a chat
* @property {number} chat_size - The size of the actual chat file
* @property {number} date_first_chat - Timestamp of the first chat message (made by user)
* @property {number} date_last_chat - Timestamp of the last chat message in this chat (made by anyone)
* @property {number} total_gen_time - Total generation time in milliseconds
* @property {number} total_msg_count - The total messages of user and non-user, not including swipes
* @property {number} total_swipe_count - The number of swipes in the whole chat
* @property {number} avg_gen_time - Average generation tme in milliseconds
* @property {number} avg_swipe_count - Average swipes per non-user message
*
* @property {number} avg_chat_msg_count - Average messages per chat
* @property {number} avg_chat_duration - Average duration of a chat (from first till last message)
*
* @property {AggregateStat} msg -
* @property {AggregateStat} user_msg -
* @property {AggregateStat} non_user_msg -
* @property {AggregateStat} words -
* @property {AggregateStat} user_words -
* @property {AggregateStat} non_user_words -
*
*/
/** @type {StatsCollection} The collection of all stats, accessable via their key */
let charStats = { timestamp: 0, version: CURRENT_STATS_VERSION, stats: {} };
/**
* Creates an empty new stats object with default values
* @returns {Stats} The stats
*/
function createEmptyStats() {
return {
total_gen_time: 0,
user_word_count: 0,
non_user_word_count: 0,
user_msg_count: 0,
non_user_msg_count: 0,
total_swipe_count: 0,
chat_size: 0,
date_first_chat: MAX_TIMESTAMP,
date_last_chat: MIN_TIMESTAMP,
};
}
/**
@ -32,51 +78,91 @@ function verifyStatValue(stat) {
/**
* Calculates total stats from character statistics.
*
* @returns {Object} - Object containing total statistics.
* @returns {Stats} - Object containing total statistics.
*/
function calculateTotalStats() {
let totalStats = {
total_gen_time: 0,
user_msg_count: 0,
non_user_msg_count: 0,
user_word_count: 0,
non_user_word_count: 0,
total_swipe_count: 0,
date_last_chat: 0,
date_first_chat: new Date('9999-12-31T23:59:59.999Z').getTime(),
};
let totalStats = createEmptyStats();
for (let stats of Object.values(charStats)) {
for (let stats of Object.values(charStats.stats)) {
totalStats.total_gen_time += verifyStatValue(stats.total_gen_time);
totalStats.user_msg_count += verifyStatValue(stats.user_msg_count);
totalStats.non_user_msg_count += verifyStatValue(
stats.non_user_msg_count,
);
totalStats.non_user_msg_count += verifyStatValue(stats.non_user_msg_count);
totalStats.user_word_count += verifyStatValue(stats.user_word_count);
totalStats.non_user_word_count += verifyStatValue(
stats.non_user_word_count,
);
totalStats.total_swipe_count += verifyStatValue(
stats.total_swipe_count,
);
if (verifyStatValue(stats.date_last_chat) != 0) {
totalStats.date_last_chat = Math.max(
totalStats.date_last_chat,
stats.date_last_chat,
);
}
if (verifyStatValue(stats.date_first_chat) != 0) {
totalStats.date_first_chat = Math.min(
totalStats.date_first_chat,
stats.date_first_chat,
);
}
totalStats.non_user_word_count += verifyStatValue(stats.non_user_word_count);
totalStats.total_swipe_count += verifyStatValue(stats.total_swipe_count);
totalStats.date_last_chat = Math.max(totalStats.date_last_chat, verifyStatValue(stats.date_last_chat) || MIN_TIMESTAMP);
totalStats.date_first_chat = Math.min(totalStats.date_first_chat, verifyStatValue(stats.date_first_chat) || MAX_TIMESTAMP);
}
return totalStats;
}
/**
* Build a humanized string for a duration
* @param {number} start - Start time (in milliseconds)
* @param {number|null} end - End time (in milliseconds), if null will be replaced with Date.now()
* @param {object} param2 - Optional parameters
* @param {string} [param2.fallback='Never'] - Fallback value no duration can be calculated
* @param {function(string): string} [param2.wrapper=null] - Optional function to wrap/format the resulting humanized duration
* @returns {string} Humanized duration string
*/
function humanizedDuration(start, end = null, { fallback = 'Never', wrapper = null } = {}) {
end = end ?? Date.now();
if (!start || start > end) {
return fallback;
}
// @ts-ignore
const humanized = moment.duration(end - start).humanize();
return wrapper ? wrapper(humanized) : humanized;
}
/**
* @typedef {object} StatField A stat block value to print
* @property {any} value - The value to print
* @property {boolean} [isHeader=false] - Flag indicating whether this is a header
* @property {string|null} [info=null] - Optional text that will be shown as an info icon
* @property {string|null} [title=null] - Optional title for the value - if null and info is set, info will be used as title too
* @property {string[]|null} [classes=null] - Optional list of classes for the stat field
*/
/** @param {StatField|any} x @returns {StatField} gets the stat field object for any value */
function field(x) { return (typeof x === 'object' && x !== null && Object.hasOwn(x, 'value')) ? x : { value: x }; }
/**
* Creates an HTML stat block
*
* @param {StatField|any} name - The name content of the stat to be displayed
* @param {StatField[]|any[]} values - Value or values to be listed for the stat block
* @returns {string} - An HTML string representing the stat block
*/
function createStatBlock(name, ...values) {
/** @param {StatField} stat @returns {string} */
function buildField(stat) {
const classes = ['rm_stat_field', stat.isHeader ? 'rm_stat_header' : '', ...(stat.classes ?? [])].filter(x => x?.length);
return `<div class="${classes.join(' ')}" ${stat.title || stat.info ? `title="${stat.title ?? stat.info}"` : ''}>
${stat.value === null || stat.value === '' ? '&zwnj;' : stat.value}
${stat.info ? `<small><div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]${stat.info}" title="${stat.info}"></div></small>` : ''}
</div>`;
}
const statName = field(name);
const statValues = values.flat(Infinity).map(field);
const isDataRow = !statName.isHeader && !statValues.some(x => x.isHeader);
const isRightSpacing = statValues.slice(-1)[0]?.classes?.includes('rm_stat_right_spacing');
// Hack right spacing, which is added via a value just having the class
if (isRightSpacing) {
statValues.pop();
}
const classes = ['rm_stat_block', isDataRow ? 'rm_stat_block_data_row' : null, isRightSpacing ? 'rm_stat_right_spacing' : null].filter(x => x?.length);
return `<div class="${classes.join(' ')}">
<div class="rm_stat_name">${buildField(statName)}</div>
<div class="rm_stat_values">${statValues.map(x => buildField(x)).join('')}</div>
</div>`;
}
/**
* Generates an HTML report of stats.
*
@ -84,45 +170,124 @@ function calculateTotalStats() {
* 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 {string} statsType - The type of stats (e.g., "User", "Character").
* @param {Object} stats - The stats data. Expected keys in this object include:
* total_gen_time - total generation time
* date_first_chat - timestamp of the first chat
* date_last_chat - timestamp of the most recent chat
* user_msg_count - count of user messages
* non_user_msg_count - count of non-user messages
* user_word_count - count of words used by the user
* non_user_word_count - count of words used by the non-user
* total_swipe_count - total swipe count
* @param {'User'|'Character'} statsType - The type of stats (e.g., "User", "Character")
* @param {number|null} characterId - Character id for these stats, null if global
* @param {Stats} stats - The stats data
*/
function createHtml(statsType, stats) {
// Get time string
let timeStirng = humanizeGenTime(stats.total_gen_time);
let chatAge = 'Never';
if (stats.date_first_chat < Date.now()) {
chatAge = moment
.duration(stats.date_last_chat - stats.date_first_chat)
.humanize();
function createHtml(statsType, characterId, stats) {
const NOW = Date.now();
const name = characters[characterId]?.name || 'User';
const HMTL_STAT_SPACER = '<div class="rm_stat_spacer"></div>';
/** @param {number} charVal @param {number} userVal @returns {string} */
function buildBar(userVal, charVal) {
const percentUser = (userVal / (userVal + charVal)) * 100;
const percentChar = 100 - percentUser;
return `<div class="rm_stat_bar">
<div style="width: ${percentUser}%" title="User: ${userVal} (${percentUser.toFixed(1)}%)" class="rm_stat_bar_user"></div>
<div style="width: ${percentChar}%" title="${name}: ${charVal} (${percentChar.toFixed(1)}%)" class="rm_stat_bar_char"></div>
</div>`;
}
/** @param {any[]} values @returns {StatField[]} */
function buildBarDesc(...values) {
return values.flat(Infinity).map(field).map((x, i) => i % 2 == 0 ? { classes: [...(x.classes ?? []), 'rm_stat_field_lefty'], ...x } : x);
}
// Create popup HTML with stats
let html = `<h3>${statsType} Stats</h3>`;
if (statsType === 'User') {
html += createStatBlock('Chatting Since', `${chatAge} ago`);
} else {
html += createStatBlock('First Interaction', `${chatAge} ago`);
}
html += createStatBlock('Chat Time', timeStirng);
html += createStatBlock('User Messages', stats.user_msg_count);
html += createStatBlock(
'Character Messages',
stats.non_user_msg_count - stats.total_swipe_count,
let html = `<h3>${statsType} Stats - ${name}</h3>`;
html += HMTL_STAT_SPACER;
html += createStatBlock({ value: 'Character Overview', isHeader: true });
html += createStatBlock('Chats', { value: 34 }, { value: null, classes: ['rm_stat_right_spacing'] });
html += createStatBlock({ value: 'Chats Size', info: 'The chat file sizes calculated and summed.\nThis value is only estimated, and will be refreshed sporadically.' }, { value: `~${'3.54 mb'}` }, { value: null, classes: ['rm_stat_right_spacing'] });
html += createStatBlock('Most Used Model', { value: 'Noromaid' }, { value: null, classes: ['rm_stat_right_spacing'] });
html += HMTL_STAT_SPACER;
html += createStatBlock('',
{ value: 'First', isHeader: true, info: `The data corresponding to the first chat with ${name}` },
{ value: 'Last', isHeader: true, info: `The data corresponding to the last chat with ${name}` },
{ value: null, classes: ['rm_stat_right_spacing'] },
);
html += createStatBlock({ value: 'New Chat', info: 'The first/last time when a new chat was started' },
{ value: humanizedDuration(stats.date_first_chat, NOW, { wrapper: x => `${x} ago` }), title: timestampToMoment(stats.date_first_chat).format('LL LT') },
{ value: humanizedDuration(stats.date_last_chat, NOW, { wrapper: x => `${x} ago` }), title: timestampToMoment(stats.date_last_chat).format('LL LT') },
{ value: null, classes: ['rm_stat_right_spacing'] },
);
html += createStatBlock({ value: 'Chat Ended', info: 'The first/last time when the last message was send to a chat' },
{ value: humanizedDuration(stats.date_first_chat, NOW, { wrapper: x => `${x} ago` }), title: timestampToMoment(stats.date_first_chat).format('LL LT') },
{ value: humanizedDuration(stats.date_last_chat, NOW, { wrapper: x => `${x} ago` }), title: timestampToMoment(stats.date_last_chat).format('LL LT') },
{ value: null, classes: ['rm_stat_right_spacing'] },
);
html += createStatBlock('User Words', stats.user_word_count);
html += createStatBlock('Character Words', stats.non_user_word_count);
html += createStatBlock('Swipes', stats.total_swipe_count);
callPopup(html, 'text');
html += HMTL_STAT_SPACER;
html += HMTL_STAT_SPACER;
html += createStatBlock({ value: 'Aggregated Stats', isHeader: true, info: 'Values per chat, aggregated over all chats' });
html += createStatBlock(null,
{ value: 'Total', isHeader: true, info: 'Total summed value over all chats' },
{ value: 'Min', isHeader: true, info: 'Minium value for any chat' },
{ value: 'Avg', isHeader: true, info: 'Average value over all chats' },
{ value: 'Max', isHeader: true, info: 'Maximum value for any chat' }
);
html += createStatBlock({ value: 'Chatting Time', info: 'Total chatting time over all chats, and min/avg/max chatting time per chat' },
{ value: humanizeGenTime(114387009, true) }, { value: humanizeGenTime(7203, true) }, { value: humanizeGenTime(159017, true) }, { value: humanizeGenTime(7884930, true) });
html += createStatBlock({ value: 'Generation Time', info: 'Total generation time over all chats, and min/avg/max generation time per chat' },
humanizeGenTime(34680309, true), humanizeGenTime(4566, true), humanizeGenTime(23523, true), humanizeGenTime(286230, true));
html += createStatBlock('Generated Tokens', 2355, 43, 180, 2400);
html += HMTL_STAT_SPACER;
html += createStatBlock('Swiping Time', humanizeGenTime(34680309, true), humanizeGenTime(4566, true), humanizeGenTime(23523, true), humanizeGenTime(286230, true));
html += createStatBlock({ value: 'Swipes', info: 'Total swipes over all chats, and min/avg/max swipes per chat' },
{ value: 256 }, { value: 1 }, { value: 4 }, { value: 25 });
html += HMTL_STAT_SPACER;
html += createStatBlock('User Response Time', humanizeGenTime(34680309, true), humanizeGenTime(4566, true), humanizeGenTime(23523, true), humanizeGenTime(286230, true));
html += HMTL_STAT_SPACER;
html += createStatBlock({ value: 'Messages', info: 'Total messages over all chats (excluding swipes), and min/avg/max messages per chat' },
512, 2, 12, 100);
html += createStatBlock('System Messages', 47, 0, 4, 85);
html += createStatBlock({ value: 'Messages (User / Char)', classes: ['rm_stat_field_smaller'] }, buildBarDesc(145, 359, 2, 27, 8, 54, 66, 100));
html += createStatBlock({ value: '', info: '' },
buildBar(145, 359), buildBar(2, 27), buildBar(8, 54), buildBar(66, 100));
html += HMTL_STAT_SPACER;
html += createStatBlock({ value: 'Words', info: 'Total words over all chats, and min/avg/max words per chat' },
{ value: 5124 }, { value: 26 }, { value: 122 }, { value: 1008 });
html += createStatBlock({ value: 'Words (User / Char)', classes: ['rm_stat_field_smaller'] }, buildBarDesc(1451, 3594, 22, 279, 84, 625, 762, 2505));
html += createStatBlock({ value: '', info: '' },
buildBar(1451, 3594), buildBar(22, 279), buildBar(84, 625), buildBar(762, 2505));
html += HMTL_STAT_SPACER;
html += HMTL_STAT_SPACER;
html += createStatBlock({ value: 'Per Message Stats', isHeader: true, info: 'Values per message, aggregated over all chats' });
html += createStatBlock('',
null,
{ value: 'Min', isHeader: true, info: 'Minium ' },
{ value: 'Avg', isHeader: true },
{ value: 'Max', isHeader: true }
);
html += createStatBlock({ value: 'Generation Time', info: 'min/avg/max generation time per message' },
null, { value: humanizeGenTime(4566, true) }, { value: humanizeGenTime(23523, true) }, { value: humanizeGenTime(286230, true) });
html += createStatBlock('Generated Tokens', null, 43, 180, 2400);
html += HMTL_STAT_SPACER;
html += createStatBlock('Swiping Time', null, humanizeGenTime(1456, true), humanizeGenTime(2523, true), humanizeGenTime(28230, true));
html += createStatBlock({ value: 'Swipes', info: 'min/avg/max swipes per <b>non-user</b> message' },
null, { value: 1 }, { value: 4 }, { value: 25 });
html += HMTL_STAT_SPACER;
html += createStatBlock('User Response Time', null, humanizeGenTime(0, true), humanizeGenTime(233, true), humanizeGenTime(13630, true));
html += HMTL_STAT_SPACER;
html += createStatBlock({ value: 'Words', info: 'min/avg/max words per message' },
null, { value: 4 }, { value: 145 }, { value: 431 });
html += createStatBlock({ value: 'Words (User / Char)', classes: ['rm_stat_field_smaller'] }, buildBarDesc(null, null, 22, 279, 84, 625, 762, 2505));
html += createStatBlock({ value: '', info: '' },
null, buildBar(22, 279), buildBar(84, 625), buildBar(762, 2505));
html += HMTL_STAT_SPACER;
html += HMTL_STAT_SPACER;
// Hijack avatar list function to draw the user avatar
if (characters[characterId]) {
const placeHolder = $('<div class="rm_stat_avatar_block"></div>');
const entity = characterToEntity(characters[characterId], characterId);
buildAvatarList(placeHolder, [entity]);
html = placeHolder.prop('outerHTML') + html;
}
callPopup(html, 'text', '', { wider: true, allowVerticalScrolling: true });
}
/**
@ -136,36 +301,28 @@ async function userStatsHandler() {
let totalStats = calculateTotalStats();
// Create HTML with stats
createHtml('User', totalStats);
createHtml('User', null, totalStats);
}
/**
* Handles the character stats by getting them from the server and generating the HTML report.
*
* @param {Object} characters - Object containing character data.
* @param {{[characterKey: string]: Character}} characters - Object containing character data.
* @param {string} this_chid - The character id.
*/
async function characterStatsHandler(characters, this_chid) {
// Get stats from server
await getStats();
// Get character stats
let myStats = charStats[characters[this_chid].avatar];
let myStats = charStats.stats[characters[this_chid].avatar];
if (myStats === undefined) {
myStats = {
total_gen_time: 0,
user_msg_count: 0,
non_user_msg_count: 0,
user_word_count: 0,
non_user_word_count: countWords(characters[this_chid].first_mes),
total_swipe_count: 0,
date_last_chat: 0,
date_first_chat: new Date('9999-12-31T23:59:59.999Z').getTime(),
};
charStats[characters[this_chid].avatar] = myStats;
myStats = createEmptyStats();
myStats.non_user_word_count = countWords(characters[this_chid].first_mes);
charStats.stats[characters[this_chid].avatar] = myStats;
updateStats();
}
// Create HTML with stats
createHtml('Character', myStats);
createHtml('Character', this_chid, myStats);
}
/**
@ -261,7 +418,7 @@ function countWords(str) {
*
* @param {Object} line - Object containing message data.
* @param {string} type - The type of the message processing (e.g., 'append', 'continue', 'appendFinal', 'swipe').
* @param {Object} characters - Object containing character data.
* @param {Character[]} characters - Object containing character data.
* @param {string} this_chid - The character id.
* @param {string} oldMesssage - The old message that's being processed.
*/
@ -271,52 +428,43 @@ async function statMesProcess(line, type, characters, this_chid, oldMesssage) {
}
await getStats();
let stat = charStats[characters[this_chid].avatar];
let stats = charStats.stats[characters[this_chid].avatar] ?? createEmptyStats();
if (!stat) {
stat = {
total_gen_time: 0,
user_word_count: 0,
non_user_msg_count: 0,
user_msg_count: 0,
total_swipe_count: 0,
date_first_chat: Date.now(),
date_last_chat: Date.now(),
};
}
stat.total_gen_time += calculateGenTime(
line.gen_started,
line.gen_finished,
);
stats.total_gen_time += calculateGenTime(line.gen_started, line.gen_finished);
if (line.is_user) {
if (type != 'append' && type != 'continue' && type != 'appendFinal') {
stat.user_msg_count++;
stat.user_word_count += countWords(line.mes);
stats.user_msg_count++;
stats.user_word_count += countWords(line.mes);
} else {
let oldLen = oldMesssage.split(' ').length;
stat.user_word_count += countWords(line.mes) - oldLen;
stats.user_word_count += countWords(line.mes) - oldLen;
}
} else {
// if continue, don't add a message, get the last message and subtract it from the word count of
// the new message
if (type != 'append' && type != 'continue' && type != 'appendFinal') {
stat.non_user_msg_count++;
stat.non_user_word_count += countWords(line.mes);
stats.non_user_msg_count++;
stats.non_user_word_count += countWords(line.mes);
} else {
let oldLen = oldMesssage.split(' ').length;
stat.non_user_word_count += countWords(line.mes) - oldLen;
stats.non_user_word_count += countWords(line.mes) - oldLen;
}
}
if (type === 'swipe') {
stat.total_swipe_count++;
stats.total_swipe_count++;
}
// If this is the first user message, set the first chat time
if (line.is_user) {
//get min between firstChatTime and timestampToMoment(json.send_date)
stats.date_first_chat = Math.min(timestampToMoment(line.send_date) ?? MAX_TIMESTAMP, stats.date_first_chat);
}
// For last chat time, we skip the original first message and then take all user and AI messages
if ((stats.user_msg_count + stats.non_user_msg_count) > 1) {
stats.date_last_chat = Math.max(timestampToMoment(line.send_date) ?? MIN_TIMESTAMP, stats.date_last_chat);
}
stat.date_last_chat = Date.now();
stat.date_first_chat = Math.min(
stat.date_first_chat ?? new Date('9999-12-31T23:59:59.999Z').getTime(),
Date.now(),
);
updateStats();
}