Show recent group chats

e2c44161ede573ded4048a56b04b92f070c2a754

Cohee <18619528+Cohee1207@users.noreply.github.com>

4 files changed, +125 -25Ignore whitespace
public/scripts/templates/welcomePanel.html+1 -1
@@ -37,7 +37,7 @@
37 {{/if}}37 {{/if}}
38 {{#each chats}}38 {{#each chats}}
39 {{#with this}}39 {{#with this}}
40 <div class="recentChat {{#if hidden}}hidden{{/if}}" data-file="{{chat_name}}" data-avatar="{{avatar}}">40 <div class="recentChat {{#if hidden}}hidden{{/if}} {{#if is_group}}group{{/if}}" data-file="{{chat_name}}" data-avatar="{{avatar}}" data-group="{{group}}">
41 <div class="avatar" title="{{char_name}}">41 <div class="avatar" title="{{char_name}}">
42 <img src="{{char_thumbnail}}" alt="{{char_name}}">42 <img src="{{char_thumbnail}}" alt="{{char_name}}">
43 </div>43 </div>
public/scripts/welcome-screen.js+82 -21
@@ -21,11 +21,11 @@ import {
21 system_message_types,21 system_message_types,
22 this_chid,22 this_chid,
23} from '../script.js';23} from '../script.js';
24import { is_group_generating } from './group-chats.js';24import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js';
25import { t } from './i18n.js';25import { t } from './i18n.js';
26import { renderTemplateAsync } from './templates.js';26import { renderTemplateAsync } from './templates.js';
27import { accountStorage } from './util/AccountStorage.js';27import { accountStorage } from './util/AccountStorage.js';
28import { timestampToMoment } from './utils.js';28import { sortMoments, timestampToMoment } from './utils.js';
2929
30const assistantAvatarKey = 'assistant';30const assistantAvatarKey = 'assistant';
31const defaultAssistantAvatar = 'default_Assistant.png';31const defaultAssistantAvatar = 'default_Assistant.png';
@@ -94,9 +94,13 @@ async function sendWelcomePanel() {
94 fragment.querySelectorAll('.recentChat').forEach((item) => {94 fragment.querySelectorAll('.recentChat').forEach((item) => {
95 item.addEventListener('click', () => {95 item.addEventListener('click', () => {
96 const avatarId = item.getAttribute('data-avatar');96 const avatarId = item.getAttribute('data-avatar');
97 const groupId = item.getAttribute('data-group');
97 const fileName = item.getAttribute('data-file');98 const fileName = item.getAttribute('data-file');
98 if (avatarId && fileName) {99 if (avatarId && fileName) {
99 void openRecentChat(avatarId, fileName);100 void openRecentCharacterChat(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 void newAssistantChat({ temporary: true });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 chatElement.append(fragment.firstChild);133 chatElement.append(fragment.firstChild);
118 } catch (error) {134 } catch (error) {
119 console.error('Welcome screen error:', error);135 console.error('Welcome screen error:', error);
@@ -121,11 +137,11 @@ async function sendWelcomePanel() {
121}137}
122138
123/**139/**
124 * Opens a recent chat.140 * Opens a recent character chat.
125 * @param {string} avatarId Avatar file name141 * @param {string} avatarId Avatar file name
126 * @param {string} fileName Chat file name142 * @param {string} fileName Chat file name
127 */143 */
128async function openRecentChat(avatarId, fileName) {144async function openRecentCharacterChat(avatarId, fileName) {
129 const characterId = characters.findIndex(x => x.avatar === avatarId);145 const characterId = characters.findIndex(x => x.avatar === avatarId);
130 if (characterId === -1) {146 if (characterId === -1) {
131 console.error(`Character not found for avatar ID: ${avatarId}`);147 console.error(`Character not found for avatar ID: ${avatarId}`);
@@ -147,6 +163,32 @@ async function openRecentChat(avatarId, fileName) {
147}163}
148164
149/**165/**
166 * Opens a recent group chat.
167 * @param {string} groupId Group ID
168 * @param {string} fileName Chat file name
169 */
170async 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 * Gets the list of recent chats from the server.192 * Gets the list of recent chats from the server.
151 * @returns {Promise<RecentChat[]>} List of recent chats193 * @returns {Promise<RecentChat[]>} List of recent chats
152 *194 *
@@ -156,37 +198,56 @@ async function openRecentChat(avatarId, fileName) {
156 * @property {string} file_size Size of the chat file198 * @property {string} file_size Size of the chat file
157 * @property {number} chat_items Number of items in the chat199 * @property {number} chat_items Number of items in the chat
158 * @property {string} mes Last message content200 * @property {string} mes Last message content
159 * @property {number} last_mes Timestamp of the last message201 * @property {string} last_mes Timestamp of the last message
160 * @property {string} avatar Avatar URL202 * @property {string} avatar Avatar URL
161 * @property {string} char_thumbnail Thumbnail URL203 * @property {string} char_thumbnail Thumbnail URL
162 * @property {string} char_name Character name204 * @property {string} char_name Character or group name
163 * @property {string} date_short Date in short format205 * @property {string} date_short Date in short format
164 * @property {string} date_long Date in long format206 * @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 * @property {boolean} hidden Chat will be hidden by default209 * @property {boolean} hidden Chat will be hidden by default
166 */210 */
167async function getRecentChats() {211async function getRecentChats() {
168 const response = await fetch('/api/characters/recent', {212 const charData = async () => {
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 };
175235
176 /** @type {RecentChat[]} */236 /** @type {RecentChat[]} */
177 const data = await response.json();237 const data = [...await charData(), ...await groupData()];
178238
179 data.sort((a, b) => b.last_mes - a.last_mes)239 data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)))
180 .map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar) }))240 .map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar), group: groups.find(x => x.id === chat.group) }))
181 .filter(t => t.character)241 .filter(t => t.character || t.group)
182 .forEach(({ chat, character }, index) => {242 .forEach(({ chat, character, group }, index) => {
183 const DEFAULT_DISPLAYED = 5;243 const DEFAULT_DISPLAYED = 5;
184 const chatTimestamp = timestampToMoment(chat.last_mes);244 const chatTimestamp = timestampToMoment(chat.last_mes);
185 chat.char_name = character.name;245 chat.char_name = character?.name || group?.name || '';
186 chat.date_short = chatTimestamp.format('l');246 chat.date_short = chatTimestamp.format('l');
187 chat.date_long = chatTimestamp.format('LL LT');247 chat.date_long = chatTimestamp.format('LL LT');
188 chat.chat_name = chat.file_name.replace('.jsonl', '');248 chat.chat_name = chat.file_name.replace('.jsonl', '');
189 chat.char_thumbnail = getThumbnailUrl('avatar', character.avatar);249 chat.char_thumbnail = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar;
250 chat.is_group = !!group;
190 chat.hidden = index >= DEFAULT_DISPLAYED;251 chat.hidden = index >= DEFAULT_DISPLAYED;
191 });252 });
192253
src/endpoints/characters.js+3 -3
@@ -949,7 +949,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
949 * @param {object} additionalData949 * @param {object} additionalData
950 * @returns {Promise<ChatInfo>}950 * @returns {Promise<ChatInfo>}
951 */951 */
952async function getChatInfo(pathToFile, additionalData = {}) {952export async function getChatInfo(pathToFile, additionalData = {}, isGroup = false) {
953 return new Promise(async (res) => {953 return new Promise(async (res) => {
954 const fileStream = fs.createReadStream(pathToFile);954 const fileStream = fs.createReadStream(pathToFile);
955 const stats = fs.statSync(pathToFile);955 const stats = fs.statSync(pathToFile);
@@ -982,9 +982,9 @@ async function getChatInfo(pathToFile, additionalData = {}) {
982982
983 chatData['file_name'] = path.parse(pathToFile).base;983 chatData['file_name'] = path.parse(pathToFile).base;
984 chatData['file_size'] = fileSizeInKB;984 chatData['file_size'] = fileSizeInKB;
985 chatData['chat_items'] = itemCounter - 1;985 chatData['chat_items'] = isGroup ? itemCounter : (itemCounter - 1);
986 chatData['mes'] = jsonData['mes'] || '[The chat is empty]';986 chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
987 chatData['last_mes'] = jsonData['send_date'] || Date.now();987 chatData['last_mes'] = jsonData['send_date'] || stats.mtimeMs;
988 Object.assign(chatData, additionalData);988 Object.assign(chatData, additionalData);
989989
990 res(chatData);990 res(chatData);
src/endpoints/groups.js+39 -0
@@ -6,6 +6,7 @@ import sanitize from 'sanitize-filename';
6import { sync as writeFileAtomicSync } from 'write-file-atomic';6import { sync as writeFileAtomicSync } from 'write-file-atomic';
77
8import { humanizedISO8601DateTime } from '../util.js';8import { humanizedISO8601DateTime } from '../util.js';
9import { getChatInfo } from './characters.js';
910
10export const router = express.Router();11export const router = express.Router();
1112
@@ -131,3 +132,41 @@ router.post('/delete', async (request, response) => {
131132
132 return response.send({ ok: true });133 return response.send({ ok: true });
133});134});
135
136router.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});