// statsHelper.js import { getRequestHeaders, callPopup, characters, this_chid } from "../script.js"; import { humanizeGenTime } from "./RossAscends-mods.js"; import {registerDebugFunction,} from "./power-user.js"; let charStats = {}; /** * 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. */ function createStatBlock(statName, statValue) { return `
${statName}:
${statValue}
`; } /** * 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 {Object} - 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(), }; for (let stats of Object.values(charStats)) { 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 ); 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 ); } } return totalStats; } /** * Generates an HTML report of stats. * * This function creates an HTML report from the provided stats, including chat age, * 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 */ 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(); } // Create popup HTML with stats let html = `

${statsType} Stats

`; 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 ); 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"); } /** * Handles the user stats by getting them from the server, calculating the total and generating the HTML report. * * @param {Object} charStats - Object containing character statistics. */ async function userStatsHandler() { // Get stats from server await getStats(); // Calculate total stats let totalStats = calculateTotalStats(charStats); // Create HTML with stats createHtml("User", totalStats); } /** * Handles the character stats by getting them from the server and generating the HTML report. * * @param {Object} charStats - Object containing character statistics. * @param {Object} 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]; 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; updateStats(); } // Create HTML with stats createHtml("Character", myStats); } /** * Fetches the character stats from the server. * * @param {Object} charStats - Object containing character statistics. * @returns {Object} - Object containing fetched character statistics. */ async function getStats() { const response = await fetch("/getstats", { method: "POST", headers: getRequestHeaders(), body: JSON.stringify({}), cache: "no-cache", }); if (!response.ok) { toastr.error("Stats could not be loaded. Try reloading the page."); throw new Error("Error getting stats"); } charStats = await response.json(); } /** * Asynchronously recreates the stats file from chat files. * * Sends a POST request to the "/recreatestats" endpoint. If the request fails, * it displays an error notification and throws an error. * * @throws {Error} If the request to recreate stats is unsuccessful. */ async function recreateStats() { const response = await fetch("/recreatestats", { method: "POST", headers: getRequestHeaders(), body: JSON.stringify({}), cache: "no-cache", }); if (!response.ok) { toastr.error("Stats could not be loaded. Try reloading the page."); throw new Error("Error getting stats"); } else { toastr.success("Stats file recreated successfully!"); } } /** * Calculates the generation time based on start and finish times. * * @param {string} gen_started - The start time in ISO 8601 format. * @param {string} gen_finished - The finish time in ISO 8601 format. * @returns {number} - The difference in time in milliseconds. */ function calculateGenTime(gen_started, gen_finished) { if (gen_started === undefined || gen_finished === undefined) { return 0; } let startDate = new Date(gen_started); let endDate = new Date(gen_finished); return endDate - startDate; } /** * Sends a POST request to the server to update the statistics. * * @param {Object} stats - The stats data to update. */ async function updateStats() { const response = await fetch("/updatestats", { method: "POST", headers: getRequestHeaders(), body: JSON.stringify(charStats), }); if (response.status !== 200) { console.error("Failed to update stats"); console.log(response).status; } } /** * Returns the count of words in the given string. * A word is a sequence of alphanumeric characters (including underscore). * * @param {string} str - The string to count words in. * @returns {number} - Number of words. */ function countWords(str) { const match = str.match(/\b\w+\b/g); return match ? match.length : 0; } /** * Handles stat processing for messages. * * @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 {string} this_chid - The character id. * @param {Object} charStats - Object containing character statistics. * @param {string} oldMesssage - The old message that's being processed. */ async function statMesProcess(line, type, characters, this_chid, oldMesssage) { if (this_chid === undefined) { return; } await getStats(); let stat = charStats[characters[this_chid].avatar]; if (!stat) { stat = { total_gen_time: 0, user_word_count: 0, non_user_msg_count: 0, user_msg_count: 0, non_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 ); if (line.is_user) { if (type != "append" && type != "continue" && type != "appendFinal") { stat.user_msg_count++; stat.user_word_count += countWords(line.mes); } else { let oldLen = oldMesssage.split(" ").length; stat.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); } else { let oldLen = oldMesssage.split(" ").length; stat.non_user_word_count += countWords(line.mes) - oldLen; } } if (type === "swipe") { stat.total_swipe_count++; } 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(); } export function initStats() { $(".rm_stats_button").on('click', function () { characterStatsHandler(characters, this_chid); }); // Wait for debug functions to load, then add the refresh stats function registerDebugFunction('refreshStats', 'Refresh Stat File', 'Recreates the stats file based on existing chat files', recreateStats); } export { userStatsHandler, characterStatsHandler, getStats, statMesProcess, charStats };