mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Show recent group chats
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
{{#each chats}}
|
{{#each chats}}
|
||||||
{{#with this}}
|
{{#with this}}
|
||||||
<div class="recentChat {{#if hidden}}hidden{{/if}}" data-file="{{chat_name}}" data-avatar="{{avatar}}">
|
<div class="recentChat {{#if hidden}}hidden{{/if}} {{#if is_group}}group{{/if}}" data-file="{{chat_name}}" data-avatar="{{avatar}}" data-group="{{group}}">
|
||||||
<div class="avatar" title="{{char_name}}">
|
<div class="avatar" title="{{char_name}}">
|
||||||
<img src="{{char_thumbnail}}" alt="{{char_name}}">
|
<img src="{{char_thumbnail}}" alt="{{char_name}}">
|
||||||
</div>
|
</div>
|
||||||
|
@@ -21,11 +21,11 @@ import {
|
|||||||
system_message_types,
|
system_message_types,
|
||||||
this_chid,
|
this_chid,
|
||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
import { is_group_generating } from './group-chats.js';
|
import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
import { renderTemplateAsync } from './templates.js';
|
import { renderTemplateAsync } from './templates.js';
|
||||||
import { accountStorage } from './util/AccountStorage.js';
|
import { accountStorage } from './util/AccountStorage.js';
|
||||||
import { timestampToMoment } from './utils.js';
|
import { sortMoments, timestampToMoment } from './utils.js';
|
||||||
|
|
||||||
const assistantAvatarKey = 'assistant';
|
const assistantAvatarKey = 'assistant';
|
||||||
const defaultAssistantAvatar = 'default_Assistant.png';
|
const defaultAssistantAvatar = 'default_Assistant.png';
|
||||||
@@ -94,9 +94,13 @@ async function sendWelcomePanel() {
|
|||||||
fragment.querySelectorAll('.recentChat').forEach((item) => {
|
fragment.querySelectorAll('.recentChat').forEach((item) => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
const avatarId = item.getAttribute('data-avatar');
|
const avatarId = item.getAttribute('data-avatar');
|
||||||
|
const groupId = item.getAttribute('data-group');
|
||||||
const fileName = item.getAttribute('data-file');
|
const fileName = item.getAttribute('data-file');
|
||||||
if (avatarId && fileName) {
|
if (avatarId && fileName) {
|
||||||
void openRecentChat(avatarId, fileName);
|
void openRecentCharacterChat(avatarId, fileName);
|
||||||
|
}
|
||||||
|
if (groupId && fileName) {
|
||||||
|
void openRecentGroupChat(groupId, fileName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -114,6 +118,18 @@ async function sendWelcomePanel() {
|
|||||||
void newAssistantChat({ temporary: true });
|
void newAssistantChat({ temporary: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
fragment.querySelectorAll('.recentChat.group').forEach((groupChat) => {
|
||||||
|
const groupId = groupChat.getAttribute('data-group');
|
||||||
|
const group = groups.find(x => x.id === groupId);
|
||||||
|
if (group) {
|
||||||
|
const avatar = groupChat.querySelector('.avatar');
|
||||||
|
if (!avatar) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupAvatar = getGroupAvatar(group);
|
||||||
|
$(avatar).replaceWith(groupAvatar);
|
||||||
|
}
|
||||||
|
});
|
||||||
chatElement.append(fragment.firstChild);
|
chatElement.append(fragment.firstChild);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Welcome screen error:', error);
|
console.error('Welcome screen error:', error);
|
||||||
@@ -121,11 +137,11 @@ async function sendWelcomePanel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a recent chat.
|
* Opens a recent character chat.
|
||||||
* @param {string} avatarId Avatar file name
|
* @param {string} avatarId Avatar file name
|
||||||
* @param {string} fileName Chat file name
|
* @param {string} fileName Chat file name
|
||||||
*/
|
*/
|
||||||
async function openRecentChat(avatarId, fileName) {
|
async function openRecentCharacterChat(avatarId, fileName) {
|
||||||
const characterId = characters.findIndex(x => x.avatar === avatarId);
|
const characterId = characters.findIndex(x => x.avatar === avatarId);
|
||||||
if (characterId === -1) {
|
if (characterId === -1) {
|
||||||
console.error(`Character not found for avatar ID: ${avatarId}`);
|
console.error(`Character not found for avatar ID: ${avatarId}`);
|
||||||
@@ -146,6 +162,32 @@ async function openRecentChat(avatarId, fileName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a recent group chat.
|
||||||
|
* @param {string} groupId Group ID
|
||||||
|
* @param {string} fileName Chat file name
|
||||||
|
*/
|
||||||
|
async function openRecentGroupChat(groupId, fileName) {
|
||||||
|
const group = groups.find(x => x.id === groupId);
|
||||||
|
if (!group) {
|
||||||
|
console.error(`Group not found for ID: ${groupId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await openGroupById(groupId);
|
||||||
|
const currentChatId = getCurrentChatId();
|
||||||
|
if (currentChatId === fileName) {
|
||||||
|
console.debug(`Chat ${fileName} is already open.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await openGroupChat(groupId, fileName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error opening recent group chat:', error);
|
||||||
|
toastr.error(t`Failed to open recent group chat. See console for details.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the list of recent chats from the server.
|
* Gets the list of recent chats from the server.
|
||||||
* @returns {Promise<RecentChat[]>} List of recent chats
|
* @returns {Promise<RecentChat[]>} List of recent chats
|
||||||
@@ -156,37 +198,56 @@ async function openRecentChat(avatarId, fileName) {
|
|||||||
* @property {string} file_size Size of the chat file
|
* @property {string} file_size Size of the chat file
|
||||||
* @property {number} chat_items Number of items in the chat
|
* @property {number} chat_items Number of items in the chat
|
||||||
* @property {string} mes Last message content
|
* @property {string} mes Last message content
|
||||||
* @property {number} last_mes Timestamp of the last message
|
* @property {string} last_mes Timestamp of the last message
|
||||||
* @property {string} avatar Avatar URL
|
* @property {string} avatar Avatar URL
|
||||||
* @property {string} char_thumbnail Thumbnail URL
|
* @property {string} char_thumbnail Thumbnail URL
|
||||||
* @property {string} char_name Character name
|
* @property {string} char_name Character or group name
|
||||||
* @property {string} date_short Date in short format
|
* @property {string} date_short Date in short format
|
||||||
* @property {string} date_long Date in long format
|
* @property {string} date_long Date in long format
|
||||||
|
* @property {string} group Group ID (if applicable)
|
||||||
|
* @property {boolean} is_group Indicates if the chat is a group chat
|
||||||
* @property {boolean} hidden Chat will be hidden by default
|
* @property {boolean} hidden Chat will be hidden by default
|
||||||
*/
|
*/
|
||||||
async function getRecentChats() {
|
async function getRecentChats() {
|
||||||
const response = await fetch('/api/characters/recent', {
|
const charData = async () => {
|
||||||
method: 'POST',
|
const response = await fetch('/api/characters/recent', {
|
||||||
headers: getRequestHeaders(),
|
method: 'POST',
|
||||||
});
|
headers: getRequestHeaders(),
|
||||||
if (!response.ok) {
|
});
|
||||||
throw new Error('Failed to fetch recent chats');
|
if (!response.ok) {
|
||||||
}
|
console.warn('Failed to fetch recent character chats');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupData = async () => {
|
||||||
|
const response = await fetch('/api/groups/recent', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn('Failed to fetch recent group chats');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
/** @type {RecentChat[]} */
|
/** @type {RecentChat[]} */
|
||||||
const data = await response.json();
|
const data = [...await charData(), ...await groupData()];
|
||||||
|
|
||||||
data.sort((a, b) => b.last_mes - a.last_mes)
|
data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)))
|
||||||
.map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar) }))
|
.map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar), group: groups.find(x => x.id === chat.group) }))
|
||||||
.filter(t => t.character)
|
.filter(t => t.character || t.group)
|
||||||
.forEach(({ chat, character }, index) => {
|
.forEach(({ chat, character, group }, index) => {
|
||||||
const DEFAULT_DISPLAYED = 5;
|
const DEFAULT_DISPLAYED = 5;
|
||||||
const chatTimestamp = timestampToMoment(chat.last_mes);
|
const chatTimestamp = timestampToMoment(chat.last_mes);
|
||||||
chat.char_name = character.name;
|
chat.char_name = character?.name || group?.name || '';
|
||||||
chat.date_short = chatTimestamp.format('l');
|
chat.date_short = chatTimestamp.format('l');
|
||||||
chat.date_long = chatTimestamp.format('LL LT');
|
chat.date_long = chatTimestamp.format('LL LT');
|
||||||
chat.chat_name = chat.file_name.replace('.jsonl', '');
|
chat.chat_name = chat.file_name.replace('.jsonl', '');
|
||||||
chat.char_thumbnail = getThumbnailUrl('avatar', character.avatar);
|
chat.char_thumbnail = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar;
|
||||||
|
chat.is_group = !!group;
|
||||||
chat.hidden = index >= DEFAULT_DISPLAYED;
|
chat.hidden = index >= DEFAULT_DISPLAYED;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -949,7 +949,7 @@ async function importFromPng(uploadPath, { request }, preservedFileName) {
|
|||||||
* @param {object} additionalData
|
* @param {object} additionalData
|
||||||
* @returns {Promise<ChatInfo>}
|
* @returns {Promise<ChatInfo>}
|
||||||
*/
|
*/
|
||||||
async function getChatInfo(pathToFile, additionalData = {}) {
|
export async function getChatInfo(pathToFile, additionalData = {}, isGroup = false) {
|
||||||
return new Promise(async (res) => {
|
return new Promise(async (res) => {
|
||||||
const fileStream = fs.createReadStream(pathToFile);
|
const fileStream = fs.createReadStream(pathToFile);
|
||||||
const stats = fs.statSync(pathToFile);
|
const stats = fs.statSync(pathToFile);
|
||||||
@@ -982,9 +982,9 @@ async function getChatInfo(pathToFile, additionalData = {}) {
|
|||||||
|
|
||||||
chatData['file_name'] = path.parse(pathToFile).base;
|
chatData['file_name'] = path.parse(pathToFile).base;
|
||||||
chatData['file_size'] = fileSizeInKB;
|
chatData['file_size'] = fileSizeInKB;
|
||||||
chatData['chat_items'] = itemCounter - 1;
|
chatData['chat_items'] = isGroup ? itemCounter : (itemCounter - 1);
|
||||||
chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
|
chatData['mes'] = jsonData['mes'] || '[The chat is empty]';
|
||||||
chatData['last_mes'] = jsonData['send_date'] || Date.now();
|
chatData['last_mes'] = jsonData['send_date'] || stats.mtimeMs;
|
||||||
Object.assign(chatData, additionalData);
|
Object.assign(chatData, additionalData);
|
||||||
|
|
||||||
res(chatData);
|
res(chatData);
|
||||||
|
@@ -6,6 +6,7 @@ import sanitize from 'sanitize-filename';
|
|||||||
import { sync as writeFileAtomicSync } from 'write-file-atomic';
|
import { sync as writeFileAtomicSync } from 'write-file-atomic';
|
||||||
|
|
||||||
import { humanizedISO8601DateTime } from '../util.js';
|
import { humanizedISO8601DateTime } from '../util.js';
|
||||||
|
import { getChatInfo } from './characters.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
@@ -131,3 +132,41 @@ router.post('/delete', async (request, response) => {
|
|||||||
|
|
||||||
return response.send({ ok: true });
|
return response.send({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/recent', async (request, response) => {
|
||||||
|
try {
|
||||||
|
/** @type {{groupId: string, filePath: string, mtime: number}[]} */
|
||||||
|
const allChatFiles = [];
|
||||||
|
|
||||||
|
const groups = fs.readdirSync(request.user.directories.groups).filter(x => path.extname(x) === '.json');
|
||||||
|
for (const group of groups) {
|
||||||
|
const groupPath = path.join(request.user.directories.groups, group);
|
||||||
|
const groupContents = fs.readFileSync(groupPath, 'utf8');
|
||||||
|
const groupData = JSON.parse(groupContents);
|
||||||
|
|
||||||
|
if (Array.isArray(groupData.chats)) {
|
||||||
|
for (const chat of groupData.chats) {
|
||||||
|
const filePath = path.join(request.user.directories.groupChats, `${chat}.jsonl`);
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const stats = fs.statSync(filePath);
|
||||||
|
allChatFiles.push({ groupId: groupData.id, filePath, mtime: stats.mtimeMs });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentChats = allChatFiles.sort((a, b) => b.mtime - a.mtime).slice(0, 15);
|
||||||
|
const jsonFilesPromise = recentChats.map((file) => {
|
||||||
|
return getChatInfo(file.filePath, { group: file.groupId }, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const chatData = (await Promise.allSettled(jsonFilesPromise)).filter(x => x.status === 'fulfilled').map(x => x.value);
|
||||||
|
const validFiles = chatData.filter(i => i.file_name);
|
||||||
|
|
||||||
|
return response.send(validFiles);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error while getting recent group chats', error);
|
||||||
|
return response.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user