Merge branch 'staging' into integrity

This commit is contained in:
Cohee
2025-03-18 01:51:07 +02:00
38 changed files with 631 additions and 133 deletions

View File

@@ -5,6 +5,7 @@ import { saveMetadataDebounced } from './extensions.js';
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';
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
@@ -243,7 +244,7 @@ async function getNewBackgroundName(referenceElement) {
const fileExtension = oldBg.split('.').pop();
const fileNameBase = isCustom ? oldBg.split('/').pop() : oldBg;
const oldBgExtensionless = fileNameBase.replace(`.${fileExtension}`, '');
const newBgExtensionless = await callPopup('<h3>Enter new background name:</h3>', 'input', oldBgExtensionless);
const newBgExtensionless = await callPopup('<h3>' + t`Enter new background name:` + '</h3>', 'input', oldBgExtensionless);
if (!newBgExtensionless) {
console.debug('no new_bg_extensionless');

View File

@@ -358,7 +358,7 @@ export async function convertSoloToGroupChat() {
// Click on the freshly selected group to open it
await openGroupById(group.id);
toastr.success('The chat has been successfully converted!');
toastr.success(t`The chat has been successfully converted!`);
}
/**

View File

@@ -41,6 +41,7 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types
* @property {string} chat_completion_source - Source provider for chat completion
* @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
*/
/** @typedef {Record<string, any> & ChatCompletionPayloadBase} ChatCompletionPayload */
@@ -264,7 +265,7 @@ export class ChatCompletionService {
* @param {ChatCompletionPayload} custom
* @returns {ChatCompletionPayload}
*/
static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, ...props }) {
static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) {
const payload = {
...props,
messages,
@@ -272,6 +273,7 @@ export class ChatCompletionService {
chat_completion_source,
max_tokens,
temperature,
custom_url,
stream: false,
};

View File

@@ -424,7 +424,7 @@ jQuery(async () => {
installHintButton.on('click', async function () {
const installButton = $('#third_party_extension_button');
flashHighlight(installButton, 5000);
toastr.info('Click the flashing button to install extensions.', 'How to install extensions?');
toastr.info(t`Click the flashing button to install extensions.`, t`How to install extensions?`);
});
const connectButton = windowHtml.find('#assets-connect-button');

View File

@@ -1,4 +1,4 @@
<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat.">
<div id="attachFile" class="list-group-item flex-container flexGap5" data-i18n="[title]Attach a file or image to a current chat." title="Attach a file or image to a current chat.">
<div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
<span data-i18n="Attach a File">Attach a File</span>
</div>

View File

@@ -39,6 +39,7 @@ const CC_COMMANDS = [
'proxy',
'stop-strings',
'start-reply-with',
'reasoning-template',
];
const TC_COMMANDS = [
@@ -54,6 +55,7 @@ const TC_COMMANDS = [
'tokenizer',
'stop-strings',
'start-reply-with',
'reasoning-template',
];
const FANCY_NAMES = {
@@ -70,6 +72,7 @@ const FANCY_NAMES = {
'tokenizer': 'Tokenizer',
'stop-strings': 'Custom Stopping Strings',
'start-reply-with': 'Start Reply With',
'reasoning-template': 'Reasoning Template',
};
/**
@@ -154,6 +157,7 @@ const profilesProvider = () => [
* @property {string} [tokenizer] Tokenizer
* @property {string} [stop-strings] Custom Stopping Strings
* @property {string} [start-reply-with] Start Reply With
* @property {string} [reasoning-template] Reasoning Template
* @property {string[]} [exclude] Commands to exclude
*/

View File

@@ -2154,7 +2154,7 @@ function migrateSettings() {
imgElement.src = '';
}
setExpressionOverrideHtml();
setExpressionOverrideHtml(true); // force-clear, as the character might not have an override defined
if (isVisualNovelMode()) {
$('#visual-novel-wrapper').empty();

View File

@@ -19,7 +19,7 @@
<div id="regex_info_block_wrapper">
<div id="regex_info_block" class="info-block"></div>
<a id="regex_info_block_flags_hint" href="https://docs.sillytavern.app/extensions/regex/#flags" target="_blank" rel="noopener noreferrer">
<i class="fa-solid fa-circle-info" title="Click here to learn more about regex flags."></i>
<i class="fa-solid fa-circle-info" data-i18n="[title]ext_regex_flags_help" title="Click here to learn more about regex flags."></i>
</a>
</div>
@@ -147,7 +147,7 @@
</label>
<span>
<small data-i18n="ext_regex_other_options" data-i18n="Ephemerality">Ephemerality</small>
<span class="fa-solid fa-circle-question note-link-span" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
<span class="fa-solid fa-circle-question note-link-span" data-i18n="[title]ext_regex_other_options_desc" title="By default, regex scripts alter the chat file directly and irreversibly.&#13;Enabling either (or both) of the options below will prevent chat file alteration, while still altering the specified item(s)."></span>
</span>
<label class="checkbox flex-container" data-i18n="[title]ext_regex_only_format_visual_desc" title="Chat history file contents won't change, but regex will be applied to the messages displayed in the Chat UI.">
<input type="checkbox" name="only_format_display" />

View File

@@ -398,7 +398,7 @@ function runRegexCallback(args, value) {
for (const script of scripts) {
if (script.scriptName.toLowerCase() === scriptName.toLowerCase()) {
if (script.disabled) {
toastr.warning(`Regex script "${scriptName}" is disabled.`);
toastr.warning(t`Regex script "${scriptName}" is disabled.`);
return value;
}

View File

@@ -323,6 +323,7 @@ export class ConnectionManagerRequestService {
max_tokens: maxTokens,
model: profile.model,
chat_completion_source: selectedApiMap.source,
custom_url: profile['api-url'],
}, {
presetName: includePreset ? profile.preset : undefined,
}, extractData);

View File

@@ -1207,8 +1207,8 @@ jQuery(async function () {
eventSource.on(event_types.GROUP_UPDATED, onChatChanged);
eventSource.on(event_types.GENERATION_STARTED, onGenerationStarted);
eventSource.on(event_types.GENERATION_ENDED, onGenerationEnded);
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, onMessageEvent);
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, onMessageEvent);
eventSource.makeLast(event_types.CHARACTER_MESSAGE_RENDERED, (messageId) => onMessageEvent(messageId));
eventSource.makeLast(event_types.USER_MESSAGE_RENDERED, (messageId) => onMessageEvent(messageId));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'speak',
callback: async (args, value) => {

View File

@@ -696,7 +696,7 @@ export function getGroupBlock(group) {
template.find('.group_fav_icon').css('display', 'none');
template.addClass(group.fav ? 'is_fav' : '');
template.find('.ch_fav').val(group.fav);
template.find('.group_select_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);
template.find('.group_select_counter').text(count + ' ' + (count != 1 ? t`characters` : t`character`));
template.find('.group_select_block_list').text(namesList.join(', '));
// Display inline tags

View File

@@ -10,6 +10,7 @@ import { SECRET_KEYS, writeSecret } from './secrets.js';
import { delay } from './utils.js';
import { isMobile } from './RossAscends-mods.js';
import { autoSelectInstructPreset } from './instruct-mode.js';
import { t } from './i18n.js';
export {
horde_settings,
@@ -169,7 +170,7 @@ async function adjustHordeGenerationParams(max_context_length, max_length) {
}
}
console.log(maxContextLength, maxLength);
$('#adjustedHordeParams').text(`Context: ${maxContextLength}, Response: ${maxLength}`);
$('#adjustedHordeParams').text(t`Context` + `: ${maxContextLength}, ` + t`Response` + `: ${maxLength}`);
return { maxContextLength, maxLength };
}
@@ -177,7 +178,7 @@ function setContextSizePreview() {
if (horde_settings.models.length) {
adjustHordeGenerationParams(max_context, amount_gen);
} else {
$('#adjustedHordeParams').text('Context: --, Response: --');
$('#adjustedHordeParams').text(t`Context` + ': --, ' + t`Response` + ': --');
}
}
@@ -404,7 +405,7 @@ jQuery(function () {
if (horde_settings.models.length) {
adjustHordeGenerationParams(max_context, amount_gen);
} else {
$('#adjustedHordeParams').text('Context: --, Response: --');
$('#adjustedHordeParams').text(t`Context` + ': --, ' + t`Response` + ': --');
}
saveSettingsDebounced();

View File

@@ -398,23 +398,26 @@ export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvata
/**
* Formats instruct mode system prompt.
* @param {string} systemPrompt System prompt string.
* @param {InstructSettings} customInstruct Custom instruct mode settings.
* @returns {string} Formatted instruct mode system prompt.
*/
export function formatInstructModeSystemPrompt(systemPrompt) {
export function formatInstructModeSystemPrompt(systemPrompt, customInstruct = null) {
if (!systemPrompt) {
return '';
}
const separator = power_user.instruct.wrap ? '\n' : '';
const instruct = structuredClone(customInstruct ?? power_user.instruct);
if (power_user.instruct.system_sequence_prefix) {
const separator = instruct.wrap ? '\n' : '';
if (instruct.system_sequence_prefix) {
// TODO: Replace with a proper 'System' prompt entity name input
const prefix = power_user.instruct.system_sequence_prefix.replace(/{{name}}/gi, 'System');
const prefix = instruct.system_sequence_prefix.replace(/{{name}}/gi, 'System');
systemPrompt = prefix + separator + systemPrompt;
}
if (power_user.instruct.system_sequence_suffix) {
systemPrompt = systemPrompt + separator + power_user.instruct.system_sequence_suffix;
if (instruct.system_sequence_suffix) {
systemPrompt = systemPrompt + separator + instruct.system_sequence_suffix;
}
return systemPrompt;

View File

@@ -705,16 +705,18 @@ export function parseExampleIntoIndividual(messageExampleString, appendNamesForG
return result;
}
function formatWorldInfo(value) {
export function formatWorldInfo(value, { wiFormat = null } = {}) {
if (!value) {
return '';
}
if (!oai_settings.wi_format.trim()) {
const format = wiFormat ?? oai_settings.wi_format;
if (!format.trim()) {
return value;
}
return stringFormat(oai_settings.wi_format, value);
return stringFormat(format, value);
}
/**
@@ -952,7 +954,7 @@ async function populateDialogueExamples(prompts, chatCompletion, messageExamples
* @param {number} position - Prompt position in the extensions object.
* @returns {string|false} - The prompt position for prompt collection.
*/
function getPromptPosition(position) {
export function getPromptPosition(position) {
if (position == extension_prompt_types.BEFORE_PROMPT) {
return 'start';
}
@@ -969,7 +971,7 @@ function getPromptPosition(position) {
* @param {number} role Role of the prompt.
* @returns {string} Mapped role.
*/
function getPromptRole(role) {
export function getPromptRole(role) {
switch (role) {
case extension_prompt_roles.SYSTEM:
return 'system';
@@ -3476,7 +3478,7 @@ async function getStatusOpen() {
let status;
if ('ai' in window) {
status = 'Valid';
status = t`Valid`;
}
else {
showWindowExtensionError();
@@ -3525,7 +3527,7 @@ async function getStatusOpen() {
const canBypass = (oai_settings.chat_completion_source === chat_completion_sources.OPENAI && oai_settings.bypass_status_check) || oai_settings.chat_completion_source === chat_completion_sources.CUSTOM;
if (canBypass) {
setOnlineStatus('Status check bypassed');
setOnlineStatus(t`Status check bypassed`);
}
try {
@@ -3547,7 +3549,7 @@ async function getStatusOpen() {
saveModelList(responseData.data);
}
if (!('error' in responseData)) {
setOnlineStatus('Valid');
setOnlineStatus(t`Valid`);
}
} catch (error) {
console.error(error);

View File

@@ -800,7 +800,7 @@ async function selectCurrentPersona({ toastPersonaNameChange = true } = {}) {
chat_metadata['persona'] = user_avatar;
console.log(`Auto locked persona to ${user_avatar}`);
if (toastPersonaNameChange && power_user.persona_show_notifications) {
toastr.success(`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
toastr.success(t`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`);
}
saveMetadataDebounced();
updatePersonaUIStates();

View File

@@ -55,6 +55,7 @@ import { POPUP_TYPE, callGenericPopup } from './popup.js';
import { loadSystemPrompts } from './sysprompt.js';
import { fuzzySearchCategories } from './filters.js';
import { accountStorage } from './util/AccountStorage.js';
import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
export {
loadPowerUserSettings,
@@ -257,6 +258,7 @@ let power_user = {
},
reasoning: {
name: DEFAULT_REASONING_TEMPLATE,
auto_parse: false,
add_to_prompts: false,
auto_expand: false,
@@ -1624,6 +1626,7 @@ async function loadPowerUserSettings(settings, data) {
await loadInstructMode(data);
await loadContextSettings();
await loadSystemPrompts(data);
await loadReasoningTemplates(data);
loadMaxContextUnlocked();
switchWaifuMode();
switchSpoilerMode();
@@ -1985,15 +1988,21 @@ export function fuzzySearchGroups(searchValue, fuzzySearchCaches = null) {
/**
* Renders a story string template with the given parameters.
* @param {object} params Template parameters.
* @param {object} [options] Additional options.
* @param {string} [options.customStoryString] Custom story string template.
* @param {InstructSettings} [options.customInstructSettings] Custom instruct settings.
* @returns {string} The rendered story string.
*/
export function renderStoryString(params) {
export function renderStoryString(params, { customStoryString = null, customInstructSettings = null } = {}) {
try {
const storyString = customStoryString ?? power_user.context.story_string;
const instructSettings = structuredClone(customInstructSettings ?? power_user.instruct);
// Validate and log possible warnings/errors
validateStoryString(power_user.context.story_string, params);
validateStoryString(storyString, params);
// compile the story string template into a function, with no HTML escaping
const compiledTemplate = Handlebars.compile(power_user.context.story_string, { noEscape: true });
const compiledTemplate = Handlebars.compile(storyString, { noEscape: true });
// render the story string template with the given params
let output = compiledTemplate(params);
@@ -2006,7 +2015,7 @@ export function renderStoryString(params) {
// add a newline to the end of the story string if it doesn't have one
if (output.length > 0 && !output.endsWith('\n')) {
if (!power_user.instruct.enabled || power_user.instruct.wrap) {
if (!instructSettings.enabled || instructSettings.wrap) {
output += '\n';
}
}
@@ -4229,14 +4238,13 @@ $(document).ready(() => {
],
callback: (args, value) => {
const force = isTrueBoolean(String(args?.force ?? false));
value = String(value ?? '').trim();
// Skip processing if no value and not forced
if (!force && !value) {
return power_user.user_prompt_bias;
}
power_user.user_prompt_bias = value;
power_user.user_prompt_bias = String(value ?? '');
$('#start_reply_with').val(power_user.user_prompt_bias);
saveSettingsDebounced();

View File

@@ -21,7 +21,7 @@ import { groups, selected_group } from './group-chats.js';
import { instruct_presets } from './instruct-mode.js';
import { kai_settings } from './kai-settings.js';
import { convertNovelPreset } from './nai-settings.js';
import { openai_settings, openai_setting_names, oai_settings } from './openai.js';
import { openai_settings, openai_setting_names } from './openai.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { context_presets, getContextSettings, power_user } from './power-user.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
@@ -38,6 +38,7 @@ import {
} from './textgen-settings.js';
import { download, parseJsonFile, waitUntilCondition } from './utils.js';
import { t } from './i18n.js';
import { reasoning_templates } from './reasoning.js';
const presetManagers = {};
@@ -168,6 +169,20 @@ class PresetManager {
},
isValid: (data) => PresetManager.isPossiblyTextCompletionData(data),
},
'reasoning': {
name: 'Reasoning Formatting',
getData: () => {
const manager = getPresetManager('reasoning');
const name = manager.getSelectedPresetName();
return manager.getPresetSettings(name);
},
setData: (data) => {
const manager = getPresetManager('reasoning');
const name = data.name;
return manager.savePreset(name, data);
},
isValid: (data) => PresetManager.isPossiblyReasoningData(data),
},
};
static isPossiblyInstructData(data) {
@@ -190,6 +205,11 @@ class PresetManager {
return data && textCompletionProps.every(prop => Object.keys(data).includes(prop));
}
static isPossiblyReasoningData(data) {
const reasoningProps = ['name', 'prefix', 'suffix', 'separator'];
return data && reasoningProps.every(prop => Object.keys(data).includes(prop));
}
/**
* Imports master settings from JSON data.
* @param {object} data Data to import
@@ -227,6 +247,12 @@ class PresetManager {
return await getPresetManager('textgenerationwebui').savePreset(fileName, data);
}
// 5. Reasoning Template
if (this.isPossiblyReasoningData(data)) {
toastr.info(t`Importing as reasoning template...`, t`Reasoning template detected`);
return await getPresetManager('reasoning').savePreset(data.name, data);
}
const validSections = [];
for (const [key, section] of Object.entries(this.masterSections)) {
if (key in data && section.isValid(data[key])) {
@@ -478,6 +504,10 @@ class PresetManager {
presets = system_prompts;
preset_names = system_prompts.map(x => x.name);
break;
case 'reasoning':
presets = reasoning_templates;
preset_names = reasoning_templates.map(x => x.name);
break;
default:
console.warn(`Unknown API ID ${api}`);
}
@@ -490,7 +520,7 @@ class PresetManager {
}
isAdvancedFormatting() {
return this.apiId == 'context' || this.apiId == 'instruct' || this.apiId == 'sysprompt';
return ['context', 'instruct', 'sysprompt', 'reasoning'].includes(this.apiId);
}
updateList(name, preset) {
@@ -553,6 +583,11 @@ class PresetManager {
sysprompt_preset['name'] = name || power_user.sysprompt.preset;
return sysprompt_preset;
}
case 'reasoning': {
const reasoning_preset = structuredClone(power_user.reasoning);
reasoning_preset['name'] = name || power_user.reasoning.preset;
return reasoning_preset;
}
default:
console.warn(`Unknown API ID ${apiId}`);
return {};
@@ -599,6 +634,13 @@ class PresetManager {
'include_reasoning',
'global_banned_tokens',
'send_banned_tokens',
// Reasoning exclusions
'auto_parse',
'add_to_prompts',
'auto_expand',
'show_hidden',
'max_additions',
];
const settings = Object.assign({}, getSettingsByApiId(this.apiId));

View File

@@ -7,14 +7,46 @@ import { getCurrentLocale, t, translate } from './i18n.js';
import { MacrosParser } from './macros.js';
import { chat_completion_sources, getChatCompletionModel, oai_settings } from './openai.js';
import { Popup } from './popup.js';
import { power_user } from './power-user.js';
import { performFuzzySearch, power_user } from './power-user.js';
import { getPresetManager } from './preset-manager.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { enumTypes, SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
import { copyText, escapeRegex, isFalseBoolean, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js';
/**
* @typedef {object} ReasoningTemplate
* @property {string} name - The name of the template
* @property {string} prefix - Reasoning prefix
* @property {string} suffix - Reasoning suffix
* @property {string} separator - Reasoning separator
*/
/**
* @type {ReasoningTemplate[]} List of reasoning templates
*/
export const reasoning_templates = [];
export const DEFAULT_REASONING_TEMPLATE = 'DeepSeek';
/**
* @type {Record<string, JQuery<HTMLElement>>} List of UI elements for reasoning settings
* @readonly
*/
const UI = {
$select: $('#reasoning_select'),
$suffix: $('#reasoning_suffix'),
$prefix: $('#reasoning_prefix'),
$separator: $('#reasoning_separator'),
$autoParse: $('#reasoning_auto_parse'),
$autoExpand: $('#reasoning_auto_expand'),
$showHidden: $('#reasoning_show_hidden'),
$addToPrompts: $('#reasoning_add_to_prompts'),
$maxAdditions: $('#reasoning_max_additions'),
};
/**
* Enum representing the type of the reasoning for a message (where it came from)
@@ -61,7 +93,7 @@ export function extractReasoningFromData(data, {
mainApi = null,
ignoreShowThoughts = false,
textGenType = null,
chatCompletionSource = null
chatCompletionSource = null,
} = {}) {
switch (mainApi ?? main_api) {
case 'textgenerationwebui':
@@ -669,57 +701,102 @@ export class PromptReasoning {
}
function loadReasoningSettings() {
$('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
$('#reasoning_add_to_prompts').on('change', function () {
UI.$addToPrompts.prop('checked', power_user.reasoning.add_to_prompts);
UI.$addToPrompts.on('change', function () {
power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#reasoning_prefix').val(power_user.reasoning.prefix);
$('#reasoning_prefix').on('input', function () {
UI.$prefix.val(power_user.reasoning.prefix);
UI.$prefix.on('input', function () {
power_user.reasoning.prefix = String($(this).val());
saveSettingsDebounced();
});
$('#reasoning_suffix').val(power_user.reasoning.suffix);
$('#reasoning_suffix').on('input', function () {
UI.$suffix.val(power_user.reasoning.suffix);
UI.$suffix.on('input', function () {
power_user.reasoning.suffix = String($(this).val());
saveSettingsDebounced();
});
$('#reasoning_separator').val(power_user.reasoning.separator);
$('#reasoning_separator').on('input', function () {
UI.$separator.val(power_user.reasoning.separator);
UI.$separator.on('input', function () {
power_user.reasoning.separator = String($(this).val());
saveSettingsDebounced();
});
$('#reasoning_max_additions').val(power_user.reasoning.max_additions);
$('#reasoning_max_additions').on('input', function () {
UI.$maxAdditions.val(power_user.reasoning.max_additions);
UI.$maxAdditions.on('input', function () {
power_user.reasoning.max_additions = Number($(this).val());
saveSettingsDebounced();
});
$('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
$('#reasoning_auto_parse').on('change', function () {
UI.$autoParse.prop('checked', power_user.reasoning.auto_parse);
UI.$autoParse.on('change', function () {
power_user.reasoning.auto_parse = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#reasoning_auto_expand').prop('checked', power_user.reasoning.auto_expand);
$('#reasoning_auto_expand').on('change', function () {
UI.$autoExpand.prop('checked', power_user.reasoning.auto_expand);
UI.$autoExpand.on('change', function () {
power_user.reasoning.auto_expand = !!$(this).prop('checked');
toggleReasoningAutoExpand();
saveSettingsDebounced();
});
toggleReasoningAutoExpand();
$('#reasoning_show_hidden').prop('checked', power_user.reasoning.show_hidden);
$('#reasoning_show_hidden').on('change', function () {
UI.$showHidden.prop('checked', power_user.reasoning.show_hidden);
UI.$showHidden.on('change', function () {
power_user.reasoning.show_hidden = !!$(this).prop('checked');
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
saveSettingsDebounced();
});
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null);
UI.$select.on('change', async function () {
const name = String($(this).val());
const template = reasoning_templates.find(p => p.name === name);
if (!template) {
return;
}
UI.$prefix.val(template.prefix);
UI.$suffix.val(template.suffix);
UI.$separator.val(template.separator);
power_user.reasoning.name = name;
power_user.reasoning.prefix = template.prefix;
power_user.reasoning.suffix = template.suffix;
power_user.reasoning.separator = template.separator;
saveSettingsDebounced();
});
}
function selectReasoningTemplateCallback(args, name) {
if (!name) {
return power_user.reasoning.name ?? '';
}
const quiet = isTrueBoolean(args?.quiet);
const templateNames = reasoning_templates.map(preset => preset.name);
let foundName = templateNames.find(x => x.toLowerCase() === name.toLowerCase());
if (!foundName) {
const result = performFuzzySearch('reasoning-templates', templateNames, [], name);
if (result.length === 0) {
!quiet && toastr.warning(`Reasoning template "${name}" not found`);
return '';
}
foundName = result[0].item;
}
UI.$select.val(foundName).trigger('change');
!quiet && toastr.success(`Reasoning template "${foundName}" selected`);
return foundName;
}
function registerReasoningSlashCommands() {
@@ -853,6 +930,42 @@ function registerReasoningSlashCommands() {
: parsedReasoning.reasoning;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reasoning-template',
aliases: ['reasoning-formatting', 'reasoning-preset'],
callback: selectReasoningTemplateCallback,
returns: 'template name',
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'quiet',
description: 'Suppress the toast message on template change',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'false',
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'reasoning template name',
typeList: [ARGUMENT_TYPE.STRING],
enumProvider: () => reasoning_templates.map(x => new SlashCommandEnumValue(x.name, null, enumTypes.enum, enumIcons.preset)),
}),
],
helpString: `
<div>
Selects a reasoning template by name, using fuzzy search to find the closest match.
Gets the current template if no name is provided.
</div>
<div>
<strong>Example:</strong>
<ul>
<li>
<pre><code class="language-stscript">/reasoning-template DeepSeek</code></pre>
</li>
</ul>
</div>
`,
}));
}
function registerReasoningMacros() {
@@ -1212,6 +1325,53 @@ function registerReasoningAppEvents() {
}
}
/**
* Loads reasoning templates from the settings data.
* @param {object} data Settings data
* @param {ReasoningTemplate[]} data.reasoning Reasoning templates
* @returns {Promise<void>}
*/
export async function loadReasoningTemplates(data) {
if (data.reasoning !== undefined) {
reasoning_templates.splice(0, reasoning_templates.length, ...data.reasoning);
}
for (const template of reasoning_templates) {
$('<option>').val(template.name).text(template.name).appendTo(UI.$select);
}
// No template name, need to migrate
if (power_user.reasoning.name === undefined) {
const defaultTemplate = reasoning_templates.find(p => p.name === DEFAULT_REASONING_TEMPLATE);
if (defaultTemplate) {
// If the reasoning settings were modified - migrate them to a custom template
if (power_user.reasoning.prefix !== defaultTemplate.prefix || power_user.reasoning.suffix !== defaultTemplate.suffix || power_user.reasoning.separator !== defaultTemplate.separator) {
/** @type {ReasoningTemplate} */
const data = {
name: '[Migrated] Custom',
prefix: power_user.reasoning.prefix,
suffix: power_user.reasoning.suffix,
separator: power_user.reasoning.separator,
};
await getPresetManager('reasoning')?.savePreset(data.name, data);
power_user.reasoning.name = data.name;
} else {
power_user.reasoning.name = defaultTemplate.name;
}
} else {
// Template not found (deleted or content check skipped - leave blank)
power_user.reasoning.name = '';
}
saveSettingsDebounced();
}
UI.$select.val(power_user.reasoning.name);
}
/**
* Initializes reasoning settings and event handlers.
*/
export function initReasoning() {
loadReasoningSettings();
setReasoningEventHandlers();

View File

@@ -9,6 +9,7 @@ import { power_user } from './power-user.js';
//import { getSortableDelay, onlyUnique } from './utils.js';
//import { getCfgPrompt } from './cfg-scale.js';
import { setting_names } from './textgen-settings.js';
import { renderTemplateAsync } from './templates.js';
const TGsamplerNames = setting_names;
@@ -25,25 +26,7 @@ async function showSamplerSelectPopup() {
const html = $(document.createElement('div'));
html.attr('id', 'sampler_view_list')
.addClass('flex-container flexFlowColumn');
html.append(`
<div class="title_restorable flexFlowColumn alignItemsBaseline">
<div class="flex-container justifyCenter">
<h3>Sampler Select</h3>
<div class="flex-container alignItemsBaseline">
<div id="resetSelectedSamplers" class="menu_button menu_button_icon" title="Reset custom sampler selection">
<i class="fa-solid fa-recycle"></i>
</div>
</div>
<!--<div class="flex-container alignItemsBaseline">
<div class="menu_button menu_button_icon" title="Create a new sampler">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Create">Create</span>
</div>
</div>-->
</div>
<small>Here you can toggle the display of individual samplers. (WIP)</small>
</div>
<hr>`);
html.append(await renderTemplateAsync('samplerSelector'));
const listContainer = $('<div id="apiSamplersList" class="flex-container flexNoGap"></div>');
const APISamplers = await listSamplers(main_api);

View File

@@ -1,5 +1,6 @@
import { DOMPurify } from '../lib.js';
import { callPopup, getRequestHeaders } from '../script.js';
import { t } from './i18n.js';
export const SECRET_KEYS = {
HORDE: 'api_key_horde',
@@ -104,7 +105,7 @@ async function viewSecrets() {
});
if (response.status == 403) {
callPopup('<h3>Forbidden</h3><p>To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.</p>', 'text');
callPopup('<h3>' + t`Forbidden` + '</h3><p>' + t`To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.` + '</p>', 'text');
return;
}

View File

@@ -49,6 +49,7 @@ import {
clearChat,
unshallowCharacter,
deleteLastMessage,
getCharacterCardFields,
} from '../script.js';
import {
extension_settings,
@@ -78,7 +79,7 @@ import { ToolManager } from './tool-calling.js';
import { accountStorage } from './util/AccountStorage.js';
import { timestampToMoment, uuidv4 } from './utils.js';
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
import { convertCharacterBook, getWorldInfoPrompt, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
import { ChatCompletionService, TextCompletionService } from './custom-request.js';
import { ConnectionManagerRequestService } from './extensions/shared.js';
import { updateReasoningUI, parseReasoningFromString } from './reasoning.js';
@@ -189,6 +190,7 @@ export function getContext() {
textCompletionSettings: textgenerationwebui_settings,
powerUserSettings: power_user,
getCharacters,
getCharacterCardFields,
uuidv4,
humanizedDateTime,
updateMessageBlock,
@@ -207,6 +209,7 @@ export function getContext() {
saveWorldInfo,
updateWorldInfoList,
convertCharacterBook,
getWorldInfoPrompt,
CONNECT_API_MAP,
getTextGenServer,
extractMessageFromData,

View File

@@ -1,9 +1,9 @@
<div data-type="assistant_note">
<div>
<b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
<span>Click the button to save it as a file.</span>
<span data-i18n="Click the button to save it as a file.">Click the button to save it as a file.</span>
</div>
<div class="assistant_note_export menu_button menu_button_icon" title="Export as JSONL">
<div class="assistant_note_export menu_button menu_button_icon" data-i18n="[title]Export as JSONL" title="Export as JSONL">
<i class="fa-solid fa-file-export"></i>
</div>
</div>

View File

@@ -0,0 +1,18 @@
<div class="title_restorable flexFlowColumn alignItemsBaseline">
<div class="flex-container justifyCenter">
<h3 data-i18n="Sampler Select">Sampler Select</h3>
<div class="flex-container alignItemsBaseline">
<div id="resetSelectedSamplers" class="menu_button menu_button_icon" data-i18n="[title]Reset custom sampler selection" title="Reset custom sampler selection">
<i class="fa-solid fa-recycle"></i>
</div>
</div>
<!--<div class="flex-container alignItemsBaseline">
<div class="menu_button menu_button_icon" title="Create a new sampler">
<i class="fa-solid fa-plus"></i>
<span data-i18n="Create">Create</span>
</div>
</div>-->
</div>
<small data-i18n="Here you can toggle the display of individual samplers. (WIP)">Here you can toggle the display of individual samplers. (WIP)</small>
</div>
<hr>

View File

@@ -753,10 +753,17 @@ export const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOn
/**
* Gets the world info based on chat messages.
* @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.
* @typedef {{worldInfoString: string, worldInfoBefore: string, worldInfoAfter: string, worldInfoExamples: any[], worldInfoDepth: any[]}} WIPromptResult
* @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.
* @typedef {object} WIPromptResult
* @property {string} worldInfoString - Complete world info string
* @property {string} worldInfoBefore - World info that goes before the prompt
* @property {string} worldInfoAfter - World info that goes after the prompt
* @property {Array} worldInfoExamples - Array of example entries
* @property {Array} worldInfoDepth - Array of depth entries
* @property {Array} anBefore - Array of entries before Author's Note
* @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) {
@@ -778,6 +785,8 @@ export async function getWorldInfoPrompt(chat, maxContext, isDryRun) {
worldInfoAfter,
worldInfoExamples: activatedWorldInfo.EMEntries ?? [],
worldInfoDepth: activatedWorldInfo.WIDepthEntries ?? [],
anBefore: activatedWorldInfo.ANBeforeEntries ?? [],
anAfter: activatedWorldInfo.ANAfterEntries ?? [],
};
}
@@ -3862,7 +3871,14 @@ 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.
* @typedef {{ worldInfoBefore: string, worldInfoAfter: string, EMEntries: any[], WIDepthEntries: any[], allActivatedEntries: Set<any> }} WIActivated
* @typedef {object} WIActivated
* @property {string} worldInfoBefore The world info before the chat.
* @property {string} worldInfoAfter The world info after the chat.
* @property {any[]} EMEntries The entries for examples.
* @property {any[]} WIDepthEntries The depth entries.
* @property {any[]} ANBeforeEntries The entries before Author's Note.
* @property {any[]} ANAfterEntries The entries after Author's Note.
* @property {Set<any>} allActivatedEntries All entries.
* @returns {Promise<WIActivated>} The world info activated.
*/
export async function checkWorldInfo(chat, maxContext, isDryRun) {
@@ -3906,7 +3922,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
timedEffects.checkTimedEffects();
if (sortedEntries.length === 0) {
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], allActivatedEntries: new Set() };
return { worldInfoBefore: '', worldInfoAfter: '', WIDepthEntries: [], EMEntries: [], ANBeforeEntries: [], ANAfterEntries: [], allActivatedEntries: new Set() };
}
/** @type {number[]} Represents the delay levels for entries that are delayed until recursion */
@@ -4355,7 +4371,7 @@ export async function checkWorldInfo(chat, maxContext, isDryRun) {
console.log(`[WI] ${isDryRun ? 'Hypothetically adding' : 'Adding'} ${allActivatedEntries.size} entries to prompt`, Array.from(allActivatedEntries.values()));
console.debug(`[WI] --- DONE${isDryRun ? ' (DRY RUN)' : ''} ---`);
return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
return { worldInfoBefore, worldInfoAfter, EMEntries, WIDepthEntries, ANBeforeEntries: ANTopEntries, ANAfterEntries: ANBottomEntries, allActivatedEntries: new Set(allActivatedEntries.values()) };
}
/**