Merge branch 'staging' into smol-tag-improvements

This commit is contained in:
Wolfsblvt
2024-06-02 21:07:46 +02:00
93 changed files with 4666 additions and 1911 deletions

View File

@ -1,15 +1,301 @@
import { renderExtensionTemplateAsync } from '../../extensions.js';
import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
/**
* List of attachment sources
* @type {string[]}
*/
const TYPES = ['global', 'character', 'chat'];
const FIELDS = ['name', 'url'];
/**
* Get attachments from the data bank. Includes disabled attachments.
* @param {string} [source] Source for the attachments
* @returns {import('../../chats').FileAttachment[]} List of attachments
*/
function getAttachments(source) {
if (!source || !TYPES.includes(source)) {
return getDataBankAttachments(true);
}
return getDataBankAttachmentsForSource(source, true);
}
/**
* Get attachment by a single name or URL.
* @param {import('../../chats').FileAttachment[]} attachments List of attachments
* @param {string} value Name or URL of the attachment
* @returns {import('../../chats').FileAttachment} Attachment
*/
function getAttachmentByField(attachments, value) {
const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase();
const fullMatchByURL = attachments.find(it => match(it.url));
const fullMatchByName = attachments.find(it => match(it.name));
return fullMatchByURL || fullMatchByName;
}
/**
* Get attachment by multiple fields.
* @param {import('../../chats').FileAttachment[]} attachments List of attachments
* @param {string[]} values Name and URL of the attachment to search for
* @returns
*/
function getAttachmentByFields(attachments, values) {
for (const value of values) {
const attachment = getAttachmentByField(attachments, value);
if (attachment) {
return attachment;
}
}
return null;
}
/**
* Callback for listing attachments in the data bank.
* @param {object} args Named arguments
* @returns {string} JSON string of the list of attachments
*/
function listDataBankAttachments(args) {
const attachments = getAttachments(args?.source);
const field = args?.field;
return JSON.stringify(attachments.map(a => FIELDS.includes(field) ? a[field] : a.url));
}
/**
* Callback for getting text from an attachment in the data bank.
* @param {object} args Named arguments
* @param {string} value Name or URL of the attachment
* @returns {Promise<string>} Content of the attachment
*/
async function getDataBankText(args, value) {
if (!value) {
toastr.warning('No attachment name or URL provided.');
return;
}
const attachments = getAttachments(args?.source);
const attachment = getAttachmentByField(attachments, value);
if (!attachment) {
toastr.warning('Attachment not found.');
return;
}
const content = await getFileAttachment(attachment.url);
return content;
}
/**
* Callback for adding an attachment to the data bank.
* @param {object} args Named arguments
* @param {string} value Content of the attachment
* @returns {Promise<string>} URL of the attachment
*/
async function uploadDataBankAttachment(args, value) {
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
const name = args?.name || new Date().toLocaleString();
const file = new File([value], name, { type: 'text/plain' });
const url = await uploadFileAttachmentToServer(file, source);
return url;
}
/**
* Callback for updating an attachment in the data bank.
* @param {object} args Named arguments
* @param {string} value Content of the attachment
* @returns {Promise<string>} URL of the attachment
*/
async function updateDataBankAttachment(args, value) {
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
const attachments = getAttachments(source);
const attachment = getAttachmentByFields(attachments, [args?.url, args?.name]);
if (!attachment) {
toastr.warning('Attachment not found.');
return '';
}
await deleteAttachment(attachment, source, () => { }, false);
const file = new File([value], attachment.name, { type: 'text/plain' });
const url = await uploadFileAttachmentToServer(file, source);
return url;
}
/**
* Callback for deleting an attachment from the data bank.
* @param {object} args Named arguments
* @param {string} value Name or URL of the attachment
* @returns {Promise<string>} Empty string
*/
async function deleteDataBankAttachment(args, value) {
const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
const attachments = getAttachments(source);
const attachment = getAttachmentByField(attachments, value);
if (!attachment) {
toastr.warning('Attachment not found.');
return '';
}
await deleteAttachment(attachment, source, () => { }, false);
return '';
}
/**
* Callback for disabling an attachment in the data bank.
* @param {object} args Named arguments
* @param {string} value Name or URL of the attachment
* @returns {Promise<string>} Empty string
*/
async function disableDataBankAttachment(args, value) {
const attachments = getAttachments(args?.source);
const attachment = getAttachmentByField(attachments, value);
if (!attachment) {
toastr.warning('Attachment not found.');
return '';
}
if (extension_settings.disabled_attachments.includes(attachment.url)) {
return '';
}
extension_settings.disabled_attachments.push(attachment.url);
return '';
}
/**
* Callback for enabling an attachment in the data bank.
* @param {object} args Named arguments
* @param {string} value Name or URL of the attachment
* @returns {Promise<string>} Empty string
*/
async function enableDataBankAttachment(args, value) {
const attachments = getAttachments(args?.source);
const attachment = getAttachmentByField(attachments, value);
if (!attachment) {
toastr.warning('Attachment not found.');
return '';
}
const index = extension_settings.disabled_attachments.indexOf(attachment.url);
if (index === -1) {
return '';
}
extension_settings.disabled_attachments.splice(index, 1);
return '';
}
jQuery(async () => {
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
$('#extensionsMenu').prepend(buttons);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'db',
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db',
callback: () => document.getElementById('manageAttachments')?.click(),
aliases: ['databank', 'data-bank'],
helpString: 'Open the data bank',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-list',
callback: listDataBankAttachments,
aliases: ['databank-list', 'data-bank-list'],
helpString: 'List attachments in the Data Bank as a JSON-serialized array. Optionally, provide the source of the attachments and the field to list by.',
namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source of the attachments.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
new SlashCommandNamedArgument('field', 'The field to list by.', ARGUMENT_TYPE.STRING, false, false, 'url', FIELDS),
],
returns: ARGUMENT_TYPE.LIST,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-get',
callback: getDataBankText,
aliases: ['databank-get', 'data-bank-get'],
helpString: 'Get attachment text from the Data Bank. Either provide the name or URL of the attachment. Optionally, provide the source of the attachment.',
namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
],
unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
],
returns: ARGUMENT_TYPE.STRING,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-add',
callback: uploadDataBankAttachment,
aliases: ['databank-add', 'data-bank-add'],
helpString: 'Add an attachment to the Data Bank. If name is not provided, it will be generated automatically. Returns the URL of the attachment.',
namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false),
],
unnamedArgumentList: [
new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
],
returns: ARGUMENT_TYPE.STRING,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-update',
callback: updateDataBankAttachment,
aliases: ['databank-update', 'data-bank-update'],
helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.',
namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false),
new SlashCommandNamedArgument('url', 'The URL of the attachment to update.', ARGUMENT_TYPE.STRING, false, false),
],
unnamedArgumentList: [
new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
],
returns: ARGUMENT_TYPE.STRING,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-disable',
callback: disableDataBankAttachment,
aliases: ['databank-disable', 'data-bank-disable'],
helpString: 'Disable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
],
unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-enable',
callback: enableDataBankAttachment,
aliases: ['databank-enable', 'data-bank-enable'],
helpString: 'Enable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
],
unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-delete',
callback: deleteDataBankAttachment,
aliases: ['databank-delete', 'data-bank-delete'],
helpString: 'Delete an attachment from the Data Bank.',
namedArgumentList: [
new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
],
unnamedArgumentList: [
new SlashCommandArgument('The name or URL of the attachment.', ARGUMENT_TYPE.STRING, true, false),
],
}));
});

View File

@ -1,4 +1,4 @@
<div class="wide100p padding5">
<div class="wide100p padding5 dataBankAttachments">
<h2 class="marginBot5">
<span data-i18n="Data Bank">
Data Bank
@ -37,7 +37,27 @@
Size (Largest First)
</option>
</select>
<label class="margin0 menu_button menu_button_icon attachmentsBulkEditButton">
<i class="fa-solid fa-edit"></i>
<span data-i18n="Bulk Edit">Bulk Edit</span>
<input type="checkbox" class="displayNone attachmentsBulkEditCheckbox" hidden>
</label>
</div>
<div class="attachmentBulkActionsContainer flex-container marginTopBot5 alignItemsBaseline">
<div class="flex-container">
<div class="menu_button menu_button_icon bulkActionSelectAll" title="Select all *visible* attachments">
<i class="fa-solid fa-check-square"></i>
<span data-i18n="Select All">Select All</span>
</div>
<div class="menu_button menu_button_icon bulkActionSelectNone" title="Deselect all *visible* attachments">
<i class="fa-solid fa-square"></i>
<span data-i18n="Select None">Select None</span>
</div>
<div class="menu_button menu_button_icon bulkActionDelete" title="Delete selected attachments">
<i class="fa-solid fa-trash"></i>
<span data-i18n="Delete">Delete</span>
</div>
</div>
</div>
<div class="justifyLeft globalAttachmentsBlock marginBot10">
<h3 class="globalAttachmentsTitle margin0 title_restorable">
@ -102,6 +122,7 @@
<div class="attachmentListItemTemplate template_element">
<div class="attachmentListItem flex-container alignItemsCenter flexGap10">
<div class="attachmentListItemCheckboxContainer"><input type="checkbox" class="attachmentListItemCheckbox"></div>
<div class="attachmentFileIcon fa-solid fa-file-alt"></div>
<div class="attachmentListItemName flex1"></div>
<small class="attachmentListItemCreated"></small>

View File

@ -37,3 +37,27 @@
.attachmentListItemCreated {
text-align: right;
}
.attachmentListItemCheckboxContainer,
.attachmentBulkActionsContainer,
.attachmentsBulkEditCheckbox {
display: none;
}
@supports selector(:has(*)) {
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentsBulkEditButton {
color: var(--golden);
}
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentBulkActionsContainer {
display: flex;
}
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentListItemCheckboxContainer {
display: inline-flex;
}
.dataBankAttachments:has(.attachmentsBulkEditCheckbox:checked) .attachmentFileIcon {
display: none;
}
}

View File

