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 @@
3737 {{/if}}
3838 {{#each chats}}
3939 {{#with this}}
4040 <div class="recentChat {{#if hidden}}hidden{{/if}} {{#if is_group}}group{{/if}}" data-file="{{chat_name}}" data-avatar="{{avatar}}" data-group="{{group}}">
4141 <div class="avatar" title="{{char_name}}">
4242 <img src="{{char_thumbnail}}" alt="{{char_name}}">
4343 </div>
public/scripts/welcome-screen.js+82 -21
@@ -21,11 +21,11 @@ import {
2121 system_message_types,
2222 this_chid,
2323} from '../script.js';
2424import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js';
2525import { t } from './i18n.js';
2626import { renderTemplateAsync } from './templates.js';
2727import { accountStorage } from './util/AccountStorage.js';
2828import { sortMoments, timestampToMoment } from './utils.js';
2929
3030const assistantAvatarKey = 'assistant';
3131const defaultAssistantAvatar = 'default_Assistant.png';
@@ -94,9 +94,13 @@ async function sendWelcomePanel() {
9494 fragment.querySelectorAll('.recentChat').forEach((item) => {
9595 item.addEventListener('click', () => {
9696 const avatarId = item.getAttribute('data-avatar');
97+ const groupId = item.getAttribute('data-group');
9798 const fileName = item.getAttribute('data-file');
9899 if (avatarId && fileName) {
99100 void openRecentChatopenRecentCharacterChat(avatarId, fileName);
101+ }
102+ if (groupId && fileName) {
103+ void openRecentGroupChat(groupId, fileName);
100104 }
101105 });
102106 });
@@ -114,6 +118,18 @@ async function sendWelcomePanel() {
114118 void newAssistantChat({ temporary: true });
115119 });
116120 });
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+ });
117133 chatElement.append(fragment.firstChild);
118134 } catch (error) {
119135 console.error('Welcome screen error:', error);
@@ -121,11 +137,11 @@ async function sendWelcomePanel() {
121137}
122138
123139/**
124140 * Opens a recent character chat.
125141 * @param {string} avatarId Avatar file name
126142 * @param {string} fileName Chat file name
127143 */
128144async function openRecentChatopenRecentCharacterChat(avatarId, fileName) {
129145 const characterId = characters.findIndex(x => x.avatar === avatarId);
130146 if (characterId === -1) {
131147 console.error(`Character not found for avatar ID: ${avatarId}`);
@@ -147,6 +163,32 @@ async function openRecentChat(avatarId, fileName) {
147163}
148164
149165/**
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+/**
150192 * Gets the list of recent chats from the server.
151193 * @returns {Promise<RecentChat[]>} List of recent chats
152194 *
@@ -156,37 +198,56 @@ async function openRecentChat(avatarId, fileName) {
156198 * @property {string} file_size Size of the chat file
157199 * @property {number} chat_items Number of items in the chat
158200 * @property {string} mes Last message content
159201 * @property {numberstring} last_mes Timestamp of the last message
160202 * @property {string} avatar Avatar URL
161203 * @property {string} char_thumbnail Thumbnail URL
162204 * @property {string} char_name Character or group name
163205 * @property {string} date_short Date in short format
164206 * @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
165209 * @property {boolean} hidden Chat will be hidden by default
166210 */
167211async function getRecentChats() {
168212 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+ };
175235
176236 /** @type {RecentChat[]} */
177237 const data = [...await responsecharData(), .json..await groupData()];
178238
179239 data.sort((a, b) => bsortMoments(timestampToMoment(a.last_mes -), atimestampToMoment(b.last_mes)))
180240 .map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar), group: groups.find(x => x.id === chat.group) }))
181241 .filter(t => t.character || t.group)
182242 .forEach(({ chat, character, group }, index) => {
183243 const DEFAULT_DISPLAYED = 5;
184244 const chatTimestamp = timestampToMoment(chat.last_mes);
185245 chat.char_name = character?.name || group?.name || '';
186246 chat.date_short = chatTimestamp.format('l');
187247 chat.date_long = chatTimestamp.format('LL LT');
188248 chat.chat_name = chat.file_name.replace('.jsonl', '');
189249 chat.char_thumbnail = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar;
250+ chat.is_group = !!group;
190251 chat.hidden = index >= DEFAULT_DISPLAYED;
191252 });
192253
src/endpoints/characters.js+3 -3
@@ -949,7 +949,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
949949 * @param {object} additionalData
950950 * @returns {Promise<ChatInfo>}
951951 */
952952export async function getChatInfo(pathToFile, additionalData = {}, isGroup = false) {
953953 return new Promise(async (res) => {
954954 const fileStream = fs.createReadStream(pathToFile);
955955 const stats = fs.statSync(pathToFile);
@@ -982,9 +982,9 @@ async function getChatInfo(pathToFile, additionalData = {}) {
982982
983983 chatData['file_name'] = path.parse(pathToFile).base;
984984 chatData['file_size'] = fileSizeInKB;
985985 chatData['chat_items'] = isGroup ? itemCounter : (itemCounter - 1);
986986 chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
987987 chatData['last_mes'] = jsonData['send_date'] || Datestats.now()mtimeMs;
988988 Object.assign(chatData, additionalData);
989989
990990 res(chatData);
src/endpoints/groups.js+39 -0
@@ -6,6 +6,7 @@ import sanitize from 'sanitize-filename';
66import { sync as writeFileAtomicSync } from 'write-file-atomic';
77
88import { humanizedISO8601DateTime } from '../util.js';
9+import { getChatInfo } from './characters.js';
910
1011export const router = express.Router();
1112
@@ -131,3 +132,41 @@ router.post('/delete', async (request, response) => {
131132
132133 return response.send({ ok: true });
133134});
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+});