diff --git a/public/css/stats.css b/public/css/stats.css
index ac86a2152..232d73164 100644
--- a/public/css/stats.css
+++ b/public/css/stats.css
@@ -40,6 +40,7 @@
flex: 1;
height: calc(var(--mainFontSize) * 1.33333333333);
text-align: right;
+ overflow: hidden;
padding-left: 2px;
padding-right: 2px;
}
diff --git a/public/script.js b/public/script.js
index 2afa7d005..e1ef45440 100644
--- a/public/script.js
+++ b/public/script.js
@@ -1,5 +1,5 @@
import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile, initRossMods, shouldSendOnEnter } from './scripts/RossAscends-mods.js';
-import { userStatsHandler, statMesProcess, initStats } from './scripts/stats.js';
+import { initStats } from './scripts/stats.js';
import {
generateKoboldWithStreaming,
kai_settings,
@@ -228,6 +228,7 @@ export {
clearChat,
getChat,
getCharacters,
+ getCharacter,
getGeneratingApi,
callPopup,
substituteParams,
@@ -1459,6 +1460,23 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
return entities;
}
+/**
+ * Get one character from the character list via its character key
+ *
+ * To retrieve/refresh it from the API, use `getOneCharacter` to update it first.
+ *
+ * @param {string} characterKey - The character key / avatar url
+ * @returns {object}
+ */
+function getCharacter(characterKey) {
+ return characters.find(x => x.avatar === characterKey);
+}
+
+/**
+ * Gets one character from via API
+ *
+ * @param {string} avatarUrl - The avatar url / character key
+ */
export async function getOneCharacter(avatarUrl) {
const response = await fetch('/api/characters/get', {
method: 'POST',
@@ -4461,7 +4479,7 @@ export async function sendMessageAsUser(messageText, messageBias, insertAt = nul
}
await populateFileAttachment(message);
- statMesProcess(message, 'user', characters, this_chid, '');
+ // statMesProcess(message, 'user', characters, this_chid, '');
if (typeof insertAt === 'number' && insertAt >= 0 && insertAt <= chat.length) {
chat.splice(insertAt, 0, message);
@@ -10504,10 +10522,6 @@ jQuery(async function () {
}, 2000); */
//});
- $('.user_stats_button').on('click', function () {
- userStatsHandler();
- });
-
$('#external_import_button').on('click', async () => {
const html = `
Enter the URL of the content to import
Supported sources:
diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js
index 961d8a0ad..463915fa5 100644
--- a/public/scripts/RossAscends-mods.js
+++ b/public/scripts/RossAscends-mods.js
@@ -80,19 +80,21 @@ observer.observe(document.documentElement, observerConfig);
/**
- * Converts generation time from milliseconds to a human-readable format.
+ * Converts a timespan from milliseconds to a human-readable format.
*
- * The function takes total generation time as an input, then converts it to a format
+ * The function takes a total timespan as an input, then converts it to a format
* of "_ Days, _ Hours, _ Minutes, _ Seconds". If the generation time does not exceed a
* particular measure (like days or hours), that measure will not be included in the output.
*
- * @param {number} total_gen_time - The total generation time in milliseconds.
- * @param {boolean} [short=false] - Optional flag indicating whether short form should be used. ('2h' instead of '2 Hours')
- * @returns {string} - A human-readable string that represents the time spent generating characters.
+ * @param {number} timespan - The total timespan in milliseconds.
+ * @param {object} [options] - Optional parameters
+ * @param {boolean} [options.short=false] - Flag indicating whether short form should be used. ('2h' instead of '2 Hours')
+ * @param {number} [options.onlyHighest] - Number of maximum blocks to be returned. (If, and daya is the highest matching unit, only returns days and hours, cutting of minutes and seconds)
+ * @returns {string} - A human-readable string that represents the timespan.
*/
-export function humanizeGenTime(total_gen_time, short = false) {
+export function humanizeTimespan(timespan, { short = false, onlyHighest = 2 } = {}) {
//convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds
- let time_spent = total_gen_time || 0;
+ let time_spent = timespan || 0;
time_spent = Math.floor(time_spent / 1000);
let seconds = time_spent % 60;
time_spent = Math.floor(time_spent / 60);
@@ -101,13 +103,36 @@ export function humanizeGenTime(total_gen_time, short = false) {
let hours = time_spent % 24;
time_spent = Math.floor(time_spent / 24);
let days = time_spent;
- let parts = [];
- if (days > 0) { parts.push(short ? `${days}d` : `${days} Days`); }
- if (hours > 0) { parts.push(short ? `${hours}h` : `${hours} Hours`); }
- if (minutes > 0) { parts.push(short ? `${minutes}m` : `${minutes} Minutes`); }
- if (seconds > 0) { parts.push(short ? `${seconds}s` : `${seconds} Seconds`); }
- if (!parts.length) { parts.push(short ? '<1s' : 'Instant') }
- return parts.join(short ? ' ' : ', ');
+
+ let parts = [
+ { singular: 'Day', plural: 'Days', short: 'd', value: days },
+ { singular: 'Hour', plural: 'Hours', short: 'h', value: hours },
+ { singular: 'Minute', plural: 'Minutes', short: 'm', value: minutes },
+ { singular: 'Second', plural: 'Seconds', short: 's', value: seconds },
+ ];
+
+ // Build the final string based on the highest significant units and respecting zeros
+ let resultParts = [];
+ let count = 0;
+ for (let part of parts) {
+ if (part.value > 0) {
+ resultParts.push(part);
+ }
+
+ // If we got a match, we count from there. Take a maximum of X elements
+ if (resultParts.length) count++;
+ if (count >= onlyHighest) {
+ break;
+ }
+ }
+
+ if (!resultParts.length) {
+ return short ? '<1s' : 'Instant';
+ }
+
+ return resultParts.map(part => {
+ return short ? `${part.value}${part.short}` : `${part.value} ${part.value === 1 ? part.singular : part.plural}`;
+ }).join(short ? ' ' : ', ');
}
/**
diff --git a/public/scripts/stats.js b/public/scripts/stats.js
index 092722e63..b3e79531e 100644
--- a/public/scripts/stats.js
+++ b/public/scripts/stats.js
@@ -1,133 +1,76 @@
// statsHelper.js
-import { getRequestHeaders, callPopup, characters, this_chid, buildAvatarList, characterToEntity } from '../script.js';
-import { humanizeGenTime } from './RossAscends-mods.js';
+import { getRequestHeaders, callPopup, characters, this_chid, buildAvatarList, characterToEntity, getOneCharacter, getCharacter } from '../script.js';
+import { humanizeTimespan } from './RossAscends-mods.js';
import { registerDebugFunction } from './power-user.js';
-import { timestampToMoment } from './utils.js';
+import { humanFileSize, humanizedDuration, parseJson, sensibleRound, smartTruncate } from './utils.js';
-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 */
+/** @typedef {import('../../src/endpoints/stats.js').UserStatsCollection} UserStatsCollection */
+/** @typedef {import('../../src/endpoints/stats.js').CharacterStats} CharacterStats */
+/** @typedef {import('../../src/endpoints/stats.js').ChatStats} ChatStats */
+/** @typedef {import('../../src/endpoints/stats.js').MessageStats} MessageStats */
+/** @typedef {import('../../src/endpoints/stats.js').StatsRequestBody} StatsRequestBody */
/**
* @typedef {object} AggregateStat
- * @property {number[]} list - The values stored, so they can be recalculated
+ * @property {number} count - The number of stats used for this aggregation - used for recalculating avg
* @property {number} total - Total / Sum
* @property {number} min - Minimum value
* @property {number} max - Maximum value
* @property {number} avg - Average value
+ * @property {number[]} values - All values listed and saved, so the aggregate stats can be updated if needed when elements get removed
+ * @property {number?} subCount - The number of stats used when this is aggregated over the totals of aggregated stats, meaning based on any amount of sub/inner values
*/
-/**
- * @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,
- };
-}
-
-/**
- * Verifies and returns a numerical stat value. If the provided stat is not a number, returns 0.
- *
- * @param {number|string} stat - The stat value to be checked and returned.
- * @returns {number} - The stat value if it is a number, otherwise 0.
- */
-function verifyStatValue(stat) {
- return isNaN(Number(stat)) ? 0 : Number(stat);
-}
-
-/**
- * Calculates total stats from character statistics.
- *
- * @returns {Stats} - Object containing total statistics.
- */
-function calculateTotalStats() {
- let totalStats = createEmptyStats();
-
- 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.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);
- 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|'info'|null} [title=null] - Optional title for the value - if set to 'info', info will be used as title too
* @property {string[]|null} [classes=null] - Optional list of classes for the stat field
*/
+/**
+ * @typedef {object} AggBuildOptions Blah
+ * @property {string | {singular: string, plural: string}} [options.basedOn='chat'] -
+ * @property {string | {singular: string, plural: string}} [options.basedOnSub='message'] -
+ * @property {boolean} [options.excludeTotal=false] - Exclude
+ * @property {((value: *) => string)} [options.transform=null] -
+ */
-/** @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 }; }
+/** @type {AggBuildOptions} */
+const DEFAULT_AGG_BUILD_OPTIONS = { basedOn: 'chat', basedOnSub: 'message', excludeTotal: false, transform: null };
+
+/**
+ * Gets the fields for an aggregated value
+ * @param {AggregateStat} agg -
+ * @param {AggBuildOptions} [options=DEFAULT_AGG_BUILD_OPTIONS] -
+ * @returns {StatField[]}
+ */
+function aggregateFields(agg, options = DEFAULT_AGG_BUILD_OPTIONS) {
+ options = { ...DEFAULT_AGG_BUILD_OPTIONS, ...options };
+ const basedOn = (typeof options.basedOn !== 'object' || options.basedOn === null) ? { singular: `${options.basedOn}`, plural: `${options.basedOn}s` } : options.basedOn;
+ const basedOnSub = (typeof options.basedOnSub !== 'object' || options.basedOnSub === null) ? { singular: `${options.basedOnSub}`, plural: `${options.basedOnSub}s` } : options.basedOnSub;
+
+ /** @param {*|number} val @param {string} name @returns {StatField} */
+ const build = (val, name) => {
+ // Apply transform and rounding
+ let value = options.transform ? options.transform(val) : val;
+ value = typeof value === 'number' ? sensibleRound(value) : value;
+
+ // Build title tooltip
+ let title = `${name}, based on ${agg.count} ${agg.count !== 1 ? basedOn.plural : basedOn.singular}`
+ if (agg.subCount) title += ` and ${agg.subCount} ${agg.subCount !== 1 ? basedOnSub.plural : basedOnSub.singular}`;
+
+ return { value: value, title: title };
+ };
+ return [options.excludeTotal ? null : build(agg.total, 'Total'), build(agg.min, 'Minimum'), build(agg.avg, 'Average'), build(agg.max, 'Maximum')];
+}
+
+/** Gets the stat field object for any value @param {StatField|any} x @returns {StatField} */
+function statField(x) { return (typeof x === 'object' && x !== null && Object.hasOwn(x, 'value')) ? x : { value: x }; }
/**
* Creates an HTML stat block
@@ -140,14 +83,14 @@ 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 `
`;
}
- const statName = field(name);
- const statValues = values.flat(Infinity).map(field);
+ const statName = statField(name);
+ const statValues = values.flat(Infinity).map(statField);
const isDataRow = !statName.isHeader && !statValues.some(x => x.isHeader);
const isRightSpacing = statValues.slice(-1)[0]?.classes?.includes('rm_stat_right_spacing');
@@ -171,85 +114,84 @@ function createStatBlock(name, ...values) {
* 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 {number|null} characterId - Character id for these stats, null if global
- * @param {Stats} stats - The stats data
+ * @param {CharacterStats} stats - The stats data
*/
-function createHtml(statsType, characterId, stats) {
+function createHtml(statsType, stats) {
const NOW = Date.now();
- const name = characters[characterId]?.name || 'User';
- const HMTL_STAT_SPACER = '';
+ const character = getCharacter(stats.characterKey);
- /** @param {number} charVal @param {number} userVal @returns {string} */
- function buildBar(userVal, charVal) {
- const percentUser = (userVal / (userVal + charVal)) * 100;
- const percentChar = 100 - percentUser;
- return `
-
-
-
`;
- }
- /** @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);
- }
+ const HMTL_STAT_SPACER = '';
+ const VAL_RIGHT_SPACING = { value: null, classes: ['rm_stat_right_spacing'] };
+ const BASED_ON_MES_AND_SWIPE = { singular: 'message or swipe', plural: 'messages and swipes' };
+ const HOVER_TOOLTIP_SUFFIX = '\n\nHover over any value to see what it is based on.';
+ const GEN_TOKEN_WARNING = '(Token count is only correct, if setting \'Message Token Count\' was turned on during generation)';
+
+ // some pre calculations
+ const mostUsedModel = findHighestModel(stats.genModels);
// Create popup HTML with stats
- let html = `
${statsType} Stats - ${name}
`;
+ let html = `
${statsType} Stats - ${stats.charName}
`;
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 += 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.' },
+ humanFileSize(stats.chatSize), VAL_RIGHT_SPACING);
+ html += createStatBlock({ value: 'Most Used Model', info: 'Most used model for generations, both messages and swipes.\n(Does not include internal generation commands like /gen or /impersonate)\n\nHover over the value to see the numbers behind.' },
+ { value: smartTruncate(mostUsedModel.model, 32), title: 'info', info: `${mostUsedModel.model}\nUsed ${mostUsedModel.count} times to generate ${mostUsedModel.tokens} tokens\n\n${GEN_TOKEN_WARNING}.` }, VAL_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'] },
+ { value: 'First', isHeader: true, info: `Data corresponding to the first chat with ${stats.charName}` },
+ { value: 'Last', isHeader: true, info: `Data corresponding to the last chat with ${stats.charName}` },
+ VAL_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: 'New Chat', info: 'The first/last time when a new chat was started.' },
+ { value: humanizedDuration(stats.firstCreateDate, NOW, { wrapper: x => `${x} ago` }), title: stats.firstCreateDate },
+ { value: humanizedDuration(stats.lastCreateDate, NOW, { wrapper: x => `${x} ago` }), title: stats.lastCreateDate },
+ VAL_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({ value: 'Chat Ended', info: 'The first/last time when the last message was send to a chat.' },
+ { value: humanizedDuration(stats.firstlastInteractionDate, NOW, { wrapper: x => `${x} ago` }), title: stats.firstlastInteractionDate },
+ { value: humanizedDuration(stats.lastLastInteractionDate, NOW, { wrapper: x => `${x} ago` }), title: stats.lastLastInteractionDate },
+ VAL_RIGHT_SPACING,
);
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({ value: 'Aggregated Stats', isHeader: true, info: 'Values per chat, aggregated over all chats\n\n • Total: Total summed value over all chats\n • Min: Minium value for any chat\n • Avg: Average value over all chats\n • Max: Maximum value for any chat' });
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 += createStatBlock({ value: 'Chatting Time', info: 'Chatting time per chat\nCalculated based on chat creation and the last interaction in that chat.' + HOVER_TOOLTIP_SUFFIX },
+ ...aggregateFields(stats.chattingTime, { transform: time => humanizeTimespan(time, { short: true }) }));
+ html += createStatBlock({ value: 'Generation Time', info: 'Generation time per chat\nSummed generation times of all messages and swipes.' + HOVER_TOOLTIP_SUFFIX },
+ ...aggregateFields(stats.genTime, { transform: time => humanizeTimespan(time, { short: true }) }));
+ html += createStatBlock({ value: 'Generated Tokens', info: `Generated tokens per chat\nSummed token counts of all messages and swipes.\n${GEN_TOKEN_WARNING}` + HOVER_TOOLTIP_SUFFIX },
+ ...aggregateFields(stats.genTokenCount));
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 += createStatBlock({ value: 'Swiping Time', info: 'Swiping time per chat\nSummed time spend on generation alternative swipes. Excludes the final message that was chosen to continue the chat.' + HOVER_TOOLTIP_SUFFIX },
+ ...aggregateFields(stats.swipeGenTime, { transform: time => humanizeTimespan(time, { short: true }) }));
+ html += createStatBlock({ value: 'Swipes', info: 'Swipes per chat\nCounts all generated messages/swipes that were not chosen to continue the chat.' + HOVER_TOOLTIP_SUFFIX },
+ ...aggregateFields(stats.swipes));
html += HMTL_STAT_SPACER;
- html += createStatBlock('User Response Time', humanizeGenTime(34680309, true), humanizeGenTime(4566, true), humanizeGenTime(23523, true), humanizeGenTime(286230, true));
+ html += createStatBlock({ value: 'User Response Time', info: 'User response time per chat\nCalculated based on the time between the last action of the message before and the next user message.\nAs \'action\' counts both the message send time and when the last generation of it ended, even if that swipe wasn\'t chosen.' + HOVER_TOOLTIP_SUFFIX },
+ ...aggregateFields(stats.userResponseTime, { transform: time => humanizeTimespan(time, { short: 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));
+ ...aggregateFields(stats.messages));
+ html += createStatBlock('System Messages', ...aggregateFields(stats.systemMessages));
+ html += createStatBlock({ value: 'Messages (User / Char)', classes: ['rm_stat_field_smaller'] }, ...buildBarDescsFromAggregates(stats.userMessages, stats.charMessages));
html += createStatBlock({ value: '', info: '' },
- buildBar(145, 359), buildBar(2, 27), buildBar(8, 54), buildBar(66, 100));
+ ...buildBarsFromAggregates(stats.userMessages, stats.charMessages));
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));
+ ...aggregateFields(stats.words));
+ html += createStatBlock({ value: 'Words (User / Char)', classes: ['rm_stat_field_smaller'] }, ...buildBarDescsFromAggregates(stats.userWords, stats.charWords));
html += createStatBlock({ value: '', info: '' },
- buildBar(1451, 3594), buildBar(22, 279), buildBar(84, 625), buildBar(762, 2505));
+ ...buildBarsFromAggregates(stats.userWords, stats.charWords));
html += HMTL_STAT_SPACER;
html += HMTL_STAT_SPACER;
@@ -261,78 +203,147 @@ function createHtml(statsType, characterId, stats) {
{ 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);
+ ...aggregateFields(stats.perMessageGenTime, { basedOn: 'message', excludeTotal: true, transform: time => humanizeTimespan(time, { short: true }) }));
+ html += createStatBlock('Generated Tokens', ...aggregateFields(stats.perMessageGenTokenCount, { basedOn: 'message', excludeTotal: true }));
html += HMTL_STAT_SPACER;
- html += createStatBlock('Swiping Time', null, humanizeGenTime(1456, true), humanizeGenTime(2523, true), humanizeGenTime(28230, true));
+ html += createStatBlock('Swiping Time', ...aggregateFields(stats.perMessageSwipeGenTime, { basedOn: 'message', excludeTotal: true, transform: time => humanizeTimespan(time, { short: true }) }));
html += createStatBlock({ value: 'Swipes', info: 'min/avg/max swipes per non-user message' },
- null, { value: 1 }, { value: 4 }, { value: 25 });
+ ...aggregateFields(stats.perMessageSwipeCount, { basedOn: 'message', excludeTotal: true }));
html += HMTL_STAT_SPACER;
- html += createStatBlock('User Response Time', null, humanizeGenTime(0, true), humanizeGenTime(233, true), humanizeGenTime(13630, true));
+ html += createStatBlock('User Response Time', ...aggregateFields(stats.perMessageUserResponseTime, { basedOn: 'message', excludeTotal: true, transform: time => humanizeTimespan(time, { short: 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));
+ ...aggregateFields(stats.perMessageWords, { basedOn: 'message', excludeTotal: true }));
+ html += createStatBlock({ value: 'Words (User / Char)', classes: ['rm_stat_field_smaller'] }, ...buildBarDescsFromAggregates(stats.perMessageUserWords, stats.perMessageCharWords, { basedOn: 'message', excludeTotal: true }));
html += createStatBlock({ value: '', info: '' },
- null, buildBar(22, 279), buildBar(84, 625), buildBar(762, 2505));
+ ...buildBarsFromAggregates(stats.perMessageUserWords, stats.perMessageCharWords, { basedOn: 'message', excludeTotal: true }));
html += HMTL_STAT_SPACER;
html += HMTL_STAT_SPACER;
// Hijack avatar list function to draw the user avatar
- if (characters[characterId]) {
+ if (character) {
const placeHolder = $('');
- const entity = characterToEntity(characters[characterId], characterId);
+ const cid = characters.indexOf(x => x === character);
+ const entity = characterToEntity(character, cid);
buildAvatarList(placeHolder, [entity]);
html = placeHolder.prop('outerHTML') + html;
}
callPopup(html, 'text', '', { wider: true, allowVerticalScrolling: true });
+
+ return;
+
+ /** @param {AggregateStat} agg1 @param {AggregateStat} agg2 @param {AggBuildOptions} options @returns {StatField[]} */
+ function buildBarsFromAggregates(agg1, agg2, options = DEFAULT_AGG_BUILD_OPTIONS) {
+ options = { ...DEFAULT_AGG_BUILD_OPTIONS, ...options };
+ const f1 = aggregateFields(agg1, options);
+ const f2 = aggregateFields(agg2, options);
+ const bars = f1.map((_, i) => buildBar(f1[i]?.value, f2[i]?.value));
+ return bars.map(statField);
+ }
+ /** @param {number} charVal @param {number} userVal @returns {string} */
+ function buildBar(userVal, charVal) {
+ const percentUser = (userVal / (userVal + charVal)) * 100;
+ const percentChar = 100 - percentUser;
+ return `
+
+
+
`;
+ }
+ /** @param {AggregateStat} agg1 @param {AggregateStat} agg2 @param {AggBuildOptions} options @returns {StatField[]} */
+ function buildBarDescsFromAggregates(agg1, agg2, options = DEFAULT_AGG_BUILD_OPTIONS) {
+ options = { ...DEFAULT_AGG_BUILD_OPTIONS, ...options };
+ const f1 = aggregateFields(agg1, options);
+ const f2 = aggregateFields(agg2, options);
+ const values = [f1[0], f2[0], f1[1], f2[1], f1[2], f2[2], f1[3], f2[3]];
+ return buildBarDescs(values);
+ }
+ /** @param {any[]} values @returns {StatField[]} */
+ function buildBarDescs(...values) {
+ return values.flat(Infinity).map(statField).map((x, i) => i % 2 == 0 ? { classes: [...(x.classes ?? []), 'rm_stat_field_lefty'], ...x } : x);
+ }
+}
+
+/**
+ * Finds the model with the highest count and returns its name and values
+ *
+ * @param {{[model: string]: { count: number, tokens: number }}} genModels - Object containing model usages
+ * @returns {{ model: string, count: number, tokens: number }} - Object containing the name and values of the model with the highest count
+ */
+function findHighestModel(genModels) {
+ return Object.entries(genModels).reduce((acc, [model, values]) => {
+ return values.count > acc.count ? { model: model, count: values.count, tokens: values.tokens } : acc;
+ }, { model: '', count: 0, tokens: 0 });
}
/**
* Handles the user stats by getting them from the server, calculating the total and generating the HTML report.
*/
-async function userStatsHandler() {
+async function showUserStatsPopup() {
// Get stats from server
- await getStats();
-
- // Calculate total stats
- let totalStats = calculateTotalStats();
+ const globalStats = await getGlobalStats();
// Create HTML with stats
- createHtml('User', null, totalStats);
+ createHtml('User', globalStats);
}
/**
* Handles the character stats by getting them from the server and generating the HTML report.
*
- * @param {{[characterKey: string]: Character}} characters - Object containing character data.
- * @param {string} this_chid - The character id.
+ * @param {string} characterKey - The character key.
*/
-async function characterStatsHandler(characters, this_chid) {
+async function showCharacterStatsPopup(characterKey) {
// Get stats from server
- await getStats();
- // Get character stats
- let myStats = charStats.stats[characters[this_chid].avatar];
- if (myStats === undefined) {
- myStats = createEmptyStats();
- myStats.non_user_word_count = countWords(characters[this_chid].first_mes);
- charStats.stats[characters[this_chid].avatar] = myStats;
- updateStats();
+ const charStats = await getCharStats(characterKey);
+ if (charStats === null) {
+ toastr.info('No stats exist for the the current character.');
+ return;
}
+
// Create HTML with stats
- createHtml('Character', this_chid, myStats);
+ createHtml('Character', charStats);
+}
+
+
+/**
+ *
+ * @param {string} characterKey - The key of the character
+ * @returns {Promise}
+ */
+async function getCharStats(characterKey) {
+ const stats = await callGetStats({ characterKey: characterKey });
+ return stats;
+}
+
+/**
+ *
+ * @returns {Promise}
+ */
+async function getGlobalStats() {
+ const stats = await callGetStats({ global: true });
+ return stats;
+}
+/**
+ *
+ * @returns {Promise}
+ */
+async function getFullStatsCollection() {
+ const stats = await callGetStats();
+ return stats;
}
/**
* Fetches the character stats from the server.
+ * For retrieving, use the more specific functions `getCharStats`, `getGlobalStats` and `getFullStatsCollection`.
+ * @param {StatsRequestBody} [params={}] Optional parameter for the get request
+ * @returns {Promise