[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 | import { getContext } from './scripts/st-context.js'; | 282 | import { getContext } from './scripts/st-context.js'; |
| 283 | import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; | 283 | import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; |
| 284 | import { accountStorage } from './scripts/util/AccountStorage.js'; | 284 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 285 | import { initWelcomeScreen } from './scripts/welcome-screen.js'; | ||
| 285 | 286 | ||
| 286 | // API OBJECT FOR EXTERNAL WIRING | 287 | // API OBJECT FOR EXTERNAL WIRING |
| 287 | globalThis.SillyTavern = { | 288 | globalThis.SillyTavern = { |
| @@ -563,7 +564,7 @@ let chat_create_date = ''; | |||
| 563 | let firstRun = false; | 564 | let firstRun = false; |
| 564 | let settingsReady = false; | 565 | let settingsReady = false; |
| 565 | let currentVersion = '0.0.0'; | 566 | let currentVersion = '0.0.0'; |
| 566 | let displayVersion = 'SillyTavern'; | 567 | export let displayVersion = 'SillyTavern'; |
| 567 | 568 | ||
| 568 | let generatedPromptCache = ''; | 569 | let generatedPromptCache = ''; |
| 569 | let generation_started = new Date(); | 570 | let generation_started = new Date(); |
| @@ -983,8 +984,6 @@ async function firstLoadInit() { | |||
| 983 | ToolManager.initToolSlashCommands(); | 984 | ToolManager.initToolSlashCommands(); |
| 984 | await initPresetManager(); | 985 | await initPresetManager(); |
| 985 | await getSystemMessages(); | 986 | await getSystemMessages(); |
| 986 | sendSystemMessage(system_message_types.WELCOME); | ||
| 987 | sendSystemMessage(system_message_types.WELCOME_PROMPT); | ||
| 988 | await getSettings(); | 987 | await getSettings(); |
| 989 | initKeyboard(); | 988 | initKeyboard(); |
| 990 | initDynamicStyles(); | 989 | initDynamicStyles(); |
| @@ -1009,6 +1008,7 @@ async function firstLoadInit() { | |||
| 1009 | initSettingsSearch(); | 1008 | initSettingsSearch(); |
| 1010 | initBulkEdit(); | 1009 | initBulkEdit(); |
| 1011 | initReasoning(); | 1010 | initReasoning(); |
| 1011 | initWelcomeScreen(); | ||
| 1012 | await initScrapers(); | 1012 | await initScrapers(); |
| 1013 | initCustomSelectedSamplers(); | 1013 | initCustomSelectedSamplers(); |
| 1014 | addDebugFunctions(); | 1014 | addDebugFunctions(); |
| @@ -11176,8 +11176,6 @@ jQuery(async function () { | |||
| 11176 | selected_button = 'characters'; | 11176 | selected_button = 'characters'; |
| 11177 | $('#rm_button_selected_ch').children('h2').text(''); | 11177 | $('#rm_button_selected_ch').children('h2').text(''); |
| 11178 | select_rm_characters(); | 11178 | select_rm_characters(); |
| 11179 | sendSystemMessage(system_message_types.WELCOME); | ||
| 11180 | sendSystemMessage(system_message_types.WELCOME_PROMPT); | ||
| 11181 | await getClientVersion(); | 11179 | await getClientVersion(); |
| 11182 | await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); | 11180 | await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); |
| 11183 | } else { | 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 | @import url(css/accounts.css); | 10 | @import url(css/accounts.css); |
| 11 | @import url(css/tags.css); | 11 | @import url(css/tags.css); |
| 12 | @import url(css/scrollable-button.css); | 12 | @import url(css/scrollable-button.css); |
| 13 | @import url(css/welcome.css); | ||
| 13 | 14 | ||
| 14 | :root { | 15 | :root { |
| 15 | --doc-height: 100%; | 16 | --doc-height: 100%; |
| @@ -934,6 +934,69 @@ async function importFromPng(uploadPath, { request }, preservedFileName) { | |||
| 934 | return ''; | 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 | export const router = express.Router(); | 1000 | export const router = express.Router(); |
| 938 | 1001 | ||
| 939 | router.post('/create', async function (request, response) { | 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 | router.post('/chats', validateAvatarUrlMiddleware, async function (request, response) { | 1330 | router.post('/chats', validateAvatarUrlMiddleware, async function (request, response) { |
| 1227 | try { | 1331 | try { |
| 1228 | if (!request.body) return response.sendStatus(400); | 1332 | if (!request.body) return response.sendStatus(400); |
| 1229 | 1333 | ||
| 1230 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); | 1334 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); |
| 1231 | |||
| 1232 | const chatsDirectory = path.join(request.user.directories.chats, characterDirectory); | 1335 | const chatsDirectory = path.join(request.user.directories.chats, characterDirectory); |
| 1233 | 1336 | ||
| 1234 | if (!fs.existsSync(chatsDirectory)) { | 1337 | if (!fs.existsSync(chatsDirectory)) { |
| @@ -1248,54 +1351,11 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp | |||
| 1248 | } | 1351 | } |
| 1249 | 1352 | ||
| 1250 | const jsonFilesPromise = jsonFiles.map((file) => { | 1353 | const jsonFilesPromise = jsonFiles.map((file) => { |
| 1251 | return new Promise(async (res) => { | ||
| 1252 | const pathToFile = path.join(request.user.directories.chats, characterDirectory, file); | 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 | const validFiles = chatData.filter(i => i.file_name); | 1359 | const validFiles = chatData.filter(i => i.file_name); |
| 1300 | 1360 | ||
| 1301 | return response.send(validFiles); | 1361 | return response.send(validFiles); |