Merge branch 'staging' of github.com-qvink:SillyTavern/SillyTavern into get_chat_completion_presets_from_preset_manager

This commit is contained in:
qvink
2025-02-23 21:24:13 -07:00
68 changed files with 2160 additions and 1287 deletions

View File

@@ -280,17 +280,32 @@ async function RA_autoloadchat() {
// active character is the name, we should look it up in the character list and get the id
if (active_character !== null && active_character !== undefined) {
const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character);
if (active_character_id !== null) {
if (active_character_id !== -1) {
await selectCharacterById(String(active_character_id));
// Do a little tomfoolery to spoof the tag selector
const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`);
applyTagsOnCharacterSelect.call(selectedCharElement);
} else {
setActiveCharacter(null);
saveSettingsDebounced();
console.warn(`Currently active character with ID ${active_character} not found. Resetting to no active character.`);
}
}
if (active_group !== null && active_group !== undefined) {
await openGroupById(String(active_group));
if (active_character) {
console.warn('Active character and active group are both set. Only active character will be loaded. Resetting active group.');
setActiveGroup(null);
saveSettingsDebounced();
} else {
const result = await openGroupById(String(active_group));
if (!result) {
setActiveGroup(null);
saveSettingsDebounced();
console.warn(`Currently active group with ID ${active_group} not found. Resetting to no active group.`);
}
}
}
// if the character list hadn't been loaded yet, try again.

View File

@@ -1487,7 +1487,7 @@ jQuery(function () {
...chat.filter(x => x?.extra?.type !== system_message_types.ASSISTANT_NOTE),
];
download(JSON.stringify(chatToSave, null, 4), `Assistant - ${humanizedDateTime()}.json`, 'application/json');
download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');
});
// Do not change. #attachFile is added by extension.

View File

@@ -154,8 +154,18 @@ export const extension_settings = {
refine_mode: false,
},
expressions: {
/** @type {number} see `EXPRESSION_API` */
api: undefined,
/** @type {string[]} */
custom: [],
showDefault: false,
translate: false,
/** @type {string} */
fallback_expression: undefined,
/** @type {string} */
llmPrompt: undefined,
allowMultiple: true,
rerollIfSame: false,
},
connectionManager: {
selectedProfile: '',
@@ -603,12 +613,12 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
}
let toggleElement = isActive || isDisabled ?
`<input type="checkbox" title="Click to toggle" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
'<input type="checkbox" title="' + t`Click to toggle` + `" data-name="${name}" class="${isActive ? 'toggle_disable' : 'toggle_enable'} ${checkboxClass}" ${isActive ? 'checked' : ''}>` :
`<input type="checkbox" title="Cannot enable extension" data-name="${name}" class="extension_missing ${checkboxClass}" disabled>`;
let deleteButton = isExternal ? `<button class="btn_delete menu_button" data-name="${externalId}" title="Delete"><i class="fa-fw fa-solid fa-trash-can"></i></button>` : '';
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}" title="Move"><i class="fa-solid fa-folder-tree 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 modulesInfo = '';
if (isActive && Array.isArray(manifest.optional)) {
@@ -616,7 +626,7 @@ function generateExtensionHtml(name, manifest, isActive, isDisabled, isExternal,
modules.forEach(x => optional.delete(x));
if (optional.size > 0) {
const optionalString = DOMPurify.sanitize([...optional].join(', '));
modulesInfo = `<div class="extension_modules">Optional modules: <span class="optional">${optionalString}</span></div>`;
modulesInfo = '<div class="extension_modules">' + t`Optional modules:` + ` <span class="optional">${optionalString}</span></div>`;
}
} else if (!isDisabled) { // Neither active nor disabled
const requirements = new Set(manifest.requires);

View File

@@ -10,6 +10,7 @@ import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
import { executeSlashCommands } from '../../slash-commands.js';
import { accountStorage } from '../../util/AccountStorage.js';
import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
import { t } from '../../i18n.js';
export { MODULE_NAME };
const MODULE_NAME = 'assets';
@@ -59,11 +60,11 @@ const KNOWN_TYPES = {
'blip': 'Blip sounds',
};
function downloadAssetsList(url) {
updateCurrentAssets().then(function () {
async function downloadAssetsList(url) {
updateCurrentAssets().then(async function () {
fetch(url, { cache: 'no-cache' })
.then(response => response.json())
.then(json => {
.then(async function(json) {
availableAssets = {};
$('#assets_menu').empty();
@@ -84,10 +85,10 @@ function downloadAssetsList(url) {
$('#assets_type_select').empty();
$('#assets_search').val('');
$('#assets_type_select').append($('<option />', { value: '', text: 'All' }));
$('#assets_type_select').append($('<option />', { value: '', text: t`All` }));
for (const type of assetTypes) {
const option = $('<option />', { value: type, text: KNOWN_TYPES[type] || type });
const option = $('<option />', { value: type, text: t([KNOWN_TYPES[type] || type]) });
$('#assets_type_select').append(option);
}
@@ -104,11 +105,7 @@ function downloadAssetsList(url) {
assetTypeMenu.append(`<h3>${KNOWN_TYPES[assetType] || assetType}</h3>`).hide();
if (assetType == 'extension') {
assetTypeMenu.append(`
<div class="assets-list-git">
To download extensions from this page, you need to have <a href="https://git-scm.com/downloads" target="_blank">Git</a> installed.<br>
Click the <i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i> icon to visit the Extension's repo for tips on how to use it.
</div>`);
assetTypeMenu.append(await renderExtensionTemplateAsync('assets', 'installation'));
}
for (const i in availableAssets[assetType].sort((a, b) => a?.name && b?.name && a['name'].localeCompare(b['name']))) {
@@ -184,7 +181,7 @@ function downloadAssetsList(url) {
const displayName = DOMPurify.sanitize(asset['name'] || asset['id']);
const description = DOMPurify.sanitize(asset['description'] || '');
const url = isValidUrl(asset['url']) ? asset['url'] : '';
const title = assetType === 'extension' ? `Extension repo/guide: ${url}` : 'Preview in browser';
const title = assetType === 'extension' ? t`Extension repo/guide:` + ` ${url}` : t`Preview in browser`;
const previewIcon = (assetType === 'extension' || assetType === 'character') ? 'fa-arrow-up-right-from-square' : 'fa-headphones-simple';
const toolTag = assetType === 'extension' && asset['tool'];
@@ -195,9 +192,10 @@ function downloadAssetsList(url) {
<b>${displayName}</b>
<a class="asset_preview" href="${url}" target="_blank" title="${title}">
<i class="fa-solid fa-sm ${previewIcon}"></i>
</a>
${toolTag ? '<span class="tag" title="Adds a function tool"><i class="fa-solid fa-sm fa-wrench"></i> Tool</span>' : ''}
</span>
</a>` +
(toolTag ? '<span class="tag" title="' + t`Adds a function tool` + '"><i class="fa-solid fa-sm fa-wrench"></i> ' +
t`Tool` + '</span>' : '') +
`</span>
<small class="asset-description">
${description}
</small>
@@ -435,7 +433,7 @@ jQuery(async () => {
const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
const skipConfirm = accountStorage.getItem(rememberKey) === 'true';
const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {
const confirmation = skipConfirm || await Popup.show.confirm(t`Loading Asset List`, '<span>' + t`Are you sure you want to connect to the following url?` + `</span><var>${url}</var>`, {
customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
onClose: popup => {
if (popup.result) {

View File

@@ -0,0 +1,4 @@
<div class="assets-list-git">
<span data-i18n="extension_install_1">To download extensions from this page, you need to have </span><a href="https://git-scm.com/downloads" target="_blank">Git</a><span data-i18n="extension_install_2"> installed.</span><br>
<span data-i18n="extension_install_3">Click the </span><i class="fa-solid fa-sm fa-arrow-up-right-from-square"></i><span data-i18n="extension_install_4"> icon to visit the Extension's repo for tips on how to use it.</span>
</div>

View File

@@ -33,7 +33,7 @@ To install a single 3rd party extension, use the &quot;Install Extensions&quot;
<div id="assets_filters" class="flex-container">
<select id="assets_type_select" class="text_pole flex1">
</select>
<input id="assets_search" class="text_pole flex1" placeholder="Search" type="search">
<input id="assets_search" class="text_pole flex1" data-i18n="[placeholder]Search" placeholder="Search" type="search">
<div id="assets-characters-button" class="menu_button menu_button_icon">
<i class="fa-solid fa-image-portrait"></i>
<span data-i18n="Characters">Characters</span>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
<div id="{{item}}" class="expression_list_item">
{{#each images}}
<div class="expression_list_item interactable" data-expression="{{../expression}}" data-expression-type="{{this.type}}" data-filename="{{this.fileName}}">
<div class="expression_list_buttons">
<div class="menu_button expression_list_upload" title="Upload image">
<i class="fa-solid fa-upload"></i>
@@ -7,11 +8,14 @@
<i class="fa-solid fa-trash"></i>
</div>
</div>
<div class="expression_list_title {{textClass}}">
<span>{{item}}</span>
{{#if isCustom}}
<div class="expression_list_title">
<span>{{../expression}}</span>
{{#if ../isCustom}}
<small class="expression_list_custom">(custom)</small>
{{/if}}
</div>
<img class="expression_list_image" src="{{imageSrc}}" />
<div class="expression_list_image_container" title="{{this.title}}">
<img class="expression_list_image" src="{{this.imageSrc}}" alt="{{this.title}}" data-epression="{{../expression}}" />
</div>
</div>
{{/each}}

View File

@@ -6,17 +6,17 @@
</div>
<div class="inline-drawer-content">
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings.">
<label class="checkbox_label" for="expression_translate" title="Use the selected API from Chat Translation extension settings." data-i18n="[title]Use the selected API from Chat Translation extension settings.">
<input id="expression_translate" type="checkbox">
<span data-i18n="Translate text to English before classification">Translate text to English before classification</span>
</label>
<label class="checkbox_label" for="expressions_show_default">
<input id="expressions_show_default" type="checkbox">
<span data-i18n="Show default images (emojis) if sprite missing">Show default images (emojis) if sprite missing</span>
<label class="checkbox_label" for="expressions_allow_multiple" title="A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected." data-i18n="[title]A single expression can have multiple sprites. Whenever the expression is chosen, a random sprite for this expression will be selected.">
<input id="expressions_allow_multiple" type="checkbox">
<span data-i18n="Allow multiple sprites per expression">Allow multiple sprites per expression</span>
</label>
<label id="image_type_block" class="checkbox_label" for="image_type_toggle">
<input id="image_type_toggle" type="checkbox">
<span data-i18n="Image Type - talkinghead (extras)">Image Type - talkinghead (extras)</span>
<label class="checkbox_label" for="expressions_reroll_if_same" title="If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned." data-i18n="[title]If the same expression is used again, re-roll the sprite. This only applies to expressions that have multiple available sprites assigned.">
<input id="expressions_reroll_if_same" type="checkbox">
<span data-i18n="Re-roll if same expression is used again">Re-roll if same sprite is used again</span>
</label>
<div class="expression_api_block m-b-1 m-t-1">
<label for="expression_api" data-i18n="Classifier API">Classifier API</label>
@@ -75,8 +75,20 @@
<span data-i18n="Remove all image overrides">Remove all image overrides</span>
</div>
</div>
<p class="hint"><b data-i18n="Hint:">Hint:</b> <i><span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt></i></p>
<p class="hint">
<b data-i18n="Hint:">Hint:</b>
<i>
<span data-i18n="Create new folder in the _space">Create new folder in the </span><b>/characters/</b> <span data-i18n="folder of your user data directory and name it as the name of the character.">folder of your user data directory and name it as the name of the character.</span>
<span data-i18n="Put images with expressions there. File names should follow the pattern:">Put images with expressions there. File names should follow the pattern: </span><tt data-i18n="expression_label_pattern">[expression_label].[image_format]</tt>
</i>
</p>
<p>
<i>
<span>In case of multiple files per expression, file names can contain a suffix, either separated by a dot or a
dash.
Examples: </span><tt>joy.png</tt>, <tt>joy-1.png</tt>, <tt>joy.expressive.png</tt>
</i>
</p>
<h3 id="image_list_header">
<strong data-i18n="Sprite set:">Sprite set:</strong>&nbsp;<span id="image_list_header_name"></span>
</h3>

View File

@@ -111,6 +111,10 @@ img.expression.default {
justify-content: center;
}
.expression_list_image_container {
overflow: hidden;
}
.expression_list_title {
position: absolute;
bottom: 0;
@@ -126,6 +130,9 @@ img.expression.default {
flex-direction: column;
line-height: 1;
}
.expression_list_custom {
font-size: 0.66rem;
}
.expression_list_buttons {
position: absolute;
@@ -162,11 +169,24 @@ img.expression.default {
row-gap: 1rem;
}
#image_list .success {
#image_list .expression_list_item[data-expression-type="success"] .expression_list_title {
color: green;
}
#image_list .failure {
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title {
color: darkolivegreen;
}
#image_list .expression_list_item[data-expression-type="additional"] .expression_list_title::before {
content: '';
position: absolute;
top: -7px;
left: -9px;
font-size: 14px;
color: transparent;
text-shadow: 0 0 0 darkolivegreen;
}
#image_list .expression_list_item[data-expression-type="failure"] .expression_list_title {
color: red;
}
@@ -189,3 +209,12 @@ img.expression.default {
flex-direction: row;
}
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"],
#expressions_container:has(#expressions_allow_multiple:not(:checked)) label[for="expressions_reroll_if_same"] {
opacity: 0.3;
transition: opacity var(--animation-duration) ease;
}
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:hover,
#expressions_container:has(#expressions_allow_multiple:not(:checked)) #image_list .expression_list_item[data-expression-type="additional"]:focus {
opacity: unset;
}

View File

@@ -0,0 +1,12 @@
<div class="m-b-1" data-i18n="upload_expression_request">Please enter a name for the sprite (without extension).</div>
<div class="m-b-1" data-i18n="upload_expression_naming_1">
Sprite names must follow the naming schema for the selected expression: {{expression}}
</div>
<div data-i18n="upload_expression_naming_2">
For multiple expressions, the name must follow the expression name and a valid suffix. Allowed separators are '-' or dot '.'.
</div>
<span class="m-b-1" data-i18n="Examples:">Examples:</span> <tt>{{expression}}.png</tt>, <tt>{{expression}}-1.png</tt>, <tt>{{expression}}.expressive.png</tt>
{{#if clickedFileName}}
<div class="m-t-1" data-i18n="upload_expression_replace">Click 'Replace' to replace the existing expression:</div>
<tt>{{clickedFileName}}</tt>
{{/if}}

View File

@@ -6,6 +6,8 @@ import { getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, tokenizers
import { resetScrollHeight, debounce } from '../../utils.js';
import { debounce_timeout } from '../../constants.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
import { renderExtensionTemplateAsync } from '../../extensions.js';
import { t } from '../../i18n.js';
function rgb2hex(rgb) {
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
@@ -22,23 +24,7 @@ $('button').click(function () {
async function doTokenCounter() {
const { tokenizerName, tokenizerId } = getFriendlyTokenizerName(main_api);
const html = `
<div class="wide100p">
<h3>Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<h4>Type / paste in the box below to see the number of tokens in the text.</h4>
<p>Selected tokenizer: ${tokenizerName}</p>
<div>Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div>Tokens: <span id="token_counter_result">0</span></div>
<hr>
<div>Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p">—</div>
<hr>
<div>Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1">—</textarea>
</div>
</div>`;
const html = await renderExtensionTemplateAsync('token-counter', 'window', {tokenizerName});
const dialog = $(html);
const countDebounced = debounce(async () => {
@@ -131,9 +117,9 @@ async function doCount() {
jQuery(() => {
const buttonHtml = `
<div id="token_counter" class="list-group-item flex-container flexGap5">
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>
Token Counter
</div>`;
<div class="fa-solid fa-1 extensionsMenuExtensionButton" /></div>` +
t`Token Counter` +
'</div>';
$('#token_counter_wand_container').append(buttonHtml);
$('#token_counter').on('click', doTokenCounter);
SlashCommandParser.addCommandObject(SlashCommand.fromProps({

View File

@@ -0,0 +1,16 @@
<div class="wide100p">
<h3 data-i18n="Token Counter">Token Counter</h3>
<div class="justifyLeft flex-container flexFlowColumn">
<h4 data-i18n="Type / paste in the box below to see the number of tokens in the text.">Type / paste in the box below to see the number of tokens in the text.</h4>
<p><span data-i18n="Selected tokenizer:">Selected tokenizer:</span> {{tokenizerName}}</p>
<div data-i18n="Input:">Input:</div>
<textarea id="token_counter_textarea" class="wide100p textarea_compact" rows="1"></textarea>
<div><span data-i18n="Tokens:">Tokens:</span> <span id="token_counter_result">0</span></div>
<hr>
<div data-i18n="Tokenized text:">Tokenized text:</div>
<div id="tokenized_chunks_display" class="wide100p"></div>
<hr>
<div data-i18n="Token IDs:">Token IDs:</div>
<textarea id="token_counter_ids" class="wide100p textarea_compact" readonly rows="1"></textarea>
</div>
</div>

View File

@@ -605,7 +605,7 @@ const handleOutgoingMessage = createEventHandler(translateOutgoingMessage, () =>
const handleImpersonateReady = createEventHandler(translateImpersonate, () => shouldTranslate(incomingTypes));
const handleMessageEdit = createEventHandler(translateMessageEdit, () => true);
window['translate'] = translate;
globalThis.translate = translate;
jQuery(async () => {
const html = await renderExtensionTemplateAsync('translate', 'index');

View File

@@ -27,14 +27,12 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
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}
let talkingHeadState = false;
let lastChatId = null;
let lastMessage = null;
let lastMessageHash = null;
@@ -166,27 +164,6 @@ async function moduleWorker() {
updateUiAudioPlayState();
}
function talkingAnimation(switchValue) {
if (!modules.includes('talkinghead')) {
console.debug('Talking Animation module not loaded');
return;
}
const apiUrl = getApiUrl();
const animationType = switchValue ? 'start' : 'stop';
if (switchValue !== talkingHeadState) {
try {
console.log(animationType + ' Talking Animation');
doExtrasFetch(`${apiUrl}/api/talkinghead/${animationType}_talking`);
talkingHeadState = switchValue;
} catch (error) {
// Handle the error here or simply ignore it to prevent logging
}
}
updateUiAudioPlayState();
}
function resetTtsPlayback() {
// Stop system TTS utterance
cancelTtsPlay();
@@ -378,7 +355,6 @@ function onAudioControlClicked() {
// Not pausing, doing a full stop to anything TTS is doing. Better UX as pause is not as useful
if (!audioElement.paused || isTtsProcessing()) {
resetTtsPlayback();
talkingAnimation(false);
} else {
// Default play behavior if not processing or playing is to play the last message.
processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
@@ -405,7 +381,6 @@ function addAudioControl() {
function completeCurrentAudioJob() {
audioQueueProcessorReady = true;
currentAudioJob = null;
talkingAnimation(false); //stop lip animation
// updateUiPlayState();
wrapper.update();
}
@@ -436,7 +411,6 @@ async function processAudioJobQueue() {
audioQueueProcessorReady = false;
currentAudioJob = audioJobQueue.shift();
playAudioData(currentAudioJob);
talkingAnimation(true);
} catch (error) {
toastr.error(error.toString());
console.error(error);

View File

@@ -25,7 +25,7 @@ class OpenAICompatibleTtsProvider {
<label for="openai_compatible_tts_endpoint">Provider Endpoint:</label>
<div class="flex-container alignItemsCenter">
<div class="flex1">
<input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.provider_endpoint}"/>
<input id="openai_compatible_tts_endpoint" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.provider_endpoint}"/>
</div>
<div id="openai_compatible_tts_key" class="menu_button menu_button_icon">
<i class="fa-solid fa-key"></i>
@@ -33,9 +33,9 @@ class OpenAICompatibleTtsProvider {
</div>
</div>
<label for="openai_compatible_model">Model:</label>
<input id="openai_compatible_model" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.model}"/>
<input id="openai_compatible_model" type="text" class="text_pole" maxlength="500" value="${this.defaultSettings.model}"/>
<label for="openai_compatible_tts_voices">Available Voices (comma separated):</label>
<input id="openai_compatible_tts_voices" type="text" class="text_pole" maxlength="250" value="${this.defaultSettings.available_voices.join()}"/>
<input id="openai_compatible_tts_voices" type="text" class="text_pole" value="${this.defaultSettings.available_voices.join()}"/>
<label for="openai_compatible_tts_speed">Speed: <span id="openai_compatible_tts_speed_output"></span></label>
<input type="range" id="openai_compatible_tts_speed" value="1" min="0.25" max="4" step="0.05">`;
return html;

View File

@@ -1,6 +1,5 @@
import { isMobile } from '../../RossAscends-mods.js';
import { getPreviewString } from './index.js';
import { talkingAnimation } from './index.js';
import { saveTtsProviderSettings } from './index.js';
export { SystemTtsProvider };
@@ -70,7 +69,6 @@ var speechUtteranceChunker = function (utt, settings, callback) {
//placing the speak invocation inside a callback fixes ordering and onend issues.
setTimeout(function () {
speechSynthesis.speak(newUtt);
talkingAnimation(true);
}, 0);
};
@@ -240,7 +238,6 @@ class SystemTtsProvider {
//some code to execute when done
resolve(silence);
console.log('System TTS done');
talkingAnimation(false);
});
});
}

View File

@@ -561,9 +561,9 @@ async function retrieveFileChunks(queryText, collectionId) {
*/
async function vectorizeFile(fileText, fileName, collectionId, chunkSize, overlapPercent) {
try {
if (settings.translate_files && typeof window['translate'] === 'function') {
if (settings.translate_files && typeof globalThis.translate === 'function') {
console.log(`Vectors: Translating file ${fileName} to English...`);
const translatedText = await window['translate'](fileText, 'en');
const translatedText = await globalThis.translate(fileText, 'en');
fileText = translatedText;
}
@@ -745,6 +745,44 @@ async function getQueryText(chat, initiator) {
return collapseNewlines(queryText).trim();
}
/**
* Gets common body parameters for vector requests.
* @returns {object}
*/
function getVectorsRequestBody() {
const body = {};
switch (settings.source) {
case 'extras':
body.extrasUrl = extension_settings.apiUrl;
body.extrasKey = extension_settings.apiKey;
break;
case 'togetherai':
body.model = extension_settings.vectors.togetherai_model;
break;
case 'openai':
body.model = extension_settings.vectors.openai_model;
break;
case 'cohere':
body.model = extension_settings.vectors.cohere_model;
break;
case 'ollama':
body.model = extension_settings.vectors.ollama_model;
body.apiUrl = 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];
break;
case 'vllm':
body.apiUrl = textgenerationwebui_settings.server_urls[textgen_types.VLLM];
body.model = extension_settings.vectors.vllm_model;
break;
default:
break;
}
return body;
}
/**
* Gets the saved hashes for a collection
* @param {string} collectionId
@@ -753,8 +791,9 @@ async function getQueryText(chat, initiator) {
async function getSavedHashes(collectionId) {
const response = await fetch('/api/vector/list', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
source: settings.source,
}),
@@ -768,54 +807,6 @@ async function getSavedHashes(collectionId) {
return hashes;
}
function getVectorHeaders() {
const headers = getRequestHeaders();
switch (settings.source) {
case 'extras':
Object.assign(headers, {
'X-Extras-Url': extension_settings.apiUrl,
'X-Extras-Key': extension_settings.apiKey,
});
break;
case 'togetherai':
Object.assign(headers, {
'X-Togetherai-Model': extension_settings.vectors.togetherai_model,
});
break;
case 'openai':
Object.assign(headers, {
'X-OpenAI-Model': extension_settings.vectors.openai_model,
});
break;
case 'cohere':
Object.assign(headers, {
'X-Cohere-Model': extension_settings.vectors.cohere_model,
});
break;
case 'ollama':
Object.assign(headers, {
'X-Ollama-Model': extension_settings.vectors.ollama_model,
'X-Ollama-URL': textgenerationwebui_settings.server_urls[textgen_types.OLLAMA],
'X-Ollama-Keep': !!extension_settings.vectors.ollama_keep,
});
break;
case 'llamacpp':
Object.assign(headers, {
'X-LlamaCpp-URL': textgenerationwebui_settings.server_urls[textgen_types.LLAMACPP],
});
break;
case 'vllm':
Object.assign(headers, {
'X-Vllm-URL': textgenerationwebui_settings.server_urls[textgen_types.VLLM],
'X-Vllm-Model': extension_settings.vectors.vllm_model,
});
break;
default:
break;
}
return headers;
}
/**
* Inserts vector items into a collection
* @param {string} collectionId - The collection to insert into
@@ -825,12 +816,11 @@ function getVectorHeaders() {
async function insertVectorItems(collectionId, items) {
throwIfSourceInvalid();
const headers = getVectorHeaders();
const response = await fetch('/api/vector/insert', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
items: items,
source: settings.source,
@@ -879,8 +869,9 @@ function throwIfSourceInvalid() {
async function deleteVectorItems(collectionId, hashes) {
const response = await fetch('/api/vector/delete', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
hashes: hashes,
source: settings.source,
@@ -899,12 +890,11 @@ async function deleteVectorItems(collectionId, hashes) {
* @returns {Promise<{ hashes: number[], metadata: object[]}>} - Hashes of the results
*/
async function queryCollection(collectionId, searchText, topK) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
searchText: searchText,
topK: topK,
@@ -929,12 +919,11 @@ async function queryCollection(collectionId, searchText, topK) {
* @returns {Promise<Record<string, { hashes: number[], metadata: object[] }>>} - Results mapped to collection IDs
*/
async function queryMultipleCollections(collectionIds, searchText, topK, threshold) {
const headers = getVectorHeaders();
const response = await fetch('/api/vector/query-multi', {
method: 'POST',
headers: headers,
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionIds: collectionIds,
searchText: searchText,
topK: topK,
@@ -965,8 +954,9 @@ async function purgeFileVectorIndex(fileUrl) {
const response = await fetch('/api/vector/purge', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
}),
});
@@ -994,8 +984,9 @@ async function purgeVectorIndex(collectionId) {
const response = await fetch('/api/vector/purge', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
collectionId: collectionId,
}),
});
@@ -1019,7 +1010,10 @@ async function purgeAllVectorIndexes() {
try {
const response = await fetch('/api/vector/purge-all', {
method: 'POST',
headers: getVectorHeaders(),
headers: getRequestHeaders(),
body: JSON.stringify({
...getVectorsRequestBody(),
}),
});
if (!response.ok) {

View File

@@ -1664,12 +1664,12 @@ function updateFavButtonState(state) {
export async function openGroupById(groupId) {
if (isChatSaving) {
toastr.info(t`Please wait until the chat is saved before switching characters.`, t`Your chat is still saving...`);
return;
return false;
}
if (!groups.find(x => x.id === groupId)) {
console.log('Group not found', groupId);
return;
return false;
}
if (!is_send_press && !is_group_generating) {
@@ -1686,8 +1686,11 @@ export async function openGroupById(groupId) {
updateChatMetadata({}, true);
chat.length = 0;
await getGroupChat(groupId);
return true;
}
}
return false;
}
function openCharacterDefinition(characterSelect) {

View File

@@ -2167,6 +2167,14 @@ function getStreamingReply(data, state) {
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
}
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) {
if (oai_settings.show_thoughts) {
state.reasoning +=
data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
'';
}
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else {
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
}
@@ -4107,6 +4115,40 @@ function getMaxContextWindowAI(value) {
}
}
/**
* Get the maximum context size for the Groq model
* @param {string} model Model identifier
* @param {boolean} isUnlocked Whether context limits are unlocked
* @returns {number} Maximum context size in tokens
*/
function getGroqMaxContext(model, isUnlocked) {
if (isUnlocked) {
return unlocked_max;
}
const contextMap = {
'gemma2-9b-it': max_8k,
'llama-3.3-70b-versatile': max_128k,
'llama-3.1-8b-instant': max_128k,
'llama3-70b-8192': max_8k,
'llama3-8b-8192': max_8k,
'llama-guard-3-8b': max_8k,
'mixtral-8x7b-32768': max_32k,
'deepseek-r1-distill-llama-70b': max_128k,
'llama-3.3-70b-specdec': max_8k,
'llama-3.2-1b-preview': max_128k,
'llama-3.2-3b-preview': max_128k,
'llama-3.2-11b-vision-preview': max_128k,
'llama-3.2-90b-vision-preview': max_128k,
'qwen-2.5-32b': max_128k,
'deepseek-r1-distill-qwen-32b': max_128k,
'deepseek-r1-distill-llama-70b-specdec': max_128k,
};
// Return context size if model found, otherwise default to 128k
return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k;
}
async function onModelChange() {
biasCache = undefined;
let value = String($(this).val() || '');
@@ -4387,7 +4429,7 @@ 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)) {
else if (['sonar', 'sonar-reasoning', 'sonar-reasoning-pro', 'r1-1776'].includes(oai_settings.perplexity_model)) {
$('#openai_max_context').attr('max', 127000);
}
else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
@@ -4408,33 +4450,8 @@ async function onModelChange() {
}
if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
if (oai_settings.max_context_unlocked) {
$('#openai_max_context').attr('max', unlocked_max);
} else if (oai_settings.groq_model.includes('gemma2-9b-it')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('llama-3.3-70b-versatile')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.1-8b-instant')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama3-70b-8192')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('llama3-8b-8192')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('mixtral-8x7b-32768')) {
$('#openai_max_context').attr('max', max_32k);
} else if (oai_settings.groq_model.includes('deepseek-r1-distill-llama-70b')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.3-70b-specdec')) {
$('#openai_max_context').attr('max', max_8k);
} else if (oai_settings.groq_model.includes('llama-3.2-1b-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-3b-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-11b-vision-preview')) {
$('#openai_max_context').attr('max', max_128k);
} else if (oai_settings.groq_model.includes('llama-3.2-90b-vision-preview')) {
$('#openai_max_context').attr('max', max_128k);
}
const maxContext = getGroqMaxContext(oai_settings.groq_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext);
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');
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);

View File

@@ -1845,14 +1845,15 @@ async function loadContextSettings() {
/**
* Common function to perform fuzzy search with optional caching
* @template T
* @param {string} type - Type of search from fuzzySearchCategories
* @param {any[]} data - Data array to search in
* @param {Array<{name: string, weight: number, getFn?: (obj: any) => string}>} keys - Fuse.js keys configuration
* @param {T[]} data - Data array to search in
* @param {Array<{name: string, weight: number, getFn?: (obj: T) => string}>} keys - Fuse.js keys configuration
* @param {string} searchValue - The search term
* @param {Object.<string, { resultMap: Map<string, any> }>} [fuzzySearchCaches=null] - Optional fuzzy search caches
* @returns {import('fuse.js').FuseResult<any>[]} Results as items with their score
* @returns {import('fuse.js').FuseResult<T>[]} Results as items with their score
*/
function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
export function performFuzzySearch(type, data, keys, searchValue, fuzzySearchCaches = null) {
// Check cache if provided
if (fuzzySearchCaches) {
const cache = fuzzySearchCaches[type];

View File

@@ -1,19 +1,32 @@
import {
moment,
} from '../lib.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveChatDebounced, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { getCurrentLocale, t } from './i18n.js';
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 { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.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 } from './utils.js';
import { copyText, escapeRegex, isFalseBoolean, setDatasetProperty, trimSpaces } from './utils.js';
/**
* Enum representing the type of the reasoning for a message (where it came from)
* @enum {string}
* @readonly
*/
export const ReasoningType = {
Model: 'model',
Parsed: 'parsed',
Manual: 'manual',
Edited: 'edited',
};
/**
* Gets a message from a jQuery element.
@@ -63,6 +76,11 @@ export function extractReasoningFromData(data) {
return data?.choices?.[0]?.message?.reasoning ?? '';
case chat_completion_sources.MAKERSUITE:
return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
case chat_completion_sources.CUSTOM: {
return data?.choices?.[0]?.message?.reasoning_content
?? data?.choices?.[0]?.message?.reasoning
?? '';
}
}
break;
}
@@ -94,7 +112,7 @@ export function isHiddenReasoningModel() {
{ name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith },
];
const model = getChatCompletionModel();
const model = getChatCompletionModel() || '';
const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name));
return isHidden;
@@ -129,7 +147,12 @@ export const ReasoningState = {
* This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
*/
export class ReasoningHandler {
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
#isHiddenReasoningModel;
/** @type {boolean} True if the handler is currently handling a manual parse of reasoning blocks */
#isParsingReasoning = false;
/** @type {number?} When reasoning is being parsed manually, and the reasoning has ended, this will be the index at which the actual messages starts */
#parsingReasoningMesStartIndex = null;
/**
* @param {Date?} [timeStarted=null] - When the generation started
@@ -137,6 +160,8 @@ export class ReasoningHandler {
constructor(timeStarted = null) {
/** @type {ReasoningState} The current state of the reasoning process */
this.state = ReasoningState.None;
/** @type {ReasoningType?} The type of the reasoning (where it came from) */
this.type = null;
/** @type {string} The reasoning output */
this.reasoning = '';
/** @type {Date} When the reasoning started */
@@ -147,7 +172,6 @@ export class ReasoningHandler {
/** @type {Date} Initial starting time of the generation */
this.initialTime = timeStarted ?? new Date();
/** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
this.#isHiddenReasoningModel = isHiddenReasoningModel();
// Cached DOM elements for reasoning
@@ -194,6 +218,7 @@ export class ReasoningHandler {
this.state = ReasoningState.Hidden;
}
this.type = extra?.reasoning_type;
this.reasoning = extra?.reasoning ?? '';
if (this.state !== ReasoningState.None) {
@@ -208,6 +233,7 @@ export class ReasoningHandler {
// Make sure reset correctly clears all relevant states
if (reset) {
this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None;
this.type = null;
this.reasoning = '';
this.initialTime = new Date();
this.startTime = null;
@@ -237,18 +263,19 @@ export class ReasoningHandler {
* Updates the reasoning text/string for a message.
*
* @param {number} messageId - The ID of the message to update
* @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning
* @param {string?} [reasoning=null] - The reasoning text to update - If null or empty, uses the current reasoning
* @param {Object} [options={}] - Optional arguments
* @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
* @param {boolean} [options.allowReset=false] - Whether to allow empty reasoning provided to reset the reasoning, instead of just taking the existing one
* @returns {boolean} - Returns true if the reasoning was changed, otherwise false
*/
updateReasoning(messageId, reasoning = null, { persist = false } = {}) {
updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) {
if (messageId == -1 || !chat[messageId]) {
return false;
}
reasoning = reasoning ?? this.reasoning;
reasoning = power_user.trim_spaces ? reasoning.trim() : reasoning;
reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning;
reasoning = trimSpaces(reasoning);
// Ensure the chat extra exists
if (!chat[messageId].extra) {
@@ -259,10 +286,13 @@ export class ReasoningHandler {
const reasoningChanged = extra.reasoning !== reasoning;
this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
if (persist) {
// Build and save the reasoning data to message extras
extra.reasoning = this.reasoning;
extra.reasoning_duration = this.getDuration();
extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model;
}
return reasoningChanged;
@@ -279,7 +309,10 @@ export class ReasoningHandler {
* @returns {Promise<void>}
*/
async process(messageId, mesChanged) {
if (!this.reasoning && !this.#isHiddenReasoningModel) return;
mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged);
if (!this.reasoning && !this.#isHiddenReasoningModel)
return;
// Ensure reasoning string is updated and regexes are applied correctly
const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
@@ -294,6 +327,54 @@ export class ReasoningHandler {
}
}
#autoParseReasoningFromMessage(messageId, mesChanged) {
if (!power_user.reasoning.auto_parse)
return;
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix)
return mesChanged;
/** @type {{ mes: string, [key: string]: any}} */
const message = chat[messageId];
if (!message) return mesChanged;
// If we are done with reasoning parse, we just split the message correctly so the reasoning doesn't show up inside of it.
if (this.#parsingReasoningMesStartIndex) {
message.mes = trimSpaces(message.mes.slice(this.#parsingReasoningMesStartIndex));
return mesChanged;
}
if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) {
// If streamed message starts with the opening, cut it out and put all inside reasoning
if (message.mes.startsWith(power_user.reasoning.prefix) && message.mes.length > power_user.reasoning.prefix.length) {
this.#isParsingReasoning = true;
// Manually set starting state here, as we might already have received the ending suffix
this.state = ReasoningState.Thinking;
this.startTime = this.startTime ?? this.initialTime;
this.endTime = null;
}
}
if (!this.#isParsingReasoning)
return mesChanged;
// If we are in manual parsing mode, all currently streaming mes tokens will go the the reasoning block
const originalMes = message.mes;
this.reasoning = originalMes.slice(power_user.reasoning.prefix.length);
message.mes = '';
// If the reasoning contains the ending suffix, we cut that off and continue as message streaming
if (this.reasoning.includes(power_user.reasoning.suffix)) {
this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix));
this.#parsingReasoningMesStartIndex = originalMes.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length;
message.mes = trimSpaces(originalMes.slice(this.#parsingReasoningMesStartIndex));
this.#isParsingReasoning = false;
}
// Only return the original mesChanged value if we haven't cut off the complete message
return message.mes.length ? mesChanged : false;
}
/**
* Completes the reasoning process for a message.
*
@@ -336,9 +417,10 @@ export class ReasoningHandler {
// Update states to the relevant DOM elements
setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null);
setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state);
setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type);
// Update the reasoning message
const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
const reasoning = trimSpaces(this.reasoning);
const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
this.messageReasoningContentDom.innerHTML = displayReasoning;
@@ -393,17 +475,14 @@ export class ReasoningHandler {
const element = this.messageReasoningHeaderDom;
const duration = this.getDuration();
let data = null;
let title = '';
if (duration) {
const seconds = moment.duration(duration).asSeconds();
const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
const secondsStr = moment.duration(duration).asSeconds();
const span = document.createElement('span');
span.title = t`${secondsStr} seconds`;
span.textContent = durationStr;
element.textContent = t`Thought for `;
element.appendChild(span);
data = String(secondsStr);
element.textContent = t`Thought for ${durationStr}`;
data = String(seconds);
title = `${seconds} seconds`;
} else if ([ReasoningState.Done, ReasoningState.Hidden].includes(this.state)) {
element.textContent = t`Thought for some time`;
data = 'unknown';
@@ -412,6 +491,12 @@ export class ReasoningHandler {
data = null;
}
if (this.type !== ReasoningType.Model) {
title += ` [${translate(this.type)}]`;
title = title.trim();
}
element.title = title;
setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data);
setDatasetProperty(element, 'duration', data);
}
@@ -573,11 +658,16 @@ function registerReasoningSlashCommands() {
callback: async (args, value) => {
const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
const message = chat[messageId];
if (!message?.extra) {
if (!message) {
return '';
}
// Make sure the message has an extra object
if (!message.extra || typeof message.extra !== 'object') {
message.extra = {};
}
message.extra.reasoning = String(value ?? '');
message.extra.reasoning_type = ReasoningType.Manual;
await saveChatConditional();
closeMessageEditor('reasoning');
@@ -598,7 +688,26 @@ function registerReasoningSlashCommands() {
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumProvider: commonEnumProviders.boolean('trueFalse'),
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
SlashCommandNamedArgument.fromProps({
name: 'return',
description: 'Whether to return the parsed reasoning or the content without reasoning',
typeList: [ARGUMENT_TYPE.STRING],
defaultValue: 'reasoning',
isRequired: false,
enumList: [
new SlashCommandEnumValue('reasoning', null, enumTypes.enum, enumIcons.reasoning),
new SlashCommandEnumValue('content', null, enumTypes.enum, enumIcons.message),
],
}),
SlashCommandNamedArgument.fromProps({
name: 'strict',
description: 'Whether to require the reasoning block to be at the beginning of the string (excluding whitespaces).',
typeList: [ARGUMENT_TYPE.BOOLEAN],
defaultValue: 'true',
isRequired: false,
enumList: commonEnumProviders.boolean('trueFalse')(),
}),
],
unnamedArgumentList: [
@@ -608,19 +717,27 @@ function registerReasoningSlashCommands() {
}),
],
callback: (args, value) => {
if (!value) {
if (!value || typeof value !== 'string') {
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);
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`, t`Reasoning Parse`);
return value;
}
if (typeof args.return !== 'string' || !['reasoning', 'content'].includes(args.return)) {
toastr.warning(t`Invalid return type '${args.return}', defaulting to 'reasoning'.`, t`Reasoning Parse`);
}
const parsedReasoning = parseReasoningFromString(String(value));
const returnMessage = args.return === 'content';
const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) });
if (!parsedReasoning) {
return '';
return returnMessage ? value : '';
}
if (returnMessage) {
return parsedReasoning.content;
}
const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
@@ -638,6 +755,17 @@ function registerReasoningMacros() {
}
function setReasoningEventHandlers() {
/**
* Updates the reasoning block of a message from a value.
* @param {object} message Message object
* @param {string} value Reasoning value
*/
function updateReasoningFromValue(message, value) {
const reasoning = getRegexedString(value, regex_placement.REASONING, { isEdit: true });
message.extra.reasoning = reasoning;
message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
}
$(document).on('click', '.mes_reasoning_details', function (e) {
if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) {
e.preventDefault();
@@ -718,8 +846,7 @@ function setReasoningEventHandlers() {
}
const textarea = messageBlock.find('.reasoning_edit_textarea');
const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
message.extra.reasoning = reasoning;
updateReasoningFromValue(message, String(textarea.val()));
await saveChatConditional();
updateMessageBlock(messageId, message);
textarea.remove();
@@ -780,6 +907,8 @@ function setReasoningEventHandlers() {
return;
}
message.extra.reasoning = '';
delete message.extra.reasoning_type;
delete message.extra.reasoning_duration;
await saveChatConditional();
updateMessageBlock(messageId, message);
const textarea = messageBlock.find('.reasoning_edit_textarea');
@@ -797,6 +926,20 @@ function setReasoningEventHandlers() {
await copyText(reasoning);
toastr.info(t`Copied!`, '', { timeOut: 2000 });
});
$(document).on('input', '.reasoning_edit_textarea', function () {
if (!power_user.auto_save_msg_edits) {
return;
}
const { message } = getMessageFromJquery(this);
if (!message?.extra) {
return;
}
updateReasoningFromValue(message, String($(this).val()));
saveChatDebounced();
});
}
/**
@@ -819,16 +962,18 @@ export function removeReasoningFromString(str) {
* @property {string} reasoning Reasoning block
* @property {string} content Message content
* @param {string} str Content of the message
* @param {Object} options Optional arguments
* @param {boolean} [options.strict=true] Whether the reasoning block **has** to be at the beginning of the provided string (excluding whitespaces), or can be anywhere in it
* @returns {ParsedReasoning|null} Parsed reasoning block and message content
*/
function parseReasoningFromString(str) {
function parseReasoningFromString(str, { strict = true } = {}) {
// 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');
const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
let didReplace = false;
let reasoning = '';
@@ -838,9 +983,9 @@ function parseReasoningFromString(str) {
return '';
});
if (didReplace && power_user.trim_spaces) {
reasoning = reasoning.trim();
content = content.trim();
if (didReplace) {
reasoning = trimSpaces(reasoning);
content = trimSpaces(content);
}
return { reasoning, content };
@@ -851,7 +996,7 @@ function parseReasoningFromString(str) {
}
function registerReasoningAppEvents() {
eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
const eventHandler = (/** @type {number} */ idx) => {
if (!power_user.reasoning.auto_parse) {
return;
}
@@ -869,6 +1014,11 @@ function registerReasoningAppEvents() {
return null;
}
if (message.extra?.reasoning) {
console.debug('[Reasoning] Message already has reasoning', idx);
return null;
}
const parsedReasoning = parseReasoningFromString(message.mes);
// No reasoning block found
@@ -886,6 +1036,7 @@ function registerReasoningAppEvents() {
// If reasoning was found, add it to the message
if (parsedReasoning.reasoning) {
message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
message.extra.reasoning_type = ReasoningType.Parsed;
}
// Update the message text if it was changed
@@ -901,7 +1052,11 @@ function registerReasoningAppEvents() {
updateMessageBlock(idx, message);
}
}
});
};
for (const event of [event_types.MESSAGE_RECEIVED, event_types.MESSAGE_UPDATED]) {
eventSource.on(event, eventHandler);
}
}
export function initReasoning() {

View File

@@ -34,6 +34,7 @@ export const enumIcons = {
preset: '⚙️',
file: '📄',
message: '💬',
reasoning: '💡',
voice: '🎤',
server: '🖥️',
popup: '🗔',

View File

@@ -311,7 +311,7 @@ export function validateTextGenUrl() {
const formattedUrl = formatTextGenURL(url);
if (!formattedUrl) {
toastr.error('Enter a valid API URL', 'Text Completion API');
toastr.error(t`Enter a valid API URL`, 'Text Completion API');
return;
}
@@ -1187,7 +1187,7 @@ export function getTextGenModel() {
return settings.aphrodite_model;
case OLLAMA:
if (!settings.ollama_model) {
toastr.error('No Ollama model selected.', 'Text Completion API');
toastr.error(t`No Ollama model selected.`, 'Text Completion API');
throw new Error('No Ollama model selected');
}
return settings.ollama_model;

View File

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

View File

@@ -9,6 +9,9 @@ import { ensureImageFormatSupported, getBase64Async, humanFileSize } from './uti
export let currentUser = null;
export let accountsEnabled = false;
// Extend the session every 30 minutes
const SESSION_EXTEND_INTERVAL = 30 * 60 * 1000;
/**
* Enable or disable user account controls in the UI.
* @param {boolean} isEnabled User account controls enabled
@@ -894,6 +897,24 @@ async function slugify(text) {
}
}
/**
* Pings the server to extend the user session.
*/
async function extendUserSession() {
try {
const response = await fetch('/api/ping?extend=1', {
method: 'GET',
headers: getRequestHeaders(),
});
if (!response.ok) {
throw new Error('Ping did not succeed', { cause: response.status });
}
} catch (error) {
console.error('Failed to extend user session', error);
}
}
jQuery(() => {
$('#logout_button').on('click', () => {
logout();
@@ -904,4 +925,9 @@ jQuery(() => {
$('#account_button').on('click', () => {
openUserProfile();
});
setInterval(async () => {
if (currentUser) {
await extendUserSession();
}
}, SESSION_EXTEND_INTERVAL);
});

View File

@@ -8,7 +8,7 @@ import {
import { getContext } from './extensions.js';
import { characters, getRequestHeaders, this_chid } from '../script.js';
import { isMobile } from './RossAscends-mods.js';
import { collapseNewlines } from './power-user.js';
import { collapseNewlines, power_user } from './power-user.js';
import { debounce_timeout } from './constants.js';
import { Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
@@ -676,6 +676,19 @@ export function sortByCssOrder(a, b) {
return _a - _b;
}
/**
* Trims leading and trailing whitespace from the input string based on a configuration setting.
* @param {string} input - The string to be trimmed
* @returns {string} The trimmed string if trimming is enabled; otherwise, returns the original string
*/
export function trimSpaces(input) {
if (!input || typeof input !== 'string') {
return input;
}
return power_user.trim_spaces ? input.trim() : input;
}
/**
* Trims a string to the end of a nearest sentence.
* @param {string} input The string to trim.
@@ -994,13 +1007,18 @@ export function getImageSizeFromDataURL(dataUrl) {
});
}
export function getCharaFilename(chid) {
/**
* Gets the filename of the character avatar without extension
* @param {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
*/
export function getCharaFilename(chid = null, { manualAvatarKey = null } = {}) {
const context = getContext();
const fileName = context.characters[chid ?? context.characterId]?.avatar;
const fileName = manualAvatarKey ?? context.characters[chid ?? context.characterId]?.avatar;
if (fileName) {
return fileName.replace(/\.[^/.]+$/, '');
}
return fileName?.replace(/\.[^/.]+$/, '') ?? null;
}
/**