mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
[wip] Welcome screen prototype
This commit is contained in:
176
public/css/welcome.css
Normal file
176
public/css/welcome.css
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
.welcomePanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.bubblechat .welcomePanel {
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--SmartThemeBotMesBlurTintColor);
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .welcomeHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .recentChatsTitle {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: calc(var(--mainFontSize) * 1.1);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .welcomeHeaderTitle {
|
||||||
|
margin: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .welcomeHeaderVersionDisplay {
|
||||||
|
font-size: calc(var(--mainFontSize) * 1.2);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .welcomeHeaderLogo {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .welcomeShortcuts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .welcomeShortcuts .welcomeShortcutsSeparator {
|
||||||
|
margin: 0 2px;
|
||||||
|
color: var(--SmartThemeBorderColor);
|
||||||
|
font-size: calc(var(--mainFontSize) * 1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .welcomePanelLoader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomePanel .recentChatList .noRecentChat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
transition: filter 0.2s;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 10px;
|
||||||
|
border: 1px solid var(--SmartThemeBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .avatar {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat.selected {
|
||||||
|
background-color: var(--cobalt30a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat:hover {
|
||||||
|
background-color: var(--white30a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .recentChatInfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .chatNameContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
font-size: calc(var(--mainFontSize) * 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .chatNameContainer .chatName {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .chatMessageContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: calc(var(--mainFontSize) * 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .chatMessageContainer .chatMessage {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .chatStats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .chatStats .counterBlock {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcomeRecent .recentChatList .recentChat .chatStats .counterBlock::after {
|
||||||
|
content: "|";
|
||||||
|
color: var(--SmartThemeBorderColor);
|
||||||
|
font-size: calc(var(--mainFontSize) * 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
.welcomePanel .welcomeShortcuts a span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
@ -282,6 +282,7 @@ import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
|
|||||||
import { getContext } from './scripts/st-context.js';
|
import { getContext } from './scripts/st-context.js';
|
||||||
import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
|
import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
|
||||||
import { accountStorage } from './scripts/util/AccountStorage.js';
|
import { accountStorage } from './scripts/util/AccountStorage.js';
|
||||||
|
import { initWelcomeScreen } from './scripts/welcome-screen.js';
|
||||||
|
|
||||||
// API OBJECT FOR EXTERNAL WIRING
|
// API OBJECT FOR EXTERNAL WIRING
|
||||||
globalThis.SillyTavern = {
|
globalThis.SillyTavern = {
|
||||||
@ -563,7 +564,7 @@ let chat_create_date = '';
|
|||||||
let firstRun = false;
|
let firstRun = false;
|
||||||
let settingsReady = false;
|
let settingsReady = false;
|
||||||
let currentVersion = '0.0.0';
|
let currentVersion = '0.0.0';
|
||||||
let displayVersion = 'SillyTavern';
|
export let displayVersion = 'SillyTavern';
|
||||||
|
|
||||||
let generatedPromptCache = '';
|
let generatedPromptCache = '';
|
||||||
let generation_started = new Date();
|
let generation_started = new Date();
|
||||||
@ -983,8 +984,6 @@ async function firstLoadInit() {
|
|||||||
ToolManager.initToolSlashCommands();
|
ToolManager.initToolSlashCommands();
|
||||||
await initPresetManager();
|
await initPresetManager();
|
||||||
await getSystemMessages();
|
await getSystemMessages();
|
||||||
sendSystemMessage(system_message_types.WELCOME);
|
|
||||||
sendSystemMessage(system_message_types.WELCOME_PROMPT);
|
|
||||||
await getSettings();
|
await getSettings();
|
||||||
initKeyboard();
|
initKeyboard();
|
||||||
initDynamicStyles();
|
initDynamicStyles();
|
||||||
@ -1009,6 +1008,7 @@ async function firstLoadInit() {
|
|||||||
initSettingsSearch();
|
initSettingsSearch();
|
||||||
initBulkEdit();
|
initBulkEdit();
|
||||||
initReasoning();
|
initReasoning();
|
||||||
|
initWelcomeScreen();
|
||||||
await initScrapers();
|
await initScrapers();
|
||||||
initCustomSelectedSamplers();
|
initCustomSelectedSamplers();
|
||||||
addDebugFunctions();
|
addDebugFunctions();
|
||||||
@ -11176,8 +11176,6 @@ jQuery(async function () {
|
|||||||
selected_button = 'characters';
|
selected_button = 'characters';
|
||||||
$('#rm_button_selected_ch').children('h2').text('');
|
$('#rm_button_selected_ch').children('h2').text('');
|
||||||
select_rm_characters();
|
select_rm_characters();
|
||||||
sendSystemMessage(system_message_types.WELCOME);
|
|
||||||
sendSystemMessage(system_message_types.WELCOME_PROMPT);
|
|
||||||
await getClientVersion();
|
await getClientVersion();
|
||||||
await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
|
await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId());
|
||||||
} else {
|
} else {
|
||||||
|
71
public/scripts/templates/welcomePanel.html
Normal file
71
public/scripts/templates/welcomePanel.html
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<div class="welcomePanel">
|
||||||
|
<div class="welcomeHeaderTitle">
|
||||||
|
<img src="img/logo.png" alt="SillyTavern Logo" class="welcomeHeaderLogo">
|
||||||
|
<span class="welcomeHeaderVersionDisplay">{{version}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="welcomeHeader">
|
||||||
|
<div class="recentChatsTitle" data-i18n="Recent Chats">
|
||||||
|
Recent Chats
|
||||||
|
</div>
|
||||||
|
<div class="welcomeShortcuts">
|
||||||
|
<a class="menu_button menu_button_icon" target="_blank" href="https://docs.sillytavern.app/">
|
||||||
|
<i class="fa-solid fa-question-circle"></i>
|
||||||
|
<span data-i18n="Docs">Docs</span>
|
||||||
|
</a>
|
||||||
|
<a class="menu_button menu_button_icon" target="_blank" href="https://github.com/SillyTavern/SillyTavern">
|
||||||
|
<i class="fa-brands fa-github"></i>
|
||||||
|
<span data-i18n="GitHub">GitHub</span>
|
||||||
|
</a>
|
||||||
|
<a class="menu_button menu_button_icon" target="_blank" href="https://discord.gg/sillytavern">
|
||||||
|
<i class="fa-brands fa-discord"></i>
|
||||||
|
<span data-i18n="Discord">Discord</span>
|
||||||
|
</a>
|
||||||
|
<span class="welcomeShortcutsSeparator">|</span>
|
||||||
|
<button class="openTemporaryChat menu_button menu_button_icon">
|
||||||
|
<i class="fa-solid fa-comment-dots"></i>
|
||||||
|
<span data-i18n="Temporary Chat">Temporary Chat</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="welcomeRecent">
|
||||||
|
<div class="recentChatList">
|
||||||
|
{{#if empty}}
|
||||||
|
<div class="noRecentChat">
|
||||||
|
<i class="fa-solid fa-comment-dots"></i>
|
||||||
|
<span data-i18n="No recent chats">No recent chats</span>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{#each chats}}
|
||||||
|
{{#with this}}
|
||||||
|
<div class="recentChat" data-file="{{chat_name}}" data-avatar="{{avatar}}">
|
||||||
|
<div class="avatar" title="{{char_name}}">
|
||||||
|
<img src="{{char_thumbnail}}" alt="{{char_name}}">
|
||||||
|
</div>
|
||||||
|
<div class="recentChatInfo">
|
||||||
|
<div class="chatNameContainer">
|
||||||
|
<div class="chatName" title="{{file_name}}">
|
||||||
|
<strong class="characterName">{{char_name}}</strong>
|
||||||
|
<span>–</span>
|
||||||
|
<span>{{chat_name}}</span>
|
||||||
|
</div>
|
||||||
|
<small class="chatDate" title="{{date_full}}">{{date_short}}</small>
|
||||||
|
</div>
|
||||||
|
<div class="chatMessageContainer">
|
||||||
|
<div class="chatMessage" title="{{mes}}">
|
||||||
|
{{mes}}
|
||||||
|
</div>
|
||||||
|
<div class="chatStats">
|
||||||
|
<div class="counterBlock">
|
||||||
|
<i class="fa-solid fa-comment fa-xs"></i>
|
||||||
|
<small>{{chat_items}}</small>
|
||||||
|
</div>
|
||||||
|
<small class="fileSize">{{file_size}}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/with}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
135
public/scripts/welcome-screen.js
Normal file
135
public/scripts/welcome-screen.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
characters,
|
||||||
|
displayVersion,
|
||||||
|
event_types,
|
||||||
|
eventSource,
|
||||||
|
getCurrentChatId,
|
||||||
|
getRequestHeaders,
|
||||||
|
getThumbnailUrl,
|
||||||
|
openCharacterChat,
|
||||||
|
selectCharacterById,
|
||||||
|
sendSystemMessage,
|
||||||
|
system_message_types,
|
||||||
|
} from '../script.js';
|
||||||
|
import { t } from './i18n.js';
|
||||||
|
import { renderTemplateAsync } from './templates.js';
|
||||||
|
import { timestampToMoment } from './utils.js';
|
||||||
|
|
||||||
|
export async function openWelcomeScreen() {
|
||||||
|
const currentChatId = getCurrentChatId();
|
||||||
|
if (currentChatId !== undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendWelcomePanel();
|
||||||
|
sendSystemMessage(system_message_types.WELCOME_PROMPT);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendWelcomePanel() {
|
||||||
|
try {
|
||||||
|
const chatElement = document.getElementById('chat');
|
||||||
|
if (!chatElement) {
|
||||||
|
console.error('Chat element not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chats = await getRecentChats();
|
||||||
|
const templateData = {
|
||||||
|
chats,
|
||||||
|
empty: !chats.length ,
|
||||||
|
version: displayVersion,
|
||||||
|
};
|
||||||
|
const template = await renderTemplateAsync('welcomePanel', templateData);
|
||||||
|
const fragment = document.createRange().createContextualFragment(template);
|
||||||
|
fragment.querySelectorAll('.recentChat').forEach((item) => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
const avatarId = item.getAttribute('data-avatar');
|
||||||
|
const fileName = item.getAttribute('data-file');
|
||||||
|
if (avatarId && fileName) {
|
||||||
|
void openRecentChat(avatarId, fileName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => {
|
||||||
|
toastr.info('This button does nothing at the moment. Try again later.');
|
||||||
|
});
|
||||||
|
chatElement.append(fragment.firstChild);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Welcome screen error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a recent chat.
|
||||||
|
* @param {string} avatarId Avatar file name
|
||||||
|
* @param {string} fileName Chat file name
|
||||||
|
*/
|
||||||
|
async function openRecentChat(avatarId, fileName) {
|
||||||
|
const characterId = characters.findIndex(x => x.avatar === avatarId);
|
||||||
|
if (characterId === -1) {
|
||||||
|
console.error(`Character not found for avatar ID: ${avatarId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await selectCharacterById(characterId);
|
||||||
|
await openCharacterChat(fileName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening recent chat:', error);
|
||||||
|
toastr.error(t`Failed to open recent chat. See console for details.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of recent chats from the server.
|
||||||
|
* @returns {Promise<RecentChat[]>} List of recent chats
|
||||||
|
*
|
||||||
|
* @typedef {object} RecentChat
|
||||||
|
* @property {string} file_name Name of the chat file
|
||||||
|
* @property {string} chat_name Name of the chat (without extension)
|
||||||
|
* @property {string} file_size Size of the chat file
|
||||||
|
* @property {number} chat_items Number of items in the chat
|
||||||
|
* @property {string} mes Last message content
|
||||||
|
* @property {number} last_mes Timestamp of the last message
|
||||||
|
* @property {string} avatar Avatar URL
|
||||||
|
* @property {string} char_thumbnail Thumbnail URL
|
||||||
|
* @property {string} char_name Character name
|
||||||
|
* @property {string} date_short Date in short format
|
||||||
|
* @property {string} date_long Date in long format
|
||||||
|
*/
|
||||||
|
async function getRecentChats() {
|
||||||
|
const response = await fetch('/api/characters/recent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch recent chats');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {RecentChat[]} */
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
data.sort((a, b) => b.last_mes - a.last_mes).forEach((chat, index) => {
|
||||||
|
const character = characters.find(x => x.avatar === chat.avatar);
|
||||||
|
if (!character) {
|
||||||
|
console.warn(`Character not found for chat: ${chat.file_name}`);
|
||||||
|
data.splice(index, 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatTimestamp = timestampToMoment(chat.last_mes);
|
||||||
|
chat.char_name = character.name;
|
||||||
|
chat.date_short = chatTimestamp.format('l');
|
||||||
|
chat.date_long = chatTimestamp.format('LL LT');
|
||||||
|
chat.chat_name = chat.file_name.replace('.jsonl', '');
|
||||||
|
chat.char_thumbnail = getThumbnailUrl('avatar', character.avatar);
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initWelcomeScreen() {
|
||||||
|
const events = [event_types.CHAT_CHANGED, event_types.APP_READY];
|
||||||
|
for (const event of events) {
|
||||||
|
eventSource.makeFirst(event, openWelcomeScreen);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@
|
|||||||
@import url(css/accounts.css);
|
@import url(css/accounts.css);
|
||||||
@import url(css/tags.css);
|
@import url(css/tags.css);
|
||||||
@import url(css/scrollable-button.css);
|
@import url(css/scrollable-button.css);
|
||||||
|
@import url(css/welcome.css);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--doc-height: 100%;
|
--doc-height: 100%;
|
||||||
|
@ -934,6 +934,69 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ChatInfo
|
||||||
|
* @property {string} [file_name] - The name of the chat file
|
||||||
|
* @property {string} [file_size] - The size of the chat file
|
||||||
|
* @property {number} [chat_items] - The number of chat items in the file
|
||||||
|
* @property {string} [mes] - The last message in the chat
|
||||||
|
* @property {number} [last_mes] - The timestamp of the last message
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the information from a chat file.
|
||||||
|
* @param {string} pathToFile
|
||||||
|
* @param {object} additionalData
|
||||||
|
* @returns {Promise<ChatInfo>}
|
||||||
|
*/
|
||||||
|
async function getChatInfo(pathToFile, additionalData = {}) {
|
||||||
|
return new Promise(async (res) => {
|
||||||
|
const fileStream = fs.createReadStream(pathToFile);
|
||||||
|
const stats = fs.statSync(pathToFile);
|
||||||
|
const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
|
||||||
|
|
||||||
|
if (stats.size === 0) {
|
||||||
|
console.warn(`Found an empty chat file: ${pathToFile}`);
|
||||||
|
res({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: fileStream,
|
||||||
|
crlfDelay: Infinity,
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastLine;
|
||||||
|
let itemCounter = 0;
|
||||||
|
rl.on('line', (line) => {
|
||||||
|
itemCounter++;
|
||||||
|
lastLine = line;
|
||||||
|
});
|
||||||
|
rl.on('close', () => {
|
||||||
|
rl.close();
|
||||||
|
|
||||||
|
if (lastLine) {
|
||||||
|
const jsonData = tryParse(lastLine);
|
||||||
|
if (jsonData && (jsonData.name || jsonData.character_name)) {
|
||||||
|
const chatData = {};
|
||||||
|
|
||||||
|
chatData['file_name'] = path.parse(pathToFile).base;
|
||||||
|
chatData['file_size'] = fileSizeInKB;
|
||||||
|
chatData['chat_items'] = itemCounter - 1;
|
||||||
|
chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
|
||||||
|
chatData['last_mes'] = jsonData['send_date'] || Date.now();
|
||||||
|
Object.assign(chatData, additionalData);
|
||||||
|
|
||||||
|
res(chatData);
|
||||||
|
} else {
|
||||||
|
console.warn('Found an invalid or corrupted chat file:', pathToFile);
|
||||||
|
res({});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
router.post('/create', async function (request, response) {
|
router.post('/create', async function (request, response) {
|
||||||
@ -1223,12 +1286,52 @@ router.post('/get', validateAvatarUrlMiddleware, async function (request, respon
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/recent', async function (request, response) {
|
||||||
|
try {
|
||||||
|
/** @type {{pngFile: string, filePath: string, mtime: number}[]} */
|
||||||
|
const allChatFiles = [];
|
||||||
|
|
||||||
|
const pngFiles = fs
|
||||||
|
.readdirSync(request.user.directories.characters, { withFileTypes: true })
|
||||||
|
.filter(dirent => dirent.isFile() && dirent.name.endsWith('.png'))
|
||||||
|
.map(dirent => dirent.name);
|
||||||
|
|
||||||
|
for (const pngFile of pngFiles) {
|
||||||
|
const chatsDirectory = pngFile.replace('.png', '');
|
||||||
|
const pathToChats = path.join(request.user.directories.chats, chatsDirectory);
|
||||||
|
if (fs.existsSync(pathToChats) && fs.statSync(pathToChats).isDirectory()) {
|
||||||
|
const chatFiles = fs.readdirSync(pathToChats);
|
||||||
|
const chatFilesWithDate = chatFiles
|
||||||
|
.filter(file => file.endsWith('.jsonl'))
|
||||||
|
.map(file => {
|
||||||
|
const filePath = path.join(pathToChats, file);
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
return { pngFile, filePath, mtime: stats.mtimeMs };
|
||||||
|
});
|
||||||
|
allChatFiles.push(...chatFilesWithDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentChats = allChatFiles.sort((a, b) => b.mtime - a.mtime).slice(0, 5);
|
||||||
|
const jsonFilesPromise = recentChats.map((file) => {
|
||||||
|
return getChatInfo(file.filePath, { avatar: file.pngFile });
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value);
|
||||||
|
const validFiles = chatData.filter(i => i.file_name);
|
||||||
|
|
||||||
|
return response.send(validFiles);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
return response.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/chats', validateAvatarUrlMiddleware, async function (request, response) {
|
router.post('/chats', validateAvatarUrlMiddleware, async function (request, response) {
|
||||||
try {
|
try {
|
||||||
if (!request.body) return response.sendStatus(400);
|
if (!request.body) return response.sendStatus(400);
|
||||||
|
|
||||||
const characterDirectory = (request.body.avatar_url).replace('.png', '');
|
const characterDirectory = (request.body.avatar_url).replace('.png', '');
|
||||||
|
|
||||||
const chatsDirectory = path.join(request.user.directories.chats, characterDirectory);
|
const chatsDirectory = path.join(request.user.directories.chats, characterDirectory);
|
||||||
|
|
||||||
if (!fs.existsSync(chatsDirectory)) {
|
if (!fs.existsSync(chatsDirectory)) {
|
||||||
@ -1248,54 +1351,11 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp
|
|||||||
}
|
}
|
||||||
|
|
||||||
const jsonFilesPromise = jsonFiles.map((file) => {
|
const jsonFilesPromise = jsonFiles.map((file) => {
|
||||||
return new Promise(async (res) => {
|
const pathToFile = path.join(request.user.directories.chats, characterDirectory, file);
|
||||||
const pathToFile = path.join(request.user.directories.chats, characterDirectory, file);
|
return getChatInfo(pathToFile);
|
||||||
const fileStream = fs.createReadStream(pathToFile);
|
|
||||||
const stats = fs.statSync(pathToFile);
|
|
||||||
const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`;
|
|
||||||
|
|
||||||
if (stats.size === 0) {
|
|
||||||
console.warn(`Found an empty chat file: ${pathToFile}`);
|
|
||||||
res({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: fileStream,
|
|
||||||
crlfDelay: Infinity,
|
|
||||||
});
|
|
||||||
|
|
||||||
let lastLine;
|
|
||||||
let itemCounter = 0;
|
|
||||||
rl.on('line', (line) => {
|
|
||||||
itemCounter++;
|
|
||||||
lastLine = line;
|
|
||||||
});
|
|
||||||
rl.on('close', () => {
|
|
||||||
rl.close();
|
|
||||||
|
|
||||||
if (lastLine) {
|
|
||||||
const jsonData = tryParse(lastLine);
|
|
||||||
if (jsonData && (jsonData.name || jsonData.character_name)) {
|
|
||||||
const chatData = {};
|
|
||||||
|
|
||||||
chatData['file_name'] = file;
|
|
||||||
chatData['file_size'] = fileSizeInKB;
|
|
||||||
chatData['chat_items'] = itemCounter - 1;
|
|
||||||
chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
|
|
||||||
chatData['last_mes'] = jsonData['send_date'] || Date.now();
|
|
||||||
|
|
||||||
res(chatData);
|
|
||||||
} else {
|
|
||||||
console.warn('Found an invalid or corrupted chat file:', pathToFile);
|
|
||||||
res({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const chatData = await Promise.all(jsonFilesPromise);
|
const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value);
|
||||||
const validFiles = chatData.filter(i => i.file_name);
|
const validFiles = chatData.filter(i => i.file_name);
|
||||||
|
|
||||||
return response.send(validFiles);
|
return response.send(validFiles);
|
||||||
|
Reference in New Issue
Block a user