mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			333 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			333 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| // statsHelper.js
 | |
| import { moment } from '../lib.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 `<div class="rm_stat_block">
 | |
|                 <div class="rm_stat_name">${statName}:</div>
 | |
|                 <div class="rm_stat_value">${statValue}</div>
 | |
|             </div>`;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * 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 = `<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,
 | |
|     );
 | |
|     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.
 | |
|  */
 | |
| async function userStatsHandler() {
 | |
|     // Get stats from server
 | |
|     await getStats();
 | |
| 
 | |
|     // Calculate total stats
 | |
|     let totalStats = calculateTotalStats();
 | |
| 
 | |
|     // Create HTML with stats
 | |
|     createHtml('User', totalStats);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Handles the character stats by getting them from the server and generating the HTML report.
 | |
|  *
 | |
|  * @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.
 | |
|  */
 | |
| async function getStats() {
 | |
|     const response = await fetch('/api/stats/get', {
 | |
|         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 "/api/stats/recreate" 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('/api/stats/recreate', {
 | |
|         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.getTime() - startDate.getTime();
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Sends a POST request to the server to update the statistics.
 | |
|  */
 | |
| async function updateStats() {
 | |
|     const response = await fetch('/api/stats/update', {
 | |
|         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 {string} oldMesssage - The old message that's being processed.
 | |
|  */
 | |
| async function statMesProcess(line, type, characters, this_chid, oldMesssage) {
 | |
|     if (this_chid === undefined || characters[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,
 | |
|             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 };
 |