Merge branch 'staging' into fnr

This commit is contained in:
Cohee
2025-01-31 21:09:33 +02:00
29 changed files with 537 additions and 177 deletions

View File

@@ -566,7 +566,7 @@ export function initAuthorsNote() {
namedArgumentList: [],
unnamedArgumentList: [
new SlashCommandArgument(
'position', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
),
],
helpString: `

View File

@@ -96,8 +96,13 @@ function highlightLockedBackground() {
});
}
/**
* Locks the background for the current chat
* @param {Event} e Click event
* @returns {string} Empty string
*/
function onLockBackgroundClick(e) {
e.stopPropagation();
e?.stopPropagation();
const chatName = getCurrentChatId();
@@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
return '';
}
const relativeBgImage = getUrlParameter(this);
const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
saveBackgroundMetadata(relativeBgImage);
setCustomBackground();
@@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
return '';
}
/**
* Locks the background for the current chat
* @param {Event} e Click event
* @returns {string} Empty string
*/
function onUnlockBackgroundClick(e) {
e.stopPropagation();
e?.stopPropagation();
removeBackgroundMetadata();
unsetCustomBackground();
highlightLockedBackground();
@@ -513,12 +523,12 @@ export function initBackgrounds() {
$('#add_bg_button').on('change', onBackgroundUploadSelected);
$('#bg-filter').on('input', onBackgroundFilterInput);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
callback: onLockBackgroundClick,
callback: () => onLockBackgroundClick(new CustomEvent('click')),
aliases: ['bglock'],
helpString: 'Locks a background for the currently selected chat',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
callback: onUnlockBackgroundClick,
callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
aliases: ['bgunlock'],
helpString: 'Unlocks a background for the currently selected chat',
}));

View File

@@ -30,6 +30,7 @@ const CC_COMMANDS = [
'api-url',
'model',
'proxy',
'stop-strings',
];
const TC_COMMANDS = [
@@ -43,6 +44,7 @@ const TC_COMMANDS = [
'context',
'instruct-state',
'tokenizer',
'stop-strings',
];
const FANCY_NAMES = {
@@ -57,6 +59,7 @@ const FANCY_NAMES = {
'instruct': 'Instruct Template',
'context': 'Context Template',
'tokenizer': 'Tokenizer',
'stop-strings': 'Custom Stopping Strings',
};
/**
@@ -138,6 +141,7 @@ const profilesProvider = () => [
* @property {string} [context] Context Template
* @property {string} [instruct-state] Instruct Mode
* @property {string} [tokenizer] Tokenizer
* @property {string} [stop-strings] Custom Stopping Strings
* @property {string[]} [exclude] Commands to exclude
*/

View File

@@ -883,6 +883,10 @@ export class SlashCommandHandler {
}
}
getQuickReply(args) {
if (!args.id && !args.label) {
toastr.error('Please provide a valid id or label.');
return '';
}
try {
return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
} catch (ex) {

View File

@@ -94,6 +94,12 @@
<span data-i18n="World Info">World Info</span>
</label>
</div>
<div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
<label class="checkbox flex-container">
<input type="checkbox" name="replace_position" value="6">
<span data-i18n="Reasoning">Reasoning</span>
</label>
</div>
<div class="flex-container wide100p marginTop5">
<div class="flex1 flex-container flexNoGap">
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">

View File

@@ -20,6 +20,7 @@ const regex_placement = {
SLASH_COMMAND: 3,
// 4 - sendAs (legacy)
WORLD_INFO: 5,
REASONING: 6,
};
export const substitute_find_regex = {
@@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
// Script applies to Generate and input is Generate
(script.promptOnly && isPrompt) ||
// Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
(!script.markdownOnly && !script.promptOnly && !isMarkdown)
(!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
) {
if (isEdit && !script.runOnEdit) {
console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);

View File

@@ -18,7 +18,7 @@ import { t } from '../../i18n.js';
* @property {string} replaceString - The replace string
* @property {string[]} trimStrings - The trim strings
* @property {string?} findRegex - The find regex
* @property {string?} substituteRegex - The substitute regex
* @property {number?} substituteRegex - The substitute regex
*/
/**

View File

@@ -388,7 +388,7 @@ class AllTalkTtsProvider {
}
async fetchRvcVoiceObjects() {
if (this.settings.server_version == 'v2') {
if (this.settings.server_version == 'v1') {
console.log('Skipping RVC voices fetch for V1 server');
return [];
}
@@ -1031,14 +1031,18 @@ class AllTalkTtsProvider {
console.error('fetchTtsGeneration Error Response Text:', errorText);
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
// Handle V1/V2 URL differences
const outputUrl = this.settings.server_version === 'v1'
? data.output_file_url // V1 returns full URL
: `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path
// V1 returns a complete URL, V2 returns a relative path
if (this.settings.server_version === 'v1') {
// V1: Use the complete URL directly from the response
return data.output_file_url;
} else {
// V2: Combine the endpoint with the relative path
return `${this.settings.provider_endpoint}${data.output_file_url}`;
}
return outputUrl;
} catch (error) {
console.error('[fetchTtsGeneration] Exception caught:', error);
throw error;

View File

@@ -30,6 +30,7 @@ import { GoogleTranslateTtsProvider } from './google-translate.js';
export { talkingAnimation };
const UPDATE_INTERVAL = 1000;
const wrapper = new ModuleWorkerWrapper(moduleWorker);
let voiceMapEntries = [];
let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
@@ -120,7 +121,7 @@ async function onNarrateOneMessage() {
}
resetTtsPlayback();
ttsJobQueue.push(message);
processAndQueueTtsMessage(message);
moduleWorker();
}
@@ -147,7 +148,7 @@ async function onNarrateText(args, text) {
}
resetTtsPlayback();
ttsJobQueue.push({ mes: text, name: name });
processAndQueueTtsMessage({ mes: text, name: name });
await moduleWorker();
// Return back to the chat voices
@@ -220,6 +221,36 @@ function isTtsProcessing() {
return processing;
}
/**
* Splits a message into lines and adds each non-empty line to the TTS job queue.
* @param {Object} message - The message object to be processed.
* @param {string} message.mes - The text of the message to be split into lines.
* @param {string} message.name - The name associated with the message.
* @returns {void}
*/
function processAndQueueTtsMessage(message) {
if (!extension_settings.tts.narrate_by_paragraphs) {
ttsJobQueue.push(message);
return;
}
const lines = message.mes.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.length === 0) {
continue;
}
ttsJobQueue.push(
Object.assign({}, message, {
mes: line,
}),
);
}
}
function debugTtsPlayback() {
console.log(JSON.stringify(
{
@@ -350,7 +381,7 @@ function onAudioControlClicked() {
talkingAnimation(false);
} else {
// Default play behavior if not processing or playing is to play the last message.
ttsJobQueue.push(context.chat[context.chat.length - 1]);
processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
}
updateUiAudioPlayState();
}
@@ -376,6 +407,7 @@ function completeCurrentAudioJob() {
currentAudioJob = null;
talkingAnimation(false); //stop lip animation
// updateUiPlayState();
wrapper.update();
}
/**
@@ -466,7 +498,7 @@ async function processTtsQueue() {
}
if (extension_settings.tts.skip_tags) {
text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim();
text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim();
}
if (!extension_settings.tts.pass_asterisks) {
@@ -569,6 +601,7 @@ function loadSettings() {
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
$('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
$('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
$('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
$('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
$('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
@@ -638,6 +671,11 @@ function onPeriodicAutoGenerationClick() {
saveSettingsDebounced();
}
function onNarrateByParagraphsClick() {
extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
saveSettingsDebounced();
}
function onNarrateDialoguesClick() {
extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
@@ -816,7 +854,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
lastChatId = context.chatId;
console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
ttsJobQueue.push(message);
if (extension_settings.tts.periodic_auto_generation) {
ttsJobQueue.push(message);
} else {
processAndQueueTtsMessage(message);
}
}
async function onMessageDeleted() {
@@ -1156,6 +1199,7 @@ jQuery(async function () {
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
$('#tts_auto_generation').on('click', onAutoGenerationClick);
$('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
$('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
$('#tts_narrate_user').on('click', onNarrateUserClick);
$('#playback_rate').on('input', function () {
@@ -1177,7 +1221,6 @@ jQuery(async function () {
loadSettings(); // Depends on Extension Controls and loadTtsProvider
loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
addAudioControl(); // Depends on Extension Controls
const wrapper = new ModuleWorkerWrapper(moduleWorker);
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);

View File

@@ -30,6 +30,10 @@
<input type="checkbox" id="tts_periodic_auto_generation">
<small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
</label>
<label class="checkbox_label" for="tts_narrate_by_paragraphs">
<input type="checkbox" id="tts_narrate_by_paragraphs">
<small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
</label>
<label class="checkbox_label" for="tts_narrate_quoted">
<input type="checkbox" id="tts_narrate_quoted">
<small data-i18n="Only narrate quotes">Only narrate "quotes"</small>

View File

@@ -27,24 +27,45 @@ export async function hideLoader() {
}
return new Promise((resolve) => {
// Spinner blurs/fades out
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
const spinner = $('#load-spinner');
if (!spinner.length) {
console.warn('Spinner element not found, skipping animation');
cleanup();
return;
}
// Check if transitions are enabled
const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
const hasTransitions = parseFloat(transitionDuration) > 0;
if (hasTransitions) {
Promise.race([
new Promise((r) => setTimeout(r, 500)), // Fallback timeout
new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
]).finally(cleanup);
} else {
cleanup();
}
function cleanup() {
$('#loader').remove();
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
// If it's present, we remove it once and then it's gone.
yoinkPreloader();
loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {
loaderPopup = null;
resolve();
});
});
loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
.catch((err) => console.error('Error completing loaderPopup:', err))
.finally(() => {
loaderPopup = null;
resolve();
});
}
$('#load-spinner')
.css({
'filter': 'blur(15px)',
'opacity': '0',
});
// Apply the styles
spinner.css({
'filter': 'blur(15px)',
'opacity': '0',
});
});
}

View File

@@ -258,7 +258,7 @@ const default_settings = {
ai21_model: 'jamba-1.5-large',
mistralai_model: 'mistral-large-latest',
cohere_model: 'command-r-plus',
perplexity_model: 'llama-3.1-70b-instruct',
perplexity_model: 'sonar-pro',
groq_model: 'llama-3.1-70b-versatile',
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
@@ -337,7 +337,7 @@ const oai_settings = {
ai21_model: 'jamba-1.5-large',
mistralai_model: 'mistral-large-latest',
cohere_model: 'command-r-plus',
perplexity_model: 'llama-3.1-70b-instruct',
perplexity_model: 'sonar-pro',
groq_model: 'llama-3.1-70b-versatile',
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
@@ -4380,28 +4380,19 @@ async function onModelChange() {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
}
else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 127000);
}
else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 200000);
}
else if (oai_settings.perplexity_model.includes('llama-3.1')) {
const isOnline = oai_settings.perplexity_model.includes('online');
const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
$('#openai_max_context').attr('max', contextSize);
}
else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', max_32k);
}
else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 28000);
}
else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', max_16k);
}
else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', max_8k);
}
else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 12000);
}
else {
$('#openai_max_context').attr('max', max_4k);
$('#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');

View File

@@ -254,6 +254,7 @@ let power_user = {
},
reasoning: {
auto_parse: false,
add_to_prompts: false,
prefix: '<think>\n',
suffix: '\n</think>',
@@ -2916,6 +2917,46 @@ export function flushEphemeralStoppingStrings() {
EPHEMERAL_STOPPING_STRINGS.splice(0, EPHEMERAL_STOPPING_STRINGS.length);
}
/**
* Checks if the generated text should be filtered based on the auto-swipe settings.
* @param {string} text The text to check
* @returns {boolean} If the generated text should be filtered
*/
export function generatedTextFiltered(text) {
/**
* Checks if the given text contains any of the blacklisted words.
* @param {string} text The text to check
* @param {string[]} blacklist The list of blacklisted words
* @param {number} threshold The number of blacklisted words that need to be present to trigger the check
* @returns {boolean} Whether the text contains blacklisted words
*/
function containsBlacklistedWords(text, blacklist, threshold) {
const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
const matches = text.match(regex) || [];
return matches.length >= threshold;
}
// Make sure a generated text is non-empty
// Otherwise we might get in a loop with a broken API
text = text.trim();
if (text.length > 0) {
if (power_user.auto_swipe_minimum_length) {
if (text.length < power_user.auto_swipe_minimum_length) {
console.log('Generated text size too small');
return true;
}
}
if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
console.log('Generated text has blacklisted words');
return true;
}
}
}
return false;
}
/**
* Gets the custom stopping strings from the power user settings.
* @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
@@ -4072,4 +4113,45 @@ $(document).ready(() => {
],
helpString: 'activates a movingUI preset by name',
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'stop-strings',
aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
helpString: `
<div>
Sets a list of custom stopping strings. Gets the list if no value is provided.
</div>
<div>
<strong>Examples:</strong>
</div>
<ul>
<li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
<li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
</ul>
`,
returns: ARGUMENT_TYPE.LIST,
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'list of strings',
typeList: [ARGUMENT_TYPE.LIST],
acceptsMultiple: false,
isRequired: false,
}),
],
callback: (_, value) => {
if (String(value ?? '').trim()) {
const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
if (!parsedValue || !Array.isArray(parsedValue)) {
throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
}
parsedValue.forEach((item, index) => {
parsedValue[index] = String(item);
});
power_user.custom_stopping_strings = JSON.stringify(parsedValue);
$('#custom_stopping_strings').val(power_user.custom_stopping_strings);
saveSettingsDebounced();
}
return power_user.custom_stopping_strings;
},
}));
});

View File

@@ -1,4 +1,5 @@
import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { t } from './i18n.js';
import { MacrosParser } from './macros.js';
import { Popup } from './popup.js';
@@ -7,7 +8,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { copyText } from './utils.js';
import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
/**
* Gets a message from a jQuery element.
@@ -49,11 +50,12 @@ export class PromptReasoning {
* Add reasoning to a message according to the power user settings.
* @param {string} content Message content
* @param {string} reasoning Message reasoning
* @param {boolean} isPrefix Whether this is the last message prefix
* @returns {string} Message content with reasoning
*/
addToMessage(content, reasoning) {
addToMessage(content, reasoning, isPrefix) {
// Disabled or reached limit of additions
if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {
if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
return content;
}
@@ -70,6 +72,11 @@ export class PromptReasoning {
const separator = substituteParams(power_user.reasoning.separator || '');
const suffix = substituteParams(power_user.reasoning.suffix || '');
// Combine parts with reasoning only
if (isPrefix && !content) {
return `${prefix}${reasoning}`;
}
// Combine parts with reasoning and content
return `${prefix}${reasoning}${suffix}${separator}${content}`;
}
@@ -105,11 +112,18 @@ function loadReasoningSettings() {
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 () {
power_user.reasoning.auto_parse = !!$(this).prop('checked');
saveSettingsDebounced();
});
}
function registerReasoningSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reasoning-get',
aliases: ['get-reasoning'],
returns: ARGUMENT_TYPE.STRING,
helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
unnamedArgumentList: [
@@ -129,6 +143,7 @@ function registerReasoningSlashCommands() {
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reasoning-set',
aliases: ['set-reasoning'],
returns: ARGUMENT_TYPE.STRING,
helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
namedArgumentList: [
@@ -146,7 +161,7 @@ function registerReasoningSlashCommands() {
}),
],
callback: async (args, value) => {
const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1;
const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
const message = chat[messageId];
if (!message?.extra) {
return '';
@@ -160,6 +175,50 @@ function registerReasoningSlashCommands() {
return message.extra.reasoning;
},
}));
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
name: 'reasoning-parse',
aliases: ['parse-reasoning'],
returns: 'reasoning string',
helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
namedArgumentList: [
SlashCommandNamedArgument.fromProps({
name: 'regex',
description: 'Whether to apply regex scripts to the reasoning content.',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
}),
],
unnamedArgumentList: [
SlashCommandArgument.fromProps({
description: 'input string',
typeList: [ARGUMENT_TYPE.STRING],
}),
],
callback: (args, value) => {
if (!value) {
return '';
}
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
return String(value);
}
const parsedReasoning = parseReasoningFromString(String(value));
if (!parsedReasoning) {
return '';
}
const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
return applyRegex
? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
: parsedReasoning.reasoning;
},
}));
}
function registerReasoningMacros() {
@@ -168,7 +227,7 @@ function registerReasoningMacros() {
MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
}
function setReasoningEventHandlers(){
function setReasoningEventHandlers() {
$(document).on('click', '.mes_reasoning_copy', (e) => {
e.stopPropagation();
e.preventDefault();
@@ -224,7 +283,7 @@ function setReasoningEventHandlers(){
}
const textarea = messageBlock.find('.reasoning_edit_textarea');
const reasoning = String(textarea.val());
const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
message.extra.reasoning = reasoning;
await saveChatConditional();
updateMessageBlock(messageId, message);
@@ -289,9 +348,101 @@ function setReasoningEventHandlers(){
});
}
/**
* Parses reasoning from a string using the power user reasoning settings.
* @typedef {Object} ParsedReasoning
* @property {string} reasoning Reasoning block
* @property {string} content Message content
* @param {string} str Content of the message
* @returns {ParsedReasoning|null} Parsed reasoning block and message content
*/
function parseReasoningFromString(str) {
// Both prefix and suffix must be defined
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
return null;
}
try {
const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
let didReplace = false;
let reasoning = '';
let content = String(str).replace(regex, (_match, captureGroup) => {
didReplace = true;
reasoning = captureGroup;
return '';
});
if (didReplace && power_user.trim_spaces) {
reasoning = reasoning.trim();
content = content.trim();
}
return { reasoning, content };
} catch (error) {
console.error('[Reasoning] Error parsing reasoning block', error);
return null;
}
}
function registerReasoningAppEvents() {
eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
if (!power_user.reasoning.auto_parse) {
return;
}
console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
const message = chat[idx];
if (!message) {
console.warn('[Reasoning] Message not found', idx);
return null;
}
if (!message.mes || message.mes === '...') {
console.debug('[Reasoning] Message content is empty or a placeholder', idx);
return null;
}
const parsedReasoning = parseReasoningFromString(message.mes);
// No reasoning block found
if (!parsedReasoning) {
return;
}
// Make sure the message has an extra object
if (!message.extra || typeof message.extra !== 'object') {
message.extra = {};
}
const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
// If reasoning was found, add it to the message
if (parsedReasoning.reasoning) {
message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
}
// Update the message text if it was changed
if (parsedReasoning.content !== message.mes) {
message.mes = parsedReasoning.content;
}
// Find if a message already exists in DOM and must be updated
if (contentUpdated) {
const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
if (messageRendered) {
console.debug('[Reasoning] Updating message block', idx);
updateMessageBlock(idx, message);
}
}
});
}
export function initReasoning() {
loadReasoningSettings();
setReasoningEventHandlers();
registerReasoningSlashCommands();
registerReasoningMacros();
registerReasoningAppEvents();
}

