mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into GPT-SoVITS-V2
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
|
||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { appendMediaToMessage, callPopup, eventSource, event_types, getRequestHeaders, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
|
||||
import { appendMediaToMessage, callPopup, eventSource, event_types, getRequestHeaders, main_api, saveChatConditional, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
|
||||
import { getMessageTimeStamp } from '../../RossAscends-mods.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getMultimodalCaption } from '../shared.js';
|
||||
@@ -358,10 +358,10 @@ function onRefineModeInput() {
|
||||
*/
|
||||
async function captionCommandCallback(args, prompt) {
|
||||
const quiet = isTrueBoolean(args?.quiet);
|
||||
const id = args?.id;
|
||||
const mesId = args?.mesId ?? args?.id;
|
||||
|
||||
if (!isNaN(Number(id))) {
|
||||
const message = getContext().chat[id];
|
||||
if (!isNaN(Number(mesId))) {
|
||||
const message = getContext().chat[mesId];
|
||||
if (message?.extra?.image) {
|
||||
try {
|
||||
const fetchResult = await fetch(message.extra.image);
|
||||
@@ -403,6 +403,7 @@ jQuery(async function () {
|
||||
(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 === 'zerooneai' && secret_state[SECRET_KEYS.ZEROONEAI]) ||
|
||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'mistral' && (secret_state[SECRET_KEYS.MISTRALAI] || extension_settings.caption.allow_reverse_proxy)) ||
|
||||
(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]) ||
|
||||
@@ -431,14 +432,9 @@ jQuery(async function () {
|
||||
$('#form_sheld').append(imgForm);
|
||||
$('#img_file').on('change', (e) => onSelectImage(e.originalEvent, '', false));
|
||||
}
|
||||
function switchMultimodalBlocks() {
|
||||
async function switchMultimodalBlocks() {
|
||||
await addOpenRouterModels();
|
||||
const isMultimodal = extension_settings.caption.source === 'multimodal';
|
||||
$('#caption_ollama_pull').on('click', (e) => {
|
||||
const presetModel = extension_settings.caption.multimodal_model !== 'ollama_current' ? extension_settings.caption.multimodal_model : '';
|
||||
e.preventDefault();
|
||||
$('#ollama_download_model').trigger('click');
|
||||
$('#dialogue_popup_input').val(presetModel);
|
||||
});
|
||||
$('#caption_multimodal_block').toggle(isMultimodal);
|
||||
$('#caption_prompt_block').toggle(isMultimodal);
|
||||
$('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
|
||||
@@ -448,30 +444,48 @@ jQuery(async function () {
|
||||
const types = type.split(',');
|
||||
$(this).toggle(types.includes(extension_settings.caption.multimodal_api));
|
||||
});
|
||||
$('#caption_multimodal_api').on('change', () => {
|
||||
const api = String($('#caption_multimodal_api').val());
|
||||
const model = String($(`#caption_multimodal_model option[data-type="${api}"]`).first().val());
|
||||
extension_settings.caption.multimodal_api = api;
|
||||
extension_settings.caption.multimodal_model = model;
|
||||
saveSettingsDebounced();
|
||||
switchMultimodalBlocks();
|
||||
});
|
||||
$('#caption_multimodal_model').on('change', () => {
|
||||
extension_settings.caption.multimodal_model = String($('#caption_multimodal_model').val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
async function addSettings() {
|
||||
const html = await renderExtensionTemplateAsync('caption', 'settings', { TEMPLATE_DEFAULT, PROMPT_DEFAULT });
|
||||
$('#caption_container').append(html);
|
||||
}
|
||||
async function addOpenRouterModels() {
|
||||
const dropdown = document.getElementById('caption_multimodal_model');
|
||||
if (!(dropdown instanceof HTMLSelectElement)) {
|
||||
return;
|
||||
}
|
||||
if (extension_settings.caption.source !== 'multimodal' || extension_settings.caption.multimodal_api !== 'openrouter') {
|
||||
return;
|
||||
}
|
||||
const options = Array.from(dropdown.options);
|
||||
const response = await fetch('/api/openrouter/models/multimodal', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
const modelIds = await response.json();
|
||||
if (Array.isArray(modelIds) && modelIds.length > 0) {
|
||||
modelIds.forEach((modelId) => {
|
||||
if (!modelId || typeof modelId !== 'string' || options.some(o => o.value === modelId)) {
|
||||
return;
|
||||
}
|
||||
const option = document.createElement('option');
|
||||
option.value = modelId;
|
||||
option.textContent = modelId;
|
||||
option.dataset.type = 'openrouter';
|
||||
dropdown.add(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await addSettings();
|
||||
addPictureSendForm();
|
||||
addSendPictureButton();
|
||||
setImageIcon();
|
||||
migrateSettings();
|
||||
switchMultimodalBlocks();
|
||||
await switchMultimodalBlocks();
|
||||
|
||||
$('#caption_refine_mode').prop('checked', !!(extension_settings.caption.refine_mode));
|
||||
$('#caption_allow_reverse_proxy').prop('checked', !!(extension_settings.caption.allow_reverse_proxy));
|
||||
@@ -506,6 +520,24 @@ jQuery(async function () {
|
||||
extension_settings.caption.auto_mode = !!$('#caption_auto_mode').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#caption_ollama_pull').on('click', (e) => {
|
||||
const presetModel = extension_settings.caption.multimodal_model !== 'ollama_current' ? extension_settings.caption.multimodal_model : '';
|
||||
e.preventDefault();
|
||||
$('#ollama_download_model').trigger('click');
|
||||
$('#dialogue_popup_input').val(presetModel);
|
||||
});
|
||||
$('#caption_multimodal_api').on('change', () => {
|
||||
const api = String($('#caption_multimodal_api').val());
|
||||
const model = String($(`#caption_multimodal_model option[data-type="${api}"]`).first().val());
|
||||
extension_settings.caption.multimodal_api = api;
|
||||
extension_settings.caption.multimodal_model = model;
|
||||
saveSettingsDebounced();
|
||||
switchMultimodalBlocks();
|
||||
});
|
||||
$('#caption_multimodal_model').on('change', () => {
|
||||
extension_settings.caption.multimodal_model = String($('#caption_multimodal_model').val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
const onMessageEvent = async (index) => {
|
||||
if (!extension_settings.caption.auto_mode) {
|
||||
@@ -531,14 +563,15 @@ jQuery(async function () {
|
||||
await captionExistingMessage(data);
|
||||
appendMediaToMessage(data, messageBlock, false);
|
||||
await saveChatConditional();
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error('Message image recaption failed', e);
|
||||
} finally {
|
||||
messageImg.removeClass(animationClass);
|
||||
}
|
||||
});
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'caption',
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'caption',
|
||||
callback: captionCommandCallback,
|
||||
returns: 'caption',
|
||||
namedArgumentList: [
|
||||
@@ -546,7 +579,7 @@ jQuery(async function () {
|
||||
'quiet', 'suppress sending a captioned message', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false',
|
||||
),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'id',
|
||||
name: 'mesId',
|
||||
description: 'get image from a message with this ID',
|
||||
typeList: [ARGUMENT_TYPE.NUMBER],
|
||||
enumProvider: commonEnumProviders.messages(),
|
||||
|
@@ -23,6 +23,7 @@
|
||||
<option value="google">Google AI Studio</option>
|
||||
<option value="koboldcpp">KoboldCpp</option>
|
||||
<option value="llamacpp">llama.cpp</option>
|
||||
<option value="mistral">MistralAI</option>
|
||||
<option value="ollama">Ollama</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="openrouter">OpenRouter</option>
|
||||
@@ -33,6 +34,8 @@
|
||||
<div class="flex1 flex-container flexFlowColumn flexNoGap">
|
||||
<label for="caption_multimodal_model" data-i18n="Model">Model</label>
|
||||
<select id="caption_multimodal_model" class="flex1 text_pole">
|
||||
<option data-type="mistral" value="pixtral-latest">pixtral-latest</option>
|
||||
<option data-type="mistral" value="pixtral-12b-2409">pixtral-12b-2409</option>
|
||||
<option data-type="zerooneai" value="yi-vision">yi-vision</option>
|
||||
<option data-type="openai" value="gpt-4-vision-preview">gpt-4-vision-preview</option>
|
||||
<option data-type="openai" value="gpt-4-turbo">gpt-4-turbo</option>
|
||||
@@ -43,14 +46,28 @@
|
||||
<option data-type="anthropic" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
|
||||
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-exp-0827">gemini-1.5-flash-exp-0827</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
|
||||
<option data-type="google" value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
|
||||
<option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option>
|
||||
<option data-type="google" value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
|
||||
<option data-type="google" value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
|
||||
<option data-type="google" value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
|
||||
<option data-type="google" value="gemini-1.5-pro-exp-0801">gemini-1.5-pro-exp-0801</option>
|
||||
<option data-type="google" value="gemini-1.5-pro-exp-0827">gemini-1.5-pro-exp-0827</option>
|
||||
<option data-type="google" value="gemini-pro-vision">gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o-2024-08-06">openai/gpt-4o-2024-08-06</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4-turbo">openai/gpt-4-turbo</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o-mini">openai/gpt-4o-mini</option>
|
||||
<option data-type="openrouter" value="openai/gpt-4o-mini-2024-07-18">openai/gpt-4o-mini-2024-07-18</option>
|
||||
<option data-type="openrouter" value="openai/chatgpt-4o-latest">openai/chatgpt-4o-latest</option>
|
||||
<option data-type="openrouter" value="haotian-liu/llava-13b">haotian-liu/llava-13b</option>
|
||||
<option data-type="openrouter" value="fireworks/firellava-13b">fireworks/firellava-13b</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3.5-sonnet">anthropic/claude-3.5-sonnet</option>
|
||||
@@ -62,8 +79,12 @@
|
||||
<option data-type="openrouter" value="anthropic/claude-3-sonnet:beta">anthropic/claude-3-sonnet:beta</option>
|
||||
<option data-type="openrouter" value="anthropic/claude-3-opus:beta">anthropic/claude-3-opus:beta</option>
|
||||
<option data-type="openrouter" value="nousresearch/nous-hermes-2-vision-7b">nousresearch/nous-hermes-2-vision-7b</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="google/gemini-flash-8b-1.5-exp">google/gemini-flash-8b-1.5-exp</option>
|
||||
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
|
||||
<option data-type="openrouter" value="google/gemini-flash-1.5-exp">google/gemini-flash-1.5-exp</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-1.5">google/gemini-pro-1.5</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-1.5-exp">google/gemini-pro-1.5-exp</option>
|
||||
<option data-type="openrouter" value="google/gemini-pro-vision">google/gemini-pro-vision</option>
|
||||
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
|
||||
<option data-type="ollama" value="ollama_current" data-i18n="currently_selected">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava">bakllava</option>
|
||||
@@ -81,7 +102,7 @@
|
||||
<div data-type="ollama">
|
||||
The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>.
|
||||
</div>
|
||||
<label data-type="openai,anthropic,google" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
|
||||
<label data-type="openai,anthropic,google,mistral" 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">
|
||||
<span data-i18n="Allow reverse proxy">Allow reverse proxy</span>
|
||||
</label>
|
||||
|
12
public/scripts/extensions/connection-manager/edit.html
Normal file
12
public/scripts/extensions/connection-manager/edit.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<div>
|
||||
<h3>Included settings:</h3>
|
||||
<div class="justifyLeft flex-container flexFlowColumn flexNoGap">
|
||||
{{#each settings}}
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}>
|
||||
<span>{{@key}}</span>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
<h3>Profile name:</h3>
|
||||
</div>
|
728
public/scripts/extensions/connection-manager/index.js
Normal file
728
public/scripts/extensions/connection-manager/index.js
Normal file
@@ -0,0 +1,728 @@
|
||||
import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
|
||||
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
|
||||
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { commonEnumProviders, enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { SlashCommandDebugController } from '../../slash-commands/SlashCommandDebugController.js';
|
||||
import { enumTypes, SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommandScope } from '../../slash-commands/SlashCommandScope.js';
|
||||
import { collapseSpaces, getUniqueName, isFalseBoolean, uuidv4 } from '../../utils.js';
|
||||
|
||||
const MODULE_NAME = 'connection-manager';
|
||||
const NONE = '<None>';
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
profiles: [],
|
||||
selectedProfile: null,
|
||||
};
|
||||
|
||||
const CC_COMMANDS = [
|
||||
'api',
|
||||
'preset',
|
||||
// Do not fix; CC needs to set the API twice because it could be overridden by the preset
|
||||
'api',
|
||||
'api-url',
|
||||
'model',
|
||||
'proxy',
|
||||
];
|
||||
|
||||
const TC_COMMANDS = [
|
||||
'api',
|
||||
'preset',
|
||||
'api-url',
|
||||
'model',
|
||||
'sysprompt',
|
||||
'sysprompt-state',
|
||||
'instruct',
|
||||
'context',
|
||||
'instruct-state',
|
||||
'tokenizer',
|
||||
];
|
||||
|
||||
const FANCY_NAMES = {
|
||||
'api': 'API',
|
||||
'api-url': 'Server URL',
|
||||
'preset': 'Settings Preset',
|
||||
'model': 'Model',
|
||||
'proxy': 'Proxy Preset',
|
||||
'sysprompt-state': 'Use System Prompt',
|
||||
'sysprompt': 'System Prompt Name',
|
||||
'instruct-state': 'Instruct Mode',
|
||||
'instruct': 'Instruct Template',
|
||||
'context': 'Context Template',
|
||||
'tokenizer': 'Tokenizer',
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper for the connection manager spinner.
|
||||
*/
|
||||
class ConnectionManagerSpinner {
|
||||
/**
|
||||
* @type {AbortController[]}
|
||||
*/
|
||||
static abortControllers = [];
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
spinnerElement;
|
||||
|
||||
/** @type {AbortController} */
|
||||
abortController = new AbortController();
|
||||
|
||||
constructor() {
|
||||
// @ts-ignore
|
||||
this.spinnerElement = document.getElementById('connection_profile_spinner');
|
||||
this.abortController = new AbortController();
|
||||
}
|
||||
|
||||
start() {
|
||||
ConnectionManagerSpinner.abortControllers.push(this.abortController);
|
||||
this.spinnerElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.spinnerElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
isAborted() {
|
||||
return this.abortController.signal.aborted;
|
||||
}
|
||||
|
||||
static abort() {
|
||||
for (const controller of ConnectionManagerSpinner.abortControllers) {
|
||||
controller.abort();
|
||||
}
|
||||
ConnectionManagerSpinner.abortControllers = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get named arguments for the command callback.
|
||||
* @param {object} [args] Additional named arguments
|
||||
* @returns {object} Named arguments
|
||||
*/
|
||||
function getNamedArguments(args = {}) {
|
||||
// None of the commands here use underscored args, but better safe than sorry
|
||||
return {
|
||||
_scope: new SlashCommandScope(),
|
||||
_abortController: new SlashCommandAbortController(),
|
||||
_debugController: new SlashCommandDebugController(),
|
||||
_parserFlags: {},
|
||||
_hasUnnamedArgument: false,
|
||||
quiet: 'true',
|
||||
...args,
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {() => SlashCommandEnumValue[]} */
|
||||
const profilesProvider = () => [
|
||||
new SlashCommandEnumValue(NONE),
|
||||
...extension_settings.connectionManager.profiles.map(p => new SlashCommandEnumValue(p.name, null, enumTypes.name, enumIcons.server)),
|
||||
];
|
||||
|
||||
/**
|
||||
* @typedef {Object} ConnectionProfile
|
||||
* @property {string} id Unique identifier
|
||||
* @property {string} mode Mode of the connection profile
|
||||
* @property {string} [name] Name of the connection profile
|
||||
* @property {string} [api] API
|
||||
* @property {string} [preset] Settings Preset
|
||||
* @property {string} [model] Model
|
||||
* @property {string} [proxy] Proxy Preset
|
||||
* @property {string} [instruct] Instruct Template
|
||||
* @property {string} [context] Context Template
|
||||
* @property {string} [instruct-state] Instruct Mode
|
||||
* @property {string} [tokenizer] Tokenizer
|
||||
* @property {string[]} [exclude] Commands to exclude
|
||||
*/
|
||||
|
||||
/**
|
||||
* Finds the best match for the search value.
|
||||
* @param {string} value Search value
|
||||
* @returns {ConnectionProfile|null} Best match or null
|
||||
*/
|
||||
function findProfileByName(value) {
|
||||
// Try to find exact match
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.name === value);
|
||||
|
||||
if (profile) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
// Try to find fuzzy match
|
||||
const fuse = new Fuse(extension_settings.connectionManager.profiles, { keys: ['name'] });
|
||||
const results = fuse.search(value);
|
||||
|
||||
if (results.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bestMatch = results[0];
|
||||
return bestMatch.item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the connection profile from the commands.
|
||||
* @param {string} mode Mode of the connection profile
|
||||
* @param {ConnectionProfile} profile Connection profile
|
||||
* @param {boolean} [cleanUp] Whether to clean up the profile
|
||||
*/
|
||||
async function readProfileFromCommands(mode, profile, cleanUp = false) {
|
||||
const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
|
||||
const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS;
|
||||
const excludeList = Array.isArray(profile.exclude) ? profile.exclude : [];
|
||||
for (const command of commands) {
|
||||
try {
|
||||
if (excludeList.includes(command)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const args = getNamedArguments();
|
||||
const result = await SlashCommandParser.commands[command].callback(args, '');
|
||||
if (result) {
|
||||
profile[command] = result;
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to execute command: ${command}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanUp) {
|
||||
for (const command of commands) {
|
||||
if (command.endsWith('-state') && profile[command] === 'false') {
|
||||
delete profile[command.replace('-state', '')];
|
||||
}
|
||||
}
|
||||
for (const command of opposingCommands) {
|
||||
if (commands.includes(command)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
delete profile[command];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new connection profile.
|
||||
* @param {string} [forceName] Name of the connection profile
|
||||
* @returns {Promise<ConnectionProfile>} Created connection profile
|
||||
*/
|
||||
async function createConnectionProfile(forceName = null) {
|
||||
const mode = main_api === 'openai' ? 'cc' : 'tc';
|
||||
const id = uuidv4();
|
||||
/** @type {ConnectionProfile} */
|
||||
const profile = {
|
||||
id,
|
||||
mode,
|
||||
exclude: [],
|
||||
};
|
||||
|
||||
await readProfileFromCommands(mode, profile);
|
||||
|
||||
const profileForDisplay = makeFancyProfile(profile);
|
||||
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }));
|
||||
template.find('input[name="exclude"]').on('input', function () {
|
||||
const fancyName = String($(this).val());
|
||||
const keyName = Object.entries(FANCY_NAMES).find(x => x[1] === fancyName)?.[0];
|
||||
if (!keyName) {
|
||||
console.warn('Key not found for fancy name:', fancyName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(profile.exclude)) {
|
||||
profile.exclude = [];
|
||||
}
|
||||
|
||||
const excludeState = !$(this).prop('checked');
|
||||
if (excludeState) {
|
||||
profile.exclude.push(keyName);
|
||||
} else {
|
||||
const index = profile.exclude.indexOf(keyName);
|
||||
index !== -1 && profile.exclude.splice(index, 1);
|
||||
}
|
||||
});
|
||||
const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n);
|
||||
const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken);
|
||||
const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 });
|
||||
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isNameTaken(name) || name === NONE) {
|
||||
toastr.error('A profile with the same name already exists.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(profile.exclude)) {
|
||||
for (const command of profile.exclude) {
|
||||
delete profile[command];
|
||||
}
|
||||
}
|
||||
|
||||
profile.name = String(name);
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the selected connection profile.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function deleteConnectionProfile() {
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
if (!selectedProfile) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = extension_settings.connectionManager.profiles.findIndex(p => p.id === selectedProfile);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = extension_settings.connectionManager.profiles[index].name;
|
||||
const confirm = await Popup.show.confirm('Are you sure you want to delete the selected profile?', name);
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
extension_settings.connectionManager.profiles.splice(index, 1);
|
||||
extension_settings.connectionManager.selectedProfile = null;
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the connection profile for display.
|
||||
* @param {ConnectionProfile} profile Connection profile
|
||||
* @returns {Object} Fancy profile
|
||||
*/
|
||||
function makeFancyProfile(profile) {
|
||||
return Object.entries(FANCY_NAMES).reduce((acc, [key, value]) => {
|
||||
if (!profile[key]) return acc;
|
||||
acc[value] = profile[key];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the connection profile.
|
||||
* @param {ConnectionProfile} profile Connection profile
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function applyConnectionProfile(profile) {
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort any ongoing profile application
|
||||
ConnectionManagerSpinner.abort();
|
||||
|
||||
const mode = profile.mode;
|
||||
const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
|
||||
const spinner = new ConnectionManagerSpinner();
|
||||
spinner.start();
|
||||
|
||||
for (const command of commands) {
|
||||
if (spinner.isAborted()) {
|
||||
throw new Error('Profile application aborted');
|
||||
}
|
||||
|
||||
const argument = profile[command];
|
||||
if (!argument) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const args = getNamedArguments();
|
||||
await SlashCommandParser.commands[command].callback(args, argument);
|
||||
} catch (error) {
|
||||
console.error(`Failed to execute command: ${command} ${argument}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
spinner.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the selected connection profile.
|
||||
* @param {ConnectionProfile} profile Connection profile
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function updateConnectionProfile(profile) {
|
||||
profile.mode = main_api === 'openai' ? 'cc' : 'tc';
|
||||
await readProfileFromCommands(profile.mode, profile, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the connection profile details.
|
||||
* @param {HTMLSelectElement} profiles Select element containing connection profiles
|
||||
*/
|
||||
function renderConnectionProfiles(profiles) {
|
||||
profiles.innerHTML = '';
|
||||
const noneOption = document.createElement('option');
|
||||
|
||||
noneOption.value = '';
|
||||
noneOption.textContent = NONE;
|
||||
noneOption.selected = !extension_settings.connectionManager.selectedProfile;
|
||||
profiles.appendChild(noneOption);
|
||||
|
||||
for (const profile of extension_settings.connectionManager.profiles.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
const option = document.createElement('option');
|
||||
option.value = profile.id;
|
||||
option.textContent = profile.name;
|
||||
option.selected = profile.id === extension_settings.connectionManager.selectedProfile;
|
||||
profiles.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of the details element.
|
||||
* @param {HTMLElement} detailsContent Content element of the details
|
||||
*/
|
||||
async function renderDetailsContent(detailsContent) {
|
||||
detailsContent.innerHTML = '';
|
||||
if (detailsContent.classList.contains('hidden')) {
|
||||
return;
|
||||
}
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
|
||||
if (profile) {
|
||||
const profileForDisplay = makeFancyProfile(profile);
|
||||
const templateParams = { profile: profileForDisplay };
|
||||
if (Array.isArray(profile.exclude) && profile.exclude.length > 0) {
|
||||
templateParams.omitted = profile.exclude.map(e => FANCY_NAMES[e]).join(', ');
|
||||
}
|
||||
const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', templateParams);
|
||||
detailsContent.innerHTML = template;
|
||||
} else {
|
||||
detailsContent.textContent = 'No profile selected';
|
||||
}
|
||||
}
|
||||
|
||||
(async function () {
|
||||
extension_settings.connectionManager = extension_settings.connectionManager || structuredClone(DEFAULT_SETTINGS);
|
||||
|
||||
for (const key of Object.keys(DEFAULT_SETTINGS)) {
|
||||
if (extension_settings.connectionManager[key] === undefined) {
|
||||
extension_settings.connectionManager[key] = DEFAULT_SETTINGS[key];
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.getElementById('rm_api_block');
|
||||
const settings = await renderExtensionTemplateAsync(MODULE_NAME, 'settings');
|
||||
container.insertAdjacentHTML('afterbegin', settings);
|
||||
|
||||
/** @type {HTMLSelectElement} */
|
||||
// @ts-ignore
|
||||
const profiles = document.getElementById('connection_profiles');
|
||||
renderConnectionProfiles(profiles);
|
||||
|
||||
function toggleProfileSpecificButtons() {
|
||||
const profileId = extension_settings.connectionManager.selectedProfile;
|
||||
const profileSpecificButtons = ['update_connection_profile', 'reload_connection_profile', 'delete_connection_profile'];
|
||||
profileSpecificButtons.forEach(id => document.getElementById(id).classList.toggle('disabled', !profileId));
|
||||
}
|
||||
toggleProfileSpecificButtons();
|
||||
|
||||
profiles.addEventListener('change', async function () {
|
||||
const selectedProfile = profiles.selectedOptions[0];
|
||||
if (!selectedProfile) {
|
||||
// Safety net for preventing the command getting stuck
|
||||
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);
|
||||
return;
|
||||
}
|
||||
|
||||
const profileId = selectedProfile.value;
|
||||
extension_settings.connectionManager.selectedProfile = profileId;
|
||||
saveSettingsDebounced();
|
||||
await renderDetailsContent(detailsContent);
|
||||
|
||||
toggleProfileSpecificButtons();
|
||||
|
||||
// None option selected
|
||||
if (!profileId) {
|
||||
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === profileId);
|
||||
|
||||
if (!profile) {
|
||||
console.log(`Profile not found: ${profileId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await applyConnectionProfile(profile);
|
||||
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
|
||||
});
|
||||
|
||||
const reloadButton = document.getElementById('reload_connection_profile');
|
||||
reloadButton.addEventListener('click', async () => {
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
|
||||
if (!profile) {
|
||||
console.log('No profile selected');
|
||||
return;
|
||||
}
|
||||
await applyConnectionProfile(profile);
|
||||
await renderDetailsContent(detailsContent);
|
||||
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
|
||||
toastr.success('Connection profile reloaded', '', { timeOut: 1500 });
|
||||
});
|
||||
|
||||
const createButton = document.getElementById('create_connection_profile');
|
||||
createButton.addEventListener('click', async () => {
|
||||
const profile = await createConnectionProfile();
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
extension_settings.connectionManager.profiles.push(profile);
|
||||
extension_settings.connectionManager.selectedProfile = profile.id;
|
||||
saveSettingsDebounced();
|
||||
renderConnectionProfiles(profiles);
|
||||
await renderDetailsContent(detailsContent);
|
||||
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
|
||||
});
|
||||
|
||||
const updateButton = document.getElementById('update_connection_profile');
|
||||
updateButton.addEventListener('click', async () => {
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
|
||||
if (!profile) {
|
||||
console.log('No profile selected');
|
||||
return;
|
||||
}
|
||||
await updateConnectionProfile(profile);
|
||||
await renderDetailsContent(detailsContent);
|
||||
saveSettingsDebounced();
|
||||
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, profile.name);
|
||||
toastr.success('Connection profile updated', '', { timeOut: 1500 });
|
||||
});
|
||||
|
||||
const deleteButton = document.getElementById('delete_connection_profile');
|
||||
deleteButton.addEventListener('click', async () => {
|
||||
await deleteConnectionProfile();
|
||||
renderConnectionProfiles(profiles);
|
||||
await renderDetailsContent(detailsContent);
|
||||
await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE);
|
||||
});
|
||||
|
||||
const editButton = document.getElementById('edit_connection_profile');
|
||||
editButton.addEventListener('click', async () => {
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
|
||||
if (!profile) {
|
||||
console.log('No profile selected');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Array.isArray(profile.exclude)) {
|
||||
profile.exclude = [];
|
||||
}
|
||||
|
||||
let saveChanges = false;
|
||||
const sortByViewOrder = (a, b) => Object.keys(FANCY_NAMES).indexOf(a) - Object.keys(FANCY_NAMES).indexOf(b);
|
||||
const commands = profile.mode === 'cc' ? CC_COMMANDS : TC_COMMANDS;
|
||||
const settings = commands.slice().sort(sortByViewOrder).reduce((acc, command) => {
|
||||
const fancyName = FANCY_NAMES[command];
|
||||
acc[fancyName] = !profile.exclude.includes(command);
|
||||
return acc;
|
||||
}, {});
|
||||
const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings }));
|
||||
const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, {
|
||||
customButtons: [{
|
||||
text: 'Save and Update',
|
||||
classes: ['popup-button-ok'],
|
||||
result: POPUP_RESULT.AFFIRMATIVE,
|
||||
action: () => {
|
||||
saveChanges = true;
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) {
|
||||
toastr.error('A profile with the same name already exists.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newExcludeList = template.find('input[name="exclude"]:not(:checked)').map(function () {
|
||||
return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0];
|
||||
}).get();
|
||||
|
||||
if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) {
|
||||
profile.exclude = newExcludeList;
|
||||
for (const command of newExcludeList) {
|
||||
delete profile[command];
|
||||
}
|
||||
if (saveChanges) {
|
||||
await updateConnectionProfile(profile);
|
||||
} else {
|
||||
toastr.info('Press "Update" to record them into the profile.', 'Included settings list updated');
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.name !== newName) {
|
||||
toastr.success('Connection profile renamed.');
|
||||
profile.name = String(newName);
|
||||
}
|
||||
|
||||
saveSettingsDebounced();
|
||||
renderConnectionProfiles(profiles);
|
||||
await renderDetailsContent(detailsContent);
|
||||
});
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
const viewDetails = document.getElementById('view_connection_profile');
|
||||
const detailsContent = document.getElementById('connection_profile_details_content');
|
||||
viewDetails.addEventListener('click', async () => {
|
||||
viewDetails.classList.toggle('active');
|
||||
detailsContent.classList.toggle('hidden');
|
||||
await renderDetailsContent(detailsContent);
|
||||
});
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'profile',
|
||||
helpString: 'Switch to a connection profile or return the name of the current profile in no argument is provided. Use <code><None></code> to switch to no profile.',
|
||||
returns: 'name of the profile',
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'Name of the connection profile',
|
||||
enumProvider: profilesProvider,
|
||||
isRequired: false,
|
||||
}),
|
||||
],
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'await',
|
||||
description: 'Wait for the connection profile to be applied before returning.',
|
||||
isRequired: false,
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'true',
|
||||
enumList: commonEnumProviders.boolean('trueFalse')(),
|
||||
}),
|
||||
],
|
||||
callback: async (args, value) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
|
||||
if (!profile) {
|
||||
return NONE;
|
||||
}
|
||||
return profile.name;
|
||||
}
|
||||
|
||||
if (value === NONE) {
|
||||
profiles.selectedIndex = 0;
|
||||
profiles.dispatchEvent(new Event('change'));
|
||||
return NONE;
|
||||
}
|
||||
|
||||
const profile = findProfileByName(value);
|
||||
|
||||
if (!profile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const shouldAwait = !isFalseBoolean(String(args?.await));
|
||||
const awaitPromise = new Promise((resolve) => eventSource.once(event_types.CONNECTION_PROFILE_LOADED, resolve));
|
||||
|
||||
profiles.selectedIndex = Array.from(profiles.options).findIndex(o => o.value === profile.id);
|
||||
profiles.dispatchEvent(new Event('change'));
|
||||
|
||||
if (shouldAwait) {
|
||||
await awaitPromise;
|
||||
}
|
||||
|
||||
return profile.name;
|
||||
},
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'profile-list',
|
||||
helpString: 'List all connection profile names.',
|
||||
returns: 'list of profile names',
|
||||
callback: () => JSON.stringify(extension_settings.connectionManager.profiles.map(p => p.name)),
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'profile-create',
|
||||
returns: 'name of the new profile',
|
||||
helpString: 'Create a new connection profile using the current settings.',
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'name of the new connection profile',
|
||||
isRequired: true,
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
callback: async (_args, name) => {
|
||||
if (!name || typeof name !== 'string') {
|
||||
toastr.warning('Please provide a name for the new connection profile.');
|
||||
return '';
|
||||
}
|
||||
const profile = await createConnectionProfile(name);
|
||||
if (!profile) {
|
||||
return '';
|
||||
}
|
||||
extension_settings.connectionManager.profiles.push(profile);
|
||||
extension_settings.connectionManager.selectedProfile = profile.id;
|
||||
saveSettingsDebounced();
|
||||
renderConnectionProfiles(profiles);
|
||||
await renderDetailsContent(detailsContent);
|
||||
return profile.name;
|
||||
},
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'profile-update',
|
||||
helpString: 'Update the selected connection profile.',
|
||||
callback: async () => {
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
|
||||
if (!profile) {
|
||||
toastr.warning('No profile selected.');
|
||||
return '';
|
||||
}
|
||||
await updateConnectionProfile(profile);
|
||||
await renderDetailsContent(detailsContent);
|
||||
saveSettingsDebounced();
|
||||
return profile.name;
|
||||
},
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'profile-get',
|
||||
helpString: 'Get the details of the connection profile. Returns the selected profile if no argument is provided.',
|
||||
returns: 'object of the selected profile',
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'Name of the connection profile',
|
||||
enumProvider: profilesProvider,
|
||||
isRequired: false,
|
||||
}),
|
||||
],
|
||||
callback: async (_args, value) => {
|
||||
if (!value || typeof value !== 'string') {
|
||||
const selectedProfile = extension_settings.connectionManager.selectedProfile;
|
||||
const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile);
|
||||
if (!profile) {
|
||||
return '';
|
||||
}
|
||||
return JSON.stringify(profile);
|
||||
}
|
||||
|
||||
const profile = findProfileByName(value);
|
||||
if (!profile) {
|
||||
return '';
|
||||
}
|
||||
return JSON.stringify(profile);
|
||||
},
|
||||
}));
|
||||
})();
|
11
public/scripts/extensions/connection-manager/manifest.json
Normal file
11
public/scripts/extensions/connection-manager/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"display_name": "Connection Profiles",
|
||||
"loading_order": 1,
|
||||
"requires": [],
|
||||
"optional": [],
|
||||
"js": "index.js",
|
||||
"css": "style.css",
|
||||
"author": "Cohee1207",
|
||||
"version": "1.0.0",
|
||||
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||
}
|
22
public/scripts/extensions/connection-manager/profile.html
Normal file
22
public/scripts/extensions/connection-manager/profile.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<div>
|
||||
<h2 data-i18n="Creating a Connection Profile">
|
||||
Creating a Connection Profile
|
||||
</h2>
|
||||
<div class="justifyLeft flex-container flexFlowColumn flexNoGap">
|
||||
{{#each profile}}
|
||||
<label class="checkbox_label">
|
||||
<input type="checkbox" value="{{@key}}" name="exclude" checked>
|
||||
<span><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</span>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="marginTop5">
|
||||
<small>
|
||||
<b>Hint:</b>
|
||||
<i>Click on the setting name to omit it from the profile.</i>
|
||||
</small>
|
||||
</div>
|
||||
<h3 data-i18n="Enter a name:">
|
||||
Enter a name:
|
||||
</h3>
|
||||
</div>
|
21
public/scripts/extensions/connection-manager/settings.html
Normal file
21
public/scripts/extensions/connection-manager/settings.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<div class="wide100p">
|
||||
<div class="flex-container alignItemsBaseline">
|
||||
<h3 class="margin0">
|
||||
<span data-i18n="Connection Profile">Connection Profile</span>
|
||||
<a href="https://docs.sillytavern.app/usage/core-concepts/connection-profiles" target="_blank" class="notes-link">
|
||||
<span class="fa-solid fa-circle-question note-link-span"></span>
|
||||
</a>
|
||||
</h3>
|
||||
<i id="connection_profile_spinner" class="fa-solid fa-spinner fa-spin hidden"></i>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<select class="text_pole flex1" id="connection_profiles"></select>
|
||||
<i id="view_connection_profile" class="menu_button fa-solid fa-info-circle" title="View connection profile details" data-i18n="[title]View connection profile details"></i>
|
||||
<i id="create_connection_profile" class="menu_button fa-solid fa-file-circle-plus" title="Create a new connection profile" data-i18n="[title]Create a new connection profile"></i>
|
||||
<i id="update_connection_profile" class="menu_button fa-solid fa-save" title="Update a connection profile" data-i18n="[title]Update a connection profile"></i>
|
||||
<i id="edit_connection_profile" class="menu_button fa-solid fa-pencil" title="Edit a connection profile" data-i18n="[title]Edit a connection profile"></i>
|
||||
<i id="reload_connection_profile" class="menu_button fa-solid fa-recycle" title="Reload a connection profile" data-i18n="[title]Reload a connection profile"></i>
|
||||
<i id="delete_connection_profile" class="menu_button fa-solid fa-trash-can" title="Delete a connection profile" data-i18n="[title]Delete a connection profile"></i>
|
||||
</div>
|
||||
<div id="connection_profile_details_content" class="hidden"></div>
|
||||
</div>
|
11
public/scripts/extensions/connection-manager/style.css
Normal file
11
public/scripts/extensions/connection-manager/style.css
Normal file
@@ -0,0 +1,11 @@
|
||||
#connection_profile_details_content {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#connection_profile_details_content ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#connection_profile_spinner {
|
||||
margin-left: 5px;
|
||||
}
|
10
public/scripts/extensions/connection-manager/view.html
Normal file
10
public/scripts/extensions/connection-manager/view.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<ul>
|
||||
{{#each profile}}
|
||||
<li><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if omitted}}
|
||||
<div class="margin5">
|
||||
<strong data-i18n="Omitted Settings:">Omitted Settings:</strong> <span>{{omitted}}</span>
|
||||
</div>
|
||||
{{/if}}
|
@@ -8,7 +8,7 @@ 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 { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { isFunctionCallingSupported } from '../../openai.js';
|
||||
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
|
||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
@@ -20,7 +20,7 @@ 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_LLM_PROMPT = 'Ignore previous instructions. 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',
|
||||
'admiration',
|
||||
@@ -52,6 +52,7 @@ const DEFAULT_EXPRESSIONS = [
|
||||
'surprise',
|
||||
'neutral',
|
||||
];
|
||||
/** @enum {number} */
|
||||
const EXPRESSION_API = {
|
||||
local: 0,
|
||||
extras: 1,
|
||||
@@ -920,18 +921,24 @@ async function setSpriteSetCommand(_, folder) {
|
||||
return '';
|
||||
}
|
||||
|
||||
async function classifyCommand(_, text) {
|
||||
async function classifyCallback(/** @type {{api: string?, prompt: string?}} */ { api = null, prompt = null }, text) {
|
||||
if (!text) {
|
||||
console.log('No text provided');
|
||||
toastr.warning('No text provided');
|
||||
return '';
|
||||
}
|
||||
if (api && !Object.keys(EXPRESSION_API).includes(api)) {
|
||||
toastr.warning('Invalid API provided');
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) {
|
||||
const expressionApi = EXPRESSION_API[api] || extension_settings.expressions.api;
|
||||
|
||||
if (!modules.includes('classify') && expressionApi == EXPRESSION_API.extras) {
|
||||
toastr.warning('Text classification is disabled or not available');
|
||||
return '';
|
||||
}
|
||||
|
||||
const label = getExpressionLabel(text);
|
||||
const label = await getExpressionLabel(text, expressionApi, { customPrompt: prompt });
|
||||
console.debug(`Classification result for "${text}": ${label}`);
|
||||
return label;
|
||||
}
|
||||
@@ -1108,9 +1115,18 @@ function onTextGenSettingsReady(args) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getExpressionLabel(text) {
|
||||
/**
|
||||
* Retrieves the label of an expression via classification based on the provided text.
|
||||
* Optionally allows to override the expressions API being used.
|
||||
* @param {string} text - The text to classify and retrieve the expression label for.
|
||||
* @param {EXPRESSION_API} [expressionsApi=extension_settings.expressions.api] - The expressions API to use for classification.
|
||||
* @param {object} [options={}] - Optional arguments.
|
||||
* @param {string?} [options.customPrompt=null] - The custom prompt to use for classification.
|
||||
* @returns {Promise<string>} - The label of the expression.
|
||||
*/
|
||||
export async function getExpressionLabel(text, expressionsApi = extension_settings.expressions.api, { customPrompt = null } = {}) {
|
||||
// Return if text is undefined, saving a costly fetch request
|
||||
if ((!modules.includes('classify') && extension_settings.expressions.api == EXPRESSION_API.extras) || !text) {
|
||||
if ((!modules.includes('classify') && expressionsApi == EXPRESSION_API.extras) || !text) {
|
||||
return getFallbackExpression();
|
||||
}
|
||||
|
||||
@@ -1121,7 +1137,7 @@ async function getExpressionLabel(text) {
|
||||
text = sampleClassifyText(text);
|
||||
|
||||
try {
|
||||
switch (extension_settings.expressions.api) {
|
||||
switch (expressionsApi) {
|
||||
// Local BERT pipeline
|
||||
case EXPRESSION_API.local: {
|
||||
const localResult = await fetch('/api/extra/classify', {
|
||||
@@ -1145,7 +1161,7 @@ async function getExpressionLabel(text) {
|
||||
}
|
||||
|
||||
const expressionsList = await getExpressionsList();
|
||||
const prompt = await getLlmPrompt(expressionsList);
|
||||
const prompt = substituteParamsExtended(customPrompt, { labels: expressionsList }) || await getLlmPrompt(expressionsList);
|
||||
let functionResult = null;
|
||||
eventSource.once(event_types.TEXT_COMPLETION_SETTINGS_READY, onTextGenSettingsReady);
|
||||
eventSource.once(event_types.LLM_FUNCTION_TOOL_REGISTER, onFunctionToolRegister);
|
||||
@@ -1338,7 +1354,7 @@ function getCachedExpressions() {
|
||||
return [...expressionsList, ...extension_settings.expressions.custom].filter(onlyUnique);
|
||||
}
|
||||
|
||||
async function getExpressionsList() {
|
||||
export async function getExpressionsList() {
|
||||
// Return cached list if available
|
||||
if (Array.isArray(expressionsList)) {
|
||||
return getCachedExpressions();
|
||||
@@ -2048,6 +2064,11 @@ function migrateSettings() {
|
||||
});
|
||||
eventSource.on(event_types.MOVABLE_PANELS_RESET, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.GROUP_UPDATED, updateVisualNovelModeDebounced);
|
||||
eventSource.on(event_types.EXTRAS_CONNECTED, () => {
|
||||
if (extension_settings.expressions.talkinghead) {
|
||||
setTalkingHeadState(extension_settings.expressions.talkinghead);
|
||||
}
|
||||
});
|
||||
|
||||
const localEnumProviders = {
|
||||
expressions: () => getCachedExpressions().map(expression => {
|
||||
@@ -2069,7 +2090,7 @@ function migrateSettings() {
|
||||
}),
|
||||
],
|
||||
helpString: 'Force sets the sprite for the current character.',
|
||||
returns: 'label',
|
||||
returns: 'the currently set sprite label after setting it.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'spriteoverride',
|
||||
@@ -2085,7 +2106,7 @@ function migrateSettings() {
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'lastsprite',
|
||||
callback: (_, value) => lastExpression[String(value).trim()] ?? '',
|
||||
returns: 'sprite',
|
||||
returns: 'the last set sprite / expression for the named character.',
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'character name',
|
||||
@@ -2101,11 +2122,50 @@ function migrateSettings() {
|
||||
callback: toggleTalkingHeadCommand,
|
||||
aliases: ['talkinghead'],
|
||||
helpString: 'Character Expressions: toggles <i>Image Type - talkinghead (extras)</i> on/off.',
|
||||
returns: ARGUMENT_TYPE.BOOLEAN,
|
||||
returns: 'the current state of the <i>Image Type - talkinghead (extras)</i> on/off.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'classify-expressions',
|
||||
aliases: ['expressions'],
|
||||
callback: async (args) => {
|
||||
const list = await getExpressionsList();
|
||||
switch (String(args.format).toLowerCase()) {
|
||||
case 'json':
|
||||
return JSON.stringify(list);
|
||||
default:
|
||||
return list.join(', ');
|
||||
}
|
||||
},
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'format',
|
||||
description: 'The format to return the list in: comma-separated plain text or JSON array. Default is plain text.',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
enumList: [
|
||||
new SlashCommandEnumValue('plain', null, enumTypes.enum, ', '),
|
||||
new SlashCommandEnumValue('json', null, enumTypes.enum, '[]'),
|
||||
],
|
||||
}),
|
||||
],
|
||||
returns: 'The comma-separated list of available expressions, including custom expressions.',
|
||||
helpString: 'Returns a list of available expressions, including custom expressions.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'classify',
|
||||
callback: classifyCommand,
|
||||
callback: classifyCallback,
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'api',
|
||||
description: 'The Classifier API to classify with. If not specified, the configured one will be used.',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
enumList: Object.keys(EXPRESSION_API).map(api => new SlashCommandEnumValue(api, null, enumTypes.enum)),
|
||||
}),
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'prompt',
|
||||
description: 'Custom prompt for classification. Only relevant if Classifier API is set to LLM.',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'text', [ARGUMENT_TYPE.STRING], true,
|
||||
@@ -2116,6 +2176,9 @@ function migrateSettings() {
|
||||
<div>
|
||||
Performs an emotion classification of the given text and returns a label.
|
||||
</div>
|
||||
<div>
|
||||
Allows to specify which Classifier API to perform the classification with.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
|
@@ -26,9 +26,11 @@ let paginationVisiblePages = 10;
|
||||
let paginationMaxLinesPerPage = 2;
|
||||
let galleryMaxRows = 3;
|
||||
|
||||
$('body').on('click', '.dragClose', function () {
|
||||
const relatedId = $(this).data('related-id'); // Get the ID of the related draggable
|
||||
$(`body > .draggable[id="${relatedId}"]`).remove(); // Remove the associated draggable
|
||||
// Remove all draggables associated with the gallery
|
||||
$('#movingDivs').on('click', '.dragClose', function () {
|
||||
const relatedId = $(this).data('related-id');
|
||||
if (!relatedId) return;
|
||||
$(`#movingDivs > .draggable[id="${relatedId}"]`).remove();
|
||||
});
|
||||
|
||||
const CUSTOM_GALLERY_REMOVED_EVENT = 'galleryRemoved';
|
||||
@@ -290,7 +292,7 @@ function makeMovable(id = 'gallery') {
|
||||
|
||||
$('#dragGallery').css('display', 'block');
|
||||
|
||||
$('body').append(newElement);
|
||||
$('#movingDivs').append(newElement);
|
||||
|
||||
loadMovingUIState();
|
||||
$(`.draggable[forChar="${id}"]`).css('display', 'block');
|
||||
@@ -362,8 +364,8 @@ function makeDragImg(id, url) {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Attach it to the body
|
||||
document.body.appendChild(newElement);
|
||||
// Step 3: Attach it to the movingDivs container
|
||||
document.getElementById('movingDivs').appendChild(newElement);
|
||||
|
||||
// Step 4: Call dragElement and loadMovingUIState
|
||||
const appendedElement = document.getElementById(uniqueId);
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
generateRaw,
|
||||
getMaxContextSize,
|
||||
setExtensionPrompt,
|
||||
streamingProcessor,
|
||||
} from '../../../script.js';
|
||||
import { is_group_generating, selected_group } from '../../group-chats.js';
|
||||
import { loadMovingUIState } from '../../power-user.js';
|
||||
@@ -101,7 +102,7 @@ const prompt_builders = {
|
||||
RAW_NON_BLOCKING: 2,
|
||||
};
|
||||
|
||||
const defaultPrompt = '[Pause your roleplay. Summarize the most important facts and events in the story so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.]';
|
||||
const defaultPrompt = 'Ignore previous instructions. Summarize the most important facts and events in the story so far. If a summary already exists in your memory, use that as a base and expand with new facts. Limit the summary to {{words}} words or less. Your response should include nothing but the summary.';
|
||||
const defaultTemplate = '[Summary: {{summary}}]';
|
||||
|
||||
const defaultSettings = {
|
||||
@@ -408,8 +409,8 @@ async function onChatEvent() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generation is in progress, summary prevented
|
||||
if (is_send_press) {
|
||||
// Streaming in-progress
|
||||
if (streamingProcessor && !streamingProcessor.isFinished) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -446,15 +447,9 @@ async function onChatEvent() {
|
||||
delete chat[chat.length - 1].extra.memory;
|
||||
}
|
||||
|
||||
try {
|
||||
await summarizeChat(context);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
finally {
|
||||
saveLastValues();
|
||||
}
|
||||
summarizeChat(context)
|
||||
.catch(console.error)
|
||||
.finally(saveLastValues);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -567,7 +562,7 @@ async function getSummaryPromptForNow(context, force) {
|
||||
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
|
||||
}
|
||||
// Wait for the send button to be released
|
||||
waitUntilCondition(() => is_send_press === false, 30000, 100);
|
||||
await waitUntilCondition(() => is_send_press === false, 30000, 100);
|
||||
} catch {
|
||||
console.debug('Timeout waiting for is_send_press');
|
||||
return '';
|
||||
@@ -650,19 +645,29 @@ async function summarizeChatWebLLM(context, force) {
|
||||
params.max_tokens = extension_settings.memory.overrideResponseLength;
|
||||
}
|
||||
|
||||
const summary = await generateWebLlmChatPrompt(messages, params);
|
||||
const newContext = getContext();
|
||||
try {
|
||||
inApiCall = true;
|
||||
const summary = await generateWebLlmChatPrompt(messages, params);
|
||||
const newContext = getContext();
|
||||
|
||||
// something changed during summarization request
|
||||
if (newContext.groupId !== context.groupId ||
|
||||
newContext.chatId !== context.chatId ||
|
||||
(!newContext.groupId && (newContext.characterId !== context.characterId))) {
|
||||
console.log('Context changed, summary discarded');
|
||||
return;
|
||||
if (!summary) {
|
||||
console.warn('Empty summary received');
|
||||
return;
|
||||
}
|
||||
|
||||
// something changed during summarization request
|
||||
if (newContext.groupId !== context.groupId ||
|
||||
newContext.chatId !== context.chatId ||
|
||||
(!newContext.groupId && (newContext.characterId !== context.characterId))) {
|
||||
console.log('Context changed, summary discarded');
|
||||
return;
|
||||
}
|
||||
|
||||
setMemoryContext(summary, true, lastUsedIndex);
|
||||
return summary;
|
||||
} finally {
|
||||
inApiCall = false;
|
||||
}
|
||||
|
||||
setMemoryContext(summary, true, lastUsedIndex);
|
||||
return summary;
|
||||
}
|
||||
|
||||
async function summarizeChatMain(context, force, skipWIAN) {
|
||||
@@ -677,12 +682,18 @@ async function summarizeChatMain(context, force, skipWIAN) {
|
||||
let index = null;
|
||||
|
||||
if (prompt_builders.DEFAULT === extension_settings.memory.prompt_builder) {
|
||||
summary = await generateQuietPrompt(prompt, false, skipWIAN, '', '', extension_settings.memory.overrideResponseLength);
|
||||
try {
|
||||
inApiCall = true;
|
||||
summary = await generateQuietPrompt(prompt, false, skipWIAN, '', '', extension_settings.memory.overrideResponseLength);
|
||||
} finally {
|
||||
inApiCall = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ([prompt_builders.RAW_BLOCKING, prompt_builders.RAW_NON_BLOCKING].includes(extension_settings.memory.prompt_builder)) {
|
||||
const lock = extension_settings.memory.prompt_builder === prompt_builders.RAW_BLOCKING;
|
||||
try {
|
||||
inApiCall = true;
|
||||
if (lock) {
|
||||
deactivateSendButtons();
|
||||
}
|
||||
@@ -700,12 +711,18 @@ async function summarizeChatMain(context, force, skipWIAN) {
|
||||
summary = await generateRaw(rawPrompt, '', false, false, prompt, extension_settings.memory.overrideResponseLength);
|
||||
index = lastUsedIndex;
|
||||
} finally {
|
||||
inApiCall = false;
|
||||
if (lock) {
|
||||
activateSendButtons();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!summary) {
|
||||
console.warn('Empty summary received');
|
||||
return;
|
||||
}
|
||||
|
||||
const newContext = getContext();
|
||||
|
||||
// something changed during summarization request
|
||||
@@ -840,6 +857,11 @@ async function summarizeChatExtras(context) {
|
||||
const summary = await callExtrasSummarizeAPI(resultingString);
|
||||
const newContext = getContext();
|
||||
|
||||
if (!summary) {
|
||||
console.warn('Empty summary received');
|
||||
return;
|
||||
}
|
||||
|
||||
// something changed during summarization request
|
||||
if (newContext.groupId !== context.groupId
|
||||
|| newContext.chatId !== context.chatId
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
<div class="flex-container justifyspacebetween alignitemscenter">
|
||||
<span class="flex1" data-i18n="ext_sum_current_summary">Current summary:</span>
|
||||
<div id="memory_restore" class="menu_button flex1 margin0">
|
||||
<div id="memory_restore" class="menu_button flex1 margin0" data-i18n="[title]ext_sum_restore_tip" title="Restore a previous summary; use repeatedly to clear summarization state for this chat.">
|
||||
<span data-i18n="ext_sum_restore_previous">Restore Previous</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -360,7 +360,7 @@ export class QuickReply {
|
||||
del.classList.add('fa-solid');
|
||||
del.classList.add('fa-trash-can');
|
||||
del.classList.add('redWarningBG');
|
||||
del.title = 'Remove Quick Reply\n---\nShit+Click to skip confirmation';
|
||||
del.title = 'Remove Quick Reply\n---\nShift+Click to skip confirmation';
|
||||
del.addEventListener('click', async(evt)=>{
|
||||
if (!evt.shiftKey) {
|
||||
const result = await Popup.show.confirm(
|
||||
@@ -1904,6 +1904,7 @@ export class QuickReply {
|
||||
executeOnAi: this.executeOnAi,
|
||||
executeOnChatChange: this.executeOnChatChange,
|
||||
executeOnGroupMemberDraft: this.executeOnGroupMemberDraft,
|
||||
executeOnNewChat: this.executeOnNewChat,
|
||||
automationId: this.automationId,
|
||||
};
|
||||
}
|
||||
|
@@ -35,6 +35,7 @@ export class SlashCommandHandler {
|
||||
if (qr.executeOnUser) icons += enumIcons.user;
|
||||
if (qr.executeOnAi) icons += enumIcons.assistant;
|
||||
if (qr.executeOnChatChange) icons += '💬';
|
||||
if (qr.executeOnNewChat) icons += '🆕';
|
||||
if (qr.executeOnGroupMemberDraft) icons += enumIcons.group;
|
||||
return icons;
|
||||
}
|
||||
@@ -265,6 +266,7 @@ export class SlashCommandHandler {
|
||||
new SlashCommandNamedArgument('user', 'auto execute on user message, e.g., user=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('bot', 'auto execute on AI message, e.g., bot=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('load', 'auto execute on chat load, e.g., load=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('new', 'auto execute on new chat, e.g., new=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('group', 'auto execute on group member selection, e.g., group=true', [ARGUMENT_TYPE.BOOLEAN], false, false, 'false'),
|
||||
new SlashCommandNamedArgument('title', 'title / tooltip to be shown on button, e.g., title="My Fancy Button"', [ARGUMENT_TYPE.STRING], false),
|
||||
];
|
||||
@@ -857,6 +859,7 @@ export class SlashCommandHandler {
|
||||
executeOnUser: isTrueBoolean(args.user),
|
||||
executeOnAi: isTrueBoolean(args.bot),
|
||||
executeOnChatChange: isTrueBoolean(args.load),
|
||||
executeOnNewChat: isTrueBoolean(args.new),
|
||||
executeOnGroupMemberDraft: isTrueBoolean(args.group),
|
||||
automationId: args.automationId ?? '',
|
||||
},
|
||||
@@ -889,6 +892,7 @@ export class SlashCommandHandler {
|
||||
executeOnAi: args.bot === undefined ? undefined : isTrueBoolean(args.bot),
|
||||
executeOnChatChange: args.load === undefined ? undefined : isTrueBoolean(args.load),
|
||||
executeOnGroupMemberDraft: args.group === undefined ? undefined : isTrueBoolean(args.group),
|
||||
executeOnNewChat: args.new === undefined ? undefined : isTrueBoolean(args.new),
|
||||
automationId: args.automationId ?? '',
|
||||
},
|
||||
);
|
||||
|
@@ -13,14 +13,14 @@ import { createThumbnail, isValidUrl } from '../utils.js';
|
||||
*/
|
||||
export async function getMultimodalCaption(base64Img, prompt) {
|
||||
const useReverseProxy =
|
||||
(['openai', 'anthropic', 'google'].includes(extension_settings.caption.multimodal_api))
|
||||
(['openai', 'anthropic', 'google', 'mistral'].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);
|
||||
const noPrefix = ['ollama', 'llamacpp'].includes(extension_settings.caption.multimodal_api);
|
||||
|
||||
if (noPrefix && base64Img.startsWith('data:image/')) {
|
||||
base64Img = base64Img.split(',')[1];
|
||||
@@ -28,7 +28,6 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
||||
|
||||
// OpenRouter has a payload limit of ~2MB. Google is 4MB, but we love democracy.
|
||||
// Ooba requires all images to be JPEGs. Koboldcpp just asked nicely.
|
||||
const isGoogle = extension_settings.caption.multimodal_api === 'google';
|
||||
const isOllama = extension_settings.caption.multimodal_api === 'ollama';
|
||||
const isLlamaCpp = extension_settings.caption.multimodal_api === 'llamacpp';
|
||||
const isCustom = extension_settings.caption.multimodal_api === 'custom';
|
||||
@@ -37,13 +36,9 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
||||
const isVllm = extension_settings.caption.multimodal_api === 'vllm';
|
||||
const base64Bytes = base64Img.length * 0.75;
|
||||
const compressionLimit = 2 * 1024 * 1024;
|
||||
if ((['google', 'openrouter'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
|
||||
if ((['google', 'openrouter', 'mistral'].includes(extension_settings.caption.multimodal_api) && base64Bytes > compressionLimit) || isOoba || isKoboldCpp) {
|
||||
const maxSide = 1024;
|
||||
base64Img = await createThumbnail(base64Img, maxSide, maxSide, 'image/jpeg');
|
||||
|
||||
if (isGoogle) {
|
||||
base64Img = base64Img.split(',')[1];
|
||||
}
|
||||
}
|
||||
|
||||
const proxyUrl = useReverseProxy ? oai_settings.reverse_proxy : '';
|
||||
@@ -144,6 +139,10 @@ function throwIfInvalidModel(useReverseProxy) {
|
||||
throw new Error('Google AI Studio API key is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multi_modal_api === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) {
|
||||
throw new Error('Mistral AI API key is not set.');
|
||||
}
|
||||
|
||||
if (extension_settings.caption.multimodal_api === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA]) {
|
||||
throw new Error('Ollama server URL is not set.');
|
||||
}
|
||||
|
@@ -125,15 +125,14 @@ const messageTrigger = {
|
||||
const promptTemplates = {
|
||||
// Not really a prompt template, rather an outcome message template
|
||||
[generationMode.MESSAGE]: '[{{char}} sends a picture that contains: {{prompt}}].',
|
||||
/*OLD: [generationMode.CHARACTER]: "Pause your roleplay and provide comma-delimited list of phrases and keywords which describe {{char}}'s physical appearance and clothing. Ignore {{char}}'s personality traits, and chat history when crafting this description. End your response once the comma-delimited list is complete. Do not roleplay when writing this description, and do not attempt to continue the story.", */
|
||||
[generationMode.CHARACTER]: '[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\']',
|
||||
[generationMode.CHARACTER]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'',
|
||||
//face-specific prompt
|
||||
[generationMode.FACE]: '[In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'close up facial portrait,\']',
|
||||
[generationMode.FACE]: 'In the next response I want you to provide only a detailed comma-delimited list of keywords and phrases which describe {{char}}. The list must include all of the following items in this order: name, species and race, gender, age, facial features and expressions, occupation, hair and hair accessories (if any), what they are wearing on their upper body (if anything). Do not describe anything below their neck. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'close up facial portrait,\'',
|
||||
//prompt for only the last message
|
||||
[generationMode.USER]: '[Pause your roleplay and provide a detailed description of {{user}}\'s physical appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'. Ignore the rest of the story when crafting this description. Do not roleplay as {{char}} when writing this description, and do not attempt to continue the story.]',
|
||||
[generationMode.SCENARIO]: '[Pause your roleplay and provide a detailed description for all of the following: a brief recap of recent events in the story, {{char}}\'s appearance, and {{char}}\'s surroundings. Do not roleplay while writing this description.]',
|
||||
[generationMode.USER]: 'Ignore previous instructions and provide a detailed description of {{user}}\'s physical appearance from the perspective of {{char}} in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: name, species and race, gender, age, clothing, occupation, physical features and appearances. Do not include descriptions of non-visual qualities such as personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'full body portrait,\'. Ignore the rest of the story when crafting this description. Do not reply as {{char}} when writing this description, and do not attempt to continue the story.',
|
||||
[generationMode.SCENARIO]: 'Ignore previous instructions and provide a detailed description for all of the following: a brief recap of recent events in the story, {{char}}\'s appearance, and {{char}}\'s surroundings. Do not reply as {{char}} while writing this description.',
|
||||
|
||||
[generationMode.NOW]: `[Pause your roleplay. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message.
|
||||
[generationMode.NOW]: `Ignore previous instructions. Your next response must be formatted as a single comma-delimited list of concise keywords. The list will describe of the visual details included in the last chat message.
|
||||
|
||||
Only mention characters by using pronouns ('he','his','she','her','it','its') or neutral nouns ('male', 'the man', 'female', 'the woman').
|
||||
|
||||
@@ -155,14 +154,14 @@ const promptTemplates = {
|
||||
If character actions involve direct physical interaction with another character, mention specifically which body parts interacting and how.
|
||||
|
||||
A correctly formatted example response would be:
|
||||
'(location),(character list by gender),(primary action), (relative character position) POV, (character 1's description and actions), (character 2's description and actions)']`,
|
||||
'(location),(character list by gender),(primary action), (relative character position) POV, (character 1's description and actions), (character 2's description and actions)'`,
|
||||
|
||||
[generationMode.RAW_LAST]: '[Pause your roleplay and provide ONLY the last chat message string back to me verbatim. Do not write anything after the string. Do not roleplay at all in your response. Do not continue the roleplay story.]',
|
||||
[generationMode.BACKGROUND]: '[Pause your roleplay and provide a detailed description of {{char}}\'s surroundings in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: location, time of day, weather, lighting, and any other relevant details. Do not include descriptions of characters and non-visual qualities such as names, personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'background,\'. Ignore the rest of the story when crafting this description. Do not roleplay as {{user}} when writing this description, and do not attempt to continue the story.]',
|
||||
[generationMode.RAW_LAST]: 'Ignore previous instructions and provide ONLY the last chat message string back to me verbatim. Do not write anything after the string. Do not reply as {{char}} when writing this description, and do not attempt to continue the story.',
|
||||
[generationMode.BACKGROUND]: 'Ignore previous instructions and provide a detailed description of {{char}}\'s surroundings in the form of a comma-delimited list of keywords and phrases. The list must include all of the following items in this order: location, time of day, weather, lighting, and any other relevant details. Do not include descriptions of characters and non-visual qualities such as names, personality, movements, scents, mental traits, or anything which could not be seen in a still photograph. Do not write in full sentences. Prefix your description with the phrase \'background,\'. Ignore the rest of the story when crafting this description. Do not reply as {{char}} when writing this description, and do not attempt to continue the story.',
|
||||
[generationMode.FACE_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "close-up portrait".',
|
||||
[generationMode.CHARACTER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".',
|
||||
[generationMode.USER_MULTIMODAL]: 'Provide an exhaustive comma-separated list of tags describing the appearance of the character on this image in great detail. Start with "full body portrait".',
|
||||
[generationMode.FREE_EXTENDED]: 'Pause your roleplay and provide an exhaustive comma-separated list of tags describing the appearance of "{0}" in great detail. Start with {{charPrefix}} (sic) if the subject is associated with {{char}}.',
|
||||
[generationMode.FREE_EXTENDED]: 'Ignore previous instructions and provide an exhaustive comma-separated list of tags describing the appearance of "{0}" in great detail. Start with {{charPrefix}} (sic) if the subject is associated with {{char}}.',
|
||||
};
|
||||
|
||||
const defaultPrefix = 'best quality, absurdres, aesthetic,';
|
||||
@@ -283,7 +282,6 @@ const defaultSettings = {
|
||||
|
||||
// Pollinations settings
|
||||
pollinations_enhance: false,
|
||||
pollinations_refine: false,
|
||||
|
||||
// Visibility toggles
|
||||
wand_visible: false,
|
||||
@@ -425,7 +423,6 @@ async function loadSettings() {
|
||||
$('#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);
|
||||
$('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw);
|
||||
$('#sd_horde_karras').prop('checked', extension_settings.sd.horde_karras);
|
||||
@@ -724,7 +721,7 @@ function onChatChanged() {
|
||||
}
|
||||
|
||||
async function adjustElementScrollHeight() {
|
||||
if (!$('.sd_settings').is(':visible')) {
|
||||
if (CSS.supports('field-sizing', 'content') || !$('.sd_settings').is(':visible')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -737,17 +734,19 @@ async function adjustElementScrollHeight() {
|
||||
async function onCharacterPromptInput() {
|
||||
const key = getCharaFilename(this_chid);
|
||||
extension_settings.sd.character_prompts[key] = $('#sd_character_prompt').val();
|
||||
await resetScrollHeight($(this));
|
||||
saveSettingsDebounced();
|
||||
writePromptFieldsDebounced(this_chid);
|
||||
if (CSS.supports('field-sizing', 'content')) return;
|
||||
await resetScrollHeight($(this));
|
||||
}
|
||||
|
||||
async function onCharacterNegativePromptInput() {
|
||||
const key = getCharaFilename(this_chid);
|
||||
extension_settings.sd.character_negative_prompts[key] = $('#sd_character_negative_prompt').val();
|
||||
await resetScrollHeight($(this));
|
||||
saveSettingsDebounced();
|
||||
writePromptFieldsDebounced(this_chid);
|
||||
if (CSS.supports('field-sizing', 'content')) return;
|
||||
await resetScrollHeight($(this));
|
||||
}
|
||||
|
||||
function getCharacterPrefix() {
|
||||
@@ -856,14 +855,16 @@ function onStepsInput() {
|
||||
|
||||
async function onPromptPrefixInput() {
|
||||
extension_settings.sd.prompt_prefix = $('#sd_prompt_prefix').val();
|
||||
await resetScrollHeight($(this));
|
||||
saveSettingsDebounced();
|
||||
if (CSS.supports('field-sizing', 'content')) return;
|
||||
await resetScrollHeight($(this));
|
||||
}
|
||||
|
||||
async function onNegativePromptInput() {
|
||||
extension_settings.sd.negative_prompt = $('#sd_negative_prompt').val();
|
||||
await resetScrollHeight($(this));
|
||||
saveSettingsDebounced();
|
||||
if (CSS.supports('field-sizing', 'content')) return;
|
||||
await resetScrollHeight($(this));
|
||||
}
|
||||
|
||||
function onSamplerChange() {
|
||||
@@ -1005,11 +1006,6 @@ function onPollinationsEnhanceInput() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onPollinationsRefineInput() {
|
||||
extension_settings.sd.pollinations_refine = !!$('#sd_pollinations_refine').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onHordeNsfwInput() {
|
||||
extension_settings.sd.horde_nsfw = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
@@ -1688,16 +1684,17 @@ async function loadStabilityModels() {
|
||||
}
|
||||
|
||||
async function loadPollinationsModels() {
|
||||
return [
|
||||
{
|
||||
value: 'flux',
|
||||
text: 'FLUX.1 [schnell]',
|
||||
},
|
||||
{
|
||||
value: 'turbo',
|
||||
text: 'SDXL Turbo',
|
||||
},
|
||||
];
|
||||
const result = await fetch('/api/sd/pollinations/models', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const data = await result.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadTogetherAIModels() {
|
||||
@@ -2735,7 +2732,6 @@ async function generatePollinationsImage(prompt, negativePrompt, signal) {
|
||||
width: extension_settings.sd.width,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
@@ -3752,7 +3748,7 @@ async function onImageSwiped({ message, element, direction }) {
|
||||
const generationType = message?.extra?.generationType ?? generationMode.FREE;
|
||||
const dimensions = setTypeSpecificDimensions(generationType);
|
||||
const originalSeed = extension_settings.sd.seed;
|
||||
extension_settings.sd.seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
extension_settings.sd.seed = Math.round(Math.random() * (Math.pow(2, 32) - 1));
|
||||
let imagePath = '';
|
||||
|
||||
try {
|
||||
@@ -3816,7 +3812,10 @@ jQuery(async () => {
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Requests to generate an image and posts it to chat (unless <code>quiet=true</code> argument is specified).</code>.
|
||||
Requests to generate an image and posts it to chat (unless <code>quiet=true</code> argument is specified).
|
||||
</div>
|
||||
<div>
|
||||
Supported arguments: <code>${Object.values(triggerWords).flat().join(', ')}</code>.
|
||||
</div>
|
||||
<div>
|
||||
Anything else would trigger a "free mode" to make generate whatever you prompted. Example: <code>/imagine apple tree</code> would generate a picture of an apple tree. Returns a link to the generated image.
|
||||
@@ -3882,7 +3881,6 @@ jQuery(async () => {
|
||||
$('#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);
|
||||
$('#sd_comfy_url').on('input', onComfyUrlInput);
|
||||
$('#sd_comfy_workflow').on('change', onComfyWorkflowChange);
|
||||
@@ -3911,12 +3909,14 @@ jQuery(async () => {
|
||||
$('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
|
||||
$('#sd_huggingface_model_id').on('input', onHFModelInput);
|
||||
|
||||
$('.sd_settings .inline-drawer-toggle').on('click', function () {
|
||||
initScrollHeight($('#sd_prompt_prefix'));
|
||||
initScrollHeight($('#sd_negative_prompt'));
|
||||
initScrollHeight($('#sd_character_prompt'));
|
||||
initScrollHeight($('#sd_character_negative_prompt'));
|
||||
});
|
||||
if (!CSS.supports('field-sizing', 'content')) {
|
||||
$('.sd_settings .inline-drawer-toggle').on('click', function () {
|
||||
initScrollHeight($('#sd_prompt_prefix'));
|
||||
initScrollHeight($('#sd_negative_prompt'));
|
||||
initScrollHeight($('#sd_character_prompt'));
|
||||
initScrollHeight($('#sd_character_negative_prompt'));
|
||||
});
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(resolutionOptions)) {
|
||||
const option = document.createElement('option');
|
||||
|
@@ -184,18 +184,12 @@
|
||||
<a href="https://pollinations.ai">Pollinations.ai</a>
|
||||
</p>
|
||||
<div class="flex-container">
|
||||
<label class="flex1 checkbox_label" for="sd_pollinations_enhance">
|
||||
<label class="flex1 checkbox_label" for="sd_pollinations_enhance" title="Enables prompt enhancing (passes prompts through an LLM to add detail).">
|
||||
<input id="sd_pollinations_enhance" type="checkbox" />
|
||||
<span data-i18n="Enhance">
|
||||
Enhance
|
||||
</span>
|
||||
</label>
|
||||
<label class="flex1 checkbox_label" for="sd_pollinations_refine">
|
||||
<input id="sd_pollinations_refine" type="checkbox" />
|
||||
<span data-i18n="Refine">
|
||||
Refine
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div data-sd-source="stability">
|
||||
@@ -355,7 +349,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-container marginTopBot5" data-sd-source="auto,vlad,extras,horde,drawthings,comfy">
|
||||
<div class="flex-container marginTopBot5" data-sd-source="auto,vlad,extras,horde,drawthings">
|
||||
<label class="flex1 checkbox_label">
|
||||
<input id="sd_restore_faces" type="checkbox" />
|
||||
<small data-i18n="Restore Faces">Restore Faces</small>
|
||||
@@ -408,16 +402,16 @@
|
||||
</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" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
|
||||
<textarea id="sd_prompt_prefix" class="text_pole textarea_compact autoSetHeight" data-i18n="[placeholder]sd_prompt_prefix_placeholder" placeholder="Use {prompt} to specify where the generated prompt will be inserted"></textarea>
|
||||
<label for="sd_negative_prompt" data-i18n="Negative common prompt prefix">Negative common prompt prefix</label>
|
||||
<textarea id="sd_negative_prompt" class="text_pole textarea_compact"></textarea>
|
||||
<textarea id="sd_negative_prompt" class="text_pole textarea_compact autoSetHeight"></textarea>
|
||||
<div id="sd_character_prompt_block">
|
||||
<label for="sd_character_prompt" data-i18n="Character-specific prompt prefix">Character-specific prompt prefix</label>
|
||||
<small data-i18n="Won't be used in groups.">Won't be used in groups.</small>
|
||||
<textarea id="sd_character_prompt" class="text_pole textarea_compact" data-i18n="[placeholder]sd_character_prompt_placeholder" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prompt prefix. Example: female, green eyes, brown hair, pink shirt"></textarea>
|
||||
<textarea id="sd_character_prompt" class="text_pole textarea_compact autoSetHeight" data-i18n="[placeholder]sd_character_prompt_placeholder" placeholder="Any characteristics that describe the currently selected character. Will be added after a common prompt prefix. Example: female, green eyes, brown hair, pink shirt"></textarea>
|
||||
<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" 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. Example: jewellery, shoes, glasses"></textarea>
|
||||
<textarea id="sd_character_negative_prompt" class="text_pole textarea_compact autoSetHeight" 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. 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">
|
||||
|
@@ -59,8 +59,10 @@ async function doTokenCounter() {
|
||||
$('#tokenized_chunks_display').text('—');
|
||||
}
|
||||
|
||||
await resetScrollHeight($('#token_counter_textarea'));
|
||||
await resetScrollHeight($('#token_counter_ids'));
|
||||
if (!CSS.supports('field-sizing', 'content')) {
|
||||
await resetScrollHeight($('#token_counter_textarea'));
|
||||
await resetScrollHeight($('#token_counter_ids'));
|
||||
}
|
||||
}, debounce_timeout.relaxed);
|
||||
dialog.find('#token_counter_textarea').on('input', () => countDebounced());
|
||||
|
||||
|
@@ -4,3 +4,8 @@
|
||||
padding: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#token_counter_textarea,
|
||||
#token_counter_ids {
|
||||
field-sizing: content;
|
||||
}
|
||||
|
@@ -598,9 +598,20 @@ jQuery(async () => {
|
||||
$(document).on('click', '.mes_translate', onMessageTranslateClick);
|
||||
$('#translate_key_button').on('click', async () => {
|
||||
const optionText = $('#translation_provider option:selected').text();
|
||||
const key = await callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT);
|
||||
const key = await callGenericPopup(`<h3>${optionText} API Key</h3>`, POPUP_TYPE.INPUT, '', {
|
||||
customButtons: [{
|
||||
text: 'Remove Key',
|
||||
appendAtEnd: true,
|
||||
result: POPUP_RESULT.NEGATIVE,
|
||||
action: async () => {
|
||||
await writeSecret(extension_settings.translate.provider, '');
|
||||
toastr.success('API Key removed');
|
||||
$('#translate_key_button').toggleClass('success', !!secret_state[extension_settings.translate.provider]);
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
if (key == false) {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -634,7 +645,7 @@ jQuery(async () => {
|
||||
}],
|
||||
});
|
||||
|
||||
if (url == false || url == '') {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -77,14 +77,14 @@ class AzureTtsProvider {
|
||||
result: POPUP_RESULT.NEGATIVE,
|
||||
action: async () => {
|
||||
await writeSecret(SECRET_KEYS.AZURE_TTS, '');
|
||||
$('#azure_tts_key').toggleClass('success', secret_state[SECRET_KEYS.AZURE_TTS]);
|
||||
$('#azure_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.AZURE_TTS]);
|
||||
toastr.success('API Key removed');
|
||||
await this.onRefreshClick();
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
if (key == false || key == '') {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
206
public/scripts/extensions/tts/cosyvoice.js
Normal file
206
public/scripts/extensions/tts/cosyvoice.js
Normal file
@@ -0,0 +1,206 @@
|
||||
import { saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export { CosyVoiceProvider };
|
||||
|
||||
class CosyVoiceProvider {
|
||||
//########//
|
||||
// Config //
|
||||
//########//
|
||||
|
||||
settings;
|
||||
ready = false;
|
||||
voices = [];
|
||||
separator = '. ';
|
||||
audioElement = document.createElement('audio');
|
||||
|
||||
/**
|
||||
* Perform any text processing before passing to TTS engine.
|
||||
* @param {string} text Input text
|
||||
* @returns {string} Processed text
|
||||
*/
|
||||
processText(text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
audioFormats = ['wav', 'ogg', 'silk', 'mp3', 'flac'];
|
||||
|
||||
languageLabels = {
|
||||
'Auto': 'auto',
|
||||
};
|
||||
|
||||
langKey2LangCode = {
|
||||
'zh': 'zh-CN',
|
||||
'en': 'en-US',
|
||||
'ja': 'ja-JP',
|
||||
'ko': 'ko-KR',
|
||||
};
|
||||
|
||||
modelTypes = {
|
||||
CosyVoice: 'CosyVoice',
|
||||
};
|
||||
|
||||
defaultSettings = {
|
||||
provider_endpoint: 'http://localhost:9880',
|
||||
format: 'wav',
|
||||
lang: 'auto',
|
||||
streaming: false,
|
||||
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `
|
||||
|
||||
<label for="tts_endpoint">Provider Endpoint:</label>
|
||||
<input id="tts_endpoint" type="text" class="text_pole" maxlength="250" height="300" value="${this.defaultSettings.provider_endpoint}"/>
|
||||
<span>Windows users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_For_Windows">CosyVoice_For_Windows</a>(Unofficial).</span><br/>
|
||||
<span>Macos Users Use <a target="_blank" href="https://github.com/v3ucn/CosyVoice_for_MacOs">CosyVoice_for_MacOs</a>(Unofficial).</span><br/>
|
||||
<br/>
|
||||
|
||||
`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
// Used when provider settings are updated from UI
|
||||
this.settings.provider_endpoint = $('#tts_endpoint').val();
|
||||
|
||||
|
||||
saveTtsProviderSettings();
|
||||
this.changeTTSSettings();
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
// Pupulate Provider UI given input settings
|
||||
if (Object.keys(settings).length == 0) {
|
||||
console.info('Using default TTS Provider settings');
|
||||
}
|
||||
|
||||
// Only accept keys defined in defaultSettings
|
||||
this.settings = this.defaultSettings;
|
||||
|
||||
for (const key in settings) {
|
||||
if (key in this.settings) {
|
||||
this.settings[key] = settings[key];
|
||||
} else {
|
||||
console.debug(`Ignoring non-user-configurable setting: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Set initial values from the settings
|
||||
$('#tts_endpoint').val(this.settings.provider_endpoint);
|
||||
|
||||
|
||||
await this.checkReady();
|
||||
|
||||
console.info('ITS: Settings loaded');
|
||||
}
|
||||
|
||||
// Perform a simple readiness check by trying to fetch voiceIds
|
||||
async checkReady() {
|
||||
await Promise.allSettled([this.fetchTtsVoiceObjects(), this.changeTTSSettings()]);
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return;
|
||||
}
|
||||
|
||||
//#################//
|
||||
// TTS Interfaces //
|
||||
//#################//
|
||||
|
||||
async getVoice(voiceName) {
|
||||
|
||||
|
||||
|
||||
if (this.voices.length == 0) {
|
||||
this.voices = await this.fetchTtsVoiceObjects();
|
||||
}
|
||||
|
||||
|
||||
|
||||
const match = this.voices.filter(
|
||||
v => v.name == voiceName,
|
||||
)[0];
|
||||
console.log(match);
|
||||
if (!match) {
|
||||
throw `TTS Voice name ${voiceName} not found`;
|
||||
}
|
||||
return match;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
//###########//
|
||||
// API CALLS //
|
||||
//###########//
|
||||
async fetchTtsVoiceObjects() {
|
||||
const response = await fetch(`${this.settings.provider_endpoint}/speakers`);
|
||||
console.info(response);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${await response.json()}`);
|
||||
}
|
||||
const responseJson = await response.json();
|
||||
|
||||
|
||||
this.voices = responseJson;
|
||||
|
||||
return responseJson;
|
||||
}
|
||||
|
||||
// Each time a parameter is changed, we change the configuration
|
||||
async changeTTSSettings() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch TTS generation from the API.
|
||||
* @param {string} inputText Text to generate TTS for
|
||||
* @param {string} voiceId Voice ID to use (model_type&speaker_id))
|
||||
* @returns {Promise<Response|string>} Fetch response
|
||||
*/
|
||||
async fetchTtsGeneration(inputText, voiceId, lang = null, forceNoStreaming = false) {
|
||||
console.info(`Generating new TTS for voice_id ${voiceId}`);
|
||||
|
||||
const streaming = this.settings.streaming;
|
||||
|
||||
const params = {
|
||||
text: inputText,
|
||||
speaker: voiceId,
|
||||
};
|
||||
|
||||
if (streaming) {
|
||||
params['streaming'] = 1;
|
||||
}
|
||||
|
||||
const url = `${this.settings.provider_endpoint}/`;
|
||||
|
||||
const response = await fetch(
|
||||
url,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(params), // Convert parameter objects to JSON strings
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
toastr.error(response.statusText, 'TTS Generation Failed');
|
||||
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Interface not used
|
||||
async fetchTtsFromHistory(history_item_id) {
|
||||
return Promise.resolve(history_item_id);
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@ import { VITSTtsProvider } from './vits.js';
|
||||
import { GSVITtsProvider } from './gsvi.js';
|
||||
import { SBVits2TtsProvider } from './sbvits2.js';
|
||||
import { AllTalkTtsProvider } from './alltalk.js';
|
||||
import { CosyVoiceProvider } from './cosyvoice.js';
|
||||
import { SpeechT5TtsProvider } from './speecht5.js';
|
||||
import { AzureTtsProvider } from './azure.js';
|
||||
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
|
||||
@@ -87,6 +88,7 @@ const ttsProviders = {
|
||||
AllTalk: AllTalkTtsProvider,
|
||||
Azure: AzureTtsProvider,
|
||||
Coqui: CoquiTtsProvider,
|
||||
'CosyVoice (Unofficial)': CosyVoiceProvider,
|
||||
Edge: EdgeTtsProvider,
|
||||
ElevenLabs: ElevenLabsTtsProvider,
|
||||
GSVI: GSVITtsProvider,
|
||||
@@ -423,7 +425,7 @@ function completeTtsJob() {
|
||||
async function tts(text, voiceId, char) {
|
||||
async function processResponse(response) {
|
||||
// RVC injection
|
||||
if (extension_settings.rvc.enabled && typeof window['rvcVoiceConversion'] === 'function')
|
||||
if (typeof window['rvcVoiceConversion'] === 'function' && extension_settings.rvc.enabled)
|
||||
response = await window['rvcVoiceConversion'](response, char, text);
|
||||
|
||||
await addAudioJob(response, char);
|
||||
|
@@ -86,14 +86,14 @@ class OpenAICompatibleTtsProvider {
|
||||
result: POPUP_RESULT.NEGATIVE,
|
||||
action: async () => {
|
||||
await writeSecret(SECRET_KEYS.CUSTOM_OPENAI_TTS, '');
|
||||
$('#openai_compatible_tts_key').toggleClass('success', secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
|
||||
$('#openai_compatible_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CUSTOM_OPENAI_TTS]);
|
||||
toastr.success('API Key removed');
|
||||
await this.onRefreshClick();
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
if (key == false || key == '') {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -56,7 +56,7 @@ const settings = {
|
||||
summarize: false,
|
||||
summarize_sent: false,
|
||||
summary_source: 'main',
|
||||
summary_prompt: 'Pause your roleplay. Summarize the most important parts of the message. Limit yourself to 250 words or less. Your response should include nothing but the summary.',
|
||||
summary_prompt: 'Ignore previous instructions. Summarize the most important parts of the message. Limit yourself to 250 words or less. Your response should include nothing but the summary.',
|
||||
force_chunk_delimiter: '',
|
||||
|
||||
// For chats
|
||||
@@ -718,7 +718,7 @@ async function getQueryText(chat, initiator) {
|
||||
async function getSavedHashes(collectionId) {
|
||||
const response = await fetch('/api/vector/list', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
headers: getVectorHeaders(),
|
||||
body: JSON.stringify({
|
||||
collectionId: collectionId,
|
||||
source: settings.source,
|
||||
@@ -737,25 +737,43 @@ function getVectorHeaders() {
|
||||
const headers = getRequestHeaders();
|
||||
switch (settings.source) {
|
||||
case 'extras':
|
||||
addExtrasHeaders(headers);
|
||||
Object.assign(headers, {
|
||||
'X-Extras-Url': extension_settings.apiUrl,
|
||||
'X-Extras-Key': extension_settings.apiKey,
|
||||
});
|
||||
break;
|
||||
case 'togetherai':
|
||||
addTogetherAiHeaders(headers);
|
||||
Object.assign(headers, {
|
||||
'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
|
||||
});
|
||||
break;
|
||||
case 'openai':
|
||||
addOpenAiHeaders(headers);
|
||||
Object.assign(headers, {
|
||||
'X-OpenAI-Model': extension_settings.vectors.openai_model,
|
||||
});
|
||||
break;
|
||||
case 'cohere':
|
||||
addCohereHeaders(headers);
|
||||
Object.assign(headers, {
|
||||
'X-Cohere-Model': extension_settings.vectors.cohere_model,
|
||||
});
|
||||
break;
|
||||
case 'ollama':
|
||||
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,
|
||||
});
|
||||
break;
|
||||
case 'llamacpp':
|
||||
addLlamaCppHeaders(headers);
|
||||
Object.assign(headers, {
|
||||
'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
|
||||
});
|
||||
break;
|
||||
case 'vllm':
|
||||
addVllmHeaders(headers);
|
||||
Object.assign(headers, {
|
||||
'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
|
||||
'X-Vllm-Model': extension_settings.vectors.vllm_model,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -763,81 +781,6 @@ function getVectorHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the Extras API source.
|
||||
* @param {object} headers Headers object
|
||||
*/
|
||||
function addExtrasHeaders(headers) {
|
||||
console.log(`Vector source is extras, populating API URL: ${extension_settings.apiUrl}`);
|
||||
Object.assign(headers, {
|
||||
'X-Extras-Url': extension_settings.apiUrl,
|
||||
'X-Extras-Key': extension_settings.apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the TogetherAI API source.
|
||||
* @param {object} headers Headers object
|
||||
*/
|
||||
function addTogetherAiHeaders(headers) {
|
||||
Object.assign(headers, {
|
||||
'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the OpenAI API source.
|
||||
* @param {object} headers Header object
|
||||
*/
|
||||
function addOpenAiHeaders(headers) {
|
||||
Object.assign(headers, {
|
||||
'X-OpenAI-Model': extension_settings.vectors.openai_model,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the Cohere API source.
|
||||
* @param {object} headers Header object
|
||||
*/
|
||||
function addCohereHeaders(headers) {
|
||||
Object.assign(headers, {
|
||||
'X-Cohere-Model': extension_settings.vectors.cohere_model,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add headers for the VLLM API source.
|
||||
* @param {object} headers Header object
|
||||
*/
|
||||
function addVllmHeaders(headers) {
|
||||
Object.assign(headers, {
|
||||
'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
|
||||
'X-Vllm-Model': extension_settings.vectors.vllm_model,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts vector items into a collection
|
||||
* @param {string} collectionId - The collection to insert into
|
||||
@@ -901,7 +844,7 @@ function throwIfSourceInvalid() {
|
||||
async function deleteVectorItems(collectionId, hashes) {
|
||||
const response = await fetch('/api/vector/delete', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
headers: getVectorHeaders(),
|
||||
body: JSON.stringify({
|
||||
collectionId: collectionId,
|
||||
hashes: hashes,
|
||||
@@ -987,7 +930,7 @@ async function purgeFileVectorIndex(fileUrl) {
|
||||
|
||||
const response = await fetch('/api/vector/purge', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
headers: getVectorHeaders(),
|
||||
body: JSON.stringify({
|
||||
collectionId: collectionId,
|
||||
}),
|
||||
@@ -1016,7 +959,7 @@ async function purgeVectorIndex(collectionId) {
|
||||
|
||||
const response = await fetch('/api/vector/purge', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
headers: getVectorHeaders(),
|
||||
body: JSON.stringify({
|
||||
collectionId: collectionId,
|
||||
}),
|
||||
@@ -1041,7 +984,7 @@ async function purgeAllVectorIndexes() {
|
||||
try {
|
||||
const response = await fetch('/api/vector/purge-all', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
headers: getVectorHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -1331,7 +1274,6 @@ jQuery(async () => {
|
||||
saveSettingsDebounced();
|
||||
toggleSettings();
|
||||
});
|
||||
$('#vectors_modelWarning').hide();
|
||||
$('#vectors_enabled_files').prop('checked', settings.enabled_files).on('input', () => {
|
||||
settings.enabled_files = $('#vectors_enabled_files').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
@@ -1371,31 +1313,26 @@ jQuery(async () => {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_togetherai_model').val(settings.togetherai_model).on('change', () => {
|
||||
$('#vectors_modelWarning').show();
|
||||
settings.togetherai_model = String($('#vectors_togetherai_model').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_openai_model').val(settings.openai_model).on('change', () => {
|
||||
$('#vectors_modelWarning').show();
|
||||
settings.openai_model = String($('#vectors_openai_model').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_cohere_model').val(settings.cohere_model).on('change', () => {
|
||||
$('#vectors_modelWarning').show();
|
||||
settings.cohere_model = String($('#vectors_cohere_model').val());
|
||||
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_vllm_model').val(settings.vllm_model).on('input', () => {
|
||||
$('#vectors_modelWarning').show();
|
||||
settings.vllm_model = String($('#vectors_vllm_model').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
|
@@ -96,13 +96,6 @@
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<small id="vectors_modelWarning">
|
||||
<i class="fa-solid fa-exclamation-triangle"></i>
|
||||
<span data-i18n="Vectors Model Warning">
|
||||
It is recommended to purge vectors when changing the model mid-chat. Otherwise, it will lead to sub-par results.
|
||||
</span>
|
||||
</small>
|
||||
|
||||
<div class="flex-container alignItemsCenter" id="nomicai_apiKey">
|
||||
<label for="api_key_nomicai" class="flex1">
|
||||
<span data-i18n="NomicAI API Key">NomicAI API Key</span>
|
||||
|
Reference in New Issue
Block a user