mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' of github.com-qvink:SillyTavern/SillyTavern into get_chat_completion_presets_from_preset_manager
This commit is contained in:
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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);
|
||||
|
@@ -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) {
|
||||
|
4
public/scripts/extensions/assets/installation.html
Normal file
4
public/scripts/extensions/assets/installation.html
Normal 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>
|
@@ -33,7 +33,7 @@ To install a single 3rd party extension, use the "Install Extensions"
|
||||
<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
@@ -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}}
|
||||
|
@@ -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> <span id="image_list_header_name"></span>
|
||||
</h3>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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}}
|
@@ -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({
|
||||
|
16
public/scripts/extensions/token-counter/window.html
Normal file
16
public/scripts/extensions/token-counter/window.html
Normal 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>
|
@@ -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');
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
@@ -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];
|
||||
|
@@ -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() {
|
||||
|
@@ -34,6 +34,7 @@ export const enumIcons = {
|
||||
preset: '⚙️',
|
||||
file: '📄',
|
||||
message: '💬',
|
||||
reasoning: '💡',
|
||||
voice: '🎤',
|
||||
server: '🖥️',
|
||||
popup: '🗔',
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user