diff --git a/default/content/presets/instruct/Adventure.json b/default/content/presets/instruct/Adventure.json index 827c6d5c9..a4093dff7 100644 --- a/default/content/presets/instruct/Adventure.json +++ b/default/content/presets/instruct/Adventure.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "Adventure" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Alpaca-Roleplay.json b/default/content/presets/instruct/Alpaca-Roleplay.json index 5a5054340..b5aec1c92 100644 --- a/default/content/presets/instruct/Alpaca-Roleplay.json +++ b/default/content/presets/instruct/Alpaca-Roleplay.json @@ -19,5 +19,6 @@ "system_suffix": "\n\n", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "Alpaca-Roleplay" } diff --git a/default/content/presets/instruct/Alpaca-Single-Turn.json b/default/content/presets/instruct/Alpaca-Single-Turn.json index 6a6f052d2..9baca108c 100644 --- a/default/content/presets/instruct/Alpaca-Single-Turn.json +++ b/default/content/presets/instruct/Alpaca-Single-Turn.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "Alpaca-Single-Turn" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Alpaca.json b/default/content/presets/instruct/Alpaca.json index 96fd2cc83..28b2065fb 100644 --- a/default/content/presets/instruct/Alpaca.json +++ b/default/content/presets/instruct/Alpaca.json @@ -19,5 +19,6 @@ "system_suffix": "\n\n", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "Alpaca" } diff --git a/default/content/presets/instruct/ChatML.json b/default/content/presets/instruct/ChatML.json index 348ae2458..513a72820 100644 --- a/default/content/presets/instruct/ChatML.json +++ b/default/content/presets/instruct/ChatML.json @@ -19,5 +19,6 @@ "system_suffix": "<|im_end|>\n", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "ChatML" } diff --git a/default/content/presets/instruct/DreamGen Role-Play V1.json b/default/content/presets/instruct/DreamGen Role-Play V1.json index 07f0301fc..002878b4d 100644 --- a/default/content/presets/instruct/DreamGen Role-Play V1.json +++ b/default/content/presets/instruct/DreamGen Role-Play V1.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "DreamGen Role-Play V1" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Koala.json b/default/content/presets/instruct/Koala.json index 980482c1a..f5db8ff48 100644 --- a/default/content/presets/instruct/Koala.json +++ b/default/content/presets/instruct/Koala.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "Koala" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Libra-32B.json b/default/content/presets/instruct/Libra-32B.json index 6014546f6..c665eb364 100644 --- a/default/content/presets/instruct/Libra-32B.json +++ b/default/content/presets/instruct/Libra-32B.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "Libra-32B" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Lightning 1.1.json b/default/content/presets/instruct/Lightning 1.1.json index bf79e1358..9f9bd7ccf 100644 --- a/default/content/presets/instruct/Lightning 1.1.json +++ b/default/content/presets/instruct/Lightning 1.1.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "Lightning 1.1" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Llama 2 Chat.json b/default/content/presets/instruct/Llama 2 Chat.json index aeb4e13fd..dc507b777 100644 --- a/default/content/presets/instruct/Llama 2 Chat.json +++ b/default/content/presets/instruct/Llama 2 Chat.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", "system_same_as_user": true, + "last_system_sequence": "", "name": "Llama 2 Chat" } diff --git a/default/content/presets/instruct/Metharme.json b/default/content/presets/instruct/Metharme.json index 1c8474cdf..195fe5260 100644 --- a/default/content/presets/instruct/Metharme.json +++ b/default/content/presets/instruct/Metharme.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "Metharme" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Mistral.json b/default/content/presets/instruct/Mistral.json index 4f35139fa..bd3a9ff3c 100644 --- a/default/content/presets/instruct/Mistral.json +++ b/default/content/presets/instruct/Mistral.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", "system_same_as_user": true, + "last_system_sequence": "", "name": "Mistral" } diff --git a/default/content/presets/instruct/OpenOrca-OpenChat.json b/default/content/presets/instruct/OpenOrca-OpenChat.json index 924ea94f7..04d526d4d 100644 --- a/default/content/presets/instruct/OpenOrca-OpenChat.json +++ b/default/content/presets/instruct/OpenOrca-OpenChat.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "OpenOrca-OpenChat" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Pygmalion.json b/default/content/presets/instruct/Pygmalion.json index 6278c0d23..cb5b60d8a 100644 --- a/default/content/presets/instruct/Pygmalion.json +++ b/default/content/presets/instruct/Pygmalion.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "Pygmalion" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Story.json b/default/content/presets/instruct/Story.json index 1e42d3281..5c6b00cf0 100644 --- a/default/content/presets/instruct/Story.json +++ b/default/content/presets/instruct/Story.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "Story" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Synthia.json b/default/content/presets/instruct/Synthia.json index 24ec4849e..21fa535c0 100644 --- a/default/content/presets/instruct/Synthia.json +++ b/default/content/presets/instruct/Synthia.json @@ -19,5 +19,6 @@ "system_suffix": "\n", "user_alignment_message": "Let's get started. Please respond based on the information and instructions provided above.", "system_same_as_user": false, + "last_system_sequence": "", "name": "Synthia" } diff --git a/default/content/presets/instruct/Vicuna 1.0.json b/default/content/presets/instruct/Vicuna 1.0.json index fbc8a2bf5..d96bf4cb2 100644 --- a/default/content/presets/instruct/Vicuna 1.0.json +++ b/default/content/presets/instruct/Vicuna 1.0.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "Vicuna 1.0" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/Vicuna 1.1.json b/default/content/presets/instruct/Vicuna 1.1.json index a31698d03..a42e4fbfc 100644 --- a/default/content/presets/instruct/Vicuna 1.1.json +++ b/default/content/presets/instruct/Vicuna 1.1.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "Vicuna 1.1" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/WizardLM-13B.json b/default/content/presets/instruct/WizardLM-13B.json index 21e7bd555..b15fea56f 100644 --- a/default/content/presets/instruct/WizardLM-13B.json +++ b/default/content/presets/instruct/WizardLM-13B.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": true, + "last_system_sequence": "", "name": "WizardLM-13B" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/WizardLM.json b/default/content/presets/instruct/WizardLM.json index 198f6a062..18e808da4 100644 --- a/default/content/presets/instruct/WizardLM.json +++ b/default/content/presets/instruct/WizardLM.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "WizardLM" -} \ No newline at end of file +} diff --git a/default/content/presets/instruct/simple-proxy-for-tavern.json b/default/content/presets/instruct/simple-proxy-for-tavern.json index 14d32d86c..986da1697 100644 --- a/default/content/presets/instruct/simple-proxy-for-tavern.json +++ b/default/content/presets/instruct/simple-proxy-for-tavern.json @@ -19,5 +19,6 @@ "system_suffix": "", "user_alignment_message": "", "system_same_as_user": false, + "last_system_sequence": "", "name": "simple-proxy-for-tavern" -} \ No newline at end of file +} diff --git a/public/css/toggle-dependent.css b/public/css/toggle-dependent.css index 13067d50a..3f90edc42 100644 --- a/public/css/toggle-dependent.css +++ b/public/css/toggle-dependent.css @@ -439,3 +439,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint { #openai_image_inlining:checked~#image_inlining_hint { display: block; } + +#smooth_streaming:not(:checked)~#smooth_streaming_speed_control { + display: none; +} + +#smooth_streaming:checked~#smooth_streaming_speed_control { + display: block; +} diff --git a/public/index.html b/public/index.html index 813bf7592..3a46ba2ed 100644 --- a/public/index.html +++ b/public/index.html @@ -1953,10 +1953,6 @@
For privacy reasons, your API key will be hidden after you reload the page.
-
- - -

Novel AI Model @@ -1966,6 +1962,10 @@ +
+ + +
@@ -2489,6 +2489,20 @@
+

OpenRouter API Key

+
+ + Click "Authorize" below or get the key from OpenRouter. +
+ View Remaining Credits +
+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +

OpenRouter Model

- -
-
- For privacy reasons, your API key will be hidden after you reload the page. -
@@ -2691,6 +2691,7 @@ + @@ -3033,12 +3034,12 @@
-
-
+
+
+ +
+ +
+
+
@@ -3618,6 +3629,24 @@ + @@ -4247,12 +4276,21 @@

-
- Description - - - - +
+
+ Description + + + + +
+
@@ -4352,6 +4390,10 @@ + @@ -4804,7 +4846,7 @@
- +
+
+
+

+ Ability of the current character/group to use external media in chats. +

