Blame Raw
Cohee · 51ad27fb · · 737 lines (25.7 KB)
3 contributors
1import {
2 characters,
3 saveChat,
4 system_message_types,
5 syncSwipeToMes,
6 this_chid,
7 openCharacterChat,
8 chat_metadata,
9 getRequestHeaders,
10 getThumbnailUrl,
11 getCharacters,
12 chat,
13 saveChatConditional,
14 saveItemizedPrompts,
15 setActiveGroup,
16 getCurrentChatDetails,
17} from '../script.js';
18import { humanizedDateTime } from './RossAscends-mods.js';
19import {
20 DEFAULT_AUTO_MODE_DELAY,
21 group_activation_strategy,
22 group_generation_mode,
23 groups,
24 openGroupById,
25 openGroupChat,
26 saveGroupBookmarkChat,
27 selected_group,
28} from './group-chats.js';
29import { loader } from './action-loader.js';
30import { getLastMessageId } from './macros.js';
31import { Popup } from './popup.js';
32import { SlashCommand } from './slash-commands/SlashCommand.js';
33import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
34import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
35import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
36import { createTagMapFromList } from './tags.js';
37import { renderTemplateAsync } from './templates.js';
38import { compressRequest } from './request-compression.js';
39import { t } from './i18n.js';
40
41import {
42 getUniqueName,
43 isTrueBoolean,
44} from './utils.js';
45
46const bookmarkNameToken = 'Checkpoint #';
47
48/**
49 * Gets the names of existing chats for the current character or group.
50 * @returns {Promise<string[]>} - Returns a promise that resolves to an array of existing chat names.
51 */
52async function getExistingChatNames() {
53 if (selected_group) {
54 const group = groups.find(x => x.id == selected_group);
55 if (group && Array.isArray(group.chats)) {
56 return [...group.chats];
57 }
58
59 return [];
60 }
61
62 if (this_chid === undefined) {
63 return [];
64 }
65
66 const character = characters[this_chid];
67 if (!character) {
68 return [];
69 }
70
71 const response = await fetch('/api/characters/chats', {
72 method: 'POST',
73 headers: getRequestHeaders(),
74 body: JSON.stringify({ avatar_url: character.avatar, simple: true }),
75 });
76
77 if (response.ok) {
78 const data = await response.json();
79 const chats = Object.values(data).map(x => x.file_name.replace('.jsonl', ''));
80 return [...chats];
81 }
82
83 return [];
84}
85
86async function getBookmarkName({ isReplace = false, forceName = null } = {}) {
87 const mainChatName = (getCurrentChatDetails()).sessionName;
88
89 function buildCheckpointName(name, i) {
90 // Strip off existing suffixes, then build new name
91 let cleanName = name.replace(new RegExp(` - ${bookmarkNameToken}\\d+$`), '');
92 // Strip off legacy old name prefix too
93 cleanName = cleanName.replace(new RegExp(`^${bookmarkNameToken}\\d+ - `), '');
94 return `${cleanName} - ${bookmarkNameToken}${i}`;
95 }
96 const existingChats = await getExistingChatNames();
97 const suggestedName = getUniqueName(mainChatName, (x) => existingChats.includes(x), { nameBuilder: buildCheckpointName });
98
99 const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace, suggestedName: suggestedName });
100 let name = forceName ?? await Popup.show.input('Create Checkpoint', body, suggestedName);
101 // Special handling for confirmed empty input (=> auto-generate name)
102 if (name === '') {
103 name = suggestedName;
104 }
105 if (!name) {
106 return null;
107 }
108
109 return name;
110}
111
112function getMainChatName() {
113 if (chat_metadata) {
114 if (chat_metadata.main_chat) {
115 return chat_metadata.main_chat;
116 } else if (selected_group) {
117 // groups didn't support bookmarks before chat metadata was introduced
118 return null;
119 } else if (characters[this_chid].chat && characters[this_chid].chat.includes(bookmarkNameToken)) {
120 const tokenIndex = characters[this_chid].chat.lastIndexOf(bookmarkNameToken);
121 chat_metadata.main_chat = characters[this_chid].chat.substring(0, tokenIndex).trim();
122 return chat_metadata.main_chat;
123 }
124 }
125 return null;
126}
127
128export function showBookmarksButtons() {
129 try {
130 if (selected_group) {
131 $('#option_convert_to_group').hide();
132 } else {
133 $('#option_convert_to_group').show();
134 }
135
136 if (chat_metadata.main_chat) {
137 // In bookmark chat
138 $('#option_back_to_main').show();
139 $('#option_new_bookmark').show();
140 } else if (!selected_group && !characters[this_chid].chat) {
141 // No chat recorded on character
142 $('#option_back_to_main').hide();
143 $('#option_new_bookmark').hide();
144 } else {
145 // In main chat
146 $('#option_back_to_main').hide();
147 $('#option_new_bookmark').show();
148 }
149 } catch {
150 $('#option_back_to_main').hide();
151 $('#option_new_bookmark').hide();
152 $('#option_convert_to_group').hide();
153 }
154}
155
156async function saveBookmarkMenu() {
157 if (!chat.length) {
158 toastr.warning('The chat is empty.', 'Checkpoint creation failed');
159 return;
160 }
161
162 return await createNewBookmark(chat.length - 1);
163}
164
165/**
166 * Builds the branch chat snapshot, optionally selecting a specific swipe for the target message.
167 * @param {number} mesId
168 * @param {{swipeId?: number|null}} [options={}]
169 * @returns {ChatMessage[]|null}
170 */
171function getBranchChatSnapshot(mesId, { swipeId = null } = {}) {
172 const snapshot = structuredClone(chat.slice(0, Number(mesId) + 1));
173
174 if (swipeId === null) {
175 return snapshot;
176 }
177
178 if (!syncSwipeToMes(null, swipeId, snapshot[mesId])) {
179 return null;
180 }
181
182 return snapshot;
183}
184
185// Export is used by Timelines extension. Do not remove.
186export async function createBranch(mesId, { swipeId = null } = {}) {
187 if (!chat.length) {
188 toastr.warning('The chat is empty.', 'Branch creation failed');
189 return;
190 }
191
192 if (mesId < 0 || mesId >= chat.length) {
193 toastr.warning('Invalid message ID.', 'Branch creation failed');
194 return;
195 }
196
197 const lastMes = chat[mesId];
198 const mainChatName = (getCurrentChatDetails()).sessionName;
199 const newMetadata = { main_chat: mainChatName };
200 const selectedSwipeId = swipeId === null ? null : Number(swipeId);
201
202 if (selectedSwipeId !== null && (!Number.isInteger(selectedSwipeId) || selectedSwipeId < 0 || selectedSwipeId >= (lastMes?.swipes?.length ?? 0))) {
203 toastr.warning('Invalid swipe ID.', 'Branch creation failed');
204 return;
205 }
206
207 function buildBranchName(name, i) {
208 // Strip off existing suffixes, then build new name
209 let cleanName = name.replace(/ - Branch #\d+$/, '');
210 // Strip off legacy old name prefix too
211 cleanName = cleanName.replace(/^Branch #\d+ - /, '');
212 return `${cleanName} - Branch #${i}`;
213 }
214 const existingChats = await getExistingChatNames();
215 const name = getUniqueName(mainChatName, (x) => existingChats.includes(x), { nameBuilder: buildBranchName });
216 if (!name) {
217 console.error('Could not generate a unique branch name.');
218 toastr.error('Could not generate a unique branch name.', 'Branch creation failed');
219 return;
220 }
221
222 const branchChatSnapshot = getBranchChatSnapshot(mesId, { swipeId: selectedSwipeId });
223 if (!branchChatSnapshot) {
224 toastr.warning('Could not prepare the selected swipe for branching.', 'Branch creation failed');
225 return;
226 }
227
228 if (selected_group) {
229 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId, branchChatSnapshot);
230 } else {
231 await saveChat({ chatName: name, withMetadata: newMetadata, mesId, chatData: branchChatSnapshot });
232 }
233 // append to branches list if it exists
234 // otherwise create it
235 if (typeof lastMes.extra !== 'object') {
236 lastMes.extra = {};
237 }
238 if (typeof lastMes.extra.branches !== 'object') {
239 lastMes.extra.branches = [];
240 }
241 lastMes.extra.branches.push(name);
242 return name;
243}
244
245/**
246 * Creates a new bookmark for a message.
247 *
248 * @param {number} mesId - The ID of the message.
249 * @param {Object} [options={}] - Optional parameters.
250 * @param {string?} [options.forceName=null] - The name to force for the bookmark.
251 * @returns {Promise<string?>} - A promise that resolves to the bookmark name when the bookmark is created.
252 */
253export async function createNewBookmark(mesId, { forceName = null } = {}) {
254 if (this_chid === undefined && !selected_group) {
255 toastr.info('No character selected.', 'Create Checkpoint');
256 return null;
257 }
258 if (!chat.length) {
259 toastr.warning('The chat is empty.', 'Create Checkpoint');
260 return null;
261 }
262 if (!chat[mesId]) {
263 toastr.warning('Invalid message ID.', 'Create Checkpoint');
264 return null;
265 }
266
267 const lastMes = chat[mesId];
268
269 if (typeof lastMes.extra !== 'object') {
270 lastMes.extra = {};
271 }
272
273 const isReplace = lastMes.extra.bookmark_link;
274
275 let name = await getBookmarkName({ isReplace: isReplace, forceName: forceName });
276 if (!name) {
277 return null;
278 }
279
280 const mainChat = selected_group ? groups?.find(x => x.id == selected_group)?.chat_id : characters[this_chid].chat;
281 const newMetadata = { main_chat: mainChat };
282 await saveItemizedPrompts(name);
283
284 if (selected_group) {
285 await saveGroupBookmarkChat(selected_group, name, newMetadata, mesId);
286 } else {
287 await saveChat({ chatName: name, withMetadata: newMetadata, mesId });
288 }
289
290 lastMes.extra.bookmark_link = name;
291
292 const mes = $(`.mes[mesid="${mesId}"]`);
293 updateBookmarkDisplay(mes, name);
294
295 await saveChatConditional();
296 toastr.success('Click the flag icon next to the message to open the checkpoint chat.', 'Create Checkpoint', { timeOut: 10000 });
297 return name;
298}
299
300
301/**
302 * Updates the display of the bookmark on a chat message.
303 * @param {JQuery<HTMLElement>} mes - The message element
304 * @param {string?} [newBookmarkLink=null] - The new bookmark link (optional)
305 */
306export function updateBookmarkDisplay(mes, newBookmarkLink = null) {
307 newBookmarkLink && mes.attr('bookmark_link', newBookmarkLink);
308 const bookmarkFlag = mes.find('.mes_bookmark');
309 bookmarkFlag.attr('title', `Checkpoint\n${mes.attr('bookmark_link')}\n\n${bookmarkFlag.data('tooltip')}`);
310}
311
312async function backToMainChat() {
313 const mainChatName = getMainChatName();
314 const allChats = await getExistingChatNames();
315
316 if (allChats.includes(mainChatName)) {
317 if (selected_group) {
318 await openGroupChat(selected_group, mainChatName);
319 } else {
320 await openCharacterChat(mainChatName);
321 }
322 return mainChatName;
323 }
324
325 return null;
326}
327
328export async function convertSoloToGroupChat() {
329 if (selected_group) {
330 console.log('Already in group. No need for conversion');
331 return;
332 }
333
334 if (this_chid === undefined) {
335 console.log('Need to have a character selected');
336 return;
337 }
338
339 const confirm = await Popup.show.confirm(t`Convert to group chat`, t`Are you sure you want to convert this chat to a group chat?` + '<br />' + t`This cannot be reverted.`);
340 if (!confirm) {
341 return;
342 }
343
344 const character = characters[this_chid];
345
346 // Populate group required fields
347 const name = getUniqueName(`Group: ${character.name}`, y => groups.findIndex(x => x.name === y) !== -1);
348 const avatar = getThumbnailUrl('avatar', character.avatar);
349 const chatName = humanizedDateTime();
350 const chats = [chatName];
351 const members = [character.avatar];
352 const favChecked = character.fav || character.fav == 'true';
353 /** @type {ChatMetadata} */
354 const metadata = Object.assign({}, chat_metadata);
355 delete metadata.main_chat;
356 /** @type {ChatHeader} */
357 const chatHeader = {
358 chat_metadata: metadata,
359 user_name: 'unused',
360 character_name: 'unused',
361 };
362 /** @type {Omit<Group, 'id'>} */
363 const groupCreateModel = {
364 name: name,
365 members: members,
366 avatar_url: avatar,
367 allow_self_responses: false,
368 activation_strategy: group_activation_strategy.NATURAL,
369 disabled_members: [],
370 fav: favChecked,
371 chat_id: chatName,
372 chats: chats,
373 hideMutedSprites: false,
374 generation_mode: group_generation_mode.SWAP,
375 auto_mode_delay: DEFAULT_AUTO_MODE_DELAY,
376 };
377
378 const createGroupResponse = await fetch('/api/groups/create', {
379 method: 'POST',
380 headers: getRequestHeaders(),
381 body: JSON.stringify(groupCreateModel),
382 });
383
384 if (!createGroupResponse.ok) {
385 console.error('Group creation unsuccessful');
386 return;
387 }
388
389 /** @type {Group} */
390 const group = await createGroupResponse.json();
391
392 // Convert tags list and assign to group
393 createTagMapFromList('#tagList', group.id);
394
395 // Update chars list
396 await getCharacters();
397
398 // Convert chat to group format
399 const groupChat = [...chat].map(m => structuredClone(m));
400 const genIdFirst = Date.now();
401
402 for (let index = 0; index < groupChat.length; index++) {
403 const message = groupChat[index];
404
405 // Skip messages we don't care about
406 if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR || message.force_avatar !== undefined) {
407 continue;
408 }
409
410 if (!message.extra || typeof message.extra !== 'object') {
411 message.extra = {};
412 }
413
414 // Set force fields for solo character
415 message.name = character.name;
416 message.original_avatar = character.avatar;
417 message.force_avatar = getThumbnailUrl('avatar', character.avatar);
418 // Allow regens of a single message in group
419 message.extra.gen_id = genIdFirst + index;
420 }
421
422 // Save group chat
423 const createChatRequest = await compressRequest({
424 method: 'POST',
425 headers: getRequestHeaders(),
426 body: JSON.stringify({ id: chatName, chat: [chatHeader, ...groupChat] }),
427 });
428 const createChatResponse = await fetch('/api/chats/group/save', createChatRequest);
429
430 if (!createChatResponse.ok) {
431 console.error('Group chat creation unsuccessful');
432 toastr.error('Group chat creation unsuccessful');
433 return;
434 }
435
436 // Click on the freshly selected group to open it
437 setActiveGroup(group.id);
438 await openGroupById(group.id);
439
440 toastr.success(t`The chat has been successfully converted!`);
441}
442
443/**
444 * Creates a new branch from the message with the given ID
445 * @param {number} mesId Message ID
446 * @param {{swipeId?: number|null}} [options={}] Branch options
447 * @returns {Promise<string?>} Branch file name
448 */
449export async function branchChat(mesId, { swipeId = null } = {}) {
450 if (this_chid === undefined && !selected_group) {
451 toastr.info('No character selected.', 'Create Branch');
452 return null;
453 }
454
455 const fileName = await createBranch(mesId, { swipeId });
456 if (!fileName) {
457 return null;
458 }
459
460 await saveItemizedPrompts(fileName);
461
462 if (selected_group) {
463 await openGroupChat(selected_group, fileName);
464 } else {
465 await openCharacterChat(fileName);
466 }
467
468 return fileName;
469}
470
471function registerBookmarksSlashCommands() {
472 /**
473 * Validates a message ID. (Is a number, exists as a message)
474 *
475 * @param {number} mesId - The message ID to validate.
476 * @param {string} context - The context of the slash command. Will be used as the title of any toasts.
477 * @returns {boolean} - Returns true if the message ID is valid, otherwise false.
478 */
479 function validateMessageId(mesId, context) {
480 if (isNaN(mesId)) {
481 toastr.warning('Invalid message ID was provided', context);
482 return false;
483 }
484 if (!chat[mesId]) {
485 toastr.warning(`Message for id ${mesId} not found`, context);
486 return false;
487 }
488 return true;
489 }
490
491 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
492 name: 'branch-create',
493 returns: 'Name of the new branch',
494 callback: async (args, text) => {
495 const mesId = Number(args.mesId ?? text ?? getLastMessageId());
496 if (!validateMessageId(mesId, 'Create Branch')) return '';
497
498 const branchName = await branchChat(mesId);
499 return branchName ?? '';
500 },
501 unnamedArgumentList: [
502 SlashCommandArgument.fromProps({
503 description: 'Message ID',
504 typeList: [ARGUMENT_TYPE.NUMBER],
505 enumProvider: commonEnumProviders.messages(),
506 }),
507 ],
508 helpString: `
509 <div>
510 Create a new branch from the selected message. If no message id is provided, will use the last message.
511 </div>
512 <div>
513 Creating a branch will automatically choose a name for the branch.<br />
514 After creating the branch, the branch chat will be automatically opened.
515 </div>
516 <div>
517 Use Checkpoints and <code>/checkpoint-create</code> instead if you do not want to jump to the new chat.
518 </div>`,
519 }));
520 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
521 name: 'checkpoint-create',
522 returns: 'Name of the new checkpoint',
523 callback: async (args, text) => {
524 const mesId = Number(args.mesId ?? getLastMessageId());
525 if (!validateMessageId(mesId, 'Create Checkpoint')) return '';
526
527 if (typeof text !== 'string') {
528 toastr.warning('Checkpoint name must be a string or empty', 'Create Checkpoint');
529 return '';
530 }
531
532 const checkPointName = await createNewBookmark(mesId, { forceName: text });
533 return checkPointName ?? '';
534 },
535 namedArgumentList: [
536 SlashCommandNamedArgument.fromProps({
537 name: 'mesId',
538 description: 'Message ID',
539 typeList: [ARGUMENT_TYPE.NUMBER],
540 enumProvider: commonEnumProviders.messages(),
541 }),
542 ],
543 unnamedArgumentList: [
544 SlashCommandArgument.fromProps({
545 description: 'Checkpoint name',
546 typeList: [ARGUMENT_TYPE.STRING],
547 }),
548 ],
549 helpString: `
550 <div>
551 Create a new checkpoint for the selected message with the provided name. If no message id is provided, will use the last message.<br />
552 Leave the checkpoint name empty to auto-generate one.
553 </div>
554 <div>
555 A created checkpoint will be permanently linked with the message.<br />
556 If a checkpoint already exists, the link to it will be overwritten.<br />
557 After creating the checkpoint, the checkpoint chat can be opened with the checkpoint flag,
558 using the <code>/go</code> command with the checkpoint name or the <code>/checkpoint-go</code> command on the message.
559 </div>
560 <div>
561 Use Branches and <code>/branch-create</code> instead if you do want to jump to the new chat.
562 </div>
563 <div>
564 <strong>Example:</strong>
565 <ul>
566 <li>
567 <pre><code>/checkpoint-create mes={{lastCharMessage}} Checkpoint for char reply | /setvar key=rememberCheckpoint {{pipe}}</code></pre>
568 Will create a new checkpoint to the latest message of the current character, and save it as a local variable for future use.
569 </li>
570 </ul>
571 </div>`,
572 }));
573 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
574 name: 'checkpoint-go',
575 returns: 'Name of the checkpoint',
576 callback: async (args, text) => {
577 const mesId = Number(args.mesId ?? text ?? getLastMessageId());
578 if (!validateMessageId(mesId, 'Open Checkpoint')) return '';
579
580 const checkPointName = chat[mesId].extra?.bookmark_link;
581 if (!checkPointName) {
582 toastr.warning('No checkpoint is linked to the selected message', 'Open Checkpoint');
583 return '';
584 }
585
586 if (selected_group) {
587 await openGroupChat(selected_group, checkPointName);
588 } else {
589 await openCharacterChat(checkPointName);
590 }
591
592 return checkPointName;
593 },
594 unnamedArgumentList: [
595 SlashCommandArgument.fromProps({
596 description: 'Message ID',
597 typeList: [ARGUMENT_TYPE.NUMBER],
598 enumProvider: commonEnumProviders.messages(),
599 }),
600 ],
601 helpString: `
602 <div>
603 Open the checkpoint linked to the selected message. If no message id is provided, will use the last message.
604 </div>
605 <div>
606 Use <code>/checkpoint-get</code> if you want to make sure that the selected message has a checkpoint.
607 </div>`,
608 }));
609 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
610 name: 'checkpoint-exit',
611 returns: 'The name of the chat exited to. Returns an empty string if not in a checkpoint chat.',
612 callback: async () => {
613 const mainChat = await backToMainChat();
614 return mainChat ?? '';
615 },
616 helpString: 'Exit the checkpoint chat.<br />If not in a checkpoint chat, returns empty string.',
617 }));
618 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
619 name: 'checkpoint-parent',
620 returns: 'Name of the parent chat for this checkpoint',
621 callback: async () => {
622 const mainChatName = getMainChatName();
623 return mainChatName ?? '';
624 },
625 helpString: 'Get the name of the parent chat for this checkpoint.<br />If not in a checkpoint chat, returns empty string.',
626 }));
627 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
628 name: 'checkpoint-get',
629 returns: 'Name of the chat',
630 callback: async (args, text) => {
631 const mesId = Number(args.mesId ?? text ?? getLastMessageId());
632 if (!validateMessageId(mesId, 'Get Checkpoint')) return '';
633
634 const checkPointName = chat[mesId].extra?.bookmark_link;
635 return checkPointName ?? '';
636 },
637 unnamedArgumentList: [
638 SlashCommandArgument.fromProps({
639 description: 'Message ID',
640 typeList: [ARGUMENT_TYPE.NUMBER],
641 enumProvider: commonEnumProviders.messages(),
642 }),
643 ],
644 helpString: `
645 <div>
646 Get the name of the checkpoint linked to the selected message. If no message id is provided, will use the last message.<br />
647 If no checkpoint is linked, the result will be empty.
648 </div>`,
649 }));
650 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
651 name: 'checkpoint-list',
652 returns: 'JSON array of all existing checkpoints in this chat, as an array',
653 /** @param {{links?: string}} args @returns {Promise<string>} */
654 callback: async (args, _) => {
655 const result = Object.entries(chat)
656 .filter(([_, message]) => message.extra?.bookmark_link)
657 .map(([mesId, message]) => isTrueBoolean(args.links) ? message.extra.bookmark_link : Number(mesId));
658 return JSON.stringify(result);
659 },
660 namedArgumentList: [
661 SlashCommandNamedArgument.fromProps({
662 name: 'links',
663 description: 'Get a list of all links / chat names of the checkpoints, instead of the message ids',
664 typeList: [ARGUMENT_TYPE.BOOLEAN],
665 enumList: commonEnumProviders.boolean('trueFalse')(),
666 defaultValue: 'false',
667 }),
668 ],
669 helpString: `
670 <div>
671 List all existing checkpoints in this chat.
672 </div>
673 <div>
674 Returns a list of all message ids that have a checkpoint, or all checkpoint links if <code>links</code> is set to <code>true</code>.<br />
675 The value will be a JSON array.
676 </div>`,
677 }));
678}
679
680export function initBookmarks() {
681 $('#option_new_bookmark').on('click', saveBookmarkMenu);
682 $('#option_back_to_main').on('click', backToMainChat);
683 $('#option_convert_to_group').on('click', convertSoloToGroupChat);
684
685 $(document).on('click', '.select_chat_block, .mes_bookmark', async function (e) {
686 // If shift is held down, we are not following the bookmark, but creating a new one
687 const mes = $(this).closest('.mes');
688 if (e.shiftKey && mes.length) {
689 const selectedMesId = mes.attr('mesid');
690 await createNewBookmark(Number(selectedMesId));
691 return;
692 }
693
694 const fileName = $(this).hasClass('mes_bookmark')
695 ? $(this).closest('.mes').attr('bookmark_link')
696 : $(this).attr('file_name');
697
698 if (!fileName) {
699 return;
700 }
701
702 const loaderHandle = loader.show({
703 slug: 'chat-load',
704 title: t`Chat History`,
705 message: t`Loading chat…`,
706 toastMode: loader.ToastMode.STATIC,
707 });
708
709 try {
710 if (selected_group) {
711 await openGroupChat(selected_group, fileName);
712 } else {
713 await openCharacterChat(fileName);
714 }
715 } finally {
716 await loaderHandle.hide();
717 }
718
719 $('#shadow_select_chat_popup').css('display', 'none');
720 });
721
722 $(document).on('click', '.mes_create_bookmark', async function () {
723 const mesId = $(this).closest('.mes').attr('mesid');
724 if (mesId !== undefined) {
725 await createNewBookmark(Number(mesId));
726 }
727 });
728
729 $(document).on('click', '.mes_create_branch', async function () {
730 const mesId = $(this).closest('.mes').attr('mesid');
731 if (mesId !== undefined) {
732 await branchChat(Number(mesId));
733 }
734 });
735
736 registerBookmarksSlashCommands();
737}