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 @@
+
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 };
}
/**