+ + Media: images, videos, audio. External: not hosted on the local server. + + + + +
+
CHAR is typing
diff --git a/public/lib/eventemitter.js b/public/lib/eventemitter.js index 046991a05..a1b40e38c 100644 --- a/public/lib/eventemitter.js +++ b/public/lib/eventemitter.js @@ -29,6 +29,12 @@ var EventEmitter = function () { }; EventEmitter.prototype.on = function (event, listener) { + // Unknown event used by external libraries? + if (event === undefined) { + console.trace('EventEmitter: Cannot listen to undefined event'); + return; + } + if (typeof this.events[event] !== 'object') { this.events[event] = []; } diff --git a/public/script.js b/public/script.js index 3078d8a88..18e15e8e2 100644 --- a/public/script.js +++ b/public/script.js @@ -208,7 +208,7 @@ import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_set import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js'; -import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags } from './scripts/chats.js'; +import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js'; import { initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros } from './scripts/macros.js'; @@ -324,10 +324,13 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { return; } - if (!power_user.forbid_external_images) { + const isMediaAllowed = isExternalMediaAllowed(); + if (isMediaAllowed) { return; } + let mediaBlocked = false; + switch (node.tagName) { case 'AUDIO': case 'VIDEO': @@ -350,6 +353,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { if (isExternalUrl(url)) { console.warn('External media blocked', url); node.remove(); + mediaBlocked = true; break; } } @@ -357,16 +361,37 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => { if (src && isExternalUrl(src)) { console.warn('External media blocked', src); + mediaBlocked = true; node.remove(); } if (data && isExternalUrl(data)) { console.warn('External media blocked', data); + mediaBlocked = true; node.remove(); } } break; } + + if (mediaBlocked) { + const entityId = getCurrentEntityId(); + const warningShownKey = `mediaWarningShown:${entityId}`; + + if (localStorage.getItem(warningShownKey) === null) { + const warningToast = toastr.warning( + 'Use the "Ext. Media" button to allow it. Click on this message to dismiss.', + 'External media has been blocked', + { + timeOut: 0, + preventDuplicates: true, + onclick: () => toastr.clear(warningToast), + }, + ); + + localStorage.setItem(warningShownKey, 'true'); + } + } }); // API OBJECT FOR EXTERNAL WIRING @@ -416,6 +441,7 @@ export const event_types = { // TODO: Naming convention is inconsistent with other events CHARACTER_DELETED: 'characterDeleted', CHARACTER_DUPLICATED: 'character_duplicated', + SMOOTH_STREAM_TOKEN_RECEIVED: 'smooth_stream_token_received', }; export const eventSource = new EventEmitter(); @@ -751,7 +777,6 @@ function reloadMarkdownProcessor(render_formulas = false) { } function getCurrentChatId() { - console.debug(`selectedGroup:${selected_group}, this_chid:${this_chid}`); if (selected_group) { return groups.find(x => x.id == selected_group)?.chat_id; } @@ -1692,7 +1717,7 @@ export async function reloadCurrentChat() { chat.length = 0; if (selected_group) { - await getGroupChat(selected_group); + await getGroupChat(selected_group, true); } else if (this_chid) { await getChat(); @@ -3032,8 +3057,8 @@ function saveResponseLength(api, responseLength) { oldValue = oai_settings.openai_max_tokens; oai_settings.openai_max_tokens = responseLength; } else { - oldValue = max_context; - max_context = responseLength; + oldValue = amount_gen; + amount_gen = responseLength; } return oldValue; } @@ -3048,7 +3073,7 @@ function restoreResponseLength(api, responseLength) { if (api === 'openai') { oai_settings.openai_max_tokens = responseLength; } else { - max_context = responseLength; + amount_gen = responseLength; } } @@ -4058,6 +4083,10 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu await streamingProcessor.onFinishStreaming(streamingProcessor.messageId, getMessage); streamingProcessor = null; triggerAutoContinue(messageChunk, isImpersonate); + return Object.defineProperties(new String(getMessage), { + 'messageChunk': { value: messageChunk }, + 'fromStream': { value: true }, + }); } } else { return await sendGenerationRequest(type, generate_data); @@ -4068,6 +4097,11 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu async function onSuccess(data) { if (!data) return; + + if (data?.fromStream) { + return data; + } + let messageChunk = ''; if (data.error) { @@ -4177,6 +4211,9 @@ async function Generate(type, { automatic_trigger, force_name2, quiet_prompt, qu if (type !== 'quiet') { triggerAutoContinue(messageChunk, isImpersonate); } + + // Don't break the API chain that expects a single string in return + return Object.defineProperty(new String(getMessage), 'messageChunk', { value: messageChunk }); } function onError(exception) { @@ -4271,57 +4308,81 @@ export function getNextMessageId(type) { } /** - * - * @param {string} messageChunk - * @param {boolean} isImpersonate - * @returns {void} + * Determines if the message should be auto-continued. + * @param {string} messageChunk Current message chunk + * @param {boolean} isImpersonate Is the user impersonation + * @returns {boolean} Whether the message should be auto-continued + */ +export function shouldAutoContinue(messageChunk, isImpersonate) { + if (!power_user.auto_continue.enabled) { + console.debug('Auto-continue is disabled by user.'); + return false; + } + + if (typeof messageChunk !== 'string') { + console.debug('Not triggering auto-continue because message chunk is not a string'); + return false; + } + + if (isImpersonate) { + console.log('Continue for impersonation is not implemented yet'); + return false; + } + + if (is_send_press) { + console.debug('Auto-continue is disabled because a message is currently being sent.'); + return false; + } + + if (power_user.auto_continue.target_length <= 0) { + console.log('Auto-continue target length is 0, not triggering auto-continue'); + return false; + } + + if (main_api === 'openai' && !power_user.auto_continue.allow_chat_completions) { + console.log('Auto-continue for OpenAI is disabled by user.'); + return false; + } + + const textareaText = String($('#send_textarea').val()); + const USABLE_LENGTH = 5; + + if (textareaText.length > 0) { + console.log('Not triggering auto-continue because user input is not empty'); + return false; + } + + if (messageChunk.trim().length > USABLE_LENGTH && chat.length) { + const lastMessage = chat[chat.length - 1]; + const messageLength = getTokenCount(lastMessage.mes); + const shouldAutoContinue = messageLength < power_user.auto_continue.target_length; + + if (shouldAutoContinue) { + console.log(`Triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}. Message chunk: ${messageChunk}`); + return true; + } else { + console.log(`Not triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}`); + return false; + } + } else { + console.log('Last generated chunk was empty, not triggering auto-continue'); + return false; + } +} + +/** + * Triggers auto-continue if the message meets the criteria. + * @param {string} messageChunk Current message chunk + * @param {boolean} isImpersonate Is the user impersonation */ export function triggerAutoContinue(messageChunk, isImpersonate) { if (selected_group) { - console.log('Auto-continue is disabled for group chat'); + console.debug('Auto-continue is disabled for group chat'); return; } - if (power_user.auto_continue.enabled && !is_send_press) { - if (power_user.auto_continue.target_length <= 0) { - console.log('Auto-continue target length is 0, not triggering auto-continue'); - return; - } - - if (main_api === 'openai' && !power_user.auto_continue.allow_chat_completions) { - console.log('Auto-continue for OpenAI is disabled by user.'); - return; - } - - if (isImpersonate) { - console.log('Continue for impersonation is not implemented yet'); - return; - } - - const textareaText = String($('#send_textarea').val()); - const USABLE_LENGTH = 5; - - if (textareaText.length > 0) { - console.log('Not triggering auto-continue because user input is not empty'); - return; - } - - if (messageChunk.trim().length > USABLE_LENGTH && chat.length) { - const lastMessage = chat[chat.length - 1]; - const messageLength = getTokenCount(lastMessage.mes); - const shouldAutoContinue = messageLength < power_user.auto_continue.target_length; - - if (shouldAutoContinue) { - console.log(`Triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}. Message chunk: ${messageChunk}`); - $('#option_continue').trigger('click'); - } else { - console.log(`Not triggering auto-continue. Message tokens: ${messageLength}. Target tokens: ${power_user.auto_continue.target_length}`); - return; - } - } else { - console.log('Last generated chunk was empty, not triggering auto-continue'); - return; - } + if (shouldAutoContinue(messageChunk, isImpersonate)) { + $('#option_continue').trigger('click'); } } @@ -6863,6 +6924,12 @@ export function select_selected_character(chid) { $('#form_create').attr('actiontype', 'editcharacter'); $('.form_create_bottom_buttons_block .chat_lorebook_button').show(); + + const externalMediaState = isExternalMediaAllowed(); + $('#character_open_media_overrides').toggle(!selected_group); + $('#character_media_allowed_icon').toggle(externalMediaState); + $('#character_media_forbidden_icon').toggle(!externalMediaState); + saveSettingsDebounced(); } @@ -6923,6 +6990,7 @@ function select_rm_create() { $('#form_create').attr('actiontype', 'createcharacter'); $('.form_create_bottom_buttons_block .chat_lorebook_button').hide(); + $('#character_open_media_overrides').hide(); } function select_rm_characters() { @@ -8211,8 +8279,7 @@ const CONNECT_API_MAP = { async function selectContextCallback(_, name) { if (!name) { - toastr.warning('Context preset name is required'); - return ''; + return power_user.context.preset; } const contextNames = context_presets.map(preset => preset.name); @@ -8231,8 +8298,7 @@ async function selectContextCallback(_, name) { async function selectInstructCallback(_, name) { if (!name) { - toastr.warning('Instruct preset name is required'); - return ''; + return power_user.instruct.preset; } const instructNames = instruct_presets.map(preset => preset.name); @@ -8581,10 +8647,10 @@ jQuery(async function () { registerSlashCommand('closechat', doCloseChat, [], '– closes the current chat', true, true); registerSlashCommand('panels', doTogglePanels, ['togglepanels'], '– toggle UI panels on/off', true, true); registerSlashCommand('forcesave', doForceSave, [], '– forces a save of the current chat and settings', true, true); - registerSlashCommand('instruct', selectInstructCallback, [], '(name) – selects instruct mode preset by name', true, true); + registerSlashCommand('instruct', selectInstructCallback, [], '(name) – selects instruct mode preset by name. Gets the current instruct if no name is provided', true, true); registerSlashCommand('instruct-on', enableInstructCallback, [], '– enables instruct mode', true, true); registerSlashCommand('instruct-off', disableInstructCallback, [], '– disables instruct mode', true, true); - registerSlashCommand('context', selectContextCallback, [], '(name) – selects context template by name', true, true); + registerSlashCommand('context', selectContextCallback, [], '(name) – selects context template by name. Gets the current template if no name is provided', true, true); registerSlashCommand('chat-manager', () => $('#option_select_chat').trigger('click'), ['chat-history', 'manage-chats'], '– opens the chat manager for the current character/group', true, true); setTimeout(function () { @@ -9873,14 +9939,14 @@ jQuery(async function () { $('#character_import_file').click(); }); - $('#character_import_file').on('change', function (e) { + $('#character_import_file').on('change', async function (e) { $('#rm_info_avatar').html(''); if (!e.target.files.length) { return; } for (const file of e.target.files) { - importCharacter(file); + await importCharacter(file); } }); diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 847915fc4..41e54fd48 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -5,6 +5,7 @@ import { addCopyToCodeBlocks, appendMediaToMessage, callPopup, + characters, chat, eventSource, event_types, @@ -12,9 +13,14 @@ import { getRequestHeaders, hideSwipeButtons, name2, + reloadCurrentChat, saveChatDebounced, + saveSettingsDebounced, showSwipeButtons, + this_chid, } from '../script.js'; +import { selected_group } from './group-chats.js'; +import { power_user } from './power-user.js'; import { extractTextFromHTML, extractTextFromMarkdown, @@ -416,6 +422,56 @@ export function decodeStyleTags(text) { }); } +async function openExternalMediaOverridesDialog() { + const entityId = getCurrentEntityId(); + + if (!entityId) { + toastr.info('No character or group selected'); + return; + } + + const template = $('#forbid_media_override_template > .forbid_media_override').clone(); + template.find('.forbid_media_global_state_forbidden').toggle(power_user.forbid_external_images); + template.find('.forbid_media_global_state_allowed').toggle(!power_user.forbid_external_images); + + if (power_user.external_media_allowed_overrides.includes(entityId)) { + template.find('#forbid_media_override_allowed').prop('checked', true); + } + else if (power_user.external_media_forbidden_overrides.includes(entityId)) { + template.find('#forbid_media_override_forbidden').prop('checked', true); + } + else { + template.find('#forbid_media_override_global').prop('checked', true); + } + + callPopup(template, 'text', '', { wide: false, large: false }); +} + +export function getCurrentEntityId() { + if (selected_group) { + return String(selected_group); + } + + return characters[this_chid]?.avatar ?? null; +} + +export function isExternalMediaAllowed() { + const entityId = getCurrentEntityId(); + if (!entityId) { + return !power_user.forbid_external_images; + } + + if (power_user.external_media_allowed_overrides.includes(entityId)) { + return true; + } + + if (power_user.external_media_forbidden_overrides.includes(entityId)) { + return false; + } + + return !power_user.forbid_external_images; +} + jQuery(function () { $(document).on('click', '.mes_hide', async function () { const messageBlock = $(this).closest('.mes'); @@ -511,6 +567,32 @@ jQuery(function () { $(this).closest('.mes').find('.mes_edit').trigger('click'); }); + $(document).on('click', '.open_media_overrides', openExternalMediaOverridesDialog); + $(document).on('input', '#forbid_media_override_allowed', function () { + const entityId = getCurrentEntityId(); + if (!entityId) return; + power_user.external_media_allowed_overrides.push(entityId); + power_user.external_media_forbidden_overrides = power_user.external_media_forbidden_overrides.filter((v) => v !== entityId); + saveSettingsDebounced(); + reloadCurrentChat(); + }); + $(document).on('input', '#forbid_media_override_forbidden', function () { + const entityId = getCurrentEntityId(); + if (!entityId) return; + power_user.external_media_forbidden_overrides.push(entityId); + power_user.external_media_allowed_overrides = power_user.external_media_allowed_overrides.filter((v) => v !== entityId); + saveSettingsDebounced(); + reloadCurrentChat(); + }); + $(document).on('input', '#forbid_media_override_global', function () { + const entityId = getCurrentEntityId(); + if (!entityId) return; + power_user.external_media_allowed_overrides = power_user.external_media_allowed_overrides.filter((v) => v !== entityId); + power_user.external_media_forbidden_overrides = power_user.external_media_forbidden_overrides.filter((v) => v !== entityId); + saveSettingsDebounced(); + reloadCurrentChat(); + }); + $('#file_form_input').on('change', onFileAttach); $('#file_form').on('reset', function () { $('#file_form').addClass('displayNone'); diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 307455a17..fff8a798a 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -354,15 +354,15 @@ jQuery(function () {
@@ -375,6 +375,14 @@ jQuery(function () { + + + + + + + + diff --git a/public/scripts/extensions/memory/index.js b/public/scripts/extensions/memory/index.js index 7071eb8a3..348775f06 100644 --- a/public/scripts/extensions/memory/index.js +++ b/public/scripts/extensions/memory/index.js @@ -91,7 +91,7 @@ const defaultSettings = { maxMessagesPerRequestMin: 0, maxMessagesPerRequestMax: 250, maxMessagesPerRequestStep: 1, - prompt_builder: prompt_builders.RAW_BLOCKING, + prompt_builder: prompt_builders.DEFAULT, }; function loadSettings() { diff --git a/public/scripts/extensions/quick-reply/html/qrEditor.html b/public/scripts/extensions/quick-reply/html/qrEditor.html index 24a149333..08cecbc23 100644 --- a/public/scripts/extensions/quick-reply/html/qrEditor.html +++ b/public/scripts/extensions/quick-reply/html/qrEditor.html @@ -78,7 +78,7 @@ Execute before group member message -
+
Automation ID
diff --git a/public/scripts/extensions/quick-reply/index.js b/public/scripts/extensions/quick-reply/index.js index c25279e28..28053ec64 100644 --- a/public/scripts/extensions/quick-reply/index.js +++ b/public/scripts/extensions/quick-reply/index.js @@ -104,7 +104,7 @@ const loadSets = async () => { qr.executeOnAi = slot.autoExecute_botMessage ?? false; qr.executeOnChatChange = slot.autoExecute_chatLoad ?? false; qr.executeOnGroupMemberDraft = slot.autoExecute_groupMemberDraft ?? false; - qr.automationId = slot.automationId ?? false; + qr.automationId = slot.automationId ?? ''; qr.contextList = (slot.contextMenu ?? []).map(it=>({ set: it.preset, isChained: it.chain, diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index f64ba5156..f4e47ca84 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -1808,7 +1808,7 @@ function processReply(str) { str = str.replaceAll('β€œ', ''); str = str.replaceAll('.', ','); str = str.replaceAll('\n', ', '); - str = str.replace(/[^a-zA-Z0-9,:()']+/g, ' '); // Replace everything except alphanumeric characters and commas with spaces + str = str.replace(/[^a-zA-Z0-9,:()\-']+/g, ' '); // Replace everything except alphanumeric characters and commas with spaces str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one str = str.trim(); diff --git a/public/scripts/group-chats.js b/public/scripts/group-chats.js index 3b2f1c121..412f52aaa 100644 --- a/public/scripts/group-chats.js +++ b/public/scripts/group-chats.js @@ -69,9 +69,11 @@ import { loadItemizedPrompts, animation_duration, depth_prompt_role_default, + shouldAutoContinue, } from '../script.js'; import { printTagList, createTagMapFromList, applyTagsOnCharacterSelect, tag_map } from './tags.js'; import { FILTER_TYPES, FilterHelper } from './filters.js'; +import { isExternalMediaAllowed } from './chats.js'; export { selected_group, @@ -175,7 +177,7 @@ async function loadGroupChat(chatId) { return []; } -export async function getGroupChat(groupId) { +export async function getGroupChat(groupId, reload = false) { const group = groups.find((x) => x.id === groupId); const chat_id = group.chat_id; const data = await loadGroupChat(chat_id); @@ -215,6 +217,10 @@ export async function getGroupChat(groupId) { updateChatMetadata(metadata, true); } + if (reload) { + select_group_chats(groupId, true); + } + await eventSource.emit(event_types.CHAT_CHANGED, getCurrentChatId()); } @@ -678,9 +684,10 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { await delay(1); } - const group = groups.find((x) => x.id === selected_group); - let typingIndicator = $('#chat .typing_indicator'); + /** @type {any} Caution: JS war crimes ahead */ let textResult = ''; + let typingIndicator = $('#chat .typing_indicator'); + const group = groups.find((x) => x.id === selected_group); if (!group || !Array.isArray(group.members) || !group.members.length) { sendSystemMessage(system_message_types.EMPTY, '', { isSmallSys: true }); @@ -778,8 +785,15 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { } // Wait for generation to finish - const generateFinished = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) }); - textResult = await generateFinished; + textResult = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) }); + let messageChunk = textResult?.messageChunk; + + if (messageChunk) { + while (shouldAutoContinue(messageChunk, type === 'impersonate')) { + textResult = await Generate('continue', { automatic_trigger: by_auto_mode, ...(params || {}) }); + messageChunk = textResult?.messageChunk; + } + } } } finally { typingIndicator.hide(); @@ -1297,6 +1311,10 @@ function select_group_chats(groupId, skipAnimation) { $('#rm_group_delete').show(); $('#rm_group_scenario').show(); $('#group-metadata-controls .chat_lorebook_button').removeClass('disabled').prop('disabled', false); + $('#group_open_media_overrides').show(); + const isMediaAllowed = isExternalMediaAllowed(); + $('#group_media_allowed_icon').toggle(isMediaAllowed); + $('#group_media_forbidden_icon').toggle(!isMediaAllowed); } else { $('#rm_group_submit').show(); if ($('#groupAddMemberListToggle .inline-drawer-content').css('display') !== 'block') { @@ -1305,6 +1323,7 @@ function select_group_chats(groupId, skipAnimation) { $('#rm_group_delete').hide(); $('#rm_group_scenario').hide(); $('#group-metadata-controls .chat_lorebook_button').addClass('disabled').prop('disabled', true); + $('#group_open_media_overrides').hide(); } updateFavButtonState(group?.fav ?? false); diff --git a/public/scripts/instruct-mode.js b/public/scripts/instruct-mode.js index 033794009..7fc924274 100644 --- a/public/scripts/instruct-mode.js +++ b/public/scripts/instruct-mode.js @@ -26,6 +26,7 @@ const controls = [ { id: 'instruct_output_suffix', property: 'output_suffix', isCheckbox: false }, { id: 'instruct_system_sequence', property: 'system_sequence', isCheckbox: false }, { id: 'instruct_system_suffix', property: 'system_suffix', isCheckbox: false }, + { id: 'instruct_last_system_sequence', property: 'last_system_sequence', isCheckbox: false }, { id: 'instruct_user_alignment_message', property: 'user_alignment_message', isCheckbox: false }, { id: 'instruct_stop_sequence', property: 'stop_sequence', isCheckbox: false }, { id: 'instruct_names', property: 'names', isCheckbox: true }, @@ -56,6 +57,7 @@ function migrateInstructModeSettings(settings) { system_sequence: '', system_suffix: '', user_alignment_message: '', + last_system_sequence: '', names_force_groups: true, skip_examples: false, system_same_as_user: false, @@ -243,14 +245,15 @@ export function getInstructStoppingSequences() { const result = []; if (power_user.instruct.enabled) { - const stop_sequence = power_user.instruct.stop_sequence; - const input_sequence = power_user.instruct.input_sequence.replace(/{{name}}/gi, name1); - const output_sequence = power_user.instruct.output_sequence.replace(/{{name}}/gi, name2); - const first_output_sequence = power_user.instruct.first_output_sequence.replace(/{{name}}/gi, name2); - const last_output_sequence = power_user.instruct.last_output_sequence.replace(/{{name}}/gi, name2); - const system_sequence = power_user.instruct.system_sequence.replace(/{{name}}/gi, 'System'); + const stop_sequence = power_user.instruct.stop_sequence || ''; + const input_sequence = power_user.instruct.input_sequence?.replace(/{{name}}/gi, name1) || ''; + const output_sequence = power_user.instruct.output_sequence?.replace(/{{name}}/gi, name2) || ''; + const first_output_sequence = power_user.instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || ''; + const last_output_sequence = power_user.instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || ''; + const system_sequence = power_user.instruct.system_sequence?.replace(/{{name}}/gi, 'System') || ''; + const last_system_sequence = power_user.instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || ''; - const combined_sequence = `${stop_sequence}\n${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}\n${system_sequence}`; + const combined_sequence = `${stop_sequence}\n${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}\n${system_sequence}\n${last_system_sequence}`; combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence); } @@ -452,9 +455,10 @@ export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, return power_user.instruct.input_sequence; } - // Neutral / system prompt + // Neutral / system / quiet prompt + // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise if (isQuiet && !isQuietToLoud) { - return power_user.instruct.output_sequence; + return power_user.instruct.last_system_sequence || power_user.instruct.output_sequence; } // Quiet in-character prompt @@ -517,20 +521,28 @@ export function replaceInstructMacros(input) { if (!input) { return ''; } + const instructMacros = { + 'instructSystem|instructSystemPrompt': power_user.instruct.system_prompt, + 'instructSystemPromptPrefix': power_user.instruct.system_sequence_prefix, + 'instructSystemPromptSuffix': power_user.instruct.system_sequence_suffix, + 'instructInput|instructUserPrefix': power_user.instruct.input_sequence, + 'instructUserSuffix': power_user.instruct.input_suffix, + 'instructOutput|instructAssistantPrefix': power_user.instruct.output_sequence, + 'instructSeparator|instructAssistantSuffix': power_user.instruct.output_suffix, + 'instructSystemPrefix': power_user.instruct.system_sequence, + 'instructSystemSuffix': power_user.instruct.system_suffix, + 'instructFirstOutput|instructFirstAssistantPrefix': power_user.instruct.first_output_sequence || power_user.instruct.output_sequence, + 'instructLastOutput|instructLastAssistantPrefix': power_user.instruct.last_output_sequence || power_user.instruct.output_sequence, + 'instructStop': power_user.instruct.stop_sequence, + 'instructUserFiller': power_user.instruct.user_alignment_message, + 'instructSystemInstructionPrefix': power_user.instruct.last_system_sequence, + }; + + for (const [placeholder, value] of Object.entries(instructMacros)) { + const regex = new RegExp(`{{(${placeholder})}}`, 'gi'); + input = input.replace(regex, power_user.instruct.enabled ? value : ''); + } - input = input.replace(/{{(instructSystem|instructSystemPrompt)}}/gi, power_user.instruct.enabled ? power_user.instruct.system_prompt : ''); - input = input.replace(/{{instructSystemPromptPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_prefix : ''); - input = input.replace(/{{instructSystemPromptSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence_suffix : ''); - input = input.replace(/{{(instructInput|instructUserPrefix)}}/gi, power_user.instruct.enabled ? power_user.instruct.input_sequence : ''); - input = input.replace(/{{instructUserSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.input_suffix : ''); - input = input.replace(/{{(instructOutput|instructAssistantPrefix)}}/gi, power_user.instruct.enabled ? power_user.instruct.output_sequence : ''); - input = input.replace(/{{(instructSeparator|instructAssistantSuffix)}}/gi, power_user.instruct.enabled ? power_user.instruct.output_suffix : ''); - input = input.replace(/{{instructSystemPrefix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_sequence : ''); - input = input.replace(/{{instructSystemSuffix}}/gi, power_user.instruct.enabled ? power_user.instruct.system_suffix : ''); - input = input.replace(/{{(instructFirstOutput|instructFirstAssistantPrefix)}}/gi, power_user.instruct.enabled ? (power_user.instruct.first_output_sequence || power_user.instruct.output_sequence) : ''); - input = input.replace(/{{(instructLastOutput|instructLastAssistantPrefix)}}/gi, power_user.instruct.enabled ? (power_user.instruct.last_output_sequence || power_user.instruct.output_sequence) : ''); - input = input.replace(/{{instructStop}}/gi, power_user.instruct.enabled ? power_user.instruct.stop_sequence : ''); - input = input.replace(/{{instructUserFiller}}/gi, power_user.instruct.enabled ? power_user.instruct.user_alignment_message : ''); input = input.replace(/{{exampleSeparator}}/gi, power_user.context.example_separator); input = input.replace(/{{chatStart}}/gi, power_user.context.chat_start); diff --git a/public/scripts/kai-settings.js b/public/scripts/kai-settings.js index b6d6b73b7..27a204c42 100644 --- a/public/scripts/kai-settings.js +++ b/public/scripts/kai-settings.js @@ -9,7 +9,7 @@ import { import { power_user, } from './power-user.js'; -import EventSourceStream from './sse-stream.js'; +import { getEventSourceStream } from './sse-stream.js'; import { getSortableDelay } from './utils.js'; export const kai_settings = { @@ -174,7 +174,7 @@ export async function generateKoboldWithStreaming(generate_data, signal) { tryParseStreamingError(response, await response.text()); throw new Error(`Got response status ${response.status}`); } - const eventStream = new EventSourceStream(); + const eventStream = getEventSourceStream(); response.body.pipeThrough(eventStream); const reader = eventStream.readable.getReader(); diff --git a/public/scripts/logprobs.js b/public/scripts/logprobs.js index 2aef6e61b..b2e682286 100644 --- a/public/scripts/logprobs.js +++ b/public/scripts/logprobs.js @@ -8,6 +8,7 @@ import { Generate, getGeneratingApi, is_send_press, + isStreamingEnabled, } from '../script.js'; import { debounce, delay, getStringHash } from './utils.js'; import { decodeTextTokens, getTokenizerBestMatch } from './tokenizers.js'; @@ -64,11 +65,15 @@ function renderAlternativeTokensView() { renderTopLogprobs(); const { messageLogprobs, continueFrom } = getActiveMessageLogprobData() || {}; - if (!messageLogprobs?.length) { + const usingSmoothStreaming = isStreamingEnabled() && power_user.smooth_streaming; + if (!messageLogprobs?.length || usingSmoothStreaming) { const emptyState = $('
'); + const noTokensMsg = usingSmoothStreaming + ? 'Token probabilities are not available when using Smooth Streaming.' + : 'No token probabilities available for the current message.'; const msg = power_user.request_token_probabilities - ? 'No token probabilities available for the current message.' - : `Enable Request token probabilities in the User Settings menu to use this feature.`; + ? noTokensMsg + : 'Enable Request token probabilities in the User Settings menu to use this feature.'; emptyState.html(msg); emptyState.addClass('logprobs_empty_state'); view.append(emptyState); diff --git a/public/scripts/nai-settings.js b/public/scripts/nai-settings.js index 5fcc851e4..edc69d70b 100644 --- a/public/scripts/nai-settings.js +++ b/public/scripts/nai-settings.js @@ -10,7 +10,7 @@ import { import { getCfgPrompt } from './cfg-scale.js'; import { MAX_CONTEXT_DEFAULT, MAX_RESPONSE_DEFAULT, power_user } from './power-user.js'; import { getTextTokens, tokenizers } from './tokenizers.js'; -import EventSourceStream from './sse-stream.js'; +import { getEventSourceStream } from './sse-stream.js'; import { getSortableDelay, getStringHash, @@ -614,7 +614,7 @@ export async function generateNovelWithStreaming(generate_data, signal) { tryParseStreamingError(response, await response.text()); throw new Error(`Got response status ${response.status}`); } - const eventStream = new EventSourceStream(); + const eventStream = getEventSourceStream(); response.body.pipeThrough(eventStream); const reader = eventStream.readable.getReader(); diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 0c30c0640..59256df75 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -45,7 +45,7 @@ import { import { getCustomStoppingStrings, persona_description_positions, power_user } from './power-user.js'; import { SECRET_KEYS, secret_state, writeSecret } from './secrets.js'; -import EventSourceStream from './sse-stream.js'; +import { getEventSourceStream } from './sse-stream.js'; import { delay, download, @@ -1772,7 +1772,7 @@ async function sendOpenAIRequest(type, messages, signal) { throw new Error(`Got response status ${response.status}`); } if (stream) { - const eventStream = new EventSourceStream(); + const eventStream = getEventSourceStream(); response.body.pipeThrough(eventStream); const reader = eventStream.readable.getReader(); return async function* streamData() { @@ -3661,7 +3661,7 @@ async function onModelChange() { else if (['command-light-nightly', 'command-nightly'].includes(oai_settings.cohere_model)) { $('#openai_max_context').attr('max', max_8k); } - else if (['command-r'].includes(oai_settings.cohere_model)) { + else if (['command-r', 'command-r-plus'].includes(oai_settings.cohere_model)) { $('#openai_max_context').attr('max', max_128k); } else { diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 00b0b0519..48a7ab4ef 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -118,6 +118,8 @@ let power_user = { markdown_escape_strings: '', chat_truncation: 100, streaming_fps: 30, + smooth_streaming: false, + smooth_streaming_speed: 50, ui_mode: ui_mode.POWER, fast_ui_mode: true, @@ -202,6 +204,7 @@ let power_user = { output_suffix: '', system_sequence: '', system_suffix: '', + last_system_sequence: '', first_output_sequence: '', last_output_sequence: '', system_sequence_prefix: '', @@ -255,6 +258,8 @@ let power_user = { auto_connect: false, auto_load_chat: false, forbid_external_images: false, + external_media_allowed_overrides: [], + external_media_forbidden_overrides: [], }; let themes = []; @@ -1548,6 +1553,9 @@ function loadPowerUserSettings(settings, data) { $('#streaming_fps').val(power_user.streaming_fps); $('#streaming_fps_counter').val(power_user.streaming_fps); + $('#smooth_streaming').prop('checked', power_user.smooth_streaming); + $('#smooth_streaming_speed').val(power_user.smooth_streaming_speed); + $('#font_scale').val(power_user.font_scale); $('#font_scale_counter').val(power_user.font_scale); @@ -2759,22 +2767,35 @@ export function getCustomStoppingStrings(limit = undefined) { } $(document).ready(() => { + const adjustAutocompleteDebounced = debounce(() => { + $('.ui-autocomplete-input').each(function () { + const isOpen = $(this).autocomplete('widget')[0].style.display !== 'none'; + if (isOpen) { + $(this).autocomplete('search'); + } + }); + }); - $(window).on('resize', async () => { - if (isMobile()) { - return; - } - - //console.log('Window resized!'); + const reportZoomLevelDebounced = debounce(() => { const zoomLevel = Number(window.devicePixelRatio).toFixed(2); const winWidth = window.innerWidth; const winHeight = window.innerHeight; console.debug(`Zoom: ${zoomLevel}, X:${winWidth}, Y:${winHeight}`); + }); + + $(window).on('resize', async () => { + adjustAutocompleteDebounced(); + setHotswapsDebounced(); + + if (isMobile()) { + return; + } + + reportZoomLevelDebounced(); + if (Object.keys(power_user.movingUIState).length > 0) { resetMovablePanels('resize'); } - // Adjust layout and styling here - setHotswapsDebounced(); }); // Settings that go to settings.json @@ -2945,6 +2966,16 @@ $(document).ready(() => { saveSettingsDebounced(); }); + $('#smooth_streaming').on('input', function () { + power_user.smooth_streaming = !!$(this).prop('checked'); + saveSettingsDebounced(); + }); + + $('#smooth_streaming_speed').on('input', function () { + power_user.smooth_streaming_speed = Number($('#smooth_streaming_speed').val()); + saveSettingsDebounced(); + }); + $('input[name="font_scale"]').on('input', async function (e) { power_user.font_scale = Number(e.target.value); $('#font_scale_counter').val(power_user.font_scale); diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index ede6346d8..1a28f075c 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -470,7 +470,7 @@ async function waitForConnection() { export async function initPresetManager() { eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset); registerPresetManagers(); - registerSlashCommand('preset', presetCommandCallback, [], '(name) – sets a preset by name for the current API', true, true); + registerSlashCommand('preset', presetCommandCallback, [], '(name) – sets a preset by name for the current API. Gets the current preset if no name is provided', true, true); $(document).on('click', '[data-preset-manager-update]', async function () { const apiId = $(this).data('preset-manager-update'); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index cd2cf35fb..2c9e6079f 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -254,7 +254,7 @@ parser.addCommand('inject', injectCallback, [], 'id=inje parser.addCommand('listinjects', listInjectsCallback, [], ' – lists all script injections for the current chat.', true, true); parser.addCommand('flushinjects', flushInjectsCallback, [], ' – removes all script injections for the current chat.', true, true); parser.addCommand('tokens', (_, text) => getTokenCount(text), [], '(text) – counts the number of tokens in the text.', true, true); -parser.addCommand('model', modelCallback, [], '(model name) – sets the model for the current API.', true, true); +parser.addCommand('model', modelCallback, [], '(model name) – sets the model for the current API. Gets the current model name if no argument is provided.', true, true); registerVariableCommands(); const NARRATOR_NAME_KEY = 'narrator_name'; @@ -1653,16 +1653,10 @@ function setBackgroundCallback(_, bg) { /** * Sets a model for the current API. * @param {object} _ Unused - * @param {string} model Model name - * @returns {void} + * @param {string} model New model name + * @returns {string} New or existing model name */ function modelCallback(_, model) { - if (!model) { - return; - } - - console.log('Set model to ' + model); - const modelSelectMap = [ { id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI }, { id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER }, @@ -1700,23 +1694,31 @@ function modelCallback(_, model) { if (!modelSelectItem) { toastr.info('Setting a model for your API is not supported or not implemented yet.'); - return; + return ''; } const modelSelectControl = document.getElementById(modelSelectItem); if (!(modelSelectControl instanceof HTMLSelectElement)) { toastr.error(`Model select control not found: ${main_api}[${apiSubType}]`); - return; + return ''; } const options = Array.from(modelSelectControl.options); if (!options.length) { toastr.warning('No model options found. Check your API settings.'); - return; + return ''; } + model = String(model || '').trim(); + + if (!model) { + return modelSelectControl.value; + } + + console.log('Set model to ' + model); + let newSelectedOption = null; const fuse = new Fuse(options, { keys: ['text', 'value'] }); @@ -1737,8 +1739,10 @@ function modelCallback(_, model) { modelSelectControl.value = newSelectedOption.value; $(modelSelectControl).trigger('change'); toastr.success(`Model set to "${newSelectedOption.text}"`); + return newSelectedOption.value; } else { toastr.warning(`No model found with name "${model}"`); + return ''; } } diff --git a/public/scripts/sse-stream.js b/public/scripts/sse-stream.js index c9f7158d7..9e335600d 100644 --- a/public/scripts/sse-stream.js +++ b/public/scripts/sse-stream.js @@ -1,3 +1,7 @@ +import { eventSource, event_types } from '../script.js'; +import { power_user } from './power-user.js'; +import { delay } from './utils.js'; + /** * A stream which handles Server-Sent Events from a binary ReadableStream like you get from the fetch API. */ @@ -74,4 +78,215 @@ class EventSourceStream { } } +/** + * Gets a delay based on the character. + * @param {string} s The character. + * @returns {number} The delay in milliseconds. + */ +function getDelay(s) { + if (!s) { + return 0; + } + + const speedFactor = Math.max(100 - power_user.smooth_streaming_speed, 1); + const defaultDelayMs = speedFactor * 0.4; + const punctuationDelayMs = defaultDelayMs * 25; + + if ([',', '\n'].includes(s)) { + return punctuationDelayMs / 2; + } + + if (['.', '!', '?'].includes(s)) { + return punctuationDelayMs; + } + + return defaultDelayMs; +} + +/** + * Parses the stream data and returns the parsed data and the chunk to be sent. + * @param {object} json The JSON data. + * @returns {AsyncGenerator<{data: object, chunk: string}>} The parsed data and the chunk to be sent. + */ +async function* parseStreamData(json) { + // Claude + if (typeof json.delta === 'object') { + if (typeof json.delta.text === 'string' && json.delta.text.length > 0) { + for (let i = 0; i < json.delta.text.length; i++) { + const str = json.delta.text[i]; + yield { + data: { ...json, delta: { text: str } }, + chunk: str, + }; + } + } + return; + } + // MakerSuite + else if (Array.isArray(json.candidates)) { + for (let i = 0; i < json.candidates.length; i++) { + const isNotPrimary = json.candidates?.[0]?.index > 0; + if (isNotPrimary || json.candidates.length === 0) { + return null; + } + if (typeof json.candidates[0].content === 'object' && Array.isArray(json.candidates[i].content.parts)) { + for (let j = 0; j < json.candidates[i].content.parts.length; j++) { + if (typeof json.candidates[i].content.parts[j].text === 'string') { + for (let k = 0; k < json.candidates[i].content.parts[j].text.length; k++) { + const str = json.candidates[i].content.parts[j].text[k]; + const candidateClone = structuredClone(json.candidates[0]); + candidateClone.content.parts[j].text = str; + const candidates = [candidateClone]; + yield { + data: { ...json, candidates }, + chunk: str, + }; + } + } + } + } + } + return; + } + // NovelAI / KoboldCpp Classic + else if (typeof json.token === 'string' && json.token.length > 0) { + for (let i = 0; i < json.token.length; i++) { + const str = json.token[i]; + yield { + data: { ...json, token: str }, + chunk: str, + }; + } + return; + } + // llama.cpp? + else if (typeof json.content === 'string' && json.content.length > 0) { + for (let i = 0; i < json.content.length; i++) { + const str = json.content[i]; + yield { + data: { ...json, content: str }, + chunk: str, + }; + } + return; + } + // OpenAI-likes + else if (Array.isArray(json.choices)) { + const isNotPrimary = json?.choices?.[0]?.index > 0; + if (isNotPrimary || json.choices.length === 0) { + return null; + } + + if (typeof json.choices[0].text === 'string' && json.choices[0].text.length > 0) { + for (let j = 0; j < json.choices[0].text.length; j++) { + const str = json.choices[0].text[j]; + const choiceClone = structuredClone(json.choices[0]); + choiceClone.text = str; + const choices = [choiceClone]; + yield { + data: { ...json, choices }, + chunk: str, + }; + } + return; + } + else if (typeof json.choices[0].delta === 'object') { + if (typeof json.choices[0].delta.text === 'string' && json.choices[0].delta.text.length > 0) { + for (let j = 0; j < json.choices[0].delta.text.length; j++) { + const str = json.choices[0].delta.text[j]; + const choiceClone = structuredClone(json.choices[0]); + choiceClone.delta.text = str; + const choices = [choiceClone]; + yield { + data: { ...json, choices }, + chunk: str, + }; + } + return; + } + else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) { + for (let j = 0; j < json.choices[0].delta.content.length; j++) { + const str = json.choices[0].delta.content[j]; + const choiceClone = structuredClone(json.choices[0]); + choiceClone.delta.content = str; + const choices = [choiceClone]; + yield { + data: { ...json, choices }, + chunk: str, + }; + } + return; + } + } + else if (typeof json.choices[0].message === 'object') { + if (typeof json.choices[0].message.content === 'string' && json.choices[0].message.content.length > 0) { + for (let j = 0; j < json.choices[0].message.content.length; j++) { + const str = json.choices[0].message.content[j]; + const choiceClone = structuredClone(json.choices[0]); + choiceClone.message.content = str; + const choices = [choiceClone]; + yield { + data: { ...json, choices }, + chunk: str, + }; + } + return; + } + } + } + + throw new Error('Unknown event data format'); +} + +/** + * Like the default one, but multiplies the events by the number of letters in the event data. + */ +export class SmoothEventSourceStream extends EventSourceStream { + constructor() { + super(); + let lastStr = ''; + const transformStream = new TransformStream({ + async transform(chunk, controller) { + const event = chunk; + const data = event.data; + try { + const hasFocus = document.hasFocus(); + + if (data === '[DONE]') { + lastStr = ''; + return controller.enqueue(event); + } + + const json = JSON.parse(data); + + if (!json) { + lastStr = ''; + return controller.enqueue(event); + } + + for await (const parsed of parseStreamData(json)) { + hasFocus && await delay(getDelay(lastStr)); + controller.enqueue(new MessageEvent(event.type, { data: JSON.stringify(parsed.data) })); + lastStr = parsed.chunk; + hasFocus && await eventSource.emit(event_types.SMOOTH_STREAM_TOKEN_RECEIVED, parsed.chunk); + } + } catch (error) { + console.error('Smooth Streaming parsing error', error); + controller.enqueue(event); + } + }, + }); + + this.readable = this.readable.pipeThrough(transformStream); + } +} + +export function getEventSourceStream() { + if (power_user.smooth_streaming) { + return new SmoothEventSourceStream(); + } + + return new EventSourceStream(); +} + export default EventSourceStream; diff --git a/public/scripts/tags.js b/public/scripts/tags.js index ac2e722da..7edd2feab 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -43,6 +43,9 @@ export { const CHARACTER_FILTER_SELECTOR = '#rm_characters_block .rm_tag_filter'; const GROUP_FILTER_SELECTOR = '#rm_group_chats_block .rm_tag_filter'; +const TAG_TEMPLATE = $('#tag_template .tag'); +const FOLDER_TEMPLATE = $('#bogus_folder_template .bogus_folder_select'); +const VIEW_TAG_TEMPLATE = $('#tag_view_template .tag_view_item'); function getFilterHelper(listSelector) { return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter; @@ -271,7 +274,7 @@ function getTagBlock(tag, entities, hidden = 0) { const tagFolder = TAG_FOLDER_TYPES[tag.folder_type]; - const template = $('#bogus_folder_template .bogus_folder_select').clone(); + const template = FOLDER_TEMPLATE.clone(); template.addClass(tagFolder.class); template.attr({ 'tagid': tag.id, 'id': `BogusFolder${tag.id}` }); template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`); @@ -665,7 +668,7 @@ function appendTagToList(listElement, tag, { removable = false, selectable = fal return; } - let tagElement = $('#tag_template .tag').clone(); + let tagElement = TAG_TEMPLATE.clone(); tagElement.attr('id', tag.id); //tagElement.css('color', 'var(--SmartThemeBodyColor)'); @@ -765,7 +768,9 @@ function toggleTagThreeState(element, { stateOverride = undefined, simulateClick element.toggleClass(FILTER_STATES[state].class, state === states[targetStateIndex]); }); - console.debug('toggle three-way filter from', states[currentStateIndex], 'to', states[targetStateIndex], 'on', element); + if (states[currentStateIndex] !== states[targetStateIndex]) { + console.debug('toggle three-way filter from', states[currentStateIndex], 'to', states[targetStateIndex], 'on', element); + } } @@ -1129,7 +1134,7 @@ function onTagCreateClick() { function appendViewTagToList(list, tag, everything) { const count = everything.filter(x => x == tag.id).length; - const template = $('#tag_view_template .tag_view_item').clone(); + const template = VIEW_TAG_TEMPLATE.clone(); template.attr('id', tag.id); template.find('.tag_view_counter_value').text(count); template.find('.tag_view_name').text(tag.name); @@ -1146,16 +1151,18 @@ function appendViewTagToList(list, tag, everything) { template.find('.tag_as_folder').hide(); } - template.find('.tagColorPickerHolder').html( - ``, - ); - template.find('.tagColorPicker2Holder').html( - ``, - ); + const primaryColorPicker = $('') + .addClass('tag-color') + .attr({ id: colorPickerId, color: tag.color }); + + const secondaryColorPicker = $('') + .addClass('tag-color2') + .attr({ id: colorPicker2Id, color: tag.color2 }); + + template.find('.tagColorPickerHolder').append(primaryColorPicker); + template.find('.tagColorPicker2Holder').append(secondaryColorPicker); template.find('.tag_as_folder').attr('id', tagAsFolderId); - template.find('.tag-color').attr('id', colorPickerId); - template.find('.tag-color2').attr('id', colorPicker2Id); list.append(template); diff --git a/public/scripts/templates/macros.html b/public/scripts/templates/macros.html index a0f18ab9d..f3291333f 100644 --- a/public/scripts/templates/macros.html +++ b/public/scripts/templates/macros.html @@ -60,6 +60,7 @@
  • {{instructLastAssistantPrefix}} – instruct assistant last output sequence
  • {{instructSystemPrefix}} – instruct system message prefix sequence
  • {{instructSystemSuffix}} – instruct system message suffix sequence
  • +
  • {{instructSystemInstructionPrefix}} – instruct system instruction prefix
  • {{instructUserFiller}} – instruct first user message filler
  • {{instructStop}} – instruct stop sequence
  • diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 6653149ba..f871434a3 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -12,7 +12,7 @@ import { import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasListResult } from './logit-bias.js'; import { power_user, registerDebugFunction } from './power-user.js'; -import EventSourceStream from './sse-stream.js'; +import { getEventSourceStream } from './sse-stream.js'; import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer } from './textgen-models.js'; import { SENTENCEPIECE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js'; import { getSortableDelay, onlyUnique } from './utils.js'; @@ -821,7 +821,7 @@ async function generateTextGenWithStreaming(generate_data, signal) { throw new Error(`Got response status ${response.status}`); } - const eventStream = new EventSourceStream(); + const eventStream = getEventSourceStream(); response.body.pipeThrough(eventStream); const reader = eventStream.readable.getReader(); diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 29582a1f0..a8999e228 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -42,6 +42,8 @@ const world_info_logic = { AND_ALL: 3, }; +const WI_ENTRY_EDIT_TEMPLATE = $('#entry_edit_template .world_entry'); + let world_info = {}; let selected_world_info = []; let world_names; @@ -95,6 +97,11 @@ class WorldInfoBuffer { */ #skew = 0; + /** + * @type {number} The starting depth of the global scan depth. Incremented by "min activations" feature to not repeat scans. When > 0 it means a complete scan was done up to #startDepth already, and `advanceScanPosition` was called. + */ + #startDepth = 0; + /** * Initialize the buffer with the given messages. * @param {string[]} messages Array of messages to add to the buffer @@ -137,7 +144,10 @@ class WorldInfoBuffer { * @returns {string} A slice of buffer until the given depth (inclusive) */ get(entry) { - let depth = entry.scanDepth ?? (world_info_depth + this.#skew); + let depth = entry.scanDepth ?? this.getDepth(); + if (depth <= this.#startDepth) { + return ''; + } if (depth < 0) { console.error(`Invalid WI scan depth ${depth}. Must be >= 0`); @@ -149,7 +159,7 @@ class WorldInfoBuffer { depth = MAX_SCAN_DEPTH; } - let result = this.#depthBuffer.slice(0, depth).join('\n'); + let result = this.#depthBuffer.slice(this.#startDepth, depth).join('\n'); if (this.#recurseBuffer.length > 0) { result += '\n' + this.#recurseBuffer.join('\n'); @@ -197,11 +207,19 @@ class WorldInfoBuffer { } /** - * Adds an increment to depth skew. + * Increments skew and sets startDepth to previous depth. */ - addSkew() { + advanceScanPosition() { + this.#startDepth = this.getDepth(); this.#skew++; } + + /** + * @returns {number} Settings' depth + current skew. + */ + getDepth() { + return world_info_depth + this.#skew; + } } export function getWorldInfoSettings() { @@ -783,6 +801,11 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) { afterSizeSelectorChange: function (e) { localStorage.setItem(storageKey, e.target.value); }, + afterPaging: function () { + $('#world_popup_entries_list textarea[name="comment"]').each(function () { + initScrollHeight($(this)); + }); + }, }); if (typeof navigation === 'number' && Number(navigation) >= 0) { @@ -970,7 +993,7 @@ function getWorldEntry(name, data, entry) { return; } - const template = $('#entry_edit_template .world_entry').clone(); + const template = WI_ENTRY_EDIT_TEMPLATE.clone(); template.data('uid', entry.uid); template.attr('uid', entry.uid); @@ -982,10 +1005,10 @@ function getWorldEntry(name, data, entry) { event.stopPropagation(); }); - keyInput.on('input', function () { + keyInput.on('input', function (_, { skipReset } = {}) { const uid = $(this).data('uid'); const value = String($(this).val()); - resetScrollHeight(this); + !skipReset && resetScrollHeight(this); data.entries[uid].key = value .split(',') .map((x) => x.trim()) @@ -994,7 +1017,7 @@ function getWorldEntry(name, data, entry) { setOriginalDataValue(data, uid, 'keys', data.entries[uid].key); saveWorldInfo(name, data); }); - keyInput.val(entry.key.join(', ')).trigger('input'); + keyInput.val(entry.key.join(', ')).trigger('input', { skipReset: true }); //initScrollHeight(keyInput); // logic AND/NOT @@ -1008,7 +1031,6 @@ function getWorldEntry(name, data, entry) { selectiveLogicDropdown.on('input', function () { const uid = $(this).data('uid'); const value = Number($(this).val()); - console.debug(`logic for ${entry.uid} set to ${value}`); data.entries[uid].selectiveLogic = !isNaN(value) ? value : world_info_logic.AND_ANY; setOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic); saveWorldInfo(name, data); @@ -1118,10 +1140,10 @@ function getWorldEntry(name, data, entry) { // keysecondary const keySecondaryInput = template.find('textarea[name="keysecondary"]'); keySecondaryInput.data('uid', entry.uid); - keySecondaryInput.on('input', function () { + keySecondaryInput.on('input', function (_, { skipReset } = {}) { const uid = $(this).data('uid'); const value = String($(this).val()); - resetScrollHeight(this); + !skipReset && resetScrollHeight(this); data.entries[uid].keysecondary = value .split(',') .map((x) => x.trim()) @@ -1131,17 +1153,17 @@ function getWorldEntry(name, data, entry) { saveWorldInfo(name, data); }); - keySecondaryInput.val(entry.keysecondary.join(', ')).trigger('input'); - initScrollHeight(keySecondaryInput); + keySecondaryInput.val(entry.keysecondary.join(', ')).trigger('input', { skipReset: true }); + //initScrollHeight(keySecondaryInput); // comment const commentInput = template.find('textarea[name="comment"]'); const commentToggle = template.find('input[name="addMemo"]'); commentInput.data('uid', entry.uid); - commentInput.on('input', function () { + commentInput.on('input', function (_, { skipReset } = {}) { const uid = $(this).data('uid'); const value = $(this).val(); - resetScrollHeight(this); + !skipReset && resetScrollHeight(this); data.entries[uid].comment = value; setOriginalDataValue(data, uid, 'comment', data.entries[uid].comment); @@ -1160,8 +1182,8 @@ function getWorldEntry(name, data, entry) { value ? commentContainer.show() : commentContainer.hide(); }); - commentInput.val(entry.comment).trigger('input'); - initScrollHeight(commentInput); + commentInput.val(entry.comment).trigger('input', { skipReset: true }); + //initScrollHeight(commentInput); commentToggle.prop('checked', true /* entry.addMemo */).trigger('input'); commentToggle.parent().hide(); @@ -1196,6 +1218,8 @@ function getWorldEntry(name, data, entry) { if (counter.data('first-run')) { counter.data('first-run', false); countTokensDebounced(counter, contentInput.val()); + initScrollHeight(keyInput); + initScrollHeight(keySecondaryInput); } }); @@ -1362,7 +1386,7 @@ function getWorldEntry(name, data, entry) { } const positionInput = template.find('select[name="position"]'); - initScrollHeight(positionInput); + //initScrollHeight(positionInput); positionInput.data('uid', entry.uid); positionInput.on('click', function (event) { // Prevent closing the drawer on clicking the input @@ -1419,7 +1443,6 @@ function getWorldEntry(name, data, entry) { //new tri-state selector for constant/normal/disabled const entryStateSelector = template.find('select[name="entryStateSelector"]'); entryStateSelector.data('uid', entry.uid); - console.log(entry.uid); entryStateSelector.on('click', function (event) { // Prevent closing the drawer on clicking the input event.stopPropagation(); @@ -1434,7 +1457,6 @@ function getWorldEntry(name, data, entry) { setOriginalDataValue(data, uid, 'enabled', true); setOriginalDataValue(data, uid, 'constant', true); template.removeClass('disabledWIEntry'); - console.debug('set to constant'); break; case 'normal': data.entries[uid].constant = false; @@ -1442,7 +1464,6 @@ function getWorldEntry(name, data, entry) { setOriginalDataValue(data, uid, 'enabled', true); setOriginalDataValue(data, uid, 'constant', false); template.removeClass('disabledWIEntry'); - console.debug('set to normal'); break; case 'disabled': data.entries[uid].constant = false; @@ -1450,7 +1471,6 @@ function getWorldEntry(name, data, entry) { setOriginalDataValue(data, uid, 'enabled', false); setOriginalDataValue(data, uid, 'constant', false); template.addClass('disabledWIEntry'); - console.debug('set to disabled'); break; } saveWorldInfo(name, data); @@ -1458,19 +1478,13 @@ function getWorldEntry(name, data, entry) { }); const entryState = function () { - - console.log(`constant: ${entry.constant}, disabled: ${entry.disable}`); if (entry.constant === true) { - console.debug('found constant'); return 'constant'; } else if (entry.disable === true) { - console.debug('found disabled'); return 'disabled'; } else { - console.debug('found normal'); return 'normal'; } - }; template .find(`select[name="entryStateSelector"] option[value=${entryState()}]`) @@ -1966,15 +1980,12 @@ async function getSortedEntries() { switch (Number(world_info_character_strategy)) { case world_info_insertion_strategy.evenly: - console.debug('WI using evenly'); entries = [...globalLore, ...characterLore].sort(sortFn); break; case world_info_insertion_strategy.character_first: - console.debug('WI using char first'); entries = [...characterLore.sort(sortFn), ...globalLore.sort(sortFn)]; break; case world_info_insertion_strategy.global_first: - console.debug('WI using global first'); entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)]; break; default: @@ -2009,7 +2020,6 @@ async function checkWorldInfo(chat, maxContext) { const buffer = new WorldInfoBuffer(chat); // Combine the chat - let minActivationMsgIndex = world_info_depth; // tracks chat index to satisfy `world_info_min_activations` // Add the depth or AN if enabled // Put this code here since otherwise, the chat reference is modified @@ -2102,8 +2112,6 @@ async function checkWorldInfo(chat, maxContext) { const substituted = substituteParams(key); const textToScan = buffer.get(entry); - console.debug(`${entry.uid}: ${substituted}`); - if (substituted && buffer.matchKeys(textToScan, substituted.trim(), entry)) { console.debug(`WI UID ${entry.uid} found by primary match: ${substituted}.`); @@ -2160,7 +2168,7 @@ async function checkWorldInfo(chat, maxContext) { activatedNow.add(entry); break primary; } - } else { console.debug(`No active entries for logic checks for word: ${substituted}.`); } + } } } } @@ -2225,15 +2233,14 @@ async function checkWorldInfo(chat, maxContext) { // world_info_min_activations if (!needsToScan && !token_budget_overflowed) { if (world_info_min_activations > 0 && (allActivatedEntries.size < world_info_min_activations)) { - let over_max = false; - over_max = ( + let over_max = ( world_info_min_activations_depth_max > 0 && - minActivationMsgIndex > world_info_min_activations_depth_max - ) || (minActivationMsgIndex >= chat.length); + buffer.getDepth() > world_info_min_activations_depth_max + ) || (buffer.getDepth() > chat.length); + if (!over_max) { - needsToScan = true; - minActivationMsgIndex += 1; - buffer.addSkew(); + needsToScan = true; // loop + buffer.advanceScanPosition(); } } } diff --git a/public/style.css b/public/style.css index 16a49ac9d..f9101f6fa 100644 --- a/public/style.css +++ b/public/style.css @@ -539,6 +539,7 @@ body.reduced-motion #bg_custom { margin-right: 5px; z-index: 2000; min-width: 55px; + justify-content: flex-end; } .panelControlBar .drag-grabber { diff --git a/server.js b/server.js index d57f73f72..b6f59f0a3 100644 --- a/server.js +++ b/server.js @@ -475,7 +475,15 @@ const autorunUrl = new URL( const setupTasks = async function () { const version = await getVersion(); - console.log(`SillyTavern ${version.pkgVersion}` + (version.gitBranch ? ` '${version.gitBranch}' (${version.gitRevision})` : '')); + // Print formatted header + console.log(); + console.log(`SillyTavern ${version.pkgVersion}`); + console.log(version.gitBranch ? `Running '${version.gitBranch}' (${version.gitRevision}) - ${version.commitDate}` : ''); + if (version.gitBranch && !version.isLatest && ['staging', 'release'].includes(version.gitBranch)) { + console.log('INFO: Currently not on the latest commit.'); + console.log(' Run \'git pull\' to update. If you have any merge conflicts, run \'git reset --hard\' and \'git pull\' to reset your branch.'); + } + console.log(); // TODO: do endpoint init functions depend on certain directories existing or not existing? They should be callable // in any order for encapsulation reasons, but right now it's unknown if that would break anything. diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 8fe7cb6bf..593f034b2 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -36,7 +36,13 @@ async function parseCohereStream(jsonStream, request, response) { } catch (e) { break; } - if (json.event_type === 'text-generation') { + if (json.message) { + const message = json.message || 'Unknown error'; + const chunk = { error: { message: message } }; + response.write(`data: ${JSON.stringify(chunk)}\n\n`); + partialData = ''; + break; + } else if (json.event_type === 'text-generation') { const text = json.text || ''; const chunk = { choices: [{ text }] }; response.write(`data: ${JSON.stringify(chunk)}\n\n`); diff --git a/src/util.js b/src/util.js index 4f05fc0c6..e23acb689 100644 --- a/src/util.js +++ b/src/util.js @@ -73,19 +73,31 @@ function getBasicAuthHeader(auth) { /** * Returns the version of the running instance. Get the version from the package.json file and the git revision. * Also returns the agent string for the Horde API. - * @returns {Promise<{agent: string, pkgVersion: string, gitRevision: string | null, gitBranch: string | null}>} Version info object + * @returns {Promise<{agent: string, pkgVersion: string, gitRevision: string | null, gitBranch: string | null, commitDate: string | null, isLatest: boolean}>} Version info object */ async function getVersion() { let pkgVersion = 'UNKNOWN'; let gitRevision = null; let gitBranch = null; + let commitDate = null; + let isLatest = true; + try { const pkgJson = require(path.join(process.cwd(), './package.json')); pkgVersion = pkgJson.version; if (!process['pkg'] && commandExistsSync('git')) { const git = simpleGit(); - gitRevision = await git.cwd(process.cwd()).revparse(['--short', 'HEAD']); - gitBranch = await git.cwd(process.cwd()).revparse(['--abbrev-ref', 'HEAD']); + const cwd = process.cwd(); + gitRevision = await git.cwd(cwd).revparse(['--short', 'HEAD']); + gitBranch = await git.cwd(cwd).revparse(['--abbrev-ref', 'HEAD']); + commitDate = await git.cwd(cwd).show(['-s', '--format=%ci', gitRevision]); + + const trackingBranch = await git.cwd(cwd).revparse(['--abbrev-ref', '@{u}']); + + // Might fail, but exception is caught. Just don't run anything relevant after in this block... + const localLatest = await git.cwd(cwd).revparse(['HEAD']); + const remoteLatest = await git.cwd(cwd).revparse([trackingBranch]); + isLatest = localLatest === remoteLatest; } } catch { @@ -93,7 +105,7 @@ async function getVersion() { } const agent = `SillyTavern:${pkgVersion}:Cohee#1207`; - return { agent, pkgVersion, gitRevision, gitBranch }; + return { agent, pkgVersion, gitRevision, gitBranch, commitDate: commitDate?.trim() ?? null, isLatest }; } /**