@ -348,8 +348,8 @@ jQuery(function () {
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && (secret_state[SECRET_KEYS.OPENAI] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openrouter' && secret_state[SECRET_KEYS.OPENROUTER]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && secret_state[SECRET_KEYS.MAKERSUITE]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && secret_state[SECRET_KEYS.CLAUDE]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'google' && (secret_state[SECRET_KEYS.MAKERSUITE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'anthropic' && (secret_state[SECRET_KEYS.CLAUDE] || extension_settings.caption.allow_reverse_proxy)) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ollama' && textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'llamacpp' && textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) ||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'ooba' && textgenerationwebui_settings.server_urls[textgen_types.OOBA]) ||
@ -465,7 +465,7 @@ jQuery(function () {
<option data-type="custom" value="custom_current">[Currently selected]</option>
</select>
</div>
<label data-type="openai,anthropic" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
<label data-type="openai,anthropic,google" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
Allow reverse proxy
</label>

View File

@ -1,14 +1,15 @@
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { callPopup, eventSource, event_types, generateQuietPrompt, getRequestHeaders, online_status, saveSettingsDebounced, substituteParams } from '../../../script.js';
import { dragElement, isMobile } from '../../RossAscends-mods.js';
import { getContext, getApiUrl, modules, extension_settings, ModuleWorkerWrapper, doExtrasFetch, renderExtensionTemplateAsync } from '../../extensions.js';
import { loadMovingUIState, power_user } from '../../power-user.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence } from '../../utils.js';
import { onlyUnique, debounce, getCharaFilename, trimToEndSentence, trimToStartSentence, waitUntilCondition } from '../../utils.js';
import { hideMutedSprites } from '../../group-chats.js';
import { isJsonSchemaSupported } from '../../textgen-settings.js';
import { debounce_timeout } from '../../constants.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument } from '../../slash-commands/SlashCommandArgument.js';
import { isFunctionCallingSupported } from '../../openai.js';
export { MODULE_NAME };
const MODULE_NAME = 'expressions';
@ -16,6 +17,7 @@ const UPDATE_INTERVAL = 2000;
const STREAMING_UPDATE_INTERVAL = 10000;
const TALKINGCHECK_UPDATE_INTERVAL = 500;
const DEFAULT_FALLBACK_EXPRESSION = 'joy';
const FUNCTION_NAME = 'set_emotion';
const DEFAULT_LLM_PROMPT = 'Pause your roleplay. Classify the emotion of the last message. Output just one word, e.g. "joy" or "anger". Choose only one of the following labels: {{labels}}';
const DEFAULT_EXPRESSIONS = [
'talkinghead',
@ -1001,6 +1003,10 @@ async function getLlmPrompt(labels) {
return '';
}
if (isFunctionCallingSupported()) {
return '';
}
const labelsString = labels.map(x => `"${x}"`).join(', ');
const prompt = substituteParams(String(extension_settings.expressions.llmPrompt))
.replace(/{{labels}}/gi, labelsString);
@ -1014,11 +1020,16 @@ async function getLlmPrompt(labels) {
* @returns {string} The parsed emotion or the fallback expression.
*/
function parseLlmResponse(emotionResponse, labels) {
const fallbackExpression = getFallbackExpression();
try {
const parsedEmotion = JSON.parse(emotionResponse);
return parsedEmotion?.emotion ?? fallbackExpression;
const response = parsedEmotion?.emotion?.trim()?.toLowerCase();
if (!response || !labels.includes(response)) {
console.debug(`Parsed emotion response: ${response} not in labels: ${labels}`);
throw new Error('Emotion not in labels');
}
return response;
} catch {
const fuse = new Fuse(labels, { includeScore: true });
console.debug('Using fuzzy search in labels:', labels);
@ -1032,6 +1043,41 @@ function parseLlmResponse(emotionResponse, labels) {
throw new Error('Could not parse emotion response ' + emotionResponse);
}
/**
* Registers the function tool for the LLM API.
* @param {FunctionToolRegister} args Function tool register arguments.
*/
function onFunctionToolRegister(args) {
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isFunctionCallingSupported()) {
// Only trigger on quiet mode
if (args.type !== 'quiet') {
return;
}
const emotions = DEFAULT_EXPRESSIONS.filter((e) => e != 'talkinghead');
const jsonSchema = {
$schema: 'http://json-schema.org/draft-04/schema#',
type: 'object',
properties: {
emotion: {
type: 'string',
enum: emotions,
description: `One of the following: ${JSON.stringify(emotions)}`,
},
},
required: [
'emotion',
],
};
args.registerFunctionTool(
FUNCTION_NAME,
substituteParams('Sets the label that best describes the current emotional state of {{char}}. Only select one of the enumerated values.'),
jsonSchema,
true,
);
}
}
function onTextGenSettingsReady(args) {
// Only call if inside an API call
if (inApiCall && extension_settings.expressions.api === EXPRESSION_API.llm && isJsonSchemaSupported()) {
@ -1087,11 +1133,27 @@ async function getExpressionLabel(text) {
} break;
// Using LLM
case EXPRESSION_API.llm: {
try {
await waitUntilCondition(() => online_status !== 'no_connection', 3000, 250);
} catch (error) {
console.warn('No LLM connection. Using fallback expression', error);
return getFallbackExpression();
}
const expressionsList = await getExpressionsList();
const prompt = await getLlmPrompt(expressionsList);
let functionResult = null;
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
eventSource.once(event_types.LLM_FUNCTION_TOOL_CALL, (/** @type {FunctionToolCall} */ args) => {
if (args.name !== FUNCTION_NAME) {
return;
}
functionResult = args?.arguments;
});
const emotionResponse = await generateQuietPrompt(prompt, false, false);
return parseLlmResponse(emotionResponse, expressionsList);
return parseLlmResponse(functionResult || emotionResponse, expressionsList);
}
// Extras
default: {

View File

@ -34,7 +34,7 @@
<i class="fa-solid fa-clock-rotate-left fa-sm"></i>
</div>
</label>
<small>Will be used if the API doesn't support JSON schemas.</small>
<small>Will be used if the API doesn't support JSON schemas or function calling.</small>
<textarea id="expression_llm_prompt" type="text" class="text_pole textarea_compact" rows="2" placeholder="Use &lcub;&lcub;labels&rcub;&rcub; special macro."></textarea>
</div>
<div class="expression_fallback_block m-b-1 m-t-1">

View File

@ -926,5 +926,6 @@ jQuery(async function () {
new SlashCommandArgument('text to summarize', [ARGUMENT_TYPE.STRING], false, false, ''),
],
helpString: 'Summarizes the given text. If no text is provided, the current chat will be summarized. Can specify the source and the prompt to use.',
returns: ARGUMENT_TYPE.STRING,
}));
});

View File

@ -1,24 +1,52 @@
<div class="regex_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Regex</b>
<b data-i18n="ext_regex_title">
Regex
</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">
<div class="flex-container">
<div id="open_regex_editor" class="menu_button">
<div id="open_regex_editor" class="menu_button menu_button_icon" title="New global regex script">
<i class="fa-solid fa-pen-to-square"></i>
<span data-i18n="ext_regex_open_editor">Open Editor</span>
<small data-i18n="ext_regex_new_global_script">+ Global</small>
</div>
<div id="import_regex" class="menu_button">
<div id="open_scoped_editor" class="menu_button menu_button_icon" title="New scoped regex script">
<i class="fa-solid fa-address-card"></i>
<small data-i18n="ext_regex_new_scoped_script">+ Scoped</small>
</div>
<div id="import_regex" class="menu_button menu_button_icon">
<i class="fa-solid fa-file-import"></i>
<span data-i18n="ext_regex_import_script">Import Script</span>
<small data-i18n="ext_regex_import_script">Import</small>
</div>
<input type="file" id="import_regex_file" hidden accept="*.json" multiple />
</div>
<hr />
<label data-i18n="ext_regex_saved_scripts">Saved Scripts</label>
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
<div id="global_scripts_block" class="padding5">
<div>
<strong data-i18n="ext_regex_global_scripts">Global Scripts</strong>
</div>
<small data-i18n="ext_regex_global_scripts_desc">
Available for all characters. Saved to local settings.
</small>
<div id="saved_regex_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
</div>
<hr />
<div id="scoped_scripts_block" class="padding5">
<div class="flex-container alignItemsBaseline">
<strong class="flex1" data-i18n="ext_regex_scoped_scripts">Scoped Scripts</strong>
<label id="toggle_scoped_regex" class="checkbox flex-container" for="regex_scoped_toggle">
<input type="checkbox" id="regex_scoped_toggle" class="enable_scoped" />
<span class="regex-toggle-on fa-solid fa-toggle-on fa-lg" title="Disallow using scoped regex"></span>
<span class="regex-toggle-off fa-solid fa-toggle-off fa-lg" title="Allow using scoped regex"></span>
</label>
</div>
<small data-i18n="ext_regex_scoped_scripts_desc">
Only available for this character. Saved to the card data.
</small>
<div id="saved_scoped_scripts" class="flex-container regex-script-container flexFlowColumn"></div>
</div>
</div>
</div>
</div>

View File

@ -56,7 +56,7 @@
<div>
<textarea
class="regex_replace_string text_pole wide100p textarea_compact"
data-i18n="[placeholder]ext_regex_replace_string_placeholder"
data-i18n="[placeholder]ext_regex_replace_string_placeholder"
placeholder="Use {{match}} to include the matched text from the Find Regex or $1, $2, etc. for capture groups."
rows="2"
></textarea>

View File

@ -0,0 +1,5 @@
<div>
<h3>This character has embedded regex script(s).</h3>
<h3>Would you like to allow using them?</h3>
<div class="m-b-1">If you want to do it later, select "Regex" from the extensions menu.</div>
</div>

View File

@ -1,4 +1,4 @@
import { substituteParams } from '../../../script.js';
import { characters, substituteParams, this_chid } from '../../../script.js';
import { extension_settings } from '../../extensions.js';
import { regexFromString } from '../../utils.js';
export {
@ -22,6 +22,22 @@ const regex_placement = {
WORLD_INFO: 5,
};
function getScopedRegex() {
const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
if (!isAllowed) {
return [];
}
const scripts = characters[this_chid]?.data?.extensions?.regex_scripts;
if (!Array.isArray(scripts)) {
return [];
}
return scripts;
}
/**
* Parent function to fetch a regexed version of a raw string
* @param {string} rawString The raw string to be regexed
@ -42,7 +58,8 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
return finalString;
}
extension_settings.regex.forEach((script) => {
const allRegex = [...(extension_settings.regex ?? []), ...(getScopedRegex() ?? [])];
allRegex.forEach((script) => {
if (
// Script applies to Markdown and input is Markdown
(script.markdownOnly && isMarkdown) ||
@ -95,7 +112,7 @@ function runRegexScript(regexScript, rawString, { characterOverride } = {}) {
}
// Run replacement. Currently does not support the Overlay strategy
newString = rawString.replace(findRegex, function(match) {
newString = rawString.replace(findRegex, function (match) {
const args = [...arguments];
const replaceString = regexScript.replaceString.replace(/{{match}}/gi, '$0');
const replaceWithGroups = replaceString.replaceAll(/\$(\d+)/g, (_, num) => {

View File

@ -0,0 +1,19 @@
<div>
<h3 data-i18n="ext_regex_import_target">
Import To:
</h3>
<div class="flex-container flexFlowColumn wide100p padding10 justifyLeft">
<label for="regex_import_target_global">
<input type="radio" name="regex_import_target" id="regex_import_target_global" value="global" checked />
<span data-i18n="ext_regex_global_scripts">
Global Scripts
</span>
</label>
<label for="regex_import_target_scoped">
<input type="radio" name="regex_import_target" id="regex_import_target_scoped" value="scoped" />
<span data-i18n="ext_regex_scoped_scripts">
Scoped Scripts
</span>
</label>
</div>
</div>

View File

@ -1,5 +1,6 @@
import { callPopup, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
import { callPopup, characters, eventSource, event_types, getCurrentChatId, reloadCurrentChat, saveSettingsDebounced, this_chid } from '../../../script.js';
import { extension_settings, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
import { selected_group } from '../../group-chats.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@ -7,8 +8,21 @@ import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js'
import { resolveVariable } from '../../variables.js';
import { regex_placement, runRegexScript } from './engine.js';
async function saveRegexScript(regexScript, existingScriptIndex) {
/**
* Saves a regex script to the extension settings or character data.
* @param {import('../../char-data.js').RegexScriptData} regexScript
* @param {number} existingScriptIndex Index of the existing script
* @param {boolean} isScoped Is the script scoped to a character?
* @returns {Promise<void>}
*/
async function saveRegexScript(regexScript, existingScriptIndex, isScoped) {
// If not editing
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
// Assign a UUID if it doesn't exist
if (!regexScript.id) {
regexScript.id = uuidv4();
}
// Is the script name undefined or empty?
if (!regexScript.scriptName) {
@ -16,22 +30,6 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
return;
}
if (existingScriptIndex === -1) {
// Does the script name already exist?
if (extension_settings.regex.find((e) => e.scriptName === regexScript.scriptName)) {
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
return;
}
} else {
// Does the script name already exist somewhere else?
// (If this fails, make it a .filter().map() to index array)
const foundIndex = extension_settings.regex.findIndex((e) => e.scriptName === regexScript.scriptName);
if (foundIndex !== existingScriptIndex && foundIndex !== -1) {
toastr.error(`Could not save regex script: A script with name ${regexScript.scriptName} already exists.`);
return;
}
}
// Is a find regex present?
if (regexScript.findRegex.length === 0) {
toastr.warning('This regex script will not work, but was saved anyway: A find regex isn\'t present.');
@ -43,9 +41,18 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
}
if (existingScriptIndex !== -1) {
extension_settings.regex[existingScriptIndex] = regexScript;
array[existingScriptIndex] = regexScript;
} else {
extension_settings.regex.push(regexScript);
array.push(regexScript);
}
if (isScoped) {
await writeExtensionField(this_chid, 'regex_scripts', array);
// Add the character to the allowed list
if (!extension_settings.character_allowed_regex.includes(characters[this_chid].avatar)) {
extension_settings.character_allowed_regex.push(characters[this_chid].avatar);
}
}
saveSettingsDebounced();
@ -58,12 +65,16 @@ async function saveRegexScript(regexScript, existingScriptIndex) {
}
}
async function deleteRegexScript({ existingId }) {
let scriptName = $(`#${existingId}`).find('.regex_script_name').text();
async function deleteRegexScript({ id, isScoped }) {
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
const existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === scriptName);
const existingScriptIndex = array.findIndex((script) => script.id === id);
if (!existingScriptIndex || existingScriptIndex !== -1) {
extension_settings.regex.splice(existingScriptIndex, 1);
array.splice(existingScriptIndex, 1);
if (isScoped) {
await writeExtensionField(this_chid, 'regex_scripts', array);
}
saveSettingsDebounced();
await loadRegexScripts();
@ -72,19 +83,32 @@ async function deleteRegexScript({ existingId }) {
async function loadRegexScripts() {
$('#saved_regex_scripts').empty();
$('#saved_scoped_scripts').empty();
const scriptTemplate = $(await renderExtensionTemplateAsync('regex', 'scriptTemplate'));
extension_settings.regex.forEach((script) => {
/**
* Renders a script to the UI.
* @param {string} container Container to render the script to
* @param {import('../../char-data.js').RegexScriptData} script Script data
* @param {boolean} isScoped Script is scoped to a character
* @param {number} index Index of the script in the array
*/
function renderScript(container, script, isScoped, index) {
// Have to clone here
const scriptHtml = scriptTemplate.clone();
scriptHtml.attr('id', uuidv4());
const save = () => saveRegexScript(script, index, isScoped);
if (!script.id) {
script.id = uuidv4();
}
scriptHtml.attr('id', script.id);
scriptHtml.find('.regex_script_name').text(script.scriptName);
scriptHtml.find('.disable_regex').prop('checked', script.disabled ?? false)
.on('input', function () {
.on('input', async function () {
script.disabled = !!$(this).prop('checked');
reloadCurrentChat();
saveSettingsDebounced();
await save();
});
scriptHtml.find('.regex-toggle-on').on('click', function () {
scriptHtml.find('.disable_regex').prop('checked', true).trigger('input');
@ -93,7 +117,37 @@ async function loadRegexScripts() {
scriptHtml.find('.disable_regex').prop('checked', false).trigger('input');
});
scriptHtml.find('.edit_existing_regex').on('click', async function () {
await onRegexEditorOpenClick(scriptHtml.attr('id'));
await onRegexEditorOpenClick(scriptHtml.attr('id'), isScoped);
});
scriptHtml.find('.move_to_global').on('click', async function () {
const confirm = await callPopup('Are you sure you want to move this regex script to global?', 'confirm');
if (!confirm) {
return;
}
await deleteRegexScript({ id: script.id, isScoped: true });
await saveRegexScript(script, -1, false);
});
scriptHtml.find('.move_to_scoped').on('click', async function () {
if (this_chid === undefined) {
toastr.error('No character selected.');
return;
}
if (selected_group) {
toastr.error('Cannot edit scoped scripts in group chats.');
return;
}
const confirm = await callPopup('Are you sure you want to move this regex script to scoped?', 'confirm');
if (!confirm) {
return;
}
await deleteRegexScript({ id: script.id, isScoped: false });
await saveRegexScript(script, -1, true);
});
scriptHtml.find('.export_regex').on('click', async function () {
const fileName = `${script.scriptName.replace(/[\s.<>:"/\\|?*\x00-\x1F\x7F]/g, '_').toLowerCase()}.json`;
@ -107,23 +161,36 @@ async function loadRegexScripts() {
return;
}
await deleteRegexScript({ existingId: scriptHtml.attr('id') });
await deleteRegexScript({ id: script.id, isScoped });
await reloadCurrentChat();
});
$('#saved_regex_scripts').append(scriptHtml);
});
$(container).append(scriptHtml);
}
extension_settings?.regex?.forEach((script, index, array) => renderScript('#saved_regex_scripts', script, false, index, array));
characters[this_chid]?.data?.extensions?.regex_scripts?.forEach((script, index, array) => renderScript('#saved_scoped_scripts', script, true, index, array));
const isAllowed = extension_settings?.character_allowed_regex?.includes(characters?.[this_chid]?.avatar);
$('#regex_scoped_toggle').prop('checked', isAllowed);
}
async function onRegexEditorOpenClick(existingId) {
/**
* Opens the regex editor.
* @param {string|boolean} existingId Existing ID
* @param {boolean} isScoped Is the script scoped to a character?
* @returns {Promise<void>}
*/
async function onRegexEditorOpenClick(existingId, isScoped) {
const editorHtml = $(await renderExtensionTemplateAsync('regex', 'editor'));
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
// If an ID exists, fill in all the values
let existingScriptIndex = -1;
if (existingId) {
const existingScriptName = $(`#${existingId}`).find('.regex_script_name').text();
existingScriptIndex = extension_settings.regex.findIndex((script) => script.scriptName === existingScriptName);
existingScriptIndex = array.findIndex((script) => script.id === existingId);
if (existingScriptIndex !== -1) {
const existingScript = extension_settings.regex[existingScriptIndex];
const existingScript = array[existingScriptIndex];
if (existingScript.scriptName) {
editorHtml.find('.regex_script_name').val(existingScript.scriptName);
} else {
@ -173,6 +240,7 @@ async function onRegexEditorOpenClick(existingId) {
}
const testScript = {
id: uuidv4(),
scriptName: editorHtml.find('.regex_script_name').val(),
findRegex: editorHtml.find('.find_regex').val(),
replaceString: editorHtml.find('.regex_replace_string').val(),
@ -189,9 +257,10 @@ async function onRegexEditorOpenClick(existingId) {
const popupResult = await callPopup(editorHtml, 'confirm', undefined, { okButton: 'Save' });
if (popupResult) {
const newRegexScript = {
scriptName: editorHtml.find('.regex_script_name').val(),
findRegex: editorHtml.find('.find_regex').val(),
replaceString: editorHtml.find('.regex_replace_string').val(),
id: existingId ? String(existingId) : uuidv4(),
scriptName: String(editorHtml.find('.regex_script_name').val()),
findRegex: String(editorHtml.find('.find_regex').val()),
replaceString: String(editorHtml.find('.regex_replace_string').val()),
trimStrings: editorHtml.find('.regex_trim_strings').val().split('\n').filter((e) => e.length !== 0) || [],
placement:
editorHtml
@ -209,7 +278,7 @@ async function onRegexEditorOpenClick(existingId) {
maxDepth: parseInt(String(editorHtml.find('input[name="max_depth"]').val())),
};
saveRegexScript(newRegexScript, existingScriptIndex);
saveRegexScript(newRegexScript, existingScriptIndex, isScoped);
}
}
@ -220,6 +289,11 @@ function migrateSettings() {
// Current: If MD Display is present in placement, remove it and add new placements/MD option
extension_settings.regex.forEach((script) => {
if (!script.id) {
script.id = uuidv4();
performSave = true;
}
if (script.placement.includes(regex_placement.MD_DISPLAY)) {
script.placement = script.placement.length === 1 ?
Object.values(regex_placement).filter((e) => e !== regex_placement.MD_DISPLAY) :
@ -242,6 +316,11 @@ function migrateSettings() {
}
});
if (!extension_settings.character_allowed_regex) {
extension_settings.character_allowed_regex = [];
performSave = true;
}
if (performSave) {
saveSettingsDebounced();
}
@ -260,8 +339,9 @@ function runRegexCallback(args, value) {
}
const scriptName = String(resolveVariable(args.name));
const scripts = [...(extension_settings.regex ?? []), ...(characters[this_chid]?.data?.extensions?.regex_scripts ?? [])];
for (const script of extension_settings.regex) {
for (const script of scripts) {
if (String(script.scriptName).toLowerCase() === String(scriptName).toLowerCase()) {
if (script.disabled) {
toastr.warning(`Regex script "${scriptName}" is disabled.`);
@ -280,8 +360,9 @@ function runRegexCallback(args, value) {
/**
* Performs the import of the regex file.
* @param {File} file Input file
* @param {boolean} isScoped Is the script scoped to a character?
*/
async function onRegexImportFileChange(file) {
async function onRegexImportFileChange(file, isScoped) {
if (!file) {
toastr.error('No file provided.');
return;
@ -294,7 +375,15 @@ async function onRegexImportFileChange(file) {
throw new Error('No script name provided.');
}
extension_settings.regex.push(regexScript);
// Assign a new UUID
regexScript.id = uuidv4();
const array = (isScoped ? characters[this_chid]?.data?.extensions?.regex_scripts : extension_settings.regex) ?? [];
array.push(regexScript);
if (isScoped) {
await writeExtensionField(this_chid, 'regex_scripts', array);
}
saveSettingsDebounced();
await loadRegexScripts();
@ -306,6 +395,47 @@ async function onRegexImportFileChange(file) {
}
}
function purgeEmbeddedRegexScripts( { character }){
const avatar = character?.avatar;
if (avatar && extension_settings.character_allowed_regex?.includes(avatar)) {
const index = extension_settings.character_allowed_regex.indexOf(avatar);
if (index !== -1) {
extension_settings.character_allowed_regex.splice(index, 1);
saveSettingsDebounced();
}
}
}
async function checkEmbeddedRegexScripts() {
const chid = this_chid;
if (chid !== undefined && !selected_group) {
const avatar = characters[chid]?.avatar;
const scripts = characters[chid]?.data?.extensions?.regex_scripts;
if (Array.isArray(scripts) && scripts.length > 0) {
if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
const checkKey = `AlertRegex_${characters[chid].avatar}`;
if (!localStorage.getItem(checkKey)) {
localStorage.setItem(checkKey, 'true');
const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
const result = await callPopup(template, 'confirm', '', { okButton: 'Yes' });
if (result) {
extension_settings.character_allowed_regex.push(avatar);
await reloadCurrentChat();
saveSettingsDebounced();
}
}
}
}
}
loadRegexScripts();
}
// Workaround for loading in sequence with other extensions
// NOTE: Always puts extension at the top of the list, but this is fine since it's static
jQuery(async () => {
@ -321,12 +451,32 @@ jQuery(async () => {
const settingsHtml = $(await renderExtensionTemplateAsync('regex', 'dropdown'));
$('#extensions_settings2').append(settingsHtml);
$('#open_regex_editor').on('click', function () {
onRegexEditorOpenClick(false);
onRegexEditorOpenClick(false, false);
});
$('#open_scoped_editor').on('click', function () {
if (this_chid === undefined) {
toastr.error('No character selected.');
return;
}
if (selected_group) {
toastr.error('Cannot edit scoped scripts in group chats.');
return;
}
onRegexEditorOpenClick(false, true);
});
$('#import_regex_file').on('change', async function () {
let target = 'global';
const template = $(await renderExtensionTemplateAsync('regex', 'importTarget'));
template.find('#regex_import_target_global').on('input', () => target = 'global');
template.find('#regex_import_target_scoped').on('input', () => target = 'scoped');
await callPopup(template, 'text');
const inputElement = this instanceof HTMLInputElement && this;
for (const file of inputElement.files) {
await onRegexImportFileChange(file);
await onRegexImportFileChange(file, target === 'scoped');
}
inputElement.value = '';
});
@ -334,30 +484,75 @@ jQuery(async () => {
$('#import_regex_file').trigger('click');
});
$('#saved_regex_scripts').sortable({
delay: getSortableDelay(),
stop: function () {
let newScripts = [];
$('#saved_regex_scripts').children().each(function () {
const scriptName = $(this).find('.regex_script_name').text();
const existingScript = extension_settings.regex.find((e) => e.scriptName === scriptName);
if (existingScript) {
newScripts.push(existingScript);
}
});
extension_settings.regex = newScripts;
saveSettingsDebounced();
console.debug('Regex scripts reordered');
// TODO: Maybe reload regex scripts after move
let sortableDatas = [
{
selector: '#saved_regex_scripts',
setter: x => extension_settings.regex = x,
getter: () => extension_settings.regex ?? [],
},
{
selector: '#saved_scoped_scripts',
setter: x => writeExtensionField(this_chid, 'regex_scripts', x),
getter: () => characters[this_chid]?.data?.extensions?.regex_scripts ?? [],
},
];
for (const { selector, setter, getter } of sortableDatas) {
$(selector).sortable({
delay: getSortableDelay(),
stop: async function () {
const oldScripts = getter();
const newScripts = [];
$(selector).children().each(function () {
const id = $(this).attr('id');
const existingScript = oldScripts.find((e) => e.id === id);
if (existingScript) {
newScripts.push(existingScript);
}
});
await setter(newScripts);
saveSettingsDebounced();
console.debug(`Regex scripts in ${selector} reordered`);
await loadRegexScripts();
},
});
}
$('#regex_scoped_toggle').on('input', function () {
if (this_chid === undefined) {
toastr.error('No character selected.');
return;
}
if (selected_group) {
toastr.error('Cannot edit scoped scripts in group chats.');
return;
}
const isEnable = !!$(this).prop('checked');
const avatar = characters[this_chid].avatar;
if (isEnable) {
if (!extension_settings.character_allowed_regex.includes(avatar)) {
extension_settings.character_allowed_regex.push(avatar);
}
} else {
const index = extension_settings.character_allowed_regex.indexOf(avatar);
if (index !== -1) {
extension_settings.character_allowed_regex.splice(index, 1);
}
}
saveSettingsDebounced();
reloadCurrentChat();
});
await loadRegexScripts();
$('#saved_regex_scripts').sortable('enable');
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'regex',
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'regex',
callback: runRegexCallback,
returns: 'replaced text',
namedArgumentList: [
@ -373,4 +568,6 @@ jQuery(async () => {
helpString: 'Runs a Regex extension script by name on the provided string. The script must be enabled.',
}));
eventSource.on(event_types.CHAT_CHANGED, checkEmbeddedRegexScripts);
eventSource.on(event_types.CHARACTER_DELETED, purgeEmbeddedRegexScripts);
});

View File

@ -10,6 +10,12 @@
<div class="edit_existing_regex menu_button" data-i18n="[title]ext_regex_edit_script" title="Edit script">
<i class="fa-solid fa-pencil"></i>
</div>
<div class="move_to_global menu_button" data-i18n="[title]ext_regex_move_to_global" title="Move to global scripts">
<i class="fa-solid fa-arrow-up"></i>
</div>
<div class="move_to_scoped menu_button" data-i18n="[title]ext_regex_move_to_scoped" title="Move to scoped scripts">
<i class="fa-solid fa-arrow-down"></i>
</div>
<div class="export_regex menu_button" data-i18n="[title]ext_regex_export_script" title="Export script">
<i class="fa-solid fa-file-export"></i>
</div>

View File

@ -14,6 +14,47 @@
margin-bottom: 10px;
}
.regex-script-container:empty::after {
content: "No scripts found";
font-size: 0.95em;
opacity: 0.7;
display: block;
text-align: center;
}
#scoped_scripts_block {
opacity: 1;
transition: opacity 0.2s ease-in-out;
}
#scoped_scripts_block .move_to_scoped {
display: none;
}
#global_scripts_block .move_to_global {
display: none;
}
#scoped_scripts_block:not(:has(#regex_scoped_toggle:checked)) {
opacity: 0.5;
}
.enable_scoped:checked ~ .regex-toggle-on {
display: block;
}
.enable_scoped:checked ~ .regex-toggle-off {
display: none;
}
.enable_scoped:not(:checked) ~ .regex-toggle-on {
display: none;
}
.enable_scoped:not(:checked) ~ .regex-toggle-off {
display: block;
}
.regex-script-label {
align-items: center;
border: 1px solid var(--SmartThemeBorderColor);
@ -23,7 +64,13 @@
margin-bottom: 1px;
}
input.disable_regex {
.regex-script-label:has(.disable_regex:checked) .regex_script_name {
text-decoration: line-through;
filter: grayscale(0.5);
}
input.disable_regex,
input.enable_scoped {
display: none !important;
}
@ -31,6 +78,12 @@ input.disable_regex {
cursor: pointer;
opacity: 0.5;
filter: grayscale(0.5);
transition: opacity 0.2s ease-in-out;
}
.regex-toggle-off:hover {
opacity: 1;
filter: none;
}
.regex-toggle-on {

View File

@ -12,7 +12,13 @@ import { createThumbnail, isValidUrl } from '../utils.js';
* @returns {Promise<string>} Generated caption
*/
export async function getMultimodalCaption(base64Img, prompt) {
throwIfInvalidModel();
const useReverseProxy =
(['openai', 'anthropic', 'google'].includes(extension_settings.caption.multimodal_api))
&& extension_settings.caption.allow_reverse_proxy
&& oai_settings.reverse_proxy
&& isValidUrl(oai_settings.reverse_proxy);
throwIfInvalidModel(useReverseProxy);
const noPrefix = ['google', 'ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api);
@ -39,27 +45,18 @@ export async function getMultimodalCaption(base64Img, prompt) {
}
}
const useReverseProxy =
(extension_settings.caption.multimodal_api === 'openai' || extension_settings.caption.multimodal_api === 'anthropic')
&& extension_settings.caption.allow_reverse_proxy
&& oai_settings.reverse_proxy
&& isValidUrl(oai_settings.reverse_proxy);
const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
const proxyPassword = useReverseProxy ? oai_settings.proxy_password : '';
const requestBody = {
image: base64Img,
prompt: prompt,
reverse_proxy: proxyUrl,
proxy_password: proxyPassword,
api: extension_settings.caption.multimodal_api || 'openai',
model: extension_settings.caption.multimodal_model || 'gpt-4-turbo',
};
if (!isGoogle) {
requestBody.api = extension_settings.caption.multimodal_api || 'openai';
requestBody.model = extension_settings.caption.multimodal_model || 'gpt-4-turbo';
requestBody.reverse_proxy = proxyUrl;
requestBody.proxy_password = proxyPassword;
}
if (isOllama) {
if (extension_settings.caption.multimodal_model === 'ollama_current') {
requestBody.model = textgenerationwebui_settings.ollama_model;
@ -117,8 +114,8 @@ export async function getMultimodalCaption(base64Img, prompt) {
return String(caption).trim();
}
function throwIfInvalidModel() {
if (extension_settings.caption.multimodal_api === 'openai' && !secret_state[SECRET_KEYS.OPENAI]) {
function throwIfInvalidModel(useReverseProxy) {
if (extension_settings.caption.multimodal_api === 'openai' && !secret_state[SECRET_KEYS.OPENAI] && !useReverseProxy) {
throw new Error('OpenAI API key is not set.');
}
@ -126,7 +123,11 @@ function throwIfInvalidModel() {
throw new Error('OpenRouter API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE]) {
if (extension_settings.caption.multimodal_api === 'anthropic' && !secret_state[SECRET_KEYS.CLAUDE] && !useReverseProxy) {
throw new Error('Anthropic (Claude) API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'google' && !secret_state[SECRET_KEYS.MAKERSUITE] && !useReverseProxy) {
throw new Error('MakerSuite API key is not set.');
}

View File

@ -0,0 +1,4 @@
<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>
Generate Image
</div>

View File

@ -17,6 +17,7 @@
<li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li>
<li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li>
<li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li>
<li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li>
<li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li>
<li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li>
<li data-placeholder="user_avatar" class="sd_comfy_workflow_editor_not_found">"%user_avatar%"</li>

View File

@ -0,0 +1,12 @@
<div id="sd_dropdown">
<ul class="list-group">
<span>Send me a picture of:</span>
<li class="list-group-item" id="sd_you" data-value="you" data-i18n="sd_Yourself">Yourself</li>
<li class="list-group-item" id="sd_face" data-value="face" data-i18n="sd_Your_Face">Your Face</li>
<li class="list-group-item" id="sd_me" data-value="me" data-i18n="sd_Me">Me</li>
<li class="list-group-item" id="sd_world" data-value="world" data-i18n="sd_The_Whole_Story">The Whole Story</li>
<li class="list-group-item" id="sd_last" data-value="last" data-i18n="sd_The_Last_Message">The Last Message</li>
<li class="list-group-item" id="sd_raw_last" data-value="raw_last" data-i18n="sd_Raw_Last_Message">Raw Last Message</li>
<li class="list-group-item" id="sd_background" data-value="background" data-i18n="sd_Background">Background</li>
</ul>
</div>

View File

@ -18,9 +18,9 @@ import {
formatCharacterAvatar,
substituteParams,
} from '../../../script.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplateAsync, writeExtensionField } from '../../extensions.js';
import { selected_group } from '../../group-chats.js';
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean } from '../../utils.js';
import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay, isTrueBoolean, debounce } from '../../utils.js';
import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from '../../nai-settings.js';
@ -29,6 +29,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
import { resolveVariable } from '../../variables.js';
import { debounce_timeout } from '../../constants.js';
export { MODULE_NAME };
const MODULE_NAME = 'sd';
@ -185,6 +186,7 @@ const defaultSettings = {
sampler: 'DDIM',
model: '',
vae: '',
seed: -1,
// Automatic1111/Horde exclusives
restore_faces: false,
@ -229,6 +231,12 @@ const defaultSettings = {
hr_second_pass_steps_max: 150,
hr_second_pass_steps_step: 1,
// CLIP skip
clip_skip_min: 1,
clip_skip_max: 12,
clip_skip_step: 1,
clip_skip: 1,
// NovelAI settings
novel_upscale_ratio_min: 1.0,
novel_upscale_ratio_max: 4.0,
@ -237,6 +245,7 @@ const defaultSettings = {
novel_anlas_guard: false,
novel_sm: false,
novel_sm_dyn: false,
novel_decrisper: false,
// OpenAI settings
openai_style: 'vivid',
@ -254,6 +263,8 @@ const defaultSettings = {
pollinations_refine: false,
};
const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
function processTriggers(chat, _, abort) {
if (!extension_settings.sd.interactive_mode) {
return;
@ -381,6 +392,7 @@ async function loadSettings() {
$('#sd_novel_sm').prop('checked', extension_settings.sd.novel_sm);
$('#sd_novel_sm_dyn').prop('checked', extension_settings.sd.novel_sm_dyn);
$('#sd_novel_sm_dyn').prop('disabled', !extension_settings.sd.novel_sm);
$('#sd_novel_decrisper').prop('checked', extension_settings.sd.novel_decrisper);
$('#sd_pollinations_enhance').prop('checked', extension_settings.sd.pollinations_enhance);
$('#sd_pollinations_refine').prop('checked', extension_settings.sd.pollinations_refine);
$('#sd_horde').prop('checked', extension_settings.sd.horde);
@ -404,6 +416,9 @@ async function loadSettings() {
$('#sd_comfy_url').val(extension_settings.sd.comfy_url);
$('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
$('#sd_snap').prop('checked', extension_settings.sd.snap);
$('#sd_clip_skip').val(extension_settings.sd.clip_skip);
$('#sd_clip_skip_value').text(extension_settings.sd.clip_skip);
$('#sd_seed').val(extension_settings.sd.seed);
for (const style of extension_settings.sd.styles) {
const option = document.createElement('option');
@ -465,7 +480,7 @@ function addPromptTemplates() {
const label = $('<label></label>')
.text(modeLabels[name])
.attr('for', `sd_prompt_${name}`)
.attr('data-i18n', `sd_prompt_${name}`);
.attr('data-i18n', `sd_prompt_${name}`);
const textarea = $('<textarea></textarea>')
.addClass('textarea_compact text_pole')
.attr('id', `sd_prompt_${name}`)
@ -477,7 +492,7 @@ function addPromptTemplates() {
const button = $('<button></button>')
.addClass('menu_button fa-solid fa-undo')
.attr('title', 'Restore default')
.attr('data-i18n', 'Restore default')
.attr('data-i18n', 'Restore default')
.on('click', () => {
textarea.val(promptTemplates[name]);
extension_settings.sd.prompts[name] = promptTemplates[name];
@ -522,6 +537,42 @@ function onStyleSelect() {
saveSettingsDebounced();
}
async function onDeleteStyleClick() {
const selectedStyle = String($('#sd_style').find(':selected').val());
const styleObject = extension_settings.sd.styles.find(x => x.name === selectedStyle);
if (!styleObject) {
return;
}
const confirmed = await callPopup(`Are you sure you want to delete the style "${selectedStyle}"?`, 'confirm', '', { okButton: 'Delete' });
if (!confirmed) {
return;
}
const index = extension_settings.sd.styles.indexOf(styleObject);
if (index === -1) {
return;
}
extension_settings.sd.styles.splice(index, 1);
$('#sd_style').find(`option[value="${selectedStyle}"]`).remove();
if (extension_settings.sd.styles.length > 0) {
extension_settings.sd.style = extension_settings.sd.styles[0].name;
$('#sd_style').val(extension_settings.sd.style).trigger('change');
} else {
extension_settings.sd.style = '';
$('#sd_prompt_prefix').val('').trigger('input');
$('#sd_negative_prompt').val('').trigger('input');
$('#sd_style').val('');
}
saveSettingsDebounced();
}
async function onSaveStyleClick() {
const userInput = await callPopup('Enter style name:', 'input', '', { okButton: 'Save' });
@ -611,9 +662,27 @@ function onChatChanged() {
}
$('#sd_character_prompt_block').show();
const key = getCharaFilename(this_chid);
$('#sd_character_prompt').val(key ? (extension_settings.sd.character_prompts[key] || '') : '');
$('#sd_character_negative_prompt').val(key ? (extension_settings.sd.character_negative_prompts[key] || '') : '');
let characterPrompt = key ? (extension_settings.sd.character_prompts[key] || '') : '';
let negativePrompt = key ? (extension_settings.sd.character_negative_prompts[key] || '') : '';
const context = getContext();
const sharedPromptData = context?.characters[this_chid]?.data?.extensions?.sd_character_prompt;
const hasSharedData = sharedPromptData && typeof sharedPromptData === 'object';
if (typeof sharedPromptData?.positive === 'string' && !characterPrompt && sharedPromptData.positive) {
characterPrompt = sharedPromptData.positive;
extension_settings.sd.character_prompts[key] = characterPrompt;
}
if (typeof sharedPromptData?.negative === 'string' && !negativePrompt && sharedPromptData.negative) {
negativePrompt = sharedPromptData.negative;
extension_settings.sd.character_negative_prompts[key] = negativePrompt;
}
$('#sd_character_prompt').val(characterPrompt);
$('#sd_character_negative_prompt').val(negativePrompt);
$('#sd_character_prompt_share').prop('checked', hasSharedData);
}
function onCharacterPromptInput() {
@ -621,6 +690,7 @@ function onCharacterPromptInput() {
extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();
resetScrollHeight($(this));
saveSettingsDebounced();
writePromptFieldsDebounced(this_chid);
}
function onCharacterNegativePromptInput() {
@ -628,6 +698,7 @@ function onCharacterNegativePromptInput() {
extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();
resetScrollHeight($(this));
saveSettingsDebounced();
writePromptFieldsDebounced(this_chid);
}
function getCharacterPrefix() {
@ -691,6 +762,17 @@ function onRefineModeInput() {
saveSettingsDebounced();
}
function onClipSkipInput() {
extension_settings.sd.clip_skip = Number($('#sd_clip_skip').val());
$('#sd_clip_skip_value').text(extension_settings.sd.clip_skip);
saveSettingsDebounced();
}
function onSeedInput() {
extension_settings.sd.seed = Number($('#sd_seed').val());
saveSettingsDebounced();
}
function onScaleInput() {
extension_settings.sd.scale = Number($('#sd_scale').val());
$('#sd_scale_value').text(extension_settings.sd.scale.toFixed(1));
@ -776,6 +858,7 @@ async function onSourceChange() {
extension_settings.sd.source = $('#sd_source').find(':selected').val();
extension_settings.sd.model = null;
extension_settings.sd.sampler = null;
extension_settings.sd.scheduler = null;
toggleSourceControls();
saveSettingsDebounced();
await loadSettingOptions();
@ -832,6 +915,11 @@ function onNovelSmDynInput() {
saveSettingsDebounced();
}
function onNovelDecrisperInput() {
extension_settings.sd.novel_decrisper = !!$('#sd_novel_decrisper').prop('checked');
saveSettingsDebounced();
}
function onPollinationsEnhanceInput() {
extension_settings.sd.pollinations_enhance = !!$('#sd_pollinations_enhance').prop('checked');
saveSettingsDebounced();
@ -1118,6 +1206,26 @@ async function getAutoRemoteUpscalers() {
}
}
async function getAutoRemoteSchedulers() {
try {
const result = await fetch('/api/sd/schedulers', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getSdRequestBody()),
});
if (!result.ok) {
throw new Error('SD WebUI returned an error.');
}
const data = await result.json();
return data;
} catch (error) {
console.error(error);
return ['N/A'];
}
}
async function getVladRemoteUpscalers() {
try {
const result = await fetch('/api/sd/sd-next/upscalers', {
@ -1138,6 +1246,27 @@ async function getVladRemoteUpscalers() {
}
}
async function getDrawthingsRemoteUpscalers() {
try {
const result = await fetch('/api/sd/drawthings/get-upscaler', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(getSdRequestBody()),
});
if (!result.ok) {
throw new Error('SD DrawThings API returned an error.');
}
const data = await result.text();
return data ? [data] : ['N/A'];
} catch (error) {
console.error(error);
return ['N/A'];
}
}
async function updateAutoRemoteModel() {
try {
const result = await fetch('/api/sd/set-model', {
@ -1572,6 +1701,21 @@ async function loadDrawthingsModels() {
const data = [{ value: currentModel, text: currentModel }];
const upscalers = await getDrawthingsRemoteUpscalers();
if (Array.isArray(upscalers) && upscalers.length > 0) {
$('#sd_hr_upscaler').empty();
for (const upscaler of upscalers) {
const option = document.createElement('option');
option.innerText = upscaler;
option.value = upscaler;
option.selected = upscaler === extension_settings.sd.hr_upscaler;
$('#sd_hr_upscaler').append(option);
}
}
return data;
} catch (error) {
console.log('Error loading DrawThings API models:', error);
@ -1697,7 +1841,7 @@ async function loadSchedulers() {
schedulers = ['N/A'];
break;
case sources.auto:
schedulers = ['N/A'];
schedulers = await getAutoRemoteSchedulers();
break;
case sources.novel:
schedulers = ['N/A'];
@ -1729,6 +1873,11 @@ async function loadSchedulers() {
option.selected = scheduler === extension_settings.sd.scheduler;
$('#sd_scheduler').append(option);
}
if (!extension_settings.sd.scheduler && schedulers.length > 0 && schedulers[0] !== 'N/A') {
extension_settings.sd.scheduler = schedulers[0];
$('#sd_scheduler').val(extension_settings.sd.scheduler).trigger('change');
}
}
async function loadComfySchedulers() {
@ -2121,6 +2270,7 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
}
try {
const toast = toastr.info('Generating multimodal caption...', 'Image Generation');
const response = await fetch(avatarUrl);
if (!response.ok) {
@ -2131,6 +2281,7 @@ async function generateMultimodalPrompt(generationType, quietPrompt) {
const avatarBase64 = await getBase64Async(avatarBlob);
const caption = await getMultimodalCaption(avatarBase64, quietPrompt);
toastr.clear(toast);
if (!caption) {
throw new Error('No caption returned from the API.');
@ -2269,6 +2420,7 @@ async function generateTogetherAIImage(prompt, negativePrompt) {
steps: extension_settings.sd.steps,
width: extension_settings.sd.width,
height: extension_settings.sd.height,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
}),
});
@ -2293,6 +2445,7 @@ async function generatePollinationsImage(prompt, negativePrompt) {
height: extension_settings.sd.height,
enhance: extension_settings.sd.pollinations_enhance,
refine: extension_settings.sd.pollinations_refine,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
}),
});
@ -2335,6 +2488,7 @@ async function generateExtrasImage(prompt, negativePrompt) {
hr_scale: extension_settings.sd.hr_scale,
denoising_strength: extension_settings.sd.denoising_strength,
hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
}),
});
@ -2371,6 +2525,8 @@ async function generateHordeImage(prompt, negativePrompt) {
restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr,
sanitize: !!extension_settings.sd.horde_sanitize,
clip_skip: extension_settings.sd.clip_skip,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
}),
});
@ -2399,6 +2555,7 @@ async function generateAutoImage(prompt, negativePrompt) {
prompt: prompt,
negative_prompt: negativePrompt,
sampler_name: extension_settings.sd.sampler,
scheduler: extension_settings.sd.scheduler,
steps: extension_settings.sd.steps,
cfg_scale: extension_settings.sd.scale,
width: extension_settings.sd.width,
@ -2409,6 +2566,14 @@ async function generateAutoImage(prompt, negativePrompt) {
hr_scale: extension_settings.sd.hr_scale,
denoising_strength: extension_settings.sd.denoising_strength,
hr_second_pass_steps: extension_settings.sd.hr_second_pass_steps,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
// For AUTO1111
override_settings: {
CLIP_stop_at_last_layers: extension_settings.sd.clip_skip,
},
override_settings_restore_afterwards: true,
// For SD.Next
clip_skip: extension_settings.sd.clip_skip,
// Ensure generated img is saved to disk
save_images: true,
send_images: true,
@ -2449,6 +2614,9 @@ async function generateDrawthingsImage(prompt, negativePrompt) {
restore_faces: !!extension_settings.sd.restore_faces,
enable_hr: !!extension_settings.sd.enable_hr,
denoising_strength: extension_settings.sd.denoising_strength,
clip_skip: extension_settings.sd.clip_skip,
upscaler_scale: extension_settings.sd.hr_scale,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
// TODO: advanced API parameters: hr, upscaler
}),
});
@ -2485,8 +2653,10 @@ async function generateNovelImage(prompt, negativePrompt) {
height: height,
negative_prompt: negativePrompt,
upscale_ratio: extension_settings.sd.novel_upscale_ratio,
decrisper: extension_settings.sd.novel_decrisper,
sm: sm,
sm_dyn: sm_dyn,
seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
}),
});
@ -2633,6 +2803,7 @@ async function generateComfyImage(prompt, negativePrompt) {
'scale',
'width',
'height',
'clip_skip',
];
const workflowResponse = await fetch('/api/sd/comfy/workflow', {
@ -2648,7 +2819,9 @@ async function generateComfyImage(prompt, negativePrompt) {
}
let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt));
workflow = workflow.replace('"%negative_prompt%"', JSON.stringify(negativePrompt));
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)));
const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed));
placeholders.forEach(ph => {
workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph]));
});
@ -2860,41 +3033,16 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
context.saveChat();
}
function addSDGenButtons() {
const buttonHtml = `
<div id="sd_gen" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-paintbrush extensionsMenuExtensionButton" title="Trigger Stable Diffusion" /></div>
Generate Image
</div>
`;
const waitButtonHtml = `
<div id="sd_gen_wait" class="fa-solid fa-hourglass-half" /></div>
`;
const dropdownHtml = `
<div id="sd_dropdown">
<ul class="list-group">
<span>Send me a picture of:</span>
<li class="list-group-item" id="sd_you" data-value="you">Yourself</li>
<li class="list-group-item" id="sd_face" data-value="face">Your Face</li>
<li class="list-group-item" id="sd_me" data-value="me">Me</li>
<li class="list-group-item" id="sd_world" data-value="world">The Whole Story</li>
<li class="list-group-item" id="sd_last" data-value="last">The Last Message</li>
<li class="list-group-item" id="sd_raw_last" data-value="raw_last">Raw Last Message</li>
<li class="list-group-item" id="sd_background" data-value="background">Background</li>
</ul>
</div>`;
async function addSDGenButtons() {
const buttonHtml = await renderExtensionTemplateAsync('stable-diffusion', 'button');
const dropdownHtml = await renderExtensionTemplateAsync('stable-diffusion', 'dropdown');
$('#extensionsMenu').prepend(buttonHtml);
$('#extensionsMenu').prepend(waitButtonHtml);
$(document.body).append(dropdownHtml);
const messageButton = $('.sd_message_gen');
const button = $('#sd_gen');
const waitButton = $('#sd_gen_wait');
const dropdown = $('#sd_dropdown');
waitButton.hide();
dropdown.hide();
button.hide();
messageButton.hide();
@ -2917,6 +3065,26 @@ function addSDGenButtons() {
dropdown.fadeOut(animation_duration);
}
});
$('#sd_dropdown [id]').on('click', function () {
const id = $(this).attr('id');
const idParamMap = {
'sd_you': 'you',
'sd_face': 'face',
'sd_me': 'me',
'sd_world': 'scene',
'sd_last': 'last',
'sd_raw_last': 'raw_last',
'sd_background': 'background',
};
const param = idParamMap[id];
if (param) {
console.log('doing /sd ' + param);
generatePicture('sd', param);
}
});
}
function isValidState() {
@ -2955,7 +3123,6 @@ async function moduleWorker() {
}
}
addSDGenButtons();
setInterval(moduleWorker, UPDATE_INTERVAL);
async function sdMessageButton(e) {
@ -3028,28 +3195,37 @@ async function sdMessageButton(e) {
}
}
$('#sd_dropdown [id]').on('click', function () {
const id = $(this).attr('id');
const idParamMap = {
'sd_you': 'you',
'sd_face': 'face',
'sd_me': 'me',
'sd_world': 'scene',
'sd_last': 'last',
'sd_raw_last': 'raw_last',
'sd_background': 'background',
};
const param = idParamMap[id];
if (param) {
console.log('doing /sd ' + param);
generatePicture('sd', param);
async function onCharacterPromptShareInput() {
// Not a valid state to share character prompt
if (this_chid === undefined || selected_group) {
return;
}
});
const shouldShare = !!$('#sd_character_prompt_share').prop('checked');
if (shouldShare) {
await writePromptFields(this_chid);
} else {
await writeExtensionField(this_chid, 'sd_character_prompt', null);
}
}
async function writePromptFields(characterId) {
const key = getCharaFilename(characterId);
const promptPrefix = key ? (extension_settings.sd.character_prompts[key] || '') : '';
const negativePromptPrefix = key ? (extension_settings.sd.character_negative_prompts[key] || '') : '';
const promptObject = {
positive: promptPrefix,
negative: negativePromptPrefix,
};
await writeExtensionField(characterId, 'sd_character_prompt', promptObject);
}
jQuery(async () => {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine',
await addSDGenButtons();
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine',
callback: generatePicture,
aliases: ['sd', 'img', 'image'],
namedArgumentList: [
@ -3075,7 +3251,8 @@ jQuery(async () => {
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'imagine-comfy-workflow',
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'imagine-comfy-workflow',
callback: changeComfyWorkflow,
aliases: ['icw'],
unnamedArgumentList: [
@ -3127,6 +3304,7 @@ jQuery(async () => {
$('#sd_novel_view_anlas').on('click', onViewAnlasClick);
$('#sd_novel_sm').on('input', onNovelSmInput);
$('#sd_novel_sm_dyn').on('input', onNovelSmDynInput);
$('#sd_novel_decrisper').on('input', onNovelDecrisperInput);
$('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);
$('#sd_pollinations_refine').on('input', onPollinationsRefineInput);
$('#sd_comfy_validate').on('click', validateComfyUrl);
@ -3138,12 +3316,16 @@ jQuery(async () => {
$('#sd_expand').on('input', onExpandInput);
$('#sd_style').on('change', onStyleSelect);
$('#sd_save_style').on('click', onSaveStyleClick);
$('#sd_delete_style').on('click', onDeleteStyleClick);
$('#sd_character_prompt_block').hide();
$('#sd_interactive_mode').on('input', onInteractiveModeInput);
$('#sd_openai_style').on('change', onOpenAiStyleSelect);
$('#sd_openai_quality').on('change', onOpenAiQualitySelect);
$('#sd_multimodal_captioning').on('input', onMultimodalCaptioningInput);
$('#sd_snap').on('input', onSnapInput);
$('#sd_clip_skip').on('input', onClipSkipInput);
$('#sd_seed').on('input', onSeedInput);
$('#sd_character_prompt_share').on('input', onCharacterPromptShareInput);
$('.sd_settings .inline-drawer-toggle').on('click', function () {
initScrollHeight($('#sd_prompt_prefix'));

View File

@ -182,23 +182,33 @@
</div>
<label for="sd_scale" data-i18n="CFG Scale">CFG Scale (<span id="sd_scale_value"></span>)</label>
<input id="sd_scale" type="range" min="{{scale_min}}" max="{{scale_max}}" step="{{scale_step}}" value="{{scale}}" />
<div data-sd-source="novel" class="marginTopBot5">
<label class="checkbox_label" for="sd_novel_decrisper" title="Reduce artifacts caused by high guidance values.">
<input id="sd_novel_decrisper" type="checkbox" />
<span data-i18n="Decrisper">
Decrisper
</span>
</label>
</div>
<label for="sd_steps" data-i18n="Sampling steps">Sampling steps (<span id="sd_steps_value"></span>)</label>
<input id="sd_steps" type="range" min="{{steps_min}}" max="{{steps_max}}" step="{{steps_step}}" value="{{steps}}" />
<label for="sd_width" data-i18n="Width">Width (<span id="sd_width_value"></span>)</label>
<input id="sd_width" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{width}}" />
<label for="sd_height" data-i18n="Height">Height (<span id="sd_height_value"></span>)</label>
<input id="sd_height" type="range" max="{{dimension_max}}" min="{{dimension_min}}" step="{{dimension_step}}" value="{{height}}" />
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
<select id="sd_resolution"><!-- Populated in JS --></select>
<label for="sd_model" data-i18n="Model">Model</label>
<select id="sd_model"></select>
<label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
<select id="sd_sampler"></select>
<label data-sd-source="horde" for="sd_horde_karras" class="checkbox_label">
<label data-sd-source="horde" for="sd_horde_karras" class="checkbox_label marginTopBot5">
<input id="sd_horde_karras" type="checkbox" />
<span data-i18n="Karras (not all samplers supported)">
Karras (not all samplers supported)
</span>
</label>
<div data-sd-source="novel" class="flex-container">
<div data-sd-source="novel" class="flex-container marginTopBot5">
<label class="flex1 checkbox_label" data-i18n="[title]SMEA versions of samplers are modified to perform better at high resolution." title="SMEA versions of samplers are modified to perform better at high resolution.">
<input id="sd_novel_sm" type="checkbox" />
<span data-i18n="SMEA">
@ -212,9 +222,7 @@
</span>
</label>
</div>
<label for="sd_resolution" data-i18n="Resolution">Resolution</label>
<select id="sd_resolution"><!-- Populated in JS --></select>
<div data-sd-source="comfy">
<div data-sd-source="comfy,auto">
<label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
<select id="sd_scheduler"></select>
</div>
@ -222,7 +230,7 @@
<label for="sd_vae">VAE</label>
<select id="sd_vae"></select>
</div>
<div class="flex-container marginTop10 margin-bot-10px">
<div class="flex-container marginTopBot5">
<label class="flex1 checkbox_label">
<input id="sd_restore_faces" type="checkbox" />
<span data-i18n="Restore Faces">Restore Faces</span>
@ -232,11 +240,17 @@
<span data-i18n="Hires. Fix">Hires. Fix</span>
</label>
</div>
<div data-sd-source="auto,vlad">
<div data-sd-source="auto,vlad,comfy,horde,drawthings,extras" class="marginTopBot5">
<label for="sd_clip_skip">CLIP Skip (<span id="sd_clip_skip_value"></span>)</label>
<input type="range" id="sd_clip_skip" min="{{clip_skip_min}}" max="{{clip_skip_max}}" step="{{clip_skip_step}}" value="{{clip_skip}}" />
</div>
<div data-sd-source="auto,vlad,drawthings">
<label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>
<select id="sd_hr_upscaler"></select>
<label for="sd_hr_scale"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_hr_scale_value"></span>)</label>
<input id="sd_hr_scale" type="range" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" />
</div>
<div data-sd-source="auto,vlad">
<label for="sd_denoising_strength"><span data-i18n="Denoising strength">Denoising strength</span> (<span id="sd_denoising_strength_value"></span>)</label>
<input id="sd_denoising_strength" type="range" min="{{denoising_strength_min}}" max="{{denoising_strength_max}}" step="{{denoising_strength_step}}" value="{{denoising_strength}}" />
<label for="sd_hr_second_pass_steps"><span data-i18n="Hires steps (2nd pass)">Hires steps (2nd pass)</span> (<span id="sd_hr_second_pass_steps_value"></span>)</label>
@ -246,6 +260,11 @@
<label for="sd_novel_upscale_ratio"><span data-i18n="Upscale by">Upscale by</span> (<span id="sd_novel_upscale_ratio_value"></span>)</label>
<input id="sd_novel_upscale_ratio" type="range" min="{{novel_upscale_ratio_min}}" max="{{novel_upscale_ratio_max}}" step="{{novel_upscale_ratio_step}}" value="{{novel_upscale_ratio}}" />
</div>
<div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras" class="marginTop5">
<label for="sd_seed">Seed</label>
<small>(-1 for random)</small>
<input id="sd_seed" type="number" class="text_pole" min="-1" max="9999999999" step="1" />
</div>
<hr>
<h4 data-i18n="[title]Preset for prompt prefix and negative prompt" title="Preset for prompt prefix and negative prompt">
<span data-i18n="Style">Style</span>
@ -255,6 +274,9 @@
<div id="sd_save_style" data-i18n="[title]Save style" title="Save style" class="menu_button">
<i class="fa-solid fa-save"></i>
</div>
<div id="sd_delete_style" data-i18n="[title]Delete style" title="Delete style" class="menu_button">
<i class="fa-solid fa-trash-can"></i>
</div>
</div>
<label for="sd_prompt_prefix" data-i18n="Common prompt prefix">Common prompt prefix</label>
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
@ -267,6 +289,15 @@
<label for="sd_character_negative_prompt" data-i18n="Character-specific negative prompt prefix">Character-specific negative prompt prefix</label>
<small data-i18n="Won't be used in groups.">Won't be used in groups.</small>
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact" rows="3" data-i18n="[placeholder]sd_character_negative_prompt_placeholder" placeholder="Any characteristics that should not appear for the selected character. Will be added after a negative common prompt prefix.&#10;Example: jewellery, shoes, glasses"></textarea>
<label for="sd_character_prompt_share" class="checkbox_label flexWrap marginTop5">
<input id="sd_character_prompt_share" type="checkbox" />
<span data-i18n="Shareable">
Shareable
</span>
<small class="flexBasis100p">
When checked, character-specific prompts will be saved with the character card data.
</small>
</label>
</div>
</div>
</div>

View File

@ -12,6 +12,9 @@ import {
} from '../../../script.js';
import { extension_settings, getContext } from '../../extensions.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 { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { splitRecursive } from '../../utils.js';
export const autoModeOptions = {
@ -649,4 +652,21 @@ jQuery(() => {
eventSource.on(event_types.MESSAGE_UPDATED, handleMessageEdit);
document.body.classList.add('translate');
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'translate',
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)),
],
unnamedArgumentList: [
new SlashCommandArgument('The text to translate', ARGUMENT_TYPE.STRING, true, false, ''),
],
callback: async (args, value) => {
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);
},
}));
});

View File

@ -21,10 +21,14 @@ import {
} from '../../extensions.js';
import { collapseNewlines } from '../../power-user.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { getDataBankAttachments, getFileAttachment } from '../../chats.js';
import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive } from '../../utils.js';
import { debounce_timeout } from '../../constants.js';
import { getSortedEntries } from '../../world-info.js';
import { textgen_types, textgenerationwebui_settings } from '../../textgen-settings.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
const MODULE_NAME = 'vectors';
@ -38,6 +42,8 @@ const settings = {
togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
openai_model: 'text-embedding-ada-002',
cohere_model: 'embed-english-v3.0',
ollama_model: 'mxbai-embed-large',
ollama_keep: false,
summarize: false,
summarize_sent: false,
summary_source: 'main',
@ -52,6 +58,7 @@ const settings = {
insert: 3,
query: 2,
message_chunk_size: 400,
score_threshold: 0.25,
// For files
enabled_files: false,
@ -271,6 +278,10 @@ async function synchronizeChat(batchSize = 5) {
switch (cause) {
case 'api_key_missing':
return 'API key missing. Save it in the "API Connections" panel.';
case 'api_url_missing':
return 'API URL missing. Save it in the "API Connections" panel.';
case 'api_model_missing':
return 'Vectorization Source Model is required, but not set.';
case 'extras_module_missing':
return 'Extras API must provide an "embeddings" module.';
default:
@ -324,28 +335,7 @@ async function processFiles(chat) {
return;
}
const dataBank = getDataBankAttachments();
const dataBankCollectionIds = [];
for (const file of dataBank) {
const collectionId = getFileCollectionId(file.url);
const hashesInCollection = await getSavedHashes(collectionId);
dataBankCollectionIds.push(collectionId);
// File is already in the collection
if (hashesInCollection.length) {
continue;
}
// Download and process the file
file.text = await getFileAttachment(file.url);
console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
// Convert kilobytes to string length
const thresholdLength = settings.size_threshold_db * 1024;
// Use chunk size from settings if file is larger than threshold
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
await vectorizeFile(file.text, file.name, collectionId, chunkSize);
}
const dataBankCollectionIds = await ingestDataBankAttachments();
if (dataBankCollectionIds.length) {
const queryText = await getQueryText(chat);
@ -392,6 +382,39 @@ async function processFiles(chat) {
}
}
/**
* Ensures that data bank attachments are ingested and inserted into the vector index.
* @param {string} [source] Optional source filter for data bank attachments.
* @returns {Promise<string[]>} Collection IDs
*/
async function ingestDataBankAttachments(source) {
// Exclude disabled files
const dataBank = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
const dataBankCollectionIds = [];
for (const file of dataBank) {
const collectionId = getFileCollectionId(file.url);
const hashesInCollection = await getSavedHashes(collectionId);
dataBankCollectionIds.push(collectionId);
// File is already in the collection
if (hashesInCollection.length) {
continue;
}
// Download and process the file
file.text = await getFileAttachment(file.url);
console.log(`Vectors: Retrieved file ${file.name} from Data Bank`);
// Convert kilobytes to string length
const thresholdLength = settings.size_threshold_db * 1024;
// Use chunk size from settings if file is larger than threshold
const chunkSize = file.size > thresholdLength ? settings.chunk_size_db : -1;
await vectorizeFile(file.text, file.name, collectionId, chunkSize);
}
return dataBankCollectionIds;
}
/**
* Inserts file chunks from the Data Bank into the prompt.
* @param {string} queryText Text to query
@ -400,7 +423,7 @@ async function processFiles(chat) {
*/
async function injectDataBankChunks(queryText, collectionIds) {
try {
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db);
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.chunk_count_db, settings.score_threshold);
console.debug(`Vectors: Retrieved ${collectionIds.length} Data Bank collections`, queryResults);
let textResult = '';
@ -636,6 +659,12 @@ function getVectorHeaders() {
case 'cohere':
addCohereHeaders(headers);
break;
case 'ollama':
addOllamaHeaders(headers);
break;
case 'llamacpp':
addLlamaCppHeaders(headers);
break;
default:
break;
}
@ -684,6 +713,28 @@ function addCohereHeaders(headers) {
});
}
/**
* Add headers for the Ollama API source.
* @param {object} headers Header object
*/
function addOllamaHeaders(headers) {
Object.assign(headers, {
'X-Ollama-Model': extension_settings.vectors.ollama_model,
'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
});
}
/**
* Add headers for the LlamaCpp API source.
* @param {object} headers Header object
*/
function addLlamaCppHeaders(headers) {
Object.assign(headers, {
'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
});
}
/**
* Inserts vector items into a collection
* @param {string} collectionId - The collection to insert into
@ -691,18 +742,7 @@ function addCohereHeaders(headers) {
* @returns {Promise<void>}
*/
async function insertVectorItems(collectionId, items) {
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||
settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||
settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) {
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
}
if (settings.source === 'extras' && !modules.includes('embeddings')) {
throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' });
}
throwIfSourceInvalid();
const headers = getVectorHeaders();
@ -721,6 +761,33 @@ async function insertVectorItems(collectionId, items) {
}
}
/**
* Throws an error if the source is invalid (missing API key or URL, or missing module)
*/
function throwIfSourceInvalid() {
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
settings.source === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] ||
settings.source === 'togetherai' && !secret_state[SECRET_KEYS.TOGETHERAI] ||
settings.source === 'nomicai' && !secret_state[SECRET_KEYS.NOMICAI] ||
settings.source === 'cohere' && !secret_state[SECRET_KEYS.COHERE]) {
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
}
if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
}
if (settings.source === 'ollama' && !settings.ollama_model) {
throw new Error('Vectors: API model missing', { cause: 'api_model_missing' });
}
if (settings.source === 'extras' && !modules.includes('embeddings')) {
throw new Error('Vectors: Embeddings module missing', { cause: 'extras_module_missing' });
}
}
/**
* Deletes vector items from a collection
* @param {string} collectionId - The collection to delete from
@ -760,6 +827,7 @@ async function queryCollection(collectionId, searchText, topK) {
searchText: searchText,
topK: topK,
source: settings.source,
threshold: settings.score_threshold,
}),
});
@ -775,9 +843,10 @@ async function queryCollection(collectionId, searchText, topK) {
* @param {string[]} collectionIds - Collection IDs to query
* @param {string} searchText - Text to query
* @param {number} topK - Number of results to return
* @param {number} threshold - Score threshold
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
*/
async function queryMultipleCollections(collectionIds, searchText, topK) {
async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query-multi', {
@ -788,6 +857,7 @@ async function queryMultipleCollections(collectionIds, searchText, topK) {
searchText: searchText,
topK: topK,
source: settings.source,
threshold: threshold ?? settings.score_threshold,
}),
});
@ -867,6 +937,8 @@ function toggleSettings() {
$('#together_vectorsModel').toggle(settings.source === 'togetherai');
$('#openai_vectorsModel').toggle(settings.source === 'openai');
$('#cohere_vectorsModel').toggle(settings.source === 'cohere');
$('#ollama_vectorsModel').toggle(settings.source === 'ollama');
$('#llamacpp_vectorsModel').toggle(settings.source === 'llamacpp');
$('#nomicai_apiKey').toggle(settings.source === 'nomicai');
}
@ -897,8 +969,8 @@ async function onViewStatsClick() {
toastr.info(`Total hashes: <b>${totalHashes}</b><br>
Unique hashes: <b>${uniqueHashes}</b><br><br>
I'll mark collected messages with a green circle.`,
`Stats for chat ${chatId}`,
{ timeOut: 10000, escapeHtml: false });
`Stats for chat ${chatId}`,
{ timeOut: 10000, escapeHtml: false });
const chat = getContext().chat;
for (const message of chat) {
@ -1069,7 +1141,7 @@ async function activateWorldInfo(chat) {
return;
}
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries);
const queryResults = await queryMultipleCollections(collectionIds, queryText, settings.max_entries, settings.score_threshold);
const activatedHashes = Object.values(queryResults).flatMap(x => x.hashes).filter(onlyUnique);
const activatedEntries = [];
@ -1151,6 +1223,17 @@ jQuery(async () => {
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_ollama_model').val(settings.ollama_model).on('input', () => {
$('#vectors_modelWarning').show();
settings.ollama_model = String($('#vectors_ollama_model').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_ollama_keep').prop('checked', settings.ollama_keep).on('input', () => {
settings.ollama_keep = $('#vectors_ollama_keep').prop('checked');
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vectors_template').val(settings.template).on('input', () => {
settings.template = String($('#vectors_template').val());
Object.assign(extension_settings.vectors, settings);
@ -1310,6 +1393,12 @@ jQuery(async () => {
saveSettingsDebounced();
});
$('#vectors_score_threshold').val(settings.score_threshold).on('input', () => {
settings.score_threshold = Number($('#vectors_score_threshold').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
$('#api_key_nomicai').attr('placeholder', placeholder);
@ -1323,4 +1412,60 @@ jQuery(async () => {
eventSource.on(event_types.CHAT_DELETED, purgeVectorIndex);
eventSource.on(event_types.GROUP_CHAT_DELETED, purgeVectorIndex);
eventSource.on(event_types.FILE_ATTACHMENT_DELETED, purgeFileVectorIndex);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-ingest',
callback: async () => {
await ingestDataBankAttachments();
return '';
},
aliases: ['databank-ingest', 'data-bank-ingest'],
helpString: 'Force the ingestion of all Data Bank attachments.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-purge',
callback: async () => {
const dataBank = getDataBankAttachments();
for (const file of dataBank) {
await purgeFileVectorIndex(file.url);
}
return '';
},
aliases: ['databank-purge', 'data-bank-purge'],
helpString: 'Purge the vector index for all Data Bank attachments.',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'db-search',
callback: async (args, query) => {
const clamp = (v) => Number.isNaN(v) ? null : Math.min(1, Math.max(0, v));
const threshold = clamp(Number(args?.threshold ?? settings.score_threshold));
const source = String(args?.source ?? '');
const attachments = source ? getDataBankAttachmentsForSource(source, false) : getDataBankAttachments(false);
const collectionIds = await ingestDataBankAttachments(String(source));
const queryResults = await queryMultipleCollections(collectionIds, String(query), settings.chunk_count_db, threshold);
// Map collection IDs to file URLs
const urls = Object
.keys(queryResults)
.map(x => attachments.find(y => getFileCollectionId(y.url) === x))
.filter(x => x)
.map(x => x.url);
return JSON.stringify(urls);
},
aliases: ['databank-search', 'data-bank-search'],
helpString: 'Search the Data Bank for a specific query using vector similarity. Returns a list of file URLs with the most relevant content.',
namedArgumentList: [
new SlashCommandNamedArgument('threshold', 'Threshold for the similarity score in the [0, 1] range. Uses the global config value if not set.', ARGUMENT_TYPE.NUMBER, false, false, ''),
new SlashCommandNamedArgument('source', 'Optional filter for the attachments by source.', ARGUMENT_TYPE.STRING, false, false, '', ['global', 'character', 'chat']),
],
unnamedArgumentList: [
new SlashCommandArgument('Query to search by.', ARGUMENT_TYPE.STRING, true, false),
],
returns: ARGUMENT_TYPE.LIST,
}));
});

View File

@ -12,14 +12,37 @@
<select id="vectors_source" class="text_pole">
<option value="cohere">Cohere</option>
<option value="extras">Extras</option>
<option value="palm">Google MakerSuite (PaLM)</option>
<option value="palm">Google MakerSuite</option>
<option value="llamacpp">llama.cpp</option>
<option value="transformers">Local (Transformers)</option>
<option value="mistral">MistralAI</option>
<option value="nomicai">NomicAI</option>
<option value="ollama">Ollama</option>
<option value="openai">OpenAI</option>
<option value="togetherai">TogetherAI</option>
</select>
</div>
<div class="flex-container flexFlowColumn" id="ollama_vectorsModel">
<label for="vectors_ollama_model">
Vectorization Model
</label>
<input id="vectors_ollama_model" class="text_pole" type="text" placeholder="Model tag, e.g. llama3" />
<label for="vectors_ollama_keep" class="checkbox_label" title="When checked, the model will not be unloaded after use.">
<input id="vectors_ollama_keep" type="checkbox" />
<span>Keep model in memory</span>
</label>
<i>
Hint: Download models and set the URL in the API connection settings.
</i>
</div>
<div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel">
<span>
The server MUST be started with the <code>--embedding</code> flag to use this feature!
</span>
<i>
Hint: Set the URL in the API connection settings.
</i>
</div>
<div class="flex-container flexFlowColumn" id="openai_vectorsModel">
<label for="vectors_openai_model">
Vectorization Model
@ -81,11 +104,19 @@
</div>
</div>
<div class="flex-container flexFlowColumn" title="How many last messages will be matched for relevance.">
<label for="vectors_query">
<span>Query messages</span>
</label>
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" />
<div class="flex-container marginTopBot5">
<div class="flex-container flex1 flexFlowColumn" title="How many last messages will be matched for relevance.">
<label for="vectors_query">
<span>Query messages</span>
</label>
<input type="number" id="vectors_query" class="text_pole widthUnset" min="1" max="99" />
</div>
<div class="flex-container flex1 flexFlowColumn" title="Cut-off score for relevance. Helps to filter out irrelevant data.">
<label for="vectors_query">
<span>Score threshold</span>
</label>
<input type="number" id="vectors_score_threshold" class="text_pole widthUnset" min="0" max="1" step="0.05" />
</div>
</div>
<div class="flex-container">