Merge pull request #2803 from SillyTavern/small-bookmark-updates Update Checkpoints Feature (Expand tooltip and icons, add slash commands, refactoring)

0a05ef63c6a82a1eebf958aff36cb1f34ecc660d

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
10 files changed, +384 -134Ignore whitespace
public/index.html+1 -1
@@ -5799,7 +5799,7 @@
57995799 <div title="Create branch" class="mes_button mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
58005800 <div title="Copy" class="mes_button mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
58015801 </div>
58025802 <div titledata-tooltip="OpenClick to open checkpoint chat&#10;Shift+Click to replace the existing checkpoint with a new one" class="mes_button mes_bookmark fa-solid fa-flag" data-i18n="[titledata-tooltip]Open checkpoint chat&#10;Shift+Click to replace the existing checkpoint with a new one"></div>
58035803 <div title="Edit" class="mes_button mes_edit fa-solid fa-pencil " data-i18n="[title]Edit"></div>
58045804 </div>
58055805 <div class="mes_edit_buttons">
public/script.js+4 -79
@@ -117,9 +117,9 @@ import {
117117} from './scripts/nai-settings.js';
118118
119119import {
120120 createNewBookmarkinitBookmarks,
121121 showBookmarksButtons,
122122 createBranchupdateBookmarkDisplay,
123123} from './scripts/bookmarks.js';
124124
125125import {
@@ -558,8 +558,6 @@ export const system_message_types = {
558558 GROUP: 'group',
559559 EMPTY: 'empty',
560560 GENERIC: 'generic',
561- BOOKMARK_CREATED: 'bookmark_created',
562- BOOKMARK_BACK: 'bookmark_back',
563561 NARRATOR: 'narrator',
564562 COMMENT: 'comment',
565563 SLASH_COMMANDS: 'slash_commands',
@@ -671,20 +669,6 @@ async function getSystemMessages() {
671669 is_system: true,
672670 mes: 'Generic system message. User `text` parameter to override the contents',
673671 },
674- bookmark_created: {
675- name: systemUserName,
676- force_avatar: system_avatar,
677- is_user: false,
678- is_system: true,
679- mes: 'Checkpoint created! Click here to open the checkpoint chat: <a class="bookmark_link" file_name="{0}" href="javascript:void(null);">{1}</a>',
680- },
681- bookmark_back: {
682- name: systemUserName,
683- force_avatar: system_avatar,
684- is_user: false,
685- is_system: true,
686- mes: 'Click here to return to the previous chat: <a class="bookmark_link" file_name="{0}" href="javascript:void(null);">Return</a>',
687- },
688672 welcome_prompt: {
689673 name: systemUserName,
690674 force_avatar: system_avatar,
@@ -954,6 +938,7 @@ async function firstLoadInit() {
954938 initDynamicStyles();
955939 initTags();
956940 initOpenai();
941+ initBookmarks();
957942 await getUserAvatars(true, user_avatar);
958943 await getCharacters();
959944 await getBackgrounds();
@@ -2128,6 +2113,7 @@ function getMessageFromTemplate({
21282113 tokenCount && mes.find('.tokenCounterDisplay').text(`${tokenCount}t`);
21292114 title && mes.attr('title', title);
21302115 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2116+ bookmarkLink && updateBookmarkDisplay(mes);
21312117
21322118 if (power_user.timestamp_model_icon && extra?.api) {
21332119 insertSVGIcon(mes, extra);
@@ -8246,29 +8232,6 @@ function swipe_left() { // when we swipe left..but no generation.
82468232 }
82478233}
82488234
8249-/**
8250- * Creates a new branch from the message with the given ID
8251- * @param {number} mesId Message ID
8252- * @returns {Promise<string>} Branch file name
8253- */
8254-async function branchChat(mesId) {
8255- if (this_chid === undefined && !selected_group) {
8256- toastr.info('No character selected.', 'Branch creation aborted');
8257- return;
8258- }
8259-
8260- const fileName = await createBranch(mesId);
8261- await saveItemizedPrompts(fileName);
8262-
8263- if (selected_group) {
8264- await openGroupChat(selected_group, fileName);
8265- } else {
8266- await openCharacterChat(fileName);
8267- }
8268-
8269- return fileName;
8270-}
8271-
82728235// when we click swipe right button
82738236const swipe_right = () => {
82748237 if (chat.length - 1 === Number(this_edit_mes_id)) {
@@ -10582,44 +10545,6 @@ jQuery(async function () {
1058210545 await duplicateCharacter();
1058310546 });
1058410547
10585- $(document).on('click', '.select_chat_block, .bookmark_link, .mes_bookmark', async function () {
10586- let file_name = $(this).hasClass('mes_bookmark')
10587- ? $(this).closest('.mes').attr('bookmark_link')
10588- : $(this).attr('file_name').replace('.jsonl', '');
10589-
10590- if (!file_name) {
10591- return;
10592- }
10593-
10594- try {
10595- showLoader();
10596- if (selected_group) {
10597- await openGroupChat(selected_group, file_name);
10598- } else {
10599- await openCharacterChat(file_name);
10600- }
10601- } finally {
10602- hideLoader();
10603- }
10604-
10605- $('#shadow_select_chat_popup').css('display', 'none');
10606- $('#load_select_chat_div').css('display', 'block');
10607- });
10608-
10609- $(document).on('click', '.mes_create_bookmark', async function () {
10610- var selected_mes_id = $(this).closest('.mes').attr('mesid');
10611- if (selected_mes_id !== undefined) {
10612- createNewBookmark(selected_mes_id);
10613- }
10614- });
10615-
10616- $(document).on('click', '.mes_create_branch', async function () {
10617- var selected_mes_id = $(this).closest('.mes').attr('mesid');
10618- if (selected_mes_id !== undefined) {
10619- branchChat(Number(selected_mes_id));
10620- }
10621- });
10622-
1062310548 $(document).on('click', '.mes_stop', function () {
1062410549 stopGeneration();
1062510550 });
public/scripts/bookmarks.js+342 -43
@@ -6,7 +6,6 @@ import {
66 this_chid,
77 openCharacterChat,
88 chat_metadata,
9- callPopup,
109 getRequestHeaders,
1110 getThumbnailUrl,
1211 getCharacters,
@@ -24,19 +23,21 @@ import {
2423 saveGroupBookmarkChat,
2524 selected_group,
2625} from './group-chats.js';
26+import { hideLoader, showLoader } from './loader.js';
27+import { getLastMessageId } from './macros.js';
2728import { Popup } from './popup.js';
29+import { SlashCommand } from './slash-commands/SlashCommand.js';
30+import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
31+import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
32+import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
2833import { createTagMapFromList } from './tags.js';
34+import { renderTemplateAsync } from './templates.js';
2935
3036import {
31- delay,
3237 getUniqueName,
38+ isTrueBoolean,
3339} from './utils.js';
3440
35-export {
36- createNewBookmark,
37- showBookmarksButtons,
38-};
39-
4041const bookmarkNameToken = 'Checkpoint #';
4142
4243async function getExistingChatNames() {
@@ -57,16 +58,13 @@ async function getExistingChatNames() {
5758 }
5859}
5960
60-async function getBookmarkName() {
61+async function getBookmarkName({ isReplace = false, forceName = null } = {}) {
6162 const chatNames = await getExistingChatNames();
62- const popupText = `<h3>Enter the checkpoint name:<h3>
63- <small>Leave empty to auto-generate.</small>`;
64- let name = await callPopup(popupText, 'input');
6563
66- if (name === false) {
64+ const body = await renderTemplateAsync('createCheckpoint', { isReplace: isReplace });
67- return null;
65+ let name = forceName ?? await Popup.show.input('Create Checkpoint', body);
68- }
66+ // Special handling for confirmed empty input (=> auto-generate name)
6967 else if (name === '') {
7068 for (let i = chatNames.length; i < 1000; i++) {
7169 name = bookmarkNameToken + i;
7270 if (!chatNames.includes(name)) {
@@ -74,6 +72,9 @@ async function getBookmarkName() {
7472 }
7573 }
7674 }
75+ if (!name) {
76+ return null;
77+ }
7778
7879 return `${name} - ${humanizedDateTime()}`;
7980}
@@ -96,7 +97,7 @@ function getMainChatName() {
9697 return null;
9798}
9899
99100export function showBookmarksButtons() {
100101 try {
101102 if (selected_group) {
102103 $('#option_convert_to_group').hide();
@@ -131,9 +132,10 @@ async function saveBookmarkMenu() {
131132 return;
132133 }
133134
134135 return await createNewBookmark(chat.length - 1);
135136}
136137
138+// Export is used by Timelines extension. Do not remove.
137139export async function createBranch(mesId) {
138140 if (!chat.length) {
139141 toastr.warning('The chat is empty.', 'Branch creation failed');
@@ -167,20 +169,26 @@ export async function createBranch(mesId) {
167169 return name;
168170}
169171
170-async function createNewBookmark(mesId) {
172+/**
173+ * Creates a new bookmark for a message.
174+ *
175+ * @param {number} mesId - The ID of the message.
176+ * @param {Object} [options={}] - Optional parameters.
177+ * @param {string?} [options.forceName=null] - The name to force for the bookmark.
178+ * @returns {Promise<string?>} - A promise that resolves to the bookmark name when the bookmark is created.
179+ */
180+export async function createNewBookmark(mesId, { forceName = null } = {}) {
171181 if (this_chid === undefined && !selected_group) {
172182 toastr.info('No character selected.', 'Checkpoint creationCreate abortedCheckpoint');
173183 return null;
174184 }
175-
176185 if (!chat.length) {
177186 toastr.warning('The chat is empty.', 'Checkpoint creationCreate failedCheckpoint');
178187 return null;
179188 }
180-
189+ if (!chat[mesId]) {
181- if (mesId < 0 || mesId >= chat.length) {
190+ toastr.warning('Invalid message ID.', 'Create Checkpoint');
182- toastr.warning('Invalid message ID.', 'Checkpoint creation failed');
191+ return null;
183- return;
184192 }
185193
186194 const lastMes = chat[mesId];
@@ -189,19 +197,11 @@ async function createNewBookmark(mesId) {
189197 lastMes.extra = {};
190198 }
191199
192200 ifconst (isReplace = lastMes.extra.bookmark_link) {;
193- const confirm = await callPopup('Checkpoint for the last message already exists. Would you like to replace it?', 'confirm');
194-
195- if (!confirm) {
196- return;
197- }
198- }
199-
200- await delay(250);
201- let name = await getBookmarkName();
202201
202+ let name = await getBookmarkName({ isReplace: isReplace, forceName: forceName });
203203 if (!name) {
204204 return null;
205205 }
206206
207207 const mainChat = selected_group ? groups?.find(x => x.id == selected_group)?.chat_id : characters[this_chid].chat;
@@ -215,10 +215,25 @@ async function createNewBookmark(mesId) {
215215 }
216216
217217 lastMes.extra['bookmark_link'] = name;
218- $(`.mes[mesid="${mesId}"]`).attr('bookmark_link', name);
218+
219+ const mes = $(`.mes[mesid="${mesId}"]`);
220+ updateBookmarkDisplay(mes, name);
219221
220222 await saveChatConditional();
221223 toastr.success('Click the flag icon innext theto lastthe message to open the checkpoint chat.', 'CheckpointCreate createdCheckpoint', { timeOut: 10000 });
224+ return name;
225+}
226+
227+
228+/**
229+ * Updates the display of the bookmark on a chat message.
230+ * @param {JQuery<HTMLElement>} mes - The message element
231+ * @param {string?} [newBookmarkLink=null] - The new bookmark link (optional)
232+ */
233+export function updateBookmarkDisplay(mes, newBookmarkLink = null) {
234+ newBookmarkLink && mes.attr('bookmark_link', newBookmarkLink);
235+ const bookmarkFlag = mes.find('.mes_bookmark');
236+ bookmarkFlag.attr('title', `Checkpoint\n${mes.attr('bookmark_link')}\n\n${bookmarkFlag.data('tooltip')}`);
222237}
223238
224239async function backToMainChat() {
@@ -231,10 +246,13 @@ async function backToMainChat() {
231246 } else {
232247 await openCharacterChat(mainChatName);
233248 }
249+ return mainChatName;
234250 }
251+
252+ return null;
235253}
236254
237255export async function convertSoloToGroupChat() {
238256 if (selected_group) {
239257 console.log('Already in group. No need for conversion');
240258 return;
@@ -261,6 +279,7 @@ async function convertSoloToGroupChat() {
261279 const activationStrategy = group_activation_strategy.NATURAL;
262280 const allowSelfResponses = false;
263281 const favChecked = character.fav || character.fav == 'true';
282+ /** @type {any} */
264283 const metadata = Object.assign({}, chat_metadata);
265284 delete metadata.main_chat;
266285
@@ -351,8 +370,288 @@ async function convertSoloToGroupChat() {
351370 toastr.success('The chat has been successfully converted!');
352371}
353372
354-jQuery(function () {
373+/**
374+ * Creates a new branch from the message with the given ID
375+ * @param {number} mesId Message ID
376+ * @returns {Promise<string?>} Branch file name
377+ */
378+export async function branchChat(mesId) {
379+ if (this_chid === undefined && !selected_group) {
380+ toastr.info('No character selected.', 'Create Branch');
381+ return null;
382+ }
383+
384+ const fileName = await createBranch(mesId);
385+ await saveItemizedPrompts(fileName);
386+
387+ if (selected_group) {
388+ await openGroupChat(selected_group, fileName);
389+ } else {
390+ await openCharacterChat(fileName);
391+ }
392+
393+ return fileName;
394+}
395+
396+function registerBookmarksSlashCommands() {
397+ /**
398+ * Validates a message ID. (Is a number, exists as a message)
399+ *
400+ * @param {number} mesId - The message ID to validate.
401+ * @param {string} context - The context of the slash command. Will be used as the title of any toasts.
402+ * @returns {boolean} - Returns true if the message ID is valid, otherwise false.
403+ */
404+ function validateMessageId(mesId, context) {
405+ if (isNaN(mesId)) {
406+ toastr.warning('Invalid message ID was provided', context);
407+ return false;
408+ }
409+ if (!chat[mesId]) {
410+ toastr.warning(`Message for id ${mesId} not found`, context);
411+ return false;
412+ }
413+ return true;
414+ }
415+
416+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
417+ name: 'branch-create',
418+ returns: 'Name of the new branch',
419+ callback: async (args, text) => {
420+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
421+ if (!validateMessageId(mesId, 'Create Branch')) return '';
422+
423+ const branchName = await branchChat(mesId);
424+ return branchName ?? '';
425+ },
426+ unnamedArgumentList: [
427+ SlashCommandArgument.fromProps({
428+ description: 'Message ID',
429+ typeList: [ARGUMENT_TYPE.NUMBER],
430+ enumProvider: commonEnumProviders.messages(),
431+ }),
432+ ],
433+ helpString: `
434+ <div>
435+ Create a new branch from the selected message. If no message id is provided, will use the last message.
436+ </div>
437+ <div>
438+ Creating a branch will automatically choose a name for the branch.<br />
439+ After creating the branch, the branch chat will be automatically opened.
440+ </div>
441+ <div>
442+ Use Checkpoints and <code>/checkpoint-create</code> instead if you do not want to jump to the new chat.
443+ </div>`,
444+ }));
445+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
446+ name: 'checkpoint-create',
447+ returns: 'Name of the new checkpoint',
448+ callback: async (args, text) => {
449+ const mesId = Number(args.mesId ?? getLastMessageId());
450+ if (!validateMessageId(mesId, 'Create Checkpoint')) return '';
451+
452+ if (typeof text !== 'string') {
453+ toastr.warning('Checkpoint name must be a string or empty', 'Create Checkpoint');
454+ return '';
455+ }
456+
457+ const checkPointName = await createNewBookmark(mesId, { forceName: text });
458+ return checkPointName ?? '';
459+ },
460+ namedArgumentList: [
461+ SlashCommandNamedArgument.fromProps({
462+ name: 'mesId',
463+ description: 'Message ID',
464+ typeList: [ARGUMENT_TYPE.NUMBER],
465+ enumProvider: commonEnumProviders.messages(),
466+ }),
467+ ],
468+ unnamedArgumentList: [
469+ SlashCommandArgument.fromProps({
470+ description: 'Checkpoint name',
471+ typeList: [ARGUMENT_TYPE.STRING],
472+ }),
473+ ],
474+ helpString: `
475+ <div>
476+ Create a new checkpoint for the selected message with the provided name. If no message id is provided, will use the last message.<br />
477+ Leave the checkpoint name empty to auto-generate one.
478+ </div>
479+ <div>
480+ A created checkpoint will be permanently linked with the message.<br />
481+ If a checkpoint already exists, the link to it will be overwritten.<br />
482+ After creating the checkpoint, the checkpoint chat can be opened with the checkpoint flag,
483+ using the <code>/go</code> command with the checkpoint name or the <code>/checkpoint-go</code> command on the message.
484+ </div>
485+ <div>
486+ Use Branches and <code>/branch-create</code> instead if you do want to jump to the new chat.
487+ </div>
488+ <div>
489+ <strong>Example:</strong>
490+ <ul>
491+ <li>
492+ <pre><code>/checkpoint-create mes={{lastCharMessage}} Checkpoint for char reply | /setvar key=rememberCheckpoint {{pipe}}</code></pre>
493+ Will create a new checkpoint to the latest message of the current character, and save it as a local variable for future use.
494+ </li>
495+ </ul>
496+ </div>`,
497+ }));
498+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
499+ name: 'checkpoint-go',
500+ returns: 'Name of the checkpoint',
501+ callback: async (args, text) => {
502+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
503+ if (!validateMessageId(mesId, 'Open Checkpoint')) return '';
504+
505+ const checkPointName = chat[mesId].extra?.bookmark_link;
506+ if (!checkPointName) {
507+ toastr.warning('No checkpoint is linked to the selected message', 'Open Checkpoint');
508+ return '';
509+ }
510+
511+ if (selected_group) {
512+ await openGroupChat(selected_group, checkPointName);
513+ } else {
514+ await openCharacterChat(checkPointName);
515+ }
516+
517+ return checkPointName;
518+ },
519+ unnamedArgumentList: [
520+ SlashCommandArgument.fromProps({
521+ description: 'Message ID',
522+ typeList: [ARGUMENT_TYPE.NUMBER],
523+ enumProvider: commonEnumProviders.messages(),
524+ }),
525+ ],
526+ helpString: `
527+ <div>
528+ Open the checkpoint linked to the selected message. If no message id is provided, will use the last message.
529+ </div>
530+ <div>
531+ Use <code>/checkpoint-get</code> if you want to make sure that the selected message has a checkpoint.
532+ </div>`,
533+ }));
534+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
535+ name: 'checkpoint-exit',
536+ returns: 'The name of the chat exited to. Returns an empty string if not in a checkpoint chat.',
537+ callback: async () => {
538+ const mainChat = await backToMainChat();
539+ return mainChat ?? '';
540+ },
541+ helpString: 'Exit the checkpoint chat.<br />If not in a checkpoint chat, returns empty string.',
542+ }));
543+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
544+ name: 'checkpoint-parent',
545+ returns: 'Name of the parent chat for this checkpoint',
546+ callback: async () => {
547+ const mainChatName = getMainChatName();
548+ return mainChatName ?? '';
549+ },
550+ helpString: 'Get the name of the parent chat for this checkpoint.<br />If not in a checkpoint chat, returns empty string.',
551+ }));
552+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
553+ name: 'checkpoint-get',
554+ returns: 'Name of the chat',
555+ callback: async (args, text) => {
556+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
557+ if (!validateMessageId(mesId, 'Get Checkpoint')) return '';
558+
559+ const checkPointName = chat[mesId].extra?.bookmark_link;
560+ return checkPointName ?? '';
561+ },
562+ unnamedArgumentList: [
563+ SlashCommandArgument.fromProps({
564+ description: 'Message ID',
565+ typeList: [ARGUMENT_TYPE.NUMBER],
566+ enumProvider: commonEnumProviders.messages(),
567+ }),
568+ ],
569+ helpString: `
570+ <div>
571+ Get the name of the checkpoint linked to the selected message. If no message id is provided, will use the last message.<br />
572+ If no checkpoint is linked, the result will be empty.
573+ </div>`,
574+ }));
575+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
576+ name: 'checkpoint-list',
577+ returns: 'JSON array of all existing checkpoints in this chat, as an array',
578+ /** @param {{links?: string}} args @returns {Promise<string>} */
579+ callback: async (args, _) => {
580+ const result = Object.entries(chat)
581+ .filter(([_, message]) => message.extra?.bookmark_link)
582+ .map(([mesId, message]) => isTrueBoolean(args.links) ? message.extra.bookmark_link : Number(mesId));
583+ return JSON.stringify(result);
584+ },
585+ namedArgumentList: [
586+ SlashCommandNamedArgument.fromProps({
587+ name: 'links',
588+ description: 'Get a list of all links / chat names of the checkpoints, instead of the message ids',
589+ typeList: [ARGUMENT_TYPE.BOOLEAN],
590+ enumList: commonEnumProviders.boolean('trueFalse')(),
591+ defaultValue: 'false',
592+ }),
593+ ],
594+ helpString: `
595+ <div>
596+ List all existing checkpoints in this chat.
597+ </div>
598+ <div>
599+ 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 />
600+ The value will be a JSON array.
601+ </div>`,
602+ }));
603+}
604+
605+export function initBookmarks() {
355606 $('#option_new_bookmark').on('click', saveBookmarkMenu);
356607 $('#option_back_to_main').on('click', backToMainChat);
357608 $('#option_convert_to_group').on('click', convertSoloToGroupChat);
358-});
609+
610+ $(document).on('click', '.select_chat_block, .mes_bookmark', async function (e) {
611+ // If shift is held down, we are not following the bookmark, but creating a new one
612+ const mes = $(this).closest('.mes');
613+ if (e.shiftKey && mes.length) {
614+ const selectedMesId = mes.attr('mesid');
615+ await createNewBookmark(Number(selectedMesId));
616+ return;
617+ }
618+
619+ const fileName = $(this).hasClass('mes_bookmark')
620+ ? $(this).closest('.mes').attr('bookmark_link')
621+ : $(this).attr('file_name').replace('.jsonl', '');
622+
623+ if (!fileName) {
624+ return;
625+ }
626+
627+ try {
628+ showLoader();
629+ if (selected_group) {
630+ await openGroupChat(selected_group, fileName);
631+ } else {
632+ await openCharacterChat(fileName);
633+ }
634+ } finally {
635+ await hideLoader();
636+ }
637+
638+ $('#shadow_select_chat_popup').css('display', 'none');
639+ $('#load_select_chat_div').css('display', 'block');
640+ });
641+
642+ $(document).on('click', '.mes_create_bookmark', async function () {
643+ const mesId = $(this).closest('.mes').attr('mesid');
644+ if (mesId !== undefined) {
645+ await createNewBookmark(Number(mesId));
646+ }
647+ });
648+
649+ $(document).on('click', '.mes_create_branch', async function () {
650+ const mesId = $(this).closest('.mes').attr('mesid');
651+ if (mesId !== undefined) {
652+ await branchChat(Number(mesId));
653+ }
654+ });
655+
656+ registerBookmarksSlashCommands();
657+}
public/scripts/extensions/caption/index.js+4 -4
@@ -358,10 +358,10 @@ function onRefineModeInput() {
358358 */
359359async function captionCommandCallback(args, prompt) {
360360 const quiet = isTrueBoolean(args?.quiet);
361361 const idmesId = args?.mesId ?? args?.id;
362362
363363 if (!isNaN(Number(idmesId))) {
364364 const message = getContext().chat[idmesId];
365365 if (message?.extra?.image) {
366366 try {
367367 const fetchResult = await fetch(message.extra.image);
@@ -546,7 +546,7 @@ jQuery(async function () {
546546 'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
547547 ),
548548 SlashCommandNamedArgument.fromProps({
549549 name: 'idmesId',
550550 description: 'get image from a message with this ID',
551551 typeList: [ARGUMENT_TYPE.NUMBER],
552552 enumProvider: commonEnumProviders.messages(),
public/scripts/extensions/translate/index.js+14 -3
@@ -598,9 +598,20 @@ jQuery(async () => {
598598 $(document).on('click', '.mes_translate', onMessageTranslateClick);
599599 $('#translate_key_button').on('click', async () => {
600600 const optionText = $('#translation_provider option:selected').text();
601601 const key = await callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT);, '', {
602+ customButtons: [{
603+ text: 'Remove Key',
604+ appendAtEnd: true,
605+ result: POPUP_RESULT.NEGATIVE,
606+ action: async () => {
607+ await writeSecret(extension_settings.translate.provider, '');
608+ toastr.success('API Key removed');
609+ $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
610+ },
611+ }],
612+ });
602613
603614 if (!key == false) {
604615 return;
605616 }
606617
@@ -634,7 +645,7 @@ jQuery(async () => {
634645 }],
635646 });
636647
637- if (url == false || url == '') {
648+ if (!url) {
638649 return;
639650 }
640651
public/scripts/extensions/tts/azure.js+2 -2
@@ -77,14 +77,14 @@ class AzureTtsProvider {
7777 result: POPUP_RESULT.NEGATIVE,
7878 action: async () => {
7979 await writeSecret(SECRET_KEYS.AZURE_TTS, '');
8080 $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
8181 toastr.success('API Key removed');
8282 await this.onRefreshClick();
8383 },
8484 }],
8585 });
8686
87- if (key == false || key == '') {
87+ if (!key) {
8888 return;
8989 }
9090
public/scripts/extensions/tts/openai-compatible.js+2 -2
@@ -86,14 +86,14 @@ class OpenAICompatibleTtsProvider {
8686 result: POPUP_RESULT.NEGATIVE,
8787 action: async () => {
8888 await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, '');
8989 $('#openai_compatible_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
9090 toastr.success('API Key removed');
9191 await this.onRefreshClick();
9292 },
9393 }],
9494 });
9595
96- if (key == false || key == '') {
96+ if (!key) {
9797 return;
9898 }
9999
public/scripts/popup.js+3 -0
@@ -83,6 +83,9 @@ const showPopupHelper = {
8383 const content = PopupUtils.BuildTextWithHeader(header, text);
8484 const popup = new Popup(content, POPUP_TYPE.INPUT, defaultValue, popupOptions);
8585 const value = await popup.show();
86+ // Return values: If empty string, we explicitly handle that as returning that empty string as "success" provided.
87+ // Otherwise, all non-truthy values (false, null, undefined) are treated as "cancel" and return null.
88+ if (value === '') return '';
8689 return value ? String(value) : null;
8790 },
8891
public/scripts/templates/createCheckpoint.html+8 -0
@@ -0,0 +1,8 @@
1+<div>
2+ <span class="margin-right-10px">Enter Checkpoint Name:</span><small>(Leave empty to auto-generate)</small>
3+</div>
4+{{#if isReplace}}
5+<div class="m-t-1">
6+ <small>The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.</small>
7+</div>
8+{{/if}}
public/style.css+4 -0
@@ -3964,6 +3964,10 @@ input[type="range"]::-webkit-slider-thumb {
39643964 display: inline-block;
39653965}
39663966
3967+.mes:not([bookmark_link='']) .mes_create_bookmark {
3968+ display: none;
3969+}
3970+
39673971.mes_edit_buttons {
39683972 display: none;
39693973 flex-direction: row;