Show recent group chats
| @@ -37,7 +37,7 @@ | ||
| 37 | 37 | {{/if}} |
| 38 | 38 | {{#each chats}} |
| 39 | 39 | {{#with this}} |
| 40 | 40 | <div class="recentChat {{#if hidden}}hidden{{/if}} {{#if is_group}}group{{/if}}" data-file="{{chat_name}}" data-avatar="{{avatar}}" data-group="{{group}}"> |
| 41 | 41 | <div class="avatar" title="{{char_name}}"> |
| 42 | 42 | <img src="{{char_thumbnail}}" alt="{{char_name}}"> |
| 43 | 43 | </div> |
| @@ -21,11 +21,11 @@ import { | ||
| 21 | 21 | system_message_types, |
| 22 | 22 | this_chid, |
| 23 | 23 | } from '../script.js'; |
| 24 | 24 | import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js'; |
| 25 | 25 | import { t } from './i18n.js'; |
| 26 | 26 | import { renderTemplateAsync } from './templates.js'; |
| 27 | 27 | import { accountStorage } from './util/AccountStorage.js'; |
| 28 | 28 | import { sortMoments, timestampToMoment } from './utils.js'; |
| 29 | 29 | |
| 30 | 30 | const assistantAvatarKey = 'assistant'; |
| 31 | 31 | const defaultAssistantAvatar = 'default_Assistant.png'; |
| @@ -94,9 +94,13 @@ async function sendWelcomePanel() { | ||
| 94 | 94 | fragment.querySelectorAll('.recentChat').forEach((item) => { |
| 95 | 95 | item.addEventListener('click', () => { |
| 96 | 96 | const avatarId = item.getAttribute('data-avatar'); |
| 97 | + const groupId = item.getAttribute('data-group'); | |
| 97 | 98 | const fileName = item.getAttribute('data-file'); |
| 98 | 99 | if (avatarId && fileName) { |
| 99 | 100 | void openRecentChatopenRecentCharacterChat(avatarId, fileName); |
| 101 | + } | |
| 102 | + if (groupId && fileName) { | |
| 103 | + void openRecentGroupChat(groupId, fileName); | |
| 100 | 104 | } |
| 101 | 105 | }); |
| 102 | 106 | }); |
| @@ -114,6 +118,18 @@ async function sendWelcomePanel() { | ||
| 114 | 118 | void newAssistantChat({ temporary: true }); |
| 115 | 119 | }); |
| 116 | 120 | }); |
| 121 | + fragment.querySelectorAll('.recentChat.group').forEach((groupChat) => { | |
| 122 | + const groupId = groupChat.getAttribute('data-group'); | |
| 123 | + const group = groups.find(x => x.id === groupId); | |
| 124 | + if (group) { | |
| 125 | + const avatar = groupChat.querySelector('.avatar'); | |
| 126 | + if (!avatar) { | |
| 127 | + return; | |
| 128 | + } | |
| 129 | + const groupAvatar = getGroupAvatar(group); | |
| 130 | + $(avatar).replaceWith(groupAvatar); | |
| 131 | + } | |
| 132 | + }); | |
| 117 | 133 | chatElement.append(fragment.firstChild); |
| 118 | 134 | } catch (error) { |
| 119 | 135 | console.error('Welcome screen error:', error); |
| @@ -121,11 +137,11 @@ async function sendWelcomePanel() { | ||
| 121 | 137 | } |
| 122 | 138 | |
| 123 | 139 | /** |
| 124 | 140 | * Opens a recent character chat. |
| 125 | 141 | * @param {string} avatarId Avatar file name |
| 126 | 142 | * @param {string} fileName Chat file name |
| 127 | 143 | */ |
| 128 | 144 | async function openRecentChatopenRecentCharacterChat(avatarId, fileName) { |
| 129 | 145 | const characterId = characters.findIndex(x => x.avatar === avatarId); |
| 130 | 146 | if (characterId === -1) { |
| 131 | 147 | console.error(`Character not found for avatar ID: ${avatarId}`); |
| @@ -147,6 +163,32 @@ async function openRecentChat(avatarId, fileName) { | ||
| 147 | 163 | } |
| 148 | 164 | |
| 149 | 165 | /** |
| 166 | + * Opens a recent group chat. | |
| 167 | + * @param {string} groupId Group ID | |
| 168 | + * @param {string} fileName Chat file name | |
| 169 | + */ | |
| 170 | +async function openRecentGroupChat(groupId, fileName) { | |
| 171 | + const group = groups.find(x => x.id === groupId); | |
| 172 | + if (!group) { | |
| 173 | + console.error(`Group not found for ID: ${groupId}`); | |
| 174 | + return; | |
| 175 | + } | |
| 176 | + | |
| 177 | + try { | |
| 178 | + await openGroupById(groupId); | |
| 179 | + const currentChatId = getCurrentChatId(); | |
| 180 | + if (currentChatId === fileName) { | |
| 181 | + console.debug(`Chat ${fileName} is already open.`); | |
| 182 | + return; | |
| 183 | + } | |
| 184 | + await openGroupChat(groupId, fileName); | |
| 185 | + } catch (error) { | |
| 186 | + console.error('Error opening recent group chat:', error); | |
| 187 | + toastr.error(t`Failed to open recent group chat. See console for details.`); | |
| 188 | + } | |
| 189 | +} | |
| 190 | + | |
| 191 | +/** | |
| 150 | 192 | * Gets the list of recent chats from the server. |
| 151 | 193 | * @returns {Promise<RecentChat[]>} List of recent chats |
| 152 | 194 | * |
| @@ -156,37 +198,56 @@ async function openRecentChat(avatarId, fileName) { | ||
| 156 | 198 | * @property {string} file_size Size of the chat file |
| 157 | 199 | * @property {number} chat_items Number of items in the chat |
| 158 | 200 | * @property {string} mes Last message content |
| 159 | 201 | * @property {numberstring} last_mes Timestamp of the last message |
| 160 | 202 | * @property {string} avatar Avatar URL |
| 161 | 203 | * @property {string} char_thumbnail Thumbnail URL |
| 162 | 204 | * @property {string} char_name Character or group name |
| 163 | 205 | * @property {string} date_short Date in short format |
| 164 | 206 | * @property {string} date_long Date in long format |
| 207 | + * @property {string} group Group ID (if applicable) | |
| 208 | + * @property {boolean} is_group Indicates if the chat is a group chat | |
| 165 | 209 | * @property {boolean} hidden Chat will be hidden by default |
| 166 | 210 | */ |
| 167 | 211 | async function getRecentChats() { |
| 168 | 212 | const responsecharData = awaitasync fetch('/api/characters/recent',) => { |
| 169 | - method: 'POST', | |
| 213 | + const response = await fetch('/api/characters/recent', { | |
| 170 | - headers: getRequestHeaders(), | |
| 214 | + method: 'POST', | |
| 171 | - }); | |
| 215 | + headers: getRequestHeaders(), | |
| 172 | - if (!response.ok) { | |
| 216 | + }); | |
| 173 | - throw new Error('Failed to fetch recent chats'); | |
| 217 | + if (!response.ok) { | |
| 174 | - } | |
| 218 | + console.warn('Failed to fetch recent character chats'); | |
| 219 | + return []; | |
| 220 | + } | |
| 221 | + return await response.json(); | |
| 222 | + }; | |
| 223 | + | |
| 224 | + const groupData = async () => { | |
| 225 | + const response = await fetch('/api/groups/recent', { | |
| 226 | + method: 'POST', | |
| 227 | + headers: getRequestHeaders(), | |
| 228 | + }); | |
| 229 | + if (!response.ok) { | |
| 230 | + console.warn('Failed to fetch recent group chats'); | |
| 231 | + return []; | |
| 232 | + } | |
| 233 | + return await response.json(); | |
| 234 | + }; | |
| 175 | 235 | |
| 176 | 236 | /** @type {RecentChat[]} */ |
| 177 | 237 | const data = [...await responsecharData(), .json..await groupData()]; |
| 178 | 238 | |
| 179 | 239 | data.sort((a, b) => bsortMoments(timestampToMoment(a.last_mes -), atimestampToMoment(b.last_mes))) |
| 180 | 240 | .map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar), group: groups.find(x => x.id === chat.group) })) |
| 181 | 241 | .filter(t => t.character || t.group) |
| 182 | 242 | .forEach(({ chat, character, group }, index) => { |
| 183 | 243 | const DEFAULT_DISPLAYED = 5; |
| 184 | 244 | const chatTimestamp = timestampToMoment(chat.last_mes); |
| 185 | 245 | chat.char_name = character?.name || group?.name || ''; |
| 186 | 246 | chat.date_short = chatTimestamp.format('l'); |
| 187 | 247 | chat.date_long = chatTimestamp.format('LL LT'); |
| 188 | 248 | chat.chat_name = chat.file_name.replace('.jsonl', ''); |
| 189 | 249 | chat.char_thumbnail = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar; |
| 250 | + chat.is_group = !!group; | |
| 190 | 251 | chat.hidden = index >= DEFAULT_DISPLAYED; |
| 191 | 252 | }); |
| 192 | 253 | |
| @@ -949,7 +949,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) { | ||
| 949 | 949 | * @param {object} additionalData |
| 950 | 950 | * @returns {Promise<ChatInfo>} |
| 951 | 951 | */ |
| 952 | 952 | export async function getChatInfo(pathToFile, additionalData = {}, isGroup = false) { |
| 953 | 953 | return new Promise(async (res) => { |
| 954 | 954 | const fileStream = fs.createReadStream(pathToFile); |
| 955 | 955 | const stats = fs.statSync(pathToFile); |
| @@ -982,9 +982,9 @@ async function getChatInfo(pathToFile, additionalData = {}) { | ||
| 982 | 982 | |
| 983 | 983 | chatData['file_name'] = path.parse(pathToFile).base; |
| 984 | 984 | chatData['file_size'] = fileSizeInKB; |
| 985 | 985 | chatData['chat_items'] = isGroup ? itemCounter : (itemCounter - 1); |
| 986 | 986 | chatData['mes'] = jsonData['mes'] || '[The chat is empty]'; |
| 987 | 987 | chatData['last_mes'] = jsonData['send_date'] || Datestats.now()mtimeMs; |
| 988 | 988 | Object.assign(chatData, additionalData); |
| 989 | 989 | |
| 990 | 990 | res(chatData); |
| @@ -6,6 +6,7 @@ import sanitize from 'sanitize-filename'; | ||
| 6 | 6 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 7 | 7 | |
| 8 | 8 | import { humanizedISO8601DateTime } from '../util.js'; |
| 9 | +import { getChatInfo } from './characters.js'; | |
| 9 | 10 | |
| 10 | 11 | export const router = express.Router(); |
| 11 | 12 | |
| @@ -131,3 +132,41 @@ router.post('/delete', async (request, response) => { | ||
| 131 | 132 | |
| 132 | 133 | return response.send({ ok: true }); |
| 133 | 134 | }); |
| 135 | + | |
| 136 | +router.post('/recent', async (request, response) => { | |
| 137 | + try { | |
| 138 | + /** @type {{groupId: string, filePath: string, mtime: number}[]} */ | |
| 139 | + const allChatFiles = []; | |
| 140 | + | |
| 141 | + const groups = fs.readdirSync(request.user.directories.groups).filter(x => path.extname(x) === '.json'); | |
| 142 | + for (const group of groups) { | |
| 143 | + const groupPath = path.join(request.user.directories.groups, group); | |
| 144 | + const groupContents = fs.readFileSync(groupPath, 'utf8'); | |
| 145 | + const groupData = JSON.parse(groupContents); | |
| 146 | + | |
| 147 | + if (Array.isArray(groupData.chats)) { | |
| 148 | + for (const chat of groupData.chats) { | |
| 149 | + const filePath = path.join(request.user.directories.groupChats, `${chat}.jsonl`); | |
| 150 | + if (!fs.existsSync(filePath)) { | |
| 151 | + continue; | |
| 152 | + } | |
| 153 | + const stats = fs.statSync(filePath); | |
| 154 | + allChatFiles.push({ groupId: groupData.id, filePath, mtime: stats.mtimeMs }); | |
| 155 | + } | |
| 156 | + } | |
| 157 | + } | |
| 158 | + | |
| 159 | + const recentChats = allChatFiles.sort((a, b) => b.mtime - a.mtime).slice(0, 15); | |
| 160 | + const jsonFilesPromise = recentChats.map((file) => { | |
| 161 | + return getChatInfo(file.filePath, { group: file.groupId }, true); | |
| 162 | + }); | |
| 163 | + | |
| 164 | + const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value); | |
| 165 | + const validFiles = chatData.filter(i => i.file_name); | |
| 166 | + | |
| 167 | + return response.send(validFiles); | |
| 168 | + } catch (error) { | |
| 169 | + console.error('Error while getting recent group chats', error); | |
| 170 | + return response.sendStatus(500); | |
| 171 | + } | |
| 172 | +}); | |