Merge pull request #3990 from SillyTavern/welcome-screen New welcome screen + quick chat functionality
Signed| @@ -0,0 +1,209 @@ | |||
| 1 | #chat .mes[type="assistant_message"] .mes_button { | ||
| 2 | display: none; | ||
| 3 | } | ||
| 4 | |||
| 5 | .welcomePanel { | ||
| 6 | display: flex; | ||
| 7 | flex-direction: column; | ||
| 8 | gap: 5px; | ||
| 9 | padding: 10px; | ||
| 10 | width: 100%; | ||
| 11 | } | ||
| 12 | |||
| 13 | .welcomePanel:has(.showMoreChats) { | ||
| 14 | padding-bottom: 5px; | ||
| 15 | } | ||
| 16 | |||
| 17 | .welcomePanel.recentHidden .welcomeRecent, | ||
| 18 | .welcomePanel.recentHidden .recentChatsTitle, | ||
| 19 | .welcomePanel.recentHidden .hideRecentChats, | ||
| 20 | .welcomePanel:not(.recentHidden) .showRecentChats { | ||
| 21 | display: none; | ||
| 22 | } | ||
| 23 | |||
| 24 | body.bubblechat .welcomePanel { | ||
| 25 | border-radius: 10px; | ||
| 26 | background-color: var(--SmartThemeBotMesBlurTintColor); | ||
| 27 | border: 1px solid var(--SmartThemeBorderColor); | ||
| 28 | margin-bottom: 5px; | ||
| 29 | } | ||
| 30 | |||
| 31 | .welcomePanel .welcomeHeader { | ||
| 32 | display: flex; | ||
| 33 | flex-direction: row; | ||
| 34 | align-items: center; | ||
| 35 | justify-content: flex-end; | ||
| 36 | } | ||
| 37 | |||
| 38 | .welcomePanel .recentChatsTitle { | ||
| 39 | flex-grow: 1; | ||
| 40 | font-size: calc(var(--mainFontSize) * 1.15); | ||
| 41 | font-weight: 600; | ||
| 42 | } | ||
| 43 | |||
| 44 | .welcomePanel .welcomeHeaderTitle { | ||
| 45 | margin: 0; | ||
| 46 | flex-grow: 1; | ||
| 47 | display: flex; | ||
| 48 | flex-direction: row; | ||
| 49 | align-items: center; | ||
| 50 | gap: 10px; | ||
| 51 | } | ||
| 52 | |||
| 53 | .welcomePanel .welcomeHeaderVersionDisplay { | ||
| 54 | font-size: calc(var(--mainFontSize) * 1.3); | ||
| 55 | font-weight: 600; | ||
| 56 | flex-grow: 1; | ||
| 57 | } | ||
| 58 | |||
| 59 | .welcomePanel .welcomeHeaderLogo { | ||
| 60 | width: 30px; | ||
| 61 | height: 30px; | ||
| 62 | } | ||
| 63 | |||
| 64 | .welcomePanel .welcomeShortcuts { | ||
| 65 | display: flex; | ||
| 66 | flex-direction: row; | ||
| 67 | flex-wrap: wrap; | ||
| 68 | align-items: center; | ||
| 69 | justify-content: center; | ||
| 70 | gap: 5px; | ||
| 71 | } | ||
| 72 | |||
| 73 | .welcomePanel .welcomeShortcuts .welcomeShortcutsSeparator { | ||
| 74 | margin: 0 2px; | ||
| 75 | color: var(--SmartThemeBorderColor); | ||
| 76 | font-size: calc(var(--mainFontSize) * 1.1); | ||
| 77 | } | ||
| 78 | |||
| 79 | .welcomeRecent .recentChatList { | ||
| 80 | display: flex; | ||
| 81 | flex-direction: column; | ||
| 82 | width: 100%; | ||
| 83 | gap: 2px; | ||
| 84 | } | ||
| 85 | |||
| 86 | .welcomeRecent .welcomePanelLoader { | ||
| 87 | display: flex; | ||
| 88 | justify-content: center; | ||
| 89 | align-items: center; | ||
| 90 | flex: 1; | ||
| 91 | width: 100%; | ||
| 92 | height: 100%; | ||
| 93 | position: absolute; | ||
| 94 | } | ||
| 95 | |||
| 96 | .welcomePanel .recentChatList .noRecentChat { | ||
| 97 | display: flex; | ||
| 98 | flex-direction: row; | ||
| 99 | justify-content: center; | ||
| 100 | align-items: baseline; | ||
| 101 | gap: 5px; | ||
| 102 | padding: 10px; | ||
| 103 | } | ||
| 104 | |||
| 105 | .welcomeRecent .recentChatList .recentChat { | ||
| 106 | display: flex; | ||
| 107 | flex-direction: row; | ||
| 108 | align-items: center; | ||
| 109 | padding: 5px 10px; | ||
| 110 | border-radius: 10px; | ||
| 111 | cursor: pointer; | ||
| 112 | gap: 10px; | ||
| 113 | border: 1px solid var(--SmartThemeBorderColor); | ||
| 114 | } | ||
| 115 | |||
| 116 | .welcomeRecent .recentChatList .recentChat .avatar { | ||
| 117 | flex: 0; | ||
| 118 | align-self: center; | ||
| 119 | } | ||
| 120 | |||
| 121 | .welcomeRecent .recentChatList .recentChat:hover { | ||
| 122 | background-color: var(--white30a); | ||
| 123 | } | ||
| 124 | |||
| 125 | .welcomeRecent .recentChatList .recentChat .recentChatInfo { | ||
| 126 | display: flex; | ||
| 127 | flex-direction: column; | ||
| 128 | flex-wrap: nowrap; | ||
| 129 | flex-grow: 1; | ||
| 130 | overflow: hidden; | ||
| 131 | justify-content: center; | ||
| 132 | align-self: flex-start; | ||
| 133 | } | ||
| 134 | |||
| 135 | .welcomeRecent .recentChatList .recentChat .chatNameContainer { | ||
| 136 | display: flex; | ||
| 137 | flex-direction: row; | ||
| 138 | justify-content: space-between; | ||
| 139 | align-items: baseline; | ||
| 140 | font-size: calc(var(--mainFontSize) * 1); | ||
| 141 | } | ||
| 142 | |||
| 143 | .welcomeRecent .recentChatList .recentChat .chatNameContainer .chatName { | ||
| 144 | white-space: nowrap; | ||
| 145 | text-overflow: ellipsis; | ||
| 146 | overflow: hidden; | ||
| 147 | } | ||
| 148 | |||
| 149 | .welcomeRecent .recentChatList .recentChat .chatMessageContainer { | ||
| 150 | display: flex; | ||
| 151 | flex-direction: row; | ||
| 152 | align-items: center; | ||
| 153 | justify-content: space-between; | ||
| 154 | gap: 5px; | ||
| 155 | font-size: calc(var(--mainFontSize) * 0.85); | ||
| 156 | } | ||
| 157 | |||
| 158 | .welcomeRecent .recentChatList .recentChat .chatMessageContainer .chatMessage { | ||
| 159 | display: -webkit-box; | ||
| 160 | -webkit-box-orient: vertical; | ||
| 161 | -webkit-line-clamp: 2; | ||
| 162 | line-clamp: 2; | ||
| 163 | overflow: hidden; | ||
| 164 | } | ||
| 165 | |||
| 166 | body.big-avatars .welcomeRecent .recentChatList .recentChat .chatMessageContainer .chatMessage { | ||
| 167 | -webkit-line-clamp: 4; | ||
| 168 | line-clamp: 4; | ||
| 169 | } | ||
| 170 | |||
| 171 | .welcomeRecent .recentChatList .recentChat .chatStats { | ||
| 172 | display: flex; | ||
| 173 | flex-direction: row; | ||
| 174 | justify-content: flex-end; | ||
| 175 | align-items: baseline; | ||
| 176 | align-self: flex-start; | ||
| 177 | gap: 5px; | ||
| 178 | } | ||
| 179 | |||
| 180 | .welcomeRecent .recentChatList .recentChat .chatStats .counterBlock { | ||
| 181 | display: flex; | ||
| 182 | flex-direction: row; | ||
| 183 | align-items: baseline; | ||
| 184 | gap: 5px; | ||
| 185 | } | ||
| 186 | |||
| 187 | .welcomeRecent .recentChatList .recentChat .chatStats .counterBlock::after { | ||
| 188 | content: "|"; | ||
| 189 | color: var(--SmartThemeBorderColor); | ||
| 190 | font-size: calc(var(--mainFontSize) * 0.95); | ||
| 191 | } | ||
| 192 | |||
| 193 | .welcomeRecent .recentChatList .recentChat.hidden { | ||
| 194 | display: none; | ||
| 195 | } | ||
| 196 | |||
| 197 | .welcomeRecent .recentChatList .showMoreChats { | ||
| 198 | align-self: center; | ||
| 199 | } | ||
| 200 | |||
| 201 | .welcomeRecent .recentChatList .showMoreChats.rotated { | ||
| 202 | transform: rotate(180deg); | ||
| 203 | } | ||
| 204 | |||
| 205 | @media screen and (max-width: 1000px) { | ||
| 206 | .welcomePanel .welcomeShortcuts a span { | ||
| 207 | display: none; | ||
| 208 | } | ||
| 209 | } | ||
| @@ -5362,6 +5362,9 @@ | |||
| 5362 | <option id="import_tags" data-i18n="Import Tags"> | 5362 | <option id="import_tags" data-i18n="Import Tags"> |
| 5363 | Import Tags | 5363 | Import Tags |
| 5364 | </option> | 5364 | </option> |
| 5365 | <option id="set_as_assistant" data-i18n="Set / Unset as Welcome Page Assistant"> | ||
| 5366 | Set / Unset as Welcome Page Assistant | ||
| 5367 | </option> | ||
| 5365 | <!--<option id="dupe_button"> | 5368 | <!--<option id="dupe_button"> |
| 5366 | Duplicate | 5369 | Duplicate |
| 5367 | </option> | 5370 | </option> |
| @@ -6383,6 +6386,9 @@ | |||
| 6383 | <div class="wide100p character_name_block"> | 6386 | <div class="wide100p character_name_block"> |
| 6384 | <span class="ch_name"></span> | 6387 | <span class="ch_name"></span> |
| 6385 | <small class="ch_additional_info ch_add_placeholder">+++</small> | 6388 | <small class="ch_additional_info ch_add_placeholder">+++</small> |
| 6389 | <small class="ch_assistant" title="This character will be used as a welcome page assistant." data-i18n="[title]This character will be used as a welcome page assistant."> | ||
| 6390 | <i class="fa-solid fa-sm fa-user-graduate"></i> | ||
| 6391 | </small> | ||
| 6386 | <small class="ch_additional_info character_version"></small> | 6392 | <small class="ch_additional_info character_version"></small> |
| 6387 | <small class="ch_additional_info ch_avatar_url"></small> | 6393 | <small class="ch_additional_info ch_avatar_url"></small> |
| 6388 | </div> | 6394 | </div> |
| @@ -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, openPermanentAssistantChat, openPermanentAssistantCard, getPermanentAssistantAvatar } 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 = { |
| @@ -529,6 +530,7 @@ export const event_types = { | |||
| 529 | CONNECTION_PROFILE_UPDATED: 'connection_profile_updated', | 530 | CONNECTION_PROFILE_UPDATED: 'connection_profile_updated', |
| 530 | TOOL_CALLS_PERFORMED: 'tool_calls_performed', | 531 | TOOL_CALLS_PERFORMED: 'tool_calls_performed', |
| 531 | TOOL_CALLS_RENDERED: 'tool_calls_rendered', | 532 | TOOL_CALLS_RENDERED: 'tool_calls_rendered', |
| 533 | CHARACTER_MANAGEMENT_DROPDOWN: 'charManagementDropdown', | ||
| 532 | }; | 534 | }; |
| 533 | 535 | ||
| 534 | export const eventSource = new EventEmitter([event_types.APP_READY]); | 536 | export const eventSource = new EventEmitter([event_types.APP_READY]); |
| @@ -563,7 +565,7 @@ let chat_create_date = ''; | |||
| 563 | let firstRun = false; | 565 | let firstRun = false; |
| 564 | let settingsReady = false; | 566 | let settingsReady = false; |
| 565 | let currentVersion = '0.0.0'; | 567 | let currentVersion = '0.0.0'; |
| 566 | let displayVersion = 'SillyTavern'; | 568 | export let displayVersion = 'SillyTavern'; |
| 567 | 569 | ||
| 568 | let generatedPromptCache = ''; | 570 | let generatedPromptCache = ''; |
| 569 | let generation_started = new Date(); | 571 | let generation_started = new Date(); |
| @@ -636,6 +638,7 @@ export const system_message_types = { | |||
| 636 | MACROS: 'macros', | 638 | MACROS: 'macros', |
| 637 | WELCOME_PROMPT: 'welcome_prompt', | 639 | WELCOME_PROMPT: 'welcome_prompt', |
| 638 | ASSISTANT_NOTE: 'assistant_note', | 640 | ASSISTANT_NOTE: 'assistant_note', |
| 641 | ASSISTANT_MESSAGE: 'assistant_message', | ||
| 639 | }; | 642 | }; |
| 640 | 643 | ||
| 641 | /** | 644 | /** |
| @@ -737,6 +740,7 @@ async function getSystemMessages() { | |||
| 737 | force_avatar: system_avatar, | 740 | force_avatar: system_avatar, |
| 738 | is_user: false, | 741 | is_user: false, |
| 739 | is_system: true, | 742 | is_system: true, |
| 743 | uses_system_ui: true, | ||
| 740 | mes: await renderTemplateAsync('welcomePrompt'), | 744 | mes: await renderTemplateAsync('welcomePrompt'), |
| 741 | extra: { | 745 | extra: { |
| 742 | isSmallSys: true, | 746 | isSmallSys: true, |
| @@ -983,8 +987,6 @@ async function firstLoadInit() { | |||
| 983 | ToolManager.initToolSlashCommands(); | 987 | ToolManager.initToolSlashCommands(); |
| 984 | await initPresetManager(); | 988 | await initPresetManager(); |
| 985 | await getSystemMessages(); | 989 | await getSystemMessages(); |
| 986 | sendSystemMessage(system_message_types.WELCOME); | ||
| 987 | sendSystemMessage(system_message_types.WELCOME_PROMPT); | ||
| 988 | await getSettings(); | 990 | await getSettings(); |
| 989 | initKeyboard(); | 991 | initKeyboard(); |
| 990 | initDynamicStyles(); | 992 | initDynamicStyles(); |
| @@ -1009,6 +1011,7 @@ async function firstLoadInit() { | |||
| 1009 | initSettingsSearch(); | 1011 | initSettingsSearch(); |
| 1010 | initBulkEdit(); | 1012 | initBulkEdit(); |
| 1011 | initReasoning(); | 1013 | initReasoning(); |
| 1014 | initWelcomeScreen(); | ||
| 1012 | await initScrapers(); | 1015 | await initScrapers(); |
| 1013 | initCustomSelectedSamplers(); | 1016 | initCustomSelectedSamplers(); |
| 1014 | addDebugFunctions(); | 1017 | addDebugFunctions(); |
| @@ -1472,6 +1475,11 @@ function getCharacterBlock(item, id) { | |||
| 1472 | template.toggleClass('is_fav', item.fav || item.fav == 'true'); | 1475 | template.toggleClass('is_fav', item.fav || item.fav == 'true'); |
| 1473 | template.find('.ch_fav').val(item.fav); | 1476 | template.find('.ch_fav').val(item.fav); |
| 1474 | 1477 | ||
| 1478 | const isAssistant = item.avatar === getPermanentAssistantAvatar(); | ||
| 1479 | if (!isAssistant) { | ||
| 1480 | template.find('.ch_assistant').remove(); | ||
| 1481 | } | ||
| 1482 | |||
| 1475 | const description = item.data?.creator_notes || ''; | 1483 | const description = item.data?.creator_notes || ''; |
| 1476 | if (description) { | 1484 | if (description) { |
| 1477 | template.find('.ch_description').text(description); | 1485 | template.find('.ch_description').text(description); |
| @@ -2040,7 +2048,7 @@ export async function sendTextareaMessage() { | |||
| 2040 | } | 2048 | } |
| 2041 | 2049 | ||
| 2042 | if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) { | 2050 | if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) { |
| 2043 | await newAssistantChat(); | 2051 | await newAssistantChat({ temporary: false }); |
| 2044 | } | 2052 | } |
| 2045 | 2053 | ||
| 2046 | Generate(generateType); | 2054 | Generate(generateType); |
| @@ -2291,6 +2299,7 @@ function getMessageFromTemplate({ | |||
| 2291 | timestamp, | 2299 | timestamp, |
| 2292 | tokenCount, | 2300 | tokenCount, |
| 2293 | extra, | 2301 | extra, |
| 2302 | type, | ||
| 2294 | }) { | 2303 | }) { |
| 2295 | const mes = messageTemplate.clone(); | 2304 | const mes = messageTemplate.clone(); |
| 2296 | mes.attr({ | 2305 | mes.attr({ |
| @@ -2302,6 +2311,7 @@ function getMessageFromTemplate({ | |||
| 2302 | 'bookmark_link': bookmarkLink, | 2311 | 'bookmark_link': bookmarkLink, |
| 2303 | 'force_avatar': !!forceAvatar, | 2312 | 'force_avatar': !!forceAvatar, |
| 2304 | 'timestamp': timestamp, | 2313 | 'timestamp': timestamp, |
| 2314 | ...(type ? { type } : {}), | ||
| 2305 | }); | 2315 | }); |
| 2306 | mes.find('.avatar img').attr('src', avatarImg); | 2316 | mes.find('.avatar img').attr('src', avatarImg); |
| 2307 | mes.find('.ch_name .name_text').text(characterName); | 2317 | mes.find('.ch_name .name_text').text(characterName); |
| @@ -2513,6 +2523,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | |||
| 2513 | timestamp: timestamp, | 2523 | timestamp: timestamp, |
| 2514 | extra: mes.extra, | 2524 | extra: mes.extra, |
| 2515 | tokenCount: mes.extra?.token_count ?? 0, | 2525 | tokenCount: mes.extra?.token_count ?? 0, |
| 2526 | type: mes.extra?.type ?? '', | ||
| 2516 | ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration, mes.extra?.time_to_first_token), | 2527 | ...formatGenerationTimer(mes.gen_started, mes.gen_finished, mes.extra?.token_count, mes.extra?.reasoning_duration, mes.extra?.time_to_first_token), |
| 2517 | }; | 2528 | }; |
| 2518 | 2529 | ||
| @@ -2882,7 +2893,14 @@ export async function processCommands(message) { | |||
| 2882 | return true; | 2893 | return true; |
| 2883 | } | 2894 | } |
| 2884 | 2895 | ||
| 2885 | export function sendSystemMessage(type, text, extra = {}) { | 2896 | /** |
| 2897 | * Gets a system message by type. | ||
| 2898 | * @param {string} type Type of system message | ||
| 2899 | * @param {string} [text] Text to be sent | ||
| 2900 | * @param {object} [extra] Additional data to be added to the message | ||
| 2901 | * @returns {object} System message object | ||
| 2902 | */ | ||
| 2903 | export function getSystemMessageByType(type, text, extra = {}) { | ||
| 2886 | const systemMessage = system_messages[type]; | 2904 | const systemMessage = system_messages[type]; |
| 2887 | 2905 | ||
| 2888 | if (!systemMessage) { | 2906 | if (!systemMessage) { |
| @@ -2905,7 +2923,17 @@ export function sendSystemMessage(type, text, extra = {}) { | |||
| 2905 | 2923 | ||
| 2906 | newMessage.extra = Object.assign(newMessage.extra, extra); | 2924 | newMessage.extra = Object.assign(newMessage.extra, extra); |
| 2907 | newMessage.extra.type = type; | 2925 | newMessage.extra.type = type; |
| 2926 | return newMessage; | ||
| 2927 | } | ||
| 2908 | 2928 | ||
| 2929 | /** | ||
| 2930 | * Sends a system message to the chat. | ||
| 2931 | * @param {string} type Type of system message | ||
| 2932 | * @param {string} [text] Text to be sent | ||
| 2933 | * @param {object} [extra] Additional data to be added to the message | ||
| 2934 | */ | ||
| 2935 | export function sendSystemMessage(type, text, extra = {}) { | ||
| 2936 | const newMessage = getSystemMessageByType(type, text, extra); | ||
| 2909 | chat.push(newMessage); | 2937 | chat.push(newMessage); |
| 2910 | addOneMessage(newMessage); | 2938 | addOneMessage(newMessage); |
| 2911 | is_send_press = false; | 2939 | is_send_press = false; |
| @@ -10112,8 +10140,17 @@ async function removeCharacterFromUI() { | |||
| 10112 | saveSettingsDebounced(); | 10140 | saveSettingsDebounced(); |
| 10113 | } | 10141 | } |
| 10114 | 10142 | ||
| 10115 | async function newAssistantChat() { | 10143 | /** |
| 10144 | * Creates a new assistant chat. | ||
| 10145 | * @param {object} params - Parameters for the new assistant chat | ||
| 10146 | * @param {boolean} [params.temporary=false] I need a temporary secretary | ||
| 10147 | * @returns {Promise<void>} - A promise that resolves when the new assistant chat is created | ||
| 10148 | */ | ||
| 10149 | export async function newAssistantChat({ temporary = false } = {}) { | ||
| 10116 | await clearChat(); | 10150 | await clearChat(); |
| 10151 | if (!temporary) { | ||
| 10152 | return openPermanentAssistantChat(); | ||
| 10153 | } | ||
| 10117 | chat.splice(0, chat.length); | 10154 | chat.splice(0, chat.length); |
| 10118 | chat_metadata = {}; | 10155 | chat_metadata = {}; |
| 10119 | setCharacterName(neutralCharacterName); | 10156 | setCharacterName(neutralCharacterName); |
| @@ -10426,7 +10463,7 @@ jQuery(async function () { | |||
| 10426 | if (chatId) { | 10463 | if (chatId) { |
| 10427 | return reject('Not in a temporary chat'); | 10464 | return reject('Not in a temporary chat'); |
| 10428 | } | 10465 | } |
| 10429 | await newAssistantChat(); | 10466 | await newAssistantChat({ temporary: true }); |
| 10430 | return resolve(''); | 10467 | return resolve(''); |
| 10431 | }; | 10468 | }; |
| 10432 | eventSource.once(event_types.CHAT_CHANGED, eventCallback); | 10469 | eventSource.once(event_types.CHAT_CHANGED, eventCallback); |
| @@ -11093,6 +11130,9 @@ jQuery(async function () { | |||
| 11093 | }); | 11130 | }); |
| 11094 | 11131 | ||
| 11095 | if (id == 'option_select_chat') { | 11132 | if (id == 'option_select_chat') { |
| 11133 | if (this_chid === undefined && !is_send_press) { | ||
| 11134 | await openPermanentAssistantCard(); | ||
| 11135 | } | ||
| 11096 | if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) { | 11136 | if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) { |
| 11097 | await displayPastChats(); | 11137 | await displayPastChats(); |
| 11098 | //this is just to avoid the shadow for past chat view when using /delchat | 11138 | //this is just to avoid the shadow for past chat view when using /delchat |
| @@ -11123,7 +11163,7 @@ jQuery(async function () { | |||
| 11123 | await doNewChat({ deleteCurrentChat: deleteCurrentChat }); | 11163 | await doNewChat({ deleteCurrentChat: deleteCurrentChat }); |
| 11124 | } | 11164 | } |
| 11125 | if (!selected_group && this_chid === undefined && !is_send_press) { | 11165 | if (!selected_group && this_chid === undefined && !is_send_press) { |
| 11126 | await newAssistantChat(); | 11166 | await newAssistantChat({ temporary: true }); |
| 11127 | } | 11167 | } |
| 11128 | } | 11168 | } |
| 11129 | 11169 | ||
| @@ -11176,9 +11216,6 @@ jQuery(async function () { | |||
| 11176 | selected_button = 'characters'; | 11216 | selected_button = 'characters'; |
| 11177 | $('#rm_button_selected_ch').children('h2').text(''); | 11217 | $('#rm_button_selected_ch').children('h2').text(''); |
| 11178 | select_rm_characters(); | 11218 | select_rm_characters(); |
| 11179 | sendSystemMessage(system_message_types.WELCOME); | ||
| 11180 | sendSystemMessage(system_message_types.WELCOME_PROMPT); | ||
| 11181 | await getClientVersion(); | ||
| 11182 | await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); | 11219 | await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); |
| 11183 | } else { | 11220 | } else { |
| 11184 | toastr.info('Please stop the message generation first.'); | 11221 | toastr.info('Please stop the message generation first.'); |
| @@ -12108,7 +12145,7 @@ jQuery(async function () { | |||
| 12108 | ); | 12145 | ); |
| 12109 | break;*/ | 12146 | break;*/ |
| 12110 | default: | 12147 | default: |
| 12111 | await eventSource.emit('charManagementDropdown', target); | 12148 | await eventSource.emit(event_types.CHARACTER_MANAGEMENT_DROPDOWN, target); |
| 12112 | } | 12149 | } |
| 12113 | $('#char-management-dropdown').prop('selectedIndex', 0); | 12150 | $('#char-management-dropdown').prop('selectedIndex', 0); |
| 12114 | }); | 12151 | }); |
| @@ -23,6 +23,9 @@ import { | |||
| 23 | neutralCharacterName, | 23 | neutralCharacterName, |
| 24 | updateChatMetadata, | 24 | updateChatMetadata, |
| 25 | system_message_types, | 25 | system_message_types, |
| 26 | getSystemMessageByType, | ||
| 27 | printMessages, | ||
| 28 | clearChat, | ||
| 26 | } from '../script.js'; | 29 | } from '../script.js'; |
| 27 | import { selected_group } from './group-chats.js'; | 30 | import { selected_group } from './group-chats.js'; |
| 28 | import { power_user } from './power-user.js'; | 31 | import { power_user } from './power-user.js'; |
| @@ -37,6 +40,7 @@ import { | |||
| 37 | saveBase64AsFile, | 40 | saveBase64AsFile, |
| 38 | extractTextFromOffice, | 41 | extractTextFromOffice, |
| 39 | download, | 42 | download, |
| 43 | getFileText, | ||
| 40 | } from './utils.js'; | 44 | } from './utils.js'; |
| 41 | import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; | 45 | import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; |
| 42 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 46 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| @@ -1497,6 +1501,35 @@ jQuery(function () { | |||
| 1497 | download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json'); | 1501 | download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json'); |
| 1498 | }); | 1502 | }); |
| 1499 | 1503 | ||
| 1504 | $(document).on('click', '.assistant_note_import', async function () { | ||
| 1505 | const importFile = async () => { | ||
| 1506 | const file = fileInput.files[0]; | ||
| 1507 | if (!file) { | ||
| 1508 | return; | ||
| 1509 | } | ||
| 1510 | |||
| 1511 | try { | ||
| 1512 | const text = await getFileText(file); | ||
| 1513 | const lines = text.split('\n').filter(line => line.trim() !== ''); | ||
| 1514 | const messages = lines.map(line => JSON.parse(line)); | ||
| 1515 | const metadata = messages.shift()?.chat_metadata || {}; | ||
| 1516 | messages.unshift(getSystemMessageByType(system_message_types.ASSISTANT_NOTE)); | ||
| 1517 | await clearChat(); | ||
| 1518 | chat.splice(0, chat.length, ...messages); | ||
| 1519 | updateChatMetadata(metadata, true); | ||
| 1520 | await printMessages(); | ||
| 1521 | } catch (error) { | ||
| 1522 | console.error('Error importing assistant chat:', error); | ||
| 1523 | toastr.error(t`It's either corrupted or not a valid JSONL file.`, t`Failed to import chat`); | ||
| 1524 | } | ||
| 1525 | }; | ||
| 1526 | const fileInput = document.createElement('input'); | ||
| 1527 | fileInput.type = 'file'; | ||
| 1528 | fileInput.accept = '.jsonl'; | ||
| 1529 | fileInput.addEventListener('change', importFile); | ||
| 1530 | fileInput.click(); | ||
| 1531 | }); | ||
| 1532 | |||
| 1500 | // Do not change. #attachFile is added by extension. | 1533 | // Do not change. #attachFile is added by extension. |
| 1501 | $(document).on('click', '#attachFile', function () { | 1534 | $(document).on('click', '#attachFile', function () { |
| 1502 | $('#file_form_input').trigger('click'); | 1535 | $('#file_form_input').trigger('click'); |
| @@ -710,7 +710,7 @@ async function listGalleryCommand(args) { | |||
| 710 | delete context.extensionSettings.gallery.folders[avatar]; | 710 | delete context.extensionSettings.gallery.folders[avatar]; |
| 711 | context.saveSettingsDebounced(); | 711 | context.saveSettingsDebounced(); |
| 712 | }); | 712 | }); |
| 713 | eventSource.on('charManagementDropdown', (selectedOptionId) => { | 713 | eventSource.on(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (selectedOptionId) => { |
| 714 | if (selectedOptionId === 'show_char_gallery') { | 714 | if (selectedOptionId === 'show_char_gallery') { |
| 715 | showCharGallery(); | 715 | showCharGallery(); |
| 716 | } | 716 | } |
| @@ -1970,7 +1970,7 @@ export async function initPersonas() { | |||
| 1970 | 1970 | ||
| 1971 | $('#char_connections_button').on('click', showCharConnections); | 1971 | $('#char_connections_button').on('click', showCharConnections); |
| 1972 | 1972 | ||
| 1973 | eventSource.on('charManagementDropdown', (target) => { | 1973 | eventSource.on(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (target) => { |
| 1974 | if (target === 'convert_to_persona') { | 1974 | if (target === 'convert_to_persona') { |
| 1975 | convertCharacterToPersona(); | 1975 | convertCharacterToPersona(); |
| 1976 | } | 1976 | } |
| @@ -1,9 +1,13 @@ | |||
| 1 | <div data-type="assistant_note"> | 1 | <div data-type="assistant_note"> |
| 2 | <div> | 2 | <div class="assistant_note_title"> |
| 3 | <b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span> | 3 | <b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span> |
| 4 | <span data-i18n="Click the button to save it as a file.">Click the button to save it as a file.</span> | ||
| 5 | </div> | 4 | </div> |
| 6 | <div class="assistant_note_export menu_button menu_button_icon" data-i18n="[title]Export as JSONL" title="Export as JSONL"> | 5 | <button class="assistant_note_import menu_button menu_button_icon margin0" data-i18n="[title]Import from JSONL" title="Import from JSONL"> |
| 6 | <i class="fa-solid fa-file-import"></i> | ||
| 7 | <span data-i18n="Load">Load</span> | ||
| 8 | </button> | ||
| 9 | <button class="assistant_note_export menu_button menu_button_icon margin0" data-i18n="[title]Export as JSONL" title="Export as JSONL"> | ||
| 7 | <i class="fa-solid fa-file-export"></i> | 10 | <i class="fa-solid fa-file-export"></i> |
| 8 | </div> | 11 | <span data-i18n="Save">Save</span> |
| 12 | </button> | ||
| 9 | </div> | 13 | </div> |
| @@ -0,0 +1,82 @@ | |||
| 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 class="mes_button showRecentChats" title="Show recent chats" data-i18n="[title]Show recent chats"> | ||
| 6 | <i class="fa-solid fa-circle-chevron-down fa-fw fa-lg"></i> | ||
| 7 | </div> | ||
| 8 | <div class="mes_button hideRecentChats" title="Hide recent chats" data-i18n="[title]Hide recent chats"> | ||
| 9 | <i class="fa-solid fa-circle-xmark fa-fw fa-lg"></i> | ||
| 10 | </div> | ||
| 11 | </div> | ||
| 12 | <div class="welcomeHeader"> | ||
| 13 | <div class="recentChatsTitle" data-i18n="Recent Chats"> | ||
| 14 | Recent Chats | ||
| 15 | </div> | ||
| 16 | <div class="welcomeShortcuts"> | ||
| 17 | <a class="menu_button menu_button_icon" target="_blank" href="https://docs.sillytavern.app/"> | ||
| 18 | <i class="fa-solid fa-question-circle"></i> | ||
| 19 | <span data-i18n="Docs">Docs</span> | ||
| 20 | </a> | ||
| 21 | <a class="menu_button menu_button_icon" target="_blank" href="https://github.com/SillyTavern/SillyTavern"> | ||
| 22 | <i class="fa-brands fa-github"></i> | ||
| 23 | <span data-i18n="GitHub">GitHub</span> | ||
| 24 | </a> | ||
| 25 | <a class="menu_button menu_button_icon" target="_blank" href="https://discord.gg/sillytavern"> | ||
| 26 | <i class="fa-brands fa-discord"></i> | ||
| 27 | <span data-i18n="Discord">Discord</span> | ||
| 28 | </a> | ||
| 29 | <span class="welcomeShortcutsSeparator">|</span> | ||
| 30 | <button class="openTemporaryChat menu_button menu_button_icon"> | ||
| 31 | <i class="fa-solid fa-comment-dots"></i> | ||
| 32 | <span data-i18n="Temporary Chat">Temporary Chat</span> | ||
| 33 | </button> | ||
| 34 | </div> | ||
| 35 | </div> | ||
| 36 | <div class="welcomeRecent"> | ||
| 37 | <div class="recentChatList"> | ||
| 38 | {{#if empty}} | ||
| 39 | <div class="noRecentChat"> | ||
| 40 | <i class="fa-solid fa-comment-dots"></i> | ||
| 41 | <span data-i18n="No recent chats">No recent chats</span> | ||
| 42 | </div> | ||
| 43 | {{/if}} | ||
| 44 | {{#each chats}} | ||
| 45 | {{#with this}} | ||
| 46 | <div class="recentChat {{#if hidden}}hidden{{/if}} {{#if is_group}}group{{/if}}" data-file="{{chat_name}}" data-avatar="{{avatar}}" data-group="{{group}}"> | ||
| 47 | <div class="avatar" title="[Character] {{char_name}} File: {{avatar}}"> | ||
| 48 | <img src="{{char_thumbnail}}" alt="{{char_name}}"> | ||
| 49 | </div> | ||
| 50 | <div class="recentChatInfo"> | ||
| 51 | <div class="chatNameContainer"> | ||
| 52 | <div class="chatName" title="{{file_name}}"> | ||
| 53 | <strong class="characterName">{{char_name}}</strong> | ||
| 54 | <span>–</span> | ||
| 55 | <span>{{chat_name}}</span> | ||
| 56 | </div> | ||
| 57 | <small class="chatDate" title="{{date_long}}">{{date_short}}</small> | ||
| 58 | </div> | ||
| 59 | <div class="chatMessageContainer"> | ||
| 60 | <div class="chatMessage" title="{{mes}}"> | ||
| 61 | {{mes}} | ||
| 62 | </div> | ||
| 63 | <div class="chatStats"> | ||
| 64 | <div class="counterBlock"> | ||
| 65 | <i class="fa-solid fa-comment fa-xs"></i> | ||
| 66 | <small>{{chat_items}}</small> | ||
| 67 | </div> | ||
| 68 | <small class="fileSize">{{file_size}}</small> | ||
| 69 | </div> | ||
| 70 | </div> | ||
| 71 | </div> | ||
| 72 | </div> | ||
| 73 | {{/with}} | ||
| 74 | {{/each}} | ||
| 75 | {{#if more}} | ||
| 76 | <button class="menu_button menu_button_icon showMoreChats"> | ||
| 77 | <small class="fa-solid fa-chevron-down fa-fw fa-1x"></small> | ||
| 78 | </button> | ||
| 79 | {{/if}} | ||
| 80 | </div> | ||
| 81 | </div> | ||
| 82 | </div> | ||
| @@ -1,3 +1,14 @@ | |||
| 1 | <strong data-i18n="If you're connected to an API, try asking me something!"> | 1 | <div class="flex-container"> |
| 2 | If you're connected to an API, try asking me something! | 2 | <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="sys-settings-button"> |
| 3 | </strong> | 3 | <i class="fa-solid fa-plug"></i> |
| 4 | <span data-i18n="API Connections">API Connections</span> | ||
| 5 | </button> | ||
| 6 | <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="rightNavHolder"> | ||
| 7 | <i class="fa-solid fa-address-card"></i> | ||
| 8 | <span data-i18n="Character Management">Character Management</span> | ||
| 9 | </button> | ||
| 10 | <button class="menu_button menu_button_icon drawer-opener inline-flex" data-target="extensions-settings-button"> | ||
| 11 | <i class="fa-solid fa-cubes"></i> | ||
| 12 | <span data-i18n="Extensions">Extensions</span> | ||
| 13 | </button> | ||
| 14 | </div> | ||
| @@ -0,0 +1,413 @@ | |||
| 1 | import { | ||
| 2 | addOneMessage, | ||
| 3 | characters, | ||
| 4 | chat, | ||
| 5 | displayVersion, | ||
| 6 | doNewChat, | ||
| 7 | event_types, | ||
| 8 | eventSource, | ||
| 9 | getCharacters, | ||
| 10 | getCurrentChatId, | ||
| 11 | getRequestHeaders, | ||
| 12 | getSystemMessageByType, | ||
| 13 | getThumbnailUrl, | ||
| 14 | is_send_press, | ||
| 15 | neutralCharacterName, | ||
| 16 | newAssistantChat, | ||
| 17 | openCharacterChat, | ||
| 18 | printCharactersDebounced, | ||
| 19 | selectCharacterById, | ||
| 20 | system_avatar, | ||
| 21 | system_message_types, | ||
| 22 | this_chid, | ||
| 23 | } from '../script.js'; | ||
| 24 | import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js'; | ||
| 25 | import { t } from './i18n.js'; | ||
| 26 | import { renderTemplateAsync } from './templates.js'; | ||
| 27 | import { accountStorage } from './util/AccountStorage.js'; | ||
| 28 | import { sortMoments, timestampToMoment } from './utils.js'; | ||
| 29 | |||
| 30 | const assistantAvatarKey = 'assistant'; | ||
| 31 | const defaultAssistantAvatar = 'default_Assistant.png'; | ||
| 32 | |||
| 33 | const DEFAULT_DISPLAYED = 3; | ||
| 34 | const MAX_DISPLAYED = 15; | ||
| 35 | |||
| 36 | export function getPermanentAssistantAvatar() { | ||
| 37 | const assistantAvatar = accountStorage.getItem(assistantAvatarKey); | ||
| 38 | if (assistantAvatar === null) { | ||
| 39 | return defaultAssistantAvatar; | ||
| 40 | } | ||
| 41 | |||
| 42 | const character = characters.find(x => x.avatar === assistantAvatar); | ||
| 43 | if (character === undefined) { | ||
| 44 | accountStorage.removeItem(assistantAvatarKey); | ||
| 45 | return defaultAssistantAvatar; | ||
| 46 | } | ||
| 47 | |||
| 48 | return assistantAvatar; | ||
| 49 | } | ||
| 50 | |||
| 51 | export async function openWelcomeScreen() { | ||
| 52 | const currentChatId = getCurrentChatId(); | ||
| 53 | if (currentChatId !== undefined || chat.length > 0) { | ||
| 54 | return; | ||
| 55 | } | ||
| 56 | |||
| 57 | await sendWelcomePanel(); | ||
| 58 | sendAssistantMessage(); | ||
| 59 | sendWelcomePrompt(); | ||
| 60 | } | ||
| 61 | |||
| 62 | function sendAssistantMessage() { | ||
| 63 | const currentAssistantAvatar = getPermanentAssistantAvatar(); | ||
| 64 | const character = characters.find(x => x.avatar === currentAssistantAvatar); | ||
| 65 | const name = character ? character.name : neutralCharacterName; | ||
| 66 | const avatar = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar; | ||
| 67 | |||
| 68 | const message = { | ||
| 69 | name: name, | ||
| 70 | force_avatar: avatar, | ||
| 71 | mes: t`If you're connected to an API, try asking me something!` + '\n***\n' + t`**Hint:** Set any character as your welcome page assistant from their "More..." menu.`, | ||
| 72 | is_system: false, | ||
| 73 | is_user: false, | ||
| 74 | extra: { | ||
| 75 | type: system_message_types.ASSISTANT_MESSAGE, | ||
| 76 | }, | ||
| 77 | }; | ||
| 78 | |||
| 79 | chat.push(message); | ||
| 80 | addOneMessage(message, { scroll: false }); | ||
| 81 | } | ||
| 82 | |||
| 83 | function sendWelcomePrompt() { | ||
| 84 | const message = getSystemMessageByType(system_message_types.WELCOME_PROMPT); | ||
| 85 | chat.push(message); | ||
| 86 | addOneMessage(message, { scroll: false }); | ||
| 87 | } | ||
| 88 | |||
| 89 | async function sendWelcomePanel() { | ||
| 90 | try { | ||
| 91 | const chatElement = document.getElementById('chat'); | ||
| 92 | const sendTextArea = document.getElementById('send_textarea'); | ||
| 93 | if (!chatElement) { | ||
| 94 | console.error('Chat element not found'); | ||
| 95 | return; | ||
| 96 | } | ||
| 97 | const chats = await getRecentChats(); | ||
| 98 | const templateData = { | ||
| 99 | chats, | ||
| 100 | empty: !chats.length, | ||
| 101 | version: displayVersion, | ||
| 102 | more: chats.some(chat => chat.hidden), | ||
| 103 | }; | ||
| 104 | const template = await renderTemplateAsync('welcomePanel', templateData); | ||
| 105 | const fragment = document.createRange().createContextualFragment(template); | ||
| 106 | fragment.querySelectorAll('.welcomePanel').forEach((root) => { | ||
| 107 | const recentHiddenClass = 'recentHidden'; | ||
| 108 | const recentHiddenKey = 'WelcomePage_RecentChatsHidden'; | ||
| 109 | if (accountStorage.getItem(recentHiddenKey) === 'true') { | ||
| 110 | root.classList.add(recentHiddenClass); | ||
| 111 | } | ||
| 112 | root.querySelectorAll('.showRecentChats').forEach((button) => { | ||
| 113 | button.addEventListener('click', () => { | ||
| 114 | root.classList.remove(recentHiddenClass); | ||
| 115 | accountStorage.setItem(recentHiddenKey, 'false'); | ||
| 116 | }); | ||
| 117 | }); | ||
| 118 | root.querySelectorAll('.hideRecentChats').forEach((button) => { | ||
| 119 | button.addEventListener('click', () => { | ||
| 120 | root.classList.add(recentHiddenClass); | ||
| 121 | accountStorage.setItem(recentHiddenKey, 'true'); | ||
| 122 | }); | ||
| 123 | }); | ||
| 124 | }); | ||
| 125 | fragment.querySelectorAll('.recentChat').forEach((item) => { | ||
| 126 | item.addEventListener('click', () => { | ||
| 127 | const avatarId = item.getAttribute('data-avatar'); | ||
| 128 | const groupId = item.getAttribute('data-group'); | ||
| 129 | const fileName = item.getAttribute('data-file'); | ||
| 130 | if (avatarId && fileName) { | ||
| 131 | void openRecentCharacterChat(avatarId, fileName); | ||
| 132 | } | ||
| 133 | if (groupId && fileName) { | ||
| 134 | void openRecentGroupChat(groupId, fileName); | ||
| 135 | } | ||
| 136 | }); | ||
| 137 | }); | ||
| 138 | const hiddenChats = fragment.querySelectorAll('.recentChat.hidden'); | ||
| 139 | fragment.querySelectorAll('button.showMoreChats').forEach((button) => { | ||
| 140 | const showRecentChatsTitle = t`Show more recent chats`; | ||
| 141 | const hideRecentChatsTitle = t`Show less recent chats`; | ||
| 142 | |||
| 143 | button.setAttribute('title', showRecentChatsTitle); | ||
| 144 | button.addEventListener('click', () => { | ||
| 145 | const rotate = button.classList.contains('rotated'); | ||
| 146 | hiddenChats.forEach((chatItem) => { | ||
| 147 | chatItem.classList.toggle('hidden', rotate); | ||
| 148 | }); | ||
| 149 | button.classList.toggle('rotated', !rotate); | ||
| 150 | button.setAttribute('title', rotate ? showRecentChatsTitle : hideRecentChatsTitle); | ||
| 151 | }); | ||
| 152 | }); | ||
| 153 | fragment.querySelectorAll('button.openTemporaryChat').forEach((button) => { | ||
| 154 | button.addEventListener('click', async () => { | ||
| 155 | await newAssistantChat({ temporary: true }); | ||
| 156 | if (sendTextArea instanceof HTMLTextAreaElement) { | ||
| 157 | sendTextArea.focus(); | ||
| 158 | } | ||
| 159 | }); | ||
| 160 | }); | ||
| 161 | fragment.querySelectorAll('.recentChat.group').forEach((groupChat) => { | ||
| 162 | const groupId = groupChat.getAttribute('data-group'); | ||
| 163 | const group = groups.find(x => x.id === groupId); | ||
| 164 | if (group) { | ||
| 165 | const avatar = groupChat.querySelector('.avatar'); | ||
| 166 | if (!avatar) { | ||
| 167 | return; | ||
| 168 | } | ||
| 169 | const groupAvatar = getGroupAvatar(group); | ||
| 170 | $(avatar).replaceWith(groupAvatar); | ||
| 171 | } | ||
| 172 | }); | ||
| 173 | chatElement.append(fragment.firstChild); | ||
| 174 | } catch (error) { | ||
| 175 | console.error('Welcome screen error:', error); | ||
| 176 | } | ||
| 177 | } | ||
| 178 | |||
| 179 | /** | ||
| 180 | * Opens a recent character chat. | ||
| 181 | * @param {string} avatarId Avatar file name | ||
| 182 | * @param {string} fileName Chat file name | ||
| 183 | */ | ||
| 184 | async function openRecentCharacterChat(avatarId, fileName) { | ||
| 185 | const characterId = characters.findIndex(x => x.avatar === avatarId); | ||
| 186 | if (characterId === -1) { | ||
| 187 | console.error(`Character not found for avatar ID: ${avatarId}`); | ||
| 188 | return; | ||
| 189 | } | ||
| 190 | |||
| 191 | try { | ||
| 192 | await selectCharacterById(characterId); | ||
| 193 | const currentChatId = getCurrentChatId(); | ||
| 194 | if (currentChatId === fileName) { | ||
| 195 | console.debug(`Chat ${fileName} is already open.`); | ||
| 196 | return; | ||
| 197 | } | ||
| 198 | await openCharacterChat(fileName); | ||
| 199 | } catch (error) { | ||
| 200 | console.error('Error opening recent chat:', error); | ||
| 201 | toastr.error(t`Failed to open recent chat. See console for details.`); | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | /** | ||
| 206 | * Opens a recent group chat. | ||
| 207 | * @param {string} groupId Group ID | ||
| 208 | * @param {string} fileName Chat file name | ||
| 209 | */ | ||
| 210 | async function openRecentGroupChat(groupId, fileName) { | ||
| 211 | const group = groups.find(x => x.id === groupId); | ||
| 212 | if (!group) { | ||
| 213 | console.error(`Group not found for ID: ${groupId}`); | ||
| 214 | return; | ||
| 215 | } | ||
| 216 | |||
| 217 | try { | ||
| 218 | await openGroupById(groupId); | ||
| 219 | const currentChatId = getCurrentChatId(); | ||
| 220 | if (currentChatId === fileName) { | ||
| 221 | console.debug(`Chat ${fileName} is already open.`); | ||
| 222 | return; | ||
| 223 | } | ||
| 224 | await openGroupChat(groupId, fileName); | ||
| 225 | } catch (error) { | ||
| 226 | console.error('Error opening recent group chat:', error); | ||
| 227 | toastr.error(t`Failed to open recent group chat. See console for details.`); | ||
| 228 | } | ||
| 229 | } | ||
| 230 | |||
| 231 | /** | ||
| 232 | * Gets the list of recent chats from the server. | ||
| 233 | * @returns {Promise<RecentChat[]>} List of recent chats | ||
| 234 | * | ||
| 235 | * @typedef {object} RecentChat | ||
| 236 | * @property {string} file_name Name of the chat file | ||
| 237 | * @property {string} chat_name Name of the chat (without extension) | ||
| 238 | * @property {string} file_size Size of the chat file | ||
| 239 | * @property {number} chat_items Number of items in the chat | ||
| 240 | * @property {string} mes Last message content | ||
| 241 | * @property {string} last_mes Timestamp of the last message | ||
| 242 | * @property {string} avatar Avatar URL | ||
| 243 | * @property {string} char_thumbnail Thumbnail URL | ||
| 244 | * @property {string} char_name Character or group name | ||
| 245 | * @property {string} date_short Date in short format | ||
| 246 | * @property {string} date_long Date in long format | ||
| 247 | * @property {string} group Group ID (if applicable) | ||
| 248 | * @property {boolean} is_group Indicates if the chat is a group chat | ||
| 249 | * @property {boolean} hidden Chat will be hidden by default | ||
| 250 | */ | ||
| 251 | async function getRecentChats() { | ||
| 252 | const response = await fetch('/api/chats/recent', { | ||
| 253 | method: 'POST', | ||
| 254 | headers: getRequestHeaders(), | ||
| 255 | body: JSON.stringify({ max: MAX_DISPLAYED }), | ||
| 256 | }); | ||
| 257 | |||
| 258 | if (!response.ok) { | ||
| 259 | console.warn('Failed to fetch recent character chats'); | ||
| 260 | return []; | ||
| 261 | } | ||
| 262 | |||
| 263 | /** @type {RecentChat[]} */ | ||
| 264 | const data = await response.json(); | ||
| 265 | |||
| 266 | data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))) | ||
| 267 | .map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar), group: groups.find(x => x.id === chat.group) })) | ||
| 268 | .filter(t => t.character || t.group) | ||
| 269 | .forEach(({ chat, character, group }, index) => { | ||
| 270 | const chatTimestamp = timestampToMoment(chat.last_mes); | ||
| 271 | chat.char_name = character?.name || group?.name || ''; | ||
| 272 | chat.date_short = chatTimestamp.format('l'); | ||
| 273 | chat.date_long = chatTimestamp.format('LL LT'); | ||
| 274 | chat.chat_name = chat.file_name.replace('.jsonl', ''); | ||
| 275 | chat.char_thumbnail = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar; | ||
| 276 | chat.is_group = !!group; | ||
| 277 | chat.hidden = index >= DEFAULT_DISPLAYED; | ||
| 278 | chat.avatar = chat.avatar || ''; | ||
| 279 | chat.group = chat.group || ''; | ||
| 280 | }); | ||
| 281 | |||
| 282 | return data; | ||
| 283 | } | ||
| 284 | |||
| 285 | export async function openPermanentAssistantChat({ tryCreate = true, created = false } = {}) { | ||
| 286 | const avatar = getPermanentAssistantAvatar(); | ||
| 287 | const characterId = characters.findIndex(x => x.avatar === avatar); | ||
| 288 | if (characterId === -1) { | ||
| 289 | if (!tryCreate) { | ||
| 290 | console.error(`Character not found for avatar ID: ${avatar}. Cannot create.`); | ||
| 291 | return; | ||
| 292 | } | ||
| 293 | |||
| 294 | try { | ||
| 295 | console.log(`Character not found for avatar ID: ${avatar}. Creating new assistant.`); | ||
| 296 | await createPermanentAssistant(); | ||
| 297 | return openPermanentAssistantChat({ tryCreate: false, created: true }); | ||
| 298 | } | ||
| 299 | catch (error) { | ||
| 300 | console.error('Error creating permanent assistant:', error); | ||
| 301 | toastr.error(t`Failed to create ${neutralCharacterName}. See console for details.`); | ||
| 302 | return; | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | try { | ||
| 307 | await selectCharacterById(characterId); | ||
| 308 | if (!created) { | ||
| 309 | await doNewChat({ deleteCurrentChat: false }); | ||
| 310 | } | ||
| 311 | console.log(`Opened permanent assistant chat for ${neutralCharacterName}.`, getCurrentChatId()); | ||
| 312 | } catch (error) { | ||
| 313 | console.error('Error opening permanent assistant chat:', error); | ||
| 314 | toastr.error(t`Failed to open permanent assistant chat. See console for details.`); | ||
| 315 | } | ||
| 316 | } | ||
| 317 | |||
| 318 | async function createPermanentAssistant() { | ||
| 319 | if (is_group_generating || is_send_press) { | ||
| 320 | throw new Error(t`Cannot create while generating.`); | ||
| 321 | } | ||
| 322 | |||
| 323 | const formData = new FormData(); | ||
| 324 | formData.append('ch_name', neutralCharacterName); | ||
| 325 | formData.append('file_name', defaultAssistantAvatar.replace('.png', '')); | ||
| 326 | formData.append('creator_notes', t`Automatically created character. Feel free to edit.`); | ||
| 327 | |||
| 328 | try { | ||
| 329 | const avatarResponse = await fetch(system_avatar); | ||
| 330 | const avatarBlob = await avatarResponse.blob(); | ||
| 331 | formData.append('avatar', avatarBlob, defaultAssistantAvatar); | ||
| 332 | } catch (error) { | ||
| 333 | console.warn('Error fetching system avatar. Fallback image will be used.', error); | ||
| 334 | } | ||
| 335 | |||
| 336 | const headers = getRequestHeaders(); | ||
| 337 | delete headers['Content-Type']; | ||
| 338 | |||
| 339 | const fetchResult = await fetch('/api/characters/create', { | ||
| 340 | method: 'POST', | ||
| 341 | headers: headers, | ||
| 342 | body: formData, | ||
| 343 | cache: 'no-cache', | ||
| 344 | }); | ||
| 345 | |||
| 346 | if (!fetchResult.ok) { | ||
| 347 | throw new Error(t`Creation request did not succeed.`); | ||
| 348 | } | ||
| 349 | |||
| 350 | await getCharacters(); | ||
| 351 | } | ||
| 352 | |||
| 353 | export async function openPermanentAssistantCard() { | ||
| 354 | const avatar = getPermanentAssistantAvatar(); | ||
| 355 | const characterId = characters.findIndex(x => x.avatar === avatar); | ||
| 356 | if (characterId === -1) { | ||
| 357 | toastr.info(t`Assistant not found. Try sending a chat message.`); | ||
| 358 | return; | ||
| 359 | } | ||
| 360 | |||
| 361 | await selectCharacterById(characterId); | ||
| 362 | } | ||
| 363 | |||
| 364 | /** | ||
| 365 | * Assigns a character as the assistant. | ||
| 366 | * @param {string?} characterId Character ID | ||
| 367 | */ | ||
| 368 | export function assignCharacterAsAssistant(characterId) { | ||
| 369 | if (characterId === undefined) { | ||
| 370 | return; | ||
| 371 | } | ||
| 372 | /** @type {import('./char-data.js').v1CharData} */ | ||
| 373 | const character = characters[characterId]; | ||
| 374 | if (!character) { | ||
| 375 | return; | ||
| 376 | } | ||
| 377 | |||
| 378 | const currentAssistantAvatar = getPermanentAssistantAvatar(); | ||
| 379 | if (currentAssistantAvatar === character.avatar) { | ||
| 380 | if (character.avatar === defaultAssistantAvatar) { | ||
| 381 | toastr.info(t`${character.name} is a system assistant. Choose another character.`); | ||
| 382 | return; | ||
| 383 | } | ||
| 384 | |||
| 385 | toastr.info(t`${character.name} is no longer your assistant.`); | ||
| 386 | accountStorage.removeItem(assistantAvatarKey); | ||
| 387 | return; | ||
| 388 | } | ||
| 389 | |||
| 390 | accountStorage.setItem(assistantAvatarKey, character.avatar); | ||
| 391 | printCharactersDebounced(); | ||
| 392 | toastr.success(t`Set ${character.name} as your assistant.`); | ||
| 393 | } | ||
| 394 | |||
| 395 | export function initWelcomeScreen() { | ||
| 396 | const events = [event_types.CHAT_CHANGED, event_types.APP_READY]; | ||
| 397 | for (const event of events) { | ||
| 398 | eventSource.makeFirst(event, openWelcomeScreen); | ||
| 399 | } | ||
| 400 | |||
| 401 | eventSource.on(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (target) => { | ||
| 402 | if (target !== 'set_as_assistant') { | ||
| 403 | return; | ||
| 404 | } | ||
| 405 | assignCharacterAsAssistant(this_chid); | ||
| 406 | }); | ||
| 407 | |||
| 408 | eventSource.on(event_types.CHARACTER_RENAMED, (oldAvatar, newAvatar) => { | ||
| 409 | if (oldAvatar === getPermanentAssistantAvatar()) { | ||
| 410 | accountStorage.setItem(assistantAvatarKey, newAvatar); | ||
| 411 | } | ||
| 412 | }); | ||
| 413 | } | ||
| @@ -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%; |
| @@ -6060,12 +6061,12 @@ body:not(.movingUI) .drawer-content.maximized { | |||
| 6060 | flex-wrap: nowrap; | 6061 | flex-wrap: nowrap; |
| 6061 | justify-content: space-between; | 6062 | justify-content: space-between; |
| 6062 | align-items: center; | 6063 | align-items: center; |
| 6063 | gap: 10px; | 6064 | gap: 5px; |
| 6064 | padding: 0 2px; | ||
| 6065 | } | 6065 | } |
| 6066 | 6066 | ||
| 6067 | .mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) { | 6067 | .mes_text div[data-type="assistant_note"]:has(.assistant_note_export) > div { |
| 6068 | flex: 1; | 6068 | flex: 1; |
| 6069 | text-align: left; | ||
| 6069 | } | 6070 | } |
| 6070 | 6071 | ||
| 6071 | .oneline-dropdown label { | 6072 | .oneline-dropdown label { |
| @@ -1,7 +1,6 @@ | |||
| 1 | import path from 'node:path'; | 1 | import path from 'node:path'; |
| 2 | import fs from 'node:fs'; | 2 | import fs from 'node:fs'; |
| 3 | import { promises as fsPromises } from 'node:fs'; | 3 | import { promises as fsPromises } from 'node:fs'; |
| 4 | import readline from 'node:readline'; | ||
| 5 | import { Buffer } from 'node:buffer'; | 4 | import { Buffer } from 'node:buffer'; |
| 6 | 5 | ||
| 7 | import express from 'express'; | 6 | import express from 'express'; |
| @@ -22,6 +21,7 @@ import { readWorldInfoFile } from './worldinfo.js'; | |||
| 22 | import { invalidateThumbnail } from './thumbnails.js'; | 21 | import { invalidateThumbnail } from './thumbnails.js'; |
| 23 | import { importRisuSprites } from './sprites.js'; | 22 | import { importRisuSprites } from './sprites.js'; |
| 24 | import { getUserDirectories } from '../users.js'; | 23 | import { getUserDirectories } from '../users.js'; |
| 24 | import { getChatInfo } from './chats.js'; | ||
| 25 | const defaultAvatarPath = './public/img/ai4.png'; | 25 | const defaultAvatarPath = './public/img/ai4.png'; |
| 26 | 26 | ||
| 27 | // With 100 MB limit it would take roughly 3000 characters to reach this limit | 27 | // With 100 MB limit it would take roughly 3000 characters to reach this limit |
| @@ -577,10 +577,10 @@ function charaFormatData(data, directories) { | |||
| 577 | _.set(char, 'mes_example', data.mes_example || ''); | 577 | _.set(char, 'mes_example', data.mes_example || ''); |
| 578 | 578 | ||
| 579 | // Old ST extension fields (for backward compatibility, will be deprecated) | 579 | // Old ST extension fields (for backward compatibility, will be deprecated) |
| 580 | _.set(char, 'creatorcomment', data.creator_notes); | 580 | _.set(char, 'creatorcomment', data.creator_notes || ''); |
| 581 | _.set(char, 'avatar', 'none'); | 581 | _.set(char, 'avatar', 'none'); |
| 582 | _.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); | 582 | _.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); |
| 583 | _.set(char, 'talkativeness', data.talkativeness); | 583 | _.set(char, 'talkativeness', data.talkativeness || 0.5); |
| 584 | _.set(char, 'fav', data.fav == 'true'); | 584 | _.set(char, 'fav', data.fav == 'true'); |
| 585 | _.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []); | 585 | _.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []); |
| 586 | 586 | ||
| @@ -604,7 +604,7 @@ function charaFormatData(data, directories) { | |||
| 604 | _.set(char, 'data.alternate_greetings', getAlternateGreetings(data)); | 604 | _.set(char, 'data.alternate_greetings', getAlternateGreetings(data)); |
| 605 | 605 | ||
| 606 | // ST extension fields to V2 object | 606 | // ST extension fields to V2 object |
| 607 | _.set(char, 'data.extensions.talkativeness', data.talkativeness); | 607 | _.set(char, 'data.extensions.talkativeness', data.talkativeness || 0.5); |
| 608 | _.set(char, 'data.extensions.fav', data.fav == 'true'); | 608 | _.set(char, 'data.extensions.fav', data.fav == 'true'); |
| 609 | _.set(char, 'data.extensions.world', data.world || ''); | 609 | _.set(char, 'data.extensions.world', data.world || ''); |
| 610 | 610 | ||
| @@ -943,7 +943,7 @@ router.post('/create', async function (request, response) { | |||
| 943 | request.body.ch_name = sanitize(request.body.ch_name); | 943 | request.body.ch_name = sanitize(request.body.ch_name); |
| 944 | 944 | ||
| 945 | const char = JSON.stringify(charaFormatData(request.body, request.user.directories)); | 945 | const char = JSON.stringify(charaFormatData(request.body, request.user.directories)); |
| 946 | const internalName = getPngName(request.body.ch_name, request.user.directories); | 946 | const internalName = request.body.file_name || getPngName(request.body.ch_name, request.user.directories); |
| 947 | const avatarName = `${internalName}.png`; | 947 | const avatarName = `${internalName}.png`; |
| 948 | const chatsPath = path.join(request.user.directories.chats, internalName); | 948 | const chatsPath = path.join(request.user.directories.chats, internalName); |
| 949 | 949 | ||
| @@ -1228,7 +1228,6 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp | |||
| 1228 | if (!request.body) return response.sendStatus(400); | 1228 | if (!request.body) return response.sendStatus(400); |
| 1229 | 1229 | ||
| 1230 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); | 1230 | const characterDirectory = (request.body.avatar_url).replace('.png', ''); |
| 1231 | |||
| 1232 | const chatsDirectory = path.join(request.user.directories.chats, characterDirectory); | 1231 | const chatsDirectory = path.join(request.user.directories.chats, characterDirectory); |
| 1233 | 1232 | ||
| 1234 | if (!fs.existsSync(chatsDirectory)) { | 1233 | if (!fs.existsSync(chatsDirectory)) { |
| @@ -1248,54 +1247,11 @@ router.post('/chats', validateAvatarUrlMiddleware, async function (request, resp | |||
| 1248 | } | 1247 | } |
| 1249 | 1248 | ||
| 1250 | const jsonFilesPromise = jsonFiles.map((file) => { | 1249 | const jsonFilesPromise = jsonFiles.map((file) => { |
| 1251 | return new Promise(async (res) => { | ||
| 1252 | const pathToFile = path.join(request.user.directories.chats, characterDirectory, file); | 1250 | const pathToFile = path.join(request.user.directories.chats, characterDirectory, file); |
| 1253 | const fileStream = fs.createReadStream(pathToFile); | 1251 | 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 | }); | 1252 | }); |
| 1297 | 1253 | ||
| 1298 | const chatData = await Promise.all(jsonFilesPromise); | 1254 | const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value); |
| 1299 | const validFiles = chatData.filter(i => i.file_name); | 1255 | const validFiles = chatData.filter(i => i.file_name); |
| 1300 | 1256 | ||
| 1301 | return response.send(validFiles); | 1257 | return response.send(validFiles); |
| @@ -351,6 +351,69 @@ async function checkChatIntegrity(filePath, integritySlug) { | |||
| 351 | return chatIntegrity === integritySlug; | 351 | return chatIntegrity === integritySlug; |
| 352 | } | 352 | } |
| 353 | 353 | ||
| 354 | /** | ||
| 355 | * @typedef {Object} ChatInfo | ||
| 356 | * @property {string} [file_name] - The name of the chat file | ||
| 357 | * @property {string} [file_size] - The size of the chat file | ||
| 358 | * @property {number} [chat_items] - The number of chat items in the file | ||
| 359 | * @property {string} [mes] - The last message in the chat | ||
| 360 | * @property {number} [last_mes] - The timestamp of the last message | ||
| 361 | */ | ||
| 362 | |||
| 363 | /** | ||
| 364 | * Reads the information from a chat file. | ||
| 365 | * @param {string} pathToFile | ||
| 366 | * @param {object} additionalData | ||
| 367 | * @returns {Promise<ChatInfo>} | ||
| 368 | */ | ||
| 369 | export async function getChatInfo(pathToFile, additionalData = {}, isGroup = false) { | ||
| 370 | return new Promise(async (res) => { | ||
| 371 | const fileStream = fs.createReadStream(pathToFile); | ||
| 372 | const stats = await fs.promises.stat(pathToFile); | ||
| 373 | const fileSizeInKB = `${(stats.size / 1024).toFixed(2)}kb`; | ||
| 374 | |||
| 375 | if (stats.size === 0) { | ||
| 376 | console.warn(`Found an empty chat file: ${pathToFile}`); | ||
| 377 | res({}); | ||
| 378 | return; | ||
| 379 | } | ||
| 380 | |||
| 381 | const rl = readline.createInterface({ | ||
| 382 | input: fileStream, | ||
| 383 | crlfDelay: Infinity, | ||
| 384 | }); | ||
| 385 | |||
| 386 | let lastLine; | ||
| 387 | let itemCounter = 0; | ||
| 388 | rl.on('line', (line) => { | ||
| 389 | itemCounter++; | ||
| 390 | lastLine = line; | ||
| 391 | }); | ||
| 392 | rl.on('close', () => { | ||
| 393 | rl.close(); | ||
| 394 | |||
| 395 | if (lastLine) { | ||
| 396 | const jsonData = tryParse(lastLine); | ||
| 397 | if (jsonData && (jsonData.name || jsonData.character_name)) { | ||
| 398 | const chatData = {}; | ||
| 399 | |||
| 400 | chatData['file_name'] = path.parse(pathToFile).base; | ||
| 401 | chatData['file_size'] = fileSizeInKB; | ||
| 402 | chatData['chat_items'] = isGroup ? itemCounter : (itemCounter - 1); | ||
| 403 | chatData['mes'] = jsonData['mes'] || '[The chat is empty]'; | ||
| 404 | chatData['last_mes'] = jsonData['send_date'] || stats.mtimeMs; | ||
| 405 | Object.assign(chatData, additionalData); | ||
| 406 | |||
| 407 | res(chatData); | ||
| 408 | } else { | ||
| 409 | console.warn('Found an invalid or corrupted chat file:', pathToFile); | ||
| 410 | res({}); | ||
| 411 | } | ||
| 412 | } | ||
| 413 | }); | ||
| 414 | }); | ||
| 415 | } | ||
| 416 | |||
| 354 | export const router = express.Router(); | 417 | export const router = express.Router(); |
| 355 | 418 | ||
| 356 | router.post('/save', validateAvatarUrlMiddleware, async function (request, response) { | 419 | router.post('/save', validateAvatarUrlMiddleware, async function (request, response) { |
| @@ -809,3 +872,79 @@ router.post('/search', validateAvatarUrlMiddleware, function (request, response) | |||
| 809 | return response.status(500).json({ error: 'Search failed' }); | 872 | return response.status(500).json({ error: 'Search failed' }); |
| 810 | } | 873 | } |
| 811 | }); | 874 | }); |
| 875 | |||
| 876 | router.post('/recent', async function (request, response) { | ||
| 877 | try { | ||
| 878 | /** @type {{pngFile?: string, groupId?: string, filePath: string, mtime: number}[]} */ | ||
| 879 | const allChatFiles = []; | ||
| 880 | |||
| 881 | const getCharacterChatFiles = async () => { | ||
| 882 | const pngDirents = await fs.promises.readdir(request.user.directories.characters, { withFileTypes: true }); | ||
| 883 | const pngFiles = pngDirents.filter(e => e.isFile() && path.extname(e.name) === '.png').map(e => e.name); | ||
| 884 | |||
| 885 | for (const pngFile of pngFiles) { | ||
| 886 | const chatsDirectory = pngFile.replace('.png', ''); | ||
| 887 | const pathToChats = path.join(request.user.directories.chats, chatsDirectory); | ||
| 888 | if (!fs.existsSync(pathToChats)) { | ||
| 889 | continue; | ||
| 890 | } | ||
| 891 | const pathStats = await fs.promises.stat(pathToChats); | ||
| 892 | if (pathStats.isDirectory()) { | ||
| 893 | const chatFiles = await fs.promises.readdir(pathToChats); | ||
| 894 | const jsonlFiles = chatFiles.filter(file => path.extname(file) === '.jsonl'); | ||
| 895 | |||
| 896 | for (const file of jsonlFiles) { | ||
| 897 | const filePath = path.join(pathToChats, file); | ||
| 898 | const stats = await fs.promises.stat(filePath); | ||
| 899 | allChatFiles.push({ pngFile, filePath, mtime: stats.mtimeMs }); | ||
| 900 | } | ||
| 901 | } | ||
| 902 | } | ||
| 903 | }; | ||
| 904 | |||
| 905 | const getGroupChatFiles = async () => { | ||
| 906 | const groupDirents = await fs.promises.readdir(request.user.directories.groups, { withFileTypes: true }); | ||
| 907 | const groups = groupDirents.filter(e => e.isFile() && path.extname(e.name) === '.json').map(e => e.name); | ||
| 908 | |||
| 909 | for (const group of groups) { | ||
| 910 | try { | ||
| 911 | const groupPath = path.join(request.user.directories.groups, group); | ||
| 912 | const groupContents = await fs.promises.readFile(groupPath, 'utf8'); | ||
| 913 | const groupData = JSON.parse(groupContents); | ||
| 914 | |||
| 915 | if (Array.isArray(groupData.chats)) { | ||
| 916 | for (const chat of groupData.chats) { | ||
| 917 | const filePath = path.join(request.user.directories.groupChats, `${chat}.jsonl`); | ||
| 918 | if (!fs.existsSync(filePath)) { | ||
| 919 | continue; | ||
| 920 | } | ||
| 921 | const stats = await fs.promises.stat(filePath); | ||
| 922 | allChatFiles.push({ groupId: groupData.id, filePath, mtime: stats.mtimeMs }); | ||
| 923 | } | ||
| 924 | } | ||
| 925 | } catch (error) { | ||
| 926 | // Skip group files that can't be read or parsed | ||
| 927 | continue; | ||
| 928 | } | ||
| 929 | } | ||
| 930 | }; | ||
| 931 | |||
| 932 | await Promise.allSettled([getCharacterChatFiles(), getGroupChatFiles()]); | ||
| 933 | |||
| 934 | const max = parseInt(request.body.max ?? Number.MAX_SAFE_INTEGER); | ||
| 935 | const recentChats = allChatFiles.sort((a, b) => b.mtime - a.mtime).slice(0, max); | ||
| 936 | const jsonFilesPromise = recentChats.map((file) => { | ||
| 937 | return file.groupId | ||
| 938 | ? getChatInfo(file.filePath, { group: file.groupId }, true) | ||
| 939 | : getChatInfo(file.filePath, { avatar: file.pngFile }, false); | ||
| 940 | }); | ||
| 941 | |||
| 942 | const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value); | ||
| 943 | const validFiles = chatData.filter(i => i.file_name); | ||
| 944 | |||
| 945 | return response.send(validFiles); | ||
| 946 | } catch (error) { | ||
| 947 | console.error(error); | ||
| 948 | return response.sendStatus(500); | ||
| 949 | } | ||
| 950 | }); | ||