mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge remote-tracking branch 'origin/staging' into bugfix/extension-translate
# Conflicts: # public/scripts/extensions.js
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { DOMPurify, Bowser } from '../lib.js';
|
||||
import { DOMPurify, Bowser, slideToggle } from '../lib.js';
|
||||
|
||||
import {
|
||||
characters,
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
menu_type,
|
||||
substituteParams,
|
||||
sendTextareaMessage,
|
||||
getSlideToggleOptions,
|
||||
} from '../script.js';
|
||||
|
||||
import {
|
||||
@@ -315,7 +316,7 @@ function RA_checkOnlineStatus() {
|
||||
if (online_status == 'no_connection') {
|
||||
const send_textarea = $('#send_textarea');
|
||||
send_textarea.attr('placeholder', send_textarea.attr('no_connection_text')); //Input bar placeholder tells users they are not connected
|
||||
//$('#send_form').addClass('no-connection'); //entire input form area is red when not connected
|
||||
$('#send_form').addClass('no-connection');
|
||||
$('#send_but').addClass('displayNone'); //send button is hidden when not connected;
|
||||
$('#mes_continue').addClass('displayNone'); //continue button is hidden when not connected;
|
||||
$('#mes_impersonate').addClass('displayNone'); //continue button is hidden when not connected;
|
||||
@@ -326,7 +327,7 @@ function RA_checkOnlineStatus() {
|
||||
if (online_status !== undefined && online_status !== 'no_connection') {
|
||||
const send_textarea = $('#send_textarea');
|
||||
send_textarea.attr('placeholder', send_textarea.attr('connected_text')); //on connect, placeholder tells user to type message
|
||||
//$('#send_form').removeClass('no-connection');
|
||||
$('#send_form').removeClass('no-connection');
|
||||
$('#API-status-top').removeClass('fa-plug-circle-exclamation redOverlayGlow');
|
||||
$('#API-status-top').addClass('fa-plug');
|
||||
connection_made = true;
|
||||
@@ -748,8 +749,8 @@ export function initRossMods() {
|
||||
$(RightNavDrawerIcon).removeClass('drawerPinnedOpen');
|
||||
|
||||
if ($(RightNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
|
||||
$(RightNavPanel).slideToggle(200, 'swing');
|
||||
$(RightNavDrawerIcon).toggleClass('openIcon closedIcon');
|
||||
slideToggle(RightNavPanel, getSlideToggleOptions());
|
||||
$(RightNavDrawerIcon).toggleClass('closedIcon openIcon');
|
||||
$(RightNavPanel).toggleClass('openDrawer closedDrawer');
|
||||
}
|
||||
}
|
||||
@@ -766,8 +767,8 @@ export function initRossMods() {
|
||||
$(LeftNavDrawerIcon).removeClass('drawerPinnedOpen');
|
||||
|
||||
if ($(LeftNavPanel).hasClass('openDrawer') && $('.openDrawer').length > 1) {
|
||||
$(LeftNavPanel).slideToggle(200, 'swing');
|
||||
$(LeftNavDrawerIcon).toggleClass('openIcon closedIcon');
|
||||
slideToggle(LeftNavPanel, getSlideToggleOptions());
|
||||
$(LeftNavDrawerIcon).toggleClass('closedIcon openIcon');
|
||||
$(LeftNavPanel).toggleClass('openDrawer closedDrawer');
|
||||
}
|
||||
}
|
||||
@@ -786,8 +787,8 @@ export function initRossMods() {
|
||||
|
||||
if ($(WorldInfo).hasClass('openDrawer') && $('.openDrawer').length > 1) {
|
||||
console.debug('closing WI after lock removal');
|
||||
$(WorldInfo).slideToggle(200, 'swing');
|
||||
$(WIDrawerIcon).toggleClass('openIcon closedIcon');
|
||||
slideToggle(WorldInfo, getSlideToggleOptions());
|
||||
$(WIDrawerIcon).toggleClass('closedIcon openIcon');
|
||||
$(WorldInfo).toggleClass('openDrawer closedDrawer');
|
||||
}
|
||||
}
|
||||
@@ -886,7 +887,40 @@ export function initRossMods() {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
const cssAutofit = CSS.supports('field-sizing', 'content');
|
||||
|
||||
if (cssAutofit) {
|
||||
let lastHeight = chatBlock.offsetHeight;
|
||||
const chatBlockResizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target !== chatBlock) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const threshold = 1;
|
||||
const newHeight = chatBlock.offsetHeight;
|
||||
const deltaHeight = newHeight - lastHeight;
|
||||
const isScrollAtBottom = Math.abs(chatBlock.scrollHeight - chatBlock.scrollTop - newHeight) <= threshold;
|
||||
|
||||
if (!isScrollAtBottom && Math.abs(deltaHeight) > threshold) {
|
||||
chatBlock.scrollTop -= deltaHeight;
|
||||
}
|
||||
lastHeight = newHeight;
|
||||
}
|
||||
});
|
||||
|
||||
chatBlockResizeObserver.observe(chatBlock);
|
||||
}
|
||||
|
||||
sendTextArea.addEventListener('input', () => {
|
||||
saveUserInputDebounced();
|
||||
|
||||
if (cssAutofit) {
|
||||
// Unset modifications made with a manual resize
|
||||
sendTextArea.style.height = 'auto';
|
||||
return;
|
||||
}
|
||||
|
||||
const hasContent = sendTextArea.value !== '';
|
||||
const fitsCurrentSize = sendTextArea.scrollHeight <= sendTextArea.offsetHeight;
|
||||
const isScrollbarShown = sendTextArea.clientWidth < sendTextArea.offsetWidth;
|
||||
@@ -894,7 +928,6 @@ export function initRossMods() {
|
||||
const needsDebounce = hasContent && (fitsCurrentSize || (isScrollbarShown && isHalfScreenHeight));
|
||||
if (needsDebounce) autoFitSendTextAreaDebounced();
|
||||
else autoFitSendTextArea();
|
||||
saveUserInputDebounced();
|
||||
});
|
||||
|
||||
restoreUserInput();
|
||||
|
@@ -12,6 +12,7 @@ const LIST_METADATA_KEY = 'chat_backgrounds';
|
||||
export let background_settings = {
|
||||
name: '__transparent.png',
|
||||
url: generateUrlParameter('__transparent.png', false),
|
||||
fitting: 'classic',
|
||||
};
|
||||
|
||||
export function loadBackgroundSettings(settings) {
|
||||
@@ -19,7 +20,12 @@ export function loadBackgroundSettings(settings) {
|
||||
if (!backgroundSettings || !backgroundSettings.name || !backgroundSettings.url) {
|
||||
backgroundSettings = background_settings;
|
||||
}
|
||||
if (!backgroundSettings.fitting) {
|
||||
backgroundSettings.fitting = 'classic';
|
||||
}
|
||||
setBackground(backgroundSettings.name, backgroundSettings.url);
|
||||
setFittingClass(backgroundSettings.fitting);
|
||||
$('#background_fitting').val(backgroundSettings.fitting);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,6 +339,14 @@ async function autoBackgroundCommand() {
|
||||
const bestMatch = fuse.search(reply, { limit: 1 });
|
||||
|
||||
if (bestMatch.length == 0) {
|
||||
for (const option of options) {
|
||||
if (String(reply).toLowerCase().includes(option.text.toLowerCase())) {
|
||||
console.debug('Fallback choosing background:', option);
|
||||
option.element.click();
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
toastr.warning('No match found. Please try again.');
|
||||
return '';
|
||||
}
|
||||
@@ -462,6 +476,18 @@ function highlightNewBackground(bg) {
|
||||
flashHighlight(newBg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fitting class for the background element
|
||||
* @param {string} fitting Fitting type
|
||||
*/
|
||||
function setFittingClass(fitting) {
|
||||
const backgrounds = $('#bg1, #bg_custom');
|
||||
backgrounds.toggleClass('cover', fitting === 'cover');
|
||||
backgrounds.toggleClass('contain', fitting === 'contain');
|
||||
backgrounds.toggleClass('stretch', fitting === 'stretch');
|
||||
backgrounds.toggleClass('center', fitting === 'center');
|
||||
}
|
||||
|
||||
function onBackgroundFilterInput() {
|
||||
const filterValue = String($(this).val()).toLowerCase();
|
||||
$('#bg_menu_content > div').each(function () {
|
||||
@@ -502,4 +528,9 @@ export function initBackgrounds() {
|
||||
helpString: 'Automatically changes the background based on the chat context using the AI request prompt',
|
||||
}));
|
||||
|
||||
$('#background_fitting').on('input', function () {
|
||||
background_settings.fitting = String($(this).val());
|
||||
setFittingClass(background_settings.fitting);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
|
@@ -451,8 +451,14 @@ function getCustomSeparator() {
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the CFG prompt
|
||||
export function getCfgPrompt(guidanceScale, isNegative) {
|
||||
/**
|
||||
* Gets the CFG prompt based on the guidance scale.
|
||||
* @param {{type: number, value: number}} guidanceScale The CFG guidance scale
|
||||
* @param {boolean} isNegative Whether to get the negative prompt
|
||||
* @param {boolean} quiet Whether to suppress console output
|
||||
* @returns {{value: string, depth: number}} The CFG prompt and insertion depth
|
||||
*/
|
||||
export function getCfgPrompt(guidanceScale, isNegative, quiet = false) {
|
||||
let splitCfgPrompt = [];
|
||||
|
||||
const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? [];
|
||||
@@ -484,7 +490,7 @@ export function getCfgPrompt(guidanceScale, isNegative) {
|
||||
const customSeparator = getCustomSeparator();
|
||||
const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator);
|
||||
const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1;
|
||||
console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
|
||||
!quiet && console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`);
|
||||
|
||||
return {
|
||||
value: combinedCfgPrompt,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -393,7 +393,7 @@ jQuery(async function () {
|
||||
const sendButton = $(`
|
||||
<div id="send_picture" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
|
||||
Generate Caption
|
||||
<span data-i18n="Generate Caption">Generate Caption</span>
|
||||
</div>`);
|
||||
|
||||
$('#caption_wand_container').append(sendButton);
|
||||
|
@@ -53,6 +53,8 @@
|
||||
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
|
||||
<option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
|
||||
<option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>
|
||||
<option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
|
||||
@@ -69,7 +71,6 @@
|
||||
<option data-type="google" value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
|
||||
<option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
|
||||
<option data-type="google" value="gemini-1.5-pro-exp-0827">gemini-1.5-pro-exp-0827</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
|
||||
<option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
|
||||
<option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
|
||||
|
@@ -14,7 +14,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
|
||||
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { slashCommandReturnHelper } from '../../slash-commands/SlashCommandReturnHelper.js';
|
||||
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
|
||||
import { generateWebLlmChatPrompt, isWebLlmSupported } from '../shared.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'expressions';
|
||||
@@ -59,6 +59,7 @@ const EXPRESSION_API = {
|
||||
local: 0,
|
||||
extras: 1,
|
||||
llm: 2,
|
||||
webllm: 3,
|
||||
};
|
||||
|
||||
let expressionsList = null;
|
||||
@@ -697,6 +698,11 @@ async function moduleWorker() {
|
||||
return;
|
||||
}
|
||||
|
||||
// If using LLM api then check if streamingProcessor is finished to avoid sending multiple requests to the API
|
||||
if (extension_settings.expressions.api === EXPRESSION_API.llm && context.streamingProcessor && !context.streamingProcessor.isFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
// API is busy
|
||||
if (inApiCall) {
|
||||
console.debug('Classification API is busy');
|
||||
@@ -847,7 +853,7 @@ function setTalkingHeadState(newState) {
|
||||
extension_settings.expressions.talkinghead = newState; // Store setting
|
||||
saveSettingsDebounced();
|
||||
|
||||
if (extension_settings.expressions.api == EXPRESSION_API.local || extension_settings.expressions.api == EXPRESSION_API.llm) {
|
||||
if ([EXPRESSION_API.local, EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -979,6 +985,71 @@ async function setSpriteSlashCommand(_, spriteId) {
|
||||
return label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the sprite folder name (including override) for a character.
|
||||
* @param {object} char Character object
|
||||
* @param {string} char.avatar Avatar filename with extension
|
||||
* @returns {string} Sprite folder name
|
||||
* @throws {Error} If character not found or avatar not set
|
||||
*/
|
||||
function spriteFolderNameFromCharacter(char) {
|
||||
const avatarFileName = char.avatar.replace(/\.[^/.]+$/, '');
|
||||
const expressionOverride = extension_settings.expressionOverrides.find(e => e.name === avatarFileName);
|
||||
return expressionOverride?.path ? expressionOverride.path : avatarFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slash command callback for /uploadsprite
|
||||
*
|
||||
* label= is required
|
||||
* if name= is provided, it will be used as a findChar lookup
|
||||
* if name= is not provided, the last character's name will be used
|
||||
* if folder= is a full path, it will be used as the folder
|
||||
* if folder= is a partial path, it will be appended to the character's name
|
||||
* if folder= is not provided, the character's override folder will be used, if set
|
||||
*
|
||||
* @param {object} args
|
||||
* @param {string} args.name Character name or avatar key, passed through findChar
|
||||
* @param {string} args.label Expression label
|
||||
* @param {string} args.folder Sprite folder path, processed using backslash rules
|
||||
* @param {string} imageUrl Image URI to fetch and upload
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function uploadSpriteCommand({ name, label, folder }, imageUrl) {
|
||||
if (!imageUrl) throw new Error('Image URL is required');
|
||||
if (!label || typeof label !== 'string') throw new Error('Expression label is required');
|
||||
|
||||
label = label.replace(/[^a-z]/gi, '').toLowerCase().trim();
|
||||
if (!label) throw new Error('Expression label must contain at least one letter');
|
||||
|
||||
name = name || getLastCharacterMessage().original_avatar || getLastCharacterMessage().name;
|
||||
const char = findChar({ name });
|
||||
|
||||
if (!folder) {
|
||||
folder = spriteFolderNameFromCharacter(char);
|
||||
} else if (folder.startsWith('/') || folder.startsWith('\\')) {
|
||||
const subfolder = folder.slice(1);
|
||||
folder = `${char.name}/${subfolder}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], 'image.png', { type: 'image/png' });
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', folder); // this is the folder or character name
|
||||
formData.append('label', label); // this is the expression label
|
||||
formData.append('avatar', file); // this is the image file
|
||||
|
||||
await handleFileUpload('/api/sprites/upload', formData);
|
||||
console.debug(`[${MODULE_NAME}] Upload of ${imageUrl} completed for ${name} with label ${label}`);
|
||||
} catch (error) {
|
||||
console.error(`[${MODULE_NAME}] Error uploading file:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the classification text to reduce the amount of text sent to the API.
|
||||
* Quotes and asterisks are to be removed. If the text is less than 300 characters, it is returned as is.
|
||||
@@ -995,6 +1066,11 @@ function sampleClassifyText(text) {
|
||||
// Replace macros, remove asterisks and quotes
|
||||
let result = substituteParams(text).replace(/[*"]/g, '');
|
||||
|
||||
// If using LLM api there is no need to check length of characters
|
||||
if (extension_settings.expressions.api === EXPRESSION_API.llm) {
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
const SAMPLE_THRESHOLD = 500;
|
||||
const HALF_SAMPLE_THRESHOLD = SAMPLE_THRESHOLD / 2;
|
||||
|
||||
@@ -1047,11 +1123,39 @@ function parseLlmResponse(emotionResponse, labels) {
|
||||
console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse);
|
||||
return result[0].item;
|
||||
}
|
||||
const lowerCaseResponse = String(emotionResponse || '').toLowerCase();
|
||||
for (const label of labels) {
|
||||
if (lowerCaseResponse.includes(label.toLowerCase())) {
|
||||
console.debug(`Found label ${label} in the LLM response:`, emotionResponse);
|
||||
return label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Could not parse emotion response ' + emotionResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the JSON schema for the LLM API.
|
||||
* @param {string[]} emotions A list of emotions to search for.
|
||||
* @returns {object} The JSON schema for the LLM API.
|
||||
*/
|
||||
function getJsonSchema(emotions) {
|
||||
return {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
emotion: {
|
||||
type: 'string',
|
||||
enum: emotions,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'emotion',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function onTextGenSettingsReady(args) {
|
||||
// Only call if inside an API call
|
||||
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
|
||||
@@ -1061,19 +1165,7 @@ function onTextGenSettingsReady(args) {
|
||||
stop: [],
|
||||
stopping_strings: [],
|
||||
custom_token_bans: [],
|
||||
json_schema: {
|
||||
$schema: 'http://json-schema.org/draft-04/schema#',
|
||||
type: 'object',
|
||||
properties: {
|
||||
emotion: {
|
||||
type: 'string',
|
||||
enum: emotions,
|
||||
},
|
||||
},
|
||||
required: [
|
||||
'emotion',
|
||||
],
|
||||
},
|
||||
json_schema: getJsonSchema(emotions),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1129,6 +1221,22 @@ export async function getExpressionLabel(text, expressionsApi = extension_settin
|
||||
const emotionResponse = await generateRaw(text, main_api, false, false, prompt);
|
||||
return parseLlmResponse(emotionResponse, expressionsList);
|
||||
}
|
||||
// Using WebLLM
|
||||
case EXPRESSION_API.webllm: {
|
||||
if (!isWebLlmSupported()) {
|
||||
console.warn('WebLLM is not supported. Using fallback expression');
|
||||
return getFallbackExpression();
|
||||
}
|
||||
|
||||
const expressionsList = await getExpressionsList();
|
||||
const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
|
||||
const messages = [
|
||||
{ role: 'user', content: text + '\n\n' + prompt },
|
||||
];
|
||||
|
||||
const emotionResponse = await generateWebLlmChatPrompt(messages);
|
||||
return parseLlmResponse(emotionResponse, expressionsList);
|
||||
}
|
||||
// Extras
|
||||
default: {
|
||||
const url = new URL(getApiUrl());
|
||||
@@ -1239,8 +1347,6 @@ async function drawSpritesList(character, labels, sprites) {
|
||||
* @returns {Promise<string>} Rendered list item template
|
||||
*/
|
||||
async function getListItem(item, imageSrc, textClass, isCustom) {
|
||||
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
imageSrc = isFirefox ? `${imageSrc}?t=${Date.now()}` : imageSrc;
|
||||
return renderExtensionTemplateAsync(MODULE_NAME, 'list-item', { item, imageSrc, textClass, isCustom });
|
||||
}
|
||||
|
||||
@@ -1593,7 +1699,7 @@ function onExpressionApiChanged() {
|
||||
const tempApi = this.value;
|
||||
if (tempApi) {
|
||||
extension_settings.expressions.api = Number(tempApi);
|
||||
$('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm);
|
||||
$('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api));
|
||||
expressionsList = null;
|
||||
spriteCache = {};
|
||||
moduleWorker();
|
||||
@@ -1930,7 +2036,7 @@ function migrateSettings() {
|
||||
|
||||
await renderAdditionalExpressionSettings();
|
||||
$('#expression_api').val(extension_settings.expressions.api ?? EXPRESSION_API.extras);
|
||||
$('.expression_llm_prompt_block').toggle(extension_settings.expressions.api === EXPRESSION_API.llm);
|
||||
$('.expression_llm_prompt_block').toggle([EXPRESSION_API.llm, EXPRESSION_API.webllm].includes(extension_settings.expressions.api));
|
||||
$('#expression_llm_prompt').val(extension_settings.expressions.llmPrompt ?? '');
|
||||
$('#expression_llm_prompt').on('input', function () {
|
||||
extension_settings.expressions.llmPrompt = $(this).val();
|
||||
@@ -2173,4 +2279,43 @@ function migrateSettings() {
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'uploadsprite',
|
||||
callback: async (args, url) => {
|
||||
await uploadSpriteCommand(args, url);
|
||||
return '';
|
||||
},
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'URL of the image to upload',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: true,
|
||||
}),
|
||||
],
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'name',
|
||||
description: 'Character name or avatar key (default is current character)',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'label',
|
||||
description: 'Sprite label/expression name',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
enumProvider: localEnumProviders.expressions,
|
||||
isRequired: true,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'folder',
|
||||
description: 'Override folder to upload into',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
],
|
||||
helpString: '<div>Upload a sprite from a URL.</div><div>Example:</div><pre><code>/uploadsprite name=Seraphina label=joy /user/images/Seraphina/Seraphina_2024-12-22@12h37m57s.png</code></pre>',
|
||||
}));
|
||||
})();
|
||||
|
@@ -24,7 +24,8 @@
|
||||
<select id="expression_api" class="flex1 margin0">
|
||||
<option value="0" data-i18n="Local">Local</option>
|
||||
<option value="1" data-i18n="Extras">Extras</option>
|
||||
<option value="2" data-i18n="LLM">LLM</option>
|
||||
<option value="2" data-i18n="Main API">Main API</option>
|
||||
<option value="3" data-i18n="WebLLM Extension">WebLLM Extension</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="expression_llm_prompt_block m-b-1 m-t-1">
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<div id="sd_gen" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" data-i18n="[title]Trigger Stable Diffusion"></div>
|
||||
<span>Generate Image</span>
|
||||
<span data-i18n="Generate Image">Generate Image</span>
|
||||
</div>
|
||||
<div id="sd_stop_gen" class="list-group-item flex-container flexGap5">
|
||||
<div class="fa-solid fa-circle-stop extensionsMenuExtensionButton" title="Abort current image generation task" data-i18n="[title]Abort current image generation task"></div>
|
||||
<span>Stop Image Generation</span>
|
||||
<span data-i18n="Stop Image Generation">Stop Image Generation</span>
|
||||
</div>
|
||||
|
@@ -218,7 +218,7 @@ const defaultSettings = {
|
||||
// CFG Scale
|
||||
scale_min: 1,
|
||||
scale_max: 30,
|
||||
scale_step: 0.5,
|
||||
scale_step: 0.1,
|
||||
scale: 7,
|
||||
|
||||
// Sampler steps
|
||||
@@ -319,6 +319,7 @@ const defaultSettings = {
|
||||
wand_visible: false,
|
||||
command_visible: false,
|
||||
interactive_visible: false,
|
||||
tool_visible: false,
|
||||
|
||||
// Stability AI settings
|
||||
stability_style_preset: 'anime',
|
||||
@@ -488,6 +489,7 @@ async function loadSettings() {
|
||||
$('#sd_wand_visible').prop('checked', extension_settings.sd.wand_visible);
|
||||
$('#sd_command_visible').prop('checked', extension_settings.sd.command_visible);
|
||||
$('#sd_interactive_visible').prop('checked', extension_settings.sd.interactive_visible);
|
||||
$('#sd_tool_visible').prop('checked', extension_settings.sd.tool_visible);
|
||||
$('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
|
||||
$('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
|
||||
$('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);
|
||||
@@ -844,6 +846,11 @@ function onInteractiveVisibleInput() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onToolVisibleInput() {
|
||||
extension_settings.sd.tool_visible = !!$('#sd_tool_visible').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onClipSkipInput() {
|
||||
extension_settings.sd.clip_skip = Number($('#sd_clip_skip').val());
|
||||
$('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
|
||||
@@ -1104,7 +1111,8 @@ function onHrSecondPassStepsInput() {
|
||||
}
|
||||
|
||||
function onComfyUrlInput() {
|
||||
extension_settings.sd.comfy_url = $('#sd_comfy_url').val();
|
||||
// Remove trailing slashes
|
||||
extension_settings.sd.comfy_url = String($('#sd_comfy_url').val());
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
@@ -1605,17 +1613,12 @@ async function loadVladSamplers() {
|
||||
}
|
||||
|
||||
async function loadNovelSamplers() {
|
||||
if (!secret_state[SECRET_KEYS.NOVEL]) {
|
||||
console.debug('NovelAI API key is not set.');
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'k_euler_ancestral',
|
||||
'k_euler',
|
||||
'k_dpmpp_2m',
|
||||
'k_dpmpp_sde',
|
||||
'k_dpmpp_2s_ancestral',
|
||||
'k_euler',
|
||||
'k_euler_ancestral',
|
||||
'k_dpm_fast',
|
||||
'ddim',
|
||||
];
|
||||
@@ -1971,12 +1974,11 @@ async function loadVladModels() {
|
||||
}
|
||||
|
||||
async function loadNovelModels() {
|
||||
if (!secret_state[SECRET_KEYS.NOVEL]) {
|
||||
console.debug('NovelAI API key is not set.');
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
value: 'nai-diffusion-4-curated-preview',
|
||||
text: 'NAI Diffusion Anime V4 (Curated Preview)',
|
||||
},
|
||||
{
|
||||
value: 'nai-diffusion-3',
|
||||
text: 'NAI Diffusion Anime V3',
|
||||
@@ -1985,22 +1987,10 @@ async function loadNovelModels() {
|
||||
value: 'nai-diffusion-2',
|
||||
text: 'NAI Diffusion Anime V2',
|
||||
},
|
||||
{
|
||||
value: 'nai-diffusion',
|
||||
text: 'NAI Diffusion Anime V1 (Full)',
|
||||
},
|
||||
{
|
||||
value: 'safe-diffusion',
|
||||
text: 'NAI Diffusion Anime V1 (Curated)',
|
||||
},
|
||||
{
|
||||
value: 'nai-diffusion-furry-3',
|
||||
text: 'NAI Diffusion Furry V3',
|
||||
},
|
||||
{
|
||||
value: 'nai-diffusion-furry',
|
||||
text: 'NAI Diffusion Furry',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2041,7 +2031,7 @@ async function loadSchedulers() {
|
||||
schedulers = await getAutoRemoteSchedulers();
|
||||
break;
|
||||
case sources.novel:
|
||||
schedulers = ['N/A'];
|
||||
schedulers = ['karras', 'native', 'exponential', 'polyexponential'];
|
||||
break;
|
||||
case sources.vlad:
|
||||
schedulers = ['N/A'];
|
||||
@@ -3150,6 +3140,7 @@ async function generateNovelImage(prompt, negativePrompt, signal) {
|
||||
prompt: prompt,
|
||||
model: extension_settings.sd.model,
|
||||
sampler: extension_settings.sd.sampler,
|
||||
scheduler: extension_settings.sd.scheduler,
|
||||
steps: steps,
|
||||
scale: extension_settings.sd.scale,
|
||||
width: width,
|
||||
@@ -3177,13 +3168,18 @@ async function generateNovelImage(prompt, negativePrompt, signal) {
|
||||
* @returns {{steps: number, width: number, height: number, sm: boolean, sm_dyn: boolean}} - A tuple of parameters for NovelAI API.
|
||||
*/
|
||||
function getNovelParams() {
|
||||
let steps = extension_settings.sd.steps;
|
||||
let steps = Math.min(extension_settings.sd.steps, 50);
|
||||
let width = extension_settings.sd.width;
|
||||
let height = extension_settings.sd.height;
|
||||
let sm = extension_settings.sd.novel_sm;
|
||||
let sm_dyn = extension_settings.sd.novel_sm_dyn;
|
||||
|
||||
if (extension_settings.sd.sampler === 'ddim') {
|
||||
// If a source was never changed after the scheduler setting was added, we need to set it to 'karras' for compatibility.
|
||||
if (!extension_settings.sd.scheduler || extension_settings.sd.scheduler === 'normal') {
|
||||
extension_settings.sd.scheduler = 'karras';
|
||||
}
|
||||
|
||||
if (extension_settings.sd.sampler === 'ddim' || extension_settings.sd.model === 'nai-diffusion-4-curated-preview') {
|
||||
sm = false;
|
||||
sm_dyn = false;
|
||||
}
|
||||
@@ -3314,7 +3310,6 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
|
||||
'scale',
|
||||
'width',
|
||||
'height',
|
||||
'clip_skip',
|
||||
];
|
||||
|
||||
const workflowResponse = await fetch('/api/sd/comfy/workflow', {
|
||||
@@ -3337,6 +3332,9 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
|
||||
const denoising_strength = extension_settings.sd.denoising_strength === undefined ? 1.0 : extension_settings.sd.denoising_strength;
|
||||
workflow = workflow.replaceAll('"%denoise%"', JSON.stringify(denoising_strength));
|
||||
|
||||
const clip_skip = isNaN(extension_settings.sd.clip_skip) ? -1 : -extension_settings.sd.clip_skip;
|
||||
workflow = workflow.replaceAll('"%clip_skip%"', JSON.stringify(clip_skip));
|
||||
|
||||
placeholders.forEach(ph => {
|
||||
workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
|
||||
});
|
||||
@@ -3670,6 +3668,8 @@ function getVisibilityByInitiator(initiator) {
|
||||
return !!extension_settings.sd.wand_visible;
|
||||
case initiators.command:
|
||||
return !!extension_settings.sd.command_visible;
|
||||
case initiators.tool:
|
||||
return !!extension_settings.sd.tool_visible;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -4417,6 +4417,7 @@ jQuery(async () => {
|
||||
$('#sd_wand_visible').on('input', onWandVisibleInput);
|
||||
$('#sd_command_visible').on('input', onCommandVisibleInput);
|
||||
$('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
|
||||
$('#sd_tool_visible').on('input', onToolVisibleInput);
|
||||
$('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
|
||||
$('#sd_stability_key').on('click', onStabilityKeyClick);
|
||||
$('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
|
||||
|
@@ -109,7 +109,7 @@
|
||||
<i data-i18n="The server must be accessible from the SillyTavern host machine.">The server must be accessible from the SillyTavern host machine.</i>
|
||||
</div>
|
||||
<div data-sd-source="horde">
|
||||
<i data-i18n="Hint: Save an API key in Horde KoboldAI API settings to use it here.">Hint: Save an API key in Horde KoboldAI API settings to use it here.</i>
|
||||
<i data-i18n="Hint: Save an API key in AI Horde API settings to use it here.">Hint: Save an API key in AI Horde API settings to use it here.</i>
|
||||
<label for="sd_horde_nsfw" class="checkbox_label">
|
||||
<input id="sd_horde_nsfw" type="checkbox" />
|
||||
<span data-i18n="Allow NSFW images from Horde">
|
||||
@@ -274,7 +274,7 @@
|
||||
<select id="sd_sampler"></select>
|
||||
</div>
|
||||
|
||||
<div class="flex1" data-sd-source="comfy,auto">
|
||||
<div class="flex1" data-sd-source="comfy,auto,novel">
|
||||
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
|
||||
<select id="sd_scheduler"></select>
|
||||
</div>
|
||||
@@ -459,25 +459,32 @@
|
||||
<div class="flex-container flexFlowColumn marginTopBot5 flexGap10">
|
||||
<label for="sd_wand_visible" class="checkbox_label">
|
||||
<span class="flex1 flex-container alignItemsCenter">
|
||||
<i class="fa-solid fa-wand-magic-sparkles"></i>
|
||||
<i class="fa-solid fa-wand-magic-sparkles fa-fw"></i>
|
||||
<span data-i18n="Extensions Menu">Extensions Menu</span>
|
||||
</span>
|
||||
<input id="sd_wand_visible" type="checkbox" />
|
||||
</label>
|
||||
<label for="sd_command_visible" class="checkbox_label">
|
||||
<span class="flex1 flex-container alignItemsCenter">
|
||||
<i class="fa-solid fa-terminal"></i>
|
||||
<i class="fa-solid fa-terminal fa-fw"></i>
|
||||
<span data-i18n="Slash Command">Slash Command</span>
|
||||
</span>
|
||||
<input id="sd_command_visible" type="checkbox" />
|
||||
</label>
|
||||
<label for="sd_interactive_visible" class="checkbox_label">
|
||||
<span class="flex1 flex-container alignItemsCenter">
|
||||
<i class="fa-solid fa-message"></i>
|
||||
<i class="fa-solid fa-message fa-fw"></i>
|
||||
<span data-i18n="Interactive Mode">Interactive Mode</span>
|
||||
</span>
|
||||
<input id="sd_interactive_visible" type="checkbox" />
|
||||
</label>
|
||||
<label for="sd_tool_visible" class="checkbox_label">
|
||||
<span class="flex1 flex-container alignItemsCenter">
|
||||
<i class="fa-solid fa-wrench fa-fw"></i>
|
||||
<span data-i18n="Function Tool">Function Tool</span>
|
||||
</span>
|
||||
<input id="sd_tool_visible" type="checkbox" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
0
public/scripts/extensions/third-party/.gitkeep
vendored
Normal file
0
public/scripts/extensions/third-party/.gitkeep
vendored
Normal file
@@ -14,16 +14,20 @@
|
||||
</select>
|
||||
<label data-i18n="ext_translate_mode_provider" for="translation_provider">Provider</label>
|
||||
<div class="flex-container gap5px flexnowrap marginBot5">
|
||||
<select id="translation_provider" name="provider" class="margin0">
|
||||
<option value="libre">Libre</option>
|
||||
<select id="translation_provider" name="provider" class="margin0 text_pole flex2">
|
||||
<option value="libre">LibreTranslate</option>
|
||||
<option value="google">Google</option>
|
||||
<option value="lingva">Lingva</option>
|
||||
<option value="deepl">DeepL</option>
|
||||
<option value="deepl">DeepL API</option>
|
||||
<option value="deeplx">DeepLX</option>
|
||||
<option value="bing">Bing</option>
|
||||
<option value="oneringtranslator">OneRingTranslator</option>
|
||||
<option value="yandex">Yandex</option>
|
||||
<select>
|
||||
<select id="deepl_api_endpoint" class="margin0 text_pole flex1" title="DeepL API Endpoint">
|
||||
<option value="free">Free</option>
|
||||
<option value="pro">Pro</option>
|
||||
</select>
|
||||
<div id="translate_key_button" class="menu_button fa-solid fa-key margin0"></div>
|
||||
<div id="translate_url_button" class="menu_button fa-solid fa-link margin0"></div>
|
||||
</div>
|
||||
@@ -35,4 +39,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -14,6 +14,8 @@ import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from '../../popup.js';
|
||||
import { findSecret, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { splitRecursive } from '../../utils.js';
|
||||
|
||||
@@ -32,6 +34,7 @@ const defaultSettings = {
|
||||
internal_language: 'en',
|
||||
provider: 'google',
|
||||
auto_mode: autoModeOptions.NONE,
|
||||
deepl_endpoint: 'free',
|
||||
};
|
||||
|
||||
const languageCodes = {
|
||||
@@ -106,7 +109,8 @@ const languageCodes = {
|
||||
'Pashto': 'ps',
|
||||
'Persian': 'fa',
|
||||
'Polish': 'pl',
|
||||
'Portuguese (Portugal, Brazil)': 'pt',
|
||||
'Portuguese (Portugal)': 'pt-PT',
|
||||
'Portuguese (Brazil)': 'pt-BR',
|
||||
'Punjabi': 'pa',
|
||||
'Romanian': 'ro',
|
||||
'Russian': 'ru',
|
||||
@@ -151,6 +155,7 @@ function showKeysButton() {
|
||||
$('#translate_key_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
|
||||
$('#translate_url_button').toggle(providerOptionalUrl);
|
||||
$('#translate_url_button').toggleClass('success', Boolean(secret_state[extension_settings.translate.provider + '_url']));
|
||||
$('#deepl_api_endpoint').toggle(extension_settings.translate.provider === 'deepl');
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
@@ -160,9 +165,10 @@ function loadSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
$(`#translation_provider option[value="${extension_settings.translate.provider}"]`).attr('selected', true);
|
||||
$(`#translation_target_language option[value="${extension_settings.translate.target_language}"]`).attr('selected', true);
|
||||
$(`#translation_auto_mode option[value="${extension_settings.translate.auto_mode}"]`).attr('selected', true);
|
||||
$(`#translation_provider option[value="${extension_settings.translate.provider}"]`).attr('selected', 'true');
|
||||
$(`#translation_target_language option[value="${extension_settings.translate.target_language}"]`).attr('selected', 'true');
|
||||
$(`#translation_auto_mode option[value="${extension_settings.translate.auto_mode}"]`).attr('selected', 'true');
|
||||
$('#deepl_api_endpoint').val(extension_settings.translate.deepl_endpoint).toggle(extension_settings.translate.provider === 'deepl');
|
||||
showKeysButton();
|
||||
}
|
||||
|
||||
@@ -284,10 +290,11 @@ async function translateProviderDeepl(text, lang) {
|
||||
throw new Error('No DeepL API key');
|
||||
}
|
||||
|
||||
const endpoint = extension_settings.translate.deepl_endpoint || 'free';
|
||||
const response = await fetch('/api/translate/deepl', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({ text: text, lang: lang }),
|
||||
body: JSON.stringify({ text: text, lang: lang, endpoint: endpoint }),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -394,9 +401,10 @@ async function chunkedTranslate(text, lang, translateFn, chunkSize = 5000) {
|
||||
* Translates text using the selected translation provider
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @param {string} provider Translation provider to use
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translate(text, lang) {
|
||||
async function translate(text, lang, provider = null) {
|
||||
try {
|
||||
if (text == '') {
|
||||
return '';
|
||||
@@ -406,13 +414,17 @@ async function translate(text, lang) {
|
||||
lang = extension_settings.translate.target_language;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
provider = extension_settings.translate.provider;
|
||||
}
|
||||
|
||||
// split text by embedded images links
|
||||
const chunks = text.split(/!\[.*?]\([^)]*\)/);
|
||||
const links = [...text.matchAll(/!\[.*?]\([^)]*\)/g)];
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
result += await translateInner(chunks[i], lang);
|
||||
result += await translateInner(chunks[i], lang, provider);
|
||||
if (i < links.length) result += links[i][0];
|
||||
}
|
||||
|
||||
@@ -423,11 +435,21 @@ async function translate(text, lang) {
|
||||
}
|
||||
}
|
||||
|
||||
async function translateInner(text, lang) {
|
||||
/**
|
||||
* Common translation function that handles the translation logic
|
||||
* @param {string} text Text to translate
|
||||
* @param {string} lang Target language code
|
||||
* @param {string} provider Translation provider to use
|
||||
* @returns {Promise<string>} Translated text
|
||||
*/
|
||||
async function translateInner(text, lang, provider) {
|
||||
if (text == '') {
|
||||
return '';
|
||||
}
|
||||
switch (extension_settings.translate.provider) {
|
||||
if (!provider) {
|
||||
provider = extension_settings.translate.provider;
|
||||
}
|
||||
switch (provider) {
|
||||
case 'libre':
|
||||
return await translateProviderLibre(text, lang);
|
||||
case 'google':
|
||||
@@ -445,7 +467,7 @@ async function translateInner(text, lang) {
|
||||
case 'yandex':
|
||||
return await translateProviderYandex(text, lang);
|
||||
default:
|
||||
console.error('Unknown translation provider', extension_settings.translate.provider);
|
||||
console.error('Unknown translation provider', provider);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -600,18 +622,34 @@ jQuery(async () => {
|
||||
}
|
||||
|
||||
$('#translation_auto_mode').on('change', (event) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.auto_mode = event.target.value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#translation_provider').on('change', (event) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.provider = event.target.value;
|
||||
showKeysButton();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#translation_target_language').on('change', (event) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.target_language = event.target.value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#deepl_api_endpoint').on('change', (event) => {
|
||||
if (!(event.target instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
extension_settings.translate.deepl_endpoint = event.target.value;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$(document).on('click', '.mes_translate', onMessageTranslateClick);
|
||||
$('#translate_key_button').on('click', async () => {
|
||||
const optionText = $('#translation_provider option:selected').text();
|
||||
@@ -687,6 +725,14 @@ jQuery(async () => {
|
||||
helpString: 'Translate text to a target language. If target language is not provided, the value from the extension settings will be used.',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('target', 'The target language code to translate to', ARGUMENT_TYPE.STRING, false, false, '', Object.values(languageCodes)),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'provider',
|
||||
description: 'The translation provider to use. If not provided, the value from the extension settings will be used.',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
enumProvider: () => Array.from(document.getElementById('translation_provider').querySelectorAll('option')).map((option) => new SlashCommandEnumValue(option.value, option.text, enumTypes.name, enumIcons.server)),
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('The text to translate', ARGUMENT_TYPE.STRING, true, false, ''),
|
||||
@@ -695,7 +741,8 @@ jQuery(async () => {
|
||||
const target = args?.target && Object.values(languageCodes).includes(String(args.target))
|
||||
? String(args.target)
|
||||
: extension_settings.translate.target_language;
|
||||
return await translate(String(value), target);
|
||||
const provider = args?.provider || extension_settings.translate.provider;
|
||||
return await translate(String(value), target, provider);
|
||||
},
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
}));
|
||||
|
@@ -448,6 +448,5 @@ export class FilterHelper {
|
||||
for (const cache of Object.values(this.fuzzySearchCaches)) {
|
||||
cache.resultMap.clear();
|
||||
}
|
||||
console.log('All fuzzy search caches cleared');
|
||||
}
|
||||
}
|
||||
|
@@ -275,6 +275,20 @@ export function getGroupMembers(groupId = selected_group) {
|
||||
return group?.members.map(member => characters.find(x => x.avatar === member)) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the member names of a group. If the group is not selected, an empty array is returned.
|
||||
* @returns {string[]} An array of character names representing the members of the group.
|
||||
*/
|
||||
export function getGroupNames() {
|
||||
if (!selected_group) {
|
||||
return [];
|
||||
}
|
||||
const groupMembers = groups.find(x => x.id == selected_group)?.members;
|
||||
return Array.isArray(groupMembers)
|
||||
? groupMembers.map(x => characters.find(y => y.avatar === x)?.name).filter(x => x)
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the character ID for a group member.
|
||||
* @param {string} arg 0-based member index or character name
|
||||
@@ -423,14 +437,20 @@ export function getGroupCharacterCards(groupId, characterId) {
|
||||
* @param {string} value Value to replace
|
||||
* @param {string} characterName Name of the character
|
||||
* @param {string} fieldName Name of the field
|
||||
* @param {function(string): string} [preprocess] Preprocess function
|
||||
* @returns {string} Prepared text
|
||||
* */
|
||||
function replaceAndPrepareForJoin(value, characterName, fieldName) {
|
||||
function replaceAndPrepareForJoin(value, characterName, fieldName, preprocess = null) {
|
||||
value = value.trim();
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Run preprocess function
|
||||
if (typeof preprocess === 'function') {
|
||||
value = preprocess(value);
|
||||
}
|
||||
|
||||
// Prepare and replace prefixes
|
||||
const prefix = customBaseChatReplace(group.generation_mode_join_prefix, fieldName, characterName);
|
||||
const suffix = customBaseChatReplace(group.generation_mode_join_suffix, fieldName, characterName);
|
||||
@@ -465,7 +485,7 @@ export function getGroupCharacterCards(groupId, characterId) {
|
||||
descriptions.push(replaceAndPrepareForJoin(character.description, character.name, 'Description'));
|
||||
personalities.push(replaceAndPrepareForJoin(character.personality, character.name, 'Personality'));
|
||||
scenarios.push(replaceAndPrepareForJoin(character.scenario, character.name, 'Scenario'));
|
||||
mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages'));
|
||||
mesExamplesArray.push(replaceAndPrepareForJoin(character.mes_example, character.name, 'Example Messages', (x) => !x.startsWith('<START>') ? `<START>\n${x}` : x));
|
||||
}
|
||||
|
||||
const description = descriptions.filter(x => x.length).join('\n');
|
||||
|
@@ -431,7 +431,8 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
|
||||
return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
|
||||
}
|
||||
|
||||
const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && power_user.instruct.names_behavior === names_behavior_types.FORCE);
|
||||
const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS;
|
||||
const includeGroupNames = selected_group && [names_behavior_types.ALWAYS, names_behavior_types.FORCE].includes(power_user.instruct.names_behavior);
|
||||
|
||||
let inputPrefix = power_user.instruct.input_sequence || '';
|
||||
let outputPrefix = power_user.instruct.output_sequence || '';
|
||||
@@ -463,7 +464,7 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
|
||||
|
||||
for (const item of mesExamplesArray) {
|
||||
const cleanedItem = item.replace(/<START>/i, '{Example Dialogue:}').replace(/\r/gm, '');
|
||||
const blockExamples = parseExampleIntoIndividual(cleanedItem);
|
||||
const blockExamples = parseExampleIntoIndividual(cleanedItem, includeGroupNames);
|
||||
|
||||
if (blockExamples.length === 0) {
|
||||
continue;
|
||||
@@ -474,8 +475,9 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
|
||||
}
|
||||
|
||||
for (const example of blockExamples) {
|
||||
// If force group/persona names is set, we should override the include names for the user placeholder
|
||||
const includeThisName = includeNames || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user');
|
||||
// If group names were included, we don't want to add any additional prefix as it already was applied.
|
||||
// Otherwise, if force group/persona names is set, we should override the include names for the user placeholder
|
||||
const includeThisName = !includeGroupNames && (includeNames || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user'));
|
||||
|
||||
const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix;
|
||||
const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix;
|
||||
@@ -489,7 +491,6 @@ export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
|
||||
if (formattedExamples.length === 0) {
|
||||
return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
|
||||
}
|
||||
|
||||
return formattedExamples;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { saveSettingsDebounced } from '../script.js';
|
||||
import { getTextTokens } from './tokenizers.js';
|
||||
import { uuidv4 } from './utils.js';
|
||||
import { getSortableDelay, uuidv4 } from './utils.js';
|
||||
|
||||
export const BIAS_CACHE = new Map();
|
||||
|
||||
@@ -16,7 +16,8 @@ export function displayLogitBias(logitBias, containerSelector) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(containerSelector).find('.logit_bias_list').empty();
|
||||
const list = $(containerSelector).find('.logit_bias_list');
|
||||
list.empty();
|
||||
|
||||
for (const entry of logitBias) {
|
||||
if (entry) {
|
||||
@@ -24,6 +25,27 @@ export function displayLogitBias(logitBias, containerSelector) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a sortable instance exists
|
||||
if (list.sortable('instance') !== undefined) {
|
||||
// Destroy the instance
|
||||
list.sortable('destroy');
|
||||
}
|
||||
|
||||
// Make the list sortable
|
||||
list.sortable({
|
||||
delay: getSortableDelay(),
|
||||
handle: '.drag-handle',
|
||||
stop: function () {
|
||||
const order = [];
|
||||
list.children().each(function () {
|
||||
order.unshift($(this).data('id'));
|
||||
});
|
||||
logitBias.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
||||
console.log('Logit bias reordered:', logitBias);
|
||||
saveSettingsDebounced();
|
||||
},
|
||||
});
|
||||
|
||||
BIAS_CACHE.delete(containerSelector);
|
||||
}
|
||||
|
||||
|
@@ -4,6 +4,7 @@ import { timestampToMoment, isDigitsOnly, getStringHash, escapeRegex, uuidv4 } f
|
||||
import { textgenerationwebui_banned_in_macros } from './textgen-settings.js';
|
||||
import { getInstructMacros } from './instruct-mode.js';
|
||||
import { getVariableMacros } from './variables.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
|
||||
/**
|
||||
* @typedef Macro
|
||||
@@ -516,7 +517,11 @@ export function evaluateMacros(content, env, postProcessFn) {
|
||||
break;
|
||||
}
|
||||
|
||||
content = content.replace(macro.regex, (...args) => postProcessFn(macro.replace(...args)));
|
||||
try {
|
||||
content = content.replace(macro.regex, (...args) => postProcessFn(macro.replace(...args)));
|
||||
} catch (e) {
|
||||
console.warn(`Macro content can't be replaced: ${macro.regex} in ${content}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
@@ -538,5 +543,6 @@ export function initMacros() {
|
||||
});
|
||||
}
|
||||
|
||||
MacrosParser.registerMacro('isMobile', () => String(isMobile()));
|
||||
initLastGenerationType();
|
||||
}
|
||||
|
@@ -33,7 +33,7 @@ import {
|
||||
system_message_types,
|
||||
this_chid,
|
||||
} from '../script.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { getGroupNames, selected_group } from './group-chats.js';
|
||||
|
||||
import {
|
||||
chatCompletionDefaultPrompts,
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
parseJsonFile,
|
||||
resetScrollHeight,
|
||||
stringFormat,
|
||||
uuidv4,
|
||||
} from './utils.js';
|
||||
import { countTokensOpenAIAsync, getTokenizerModel } from './tokenizers.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
@@ -101,16 +102,16 @@ const default_new_group_chat_prompt = '[Start a new group chat. Group members: {
|
||||
const default_new_example_chat_prompt = '[Example Chat]';
|
||||
const default_continue_nudge_prompt = '[Continue the following message. Do not include ANY parts of the original message. Use capitalization and punctuation as if your reply is a part of the original message: {{lastChatMessage}}]';
|
||||
const default_bias = 'Default (none)';
|
||||
const default_personality_format = '[{{char}}\'s personality: {{personality}}]';
|
||||
const default_scenario_format = '[Circumstances and context of the dialogue: {{scenario}}]';
|
||||
const default_personality_format = '{{personality}}';
|
||||
const default_scenario_format = '{{scenario}}';
|
||||
const default_group_nudge_prompt = '[Write the next reply only as {{char}}.]';
|
||||
const default_bias_presets = {
|
||||
[default_bias]: [],
|
||||
'Anti-bond': [
|
||||
{ text: ' bond', value: -50 },
|
||||
{ text: ' future', value: -50 },
|
||||
{ text: ' bonding', value: -50 },
|
||||
{ text: ' connection', value: -25 },
|
||||
{ id: '22154f79-dd98-41bc-8e34-87015d6a0eaf', text: ' bond', value: -50 },
|
||||
{ id: '8ad2d5c4-d8ef-49e4-bc5e-13e7f4690e0f', text: ' future', value: -50 },
|
||||
{ id: '52a4b280-0956-4940-ac52-4111f83e4046', text: ' bonding', value: -50 },
|
||||
{ id: 'e63037c7-c9d1-4724-ab2d-7756008b433b', text: ' connection', value: -25 },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -206,6 +207,12 @@ const custom_prompt_post_processing_types = {
|
||||
STRICT: 'strict',
|
||||
};
|
||||
|
||||
const openrouter_middleout_types = {
|
||||
AUTO: 'auto',
|
||||
ON: 'on',
|
||||
OFF: 'off',
|
||||
};
|
||||
|
||||
const sensitiveFields = [
|
||||
'reverse_proxy',
|
||||
'proxy_password',
|
||||
@@ -266,6 +273,7 @@ const default_settings = {
|
||||
openrouter_sort_models: 'alphabetically',
|
||||
openrouter_providers: [],
|
||||
openrouter_allow_fallbacks: true,
|
||||
openrouter_middleout: openrouter_middleout_types.ON,
|
||||
jailbreak_system: false,
|
||||
reverse_proxy: '',
|
||||
chat_completion_source: chat_completion_sources.OPENAI,
|
||||
@@ -342,6 +350,7 @@ const oai_settings = {
|
||||
openrouter_sort_models: 'alphabetically',
|
||||
openrouter_providers: [],
|
||||
openrouter_allow_fallbacks: true,
|
||||
openrouter_middleout: openrouter_middleout_types.ON,
|
||||
jailbreak_system: false,
|
||||
reverse_proxy: '',
|
||||
chat_completion_source: chat_completion_sources.OPENAI,
|
||||
@@ -543,11 +552,15 @@ function setupChatCompletionPromptManager(openAiSettings) {
|
||||
* @returns {Message[]} Array of message objects
|
||||
*/
|
||||
export function parseExampleIntoIndividual(messageExampleString, appendNamesForGroup = true) {
|
||||
const groupBotNames = getGroupNames().map(name => `${name}:`);
|
||||
|
||||
let result = []; // array of msgs
|
||||
let tmp = messageExampleString.split('\n');
|
||||
let cur_msg_lines = [];
|
||||
let in_user = false;
|
||||
let in_bot = false;
|
||||
let botName = name2;
|
||||
|
||||
// DRY my cock and balls :)
|
||||
function add_msg(name, role, system_name) {
|
||||
// join different newlines (we split them by \n and join by \n)
|
||||
@@ -571,10 +584,14 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG
|
||||
in_user = true;
|
||||
// we were in the bot mode previously, add the message
|
||||
if (in_bot) {
|
||||
add_msg(name2, 'system', 'example_assistant');
|
||||
add_msg(botName, 'system', 'example_assistant');
|
||||
}
|
||||
in_bot = false;
|
||||
} else if (cur_str.startsWith(name2 + ':')) {
|
||||
} else if (cur_str.startsWith(name2 + ':') || groupBotNames.some(n => cur_str.startsWith(n))) {
|
||||
if (!cur_str.startsWith(name2 + ':') && groupBotNames.length) {
|
||||
botName = cur_str.split(':')[0];
|
||||
}
|
||||
|
||||
in_bot = true;
|
||||
// we were in the user mode previously, add the message
|
||||
if (in_user) {
|
||||
@@ -589,7 +606,7 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG
|
||||
if (in_user) {
|
||||
add_msg(name1, 'system', 'example_user');
|
||||
} else if (in_bot) {
|
||||
add_msg(name2, 'system', 'example_assistant');
|
||||
add_msg(botName, 'system', 'example_assistant');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -611,8 +628,9 @@ function formatWorldInfo(value) {
|
||||
*
|
||||
* @param {Prompt[]} prompts - Array containing injection prompts.
|
||||
* @param {Object[]} messages - Array containing all messages.
|
||||
* @returns {Promise<Object[]>} - Array containing all messages with injections.
|
||||
*/
|
||||
function populationInjectionPrompts(prompts, messages) {
|
||||
async function populationInjectionPrompts(prompts, messages) {
|
||||
let totalInsertedMessages = 0;
|
||||
|
||||
const roleTypes = {
|
||||
@@ -635,7 +653,7 @@ function populationInjectionPrompts(prompts, messages) {
|
||||
// Get prompts for current role
|
||||
const rolePrompts = depthPrompts.filter(prompt => prompt.role === role).map(x => x.content).join(separator);
|
||||
// Get extension prompt
|
||||
const extensionPrompt = getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap);
|
||||
const extensionPrompt = await getExtensionPrompt(extension_prompt_types.IN_CHAT, i, separator, roleTypes[role], wrap);
|
||||
|
||||
const jointPrompt = [rolePrompts, extensionPrompt].filter(x => x).map(x => x.trim()).join(separator);
|
||||
|
||||
@@ -1020,7 +1038,7 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
|
||||
}
|
||||
|
||||
// Add in-chat injections
|
||||
messages = populationInjectionPrompts(absolutePrompts, messages);
|
||||
messages = await populationInjectionPrompts(absolutePrompts, messages);
|
||||
|
||||
// Decide whether dialogue examples should always be added
|
||||
if (power_user.pin_examples) {
|
||||
@@ -1051,9 +1069,9 @@ async function populateChatCompletion(prompts, chatCompletion, { bias, quietProm
|
||||
* @param {string} options.systemPromptOverride
|
||||
* @param {string} options.jailbreakPromptOverride
|
||||
* @param {string} options.personaDescription
|
||||
* @returns {Object} prompts - The prepared and merged system and user-defined prompts.
|
||||
* @returns {Promise<Object>} prompts - The prepared and merged system and user-defined prompts.
|
||||
*/
|
||||
function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) {
|
||||
async function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, worldInfoBefore, worldInfoAfter, charDescription, quietPrompt, bias, extensionPrompts, systemPromptOverride, jailbreakPromptOverride, personaDescription }) {
|
||||
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);
|
||||
@@ -1142,6 +1160,9 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
|
||||
if (!extensionPrompts[key].value) continue;
|
||||
if (![extension_prompt_types.BEFORE_PROMPT, extension_prompt_types.IN_PROMPT].includes(prompt.position)) continue;
|
||||
|
||||
const hasFilter = typeof prompt.filter === 'function';
|
||||
if (hasFilter && !await prompt.filter()) continue;
|
||||
|
||||
systemPrompts.push({
|
||||
identifier: key.replace(/\W/g, '_'),
|
||||
position: getPromptPosition(prompt.position),
|
||||
@@ -1178,7 +1199,8 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
|
||||
|
||||
// Apply character-specific main prompt
|
||||
const systemPrompt = prompts.get('main') ?? null;
|
||||
if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true) {
|
||||
const isSystemPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('main');
|
||||
if (systemPromptOverride && systemPrompt && systemPrompt.forbid_overrides !== true && !isSystemPromptDisabled) {
|
||||
const mainOriginalContent = systemPrompt.content;
|
||||
systemPrompt.content = systemPromptOverride;
|
||||
const mainReplacement = promptManager.preparePrompt(systemPrompt, mainOriginalContent);
|
||||
@@ -1187,7 +1209,8 @@ function preparePromptsForChatCompletion({ Scenario, charPersonality, name2, wor
|
||||
|
||||
// Apply character-specific jailbreak
|
||||
const jailbreakPrompt = prompts.get('jailbreak') ?? null;
|
||||
if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true) {
|
||||
const isJailbreakPromptDisabled = promptManager.isPromptDisabledForActiveCharacter('jailbreak');
|
||||
if (jailbreakPromptOverride && jailbreakPrompt && jailbreakPrompt.forbid_overrides !== true && !isJailbreakPromptDisabled) {
|
||||
const jbOriginalContent = jailbreakPrompt.content;
|
||||
jailbreakPrompt.content = jailbreakPromptOverride;
|
||||
const jbReplacement = promptManager.preparePrompt(jailbreakPrompt, jbOriginalContent);
|
||||
@@ -1252,7 +1275,7 @@ export async function prepareOpenAIMessages({
|
||||
|
||||
try {
|
||||
// Merge markers and ordered user prompts with system prompts
|
||||
const prompts = preparePromptsForChatCompletion({
|
||||
const prompts = await preparePromptsForChatCompletion({
|
||||
Scenario,
|
||||
charPersonality,
|
||||
name2,
|
||||
@@ -1860,6 +1883,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
'n': canMultiSwipe ? oai_settings.n : undefined,
|
||||
'user_name': name1,
|
||||
'char_name': name2,
|
||||
'group_names': getGroupNames(),
|
||||
};
|
||||
|
||||
// Empty array will produce a validation error
|
||||
@@ -1904,6 +1928,7 @@ async function sendOpenAIRequest(type, messages, signal) {
|
||||
generate_data['use_fallback'] = oai_settings.openrouter_use_fallback;
|
||||
generate_data['provider'] = oai_settings.openrouter_providers;
|
||||
generate_data['allow_fallbacks'] = oai_settings.openrouter_allow_fallbacks;
|
||||
generate_data['middleout'] = oai_settings.openrouter_middleout;
|
||||
|
||||
if (isTextCompletion) {
|
||||
generate_data['stop'] = getStoppingStrings(isImpersonate, isContinue);
|
||||
@@ -2073,7 +2098,7 @@ function getStreamingReply(data) {
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
|
||||
return data?.delta?.text || '';
|
||||
} else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
|
||||
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
||||
return data?.candidates?.[0]?.content?.parts?.map(x => x.text)?.join('\n\n') || '';
|
||||
} else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
|
||||
return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || '';
|
||||
} else {
|
||||
@@ -2085,7 +2110,7 @@ function getStreamingReply(data) {
|
||||
* parseChatCompletionLogprobs converts the response data returned from a chat
|
||||
* completions-like source into an array of TokenLogprobs found in the response.
|
||||
* @param {Object} data - response data from a chat completions-like source
|
||||
* @returns {import('logprobs.js').TokenLogprobs[] | null} converted logprobs
|
||||
* @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
|
||||
*/
|
||||
function parseChatCompletionLogprobs(data) {
|
||||
if (!data) {
|
||||
@@ -2114,7 +2139,7 @@ function parseChatCompletionLogprobs(data) {
|
||||
* completion API and converts into the structure used by the Token Probabilities
|
||||
* view.
|
||||
* @param {{content: { token: string, logprob: number, top_logprobs: { token: string, logprob: number }[] }[]}} logprobs
|
||||
* @returns {import('logprobs.js').TokenLogprobs[] | null} converted logprobs
|
||||
* @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
|
||||
*/
|
||||
function parseOpenAIChatLogprobs(logprobs) {
|
||||
const { content } = logprobs ?? {};
|
||||
@@ -2142,7 +2167,7 @@ function parseOpenAIChatLogprobs(logprobs) {
|
||||
* completion API and converts into the structure used by the Token Probabilities
|
||||
* view.
|
||||
* @param {{tokens: string[], token_logprobs: number[], top_logprobs: { token: string, logprob: number }[][]}} logprobs
|
||||
* @returns {import('logprobs.js').TokenLogprobs[] | null} converted logprobs
|
||||
* @returns {import('./logprobs.js').TokenLogprobs[] | null} converted logprobs
|
||||
*/
|
||||
function parseOpenAITextLogprobs(logprobs) {
|
||||
const { tokens, token_logprobs, top_logprobs } = logprobs ?? {};
|
||||
@@ -3006,6 +3031,7 @@ function loadOpenAISettings(data, settings) {
|
||||
oai_settings.openrouter_sort_models = settings.openrouter_sort_models ?? default_settings.openrouter_sort_models;
|
||||
oai_settings.openrouter_use_fallback = settings.openrouter_use_fallback ?? default_settings.openrouter_use_fallback;
|
||||
oai_settings.openrouter_allow_fallbacks = settings.openrouter_allow_fallbacks ?? default_settings.openrouter_allow_fallbacks;
|
||||
oai_settings.openrouter_middleout = settings.openrouter_middleout ?? default_settings.openrouter_middleout;
|
||||
oai_settings.ai21_model = settings.ai21_model ?? default_settings.ai21_model;
|
||||
oai_settings.mistralai_model = settings.mistralai_model ?? default_settings.mistralai_model;
|
||||
oai_settings.cohere_model = settings.cohere_model ?? default_settings.cohere_model;
|
||||
@@ -3114,6 +3140,7 @@ function loadOpenAISettings(data, settings) {
|
||||
$('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);
|
||||
$('#openrouter_allow_fallbacks').prop('checked', oai_settings.openrouter_allow_fallbacks);
|
||||
$('#openrouter_providers_chat').val(oai_settings.openrouter_providers).trigger('change');
|
||||
$('#openrouter_middleout').val(oai_settings.openrouter_middleout);
|
||||
$('#squash_system_messages').prop('checked', oai_settings.squash_system_messages);
|
||||
$('#continue_prefill').prop('checked', oai_settings.continue_prefill);
|
||||
$('#openai_function_calling').prop('checked', oai_settings.function_calling);
|
||||
@@ -3162,6 +3189,14 @@ function loadOpenAISettings(data, settings) {
|
||||
|
||||
$('#openai_logit_bias_preset').empty();
|
||||
for (const preset of Object.keys(oai_settings.bias_presets)) {
|
||||
// Backfill missing IDs
|
||||
if (Array.isArray(oai_settings.bias_presets[preset])) {
|
||||
oai_settings.bias_presets[preset].forEach((bias) => {
|
||||
if (bias && !bias.id) {
|
||||
bias.id = uuidv4();
|
||||
}
|
||||
});
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.innerText = preset;
|
||||
option.value = preset;
|
||||
@@ -3346,6 +3381,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
||||
openrouter_sort_models: settings.openrouter_sort_models,
|
||||
openrouter_providers: settings.openrouter_providers,
|
||||
openrouter_allow_fallbacks: settings.openrouter_allow_fallbacks,
|
||||
openrouter_middleout: settings.openrouter_middleout,
|
||||
ai21_model: settings.ai21_model,
|
||||
mistralai_model: settings.mistralai_model,
|
||||
cohere_model: settings.cohere_model,
|
||||
@@ -3450,7 +3486,8 @@ function onLogitBiasPresetChange() {
|
||||
}
|
||||
|
||||
oai_settings.bias_preset_selected = value;
|
||||
$('.openai_logit_bias_list').empty();
|
||||
const list = $('.openai_logit_bias_list');
|
||||
list.empty();
|
||||
|
||||
for (const entry of preset) {
|
||||
if (entry) {
|
||||
@@ -3458,12 +3495,33 @@ function onLogitBiasPresetChange() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a sortable instance exists
|
||||
if (list.sortable('instance') !== undefined) {
|
||||
// Destroy the instance
|
||||
list.sortable('destroy');
|
||||
}
|
||||
|
||||
// Make the list sortable
|
||||
list.sortable({
|
||||
delay: getSortableDelay(),
|
||||
handle: '.drag-handle',
|
||||
stop: function () {
|
||||
const order = [];
|
||||
list.children().each(function () {
|
||||
order.unshift($(this).data('id'));
|
||||
});
|
||||
preset.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
|
||||
console.log('Logit bias reordered:', preset);
|
||||
saveSettingsDebounced();
|
||||
},
|
||||
});
|
||||
|
||||
biasCache = undefined;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function createNewLogitBiasEntry() {
|
||||
const entry = { text: '', value: 0 };
|
||||
const entry = { id: uuidv4(), text: '', value: 0 };
|
||||
oai_settings.bias_presets[oai_settings.bias_preset_selected].push(entry);
|
||||
biasCache = undefined;
|
||||
createLogitBiasListItem(entry);
|
||||
@@ -3471,11 +3529,14 @@ function createNewLogitBiasEntry() {
|
||||
}
|
||||
|
||||
function createLogitBiasListItem(entry) {
|
||||
const id = oai_settings.bias_presets[oai_settings.bias_preset_selected].indexOf(entry);
|
||||
if (!entry.id) {
|
||||
entry.id = uuidv4();
|
||||
}
|
||||
const id = entry.id;
|
||||
const template = $('#openai_logit_bias_template .openai_logit_bias_form').clone();
|
||||
template.data('id', id);
|
||||
template.find('.openai_logit_bias_text').val(entry.text).on('input', function () {
|
||||
oai_settings.bias_presets[oai_settings.bias_preset_selected][id].text = String($(this).val());
|
||||
entry.text = String($(this).val());
|
||||
biasCache = undefined;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
@@ -3494,13 +3555,17 @@ function createLogitBiasListItem(entry) {
|
||||
value = max;
|
||||
}
|
||||
|
||||
oai_settings.bias_presets[oai_settings.bias_preset_selected][id].value = value;
|
||||
entry.value = value;
|
||||
biasCache = undefined;
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
template.find('.openai_logit_bias_remove').on('click', function () {
|
||||
$(this).closest('.openai_logit_bias_form').remove();
|
||||
oai_settings.bias_presets[oai_settings.bias_preset_selected].splice(id, 1);
|
||||
const preset = oai_settings.bias_presets[oai_settings.bias_preset_selected];
|
||||
const index = preset.findIndex(item => item.id === id);
|
||||
if (index >= 0) {
|
||||
preset.splice(index, 1);
|
||||
}
|
||||
onLogitBiasPresetChange();
|
||||
});
|
||||
$('.openai_logit_bias_list').prepend(template);
|
||||
@@ -3680,6 +3745,9 @@ async function onLogitBiasPresetImportFileChange(e) {
|
||||
if (typeof entry == 'object' && entry !== null) {
|
||||
if (Object.hasOwn(entry, 'text') &&
|
||||
Object.hasOwn(entry, 'value')) {
|
||||
if (!entry.id) {
|
||||
entry.id = uuidv4();
|
||||
}
|
||||
validEntries.push(entry);
|
||||
}
|
||||
}
|
||||
@@ -3780,6 +3848,7 @@ function onSettingsPresetChange() {
|
||||
openrouter_sort_models: ['#openrouter_sort_models', 'openrouter_sort_models', false],
|
||||
openrouter_providers: ['#openrouter_providers_chat', 'openrouter_providers', false],
|
||||
openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true],
|
||||
openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false],
|
||||
ai21_model: ['#model_ai21_select', 'ai21_model', false],
|
||||
mistralai_model: ['#model_mistralai_select', 'mistralai_model', false],
|
||||
cohere_model: ['#model_cohere_select', 'cohere_model', false],
|
||||
@@ -4075,19 +4144,14 @@ async function onModelChange() {
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', max_2mil);
|
||||
} else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121')) {
|
||||
} else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
} else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206')) {
|
||||
$('#openai_max_context').attr('max', max_2mil);
|
||||
} else if (value.includes('gemini-1.5-flash')) {
|
||||
} else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash-exp')) {
|
||||
$('#openai_max_context').attr('max', max_1mil);
|
||||
} else if (value.includes('gemini-1.0-pro-vision') || value === 'gemini-pro-vision') {
|
||||
$('#openai_max_context').attr('max', max_16k);
|
||||
} else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
} else if (value === 'text-bison-001') {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
// The ultra endpoints are possibly dead:
|
||||
} else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
} else {
|
||||
@@ -4209,16 +4273,13 @@ async function onModelChange() {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
}
|
||||
else if (['command-light', 'command'].includes(oai_settings.cohere_model)) {
|
||||
else if (['command-light-nightly', 'command-light', 'command'].includes(oai_settings.cohere_model)) {
|
||||
$('#openai_max_context').attr('max', max_4k);
|
||||
}
|
||||
else if (['command-light-nightly', 'command-nightly'].includes(oai_settings.cohere_model)) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
}
|
||||
else if (oai_settings.cohere_model.includes('command-r') || ['c4ai-aya-expanse-32b'].includes(oai_settings.cohere_model)) {
|
||||
else if (oai_settings.cohere_model.includes('command-r') || ['c4ai-aya-23', 'c4ai-aya-expanse-32b', 'command-nightly'].includes(oai_settings.cohere_model)) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
}
|
||||
else if (['c4ai-aya-23', 'c4ai-aya-expanse-8b'].includes(oai_settings.cohere_model)) {
|
||||
else if (['c4ai-aya-23-8b', 'c4ai-aya-expanse-8b'].includes(oai_settings.cohere_model)) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
}
|
||||
else {
|
||||
@@ -4269,7 +4330,7 @@ async function onModelChange() {
|
||||
else if (oai_settings.groq_model.includes('llama-3.2') && oai_settings.groq_model.includes('-preview')) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
}
|
||||
else if (oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
|
||||
else if (oai_settings.groq_model.includes('llama-3.3') || oai_settings.groq_model.includes('llama-3.2') || oai_settings.groq_model.includes('llama-3.1')) {
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
}
|
||||
else if (oai_settings.groq_model.includes('llama3-groq')) {
|
||||
@@ -4751,6 +4812,8 @@ export function isImageInliningSupported() {
|
||||
// gultra just isn't being offered as multimodal, thanks google.
|
||||
const visionSupportedModels = [
|
||||
'gpt-4-vision',
|
||||
'gemini-2.0-flash-thinking-exp-1219',
|
||||
'gemini-2.0-flash-exp',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-flash-latest',
|
||||
'gemini-1.5-flash-001',
|
||||
@@ -4769,7 +4832,6 @@ export function isImageInliningSupported() {
|
||||
'gemini-1.5-pro-002',
|
||||
'gemini-1.5-pro-exp-0801',
|
||||
'gemini-1.5-pro-exp-0827',
|
||||
'gemini-pro-vision',
|
||||
'claude-3',
|
||||
'claude-3-5',
|
||||
'gpt-4-turbo',
|
||||
@@ -5213,6 +5275,11 @@ export function initOpenAI() {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#openrouter_middleout').on('input', function () {
|
||||
oai_settings.openrouter_middleout = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#squash_system_messages').on('input', function () {
|
||||
oai_settings.squash_system_messages = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
|
@@ -21,8 +21,10 @@ import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSuppor
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { FILTER_TYPES, FilterHelper } from './filters.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup } from './popup.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
import { t } from './i18n.js';
|
||||
import { openWorldInfoEditor, world_names } from './world-info.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
|
||||
let savePersonasPage = 0;
|
||||
const GRID_STORAGE_KEY = 'Personas_GridView';
|
||||
@@ -375,6 +377,7 @@ export function initPersona(avatarId, personaName, personaDescription) {
|
||||
position: persona_description_positions.IN_PROMPT,
|
||||
depth: DEFAULT_DEPTH,
|
||||
role: DEFAULT_ROLE,
|
||||
lorebook: '',
|
||||
};
|
||||
|
||||
saveSettingsDebounced();
|
||||
@@ -418,6 +421,7 @@ export async function convertCharacterToPersona(characterId = null) {
|
||||
position: persona_description_positions.IN_PROMPT,
|
||||
depth: DEFAULT_DEPTH,
|
||||
role: DEFAULT_ROLE,
|
||||
lorebook: '',
|
||||
};
|
||||
|
||||
// If the user is currently using this persona, update the description
|
||||
@@ -461,6 +465,7 @@ export function setPersonaDescription() {
|
||||
.val(power_user.persona_description_role)
|
||||
.find(`option[value="${power_user.persona_description_role}"]`)
|
||||
.prop('selected', String(true));
|
||||
$('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
|
||||
countPersonaDescriptionTokens();
|
||||
}
|
||||
|
||||
@@ -490,6 +495,7 @@ async function updatePersonaNameIfExists(avatarId, newName) {
|
||||
position: persona_description_positions.IN_PROMPT,
|
||||
depth: DEFAULT_DEPTH,
|
||||
role: DEFAULT_ROLE,
|
||||
lorebook: '',
|
||||
};
|
||||
console.log(`Created persona name for ${avatarId} as ${newName}`);
|
||||
}
|
||||
@@ -535,6 +541,7 @@ async function bindUserNameToPersona(e) {
|
||||
position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT,
|
||||
depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH,
|
||||
role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE,
|
||||
lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -579,12 +586,20 @@ function selectCurrentPersona() {
|
||||
power_user.persona_description_position = descriptor.position ?? persona_description_positions.IN_PROMPT;
|
||||
power_user.persona_description_depth = descriptor.depth ?? DEFAULT_DEPTH;
|
||||
power_user.persona_description_role = descriptor.role ?? DEFAULT_ROLE;
|
||||
power_user.persona_description_lorebook = descriptor.lorebook ?? '';
|
||||
} else {
|
||||
power_user.persona_description = '';
|
||||
power_user.persona_description_position = persona_description_positions.IN_PROMPT;
|
||||
power_user.persona_description_depth = DEFAULT_DEPTH;
|
||||
power_user.persona_description_role = DEFAULT_ROLE;
|
||||
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.IN_PROMPT, depth: DEFAULT_DEPTH, role: DEFAULT_ROLE };
|
||||
power_user.persona_description_lorebook = '';
|
||||
power_user.persona_descriptions[user_avatar] = {
|
||||
description: '',
|
||||
position: persona_description_positions.IN_PROMPT,
|
||||
depth: DEFAULT_DEPTH,
|
||||
role: DEFAULT_ROLE,
|
||||
lorebook: '',
|
||||
};
|
||||
}
|
||||
|
||||
setPersonaDescription();
|
||||
@@ -652,6 +667,7 @@ async function lockPersona() {
|
||||
position: persona_description_positions.IN_PROMPT,
|
||||
depth: DEFAULT_DEPTH,
|
||||
role: DEFAULT_ROLE,
|
||||
lorebook: '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -731,6 +747,7 @@ function onPersonaDescriptionInput() {
|
||||
position: Number($('#persona_description_position').find(':selected').val()),
|
||||
depth: Number($('#persona_depth_value').val()),
|
||||
role: Number($('#persona_depth_role').find(':selected').val()),
|
||||
lorebook: '',
|
||||
};
|
||||
power_user.persona_descriptions[user_avatar] = object;
|
||||
}
|
||||
@@ -766,6 +783,52 @@ function onPersonaDescriptionDepthRoleInput() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a popup to set the lorebook for the current persona.
|
||||
* @param {PointerEvent} event Click event
|
||||
*/
|
||||
async function onPersonaLoreButtonClick(event) {
|
||||
const personaName = power_user.personas[user_avatar];
|
||||
const selectedLorebook = power_user.persona_description_lorebook;
|
||||
|
||||
if (!personaName) {
|
||||
toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona name not set`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.altKey && selectedLorebook) {
|
||||
openWorldInfoEditor(selectedLorebook);
|
||||
return;
|
||||
}
|
||||
|
||||
const template = $(await renderTemplateAsync('personaLorebook'));
|
||||
|
||||
const worldSelect = template.find('select');
|
||||
template.find('.persona_name').text(personaName);
|
||||
|
||||
for (const worldName of world_names) {
|
||||
const option = document.createElement('option');
|
||||
option.value = worldName;
|
||||
option.innerText = worldName;
|
||||
option.selected = selectedLorebook === worldName;
|
||||
worldSelect.append(option);
|
||||
}
|
||||
|
||||
worldSelect.on('change', function () {
|
||||
power_user.persona_description_lorebook = String($(this).val());
|
||||
|
||||
if (power_user.personas[user_avatar]) {
|
||||
const object = getOrCreatePersonaDescriptor();
|
||||
object.lorebook = power_user.persona_description_lorebook;
|
||||
}
|
||||
|
||||
$('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
await callGenericPopup(template, POPUP_TYPE.TEXT);
|
||||
}
|
||||
|
||||
function onPersonaDescriptionPositionInput() {
|
||||
power_user.persona_description_position = Number(
|
||||
$('#persona_description_position').find(':selected').val(),
|
||||
@@ -789,6 +852,7 @@ function getOrCreatePersonaDescriptor() {
|
||||
position: power_user.persona_description_position,
|
||||
depth: power_user.persona_description_depth,
|
||||
role: power_user.persona_description_role,
|
||||
lorebook: power_user.persona_description_lorebook,
|
||||
};
|
||||
power_user.persona_descriptions[user_avatar] = object;
|
||||
}
|
||||
@@ -1038,6 +1102,7 @@ async function duplicatePersona(avatarId) {
|
||||
position: descriptor?.position ?? persona_description_positions.IN_PROMPT,
|
||||
depth: descriptor?.depth ?? DEFAULT_DEPTH,
|
||||
role: descriptor?.role ?? DEFAULT_ROLE,
|
||||
lorebook: descriptor?.lorebook ?? '',
|
||||
};
|
||||
|
||||
await uploadUserAvatar(getUserAvatar(avatarId), newAvatarId);
|
||||
@@ -1055,6 +1120,7 @@ export function initPersonas() {
|
||||
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
|
||||
$('#persona_depth_value').on('input', onPersonaDescriptionDepthValueInput);
|
||||
$('#persona_depth_role').on('input', onPersonaDescriptionDepthRoleInput);
|
||||
$('#persona_lore_button').on('click', onPersonaLoreButtonClick);
|
||||
$('#personas_backup').on('click', onBackupPersonas);
|
||||
$('#personas_restore').on('click', () => $('#personas_restore_input').trigger('click'));
|
||||
$('#personas_restore_input').on('change', onPersonasRestoreInput);
|
||||
|
@@ -261,6 +261,7 @@ let power_user = {
|
||||
persona_description_position: persona_description_positions.IN_PROMPT,
|
||||
persona_description_role: 0,
|
||||
persona_description_depth: 2,
|
||||
persona_description_lorebook: '',
|
||||
persona_show_notifications: true,
|
||||
persona_sort_order: 'asc',
|
||||
|
||||
@@ -1756,7 +1757,7 @@ async function loadContextSettings() {
|
||||
} else {
|
||||
$element.val(power_user.context[control.property]);
|
||||
}
|
||||
console.log(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`);
|
||||
console.debug(`Setting ${$element.prop('id')} to ${power_user.context[control.property]}`);
|
||||
|
||||
// If the setting already exists, no need to duplicate it
|
||||
// TODO: Maybe check the power_user object for the setting instead of a flag?
|
||||
@@ -1767,7 +1768,7 @@ async function loadContextSettings() {
|
||||
} else {
|
||||
power_user.context[control.property] = value;
|
||||
}
|
||||
console.log(`Setting ${$element.prop('id')} to ${value}`);
|
||||
console.debug(`Setting ${$element.prop('id')} to ${value}`);
|
||||
if (!CSS.supports('field-sizing', 'content') && $(this).is('textarea')) {
|
||||
await resetScrollHeight($(this));
|
||||
}
|
||||
|
@@ -585,6 +585,7 @@ class PresetManager {
|
||||
'openrouter_allow_fallbacks',
|
||||
'tabby_model',
|
||||
'derived',
|
||||
'generic_model',
|
||||
];
|
||||
const settings = Object.assign({}, getSettingsByApiId(this.apiId));
|
||||
|
||||
|
@@ -129,6 +129,10 @@ function setSamplerListListeners() {
|
||||
relatedDOMElement = $('#sampler_priority_block_ooba');
|
||||
}
|
||||
|
||||
if (samplerName === 'samplers_priorities') { //this is for aphrodite's sampler priority
|
||||
relatedDOMElement = $('#sampler_priority_block_aphrodite');
|
||||
}
|
||||
|
||||
if (samplerName === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
|
||||
relatedDOMElement = $('#contrastiveSearchBlock');
|
||||
}
|
||||
@@ -237,6 +241,11 @@ async function listSamplers(main_api, arrayOnly = false) {
|
||||
displayname = 'Ooba Sampler Priority Block';
|
||||
}
|
||||
|
||||
if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority
|
||||
targetDOMelement = $('#sampler_priority_block_aphrodite');
|
||||
displayname = 'Aphrodite Sampler Priority Block';
|
||||
}
|
||||
|
||||
if (sampler === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block?
|
||||
targetDOMelement = $('#contrastiveSearchBlock');
|
||||
displayname = 'Contrast Search Block';
|
||||
@@ -373,6 +382,10 @@ export async function validateDisabledSamplers(redraw = false) {
|
||||
relatedDOMElement = $('#sampler_priority_block_ooba');
|
||||
}
|
||||
|
||||
if (sampler === 'samplers_priorities') { //this is for aphrodite's sampler priority
|
||||
relatedDOMElement = $('#sampler_priority_block_aphrodite');
|
||||
}
|
||||
|
||||
if (sampler === 'dry_multiplier') {
|
||||
relatedDOMElement = $('#dryBlock');
|
||||
targetDisplayType = 'block';
|
||||
|
@@ -38,6 +38,7 @@ export const SECRET_KEYS = {
|
||||
NANOGPT: 'api_key_nanogpt',
|
||||
TAVILY: 'api_key_tavily',
|
||||
BFL: 'api_key_bfl',
|
||||
GENERIC: 'api_key_generic',
|
||||
};
|
||||
|
||||
const INPUT_MAP = {
|
||||
@@ -71,6 +72,7 @@ const INPUT_MAP = {
|
||||
[SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
|
||||
[SECRET_KEYS.BLOCKENTROPY]: '#api_key_blockentropy',
|
||||
[SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
|
||||
[SECRET_KEYS.GENERIC]: '#api_key_generic',
|
||||
};
|
||||
|
||||
async function clearSecret() {
|
||||
|
@@ -47,7 +47,7 @@ import {
|
||||
} from '../script.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommandParserError } from './slash-commands/SlashCommandParserError.js';
|
||||
import { getMessageTimeStamp } from './RossAscends-mods.js';
|
||||
import { getMessageTimeStamp, isMobile } from './RossAscends-mods.js';
|
||||
import { hideChatMessageRange } from './chats.js';
|
||||
import { getContext, saveMetadataDebounced } from './extensions.js';
|
||||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||
@@ -84,6 +84,25 @@ export const parser = new SlashCommandParser();
|
||||
const registerSlashCommand = SlashCommandParser.addCommand.bind(SlashCommandParser);
|
||||
const getSlashCommandsHelp = parser.getHelpString.bind(parser);
|
||||
|
||||
/**
|
||||
* Converts a SlashCommandClosure to a filter function that returns a boolean.
|
||||
* @param {SlashCommandClosure} closure
|
||||
* @returns {() => Promise<boolean>}
|
||||
*/
|
||||
function closureToFilter(closure) {
|
||||
return async () => {
|
||||
try {
|
||||
const localClosure = closure.getCopy();
|
||||
localClosure.onProgress = () => { };
|
||||
const result = await localClosure.execute();
|
||||
return isTrueBoolean(result.pipe);
|
||||
} catch (e) {
|
||||
console.error('Error executing filter closure', e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function initDefaultSlashCommands() {
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: '?',
|
||||
@@ -1611,6 +1630,13 @@ export function initDefaultSlashCommands() {
|
||||
new SlashCommandNamedArgument(
|
||||
'ephemeral', 'remove injection after generation', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
|
||||
),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'filter',
|
||||
description: 'if a filter is defined, an injection will only be performed if the closure returns true',
|
||||
typeList: [ARGUMENT_TYPE.CLOSURE],
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
@@ -1892,6 +1918,52 @@ export function initDefaultSlashCommands() {
|
||||
],
|
||||
helpString: 'Converts the provided string to lowercase.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'substr',
|
||||
aliases: ['substring'],
|
||||
callback: (arg, text) => typeof text === 'string' ? text.slice(...[Number(arg.start), arg.end && Number(arg.end)]) : '',
|
||||
returns: 'substring',
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument(
|
||||
'start', 'start index', [ARGUMENT_TYPE.NUMBER], false, false,
|
||||
),
|
||||
new SlashCommandNamedArgument(
|
||||
'end', 'end index', [ARGUMENT_TYPE.NUMBER], false, false,
|
||||
),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'string', [ARGUMENT_TYPE.STRING], true, false,
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Extracts text from the provided string.
|
||||
</div>
|
||||
<div>
|
||||
If <code>start</code> is omitted, it's treated as 0.<br />
|
||||
If <code>start</code> < 0, the index is counted from the end of the string.<br />
|
||||
If <code>start</code> >= the string's length, an empty string is returned.<br />
|
||||
If <code>end</code> is omitted, or if <code>end</code> >= the string's length, extracts to the end of the string.<br />
|
||||
If <code>end</code> < 0, the index is counted from the end of the string.<br />
|
||||
If <code>end</code> <= <code>start</code> after normalizing negative values, an empty string is returned.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<pre>/let x The morning is upon us. || </pre>
|
||||
<pre>/substr start=-3 {{var::x}} | /echo |/# us. ||</pre>
|
||||
<pre>/substr start=-3 end=-1 {{var::x}} | /echo |/# us ||</pre>
|
||||
<pre>/substr end=-1 {{var::x}} | /echo |/# The morning is upon us ||</pre>
|
||||
<pre>/substr start=4 end=-1 {{var::x}} | /echo |/# morning is upon us ||</pre>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'is-mobile',
|
||||
callback: () => String(isMobile()),
|
||||
returns: ARGUMENT_TYPE.BOOLEAN,
|
||||
helpString: 'Returns true if the current device is a mobile device, false otherwise. Equivalent to <code>{{isMobile}}</code> macro.',
|
||||
}));
|
||||
|
||||
registerVariableCommands();
|
||||
}
|
||||
@@ -1901,6 +1973,11 @@ const NARRATOR_NAME_DEFAULT = 'System';
|
||||
export const COMMENT_NAME_DEFAULT = 'Note';
|
||||
const SCRIPT_PROMPT_KEY = 'script_inject_';
|
||||
|
||||
/**
|
||||
* Adds a new script injection to the chat.
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
|
||||
* @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value Unnamed argument
|
||||
*/
|
||||
function injectCallback(args, value) {
|
||||
const positions = {
|
||||
'before': extension_prompt_types.BEFORE_PROMPT,
|
||||
@@ -1914,8 +1991,8 @@ function injectCallback(args, value) {
|
||||
'assistant': extension_prompt_roles.ASSISTANT,
|
||||
};
|
||||
|
||||
const id = args?.id;
|
||||
const ephemeral = isTrueBoolean(args?.ephemeral);
|
||||
const id = String(args?.id);
|
||||
const ephemeral = isTrueBoolean(String(args?.ephemeral));
|
||||
|
||||
if (!id) {
|
||||
console.warn('WARN: No ID provided for /inject command');
|
||||
@@ -1931,9 +2008,15 @@ function injectCallback(args, value) {
|
||||
const depth = isNaN(depthValue) ? defaultDepth : depthValue;
|
||||
const roleValue = typeof args?.role === 'string' ? args.role.toLowerCase().trim() : Number(args?.role ?? extension_prompt_roles.SYSTEM);
|
||||
const role = roles[roleValue] ?? roles[extension_prompt_roles.SYSTEM];
|
||||
const scan = isTrueBoolean(args?.scan);
|
||||
const scan = isTrueBoolean(String(args?.scan));
|
||||
const filter = args?.filter instanceof SlashCommandClosure ? args.filter.rawText : null;
|
||||
const filterFunction = args?.filter instanceof SlashCommandClosure ? closureToFilter(args.filter) : null;
|
||||
value = value || '';
|
||||
|
||||
if (args?.filter && !String(filter ?? '').trim()) {
|
||||
throw new Error('Failed to parse the filter argument. Make sure it is a valid non-empty closure.');
|
||||
}
|
||||
|
||||
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
|
||||
|
||||
if (!chat_metadata.script_injects) {
|
||||
@@ -1941,13 +2024,13 @@ function injectCallback(args, value) {
|
||||
}
|
||||
|
||||
if (value) {
|
||||
const inject = { value, position, depth, scan, role };
|
||||
const inject = { value, position, depth, scan, role, filter };
|
||||
chat_metadata.script_injects[id] = inject;
|
||||
} else {
|
||||
delete chat_metadata.script_injects[id];
|
||||
}
|
||||
|
||||
setExtensionPrompt(prefixedId, value, position, depth, scan, role);
|
||||
setExtensionPrompt(prefixedId, String(value), position, depth, scan, role, filterFunction);
|
||||
saveMetadataDebounced();
|
||||
|
||||
if (ephemeral) {
|
||||
@@ -1958,7 +2041,7 @@ function injectCallback(args, value) {
|
||||
}
|
||||
console.log('Removing ephemeral script injection', id);
|
||||
delete chat_metadata.script_injects[id];
|
||||
setExtensionPrompt(prefixedId, '', position, depth, scan, role);
|
||||
setExtensionPrompt(prefixedId, '', position, depth, scan, role, filterFunction);
|
||||
saveMetadataDebounced();
|
||||
deleted = true;
|
||||
};
|
||||
@@ -2053,9 +2136,28 @@ export function processChatSlashCommands() {
|
||||
}
|
||||
|
||||
for (const [id, inject] of Object.entries(context.chatMetadata.script_injects)) {
|
||||
/**
|
||||
* Rehydrates a filter closure from a string.
|
||||
* @returns {SlashCommandClosure | null}
|
||||
*/
|
||||
function reviveFilterClosure() {
|
||||
if (!inject.filter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new SlashCommandParser().parse(inject.filter, true);
|
||||
} catch (error) {
|
||||
console.warn('Failed to revive filter closure for script injection', id, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const prefixedId = `${SCRIPT_PROMPT_KEY}${id}`;
|
||||
const filterClosure = reviveFilterClosure();
|
||||
const filter = filterClosure ? closureToFilter(filterClosure) : null;
|
||||
console.log('Adding script injection', id);
|
||||
setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role);
|
||||
setExtensionPrompt(prefixedId, inject.value, inject.position, inject.depth, inject.scan, inject.role, filter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3687,6 +3789,7 @@ function setBackgroundCallback(_, bg) {
|
||||
function getModelOptions(quiet) {
|
||||
const nullResult = { control: null, options: null };
|
||||
const modelSelectMap = [
|
||||
{ id: 'generic_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.GENERIC },
|
||||
{ id: 'custom_model_textgenerationwebui', api: 'textgenerationwebui', type: textgen_types.OOBA },
|
||||
{ id: 'model_togetherai_select', api: 'textgenerationwebui', type: textgen_types.TOGETHERAI },
|
||||
{ id: 'openrouter_model', api: 'textgenerationwebui', type: textgen_types.OPENROUTER },
|
||||
|
@@ -63,7 +63,7 @@ import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { tag_map, tags } from './tags.js';
|
||||
import { textgenerationwebui_settings } from './textgen-settings.js';
|
||||
import { getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
|
||||
import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
|
||||
import { ToolManager } from './tool-calling.js';
|
||||
import { timestampToMoment } from './utils.js';
|
||||
|
||||
@@ -95,6 +95,8 @@ export function getContext() {
|
||||
sendStreamingRequest,
|
||||
sendGenerationRequest,
|
||||
stopGeneration,
|
||||
tokenizers,
|
||||
getTextTokens,
|
||||
/** @deprecated Use getTokenCountAsync instead */
|
||||
getTokenCount,
|
||||
getTokenCountAsync,
|
||||
|
18
public/scripts/templates/chatLorebook.html
Normal file
18
public/scripts/templates/chatLorebook.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="chat_world range-block flexFlowColumn flex-container">
|
||||
<div class="range-block-title">
|
||||
<h4 data-i18n="Chat Lorebook"><!-- This data-i18n attribute is kept for backward compatibility, use the ones below when translating -->
|
||||
<span data-i18n="Chat Lorebook for">Chat Lorebook for</span> <span class="chat_name"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
|
||||
<span data-i18n="chat_world_template_txt">
|
||||
A selected World Info will be bound to this chat. When generating an AI reply,
|
||||
it will be combined with the entries from global and character lorebooks.
|
||||
</span>
|
||||
</div>
|
||||
<div class="range-block-range wide100p">
|
||||
<select class="chat_world_info_selector wide100p">
|
||||
<option value="">--- None ---</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
@@ -48,6 +48,7 @@
|
||||
<li><tt>{{random::(arg1)::(arg2)}}</tt> – <span data-i18n="help_macros_38">alternative syntax for random that allows to use commas in the list items.</span></li>
|
||||
<li><tt>{{pick::(args)}}</tt> – <span data-i18n="help_macros_39">picks a random item from the list. Works the same as {{random}}, with the same possible syntax options, but the pick will stay consistent for this chat once picked and won't be re-rolled on consecutive messages and prompt processing.</span></li>
|
||||
<li><tt>{{banned "text here"}}</tt> – <span data-i18n="help_macros_40">dynamically add text in the quotes to banned words sequences, if Text Generation WebUI backend used. Do nothing for others backends. Can be used anywhere (Character description, WI, AN, etc.) Quotes around the text are important.</span></li>
|
||||
<li><tt>{{isMobile}}</tt> – <span data-i18n="help_macros_isMobile">"true" if currently running in a mobile environment, "false" otherwise</span></li>
|
||||
</ul>
|
||||
<div data-i18n="Instruct Mode and Context Template Macros:">
|
||||
Instruct Mode and Context Template Macros:
|
||||
|
18
public/scripts/templates/personaLorebook.html
Normal file
18
public/scripts/templates/personaLorebook.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<div class="persona_world range-block flexFlowColumn flex-container">
|
||||
<div class="range-block-title">
|
||||
<h4>
|
||||
<span data-i18n="PErsona Lorebook for">Persona Lorebook for</span> <span class="persona_name"></span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
|
||||
<span data-i18n="persona_world_template_txt">
|
||||
A selected World Info will be bound to this persona. When generating an AI reply,
|
||||
it will be combined with the entries from global, character and chat lorebooks.
|
||||
</span>
|
||||
</div>
|
||||
<div class="range-block-range wide100p">
|
||||
<select class="persona_world_info_selector wide100p">
|
||||
<option value="">--- None ---</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
@@ -20,14 +20,14 @@
|
||||
<small>
|
||||
<span data-i18n="Drag handle to reorder. Click name to rename. Click color to change display.">Drag handle to reorder. Click name to rename. Click color to change display.</span><br>
|
||||
{{#if bogus_folders}}<span data-i18n="Click on the folder icon to use this tag as a folder.">Click on the folder icon to use this tag as a folder.</span><br>{{/if}}
|
||||
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-1" for="auto_sort_tags">
|
||||
<label class="checkbox_label flex-container alignItemsCenter m-t-1" for="auto_sort_tags">
|
||||
<input type="checkbox" id="auto_sort_tags" name="auto_sort_tags" {{#if auto_sort_tags}} checked{{/if}} />
|
||||
<span data-i18n="Use alphabetical sorting">
|
||||
Use alphabetical sorting
|
||||
</span>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]tags_sorting_desc"
|
||||
title="If enabled, tags will automatically be sorted alphabetically on creation or rename.\nIf disabled, new tags will be appended at the end.\n\nIf a tag is manually reordered by dragging, automatic sorting will be disabled.">
|
||||
</div>
|
||||
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]tags_sorting_desc"
|
||||
title="If enabled, tags will automatically be sorted alphabetically on creation or rename. If disabled, new tags will be appended at the end. If a tag is manually reordered by dragging, automatic sorting will be disabled.">
|
||||
</div>
|
||||
</label>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -25,6 +25,7 @@ const OPENROUTER_PROVIDERS = [
|
||||
'Anthropic',
|
||||
'Google',
|
||||
'Google AI Studio',
|
||||
'Amazon Bedrock',
|
||||
'Groq',
|
||||
'SambaNova',
|
||||
'Cohere',
|
||||
@@ -50,6 +51,8 @@ const OPENROUTER_PROVIDERS = [
|
||||
'Featherless',
|
||||
'Inflection',
|
||||
'xAI',
|
||||
'Cloudflare',
|
||||
'SF Compute',
|
||||
'01.AI',
|
||||
'HuggingFace',
|
||||
'Mancer',
|
||||
@@ -160,6 +163,24 @@ export async function loadInfermaticAIModels(data) {
|
||||
}
|
||||
}
|
||||
|
||||
export function loadGenericModels(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Invalid Generic models data', data);
|
||||
return;
|
||||
}
|
||||
|
||||
data.sort((a, b) => a.id.localeCompare(b.id));
|
||||
const dataList = $('#generic_model_fill');
|
||||
dataList.empty();
|
||||
|
||||
for (const model of data) {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.id;
|
||||
option.text = model.id;
|
||||
dataList.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadDreamGenModels(data) {
|
||||
if (!Array.isArray(data)) {
|
||||
console.error('Invalid DreamGen models data', data);
|
||||
|
@@ -16,7 +16,7 @@ import { power_user, registerDebugFunction } from './power-user.js';
|
||||
import { getEventSourceStream } from './sse-stream.js';
|
||||
import { getCurrentDreamGenModelTokenizer, getCurrentOpenRouterModelTokenizer } from './textgen-models.js';
|
||||
import { ENCODE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js';
|
||||
import { getSortableDelay, onlyUnique } from './utils.js';
|
||||
import { getSortableDelay, onlyUnique, arraysEqual } from './utils.js';
|
||||
|
||||
export const textgen_types = {
|
||||
OOBA: 'ooba',
|
||||
@@ -33,9 +33,11 @@ export const textgen_types = {
|
||||
OPENROUTER: 'openrouter',
|
||||
FEATHERLESS: 'featherless',
|
||||
HUGGINGFACE: 'huggingface',
|
||||
GENERIC: 'generic',
|
||||
};
|
||||
|
||||
const {
|
||||
GENERIC,
|
||||
MANCER,
|
||||
VLLM,
|
||||
APHRODITE,
|
||||
@@ -53,6 +55,7 @@ const {
|
||||
} = textgen_types;
|
||||
|
||||
const LLAMACPP_DEFAULT_ORDER = [
|
||||
'dry',
|
||||
'top_k',
|
||||
'tfs_z',
|
||||
'typical_p',
|
||||
@@ -82,6 +85,22 @@ const OOBA_DEFAULT_ORDER = [
|
||||
'encoder_repetition_penalty',
|
||||
'no_repeat_ngram',
|
||||
];
|
||||
const APHRODITE_DEFAULT_ORDER = [
|
||||
'dry',
|
||||
'penalties',
|
||||
'no_repeat_ngram',
|
||||
'temperature',
|
||||
'top_nsigma',
|
||||
'top_p_top_k',
|
||||
'top_a',
|
||||
'min_p',
|
||||
'tfs',
|
||||
'eta_cutoff',
|
||||
'epsilon_cutoff',
|
||||
'typical_p',
|
||||
'quadratic',
|
||||
'xtc',
|
||||
];
|
||||
const BIAS_KEY = '#textgenerationwebui_api-settings';
|
||||
|
||||
// Maybe let it be configurable in the future?
|
||||
@@ -104,6 +123,7 @@ export const SERVER_INPUTS = {
|
||||
[textgen_types.LLAMACPP]: '#llamacpp_api_url_text',
|
||||
[textgen_types.OLLAMA]: '#ollama_api_url_text',
|
||||
[textgen_types.HUGGINGFACE]: '#huggingface_api_url_text',
|
||||
[textgen_types.GENERIC]: '#generic_api_url_text',
|
||||
};
|
||||
|
||||
const KOBOLDCPP_ORDER = [6, 0, 1, 3, 4, 2, 5];
|
||||
@@ -163,6 +183,7 @@ const settings = {
|
||||
banned_tokens: '',
|
||||
sampler_priority: OOBA_DEFAULT_ORDER,
|
||||
samplers: LLAMACPP_DEFAULT_ORDER,
|
||||
samplers_priorities: APHRODITE_DEFAULT_ORDER,
|
||||
ignore_eos_token: false,
|
||||
spaces_between_special_tokens: true,
|
||||
speculative_ngram: false,
|
||||
@@ -188,6 +209,7 @@ const settings = {
|
||||
xtc_probability: 0,
|
||||
nsigma: 0.0,
|
||||
featherless_model: '',
|
||||
generic_model: '',
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -256,6 +278,7 @@ export const setting_names = [
|
||||
'sampler_order',
|
||||
'sampler_priority',
|
||||
'samplers',
|
||||
'samplers_priorities',
|
||||
'n',
|
||||
'logit_bias',
|
||||
'custom_model',
|
||||
@@ -264,6 +287,7 @@ export const setting_names = [
|
||||
'xtc_threshold',
|
||||
'xtc_probability',
|
||||
'nsigma',
|
||||
'generic_model',
|
||||
];
|
||||
|
||||
const DYNATEMP_BLOCK = document.getElementById('dynatemp_block_ooba');
|
||||
@@ -553,6 +577,20 @@ function sortOobaItemsByOrder(orderArray) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the Aphrodite sampler items by the given order.
|
||||
* @param {string[]} orderArray Sampler order array.
|
||||
*/
|
||||
function sortAphroditeItemsByOrder(orderArray) {
|
||||
console.debug('Preset samplers order: ', orderArray);
|
||||
const $container = $('#sampler_priority_container_aphrodite');
|
||||
|
||||
orderArray.forEach((name) => {
|
||||
const $item = $container.find(`[data-name="${name}"]`).detach();
|
||||
$container.append($item);
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(function () {
|
||||
$('#koboldcpp_order').sortable({
|
||||
delay: getSortableDelay(),
|
||||
@@ -606,6 +644,19 @@ jQuery(function () {
|
||||
},
|
||||
});
|
||||
|
||||
$('#sampler_priority_container_aphrodite').sortable({
|
||||
delay: getSortableDelay(),
|
||||
stop: function () {
|
||||
const order = [];
|
||||
$('#sampler_priority_container_aphrodite').children().each(function () {
|
||||
order.push($(this).data('name'));
|
||||
});
|
||||
settings.samplers_priorities = order;
|
||||
console.log('Samplers reordered:', settings.samplers_priorities);
|
||||
saveSettingsDebounced();
|
||||
},
|
||||
});
|
||||
|
||||
$('#tabby_json_schema').on('input', function () {
|
||||
const json_schema_string = String($(this).val());
|
||||
|
||||
@@ -624,6 +675,13 @@ jQuery(function () {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#aphrodite_default_order').on('click', function () {
|
||||
sortAphroditeItemsByOrder(APHRODITE_DEFAULT_ORDER);
|
||||
settings.samplers_priorities = APHRODITE_DEFAULT_ORDER;
|
||||
console.log('Default samplers order loaded:', settings.samplers_priorities);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#textgen_type').on('change', function () {
|
||||
const type = String($(this).val());
|
||||
settings.type = type;
|
||||
@@ -781,7 +839,14 @@ jQuery(function () {
|
||||
|
||||
function showTypeSpecificControls(type) {
|
||||
$('[data-tg-type]').each(function () {
|
||||
const mode = String($(this).attr('data-tg-type-mode') ?? '').toLowerCase().trim();
|
||||
const tgTypes = $(this).attr('data-tg-type').split(',').map(x => x.trim());
|
||||
|
||||
if (mode === 'except') {
|
||||
$(this)[tgTypes.includes(type) ? 'hide' : 'show']();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const tgType of tgTypes) {
|
||||
if (tgType === type || tgType == 'all') {
|
||||
$(this).show();
|
||||
@@ -832,6 +897,14 @@ function setSettingByName(setting, value, trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ('samplers_priorities' === setting) {
|
||||
value = Array.isArray(value) ? value : APHRODITE_DEFAULT_ORDER;
|
||||
insertMissingArrayItems(APHRODITE_DEFAULT_ORDER, value);
|
||||
sortAphroditeItemsByOrder(value);
|
||||
settings.samplers_priorities = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if ('samplers' === setting) {
|
||||
value = Array.isArray(value) ? value : LLAMACPP_DEFAULT_ORDER;
|
||||
insertMissingArrayItems(LLAMACPP_DEFAULT_ORDER, value);
|
||||
@@ -966,12 +1039,30 @@ export function parseTextgenLogprobs(token, logprobs) {
|
||||
return { token, topLogprobs: candidates };
|
||||
}
|
||||
case LLAMACPP: {
|
||||
/** @type {Record<string, number>[]} */
|
||||
if (!logprobs?.length) {
|
||||
return null;
|
||||
}
|
||||
const candidates = logprobs[0].probs.map(x => [x.tok_str, x.prob]);
|
||||
return { token, topLogprobs: candidates };
|
||||
|
||||
// 3 cases:
|
||||
// 1. Before commit 6c5bc06, "probs" key with "tok_str"/"prob", and probs are [0, 1] so use them directly.
|
||||
// 2. After commit 6c5bc06 but before commit 89d604f broke logprobs (they all return the first token's logprobs)
|
||||
// We don't know the llama.cpp version so we can't do much about this.
|
||||
// 3. After commit 89d604f uses OpenAI-compatible format with "completion_probabilities" and "token"/"logprob" keys.
|
||||
// Note that it is also the *actual* logprob (negative number), so we need to convert to [0, 1].
|
||||
if (logprobs?.[0]?.probs) {
|
||||
const candidates = logprobs?.[0]?.probs?.map(x => [x.tok_str, x.prob]);
|
||||
if (!candidates) {
|
||||
return null;
|
||||
}
|
||||
return { token, topLogprobs: candidates };
|
||||
} else if (logprobs?.[0].top_logprobs) {
|
||||
const candidates = logprobs?.[0]?.top_logprobs?.map(x => [x.token, Math.exp(x.logprob)]);
|
||||
if (!candidates) {
|
||||
return null;
|
||||
}
|
||||
return { token, topLogprobs: candidates };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
@@ -1040,6 +1131,11 @@ export function getTextGenModel() {
|
||||
return settings.custom_model;
|
||||
}
|
||||
break;
|
||||
case GENERIC:
|
||||
if (settings.generic_model) {
|
||||
return settings.generic_model;
|
||||
}
|
||||
break;
|
||||
case MANCER:
|
||||
return settings.mancer_model;
|
||||
case TOGETHERAI:
|
||||
@@ -1256,6 +1352,11 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
||||
'nsigma': settings.nsigma,
|
||||
'custom_token_bans': toIntArray(banned_tokens),
|
||||
'no_repeat_ngram_size': settings.no_repeat_ngram_size,
|
||||
'sampler_priority': settings.type === APHRODITE && !arraysEqual(
|
||||
settings.samplers_priorities,
|
||||
APHRODITE_DEFAULT_ORDER)
|
||||
? settings.samplers_priorities
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (settings.type === OPENROUTER) {
|
||||
|
@@ -32,6 +32,7 @@ export const tokenizers = {
|
||||
COMMAND_R: 16,
|
||||
NEMO: 17,
|
||||
BEST_MATCH: 99,
|
||||
MANUAL_SELECTION: 411,
|
||||
};
|
||||
|
||||
// A list of local tokenizers that support encoding and decoding token ids.
|
||||
@@ -536,7 +537,6 @@ export function getTokenizerModel() {
|
||||
return oai_settings.openai_model;
|
||||
}
|
||||
|
||||
const turbo0301Tokenizer = 'gpt-3.5-turbo-0301';
|
||||
const turboTokenizer = 'gpt-3.5-turbo';
|
||||
const gpt4Tokenizer = 'gpt-4';
|
||||
const gpt4oTokenizer = 'gpt-4o';
|
||||
@@ -562,9 +562,6 @@ export function getTokenizerModel() {
|
||||
if (oai_settings.windowai_model.includes('gpt-4')) {
|
||||
return gpt4Tokenizer;
|
||||
}
|
||||
else if (oai_settings.windowai_model.includes('gpt-3.5-turbo-0301')) {
|
||||
return turbo0301Tokenizer;
|
||||
}
|
||||
else if (oai_settings.windowai_model.includes('gpt-3.5-turbo')) {
|
||||
return turboTokenizer;
|
||||
}
|
||||
@@ -610,9 +607,6 @@ export function getTokenizerModel() {
|
||||
else if (oai_settings.openrouter_model.includes('gpt-4')) {
|
||||
return gpt4Tokenizer;
|
||||
}
|
||||
else if (oai_settings.openrouter_model.includes('gpt-3.5-turbo-0301')) {
|
||||
return turbo0301Tokenizer;
|
||||
}
|
||||
else if (oai_settings.openrouter_model.includes('gpt-3.5-turbo')) {
|
||||
return turboTokenizer;
|
||||
}
|
||||
@@ -1064,9 +1058,14 @@ function decodeTextTokensFromServer(endpoint, ids, resolve) {
|
||||
* Encodes a string to tokens using the server API.
|
||||
* @param {number} tokenizerType Tokenizer type.
|
||||
* @param {string} str String to tokenize.
|
||||
* @param {string} overrideModel Tokenizer for {tokenizers.MANUAL_SELECTION}.
|
||||
* @returns {number[]} Array of token ids.
|
||||
*/
|
||||
export function getTextTokens(tokenizerType, str) {
|
||||
export function getTextTokens(tokenizerType, str, overrideModel = undefined) {
|
||||
if (overrideModel && tokenizerType !== tokenizers.MANUAL_SELECTION) {
|
||||
console.warn('overrideModel must be undefined unless using tokenizers.MANUAL_SELECTION', tokenizerType);
|
||||
return [];
|
||||
}
|
||||
switch (tokenizerType) {
|
||||
case tokenizers.API_CURRENT:
|
||||
return getTextTokens(currentRemoteTokenizerAPI(), str);
|
||||
@@ -1087,6 +1086,9 @@ export function getTextTokens(tokenizerType, str) {
|
||||
console.warn('This tokenizer type does not support encoding', tokenizerType);
|
||||
return [];
|
||||
}
|
||||
if (tokenizerType === tokenizers.MANUAL_SELECTION) {
|
||||
endpointUrl += `?model=${overrideModel}`;
|
||||
}
|
||||
if (tokenizerType === tokenizers.OPENAI) {
|
||||
endpointUrl += `?model=${getTokenizerModel()}`;
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ import { isTrueBoolean } from './utils.js';
|
||||
* @typedef {object} ToolInvocationResult
|
||||
* @property {ToolInvocation[]} invocations Successful tool invocations
|
||||
* @property {Error[]} errors Errors that occurred during tool invocation
|
||||
* @property {string[]} stealthCalls Names of stealth tools that were invoked
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -36,6 +37,7 @@ import { isTrueBoolean } from './utils.js';
|
||||
* @property {function} action - The action to perform when the tool is invoked.
|
||||
* @property {function} [formatMessage] - A function to format the tool call message.
|
||||
* @property {function} [shouldRegister] - A function to determine if the tool should be registered.
|
||||
* @property {boolean} [stealth] - A tool call result will not be shown in the chat. No follow-up generation will be performed.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -147,6 +149,12 @@ class ToolDefinition {
|
||||
*/
|
||||
#shouldRegister;
|
||||
|
||||
/**
|
||||
* A tool call result will not be shown in the chat. No follow-up generation will be performed.
|
||||
* @type {boolean}
|
||||
*/
|
||||
#stealth;
|
||||
|
||||
/**
|
||||
* Creates a new ToolDefinition.
|
||||
* @param {string} name A unique name for the tool.
|
||||
@@ -156,8 +164,9 @@ class ToolDefinition {
|
||||
* @param {function} action A function that will be called when the tool is executed.
|
||||
* @param {function} formatMessage A function that will be called to format the tool call toast.
|
||||
* @param {function} shouldRegister A function that will be called to determine if the tool should be registered.
|
||||
* @param {boolean} stealth A tool call result will not be shown in the chat. No follow-up generation will be performed.
|
||||
*/
|
||||
constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister) {
|
||||
constructor(name, displayName, description, parameters, action, formatMessage, shouldRegister, stealth) {
|
||||
this.#name = name;
|
||||
this.#displayName = displayName;
|
||||
this.#description = description;
|
||||
@@ -165,6 +174,7 @@ class ToolDefinition {
|
||||
this.#action = action;
|
||||
this.#formatMessage = formatMessage;
|
||||
this.#shouldRegister = shouldRegister;
|
||||
this.#stealth = stealth;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,6 +224,10 @@ class ToolDefinition {
|
||||
get displayName() {
|
||||
return this.#displayName;
|
||||
}
|
||||
|
||||
get stealth() {
|
||||
return this.#stealth;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -246,7 +260,7 @@ export class ToolManager {
|
||||
* Registers a new tool with the tool registry.
|
||||
* @param {ToolRegistration} tool The tool to register.
|
||||
*/
|
||||
static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage, shouldRegister }) {
|
||||
static registerFunctionTool({ name, displayName, description, parameters, action, formatMessage, shouldRegister, stealth }) {
|
||||
// Convert WIP arguments
|
||||
if (typeof arguments[0] !== 'object') {
|
||||
[name, description, parameters, action] = arguments;
|
||||
@@ -256,7 +270,16 @@ export class ToolManager {
|
||||
console.warn(`[ToolManager] A tool with the name "${name}" has already been registered. The definition will be overwritten.`);
|
||||
}
|
||||
|
||||
const definition = new ToolDefinition(name, displayName, description, parameters, action, formatMessage, shouldRegister);
|
||||
const definition = new ToolDefinition(
|
||||
name,
|
||||
displayName,
|
||||
description,
|
||||
parameters,
|
||||
action,
|
||||
formatMessage,
|
||||
shouldRegister,
|
||||
stealth,
|
||||
);
|
||||
this.#tools.set(name, definition);
|
||||
console.log('[ToolManager] Registered function tool:', definition);
|
||||
}
|
||||
@@ -302,6 +325,20 @@ export class ToolManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a tool is a stealth tool.
|
||||
* @param {string} name The name of the tool to check.
|
||||
* @returns {boolean} Whether the tool is a stealth tool.
|
||||
*/
|
||||
static isStealthTool(name) {
|
||||
if (!this.#tools.has(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tool = this.#tools.get(name);
|
||||
return !!tool.stealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a message for a tool call by name.
|
||||
* @param {string} name The name of the tool to format the message for.
|
||||
@@ -608,6 +645,7 @@ export class ToolManager {
|
||||
const result = {
|
||||
invocations: [],
|
||||
errors: [],
|
||||
stealthCalls: [],
|
||||
};
|
||||
const toolCalls = ToolManager.#getToolCallsFromData(data);
|
||||
|
||||
@@ -625,7 +663,7 @@ export class ToolManager {
|
||||
const parameters = toolCall.function.arguments;
|
||||
const name = toolCall.function.name;
|
||||
const displayName = ToolManager.getDisplayName(name);
|
||||
|
||||
const isStealth = ToolManager.isStealthTool(name);
|
||||
const message = await ToolManager.formatToolCallMessage(name, parameters);
|
||||
const toast = message && toastr.info(message, 'Tool Calling', { timeOut: 0 });
|
||||
const toolResult = await ToolManager.invokeFunctionTool(name, parameters);
|
||||
@@ -638,6 +676,12 @@ export class ToolManager {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don't save stealth tool invocations
|
||||
if (isStealth) {
|
||||
result.stealthCalls.push(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
const invocation = {
|
||||
id,
|
||||
displayName,
|
||||
@@ -860,6 +904,14 @@ export class ToolManager {
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'stealth',
|
||||
description: 'If true, a tool call result will not be shown in the chat and no follow-up generation will be performed.',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
defaultValue: String(false),
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
@@ -891,7 +943,7 @@ export class ToolManager {
|
||||
};
|
||||
}
|
||||
|
||||
const { name, displayName, description, parameters, formatMessage, shouldRegister } = args;
|
||||
const { name, displayName, description, parameters, formatMessage, shouldRegister, stealth } = args;
|
||||
|
||||
if (!(action instanceof SlashCommandClosure)) {
|
||||
throw new Error('The unnamed argument must be a closure.');
|
||||
@@ -927,6 +979,7 @@ export class ToolManager {
|
||||
action: actionFunc,
|
||||
formatMessage: formatMessageFunc,
|
||||
shouldRegister: shouldRegisterFunc,
|
||||
stealth: stealth && isTrueBoolean(String(stealth)),
|
||||
});
|
||||
|
||||
return '';
|
||||
|
@@ -31,7 +31,7 @@ export async function setUserControls(isEnabled) {
|
||||
* Check if the current user is an admin.
|
||||
* @returns {boolean} True if the current user is an admin
|
||||
*/
|
||||
function isAdmin() {
|
||||
export function isAdmin() {
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
@@ -665,9 +665,10 @@ export function trimToEndSentence(input) {
|
||||
const characters = Array.from(input);
|
||||
for (let i = characters.length - 1; i >= 0; i--) {
|
||||
const char = characters[i];
|
||||
const emoji = isEmoji(char);
|
||||
|
||||
if (punctuation.has(char) || isEmoji(char)) {
|
||||
if (i > 0 && /[\s\n]/.test(characters[i - 1])) {
|
||||
if (punctuation.has(char) || emoji) {
|
||||
if (!emoji && i > 0 && /[\s\n]/.test(characters[i - 1])) {
|
||||
last = i - 1;
|
||||
} else {
|
||||
last = i;
|
||||
@@ -2112,7 +2113,7 @@ export async function showFontAwesomePicker(customList = null) {
|
||||
* @param {string[]?} [options.filteredByTags=null] - Tags to filter characters by
|
||||
* @param {boolean} [options.preferCurrentChar=true] - Whether to prefer the current character(s)
|
||||
* @param {boolean} [options.quiet=false] - Whether to suppress warnings
|
||||
* @returns {any?} - The found character or null if not found
|
||||
* @returns {import('./char-data.js').v1CharData?} - The found character or null if not found
|
||||
*/
|
||||
export function findChar({ name = null, allowAvatar = true, insensitive = true, filteredByTags = null, preferCurrentChar = true, quiet = false } = {}) {
|
||||
const matches = (char) => !name || (allowAvatar && char.avatar === name) || (insensitive ? equalsIgnoreCaseAndAccents(char.name, name) : char.name === name);
|
||||
@@ -2173,3 +2174,20 @@ export function getCharIndex(char) {
|
||||
if (index === -1) throw new Error(`Character not found: ${char.avatar}`);
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two arrays for equality
|
||||
* @param {any[]} a - The first array
|
||||
* @param {any[]} b - The second array
|
||||
* @returns {boolean} True if the arrays are equal, false otherwise
|
||||
*/
|
||||
export function arraysEqual(a, b) {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@@ -46,6 +46,10 @@ function getLocalVariable(name, args = {}) {
|
||||
}
|
||||
|
||||
function setLocalVariable(name, value, args = {}) {
|
||||
if (!name) {
|
||||
throw new Error('Variable name cannot be empty or undefined.');
|
||||
}
|
||||
|
||||
if (!chat_metadata.variables) {
|
||||
chat_metadata.variables = {};
|
||||
}
|
||||
@@ -99,6 +103,10 @@ function getGlobalVariable(name, args = {}) {
|
||||
}
|
||||
|
||||
function setGlobalVariable(name, value, args = {}) {
|
||||
if (!name) {
|
||||
throw new Error('Variable name cannot be empty or undefined.');
|
||||
}
|
||||
|
||||
if (args.index !== undefined) {
|
||||
try {
|
||||
let globalVariable = JSON.parse(extension_settings.variables.global[name] ?? 'null');
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Fuse } from '../lib.js';
|
||||
|
||||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce } from './utils.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce, findChar, onlyUnique } from './utils.js';
|
||||
import { extension_settings, getContext } from './extensions.js';
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||
import { isMobile } from './RossAscends-mods.js';
|
||||
@@ -930,7 +930,50 @@ function registerWorldInfoSlashCommands() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function getChatBookCallback() {
|
||||
/**
|
||||
* Gets the name of the persona-bound lorebook.
|
||||
* @returns {string} The name of the persona-bound lorebook
|
||||
*/
|
||||
function getPersonaBookCallback() {
|
||||
return power_user.persona_description_lorebook || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the character-bound lorebook.
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
|
||||
* @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} name Character name
|
||||
* @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string
|
||||
*/
|
||||
function getCharBookCallback({ type }, name) {
|
||||
const context = getContext();
|
||||
if (context.groupId && !name) throw new Error('This command is not available in groups without providing a character name');
|
||||
type = String(type ?? '').trim().toLowerCase() || 'primary';
|
||||
name = String(name ?? '') || context.characters[context.characterId]?.avatar || null;
|
||||
const character = findChar({ name });
|
||||
if (!character) {
|
||||
toastr.error('Character not found.');
|
||||
return '';
|
||||
}
|
||||
const books = [];
|
||||
if (type === 'all' || type === 'primary') {
|
||||
books.push(character.data?.extensions?.world);
|
||||
}
|
||||
if (type === 'all' || type === 'additional') {
|
||||
const fileName = getCharaFilename(context.characters.indexOf(character));
|
||||
const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);
|
||||
if (extraCharLore && Array.isArray(extraCharLore.extraBooks)) {
|
||||
books.push(...extraCharLore.extraBooks);
|
||||
}
|
||||
}
|
||||
return type === 'primary' ? (books[0] ?? '') : JSON.stringify(books.filter(onlyUnique).filter(Boolean));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the chat-bound lorebook. Creates a new one if it doesn't exist.
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
|
||||
* @returns {Promise<string>} The name of the chat-bound lorebook
|
||||
*/
|
||||
async function getChatBookCallback(args) {
|
||||
const chatId = getCurrentChatId();
|
||||
|
||||
if (!chatId) {
|
||||
@@ -942,8 +985,19 @@ function registerWorldInfoSlashCommands() {
|
||||
return chat_metadata[METADATA_KEY];
|
||||
}
|
||||
|
||||
// Replace non-alphanumeric characters with underscores, cut to 64 characters
|
||||
const name = `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64);
|
||||
const name = (() => {
|
||||
// Use the provided name if it's not in use
|
||||
if (typeof args.name === 'string') {
|
||||
const name = String(args.name);
|
||||
if (world_names.includes(name)) {
|
||||
throw new Error('This World Info file name is already in use');
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// Replace non-alphanumeric characters with underscores, cut to 64 characters
|
||||
return `Chat Book ${getCurrentChatId()}`.replace(/[^a-z0-9]/gi, '_').replace(/_{2,}/g, '_').substring(0, 64);
|
||||
})();
|
||||
await createNewWorldInfo(name);
|
||||
|
||||
chat_metadata[METADATA_KEY] = name;
|
||||
@@ -1289,8 +1343,48 @@ function registerWorldInfoSlashCommands() {
|
||||
callback: getChatBookCallback,
|
||||
returns: 'lorebook name',
|
||||
helpString: 'Get a name of the chat-bound lorebook or create a new one if was unbound, and pass it down the pipe.',
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'name',
|
||||
description: 'lorebook name if creating a new one, will be auto-generated otherwise',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
isRequired: false,
|
||||
acceptsMultiple: false,
|
||||
}),
|
||||
],
|
||||
aliases: ['getchatlore', 'getchatwi'],
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'getpersonabook',
|
||||
callback: getPersonaBookCallback,
|
||||
returns: 'lorebook name',
|
||||
helpString: 'Get a name of the current persona-bound lorebook and pass it down the pipe. Returns empty string if persona lorebook is not set.',
|
||||
aliases: ['getpersonalore', 'getpersonawi'],
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'getcharbook',
|
||||
callback: getCharBookCallback,
|
||||
returns: 'lorebook name or a list of lorebook names',
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'type',
|
||||
description: 'type of the lorebook to get, returns a list for "all" and "additional"',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
enumList: ['primary', 'additional', 'all'],
|
||||
defaultValue: 'primary',
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'Character name - or unique character identifier (avatar key). If not provided, the current character is used.',
|
||||
typeList: [ARGUMENT_TYPE.NUMBER, ARGUMENT_TYPE.STRING],
|
||||
isRequired: false,
|
||||
enumProvider: commonEnumProviders.characters('character'),
|
||||
}),
|
||||
],
|
||||
helpString: 'Get a name of the character-bound lorebook and pass it down the pipe. Returns empty string if character lorebook is not set. Does not work in group chats without providing a character avatar name.',
|
||||
aliases: ['getcharlore', 'getcharwi'],
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'findentry',
|
||||
@@ -3548,6 +3642,11 @@ async function getCharacterLore() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (power_user.persona_description_lorebook === worldName) {
|
||||
console.debug(`[WI] Character ${name}'s world ${worldName} is already activated in persona lore! Skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await loadWorldInfo(worldName);
|
||||
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: worldName, ...rest })) : [];
|
||||
entries = entries.concat(newEntries);
|
||||
@@ -3598,11 +3697,45 @@ async function getChatLore() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
async function getPersonaLore() {
|
||||
const chatWorld = chat_metadata[METADATA_KEY];
|
||||
const personaWorld = power_user.persona_description_lorebook;
|
||||
|
||||
if (!personaWorld) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (chatWorld === personaWorld) {
|
||||
console.debug(`[WI] Persona world ${personaWorld} is already activated in chat world! Skipping...`);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (selected_world_info.includes(personaWorld)) {
|
||||
console.debug(`[WI] Persona world ${personaWorld} is already activated in global world info! Skipping...`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await loadWorldInfo(personaWorld);
|
||||
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]).map(({ uid, ...rest }) => ({ uid, world: personaWorld, ...rest })) : [];
|
||||
|
||||
console.debug(`[WI] Persona lore has ${entries.length} entries`, [personaWorld]);
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
export async function getSortedEntries() {
|
||||
try {
|
||||
const globalLore = await getGlobalLore();
|
||||
const characterLore = await getCharacterLore();
|
||||
const chatLore = await getChatLore();
|
||||
const [
|
||||
globalLore,
|
||||
characterLore,
|
||||
chatLore,
|
||||
personaLore,
|
||||
] = await Promise.all([
|
||||
getGlobalLore(),
|
||||
getCharacterLore(),
|
||||
getChatLore(),
|
||||
getPersonaLore(),
|
||||
]);
|
||||
|
||||
let entries;
|
||||
|
||||
@@ -3622,8 +3755,8 @@ export async function getSortedEntries() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Chat lore always goes first
|
||||
entries = [...chatLore.sort(sortFn), ...entries];
|
||||
// Chat lore always goes first, then persona lore, then the rest
|
||||
entries = [...chatLore.sort(sortFn), ...personaLore.sort(sortFn), ...entries];
|
||||
|
||||
// Calculate hash and parse decorators. Split maps to preserve old hashes.
|
||||
entries = entries.map((entry) => {
|
||||
@@ -3721,7 +3854,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
|
||||
// Put this code here since otherwise, the chat reference is modified
|
||||
for (const key of Object.keys(context.extensionPrompts)) {
|
||||
if (context.extensionPrompts[key]?.scan) {
|
||||
const prompt = getExtensionPromptByName(key);
|
||||
const prompt = await getExtensionPromptByName(key);
|
||||
if (prompt) {
|
||||
buffer.addInject(prompt);
|
||||
}
|
||||
@@ -4816,9 +4949,33 @@ export async function importWorldInfo(file) {
|
||||
});
|
||||
}
|
||||
|
||||
export function assignLorebookToChat() {
|
||||
/**
|
||||
* Forces the world info editor to open on a specific world.
|
||||
* @param {string} worldName The name of the world to open
|
||||
*/
|
||||
export function openWorldInfoEditor(worldName) {
|
||||
console.log(`Opening lorebook for ${worldName}`);
|
||||
if (!$('#WorldInfo').is(':visible')) {
|
||||
$('#WIDrawerIcon').trigger('click');
|
||||
}
|
||||
const index = world_names.indexOf(worldName);
|
||||
$('#world_editor_select').val(index).trigger('change');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a lorebook to the current chat.
|
||||
* @param {PointerEvent} event Pointer event
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function assignLorebookToChat(event) {
|
||||
const selectedName = chat_metadata[METADATA_KEY];
|
||||
const template = $('#chat_world_template .chat_world').clone();
|
||||
|
||||
if (selectedName && event.altKey) {
|
||||
openWorldInfoEditor(selectedName);
|
||||
return;
|
||||
}
|
||||
|
||||
const template = $(await renderTemplateAsync('chatLorebook'));
|
||||
|
||||
const worldSelect = template.find('select');
|
||||
const chatName = template.find('.chat_name');
|
||||
@@ -4846,7 +5003,7 @@ export function assignLorebookToChat() {
|
||||
saveMetadata();
|
||||
});
|
||||
|
||||
callPopup(template, 'text');
|
||||
return callGenericPopup(template, POPUP_TYPE.TEXT);
|
||||
}
|
||||
|
||||
jQuery(() => {
|
||||
@@ -4997,11 +5154,7 @@ jQuery(() => {
|
||||
const worldName = characters[chid]?.data?.extensions?.world;
|
||||
const hasEmbed = checkEmbeddedWorld(chid);
|
||||
if (worldName && world_names.includes(worldName) && !event.shiftKey) {
|
||||
if (!$('#WorldInfo').is(':visible')) {
|
||||
$('#WIDrawerIcon').trigger('click');
|
||||
}
|
||||
const index = world_names.indexOf(worldName);
|
||||
$('#world_editor_select').val(index).trigger('change');
|
||||
openWorldInfoEditor(worldName);
|
||||
} else if (hasEmbed && !event.shiftKey) {
|
||||
await importEmbeddedWorldInfo();
|
||||
saveCharacterDebounced();
|
||||
|
Reference in New Issue
Block a user