[wip] Welcome screen prototype
| @@ -0,0 +1,176 @@ | ||
| 1 | +.welcomePanel { | |
| 2 | + display: flex; | |
| 3 | + flex-direction: column; | |
| 4 | + gap: 5px; | |
| 5 | + padding: 10px; | |
| 6 | + width: 100%; | |
| 7 | +} | |
| 8 | + | |
| 9 | +body.bubblechat .welcomePanel { | |
| 10 | + border-radius: 10px; | |
| 11 | + background-color: var(--SmartThemeBotMesBlurTintColor); | |
| 12 | + border: 1px solid var(--SmartThemeBorderColor); | |
| 13 | + margin-bottom: 5px; | |
| 14 | +} | |
| 15 | + | |
| 16 | +.welcomePanel .welcomeHeader { | |
| 17 | + display: flex; | |
| 18 | + flex-direction: row; | |
| 19 | + align-items: center; | |
| 20 | + justify-content: flex-end; | |
| 21 | +} | |
| 22 | + | |
| 23 | +.welcomePanel .recentChatsTitle { | |
| 24 | + flex-grow: 1; | |
| 25 | + font-size: calc(var(--mainFontSize) * 1.1); | |
| 26 | + font-weight: 600; | |
| 27 | +} | |
| 28 | + | |
| 29 | +.welcomePanel .welcomeHeaderTitle { | |
| 30 | + margin: 0; | |
| 31 | + flex-grow: 1; | |
| 32 | + display: flex; | |
| 33 | + flex-direction: row; | |
| 34 | + align-items: center; | |
| 35 | + gap: 10px; | |
| 36 | +} | |
| 37 | + | |
| 38 | +.welcomePanel .welcomeHeaderVersionDisplay { | |
| 39 | + font-size: calc(var(--mainFontSize) * 1.2); | |
| 40 | + font-weight: 600; | |
| 41 | +} | |
| 42 | + | |
| 43 | +.welcomePanel .welcomeHeaderLogo { | |
| 44 | + width: 30px; | |
| 45 | + height: 30px; | |
| 46 | +} | |
| 47 | + | |
| 48 | +.welcomePanel .welcomeShortcuts { | |
| 49 | + display: flex; | |
| 50 | + flex-direction: row; | |
| 51 | + flex-wrap: wrap; | |
| 52 | + align-items: center; | |
| 53 | + justify-content: center; | |
| 54 | + gap: 5px; | |
| 55 | +} | |
| 56 | + | |
| 57 | +.welcomePanel .welcomeShortcuts .welcomeShortcutsSeparator { | |
| 58 | + margin: 0 2px; | |
| 59 | + color: var(--SmartThemeBorderColor); | |
| 60 | + font-size: calc(var(--mainFontSize) * 1.1); | |
| 61 | +} | |
| 62 | + | |
| 63 | +.welcomeRecent .recentChatList { | |
| 64 | + flex: 1; | |
| 65 | + display: flex; | |
| 66 | + flex-direction: column; | |
| 67 | + width: 100%; | |
| 68 | + gap: 2px; | |
| 69 | +} | |
| 70 | + | |
| 71 | +.welcomeRecent .welcomePanelLoader { | |
| 72 | + display: flex; | |
| 73 | + justify-content: center; | |
| 74 | + align-items: center; | |
| 75 | + flex: 1; | |
| 76 | + width: 100%; | |
| 77 | + height: 100%; | |
| 78 | + position: absolute; | |
| 79 | +} | |
| 80 | + | |
| 81 | +.welcomePanel .recentChatList .noRecentChat { | |
| 82 | + display: flex; | |
| 83 | + flex-direction: row; | |
| 84 | + justify-content: center; | |
| 85 | + align-items: baseline; | |
| 86 | + gap: 5px; | |
| 87 | + padding: 10px; | |
| 88 | +} | |
| 89 | + | |
| 90 | +.welcomeRecent .recentChatList .recentChat { | |
| 91 | + display: flex; | |
| 92 | + flex-direction: row; | |
| 93 | + transition: filter 0.2s; | |
| 94 | + padding: 5px 10px; | |
| 95 | + border-radius: 10px; | |
| 96 | + cursor: pointer; | |
| 97 | + gap: 10px; | |
| 98 | + border: 1px solid var(--SmartThemeBorderColor); | |
| 99 | +} | |
| 100 | + | |
| 101 | +.welcomeRecent .recentChatList .recentChat .avatar { | |
| 102 | + flex: 0; | |
| 103 | +} | |
| 104 | + | |
| 105 | +.welcomeRecent .recentChatList .recentChat.selected { | |
| 106 | + background-color: var(--cobalt30a); | |
| 107 | +} | |
| 108 | + | |
| 109 | +.welcomeRecent .recentChatList .recentChat:hover { | |
| 110 | + background-color: var(--white30a); | |
| 111 | +} | |
| 112 | + | |
| 113 | +.welcomeRecent .recentChatList .recentChat .recentChatInfo { | |
| 114 | + display: flex; | |
| 115 | + flex-direction: column; | |
| 116 | + flex-wrap: nowrap; | |
| 117 | + flex-grow: 1; | |
| 118 | + overflow: hidden; | |
| 119 | + justify-content: center; | |
| 120 | +} | |
| 121 | + | |
| 122 | +.welcomeRecent .recentChatList .recentChat .chatNameContainer { | |
| 123 | + display: flex; | |
| 124 | + flex-direction: row; | |
| 125 | + justify-content: space-between; | |
| 126 | + align-items: baseline; | |
| 127 | + font-size: calc(var(--mainFontSize) * 1); | |
| 128 | +} | |
| 129 | + | |
| 130 | +.welcomeRecent .recentChatList .recentChat .chatNameContainer .chatName { | |
| 131 | + white-space: nowrap; | |
| 132 | + text-overflow: ellipsis; | |
| 133 | + overflow: hidden; | |
| 134 | +} | |
| 135 | + | |
| 136 | +.welcomeRecent .recentChatList .recentChat .chatMessageContainer { | |
| 137 | + display: flex; | |
| 138 | + flex-direction: row; | |
| 139 | + align-items: center; | |
| 140 | + justify-content: space-between; | |
| 141 | + gap: 5px; | |
| 142 | + font-size: calc(var(--mainFontSize) * 0.9); | |
| 143 | +} | |
| 144 | + | |
| 145 | +.welcomeRecent .recentChatList .recentChat .chatMessageContainer .chatMessage { | |
| 146 | + white-space: nowrap; | |
| 147 | + text-overflow: ellipsis; | |
| 148 | + overflow: hidden; | |
| 149 | +} | |
| 150 | + | |
| 151 | +.welcomeRecent .recentChatList .recentChat .chatStats { | |
| 152 | + display: flex; | |
| 153 | + flex-direction: row; | |
| 154 | + justify-content: flex-end; | |
| 155 | + align-items: baseline; | |
| 156 | + gap: 5px; | |
| 157 | +} | |
| 158 | + | |
| 159 | +.welcomeRecent .recentChatList .recentChat .chatStats .counterBlock { | |
| 160 | + display: flex; | |
| 161 | + flex-direction: row; | |
| 162 | + align-items: baseline; | |
| 163 | + gap: 5px; | |
| 164 | +} | |
| 165 | + | |
| 166 | +.welcomeRecent .recentChatList .recentChat .chatStats .counterBlock::after { | |
| 167 | + content: "|"; | |
| 168 | + color: var(--SmartThemeBorderColor); | |
| 169 | + font-size: calc(var(--mainFontSize) * 0.95); | |
| 170 | +} | |
| 171 | + | |
| 172 | +@media screen and (max-width: 1000px) { | |
| 173 | + .welcomePanel .welcomeShortcuts a span { | |
| 174 | + display: none; | |
| 175 | + } | |
| 176 | +} | |
| @@ -282,6 +282,7 @@ import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; | ||
| 282 | 282 | import { getContext } from './scripts/st-context.js'; |
| 283 | 283 | import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; |
| 284 | 284 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 285 | +import { initWelcomeScreen } from './scripts/welcome-screen.js'; | |
| 285 | 286 | |
| 286 | 287 | // API OBJECT FOR EXTERNAL WIRING |
| 287 | 288 | globalThis.SillyTavern = { |
| @@ -563,7 +564,7 @@ let chat_create_date = ''; | ||
| 563 | 564 | let firstRun = false; |
| 564 | 565 | let settingsReady = false; |
| 565 | 566 | let currentVersion = '0.0.0'; |
| 566 | 567 | export let displayVersion = 'SillyTavern'; |
| 567 | 568 | |
| 568 | 569 | let generatedPromptCache = ''; |
| 569 | 570 | let generation_started = new Date(); |
| @@ -983,8 +984,6 @@ async function firstLoadInit() { | ||
| 983 | 984 | ToolManager.initToolSlashCommands(); |
| 984 | 985 | await initPresetManager(); |
| 985 | 986 | await getSystemMessages(); |
| 986 | - sendSystemMessage(system_message_types.WELCOME); | |
| 987 | - sendSystemMessage(system_message_types.WELCOME_PROMPT); | |
| 988 | 987 | await getSettings(); |
| 989 | 988 | initKeyboard(); |
| 990 | 989 | initDynamicStyles(); |
| @@ -1009,6 +1008,7 @@ async function firstLoadInit() { | ||
| 1009 | 1008 | initSettingsSearch(); |
| 1010 | 1009 | initBulkEdit(); |
| 1011 | 1010 | initReasoning(); |
| 1011 | + initWelcomeScreen(); | |
| 1012 | 1012 | await initScrapers(); |
| 1013 | 1013 | initCustomSelectedSamplers(); |
| 1014 | 1014 | addDebugFunctions(); |
| @@ -11176,8 +11176,6 @@ jQuery(async function () { | ||
| 11176 | 11176 | selected_button = 'characters'; |
| 11177 | 11177 | $('#rm_button_selected_ch').children('h2').text(''); |
| 11178 | 11178 | select_rm_characters(); |
| 11179 | - sendSystemMessage(system_message_types.WELCOME); | |
| 11180 | - sendSystemMessage(system_message_types.WELCOME_PROMPT); | |
| 11181 | 11179 | await getClientVersion(); |
| 11182 | 11180 | await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); |
| 11183 | 11181 | } else { |
| @@ -0,0 +1,71 @@ | ||
| 1 | +<div class="welcomePanel"> | |
| 2 | + <div class="welcomeHeaderTitle"> | |
| 3 | + <img src="img/logo.png" alt="SillyTavern Logo" class="welcomeHeaderLogo"> | |
| 4 | + <span class="welcomeHeaderVersionDisplay">{{version}}</span> | |
| 5 | + </div> | |
| 6 | + <div class="welcomeHeader"> | |
| 7 | + <div class="recentChatsTitle" data-i18n="Recent Chats"> | |
| 8 | + Recent Chats | |
| 9 | + </div> | |
| 10 | + <div class="welcomeShortcuts"> | |
| 11 | + <a class="menu_button menu_button_icon" target="_blank" href="https://docs.sillytavern.app/"> | |
| 12 | + <i class="fa-solid fa-question-circle"></i> | |
| 13 | + <span data-i18n="Docs">Docs</span> | |
| 14 | + </a> | |
| 15 | + <a class="menu_button menu_button_icon" target="_blank" href="https://github.com/SillyTavern/SillyTavern"> | |
| 16 | + <i class="fa-brands fa-github"></i> | |
| 17 | + <span data-i18n="GitHub">GitHub</span> | |
| 18 | + </a> | |
| 19 | + <a class="menu_button menu_button_icon" target="_blank" href="https://discord.gg/sillytavern"> | |
| 20 | + <i class="fa-brands fa-discord"></i> | |
| 21 | + <span data-i18n="Discord">Discord</span> | |
| 22 | + </a> | |
| 23 | + <span class="welcomeShortcutsSeparator">|</span> | |
| 24 | + <button class="openTemporaryChat menu_button menu_button_icon"> | |
| 25 | + <i class="fa-solid fa-comment-dots"></i> | |
| 26 | + <span data-i18n="Temporary Chat">Temporary Chat</span> | |
| 27 | + </button> | |
| 28 | + </div> | |
| 29 | + </div> | |
| 30 | + <div class="welcomeRecent"> | |
| 31 | + <div class="recentChatList"> | |
| 32 | + {{#if empty}} | |
| 33 | + <div class="noRecentChat"> | |
| 34 | + <i class="fa-solid fa-comment-dots"></i> | |
| 35 | + <span data-i18n="No recent chats">No recent chats</span> | |
| 36 | + </div> | |
| 37 | + {{/if}} | |
| 38 | + {{#each chats}} | |
| 39 | + {{#with this}} | |
| 40 | + <div class="recentChat" data-file="{{chat_name}}" data-avatar="{{avatar}}"> | |
| 41 | + <div class="avatar" title="{{char_name}}"> | |
| 42 | + <img src="{{char_thumbnail}}" alt="{{char_name}}"> | |
| 43 | + </div> | |
| 44 | + <div class="recentChatInfo"> | |
| 45 | + <div class="chatNameContainer"> | |
| 46 | + <div class="chatName" title="{{file_name}}"> | |
| 47 | + <strong class="characterName">{{char_name}}</strong> | |
| 48 | + <span>–</span> | |
| 49 | + <span>{{chat_name}}</span> | |
| 50 | + </div> | |
| 51 | + <small class="chatDate" title="{{date_full}}">{{date_short}}</small> | |
| 52 | + </div> | |
| 53 | + <div class="chatMessageContainer"> | |
| 54 | + <div class="chatMessage" title="{{mes}}"> | |
| 55 | + {{mes}} | |
| 56 | + </div> | |
| 57 | + <div class="chatStats"> | |
| 58 | + <div class="counterBlock"> | |
| 59 | + <i class="fa-solid fa-comment fa-xs"></i> | |
| 60 | + <small>{{chat_items}}</small> | |
| 61 | + </div> | |
| 62 | + <small class="fileSize">{{file_size}}</small> | |
| 63 | + </div> | |
| 64 | + </div> | |
| 65 | + </div> | |
| 66 | + </div> | |
| 67 | + {{/with}} | |
| 68 | + {{/each}} | |
| 69 | + </div> | |
| 70 | + </div> | |
| 71 | +</div> | |
| @@ -0,0 +1,135 @@ | ||
| 1 | +import { | |
| 2 | + characters, | |
| 3 | + displayVersion, | |
| 4 | + event_types, | |
| 5 | + eventSource, | |
| 6 | + getCurrentChatId, | |
| 7 | + getRequestHeaders, | |
| 8 | + getThumbnailUrl, | |
| 9 | + openCharacterChat, | |
| 10 | + selectCharacterById, | |
| 11 | + sendSystemMessage, | |
| 12 | + system_message_types, | |
| 13 | +} from '../script.js'; | |
| 14 | +import { t } from './i18n.js'; | |
| 15 | +import { renderTemplateAsync } from './templates.js'; | |
| 16 | +import { timestampToMoment } from './utils.js'; | |
| 17 | + | |
| 18 | +export async function openWelcomeScreen() { | |
| 19 | + const currentChatId = getCurrentChatId(); | |
| 20 | + if (currentChatId !== undefined) { | |
| 21 | + return; | |
| 22 | + } | |
| 23 | + | |
| 24 | + await sendWelcomePanel(); | |
| 25 | + sendSystemMessage(system_message_types.WELCOME_PROMPT); | |
| 26 | +} | |
| 27 | + | |
| 28 | +async function sendWelcomePanel() { | |
| 29 | + try { | |
| 30 | + const chatElement = document.getElementById('chat'); | |
| 31 | + if (!chatElement) { | |
| 32 | + console.error('Chat element not found'); | |
| 33 | + return; | |
| 34 | + } | |
| 35 | + const chats = await getRecentChats(); | |
| 36 | + const templateData = { | |
| 37 | + chats, | |
| 38 | + empty: !chats.length , | |
| 39 | + version: displayVersion, | |
| 40 | + }; | |
| 41 | + const template = await renderTemplateAsync('welcomePanel', templateData); | |
| 42 | + const fragment = document.createRange().createContextualFragment(template); | |
| 43 | + fragment.querySelectorAll('.recentChat').forEach((item) => { | |
| 44 | + item.addEventListener('click', () => { | |
| 45 | + const avatarId = item.getAttribute('data-avatar'); | |
| 46 | + const fileName = item.getAttribute('data-file'); | |
| 47 | + if (avatarId && fileName) { | |
| 48 | + void openRecentChat(avatarId, fileName); | |
| 49 | + } | |
| 50 | + }); | |
| 51 | + }); | |
| 52 | + fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => { | |
| 53 | + toastr.info('This button does nothing at the moment. Try again later.'); | |
| 54 | + }); | |
| 55 | + chatElement.append(fragment.firstChild); | |
| 56 | + } catch (error) { | |
| 57 | + console.error('Welcome screen error:', error); | |
| 58 | + } | |
| 59 | +} | |
| 60 | + | |
| 61 | +/** | |
| 62 | + * Opens a recent chat. | |
| 63 | + * @param {string} avatarId Avatar file name | |
| 64 | + * @param {string} fileName Chat file name | |
| 65 | + */ | |
| 66 | +async function openRecentChat(avatarId, fileName) { | |
| 67 | + const characterId = characters.findIndex(x => x.avatar === avatarId); | |
| 68 | + if (characterId === -1) { | |
| 69 | + console.error(`Character not found for avatar ID: ${avatarId}`); | |
| 70 | + return; | |
| 71 | + } | |
| 72 | + | |
| 73 | + try { | |
| 74 | + await selectCharacterById(characterId); | |
| 75 | + await openCharacterChat(fileName); | |
| 76 | + } catch (error) { | |
| 77 | + console.error('Error opening recent chat:', error); | |
| 78 | + toastr.error(t`Failed to open recent chat. See console for details.`); | |
| 79 | + } | |
| 80 | +} | |
| 81 | + | |
| 82 | +/** | |
| 83 | + * Gets the list of recent chats from the server. | |
| 84 | + * @returns {Promise<RecentChat[]>} List of recent chats | |
| 85 | + * | |
| 86 | + * @typedef {object} RecentChat | |
| 87 | + * @property {string} file_name Name of the chat file | |
| 88 | + * @property {string} chat_name Name of the chat (without extension) | |
| 89 | + * @property {string} file_size Size of the chat file | |
| 90 | + * @property {number} chat_items Number of items in the chat | |
| 91 | + * @property {string} mes Last message content | |
| 92 | + * @property {number} last_mes Timestamp of the last message | |
| 93 | + * @property {string} avatar Avatar URL | |
| 94 | + * @property {string} char_thumbnail Thumbnail URL | |
| 95 | + * @property {string} char_name Character name | |
| 96 | + * @property {string} date_short Date in short format | |
| 97 | + * @property {string} date_long Date in long format | |
| 98 | + */ | |
| 99 | +async function getRecentChats() { | |
| 100 | + const response = await fetch('/api/characters/recent', { | |
| 101 | + method: 'POST', | |
| 102 | + headers: getRequestHeaders(), | |
| 103 | + }); | |
| 104 | + if (!response.ok) { | |
| 105 | + throw new Error('Failed to fetch recent chats'); | |
| 106 | + } | |
| 107 | + | |
| 108 | + /** @type {RecentChat[]} */ | |
| 109 | + const data = await response.json(); | |
| 110 | + | |
| 111 | + data.sort((a, b) => b.last_mes - a.last_mes).forEach((chat, index) => { | |
| 112 | + const character = characters.find(x => x.avatar === chat.avatar); | |
| 113 | + if (!character) { | |
| 114 | + console.warn(`Character not found for chat: ${chat.file_name}`); | |
| 115 | + data.splice(index, 1); | |
| 116 | + return; | |
| 117 | + } | |
| 118 | + | |
| 119 | + const chatTimestamp = timestampToMoment(chat.last_mes); | |
| 120 | + chat.char_name = character.name; | |
| 121 | + chat.date_short = chatTimestamp.format('l'); | |
| 122 | + chat.date_long = chatTimestamp.format('LL LT'); | |
| 123 | + chat.chat_name = chat.file_name.replace('.jsonl', ''); | |
| 124 | + chat.char_thumbnail = getThumbnailUrl('avatar', character.avatar); | |
| 125 | + }); | |
| 126 | + | |
| 127 | + return data; | |
| 128 | +} | |
| 129 | + | |
| 130 | +export function initWelcomeScreen() { | |
| 131 | + const events = [event_types.CHAT_CHANGED, event_types.APP_READY]; | |
| 132 | + for (const event of events) { | |
| 133 | + eventSource.makeFirst(event, openWelcomeScreen); | |
| 134 | + } | |
| 135 | +} | |
| @@ -10,6 +10,7 @@ | ||
| 10 | 10 | @import url(css/accounts.css); |
| 11 | 11 | @import url(css/tags.css); |
| 12 | 12 | @import url(css/scrollable-button.css); |
| 13 | +@import url(css/welcome.css); | |
| 13 | 14 | |
| 14 | 15 | :root { |
| 15 | 16 | --doc-height: 100%; |
| @@ -934,6 +934,69 @@ async function importFromPng(uploadPath, { request }, preservedFileName) { | ||
| 934 | 934 | return ''; |
| 935 | 935 | } |
| 936 | 936 | |
| 937 | +/** | |
| 938 | + * @typedef {Object} ChatInfo | |
| 939 | + * @property {string} [file_name] - The name of the chat file | |
| 940 | + * @property {string} [file_size] - The size of the chat file | |
| 941 | + * @property {number} [chat_items] - The number of chat items in the file | |
| 942 | + * @property {string} [mes] - The last message in the chat | |
| 943 | + * @property {number} [last_mes] - The timestamp of the last message | |
| 944 | + */ | |
| 945 | + | |
| 946 | +/** | |
| 947 | + * Reads the information from a chat file. | |
| 948 | + * @param {string} pathToFile | |
| 949 | + * @param {object} additionalData | |
| 950 | + * @returns {Promise<ChatInfo>} | |
| 951 | + */ | |
| 952 | +async function getChatInfo(pathToFile, additionalData = {}) { | |
| 953 | + return new Promise(async (res) => { | |
| 954 | + const fileStream = fs.createReadStream(pathToFile); | |
| 955 | + const stats = fs.statSync(pathToFile); | |
| 956 | + const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`; | |
| 957 | + | |
| 958 | + if (stats.size === 0) { | |
| 959 | + console.warn(`Found an empty chat file: ${pathToFile}`); | |
| 960 | + res({}); | |
| 961 | + return; | |
| 962 | + } | |
| 963 | + | |
| 964 | + const rl = readline.createInterface({ | |
| 965 | + input: fileStream, | |
| 966 | + crlfDelay: Infinity, | |
| 967 | + }); | |
| 968 | + | |
| 969 | + let lastLine; | |
| 970 | + let itemCounter = 0; | |
| 971 | + rl.on('line', (line) => { | |
| 972 | + itemCounter++; | |
| 973 | + lastLine = line; | |
| 974 | + }); | |
| 975 | + rl.on('close', () => { | |
| 976 | + rl.close(); | |
| 977 | + | |
| 978 | + if (lastLine) { | |
| 979 | + const jsonData = tryParse(lastLine); | |
| 980 | + if (jsonData && (jsonData.name || jsonData.character_name)) { | |
| 981 | + const chatData = {}; | |
| 982 | + | |
| 983 | + chatData['file_name'] = path.parse(pathToFile).base; | |
| 984 | + chatData['file_size'] = fileSizeInKB; | |
| 985 | + chatData['chat_items'] = itemCounter - 1; | |
| 986 | + chatData['mes'] = jsonData['mes'] || '[The chat is empty]'; | |
| 987 | + chatData['last_mes'] = jsonData['send_date'] || Date.now(); | |
| 988 | + Object.assign(chatData, additionalData); | |
| 989 | + | |
| 990 | + res(chatData); | |
| 991 | + } else { | |
| 992 | + console.warn('Found an invalid or corrupted chat file:', pathToFile); | |
| 993 | + res({}); | |
| 994 | + } | |
| 995 | + } | |
| 996 | + }); | |
| 997 | + }); | |
| 998 | +} | |
| 999 | + | |
| 937 | 1000 | export const router = express.Router(); |
| 938 | 1001 | |
| 939 | 1002 | router.post('/create', async function (request, response) { |
| @@ -1223,12 +1286,52 @@ router.post('/get', validateAvatarUrlMiddleware, async function (request, respon | ||
| 1223 | 1286 | } |
| 1224 | 1287 | }); |
| 1225 | 1288 | |
| 1289 | +router.post('/recent', async function (request, response) { | |
| 1290 | + try { | |
| 1291 | + /** @type {{pngFile: string, filePath: string, mtime: number}[]} */ | |
| 1292 | + const allChatFiles = []; | |
| 1293 | + | |
| 1294 | + const pngFiles = fs | |
| 1295 | + .readdirSync(request.user.directories.characters, { withFileTypes: true }) | |
| 1296 | + .filter(dirent => dirent.isFile() && dirent.name.endsWith('.png')) | |
| 1297 | + .map(dirent => dirent.name); | |
| 1298 | + | |
| 1299 | + for (const pngFile of pngFiles) { | |
| 1300 | + const chatsDirectory = pngFile.replace('.png', ''); | |
| 1301 | + const pathToChats = path.join(request.user.directories.chats, chatsDirectory); | |
| 1302 | + if (fs.existsSync(pathToChats) && fs.statSync(pathToChats).isDirectory()) { | |
| 1303 | + const chatFiles = fs.readdirSync(pathToChats); | |
| 1304 | + const chatFilesWithDate = chatFiles | |
| 1305 | + .filter(file => file.endsWith('.jsonl')) | |
| 1306 | + .map(file => { | |
| 1307 | + const filePath = path.join(pathToChats, file); | |
| 1308 | + const stats = fs.statSync(filePath); | |
| 1309 | + return { pngFile, filePath, mtime: stats.mtimeMs }; | |
| 1310 | + }); | |
| 1311 | + allChatFiles.push(...chatFilesWithDate); | |
| 1312 | + } | |
| 1313 | + } | |
| 1314 | + | |
| 1315 | + const recentChats = allChatFiles.sort((a, b) => b.mtime - a.mtime).slice(0, 5); | |
| 1316 | + const jsonFilesPromise = recentChats.map((file) => { | |
| 1317 | + return getChatInfo(file.filePath, { avatar: file.pngFile }); | |
| 1318 | + }); | |
| 1319 | + | |
| 1320 | + const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value); | |
| 1321 | + const validFiles = chatData.filter(i => i.file_name); | |
| 1322 | + | |
| 1323 | + return response.send(validFiles); | |
| 1324 | + } catch (error) { | |
| 1325 | + console.error(error); | |
| 1326 | + return response.sendStatus(500); | |
| 1327 | + } | |
| 1328 | +}); | |
| 1329 | + | |
| 1226 | 1330 | router.post('/chats', validateAvatarUrlMiddleware, async function (request, response) { |
| 1227 | 1331 | try { |
| 1228 | 1332 | if (!request.body) return response.sendStatus(400); |
| 1229 | 1333 | |
| 1230 | 1334 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); |
| 1231 | - | |
| 1232 | 1335 | const chatsDirectory = path.join(request.user.directories.chats, characterDirectory); |
| 1233 | 1336 | |
| 1234 | 1337 | if (!fs.existsSync(chatsDirectory)) { |
| @@ -1248,54 +1351,11 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp | ||
| 1248 | 1351 | } |
| 1249 | 1352 | |
| 1250 | 1353 | const jsonFilesPromise = jsonFiles.map((file) => { |
| 1251 | - return new Promise(async (res) => { | |
| 1252 | 1354 | const pathToFile = path.join(request.user.directories.chats, characterDirectory, file); |
| 1253 | - const fileStream = fs.createReadStream(pathToFile); | |
| 1355 | + return getChatInfo(pathToFile); | |
| 1254 | - const stats = fs.statSync(pathToFile); | |
| 1255 | - const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`; | |
| 1256 | - | |
| 1257 | - if (stats.size === 0) { | |
| 1258 | - console.warn(`Found an empty chat file: ${pathToFile}`); | |
| 1259 | - res({}); | |
| 1260 | - return; | |
| 1261 | - } | |
| 1262 | - | |
| 1263 | - const rl = readline.createInterface({ | |
| 1264 | - input: fileStream, | |
| 1265 | - crlfDelay: Infinity, | |
| 1266 | - }); | |
| 1267 | - | |
| 1268 | - let lastLine; | |
| 1269 | - let itemCounter = 0; | |
| 1270 | - rl.on('line', (line) => { | |
| 1271 | - itemCounter++; | |
| 1272 | - lastLine = line; | |
| 1273 | - }); | |
| 1274 | - rl.on('close', () => { | |
| 1275 | - rl.close(); | |
| 1276 | - | |
| 1277 | - if (lastLine) { | |
| 1278 | - const jsonData = tryParse(lastLine); | |
| 1279 | - if (jsonData && (jsonData.name || jsonData.character_name)) { | |
| 1280 | - const chatData = {}; | |
| 1281 | - | |
| 1282 | - chatData['file_name'] = file; | |
| 1283 | - chatData['file_size'] = fileSizeInKB; | |
| 1284 | - chatData['chat_items'] = itemCounter - 1; | |
| 1285 | - chatData['mes'] = jsonData['mes'] || '[The chat is empty]'; | |
| 1286 | - chatData['last_mes'] = jsonData['send_date'] || Date.now(); | |
| 1287 | - | |
| 1288 | - res(chatData); | |
| 1289 | - } else { | |
| 1290 | - console.warn('Found an invalid or corrupted chat file:', pathToFile); | |
| 1291 | - res({}); | |
| 1292 | - } | |
| 1293 | - } | |
| 1294 | - }); | |
| 1295 | - }); | |
| 1296 | 1356 | }); |
| 1297 | 1357 | |
| 1298 | - const chatData = await Promise.all(jsonFilesPromise); | |
| 1358 | + const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value); | |
| 1299 | 1359 | const validFiles = chatData.filter(i => i.file_name); |
| 1300 | 1360 | |
| 1301 | 1361 | return response.send(validFiles); |