View File

@@ -42,6 +42,7 @@ import {
showMoreMessages,
stopGeneration,
substituteParams,
syncCurrentSwipeInfoExtras,
system_avatar,
system_message_types,
this_chid,
@@ -2870,8 +2871,11 @@ async function addSwipeCallback(args, value) {
const newSwipeId = lastMessage.swipes.length - 1;
if (isTrueBoolean(args.switch)) {
// Make sure ad-hoc changes to extras are saved before swiping away
syncCurrentSwipeInfoExtras();
lastMessage.swipe_id = newSwipeId;
lastMessage.mes = lastMessage.swipes[newSwipeId];
lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
}
await saveChatConditional();

View File

@@ -69,6 +69,7 @@ import { textgenerationwebui_settings } from './textgen-settings.js';
import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
import { ToolManager } from './tool-calling.js';
import { timestampToMoment, uuidv4 } from './utils.js';
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
export function getContext() {
return {
@@ -175,6 +176,16 @@ export function getContext() {
humanizedDateTime,
updateMessageBlock,
appendMediaToMessage,
variables: {
local: {
get: getLocalVariable,
set: setLocalVariable,
},
global: {
get: getGlobalVariable,
set: setGlobalVariable,
},
},
};
}

View File

@@ -54,6 +54,12 @@ const OPENROUTER_PROVIDERS = [
'xAI',
'Cloudflare',
'SF Compute',
'Minimax',
'Nineteen',
'Liquid',
'Nebius',
'Chutes',
'Kluster',
'01.AI',
'HuggingFace',
'Mancer',

View File

@@ -501,7 +501,7 @@ export function loadTextGenSettings(data, loadedSettings) {
for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
const control = $(selector);
control.val(settings.server_urls[type] ?? '').on('input', function () {
settings.server_urls[type] = String($(this).val());
settings.server_urls[type] = String($(this).val()).trim();
saveSettingsDebounced();
});
}

View File

@@ -679,6 +679,9 @@ export function getTokenizerModel() {
}
if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
return deepseekTokenizer;
}
if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
return llama3Tokenizer;
}

View File

@@ -1733,17 +1733,17 @@ export function hasAnimation(control) {
/**
* Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
*
* The action will be executed after the animation ends or after the timeout, whichever comes first.
* @param {HTMLElement} control - The control element to listen for animation end event
* @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
* @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
*/
export function runAfterAnimation(control, callback) {
export function runAfterAnimation(control, callback, timeout = 500) {
if (hasAnimation(control)) {
const onAnimationEnd = () => {
control.removeEventListener('animationend', onAnimationEnd);
callback(control);
};
control.addEventListener('animationend', onAnimationEnd);
Promise.race([
new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
new Promise((r) => control.addEventListener('animationend', r, { once: true })),
]).finally(() => callback(control));
} else {
callback(control);
}

View File

@@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
const MAX_LOOPS = 100;
function getLocalVariable(name, args = {}) {
export function getLocalVariable(name, args = {}) {
if (!chat_metadata.variables) {
chat_metadata.variables = {};
}
@@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
}
function setLocalVariable(name, value, args = {}) {
export function setLocalVariable(name, value, args = {}) {
if (!name) {
throw new Error('Variable name cannot be empty or undefined.');
}
@@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
return value;
}
function getGlobalVariable(name, args = {}) {
export function getGlobalVariable(name, args = {}) {
let globalVariable = extension_settings.variables.global[args.key ?? name];
if (args.index !== undefined) {
try {
@@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
}
function setGlobalVariable(name, value, args = {}) {
export function setGlobalVariable(name, value, args = {}) {
if (!name) {
throw new Error('Variable name cannot be empty or undefined.');
}