mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into fix-char-rename-aux-connections
This commit is contained in:
@@ -603,12 +603,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 +616,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>
|
||||
|
@@ -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>
|
@@ -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) {
|
||||
|
@@ -3,17 +3,30 @@ import {
|
||||
} from '../lib.js';
|
||||
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, 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.
|
||||
@@ -129,7 +142,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 +155,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 +167,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 +213,7 @@ export class ReasoningHandler {
|
||||
this.state = ReasoningState.Hidden;
|
||||
}
|
||||
|
||||
this.type = extra?.reasoning_type;
|
||||
this.reasoning = extra?.reasoning ?? '';
|
||||
|
||||
if (this.state !== ReasoningState.None) {
|
||||
@@ -208,6 +228,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 +258,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 +281,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 +304,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 +322,53 @@ 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) {
|
||||
// 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.initialTime;
|
||||
}
|
||||
}
|
||||
|
||||
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 +411,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 +469,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 +485,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 +652,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 +682,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 +711,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 ?? ''));
|
||||
@@ -720,6 +831,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;
|
||||
message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual;
|
||||
await saveChatConditional();
|
||||
updateMessageBlock(messageId, message);
|
||||
textarea.remove();
|
||||
@@ -780,6 +892,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');
|
||||
@@ -819,16 +933,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 +954,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 };
|
||||
@@ -869,6 +985,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 +1007,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
|
||||
|
@@ -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;
|
||||
|
@@ -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.
|
||||
|
Reference in New Issue
Block a user