Blame Raw
Cohee · e3f41666 · · 334 lines (11.1 KB)
1 contributor
1// statsHelper.js
2import { moment } from '../lib.js';
3import { getRequestHeaders, characters, this_chid } from '../script.js';
4import { humanizeGenTime } from './RossAscends-mods.js';
5import { callGenericPopup, POPUP_TYPE } from './popup.js';
6import { registerDebugFunction } from './power-user.js';
7import { t, translate } from './i18n.js';
8
9let charStats = {};
10
11/**
12 * Creates an HTML stat block.
13 *
14 * @param {string} statName - The name of the stat to be displayed.
15 * @param {number|string} statValue - The value of the stat to be displayed.
16 * @returns {string} - An HTML string representing the stat block.
17 */
18function createStatBlock(statName, statValue) {
19 return `<div class="rm_stat_block">
20 <div class="rm_stat_name">${statName}:</div>
21 <div class="rm_stat_value">${statValue}</div>
22 </div>`;
23}
24
25/**
26 * Verifies and returns a numerical stat value. If the provided stat is not a number, returns 0.
27 *
28 * @param {number|string} stat - The stat value to be checked and returned.
29 * @returns {number} - The stat value if it is a number, otherwise 0.
30 */
31function verifyStatValue(stat) {
32 return isNaN(Number(stat)) ? 0 : Number(stat);
33}
34
35/**
36 * Calculates total stats from character statistics.
37 *
38 * @returns {Object} - Object containing total statistics.
39 */
40function calculateTotalStats() {
41 let totalStats = {
42 total_gen_time: 0,
43 user_msg_count: 0,
44 non_user_msg_count: 0,
45 user_word_count: 0,
46 non_user_word_count: 0,
47 total_swipe_count: 0,
48 date_last_chat: 0,
49 date_first_chat: new Date('9999-12-31T23:59:59.999Z').getTime(),
50 };
51
52 for (let stats of Object.values(charStats)) {
53 totalStats.total_gen_time += verifyStatValue(stats.total_gen_time);
54 totalStats.user_msg_count += verifyStatValue(stats.user_msg_count);
55 totalStats.non_user_msg_count += verifyStatValue(
56 stats.non_user_msg_count,
57 );
58 totalStats.user_word_count += verifyStatValue(stats.user_word_count);
59 totalStats.non_user_word_count += verifyStatValue(
60 stats.non_user_word_count,
61 );
62 totalStats.total_swipe_count += verifyStatValue(
63 stats.total_swipe_count,
64 );
65
66 if (verifyStatValue(stats.date_last_chat) != 0) {
67 totalStats.date_last_chat = Math.max(
68 totalStats.date_last_chat,
69 stats.date_last_chat,
70 );
71 }
72 if (verifyStatValue(stats.date_first_chat) != 0) {
73 totalStats.date_first_chat = Math.min(
74 totalStats.date_first_chat,
75 stats.date_first_chat,
76 );
77 }
78 }
79
80 return totalStats;
81}
82
83/**
84 * Generates an HTML report of stats.
85 *
86 * This function creates an HTML report from the provided stats, including chat age,
87 * chat time, number of user messages and character messages, word count, and swipe count.
88 * The stat blocks are tailored depending on the stats type ("User" or "Character").
89 *
90 * @param {string} statsType - The type of stats (e.g., "User", "Character").
91 * @param {Object} stats - The stats data. Expected keys in this object include:
92 * total_gen_time - total generation time
93 * date_first_chat - timestamp of the first chat
94 * date_last_chat - timestamp of the most recent chat
95 * user_msg_count - count of user messages
96 * non_user_msg_count - count of non-user messages
97 * user_word_count - count of words used by the user
98 * non_user_word_count - count of words used by the non-user
99 * total_swipe_count - total swipe count
100 */
101function createHtml(statsType, stats) {
102 // Get time string
103 let timeStirng = humanizeGenTime(stats.total_gen_time);
104 let chatAge = 'Never';
105 if (stats.date_first_chat < Date.now()) {
106 chatAge = moment
107 .duration(stats.date_last_chat - stats.date_first_chat)
108 .humanize();
109 }
110 let statsTypeTranslated = translate(statsType, `stats_header_${statsType}`);
111
112 // Create popup HTML with stats
113 let html = '<h3>' + t`${statsTypeTranslated} Stats` + '</h3>';
114 if (statsType === 'User') {
115 html += createStatBlock(t`Chatting Since`, `${chatAge} ago`);
116 } else {
117 html += createStatBlock(t`First Interaction`, `${chatAge} ago`);
118 }
119 html += createStatBlock(t`Chat Time`, timeStirng);
120 html += createStatBlock(t`User Messages`, stats.user_msg_count);
121 html += createStatBlock(
122 t`Character Messages`,
123 stats.non_user_msg_count - stats.total_swipe_count,
124 );
125 html += createStatBlock(t`User Words`, stats.user_word_count);
126 html += createStatBlock(t`Character Words`, stats.non_user_word_count);
127 html += createStatBlock(t`Swipes`, stats.total_swipe_count);
128
129 return callGenericPopup(html, POPUP_TYPE.TEXT);
130}
131
132/**
133 * Handles the user stats by getting them from the server, calculating the total and generating the HTML report.
134 */
135async function userStatsHandler() {
136 // Get stats from server
137 await getStats();
138
139 // Calculate total stats
140 let totalStats = calculateTotalStats();
141
142 // Create HTML with stats
143 createHtml('User', totalStats);
144}
145
146/**
147 * Handles the character stats by getting them from the server and generating the HTML report.
148 *
149 * @param {Object} characters - Object containing character data.
150 * @param {string} this_chid - The character id.
151 */
152async function characterStatsHandler(characters, this_chid) {
153 // Get stats from server
154 await getStats();
155 // Get character stats
156 let myStats = charStats[characters[this_chid].avatar];
157 if (myStats === undefined) {
158 myStats = {
159 total_gen_time: 0,
160 user_msg_count: 0,
161 non_user_msg_count: 0,
162 user_word_count: 0,
163 non_user_word_count: countWords(characters[this_chid].first_mes),
164 total_swipe_count: 0,
165 date_last_chat: 0,
166 date_first_chat: new Date('9999-12-31T23:59:59.999Z').getTime(),
167 };
168 charStats[characters[this_chid].avatar] = myStats;
169 updateStats();
170 }
171 // Create HTML with stats
172 createHtml('Character', myStats);
173}
174
175/**
176 * Fetches the character stats from the server.
177 */
178async function getStats() {
179 const response = await fetch('/api/stats/get', {
180 method: 'POST',
181 headers: getRequestHeaders(),
182 body: JSON.stringify({}),
183 cache: 'no-cache',
184 });
185
186 if (!response.ok) {
187 toastr.error('Stats could not be loaded. Try reloading the page.');
188 throw new Error('Error getting stats');
189 }
190 charStats = await response.json();
191}
192
193/**
194 * Asynchronously recreates the stats file from chat files.
195 *
196 * Sends a POST request to the "/api/stats/recreate" endpoint. If the request fails,
197 * it displays an error notification and throws an error.
198 *
199 * @throws {Error} If the request to recreate stats is unsuccessful.
200 */
201async function recreateStats() {
202 const response = await fetch('/api/stats/recreate', {
203 method: 'POST',
204 headers: getRequestHeaders(),
205 body: JSON.stringify({}),
206 cache: 'no-cache',
207 });
208
209 if (!response.ok) {
210 toastr.error('Stats could not be loaded. Try reloading the page.');
211 throw new Error('Error getting stats');
212 } else {
213 toastr.success('Stats file recreated successfully!');
214 }
215}
216
217
218/**
219 * Calculates the generation time based on start and finish times.
220 *
221 * @param {string} gen_started - The start time in ISO 8601 format.
222 * @param {string} gen_finished - The finish time in ISO 8601 format.
223 * @returns {number} - The difference in time in milliseconds.
224 */
225function calculateGenTime(gen_started, gen_finished) {
226 if (gen_started === undefined || gen_finished === undefined) {
227 return 0;
228 }
229 let startDate = new Date(gen_started);
230 let endDate = new Date(gen_finished);
231 return endDate.getTime() - startDate.getTime();
232}
233
234/**
235 * Sends a POST request to the server to update the statistics.
236 */
237async function updateStats() {
238 const response = await fetch('/api/stats/update', {
239 method: 'POST',
240 headers: getRequestHeaders(),
241 body: JSON.stringify(charStats),
242 });
243
244 if (response.status !== 200) {
245 console.error('Failed to update stats');
246 console.log(response.status);
247 }
248}
249
250/**
251 * Returns the count of words in the given string.
252 * A word is a sequence of alphanumeric characters (including underscore).
253 *
254 * @param {string} str - The string to count words in.
255 * @returns {number} - Number of words.
256 */
257function countWords(str) {
258 const match = str.match(/\b\w+\b/g);
259 return match ? match.length : 0;
260}
261
262/**
263 * Handles stat processing for messages.
264 *
265 * @param {Object} line - Object containing message data.
266 * @param {string} type - The type of the message processing (e.g., 'append', 'continue', 'appendFinal', 'swipe').
267 * @param {Object} characters - Object containing character data.
268 * @param {string} this_chid - The character id.
269 * @param {string} oldMessage - The old message that's being processed.
270 */
271async function statMesProcess(line, type, characters, this_chid, oldMessage) {
272 if (this_chid === undefined || characters[this_chid] === undefined) {
273 return;
274 }
275 await getStats();
276
277 let stat = charStats[characters[this_chid].avatar];
278
279 if (!stat) {
280 stat = {
281 total_gen_time: 0,
282 user_word_count: 0,
283 non_user_msg_count: 0,
284 user_msg_count: 0,
285 total_swipe_count: 0,
286 date_first_chat: Date.now(),
287 date_last_chat: Date.now(),
288 };
289 }
290
291 stat.total_gen_time += calculateGenTime(
292 line.gen_started,
293 line.gen_finished,
294 );
295 if (line.is_user) {
296 if (type != 'append' && type != 'continue' && type != 'appendFinal') {
297 stat.user_msg_count++;
298 stat.user_word_count += countWords(line.mes);
299 } else {
300 let oldLen = oldMessage.split(' ').length;
301 stat.user_word_count += countWords(line.mes) - oldLen;
302 }
303 } else {
304 // if continue, don't add a message, get the last message and subtract it from the word count of
305 // the new message
306 if (type != 'append' && type != 'continue' && type != 'appendFinal') {
307 stat.non_user_msg_count++;
308 stat.non_user_word_count += countWords(line.mes);
309 } else {
310 let oldLen = oldMessage.split(' ').length;
311 stat.non_user_word_count += countWords(line.mes) - oldLen;
312 }
313 }
314
315 if (type === 'swipe') {
316 stat.total_swipe_count++;
317 }
318 stat.date_last_chat = Date.now();
319 stat.date_first_chat = Math.min(
320 stat.date_first_chat ?? new Date('9999-12-31T23:59:59.999Z').getTime(),
321 Date.now(),
322 );
323 updateStats();
324}
325
326export function initStats() {
327 $('.rm_stats_button').on('click', function () {
328 characterStatsHandler(characters, this_chid);
329 });
330 // Wait for debug functions to load, then add the refresh stats function
331 registerDebugFunction('refreshStats', 'Refresh Stat File', 'Recreates the stats file based on existing chat files', recreateStats);
332}
333
334export { userStatsHandler, characterStatsHandler, getStats, statMesProcess, charStats };