Add slash commands for checkpoints and branch

22d2b7d2a2f0f918d2c9de79739395f3b8335e30

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +279 -24Showing whitespace changes
public/script.js+2 -0
@@ -117,6 +117,7 @@ import {
117117} from './scripts/nai-settings.js';
118118
119119import {
120+ initBookmarks,
120121 showBookmarksButtons,
121122 updateBookmarkDisplay,
122123} from './scripts/bookmarks.js';
@@ -936,6 +937,7 @@ async function firstLoadInit() {
936937 initDynamicStyles();
937938 initTags();
938939 initOpenai();
940+ initBookmarks();
939941 await getUserAvatars(true, user_avatar);
940942 await getCharacters();
941943 await getBackgrounds();
public/scripts/bookmarks.js+277 -24
@@ -24,7 +24,12 @@ import {
2424 selected_group,
2525} from './group-chats.js';
2626import { 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';
2934
3035import {
@@ -51,10 +56,10 @@ async function getExistingChatNames() {
5156 }
5257}
5358
5459async function getBookmarkName({ forceName = null } = {}) {
5560 const chatNames = await getExistingChatNames();
5661
5762 let name = forceName || await Popup.show.input('Create Checkpoint', '<span class="margin-right-10px">Enter Checkpoint Name:</span><small>(Leave empty to auto-generate)</small>');
5863 if (name === null) {
5964 return null;
6065 }
@@ -159,20 +164,26 @@ async function createBranch(mesId) {
159164 return name;
160165}
161166
162-export async function createNewBookmark(mesId) {
167+/**
168+ * Creates a new bookmark for a message.
169+ *
170+ * @param {number} mesId - The ID of the message.
171+ * @param {Object} [options={}] - Optional parameters.
172+ * @param {string?} [options.forceName=null] - The name to force for the bookmark.
173+ * @returns {Promise<string?>} - A promise that resolves to the bookmark name when the bookmark is created.
174+ */
175+export async function createNewBookmark(mesId, { forceName = null } = {}) {
163176 if (this_chid === undefined && !selected_group) {
164177 toastr.info('No character selected.', 'Checkpoint creationCreate abortedCheckpoint');
165178 return null;
166179 }
167-
168180 if (!chat.length) {
169181 toastr.warning('The chat is empty.', 'Checkpoint creationCreate failedCheckpoint');
170182 return null;
171183 }
172-
184+ if (!chat[mesId]) {
173- if (mesId < 0 || mesId >= chat.length) {
185+ toastr.warning('Invalid message ID.', 'Create Checkpoint');
174- toastr.warning('Invalid message ID.', 'Checkpoint creation failed');
186+ return null;
175- return;
176187 }
177188
178189 const lastMes = chat[mesId];
@@ -181,16 +192,16 @@ export async function createNewBookmark(mesId) {
181192 lastMes.extra = {};
182193 }
183194
184195 if (lastMes.extra.bookmark_link && !forceName) {
185196 const confirm = await Popup.show.confirm('Replace Checkpoint', 'Checkpoint for the last message already exists.<br />Would you like to replace it?');
186197 if (!confirm) {
187198 return null;
188199 }
189200 }
190201
191202 let name = await getBookmarkName({ forceName: forceName });
192203 if (!name) {
193204 return null;
194205 }
195206
196207 const mainChat = selected_group ? groups?.find(x => x.id == selected_group)?.chat_id : characters[this_chid].chat;
@@ -209,7 +220,8 @@ export async function createNewBookmark(mesId) {
209220 updateBookmarkDisplay(mes, name);
210221
211222 await saveChatConditional();
212223 toastr.success('Click the flag icon next to the message to open the checkpoint chat.', 'CheckpointCreate createdCheckpoint', { timeOut: 10000 });
224+ return name;
213225}
214226
215227
@@ -233,7 +245,10 @@ async function backToMainChat() {
233245 } else {
234246 await openCharacterChat(mainChatName);
235247 }
248+ return mainChatName;
236249 }
250+
251+ return null;
237252}
238253
239254export async function convertSoloToGroupChat() {
@@ -357,12 +372,12 @@ export async function convertSoloToGroupChat() {
357372/**
358373 * Creates a new branch from the message with the given ID
359374 * @param {number} mesId Message ID
360375 * @returns {Promise<string?>} Branch file name
361376 */
362377export async function branchChat(mesId) {
363378 if (this_chid === undefined && !selected_group) {
364379 toastr.info('No character selected.', 'Branch creationCreate abortedBranch');
365380 return null;
366381 }
367382
368383 const fileName = await createBranch(mesId);
@@ -377,7 +392,243 @@ export async function branchChat(mesId) {
377392 return fileName;
378393}
379394
380395jQuery(function registerBookmarksSlashCommands() {
396+ /**
397+ * Validates a message ID. (Is a number, exists as a message)
398+ *
399+ * @param {number} mesId - The message ID to validate.
400+ * @param {string} context - The context of the slash command. Will be used as the title of any toasts.
401+ * @returns {boolean} - Returns true if the message ID is valid, otherwise false.
402+ */
403+ function validateMessageId(mesId, context) {
404+ if (isNaN(mesId)) {
405+ toastr.warning('Invalid message ID was provided', context);
406+ return false;
407+ }
408+ if (!chat[mesId]) {
409+ toastr.warning(`Message for id ${mesId} not found`, context);
410+ return false;
411+ }
412+ return true;
413+ }
414+
415+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
416+ name: 'branch-create',
417+ returns: 'Name of the new branch',
418+ callback: async (args, text) => {
419+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
420+ if (!validateMessageId(mesId, 'Create Branch')) return '';
421+
422+ const branchName = await branchChat(mesId);
423+ return branchName ?? '';
424+ },
425+ namedArgumentList: [
426+ SlashCommandNamedArgument.fromProps({
427+ name: 'mes',
428+ description: 'Message ID',
429+ typeList: [ARGUMENT_TYPE.NUMBER],
430+ enumProvider: commonEnumProviders.messages(),
431+ }),
432+ ],
433+ unnamedArgumentList: [
434+ SlashCommandArgument.fromProps({
435+ description: 'Message ID',
436+ typeList: [ARGUMENT_TYPE.NUMBER],
437+ enumProvider: commonEnumProviders.messages(),
438+ }),
439+ ],
440+ helpString: `
441+ <div>
442+ Create a new branch from the selected message. If no message id is provided, will use the last message.
443+ </div>
444+ <div>
445+ Creating a branch will automatically choose a name for the branch.<br />
446+ After creating the branch, the branch chat will be automatically opened.
447+ </div>
448+ <div>
449+ Use Checkpoints and <code>/checkpoint-create</code> instead if you do not want to jump to the new chat.
450+ </div>`,
451+ }));
452+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
453+ name: 'checkpoint-create',
454+ returns: 'Name of the new checkpoint',
455+ callback: async (args, text) => {
456+ const mesId = Number(args.mesId ?? getLastMessageId());
457+ if (!validateMessageId(mesId, 'Create Checkpoint')) return '';
458+
459+ if (!text || typeof text !== 'string') {
460+ toastr.warning('Checkpoint name must be provided', 'Create Checkpoint');
461+ return '';
462+ }
463+
464+ const checkPointName = await createNewBookmark(mesId, { forceName: text });
465+ return checkPointName ?? '';
466+ },
467+ namedArgumentList: [
468+ SlashCommandNamedArgument.fromProps({
469+ name: 'mes',
470+ description: 'Message ID',
471+ typeList: [ARGUMENT_TYPE.NUMBER],
472+ enumProvider: commonEnumProviders.messages(),
473+ }),
474+ ],
475+ unnamedArgumentList: [
476+ SlashCommandArgument.fromProps({
477+ description: 'Checkpoint name',
478+ typeList: [ARGUMENT_TYPE.STRING],
479+ isRequired: true,
480+ }),
481+ ],
482+ helpString: `
483+ <div>
484+ Create a new checkpoint for the selected message with the provided name. If no message id is provided, will use the last message.
485+ </div>
486+ <div>
487+ A created checkpoint will be permanently linked with the message.<br />
488+ If a checkpoint already exists, the link to it will be overwritten.<br />
489+ After creating the checkpoint, the checkpoint chat can be opened with the checkpoint flag,
490+ using the <code>/go</code> command with the checkpoint name or the <code>/checkpoint-go</code> command on the message.
491+ </div>
492+ <div>
493+ Use Branches and <code>/branch-create</code> instead if you do want to jump to the new chat.
494+ </div>
495+ <div>
496+ <strong>Example:</strong>
497+ <ul>
498+ <li>
499+ <pre><code>/checkpoint-create mes={{lastCharMessage}} Checkpoint for char reply | /setvar key=rememberCheckpoint {{pipe}}</code></pre>
500+ Will create a new checkpoint to the latest message of the current character, and save it as a local variable for future use.
501+ </li>
502+ </ul>
503+ </div>`,
504+ }));
505+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
506+ name: 'checkpoint-go',
507+ returns: 'Name of the checkpoint',
508+ callback: async (args, text) => {
509+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
510+ if (!validateMessageId(mesId, 'Open Checkpoint')) return '';
511+
512+ const checkPointName = chat[mesId].extra?.bookmark_link;
513+ if (!checkPointName) {
514+ toastr.warning('No checkpoint is linked to the selected message', 'Open Checkpoint');
515+ return '';
516+ }
517+
518+ if (selected_group) {
519+ await openGroupChat(selected_group, checkPointName);
520+ } else {
521+ await openCharacterChat(checkPointName);
522+ }
523+
524+ return checkPointName;
525+ },
526+ namedArgumentList: [
527+ SlashCommandNamedArgument.fromProps({
528+ name: 'mes',
529+ description: 'Message ID',
530+ typeList: [ARGUMENT_TYPE.NUMBER],
531+ enumProvider: commonEnumProviders.messages(),
532+ }),
533+ ],
534+ unnamedArgumentList: [
535+ SlashCommandArgument.fromProps({
536+ description: 'Message ID',
537+ typeList: [ARGUMENT_TYPE.NUMBER],
538+ enumProvider: commonEnumProviders.messages(),
539+ }),
540+ ],
541+ helpString: `
542+ <div>
543+ Open the checkpoint linked to the selected message. If no message id is provided, will use the last message.
544+ </div>
545+ <div>
546+ Use <code>/checkpoint-get</code> if you want to make sure that the selected message has a checkpoint.
547+ </div>`,
548+ }));
549+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
550+ name: 'checkpoint-exit',
551+ returns: 'The name of the chat exited to. Returns null if not in a checkpoint chat.',
552+ callback: async () => {
553+ const mainChat = await backToMainChat();
554+ return mainChat ?? '';
555+ },
556+ helpString: 'Exit the checkpoint chat.<br />If not in a checkpoint chat, returns null.',
557+ }));
558+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
559+ name: 'checkpoint-parent',
560+ returns: 'Name of the parent chat for this checkpoint',
561+ callback: async () => {
562+ const mainChatName = getMainChatName();
563+ return mainChatName ?? '';
564+ },
565+ helpString: 'Get the name of the parent chat for this checkpoint. If not in a checkpoint chat, returns null.',
566+ }))
567+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
568+ name: 'checkpoint-get',
569+ returns: 'Name of the chat',
570+ callback: async (args, text) => {
571+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
572+ if (!validateMessageId(mesId, 'Get Checkpoint')) return '';
573+
574+ const checkPointName = chat[mesId].extra?.bookmark_link;
575+ return checkPointName ?? '';
576+ },
577+ namedArgumentList: [
578+ SlashCommandNamedArgument.fromProps({
579+ name: 'mes',
580+ description: 'Message ID',
581+ typeList: [ARGUMENT_TYPE.NUMBER],
582+ enumProvider: commonEnumProviders.messages(),
583+ }),
584+ ],
585+ unnamedArgumentList: [
586+ SlashCommandArgument.fromProps({
587+ description: 'Message ID',
588+ typeList: [ARGUMENT_TYPE.NUMBER],
589+ enumProvider: commonEnumProviders.messages(),
590+ }),
591+ ],
592+ helpString: `
593+ <div>
594+ Get the name of the checkpoint linked to the selected message. If no message id is provided, will use the last message.<br />
595+ If no checkpoint is linked, the result will be empty.
596+ </div>`,
597+ }));
598+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
599+ name: 'checkpoint-list',
600+ returns: 'JSON array of all existing checkpoints in this chat, as an array',
601+ /** @param {{links?: string}} args @returns {Promise<string>} */
602+ callback: async (args, _) => {
603+ const result = [];
604+ for (const mesId in chat) {
605+ if (chat[mesId].extra?.bookmark_link) {
606+ result.push(args.links ? chat[mesId].extra.bookmark_link : Number(mesId));
607+ }
608+ }
609+ return JSON.stringify(result);
610+ },
611+ namedArgumentList: [
612+ SlashCommandNamedArgument.fromProps({
613+ name: 'links',
614+ description: 'Get a list of all links / chat names of the checkpoints, instead of the message ids',
615+ typeList: [ARGUMENT_TYPE.BOOLEAN],
616+ enumList: commonEnumProviders.boolean('trueFalse')(),
617+ defaultValue: 'false',
618+ }),
619+ ],
620+ helpString: `
621+ <div>
622+ List all existing checkpoints in this chat.
623+ </div>
624+ <div>
625+ 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 />
626+ The value will be a JSON array.
627+ </div>`,
628+ }));
629+}
630+
631+export function initBookmarks() {
381632 $('#option_new_bookmark').on('click', saveBookmarkMenu);
382633 $('#option_back_to_main').on('click', backToMainChat);
383634 $('#option_convert_to_group').on('click', convertSoloToGroupChat);
@@ -386,7 +637,7 @@ jQuery(function () {
386637 // If shift is held down, we are not following the bookmark, but creating a new one
387638 if (e.shiftKey) {
388639 var selectedMesId = $(this).closest('.mes').attr('mesid');
389640 await createNewBookmark(Number(selectedMesId));
390641 return;
391642 }
392643
@@ -416,7 +667,7 @@ jQuery(function () {
416667 $(document).on('click', '.mes_create_bookmark', async function () {
417668 var selected_mes_id = $(this).closest('.mes').attr('mesid');
418669 if (selected_mes_id !== undefined) {
419670 await createNewBookmark(Number(selected_mes_id));
420671 }
421672 });
422673
@@ -426,4 +677,6 @@ jQuery(function () {
426677 await branchChat(Number(selected_mes_id));
427678 }
428679 });
429-});
680+
681+ registerBookmarksSlashCommands();
682+}