} Branch file name
+ */
+export async function branchChat(mesId) {
+ if (this_chid === undefined && !selected_group) {
+ toastr.info('No character selected.', 'Create Branch');
+ return null;
+ }
+
+ const fileName = await createBranch(mesId);
+ await saveItemizedPrompts(fileName);
+
+ if (selected_group) {
+ await openGroupChat(selected_group, fileName);
+ } else {
+ await openCharacterChat(fileName);
+ }
+
+ return fileName;
+}
+
+function registerBookmarksSlashCommands() {
+ /**
+ * Validates a message ID. (Is a number, exists as a message)
+ *
+ * @param {number} mesId - The message ID to validate.
+ * @param {string} context - The context of the slash command. Will be used as the title of any toasts.
+ * @returns {boolean} - Returns true if the message ID is valid, otherwise false.
+ */
+ function validateMessageId(mesId, context) {
+ if (isNaN(mesId)) {
+ toastr.warning('Invalid message ID was provided', context);
+ return false;
+ }
+ if (!chat[mesId]) {
+ toastr.warning(`Message for id ${mesId} not found`, context);
+ return false;
+ }
+ return true;
+ }
+
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'branch-create',
+ returns: 'Name of the new branch',
+ callback: async (args, text) => {
+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
+ if (!validateMessageId(mesId, 'Create Branch')) return '';
+
+ const branchName = await branchChat(mesId);
+ return branchName ?? '';
+ },
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Message ID',
+ typeList: [ARGUMENT_TYPE.NUMBER],
+ enumProvider: commonEnumProviders.messages(),
+ }),
+ ],
+ helpString: `
+
+ Create a new branch from the selected message. If no message id is provided, will use the last message.
+
+
+ Creating a branch will automatically choose a name for the branch.
+ After creating the branch, the branch chat will be automatically opened.
+
+
+ Use Checkpoints and /checkpoint-create
instead if you do not want to jump to the new chat.
+
`,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'checkpoint-create',
+ returns: 'Name of the new checkpoint',
+ callback: async (args, text) => {
+ const mesId = Number(args.mesId ?? getLastMessageId());
+ if (!validateMessageId(mesId, 'Create Checkpoint')) return '';
+
+ if (typeof text !== 'string') {
+ toastr.warning('Checkpoint name must be a string or empty', 'Create Checkpoint');
+ return '';
+ }
+
+ const checkPointName = await createNewBookmark(mesId, { forceName: text });
+ return checkPointName ?? '';
+ },
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'mesId',
+ description: 'Message ID',
+ typeList: [ARGUMENT_TYPE.NUMBER],
+ enumProvider: commonEnumProviders.messages(),
+ }),
+ ],
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Checkpoint name',
+ typeList: [ARGUMENT_TYPE.STRING],
+ }),
+ ],
+ helpString: `
+
+ Create a new checkpoint for the selected message with the provided name. If no message id is provided, will use the last message.
+ Leave the checkpoint name empty to auto-generate one.
+
+
+ A created checkpoint will be permanently linked with the message.
+ If a checkpoint already exists, the link to it will be overwritten.
+ After creating the checkpoint, the checkpoint chat can be opened with the checkpoint flag,
+ using the /go
command with the checkpoint name or the /checkpoint-go
command on the message.
+
+
+ Use Branches and /branch-create
instead if you do want to jump to the new chat.
+
+ `,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'checkpoint-go',
+ returns: 'Name of the checkpoint',
+ callback: async (args, text) => {
+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
+ if (!validateMessageId(mesId, 'Open Checkpoint')) return '';
+
+ const checkPointName = chat[mesId].extra?.bookmark_link;
+ if (!checkPointName) {
+ toastr.warning('No checkpoint is linked to the selected message', 'Open Checkpoint');
+ return '';
+ }
+
+ if (selected_group) {
+ await openGroupChat(selected_group, checkPointName);
+ } else {
+ await openCharacterChat(checkPointName);
+ }
+
+ return checkPointName;
+ },
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Message ID',
+ typeList: [ARGUMENT_TYPE.NUMBER],
+ enumProvider: commonEnumProviders.messages(),
+ }),
+ ],
+ helpString: `
+
+ Open the checkpoint linked to the selected message. If no message id is provided, will use the last message.
+
+
+ Use /checkpoint-get
if you want to make sure that the selected message has a checkpoint.
+
`,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'checkpoint-exit',
+ returns: 'The name of the chat exited to. Returns an empty string if not in a checkpoint chat.',
+ callback: async () => {
+ const mainChat = await backToMainChat();
+ return mainChat ?? '';
+ },
+ helpString: 'Exit the checkpoint chat.
If not in a checkpoint chat, returns empty string.',
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'checkpoint-parent',
+ returns: 'Name of the parent chat for this checkpoint',
+ callback: async () => {
+ const mainChatName = getMainChatName();
+ return mainChatName ?? '';
+ },
+ helpString: 'Get the name of the parent chat for this checkpoint.
If not in a checkpoint chat, returns empty string.',
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'checkpoint-get',
+ returns: 'Name of the chat',
+ callback: async (args, text) => {
+ const mesId = Number(args.mesId ?? text ?? getLastMessageId());
+ if (!validateMessageId(mesId, 'Get Checkpoint')) return '';
+
+ const checkPointName = chat[mesId].extra?.bookmark_link;
+ return checkPointName ?? '';
+ },
+ unnamedArgumentList: [
+ SlashCommandArgument.fromProps({
+ description: 'Message ID',
+ typeList: [ARGUMENT_TYPE.NUMBER],
+ enumProvider: commonEnumProviders.messages(),
+ }),
+ ],
+ helpString: `
+
+ Get the name of the checkpoint linked to the selected message. If no message id is provided, will use the last message.
+ If no checkpoint is linked, the result will be empty.
+
`,
+ }));
+ SlashCommandParser.addCommandObject(SlashCommand.fromProps({
+ name: 'checkpoint-list',
+ returns: 'JSON array of all existing checkpoints in this chat, as an array',
+ /** @param {{links?: string}} args @returns {Promise} */
+ callback: async (args, _) => {
+ const result = Object.entries(chat)
+ .filter(([_, message]) => message.extra?.bookmark_link)
+ .map(([mesId, message]) => isTrueBoolean(args.links) ? message.extra.bookmark_link : Number(mesId));
+ return JSON.stringify(result);
+ },
+ namedArgumentList: [
+ SlashCommandNamedArgument.fromProps({
+ name: 'links',
+ description: 'Get a list of all links / chat names of the checkpoints, instead of the message ids',
+ typeList: [ARGUMENT_TYPE.BOOLEAN],
+ enumList: commonEnumProviders.boolean('trueFalse')(),
+ defaultValue: 'false',
+ }),
+ ],
+ helpString: `
+
+ List all existing checkpoints in this chat.
+
+
+ Returns a list of all message ids that have a checkpoint, or all checkpoint links if links
is set to true
.
+ The value will be a JSON array.
+
`,
+ }));
+}
+
+export function initBookmarks() {
$('#option_new_bookmark').on('click', saveBookmarkMenu);
$('#option_back_to_main').on('click', backToMainChat);
$('#option_convert_to_group').on('click', convertSoloToGroupChat);
-});
+
+ $(document).on('click', '.select_chat_block, .mes_bookmark', async function (e) {
+ // If shift is held down, we are not following the bookmark, but creating a new one
+ const mes = $(this).closest('.mes');
+ if (e.shiftKey && mes.length) {
+ const selectedMesId = mes.attr('mesid');
+ await createNewBookmark(Number(selectedMesId));
+ return;
+ }
+
+ const fileName = $(this).hasClass('mes_bookmark')
+ ? $(this).closest('.mes').attr('bookmark_link')
+ : $(this).attr('file_name').replace('.jsonl', '');
+
+ if (!fileName) {
+ return;
+ }
+
+ try {
+ showLoader();
+ if (selected_group) {
+ await openGroupChat(selected_group, fileName);
+ } else {
+ await openCharacterChat(fileName);
+ }
+ } finally {
+ await hideLoader();
+ }
+
+ $('#shadow_select_chat_popup').css('display', 'none');
+ $('#load_select_chat_div').css('display', 'block');
+ });
+
+ $(document).on('click', '.mes_create_bookmark', async function () {
+ const mesId = $(this).closest('.mes').attr('mesid');
+ if (mesId !== undefined) {
+ await createNewBookmark(Number(mesId));
+ }
+ });
+
+ $(document).on('click', '.mes_create_branch', async function () {
+ const mesId = $(this).closest('.mes').attr('mesid');
+ if (mesId !== undefined) {
+ await branchChat(Number(mesId));
+ }
+ });
+
+ registerBookmarksSlashCommands();
+}
diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js
index 7194abc49..3ec6b8470 100644
--- a/public/scripts/extensions/caption/index.js
+++ b/public/scripts/extensions/caption/index.js
@@ -358,10 +358,10 @@ function onRefineModeInput() {
*/
async function captionCommandCallback(args, prompt) {
const quiet = isTrueBoolean(args?.quiet);
- const id = args?.id;
+ const mesId = args?.mesId ?? args?.id;
- if (!isNaN(Number(id))) {
- const message = getContext().chat[id];
+ if (!isNaN(Number(mesId))) {
+ const message = getContext().chat[mesId];
if (message?.extra?.image) {
try {
const fetchResult = await fetch(message.extra.image);
@@ -546,7 +546,7 @@ jQuery(async function () {
'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
),
SlashCommandNamedArgument.fromProps({
- name: 'id',
+ name: 'mesId',
description: 'get image from a message with this ID',
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages(),
diff --git a/public/scripts/extensions/translate/index.js b/public/scripts/extensions/translate/index.js
index e170795ee..c58ef9f08 100644
--- a/public/scripts/extensions/translate/index.js
+++ b/public/scripts/extensions/translate/index.js
@@ -598,9 +598,20 @@ jQuery(async () => {
$(document).on('click', '.mes_translate', onMessageTranslateClick);
$('#translate_key_button').on('click', async () => {
const optionText = $('#translation_provider option:selected').text();
- const key = await callGenericPopup(`${optionText} API Key
`, POPUP_TYPE.INPUT);
+ const key = await callGenericPopup(`${optionText} API Key
`, POPUP_TYPE.INPUT, '', {
+ customButtons: [{
+ text: 'Remove Key',
+ appendAtEnd: true,
+ result: POPUP_RESULT.NEGATIVE,
+ action: async () => {
+ await writeSecret(extension_settings.translate.provider, '');
+ toastr.success('API Key removed');
+ $('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
+ },
+ }],
+ });
- if (key == false) {
+ if (!key) {
return;
}
@@ -634,7 +645,7 @@ jQuery(async () => {
}],
});
- if (url == false || url == '') {
+ if (!url) {
return;
}
diff --git a/public/scripts/extensions/tts/azure.js b/public/scripts/extensions/tts/azure.js
index 967a6bf26..06d3d04cc 100644
--- a/public/scripts/extensions/tts/azure.js
+++ b/public/scripts/extensions/tts/azure.js
@@ -77,14 +77,14 @@ class AzureTtsProvider {
result: POPUP_RESULT.NEGATIVE,
action: async () => {
await writeSecret(SECRET_KEYS.AZURE_TTS, '');
- $('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]);
+ $('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
toastr.success('API Key removed');
await this.onRefreshClick();
},
}],
});
- if (key == false || key == '') {
+ if (!key) {
return;
}
diff --git a/public/scripts/extensions/tts/openai-compatible.js b/public/scripts/extensions/tts/openai-compatible.js
index 26012056d..9b46d68ee 100644
--- a/public/scripts/extensions/tts/openai-compatible.js
+++ b/public/scripts/extensions/tts/openai-compatible.js
@@ -86,14 +86,14 @@ class OpenAICompatibleTtsProvider {
result: POPUP_RESULT.NEGATIVE,
action: async () => {
await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, '');
- $('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
+ $('#openai_compatible_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
toastr.success('API Key removed');
await this.onRefreshClick();
},
}],
});
- if (key == false || key == '') {
+ if (!key) {
return;
}
diff --git a/public/scripts/popup.js b/public/scripts/popup.js
index 1a9d9ce36..226bc1c37 100644
--- a/public/scripts/popup.js
+++ b/public/scripts/popup.js
@@ -83,6 +83,9 @@ const showPopupHelper = {
const content = PopupUtils.BuildTextWithHeader(header, text);
const popup = new Popup(content, POPUP_TYPE.INPUT, defaultValue, popupOptions);
const value = await popup.show();
+ // Return values: If empty string, we explicitly handle that as returning that empty string as "success" provided.
+ // Otherwise, all non-truthy values (false, null, undefined) are treated as "cancel" and return null.
+ if (value === '') return '';
return value ? String(value) : null;
},
diff --git a/public/scripts/templates/createCheckpoint.html b/public/scripts/templates/createCheckpoint.html
new file mode 100644
index 000000000..98e9bd08b
--- /dev/null
+++ b/public/scripts/templates/createCheckpoint.html
@@ -0,0 +1,8 @@
+
+ Enter Checkpoint Name:(Leave empty to auto-generate)
+
+{{#if isReplace}}
+
+ The currently existing checkpoint will be unlinked and replaced with the new checkpoint, but can still be found in the Chat Management.
+
+{{/if}}
diff --git a/public/style.css b/public/style.css
index b4c8b714b..21387cfd4 100644
--- a/public/style.css
+++ b/public/style.css
@@ -3964,6 +3964,10 @@ input[type="range"]::-webkit-slider-thumb {
display: inline-block;
}
+.mes:not([bookmark_link='']) .mes_create_bookmark {
+ display: none;
+}
+
.mes_edit_buttons {
display: none;
flex-direction: row;