Merge branch 'staging' into release

This commit is contained in:
anachronos
2023-12-17 10:38:04 +01:00
committed by GitHub
78 changed files with 4592 additions and 2836 deletions

View File

@@ -36,6 +36,7 @@ import { chat_completion_sources, oai_settings } from './openai.js';
import { getTokenCount } from './tokenizers.js';
import { textgen_types, textgenerationwebui_settings as textgen_settings } from './textgen-settings.js';
import Bowser from '../lib/bowser.min.js';
var RPanelPin = document.getElementById('rm_button_panel_pin');
var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -98,43 +99,22 @@ export function humanizeGenTime(total_gen_time) {
return time_spent;
}
let parsedUA = null;
try {
parsedUA = Bowser.parse(navigator.userAgent);
} catch {
// In case the user agent is an empty string or Bowser can't parse it for some other reason
}
/**
* Checks if the device is a mobile device.
* @returns {boolean} - True if the device is a mobile device, false otherwise.
*/
export function isMobile() {
const mobileTypes = ['smartphone', 'tablet', 'phablet', 'feature phone', 'portable media player'];
const deviceInfo = getDeviceInfo();
const mobileTypes = ['mobile', 'tablet'];
return mobileTypes.includes(deviceInfo?.device?.type);
}
/**
* Loads device info from the server. Caches the result in sessionStorage.
* @returns {object} - The device info object.
*/
export function getDeviceInfo() {
let deviceInfo = null;
if (sessionStorage.getItem('deviceInfo')) {
deviceInfo = JSON.parse(sessionStorage.getItem('deviceInfo'));
} else {
$.ajax({
url: '/deviceinfo',
dataType: 'json',
async: false,
cache: true,
success: function (result) {
sessionStorage.setItem('deviceInfo', JSON.stringify(result));
deviceInfo = result;
},
error: function () {
console.log('Couldn\'t load device info. Defaulting to desktop');
deviceInfo = { device: { type: 'desktop' } };
},
});
}
return deviceInfo;
return mobileTypes.includes(parsedUA?.platform?.type);
}
function shouldSendOnEnter() {
@@ -415,7 +395,8 @@ function RA_autoconnect(PrevApi) {
|| (oai_settings.chat_completion_source == chat_completion_sources.WINDOWAI)
|| (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
|| (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
|| (secret_state[SECRET_KEYS.PALM] && oai_settings.chat_completion_source == chat_completion_sources.PALM)
|| (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)
|| (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)
|| (secret_state[SECRET_KEYS.TOGETHERAI] && oai_settings.chat_completion_source == chat_completion_sources.TOGETHERAI)
) {
$('#api_button_openai').trigger('click');
@@ -432,8 +413,7 @@ function RA_autoconnect(PrevApi) {
}
function OpenNavPanels() {
const deviceInfo = getDeviceInfo();
if (deviceInfo && deviceInfo.device.type === 'desktop') {
if (!isMobile()) {
//auto-open R nav if locked and previously open
if (LoadLocalBool('NavLockOn') == true && LoadLocalBool('NavOpened') == true) {
//console.log("RA -- clicking right nav to open");
@@ -509,7 +489,7 @@ export function dragElement(elmnt) {
|| Number((String(target.height).replace('px', ''))) < 50
|| Number((String(target.width).replace('px', ''))) < 50
|| power_user.movingUI === false
|| isMobile() === true
|| isMobile()
) {
console.debug('aborting mutator');
return;
@@ -717,7 +697,7 @@ export function dragElement(elmnt) {
}
export async function initMovingUI() {
if (isMobile() === false && power_user.movingUI === true) {
if (!isMobile() && power_user.movingUI === true) {
console.debug('START MOVING UI');
dragElement($('#sheld'));
dragElement($('#left-nav-panel'));
@@ -903,7 +883,7 @@ export function initRossMods() {
const chatBlock = $('#chat');
const originalScrollBottom = chatBlock[0].scrollHeight - (chatBlock.scrollTop() + chatBlock.outerHeight());
this.style.height = window.getComputedStyle(this).getPropertyValue('min-height');
this.style.height = this.scrollHeight + 0.1 + 'px';
this.style.height = this.scrollHeight + 0.3 + 'px';
if (!isFirefox) {
const newScrollTop = Math.round(chatBlock[0].scrollHeight - (chatBlock.outerHeight() + originalScrollBottom));
@@ -1124,12 +1104,14 @@ export function initRossMods() {
.not('#left-nav-panel')
.not('#right-nav-panel')
.not('#floatingPrompt')
.not('#cfgConfig')
.is(':visible')) {
let visibleDrawerContent = $('.drawer-content:visible')
.not('#WorldInfo')
.not('#left-nav-panel')
.not('#right-nav-panel')
.not('#floatingPrompt');
.not('#floatingPrompt')
.not('#cfgConfig');
$(visibleDrawerContent).parent().find('.drawer-icon').trigger('click');
return;
}
@@ -1144,6 +1126,11 @@ export function initRossMods() {
return;
}
if ($('#cfgConfig').is(':visible')) {
$('#CFGClose').trigger('click');
return;
}
if ($('#left-nav-panel').is(':visible') &&
$(LPanelPin).prop('checked') === false) {
$('#leftNavDrawerIcon').trigger('click');

View File

@@ -1,4 +1,5 @@
import {
animation_duration,
chat_metadata,
eventSource,
event_types,
@@ -312,7 +313,7 @@ export function setFloatingPrompt() {
}
}
}
context.setExtensionPrompt(MODULE_NAME, prompt, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
context.setExtensionPrompt(MODULE_NAME, prompt, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan);
$('#extension_floating_counter').text(shouldAddPrompt ? '0' : messagesTillInsertion);
}
@@ -325,7 +326,7 @@ function onANMenuItemClick() {
$('#floatingPrompt').css('opacity', 0.0);
$('#floatingPrompt').transition({
opacity: 1.0,
duration: 250,
duration: animation_duration,
}, async function () {
await delay(50);
$('#floatingPrompt').removeClass('resizing');
@@ -343,7 +344,7 @@ function onANMenuItemClick() {
$('#floatingPrompt').addClass('resizing');
$('#floatingPrompt').transition({
opacity: 0.0,
duration: 250,
duration: animation_duration,
},
async function () {
await delay(50);
@@ -351,12 +352,12 @@ function onANMenuItemClick() {
});
setTimeout(function () {
$('#floatingPrompt').hide();
}, 250);
}, animation_duration);
}
//duplicate options menu close handler from script.js
//because this listener takes priority
$('#options').stop().fadeOut(250);
$('#options').stop().fadeOut(animation_duration);
} else {
toastr.warning('Select a character before trying to use Author\'s Note', '', { timeOut: 2000 });
}
@@ -415,10 +416,10 @@ export function initAuthorsNote() {
$('#ANClose').on('click', function () {
$('#floatingPrompt').transition({
opacity: 0,
duration: 200,
duration: animation_duration,
easing: 'ease-in-out',
});
setTimeout(function () { $('#floatingPrompt').hide(); }, 200);
setTimeout(function () { $('#floatingPrompt').hide(); }, animation_duration);
});
$('#option_toggle_AN').on('click', onANMenuItemClick);

View File

@@ -1,4 +1,4 @@
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl } from '../script.js';
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
import { saveMetadataDebounced } from './extensions.js';
import { registerSlashCommand } from './slash-commands.js';
import { stringFormat } from './utils.js';
@@ -6,6 +6,19 @@ import { stringFormat } from './utils.js';
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
export let background_settings = {
name: '__transparent.png',
url: generateUrlParameter('__transparent.png', false),
};
export function loadBackgroundSettings(settings) {
let backgroundSettings = settings.background;
if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) {
backgroundSettings = background_settings;
}
setBackground(backgroundSettings.name, backgroundSettings.url);
}
/**
* Sets the background for the current chat and adds it to the list of custom backgrounds.
* @param {{url: string, path:string}} backgroundInfo
@@ -141,9 +154,8 @@ function onSelectBackgroundClick() {
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
highlightLockedBackground();
} else {
highlightLockedBackground();
}
highlightLockedBackground();
const customBg = window.getComputedStyle(document.getElementById('bg_custom')).backgroundImage;
@@ -157,8 +169,7 @@ function onSelectBackgroundClick() {
// Fetching to browser memory to reduce flicker
fetch(backgroundUrl).then(() => {
$('#bg1').css('background-image', relativeBgImage);
setBackground(bgFile);
setBackground(bgFile, relativeBgImage);
}).catch(() => {
console.log('Background could not be set: ' + backgroundUrl);
});
@@ -333,7 +344,7 @@ export async function getBackgrounds() {
'': '',
}),
});
if (response.ok === true) {
if (response.ok) {
const getData = await response.json();
//background = getData;
//console.log(getData.length);
@@ -346,7 +357,7 @@ export async function getBackgrounds() {
}
/**
* Gets the URL of the background
* Gets the CSS URL of the background
* @param {Element} block
* @returns {string} URL of the background
*/
@@ -354,6 +365,10 @@ function getUrlParameter(block) {
return $(block).closest('.bg_example').data('url');
}
function generateUrlParameter(bg, isCustom) {
return isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
}
/**
* Instantiates a background template
* @param {string} bg Path to background
@@ -363,7 +378,7 @@ function getUrlParameter(block) {
function getBackgroundFromTemplate(bg, isCustom) {
const template = $('#background_template .bg_example').clone();
const thumbPath = isCustom ? bg : getThumbnailUrl('bg', bg);
const url = isCustom ? `url("${encodeURI(bg)}")` : `url("${getBackgroundPath(bg)}")`;
const url = generateUrlParameter(bg, isCustom);
const title = isCustom ? bg.split('/').pop() : bg;
const friendlyTitle = title.slice(0, title.lastIndexOf('.'));
template.attr('title', title);
@@ -375,26 +390,11 @@ function getBackgroundFromTemplate(bg, isCustom) {
return template;
}
async function setBackground(bg) {
jQuery.ajax({
type: 'POST', //
url: '/api/backgrounds/set', //
data: JSON.stringify({
bg: bg,
}),
beforeSend: function () {
},
cache: false,
dataType: 'json',
contentType: 'application/json',
//processData: false,
success: function (html) { },
error: function (jqXHR, exception) {
console.log(exception);
console.log(jqXHR);
},
});
async function setBackground(bg, url) {
$('#bg1').css('background-image', url);
background_settings.name = bg;
background_settings.url = url;
saveSettingsDebounced();
}
async function delBackground(bg) {
@@ -435,8 +435,7 @@ function uploadBackground(formData) {
contentType: false,
processData: false,
success: async function (bg) {
setBackground(bg);
$('#bg1').css('background-image', `url("${getBackgroundPath(bg)}"`);
setBackground(bg, generateUrlParameter(bg, false));
await getBackgrounds();
highlightNewBackground(bg);
},

View File

@@ -5,6 +5,7 @@ import {
eventSource,
event_types,
saveSettingsDebounced,
animation_duration,
} from '../script.js';
import { extension_settings, saveMetadataDebounced } from './extensions.js';
import { selected_group } from './group-chats.js';
@@ -120,7 +121,7 @@ function onCfgMenuItemClick() {
$('#cfgConfig').css('opacity', 0.0);
$('#cfgConfig').transition({
opacity: 1.0,
duration: 250,
duration: animation_duration,
}, async function () {
await delay(50);
$('#cfgConfig').removeClass('resizing');
@@ -138,7 +139,7 @@ function onCfgMenuItemClick() {
$('#cfgConfig').addClass('resizing');
$('#cfgConfig').transition({
opacity: 0.0,
duration: 250,
duration: animation_duration,
},
async function () {
await delay(50);
@@ -146,12 +147,12 @@ function onCfgMenuItemClick() {
});
setTimeout(function () {
$('#cfgConfig').hide();
}, 250);
}, animation_duration);
}
//duplicate options menu close handler from script.js
//because this listener takes priority
$('#options').stop().fadeOut(250);
$('#options').stop().fadeOut(animation_duration);
} else {
toastr.warning('Select a character before trying to configure CFG', '', { timeOut: 2000 });
}
@@ -281,10 +282,10 @@ export function initCfg() {
$('#CFGClose').on('click', function () {
$('#cfgConfig').transition({
opacity: 0,
duration: 200,
duration: animation_duration,
easing: 'ease-in-out',
});
setTimeout(function () { $('#cfgConfig').hide(); }, 200);
setTimeout(function () { $('#cfgConfig').hide(); }, animation_duration);
});
$('#chat_cfg_guidance_scale').on('input', function() {

View File

@@ -341,6 +341,25 @@ function embedMessageFile(messageId, messageBlock) {
}
}
/**
* Appends file content to the message text.
* @param {object} message Message object
* @param {string} messageText Message text
* @returns {Promise<string>} Message text with file content appended.
*/
export async function appendFileContent(message, messageText) {
if (message.extra?.file) {
const fileText = message.extra.file.text || (await getFileAttachment(message.extra.file.url));
if (fileText) {
const fileWrapped = `\`\`\`\n${fileText}\n\`\`\`\n\n`;
message.extra.fileLength = fileWrapped.length;
messageText = fileWrapped + messageText;
}
}
return messageText;
}
jQuery(function () {
$(document).on('click', '.mes_hide', async function () {
const messageBlock = $(this).closest('.mes');
@@ -380,6 +399,7 @@ jQuery(function () {
$(document).on('click', '.editor_maximize', function () {
const broId = $(this).attr('data-for');
const bro = $(`#${broId}`);
const withTab = $(this).attr('data-tab');
if (!bro.length) {
console.error('Could not find editor with id', broId);
@@ -392,11 +412,41 @@ jQuery(function () {
const textarea = document.createElement('textarea');
textarea.value = String(bro.val());
textarea.classList.add('height100p', 'wide100p');
textarea.oninput = function () {
textarea.addEventListener('input', function () {
bro.val(textarea.value).trigger('input');
};
});
wrapper.appendChild(textarea);
if (withTab) {
textarea.addEventListener('keydown', (evt) => {
if (evt.key == 'Tab' && !evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
if (end - start > 0 && textarea.value.substring(start, end).includes('\n')) {
const lineStart = textarea.value.lastIndexOf('\n', start);
const count = textarea.value.substring(lineStart, end).split('\n').length - 1;
textarea.value = `${textarea.value.substring(0, lineStart)}${textarea.value.substring(lineStart, end).replace(/\n/g, '\n\t')}${textarea.value.substring(end)}`;
textarea.selectionStart = start + 1;
textarea.selectionEnd = end + count;
} else {
textarea.value = `${textarea.value.substring(0, start)}\t${textarea.value.substring(end)}`;
textarea.selectionStart = start + 1;
textarea.selectionEnd = end + 1;
}
} else if (evt.key == 'Tab' && evt.shiftKey && !evt.ctrlKey && !evt.altKey) {
evt.preventDefault();
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const lineStart = textarea.value.lastIndexOf('\n', start);
const count = textarea.value.substring(lineStart, end).split('\n\t').length - 1;
textarea.value = `${textarea.value.substring(0, lineStart)}${textarea.value.substring(lineStart, end).replace(/\n\t/g, '\n')}${textarea.value.substring(end)}`;
textarea.selectionStart = start - 1;
textarea.selectionEnd = end - count;
}
});
}
callPopup(wrapper, 'text', '', { wide: true, large: true });
});

View File

@@ -879,7 +879,7 @@ async function runGenerationInterceptors(chat, contextSize) {
exitImmediately = immediately;
};
for (const manifest of Object.values(manifests)) {
for (const manifest of Object.values(manifests).sort((a, b) => a.loading_order - b.loading_order)) {
const interceptorKey = manifest.generate_interceptor;
if (typeof window[interceptorKey] === 'function') {
try {

View File

@@ -134,7 +134,7 @@ async function doCaptionRequest(base64Img, fileData) {
case 'horde':
return await captionHorde(base64Img);
case 'multimodal':
return await captionMultimodal(fileData);
return await captionMultimodal(extension_settings.caption.multimodal_api === 'google' ? base64Img : fileData);
default:
throw new Error('Unknown caption source.');
}
@@ -273,6 +273,7 @@ jQuery(function () {
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) ||
extension_settings.caption.source === 'local' ||
extension_settings.caption.source === 'horde';
@@ -328,7 +329,7 @@ jQuery(function () {
<label for="caption_source">Source</label>
<select id="caption_source" class="text_pole">
<option value="local">Local</option>
<option value="multimodal">Multimodal (OpenAI / OpenRouter)</option>
<option value="multimodal">Multimodal (OpenAI / OpenRouter / Google)</option>
<option value="extras">Extras</option>
<option value="horde">Horde</option>
</select>
@@ -338,12 +339,14 @@ jQuery(function () {
<select id="caption_multimodal_api" class="flex1 text_pole">
<option value="openai">OpenAI</option>
<option value="openrouter">OpenRouter</option>
<option value="google">Google</option>
</select>
</div>
<div class="flex1 flex-container flexFlowColumn flexNoGap">
<label for="caption_multimodal_model">Model</label>
<select id="caption_multimodal_model" class="flex1 text_pole">
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
</select>

View File

@@ -1,6 +1,6 @@
import { getStringHash, debounce, waitUntilCondition, extractAllWords } from '../../utils.js';
import { getContext, getApiUrl, extension_settings, doExtrasFetch, modules } from '../../extensions.js';
import { eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { animation_duration, eventSource, event_types, extension_prompt_types, generateQuietPrompt, is_send_press, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { is_group_generating, selected_group } from '../../group-chats.js';
import { registerSlashCommand } from '../../slash-commands.js';
import { loadMovingUIState } from '../../power-user.js';
@@ -109,16 +109,21 @@ function loadSettings() {
$('#memory_depth').val(extension_settings.memory.depth).trigger('input');
$(`input[name="memory_position"][value="${extension_settings.memory.position}"]`).prop('checked', true).trigger('input');
$('#memory_prompt_words_force').val(extension_settings.memory.promptForceWords).trigger('input');
switchSourceControls(extension_settings.memory.source);
}
function onSummarySourceChange(event) {
const value = event.target.value;
extension_settings.memory.source = value;
switchSourceControls(value);
saveSettingsDebounced();
}
function switchSourceControls(value) {
$('#memory_settings [data-source]').each((_, element) => {
const source = $(element).data('source');
$(element).toggle(source === value);
});
saveSettingsDebounced();
}
function onMemoryShortInput() {
@@ -317,6 +322,11 @@ async function onChatEvent() {
}
async function forceSummarizeChat() {
if (extension_settings.memory.source === summary_sources.extras) {
toastr.warning('Force summarization is not supported for Extras API');
return;
}
const context = getContext();
const skipWIAN = extension_settings.memory.SkipWIAN;
@@ -589,14 +599,14 @@ function doPopout(e) {
loadSettings();
loadMovingUIState();
$('#summaryExtensionPopout').fadeIn(250);
$('#summaryExtensionPopout').fadeIn(animation_duration);
dragElement(newElement);
//setup listener for close button to restore extensions menu
$('#summaryExtensionPopoutClose').off('click').on('click', function () {
$('#summaryExtensionDrawerContents').removeClass('scrollableInnerFull');
const summaryPopoutHTML = $('#summaryExtensionDrawerContents');
$('#summaryExtensionPopout').fadeOut(250, () => {
$('#summaryExtensionPopout').fadeOut(animation_duration, () => {
originalElement.empty();
originalElement.html(summaryPopoutHTML);
$('#summaryExtensionPopout').remove();
@@ -605,7 +615,7 @@ function doPopout(e) {
});
} else {
console.debug('saw existing popout, removing');
$('#summaryExtensionPopout').fadeOut(250, () => { $('#summaryExtensionPopoutClose').trigger('click'); });
$('#summaryExtensionPopout').fadeOut(animation_duration, () => { $('#summaryExtensionPopoutClose').trigger('click'); });
}
}
@@ -659,7 +669,7 @@ jQuery(function () {
<textarea id="memory_contents" class="text_pole textarea_compact" rows="6" placeholder="Summary will be generated here..."></textarea>
<div class="memory_contents_controls">
<div id="memory_force_summarize" class="menu_button menu_button_icon">
<div id="memory_force_summarize" data-source="main" class="menu_button menu_button_icon">
<i class="fa-solid fa-database"></i>
<span>Summarize now</span>
</div>

View File

@@ -1,6 +1,6 @@
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types } from '../../../script.js';
import { saveSettingsDebounced, callPopup, getRequestHeaders, substituteParams, eventSource, event_types, animation_duration } from '../../../script.js';
import { getContext, extension_settings } from '../../extensions.js';
import { getSortableDelay, escapeHtml } from '../../utils.js';
import { getSortableDelay, escapeHtml, delay } from '../../utils.js';
import { executeSlashCommands, registerSlashCommand } from '../../slash-commands.js';
import { ContextMenu } from './src/ContextMenu.js';
import { MenuItem } from './src/MenuItem.js';
@@ -26,7 +26,7 @@ const defaultSettings = {
//method from worldinfo
async function updateQuickReplyPresetList() {
const result = await fetch('/getsettings', {
const result = await fetch('/api/settings/get', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({}),
@@ -388,7 +388,7 @@ async function doQuickReplyBarPopout() {
});
loadMovingUIState();
$('#quickReplyBarPopout').fadeIn(250);
$('#quickReplyBarPopout').fadeIn(animation_duration);
dragElement(newElement);
$('#quickReplyBarPopoutClose').off('click').on('click', function () {
@@ -396,8 +396,8 @@ async function doQuickReplyBarPopout() {
let quickRepliesClone = $('#quickReplies').html();
$('#quickReplyBar').append(newQuickRepliesDiv);
$('#quickReplies').prepend(quickRepliesClone);
$('#quickReplyBar').append(popoutButtonClone).fadeIn(250);
$('#quickReplyBarPopout').fadeOut(250, () => { $('#quickReplyBarPopout').remove(); });
$('#quickReplyBar').append(popoutButtonClone).fadeIn(animation_duration);
$('#quickReplyBarPopout').fadeOut(animation_duration, () => { $('#quickReplyBarPopout').remove(); });
$('.quickReplyButton').on('click', function () {
let index = $(this).data('index');
sendQuickReply(index);
@@ -639,7 +639,7 @@ function generateQuickReplyElements() {
<span class="drag-handle ui-sortable-handle">☰</span>
<input class="text_pole wide30p" id="quickReply${i}Label" placeholder="(Button label)">
<span class="menu_button menu_button_icon" id="quickReply${i}CtxButton" title="Additional options: context menu, auto-execution">⋮</span>
<span class="menu_button menu_button_icon editor_maximize fa-solid fa-maximize" data-for="quickReply${i}Mes" id="quickReply${i}ExpandButton" title="Expand the editor"></span>
<span class="menu_button menu_button_icon editor_maximize fa-solid fa-maximize" data-tab="true" data-for="quickReply${i}Mes" id="quickReply${i}ExpandButton" title="Expand the editor"></span>
<textarea id="quickReply${i}Mes" placeholder="(Custom message or /command)" class="text_pole widthUnset flex1" rows="2"></textarea>
</div>
`;
@@ -717,6 +717,218 @@ function saveQROrder() {
});
}
async function qrCreateCallback(args, mes) {
const qr = {
label: args.label ?? '',
mes: (mes ?? '')
.replace(/\\\|/g, '|')
.replace(/\\\{/g, '{')
.replace(/\\\}/g, '}')
,
title: args.title ?? '',
autoExecute_chatLoad: JSON.parse(args.load ?? false),
autoExecute_userMessage: JSON.parse(args.user ?? false),
autoExecute_botMessage: JSON.parse(args.bot ?? false),
autoExecute_appStartup: JSON.parse(args.startup ?? false),
hidden: JSON.parse(args.hidden ?? false),
};
const setName = args.set ?? selected_preset;
const preset = presets.find(x => x.name == setName);
if (!preset) {
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
return '';
}
preset.quickReplySlots.push(qr);
preset.numberOfSlots++;
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(preset),
});
saveSettingsDebounced();
await delay(400);
applyQuickReplyPreset(selected_preset);
return '';
}
async function qrUpdateCallback(args, mes) {
const setName = args.set ?? selected_preset;
const preset = presets.find(x => x.name == setName);
if (!preset) {
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
return '';
}
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
const oqr = preset.quickReplySlots[idx];
const qr = {
label: args.newlabel ?? oqr.label ?? '',
mes: (mes ?? oqr.mes)
.replace('\\|', '|')
.replace('\\{', '{')
.replace('\\}', '}')
,
title: args.title ?? oqr.title ?? '',
autoExecute_chatLoad: JSON.parse(args.load ?? oqr.autoExecute_chatLoad ?? false),
autoExecute_userMessage: JSON.parse(args.user ?? oqr.autoExecute_userMessage ?? false),
autoExecute_botMessage: JSON.parse(args.bot ?? oqr.autoExecute_botMessage ?? false),
autoExecute_appStartup: JSON.parse(args.startup ?? oqr.autoExecute_appStartup ?? false),
hidden: JSON.parse(args.hidden ?? oqr.hidden ?? false),
};
preset.quickReplySlots[idx] = qr;
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(preset),
});
saveSettingsDebounced();
await delay(400);
applyQuickReplyPreset(selected_preset);
return '';
}
async function qrDeleteCallback(args, label) {
const setName = args.set ?? selected_preset;
const preset = presets.find(x => x.name == setName);
if (!preset) {
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
return '';
}
const idx = preset.quickReplySlots.findIndex(x => x.label == label);
preset.quickReplySlots.splice(idx, 1);
preset.numberOfSlots--;
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(preset),
});
saveSettingsDebounced();
await delay(400);
applyQuickReplyPreset(selected_preset);
return '';
}
async function qrContextAddCallback(args, presetName) {
const setName = args.set ?? selected_preset;
const preset = presets.find(x => x.name == setName);
if (!preset) {
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
return '';
}
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
const oqr = preset.quickReplySlots[idx];
if (!oqr.contextMenu) {
oqr.contextMenu = [];
}
let item = oqr.contextMenu.find(it => it.preset == presetName);
if (item) {
item.chain = JSON.parse(args.chain ?? 'null') ?? item.chain ?? false;
} else {
oqr.contextMenu.push({ preset: presetName, chain: JSON.parse(args.chain ?? 'false') });
}
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(preset),
});
saveSettingsDebounced();
await delay(400);
applyQuickReplyPreset(selected_preset);
return '';
}
async function qrContextDeleteCallback(args, presetName) {
const setName = args.set ?? selected_preset;
const preset = presets.find(x => x.name == setName);
if (!preset) {
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
return '';
}
const idx = preset.quickReplySlots.findIndex(x => x.label == args.label);
const oqr = preset.quickReplySlots[idx];
if (!oqr.contextMenu) return;
const ctxIdx = oqr.contextMenu.findIndex(it => it.preset == presetName);
if (ctxIdx > -1) {
oqr.contextMenu.splice(ctxIdx, 1);
}
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(preset),
});
saveSettingsDebounced();
await delay(400);
applyQuickReplyPreset(selected_preset);
return '';
}
async function qrContextClearCallback(args, label) {
const setName = args.set ?? selected_preset;
const preset = presets.find(x => x.name == setName);
if (!preset) {
toastr.warning('Confirm you are using proper case sensitivity!', `QR preset '${setName}' not found`);
return '';
}
const idx = preset.quickReplySlots.findIndex(x => x.label == label);
const oqr = preset.quickReplySlots[idx];
oqr.contextMenu = [];
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(preset),
});
saveSettingsDebounced();
await delay(400);
applyQuickReplyPreset(selected_preset);
return '';
}
async function qrPresetAddCallback(args, name) {
const quickReplyPreset = {
name: name,
quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? true,
quickActionEnabled: JSON.parse(args.nosend ?? null) ?? false,
placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? false,
quickReplySlots: [],
numberOfSlots: Number(args.slots ?? '0'),
AutoInputInject: JSON.parse(args.inject ?? 'false'),
};
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(quickReplyPreset),
});
await updateQuickReplyPresetList();
}
async function qrPresetUpdateCallback(args, name) {
const preset = presets.find(it => it.name == name);
const quickReplyPreset = {
name: preset.name,
quickReplyEnabled: JSON.parse(args.enabled ?? null) ?? preset.quickReplyEnabled,
quickActionEnabled: JSON.parse(args.nosend ?? null) ?? preset.quickActionEnabled,
placeBeforeInputEnabled: JSON.parse(args.before ?? null) ?? preset.placeBeforeInputEnabled,
quickReplySlots: preset.quickReplySlots,
numberOfSlots: Number(args.slots ?? preset.numberOfSlots),
AutoInputInject: JSON.parse(args.inject ?? 'null') ?? preset.AutoInputInject,
};
Object.assign(preset, quickReplyPreset);
await fetch('/savequickreply', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(quickReplyPreset),
});
}
let onMessageSentExecuting = false;
let onMessageReceivedExecuting = false;
let onChatChangedExecuting = false;
@@ -901,4 +1113,33 @@ jQuery(async () => {
jQuery(() => {
registerSlashCommand('qr', doQR, [], '<span class="monospace">(number)</span> activates the specified Quick Reply', true, true);
registerSlashCommand('qrset', doQRPresetSwitch, [], '<span class="monospace">(name)</span> swaps to the specified Quick Reply Preset', true, true);
const qrArgs = `
label - string - text on the button, e.g., label=MyButton
set - string - name of the QR set, e.g., set=PresetName1
hidden - bool - whether the button should be hidden, e.g., hidden=true
startup - bool - auto execute on app startup, e.g., startup=true
user - bool - auto execute on user message, e.g., user=true
bot - bool - auto execute on AI message, e.g., bot=true
load - bool - auto execute on chat load, e.g., load=true
title - bool - title / tooltip to be shown on button, e.g., title="My Fancy Button"
`.trim();
const qrUpdateArgs = `
newlabel - string - new text fort the button, e.g. newlabel=MyRenamedButton
${qrArgs}
`.trim();
registerSlashCommand('qr-create', qrCreateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [message])\n arguments:\n ${qrArgs}</span> creates a new Quick Reply, example: <tt>/qr-create set=MyPreset label=MyButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-update', qrUpdateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [message])\n arguments:\n ${qrUpdateArgs}</span> updates Quick Reply, example: <tt>/qr-update set=MyPreset label=MyButton newlabel=MyRenamedButton /echo 123</tt>`, true, true);
registerSlashCommand('qr-delete', qrDeleteCallback, [], '<span class="monospace">(set=string [label])</span> deletes Quick Reply', true, true);
registerSlashCommand('qr-contextadd', qrContextAddCallback, [], '<span class="monospace">(set=string label=string chain=bool [preset name])</span> add context menu preset to a QR, example: <tt>/qr-contextadd set=MyPreset label=MyButton chain=true MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextdel', qrContextDeleteCallback, [], '<span class="monospace">(set=string label=string [preset name])</span> remove context menu preset from a QR, example: <tt>/qr-contextdel set=MyPreset label=MyButton MyOtherPreset</tt>', true, true);
registerSlashCommand('qr-contextclear', qrContextClearCallback, [], '<span class="monospace">(set=string [label])</span> remove all context menu presets from a QR, example: <tt>/qr-contextclear set=MyPreset MyButton</tt>', true, true);
const presetArgs = `
enabled - bool - enable or disable the preset
nosend - bool - disable send / insert in user input (invalid for slash commands)
before - bool - place QR before user input
slots - int - number of slots
inject - bool - inject user input automatically (if disabled use {{input}})
`.trim();
registerSlashCommand('qr-presetadd', qrPresetAddCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> create a new preset (overrides existing ones), example: <tt>/qr-presetadd slots=3 MyNewPreset</tt>`, true, true);
registerSlashCommand('qr-presetupdate', qrPresetUpdateCallback, [], `<span class="monospace" style="white-space:pre-line;">(arguments [label])\n arguments:\n ${presetArgs}</span> update an existing preset, example: <tt>/qr-presetupdate enabled=false MyPreset</tt>`, true, true);
});

View File

@@ -18,22 +18,35 @@ export async function getMultimodalCaption(base64Img, prompt) {
throw new Error('OpenRouter API key is not set.');
}
// OpenRouter has a payload limit of ~2MB
const base64Bytes = base64Img.length * 0.75;
const compressionLimit = 2 * 1024 * 1024;
if (extension_settings.caption.multimodal_api === 'openrouter' && base64Bytes > compressionLimit) {
const maxSide = 1024;
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE]) {
throw new Error('MakerSuite API key is not set.');
}
const apiResult = await fetch('/api/openai/caption-image', {
// OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
const isGoogle = extension_settings.caption.multimodal_api === 'google';
const base64Bytes = base64Img.length * 0.75;
const compressionLimit = 2 * 1024 * 1024;
if (['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) {
const maxSide = 1024;
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
if (isGoogle) {
base64Img = base64Img.split(',')[1];
}
}
const apiResult = await fetch(`/api/${isGoogle ? 'google' : 'openai'}/caption-image`, {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
image: base64Img,
prompt: prompt,
api: extension_settings.caption.multimodal_api || 'openai',
model: extension_settings.caption.multimodal_model || 'gpt-4-vision-preview',
...(isGoogle
? {}
: {
api: extension_settings.caption.multimodal_api || 'openai',
model: extension_settings.caption.multimodal_model || 'gpt-4-vision-preview',
}),
}),
});

View File

@@ -1711,7 +1711,7 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
prompt = message || getRawLastMessage();
break;
case generationMode.FREE:
prompt = trigger.trim();
prompt = generateFreeModePrompt(trigger.trim());
break;
case generationMode.FACE_MULTIMODAL:
case generationMode.CHARACTER_MULTIMODAL:
@@ -1730,6 +1730,36 @@ async function getPrompt(generationType, message, trigger, quietPrompt) {
return prompt;
}
/**
* Generates a free prompt with a character-specific prompt prefix support.
* @param {string} trigger - The prompt to use for the image generation.
* @returns {string}
*/
function generateFreeModePrompt(trigger) {
return trigger
.replace(/(?:^char(\s|,)|\{\{charPrefix\}\})/gi, (_, suffix) => {
const getLastCharacterKey = () => {
if (typeof this_chid !== 'undefined') {
return getCharaFilename(this_chid);
}
const context = getContext();
for (let i = context.chat.length - 1; i >= 0; i--) {
const message = context.chat[i];
if (message.is_user || message.is_system) {
continue;
} else if (typeof message.original_avatar === 'string') {
return message.original_avatar.replace(/\.[^/.]+$/, '');
}
}
throw new Error('No usable messages found.');
};
const key = getLastCharacterKey();
const value = (extension_settings.sd.character_prompts[key] || '').trim();
return value ? value + (suffix || '') : '';
});
}
/**
* Generates a prompt using multimodal captioning.
* @param {number} generationType - The type of image generation to perform.
@@ -1756,22 +1786,28 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
}
}
const response = await fetch(avatarUrl);
try {
const response = await fetch(avatarUrl);
if (!response.ok) {
throw new Error('Could not fetch avatar image.');
}
if (!response.ok) {
throw new Error('Could not fetch avatar image.');
}
const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob);
const avatarBlob = await response.blob();
const avatarBase64 = await getBase64Async(avatarBlob);
const caption = await getMultimodalCaption(avatarBase64, quietPrompt);
const caption = await getMultimodalCaption(avatarBase64, quietPrompt);
if (!caption) {
if (!caption) {
throw new Error('No caption returned from the API.');
}
return caption;
} catch (error) {
console.error(error);
toastr.error('Multimodal captioning failed. Please try again.', 'Image Generation');
throw new Error('Multimodal captioning failed.');
}
return caption;
}
/**
@@ -1781,7 +1817,14 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
*/
async function generatePrompt(quietPrompt) {
const reply = await generateQuietPrompt(quietPrompt, false, false);
return processReply(reply);
const processedReply = processReply(reply);
if (!processedReply) {
toastr.error('Prompt generation produced no text. Make sure you\'re using a valid instruct template and try again', 'Image Generation');
throw new Error('Prompt generation failed.');
}
return processedReply;
}
async function sendGenerationRequest(generationType, prompt, characterName = null, callback) {

View File

@@ -1,6 +1,6 @@
import { callPopup, cancelTtsPlay, eventSource, event_types, name2, saveSettingsDebounced } from '../../../script.js';
import { ModuleWorkerWrapper, doExtrasFetch, extension_settings, getApiUrl, getContext, modules } from '../../extensions.js';
import { escapeRegex, getStringHash } from '../../utils.js';
import { delay, escapeRegex, getStringHash } from '../../utils.js';
import { EdgeTtsProvider } from './edge.js';
import { ElevenLabsTtsProvider } from './elevenlabs.js';
import { SileroTtsProvider } from './silerotts.js';
@@ -482,6 +482,12 @@ async function processTtsQueue() {
console.debug('New message found, running TTS');
currentTtsJob = ttsJobQueue.shift();
let text = extension_settings.tts.narrate_translated_only ? (currentTtsJob?.extra?.display_text || currentTtsJob.mes) : currentTtsJob.mes;
if (extension_settings.tts.skip_codeblocks) {
text = text.replace(/^\s{4}.*$/gm, '').trim();
text = text.replace(/```.*?```/gs, '').trim();
}
text = extension_settings.tts.narrate_dialogues_only
? text.replace(/\*[^*]*?(\*|$)/g, '').trim() // remove asterisks content
: text.replaceAll('*', '').trim(); // remove just the asterisks
@@ -639,6 +645,11 @@ function onNarrateTranslatedOnlyClick() {
saveSettingsDebounced();
}
function onSkipCodeblocksClick() {
extension_settings.tts.skip_codeblocks = !!$('#tts_skip_codeblocks').prop('checked');
saveSettingsDebounced();
}
//##############//
// TTS Provider //
//##############//
@@ -687,7 +698,8 @@ export function saveTtsProviderSettings() {
async function onChatChanged() {
await resetTtsPlayback();
await initVoiceMap();
const voiceMapInit = initVoiceMap();
await Promise.race([voiceMapInit, delay(1000)]);
ttsLastMessage = null;
}
@@ -952,6 +964,10 @@ $(document).ready(function () {
<input type="checkbox" id="tts_narrate_translated_only">
<small>Narrate only the translated text</small>
</label>
<label class="checkbox_label" for="tts_skip_codeblocks">
<input type="checkbox" id="tts_skip_codeblocks">
<small>Skip codeblocks</small>
</label>
</div>
<div id="tts_voicemap_block">
</div>
@@ -972,6 +988,7 @@ $(document).ready(function () {
$('#tts_narrate_dialogues').on('click', onNarrateDialoguesClick);
$('#tts_narrate_quoted').on('click', onNarrateQuotedClick);
$('#tts_narrate_translated_only').on('click', onNarrateTranslatedOnlyClick);
$('#tts_skip_codeblocks').on('click', onSkipCodeblocksClick);
$('#tts_auto_generation').on('click', onAutoGenerationClick);
$('#tts_narrate_user').on('click', onNarrateUserClick);
$('#tts_voices').on('click', onTtsVoicesClick);

View File

@@ -44,7 +44,7 @@ class OpenAITtsProvider {
</div>
<div>
<label for="openai-tts-speed">Speed: <span id="openai-tts-speed-output"></span></label>
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.25">
<input type="range" id="openai-tts-speed" value="1" min="0.25" max="4" step="0.05">
</div>`;
return html;
}

View File

@@ -11,6 +11,7 @@ export const EXTENSION_PROMPT_TAG = '3_vectors';
const settings = {
// For both
source: 'transformers',
include_wi: false,
// For chats
enabled_chats: false,
@@ -254,7 +255,7 @@ async function vectorizeFile(fileText, fileName, collectionId) {
async function rearrangeChat(chat) {
try {
// Clear the extension prompt
setExtensionPrompt(EXTENSION_PROMPT_TAG, '', extension_prompt_types.IN_PROMPT, 0);
setExtensionPrompt(EXTENSION_PROMPT_TAG, '', extension_prompt_types.IN_PROMPT, 0, settings.include_wi);
if (settings.enabled_files) {
await processFiles(chat);
@@ -319,7 +320,7 @@ async function rearrangeChat(chat) {
// Format queried messages into a single string
const insertedText = getPromptText(queriedMessages);
setExtensionPrompt(EXTENSION_PROMPT_TAG, insertedText, settings.position, settings.depth);
setExtensionPrompt(EXTENSION_PROMPT_TAG, insertedText, settings.position, settings.depth, settings.include_wi);
} catch (error) {
console.error('Vectors: Failed to rearrange chat', error);
}
@@ -392,9 +393,10 @@ async function getSavedHashes(collectionId) {
* @returns {Promise<void>}
*/
async function insertVectorItems(collectionId, items) {
if ((settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI]) ||
(settings.source === 'palm' && !secret_state[SECRET_KEYS.PALM]) ||
(settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI])) {
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI]) {
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
}
@@ -575,6 +577,12 @@ jQuery(async () => {
saveSettingsDebounced();
});
$('#vectors_include_wi').prop('checked', settings.include_wi).on('input', () => {
settings.include_wi = !!$('#vectors_include_wi').prop('checked');
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
toggleSettings();
eventSource.on(event_types.MESSAGE_DELETED, onChatEvent);
eventSource.on(event_types.MESSAGE_EDITED, onChatEvent);

View File

@@ -13,6 +13,7 @@
<option value="transformers">Local (Transformers)</option>
<option value="openai">OpenAI</option>
<option value="palm">Google MakerSuite (PaLM)</option>
<option value="mistral">MistralAI</option>
<option value="togetherai">Together AI</option>
</select>
</div>
@@ -24,6 +25,11 @@
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" />
</div>
<label class="checkbox_label" for="vectors_include_wi" title="Query results can activate World Info entries.">
<input id="vectors_include_wi" type="checkbox" class="checkbox">
Include in World Info Scanning
</label>
<hr>
<h4>

View File

@@ -8,7 +8,6 @@ import {
extractAllWords,
saveBase64AsFile,
PAGINATION_TEMPLATE,
waitUntilCondition,
getBase64Async,
} from './utils.js';
import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';
@@ -46,7 +45,6 @@ import {
updateChatMetadata,
isStreamingEnabled,
getThumbnailUrl,
streamingProcessor,
getRequestHeaders,
setMenuType,
menu_type,
@@ -69,6 +67,7 @@ import {
baseChatReplace,
depth_prompt_depth_default,
loadItemizedPrompts,
animation_duration,
} from '../script.js';
import { appendTagToList, createTagMapFromList, getTagsList, applyTagsOnCharacterSelect, tag_map } from './tags.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
@@ -111,10 +110,18 @@ export const group_generation_mode = {
APPEND: 1,
};
const DEFAULT_AUTO_MODE_DELAY = 5;
export const groupCandidatesFilter = new FilterHelper(debounce(printGroupCandidates, 100));
setInterval(groupChatAutoModeWorker, 5000);
let autoModeWorker = null;
const saveGroupDebounced = debounce(async (group, reload) => await _save(group, reload), 500);
function setAutoModeWorker() {
clearInterval(autoModeWorker);
const autoModeDelay = groups.find(x => x.id === selected_group)?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY;
autoModeWorker = setInterval(groupChatAutoModeWorker, autoModeDelay * 1000);
}
async function _save(group, reload = true) {
await fetch('/api/groups/edit', {
method: 'POST',
@@ -611,14 +618,20 @@ function getGroupChatNames(groupId) {
}
async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
function throwIfAborted() {
if (params.signal instanceof AbortSignal && params.signal.aborted) {
throw new Error('AbortSignal was fired. Group generation stopped');
}
}
if (online_status === 'no_connection') {
is_group_generating = false;
setSendButtonState(false);
return;
return Promise.resolve();
}
if (is_group_generating) {
return false;
return Promise.resolve();
}
// Auto-navigate back to group menu
@@ -629,13 +642,15 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
const group = groups.find((x) => x.id === selected_group);
let typingIndicator = $('#chat .typing_indicator');
let textResult = '';
if (!group || !Array.isArray(group.members) || !group.members.length) {
sendSystemMessage(system_message_types.EMPTY, '', { isSmallSys: true });
return;
return Promise.resolve();
}
try {
throwIfAborted();
hideSwipeButtons();
is_group_generating = true;
setCharacterName('');
@@ -653,50 +668,18 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
// id of this specific batch for regeneration purposes
group_generation_id = Date.now();
const lastMessage = chat[chat.length - 1];
let messagesBefore = chat.length;
let lastMessageText = lastMessage?.mes || '';
let activationText = '';
let isUserInput = false;
let isGenerationDone = false;
let isGenerationAborted = false;
if (userInput?.length && !by_auto_mode) {
isUserInput = true;
activationText = userInput;
messagesBefore++;
} else {
if (lastMessage && !lastMessage.is_system) {
activationText = lastMessage.mes;
}
}
const resolveOriginal = params.resolve;
const rejectOriginal = params.reject;
if (params.signal instanceof AbortSignal) {
if (params.signal.aborted) {
throw new Error('Already aborted signal passed. Group generation stopped');
}
params.signal.onabort = () => {
isGenerationAborted = true;
};
}
if (typeof params.resolve === 'function') {
params.resolve = function () {
isGenerationDone = true;
resolveOriginal.apply(this, arguments);
};
}
if (typeof params.reject === 'function') {
params.reject = function () {
isGenerationDone = true;
rejectOriginal.apply(this, arguments);
};
}
const activationStrategy = Number(group.activation_strategy ?? group_activation_strategy.NATURAL);
const enabledMembers = group.members.filter(x => !group.disabled_members.includes(x));
let activatedMembers = [];
@@ -741,14 +724,12 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
// now the real generation begins: cycle through every activated character
for (const chId of activatedMembers) {
throwIfAborted();
deactivateSendButtons();
isGenerationDone = false;
const generateType = type == 'swipe' || type == 'impersonate' || type == 'quiet' || type == 'continue' ? type : 'group_chat';
setCharacterId(chId);
setCharacterName(characters[chId].name);
await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
if (type !== 'swipe' && type !== 'impersonate' && !isStreamingEnabled()) {
// update indicator and scroll down
typingIndicator
@@ -757,75 +738,9 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
typingIndicator.show();
}
// TODO: This is awful. Refactor this
while (true) {
deactivateSendButtons();
if (isGenerationAborted) {
throw new Error('Group generation aborted');
}
// if not swipe - check if message generated already
if (generateType === 'group_chat' && chat.length == messagesBefore) {
await delay(100);
}
// if swipe - see if message changed
else if (type === 'swipe') {
if (isStreamingEnabled()) {
if (streamingProcessor && !streamingProcessor.isFinished) {
await delay(100);
}
else {
break;
}
}
else {
if (lastMessageText === chat[chat.length - 1].mes) {
await delay(100);
}
else {
break;
}
}
}
else if (type === 'impersonate') {
if (isStreamingEnabled()) {
if (streamingProcessor && !streamingProcessor.isFinished) {
await delay(100);
}
else {
break;
}
}
else {
if (!$('#send_textarea').val() || $('#send_textarea').val() == userInput) {
await delay(100);
}
else {
break;
}
}
}
else if (type === 'quiet') {
if (isGenerationDone) {
break;
} else {
await delay(100);
}
}
else if (isStreamingEnabled()) {
if (streamingProcessor && !streamingProcessor.isFinished) {
await delay(100);
} else {
await waitUntilCondition(() => streamingProcessor == null, 1000, 10);
messagesBefore++;
break;
}
}
else {
messagesBefore++;
break;
}
}
// Wait for generation to finish
const generateFinished = await Generate(generateType, { automatic_trigger: by_auto_mode, ...(params || {}) });
textResult = await generateFinished;
}
} finally {
typingIndicator.hide();
@@ -838,6 +753,8 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) {
activateSendButtons();
showSwipeButtons();
}
return Promise.resolve(textResult);
}
function getLastMessageGenerationId() {
@@ -860,12 +777,35 @@ function activateImpersonate(members) {
return memberIds;
}
/**
* Activates a group member based on the last message.
* @param {string[]} members Array of group member avatar ids
* @returns {number[]} Array of character ids
*/
function activateSwipe(members) {
let activatedNames = [];
const lastMessage = chat[chat.length - 1];
if (lastMessage.is_user || lastMessage.is_system || lastMessage.extra?.type === system_message_types.NARRATOR) {
for (const message of chat.slice().reverse()) {
if (message.is_user || message.is_system || message.extra?.type === system_message_types.NARRATOR) {
continue;
}
if (message.original_avatar) {
activatedNames.push(message.original_avatar);
break;
}
}
if (activatedNames.length === 0) {
activatedNames.push(shuffle(members.slice())[0]);
}
}
// pre-update group chat swipe
if (!chat[chat.length - 1].original_avatar) {
const matches = characters.filter(x => x.name == chat[chat.length - 1].name);
if (!lastMessage.original_avatar) {
const matches = characters.filter(x => x.name == lastMessage.name);
for (const match of matches) {
if (members.includes(match.avatar)) {
@@ -875,7 +815,7 @@ function activateSwipe(members) {
}
}
else {
activatedNames.push(chat[chat.length - 1].original_avatar);
activatedNames.push(lastMessage.original_avatar);
}
const memberIds = activatedNames
@@ -1103,6 +1043,15 @@ async function onGroupGenerationModeInput(e) {
}
}
async function onGroupAutoModeDelayInput(e) {
if (openGroupId) {
let _thisGroup = groups.find((x) => x.id == openGroupId);
_thisGroup.auto_mode_delay = Number(e.target.value);
await editGroup(openGroupId, false, false);
setAutoModeWorker();
}
}
async function onGroupNameInput() {
if (openGroupId) {
let _thisGroup = groups.find((x) => x.id == openGroupId);
@@ -1299,6 +1248,7 @@ function select_group_chats(groupId, skipAnimation) {
$('#rm_group_submit').prop('disabled', !groupHasMembers);
$('#rm_group_allow_self_responses').prop('checked', group && group.allow_self_responses);
$('#rm_group_hidemutedsprites').prop('checked', group && group.hideMutedSprites);
$('#rm_group_automode_delay').val(group?.auto_mode_delay ?? DEFAULT_AUTO_MODE_DELAY);
// bottom buttons
if (openGroupId) {
@@ -1317,6 +1267,7 @@ function select_group_chats(groupId, skipAnimation) {
}
updateFavButtonState(group?.fav ?? false);
setAutoModeWorker();
// top bar
if (group) {
@@ -1509,6 +1460,7 @@ async function createGroup() {
let allowSelfResponses = !!$('#rm_group_allow_self_responses').prop('checked');
let activationStrategy = Number($('#rm_group_activation_strategy').find(':selected').val()) ?? group_activation_strategy.NATURAL;
let generationMode = Number($('#rm_group_generation_mode').find(':selected').val()) ?? group_generation_mode.SWAP;
let autoModeDelay = Number($('#rm_group_automode_delay').val()) ?? DEFAULT_AUTO_MODE_DELAY;
const members = newGroupMembers;
const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', ');
@@ -1537,6 +1489,7 @@ async function createGroup() {
fav: fav_grp_checked,
chat_id: chatName,
chats: chats,
auto_mode_delay: autoModeDelay,
}),
});
@@ -1768,17 +1721,17 @@ function doCurMemberListPopout() {
$('body').append(newElement);
loadMovingUIState();
$('#groupMemberListPopout').fadeIn(250);
$('#groupMemberListPopout').fadeIn(animation_duration);
dragElement(newElement);
$('#groupMemberListPopoutClose').off('click').on('click', function () {
$('#groupMemberListPopout').fadeOut(250, () => { $('#groupMemberListPopout').remove(); });
$('#groupMemberListPopout').fadeOut(animation_duration, () => { $('#groupMemberListPopout').remove(); });
});
// Re-add pagination not working in popout
printGroupMembers();
} else {
console.debug('saw existing popout, removing');
$('#groupMemberListPopout').fadeOut(250, () => { $('#groupMemberListPopout').remove(); });
$('#groupMemberListPopout').fadeOut(animation_duration, () => { $('#groupMemberListPopout').remove(); });
}
}
@@ -1809,6 +1762,7 @@ jQuery(() => {
$('#rm_group_allow_self_responses').on('input', onGroupSelfResponsesClick);
$('#rm_group_activation_strategy').on('change', onGroupActivationStrategyInput);
$('#rm_group_generation_mode').on('change', onGroupGenerationModeInput);
$('#rm_group_automode_delay').on('input', onGroupAutoModeDelayInput);
$('#group_avatar_button').on('input', uploadGroupAvatar);
$('#rm_group_restore_avatar').on('click', restoreGroupAvatar);
$(document).on('click', '.group_member .right_menu_button', onGroupActionClick);

View File

@@ -9,7 +9,7 @@ import {
} from '../script.js';
import { SECRET_KEYS, writeSecret } from './secrets.js';
import { delay } from './utils.js';
import { getDeviceInfo } from './RossAscends-mods.js';
import { isMobile } from './RossAscends-mods.js';
import { autoSelectInstructPreset } from './instruct-mode.js';
export {
@@ -41,7 +41,7 @@ const getRequestArgs = () => ({
},
});
async function getWorkers() {
async function getWorkers(workerType) {
const response = await fetch('https://horde.koboldai.net/api/v2/workers?type=text', getRequestArgs());
const data = await response.json();
return data;
@@ -303,8 +303,7 @@ jQuery(function () {
$('#horde_kudos').on('click', showKudos);
// Not needed on mobile
const deviceInfo = getDeviceInfo();
if (deviceInfo && deviceInfo.device.type === 'desktop') {
if (!isMobile()) {
$('#horde_model').select2({
width: '100%',
placeholder: 'Select Horde models',

View File

@@ -10,6 +10,7 @@ import {
import {
power_user,
} from './power-user.js';
import EventSourceStream from './sse-stream.js';
import { getSortableDelay } from './utils.js';
export const kai_settings = {
@@ -128,13 +129,6 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
top_p: kai_settings.top_p,
min_p: (kai_flags.can_use_min_p || isHorde) ? kai_settings.min_p : undefined,
typical: kai_settings.typical,
s1: sampler_order[0],
s2: sampler_order[1],
s3: sampler_order[2],
s4: sampler_order[3],
s5: sampler_order[4],
s6: sampler_order[5],
s7: sampler_order[6],
use_world_info: false,
singleline: false,
stop_sequence: (kai_flags.can_use_stop_sequence || isHorde) ? getStoppingStrings(isImpersonate, isContinue) : undefined,
@@ -153,44 +147,50 @@ export function getKoboldGenerationData(finalPrompt, settings, maxLength, maxCon
return generate_data;
}
function tryParseStreamingError(response, decoded) {
try {
const data = JSON.parse(decoded);
if (!data) {
return;
}
if (data.error) {
toastr.error(data.error.message || response.statusText, 'KoboldAI API');
throw new Error(data);
}
}
catch {
// No JSON. Do nothing.
}
}
export async function generateKoboldWithStreaming(generate_data, signal) {
const response = await fetch('/generate', {
const response = await fetch('/api/backends/kobold/generate', {
headers: getRequestHeaders(),
body: JSON.stringify(generate_data),
method: 'POST',
signal: signal,
});
if (!response.ok) {
tryParseStreamingError(response, await response.text());
throw new Error(`Got response status ${response.status}`);
}
const eventStream = new EventSourceStream();
response.body.pipeThrough(eventStream);
const reader = eventStream.readable.getReader();
return async function* streamData() {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = '';
let messageBuffer = '';
let text = '';
while (true) {
const { done, value } = await reader.read();
let response = decoder.decode(value);
let eventList = [];
if (done) return;
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
messageBuffer += response;
eventList = messageBuffer.split('\n\n');
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();
for (let event of eventList) {
for (let subEvent of event.split('\n')) {
if (subEvent.startsWith('data')) {
let data = JSON.parse(subEvent.substring(5));
getMessage += (data?.token || '');
yield { text: getMessage, swipes: [] };
}
}
}
if (done) {
return;
const data = JSON.parse(value.data);
if (data?.token) {
text += data.token;
}
yield { text, swipes: [] };
}
};
}
@@ -310,87 +310,24 @@ const sliders = [
},
];
export function setKoboldFlags(version, koboldVersion) {
kai_flags.can_use_stop_sequence = canUseKoboldStopSequence(version);
kai_flags.can_use_streaming = canUseKoboldStreaming(koboldVersion);
kai_flags.can_use_tokenization = canUseKoboldTokenization(koboldVersion);
kai_flags.can_use_default_badwordsids = canUseDefaultBadwordIds(version);
kai_flags.can_use_mirostat = canUseMirostat(koboldVersion);
kai_flags.can_use_grammar = canUseGrammar(koboldVersion);
kai_flags.can_use_min_p = canUseMinP(koboldVersion);
export function setKoboldFlags(koboldUnitedVersion, koboldCppVersion) {
kai_flags.can_use_stop_sequence = versionCompare(koboldUnitedVersion, MIN_STOP_SEQUENCE_VERSION);
kai_flags.can_use_streaming = versionCompare(koboldCppVersion, MIN_STREAMING_KCPPVERSION);
kai_flags.can_use_tokenization = versionCompare(koboldCppVersion, MIN_TOKENIZATION_KCPPVERSION);
kai_flags.can_use_default_badwordsids = versionCompare(koboldUnitedVersion, MIN_UNBAN_VERSION);
kai_flags.can_use_mirostat = versionCompare(koboldCppVersion, MIN_MIROSTAT_KCPPVERSION);
kai_flags.can_use_grammar = versionCompare(koboldCppVersion, MIN_GRAMMAR_KCPPVERSION);
kai_flags.can_use_min_p = versionCompare(koboldCppVersion, MIN_MIN_P_KCPPVERSION);
}
/**
* Determines if the Kobold stop sequence can be used with the given version.
* @param {string} version KoboldAI version to check.
* @returns {boolean} True if the Kobold stop sequence can be used, false otherwise.
* Compares two version numbers, returning true if srcVersion >= minVersion
* @param {string} srcVersion The current version.
* @param {string} minVersion The target version number to test against
* @returns {boolean} True if srcVersion >= minVersion, false if not
*/
function canUseKoboldStopSequence(version) {
return (version || '0.0.0').localeCompare(MIN_STOP_SEQUENCE_VERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
}
/**
* Determines if the Kobold default badword ids can be used with the given version.
* @param {string} version KoboldAI version to check.
* @returns {boolean} True if the Kobold default badword ids can be used, false otherwise.
*/
function canUseDefaultBadwordIds(version) {
return (version || '0.0.0').localeCompare(MIN_UNBAN_VERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
}
/**
* Determines if the Kobold streaming API can be used with the given version.
* @param {{ result: string; version: string; }} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold streaming API can be used, false otherwise.
*/
function canUseKoboldStreaming(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_STREAMING_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
}
/**
* Determines if the Kobold tokenization API can be used with the given version.
* @param {{ result: string; version: string; }} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold tokenization API can be used, false otherwise.
*/
function canUseKoboldTokenization(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_TOKENIZATION_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
}
/**
* Determines if the Kobold mirostat can be used with the given version.
* @param {{result: string; version: string;}} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold mirostat API can be used, false otherwise.
*/
function canUseMirostat(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_MIROSTAT_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
}
/**
* Determines if the Kobold grammar can be used with the given version.
* @param {{result: string; version:string;}} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold grammar can be used, false otherwise.
*/
function canUseGrammar(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_GRAMMAR_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
}
/**
* Determines if the Kobold min_p can be used with the given version.
* @param {{result:string, version:string;}} koboldVersion KoboldAI version object.
* @returns {boolean} True if the Kobold min_p can be used, false otherwise.
*/
function canUseMinP(koboldVersion) {
if (koboldVersion && koboldVersion.result == 'KoboldCpp') {
return (koboldVersion.version || '0.0').localeCompare(MIN_MIN_P_KCPPVERSION, undefined, { numeric: true, sensitivity: 'base' }) > -1;
} else return false;
function versionCompare(srcVersion, minVersion) {
return (srcVersion || '0.0.0').localeCompare(minVersion, undefined, { numeric: true, sensitivity: 'base' }) > -1;
}
/**

View File

@@ -1,5 +1,5 @@
import { setGenerationParamsFromPreset } from '../script.js';
import { getDeviceInfo } from './RossAscends-mods.js';
import { isMobile } from './RossAscends-mods.js';
import { textgenerationwebui_settings as textgen_settings } from './textgen-settings.js';
let models = [];
@@ -52,8 +52,7 @@ function getMancerModelTemplate(option) {
jQuery(function () {
$('#mancer_model').on('change', onMancerModelSelect);
const deviceInfo = getDeviceInfo();
if (deviceInfo && deviceInfo.device.type === 'desktop') {
if (!isMobile()) {
$('#mancer_model').select2({
placeholder: 'Select a model',
searchInputPlaceholder: 'Search models...',

View File

@@ -10,6 +10,7 @@ import {
import { getCfgPrompt } from './cfg-scale.js';
import { MAX_CONTEXT_DEFAULT, MAX_RESPONSE_DEFAULT } from './power-user.js';
import { getTextTokens, tokenizers } from './tokenizers.js';
import EventSourceStream from './sse-stream.js';
import {
getSortableDelay,
getStringHash,
@@ -663,7 +664,7 @@ export function adjustNovelInstructionPrompt(prompt) {
return stripedPrompt;
}
function tryParseStreamingError(decoded) {
function tryParseStreamingError(response, decoded) {
try {
const data = JSON.parse(decoded);
@@ -671,8 +672,8 @@ function tryParseStreamingError(decoded) {
return;
}
if (data.message && data.statusCode >= 400) {
toastr.error(data.message, 'Error');
if (data.message || data.error) {
toastr.error(data.message || data.error?.message || response.statusText, 'NovelAI API');
throw new Error(data);
}
}
@@ -690,39 +691,27 @@ export async function generateNovelWithStreaming(generate_data, signal) {
method: 'POST',
signal: signal,
});
if (!response.ok) {
tryParseStreamingError(response, await response.text());
throw new Error(`Got response status ${response.status}`);
}
const eventStream = new EventSourceStream();
response.body.pipeThrough(eventStream);
const reader = eventStream.readable.getReader();
return async function* streamData() {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = '';
let messageBuffer = '';
let text = '';
while (true) {
const { done, value } = await reader.read();
let decoded = decoder.decode(value);
let eventList = [];
if (done) return;
tryParseStreamingError(decoded);
const data = JSON.parse(value.data);
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
messageBuffer += decoded;
eventList = messageBuffer.split('\n\n');
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();
for (let event of eventList) {
for (let subEvent of event.split('\n')) {
if (subEvent.startsWith('data')) {
let data = JSON.parse(subEvent.substring(5));
getMessage += (data?.token || '');
yield { text: getMessage, swipes: [] };
}
}
if (data.token) {
text += data.token;
}
if (done) {
return;
}
yield { text, swipes: [] };
}
};
}

View File

@@ -37,13 +37,14 @@ import {
chatCompletionDefaultPrompts,
INJECTION_POSITION,
Prompt,
promptManagerDefaultPromptOrders,
PromptManager,
promptManagerDefaultPromptOrders,
} from './PromptManager.js';
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 {
delay,
download,
@@ -113,7 +114,6 @@ const max_128k = 128 * 1000;
const max_200k = 200 * 1000;
const scale_max = 8191;
const claude_max = 9000; // We have a proper tokenizer, so theoretically could be larger (up to 9k)
const palm2_max = 7400; // The real context window is 8192, spare some for padding due to using turbo tokenizer
const claude_100k_max = 99000;
let ai21_max = 9200; //can easily fit 9k gpt tokens because j2's tokenizer is efficient af
const unlocked_max = 100 * 1024;
@@ -163,7 +163,8 @@ export const chat_completion_sources = {
SCALE: 'scale',
OPENROUTER: 'openrouter',
AI21: 'ai21',
PALM: 'palm',
MAKERSUITE: 'makersuite',
MISTRALAI: 'mistralai',
TOGETHERAI: 'togetherai',
};
@@ -207,8 +208,10 @@ const default_settings = {
personality_format: default_personality_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-instant-v1',
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
togetherai_model: 'togethercomputer/GPT-NeoXT-Chat-Base-20B', // unsure here
mistralai_model: 'mistral-medium',
windowai_model: '',
openrouter_model: openrouter_website_model,
openrouter_use_fallback: false,
@@ -217,7 +220,6 @@ const default_settings = {
openrouter_sort_models: 'alphabetically',
jailbreak_system: false,
reverse_proxy: '',
legacy_streaming: false,
chat_completion_source: chat_completion_sources.OPENAI,
max_context_unlocked: false,
api_url_scale: '',
@@ -225,6 +227,7 @@ const default_settings = {
proxy_password: '',
assistant_prefill: '',
use_ai21_tokenizer: false,
use_google_tokenizer: false,
exclude_assistant: false,
use_alt_scale: false,
squash_system_messages: false,
@@ -262,8 +265,10 @@ const oai_settings = {
personality_format: default_personality_format,
openai_model: 'gpt-3.5-turbo',
claude_model: 'claude-instant-v1',
google_model: 'gemini-pro',
ai21_model: 'j2-ultra',
togetherai_model: 'togethercomputer/GPT-NeoXT-Chat-Base-20B', // unsure here
mistralai_model: 'mistral-medium',
windowai_model: '',
openrouter_model: openrouter_website_model,
openrouter_use_fallback: false,
@@ -272,7 +277,6 @@ const oai_settings = {
openrouter_sort_models: 'alphabetically',
jailbreak_system: false,
reverse_proxy: '',
legacy_streaming: false,
chat_completion_source: chat_completion_sources.OPENAI,
max_context_unlocked: false,
api_url_scale: '',
@@ -280,6 +284,7 @@ const oai_settings = {
proxy_password: '',
assistant_prefill: '',
use_ai21_tokenizer: false,
use_google_tokenizer: false,
exclude_assistant: false,
use_alt_scale: false,
squash_system_messages: false,
@@ -917,6 +922,7 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
const scenarioText = Scenario && oai_settings.scenario_format ? substituteParams(oai_settings.scenario_format) : '';
const charPersonalityText = charPersonality && oai_settings.personality_format ? substituteParams(oai_settings.personality_format) : '';
const groupNudge = substituteParams(oai_settings.group_nudge_prompt);
const impersonationPrompt = oai_settings.impersonation_prompt ? substituteParams(oai_settings.impersonation_prompt) : '';
// Create entries for system prompts
const systemPrompts = [
@@ -928,7 +934,7 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
{ role: 'system', content: scenarioText, identifier: 'scenario' },
{ role: 'system', content: personaDescription, identifier: 'personaDescription' },
// Unordered prompts without marker
{ role: 'system', content: oai_settings.impersonation_prompt, identifier: 'impersonate' },
{ role: 'system', content: impersonationPrompt, identifier: 'impersonate' },
{ role: 'system', content: quietPrompt, identifier: 'quietPrompt' },
{ role: 'system', content: bias, identifier: 'bias' },
{ role: 'system', content: groupNudge, identifier: 'groupNudge' },
@@ -1127,7 +1133,7 @@ function tryParseStreamingError(response, decoded) {
checkQuotaError(data);
if (data.error) {
toastr.error(data.error.message || response.statusText, 'API returned an error');
toastr.error(data.error.message || response.statusText, 'Chat Completion API');
throw new Error(data);
}
}
@@ -1255,14 +1261,16 @@ function getChatCompletionModel() {
return oai_settings.windowai_model;
case chat_completion_sources.SCALE:
return '';
case chat_completion_sources.PALM:
return '';
case chat_completion_sources.MAKERSUITE:
return oai_settings.google_model;
case chat_completion_sources.OPENROUTER:
return oai_settings.openrouter_model !== openrouter_website_model ? oai_settings.openrouter_model : null;
case chat_completion_sources.AI21:
return oai_settings.ai21_model;
case chat_completion_sources.TOGETHERAI:
return oai_settings.togetherai_model;
case chat_completion_sources.MISTRALAI:
return oai_settings.mistralai_model;
default:
throw new Error(`Unknown chat completion source: ${oai_settings.chat_completion_source}`);
}
@@ -1387,7 +1395,7 @@ function openRouterGroupByVendor(array) {
}
async function sendAltScaleRequest(messages, logit_bias, signal, type) {
const generate_url = '/generate_altscale';
const generate_url = '/api/backends/scale-alt/generate';
let firstSysMsgs = [];
for (let msg of messages) {
@@ -1448,21 +1456,22 @@ async function sendOpenAIRequest(type, messages, signal) {
const isOpenRouter = oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER;
const isScale = oai_settings.chat_completion_source == chat_completion_sources.SCALE;
const isAI21 = oai_settings.chat_completion_source == chat_completion_sources.AI21;
const isPalm = oai_settings.chat_completion_source == chat_completion_sources.PALM;
const isGoogle = oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE;
const isOAI = oai_settings.chat_completion_source == chat_completion_sources.OPENAI;
const isTogetherAI = oai_settings.chat_completion_source == chat_completion_sources.TOGETHERAI;
const isMistral = oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI;
const isTextCompletion = (isOAI && textCompletionModels.includes(oai_settings.openai_model)) || (isOpenRouter && oai_settings.openrouter_force_instruct && power_user.instruct.enabled);
const isQuiet = type === 'quiet';
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
const stream = oai_settings.stream_openai && !isQuiet && !isScale && !isAI21 && !isPalm;
const stream = oai_settings.stream_openai && !isQuiet && !isScale && !isAI21 && !(isGoogle && oai_settings.google_model.includes('bison'));
if (isTextCompletion && isOpenRouter) {
messages = convertChatCompletionToInstruct(messages, type);
replaceItemizedPromptText(messageId, messages);
}
if (isAI21 || isPalm) {
if (isAI21) {
const joinedMsgs = messages.reduce((acc, obj) => {
const prefix = prefixMap[obj.role];
return acc + (prefix ? (selected_group ? '\n' : prefix + ' ') : '') + obj.content + '\n';
@@ -1545,7 +1554,7 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['api_url_scale'] = oai_settings.api_url_scale;
}
if (isPalm) {
if (isGoogle) {
const nameStopString = isImpersonate ? `\n${name2}:` : `\n${name1}:`;
const stopStringsLimit = 3; // 5 - 2 (nameStopString and new_chat_prompt)
generate_data['top_k'] = Number(oai_settings.top_k_openai);
@@ -1558,11 +1567,15 @@ async function sendOpenAIRequest(type, messages, signal) {
generate_data['stop_tokens'] = [name1 + ':', oai_settings.new_chat_prompt, oai_settings.new_group_chat_prompt];
}
if ((isOAI || isOpenRouter || isTogetherAI) && oai_settings.seed >= 0) {
if (isMistral) {
generate_data['safe_mode'] = false; // already defaults to false, but just incase they change that in the future.
}
if ((isOAI || isOpenRouter || isMistral || isTogetherAI) && oai_settings.seed >= 0) {
generate_data['seed'] = oai_settings.seed;
}
const generate_url = '/generate_openai';
const generate_url = '/api/backends/chat-completions/generate';
const response = await fetch(generate_url, {
method: 'POST',
body: JSON.stringify(generate_data),
@@ -1570,58 +1583,31 @@ async function sendOpenAIRequest(type, messages, signal) {
signal: signal,
});
if (!response.ok) {
tryParseStreamingError(response, await response.text());
throw new Error(`Got response status ${response.status}`);
}
if (stream) {
let reader;
let isSSEStream = oai_settings.chat_completion_source !== chat_completion_sources.MAKERSUITE;
if (isSSEStream) {
const eventStream = new EventSourceStream();
response.body.pipeThrough(eventStream);
reader = eventStream.readable.getReader();
} else {
reader = response.body.getReader();
}
return async function* streamData() {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = '';
let messageBuffer = '';
let text = '';
let utf8Decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
let decoded = decoder.decode(value);
// Claude's streaming SSE messages are separated by \r
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
decoded = decoded.replace(/\r/g, '');
}
tryParseStreamingError(response, decoded);
let eventList = [];
// ReadableStream's buffer is not guaranteed to contain full SSE messages as they arrive in chunks
// We need to buffer chunks until we have one or more full messages (separated by double newlines)
if (!oai_settings.legacy_streaming) {
messageBuffer += decoded;
eventList = messageBuffer.split('\n\n');
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();
} else {
eventList = decoded.split('\n');
}
for (let event of eventList) {
if (event.startsWith('event: completion')) {
event = event.split('\n')[1];
}
if (typeof event !== 'string' || !event.length)
continue;
if (!event.startsWith('data'))
continue;
if (event == 'data: [DONE]') {
return;
}
let data = JSON.parse(event.substring(6));
// the first and last messages are undefined, protect against that
getMessage = getStreamingReply(getMessage, data);
yield { text: getMessage, swipes: [] };
}
if (done) {
return;
}
if (done) return;
const rawData = isSSEStream ? value.data : utf8Decoder.decode(value, { stream: true });
if (isSSEStream && rawData === '[DONE]') return;
tryParseStreamingError(response, rawData);
text += getStreamingReply(JSON.parse(rawData));
yield { text, swipes: [] };
}
};
}
@@ -1639,13 +1625,14 @@ async function sendOpenAIRequest(type, messages, signal) {
}
}
function getStreamingReply(getMessage, data) {
function getStreamingReply(data) {
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
getMessage += data?.completion || '';
return data?.completion || '';
} else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
return data?.candidates[0].content.parts[0].text || '';
} else {
getMessage += data.choices[0]?.delta?.content || data.choices[0]?.message?.content || data.choices[0]?.text || '';
return data.choices[0]?.delta?.content || data.choices[0]?.message?.content || data.choices[0]?.text || '';
}
return getMessage;
}
function handleWindowError(err) {
@@ -1683,7 +1670,7 @@ async function calculateLogitBias() {
let result = {};
try {
const reply = await fetch(`/openai_bias?model=${getTokenizerModel()}`, {
const reply = await fetch(`/api/backends/chat-completions/bias?model=${getTokenizerModel()}`, {
method: 'POST',
headers: getRequestHeaders(),
body,
@@ -1824,13 +1811,15 @@ class Message {
async addImage(image) {
const textContent = this.content;
const isDataUrl = isDataURL(image);
if (!isDataUrl) {
try {
const response = await fetch(image, { method: 'GET', cache: 'force-cache' });
if (!response.ok) throw new Error('Failed to fetch image');
const blob = await response.blob();
image = await getBase64Async(blob);
if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
image = image.split(',')[1];
}
} catch (error) {
console.error('Image adding skipped', error);
return;
@@ -2313,7 +2302,6 @@ function loadOpenAISettings(data, settings) {
oai_settings.openai_max_tokens = settings.openai_max_tokens ?? default_settings.openai_max_tokens;
oai_settings.bias_preset_selected = settings.bias_preset_selected ?? default_settings.bias_preset_selected;
oai_settings.bias_presets = settings.bias_presets ?? default_settings.bias_presets;
oai_settings.legacy_streaming = settings.legacy_streaming ?? default_settings.legacy_streaming;
oai_settings.max_context_unlocked = settings.max_context_unlocked ?? default_settings.max_context_unlocked;
oai_settings.send_if_empty = settings.send_if_empty ?? default_settings.send_if_empty;
oai_settings.wi_format = settings.wi_format ?? default_settings.wi_format;
@@ -2328,6 +2316,8 @@ function loadOpenAISettings(data, settings) {
oai_settings.openrouter_use_fallback = settings.openrouter_use_fallback ?? default_settings.openrouter_use_fallback;
oai_settings.openrouter_force_instruct = settings.openrouter_force_instruct ?? default_settings.openrouter_force_instruct;
oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model;
oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model;
oai_settings.google_model = settings.google_model ?? default_settings.google_model;
oai_settings.chat_completion_source = settings.chat_completion_source ?? default_settings.chat_completion_source;
oai_settings.api_url_scale = settings.api_url_scale ?? default_settings.api_url_scale;
oai_settings.show_external_models = settings.show_external_models ?? default_settings.show_external_models;
@@ -2350,6 +2340,7 @@ function loadOpenAISettings(data, settings) {
if (settings.names_in_completion !== undefined) oai_settings.names_in_completion = !!settings.names_in_completion;
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
if (settings.use_ai21_tokenizer !== undefined) { oai_settings.use_ai21_tokenizer = !!settings.use_ai21_tokenizer; oai_settings.use_ai21_tokenizer ? ai21_max = 8191 : ai21_max = 9200; }
if (settings.use_google_tokenizer !== undefined) oai_settings.use_google_tokenizer = !!settings.use_google_tokenizer;
if (settings.exclude_assistant !== undefined) oai_settings.exclude_assistant = !!settings.exclude_assistant;
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
@@ -2365,10 +2356,14 @@ function loadOpenAISettings(data, settings) {
$(`#model_claude_select option[value="${oai_settings.claude_model}"`).attr('selected', true);
$('#model_windowai_select').val(oai_settings.windowai_model);
$(`#model_windowai_select option[value="${oai_settings.windowai_model}"`).attr('selected', true);
$('#model_google_select').val(oai_settings.google_model);
$(`#model_google_select option[value="${oai_settings.google_model}"`).attr('selected', true);
$('#model_ai21_select').val(oai_settings.ai21_model);
$(`#model_ai21_select option[value="${oai_settings.ai21_model}"`).attr('selected', true);
$('#model_togetherai_select').val(oai_settings.togetherai_model);
$(`#model_togetherai_select option[value="${oai_settings.togetherai_model}"`).attr('selected', true);
$('#model_mistralai_select').val(oai_settings.mistralai_model);
$(`#model_mistralai_select option[value="${oai_settings.mistralai_model}"`).attr('selected', true);
$('#openai_max_context').val(oai_settings.openai_max_context);
$('#openai_max_context_counter').val(`${oai_settings.openai_max_context}`);
$('#model_openrouter_select').val(oai_settings.openrouter_model);
@@ -2379,10 +2374,10 @@ function loadOpenAISettings(data, settings) {
$('#wrap_in_quotes').prop('checked', oai_settings.wrap_in_quotes);
$('#names_in_completion').prop('checked', oai_settings.names_in_completion);
$('#jailbreak_system').prop('checked', oai_settings.jailbreak_system);
$('#legacy_streaming').prop('checked', oai_settings.legacy_streaming);
$('#openai_show_external_models').prop('checked', oai_settings.show_external_models);
$('#openai_external_category').toggle(oai_settings.show_external_models);
$('#use_ai21_tokenizer').prop('checked', oai_settings.use_ai21_tokenizer);
$('#use_google_tokenizer').prop('checked', oai_settings.use_google_tokenizer);
$('#exclude_assistant').prop('checked', oai_settings.exclude_assistant);
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
@@ -2438,6 +2433,11 @@ function loadOpenAISettings(data, settings) {
}
$('#openai_logit_bias_preset').trigger('change');
// Upgrade Palm to Makersuite
if (oai_settings.chat_completion_source === 'palm') {
oai_settings.chat_completion_source = chat_completion_sources.MAKERSUITE;
}
$('#chat_completion_source').val(oai_settings.chat_completion_source).trigger('change');
$('#oai_max_context_unlocked').prop('checked', oai_settings.max_context_unlocked);
}
@@ -2458,7 +2458,7 @@ async function getStatusOpen() {
return resultCheckStatus();
}
const noValidateSources = [chat_completion_sources.SCALE, chat_completion_sources.CLAUDE, chat_completion_sources.AI21, chat_completion_sources.PALM];
const noValidateSources = [chat_completion_sources.SCALE, chat_completion_sources.CLAUDE, chat_completion_sources.AI21, chat_completion_sources.MAKERSUITE];
if (noValidateSources.includes(oai_settings.chat_completion_source)) {
let status = 'Unable to verify key; press "Test Message" to validate.';
setOnlineStatus(status);
@@ -2471,7 +2471,7 @@ async function getStatusOpen() {
chat_completion_source: oai_settings.chat_completion_source,
};
if (oai_settings.reverse_proxy && oai_settings.chat_completion_source !== chat_completion_sources.OPENROUTER) {
if (oai_settings.reverse_proxy && (oai_settings.chat_completion_source === chat_completion_sources.OPENAI || oai_settings.chat_completion_source === chat_completion_sources.CLAUDE)) {
validateReverseProxy();
}
@@ -2481,7 +2481,7 @@ async function getStatusOpen() {
}
try {
const response = await fetch('/getstatus_openai', {
const response = await fetch('/api/backends/chat-completions/status', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(data),
@@ -2521,28 +2521,6 @@ function showWindowExtensionError() {
});
}
function trySelectPresetByName(name) {
let preset_found = null;
for (const key in openai_setting_names) {
if (name.trim() == key.trim()) {
preset_found = key;
break;
}
}
// Don't change if the current preset is the same
if (preset_found && preset_found === oai_settings.preset_settings_openai) {
return;
}
if (preset_found) {
oai_settings.preset_settings_openai = preset_found;
const value = openai_setting_names[preset_found];
$(`#settings_preset_openai option[value="${value}"]`).attr('selected', true);
$('#settings_preset_openai').val(value).trigger('change');
}
}
/**
* Persist a settings preset with the given name
*
@@ -2564,6 +2542,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
openrouter_sort_models: settings.openrouter_sort_models,
ai21_model: settings.ai21_model,
togetherai_model: settings.togetherai_model,
mistralai_model: settings.mistralai_model,
google_model: settings.google_model,
temperature: settings.temp_openai,
frequency_penalty: settings.freq_pen_openai,
presence_penalty: settings.pres_pen_openai,
@@ -2585,7 +2565,6 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
bias_preset_selected: settings.bias_preset_selected,
reverse_proxy: settings.reverse_proxy,
proxy_password: settings.proxy_password,
legacy_streaming: settings.legacy_streaming,
max_context_unlocked: settings.max_context_unlocked,
wi_format: settings.wi_format,
scenario_format: settings.scenario_format,
@@ -2598,6 +2577,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
show_external_models: settings.show_external_models,
assistant_prefill: settings.assistant_prefill,
use_ai21_tokenizer: settings.use_ai21_tokenizer,
use_google_tokenizer: settings.use_google_tokenizer,
exclude_assistant: settings.exclude_assistant,
use_alt_scale: settings.use_alt_scale,
squash_system_messages: settings.squash_system_messages,
@@ -2935,6 +2915,8 @@ function onSettingsPresetChange() {
openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false],
ai21_model: ['#model_ai21_select', 'ai21_model', false],
togetherai_model: ['#model_togetherai_select', 'togetherai_model', false],
mistralai_model: ['#model_mistralai_select', 'mistralai_model', false],
google_model: ['#model_google_select', 'google_model', false],
openai_max_context: ['#openai_max_context', 'openai_max_context', false],
openai_max_tokens: ['#openai_max_tokens', 'openai_max_tokens', false],
wrap_in_quotes: ['#wrap_in_quotes', 'wrap_in_quotes', true],
@@ -2947,7 +2929,6 @@ function onSettingsPresetChange() {
continue_nudge_prompt: ['#continue_nudge_prompt_textarea', 'continue_nudge_prompt', false],
bias_preset_selected: ['#openai_logit_bias_preset', 'bias_preset_selected', false],
reverse_proxy: ['#openai_reverse_proxy', 'reverse_proxy', false],
legacy_streaming: ['#legacy_streaming', 'legacy_streaming', true],
wi_format: ['#wi_format_textarea', 'wi_format', false],
scenario_format: ['#scenario_format_textarea', 'scenario_format', false],
personality_format: ['#personality_format_textarea', 'personality_format', false],
@@ -2960,6 +2941,7 @@ function onSettingsPresetChange() {
proxy_password: ['#openai_proxy_password', 'proxy_password', false],
assistant_prefill: ['#claude_assistant_prefill', 'assistant_prefill', false],
use_ai21_tokenizer: ['#use_ai21_tokenizer', 'use_ai21_tokenizer', true],
use_google_tokenizer: ['#use_google_tokenizer', 'use_google_tokenizer', true],
exclude_assistant: ['#exclude_assistant', 'exclude_assistant', true],
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true],
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true],
@@ -3068,7 +3050,7 @@ function getMaxContextWindowAI(value) {
return max_8k;
}
else if (value.includes('palm-2')) {
return palm2_max;
return max_8k;
}
else if (value.includes('GPT-NeoXT')) {
return max_2k;
@@ -3112,11 +3094,21 @@ async function onModelChange() {
console.log('AI21 model changed to', value);
oai_settings.ai21_model = value;
}
if ($(this).is('#model_togetherai_select')) {
console.log('TogetherAI model changed to', value);
oai_settings.togetherai_model = value;
}
if ($(this).is('#model_google_select')) {
console.log('Google model changed to', value);
oai_settings.google_model = value;
}
if ($(this).is('#model_mistralai_select')) {
console.log('MistralAI model changed to', value);
oai_settings.mistralai_model = value;
}
if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
if (oai_settings.max_context_unlocked) {
@@ -3128,13 +3120,18 @@ async function onModelChange() {
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.PALM) {
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (value === 'gemini-pro') {
$('#openai_max_context').attr('max', max_32k);
} else if (value === 'gemini-pro-vision') {
$('#openai_max_context').attr('max', max_16k);
} else {
$('#openai_max_context').attr('max', palm2_max);
$('#openai_max_context').attr('max', max_8k);
}
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
}
@@ -3226,6 +3223,16 @@ async function onModelChange() {
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
$('#openai_max_context').attr('max', max_32k);
oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
//mistral also caps temp at 1.0
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.AI21) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
@@ -3335,15 +3342,15 @@ async function onConnectButtonClick(e) {
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.PALM) {
const api_key_palm = String($('#api_key_palm').val()).trim();
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
const api_key_makersuite = String($('#api_key_makersuite').val()).trim();
if (api_key_palm.length) {
await writeSecret(SECRET_KEYS.PALM, api_key_palm);
if (api_key_makersuite.length) {
await writeSecret(SECRET_KEYS.MAKERSUITE, api_key_makersuite);
}
if (!secret_state[SECRET_KEYS.PALM]) {
console.log('No secret key saved for PALM');
if (!secret_state[SECRET_KEYS.MAKERSUITE]) {
console.log('No secret key saved for MakerSuite');
return;
}
}
@@ -3396,6 +3403,18 @@ async function onConnectButtonClick(e) {
if (!secret_state[SECRET_KEYS.TOGETHERAI]) {
console.log('No secret key saved for TogetherAI');
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
const api_key_mistralai = String($('#api_key_mistralai').val()).trim();
if (api_key_mistralai.length) {
await writeSecret(SECRET_KEYS.MISTRALAI, api_key_mistralai);
}
if (!secret_state[SECRET_KEYS.MISTRALAI]) {
console.log('No secret key saved for MistralAI');
return;
}
}
@@ -3423,8 +3442,8 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
$('#model_scale_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.PALM) {
$('#model_palm_select').trigger('change');
else if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
$('#model_google_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
$('#model_openrouter_select').trigger('change');
@@ -3435,6 +3454,9 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.TOGETHERAI) {
$('#model_togetherai_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
$('#model_mistralai_select').trigger('change');
}
$('[data-source]').each(function () {
const validSources = $(this).data('source').split(',');
$(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -3495,6 +3517,7 @@ export function isImageInliningSupported() {
}
const gpt4v = 'gpt-4-vision';
const geminiProV = 'gemini-pro-vision';
const llava13b = 'llava-13b';
if (!oai_settings.image_inlining) {
@@ -3504,6 +3527,8 @@ export function isImageInliningSupported() {
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.OPENAI:
return oai_settings.openai_model.includes(gpt4v);
case chat_completion_sources.MAKERSUITE:
return oai_settings.google_model.includes(geminiProV);
case chat_completion_sources.OPENROUTER:
return oai_settings.openrouter_model.includes(gpt4v) || oai_settings.openrouter_model.includes(llava13b);
default:
@@ -3588,6 +3613,11 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#use_google_tokenizer').on('change', function () {
oai_settings.use_google_tokenizer = !!$('#use_google_tokenizer').prop('checked');
saveSettingsDebounced();
});
$('#exclude_assistant').on('change', function () {
oai_settings.exclude_assistant = !!$('#exclude_assistant').prop('checked');
$('#claude_assistant_prefill_block').toggle(!oai_settings.exclude_assistant);
@@ -3649,29 +3679,6 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
// auto-select a preset based on character/group name
$(document).on('click', '.character_select', function () {
const chid = $(this).attr('chid');
const name = characters[chid]?.name;
if (!name) {
return;
}
trySelectPresetByName(name);
});
$(document).on('click', '.group_select', function () {
const grid = $(this).data('id');
const name = groups.find(x => x.id === grid)?.name;
if (!name) {
return;
}
trySelectPresetByName(name);
});
$('#update_oai_preset').on('click', async function () {
const name = oai_settings.preset_settings_openai;
await saveOpenAIPreset(name, oai_settings);
@@ -3732,11 +3739,6 @@ $(document).ready(async function () {
saveSettingsDebounced();
});
$('#legacy_streaming').on('input', function () {
oai_settings.legacy_streaming = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#openai_bypass_status_check').on('input', function () {
oai_settings.bypass_status_check = !!$(this).prop('checked');
getStatusOpen();
@@ -3827,12 +3829,13 @@ $(document).ready(async function () {
$('#model_claude_select').on('change', onModelChange);
$('#model_windowai_select').on('change', onModelChange);
$('#model_scale_select').on('change', onModelChange);
$('#model_palm_select').on('change', onModelChange);
$('#model_google_select').on('change', onModelChange);
$('#model_openrouter_select').on('change', onModelChange);
$('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
$('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
$('#model_ai21_select').on('change', onModelChange);
$('#model_togetherai_select').on('change', onModelChange);
$('#model_mistralai_select').on('change', onModelChange);
$('#settings_preset_openai').on('change', onSettingsPresetChange);
$('#new_oai_preset').on('click', onNewPresetClick);
$('#delete_oai_preset').on('click', onDeletePresetClick);

View File

@@ -19,6 +19,8 @@ import {
showMoreMessages,
saveSettings,
saveChatConditional,
setAnimationDuration,
ANIMATION_DURATION_DEFAULT,
} from '../script.js';
import { isMobile, initMovingUI, favsToHotswap } from './RossAscends-mods.js';
import {
@@ -35,7 +37,7 @@ import { registerSlashCommand } from './slash-commands.js';
import { tags } from './tags.js';
import { tokenizers } from './tokenizers.js';
import { countOccurrences, debounce, delay, isOdd, resetScrollHeight, sortMoments, stringToRange, timestampToMoment } from './utils.js';
import { countOccurrences, debounce, delay, isOdd, resetScrollHeight, shuffle, sortMoments, stringToRange, timestampToMoment } from './utils.js';
export {
loadPowerUserSettings,
@@ -55,7 +57,7 @@ const MAX_CONTEXT_UNLOCKED = 200 * 1024;
const MAX_RESPONSE_UNLOCKED = 16 * 1024;
const unlockedMaxContextStep = 512;
const maxContextMin = 512;
const maxContextStep = 256;
const maxContextStep = 64;
const defaultStoryString = '{{#if system}}{{system}}\n{{/if}}{{#if description}}{{description}}\n{{/if}}{{#if personality}}{{char}}\'s personality: {{personality}}\n{{/if}}{{#if scenario}}Scenario: {{scenario}}\n{{/if}}{{#if persona}}{{persona}}\n{{/if}}';
const defaultExampleSeparator = '***';
@@ -112,6 +114,7 @@ let power_user = {
},
markdown_escape_strings: '',
chat_truncation: 100,
streaming_fps: 30,
ui_mode: ui_mode.POWER,
fast_ui_mode: true,
@@ -228,6 +231,8 @@ let power_user = {
bogus_folders: false,
aux_field: 'character_version',
restore_user_input: true,
reduced_motion: false,
compact_input_area: true,
};
let themes = [];
@@ -268,6 +273,8 @@ const storage_keys = {
expand_message_actions: 'ExpandMessageActions',
enableZenSliders: 'enableZenSliders',
enableLabMode: 'enableLabMode',
reduced_motion: 'reduced_motion',
compact_input_area: 'compact_input_area',
};
const contextControls = [
@@ -436,6 +443,22 @@ function switchMessageActions() {
$('.extraMesButtons, .extraMesButtonsHint').removeAttr('style');
}
function switchReducedMotion() {
const value = localStorage.getItem(storage_keys.reduced_motion);
power_user.reduced_motion = value === null ? false : value == 'true';
jQuery.fx.off = power_user.reduced_motion;
const overrideDuration = power_user.reduced_motion ? 0 : ANIMATION_DURATION_DEFAULT;
setAnimationDuration(overrideDuration);
$('#reduced_motion').prop('checked', power_user.reduced_motion);
}
function switchCompactInputArea() {
const value = localStorage.getItem(storage_keys.compact_input_area);
power_user.compact_input_area = value === null ? true : value == 'true';
$('#send_form').toggleClass('compact', power_user.compact_input_area);
$('#compact_input_area').prop('checked', power_user.compact_input_area);
}
var originalSliderValues = [];
async function switchLabMode() {
@@ -533,7 +556,7 @@ async function CreateZenSliders(elmnt) {
var sliderMax = Number(originalSlider.attr('max'));
var sliderValue = originalSlider.val();
var sliderRange = sliderMax - sliderMin;
var numSteps = 10;
var numSteps = 20;
var decimals = 2;
var offVal, allVal;
var stepScale;
@@ -1227,6 +1250,22 @@ async function applyTheme(name) {
await printCharacters(true);
},
},
{
key: 'reduced_motion',
action: async () => {
localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
$('#reduced_motion').prop('checked', power_user.reduced_motion);
switchReducedMotion();
},
},
{
key: 'compact_input_area',
action: async () => {
localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
$('#compact_input_area').prop('checked', power_user.compact_input_area);
switchCompactInputArea();
},
},
];
for (const { key, selector, type, action } of themeProperties) {
@@ -1440,6 +1479,9 @@ function loadPowerUserSettings(settings, data) {
$('#chat_truncation').val(power_user.chat_truncation);
$('#chat_truncation_counter').val(power_user.chat_truncation);
$('#streaming_fps').val(power_user.streaming_fps);
$('#streaming_fps_counter').val(power_user.streaming_fps);
$('#font_scale').val(power_user.font_scale);
$('#font_scale_counter').val(power_user.font_scale);
@@ -1459,6 +1501,7 @@ function loadPowerUserSettings(settings, data) {
$('#shadow-color-picker').attr('color', power_user.shadow_color);
$('#border-color-picker').attr('color', power_user.border_color);
$('#ui_mode_select').val(power_user.ui_mode).find(`option[value="${power_user.ui_mode}"]`).attr('selected', true);
$('#reduced_motion').prop('checked', power_user.reduced_motion);
for (const theme of themes) {
const option = document.createElement('option');
@@ -1478,6 +1521,8 @@ function loadPowerUserSettings(settings, data) {
$(`#character_sort_order option[data-order="${power_user.sort_order}"][data-field="${power_user.sort_field}"]`).prop('selected', true);
switchReducedMotion();
switchCompactInputArea();
reloadMarkdownProcessor(power_user.render_formulas);
loadInstructMode(data);
loadContextSettings();
@@ -1504,7 +1549,7 @@ async function loadCharListState() {
}
function loadMovingUIState() {
if (isMobile() === false
if (!isMobile()
&& power_user.movingUIState
&& power_user.movingUI === true) {
console.debug('loading movingUI state');
@@ -1818,10 +1863,6 @@ export function renderStoryString(params) {
const sortFunc = (a, b) => power_user.sort_order == 'asc' ? compareFunc(a, b) : compareFunc(b, a);
const compareFunc = (first, second) => {
if (power_user.sort_order == 'random') {
return Math.random() > 0.5 ? 1 : -1;
}
const a = first[power_user.sort_field];
const b = second[power_user.sort_field];
@@ -1853,6 +1894,11 @@ function sortEntitiesList(entities) {
return;
}
if (power_user.sort_order === 'random') {
shuffle(entities);
return;
}
entities.sort((a, b) => {
if (a.type === 'tag' && b.type !== 'tag') {
return -1;
@@ -1866,11 +1912,26 @@ function sortEntitiesList(entities) {
});
}
async function saveTheme() {
const name = await callPopup('Enter a theme preset name:', 'input');
/**
* Updates the current UI theme file.
*/
async function updateTheme() {
await saveTheme(power_user.theme);
toastr.success('Theme saved.');
}
if (!name) {
return;
/**
* Saves the current theme to the server.
* @param {string|undefined} name Theme name. If undefined, a popup will be shown to enter a name.
* @returns {Promise<void>} A promise that resolves when the theme is saved.
*/
async function saveTheme(name = undefined) {
if (typeof name !== 'string') {
name = await callPopup('Enter a theme preset name:', 'input', power_user.theme);
if (!name) {
return;
}
}
const theme = {
@@ -1905,6 +1966,8 @@ async function saveTheme() {
hotswap_enabled: power_user.hotswap_enabled,
custom_css: power_user.custom_css,
bogus_folders: power_user.bogus_folders,
reduced_motion: power_user.reduced_motion,
compact_input_area: power_user.compact_input_area,
};
const response = await fetch('/savetheme', {
@@ -2678,6 +2741,12 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#streaming_fps').on('input', function () {
power_user.streaming_fps = Number($('#streaming_fps').val());
$('#streaming_fps_counter').val(power_user.streaming_fps);
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);
@@ -2771,7 +2840,8 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#ui-preset-save-button').on('click', saveTheme);
$('#ui-preset-save-button').on('click', () => saveTheme());
$('#ui-preset-update-button').on('click', () => updateTheme());
$('#movingui-preset-save-button').on('click', saveMovingUI);
$('#never_resize_avatars').on('input', function () {
@@ -3111,6 +3181,20 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#reduced_motion').on('input', function () {
power_user.reduced_motion = !!$(this).prop('checked');
localStorage.setItem(storage_keys.reduced_motion, String(power_user.reduced_motion));
switchReducedMotion();
saveSettingsDebounced();
});
$('#compact_input_area').on('input', function () {
power_user.compact_input_area = !!$(this).prop('checked');
localStorage.setItem(storage_keys.compact_input_area, String(power_user.compact_input_area));
switchCompactInputArea();
saveSettingsDebounced();
});
$(document).on('click', '#debug_table [data-debug-function]', function () {
const functionId = $(this).data('debug-function');
const functionRecord = debug_functions.find(f => f.functionId === functionId);

View File

@@ -12,6 +12,7 @@ import {
nai_settings,
novelai_setting_names,
novelai_settings,
online_status,
saveSettingsDebounced,
this_chid,
} from '../script.js';
@@ -19,6 +20,7 @@ import { groups, selected_group } from './group-chats.js';
import { instruct_presets } from './instruct-mode.js';
import { kai_settings } from './kai-settings.js';
import { context_presets, getContextSettings, power_user } from './power-user.js';
import { registerSlashCommand } from './slash-commands.js';
import {
textgenerationwebui_preset_names,
textgenerationwebui_presets,
@@ -28,6 +30,9 @@ import { download, parseJsonFile, waitUntilCondition } from './utils.js';
const presetManagers = {};
/**
* Automatically select a preset for current API based on character or group name.
*/
function autoSelectPreset() {
const presetManager = getPresetManager();
@@ -57,7 +62,12 @@ function autoSelectPreset() {
}
}
function getPresetManager(apiId) {
/**
* Gets a preset manager by API id.
* @param {string} apiId API id
* @returns {PresetManager} Preset manager
*/
function getPresetManager(apiId = '') {
if (!apiId) {
apiId = main_api == 'koboldhorde' ? 'kobold' : main_api;
}
@@ -69,6 +79,9 @@ function getPresetManager(apiId) {
return presetManagers[apiId];
}
/**
* Registers preset managers for all select elements with data-preset-manager-for attribute.
*/
function registerPresetManagers() {
$('select[data-preset-manager-for]').each((_, e) => {
const forData = $(e).data('preset-manager-for');
@@ -85,21 +98,46 @@ class PresetManager {
this.apiId = apiId;
}
/**
* Gets all preset names.
* @returns {string[]} List of preset names
*/
getAllPresets() {
return $(this.select).find('option').map((_, el) => el.text).toArray();
}
/**
* Finds a preset by name.
* @param {string} name Preset name
* @returns {any} Preset value
*/
findPreset(name) {
return $(this.select).find(`option:contains(${name})`).val();
}
/**
* Gets the selected preset value.
* @returns {any} Selected preset value
*/
getSelectedPreset() {
return $(this.select).find('option:selected').val();
}
/**
* Gets the selected preset name.
* @returns {string} Selected preset name
*/
getSelectedPresetName() {
return $(this.select).find('option:selected').text();
}
selectPreset(preset) {
$(this.select).find(`option[value=${preset}]`).prop('selected', true);
$(this.select).val(preset).trigger('change');
/**
* Selects a preset by option value.
* @param {string} value Preset option value
*/
selectPreset(value) {
$(this.select).find(`option[value=${value}]`).prop('selected', true);
$(this.select).val(value).trigger('change');
}
async updatePreset() {
@@ -334,11 +372,91 @@ class PresetManager {
}
}
jQuery(async () => {
await waitUntilCondition(() => eventSource !== undefined);
/**
* Selects a preset by name for current API.
* @param {any} _ Named arguments
* @param {string} name Unnamed arguments
* @returns {Promise<string>} Selected or current preset name
*/
async function presetCommandCallback(_, name) {
const shouldReconnect = online_status !== 'no_connection';
const presetManager = getPresetManager();
const allPresets = presetManager.getAllPresets();
const currentPreset = presetManager.getSelectedPresetName();
if (!presetManager) {
console.debug(`Preset Manager not found for API: ${main_api}`);
return '';
}
if (!name) {
console.log('No name provided for /preset command, using current preset');
return currentPreset;
}
if (!Array.isArray(allPresets) || allPresets.length === 0) {
console.log(`No presets found for API: ${main_api}`);
return currentPreset;
}
// Find exact match
const exactMatch = allPresets.find(p => p.toLowerCase().trim() === name.toLowerCase().trim());
if (exactMatch) {
console.log('Found exact preset match', exactMatch);
if (currentPreset !== exactMatch) {
const presetValue = presetManager.findPreset(exactMatch);
if (presetValue) {
presetManager.selectPreset(presetValue);
shouldReconnect && await waitForConnection();
}
}
return exactMatch;
} else {
// Find fuzzy match
const fuse = new Fuse(allPresets);
const fuzzyMatch = fuse.search(name);
if (!fuzzyMatch.length) {
console.warn(`WARN: Preset found with name ${name}`);
return currentPreset;
}
const fuzzyPresetName = fuzzyMatch[0].item;
const fuzzyPresetValue = presetManager.findPreset(fuzzyPresetName);
if (fuzzyPresetValue) {
console.log('Found fuzzy preset match', fuzzyPresetName);
if (currentPreset !== fuzzyPresetName) {
presetManager.selectPreset(fuzzyPresetValue);
shouldReconnect && await waitForConnection();
}
}
return fuzzyPresetName;
}
}
/**
* Waits for API connection to be established.
*/
async function waitForConnection() {
try {
await waitUntilCondition(() => online_status !== 'no_connection', 5000, 100);
} catch {
console.log('Timeout waiting for API to connect');
}
}
export async function initPresetManager() {
eventSource.on(event_types.CHAT_CHANGED, autoSelectPreset);
registerPresetManagers();
registerSlashCommand('preset', presetCommandCallback, [], '<span class="monospace">(name)</span> sets a preset by name for the current API', true, true);
$(document).on('click', '[data-preset-manager-update]', async function () {
const apiId = $(this).data('preset-manager-update');
const presetManager = getPresetManager(apiId);
@@ -440,7 +558,7 @@ jQuery(async () => {
saveSettingsDebounced();
});
$(document).on('click', '[data-preset-manager-restore]', async function() {
$(document).on('click', '[data-preset-manager-restore]', async function () {
const apiId = $(this).data('preset-manager-restore');
const presetManager = getPresetManager(apiId);
@@ -490,4 +608,4 @@ jQuery(async () => {
toastr.success('Preset restored');
}
});
});
}

View File

@@ -12,8 +12,9 @@ export const SECRET_KEYS = {
SCALE: 'api_key_scale',
AI21: 'api_key_ai21',
SCALE_COOKIE: 'scale_cookie',
PALM: 'api_key_palm',
MAKERSUITE: 'api_key_makersuite',
SERPAPI: 'api_key_serpapi',
MISTRALAI: 'api_key_mistralai',
TOGETHERAI: 'api_key_togetherai',
};
@@ -27,9 +28,10 @@ const INPUT_MAP = {
[SECRET_KEYS.SCALE]: '#api_key_scale',
[SECRET_KEYS.AI21]: '#api_key_ai21',
[SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie',
[SECRET_KEYS.PALM]: '#api_key_palm',
[SECRET_KEYS.MAKERSUITE]: '#api_key_makersuite',
[SECRET_KEYS.APHRODITE]: '#api_key_aphrodite',
[SECRET_KEYS.TABBY]: '#api_key_tabby',
[SECRET_KEYS.MISTRALAI]: '#api_key_mistralai',
[SECRET_KEYS.TOGETHERAI]: '#api_key_togetherai',
};

View File

@@ -186,6 +186,7 @@ parser.addCommand('trimend', trimEndCallback, [], '<span class="monospace">(text
parser.addCommand('inject', injectCallback, [], '<span class="monospace">id=injectId (position=before/after/chat depth=number [text])</span> injects a text into the LLM prompt for the current chat. Requires a unique injection ID. Positions: "before" main prompt, "after" main prompt, in-"chat" (default: after). Depth: injection depth for the prompt (default: 4).', true, true);
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), [], '<span class="monospace">(text)</span> counts the number of tokens in the text.', true, true);
registerVariableCommands();
const NARRATOR_NAME_KEY = 'narrator_name';

View File

@@ -0,0 +1,77 @@
/**
* A stream which handles Server-Sent Events from a binary ReadableStream like you get from the fetch API.
*/
class EventSourceStream {
constructor() {
const decoder = new TextDecoderStream('utf-8');
let streamBuffer = '';
let lastEventId = '';
function processChunk(controller) {
// Events are separated by two newlines
const events = streamBuffer.split(/\r\n\r\n|\r\r|\n\n/g);
if (events.length === 0) return;
// The leftover text to remain in the buffer is whatever doesn't have two newlines after it. If the buffer ended
// with two newlines, this will be an empty string.
streamBuffer = events.pop();
for (const eventChunk of events) {
let eventType = '';
// Split up by single newlines.
const lines = eventChunk.split(/\n|\r|\r\n/g);
let eventData = '';
for (const line of lines) {
const lineMatch = /([^:]+)(?:: ?(.*))?/.exec(line);
if (lineMatch) {
const field = lineMatch[1];
const value = lineMatch[2] || '';
switch (field) {
case 'event':
eventType = value;
break;
case 'data':
eventData += value;
eventData += '\n';
break;
case 'id':
// The ID field cannot contain null, per the spec
if (!value.includes('\0')) lastEventId = value;
break;
// We do nothing for the `delay` type, and other types are explicitly ignored
}
}
}
// https://html.spec.whatwg.org/multipage/server-sent-events.html#dispatchMessage
// Skip the event if the data buffer is the empty string.
if (eventData === '') continue;
if (eventData[eventData.length - 1] === '\n') {
eventData = eventData.slice(0, -1);
}
// Trim the *last* trailing newline only.
const event = new MessageEvent(eventType || 'message', { data: eventData, lastEventId });
controller.enqueue(event);
}
}
const sseStream = new TransformStream({
transform(chunk, controller) {
streamBuffer += chunk;
processChunk(controller);
},
});
decoder.readable.pipeThrough(sseStream);
this.readable = sseStream.readable;
this.writable = decoder.writable;
}
}
export default EventSourceStream;

View File

@@ -17,6 +17,7 @@
<li><tt>&lcub;&lcub;char&rcub;&rcub;</tt> the Character's name</li>
<li><tt>&lcub;&lcub;lastMessage&rcub;&rcub;</tt> - the text of the latest chat message.</li>
<li><tt>&lcub;&lcub;lastMessageId&rcub;&rcub;</tt> index # of the latest chat message. Useful for slash command batching.</li>
<li><tt>&lcub;&lcub;firstIncludedMessageId&rcub;&rcub;</tt> - the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</li>
<li><tt>&lcub;&lcub;currentSwipeId&rcub;&rcub;</tt> the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</li>
<li><tt>&lcub;&lcub;lastSwipeId&rcub;&rcub;</tt> the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</li>
<li><tt>&lcub;&lcub;// (note)&rcub;&rcub;</tt> you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</li>

View File

@@ -14,6 +14,7 @@ import {
power_user,
registerDebugFunction,
} from './power-user.js';
import EventSourceStream from './sse-stream.js';
import { SENTENCEPIECE_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
import { getSortableDelay, onlyUnique } from './utils.js';
@@ -467,7 +468,7 @@ function setSettingByName(setting, value, trigger) {
async function generateTextGenWithStreaming(generate_data, signal) {
generate_data.stream = true;
const response = await fetch('/api/textgenerationwebui/generate', {
const response = await fetch('/api/backends/text-completions/generate', {
headers: {
...getRequestHeaders(),
},
@@ -476,68 +477,50 @@ async function generateTextGenWithStreaming(generate_data, signal) {
signal: signal,
});
if (!response.ok) {
tryParseStreamingError(response, await response.text());
throw new Error(`Got response status ${response.status}`);
}
const eventStream = new EventSourceStream();
response.body.pipeThrough(eventStream);
const reader = eventStream.readable.getReader();
return async function* streamData() {
const decoder = new TextDecoder();
const reader = response.body.getReader();
let getMessage = '';
let messageBuffer = '';
let text = '';
const swipes = [];
while (true) {
const { done, value } = await reader.read();
// We don't want carriage returns in our messages
let response = decoder.decode(value).replace(/\r/g, '');
if (done) return;
if (value.data === '[DONE]') return;
tryParseStreamingError(response);
tryParseStreamingError(response, value.data);
let eventList = [];
let data = JSON.parse(value.data);
messageBuffer += response;
eventList = messageBuffer.split('\n\n');
// Last element will be an empty string or a leftover partial message
messageBuffer = eventList.pop();
for (let event of eventList) {
if (event.startsWith('event: completion')) {
event = event.split('\n')[1];
}
if (typeof event !== 'string' || !event.length)
continue;
if (!event.startsWith('data'))
continue;
if (event == 'data: [DONE]') {
return;
}
let data = JSON.parse(event.substring(6));
if (data?.choices[0]?.index > 0) {
const swipeIndex = data.choices[0].index - 1;
swipes[swipeIndex] = (swipes[swipeIndex] || '') + data.choices[0].text;
} else {
getMessage += data?.choices[0]?.text || '';
}
yield { text: getMessage, swipes: swipes };
if (data?.choices[0]?.index > 0) {
const swipeIndex = data.choices[0].index - 1;
swipes[swipeIndex] = (swipes[swipeIndex] || '') + data.choices[0].text;
} else {
text += data?.choices[0]?.text || '';
}
if (done) {
return;
}
yield { text, swipes };
}
};
}
/**
* Parses errors in streaming responses and displays them in toastr.
* @param {string} response - Response from the server.
* @param {Response} response - Response from the server.
* @param {string} decoded - Decoded response body.
* @returns {void} Nothing.
*/
function tryParseStreamingError(response) {
function tryParseStreamingError(response, decoded) {
let data = {};
try {
data = JSON.parse(response);
data = JSON.parse(decoded);
} catch {
// No JSON. Do nothing.
}
@@ -545,7 +528,7 @@ function tryParseStreamingError(response) {
const message = data?.error?.message || data?.message;
if (message) {
toastr.error(message, 'API Error');
toastr.error(message, 'Text Completion API');
throw new Error(message);
}
}

View File

@@ -1,4 +1,4 @@
import { characters, getAPIServerUrl, main_api, nai_settings, online_status, this_chid } from '../script.js';
import { characters, main_api, api_server, api_server_textgenerationwebui, nai_settings, online_status, this_chid } from '../script.js';
import { power_user, registerDebugFunction } from './power-user.js';
import { chat_completion_sources, model_list, oai_settings } from './openai.js';
import { groups, selected_group } from './group-chats.js';
@@ -18,9 +18,11 @@ export const tokenizers = {
LLAMA: 3,
NERD: 4,
NERD2: 5,
API: 6,
API_CURRENT: 6,
MISTRAL: 7,
YI: 8,
API_TEXTGENERATIONWEBUI: 9,
API_KOBOLD: 10,
BEST_MATCH: 99,
};
@@ -33,6 +35,52 @@ export const SENTENCEPIECE_TOKENIZERS = [
//tokenizers.NERD2,
];
const TOKENIZER_URLS = {
[tokenizers.GPT2]: {
encode: '/api/tokenizers/gpt2/encode',
decode: '/api/tokenizers/gpt2/decode',
count: '/api/tokenizers/gpt2/encode',
},
[tokenizers.OPENAI]: {
encode: '/api/tokenizers/openai/encode',
decode: '/api/tokenizers/openai/decode',
count: '/api/tokenizers/openai/encode',
},
[tokenizers.LLAMA]: {
encode: '/api/tokenizers/llama/encode',
decode: '/api/tokenizers/llama/decode',
count: '/api/tokenizers/llama/encode',
},
[tokenizers.NERD]: {
encode: '/api/tokenizers/nerdstash/encode',
decode: '/api/tokenizers/nerdstash/decode',
count: '/api/tokenizers/nerdstash/encode',
},
[tokenizers.NERD2]: {
encode: '/api/tokenizers/nerdstash_v2/encode',
decode: '/api/tokenizers/nerdstash_v2/decode',
count: '/api/tokenizers/nerdstash_v2/encode',
},
[tokenizers.API_KOBOLD]: {
count: '/api/tokenizers/remote/kobold/count',
encode: '/api/tokenizers/remote/kobold/count',
},
[tokenizers.MISTRAL]: {
encode: '/api/tokenizers/mistral/encode',
decode: '/api/tokenizers/mistral/decode',
count: '/api/tokenizers/mistral/encode',
},
[tokenizers.YI]: {
encode: '/api/tokenizers/yi/encode',
decode: '/api/tokenizers/yi/decode',
count: '/api/tokenizers/yi/encode',
},
[tokenizers.API_TEXTGENERATIONWEBUI]: {
encode: '/api/tokenizers/remote/textgenerationwebui/encode',
count: '/api/tokenizers/remote/textgenerationwebui/encode',
},
};
const objectStore = new localforage.createInstance({ name: 'SillyTavern_ChatCompletions' });
let tokenCache = {};
@@ -92,7 +140,18 @@ export function getFriendlyTokenizerName(forApi) {
if (forApi !== 'openai' && tokenizerId === tokenizers.BEST_MATCH) {
tokenizerId = getTokenizerBestMatch(forApi);
tokenizerName = $(`#tokenizer option[value="${tokenizerId}"]`).text();
switch (tokenizerId) {
case tokenizers.API_KOBOLD:
tokenizerName = 'API (KoboldAI Classic)';
break;
case tokenizers.API_TEXTGENERATIONWEBUI:
tokenizerName = 'API (Text Completion)';
break;
default:
tokenizerName = $(`#tokenizer option[value="${tokenizerId}"]`).text();
break;
}
}
tokenizerName = forApi == 'openai'
@@ -135,11 +194,11 @@ export function getTokenizerBestMatch(forApi) {
if (!hasTokenizerError && isConnected) {
if (forApi === 'kobold' && kai_flags.can_use_tokenization) {
return tokenizers.API;
return tokenizers.API_KOBOLD;
}
if (forApi === 'textgenerationwebui' && isTokenizerSupported) {
return tokenizers.API;
return tokenizers.API_TEXTGENERATIONWEBUI;
}
}
@@ -149,34 +208,42 @@ export function getTokenizerBestMatch(forApi) {
return tokenizers.NONE;
}
// Get the current remote tokenizer API based on the current text generation API.
function currentRemoteTokenizerAPI() {
switch (main_api) {
case 'kobold':
return tokenizers.API_KOBOLD;
case 'textgenerationwebui':
return tokenizers.API_TEXTGENERATIONWEBUI;
default:
return tokenizers.NONE;
}
}
/**
* Calls the underlying tokenizer model to the token count for a string.
* @param {number} type Tokenizer type.
* @param {string} str String to tokenize.
* @param {number} padding Number of padding tokens.
* @returns {number} Token count.
*/
function callTokenizer(type, str, padding) {
function callTokenizer(type, str) {
if (type === tokenizers.NONE) return guesstimate(str);
switch (type) {
case tokenizers.NONE:
return guesstimate(str) + padding;
case tokenizers.GPT2:
return countTokensRemote('/api/tokenizers/gpt2/encode', str, padding);
case tokenizers.LLAMA:
return countTokensRemote('/api/tokenizers/llama/encode', str, padding);
case tokenizers.NERD:
return countTokensRemote('/api/tokenizers/nerdstash/encode', str, padding);
case tokenizers.NERD2:
return countTokensRemote('/api/tokenizers/nerdstash_v2/encode', str, padding);
case tokenizers.MISTRAL:
return countTokensRemote('/api/tokenizers/mistral/encode', str, padding);
case tokenizers.YI:
return countTokensRemote('/api/tokenizers/yi/encode', str, padding);
case tokenizers.API:
return countTokensRemote('/tokenize_via_api', str, padding);
default:
console.warn('Unknown tokenizer type', type);
return callTokenizer(tokenizers.NONE, str, padding);
case tokenizers.API_CURRENT:
return callTokenizer(currentRemoteTokenizerAPI(), str);
case tokenizers.API_KOBOLD:
return countTokensFromKoboldAPI(str);
case tokenizers.API_TEXTGENERATIONWEBUI:
return countTokensFromTextgenAPI(str);
default: {
const endpointUrl = TOKENIZER_URLS[type]?.count;
if (!endpointUrl) {
console.warn('Unknown tokenizer type', type);
return apiFailureTokenCount(str);
}
return countTokensFromServer(endpointUrl, str);
}
}
}
@@ -219,7 +286,7 @@ export function getTokenCount(str, padding = undefined) {
return cacheObject[cacheKey];
}
const result = callTokenizer(tokenizerType, str, padding);
const result = callTokenizer(tokenizerType, str) + padding;
if (isNaN(result)) {
console.warn('Token count calculation returned NaN');
@@ -309,10 +376,18 @@ export function getTokenizerModel() {
}
}
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
return oai_settings.google_model;
}
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
return claudeTokenizer;
}
if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
return mistralTokenizer;
}
// Default to Turbo 3.5
return turboTokenizer;
}
@@ -322,6 +397,15 @@ export function getTokenizerModel() {
*/
export function countTokensOpenAI(messages, full = false) {
const shouldTokenizeAI21 = oai_settings.chat_completion_source === chat_completion_sources.AI21 && oai_settings.use_ai21_tokenizer;
const shouldTokenizeGoogle = oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE && oai_settings.use_google_tokenizer;
let tokenizerEndpoint = '';
if (shouldTokenizeAI21) {
tokenizerEndpoint = '/api/tokenizers/ai21/count';
} else if (shouldTokenizeGoogle) {
tokenizerEndpoint = `/api/tokenizers/google/count?model=${getTokenizerModel()}`;
} else {
tokenizerEndpoint = `/api/tokenizers/openai/count?model=${getTokenizerModel()}`;
}
const cacheObject = getTokenCacheObject();
if (!Array.isArray(messages)) {
@@ -333,7 +417,7 @@ export function countTokensOpenAI(messages, full = false) {
for (const message of messages) {
const model = getTokenizerModel();
if (model === 'claude' || shouldTokenizeAI21) {
if (model === 'claude' || shouldTokenizeAI21 || shouldTokenizeGoogle) {
full = true;
}
@@ -349,7 +433,7 @@ export function countTokensOpenAI(messages, full = false) {
jQuery.ajax({
async: false,
type: 'POST', //
url: shouldTokenizeAI21 ? '/api/tokenizers/ai21/count' : `/api/tokenizers/openai/count?model=${model}`,
url: tokenizerEndpoint,
data: JSON.stringify([message]),
dataType: 'json',
contentType: 'application/json',
@@ -391,76 +475,131 @@ function getTokenCacheObject() {
return tokenCache[String(chatId)];
}
function getRemoteTokenizationParams(str) {
return {
text: str,
main_api,
api_type: textgen_settings.type,
url: getAPIServerUrl(),
legacy_api: main_api === 'textgenerationwebui' &&
textgen_settings.legacy_api &&
textgen_settings.type !== MANCER,
};
}
/**
* Counts token using the remote server API.
* Count tokens using the server API.
* @param {string} endpoint API endpoint.
* @param {string} str String to tokenize.
* @param {number} padding Number of padding tokens.
* @returns {number} Token count with padding.
* @returns {number} Token count.
*/
function countTokensRemote(endpoint, str, padding) {
function countTokensFromServer(endpoint, str) {
let tokenCount = 0;
jQuery.ajax({
async: false,
type: 'POST',
url: endpoint,
data: JSON.stringify(getRemoteTokenizationParams(str)),
data: JSON.stringify({ text: str }),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
if (typeof data.count === 'number') {
tokenCount = data.count;
} else {
tokenCount = guesstimate(str);
console.error('Error counting tokens');
if (!sessionStorage.getItem(TOKENIZER_WARNING_KEY)) {
toastr.warning(
'Your selected API doesn\'t support the tokenization endpoint. Using estimated counts.',
'Error counting tokens',
{ timeOut: 10000, preventDuplicates: true },
);
sessionStorage.setItem(TOKENIZER_WARNING_KEY, String(true));
}
tokenCount = apiFailureTokenCount(str);
}
},
});
return tokenCount + padding;
return tokenCount;
}
/**
* Count tokens using the AI provider's API.
* @param {string} str String to tokenize.
* @returns {number} Token count.
*/
function countTokensFromKoboldAPI(str) {
let tokenCount = 0;
jQuery.ajax({
async: false,
type: 'POST',
url: TOKENIZER_URLS[tokenizers.API_KOBOLD].count,
data: JSON.stringify({
text: str,
url: api_server,
}),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
if (typeof data.count === 'number') {
tokenCount = data.count;
} else {
tokenCount = apiFailureTokenCount(str);
}
},
});
return tokenCount;
}
function getTextgenAPITokenizationParams(str) {
return {
text: str,
api_type: textgen_settings.type,
url: api_server_textgenerationwebui,
legacy_api:
textgen_settings.legacy_api &&
textgen_settings.type !== MANCER,
};
}
/**
* Count tokens using the AI provider's API.
* @param {string} str String to tokenize.
* @returns {number} Token count.
*/
function countTokensFromTextgenAPI(str) {
let tokenCount = 0;
jQuery.ajax({
async: false,
type: 'POST',
url: TOKENIZER_URLS[tokenizers.API_TEXTGENERATIONWEBUI].count,
data: JSON.stringify(getTextgenAPITokenizationParams(str)),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
if (typeof data.count === 'number') {
tokenCount = data.count;
} else {
tokenCount = apiFailureTokenCount(str);
}
},
});
return tokenCount;
}
function apiFailureTokenCount(str) {
console.error('Error counting tokens');
if (!sessionStorage.getItem(TOKENIZER_WARNING_KEY)) {
toastr.warning(
'Your selected API doesn\'t support the tokenization endpoint. Using estimated counts.',
'Error counting tokens',
{ timeOut: 10000, preventDuplicates: true },
);
sessionStorage.setItem(TOKENIZER_WARNING_KEY, String(true));
}
return guesstimate(str);
}
/**
* Calls the underlying tokenizer model to encode a string to tokens.
* @param {string} endpoint API endpoint.
* @param {string} str String to tokenize.
* @param {string} model Tokenizer model.
* @returns {number[]} Array of token ids.
*/
function getTextTokensRemote(endpoint, str, model = '') {
if (model) {
endpoint += `?model=${model}`;
}
function getTextTokensFromServer(endpoint, str) {
let ids = [];
jQuery.ajax({
async: false,
type: 'POST',
url: endpoint,
data: JSON.stringify(getRemoteTokenizationParams(str)),
data: JSON.stringify({ text: str }),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
@@ -475,16 +614,59 @@ function getTextTokensRemote(endpoint, str, model = '') {
return ids;
}
/**
* Calls the AI provider's tokenize API to encode a string to tokens.
* @param {string} str String to tokenize.
* @returns {number[]} Array of token ids.
*/
function getTextTokensFromTextgenAPI(str) {
let ids = [];
jQuery.ajax({
async: false,
type: 'POST',
url: TOKENIZER_URLS[tokenizers.API_TEXTGENERATIONWEBUI].encode,
data: JSON.stringify(getTextgenAPITokenizationParams(str)),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
ids = data.ids;
},
});
return ids;
}
/**
* Calls the AI provider's tokenize API to encode a string to tokens.
* @param {string} str String to tokenize.
* @returns {number[]} Array of token ids.
*/
function getTextTokensFromKoboldAPI(str) {
let ids = [];
jQuery.ajax({
async: false,
type: 'POST',
url: TOKENIZER_URLS[tokenizers.API_KOBOLD].encode,
data: JSON.stringify({
text: str,
url: api_server,
}),
dataType: 'json',
contentType: 'application/json',
success: function (data) {
ids = data.ids;
},
});
return ids;
}
/**
* Calls the underlying tokenizer model to decode token ids to text.
* @param {string} endpoint API endpoint.
* @param {number[]} ids Array of token ids
*/
function decodeTextTokensRemote(endpoint, ids, model = '') {
if (model) {
endpoint += `?model=${model}`;
}
function decodeTextTokensFromServer(endpoint, ids) {
let text = '';
jQuery.ajax({
async: false,
@@ -501,64 +683,64 @@ function decodeTextTokensRemote(endpoint, ids, model = '') {
}
/**
* Encodes a string to tokens using the remote server API.
* Encodes a string to tokens using the server API.
* @param {number} tokenizerType Tokenizer type.
* @param {string} str String to tokenize.
* @returns {number[]} Array of token ids.
*/
export function getTextTokens(tokenizerType, str) {
switch (tokenizerType) {
case tokenizers.GPT2:
return getTextTokensRemote('/api/tokenizers/gpt2/encode', str);
case tokenizers.LLAMA:
return getTextTokensRemote('/api/tokenizers/llama/encode', str);
case tokenizers.NERD:
return getTextTokensRemote('/api/tokenizers/nerdstash/encode', str);
case tokenizers.NERD2:
return getTextTokensRemote('/api/tokenizers/nerdstash_v2/encode', str);
case tokenizers.MISTRAL:
return getTextTokensRemote('/api/tokenizers/mistral/encode', str);
case tokenizers.YI:
return getTextTokensRemote('/api/tokenizers/yi/encode', str);
case tokenizers.OPENAI: {
const model = getTokenizerModel();
return getTextTokensRemote('/api/tokenizers/openai/encode', str, model);
case tokenizers.API_CURRENT:
return getTextTokens(currentRemoteTokenizerAPI(), str);
case tokenizers.API_TEXTGENERATIONWEBUI:
return getTextTokensFromTextgenAPI(str);
case tokenizers.API_KOBOLD:
return getTextTokensFromKoboldAPI(str);
default: {
const tokenizerEndpoints = TOKENIZER_URLS[tokenizerType];
if (!tokenizerEndpoints) {
apiFailureTokenCount(str);
console.warn('Unknown tokenizer type', tokenizerType);
return [];
}
let endpointUrl = tokenizerEndpoints.encode;
if (!endpointUrl) {
apiFailureTokenCount(str);
console.warn('This tokenizer type does not support encoding', tokenizerType);
return [];
}
if (tokenizerType === tokenizers.OPENAI) {
endpointUrl += `?model=${getTokenizerModel()}`;
}
return getTextTokensFromServer(endpointUrl, str);
}
case tokenizers.API:
return getTextTokensRemote('/tokenize_via_api', str);
default:
console.warn('Calling getTextTokens with unsupported tokenizer type', tokenizerType);
return [];
}
}
/**
* Decodes token ids to text using the remote server API.
* Decodes token ids to text using the server API.
* @param {number} tokenizerType Tokenizer type.
* @param {number[]} ids Array of token ids
*/
export function decodeTextTokens(tokenizerType, ids) {
switch (tokenizerType) {
case tokenizers.GPT2:
return decodeTextTokensRemote('/api/tokenizers/gpt2/decode', ids);
case tokenizers.LLAMA:
return decodeTextTokensRemote('/api/tokenizers/llama/decode', ids);
case tokenizers.NERD:
return decodeTextTokensRemote('/api/tokenizers/nerdstash/decode', ids);
case tokenizers.NERD2:
return decodeTextTokensRemote('/api/tokenizers/nerdstash_v2/decode', ids);
case tokenizers.MISTRAL:
return decodeTextTokensRemote('/api/tokenizers/mistral/decode', ids);
case tokenizers.YI:
return decodeTextTokensRemote('/api/tokenizers/yi/decode', ids);
case tokenizers.OPENAI: {
const model = getTokenizerModel();
return decodeTextTokensRemote('/api/tokenizers/openai/decode', ids, model);
}
default:
console.warn('Calling decodeTextTokens with unsupported tokenizer type', tokenizerType);
return '';
// Currently, neither remote API can decode, but this may change in the future. Put this guard here to be safe
if (tokenizerType === tokenizers.API_CURRENT) {
return decodeTextTokens(tokenizers.NONE, ids);
}
const tokenizerEndpoints = TOKENIZER_URLS[tokenizerType];
if (!tokenizerEndpoints) {
console.warn('Unknown tokenizer type', tokenizerType);
return [];
}
let endpointUrl = tokenizerEndpoints.decode;
if (!endpointUrl) {
console.warn('This tokenizer type does not support decoding', tokenizerType);
return [];
}
if (tokenizerType === tokenizers.OPENAI) {
endpointUrl += `?model=${getTokenizerModel()}`;
}
return decodeTextTokensFromServer(endpointUrl, ids);
}
export async function initTokenizers() {

View File

@@ -741,6 +741,38 @@ export function escapeRegex(string) {
return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
}
export class Stopwatch {
/**
* Initializes a Stopwatch class.
* @param {number} interval Update interval in milliseconds. Must be a finite number above zero.
*/
constructor(interval) {
if (isNaN(interval) || !isFinite(interval) || interval <= 0) {
console.warn('Invalid interval for Stopwatch, setting to 1');
interval = 1;
}
this.interval = interval;
this.lastAction = Date.now();
}
/**
* Executes a function if the interval passed.
* @param {(arg0: any) => any} action Action function
* @returns Promise<void>
*/
async tick(action) {
const passed = (Date.now() - this.lastAction);
if (passed < this.interval) {
return;
}
await action();
this.lastAction = Date.now();
}
}
/**
* Provides an interface for rate limiting function calls.
*/
@@ -998,6 +1030,11 @@ export function loadFileToDocument(url, type) {
* @returns {Promise<string>} A promise that resolves to the thumbnail data URL.
*/
export function createThumbnail(dataUrl, maxWidth, maxHeight, type = 'image/jpeg') {
// Someone might pass in a base64 encoded string without the data URL prefix
if (!dataUrl.includes('data:')) {
dataUrl = `data:image/jpeg;base64,${dataUrl}`;
}
return new Promise((resolve, reject) => {
const img = new Image();
img.src = dataUrl;
@@ -1143,11 +1180,13 @@ export async function extractTextFromPDF(blob) {
* @param {Blob} blob HTML content blob
* @returns {Promise<string>} A promise that resolves to the parsed text.
*/
export async function extractTextFromHTML(blob) {
export async function extractTextFromHTML(blob, textSelector = 'body') {
const html = await blob.text();
const domParser = new DOMParser();
const document = domParser.parseFromString(DOMPurify.sanitize(html), 'text/html');
const text = postProcessText(document.body.textContent);
const elements = document.querySelectorAll(textSelector);
const rawText = Array.from(elements).map(e => e.textContent).join('\n');
const text = postProcessText(rawText);
return text;
}

View File

@@ -1,6 +1,9 @@
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
import { extension_settings, saveMetadataDebounced } from './extensions.js';
import { executeSlashCommands, registerSlashCommand } from './slash-commands.js';
import { isFalseBoolean } from './utils.js';
const MAX_LOOPS = 100;
function getLocalVariable(name, args = {}) {
if (!chat_metadata.variables) {
@@ -301,8 +304,7 @@ function listVariablesCallback() {
}
async function whileCallback(args, command) {
const MAX_LOOPS = 100;
const isGuardOff = ['off', 'false', '0'].includes(args.guard?.toLowerCase());
const isGuardOff = isFalseBoolean(args.guard);
const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
for (let i = 0; i < iterations; i++) {
@@ -319,6 +321,19 @@ async function whileCallback(args, command) {
return '';
}
async function timesCallback(args, value) {
const [repeats, ...commandParts] = value.split(' ');
const command = commandParts.join(' ');
const isGuardOff = isFalseBoolean(args.guard);
const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
for (let i = 0; i < iterations; i++) {
await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i));
}
return '';
}
async function ifCallback(args, command) {
const { a, b, rule } = parseBooleanOperands(args);
const result = evalBoolean(rule, a, b);
@@ -637,6 +652,21 @@ function lenValuesCallback(value) {
return parsedValue.length;
}
function randValuesCallback(from, to, args) {
const range = to - from;
const value = from + Math.random() * range;
if (args.round == 'round') {
return Math.round(value);
}
if (args.round == 'ceil') {
return Math.ceil(value);
}
if (args.round == 'floor') {
return Math.floor(value);
}
return value;
}
export function registerVariableCommands() {
registerSlashCommand('listvar', listVariablesCallback, [], ' list registered chat variables', true, true);
registerSlashCommand('setvar', (args, value) => setLocalVariable(args.key || args.name, value, args), [], '<span class="monospace">key=varname index=listIndex (value)</span> set a local variable value and pass it down the pipe, index is optional, e.g. <tt>/setvar key=color green</tt>', true, true);
@@ -651,6 +681,7 @@ export function registerVariableCommands() {
registerSlashCommand('decglobalvar', (_, value) => decrementGlobalVariable(value), [], '<span class="monospace">(key)</span> decrement a global variable by 1 and pass the result down the pipe, e.g. <tt>/decglobalvar score</tt>', true, true);
registerSlashCommand('if', ifCallback, [], '<span class="monospace">left=varname1 right=varname2 rule=comparison else="(alt.command)" "(command)"</span> compare the value of the left operand "a" with the value of the right operand "b", and if the condition yields true, then execute any valid slash command enclosed in quotes and pass the result of the command execution down the pipe. Numeric values and string literals for left and right operands supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/if left=score right=10 rule=gte "/speak You win"</tt> triggers a /speak command if the value of "score" is greater or equals 10.', true, true);
registerSlashCommand('while', whileCallback, [], '<span class="monospace">left=varname1 right=varname2 rule=comparison "(command)"</span> compare the value of the left operand "a" with the value of the right operand "b", and if the condition yields true, then execute any valid slash command enclosed in quotes. Numeric values and string literals for left and right operands supported. Available rules: gt => a > b, gte => a >= b, lt => a < b, lte => a <= b, eq => a == b, neq => a != b, not => !a, in (strings) => a includes b, nin (strings) => a not includes b, e.g. <tt>/setvar key=i 0 | /while left=i right=10 rule=let "/addvar key=i 1"</tt> adds 1 to the value of "i" until it reaches 10. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
registerSlashCommand('times', (args, value) => timesCallback(args, value), [], '<span class="monospace">(repeats) "(command)"</span> execute any valid slash command enclosed in quotes <tt>repeats</tt> number of times, e.g. <tt>/setvar key=i 1 | /times 5 "/addvar key=i 1"</tt> adds 1 to the value of "i" 5 times. <tt>{{timesIndex}}</tt> is replaced with the iteration number (zero-based), e.g. <tt>/times 4 "/echo {{timesIndex}}"</tt> echos the numbers 0 through 4. Loops are limited to 100 iterations by default, pass guard=off to disable.', true, true);
registerSlashCommand('flushvar', (_, value) => deleteLocalVariable(value), [], '<span class="monospace">(key)</span> delete a local variable, e.g. <tt>/flushvar score</tt>', true, true);
registerSlashCommand('flushglobalvar', (_, value) => deleteGlobalVariable(value), [], '<span class="monospace">(key)</span> delete a global variable, e.g. <tt>/flushglobalvar score</tt>', true, true);
registerSlashCommand('add', (_, value) => addValuesCallback(value), [], '<span class="monospace">(a b c d)</span> performs an addition of the set of values and passes the result down the pipe, can use variable names, e.g. <tt>/add 10 i 30 j</tt>', true, true);
@@ -668,4 +699,5 @@ export function registerVariableCommands() {
registerSlashCommand('sqrt', (_, value) => sqrtValuesCallback(value), [], '<span class="monospace">(a)</span> performs a square root operation of a value and passes the result down the pipe, can use variable names, e.g. <tt>/sqrt i</tt>', true, true);
registerSlashCommand('round', (_, value) => roundValuesCallback(value), [], '<span class="monospace">(a)</span> rounds a value and passes the result down the pipe, can use variable names, e.g. <tt>/round i</tt>', true, true);
registerSlashCommand('len', (_, value) => lenValuesCallback(value), [], '<span class="monospace">(a)</span> gets the length of a value and passes the result down the pipe, can use variable names, e.g. <tt>/len i</tt>', true, true);
registerSlashCommand('rand', (args, value) => randValuesCallback(Number(args.from ?? 0), Number(args.to ?? (value.length ? value : 1)), args), [], '<span class="monospace">(from=number=0 to=number=1 round=round|ceil|floor)</span> returns a random number between from and to, e.g. <tt>/rand</tt> or <tt>/rand 10</tt> or <tt>/rand from=5 to=10</tt>', true, true);
}

View File

@@ -3,7 +3,7 @@ import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile,
import { extension_settings, getContext } from './extensions.js';
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
import { registerSlashCommand } from './slash-commands.js';
import { getDeviceInfo } from './RossAscends-mods.js';
import { isMobile } from './RossAscends-mods.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
import { getTokenCount } from './tokenizers.js';
import { power_user } from './power-user.js';
@@ -441,7 +441,7 @@ async function loadWorldInfoData(name) {
}
async function updateWorldInfoList() {
const result = await fetch('/getsettings', {
const result = await fetch('/api/settings/get', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({}),
@@ -896,8 +896,8 @@ function getWorldEntry(name, data, entry) {
const characterFilter = template.find('select[name="characterFilter"]');
characterFilter.data('uid', entry.uid);
const deviceInfo = getDeviceInfo();
if (deviceInfo && deviceInfo.device.type === 'desktop') {
if (!isMobile()) {
$(characterFilter).select2({
width: '100%',
placeholder: 'All characters will pull from this entry.',
@@ -1684,20 +1684,13 @@ async function checkWorldInfo(chat, maxContext) {
// Add the depth or AN if enabled
// Put this code here since otherwise, the chat reference is modified
if (extension_settings.note.allowWIScan) {
for (const key of Object.keys(context.extensionPrompts)) {
if (key.startsWith('DEPTH_PROMPT')) {
const depthPrompt = getExtensionPromptByName(key);
if (depthPrompt) {
textToScan = `${depthPrompt}\n${textToScan}`;
}
for (const key of Object.keys(context.extensionPrompts)) {
if (context.extensionPrompts[key]?.scan) {
const prompt = getExtensionPromptByName(key);
if (prompt) {
textToScan = `${prompt}\n${textToScan}`;
}
}
const anPrompt = getExtensionPromptByName(NOTE_MODULE_NAME);
if (anPrompt) {
textToScan = `${anPrompt}\n${textToScan}`;
}
}
// Transform the resulting string
@@ -1948,7 +1941,7 @@ async function checkWorldInfo(chat, maxContext) {
if (shouldWIAddPrompt) {
const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value;
const ANWithWI = `${ANTopEntries.join('\n')}\n${originalAN}\n${ANBottomEntries.join('\n')}`;
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth], extension_settings.note.allowWIScan);
}
return { worldInfoBefore, worldInfoAfter, WIDepthEntries };
@@ -2558,8 +2551,7 @@ jQuery(() => {
$(document).on('click', '.chat_lorebook_button', assignLorebookToChat);
// Not needed on mobile
const deviceInfo = getDeviceInfo();
if (deviceInfo && deviceInfo.device.type === 'desktop') {
if (!isMobile()) {
$('#world_info').select2({
width: '100%',
placeholder: 'No Worlds active. Click here to select.',