mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Recent chats: add delete and rename buttons (#4051)
* [wip] Add rename/delete for recent chats * Implement deleteCharacterChatByName * Fix character name usage in deleteCharacterChatByName function
This commit is contained in:
@@ -142,12 +142,28 @@ body.hideChatAvatars .welcomePanel .recentChatList .recentChat .avatar {
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font-size: calc(var(--mainFontSize) * 1);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.welcomeRecent .recentChatList .recentChat .chatNameContainer .chatName {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.welcomeRecent .recentChatList .recentChat .chatActions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.welcomeRecent .recentChatList .recentChat .chatActions button {
|
||||
margin: 0;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.welcomeRecent .recentChatList .recentChat .chatMessageContainer {
|
||||
|
132
public/script.js
132
public/script.js
@@ -1880,6 +1880,52 @@ async function delChat(chatfile) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a character chat by its name.
|
||||
* @param {string} characterId Character ID to delete chat for
|
||||
* @param {string} fileName Name of the chat file to delete (without .jsonl extension)
|
||||
* @returns {Promise<void>} A promise that resolves when the chat is deleted.
|
||||
*/
|
||||
export async function deleteCharacterChatByName(characterId, fileName) {
|
||||
// Make sure all the data is loaded.
|
||||
await unshallowCharacter(characterId);
|
||||
|
||||
/** @type {import('./scripts/char-data.js').v1CharData} */
|
||||
const character = characters[characterId];
|
||||
if (!character) {
|
||||
console.warn(`Character with ID ${characterId} not found.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/chats/delete', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
chatfile: `${fileName}.jsonl`,
|
||||
avatar_url: character.avatar,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to delete chat for character.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileName === character.chat) {
|
||||
const chatsResponse = await fetch('/api/characters/chats', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ avatar_url: character.avatar }),
|
||||
});
|
||||
const chats = Object.values(await chatsResponse.json());
|
||||
chats.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes)));
|
||||
const newChatName = chats.length && typeof chats[0] === 'object' ? chats[0].file_name.replace('.jsonl', '') : `${character.name} - ${humanizedDateTime()}`;
|
||||
await updateRemoteChatName(characterId, newChatName);
|
||||
}
|
||||
|
||||
await eventSource.emit(event_types.CHAT_DELETED, fileName);
|
||||
}
|
||||
|
||||
export async function replaceCurrentChat() {
|
||||
await clearChat();
|
||||
chat.length = 0;
|
||||
@@ -9985,16 +10031,21 @@ async function doRenameChat(_, chatName) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the currently selected chat.
|
||||
* @param {string} oldFileName Old name of the chat (no JSONL extension)
|
||||
* @param {string} newName New name for the chat (no JSONL extension)
|
||||
* Renames a group or character chat.
|
||||
* @param {object} param Parameters for renaming chat
|
||||
* @param {string} [param.characterId] Character ID to rename chat for
|
||||
* @param {string} [param.groupId] Group ID to rename chat for
|
||||
* @param {string} param.oldFileName Old name of the chat (no JSONL extension)
|
||||
* @param {string} param.newFileName New name for the chat (no JSONL extension)
|
||||
* @param {boolean} [param.loader=true] Whether to show loader during the operation
|
||||
*/
|
||||
export async function renameChat(oldFileName, newName) {
|
||||
export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader }) {
|
||||
const currentChatId = getCurrentChatId();
|
||||
const body = {
|
||||
is_group: !!selected_group,
|
||||
avatar_url: characters[this_chid]?.avatar,
|
||||
is_group: !!groupId,
|
||||
avatar_url: characters[characterId]?.avatar,
|
||||
original_file: `${oldFileName}.jsonl`,
|
||||
renamed_file: `${newName.trim()}.jsonl`,
|
||||
renamed_file: `${newFileName.trim()}.jsonl`,
|
||||
};
|
||||
|
||||
if (body.original_file === body.renamed_file) {
|
||||
@@ -10007,7 +10058,8 @@ export async function renameChat(oldFileName, newName) {
|
||||
}
|
||||
|
||||
try {
|
||||
showLoader();
|
||||
loader && showLoader();
|
||||
|
||||
const response = await fetch('/api/chats/rename', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
@@ -10025,27 +10077,69 @@ export async function renameChat(oldFileName, newName) {
|
||||
}
|
||||
|
||||
if (data.sanitizedFileName) {
|
||||
newName = data.sanitizedFileName;
|
||||
newFileName = data.sanitizedFileName;
|
||||
}
|
||||
|
||||
if (selected_group) {
|
||||
await renameGroupChat(selected_group, oldFileName, newName);
|
||||
if (groupId) {
|
||||
await renameGroupChat(groupId, oldFileName, newFileName);
|
||||
}
|
||||
else {
|
||||
if (characters[this_chid].chat == oldFileName) {
|
||||
characters[this_chid].chat = newName;
|
||||
$('#selected_chat_pole').val(characters[this_chid].chat);
|
||||
else if (characterId !== undefined && String(characterId) === String(this_chid) && characters[characterId]?.chat === oldFileName) {
|
||||
characters[characterId].chat = newFileName;
|
||||
$('#selected_chat_pole').val(characters[characterId].chat);
|
||||
await createOrEditCharacter();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChatId) {
|
||||
await reloadCurrentChat();
|
||||
}
|
||||
} catch {
|
||||
hideLoader();
|
||||
loader && hideLoader();
|
||||
await delay(500);
|
||||
await callPopup('An error has occurred. Chat was not renamed.', 'text');
|
||||
await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT);
|
||||
} finally {
|
||||
hideLoader();
|
||||
loader && hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the currently selected chat.
|
||||
* @param {string} oldFileName Old name of the chat (no JSONL extension)
|
||||
* @param {string} newName New name for the chat (no JSONL extension)
|
||||
*/
|
||||
export async function renameChat(oldFileName, newName) {
|
||||
return await renameGroupOrCharacterChat({
|
||||
characterId: this_chid,
|
||||
groupId: selected_group,
|
||||
oldFileName: oldFileName,
|
||||
newFileName: newName,
|
||||
loader: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the update of the chat name for a remote character.
|
||||
* @param {string|number} characterId Character ID to update chat name for
|
||||
* @param {string} newName New name for the chat
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function updateRemoteChatName(characterId, newName) {
|
||||
const character = characters[characterId];
|
||||
if (!character) {
|
||||
console.warn(`Character not found for ID: ${characterId}`);
|
||||
return;
|
||||
}
|
||||
character.chat = newName;
|
||||
const mergeRequest = {
|
||||
avatar: character.avatar,
|
||||
chat: newName,
|
||||
};
|
||||
const mergeResponse = await fetch('/api/characters/merge-attributes', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify(mergeRequest),
|
||||
});
|
||||
if (!mergeResponse.ok) {
|
||||
console.error('Failed to save extension field', mergeResponse.statusText);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1966,6 +1966,51 @@ export async function renameGroupChat(groupId, oldChatId, newChatId) {
|
||||
await editGroup(groupId, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group chat by its name. Doesn't affect displayed chat.
|
||||
* @param {string} groupId Group ID
|
||||
* @param {string} chatName Name of the chat to delete
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deleteGroupChatByName(groupId, chatName) {
|
||||
const group = groups.find(x => x.id === groupId);
|
||||
if (!group || !group.chats.includes(chatName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof group.past_metadata !== 'object') {
|
||||
group.past_metadata = {};
|
||||
}
|
||||
|
||||
group.chats.splice(group.chats.indexOf(chatName), 1);
|
||||
delete group.past_metadata[chatName];
|
||||
|
||||
const response = await fetch('/api/chats/group/delete', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ id: chatName }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group chat could not be deleted`);
|
||||
console.error('Group chat could not be deleted');
|
||||
return;
|
||||
}
|
||||
|
||||
// If the deleted chat was the current chat, switch to the last chat in the group
|
||||
if (group.chat_id === chatName) {
|
||||
group.chat_id = '';
|
||||
group.chat_metadata = {};
|
||||
|
||||
const newChatName = group.chats.length ? group.chats[group.chats.length - 1] : humanizedDateTime();
|
||||
group.chat_id = newChatName;
|
||||
group.chat_metadata = group.past_metadata[newChatName] || {};
|
||||
}
|
||||
|
||||
await editGroup(groupId, true, true);
|
||||
await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatName);
|
||||
}
|
||||
|
||||
export async function deleteGroupChat(groupId, chatId) {
|
||||
const group = groups.find(x => x.id === groupId);
|
||||
|
||||
|
@@ -55,6 +55,14 @@
|
||||
<span>{{chat_name}}</span>
|
||||
</div>
|
||||
<small class="chatDate" title="{{date_long}}">{{date_short}}</small>
|
||||
<div class="chatActions">
|
||||
<button class="menu_button menu_button_icon renameChat" title="Rename chat" data-i18n="[title]Rename chat">
|
||||
<i class="fa-solid fa-pen-to-square fa-fw"></i>
|
||||
</button>
|
||||
<button class="menu_button menu_button_icon deleteChat" title="Delete chat" data-i18n="[title]Delete chat">
|
||||
<i class="fa-solid fa-trash fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chatMessageContainer">
|
||||
<div class="chatMessage" title="{{mes}}">
|
||||
|
@@ -2,6 +2,7 @@ import {
|
||||
addOneMessage,
|
||||
characters,
|
||||
chat,
|
||||
deleteCharacterChatByName,
|
||||
displayVersion,
|
||||
doNewChat,
|
||||
event_types,
|
||||
@@ -16,15 +17,18 @@ import {
|
||||
newAssistantChat,
|
||||
openCharacterChat,
|
||||
printCharactersDebounced,
|
||||
renameGroupOrCharacterChat,
|
||||
selectCharacterById,
|
||||
system_avatar,
|
||||
system_message_types,
|
||||
this_chid,
|
||||
unshallowCharacter,
|
||||
updateRemoteChatName,
|
||||
} from '../script.js';
|
||||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||
import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js';
|
||||
import { deleteGroupChatByName, getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js';
|
||||
import { t } from './i18n.js';
|
||||
import { callGenericPopup, POPUP_TYPE } from './popup.js';
|
||||
import { getMessageTimeStamp } from './RossAscends-mods.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
import { accountStorage } from './util/AccountStorage.js';
|
||||
@@ -51,9 +55,16 @@ export function getPermanentAssistantAvatar() {
|
||||
return assistantAvatar;
|
||||
}
|
||||
|
||||
export async function openWelcomeScreen() {
|
||||
/**
|
||||
* Opens a welcome screen if no chat is currently active.
|
||||
* @param {object} param Additional parameters
|
||||
* @param {boolean} [param.force] If true, forces clearing of the welcome screen.
|
||||
* @param {boolean} [param.expand] If true, expands the recent chats section.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function openWelcomeScreen({ force = false, expand = false } = {}) {
|
||||
const currentChatId = getCurrentChatId();
|
||||
if (currentChatId !== undefined || chat.length > 0) {
|
||||
if (currentChatId !== undefined || (chat.length > 0 && !force)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,7 +75,13 @@ export async function openWelcomeScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
await sendWelcomePanel(recentChats);
|
||||
if (chatAfterFetch === undefined && force) {
|
||||
console.debug('Forcing welcome screen open.');
|
||||
chat.splice(0, chat.length);
|
||||
$('#chat').empty();
|
||||
}
|
||||
|
||||
await sendWelcomePanel(recentChats, expand);
|
||||
await unshallowPermanentAssistant();
|
||||
sendAssistantMessage();
|
||||
sendWelcomePrompt();
|
||||
@@ -131,8 +148,9 @@ function sendWelcomePrompt() {
|
||||
/**
|
||||
* Sends the welcome panel to the chat.
|
||||
* @param {RecentChat[]} chats List of recent chats
|
||||
* @param {boolean} [expand=false] If true, expands the recent chats section
|
||||
*/
|
||||
async function sendWelcomePanel(chats) {
|
||||
async function sendWelcomePanel(chats, expand = false) {
|
||||
try {
|
||||
const chatElement = document.getElementById('chat');
|
||||
const sendTextArea = document.getElementById('send_textarea');
|
||||
@@ -215,7 +233,50 @@ async function sendWelcomePanel(chats) {
|
||||
$(avatar).replaceWith(groupAvatar);
|
||||
}
|
||||
});
|
||||
fragment.querySelectorAll('.recentChat .renameChat').forEach((renameButton) => {
|
||||
renameButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const chatItem = renameButton.closest('.recentChat');
|
||||
if (!chatItem) {
|
||||
return;
|
||||
}
|
||||
const avatarId = chatItem.getAttribute('data-avatar');
|
||||
const groupId = chatItem.getAttribute('data-group');
|
||||
const fileName = chatItem.getAttribute('data-file');
|
||||
if (avatarId && fileName) {
|
||||
void renameRecentCharacterChat(avatarId, fileName);
|
||||
}
|
||||
if (groupId && fileName) {
|
||||
void renameRecentGroupChat(groupId, fileName);
|
||||
}
|
||||
});
|
||||
});
|
||||
fragment.querySelectorAll('.recentChat .deleteChat').forEach((deleteButton) => {
|
||||
deleteButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const chatItem = deleteButton.closest('.recentChat');
|
||||
if (!chatItem) {
|
||||
return;
|
||||
}
|
||||
const avatarId = chatItem.getAttribute('data-avatar');
|
||||
const groupId = chatItem.getAttribute('data-group');
|
||||
const fileName = chatItem.getAttribute('data-file');
|
||||
if (avatarId && fileName) {
|
||||
void deleteRecentCharacterChat(avatarId, fileName);
|
||||
}
|
||||
if (groupId && fileName) {
|
||||
void deleteRecentGroupChat(groupId, fileName);
|
||||
}
|
||||
});
|
||||
});
|
||||
chatElement.append(fragment.firstChild);
|
||||
if (expand) {
|
||||
chatElement.querySelectorAll('button.showMoreChats').forEach((button) => {
|
||||
if (button instanceof HTMLButtonElement) {
|
||||
button.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Welcome screen error:', error);
|
||||
}
|
||||
@@ -273,6 +334,144 @@ async function openRecentGroupChat(groupId, fileName) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a recent character chat.
|
||||
* @param {string} avatarId Avatar file name
|
||||
* @param {string} fileName Chat file name
|
||||
*/
|
||||
async function renameRecentCharacterChat(avatarId, fileName) {
|
||||
const characterId = characters.findIndex(x => x.avatar === avatarId);
|
||||
if (characterId === -1) {
|
||||
console.error(`Character not found for avatar ID: ${avatarId}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const popupText = await renderTemplateAsync('chatRename');
|
||||
const newName = await callGenericPopup(popupText, POPUP_TYPE.INPUT, fileName);
|
||||
if (!newName || typeof newName !== 'string' || newName === fileName) {
|
||||
console.log('No new name provided, aborting');
|
||||
return;
|
||||
}
|
||||
await renameGroupOrCharacterChat({
|
||||
characterId: String(characterId),
|
||||
oldFileName: fileName,
|
||||
newFileName: newName,
|
||||
loader: false,
|
||||
});
|
||||
await updateRemoteChatName(characterId, newName);
|
||||
await refreshWelcomeScreen();
|
||||
toastr.success(t`Chat renamed.`);
|
||||
} catch (error) {
|
||||
console.error('Error renaming recent character chat:', error);
|
||||
toastr.error(t`Failed to rename recent chat. See console for details.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a recent group chat.
|
||||
* @param {string} groupId Group ID
|
||||
* @param {string} fileName Chat file name
|
||||
*/
|
||||
async function renameRecentGroupChat(groupId, fileName) {
|
||||
const group = groups.find(x => x.id === groupId);
|
||||
if (!group) {
|
||||
console.error(`Group not found for ID: ${groupId}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const popupText = await renderTemplateAsync('chatRename');
|
||||
const newName = await callGenericPopup(popupText, POPUP_TYPE.INPUT, fileName);
|
||||
if (!newName || newName === fileName) {
|
||||
console.log('No new name provided, aborting');
|
||||
return;
|
||||
}
|
||||
await renameGroupOrCharacterChat({
|
||||
groupId: String(groupId),
|
||||
oldFileName: fileName,
|
||||
newFileName: String(newName),
|
||||
loader: false,
|
||||
});
|
||||
await refreshWelcomeScreen();
|
||||
toastr.success(t`Group chat renamed.`);
|
||||
} catch (error) {
|
||||
console.error('Error renaming recent group chat:', error);
|
||||
toastr.error(t`Failed to rename recent group chat. See console for details.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a recent character chat.
|
||||
* @param {string} avatarId Avatar file name
|
||||
* @param {string} fileName Chat file name
|
||||
*/
|
||||
async function deleteRecentCharacterChat(avatarId, fileName) {
|
||||
const characterId = characters.findIndex(x => x.avatar === avatarId);
|
||||
if (characterId === -1) {
|
||||
console.error(`Character not found for avatar ID: ${avatarId}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const confirm = await callGenericPopup(t`Delete the Chat File?`, POPUP_TYPE.CONFIRM);
|
||||
if (!confirm) {
|
||||
console.log('Deletion cancelled by user');
|
||||
return;
|
||||
}
|
||||
await deleteCharacterChatByName(String(characterId), fileName);
|
||||
await refreshWelcomeScreen();
|
||||
toastr.success(t`Chat deleted.`);
|
||||
} catch (error) {
|
||||
console.error('Error deleting recent character chat:', error);
|
||||
toastr.error(t`Failed to delete recent chat. See console for details.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a recent group chat.
|
||||
* @param {string} groupId Group ID
|
||||
* @param {string} fileName Chat file name
|
||||
*/
|
||||
async function deleteRecentGroupChat(groupId, fileName) {
|
||||
const group = groups.find(x => x.id === groupId);
|
||||
if (!group) {
|
||||
console.error(`Group not found for ID: ${groupId}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const confirm = await callGenericPopup(t`Delete the Chat File?`, POPUP_TYPE.CONFIRM);
|
||||
if (!confirm) {
|
||||
console.log('Deletion cancelled by user');
|
||||
return;
|
||||
}
|
||||
await deleteGroupChatByName(groupId, fileName);
|
||||
await refreshWelcomeScreen();
|
||||
toastr.success(t`Group chat deleted.`);
|
||||
} catch (error) {
|
||||
console.error('Error deleting recent group chat:', error);
|
||||
toastr.error(t`Failed to delete recent group chat. See console for details.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reopens the welcome screen and restores the scroll position.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function refreshWelcomeScreen() {
|
||||
const chatElement = document.getElementById('chat');
|
||||
if (!chatElement) {
|
||||
console.error('Chat element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollTop = chatElement.scrollTop;
|
||||
const scrollHeight = chatElement.scrollHeight;
|
||||
const expand = chatElement.querySelectorAll('button.showMoreChats.rotated').length > 0;
|
||||
|
||||
await openWelcomeScreen({ force: true, expand });
|
||||
|
||||
// Restore scroll position
|
||||
chatElement.scrollTop = scrollTop + (chatElement.scrollHeight - scrollHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of recent chats from the server.
|
||||
* @returns {Promise<RecentChat[]>} List of recent chats
|
||||
|
Reference in New Issue
Block a user