Unify chat timestamps format (#4806)
* Unify chat timestamps format * Handle ISO timestamps in stats.js * Refactor timestamp parsing on server * Switch to ISO timestamps for character/messages creation dates * Fix type error * Early exist in saveGroupChat if group not found * Remove redundant fields from temp.chat export header * Auto-fix char creation date format on edit * Add name to fallback chat file names * Rename parseTimestamp server side function
This commit is contained in:
+1
-4
@@ -1028,9 +1028,6 @@ function verifyCharactersSearchSortRule() {
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {object} Character - A character */
|
||||
/** @typedef {object} Group - A group */
|
||||
|
||||
/**
|
||||
* @typedef {object} Entity - Object representing a display entity
|
||||
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
|
||||
@@ -8248,7 +8245,7 @@ export function select_selected_character(chid, { switchMenu = true } = {}) {
|
||||
$('#talkativeness_slider').val(characters[chid].talkativeness || talkativeness_default);
|
||||
$('#mes_example_textarea').val(characters[chid].mes_example);
|
||||
$('#selected_chat_pole').val(characters[chid].chat);
|
||||
$('#create_date_pole').val(characters[chid].create_date);
|
||||
$('#create_date_pole').val(timestampToMoment(characters[chid].create_date).toISOString());
|
||||
$('#avatar_url_pole').val(characters[chid].avatar);
|
||||
$('#chat_import_avatar_url').val(characters[chid].avatar);
|
||||
$('#chat_import_character_name').val(characters[chid].name);
|
||||
|
||||
@@ -162,43 +162,37 @@ export function shouldSendOnEnter() {
|
||||
}
|
||||
}
|
||||
|
||||
//RossAscends: Added function to format dates used in files and chat timestamps to a humanized format.
|
||||
//Mostly I wanted this to be for file names, but couldn't figure out exactly where the filename save code was as everything seemed to be connected.
|
||||
//Does not break old characters/chats, as the code just uses whatever timestamp exists in the chat.
|
||||
//New chats made with characters will use this new formatting.
|
||||
export function humanizedDateTime() {
|
||||
const now = new Date(Date.now());
|
||||
/**
|
||||
* Gets a humanized date time string from a given timestamp.
|
||||
* @param {number} timestamp Timestamp in milliseconds
|
||||
* @returns {string} Humanized date time string in the format `YYYY-MM-DD@HHhMMmSSsMSms`
|
||||
*/
|
||||
export function humanizedDateTime(timestamp = Date.now()) {
|
||||
const date = new Date(timestamp);
|
||||
const dt = {
|
||||
year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate(),
|
||||
hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds(),
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
day: date.getDate(),
|
||||
hour: date.getHours(),
|
||||
minute: date.getMinutes(),
|
||||
second: date.getSeconds(),
|
||||
millisecond: date.getMilliseconds(),
|
||||
};
|
||||
for (const key in dt) {
|
||||
dt[key] = dt[key].toString().padStart(2, '0');
|
||||
const padLength = key === 'millisecond' ? 3 : 2;
|
||||
dt[key] = dt[key].toString().padStart(padLength, '0');
|
||||
}
|
||||
return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s`;
|
||||
return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`;
|
||||
}
|
||||
|
||||
//this is a common format version to display a timestamp on each chat message
|
||||
//returns something like: June 19, 2023 2:20pm
|
||||
export function getMessageTimeStamp() {
|
||||
const date = Date.now();
|
||||
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const d = new Date(date);
|
||||
const month = months[d.getMonth()];
|
||||
const day = d.getDate();
|
||||
const year = d.getFullYear();
|
||||
let hours = d.getHours();
|
||||
const minutes = ('0' + d.getMinutes()).slice(-2);
|
||||
let meridiem = 'am';
|
||||
if (hours >= 12) {
|
||||
meridiem = 'pm';
|
||||
hours -= 12;
|
||||
}
|
||||
if (hours === 0) {
|
||||
hours = 12;
|
||||
}
|
||||
const formattedDate = month + ' ' + day + ', ' + year + ' ' + hours + ':' + minutes + meridiem;
|
||||
return formattedDate;
|
||||
/**
|
||||
* Gets a timestamp for messages in ISO 8601 format.
|
||||
* @param {number} timestamp - optional timestamp in milliseconds
|
||||
* @returns {string} ISO 8601 formatted timestamp
|
||||
*/
|
||||
export function getMessageTimeStamp(timestamp = Date.now()) {
|
||||
const date = new Date(timestamp);
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
event_types,
|
||||
getCurrentChatId,
|
||||
getRequestHeaders,
|
||||
name1,
|
||||
name2,
|
||||
reloadCurrentChat,
|
||||
saveSettingsDebounced,
|
||||
@@ -2140,13 +2139,9 @@ export function initChatUtilities() {
|
||||
await viewMessageFile(messageId, fileIndex);
|
||||
});
|
||||
|
||||
$(document).on('click', '.assistant_note_export', async function () {
|
||||
$(document).on('click', '.assistant_note_export', async function (_e) {
|
||||
const chatToSave = [
|
||||
{
|
||||
user_name: name1,
|
||||
character_name: name2,
|
||||
chat_metadata: chat_metadata,
|
||||
},
|
||||
{ chat_metadata: chat_metadata },
|
||||
...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
|
||||
];
|
||||
|
||||
|
||||
@@ -2865,7 +2865,7 @@ function getCharacterAvatarUrl() {
|
||||
if (context.groupId) {
|
||||
const groupMembers = context.groups.find(x => x.id === context.groupId)?.members;
|
||||
const lastMessageAvatar = context.chat?.filter(x => !x.is_system && !x.is_user)?.slice(-1)[0]?.original_avatar;
|
||||
const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)]?.avatar : null;
|
||||
const randomMemberAvatar = Array.isArray(groupMembers) ? groupMembers[Math.floor(Math.random() * groupMembers.length)] : null;
|
||||
const avatarToUse = lastMessageAvatar || randomMemberAvatar;
|
||||
return formatCharacterAvatar(avatarToUse);
|
||||
} else {
|
||||
|
||||
@@ -324,7 +324,7 @@ export async function getGroupChat(groupId, reload = false) {
|
||||
* Retrieves the members of a group
|
||||
*
|
||||
* @param {string} [groupId=selected_group] - The ID of the group to retrieve members from. Defaults to the currently selected group.
|
||||
* @returns {import('../script.js').Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned.
|
||||
* @returns {Character[]} An array of character objects representing the members of the group. If the group is not found, an empty array is returned.
|
||||
*/
|
||||
export function getGroupMembers(groupId = selected_group) {
|
||||
const group = groups.find((x) => x.id === groupId);
|
||||
@@ -617,7 +617,11 @@ function resetSelectedGroup() {
|
||||
*/
|
||||
async function saveGroupChat(groupId, shouldSaveGroup, force = false) {
|
||||
const group = groups.find(x => x.id == groupId);
|
||||
const chat_id = group.chat_id;
|
||||
if (!group) {
|
||||
console.warn('Group not found', groupId);
|
||||
return;
|
||||
}
|
||||
const chatId = group.chat_id;
|
||||
group['date_last_chat'] = Date.now();
|
||||
/** @type {ChatHeader} */
|
||||
const chatHeader = {
|
||||
@@ -626,7 +630,7 @@ async function saveGroupChat(groupId, shouldSaveGroup, force = false) {
|
||||
const response = await fetch('/api/chats/group/save', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ id: chat_id, chat: [chatHeader, ...chat], force: force }),
|
||||
body: JSON.stringify({ id: chatId, chat: [chatHeader, ...chat], force: force }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1105,6 +1105,8 @@ function parseTimestamp(timestamp) {
|
||||
ms = typeof ms !== 'undefined' ? `.${ms.padStart(3, '0')}` : '';
|
||||
return `${year.padStart(4, '0')}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T${hour.padStart(2, '0')}:${min.padStart(2, '0')}:${sec.padStart(2, '0')}${ms}Z`;
|
||||
};
|
||||
// 2024-07-12@01h31m37s123ms
|
||||
dtFmt.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s(\d{1,3})ms/ });
|
||||
// 2024-7-12@01h31m37s
|
||||
dtFmt.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s/ });
|
||||
// 2024-6-5 @14h 56m 50s 682ms
|
||||
|
||||
+3
-6
@@ -2,7 +2,7 @@ import { promises as fsPromises } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import urlJoin from 'url-join';
|
||||
import { DEFAULT_AVATAR_PATH } from './constants.js';
|
||||
import { extractFileFromZipBuffer, humanizedISO8601DateTime } from './util.js';
|
||||
import { extractFileFromZipBuffer } from './util.js';
|
||||
|
||||
/**
|
||||
* A parser for BYAF (Backyard Archive Format) files.
|
||||
@@ -267,7 +267,7 @@ export class ByafParser {
|
||||
extensions: { ...(character?.displayName && { 'display_name': character?.displayName }) }, // Preserve display name unmodified using extensions. "display_name" is not used by SillyTavern currently.
|
||||
},
|
||||
// @ts-ignore Non-standard spec extension
|
||||
create_date: humanizedISO8601DateTime(),
|
||||
create_date: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
/**
|
||||
@@ -330,13 +330,10 @@ export class ByafParser {
|
||||
* @returns {string} Chat data
|
||||
*/
|
||||
static getChatFromScenario(scenario, userName, characterName, chatBackgrounds) {
|
||||
const chatStartDate = scenario?.messages?.length == 0 ? humanizedISO8601DateTime() : scenario?.messages?.filter(m => 'createdAt' in m)[0].createdAt;
|
||||
const chatStartDate = scenario?.messages?.length == 0 ? new Date().toISOString() : scenario?.messages?.filter(m => 'createdAt' in m)[0].createdAt;
|
||||
const chatBackground = chatBackgrounds.find(bg => bg.paths.includes(scenario?.backgroundImage || ''))?.name || '';
|
||||
/** @type {object[]} */
|
||||
const chat = [{
|
||||
user_name: userName,
|
||||
character_name: characterName,
|
||||
create_date: chatStartDate,
|
||||
chat_metadata: {
|
||||
scenario: scenario?.narrative ?? '',
|
||||
mes_example: ByafParser.formatExampleMessages(scenario?.exampleMessages),
|
||||
|
||||
+18
-21
@@ -14,7 +14,7 @@ import storage from 'node-persist';
|
||||
|
||||
import { AVATAR_WIDTH, AVATAR_HEIGHT, DEFAULT_AVATAR_PATH } from '../constants.js';
|
||||
import { default as validateAvatarUrlMiddleware, getFileNameValidationFunction } from '../middleware/validateFileName.js';
|
||||
import { deepMerge, humanizedISO8601DateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue, mutateJsonString, clientRelativePath, getUniqueName, sanitizeSafeCharacterReplacements } from '../util.js';
|
||||
import { deepMerge, humanizedDateTime, tryParse, extractFileFromZipBuffer, MemoryLimitedMap, getConfigValue, mutateJsonString, clientRelativePath, getUniqueName, sanitizeSafeCharacterReplacements } from '../util.js';
|
||||
import { TavernCardValidator } from '../validator/TavernCardValidator.js';
|
||||
import { parse, read, write } from '../character-card-parser.js';
|
||||
import { readWorldInfoFile } from './worldinfo.js';
|
||||
@@ -414,7 +414,7 @@ const processCharacter = async (item, directories, { shallow }) => {
|
||||
character['json_data'] = imgData;
|
||||
const charStat = fs.statSync(path.join(directories.characters, item));
|
||||
character['date_added'] = charStat.ctimeMs;
|
||||
character['create_date'] = jsonObject['create_date'] || humanizedISO8601DateTime(charStat.ctimeMs);
|
||||
character['create_date'] = jsonObject['create_date'] || new Date(Math.round(charStat.ctimeMs)).toISOString();
|
||||
const chatsDirectory = path.join(directories.chats, item.replace('.png', ''));
|
||||
|
||||
const { chatSize, dateLastChat } = calculateChatSize(chatsDirectory);
|
||||
@@ -452,7 +452,7 @@ function getCharaCardV2(jsonObject, directories, hoistDate = true) {
|
||||
jsonObject = convertToV2(jsonObject, directories);
|
||||
|
||||
if (hoistDate && !jsonObject.create_date) {
|
||||
jsonObject.create_date = humanizedISO8601DateTime();
|
||||
jsonObject.create_date = new Date().toISOString();
|
||||
}
|
||||
} else {
|
||||
jsonObject = readFromV2(jsonObject);
|
||||
@@ -486,7 +486,7 @@ function convertToV2(char, directories) {
|
||||
depth_prompt_role: char.depth_prompt_role,
|
||||
}, directories);
|
||||
|
||||
result.chat = char.chat ?? humanizedISO8601DateTime();
|
||||
result.chat = char.chat ?? `${char.name} - ${humanizedDateTime()}`;
|
||||
result.create_date = char.create_date;
|
||||
|
||||
return result;
|
||||
@@ -551,7 +551,7 @@ function readFromV2(char) {
|
||||
char[charField] = v2Value;
|
||||
});
|
||||
|
||||
char['chat'] = char['chat'] ?? humanizedISO8601DateTime();
|
||||
char['chat'] = char['chat'] ?? `${char.name} - ${humanizedDateTime()}`;
|
||||
|
||||
return char;
|
||||
}
|
||||
@@ -587,7 +587,7 @@ function charaFormatData(data, directories) {
|
||||
// Old ST extension fields (for backward compatibility, will be deprecated)
|
||||
_.set(char, 'creatorcomment', data.creator_notes || '');
|
||||
_.set(char, 'avatar', 'none');
|
||||
_.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
|
||||
_.set(char, 'chat', data.ch_name + ' - ' + humanizedDateTime());
|
||||
_.set(char, 'talkativeness', data.talkativeness || 0.5);
|
||||
_.set(char, 'fav', data.fav == 'true');
|
||||
_.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []);
|
||||
@@ -624,9 +624,6 @@ function charaFormatData(data, directories) {
|
||||
_.set(char, 'data.extensions.depth_prompt.prompt', data.depth_prompt_prompt ?? '');
|
||||
_.set(char, 'data.extensions.depth_prompt.depth', depth_value);
|
||||
_.set(char, 'data.extensions.depth_prompt.role', role_value);
|
||||
//_.set(char, 'data.extensions.create_date', humanizedISO8601DateTime());
|
||||
//_.set(char, 'data.extensions.avatar', 'none');
|
||||
//_.set(char, 'data.extensions.chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
|
||||
|
||||
// V3 fields
|
||||
_.set(char, 'data.group_only_greetings', data.group_only_greetings ?? []);
|
||||
@@ -746,8 +743,8 @@ async function importFromYaml(uploadPath, context, preservedFileName) {
|
||||
'name': yamlData.name,
|
||||
'description': yamlData.context ?? '',
|
||||
'first_mes': yamlData.greeting ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'chat': `${yamlData.name} - ${humanizedISO8601DateTime()}`,
|
||||
'create_date': new Date().toISOString(),
|
||||
'chat': `${yamlData.name} - ${humanizedDateTime()}`,
|
||||
'personality': '',
|
||||
'creatorcomment': '',
|
||||
'avatar': 'none',
|
||||
@@ -800,7 +797,7 @@ async function importFromCharX(uploadPath, { request }, preservedFileName) {
|
||||
}
|
||||
|
||||
unsetPrivateFields(card);
|
||||
card['create_date'] = humanizedISO8601DateTime();
|
||||
card['create_date'] = new Date().toISOString();
|
||||
card.name = sanitize(card.name);
|
||||
const fileName = preservedFileName || getPngName(card.name, request.user.directories);
|
||||
const result = await writeCharacterData(avatar, JSON.stringify(card), fileName, request);
|
||||
@@ -822,7 +819,7 @@ async function importFromByaf(uploadPath, { request }, preservedFileName) {
|
||||
* @param {Partial<ByafScenario>} scenario
|
||||
*/
|
||||
const createChatAsCurrentPersona = (scenario) => {
|
||||
const chatName = sanitize(`${scenario.title || card.name} - ${humanizedISO8601DateTime()} imported.jsonl`, { replacement: sanitizeSafeCharacterReplacements });
|
||||
const chatName = sanitize(`${scenario.title || card.name} - ${humanizedDateTime()} imported.jsonl`, { replacement: sanitizeSafeCharacterReplacements });
|
||||
const filePath = path.join(request.user.directories.chats, path.basename(fileName), chatName);
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
@@ -898,7 +895,7 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
|
||||
importRisuSprites(request.user.directories, jsonData);
|
||||
unsetPrivateFields(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
jsonData['create_date'] = humanizedISO8601DateTime();
|
||||
jsonData['create_date'] = new Date().toISOString();
|
||||
const pngName = preservedFileName || getPngName(jsonData.data?.name || jsonData.name, request.user.directories);
|
||||
const char = JSON.stringify(jsonData);
|
||||
const result = await writeCharacterData(DEFAULT_AVATAR_PATH, char, pngName, request);
|
||||
@@ -917,10 +914,10 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
|
||||
'personality': jsonData.personality ?? '',
|
||||
'first_mes': jsonData.first_mes ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'chat': jsonData.name + ' - ' + humanizedDateTime(),
|
||||
'mes_example': jsonData.mes_example ?? '',
|
||||
'scenario': jsonData.scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'create_date': new Date().toISOString(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
@@ -943,10 +940,10 @@ async function importFromJson(uploadPath, { request }, preservedFileName) {
|
||||
'personality': '',
|
||||
'first_mes': jsonData.char_greeting ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'chat': jsonData.name + ' - ' + humanizedDateTime(),
|
||||
'mes_example': jsonData.example_dialogue ?? '',
|
||||
'scenario': jsonData.world_scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'create_date': new Date().toISOString(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
@@ -981,7 +978,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
|
||||
importRisuSprites(request.user.directories, jsonData);
|
||||
unsetPrivateFields(jsonData);
|
||||
jsonData = readFromV2(jsonData);
|
||||
jsonData['create_date'] = humanizedISO8601DateTime();
|
||||
jsonData['create_date'] = new Date().toISOString();
|
||||
const char = JSON.stringify(jsonData);
|
||||
const result = await writeCharacterData(uploadPath, char, pngName, request);
|
||||
fs.unlinkSync(uploadPath);
|
||||
@@ -1000,10 +997,10 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
|
||||
'personality': jsonData.personality ?? '',
|
||||
'first_mes': jsonData.first_mes ?? '',
|
||||
'avatar': 'none',
|
||||
'chat': jsonData.name + ' - ' + humanizedISO8601DateTime(),
|
||||
'chat': jsonData.name + ' - ' + humanizedDateTime(),
|
||||
'mes_example': jsonData.mes_example ?? '',
|
||||
'scenario': jsonData.scenario ?? '',
|
||||
'create_date': humanizedISO8601DateTime(),
|
||||
'create_date': new Date().toISOString(),
|
||||
'talkativeness': jsonData.talkativeness ?? 0.5,
|
||||
'creator': jsonData.creator ?? '',
|
||||
'tags': jsonData.tags ?? '',
|
||||
|
||||
+22
-24
@@ -11,7 +11,7 @@ import _ from 'lodash';
|
||||
import validateAvatarUrlMiddleware from '../middleware/validateFileName.js';
|
||||
import {
|
||||
getConfigValue,
|
||||
humanizedISO8601DateTime,
|
||||
humanizedDateTime,
|
||||
tryParse,
|
||||
generateTimestamp,
|
||||
removeOldBackups,
|
||||
@@ -106,9 +106,7 @@ process.on('exit', () => {
|
||||
function importOobaChat(userName, characterName, jsonData) {
|
||||
/** @type {object[]} */
|
||||
const chat = [{
|
||||
user_name: userName,
|
||||
character_name: characterName,
|
||||
create_date: humanizedISO8601DateTime(),
|
||||
chat_metadata: {},
|
||||
}];
|
||||
|
||||
for (const arr of jsonData.data_visible) {
|
||||
@@ -116,8 +114,9 @@ function importOobaChat(userName, characterName, jsonData) {
|
||||
const userMessage = {
|
||||
name: userName,
|
||||
is_user: true,
|
||||
send_date: humanizedISO8601DateTime(),
|
||||
send_date: new Date().toISOString(),
|
||||
mes: arr[0],
|
||||
extra: {},
|
||||
};
|
||||
chat.push(userMessage);
|
||||
}
|
||||
@@ -125,8 +124,9 @@ function importOobaChat(userName, characterName, jsonData) {
|
||||
const charMessage = {
|
||||
name: characterName,
|
||||
is_user: false,
|
||||
send_date: humanizedISO8601DateTime(),
|
||||
send_date: new Date().toISOString(),
|
||||
mes: arr[1],
|
||||
extra: {},
|
||||
};
|
||||
chat.push(charMessage);
|
||||
}
|
||||
@@ -145,9 +145,7 @@ function importOobaChat(userName, characterName, jsonData) {
|
||||
function importAgnaiChat(userName, characterName, jsonData) {
|
||||
/** @type {object[]} */
|
||||
const chat = [{
|
||||
user_name: userName,
|
||||
character_name: characterName,
|
||||
create_date: humanizedISO8601DateTime(),
|
||||
chat_metadata: {},
|
||||
}];
|
||||
|
||||
for (const message of jsonData.messages) {
|
||||
@@ -155,8 +153,9 @@ function importAgnaiChat(userName, characterName, jsonData) {
|
||||
chat.push({
|
||||
name: isUser ? userName : characterName,
|
||||
is_user: isUser,
|
||||
send_date: humanizedISO8601DateTime(),
|
||||
send_date: new Date().toISOString(),
|
||||
mes: message.msg,
|
||||
extra: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -178,16 +177,15 @@ function importCAIChat(userName, characterName, jsonData) {
|
||||
*/
|
||||
function convert(history) {
|
||||
const starter = {
|
||||
user_name: userName,
|
||||
character_name: characterName,
|
||||
create_date: humanizedISO8601DateTime(),
|
||||
chat_metadata: {},
|
||||
};
|
||||
|
||||
const historyData = history.msgs.map((msg) => ({
|
||||
name: msg.src.is_human ? userName : characterName,
|
||||
is_user: msg.src.is_human,
|
||||
send_date: humanizedISO8601DateTime(),
|
||||
send_date: new Date().toISOString(),
|
||||
mes: msg.text,
|
||||
extra: {},
|
||||
}));
|
||||
|
||||
return [starter, ...historyData];
|
||||
@@ -215,7 +213,8 @@ function importKoboldLiteChat(_userName, _characterName, data) {
|
||||
name: isUser ? header.user_name : header.character_name,
|
||||
is_user: isUser,
|
||||
mes: msg.replaceAll(inputToken, '').replaceAll(outputToken, '').trim(),
|
||||
send_date: Date.now(),
|
||||
send_date: new Date().toISOString(),
|
||||
extra: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -276,9 +275,7 @@ function flattenChubChat(userName, characterName, lines) {
|
||||
function importRisuChat(userName, characterName, jsonData) {
|
||||
/** @type {object[]} */
|
||||
const chat = [{
|
||||
user_name: userName,
|
||||
character_name: characterName,
|
||||
create_date: humanizedISO8601DateTime(),
|
||||
chat_metadata: {},
|
||||
}];
|
||||
|
||||
for (const message of jsonData.data.message) {
|
||||
@@ -286,8 +283,9 @@ function importRisuChat(userName, characterName, jsonData) {
|
||||
chat.push({
|
||||
name: message.name ?? (isUser ? userName : characterName),
|
||||
is_user: isUser,
|
||||
send_date: Number(message.time ?? Date.now()),
|
||||
send_date: new Date(Number(message.time ?? Date.now())).toISOString(),
|
||||
mes: message.data ?? '',
|
||||
extra: {},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -417,7 +415,7 @@ export async function getChatInfo(pathToFile, additionalData = {}, withMetadata
|
||||
if (jsonData && (jsonData.name || jsonData.character_name || jsonData.chat_metadata)) {
|
||||
chatData.chat_items = (itemCounter - 1);
|
||||
chatData.mes = jsonData['mes'] || '[The message is empty]';
|
||||
chatData.last_mes = jsonData['send_date'] || stats.mtimeMs;
|
||||
chatData.last_mes = jsonData['send_date'] || new Date(Math.round(stats.mtimeMs)).toISOString();
|
||||
|
||||
res(chatData);
|
||||
} else {
|
||||
@@ -623,7 +621,7 @@ router.post('/group/import', function (request, response) {
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const chatname = humanizedISO8601DateTime();
|
||||
const chatname = humanizedDateTime();
|
||||
const pathToUpload = path.join(filedata.destination, filedata.filename);
|
||||
const pathToNewFile = path.join(request.user.directories.groupChats, `${chatname}.jsonl`);
|
||||
fs.copyFileSync(pathToUpload, pathToNewFile);
|
||||
@@ -675,7 +673,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response)
|
||||
}
|
||||
|
||||
const handleChat = (chat) => {
|
||||
const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
|
||||
const fileName = `${characterName} - ${humanizedDateTime()} imported.jsonl`;
|
||||
const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
|
||||
fileNames.push(fileName);
|
||||
writeFileAtomicSync(filePath, chat, 'utf8');
|
||||
@@ -714,7 +712,7 @@ router.post('/import', validateAvatarUrlMiddleware, function (request, response)
|
||||
console.warn('Failed to flatten Chub Chat data: ', error);
|
||||
}
|
||||
|
||||
const fileName = `${characterName} - ${humanizedISO8601DateTime()} imported.jsonl`;
|
||||
const fileName = `${characterName} - ${humanizedDateTime()} imported.jsonl`;
|
||||
const filePath = path.join(request.user.directories.chats, avatarUrl, fileName);
|
||||
fileNames.push(fileName);
|
||||
if (flattenedChat !== data) {
|
||||
@@ -879,7 +877,7 @@ router.post('/search', validateAvatarUrlMiddleware, function (request, response)
|
||||
}
|
||||
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const lastMesDate = lastMessage?.send_date || Math.round(fs.statSync(chatFile.path).mtimeMs);
|
||||
const lastMesDate = lastMessage?.send_date || new Date(fs.statSync(chatFile.path).mtimeMs).toISOString();
|
||||
|
||||
// If no search query, just return metadata
|
||||
if (!query) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import express from 'express';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { sync as writeFileAtomicSync, default as writeFileAtomic } from 'write-file-atomic';
|
||||
|
||||
import { color, humanizedISO8601DateTime, tryParse } from '../util.js';
|
||||
import { color, tryParse } from '../util.js';
|
||||
import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
|
||||
|
||||
export const router = express.Router();
|
||||
@@ -127,7 +127,7 @@ router.post('/all', (request, response) => {
|
||||
const group = JSON.parse(fileContents);
|
||||
const groupStat = fs.statSync(filePath);
|
||||
group['date_added'] = groupStat.birthtimeMs;
|
||||
group['create_date'] = humanizedISO8601DateTime(groupStat.birthtimeMs);
|
||||
group['create_date'] = new Date(groupStat.birthtimeMs).toISOString();
|
||||
|
||||
let chat_size = 0;
|
||||
let date_last_chat = 0;
|
||||
|
||||
+67
-65
@@ -12,6 +12,21 @@ import { getAllUserHandles, getUserDirectories } from '../users.js';
|
||||
|
||||
const STATS_FILE = 'stats.json';
|
||||
|
||||
const monthNames = [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December',
|
||||
];
|
||||
|
||||
/**
|
||||
* @type {Map<string, Object>} The stats object for each user.
|
||||
*/
|
||||
@@ -23,92 +38,79 @@ const TIMESTAMPS = new Map();
|
||||
|
||||
/**
|
||||
* 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"
|
||||
* 1. Date.now timestamps (the number of milliseconds since the Unix Epoch)
|
||||
* 2. ST "humanized" timestamps, formatted like `YYYY-MM-DD@HHhMMmSSsMSms`
|
||||
* 3. Date strings in the format `Month DD, YYYY H:MMam/pm`
|
||||
* 4. ISO 8601 formatted strings
|
||||
* 5. Date objects
|
||||
*
|
||||
* 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.
|
||||
* @param {string|number|Date} timestamp - The timestamp to convert.
|
||||
* @returns {number} The timestamp in milliseconds since the Unix Epoch, or 0 if the input cannot be parsed.
|
||||
*
|
||||
* @example
|
||||
* // Unix timestamp
|
||||
* timestampToMoment(1609459200);
|
||||
* parseTimestamp(1609459200);
|
||||
* // ST humanized timestamp
|
||||
* timestampToMoment("2021-01-01 \@00h 00m 00s 000ms");
|
||||
* parseTimestamp("2021-01-01 \@00h 00m 00s 000ms");
|
||||
* // Date string
|
||||
* timestampToMoment("January 1, 2021 12:00am");
|
||||
* parseTimestamp("January 1, 2021 12:00am");
|
||||
*/
|
||||
function timestampToMoment(timestamp) {
|
||||
function parseTimestamp(timestamp) {
|
||||
if (!timestamp) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (typeof timestamp === 'number') {
|
||||
return timestamp;
|
||||
// Date object
|
||||
if (timestamp instanceof Date) {
|
||||
return timestamp.getTime();
|
||||
}
|
||||
|
||||
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.padStart(3, '0')}Z`;
|
||||
};
|
||||
const isoTimestamp1 = timestamp.replace(pattern1, replacement1);
|
||||
if (!isNaN(Number(new Date(isoTimestamp1)))) {
|
||||
return new Date(isoTimestamp1).getTime();
|
||||
// Unix time
|
||||
if (typeof timestamp === 'number' || /^\d+$/.test(timestamp)) {
|
||||
const unixTime = Number(timestamp);
|
||||
const isValid = Number.isFinite(unixTime) && !Number.isNaN(unixTime) && unixTime >= 0;
|
||||
if (!isValid) return 0;
|
||||
return new Date(unixTime).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',
|
||||
];
|
||||
// ISO 8601 format
|
||||
const isoPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
|
||||
if (isoPattern.test(timestamp)) {
|
||||
return new Date(timestamp).getTime();
|
||||
}
|
||||
|
||||
let dateFormats = [];
|
||||
|
||||
// meridiem-based format
|
||||
const convertFromMeridiemBased = (_, month, day, year, hour, minute, meridiem) => {
|
||||
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 hour24 = meridiem.toLowerCase() === 'pm' ? (parseInt(hour, 10) % 12) + 12 : parseInt(hour, 10) % 12;
|
||||
return `${year}-${monthNum}-${day.padStart(2, '0')}T${hour24.toString().padStart(2, '0')}:${minute.padStart(2, '0')}:00`;
|
||||
};
|
||||
const isoTimestamp2 = timestamp.replace(pattern2, replacement2);
|
||||
if (!isNaN(Number(new Date(isoTimestamp2)))) {
|
||||
return new Date(isoTimestamp2).getTime();
|
||||
// June 19, 2023 2:20pm
|
||||
dateFormats.push({ callback: convertFromMeridiemBased, pattern: /(\w+)\s(\d{1,2}),\s(\d{4})\s(\d{1,2}):(\d{1,2})(am|pm)/i });
|
||||
|
||||
// ST "humanized" format patterns
|
||||
const convertFromHumanized = (_, year, month, day, hour, min, sec, ms) => {
|
||||
ms = typeof ms !== 'undefined' ? `.${ms.padStart(3, '0')}` : '';
|
||||
return `${year.padStart(4, '0')}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T${hour.padStart(2, '0')}:${min.padStart(2, '0')}:${sec.padStart(2, '0')}${ms}Z`;
|
||||
};
|
||||
// 2024-07-12@01h31m37s123ms
|
||||
dateFormats.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s(\d{1,3})ms/ });
|
||||
// 2024-7-12@01h31m37s
|
||||
dateFormats.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2})@(\d{1,2})h(\d{1,2})m(\d{1,2})s/ });
|
||||
// 2024-6-5 @14h 56m 50s 682ms
|
||||
dateFormats.push({ callback: convertFromHumanized, pattern: /(\d{4})-(\d{1,2})-(\d{1,2}) @(\d{1,2})h (\d{1,2})m (\d{1,2})s (\d{1,3})ms/ });
|
||||
|
||||
for (const x of dateFormats) {
|
||||
const rgxMatch = timestamp.match(x.pattern);
|
||||
if (!rgxMatch) continue;
|
||||
const isoTimestamp = x.callback(...rgxMatch);
|
||||
return new Date(isoTimestamp).getTime();
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -416,7 +418,7 @@ function calculateTotalGenTimeAndWordCount(
|
||||
// If this is the first user message, set the first chat time
|
||||
if (json.is_user) {
|
||||
//get min between firstChatTime and timestampToMoment(json.send_date)
|
||||
firstChatTime = Math.min(timestampToMoment(json.send_date), firstChatTime);
|
||||
firstChatTime = Math.min(parseTimestamp(json.send_date), firstChatTime);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error parsing line ${line}: ${error}`);
|
||||
|
||||
+21
-11
@@ -389,17 +389,27 @@ export function uuidv4() {
|
||||
});
|
||||
}
|
||||
|
||||
export function humanizedISO8601DateTime(date) {
|
||||
let baseDate = typeof date === 'number' ? new Date(date) : new Date();
|
||||
let humanYear = baseDate.getFullYear();
|
||||
let humanMonth = (baseDate.getMonth() + 1);
|
||||
let humanDate = baseDate.getDate();
|
||||
let humanHour = (baseDate.getHours() < 10 ? '0' : '') + baseDate.getHours();
|
||||
let humanMinute = (baseDate.getMinutes() < 10 ? '0' : '') + baseDate.getMinutes();
|
||||
let humanSecond = (baseDate.getSeconds() < 10 ? '0' : '') + baseDate.getSeconds();
|
||||
let humanMillisecond = (baseDate.getMilliseconds() < 10 ? '0' : '') + baseDate.getMilliseconds();
|
||||
let HumanizedDateTime = (humanYear + '-' + humanMonth + '-' + humanDate + ' @' + humanHour + 'h ' + humanMinute + 'm ' + humanSecond + 's ' + humanMillisecond + 'ms');
|
||||
return HumanizedDateTime;
|
||||
/**
|
||||
* Gets a humanized date time string from a given timestamp.
|
||||
* @param {number} timestamp Timestamp in milliseconds
|
||||
* @returns {string} Humanized date time string in the format `YYYY-MM-DD@HHhMMmSSsMSms`
|
||||
*/
|
||||
export function humanizedDateTime(timestamp = Date.now()) {
|
||||
const date = new Date(timestamp);
|
||||
const dt = {
|
||||
year: date.getFullYear(),
|
||||
month: date.getMonth() + 1,
|
||||
day: date.getDate(),
|
||||
hour: date.getHours(),
|
||||
minute: date.getMinutes(),
|
||||
second: date.getSeconds(),
|
||||
millisecond: date.getMilliseconds(),
|
||||
};
|
||||
for (const key in dt) {
|
||||
const padLength = key === 'millisecond' ? 3 : 2;
|
||||
dt[key] = dt[key].toString().padStart(padLength, '0');
|
||||
}
|
||||
return `${dt.year}-${dt.month}-${dt.day}@${dt.hour}h${dt.minute}m${dt.second}s${dt.millisecond}ms`;
|
||||
}
|
||||
|
||||
export function tryParse(str) {
|
||||
|
||||
Reference in New Issue
Block a user