Add slash commands for checkpoints and branch

22d2b7d2a2f0f918d2c9de79739395f3b8335e30

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +279 -24Ignore whitespace
public/script.js+2 -0
@@ -117,6 +117,7 @@ import {
117} from './scripts/nai-settings.js';117} from './scripts/nai-settings.js';
118118
119import {119import {
120 initBookmarks,
120 showBookmarksButtons,121 showBookmarksButtons,
121 updateBookmarkDisplay,122 updateBookmarkDisplay,
122} from './scripts/bookmarks.js';123} from './scripts/bookmarks.js';
@@ -936,6 +937,7 @@ async function firstLoadInit() {
936 initDynamicStyles();937 initDynamicStyles();
937 initTags();938 initTags();
938 initOpenai();939 initOpenai();
940 initBookmarks();
939 await getUserAvatars(true, user_avatar);941 await getUserAvatars(true, user_avatar);
940 await getCharacters();942 await getCharacters();
941 await getBackgrounds();943 await getBackgrounds();
public/scripts/bookmarks.js+277 -24
@@ -24,7 +24,12 @@ import {
24 selected_group,24 selected_group,
25} from './group-chats.js';25} from './group-chats.js';
26import { hideLoader, showLoader } from './loader.js';26import { hideLoader, showLoader } from './loader.js';
27import { getLastMessageId } from './macros.js';
27import { Popup } from './popup.js';28import { Popup } from './popup.js';
29import { SlashCommand } from './slash-commands/SlashCommand.js';
30import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
31import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
32import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
28import { createTagMapFromList } from './tags.js';33import { createTagMapFromList } from './tags.js';
2934
30import {35import {
@@ -51,10 +56,10 @@ async function getExistingChatNames() {
51 }56 }
52}57}
5358
54async function getBookmarkName() {59async function getBookmarkName({ forceName = null } = {}) {
55 const chatNames = await getExistingChatNames();60 const chatNames = await getExistingChatNames();
5661
57 let name = await Popup.show.input('Create Checkpoint', '<span class="margin-right-10px">Enter Checkpoint Name:</span><small>(Leave empty to auto-generate)</small>');62 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>');
58 if (name === null) {63 if (name === null) {
59 return null;64 return null;
60 }65 }
@@ -159,20 +164,26 @@ async function createBranch(mesId) {
159 return name;164 return name;
160}165}
161166
162export 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 */
175export async function createNewBookmark(mesId, { forceName = null } = {}) {
163 if (this_chid === undefined && !selected_group) {176 if (this_chid === undefined && !selected_group) {
164 toastr.info('No character selected.', 'Checkpoint creation aborted');177 toastr.info('No character selected.', 'Create Checkpoint');
165 return;178 return null;
166 }179 }
167
168 if (!chat.length) {180 if (!chat.length) {
169 toastr.warning('The chat is empty.', 'Checkpoint creation failed');181 toastr.warning('The chat is empty.', 'Create Checkpoint');
170 return;182 return null;
171 }183 }
172184 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;
176 }187 }
177188
178 const lastMes = chat[mesId];189 const lastMes = chat[mesId];
@@ -181,16 +192,16 @@ export async function createNewBookmark(mesId) {
181 lastMes.extra = {};192 lastMes.extra = {};
182 }193 }
183194
184 if (lastMes.extra.bookmark_link) {195 if (lastMes.extra.bookmark_link && !forceName) {
185 const confirm = await Popup.show.confirm('Replace Checkpoint', 'Checkpoint for the last message already exists.<br />Would you like to replace it?');196 const confirm = await Popup.show.confirm('Replace Checkpoint', 'Checkpoint for the last message already exists.<br />Would you like to replace it?');
186 if (!confirm) {197 if (!confirm) {
187 return;198 return null;
188 }199 }
189 }200 }
190201
191 let name = await getBookmarkName();202 let name = await getBookmarkName({ forceName: forceName });
192 if (!name) {203 if (!name) {
193 return;204 return null;
194 }205 }
195206
196 const mainChat = selected_group ? groups?.find(x => x.id == selected_group)?.chat_id : characters[this_chid].chat;207 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) {
209 updateBookmarkDisplay(mes, name);220 updateBookmarkDisplay(mes, name);
210221
211 await saveChatConditional();222 await saveChatConditional();
212 toastr.success('Click the flag icon next to the message to open the checkpoint chat.', 'Checkpoint created', { timeOut: 10000 });223 toastr.success('Click the flag icon next to the message to open the checkpoint chat.', 'Create Checkpoint', { timeOut: 10000 });
224 return name;
213}225}
214226
215227
@@ -233,7 +245,10 @@ async function backToMainChat() {
233 } else {245 } else {
234 await openCharacterChat(mainChatName);246 await openCharacterChat(mainChatName);
235 }247 }
248 return mainChatName;
236 }249 }
250
251 return null;
237}252}
238253
239export async function convertSoloToGroupChat() {254export async function convertSoloToGroupChat() {
@@ -357,12 +372,12 @@ export async function convertSoloToGroupChat() {
357/**372/**
358 * Creates a new branch from the message with the given ID373 * Creates a new branch from the message with the given ID
359 * @param {number} mesId Message ID374 * @param {number} mesId Message ID
360 * @returns {Promise<string>} Branch file name375 * @returns {Promise<string?>} Branch file name
361 */376 */
362export async function branchChat(mesId) {377export async function branchChat(mesId) {
363 if (this_chid === undefined && !selected_group) {378 if (this_chid === undefined && !selected_group) {
364 toastr.info('No character selected.', 'Branch creation aborted');379 toastr.info('No character selected.', 'Create Branch');
365 return;380 return null;
366 }381 }
367382
368 const fileName = await createBranch(mesId);383 const fileName = await createBranch(mesId);
@@ -377,7 +392,243 @@ export async function branchChat(mesId) {
377 return fileName;392 return fileName;
378}393}
379394
380jQuery(function () {395function 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
631export function initBookmarks() {
381 $('#option_new_bookmark').on('click', saveBookmarkMenu);632 $('#option_new_bookmark').on('click', saveBookmarkMenu);
382 $('#option_back_to_main').on('click', backToMainChat);633 $('#option_back_to_main').on('click', backToMainChat);
383 $('#option_convert_to_group').on('click', convertSoloToGroupChat);634 $('#option_convert_to_group').on('click', convertSoloToGroupChat);
@@ -386,7 +637,7 @@ jQuery(function () {
386 // If shift is held down, we are not following the bookmark, but creating a new one637 // If shift is held down, we are not following the bookmark, but creating a new one
387 if (e.shiftKey) {638 if (e.shiftKey) {
388 var selectedMesId = $(this).closest('.mes').attr('mesid');639 var selectedMesId = $(this).closest('.mes').attr('mesid');
389 await createNewBookmark(selectedMesId);640 await createNewBookmark(Number(selectedMesId));
390 return;641 return;
391 }642 }
392643
@@ -416,7 +667,7 @@ jQuery(function () {
416 $(document).on('click', '.mes_create_bookmark', async function () {667 $(document).on('click', '.mes_create_bookmark', async function () {
417 var selected_mes_id = $(this).closest('.mes').attr('mesid');668 var selected_mes_id = $(this).closest('.mes').attr('mesid');
418 if (selected_mes_id !== undefined) {669 if (selected_mes_id !== undefined) {
419 await createNewBookmark(selected_mes_id);670 await createNewBookmark(Number(selected_mes_id));
420 }671 }
421 });672 });
422673
@@ -426,4 +677,6 @@ jQuery(function () {
426 await branchChat(Number(selected_mes_id));677 await branchChat(Number(selected_mes_id));
427 }678 }
428 });679 });
429});680
681 registerBookmarksSlashCommands();
682}