mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Temp commit
- Fixed "old" popup resizing and scroll bars (now actually respecting the chosen setting)
This commit is contained in:
96
public/css/stats.css
Normal file
96
public/css/stats.css
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
.rm_stats_button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_block {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_block_data_row:hover {
|
||||||
|
background-color: var(--grey5020a);
|
||||||
|
filter: drop-shadow(0px 0px 5px var(--SmartThemeShadowColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_values {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_name .rm_stat_header {
|
||||||
|
height: calc(var(--mainFontSize) * 1.33333333333 + 3px);
|
||||||
|
padding-bottom: 3px;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_name .rm_stat_field {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_field.rm_stat_field_lefty {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_field {
|
||||||
|
flex: 1;
|
||||||
|
height: calc(var(--mainFontSize) * 1.33333333333);
|
||||||
|
text-align: right;
|
||||||
|
padding-left: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_field_smaller {
|
||||||
|
color: var(--grey70);
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_header {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_spacer {
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_bar {
|
||||||
|
width: 100%;
|
||||||
|
height: calc(var(--mainFontSize) * 1.33333333333 - 4px);
|
||||||
|
display: flex;
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
padding-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_bar_user {
|
||||||
|
background-color: rgba(130, 178, 140, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_bar_char {
|
||||||
|
background-color: rgba(178, 140, 130, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_block.rm_stat_right_spacing {
|
||||||
|
margin-right: 33.33333333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_avatar_block {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(1.17em + 7px + 20px);
|
||||||
|
right: 0px;
|
||||||
|
height: calc(8px + calc(calc(var(--mainFontSize) * 1.33333333333) * 7) + calc(12px * 3));
|
||||||
|
width: 33.33333333333%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rm_stat_avatar_block .avatar {
|
||||||
|
scale: 2;
|
||||||
|
flex: unset;
|
||||||
|
}
|
@ -139,7 +139,6 @@ body.big-avatars .bogus_folder_select .avatar {
|
|||||||
body.big-avatars .avatar {
|
body.big-avatars .avatar {
|
||||||
width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor));
|
width: calc(var(--avatar-base-width) * var(--big-avatar-width-factor));
|
||||||
height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor));
|
height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor));
|
||||||
/* width: unset; */
|
|
||||||
border-style: none;
|
border-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -64,6 +64,7 @@
|
|||||||
<link rel="stylesheet" type="text/css" href="css/select2-overrides.css">
|
<link rel="stylesheet" type="text/css" href="css/select2-overrides.css">
|
||||||
<link rel="stylesheet" type="text/css" href="css/mobile-styles.css">
|
<link rel="stylesheet" type="text/css" href="css/mobile-styles.css">
|
||||||
<link rel="stylesheet" type="text/css" href="css/user.css">
|
<link rel="stylesheet" type="text/css" href="css/user.css">
|
||||||
|
<link rel="stylesheet" type="text/css" href="css/stats.css">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<script type="module" src="scripts/i18n.js"></script>
|
<script type="module" src="scripts/i18n.js"></script>
|
||||||
<script type="module" src="script.js"></script>
|
<script type="module" src="script.js"></script>
|
||||||
|
@ -7103,16 +7103,17 @@ function onScenarioOverrideRemoveClick() {
|
|||||||
* @param {string} type
|
* @param {string} type
|
||||||
* @param {string} inputValue - Value to set the input to.
|
* @param {string} inputValue - Value to set the input to.
|
||||||
* @param {PopupOptions} options - Options for the popup.
|
* @param {PopupOptions} options - Options for the popup.
|
||||||
* @typedef {{okButton?: string, rows?: number, wide?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup.
|
* @typedef {{okButton?: string, rows?: number, wide?: boolean, wider?: boolean, large?: boolean, allowHorizontalScrolling?: boolean, allowVerticalScrolling?: boolean }} PopupOptions - Options for the popup.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) {
|
function callPopup(text, type, inputValue = '', { okButton, rows, wide, wider, large, allowHorizontalScrolling, allowVerticalScrolling } = {}) {
|
||||||
dialogueCloseStop = true;
|
dialogueCloseStop = true;
|
||||||
if (type) {
|
if (type) {
|
||||||
popup_type = type;
|
popup_type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#dialogue_popup').toggleClass('wide_dialogue_popup', !!wide);
|
$('#dialogue_popup').toggleClass('wide_dialogue_popup', !!wide);
|
||||||
|
$('#dialogue_popup').toggleClass('wider_dialogue_popup', !!wider);
|
||||||
$('#dialogue_popup').toggleClass('large_dialogue_popup', !!large);
|
$('#dialogue_popup').toggleClass('large_dialogue_popup', !!large);
|
||||||
$('#dialogue_popup').toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling);
|
$('#dialogue_popup').toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling);
|
||||||
$('#dialogue_popup').toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
|
$('#dialogue_popup').toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
|
||||||
|
@ -86,10 +86,10 @@ observer.observe(document.documentElement, observerConfig);
|
|||||||
* particular measure (like days or hours), that measure will not be included in the output.
|
* 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 {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.
|
* @returns {string} - A human-readable string that represents the time spent generating characters.
|
||||||
*/
|
*/
|
||||||
export function humanizeGenTime(total_gen_time) {
|
export function humanizeGenTime(total_gen_time, short = false) {
|
||||||
|
|
||||||
//convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds
|
//convert time_spent to humanized format of "_ Hours, _ Minutes, _ Seconds" from milliseconds
|
||||||
let time_spent = total_gen_time || 0;
|
let time_spent = total_gen_time || 0;
|
||||||
time_spent = Math.floor(time_spent / 1000);
|
time_spent = Math.floor(time_spent / 1000);
|
||||||
@ -100,12 +100,13 @@ export function humanizeGenTime(total_gen_time) {
|
|||||||
let hours = time_spent % 24;
|
let hours = time_spent % 24;
|
||||||
time_spent = Math.floor(time_spent / 24);
|
time_spent = Math.floor(time_spent / 24);
|
||||||
let days = time_spent;
|
let days = time_spent;
|
||||||
time_spent = '';
|
let parts = [];
|
||||||
if (days > 0) { time_spent += `${days} Days, `; }
|
if (days > 0) { parts.push(short ? `${days}d` : `${days} Days`); }
|
||||||
if (hours > 0) { time_spent += `${hours} Hours, `; }
|
if (hours > 0) { parts.push(short ? `${hours}h` : `${hours} Hours`); }
|
||||||
if (minutes > 0) { time_spent += `${minutes} Minutes, `; }
|
if (minutes > 0) { parts.push(short ? `${minutes}m` : `${minutes} Minutes`); }
|
||||||
time_spent += `${seconds} Seconds`;
|
if (seconds > 0) { parts.push(short ? `${seconds}s` : `${seconds} Seconds`); }
|
||||||
return time_spent;
|
if (!parts.length) { parts.push(short ? '<1s' : 'Instant') }
|
||||||
|
return parts.join(short ? ' ' : ', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsedUA = null;
|
let parsedUA = null;
|
||||||
|
@ -1,22 +1,68 @@
|
|||||||
// statsHelper.js
|
// 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 { humanizeGenTime } from './RossAscends-mods.js';
|
||||||
import { registerDebugFunction } from './power-user.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.
|
* @typedef {object} AggregateStat
|
||||||
*
|
* @property {number[]} list - The values stored, so they can be recalculated
|
||||||
* @param {string} statName - The name of the stat to be displayed.
|
* @property {number} total - Total / Sum
|
||||||
* @param {number|string} statValue - The value of the stat to be displayed.
|
* @property {number} min - Minimum value
|
||||||
* @returns {string} - An HTML string representing the stat block.
|
* @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>
|
* @typedef {object} Stats - Stats for a chat
|
||||||
<div class="rm_stat_value">${statValue}</div>
|
* @property {number} chat_size - The size of the actual chat file
|
||||||
</div>`;
|
* @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.
|
* Calculates total stats from character statistics.
|
||||||
*
|
*
|
||||||
* @returns {Object} - Object containing total statistics.
|
* @returns {Stats} - Object containing total statistics.
|
||||||
*/
|
*/
|
||||||
function calculateTotalStats() {
|
function calculateTotalStats() {
|
||||||
let totalStats = {
|
let totalStats = createEmptyStats();
|
||||||
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)) {
|
for (let stats of Object.values(charStats.stats)) {
|
||||||
totalStats.total_gen_time += verifyStatValue(stats.total_gen_time);
|
totalStats.total_gen_time += verifyStatValue(stats.total_gen_time);
|
||||||
totalStats.user_msg_count += verifyStatValue(stats.user_msg_count);
|
totalStats.user_msg_count += verifyStatValue(stats.user_msg_count);
|
||||||
totalStats.non_user_msg_count += verifyStatValue(
|
totalStats.non_user_msg_count += verifyStatValue(stats.non_user_msg_count);
|
||||||
stats.non_user_msg_count,
|
|
||||||
);
|
|
||||||
totalStats.user_word_count += verifyStatValue(stats.user_word_count);
|
totalStats.user_word_count += verifyStatValue(stats.user_word_count);
|
||||||
totalStats.non_user_word_count += verifyStatValue(
|
totalStats.non_user_word_count += verifyStatValue(stats.non_user_word_count);
|
||||||
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.total_swipe_count += verifyStatValue(
|
totalStats.date_first_chat = Math.min(totalStats.date_first_chat, verifyStatValue(stats.date_first_chat) || MAX_TIMESTAMP);
|
||||||
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;
|
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 === '' ? '‌' : 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.
|
* 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.
|
* 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").
|
* 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 {'User'|'Character'} statsType - The type of stats (e.g., "User", "Character")
|
||||||
* @param {Object} stats - The stats data. Expected keys in this object include:
|
* @param {number|null} characterId - Character id for these stats, null if global
|
||||||
* total_gen_time - total generation time
|
* @param {Stats} stats - The stats data
|
||||||
* 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) {
|
function createHtml(statsType, characterId, stats) {
|
||||||
// Get time string
|
const NOW = Date.now();
|
||||||
let timeStirng = humanizeGenTime(stats.total_gen_time);
|
const name = characters[characterId]?.name || 'User';
|
||||||
let chatAge = 'Never';
|
const HMTL_STAT_SPACER = '<div class="rm_stat_spacer"></div>';
|
||||||
if (stats.date_first_chat < Date.now()) {
|
|
||||||
chatAge = moment
|
/** @param {number} charVal @param {number} userVal @returns {string} */
|
||||||
.duration(stats.date_last_chat - stats.date_first_chat)
|
function buildBar(userVal, charVal) {
|
||||||
.humanize();
|
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
|
// Create popup HTML with stats
|
||||||
let html = `<h3>${statsType} Stats</h3>`;
|
let html = `<h3>${statsType} Stats - ${name}</h3>`;
|
||||||
if (statsType === 'User') {
|
html += HMTL_STAT_SPACER;
|
||||||
html += createStatBlock('Chatting Since', `${chatAge} ago`);
|
html += createStatBlock({ value: 'Character Overview', isHeader: true });
|
||||||
} else {
|
html += createStatBlock('Chats', { value: 34 }, { value: null, classes: ['rm_stat_right_spacing'] });
|
||||||
html += createStatBlock('First Interaction', `${chatAge} ago`);
|
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('Chat Time', timeStirng);
|
html += HMTL_STAT_SPACER;
|
||||||
html += createStatBlock('User Messages', stats.user_msg_count);
|
html += createStatBlock('',
|
||||||
html += createStatBlock(
|
{ value: 'First', isHeader: true, info: `The data corresponding to the first chat with ${name}` },
|
||||||
'Character Messages',
|
{ value: 'Last', isHeader: true, info: `The data corresponding to the last chat with ${name}` },
|
||||||
stats.non_user_msg_count - stats.total_swipe_count,
|
{ 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();
|
let totalStats = calculateTotalStats();
|
||||||
|
|
||||||
// Create HTML with stats
|
// 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.
|
* 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.
|
* @param {string} this_chid - The character id.
|
||||||
*/
|
*/
|
||||||
async function characterStatsHandler(characters, this_chid) {
|
async function characterStatsHandler(characters, this_chid) {
|
||||||
// Get stats from server
|
// Get stats from server
|
||||||
await getStats();
|
await getStats();
|
||||||
// Get character stats
|
// Get character stats
|
||||||
let myStats = charStats[characters[this_chid].avatar];
|
let myStats = charStats.stats[characters[this_chid].avatar];
|
||||||
if (myStats === undefined) {
|
if (myStats === undefined) {
|
||||||
myStats = {
|
myStats = createEmptyStats();
|
||||||
total_gen_time: 0,
|
myStats.non_user_word_count = countWords(characters[this_chid].first_mes);
|
||||||
user_msg_count: 0,
|
charStats.stats[characters[this_chid].avatar] = myStats;
|
||||||
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();
|
updateStats();
|
||||||
}
|
}
|
||||||
// Create HTML with stats
|
// 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 {Object} line - Object containing message data.
|
||||||
* @param {string} type - The type of the message processing (e.g., 'append', 'continue', 'appendFinal', 'swipe').
|
* @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} this_chid - The character id.
|
||||||
* @param {string} oldMesssage - The old message that's being processed.
|
* @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();
|
await getStats();
|
||||||
|
|
||||||
let stat = charStats[characters[this_chid].avatar];
|
let stats = charStats.stats[characters[this_chid].avatar] ?? createEmptyStats();
|
||||||
|
|
||||||
if (!stat) {
|
stats.total_gen_time += calculateGenTime(line.gen_started, line.gen_finished);
|
||||||
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 (line.is_user) {
|
||||||
if (type != 'append' && type != 'continue' && type != 'appendFinal') {
|
if (type != 'append' && type != 'continue' && type != 'appendFinal') {
|
||||||
stat.user_msg_count++;
|
stats.user_msg_count++;
|
||||||
stat.user_word_count += countWords(line.mes);
|
stats.user_word_count += countWords(line.mes);
|
||||||
} else {
|
} else {
|
||||||
let oldLen = oldMesssage.split(' ').length;
|
let oldLen = oldMesssage.split(' ').length;
|
||||||
stat.user_word_count += countWords(line.mes) - oldLen;
|
stats.user_word_count += countWords(line.mes) - oldLen;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if continue, don't add a message, get the last message and subtract it from the word count of
|
// if continue, don't add a message, get the last message and subtract it from the word count of
|
||||||
// the new message
|
// the new message
|
||||||
if (type != 'append' && type != 'continue' && type != 'appendFinal') {
|
if (type != 'append' && type != 'continue' && type != 'appendFinal') {
|
||||||
stat.non_user_msg_count++;
|
stats.non_user_msg_count++;
|
||||||
stat.non_user_word_count += countWords(line.mes);
|
stats.non_user_word_count += countWords(line.mes);
|
||||||
} else {
|
} else {
|
||||||
let oldLen = oldMesssage.split(' ').length;
|
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') {
|
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();
|
updateStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1035,7 +1035,7 @@ function onViewTagsListClick() {
|
|||||||
|
|
||||||
makeTagListDraggable(tagContainer);
|
makeTagListDraggable(tagContainer);
|
||||||
|
|
||||||
callPopup(list, 'text');
|
callPopup(list, 'text', null, { allowVerticalScrolling: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeTagListDraggable(tagContainer) {
|
function makeTagListDraggable(tagContainer) {
|
||||||
|
@ -2052,10 +2052,6 @@ grammarly-extension {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rm_stats_button {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Focus */
|
/* Focus */
|
||||||
|
|
||||||
#bulk_tag_popup,
|
#bulk_tag_popup,
|
||||||
@ -2085,11 +2081,6 @@ grammarly-extension {
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rm_stat_block {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large_dialogue_popup {
|
.large_dialogue_popup {
|
||||||
height: 90vh !important;
|
height: 90vh !important;
|
||||||
height: 90svh !important;
|
height: 90svh !important;
|
||||||
@ -2103,12 +2094,8 @@ grammarly-extension {
|
|||||||
min-width: var(--sheldWidth);
|
min-width: var(--sheldWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal_scrolling_dialogue_popup {
|
.wider_dialogue_popup {
|
||||||
overflow-x: unset !important;
|
min-width: 750px;
|
||||||
}
|
|
||||||
|
|
||||||
.vertical_scrolling_dialogue_popup {
|
|
||||||
overflow-y: unset !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#bulk_tag_popup_holder,
|
#bulk_tag_popup_holder,
|
||||||
@ -2116,20 +2103,32 @@ grammarly-extension {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: hidden;
|
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dialogue_popup_text {
|
#dialogue_popup_text {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow-y: auto;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
max-height: calc(90vh - 50px);
|
||||||
|
max-height: calc(90svh - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal_scrolling_dialogue_popup #dialogue_popup_text {
|
||||||
|
overflow-x: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vertical_scrolling_dialogue_popup #dialogue_popup_text {
|
||||||
|
overflow-y: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
#dialogue_popup_controls {
|
#dialogue_popup_controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#bulk_tag_popup_reset,
|
#bulk_tag_popup_reset,
|
||||||
|
File diff suppressed because it is too large
Load Diff
185
src/util.js
185
src/util.js
@ -227,6 +227,28 @@ async function getImageBuffers(zipFilePath) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the contents of a .jsonl file and returns the lines parsed as json as an array
|
||||||
|
*
|
||||||
|
* @param {string} filepath - The path of the file to be read
|
||||||
|
* @returns {object[]} - The lines in the file
|
||||||
|
* @throws Will throw an error if the file cannot be read
|
||||||
|
*/
|
||||||
|
function readAndParseJsonlFile(filepath) {
|
||||||
|
try {
|
||||||
|
// Copied from /chat/get endpoint
|
||||||
|
const data = fs.readFileSync(filepath, 'utf8');
|
||||||
|
const lines = data.split('\n');
|
||||||
|
|
||||||
|
// Iterate through the array of strings and parse each line as JSON
|
||||||
|
const jsonData = lines.map((l) => { try { return JSON.parse(l); } catch (_) { return; } }).filter(x => x);
|
||||||
|
return jsonData;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading file at ${filepath}: ${error}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets all chunks of data from the given readable stream.
|
* Gets all chunks of data from the given readable stream.
|
||||||
* @param {any} readableStream Readable stream to read from
|
* @param {any} readableStream Readable stream to read from
|
||||||
@ -537,6 +559,162 @@ function trimV1(str) {
|
|||||||
return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, '');
|
return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a timestamp to an integer timestamp.
|
||||||
|
* (sorry, it's momentless for now, didn't want to add a package just for this)
|
||||||
|
* This function can handle several different timestamp formats:
|
||||||
|
* 1. Unix timestamps (the number of seconds since the Unix Epoch)
|
||||||
|
* 2. ST "humanized" timestamps, formatted like "YYYY-MM-DD @HHh MMm SSs ms"
|
||||||
|
* 3. Date strings in the format "Month DD, YYYY H:MMam/pm"
|
||||||
|
*
|
||||||
|
* The function returns the timestamp as the number of milliseconds since
|
||||||
|
* the Unix Epoch, which can be converted to a JavaScript Date object with new Date().
|
||||||
|
*
|
||||||
|
* @param {string|number} timestamp - The timestamp to convert.
|
||||||
|
* @returns {number|null} The timestamp in milliseconds since the Unix Epoch, or null if the input cannot be parsed.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Unix timestamp
|
||||||
|
* timestampToMoment(1609459200);
|
||||||
|
* // ST humanized timestamp
|
||||||
|
* timestampToMoment("2021-01-01 @00h 00m 00s 000ms");
|
||||||
|
* // Date string
|
||||||
|
* timestampToMoment("January 1, 2021 12:00am");
|
||||||
|
*/
|
||||||
|
function timestampToMoment(timestamp) {
|
||||||
|
if (!timestamp) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof timestamp === 'number') {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern1 =
|
||||||
|
/(\d{4})-(\d{1,2})-(\d{1,2}) ?@(\d{1,2})h ?(\d{1,2})m ?(\d{1,2})s ?(?:(\d{1,3})ms)?/;
|
||||||
|
const replacement1 = (
|
||||||
|
match,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
second,
|
||||||
|
millisecond,
|
||||||
|
) => {
|
||||||
|
return `${year}-${month.padStart(2, '0')}-${day.padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}T${hour.padStart(2, '0')}:${minute.padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}:${second.padStart(2, '0')}.${(millisecond ?? '0').padStart(3, '0')}Z`;
|
||||||
|
};
|
||||||
|
const isoTimestamp1 = timestamp.replace(pattern1, replacement1);
|
||||||
|
if (!isNaN(Date.parse(isoTimestamp1))) {
|
||||||
|
return new Date(isoTimestamp1).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pattern2 = /(\w+)\s(\d{1,2}),\s(\d{4})\s(\d{1,2}):(\d{1,2})(am|pm)/i;
|
||||||
|
const replacement2 = (match, month, day, year, hour, minute, meridiem) => {
|
||||||
|
const monthNames = [
|
||||||
|
'January',
|
||||||
|
'February',
|
||||||
|
'March',
|
||||||
|
'April',
|
||||||
|
'May',
|
||||||
|
'June',
|
||||||
|
'July',
|
||||||
|
'August',
|
||||||
|
'September',
|
||||||
|
'October',
|
||||||
|
'November',
|
||||||
|
'December',
|
||||||
|
];
|
||||||
|
const monthNum = monthNames.indexOf(month) + 1;
|
||||||
|
const hour24 =
|
||||||
|
meridiem.toLowerCase() === 'pm'
|
||||||
|
? (parseInt(hour, 10) % 12) + 12
|
||||||
|
: parseInt(hour, 10) % 12;
|
||||||
|
return `${year}-${monthNum.toString().padStart(2, '0')}-${day.padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}T${hour24.toString().padStart(2, '0')}:${minute.padStart(
|
||||||
|
2,
|
||||||
|
'0',
|
||||||
|
)}:00Z`;
|
||||||
|
};
|
||||||
|
const isoTimestamp2 = timestamp.replace(pattern2, replacement2);
|
||||||
|
if (!isNaN(Date.parse(isoTimestamp2))) {
|
||||||
|
return new Date(isoTimestamp2).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the time difference between two dates.
|
||||||
|
*
|
||||||
|
* @param {Date|string} startTime - The start time in ISO 8601 format, or as a Date object
|
||||||
|
* @param {Date|string} endTime - The finish time in ISO 8601 format, or as a Date object
|
||||||
|
* @returns {number} - The difference in time in milliseconds, or 0 if invalid dates are provided or if start date is after end date.
|
||||||
|
*/
|
||||||
|
function calculateDuration(startTime, endTime) {
|
||||||
|
const startDate = startTime instanceof Date ? startTime : new Date(startTime);
|
||||||
|
const endDate = endTime instanceof Date ? endTime : new Date(endTime);
|
||||||
|
|
||||||
|
return startDate > endDate ? 0 : Math.max(endDate.getDate() - startDate.getDate(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} timestamp @returns {Date|null} */
|
||||||
|
function humanizedToDate(timestamp) {
|
||||||
|
const moment = timestampToMoment(timestamp);
|
||||||
|
return moment ? new Date(moment) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum from all supplied dates
|
||||||
|
* @param {...Date} dates
|
||||||
|
* @returns {Date?}
|
||||||
|
*/
|
||||||
|
function maxDate(...dates) {
|
||||||
|
dates = dates.flat(Infinity);
|
||||||
|
if (dates.length == 0) return null;
|
||||||
|
/** @type {Date?} */
|
||||||
|
let max = null;
|
||||||
|
for (const date of dates) {
|
||||||
|
if (max === null || date > max) {
|
||||||
|
max = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the minimum from all supplied dates
|
||||||
|
* @param {...Date} dates
|
||||||
|
* @returns {Date?}
|
||||||
|
*/
|
||||||
|
function minDate(...dates) {
|
||||||
|
dates = dates.flat(Infinity);
|
||||||
|
if (dates.length == 0) return null;
|
||||||
|
/** @type {Date?} */
|
||||||
|
let min = null;
|
||||||
|
for (const date of dates) {
|
||||||
|
if (min === null || date < min) {
|
||||||
|
min = date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current date and time
|
||||||
|
* (Why the fuck is `Date.now()` returning a timestamp and no date... I don't get it)
|
||||||
|
* @returns {Date}
|
||||||
|
*/
|
||||||
|
function now() { return new Date(Date.now()); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple TTL memory cache.
|
* Simple TTL memory cache.
|
||||||
*/
|
*/
|
||||||
@ -599,6 +777,7 @@ module.exports = {
|
|||||||
getBasicAuthHeader,
|
getBasicAuthHeader,
|
||||||
extractFileFromZipBuffer,
|
extractFileFromZipBuffer,
|
||||||
getImageBuffers,
|
getImageBuffers,
|
||||||
|
readAndParseJsonlFile,
|
||||||
readAllChunks,
|
readAllChunks,
|
||||||
delay,
|
delay,
|
||||||
deepMerge,
|
deepMerge,
|
||||||
@ -616,6 +795,12 @@ module.exports = {
|
|||||||
mergeObjectWithYaml,
|
mergeObjectWithYaml,
|
||||||
excludeKeysByYaml,
|
excludeKeysByYaml,
|
||||||
trimV1,
|
trimV1,
|
||||||
|
timestampToMoment,
|
||||||
|
calculateDuration,
|
||||||
|
humanizedToDate,
|
||||||
|
maxDate,
|
||||||
|
minDate,
|
||||||
|
now,
|
||||||
Cache,
|
Cache,
|
||||||
makeHttp2Request,
|
makeHttp2Request,
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user