Recent chats: add delete and rename buttons (#4051) * [wip] Add rename/delete for recent chats * Implement deleteCharacterChatByName * Fix character name usage in deleteCharacterChatByName function
Signed| @@ -142,12 +142,28 @@ body.hideChatAvatars .welcomePanel .recentChatList .recentChat .avatar { | |||
| 142 | justify-content: space-between; | 142 | justify-content: space-between; |
| 143 | align-items: baseline; | 143 | align-items: baseline; |
| 144 | font-size: calc(var(--mainFontSize) * 1); | 144 | font-size: calc(var(--mainFontSize) * 1); |
| 145 | gap: 5px; | ||
| 145 | } | 146 | } |
| 146 | 147 | ||
| 147 | .welcomeRecent .recentChatList .recentChat .chatNameContainer .chatName { | 148 | .welcomeRecent .recentChatList .recentChat .chatNameContainer .chatName { |
| 148 | white-space: nowrap; | 149 | white-space: nowrap; |
| 149 | text-overflow: ellipsis; | 150 | text-overflow: ellipsis; |
| 150 | overflow: hidden; | 151 | overflow: hidden; |
| 152 | flex: 1; | ||
| 153 | } | ||
| 154 | |||
| 155 | .welcomeRecent .recentChatList .recentChat .chatActions { | ||
| 156 | display: flex; | ||
| 157 | flex-direction: row; | ||
| 158 | align-items: center; | ||
| 159 | justify-content: center; | ||
| 160 | gap: 5px; | ||
| 161 | } | ||
| 162 | |||
| 163 | .welcomeRecent .recentChatList .recentChat .chatActions button { | ||
| 164 | margin: 0; | ||
| 165 | font-size: 0.8em; | ||
| 166 | cursor: pointer; | ||
| 151 | } | 167 | } |
| 152 | 168 | ||
| 153 | .welcomeRecent .recentChatList .recentChat .chatMessageContainer { | 169 | .welcomeRecent .recentChatList .recentChat .chatMessageContainer { |
| @@ -1880,6 +1880,52 @@ async function delChat(chatfile) { | |||
| 1880 | } | 1880 | } |
| 1881 | } | 1881 | } |
| 1882 | 1882 | ||
| 1883 | /** | ||
| 1884 | * Deletes a character chat by its name. | ||
| 1885 | * @param {string} characterId Character ID to delete chat for | ||
| 1886 | * @param {string} fileName Name of the chat file to delete (without .jsonl extension) | ||
| 1887 | * @returns {Promise<void>} A promise that resolves when the chat is deleted. | ||
| 1888 | */ | ||
| 1889 | export async function deleteCharacterChatByName(characterId, fileName) { | ||
| 1890 | // Make sure all the data is loaded. | ||
| 1891 | await unshallowCharacter(characterId); | ||
| 1892 | |||
| 1893 | /** @type {import('./scripts/char-data.js').v1CharData} */ | ||
| 1894 | const character = characters[characterId]; | ||
| 1895 | if (!character) { | ||
| 1896 | console.warn(`Character with ID ${characterId} not found.`); | ||
| 1897 | return; | ||
| 1898 | } | ||
| 1899 | |||
| 1900 | const response = await fetch('/api/chats/delete', { | ||
| 1901 | method: 'POST', | ||
| 1902 | headers: getRequestHeaders(), | ||
| 1903 | body: JSON.stringify({ | ||
| 1904 | chatfile: `${fileName}.jsonl`, | ||
| 1905 | avatar_url: character.avatar, | ||
| 1906 | }), | ||
| 1907 | }); | ||
| 1908 | |||
| 1909 | if (!response.ok) { | ||
| 1910 | console.error('Failed to delete chat for character.'); | ||
| 1911 | return; | ||
| 1912 | } | ||
| 1913 | |||
| 1914 | if (fileName === character.chat) { | ||
| 1915 | const chatsResponse = await fetch('/api/characters/chats', { | ||
| 1916 | method: 'POST', | ||
| 1917 | headers: getRequestHeaders(), | ||
| 1918 | body: JSON.stringify({ avatar_url: character.avatar }), | ||
| 1919 | }); | ||
| 1920 | const chats = Object.values(await chatsResponse.json()); | ||
| 1921 | chats.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); | ||
| 1922 | const newChatName = chats.length && typeof chats[0] === 'object' ? chats[0].file_name.replace('.jsonl', '') : `${character.name} - ${humanizedDateTime()}`; | ||
| 1923 | await updateRemoteChatName(characterId, newChatName); | ||
| 1924 | } | ||
| 1925 | |||
| 1926 | await eventSource.emit(event_types.CHAT_DELETED, fileName); | ||
| 1927 | } | ||
| 1928 | |||
| 1883 | export async function replaceCurrentChat() { | 1929 | export async function replaceCurrentChat() { |
| 1884 | await clearChat(); | 1930 | await clearChat(); |
| 1885 | chat.length = 0; | 1931 | chat.length = 0; |
| @@ -9985,16 +10031,21 @@ async function doRenameChat(_, chatName) { | |||
| 9985 | } | 10031 | } |
| 9986 | 10032 | ||
| 9987 | /** | 10033 | /** |
| 9988 | * Renames the currently selected chat. | 10034 | * Renames a group or character chat. |
| 9989 | * @param {string} oldFileName Old name of the chat (no JSONL extension) | 10035 | * @param {object} param Parameters for renaming chat |
| 9990 | * @param {string} newName New name for the chat (no JSONL extension) | 10036 | * @param {string} [param.characterId] Character ID to rename chat for |
| 10037 | * @param {string} [param.groupId] Group ID to rename chat for | ||
| 10038 | * @param {string} param.oldFileName Old name of the chat (no JSONL extension) | ||
| 10039 | * @param {string} param.newFileName New name for the chat (no JSONL extension) | ||
| 10040 | * @param {boolean} [param.loader=true] Whether to show loader during the operation | ||
| 9991 | */ | 10041 | */ |
| 9992 | export async function renameChat(oldFileName, newName) { | 10042 | export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader }) { |
| 10043 | const currentChatId = getCurrentChatId(); | ||
| 9993 | const body = { | 10044 | const body = { |
| 9994 | is_group: !!selected_group, | 10045 | is_group: !!groupId, |
| 9995 | avatar_url: characters[this_chid]?.avatar, | 10046 | avatar_url: characters[characterId]?.avatar, |
| 9996 | original_file: `${oldFileName}.jsonl`, | 10047 | original_file: `${oldFileName}.jsonl`, |
| 9997 | renamed_file: `${newName.trim()}.jsonl`, | 10048 | renamed_file: `${newFileName.trim()}.jsonl`, |
| 9998 | }; | 10049 | }; |
| 9999 | 10050 | ||
| 10000 | if (body.original_file === body.renamed_file) { | 10051 | if (body.original_file === body.renamed_file) { |
| @@ -10007,7 +10058,8 @@ export async function renameChat(oldFileName, newName) { | |||
| 10007 | } | 10058 | } |
| 10008 | 10059 | ||
| 10009 | try { | 10060 | try { |
| 10010 | showLoader(); | 10061 | loader && showLoader(); |
| 10062 | |||
| 10011 | const response = await fetch('/api/chats/rename', { | 10063 | const response = await fetch('/api/chats/rename', { |
| 10012 | method: 'POST', | 10064 | method: 'POST', |
| 10013 | body: JSON.stringify(body), | 10065 | body: JSON.stringify(body), |
| @@ -10025,27 +10077,69 @@ export async function renameChat(oldFileName, newName) { | |||
| 10025 | } | 10077 | } |
| 10026 | 10078 | ||
| 10027 | if (data.sanitizedFileName) { | 10079 | if (data.sanitizedFileName) { |
| 10028 | newName = data.sanitizedFileName; | 10080 | newFileName = data.sanitizedFileName; |
| 10029 | } | 10081 | } |
| 10030 | 10082 | ||
| 10031 | if (selected_group) { | 10083 | if (groupId) { |
| 10032 | await renameGroupChat(selected_group, oldFileName, newName); | 10084 | await renameGroupChat(groupId, oldFileName, newFileName); |
| 10033 | } | 10085 | } |
| 10034 | else { | 10086 | else if (characterId !== undefined && String(characterId) === String(this_chid) && characters[characterId]?.chat === oldFileName) { |
| 10035 | if (characters[this_chid].chat == oldFileName) { | 10087 | characters[characterId].chat = newFileName; |
| 10036 | characters[this_chid].chat = newName; | 10088 | $('#selected_chat_pole').val(characters[characterId].chat); |
| 10037 | $('#selected_chat_pole').val(characters[this_chid].chat); | 10089 | await createOrEditCharacter(); |
| 10038 | await createOrEditCharacter(); | ||
| 10039 | } | ||
| 10040 | } | 10090 | } |
| 10041 | 10091 | ||
| 10042 | await reloadCurrentChat(); | 10092 | if (currentChatId) { |
| 10093 | await reloadCurrentChat(); | ||
| 10094 | } | ||
| 10043 | } catch { | 10095 | } catch { |
| 10044 | hideLoader(); | 10096 | loader && hideLoader(); |
| 10045 | await delay(500); | 10097 | await delay(500); |
| 10046 | await callPopup('An error has occurred. Chat was not renamed.', 'text'); | 10098 | await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT); |
| 10047 | } finally { | 10099 | } finally { |
| 10048 | hideLoader(); | 10100 | loader && hideLoader(); |
| 10101 | } | ||
| 10102 | } | ||
| 10103 | |||
| 10104 | /** | ||
| 10105 | * Renames the currently selected chat. | ||
| 10106 | * @param {string} oldFileName Old name of the chat (no JSONL extension) | ||
| 10107 | * @param {string} newName New name for the chat (no JSONL extension) | ||
| 10108 | */ | ||
| 10109 | export async function renameChat(oldFileName, newName) { | ||
| 10110 | return await renameGroupOrCharacterChat({ | ||
| 10111 | characterId: this_chid, | ||
| 10112 | groupId: selected_group, | ||
| 10113 | oldFileName: oldFileName, | ||
| 10114 | newFileName: newName, | ||
| 10115 | loader: true, | ||
| 10116 | }); | ||
| 10117 | } | ||
| 10118 | |||
| 10119 | /** | ||
| 10120 | * Forces the update of the chat name for a remote character. | ||
| 10121 | * @param {string|number} characterId Character ID to update chat name for | ||
| 10122 | * @param {string} newName New name for the chat | ||
| 10123 | * @returns {Promise<void>} | ||
| 10124 | */ | ||
| 10125 | export async function updateRemoteChatName(characterId, newName) { | ||
| 10126 | const character = characters[characterId]; | ||
| 10127 | if (!character) { | ||
| 10128 | console.warn(`Character not found for ID: ${characterId}`); | ||
| 10129 | return; | ||
| 10130 | } | ||
| 10131 | character.chat = newName; | ||
| 10132 | const mergeRequest = { | ||
| 10133 | avatar: character.avatar, | ||
| 10134 | chat: newName, | ||
| 10135 | }; | ||
| 10136 | const mergeResponse = await fetch('/api/characters/merge-attributes', { | ||
| 10137 | method: 'POST', | ||
| 10138 | headers: getRequestHeaders(), | ||
| 10139 | body: JSON.stringify(mergeRequest), | ||
| 10140 | }); | ||
| 10141 | if (!mergeResponse.ok) { | ||
| 10142 | console.error('Failed to save extension field', mergeResponse.statusText); | ||
| 10049 | } | 10143 | } |
| 10050 | } | 10144 | } |
| 10051 | 10145 | ||
| @@ -1966,6 +1966,51 @@ export async function renameGroupChat(groupId, oldChatId, newChatId) { | |||
| 1966 | await editGroup(groupId, true, true); | 1966 | await editGroup(groupId, true, true); |
| 1967 | } | 1967 | } |
| 1968 | 1968 | ||
| 1969 | /** | ||
| 1970 | * Deletes a group chat by its name. Doesn't affect displayed chat. | ||
| 1971 | * @param {string} groupId Group ID | ||
| 1972 | * @param {string} chatName Name of the chat to delete | ||
| 1973 | * @returns {Promise<void>} | ||
| 1974 | */ | ||
| 1975 | export async function deleteGroupChatByName(groupId, chatName) { | ||
| 1976 | const group = groups.find(x => x.id === groupId); | ||
| 1977 | if (!group || !group.chats.includes(chatName)) { | ||
| 1978 | return; | ||
| 1979 | } | ||
| 1980 | |||
| 1981 | if (typeof group.past_metadata !== 'object') { | ||
| 1982 | group.past_metadata = {}; | ||
| 1983 | } | ||
| 1984 | |||
| 1985 | group.chats.splice(group.chats.indexOf(chatName), 1); | ||
| 1986 | delete group.past_metadata[chatName]; | ||
| 1987 | |||
| 1988 | const response = await fetch('/api/chats/group/delete', { | ||
| 1989 | method: 'POST', | ||
| 1990 | headers: getRequestHeaders(), | ||
| 1991 | body: JSON.stringify({ id: chatName }), | ||
| 1992 | }); | ||
| 1993 | |||
| 1994 | if (!response.ok) { | ||
| 1995 | toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Group chat could not be deleted`); | ||
| 1996 | console.error('Group chat could not be deleted'); | ||
| 1997 | return; | ||
| 1998 | } | ||
| 1999 | |||
| 2000 | // If the deleted chat was the current chat, switch to the last chat in the group | ||
| 2001 | if (group.chat_id === chatName) { | ||
| 2002 | group.chat_id = ''; | ||
| 2003 | group.chat_metadata = {}; | ||
| 2004 | |||
| 2005 | const newChatName = group.chats.length ? group.chats[group.chats.length - 1] : humanizedDateTime(); | ||
| 2006 | group.chat_id = newChatName; | ||
| 2007 | group.chat_metadata = group.past_metadata[newChatName] || {}; | ||
| 2008 | } | ||
| 2009 | |||
| 2010 | await editGroup(groupId, true, true); | ||
| 2011 | await eventSource.emit(event_types.GROUP_CHAT_DELETED, chatName); | ||
| 2012 | } | ||
| 2013 | |||
| 1969 | export async function deleteGroupChat(groupId, chatId) { | 2014 | export async function deleteGroupChat(groupId, chatId) { |
| 1970 | const group = groups.find(x => x.id === groupId); | 2015 | const group = groups.find(x => x.id === groupId); |
| 1971 | 2016 | ||
| @@ -55,6 +55,14 @@ | |||
| 55 | <span>{{chat_name}}</span> | 55 | <span>{{chat_name}}</span> |
| 56 | </div> | 56 | </div> |
| 57 | <small class="chatDate" title="{{date_long}}">{{date_short}}</small> | 57 | <small class="chatDate" title="{{date_long}}">{{date_short}}</small> |
| 58 | <div class="chatActions"> | ||
| 59 | <button class="menu_button menu_button_icon renameChat" title="Rename chat" data-i18n="[title]Rename chat"> | ||
| 60 | <i class="fa-solid fa-pen-to-square fa-fw"></i> | ||
| 61 | </button> | ||
| 62 | <button class="menu_button menu_button_icon deleteChat" title="Delete chat" data-i18n="[title]Delete chat"> | ||
| 63 | <i class="fa-solid fa-trash fa-fw"></i> | ||
| 64 | </button> | ||
| 65 | </div> | ||
| 58 | </div> | 66 | </div> |
| 59 | <div class="chatMessageContainer"> | 67 | <div class="chatMessageContainer"> |
| 60 | <div class="chatMessage" title="{{mes}}"> | 68 | <div class="chatMessage" title="{{mes}}"> |
| @@ -2,6 +2,7 @@ import { | |||
| 2 | addOneMessage, | 2 | addOneMessage, |
| 3 | characters, | 3 | characters, |
| 4 | chat, | 4 | chat, |
| 5 | deleteCharacterChatByName, | ||
| 5 | displayVersion, | 6 | displayVersion, |
| 6 | doNewChat, | 7 | doNewChat, |
| 7 | event_types, | 8 | event_types, |
| @@ -16,15 +17,18 @@ import { | |||
| 16 | newAssistantChat, | 17 | newAssistantChat, |
| 17 | openCharacterChat, | 18 | openCharacterChat, |
| 18 | printCharactersDebounced, | 19 | printCharactersDebounced, |
| 20 | renameGroupOrCharacterChat, | ||
| 19 | selectCharacterById, | 21 | selectCharacterById, |
| 20 | system_avatar, | 22 | system_avatar, |
| 21 | system_message_types, | 23 | system_message_types, |
| 22 | this_chid, | 24 | this_chid, |
| 23 | unshallowCharacter, | 25 | unshallowCharacter, |
| 26 | updateRemoteChatName, | ||
| 24 | } from '../script.js'; | 27 | } from '../script.js'; |
| 25 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | 28 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 26 | import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js'; | 29 | import { deleteGroupChatByName, getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js'; |
| 27 | import { t } from './i18n.js'; | 30 | import { t } from './i18n.js'; |
| 31 | import { callGenericPopup, POPUP_TYPE } from './popup.js'; | ||
| 28 | import { getMessageTimeStamp } from './RossAscends-mods.js'; | 32 | import { getMessageTimeStamp } from './RossAscends-mods.js'; |
| 29 | import { renderTemplateAsync } from './templates.js'; | 33 | import { renderTemplateAsync } from './templates.js'; |
| 30 | import { accountStorage } from './util/AccountStorage.js'; | 34 | import { accountStorage } from './util/AccountStorage.js'; |
| @@ -51,9 +55,16 @@ export function getPermanentAssistantAvatar() { | |||
| 51 | return assistantAvatar; | 55 | return assistantAvatar; |
| 52 | } | 56 | } |
| 53 | 57 | ||
| 54 | export async function openWelcomeScreen() { | 58 | /** |
| 59 | * Opens a welcome screen if no chat is currently active. | ||
| 60 | * @param {object} param Additional parameters | ||
| 61 | * @param {boolean} [param.force] If true, forces clearing of the welcome screen. | ||
| 62 | * @param {boolean} [param.expand] If true, expands the recent chats section. | ||
| 63 | * @returns {Promise<void>} | ||
| 64 | */ | ||
| 65 | export async function openWelcomeScreen({ force = false, expand = false } = {}) { | ||
| 55 | const currentChatId = getCurrentChatId(); | 66 | const currentChatId = getCurrentChatId(); |
| 56 | if (currentChatId !== undefined || chat.length > 0) { | 67 | if (currentChatId !== undefined || (chat.length > 0 && !force)) { |
| 57 | return; | 68 | return; |
| 58 | } | 69 | } |
| 59 | 70 | ||
| @@ -64,7 +75,13 @@ export async function openWelcomeScreen() { | |||
| 64 | return; | 75 | return; |
| 65 | } | 76 | } |
| 66 | 77 | ||
| 67 | await sendWelcomePanel(recentChats); | 78 | if (chatAfterFetch === undefined && force) { |
| 79 | console.debug('Forcing welcome screen open.'); | ||
| 80 | chat.splice(0, chat.length); | ||
| 81 | $('#chat').empty(); | ||
| 82 | } | ||
| 83 | |||
| 84 | await sendWelcomePanel(recentChats, expand); | ||
| 68 | await unshallowPermanentAssistant(); | 85 | await unshallowPermanentAssistant(); |
| 69 | sendAssistantMessage(); | 86 | sendAssistantMessage(); |
| 70 | sendWelcomePrompt(); | 87 | sendWelcomePrompt(); |
| @@ -131,8 +148,9 @@ function sendWelcomePrompt() { | |||
| 131 | /** | 148 | /** |
| 132 | * Sends the welcome panel to the chat. | 149 | * Sends the welcome panel to the chat. |
| 133 | * @param {RecentChat[]} chats List of recent chats | 150 | * @param {RecentChat[]} chats List of recent chats |
| 151 | * @param {boolean} [expand=false] If true, expands the recent chats section | ||
| 134 | */ | 152 | */ |
| 135 | async function sendWelcomePanel(chats) { | 153 | async function sendWelcomePanel(chats, expand = false) { |
| 136 | try { | 154 | try { |
| 137 | const chatElement = document.getElementById('chat'); | 155 | const chatElement = document.getElementById('chat'); |
| 138 | const sendTextArea = document.getElementById('send_textarea'); | 156 | const sendTextArea = document.getElementById('send_textarea'); |
| @@ -215,7 +233,50 @@ async function sendWelcomePanel(chats) { | |||
| 215 | $(avatar).replaceWith(groupAvatar); | 233 | $(avatar).replaceWith(groupAvatar); |
| 216 | } | 234 | } |
| 217 | }); | 235 | }); |
| 236 | fragment.querySelectorAll('.recentChat .renameChat').forEach((renameButton) => { | ||
| 237 | renameButton.addEventListener('click', (event) => { | ||
| 238 | event.stopPropagation(); | ||
| 239 | const chatItem = renameButton.closest('.recentChat'); | ||
| 240 | if (!chatItem) { | ||
| 241 | return; | ||
| 242 | } | ||
| 243 | const avatarId = chatItem.getAttribute('data-avatar'); | ||
| 244 | const groupId = chatItem.getAttribute('data-group'); | ||
| 245 | const fileName = chatItem.getAttribute('data-file'); | ||
| 246 | if (avatarId && fileName) { | ||
| 247 | void renameRecentCharacterChat(avatarId, fileName); | ||
| 248 | } | ||
| 249 | if (groupId && fileName) { | ||
| 250 | void renameRecentGroupChat(groupId, fileName); | ||
| 251 | } | ||
| 252 | }); | ||
| 253 | }); | ||
| 254 | fragment.querySelectorAll('.recentChat .deleteChat').forEach((deleteButton) => { | ||
| 255 | deleteButton.addEventListener('click', (event) => { | ||
| 256 | event.stopPropagation(); | ||
| 257 | const chatItem = deleteButton.closest('.recentChat'); | ||
| 258 | if (!chatItem) { | ||
| 259 | return; | ||
| 260 | } | ||
| 261 | const avatarId = chatItem.getAttribute('data-avatar'); | ||
| 262 | const groupId = chatItem.getAttribute('data-group'); | ||
| 263 | const fileName = chatItem.getAttribute('data-file'); | ||
| 264 | if (avatarId && fileName) { | ||
| 265 | void deleteRecentCharacterChat(avatarId, fileName); | ||
| 266 | } | ||
| 267 | if (groupId && fileName) { | ||
| 268 | void deleteRecentGroupChat(groupId, fileName); | ||
| 269 | } | ||
| 270 | }); | ||
| 271 | }); | ||
| 218 | chatElement.append(fragment.firstChild); | 272 | chatElement.append(fragment.firstChild); |
| 273 | if (expand) { | ||
| 274 | chatElement.querySelectorAll('button.showMoreChats').forEach((button) => { | ||
| 275 | if (button instanceof HTMLButtonElement) { | ||
| 276 | button.click(); | ||
| 277 | } | ||
| 278 | }); | ||
| 279 | } | ||
| 219 | } catch (error) { | 280 | } catch (error) { |
| 220 | console.error('Welcome screen error:', error); | 281 | console.error('Welcome screen error:', error); |
| 221 | } | 282 | } |
| @@ -274,6 +335,144 @@ async function openRecentGroupChat(groupId, fileName) { | |||
| 274 | } | 335 | } |
| 275 | 336 | ||
| 276 | /** | 337 | /** |
| 338 | * Renames a recent character chat. | ||
| 339 | * @param {string} avatarId Avatar file name | ||
| 340 | * @param {string} fileName Chat file name | ||
| 341 | */ | ||
| 342 | async function renameRecentCharacterChat(avatarId, fileName) { | ||
| 343 | const characterId = characters.findIndex(x => x.avatar === avatarId); | ||
| 344 | if (characterId === -1) { | ||
| 345 | console.error(`Character not found for avatar ID: ${avatarId}`); | ||
| 346 | return; | ||
| 347 | } | ||
| 348 | try { | ||
| 349 | const popupText = await renderTemplateAsync('chatRename'); | ||
| 350 | const newName = await callGenericPopup(popupText, POPUP_TYPE.INPUT, fileName); | ||
| 351 | if (!newName || typeof newName !== 'string' || newName === fileName) { | ||
| 352 | console.log('No new name provided, aborting'); | ||
| 353 | return; | ||
| 354 | } | ||
| 355 | await renameGroupOrCharacterChat({ | ||
| 356 | characterId: String(characterId), | ||
| 357 | oldFileName: fileName, | ||
| 358 | newFileName: newName, | ||
| 359 | loader: false, | ||
| 360 | }); | ||
| 361 | await updateRemoteChatName(characterId, newName); | ||
| 362 | await refreshWelcomeScreen(); | ||
| 363 | toastr.success(t`Chat renamed.`); | ||
| 364 | } catch (error) { | ||
| 365 | console.error('Error renaming recent character chat:', error); | ||
| 366 | toastr.error(t`Failed to rename recent chat. See console for details.`); | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | /** | ||
| 371 | * Renames a recent group chat. | ||
| 372 | * @param {string} groupId Group ID | ||
| 373 | * @param {string} fileName Chat file name | ||
| 374 | */ | ||
| 375 | async function renameRecentGroupChat(groupId, fileName) { | ||
| 376 | const group = groups.find(x => x.id === groupId); | ||
| 377 | if (!group) { | ||
| 378 | console.error(`Group not found for ID: ${groupId}`); | ||
| 379 | return; | ||
| 380 | } | ||
| 381 | try { | ||
| 382 | const popupText = await renderTemplateAsync('chatRename'); | ||
| 383 | const newName = await callGenericPopup(popupText, POPUP_TYPE.INPUT, fileName); | ||
| 384 | if (!newName || newName === fileName) { | ||
| 385 | console.log('No new name provided, aborting'); | ||
| 386 | return; | ||
| 387 | } | ||
| 388 | await renameGroupOrCharacterChat({ | ||
| 389 | groupId: String(groupId), | ||
| 390 | oldFileName: fileName, | ||
| 391 | newFileName: String(newName), | ||
| 392 | loader: false, | ||
| 393 | }); | ||
| 394 | await refreshWelcomeScreen(); | ||
| 395 | toastr.success(t`Group chat renamed.`); | ||
| 396 | } catch (error) { | ||
| 397 | console.error('Error renaming recent group chat:', error); | ||
| 398 | toastr.error(t`Failed to rename recent group chat. See console for details.`); | ||
| 399 | } | ||
| 400 | } | ||
| 401 | |||
| 402 | /** | ||
| 403 | * Deletes a recent character chat. | ||
| 404 | * @param {string} avatarId Avatar file name | ||
| 405 | * @param {string} fileName Chat file name | ||
| 406 | */ | ||
| 407 | async function deleteRecentCharacterChat(avatarId, fileName) { | ||
| 408 | const characterId = characters.findIndex(x => x.avatar === avatarId); | ||
| 409 | if (characterId === -1) { | ||
| 410 | console.error(`Character not found for avatar ID: ${avatarId}`); | ||
| 411 | return; | ||
| 412 | } | ||
| 413 | try { | ||
| 414 | const confirm = await callGenericPopup(t`Delete the Chat File?`, POPUP_TYPE.CONFIRM); | ||
| 415 | if (!confirm) { | ||
| 416 | console.log('Deletion cancelled by user'); | ||
| 417 | return; | ||
| 418 | } | ||
| 419 | await deleteCharacterChatByName(String(characterId), fileName); | ||
| 420 | await refreshWelcomeScreen(); | ||
| 421 | toastr.success(t`Chat deleted.`); | ||
| 422 | } catch (error) { | ||
| 423 | console.error('Error deleting recent character chat:', error); | ||
| 424 | toastr.error(t`Failed to delete recent chat. See console for details.`); | ||
| 425 | } | ||
| 426 | } | ||
| 427 | |||
| 428 | /** | ||
| 429 | * Deletes a recent group chat. | ||
| 430 | * @param {string} groupId Group ID | ||
| 431 | * @param {string} fileName Chat file name | ||
| 432 | */ | ||
| 433 | async function deleteRecentGroupChat(groupId, fileName) { | ||
| 434 | const group = groups.find(x => x.id === groupId); | ||
| 435 | if (!group) { | ||
| 436 | console.error(`Group not found for ID: ${groupId}`); | ||
| 437 | return; | ||
| 438 | } | ||
| 439 | try { | ||
| 440 | const confirm = await callGenericPopup(t`Delete the Chat File?`, POPUP_TYPE.CONFIRM); | ||
| 441 | if (!confirm) { | ||
| 442 | console.log('Deletion cancelled by user'); | ||
| 443 | return; | ||
| 444 | } | ||
| 445 | await deleteGroupChatByName(groupId, fileName); | ||
| 446 | await refreshWelcomeScreen(); | ||
| 447 | toastr.success(t`Group chat deleted.`); | ||
| 448 | } catch (error) { | ||
| 449 | console.error('Error deleting recent group chat:', error); | ||
| 450 | toastr.error(t`Failed to delete recent group chat. See console for details.`); | ||
| 451 | } | ||
| 452 | } | ||
| 453 | |||
| 454 | /** | ||
| 455 | * Reopens the welcome screen and restores the scroll position. | ||
| 456 | * @returns {Promise<void>} | ||
| 457 | */ | ||
| 458 | async function refreshWelcomeScreen() { | ||
| 459 | const chatElement = document.getElementById('chat'); | ||
| 460 | if (!chatElement) { | ||
| 461 | console.error('Chat element not found'); | ||
| 462 | return; | ||
| 463 | } | ||
| 464 | |||
| 465 | const scrollTop = chatElement.scrollTop; | ||
| 466 | const scrollHeight = chatElement.scrollHeight; | ||
| 467 | const expand = chatElement.querySelectorAll('button.showMoreChats.rotated').length > 0; | ||
| 468 | |||
| 469 | await openWelcomeScreen({ force: true, expand }); | ||
| 470 | |||
| 471 | // Restore scroll position | ||
| 472 | chatElement.scrollTop = scrollTop + (chatElement.scrollHeight - scrollHeight); | ||
| 473 | } | ||
| 474 | |||
| 475 | /** | ||
| 277 | * Gets the list of recent chats from the server. | 476 | * Gets the list of recent chats from the server. |
| 278 | * @returns {Promise<RecentChat[]>} List of recent chats | 477 | * @returns {Promise<RecentChat[]>} List of recent chats |
| 279 | * | 478 | * |