Merge branch 'staging' into ffmpeg-videobg

This commit is contained in:
Cohee
2025-05-04 22:05:15 +03:00
167 changed files with 3251 additions and 1779 deletions

View File

@@ -409,6 +409,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.ZEROONEAI] && oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI)
|| (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
|| (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
|| (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
) {
$('#api_button_openai').trigger('click');
@@ -1047,7 +1048,7 @@ export function initRossMods() {
//Enter to send when send_textarea in focus
if (document.activeElement == hotkeyTargets['send_textarea']) {
const sendOnEnter = shouldSendOnEnter();
if (!event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
if (!event.isComposing && !event.shiftKey && !event.ctrlKey && !event.altKey && event.key == 'Enter' && sendOnEnter) {
event.preventDefault();
sendTextareaMessage();
return;
@@ -1119,7 +1120,7 @@ export function initRossMods() {
const result = await Popup.show.confirm('Regenerate Message', 'Are you sure you want to regenerate the latest message?', {
customInputs: [{ id: 'regenerateWithCtrlEnter', label: 'Don\'t ask again' }],
onClose: (popup) => {
regenerateWithCtrlEnter = popup.inputResults.get('regenerateWithCtrlEnter') ?? false;
regenerateWithCtrlEnter = Boolean(popup.inputResults.get('regenerateWithCtrlEnter') ?? false);
},
});
if (!result) {

View File

@@ -6,6 +6,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { flashHighlight, stringFormat } from './utils.js';
import { t } from './i18n.js';
import { Popup } from './popup.js';
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
@@ -291,7 +292,7 @@ async function onDeleteBackgroundClick(e) {
const bgToDelete = $(this).closest('.bg_example');
const url = bgToDelete.data('url');
const isCustom = bgToDelete.attr('custom') === 'true';
const confirm = await callPopup('<h3>Delete the background?</h3>', 'confirm');
const confirm = await Popup.show.confirm(t`Delete the background?`, null);
const bg = bgToDelete.attr('bgfile');
if (confirm) {

View File

@@ -33,6 +33,12 @@
* @property {number} role - The specific function or purpose of the extension.
* @property {boolean} vectorized - Indicates if the extension is optimized for vectorized processing.
* @property {number} display_index - The order in which the extension should be displayed for user interfaces.
* @property {boolean} match_persona_description - Wether to match against the persona description.
* @property {boolean} match_character_description - Wether to match against the persona description.
* @property {boolean} match_character_personality - Wether to match against the character personality.
* @property {boolean} match_character_depth_prompt - Wether to match against the character depth prompt.
* @property {boolean} match_scenario - Wether to match against the character scenario.
* @property {boolean} match_creator_notes - Wether to match against the character creator notes.
*/
/**

View File

@@ -74,6 +74,11 @@ const hash_derivations = {
'b6835114b7303ddd78919a82e4d9f7d8c26ed0d7dfc36beeb12d524f6144eab1':
'DeepSeek-V2.5'
,
// THUDM-GLM 4
'854b703e44ca06bdb196cc471c728d15dbab61e744fe6cdce980086b61646ed1':
'GLM-4'
,
};
const substr_derivations = {

View File

@@ -43,10 +43,12 @@ import EventSourceStream from './sse-stream.js';
* @property {boolean?} [stream=false] - Whether to stream the response
* @property {ChatCompletionMessage[]} messages - Array of chat messages
* @property {string} [model] - Optional model name to use for completion
* @property {string} chat_completion_source - Source provider for chat completion
* @property {string} chat_completion_source - Source provider
* @property {number} max_tokens - Maximum number of tokens to generate
* @property {number} [temperature] - Optional temperature parameter for response randomness
* @property {string} [custom_url] - Optional custom URL for chat completion
* @property {string} [custom_url] - Optional custom URL
* @property {string} [reverse_proxy] - Optional reverse proxy URL
* @property {string} [proxy_password] - Optional proxy password
*/
/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
@@ -80,7 +82,6 @@ export class TextCompletionService {
*/
static createRequestData({ stream = false, prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) {
const payload = {
...props,
stream,
prompt,
max_tokens,
@@ -90,6 +91,7 @@ export class TextCompletionService {
api_server: api_server ?? getTextGenServer(api_type),
temperature,
min_p,
...props,
};
// Remove undefined values to avoid API errors
@@ -387,9 +389,8 @@ export class ChatCompletionService {
* @param {ChatCompletionPayload} custom
* @returns {ChatCompletionPayload}
*/
static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {
static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, reverse_proxy, proxy_password, ...props }) {
const payload = {
...props,
stream,
messages,
model,
@@ -397,6 +398,11 @@ export class ChatCompletionService {
max_tokens,
temperature,
custom_url,
reverse_proxy,
proxy_password,
use_makersuite_sysprompt: true,
claude_use_sysprompt: true,
...props,
};
// Remove undefined values to avoid API errors

View File

@@ -661,6 +661,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" data-i18n="[title]Delete" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
let updateButton = isExternal ? `<button class="btn_update menu_button displayNone" data-name="${externalId}" title="Update available"><i class="fa-solid fa-download fa-fw"></i></button>` : '';
let moveButton = isExternal && isUserAdmin ? `<button class="btn_move menu_button" data-name="${externalId}" data-i18n="[title]Move" title="Move"><i class="fa-solid fa-folder-tree fa-fw"></i></button>` : '';
let branchButton = isExternal && isUserAdmin ? `<button class="btn_branch menu_button" data-name="${externalId}" data-i18n="[title]Switch branch" title="Switch branch"><i class="fa-solid fa-code-branch fa-fw"></i></button>` : '';
let modulesInfo = '';
if (isActive && Array.isArray(manifest.optional)) {
@@ -701,6 +702,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
<div class="extension_actions flex-container alignItemsCenter">
${updateButton}
${branchButton}
${moveButton}
${deleteButton}
</div>
@@ -944,6 +946,44 @@ async function onDeleteClick() {
}
}
async function onBranchClick() {
const extensionName = $(this).data('name');
const isCurrentUserAdmin = isAdmin();
const isGlobal = getExtensionType(extensionName) === 'global';
if (isGlobal && !isCurrentUserAdmin) {
toastr.error(t`You don't have permission to switch branch.`);
return;
}
let newBranch = '';
const branches = await getExtensionBranches(extensionName, isGlobal);
const selectElement = document.createElement('select');
selectElement.classList.add('text_pole', 'wide100p');
selectElement.addEventListener('change', function () {
newBranch = this.value;
});
for (const branch of branches) {
const option = document.createElement('option');
option.value = branch.name;
option.textContent = `${branch.name} (${branch.commit}) [${branch.label}]`;
option.selected = branch.current;
selectElement.appendChild(option);
}
const popup = new Popup(selectElement, POPUP_TYPE.CONFIRM, '', {
okButton: t`Switch`,
cancelButton: t`Cancel`,
});
const popupResult = await popup.show();
if (!popupResult || !newBranch) {
return;
}
await switchExtensionBranch(extensionName, isGlobal, newBranch);
}
async function onMoveClick() {
const extensionName = $(this).data('name');
const isCurrentUserAdmin = isAdmin();
@@ -1055,13 +1095,83 @@ async function getExtensionVersion(extensionName, abortSignal) {
}
}
/**
* Gets the list of branches for a specific extension.
* @param {string} extensionName The name of the extension
* @param {boolean} isGlobal Whether the extension is global or not
* @returns {Promise<ExtensionBranch[]>} List of branches for the extension
* @typedef {object} ExtensionBranch
* @property {string} name The name of the branch
* @property {string} commit The commit hash of the branch
* @property {boolean} current Whether this branch is the current one
* @property {string} label The commit label of the branch
*/
async function getExtensionBranches(extensionName, isGlobal) {
try {
const response = await fetch('/api/extensions/branches', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
extensionName,
global: isGlobal,
}),
});
if (!response.ok) {
const text = await response.text();
toastr.error(text || response.statusText, t`Extension branches fetch failed`);
console.error('Extension branches fetch failed', response.status, response.statusText, text);
return [];
}
return await response.json();
} catch (error) {
console.error('Error:', error);
return [];
}
}
/**
* Switches the branch of an extension.
* @param {string} extensionName The name of the extension
* @param {boolean} isGlobal If the extension is global
* @param {string} branch Branch name to switch to
* @returns {Promise<void>}
*/
async function switchExtensionBranch(extensionName, isGlobal, branch) {
try {
const response = await fetch('/api/extensions/switch', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
extensionName,
branch,
global: isGlobal,
}),
});
if (!response.ok) {
const text = await response.text();
toastr.error(text || response.statusText, t`Extension branch switch failed`);
console.error('Extension branch switch failed', response.status, response.statusText, text);
return;
}
toastr.success(t`Extension ${extensionName} switched to ${branch}`);
await loadExtensionSettings({}, false, false);
void showExtensionsDetails();
} catch (error) {
console.error('Error:', error);
}
}
/**
* Installs a third-party extension via the API.
* @param {string} url Extension repository URL
* @param {boolean} global Is the extension global?
* @returns {Promise<void>}
*/
export async function installExtension(url, global) {
export async function installExtension(url, global, branch = '') {
console.debug('Extension installation started', url);
toastr.info(t`Please wait...`, t`Installing extension`);
@@ -1072,6 +1182,7 @@ export async function installExtension(url, global) {
body: JSON.stringify({
url,
global,
branch,
}),
});
@@ -1406,9 +1517,17 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
await popup.complete(POPUP_RESULT.AFFIRMATIVE);
},
};
/** @type {import('./popup.js').CustomPopupInput} */
const branchNameInput = {
id: 'extension_branch_name',
label: t`Branch or tag name (optional)`,
type: 'text',
tooltip: 'e.g. main, dev, v1.0.0',
};
const customButtons = isCurrentUserAdmin ? [installForAllButton] : [];
const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons });
const customInputs = [branchNameInput];
const popup = new Popup(html, POPUP_TYPE.INPUT, suggestUrl ?? '', { okButton, customButtons, customInputs });
const input = await popup.show();
if (!input) {
@@ -1417,7 +1536,8 @@ export async function openThirdPartyExtensionMenu(suggestUrl = '') {
}
const url = String(input).trim();
await installExtension(url, global);
const branchName = String(popup.inputResults.get('extension_branch_name') ?? '').trim();
await installExtension(url, global, branchName);
}
export async function initExtensions() {
@@ -1433,6 +1553,7 @@ export async function initExtensions() {
$(document).on('click', '.extensions_info .extension_block .btn_update', onUpdateClick);
$(document).on('click', '.extensions_info .extension_block .btn_delete', onDeleteClick);
$(document).on('click', '.extensions_info .extension_block .btn_move', onMoveClick);
$(document).on('click', '.extensions_info .extension_block .btn_branch', onBranchClick);
/**
* Handles the click event for the third-party extension import button.

View File

@@ -291,7 +291,7 @@ async function installAsset(url, assetType, filename) {
try {
if (category === 'extension') {
console.debug(DEBUG_PREFIX, 'Installing extension ', url);
await installExtension(url);
await installExtension(url, false);
console.debug(DEBUG_PREFIX, 'Extension installed.');
return;
}
@@ -309,7 +309,7 @@ async function installAsset(url, assetType, filename) {
console.debug(DEBUG_PREFIX, 'Importing character ', filename);
const blob = await result.blob();
const file = new File([blob], filename, { type: blob.type });
await processDroppedFiles([file], true);
await processDroppedFiles([file]);
console.debug(DEBUG_PREFIX, 'Character downloaded.');
}
}

View File

@@ -39,7 +39,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
<span data-i18n="Characters">Characters</span>
</div>
</div>
<div class="inline-drawer-content" id="assets_menu">
<div id="assets_menu">
</div>
</div>
</div>

View File

@@ -428,6 +428,7 @@ jQuery(async function () {
'zerooneai': SECRET_KEYS.ZEROONEAI,
'groq': SECRET_KEYS.GROQ,
'cohere': SECRET_KEYS.COHERE,
'xai': SECRET_KEYS.XAI,
};
if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {

View File

@@ -31,6 +31,7 @@
<option value="openrouter">OpenRouter</option>
<option value="ooba" data-i18n="Text Generation WebUI (oobabooga)">Text Generation WebUI (oobabooga)</option>
<option value="vllm">vLLM</option>
<option value="xai">xAI (Grok)</option>
</select>
</div>
<div class="flex1 flex-container flexFlowColumn flexNoGap">
@@ -46,13 +47,24 @@
<option data-type="mistral" value="mistral-small-2503">mistral-small-2503</option>
<option data-type="mistral" value="mistral-small-latest">mistral-small-latest</option>
<option data-type="zerooneai" value="yi-vision">yi-vision</option>
<option data-type="openai" value="gpt-4.1">gpt-4.1</option>
<option data-type="openai" value="gpt-4.1-2025-04-14">gpt-4.1-2025-04-14</option>
<option data-type="openai" value="gpt-4.1-mini">gpt-4.1-mini</option>
<option data-type="openai" value="gpt-4.1-mini-2025-04-14">gpt-4.1-mini-2025-04-14</option>
<option data-type="openai" value="gpt-4.1-nano">gpt-4.1-nano</option>
<option data-type="openai" value="gpt-4.1-nano-2025-04-14">gpt-4.1-nano-2025-04-14</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>
<option data-type="openai" value="gpt-4o">gpt-4o</option>
<option data-type="openai" value="gpt-4o-mini">gpt-4o-mini</option>
<option data-type="openai" value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>
<option data-type="openai" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
<option data-type="openai" value="o1">o1</option>
<option data-type="openai" value="o1-2024-12-17">o1-2024-12-17</option>
<option data-type="openai" value="o3">o3</option>
<option data-type="openai" value="o3-2025-04-16">o3-2025-04-16</option>
<option data-type="openai" value="o4-mini">o4-mini</option>
<option data-type="openai" value="o4-mini-2025-04-16">o4-mini-2025-04-16</option>
<option data-type="openai" value="gpt-4.5-preview">gpt-4.5-preview</option>
<option data-type="openai" value="gpt-4.5-preview-2025-02-27">gpt-4.5-preview-2025-02-27</option>
<option data-type="anthropic" value="claude-3-7-sonnet-latest">claude-3-7-sonnet-latest</option>
@@ -67,33 +79,33 @@
<option data-type="anthropic" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
<option data-type="google" value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
<option data-type="google" value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option>
<option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp</option>
<option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05</option>
<option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
<option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
<option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
<option data-type="google" value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
<option data-type="google" value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option>
<option data-type="google" value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option>
<option data-type="google" value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option>
<option data-type="google" value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
<option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
<option data-type="google" value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219</option>
<option data-type="google" value="gemini-1.5-flash">gemini-1.5-flash</option>
<option data-type="google" value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
<option data-type="google" value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
<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-exp-1114">gemini-exp-1114</option>
<option data-type="google" value="gemini-exp-1121">gemini-exp-1121</option>
<option data-type="google" value="gemini-exp-1206">gemini-exp-1206</option>
<option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</option>
<option data-type="google" value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
<option data-type="google" value="gemini-2.0-flash">gemini-2.0-flash</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-04-17</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-04-17</option>
<option data-type="google" value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-04-17</option>
<option data-type="google" value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
<option data-type="google" value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
<option data-type="google" value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</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-1.5-pro-001">gemini-1.5-pro-001</option>
<option data-type="google" value="gemini-1.5-pro">gemini-1.5-pro</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-002">gemini-1.5-flash-002</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">gemini-1.5-flash</option>
<option data-type="google" value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</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-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
<option data-type="google" value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>
<option data-type="google" value="learnlm-1.5-pro-experimental">learnlm-1.5-pro-experimental</option>
<option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
<option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
<option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
@@ -134,6 +146,8 @@
<option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
<option data-type="vllm" value="vllm_current" data-i18n="currently_selected">[Currently selected]</option>
<option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
<option data-type="xai" value="grok-2-vision-1212">grok-2-vision-1212</option>
<option data-type="xai" value="grok-vision-beta">grok-vision-beta</option>
</select>
</div>
<div data-type="ollama">

View File

@@ -132,7 +132,7 @@
</label>
<label class="flex-container alignItemsCenter" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="memory_position" value="1" />
<span data-i18n="In-chat @ Depth">In-chat @ Depth</span> <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
<span data-i18n="In-chat @ Depth">In-chat @ Depth</span> <input id="memory_depth" class="text_pole widthUnset" type="number" min="0" max="9999" />
<span data-i18n="as">as</span>
<select id="memory_role" class="text_pole widthNatural">
<option value="0" data-i18n="System">System</option>

View File

@@ -113,14 +113,14 @@
<span data-i18n="Min Depth">Min Depth</span>
<span class="fa-solid fa-circle-question note-link-span"></span>
</small>
<input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
<input name="min_depth" class="text_pole textarea_compact" type="number" min="-1" max="9999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
</div>
<div class="flex1 flex-container flexNoGap">
<small data-i18n="[title]ext_regex_max_depth_desc" title="When applied to prompts or display, only affect messages no more than N levels deep. 0 = last message, 1 = penultimate message, etc. System prompt and utility prompts are not affected. Max must be greater than Min for regex to apply.">
<span data-i18n="Max Depth">Max Depth</span>
<span class="fa-solid fa-circle-question note-link-span"></span>
</small>
<input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
<input name="max_depth" class="text_pole textarea_compact" type="number" min="0" max="9999" data-i18n="[placeholder]ext_regex_min_depth_placeholder" placeholder="Unlimited" />
</div>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { CONNECT_API_MAP, getRequestHeaders } from '../../script.js';
import { extension_settings, openThirdPartyExtensionMenu } from '../extensions.js';
import { t } from '../i18n.js';
import { oai_settings } from '../openai.js';
import { oai_settings, proxies } from '../openai.js';
import { SECRET_KEYS, secret_state } from '../secrets.js';
import { textgen_types, textgenerationwebui_settings } from '../textgen-settings.js';
import { getTokenCountAsync } from '../tokenizers.js';
@@ -153,6 +153,10 @@ function throwIfInvalidModel(useReverseProxy) {
throw new Error('Cohere API key is not set.');
}
if (extension_settings.caption.multimodal_api === 'xai' && !secret_state[SECRET_KEYS.XAI]) {
throw new Error('xAI 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.');
}
@@ -306,9 +310,10 @@ export class ConnectionManagerRequestService {
* @param {boolean?} [custom.includePreset=true]
* @param {boolean?} [custom.includeInstruct=true]
* @param {Partial<InstructSettings>?} [custom.instructSettings] Override instruct settings
* @param {Record<string, any>} [overridePayload] - Override payload for the request
* @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator
*/
static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) {
static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams, overridePayload = {}) {
const { stream, signal, extractData, includePreset, includeInstruct, instructSettings } = { ...this.defaultSendRequestParams, ...custom };
const context = SillyTavern.getContext();
@@ -326,6 +331,8 @@ export class ConnectionManagerRequestService {
throw new Error(`API type ${selectedApiMap.selected} does not support chat completions`);
}
const proxyPreset = proxies.find((p) => p.name === profile.proxy);
const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }];
return await context.ChatCompletionService.processRequest({
stream,
@@ -334,6 +341,9 @@ export class ConnectionManagerRequestService {
model: profile.model,
chat_completion_source: selectedApiMap.source,
custom_url: profile['api-url'],
reverse_proxy: proxyPreset?.url,
proxy_password: proxyPreset?.password,
...overridePayload,
}, {
presetName: includePreset ? profile.preset : undefined,
}, extractData, signal);
@@ -350,6 +360,7 @@ export class ConnectionManagerRequestService {
model: profile.model,
api_type: selectedApiMap.type,
api_server: profile['api-url'],
...overridePayload,
}, {
instructName: includeInstruct ? profile.instruct : undefined,
presetName: includePreset ? profile.preset : undefined,

View File

@@ -81,6 +81,7 @@ const sources = {
nanogpt: 'nanogpt',
bfl: 'bfl',
falai: 'falai',
xai: 'xai',
};
const initiators = {
@@ -1303,6 +1304,7 @@ async function onModelChange() {
sources.nanogpt,
sources.bfl,
sources.falai,
sources.xai,
];
if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1518,6 +1520,9 @@ async function loadSamplers() {
case sources.bfl:
samplers = ['N/A'];
break;
case sources.xai:
samplers = ['N/A'];
break;
}
for (const sampler of samplers) {
@@ -1708,6 +1713,9 @@ async function loadModels() {
case sources.falai:
models = await loadFalaiModels();
break;
case sources.xai:
models = await loadXAIModels();
break;
}
for (const model of models) {
@@ -1760,6 +1768,12 @@ async function loadFalaiModels() {
return [];
}
async function loadXAIModels() {
return [
{ value: 'grok-2-image-1212', text: 'grok-2-image-1212' },
];
}
async function loadPollinationsModels() {
const result = await fetch('/api/sd/pollinations/models', {
method: 'POST',
@@ -2081,6 +2095,9 @@ async function loadSchedulers() {
case sources.falai:
schedulers = ['N/A'];
break;
case sources.xai:
schedulers = ['N/A'];
break;
}
for (const scheduler of schedulers) {
@@ -2166,6 +2183,12 @@ async function loadVaes() {
case sources.bfl:
vaes = ['N/A'];
break;
case sources.falai:
vaes = ['N/A'];
break;
case sources.xai:
vaes = ['N/A'];
break;
}
for (const vae of vaes) {
@@ -2735,6 +2758,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
case sources.falai:
result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
break;
case sources.xai:
result = await generateXAIImage(prefixedPrompt, negativePrompt, signal);
break;
}
if (!result.data) {
@@ -3463,6 +3489,33 @@ async function generateBflImage(prompt, signal) {
}
}
/**
* Generates an image using the xAI API.
* @param {string} prompt The main instruction used to guide the image generation.
* @param {string} _negativePrompt Negative prompt is not used in this API
* @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request.
* @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
*/
async function generateXAIImage(prompt, _negativePrompt, signal) {
const result = await fetch('/api/sd/xai/generate', {
method: 'POST',
headers: getRequestHeaders(),
signal: signal,
body: JSON.stringify({
prompt: prompt,
model: extension_settings.sd.model,
}),
});
if (result.ok) {
const data = await result.json();
return { format: 'jpg', data: data.image };
} else {
const text = await result.text();
throw new Error(text);
}
}
/**
* Generates an image using the FAL.AI API.
* @param {string} prompt - The main instruction used to guide the image generation.
@@ -3782,6 +3835,8 @@ function isValidState() {
return secret_state[SECRET_KEYS.BFL];
case sources.falai:
return secret_state[SECRET_KEYS.FALAI];
case sources.xai:
return secret_state[SECRET_KEYS.XAI];
}
}

View File

@@ -52,6 +52,7 @@
<option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
<option value="horde">Stable Horde</option>
<option value="togetherai">TogetherAI</option>
<option value="xai">xAI (Grok)</option>
</select>
<div data-sd-source="auto">
<label for="sd_auto_url">SD Web UI URL</label>

View File

@@ -76,7 +76,7 @@
<div id="tts_voicemap_block">
</div>
<hr>
<form id="tts_provider_settings" class="inline-drawer-content">
<form id="tts_provider_settings">
</form>
<div class="tts_buttons">
<input id="tts_voices" class="menu_button" type="submit" value="Available voices" />

View File

@@ -79,6 +79,10 @@ class SystemTtsProvider {
// Config //
//########//
// Static constants for the simulated default voice
static BROWSER_DEFAULT_VOICE_ID = '__browser_default__';
static BROWSER_DEFAULT_VOICE_NAME = 'System Default Voice';
settings;
ready = false;
voices = [];
@@ -168,51 +172,97 @@ class SystemTtsProvider {
//#################//
fetchTtsVoiceObjects() {
if (!('speechSynthesis' in window)) {
return [];
return Promise.resolve([]);
}
return new Promise((resolve) => {
setTimeout(() => {
const voices = speechSynthesis
.getVoices()
.sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))
.map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));
let voices = speechSynthesis.getVoices();
resolve(voices);
}, 1);
if (voices.length === 0) {
// Edge compat: Provide default when voices empty
console.warn('SystemTTS: getVoices() returned empty list. Providing browser default option.');
const defaultVoice = {
name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME,
voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID,
preview_url: false,
lang: navigator.language || 'en-US',
};
resolve([defaultVoice]);
} else {
const mappedVoices = voices
.sort((a, b) => a.lang.localeCompare(b.lang) || a.name.localeCompare(b.name))
.map(x => ({ name: x.name, voice_id: x.voiceURI, preview_url: false, lang: x.lang }));
resolve(mappedVoices);
}
}, 50);
});
}
previewTtsVoice(voiceId) {
if (!('speechSynthesis' in window)) {
throw 'Speech synthesis API is not supported';
throw new Error('Speech synthesis API is not supported');
}
const voice = speechSynthesis.getVoices().find(x => x.voiceURI === voiceId);
let voice = null;
if (voiceId !== SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID) {
const voices = speechSynthesis.getVoices();
voice = voices.find(x => x.voiceURI === voiceId);
if (!voice) {
throw `TTS Voice id ${voiceId} not found`;
if (!voice && voices.length > 0) {
console.warn(`SystemTTS Preview: Voice ID "${voiceId}" not found among available voices. Using browser default.`);
} else if (!voice && voices.length === 0) {
console.warn('SystemTTS Preview: Voice list is empty. Using browser default.');
}
} else {
console.log('SystemTTS Preview: Using browser default voice as requested.');
}
speechSynthesis.cancel();
const text = getPreviewString(voice.lang);
const langForPreview = voice ? voice.lang : (navigator.language || 'en-US');
const text = getPreviewString(langForPreview);
const utterance = new SpeechSynthesisUtterance(text);
utterance.voice = voice;
if (voice) {
utterance.voice = voice;
}
utterance.rate = this.settings.rate || 1;
utterance.pitch = this.settings.pitch || 1;
utterance.onerror = (event) => {
console.error(`SystemTTS Preview Error: ${event.error}`, event);
};
speechSynthesis.speak(utterance);
}
async getVoice(voiceName) {
if (!('speechSynthesis' in window)) {
return { voice_id: null };
return { voice_id: null, name: 'API Not Supported' };
}
if (voiceName === SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME) {
return {
voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID,
name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME,
};
}
const voices = speechSynthesis.getVoices();
if (voices.length === 0) {
console.warn('SystemTTS: Empty voice list, using default fallback');
return {
voice_id: SystemTtsProvider.BROWSER_DEFAULT_VOICE_ID,
name: SystemTtsProvider.BROWSER_DEFAULT_VOICE_NAME,
};
}
const match = voices.find(x => x.name == voiceName);
if (!match) {
throw `TTS Voice name ${voiceName} not found`;
throw new Error(`SystemTTS getVoice: TTS Voice name "${voiceName}" not found`);
}
return { voice_id: match.voiceURI, name: match.name };
@@ -237,7 +287,6 @@ class SystemTtsProvider {
speechUtteranceChunker(utterance, {
chunkLength: 200,
}, function () {
//some code to execute when done
resolve(silence);
console.log('System TTS done');
});

View File

@@ -55,6 +55,8 @@ const getBatchSize = () => ['transformers', 'palm', 'ollama'].includes(settings.
const settings = {
// For both
source: 'transformers',
alt_endpoint_url: '',
use_alt_endpoint: false,
include_wi: false,
togetherai_model: 'togethercomputer/m2-bert-80M-32k-retrieval',
openai_model: 'text-embedding-ada-002',
@@ -109,6 +111,7 @@ const settings = {
const moduleWorker = new ModuleWorkerWrapper(synchronizeChat);
const webllmProvider = new WebLlmVectorProvider();
const cachedSummaries = new Map();
const vectorApiRequiresUrl = ['llamacpp', 'vllm', 'ollama', 'koboldcpp'];
/**
* Gets the Collection ID for a file embedded in the chat.
@@ -777,14 +780,14 @@ function getVectorsRequestBody(args = {}) {
break;
case 'ollama':
body.model = extension_settings.vectors.ollama_model;
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.OLLAMA];
body.keep = !!extension_settings.vectors.ollama_keep;
break;
case 'llamacpp':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP];
break;
case 'vllm':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
body.apiUrl = settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.VLLM];
body.model = extension_settings.vectors.vllm_model;
break;
case 'webllm':
@@ -826,11 +829,12 @@ async function getAdditionalArgs(items) {
* @returns {Promise<number[]>} Saved hashes
*/
async function getSavedHashes(collectionId) {
const args = await getAdditionalArgs([]);
const response = await fetch('/api/vector/list', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
...getVectorsRequestBody(args),
collectionId: collectionId,
source: settings.source,
}),
@@ -883,11 +887,18 @@ function throwIfSourceInvalid() {
throw new Error('Vectors: API key missing', { cause: 'api_key_missing' });
}
if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] ||
settings.source === 'koboldcpp' && !textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP] ||
settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
if (vectorApiRequiresUrl.includes(settings.source) && settings.use_alt_endpoint) {
if (!settings.alt_endpoint_url) {
throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
}
}
else {
if (settings.source === 'ollama' && !textgenerationwebui_settings.server_urls[textgen_types.OLLAMA] ||
settings.source === 'vllm' && !textgenerationwebui_settings.server_urls[textgen_types.VLLM] ||
settings.source === 'koboldcpp' && !textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP] ||
settings.source === 'llamacpp' && !textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP]) {
throw new Error('Vectors: API URL missing', { cause: 'api_url_missing' });
}
}
if (settings.source === 'ollama' && !settings.ollama_model || settings.source === 'vllm' && !settings.vllm_model) {
@@ -1087,6 +1098,7 @@ function toggleSettings() {
$('#webllm_vectorsModel').toggle(settings.source === 'webllm');
$('#koboldcpp_vectorsModel').toggle(settings.source === 'koboldcpp');
$('#google_vectorsModel').toggle(settings.source === 'palm');
$('#vector_altEndpointUrl').toggle(vectorApiRequiresUrl.includes(settings.source));
if (settings.source === 'webllm') {
loadWebLlmModels();
}
@@ -1144,6 +1156,9 @@ function loadWebLlmModels() {
* @returns {Promise<Record<string, number[]>>} Calculated embeddings
*/
async function createWebLlmEmbeddings(items) {
if (items.length === 0) {
return /** @type {Record<string, number[]>} */ ({});
}
return executeWithWebLlmErrorHandling(async () => {
const embeddings = await webllmProvider.embedTexts(items, settings.webllm_model);
const result = /** @type {Record<string, number[]>} */ ({});
@@ -1165,7 +1180,7 @@ async function createKoboldCppEmbeddings(items) {
headers: getRequestHeaders(),
body: JSON.stringify({
items: items,
server: textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP],
server: settings.use_alt_endpoint ? settings.alt_endpoint_url : textgenerationwebui_settings.server_urls[textgen_types.KOBOLDCPP],
}),
});
@@ -1467,6 +1482,16 @@ jQuery(async () => {
saveSettingsDebounced();
toggleSettings();
});
$('#vector_altEndpointUrl_enabled').prop('checked', settings.use_alt_endpoint).on('input', () => {
settings.use_alt_endpoint = $('#vector_altEndpointUrl_enabled').prop('checked');
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#vector_altEndpoint_address').val(settings.alt_endpoint_url).on('change', () => {
settings.alt_endpoint_url = String($('#vector_altEndpoint_address').val());
Object.assign(extension_settings.vectors, settings);
saveSettingsDebounced();
});
$('#api_key_nomicai').on('click', async () => {
const popupText = 'NomicAI API Key:';
const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {

View File

@@ -25,6 +25,16 @@
<option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
</select>
</div>
<div class="flex-container flexFlowColumn" id="vector_altEndpointUrl">
<label class="checkbox_label" for="vector_altEndpointUrl_enabled" title="Enable secondary endpoint URL usage, instead of the main one.">
<input id="vector_altEndpointUrl_enabled" type="checkbox" class="checkbox">
<span data-i18n="Use secondary URL">Use secondary URL</span>
</label>
<label for="vector_altEndpoint_address" data-i18n="Secondary Embedding endpoint URL">
Secondary Embedding endpoint URL
</label>
<input id="vector_altEndpoint_address" class="text_pole" type="text" placeholder="e.g. http://localhost:5001" />
</div>
<div class="flex-container flexFlowColumn" id="webllm_vectorsModel">
<label for="vectors_webllm_model" data-i18n="Vectorization Model">
Vectorization Model
@@ -87,6 +97,7 @@
Vectorization Model
</label>
<select id="vectors_cohere_model" class="text_pole">
<option value="embed-v4.0">embed-v4.0</option>
<option value="embed-english-v3.0">embed-english-v3.0</option>
<option value="embed-multilingual-v3.0">embed-multilingual-v3.0</option>
<option value="embed-english-light-v3.0">embed-english-light-v3.0</option>
@@ -310,7 +321,7 @@
<label for="vectors_file_depth_db" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="vectors_file_position_db" value="1" />
<span data-i18n="In-chat @ Depth">In-chat @ Depth</span>
<input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="999" />
<input id="vectors_file_depth_db" class="text_pole widthUnset" type="number" min="0" max="9999" />
<span>as</span>
<select id="vectors_file_depth_role_db" class="text_pole widthNatural">
<option value="0" data-i18n="System">System</option>
@@ -362,7 +373,7 @@
<label for="vectors_depth" title="How many messages before the current end of the chat." data-i18n="[title]How many messages before the current end of the chat.">
<input type="radio" name="vectors_position" value="1" />
<span data-i18n="In-chat @ Depth">In-chat @ Depth </span>
<input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="999" />
<input id="vectors_depth" class="text_pole widthUnset" type="number" min="0" max="9999" />
</label>
</div>
<div class="flex-container">

View File

@@ -13,6 +13,9 @@ import {
getBase64Async,
resetScrollHeight,
initScrollHeight,
localizePagination,
renderPaginationDropdown,
paginationDropdownChangeHandler,
} from './utils.js';
import { RA_CountCharTokens, humanizedDateTime, dragElement, favsToHotswap, getMessageTimeStamp } from './RossAscends-mods.js';
import { power_user, loadMovingUIState, sortEntitiesList } from './power-user.js';
@@ -1374,6 +1377,8 @@ function getGroupCharacters({ doFilter, onlyMembers } = {}) {
function printGroupCandidates() {
const storageKey = 'GroupCandidates_PerPage';
const pageSize = Number(accountStorage.getItem(storageKey)) || 5;
const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000];
$('#rm_group_add_members_pagination').pagination({
dataSource: getGroupCharacters({ doFilter: true, onlyMembers: false }),
pageRange: 1,
@@ -1382,18 +1387,20 @@ function printGroupCandidates() {
prevText: '<',
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
showNavigator: true,
showSizeChanger: true,
pageSize: Number(accountStorage.getItem(storageKey)) || 5,
sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
afterSizeSelectorChange: function (e) {
pageSize,
afterSizeSelectorChange: function (e, size) {
accountStorage.setItem(storageKey, e.target.value);
paginationDropdownChangeHandler(e, size);
},
callback: function (data) {
$('#rm_group_add_members').empty();
for (const i of data) {
$('#rm_group_add_members').append(getGroupCharacterBlock(i.item));
}
localizePagination($('#rm_group_add_members_pagination'));
},
});
}
@@ -1401,6 +1408,9 @@ function printGroupCandidates() {
function printGroupMembers() {
const storageKey = 'GroupMembers_PerPage';
$('.rm_group_members_pagination').each(function () {
let that = this;
const pageSize = Number(accountStorage.getItem(storageKey)) || 5;
const sizeChangerOptions = [5, 10, 25, 50, 100, 200, 500, 1000];
$(this).pagination({
dataSource: getGroupCharacters({ doFilter: false, onlyMembers: true }),
pageRange: 1,
@@ -1411,16 +1421,18 @@ function printGroupMembers() {
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
showSizeChanger: true,
pageSize: Number(accountStorage.getItem(storageKey)) || 5,
sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
afterSizeSelectorChange: function (e) {
formatSizeChanger: renderPaginationDropdown(pageSize, sizeChangerOptions),
pageSize,
afterSizeSelectorChange: function (e, size) {
accountStorage.setItem(storageKey, e.target.value);
paginationDropdownChangeHandler(e, size);
},
callback: function (data) {
$('.rm_group_members').empty();
for (const i of data) {
$('.rm_group_members').append(getGroupCharacterBlock(i.item));
}
localizePagination($(that));
},
});
});
@@ -1804,7 +1816,7 @@ async function createGroup() {
const memberNames = characters.filter(x => members.includes(x.avatar)).map(x => x.name).join(', ');
if (!name) {
name = `Group: ${memberNames}`;
name = t`Group: ${memberNames}`;
}
const avatar_url = $('#group_avatar_preview img').attr('src');

View File

@@ -394,7 +394,7 @@ function getHordeModelTemplate(option) {
`));
}
jQuery(function () {
export function initHorde () {
$('#horde_model').on('mousedown change', async function (e) {
console.log('Horde model change', e);
horde_settings.models = $('#horde_model').val();
@@ -441,7 +441,7 @@ jQuery(function () {
if (!isMobile()) {
$('#horde_model').select2({
width: '100%',
placeholder: 'Select Horde models',
placeholder: t`Select Horde models`,
allowClear: true,
closeOnSelect: false,
templateSelection: function (data) {
@@ -451,5 +451,5 @@ jQuery(function () {
templateResult: getHordeModelTemplate,
});
}
});
}

View File

@@ -208,37 +208,39 @@ export function autoSelectInstructPreset(modelId) {
// Select matching instruct preset
let foundMatch = false;
for (const instruct_preset of instruct_presets) {
// If instruct preset matches the context template
if (power_user.instruct.bind_to_context && instruct_preset.name === power_user.context.preset) {
foundMatch = true;
selectInstructPreset(instruct_preset.name, { isAuto: true });
break;
}
}
// If no match was found, auto-select instruct preset
if (!foundMatch) {
for (const preset of instruct_presets) {
// If activation regex is set, check if it matches the model id
if (preset.activation_regex) {
try {
const regex = regexFromString(preset.activation_regex);
// Stop on first match so it won't cycle back and forth between presets if multiple regexes match
if (regex instanceof RegExp && regex.test(modelId)) {
selectInstructPreset(preset.name, { isAuto: true });
for (const preset of instruct_presets) {
// If activation regex is set, check if it matches the model id
if (preset.activation_regex) {
try {
const regex = regexFromString(preset.activation_regex);
return true;
}
} catch {
// If regex is invalid, ignore it
console.warn(`Invalid instruct activation regex in preset "${preset.name}"`);
// Stop on first match so it won't cycle back and forth between presets if multiple regexes match
if (regex instanceof RegExp && regex.test(modelId)) {
selectInstructPreset(preset.name, { isAuto: true });
foundMatch = true;
break;
}
} catch {
// If regex is invalid, ignore it
console.warn(`Invalid instruct activation regex in preset "${preset.name}"`);
}
}
}
return false;
// If no match was found, auto-select instruct preset
if (!foundMatch && power_user.instruct.bind_to_context) {
for (const instruct_preset of instruct_presets) {
// If instruct preset matches the context template
if (instruct_preset.name === power_user.context.preset) {
selectInstructPreset(instruct_preset.name, { isAuto: true });
foundMatch = true;
break;
}
}
}
return foundMatch;
}
/**

View File

@@ -15,12 +15,12 @@ import {
extension_prompt_types,
Generate,
getExtensionPrompt,
getExtensionPromptMaxDepth,
getNextMessageId,
getRequestHeaders,
getStoppingStrings,
is_send_press,
main_api,
MAX_INJECTION_DEPTH,
name1,
name2,
replaceItemizedPromptText,
@@ -184,6 +184,7 @@ export const chat_completion_sources = {
ZEROONEAI: '01ai',
NANOGPT: 'nanogpt',
DEEPSEEK: 'deepseek',
XAI: 'xai',
};
const character_names_behavior = {
@@ -215,6 +216,15 @@ const openrouter_middleout_types = {
OFF: 'off',
};
export const reasoning_effort_types = {
auto: 'auto',
low: 'low',
medium: 'medium',
high: 'high',
min: 'min',
max: 'max',
};
const sensitiveFields = [
'reverse_proxy',
'proxy_password',
@@ -257,6 +267,7 @@ export const settingsToUpdate = {
nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false],
deepseek_model: ['#model_deepseek_select', 'deepseek_model', false],
zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false],
xai_model: ['#model_xai_select', 'xai_model', false],
custom_model: ['#custom_model_id', 'custom_model', false],
custom_url: ['#custom_api_url_text', 'custom_url', false],
custom_include_body: ['#custom_include_body', 'custom_include_body', false],
@@ -345,6 +356,7 @@ const default_settings = {
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
deepseek_model: 'deepseek-chat',
xai_model: 'grok-3-beta',
custom_model: '',
custom_url: '',
custom_include_body: '',
@@ -379,7 +391,7 @@ const default_settings = {
continue_postfix: continue_postfix_types.SPACE,
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
show_thoughts: true,
reasoning_effort: 'medium',
reasoning_effort: reasoning_effort_types.auto,
enable_web_search: false,
request_images: false,
seed: -1,
@@ -425,6 +437,7 @@ const oai_settings = {
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
deepseek_model: 'deepseek-chat',
xai_model: 'grok-3-beta',
custom_model: '',
custom_url: '',
custom_include_body: '',
@@ -459,7 +472,7 @@ const oai_settings = {
continue_postfix: continue_postfix_types.SPACE,
custom_prompt_post_processing: custom_prompt_post_processing_types.NONE,
show_thoughts: true,
reasoning_effort: 'medium',
reasoning_effort: reasoning_effort_types.auto,
enable_web_search: false,
request_images: false,
seed: -1,
@@ -738,7 +751,8 @@ async function populationInjectionPrompts(prompts, messages) {
'assistant': extension_prompt_roles.ASSISTANT,
};
for (let i = 0; i <= MAX_INJECTION_DEPTH; i++) {
const maxDepth = getExtensionPromptMaxDepth();
for (let i = 0; i <= maxDepth; i++) {
// Get prompts for current depth
const depthPrompts = prompts.filter(prompt => prompt.injection_depth === i && prompt.content);
@@ -1407,9 +1421,9 @@ export async function prepareOpenAIMessages({
await populateChatCompletion(prompts, chatCompletion, { bias, quietPrompt, quietImage, type, cyclePrompt, messages, messageExamples });
} catch (error) {
if (error instanceof TokenBudgetExceededError) {
toastr.error(t`An error occurred while counting tokens: Token budget exceeded.`);
chatCompletion.log('Token budget exceeded.');
promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token Limit or disable custom prompts.`;
toastr.error(t`Mandatory prompts exceed the context size.`);
chatCompletion.log('Mandatory prompts exceed the context size.');
promptManager.error = t`Not enough free tokens for mandatory prompts. Raise your token limit or disable custom prompts.`;
} else if (error instanceof InvalidCharacterNameError) {
toastr.warning(t`An error occurred while counting tokens: Invalid character name`);
chatCompletion.log('Invalid character name');
@@ -1644,6 +1658,8 @@ export function getChatCompletionModel(source = null) {
return oai_settings.nanogpt_model;
case chat_completion_sources.DEEPSEEK:
return oai_settings.deepseek_model;
case chat_completion_sources.XAI:
return oai_settings.xai_model;
default:
throw new Error(`Unknown chat completion source: ${activeSource}`);
}
@@ -1687,6 +1703,11 @@ function calculateOpenRouterCost() {
}
}
if (oai_settings.enable_web_search) {
const webSearchCost = (0.02).toFixed(2);
cost = t`${cost} + $${webSearchCost}`;
}
$('#openrouter_max_prompt_cost').text(cost);
}
@@ -1925,6 +1946,31 @@ async function sendAltScaleRequest(messages, logit_bias, signal, type) {
return data.output;
}
function getReasoningEffort() {
// These sources expect the effort as string.
const reasoningEffortSources = [
chat_completion_sources.OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.XAI,
chat_completion_sources.OPENROUTER,
];
if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) {
return oai_settings.reasoning_effort;
}
switch (oai_settings.reasoning_effort) {
case reasoning_effort_types.auto:
return undefined;
case reasoning_effort_types.min:
return reasoning_effort_types.low;
case reasoning_effort_types.max:
return reasoning_effort_types.high;
default:
return oai_settings.reasoning_effort;
}
}
/**
* Send a chat completion request to backend
* @param {string} type (impersonate, quiet, continue, etc)
@@ -1961,13 +2007,14 @@ async function sendOpenAIRequest(type, messages, signal) {
const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
const isQuiet = type === 'quiet';
const isImpersonate = type === 'impersonate';
const isContinue = type === 'continue';
const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
const useLogprobs = !!power_user.request_token_probabilities;
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom);
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI);
// If we're using the window.ai extension, use that instead
// Doesn't support logit bias yet
@@ -2010,7 +2057,7 @@ async function sendOpenAIRequest(type, messages, signal) {
'char_name': name2,
'group_names': getGroupNames(),
'include_reasoning': Boolean(oai_settings.show_thoughts),
'reasoning_effort': String(oai_settings.reasoning_effort),
'reasoning_effort': getReasoningEffort(),
'enable_web_search': Boolean(oai_settings.enable_web_search),
'request_images': Boolean(oai_settings.request_images),
'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing,
@@ -2026,14 +2073,14 @@ async function sendOpenAIRequest(type, messages, signal) {
}
// Proxy is only supported for Claude, OpenAI, Mistral, and Google MakerSuite
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK, chat_completion_sources.XAI].includes(oai_settings.chat_completion_source)) {
await validateReverseProxy();
generate_data['reverse_proxy'] = oai_settings.reverse_proxy;
generate_data['proxy_password'] = oai_settings.proxy_password;
}
// Add logprobs request (currently OpenAI only, max 5 on their side)
if (useLogprobs && (isOAI || isCustom || isDeepSeek)) {
if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI)) {
generate_data['logprobs'] = 5;
}
@@ -2152,29 +2199,42 @@ async function sendOpenAIRequest(type, messages, signal) {
}
}
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano) && oai_settings.seed >= 0) {
if (isXAI) {
if (generate_data.model.includes('grok-3-mini')) {
delete generate_data.presence_penalty;
delete generate_data.frequency_penalty;
}
if (generate_data.model.includes('grok-vision')) {
delete generate_data.tools;
delete generate_data.tool_choice;
}
}
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI) && oai_settings.seed >= 0) {
generate_data['seed'] = oai_settings.seed;
}
if (isOAI && (oai_settings.openai_model.startsWith('o1') || oai_settings.openai_model.startsWith('o3'))) {
generate_data.messages.forEach((msg) => {
if (msg.role === 'system') {
msg.role = 'user';
}
});
if (isOAI && /^(o1|o3|o4)/.test(oai_settings.openai_model)) {
generate_data.max_completion_tokens = generate_data.max_tokens;
delete generate_data.max_tokens;
delete generate_data.logprobs;
delete generate_data.top_logprobs;
delete generate_data.n;
delete generate_data.stop;
delete generate_data.logit_bias;
delete generate_data.temperature;
delete generate_data.top_p;
delete generate_data.frequency_penalty;
delete generate_data.presence_penalty;
delete generate_data.tools;
delete generate_data.tool_choice;
delete generate_data.stop;
delete generate_data.logit_bias;
if (oai_settings.openai_model.startsWith('o1')) {
generate_data.messages.forEach((msg) => {
if (msg.role === 'system') {
msg.role = 'user';
}
});
delete generate_data.n;
delete generate_data.tools;
delete generate_data.tool_choice;
}
}
await eventSource.emit(event_types.CHAT_COMPLETION_SETTINGS_READY, generate_data);
@@ -2210,7 +2270,8 @@ async function sendOpenAIRequest(type, messages, signal) {
if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) {
const swipeIndex = parsed.choices[0].index - 1;
swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state);
// FIXME: state.reasoning should be an array to support multi-swipe
swipes[swipeIndex] = (swipes[swipeIndex] || '') + getStreamingReply(parsed, state, { overrideShowThoughts: false });
} else {
text += getStreamingReply(parsed, state);
}
@@ -2278,6 +2339,11 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
}
return data.choices?.[0]?.delta?.content || '';
} else if (chat_completion_source === chat_completion_sources.XAI) {
if (show_thoughts) {
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
}
return data.choices?.[0]?.delta?.content || '';
} else if (chat_completion_source === chat_completion_sources.OPENROUTER) {
if (show_thoughts) {
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
@@ -2310,6 +2376,7 @@ function parseChatCompletionLogprobs(data) {
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.OPENAI:
case chat_completion_sources.DEEPSEEK:
case chat_completion_sources.XAI:
case chat_completion_sources.CUSTOM:
if (!data.choices?.length) {
return null;
@@ -3231,6 +3298,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model;
oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url;
oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body;
@@ -3316,6 +3384,8 @@ function loadOpenAISettings(data, settings) {
$('#model_deepseek_select').val(oai_settings.deepseek_model);
$(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
$('#model_01ai_select').val(oai_settings.zerooneai_model);
$('#model_xai_select').val(oai_settings.xai_model);
$(`#model_xai_select option[value="${oai_settings.xai_model}"`).attr('selected', true);
$('#custom_model_id').val(oai_settings.custom_model);
$('#custom_api_url_text').val(oai_settings.custom_url);
$('#openai_max_context').val(oai_settings.openai_max_context);
@@ -3512,7 +3582,7 @@ async function getStatusOpen() {
chat_completion_source: oai_settings.chat_completion_source,
};
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK].includes(oai_settings.chat_completion_source)) {
if (oai_settings.reverse_proxy && [chat_completion_sources.CLAUDE, chat_completion_sources.OPENAI, chat_completion_sources.MISTRALAI, chat_completion_sources.MAKERSUITE, chat_completion_sources.DEEPSEEK, chat_completion_sources.XAI].includes(oai_settings.chat_completion_source)) {
await validateReverseProxy();
}
@@ -3781,7 +3851,7 @@ function createLogitBiasListItem(entry) {
}
async function createNewLogitBiasPreset() {
const name = await callPopup('Preset name:', 'input');
const name = await Popup.show.input(t`Preset name:`, null);
if (!name) {
return;
@@ -4092,9 +4162,15 @@ function getMaxContextOpenAI(value) {
if (oai_settings.max_context_unlocked) {
return unlocked_max;
}
else if (value.startsWith('o1') || value.startsWith('o3')) {
else if (value.includes('gpt-4.1')) {
return max_1mil;
}
else if (value.startsWith('o1')) {
return max_128k;
}
else if (value.startsWith('o4') || value.startsWith('o3')) {
return max_200k;
}
else if (value.includes('chatgpt-4o-latest') || value.includes('gpt-4-turbo') || value.includes('gpt-4o') || value.includes('gpt-4-1106') || value.includes('gpt-4-0125') || value.includes('gpt-4-vision')) {
return max_128k;
}
@@ -4165,6 +4241,80 @@ function getMaxContextWindowAI(value) {
}
}
/**
* Get the maximum context size for the Mistral model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getMistralMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
if (Array.isArray(model_list) && model_list.length > 0) {
const contextLength = model_list.find((record) => record.id === model)?.max_context_length;
if (contextLength) {
return contextLength;
}
}
const contextMap = {
'codestral-2411-rc5': 262144,
'codestral-2412': 262144,
'codestral-2501': 262144,
'codestral-latest': 262144,
'codestral-mamba-2407': 262144,
'codestral-mamba-latest': 262144,
'open-codestral-mamba': 262144,
'ministral-3b-2410': 131072,
'ministral-3b-latest': 131072,
'ministral-8b-2410': 131072,
'ministral-8b-latest': 131072,
'mistral-large-2407': 131072,
'mistral-large-2411': 131072,
'mistral-large-latest': 131072,
'mistral-large-pixtral-2411': 131072,
'mistral-tiny-2407': 131072,
'mistral-tiny-latest': 131072,
'open-mistral-nemo': 131072,
'open-mistral-nemo-2407': 131072,
'pixtral-12b': 131072,
'pixtral-12b-2409': 131072,
'pixtral-12b-latest': 131072,
'pixtral-large-2411': 131072,
'pixtral-large-latest': 131072,
'open-mixtral-8x22b': 65536,
'open-mixtral-8x22b-2404': 65536,
'codestral-2405': 32768,
'mistral-embed': 32768,
'mistral-large-2402': 32768,
'mistral-medium': 32768,
'mistral-medium-2312': 32768,
'mistral-medium-latest': 32768,
'mistral-moderation-2411': 32768,
'mistral-moderation-latest': 32768,
'mistral-ocr-2503': 32768,
'mistral-ocr-latest': 32768,
'mistral-saba-2502': 32768,
'mistral-saba-latest': 32768,
'mistral-small': 32768,
'mistral-small-2312': 32768,
'mistral-small-2402': 32768,
'mistral-small-2409': 32768,
'mistral-small-2501': 32768,
'mistral-small-2503': 32768,
'mistral-small-latest': 32768,
'mistral-tiny': 32768,
'mistral-tiny-2312': 32768,
'open-mistral-7b': 32768,
'open-mixtral-8x7b': 32768,
};
// Return context size if model found, otherwise default to 32k
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || 32768;
}
/**
* Get the maximum context size for the Groq model
* @param {string} model Model identifier
@@ -4312,6 +4462,11 @@ async function onModelChange() {
$('#custom_model_id').val(value).trigger('input');
}
if ($(this).is('#model_xai_select')) {
console.log('XAI model changed to', value);
oai_settings.xai_model = value;
}
if (oai_settings.chat_completion_source == chat_completion_sources.SCALE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
@@ -4326,20 +4481,16 @@ async function onModelChange() {
if (oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', max_2mil);
} else if (value.includes('gemini-exp-1114') || value.includes('gemini-exp-1121') || value.includes('gemini-2.0-flash-thinking-exp-1219')) {
$('#openai_max_context').attr('max', max_32k);
} else if (value.includes('gemini-1.5-pro') || value.includes('gemini-exp-1206') || value.includes('gemini-2.0-pro')) {
} else if (value.includes('gemini-1.5-pro')) {
$('#openai_max_context').attr('max', max_2mil);
} else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash') || value.includes('gemini-2.5-pro-exp-03-25') || value.includes('gemini-2.5-pro-preview-03-25')) {
} else if (value.includes('gemini-1.5-flash') || value.includes('gemini-2.0-flash') || value.includes('gemini-2.0-pro') || value.includes('gemini-exp') || value.includes('gemini-2.5-flash') || value.includes('gemini-2.5-pro') || value.includes('learnlm-2.0-flash')) {
$('#openai_max_context').attr('max', max_1mil);
} else if (value.includes('gemini-1.0-pro') || value === 'gemini-pro') {
$('#openai_max_context').attr('max', max_32k);
} else if (value.includes('gemini-1.0-ultra') || value === 'gemini-ultra') {
$('#openai_max_context').attr('max', max_32k);
} else if (value.includes('gemma-3')) {
} else if (value.includes('gemma-3-27b-it')) {
$('#openai_max_context').attr('max', max_128k);
} else if (value.includes('gemma-3') || value.includes('learnlm-1.5-pro-experimental')) {
$('#openai_max_context').attr('max', max_32k);
} else {
$('#openai_max_context').attr('max', max_4k);
$('#openai_max_context').attr('max', max_32k);
}
let makersuite_max_temp = (value.includes('vision') || value.includes('ultra') || value.includes('gemma')) ? 1.0 : 2.0;
oai_settings.temp_openai = Math.min(makersuite_max_temp, oai_settings.temp_openai);
@@ -4428,27 +4579,10 @@ async function onModelChange() {
}
if (oai_settings.chat_completion_source === chat_completion_sources.MISTRALAI) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (['codestral-latest', 'codestral-mamba-2407', 'codestral-2411-rc5', 'codestral-2412', 'codestral-2501'].includes(oai_settings.mistralai_model)) {
$('#openai_max_context').attr('max', max_256k);
} else if (['mistral-large-2407', 'mistral-large-2411', 'mistral-large-pixtral-2411', 'mistral-large-latest'].includes(oai_settings.mistralai_model)) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.mistralai_model.includes('mistral-nemo')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.mistralai_model.includes('mixtral-8x22b')) {
$('#openai_max_context').attr('max', max_64k);
} else if (oai_settings.mistralai_model.includes('pixtral')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.mistralai_model.includes('ministral')) {
$('#openai_max_context').attr('max', max_32k);
} else {
$('#openai_max_context').attr('max', max_32k);
}
const maxContext = getMistralMaxContext(oai_settings.mistralai_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(oai_settings.openai_max_context, Number($('#openai_max_context').attr('max')));
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
//mistral also caps temp at 1.0
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
@@ -4584,6 +4718,22 @@ async function onModelChange() {
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.xai_model.includes('grok-2-vision')) {
$('#openai_max_context').attr('max', max_32k);
} else if (oai_settings.xai_model.includes('grok-vision')) {
$('#openai_max_context').attr('max', max_8k);
} else {
$('#openai_max_context').attr('max', max_128k);
}
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
$('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');
@@ -4822,6 +4972,19 @@ async function onConnectButtonClick(e) {
}
}
if (oai_settings.chat_completion_source === chat_completion_sources.XAI) {
const api_key_xai = String($('#api_key_xai').val()).trim();
if (api_key_xai.length) {
await writeSecret(SECRET_KEYS.XAI, api_key_xai);
}
if (!secret_state[SECRET_KEYS.XAI] && !oai_settings.reverse_proxy) {
console.log('No secret key saved for XAI');
return;
}
}
startStatusLoading();
saveSettingsDebounced();
await getStatusOpen();
@@ -4878,6 +5041,9 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
$('#model_deepseek_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
$('#model_xai_select').trigger('change');
}
$('[data-source]').each(function () {
const validSources = $(this).data('source').split(',');
$(this).toggle(validSources.includes(oai_settings.chat_completion_source));
@@ -4962,63 +5128,43 @@ export function isImageInliningSupported() {
// gultra just isn't being offered as multimodal, thanks google.
const visionSupportedModels = [
'gpt-4-vision',
'gemini-2.5-pro-exp-03-25',
'gemini-2.5-pro-preview-03-25',
'gemini-2.0-pro-exp',
'gemini-2.0-pro-exp-02-05',
'gemini-2.0-flash-lite-preview',
'gemini-2.0-flash-lite-preview-02-05',
'gemini-2.0-flash',
'gemini-2.0-flash-001',
'gemini-2.0-flash-thinking-exp-1219',
'gemini-2.0-flash-thinking-exp-01-21',
'gemini-2.0-flash-thinking-exp',
'gemini-2.0-flash-exp',
'gemini-2.0-flash-exp-image-generation',
'gemini-1.5-flash',
'gemini-1.5-flash-latest',
'gemini-1.5-flash-001',
'gemini-1.5-flash-002',
'gemini-1.5-flash-exp-0827',
'gemini-1.5-flash-8b',
'gemini-1.5-flash-8b-exp-0827',
'gemini-1.5-flash-8b-exp-0924',
'gemini-exp-1114',
'gemini-exp-1121',
'gemini-exp-1206',
'gemini-1.0-pro-vision-latest',
'gemini-1.5-pro',
'gemini-1.5-pro-latest',
'gemini-1.5-pro-001',
'gemini-1.5-pro-002',
'gemini-1.5-pro-exp-0801',
'gemini-1.5-pro-exp-0827',
'claude-3',
'claude-3-5',
'claude-3-7',
'gpt-4-turbo',
'gpt-4o',
'gpt-4o-mini',
'gpt-4.5-preview',
'gpt-4.5-preview-2025-02-27',
'o1',
'o1-2024-12-17',
// OpenAI
'chatgpt-4o-latest',
'gpt-4-turbo',
'gpt-4-vision',
'gpt-4.1',
'gpt-4.5-preview',
'gpt-4o',
'o1',
'o3',
'o4-mini',
// 01.AI (Yi)
'yi-vision',
'pixtral-latest',
'pixtral-12b-latest',
'pixtral-12b',
'pixtral-12b-2409',
'pixtral-large-latest',
'pixtral-large-2411',
'c4ai-aya-vision-8b',
'c4ai-aya-vision-32b',
// Claude
'claude-3',
// Cohere
'c4ai-aya-vision',
// Google AI Studio
'gemini-1.5',
'gemini-2.0',
'gemini-2.5',
'gemini-exp-1206',
'learnlm',
// MistralAI
'mistral-small-2503',
'mistral-small-latest',
'pixtral',
// xAI (Grok)
'grok-2-vision',
'grok-vision',
];
switch (oai_settings.chat_completion_source) {
case chat_completion_sources.OPENAI:
return visionSupportedModels.some(model => oai_settings.openai_model.includes(model) && !oai_settings.openai_model.includes('gpt-4-turbo-preview'));
return visionSupportedModels.some(model =>
oai_settings.openai_model.includes(model)
&& ['gpt-4-turbo-preview', 'o1-mini', 'o3-mini'].some(x => !oai_settings.openai_model.includes(x)),
);
case chat_completion_sources.MAKERSUITE:
return visionSupportedModels.some(model => oai_settings.google_model.includes(model));
case chat_completion_sources.CLAUDE:
@@ -5033,6 +5179,8 @@ export function isImageInliningSupported() {
return visionSupportedModels.some(model => oai_settings.mistralai_model.includes(model));
case chat_completion_sources.COHERE:
return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
case chat_completion_sources.XAI:
return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
default:
return false;
}
@@ -5573,6 +5721,7 @@ export function initOpenAI() {
$('#openai_enable_web_search').on('input', function () {
oai_settings.enable_web_search = !!$(this).prop('checked');
calculateOpenRouterCost();
saveSettingsDebounced();
});
@@ -5629,6 +5778,7 @@ export function initOpenAI() {
$('#model_deepseek_select').on('change', onModelChange);
$('#model_01ai_select').on('change', onModelChange);
$('#model_custom_select').on('change', onModelChange);
$('#model_xai_select').on('change', onModelChange);
$('#settings_preset_openai').on('change', onSettingsPresetChange);
$('#new_oai_preset').on('click', onNewPresetClick);
$('#delete_oai_preset').on('click', onDeletePresetClick);

View File

@@ -22,7 +22,7 @@ import {
} from '../script.js';
import { persona_description_positions, power_user } from './power-user.js';
import { getTokenCountAsync } from './tokenizers.js';
import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js';
import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock, localizePagination, renderPaginationDropdown, paginationDropdownChangeHandler } from './utils.js';
import { debounce_timeout } from './constants.js';
import { FILTER_TYPES, FilterHelper } from './filters.js';
import { groups, selected_group } from './group-chats.js';
@@ -250,16 +250,18 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
const storageKey = 'Personas_PerPage';
const listId = '#user_avatar_block';
const perPage = Number(accountStorage.getItem(storageKey)) || 5;
const sizeChangerOptions = [5, 10, 25, 50, 100, 250, 500, 1000];
$('#persona_pagination_container').pagination({
dataSource: entities,
pageSize: perPage,
sizeChangerOptions: [5, 10, 25, 50, 100, 250, 500, 1000],
sizeChangerOptions,
pageRange: 1,
pageNumber: savePersonasPage || 1,
position: 'top',
showPageNumbers: false,
showSizeChanger: true,
formatSizeChanger: renderPaginationDropdown(perPage, sizeChangerOptions),
prevText: '<',
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
@@ -270,9 +272,11 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
$(listId).append(getUserAvatarBlock(item));
}
updatePersonaUIStates();
localizePagination($('#persona_pagination_container'));
},
afterSizeSelectorChange: function (e) {
afterSizeSelectorChange: function (e, size) {
accountStorage.setItem(storageKey, e.target.value);
paginationDropdownChangeHandler(e, size);
},
afterPaging: function (e) {
savePersonasPage = e;

View File

@@ -71,7 +71,8 @@ export const POPUP_RESULT = {
* @property {string} id - The id for the html element
* @property {string} label - The label text for the input
* @property {string?} [tooltip=null] - Optional tooltip icon displayed behind the label
* @property {boolean?} [defaultState=false] - The default state when opening the popup (false if not set)
* @property {boolean|string|undefined} [defaultState=false] - The default state when opening the popup (false if not set)
* @property {string?} [type='checkbox'] - The type of the input (default is checkbox)
*/
/**
@@ -157,7 +158,7 @@ export class Popup {
/** @type {POPUP_RESULT|number} */ result;
/** @type {any} */ value;
/** @type {Map<string,boolean>?} */ inputResults;
/** @type {Map<string,string|boolean>?} */ inputResults;
/** @type {any} */ cropData;
/** @type {HTMLElement} */ lastFocus;
@@ -260,28 +261,53 @@ export class Popup {
return;
}
const label = document.createElement('label');
label.classList.add('checkbox_label', 'justifyCenter');
label.setAttribute('for', input.id);
const inputElement = document.createElement('input');
inputElement.type = 'checkbox';
inputElement.id = input.id;
inputElement.checked = input.defaultState ?? false;
label.appendChild(inputElement);
const labelText = document.createElement('span');
labelText.innerText = input.label;
labelText.dataset.i18n = input.label;
label.appendChild(labelText);
if (!input.type || input.type === 'checkbox') {
const label = document.createElement('label');
label.classList.add('checkbox_label', 'justifyCenter');
label.setAttribute('for', input.id);
const inputElement = document.createElement('input');
inputElement.type = 'checkbox';
inputElement.id = input.id;
inputElement.checked = Boolean(input.defaultState ?? false);
label.appendChild(inputElement);
const labelText = document.createElement('span');
labelText.innerText = input.label;
labelText.dataset.i18n = input.label;
label.appendChild(labelText);
if (input.tooltip) {
const tooltip = document.createElement('div');
tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
tooltip.title = input.tooltip;
tooltip.dataset.i18n = '[title]' + input.tooltip;
label.appendChild(tooltip);
if (input.tooltip) {
const tooltip = document.createElement('div');
tooltip.classList.add('fa-solid', 'fa-circle-info', 'opacity50p');
tooltip.title = input.tooltip;
tooltip.dataset.i18n = '[title]' + input.tooltip;
label.appendChild(tooltip);
}
this.inputControls.appendChild(label);
} else if (input.type === 'text') {
const label = document.createElement('label');
label.classList.add('text_label', 'justifyCenter');
label.setAttribute('for', input.id);
const inputElement = document.createElement('input');
inputElement.classList.add('text_pole');
inputElement.type = 'text';
inputElement.id = input.id;
inputElement.value = String(input.defaultState ?? '');
inputElement.placeholder = input.tooltip ?? '';
const labelText = document.createElement('span');
labelText.innerText = input.label;
labelText.dataset.i18n = input.label;
label.appendChild(labelText);
label.appendChild(inputElement);
this.inputControls.appendChild(label);
} else {
console.warn('Unknown custom input type. Only checkbox and text are supported.', input);
return;
}
this.inputControls.appendChild(label);
});
// Set the default button class
@@ -529,7 +555,8 @@ export class Popup {
this.inputResults = new Map(this.customInputs.map(input => {
/** @type {HTMLInputElement} */
const inputControl = this.dlg.querySelector(`#${input.id}`);
return [inputControl.id, inputControl.checked];
const value = input.type === 'text' ? inputControl.value : inputControl.checked;
return [inputControl.id, value];
}));
}
@@ -619,7 +646,7 @@ export class Popup {
/** @readonly @type {Popup[]} Remember all popups */
popups: [],
/** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, boolean>?}?} Last popup result */
/** @type {{value: any, result: POPUP_RESULT|number?, inputResults: Map<string, string|boolean>?}?} Last popup result */
lastResult: null,
/** @returns {boolean} Checks if any modal popup dialog is open */

View File

@@ -71,8 +71,8 @@ export {
export const MAX_CONTEXT_DEFAULT = 8192;
export const MAX_RESPONSE_DEFAULT = 2048;
const MAX_CONTEXT_UNLOCKED = 200 * 1024;
const MAX_RESPONSE_UNLOCKED = 32 * 1024;
const MAX_CONTEXT_UNLOCKED = 512 * 1024;
const MAX_RESPONSE_UNLOCKED = 64 * 1024;
const unlockedMaxContextStep = 512;
const maxContextMin = 512;
const maxContextStep = 64;
@@ -244,7 +244,6 @@ let power_user = {
chat_start: defaultChatStart,
example_separator: defaultExampleSeparator,
use_stop_strings: true,
allow_jailbreak: false,
names_as_stop_strings: true,
},
@@ -255,6 +254,7 @@ let power_user = {
enabled: true,
name: 'Neutral - Chat',
content: 'Write {{char}}\'s next reply in a fictional chat between {{char}} and {{user}}.',
post_history: '',
},
reasoning: {
@@ -334,7 +334,6 @@ const contextControls = [
{ id: 'context_example_separator', property: 'example_separator', isCheckbox: false, isGlobalSetting: false },
{ id: 'context_chat_start', property: 'chat_start', isCheckbox: false, isGlobalSetting: false },
{ id: 'context_use_stop_strings', property: 'use_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
{ id: 'context_allow_jailbreak', property: 'allow_jailbreak', isCheckbox: true, isGlobalSetting: false, defaultValue: false },
{ id: 'context_names_as_stop_strings', property: 'names_as_stop_strings', isCheckbox: true, isGlobalSetting: false, defaultValue: true },
// Existing power user settings

View File

@@ -902,6 +902,12 @@ export async function initPresetManager() {
await presetManager.renamePreset(newName);
if (apiId === 'openai') {
// This is a horrible mess, but prevents the renamed preset from being corrupted.
$('#update_oai_preset').trigger('click');
return;
}
const successToast = !presetManager.isAdvancedFormatting() ? t`Preset renamed` : t`Template renamed`;
toastr.success(successToast);
});

View File

@@ -109,6 +109,8 @@ export function extractReasoningFromData(data, {
switch (chatCompletionSource ?? oai_settings.chat_completion_source) {
case chat_completion_sources.DEEPSEEK:
return data?.choices?.[0]?.message?.reasoning_content ?? '';
case chat_completion_sources.XAI:
return data?.choices?.[0]?.message?.reasoning_content ?? '';
case chat_completion_sources.OPENROUTER:
return data?.choices?.[0]?.message?.reasoning ?? '';
case chat_completion_sources.MAKERSUITE:

View File

@@ -42,6 +42,7 @@ export const SECRET_KEYS = {
DEEPSEEK: 'api_key_deepseek',
SERPER: 'api_key_serper',
FALAI: 'api_key_falai',
XAI: 'api_key_xai',
};
const INPUT_MAP = {
@@ -76,6 +77,7 @@ const INPUT_MAP = {
[SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
[SECRET_KEYS.GENERIC]: '#api_key_generic',
[SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
[SECRET_KEYS.XAI]: '#api_key_xai',
};
async function clearSecret() {

View File

@@ -1,4 +1,5 @@
import { Fuse, DOMPurify } from '../lib.js';
import { flashHighlight } from './utils.js';
import {
Generate,
@@ -21,6 +22,7 @@ import {
extractMessageBias,
generateQuietPrompt,
generateRaw,
getFirstDisplayedMessageId,
getThumbnailUrl,
is_send_press,
main_api,
@@ -313,6 +315,11 @@ export function initDefaultSlashCommands() {
typeList: [ARGUMENT_TYPE.NUMBER],
enumProvider: commonEnumProviders.messages({ allowIdAfter: true }),
}),
SlashCommandNamedArgument.fromProps({
name: 'name',
description: 'Optional custom display name to use for this system narrator message.',
typeList: [ARGUMENT_TYPE.STRING],
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'The way how you want the return value to be provided',
@@ -2077,8 +2084,9 @@ export function initDefaultSlashCommands() {
name: 'replace',
aliases: ['re'],
callback: (async ({ mode = 'literal', pattern, replacer = '' }, text) => {
if (pattern === '')
if (!pattern) {
throw new Error('Argument of \'pattern=\' cannot be empty');
}
switch (mode) {
case 'literal':
return text.replaceAll(pattern, replacer);
@@ -2121,14 +2129,160 @@ export function initDefaultSlashCommands() {
</div>
<div>
<strong>Example:</strong>
<pre>/let x Blue house and blue car || </pre>
<pre>/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</pre>
<pre>/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</pre>
<pre>/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</pre>
<pre>/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</pre>
<pre><code class="language-stscript">/let x Blue house and blue car || </code></pre>
<pre><code class="language-stscript">/replace pattern="blue" {{var::x}} | /echo |/# Blue house and car ||</code></pre>
<pre><code class="language-stscript">/replace pattern="blue" replacer="red" {{var::x}} | /echo |/# Blue house and red car ||</code></pre>
<pre><code class="language-stscript">/replace mode=regex pattern="/blue/i" replacer="red" {{var::x}} | /echo |/# red house and blue car ||</code></pre>
<pre><code class="language-stscript">/replace mode=regex pattern="/blue/gi" replacer="red" {{var::x}} | /echo |/# red house and red car ||</code></pre>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'test',
callback: (({ pattern }, text) => {
if (!pattern) {
throw new Error('Argument of \'pattern=\' cannot be empty');
}
const re = regexFromString(pattern.toString());
if (!re) {
throw new Error('The value of \'pattern\' argument is not a valid regular expression.');
}
return JSON.stringify(re.test(text.toString()));
}),
returns: 'true | false',
namedArgumentList: [
new SlashCommandNamedArgument(
'pattern', 'pattern to find', [ARGUMENT_TYPE.STRING], true, false,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'text to test', [ARGUMENT_TYPE.STRING], true, false,
),
],
helpString: `
<div>
Tests text for a regular expression match.
</div>
<div>
Returns <code>true</code> if the match is found, <code>false</code> otherwise.
</div>
<div>
<strong>Example:</strong>
<pre><code class="language-stscript">/let x Blue house and green car ||</code></pre>
<pre><code class="language-stscript">/test pattern="green" {{var::x}} | /echo |/# true ||</code></pre>
<pre><code class="language-stscript">/test pattern="blue" {{var::x}} | /echo |/# false ||</code></pre>
<pre><code class="language-stscript">/test pattern="/blue/i" {{var::x}} | /echo |/# true ||</code></pre>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'match',
callback: (({ pattern }, text) => {
if (!pattern) {
throw new Error('Argument of \'pattern=\' cannot be empty');
}
const re = regexFromString(pattern.toString());
if (!re) {
throw new Error('The value of \'pattern\' argument is not a valid regular expression.');
}
if (re.flags.includes('g')) {
return JSON.stringify([...text.toString().matchAll(re)]);
} else {
const match = text.toString().match(re);
return match ? JSON.stringify(match) : '';
}
}),
returns: 'group array for each match',
namedArgumentList: [
new SlashCommandNamedArgument(
'pattern', 'pattern to find', [ARGUMENT_TYPE.STRING], true, false,
),
],
unnamedArgumentList: [
new SlashCommandArgument(
'text to match against', [ARGUMENT_TYPE.STRING], true, false,
),
],
helpString: `
<div>
Retrieves regular expression matches in the given text
</div>
<div>
Returns an array of groups (with the first group being the full match). If the regex contains the global flag (i.e. <code>/g</code>),
multiple nested arrays are returned for each match. If the regex is global, returns <code>[]</code> if no matches are found,
otherwise it returns an empty string.
</div>
<div>
<strong>Example:</strong>
<pre><code class="language-stscript">/let x color_green green lamp color_blue ||</code></pre>
<pre><code class="language-stscript">/match pattern="green" {{var::x}} | /echo |/# [ "green" ] ||</code></pre>
<pre><code class="language-stscript">/match pattern="color_(\\w+)" {{var::x}} | /echo |/# [ "color_green", "green" ] ||</code></pre>
<pre><code class="language-stscript">/match pattern="/color_(\\w+)/g" {{var::x}} | /echo |/# [ [ "color_green", "green" ], [ "color_blue", "blue" ] ] ||</code></pre>
<pre><code class="language-stscript">/match pattern="orange" {{var::x}} | /echo |/# ||</code></pre>
<pre><code class="language-stscript">/match pattern="/orange/g" {{var::x}} | /echo |/# [] ||</code></pre>
</div>
`,
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'chat-jump',
aliases: ['chat-scrollto', 'floor-teleport'],
callback: async (_, index) => {
const messageIndex = Number(index);
if (isNaN(messageIndex) || messageIndex < 0 || messageIndex >= chat.length) {
toastr.warning(t`Invalid message index: ${index}. Please enter a number between 0 and ${chat.length}.`);
console.warn(`WARN: Invalid message index provided for /chat-jump: ${index}. Max index: ${chat.length}`);
return '';
}
// Load more messages if needed
const firstDisplayedMessageId = getFirstDisplayedMessageId();
if (isFinite(firstDisplayedMessageId) && messageIndex < firstDisplayedMessageId) {
const needToLoadCount = firstDisplayedMessageId - messageIndex;
await showMoreMessages(needToLoadCount);
await delay(1);
}
const chatContainer = document.getElementById('chat');
const messageElement = document.querySelector(`#chat .mes[mesid="${messageIndex}"]`);
if (messageElement instanceof HTMLElement && chatContainer instanceof HTMLElement) {
const elementRect = messageElement.getBoundingClientRect();
const containerRect = chatContainer.getBoundingClientRect();
const scrollPosition = elementRect.top - containerRect.top + chatContainer.scrollTop;
chatContainer.scrollTo({
top: scrollPosition,
behavior: 'smooth',
});
flashHighlight($(messageElement), 2000);
} else {
toastr.warning(t`Could not find element for message ${messageIndex}. It might not be rendered yet or the index is invalid.`);
console.warn(`WARN: Element not found for message index ${messageIndex} in /chat-jump.`);
}
return '';
},
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'The message index (0-based) to scroll to.',
typeList: [ARGUMENT_TYPE.NUMBER],
isRequired: true,
enumProvider: commonEnumProviders.messages(),
}),
],
helpString: `
<div>
Scrolls the chat view to the specified message index. Index starts at 0.
</div>
<div>
<strong>Example:</strong> <pre><code>/chat-jump 10</code></pre> Scrolls to the 11th message (id=10).
</div>
`,
}));
registerVariableCommands();
}
@@ -3702,7 +3856,7 @@ export async function sendMessageAs(args, text) {
export async function sendNarratorMessage(args, text) {
text = String(text ?? '');
const name = chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT;
const name = args.name ?? (chat_metadata[NARRATOR_NAME_KEY] || NARRATOR_NAME_DEFAULT);
// Messages that do nothing but set bias will be hidden from the context
const bias = extractMessageBias(text);
const isSystem = bias && !removeMacros(text).length;
@@ -3942,6 +4096,7 @@ function getModelOptions(quiet) {
{ id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
{ id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
{ id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
{ id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
{ id: 'model_novel_select', api: 'novel', type: null },
{ id: 'horde_model', api: 'koboldhorde', type: null },
];

View File

@@ -50,6 +50,8 @@ import {
unshallowCharacter,
deleteLastMessage,
getCharacterCardFields,
swipe_right,
swipe_left,
} from '../script.js';
import {
extension_settings,
@@ -196,6 +198,7 @@ export function getContext() {
humanizedDateTime,
updateMessageBlock,
appendMediaToMessage,
swipe: { left: swipe_left, right: swipe_right },
variables: {
local: {
get: getLocalVariable,

View File

@@ -17,6 +17,7 @@ export let system_prompts = [];
const $enabled = $('#sysprompt_enabled');
const $select = $('#sysprompt_select');
const $content = $('#sysprompt_content');
const $postHistory = $('#sysprompt_post_history');
const $contentBlock = $('#SystemPromptBlock');
async function migrateSystemPromptFromInstructMode() {
@@ -25,6 +26,7 @@ async function migrateSystemPromptFromInstructMode() {
delete power_user.instruct.system_prompt;
power_user.sysprompt.enabled = power_user.instruct.enabled;
power_user.sysprompt.content = prompt;
power_user.sysprompt.post_history = '';
const existingPromptName = system_prompts.find(x => x.content === prompt)?.name;
@@ -59,7 +61,8 @@ export async function loadSystemPrompts(data) {
$enabled.prop('checked', power_user.sysprompt.enabled);
$select.val(power_user.sysprompt.name);
$content.val(power_user.sysprompt.content);
$content.val(power_user.sysprompt.content || '');
$postHistory.val(power_user.sysprompt.post_history || '');
if (!CSS.supports('field-sizing', 'content')) {
await resetScrollHeight($content);
}
@@ -165,13 +168,17 @@ export function initSystemPrompts() {
const name = String($(this).val());
const prompt = system_prompts.find(p => p.name === name);
if (prompt) {
$content.val(prompt.content);
$content.val(prompt.content || '');
$postHistory.val(prompt.post_history || '');
if (!CSS.supports('field-sizing', 'content')) {
await resetScrollHeight($content);
await resetScrollHeight($postHistory);
}
power_user.sysprompt.name = name;
power_user.sysprompt.content = prompt.content;
power_user.sysprompt.content = prompt.content || '';
power_user.sysprompt.post_history = prompt.post_history || '';
}
saveSettingsDebounced();
});
@@ -181,6 +188,11 @@ export function initSystemPrompts() {
saveSettingsDebounced();
});
$postHistory.on('input', function () {
power_user.sysprompt.post_history = String($(this).val());
saveSettingsDebounced();
});
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'sysprompt',
aliases: ['system-prompt'],

View File

@@ -27,7 +27,7 @@ import { debounce_timeout } from './constants.js';
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { renderTemplateAsync } from './templates.js';
import { t } from './i18n.js';
import { t, translate } from './i18n.js';
export {
TAG_FOLDER_TYPES,
@@ -318,7 +318,7 @@ function getTagBlock(tag, entities, hidden = 0, isUseless = false) {
template.find('.avatar').css({ 'background-color': tag.color, 'color': tag.color2 }).attr('title', `[Folder] ${tag.name}`);
template.find('.ch_name').text(tag.name).attr('title', `[Folder] ${tag.name}`);
template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : '');
template.find('.bogus_folder_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);
template.find('.bogus_folder_counter').text(`${count} ` + (count != 1 ? t`characters` : t`character`));
template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon);
if (isUseless) template.addClass('useless');
@@ -1057,7 +1057,7 @@ function appendTagToList(listElement, tag, { removable = false, isFilter = false
tagElement.attr('title', tag.title);
}
if (tag.icon) {
tagElement.find('.tag_name').text('').attr('title', `${tag.name} ${tag.title || ''}`.trim()).addClass(tag.icon);
tagElement.find('.tag_name').text('').attr('title', `${translate(tag.name)} ${tag.title || ''}`.trim()).addClass(tag.icon);
tagElement.addClass('actionable');
}
@@ -1644,6 +1644,7 @@ function updateDrawTagFolder(element, tag) {
// Draw/update css attributes for this class
folderElement.attr('title', tagFolder.tooltip);
folderElement.attr('data-i18n', '[title]' + tagFolder.tooltip);
const indicator = folderElement.find('.tag_folder_indicator');
indicator.text(tagFolder.icon);
indicator.css('color', tagFolder.color);
@@ -1655,14 +1656,7 @@ async function onTagDeleteClick() {
const tag = tags.find(x => x.id === id);
const otherTags = sortTags(tags.filter(x => x.id !== id).map(x => ({ id: x.id, name: x.name })));
const popupContent = $(`
<h3>Delete Tag</h3>
<div>Do you want to delete the tag <div id="tag_to_delete" class="tags_inline inline-flex margin-r2"></div>?</div>
<div class="m-t-2 marginBot5">If you want to merge all references to this tag into another tag, select it below:</div>
<select id="merge_tag_select">
<option value="">--- None ---</option>
${otherTags.map(x => `<option value="${x.id}">${x.name}</option>`).join('')}
</select>`);
const popupContent = $(await renderTemplateAsync('deleteTag', { otherTags }));
appendTagToList(popupContent.find('#tag_to_delete'), tag);

View File

@@ -0,0 +1,9 @@
<h3 data-i18n="Delete Tag">Delete Tag</h3>
<div><span data-i18n="Do you want to delete the tag">Do you want to delete the tag</span> <div id="tag_to_delete" class="tags_inline inline-flex margin-r2"></div>?</div>
<div class="m-t-2 marginBot5" data-i18n="If you want to merge all references to this tag into another tag, select it below:">If you want to merge all references to this tag into another tag, select it below:</div>
<select id="merge_tag_select">
<option value="">--- None ---</option>
{{#each otherTags}}
<option value="{{this.id}}">{{this.name}}</option>
{{/each}}
</select>

View File

@@ -0,0 +1,7 @@
<div class="text_block empty_block">
<i class="fa-solid {{icon}} fa-4x"></i>
<h1>{{text}}</h1>
<p data-i18n="There are no items to display.">
There are no items to display.
</p>
</div>

View File

@@ -0,0 +1,6 @@
<div class="text_block hidden_block">
<small>
<p>{{text}}</p>
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Characters and groups hidden by filters or closed folders" title="Characters and groups hidden by filters or closed folders"></div>
</small>
</div>

View File

@@ -83,7 +83,10 @@
<div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
</div>
<div class="flex-container ">
<div class=" flex1 tokenItemizingSubclass">-- Examples:</div>
<div class=" flex1 tokenItemizingSubclass">
<span>-- Examples:</span>
{{#if examplesCount}}<small>({{examplesCount}})</small>{{/if}}
</div>
<div class="tokenItemizingSubclass">{{examplesStringTokens}}</div>
</div>
<div class="flex-container ">
@@ -96,7 +99,10 @@
<div class="">{{worldInfoStringTokens}}</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: palegreen;"><span data-i18n="Chat History:">Chat History:</span></div>
<div class="flex1" style="color: palegreen;">
<span data-i18n="Chat History:">Chat History:</span>
{{#if messagesCount}}<small>({{messagesCount}})</small>{{/if}}
</div>
<div class="">{{ActualChatHistoryTokens}}</div>
</div>
<div class="wide100p flex-container flexNoGap flexFlowColumn">

View File

@@ -51,7 +51,10 @@
<div class="tokenItemizingSubclass">{{scenarioTextTokens}}</div>
</div>
<div class="flex-container">
<div class=" flex1 tokenItemizingSubclass">-- Examples:</div>
<div class=" flex1 tokenItemizingSubclass">
<span>-- Examples:</span>
{{#if examplesCount}}<small>({{examplesCount}})</small>{{/if}}
</div>
<div class="tokenItemizingSubclass"> {{examplesStringTokens}}</div>
</div>
<div class="flex-container">
@@ -68,7 +71,10 @@
<div class="">{{worldInfoStringTokens}}</div>
</div>
<div class="wide100p flex-container">
<div class="flex1" style="color: palegreen;">Chat History:</div>
<div class="flex1" style="color: palegreen;">
<span data-i18n="Chat History:">Chat History:</span>
{{#if messagesCount}}<small>({{messagesCount}})</small>{{/if}}
</div>
<div class=""> {{ActualChatHistoryTokens}}</div>
</div>
<div class="wide100p flex-container flexNoGap flexFlowColumn">

View File

@@ -7,6 +7,7 @@ import { renderTemplateAsync } from './templates.js';
import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { t } from './i18n.js';
import { accountStorage } from './util/AccountStorage.js';
import { localizePagination, PAGINATION_TEMPLATE } from './utils.js';
let mancerModels = [];
let togetherModels = [];
@@ -41,12 +42,7 @@ const OPENROUTER_PROVIDERS = [
'Avian',
'Lambda',
'Azure',
'Modal',
'AnyScale',
'Replicate',
'Perplexity',
'Recursal',
'OctoAI',
'DeepSeek',
'Infermatic',
'AI21',
@@ -54,10 +50,12 @@ const OPENROUTER_PROVIDERS = [
'Inflection',
'xAI',
'Cloudflare',
'SF Compute',
'Minimax',
'Nineteen',
'Liquid',
'GMICloud',
'Stealth',
'NCompass',
'InferenceNet',
'Friendli',
'AionLabs',
@@ -69,14 +67,16 @@ const OPENROUTER_PROVIDERS = [
'Targon',
'Ubicloud',
'Parasail',
'01.AI',
'HuggingFace',
'Phala',
'Cent-ML',
'Venice',
'OpenInference',
'Atoma',
'Enfer',
'Mancer',
'Mancer 2',
'Hyperbolic',
'Hyperbolic 2',
'Lynn 2',
'Lynn',
'Reflection',
];
@@ -362,9 +362,7 @@ export async function loadFeatherlessModels(data) {
showSizeChanger: false,
prevText: '<',
nextText: '>',
formatNavigator: function (currentPage, totalPage) {
return (currentPage - 1) * perPage + 1 + ' - ' + currentPage * perPage + ' of ' + totalPage * perPage;
},
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
callback: function (modelsOnPage, pagination) {
modelCardBlock.innerHTML = '';
@@ -386,15 +384,15 @@ export async function loadFeatherlessModels(data) {
const modelClassDiv = document.createElement('div');
modelClassDiv.classList.add('model-class');
modelClassDiv.textContent = `Class: ${model.model_class || 'N/A'}`;
modelClassDiv.textContent = t`Class` + `: ${model.model_class || 'N/A'}`;
const contextLengthDiv = document.createElement('div');
contextLengthDiv.classList.add('model-context-length');
contextLengthDiv.textContent = `Context Length: ${model.context_length}`;
contextLengthDiv.textContent = t`Context Length` + `: ${model.context_length}`;
const dateAddedDiv = document.createElement('div');
dateAddedDiv.classList.add('model-date-added');
dateAddedDiv.textContent = `Added On: ${new Date(model.created * 1000).toLocaleDateString()}`;
dateAddedDiv.textContent = t`Added On` + `: ${new Date(model.created * 1000).toLocaleDateString()}`;
detailsContainer.appendChild(modelClassDiv);
detailsContainer.appendChild(contextLengthDiv);
@@ -418,6 +416,7 @@ export async function loadFeatherlessModels(data) {
// Update the current page value whenever the page changes
featherlessCurrentPage = pagination.pageNumber;
localizePagination(paginationContainer);
},
afterSizeSelectorChange: function (e) {
const newPerPage = e.target.value;
@@ -923,6 +922,10 @@ export function getCurrentDreamGenModelTokenizer() {
return tokenizers.YI;
} else if (model.id.startsWith('opus-v1-xl')) {
return tokenizers.LLAMA;
} else if (model.id.startsWith('lucid-v1-medium')) {
return tokenizers.NEMO;
} else if (model.id.startsWith('lucid-v1-extra-large')) {
return tokenizers.LLAMA3;
} else {
return tokenizers.MISTRAL;
}

View File

@@ -1146,7 +1146,7 @@ function tryParseStreamingError(response, decoded) {
// No JSON. Do nothing.
}
const message = data?.error?.message || data?.message || data?.detail;
const message = data?.error?.message || data?.error || data?.message || data?.detail;
if (message) {
toastr.error(message, 'Text Completion API');

View File

@@ -586,6 +586,7 @@ export class ToolManager {
chat_completion_sources.DEEPSEEK,
chat_completion_sources.MAKERSUITE,
chat_completion_sources.AI21,
chat_completion_sources.XAI,
];
return supportedSources.includes(oai_settings.chat_completion_source);
}

View File

@@ -20,7 +20,46 @@ import { getCurrentLocale, t } from './i18n.js';
* Pagination status string template.
* @type {string}
*/
export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> of <%= totalNumber %>';
export const PAGINATION_TEMPLATE = '<%= rangeStart %>-<%= rangeEnd %> .. <%= totalNumber %>';
export const localizePagination = function(container) {
container.find('[title="Next page"]').attr('title', t`Next page`);
container.find('[title="Previous page"]').attr('title', t`Previous page`);
};
/**
* Renders a dropdown for selecting page size in pagination.
* @param {number} pageSize Page size
* @param {number[]} sizeChangerOptions Array of page size options
* @returns {string} The rendered dropdown element as a string
*/
export const renderPaginationDropdown = function(pageSize, sizeChangerOptions) {
const sizeSelect = document.createElement('select');
sizeSelect.classList.add('J-paginationjs-size-select');
if (sizeChangerOptions.indexOf(pageSize) === -1) {
sizeChangerOptions.unshift(pageSize);
sizeChangerOptions.sort((a, b) => a - b);
}
for (let i = 0; i < sizeChangerOptions.length; i++) {
const option = document.createElement('option');
option.value = `${sizeChangerOptions[i]}`;
option.textContent = `${sizeChangerOptions[i]} ${t`/ page`}`;
if (sizeChangerOptions[i] === pageSize) {
option.setAttribute('selected', 'selected');
}
sizeSelect.appendChild(option);
}
return sizeSelect.outerHTML;
};
export const paginationDropdownChangeHandler = function(event, size) {
let dropdown = $(event?.originalEvent?.currentTarget || event.delegateTarget).find('select');
dropdown.find('[selected]').removeAttr('selected');
dropdown.find(`[value=${size}]`).attr('selected', '');
};
/**
* Navigation options for pagination.
@@ -1047,7 +1086,7 @@ export function getImageSizeFromDataURL(dataUrl) {
/**
* Gets the filename of the character avatar without extension
* @param {number?} [chid=null] - Character ID. If not provided, uses the current character ID
* @param {string|number?} [chid=null] - Character ID. If not provided, uses the current character ID
* @param {object} [options={}] - Options arguments
* @param {string?} [options.manualAvatarKey=null] - Manually take the following avatar key, instead of using the chid to determine the name
* @returns {string?} The filename of the character avatar without extension, or null if the character ID is invalid

View File

@@ -97,12 +97,29 @@ export const MAX_SCAN_DEPTH = 1000;
const KNOWN_DECORATORS = ['@@activate', '@@dont_activate'];
// Typedef area
/**
* @typedef {object} WIGlobalScanData The chat-independent data to be scanned. Each of
* these fields can be enabled for scanning per entry.
* @property {string} personaDescription User persona description
* @property {string} characterDescription Character description
* @property {string} characterPersonality Character personality
* @property {string} characterDepthPrompt Character depth prompt (sometimes referred to as character notes)
* @property {string} scenario Character defined scenario
* @property {string} creatorNotes Character creator notes
*/
/**
* @typedef {object} WIScanEntry The entry that triggered the scan
* @property {number} [scanDepth] The depth of the scan
* @property {boolean} [caseSensitive] If the scan is case sensitive
* @property {boolean} [matchWholeWords] If the scan should match whole words
* @property {boolean} [useGroupScoring] If the scan should use group scoring
* @property {boolean} [matchPersonaDescription] If the scan should match against the persona description
* @property {boolean} [matchCharacterDescription] If the scan should match against the character description
* @property {boolean} [matchCharacterPersonality] If the scan should match against the character personality
* @property {boolean} [matchCharacterDepthPrompt] If the scan should match against the character depth prompt
* @property {boolean} [matchScenario] If the scan should match against the character scenario
* @property {boolean} [matchCreatorNotes] If the scan should match against the creator notes
* @property {number} [uid] The UID of the entry that triggered the scan
* @property {string} [world] The world info book of origin of the entry
* @property {string[]} [key] The primary keys to scan for
@@ -138,6 +155,11 @@ class WorldInfoBuffer {
*/
static externalActivations = new Map();
/**
* @type {WIGlobalScanData} Chat independent data to be scanned, such as persona and character descriptions
*/
#globalScanData = null;
/**
* @type {string[]} Array of messages sorted by ascending depth
*/
@@ -166,9 +188,11 @@ class WorldInfoBuffer {
/**
* Initialize the buffer with the given messages.
* @param {string[]} messages Array of messages to add to the buffer
* @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
*/
constructor(messages) {
constructor(messages, globalScanData) {
this.#initDepthBuffer(messages);
this.#globalScanData = globalScanData;
}
/**
@@ -225,6 +249,25 @@ class WorldInfoBuffer {
const JOINER = '\n' + MATCHER;
let result = MATCHER + this.#depthBuffer.slice(this.#startDepth, depth).join(JOINER);
if (entry.matchPersonaDescription && this.#globalScanData.personaDescription) {
result += JOINER + this.#globalScanData.personaDescription;
}
if (entry.matchCharacterDescription && this.#globalScanData.characterDescription) {
result += JOINER + this.#globalScanData.characterDescription;
}
if (entry.matchCharacterPersonality && this.#globalScanData.characterPersonality) {
result += JOINER + this.#globalScanData.characterPersonality;
}
if (entry.matchCharacterDepthPrompt && this.#globalScanData.characterDepthPrompt) {
result += JOINER + this.#globalScanData.characterDepthPrompt;
}
if (entry.matchScenario && this.#globalScanData.scenario) {
result += JOINER + this.#globalScanData.scenario;
}
if (entry.matchCreatorNotes && this.#globalScanData.creatorNotes) {
result += JOINER + this.#globalScanData.creatorNotes;
}
if (this.#injectBuffer.length > 0) {
result += JOINER + this.#injectBuffer.join(JOINER);
}
@@ -756,6 +799,7 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
* @param {string[]} chat - The chat messages to scan, in reverse order.
* @param {number} maxContext - The maximum context size of the generation.
* @param {boolean} isDryRun - If true, the function will not emit any events.
* @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
* @typedef {object} WIPromptResult
* @property {string} worldInfoString - Complete world info string
* @property {string} worldInfoBefore - World info that goes before the prompt
@@ -766,10 +810,10 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
* @property {Array} anAfter - Array of entries after Author's Note
* @returns {Promise<WIPromptResult>} The world info string and depth.
*/
export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
export async function getWorldInfoPrompt(chat, maxContext, isDryRun, globalScanData) {
let worldInfoString = '', worldInfoBefore = '', worldInfoAfter = '';
const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun);
const activatedWorldInfo = await checkWorldInfo(chat, maxContext, isDryRun, globalScanData);
worldInfoBefore = activatedWorldInfo.worldInfoBefore;
worldInfoAfter = activatedWorldInfo.worldInfoAfter;
worldInfoString = worldInfoBefore + worldInfoAfter;
@@ -966,7 +1010,7 @@ function registerWorldInfoSlashCommands() {
/**
* Gets the name of the character-bound lorebook.
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args Named arguments
* @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} name Character name
* @param {string} name Character name
* @returns {string} The name of the character-bound lorebook, a JSON string of the character's lorebooks, or an empty string
*/
function getCharBookCallback({ type }, name) {
@@ -1338,6 +1382,18 @@ function registerWorldInfoSlashCommands() {
}
}
async function getGlobalBooksCallback() {
if (!selected_world_info?.length) {
return JSON.stringify([]);
}
let entries = selected_world_info.slice();
console.debug(`[WI] Selected global world info has ${entries.length} entries`, selected_world_info);
return JSON.stringify(entries);
}
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'world',
callback: onWorldInfoChange,
@@ -1379,6 +1435,13 @@ function registerWorldInfoSlashCommands() {
],
aliases: ['getchatlore', 'getchatwi'],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'getglobalbooks',
callback: getGlobalBooksCallback,
returns: 'list of selected lorebook names',
helpString: 'Get a list of names of the selected global lorebooks and pass it down the pipe.',
aliases: ['getgloballore', 'getglobalwi'],
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'getpersonabook',
callback: getPersonaBookCallback,
@@ -2172,6 +2235,12 @@ export const originalWIDataKeyMap = {
'matchWholeWords': 'extensions.match_whole_words',
'useGroupScoring': 'extensions.use_group_scoring',
'caseSensitive': 'extensions.case_sensitive',
'matchPersonaDescription': 'extensions.match_persona_description',
'matchCharacterDescription': 'extensions.match_character_description',
'matchCharacterPersonality': 'extensions.match_character_personality',
'matchCharacterDepthPrompt': 'extensions.match_character_depth_prompt',
'matchScenario': 'extensions.match_scenario',
'matchCreatorNotes': 'extensions.match_creator_notes',
'scanDepth': 'extensions.scan_depth',
'automationId': 'extensions.automation_id',
'vectorized': 'extensions.vectorized',
@@ -2589,7 +2658,7 @@ export async function getWorldEntry(name, data, entry) {
if (!isMobile()) {
$(characterFilter).select2({
width: '100%',
placeholder: 'Tie this entry to specific characters or characters with specific tags',
placeholder: t`Tie this entry to specific characters or characters with specific tags`,
allowClear: true,
closeOnSelect: false,
});
@@ -3189,7 +3258,7 @@ export async function getWorldEntry(name, data, entry) {
// Create wrapper div
const wrapper = document.createElement('div');
wrapper.textContent = t`Move "${sourceName}" to:`;
wrapper.textContent = t`Move '${sourceName}' to:`;
// Create container and append elements
const container = document.createElement('div');
@@ -3289,6 +3358,28 @@ export async function getWorldEntry(name, data, entry) {
});
useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');
function handleMatchCheckbox(fieldName) {
const key = originalWIDataKeyMap[fieldName];
const checkBoxElem = template.find(`input[type="checkbox"][name="${fieldName}"]`);
checkBoxElem.data('uid', entry.uid);
checkBoxElem.on('input', async function () {
const uid = $(this).data('uid');
const value = $(this).prop('checked');
data.entries[uid][fieldName] = value;
setWIOriginalDataValue(data, uid, key, data.entries[uid][fieldName]);
await saveWorldInfo(name, data);
});
checkBoxElem.prop('checked', !!entry[fieldName]).trigger('input');
}
handleMatchCheckbox('matchPersonaDescription');
handleMatchCheckbox('matchCharacterDescription');
handleMatchCheckbox('matchCharacterPersonality');
handleMatchCheckbox('matchCharacterDepthPrompt');
handleMatchCheckbox('matchScenario');
handleMatchCheckbox('matchCreatorNotes');
// automation id
const automationIdInput = template.find('input[name="automationId"]');
automationIdInput.data('uid', entry.uid);
@@ -3424,7 +3515,7 @@ function createEntryInputAutocomplete(input, callback, { allowMultiple = false }
});
$(input).on('focus click', function () {
$(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : $(input).val());
$(input).autocomplete('search', allowMultiple ? String($(input).val()).split(/,\s*/).pop() : String($(input).val()));
});
}
@@ -3495,6 +3586,12 @@ export const newWorldInfoEntryDefinition = {
disable: { default: false, type: 'boolean' },
excludeRecursion: { default: false, type: 'boolean' },
preventRecursion: { default: false, type: 'boolean' },
matchPersonaDescription: { default: false, type: 'boolean' },
matchCharacterDescription: { default: false, type: 'boolean' },
matchCharacterPersonality: { default: false, type: 'boolean' },
matchCharacterDepthPrompt: { default: false, type: 'boolean' },
matchScenario: { default: false, type: 'boolean' },
matchCreatorNotes: { default: false, type: 'boolean' },
delayUntilRecursion: { default: 0, type: 'number' },
probability: { default: 100, type: 'number' },
useProbability: { default: true, type: 'boolean' },
@@ -3959,6 +4056,7 @@ function parseDecorators(content) {
* @param {string[]} chat The chat messages to scan, in reverse order.
* @param {number} maxContext The maximum context size of the generation.
* @param {boolean} isDryRun Whether to perform a dry run.
* @param {WIGlobalScanData} globalScanData Chat independent context to be scanned
* @typedef {object} WIActivated
* @property {string} worldInfoBefore The world info before the chat.
* @property {string} worldInfoAfter The world info after the chat.
@@ -3969,9 +4067,9 @@ function parseDecorators(content) {
* @property {Set<any>} allActivatedEntries All entries.
* @returns {Promise<WIActivated>} The world info activated.
*/
export async function checkWorldInfo(chat, maxContext, isDryRun) {
export async function checkWorldInfo(chat, maxContext, isDryRun, globalScanData) {
const context = getContext();
const buffer = new WorldInfoBuffer(chat);
const buffer = new WorldInfoBuffer(chat, globalScanData);
console.debug(`[WI] --- START WI SCAN (on ${chat.length} messages)${isDryRun ? ' (DRY RUN)' : ''} ---`);
@@ -4829,6 +4927,12 @@ export function convertCharacterBook(characterBook) {
sticky: entry.extensions?.sticky ?? null,
cooldown: entry.extensions?.cooldown ?? null,
delay: entry.extensions?.delay ?? null,
matchPersonaDescription: entry.extensions?.match_persona_description ?? false,
matchCharacterDescription: entry.extensions?.match_character_description ?? false,
matchCharacterPersonality: entry.extensions?.match_character_personality ?? false,
matchCharacterDepthPrompt: entry.extensions?.match_character_depth_prompt ?? false,
matchScenario: entry.extensions?.match_scenario ?? false,
matchCreatorNotes: entry.extensions?.match_creator_notes ?? false,
extensions: entry.extensions ?? {},
};
});
@@ -5099,7 +5203,7 @@ export function openWorldInfoEditor(worldName) {
/**
* Assigns a lorebook to the current chat.
* @param {PointerEvent} event Pointer event
* @param {JQuery.ClickEvent<Document, undefined, any, any>} event Pointer event
* @returns {Promise<void>}
*/
export async function assignLorebookToChat(event) {
@@ -5138,11 +5242,106 @@ export async function assignLorebookToChat(event) {
saveMetadata();
});
return callGenericPopup(template, POPUP_TYPE.TEXT);
await callGenericPopup(template, POPUP_TYPE.TEXT);
}
jQuery(() => {
/**
* Moves a World Info entry from a source lorebook to a target lorebook.
*
* @param {string} sourceName - The name of the source lorebook file.
* @param {string} targetName - The name of the target lorebook file.
* @param {string|number} uid - The UID of the entry to move from the source lorebook.
* @returns {Promise<boolean>} True if the move was successful, false otherwise.
*/
export async function moveWorldInfoEntry(sourceName, targetName, uid) {
if (sourceName === targetName) {
return false;
}
if (!world_names.includes(sourceName)) {
toastr.error(t`Source lorebook '${sourceName}' not found.`);
console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`);
return false;
}
if (!world_names.includes(targetName)) {
toastr.error(t`Target lorebook '${targetName}' not found.`);
console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`);
return false;
}
const entryUidString = String(uid);
try {
const sourceData = await loadWorldInfo(sourceName);
const targetData = await loadWorldInfo(targetName);
if (!sourceData || !sourceData.entries) {
toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`);
console.error(`[WI Move] Could not load source data for '${sourceName}'.`);
return false;
}
if (!targetData || !targetData.entries) {
toastr.error(t`Failed to load data for target lorebook '${targetName}'.`);
console.error(`[WI Move] Could not load target data for '${targetName}'.`);
return false;
}
if (!sourceData.entries[entryUidString]) {
toastr.error(t`Entry not found in source lorebook '${sourceName}'.`);
console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`);
return false;
}
const entryToMove = structuredClone(sourceData.entries[entryUidString]);
const newUid = getFreeWorldEntryUid(targetData);
if (newUid === null) {
console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
return false;
}
entryToMove.uid = newUid;
// Place the entry at the end of the target lorebook
const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1);
entryToMove.displayIndex = maxDisplayIndex + 1;
targetData.entries[newUid] = entryToMove;
delete sourceData.entries[entryUidString];
// Remove from originalData if it exists
deleteWIOriginalDataValue(sourceData, entryUidString);
// TODO: setWIOriginalDataValue
console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
await saveWorldInfo(targetName, targetData, true);
console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
await saveWorldInfo(sourceName, sourceData, true);
console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`);
// Check if the currently viewed book in the editor is the source or target and reload it
const currentEditorBookIndex = Number($('#world_editor_select').val());
if (!isNaN(currentEditorBookIndex)) {
const currentEditorBookName = world_names[currentEditorBookIndex];
if (currentEditorBookName === sourceName || currentEditorBookName === targetName) {
reloadEditor(currentEditorBookName);
}
}
return true;
} catch (error) {
toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
console.error('[WI Move] Unexpected error:', error);
return false;
}
}
export function initWorldInfo() {
$('#world_info').on('mousedown change', async function (e) {
// If there's no world names, don't do anything
if (world_names.length === 0) {
@@ -5329,7 +5528,7 @@ jQuery(() => {
if (!isMobile()) {
$('#world_info').select2({
width: '100%',
placeholder: 'No Worlds active. Click here to select.',
placeholder: t`No Worlds active. Click here to select.`,
allowClear: true,
closeOnSelect: false,
});
@@ -5354,100 +5553,4 @@ jQuery(() => {
}
});
});
});
/**
* Moves a World Info entry from a source lorebook to a target lorebook.
*
* @param {string} sourceName - The name of the source lorebook file.
* @param {string} targetName - The name of the target lorebook file.
* @param {string|number} uid - The UID of the entry to move from the source lorebook.
* @returns {Promise<boolean>} True if the move was successful, false otherwise.
*/
export async function moveWorldInfoEntry(sourceName, targetName, uid) {
if (sourceName === targetName) {
return false;
}
if (!world_names.includes(sourceName)) {
toastr.error(t`Source lorebook '${sourceName}' not found.`);
console.error(`[WI Move] Source lorebook '${sourceName}' does not exist.`);
return false;
}
if (!world_names.includes(targetName)) {
toastr.error(t`Target lorebook '${targetName}' not found.`);
console.error(`[WI Move] Target lorebook '${targetName}' does not exist.`);
return false;
}
const entryUidString = String(uid);
try {
const sourceData = await loadWorldInfo(sourceName);
const targetData = await loadWorldInfo(targetName);
if (!sourceData || !sourceData.entries) {
toastr.error(t`Failed to load data for source lorebook '${sourceName}'.`);
console.error(`[WI Move] Could not load source data for '${sourceName}'.`);
return false;
}
if (!targetData || !targetData.entries) {
toastr.error(t`Failed to load data for target lorebook '${targetName}'.`);
console.error(`[WI Move] Could not load target data for '${targetName}'.`);
return false;
}
if (!sourceData.entries[entryUidString]) {
toastr.error(t`Entry not found in source lorebook '${sourceName}'.`);
console.error(`[WI Move] Entry UID ${entryUidString} not found in '${sourceName}'.`);
return false;
}
const entryToMove = structuredClone(sourceData.entries[entryUidString]);
const newUid = getFreeWorldEntryUid(targetData);
if (newUid === null) {
console.error(`[WI Move] Failed to get a free UID in '${targetName}'.`);
return false;
}
entryToMove.uid = newUid;
// Place the entry at the end of the target lorebook
const maxDisplayIndex = Object.values(targetData.entries).reduce((max, entry) => Math.max(max, entry.displayIndex ?? -1), -1);
entryToMove.displayIndex = maxDisplayIndex + 1;
targetData.entries[newUid] = entryToMove;
delete sourceData.entries[entryUidString];
// Remove from originalData if it exists
deleteWIOriginalDataValue(sourceData, entryUidString);
// TODO: setWIOriginalDataValue
console.debug(`[WI Move] Removed entry UID ${entryUidString} from source '${sourceName}'.`);
await saveWorldInfo(targetName, targetData, true);
console.debug(`[WI Move] Saved target lorebook '${targetName}'.`);
await saveWorldInfo(sourceName, sourceData, true);
console.debug(`[WI Move] Saved source lorebook '${sourceName}'.`);
console.log(`[WI Move] ${entryToMove.comment} moved successfully to '${targetName}'.`);
// Check if the currently viewed book in the editor is the source or target and reload it
const currentEditorBookIndex = Number($('#world_editor_select').val());
if (!isNaN(currentEditorBookIndex)) {
const currentEditorBookName = world_names[currentEditorBookIndex];
if (currentEditorBookName === sourceName || currentEditorBookName === targetName) {
reloadEditor(currentEditorBookName);
}
}
return true;
} catch (error) {
toastr.error(t`An unexpected error occurred while moving the entry: ${error.message}`);
console.error('[WI Move] Unexpected error:', error);
return false;
}
}