mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Merge branch 'staging' into fnr
This commit is contained in:
@ -472,6 +472,11 @@ label[for="trim_spaces"]:has(input:checked) i.warning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label[for="trim_spaces"]:not(:has(input:checked)) small {
|
||||
color: var(--warning);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#claude_function_prefill_warning {
|
||||
display: none;
|
||||
color: red;
|
||||
|
@ -1981,13 +1981,13 @@
|
||||
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
|
||||
<input id="openai_show_thoughts" type="checkbox" />
|
||||
<span>
|
||||
<span data-i18n="Show model reasoning">Show model reasoning</span>
|
||||
<span data-i18n="Request model reasoning">Request model reasoning</span>
|
||||
<i class="opacity50p fa-solid fa-circle-info" title="Gemini 2.0 Thinking / DeepSeek Reasoner"></i>
|
||||
</span>
|
||||
</label>
|
||||
<div class="toggle-description justifyLeft marginBot5">
|
||||
<span data-i18n="Display the model's internal thoughts in the response.">
|
||||
Display the model's internal thoughts in the response.
|
||||
<span data-i18n="Allows the model to return its thinking process.">
|
||||
Allows the model to return its thinking process.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -3227,32 +3227,19 @@
|
||||
<h4 data-i18n="Perplexity Model">Perplexity Model</h4>
|
||||
<select id="model_perplexity_select">
|
||||
<optgroup label="Perplexity Sonar Models">
|
||||
<option value="sonar">sonar</option>
|
||||
<option value="sonar-pro">sonar-pro</option>
|
||||
<option value="sonar-reasoning">sonar-reasoning</option>
|
||||
</optgroup>
|
||||
<optgroup label="Deprecated Models">
|
||||
<!-- These are scheduled for deprecation after 2/22/2025 -->
|
||||
<option value="llama-3.1-sonar-small-128k-online">llama-3.1-sonar-small-128k-online</option>
|
||||
<option value="llama-3.1-sonar-large-128k-online">llama-3.1-sonar-large-128k-online</option>
|
||||
<option value="llama-3.1-sonar-huge-128k-online">llama-3.1-sonar-huge-128k-online</option>
|
||||
</optgroup>
|
||||
<optgroup label="Perplexity Chat Models">
|
||||
<!-- These are not listed on the site anymore -->
|
||||
<option value="llama-3.1-sonar-small-128k-chat">llama-3.1-sonar-small-128k-chat</option>
|
||||
<option value="llama-3.1-sonar-large-128k-chat">llama-3.1-sonar-large-128k-chat</option>
|
||||
</optgroup>
|
||||
<optgroup label="Open-Source Models">
|
||||
<option value="llama-3.1-8b-instruct">llama-3.1-8b-instruct</option>
|
||||
<option value="llama-3.1-70b-instruct">llama-3.1-70b-instruct</option>
|
||||
</optgroup>
|
||||
<optgroup label="Deprecated Models">
|
||||
<option value="llama-3-sonar-small-32k-chat">llama-3-sonar-small-32k-chat</option>
|
||||
<option value="llama-3-sonar-small-32k-online">llama-3-sonar-small-32k-online</option>
|
||||
<option value="llama-3-sonar-large-32k-chat">llama-3-sonar-large-32k-chat</option>
|
||||
<option value="llama-3-sonar-large-32k-online">llama-3-sonar-large-32k-online</option>
|
||||
<option value="sonar-small-chat">sonar-small-chat</option>
|
||||
<option value="sonar-small-online">sonar-small-online</option>
|
||||
<option value="sonar-medium-chat">sonar-medium-chat</option>
|
||||
<option value="sonar-medium-online">sonar-medium-online</option>
|
||||
<option value="llama-3-8b-instruct">llama-3-8b-instruct</option>
|
||||
<option value="llama-3-70b-instruct">llama-3-70b-instruct</option>
|
||||
<option value="mistral-7b-instruct">mistral-7b-instruct (v0.2)</option>
|
||||
<option value="mixtral-8x7b-instruct">mixtral-8x7b-instruct</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</div>
|
||||
<form id="cohere_form" data-source="cohere" action="javascript:void(null);" method="post" enctype="multipart/form-data">
|
||||
@ -3804,6 +3791,12 @@
|
||||
<span data-i18n="Reasoning">Reasoning</span>
|
||||
</h4>
|
||||
<div>
|
||||
<label class="checkbox_label" for="reasoning_auto_parse" title="Automatically parse reasoning blocks from main content between the reasoning prefix/suffix. Both fields must be defined and non-empty." data-i18n="[title]reasoning_auto_parse">
|
||||
<input id="reasoning_auto_parse" type="checkbox" />
|
||||
<small data-i18n="Auto-Parse Reasoning">
|
||||
Auto-Parse Reasoning
|
||||
</small>
|
||||
</label>
|
||||
<label class="checkbox_label" for="reasoning_add_to_prompts" title="Add existing reasoning blocks to prompts. To add a new reasoning block, use the message edit menu." data-i18n="[title]reasoning_add_to_prompts">
|
||||
<input id="reasoning_add_to_prompts" type="checkbox" />
|
||||
<small data-i18n="Add Reasoning to Prompts">
|
||||
|
@ -1385,8 +1385,8 @@
|
||||
"enable_functions_desc_1": "Autorise l'utilisation",
|
||||
"enable_functions_desc_2": "outils de fonction",
|
||||
"enable_functions_desc_3": "Peut être utilisé par diverses extensions pour fournir des fonctionnalités supplémentaires.",
|
||||
"Show model reasoning": "Afficher les pensées du modèle",
|
||||
"Display the model's internal thoughts in the response.": "Afficher les pensées internes du modèle dans la réponse.",
|
||||
"Request model reasoning": "Demander les pensées du modèle",
|
||||
"Allows the model to return its thinking process.": "Permet au modèle de retourner son processus de réflexion.",
|
||||
"Confirm token parsing with": "Confirmer l'analyse des tokens avec",
|
||||
"openai_logit_bias_no_items": "Aucun élément",
|
||||
"api_no_connection": "Pas de connection...",
|
||||
|
@ -266,8 +266,8 @@
|
||||
"Use system prompt": "使用系统提示词",
|
||||
"Merges_all_system_messages_desc_1": "合并所有系统消息,直到第一条具有非系统角色的消息,然后通过",
|
||||
"Merges_all_system_messages_desc_2": "字段发送。",
|
||||
"Show model reasoning": "展示思维链",
|
||||
"Display the model's internal thoughts in the response.": "展示模型在回复时的内部思维链。",
|
||||
"Request model reasoning": "请求思维链",
|
||||
"Allows the model to return its thinking process.": "允许模型返回其思维过程。",
|
||||
"Assistant Prefill": "AI预填",
|
||||
"Expand the editor": "展开编辑器",
|
||||
"Start Claude's answer with...": "以如下内容开始Claude的回答...",
|
||||
|
@ -2357,8 +2357,8 @@
|
||||
"Forbid": "禁止",
|
||||
"Aphrodite only. Determines the order of samplers. Skew is always applied post-softmax, so it's not included here.": "僅限 Aphrodite 使用。決定採樣器的順序。偏移總是在 softmax 後應用,因此不包括在此。",
|
||||
"Aphrodite only. Determines the order of samplers.": "僅限 Aphrodite 使用。決定採樣器的順序。",
|
||||
"Show model reasoning": "顯示模型思維鏈",
|
||||
"Display the model's internal thoughts in the response.": "在回應中顯示模型的思維鏈(內部思考過程)。",
|
||||
"Request model reasoning": "請求模型思維鏈",
|
||||
"Allows the model to return its thinking process.": "讓模型回傳其思考過程。",
|
||||
"Generic (OpenAI-compatible) [LM Studio, LiteLLM, etc.]": "通用(兼容 OpenAI)[LM Studio, LiteLLM 等]",
|
||||
"Model ID (optional)": "模型 ID(可選)",
|
||||
"DeepSeek API Key": "DeepSeek API 金鑰",
|
||||
|
164
public/script.js
164
public/script.js
@ -95,6 +95,7 @@ import {
|
||||
resetMovableStyles,
|
||||
forceCharacterEditorTokenize,
|
||||
applyPowerUserSettings,
|
||||
generatedTextFiltered,
|
||||
} from './scripts/power-user.js';
|
||||
|
||||
import {
|
||||
@ -169,6 +170,7 @@ import {
|
||||
toggleDrawer,
|
||||
isElementInViewport,
|
||||
copyText,
|
||||
escapeHtml,
|
||||
} from './scripts/utils.js';
|
||||
import { debounce_timeout } from './scripts/constants.js';
|
||||
|
||||
@ -1993,14 +1995,15 @@ export async function sendTextareaMessage() {
|
||||
* @param {boolean} isUser If the message was sent by the user
|
||||
* @param {number} messageId Message index in chat array
|
||||
* @param {object} [sanitizerOverrides] DOMPurify sanitizer option overrides
|
||||
* @param {boolean} [isReasoning] If the message is reasoning output
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}) {
|
||||
export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, sanitizerOverrides = {}, isReasoning = false) {
|
||||
if (!mes) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (Number(messageId) === 0 && !isSystem && !isUser) {
|
||||
if (Number(messageId) === 0 && !isSystem && !isUser && !isReasoning) {
|
||||
const mesBeforeReplace = mes;
|
||||
const chatMessage = chat[messageId];
|
||||
mes = substituteParams(mes, undefined, ch_name);
|
||||
@ -2029,6 +2032,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
|
||||
if (!isSystem) {
|
||||
function getRegexPlacement() {
|
||||
try {
|
||||
if (isReasoning) {
|
||||
return regex_placement.REASONING;
|
||||
}
|
||||
if (isUser) {
|
||||
return regex_placement.USER_INPUT;
|
||||
} else if (chat[messageId]?.extra?.type === 'narrator') {
|
||||
@ -2062,6 +2068,17 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san
|
||||
mes = mes.replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
|
||||
// Make sure reasoning strings are always shown, even if they include "<" or ">"
|
||||
[power_user.reasoning.prefix, power_user.reasoning.suffix].forEach((reasoningString) => {
|
||||
if (!reasoningString || !reasoningString.trim().length) {
|
||||
return;
|
||||
}
|
||||
// Only replace the first occurrence of the reasoning string
|
||||
if (mes.includes(reasoningString)) {
|
||||
mes = mes.replace(reasoningString, escapeHtml(reasoningString));
|
||||
}
|
||||
});
|
||||
|
||||
if (!isSystem) {
|
||||
// Save double quotes in tags as a special character to prevent them from being encoded
|
||||
if (!power_user.encode_tags) {
|
||||
@ -2250,8 +2267,8 @@ function getMessageFromTemplate({
|
||||
export function updateMessageBlock(messageId, message) {
|
||||
const messageElement = $(`#chat [mesid="${messageId}"]`);
|
||||
const text = message?.extra?.display_text ?? message.mes;
|
||||
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId));
|
||||
messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, -1));
|
||||
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
|
||||
messageElement.find('.mes_reasoning').html(messageFormatting(message.extra?.reasoning ?? '', '', false, false, messageId, {}, true));
|
||||
addCopyToCodeBlocks(messageElement);
|
||||
appendMediaToMessage(message, messageElement);
|
||||
}
|
||||
@ -2408,9 +2425,10 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
|
||||
mes.is_user,
|
||||
chat.indexOf(mes),
|
||||
sanitizerOverrides,
|
||||
false,
|
||||
);
|
||||
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1);
|
||||
const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, -1);
|
||||
const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
|
||||
const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
|
||||
let bookmarkLink = mes?.extra?.bookmark_link ?? '';
|
||||
|
||||
let params = {
|
||||
@ -3153,7 +3171,7 @@ class StreamingProcessor {
|
||||
this.sendTextarea.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
else {
|
||||
await saveReply(this.type, text, true);
|
||||
await saveReply(this.type, text, true, '', [], '');
|
||||
messageId = chat.length - 1;
|
||||
this.#checkDomElements(messageId);
|
||||
this.showMessageButtons(messageId);
|
||||
@ -3203,9 +3221,9 @@ class StreamingProcessor {
|
||||
}
|
||||
|
||||
if (this.reasoning) {
|
||||
chat[messageId]['extra']['reasoning'] = this.reasoning;
|
||||
chat[messageId]['extra']['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
|
||||
if (this.messageReasoningDom instanceof HTMLElement) {
|
||||
const formattedReasoning = messageFormatting(this.reasoning, '', false, false, -1);
|
||||
const formattedReasoning = messageFormatting(this.reasoning, '', false, false, messageId, {}, true);
|
||||
this.messageReasoningDom.innerHTML = formattedReasoning;
|
||||
}
|
||||
}
|
||||
@ -3232,6 +3250,8 @@ class StreamingProcessor {
|
||||
chat[messageId].is_system,
|
||||
chat[messageId].is_user,
|
||||
messageId,
|
||||
{},
|
||||
false,
|
||||
);
|
||||
if (this.messageTextDom instanceof HTMLElement) {
|
||||
this.messageTextDom.innerHTML = formattedText;
|
||||
@ -3283,39 +3303,11 @@ class StreamingProcessor {
|
||||
unblockGeneration();
|
||||
generatedPromptCache = '';
|
||||
|
||||
//console.log("Generated text size:", text.length, text)
|
||||
|
||||
const isAborted = this.abortController.signal.aborted;
|
||||
if (power_user.auto_swipe && !isAborted) {
|
||||
function containsBlacklistedWords(str, blacklist, threshold) {
|
||||
const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
|
||||
const matches = str.match(regex) || [];
|
||||
return matches.length >= threshold;
|
||||
if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {
|
||||
return swipe_right();
|
||||
}
|
||||
|
||||
const generatedTextFiltered = (text) => {
|
||||
if (text) {
|
||||
if (power_user.auto_swipe_minimum_length) {
|
||||
if (text.length < power_user.auto_swipe_minimum_length && text.length !== 0) {
|
||||
console.log('Generated text size too small');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (power_user.auto_swipe_blacklist_threshold) {
|
||||
if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
|
||||
console.log('Generated text has blacklisted words');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (generatedTextFiltered(text)) {
|
||||
swipe_right();
|
||||
return;
|
||||
}
|
||||
}
|
||||
playMessageSound();
|
||||
}
|
||||
|
||||
@ -3373,7 +3365,7 @@ class StreamingProcessor {
|
||||
const timestamps = [];
|
||||
for await (const { text, swipes, logprobs, toolCalls, state } of this.generator()) {
|
||||
timestamps.push(Date.now());
|
||||
if (this.isStopped) {
|
||||
if (this.isStopped || this.abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -3383,7 +3375,7 @@ class StreamingProcessor {
|
||||
if (logprobs) {
|
||||
this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
|
||||
}
|
||||
this.reasoning = state?.reasoning ?? '';
|
||||
this.reasoning = getRegexedString(state?.reasoning ?? '', regex_placement.REASONING);
|
||||
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
|
||||
await sw.tick(() => this.onProgressStreaming(this.messageId, this.continueMessage + text));
|
||||
}
|
||||
@ -3850,14 +3842,6 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
coreChat.pop();
|
||||
}
|
||||
|
||||
const reasoning = new PromptReasoning();
|
||||
for (let i = coreChat.length - 1; i >= 0; i--) {
|
||||
if (reasoning.isLimitReached()) {
|
||||
break;
|
||||
}
|
||||
coreChat[i] = { ...coreChat[i], mes: reasoning.addToMessage(coreChat[i].mes, coreChat[i].extra?.reasoning) };
|
||||
}
|
||||
|
||||
coreChat = await Promise.all(coreChat.map(async (chatItem, index) => {
|
||||
let message = chatItem.mes;
|
||||
let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
|
||||
@ -3877,6 +3861,27 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
};
|
||||
}));
|
||||
|
||||
const reasoning = new PromptReasoning();
|
||||
for (let i = coreChat.length - 1; i >= 0; i--) {
|
||||
const depth = coreChat.length - i - 1;
|
||||
const isPrefix = isContinue && i === coreChat.length - 1;
|
||||
coreChat[i] = {
|
||||
...coreChat[i],
|
||||
mes: reasoning.addToMessage(
|
||||
coreChat[i].mes,
|
||||
getRegexedString(
|
||||
String(coreChat[i].extra?.reasoning ?? ''),
|
||||
regex_placement.REASONING,
|
||||
{ isPrompt: true, depth: depth },
|
||||
),
|
||||
isPrefix,
|
||||
),
|
||||
};
|
||||
if (reasoning.isLimitReached()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine token limit
|
||||
let this_max_context = getMaxContextSize();
|
||||
|
||||
@ -4785,6 +4790,11 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
const swipes = extractMultiSwipes(data, type);
|
||||
|
||||
messageChunk = cleanUpMessage(getMessage, isImpersonate, isContinue, false);
|
||||
reasoning = getRegexedString(reasoning, regex_placement.REASONING);
|
||||
|
||||
if (power_user.trim_spaces) {
|
||||
reasoning = reasoning.trim();
|
||||
}
|
||||
|
||||
if (isContinue) {
|
||||
getMessage = continue_mag + getMessage;
|
||||
@ -4843,32 +4853,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
|
||||
}
|
||||
|
||||
const isAborted = abortController && abortController.signal.aborted;
|
||||
if (power_user.auto_swipe && !isAborted) {
|
||||
console.debug('checking for autoswipeblacklist on non-streaming message');
|
||||
function containsBlacklistedWords(getMessage, blacklist, threshold) {
|
||||
console.debug('checking blacklisted words');
|
||||
const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
|
||||
const matches = getMessage.match(regex) || [];
|
||||
return matches.length >= threshold;
|
||||
}
|
||||
|
||||
const generatedTextFiltered = (getMessage) => {
|
||||
if (power_user.auto_swipe_blacklist_threshold) {
|
||||
if (containsBlacklistedWords(getMessage, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
|
||||
console.debug('Generated text has blacklisted words');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
if (generatedTextFiltered(getMessage)) {
|
||||
console.debug('swiping right automatically');
|
||||
if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
|
||||
is_send_press = false;
|
||||
swipe_right();
|
||||
// TODO: do we want to resolve after an auto-swipe?
|
||||
return;
|
||||
}
|
||||
return swipe_right();
|
||||
}
|
||||
|
||||
console.debug('/api/chats/save called by /Generate');
|
||||
@ -6074,6 +6061,19 @@ export async function saveReply(type, getMessage, fromStreaming, title, swipes,
|
||||
return { type, getMessage };
|
||||
}
|
||||
|
||||
export function syncCurrentSwipeInfoExtras() {
|
||||
if (!chat.length) {
|
||||
return;
|
||||
}
|
||||
const currentMessage = chat[chat.length - 1];
|
||||
if (currentMessage && Array.isArray(currentMessage.swipe_info) && typeof currentMessage.swipe_id === 'number') {
|
||||
const swipeInfo = currentMessage.swipe_info[currentMessage.swipe_id];
|
||||
if (swipeInfo && typeof swipeInfo === 'object') {
|
||||
swipeInfo.extra = structuredClone(currentMessage.extra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function saveImageToMessage(img, mes) {
|
||||
if (mes && img.image) {
|
||||
if (!mes.extra || typeof mes.extra !== 'object') {
|
||||
@ -7177,9 +7177,11 @@ function messageEditAuto(div) {
|
||||
mes.is_system,
|
||||
mes.is_user,
|
||||
this_edit_mes_id,
|
||||
{},
|
||||
false,
|
||||
));
|
||||
mesBlock.find('.mes_bias').empty();
|
||||
mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));
|
||||
mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
|
||||
saveChatDebounced();
|
||||
}
|
||||
|
||||
@ -7201,10 +7203,12 @@ async function messageEditDone(div) {
|
||||
mes.is_system,
|
||||
mes.is_user,
|
||||
this_edit_mes_id,
|
||||
{},
|
||||
false,
|
||||
),
|
||||
);
|
||||
mesBlock.find('.mes_bias').empty();
|
||||
mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1));
|
||||
mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1, {}, false));
|
||||
appendMediaToMessage(mes, div.closest('.mes'));
|
||||
addCopyToCodeBlocks(div.closest('.mes'));
|
||||
|
||||
@ -8501,6 +8505,9 @@ function swipe_left() { // when we swipe left..but no generation.
|
||||
streamingProcessor.onStopStreaming();
|
||||
}
|
||||
|
||||
// Make sure ad-hoc changes to extras are saved before swiping away
|
||||
syncCurrentSwipeInfoExtras();
|
||||
|
||||
const swipe_duration = 120;
|
||||
const swipe_range = '700px';
|
||||
chat[chat.length - 1]['swipe_id']--;
|
||||
@ -8636,6 +8643,9 @@ const swipe_right = () => {
|
||||
return unblockGeneration();
|
||||
}
|
||||
|
||||
// Make sure ad-hoc changes to extras are saved before swiping away
|
||||
syncCurrentSwipeInfoExtras();
|
||||
|
||||
const swipe_duration = 200;
|
||||
const swipe_range = 700;
|
||||
//console.log(swipe_range);
|
||||
@ -10841,6 +10851,8 @@ jQuery(async function () {
|
||||
chat[this_edit_mes_id].is_system,
|
||||
chat[this_edit_mes_id].is_user,
|
||||
this_edit_mes_id,
|
||||
{},
|
||||
false,
|
||||
));
|
||||
appendMediaToMessage(chat[this_edit_mes_id], $(this).closest('.mes'));
|
||||
addCopyToCodeBlocks($(this).closest('.mes'));
|
||||
|
@ -566,7 +566,7 @@ export function initAuthorsNote() {
|
||||
namedArgumentList: [],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument(
|
||||
'position', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
|
||||
'role', [ARGUMENT_TYPE.STRING], false, false, null, ['system', 'user', 'assistant'],
|
||||
),
|
||||
],
|
||||
helpString: `
|
||||
|
@ -96,8 +96,13 @@ function highlightLockedBackground() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the background for the current chat
|
||||
* @param {Event} e Click event
|
||||
* @returns {string} Empty string
|
||||
*/
|
||||
function onLockBackgroundClick(e) {
|
||||
e.stopPropagation();
|
||||
e?.stopPropagation();
|
||||
|
||||
const chatName = getCurrentChatId();
|
||||
|
||||
@ -106,7 +111,7 @@ function onLockBackgroundClick(e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const relativeBgImage = getUrlParameter(this);
|
||||
const relativeBgImage = getUrlParameter(this) ?? background_settings.url;
|
||||
|
||||
saveBackgroundMetadata(relativeBgImage);
|
||||
setCustomBackground();
|
||||
@ -114,8 +119,13 @@ function onLockBackgroundClick(e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the background for the current chat
|
||||
* @param {Event} e Click event
|
||||
* @returns {string} Empty string
|
||||
*/
|
||||
function onUnlockBackgroundClick(e) {
|
||||
e.stopPropagation();
|
||||
e?.stopPropagation();
|
||||
removeBackgroundMetadata();
|
||||
unsetCustomBackground();
|
||||
highlightLockedBackground();
|
||||
@ -513,12 +523,12 @@ export function initBackgrounds() {
|
||||
$('#add_bg_button').on('change', onBackgroundUploadSelected);
|
||||
$('#bg-filter').on('input', onBackgroundFilterInput);
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'lockbg',
|
||||
callback: onLockBackgroundClick,
|
||||
callback: () => onLockBackgroundClick(new CustomEvent('click')),
|
||||
aliases: ['bglock'],
|
||||
helpString: 'Locks a background for the currently selected chat',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'unlockbg',
|
||||
callback: onUnlockBackgroundClick,
|
||||
callback: () => onUnlockBackgroundClick(new CustomEvent('click')),
|
||||
aliases: ['bgunlock'],
|
||||
helpString: 'Unlocks a background for the currently selected chat',
|
||||
}));
|
||||
|
@ -30,6 +30,7 @@ const CC_COMMANDS = [
|
||||
'api-url',
|
||||
'model',
|
||||
'proxy',
|
||||
'stop-strings',
|
||||
];
|
||||
|
||||
const TC_COMMANDS = [
|
||||
@ -43,6 +44,7 @@ const TC_COMMANDS = [
|
||||
'context',
|
||||
'instruct-state',
|
||||
'tokenizer',
|
||||
'stop-strings',
|
||||
];
|
||||
|
||||
const FANCY_NAMES = {
|
||||
@ -57,6 +59,7 @@ const FANCY_NAMES = {
|
||||
'instruct': 'Instruct Template',
|
||||
'context': 'Context Template',
|
||||
'tokenizer': 'Tokenizer',
|
||||
'stop-strings': 'Custom Stopping Strings',
|
||||
};
|
||||
|
||||
/**
|
||||
@ -138,6 +141,7 @@ const profilesProvider = () => [
|
||||
* @property {string} [context] Context Template
|
||||
* @property {string} [instruct-state] Instruct Mode
|
||||
* @property {string} [tokenizer] Tokenizer
|
||||
* @property {string} [stop-strings] Custom Stopping Strings
|
||||
* @property {string[]} [exclude] Commands to exclude
|
||||
*/
|
||||
|
||||
|
@ -883,6 +883,10 @@ export class SlashCommandHandler {
|
||||
}
|
||||
}
|
||||
getQuickReply(args) {
|
||||
if (!args.id && !args.label) {
|
||||
toastr.error('Please provide a valid id or label.');
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(this.api.getQrByLabel(args.set, args.id !== undefined ? Number(args.id) : args.label));
|
||||
} catch (ex) {
|
||||
|
@ -94,6 +94,12 @@
|
||||
<span data-i18n="World Info">World Info</span>
|
||||
</label>
|
||||
</div>
|
||||
<div data-i18n="[title]ext_regex_reasoning_desc" title="Reasoning block contents. When 'Only Format Prompt' is checked, it will also affect the reasoning contents added to the prompt.">
|
||||
<label class="checkbox flex-container">
|
||||
<input type="checkbox" name="replace_position" value="6">
|
||||
<span data-i18n="Reasoning">Reasoning</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-container wide100p marginTop5">
|
||||
<div class="flex1 flex-container flexNoGap">
|
||||
<small data-i18n="[title]ext_regex_min_depth_desc" title="When applied to prompts or display, only affect messages that are at least N levels deep. 0 = last message, 1 = penultimate message, etc. Only counts WI entries @Depth and usable messages, i.e. not hidden or system.">
|
||||
|
@ -20,6 +20,7 @@ const regex_placement = {
|
||||
SLASH_COMMAND: 3,
|
||||
// 4 - sendAs (legacy)
|
||||
WORLD_INFO: 5,
|
||||
REASONING: 6,
|
||||
};
|
||||
|
||||
export const substitute_find_regex = {
|
||||
@ -94,7 +95,7 @@ function getRegexedString(rawString, placement, { characterOverride, isMarkdown,
|
||||
// Script applies to Generate and input is Generate
|
||||
(script.promptOnly && isPrompt) ||
|
||||
// Script applies to all cases when neither "only"s are true, but there's no need to do it when `isMarkdown`, the as source (chat history) should already be changed beforehand
|
||||
(!script.markdownOnly && !script.promptOnly && !isMarkdown)
|
||||
(!script.markdownOnly && !script.promptOnly && !isMarkdown && !isPrompt)
|
||||
) {
|
||||
if (isEdit && !script.runOnEdit) {
|
||||
console.debug(`getRegexedString: Skipping script ${script.scriptName} because it does not run on edit`);
|
||||
|
@ -18,7 +18,7 @@ import { t } from '../../i18n.js';
|
||||
* @property {string} replaceString - The replace string
|
||||
* @property {string[]} trimStrings - The trim strings
|
||||
* @property {string?} findRegex - The find regex
|
||||
* @property {string?} substituteRegex - The substitute regex
|
||||
* @property {number?} substituteRegex - The substitute regex
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -388,7 +388,7 @@ class AllTalkTtsProvider {
|
||||
}
|
||||
|
||||
async fetchRvcVoiceObjects() {
|
||||
if (this.settings.server_version == 'v2') {
|
||||
if (this.settings.server_version == 'v1') {
|
||||
console.log('Skipping RVC voices fetch for V1 server');
|
||||
return [];
|
||||
}
|
||||
@ -1031,14 +1031,18 @@ class AllTalkTtsProvider {
|
||||
console.error('fetchTtsGeneration Error Response Text:', errorText);
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Handle V1/V2 URL differences
|
||||
const outputUrl = this.settings.server_version === 'v1'
|
||||
? data.output_file_url // V1 returns full URL
|
||||
: `${this.settings.provider_endpoint}${data.output_file_url}`; // V2 returns relative path
|
||||
// V1 returns a complete URL, V2 returns a relative path
|
||||
if (this.settings.server_version === 'v1') {
|
||||
// V1: Use the complete URL directly from the response
|
||||
return data.output_file_url;
|
||||
} else {
|
||||
// V2: Combine the endpoint with the relative path
|
||||
return `${this.settings.provider_endpoint}${data.output_file_url}`;
|
||||
}
|
||||
|
||||
return outputUrl;
|
||||
} catch (error) {
|
||||
console.error('[fetchTtsGeneration] Exception caught:', error);
|
||||
throw error;
|
||||
|
@ -30,6 +30,7 @@ import { GoogleTranslateTtsProvider } from './google-translate.js';
|
||||
export { talkingAnimation };
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
|
||||
let voiceMapEntries = [];
|
||||
let voiceMap = {}; // {charName:voiceid, charName2:voiceid2}
|
||||
@ -120,7 +121,7 @@ async function onNarrateOneMessage() {
|
||||
}
|
||||
|
||||
resetTtsPlayback();
|
||||
ttsJobQueue.push(message);
|
||||
processAndQueueTtsMessage(message);
|
||||
moduleWorker();
|
||||
}
|
||||
|
||||
@ -147,7 +148,7 @@ async function onNarrateText(args, text) {
|
||||
}
|
||||
|
||||
resetTtsPlayback();
|
||||
ttsJobQueue.push({ mes: text, name: name });
|
||||
processAndQueueTtsMessage({ mes: text, name: name });
|
||||
await moduleWorker();
|
||||
|
||||
// Return back to the chat voices
|
||||
@ -220,6 +221,36 @@ function isTtsProcessing() {
|
||||
return processing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a message into lines and adds each non-empty line to the TTS job queue.
|
||||
* @param {Object} message - The message object to be processed.
|
||||
* @param {string} message.mes - The text of the message to be split into lines.
|
||||
* @param {string} message.name - The name associated with the message.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processAndQueueTtsMessage(message) {
|
||||
if (!extension_settings.tts.narrate_by_paragraphs) {
|
||||
ttsJobQueue.push(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = message.mes.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ttsJobQueue.push(
|
||||
Object.assign({}, message, {
|
||||
mes: line,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function debugTtsPlayback() {
|
||||
console.log(JSON.stringify(
|
||||
{
|
||||
@ -350,7 +381,7 @@ function onAudioControlClicked() {
|
||||
talkingAnimation(false);
|
||||
} else {
|
||||
// Default play behavior if not processing or playing is to play the last message.
|
||||
ttsJobQueue.push(context.chat[context.chat.length - 1]);
|
||||
processAndQueueTtsMessage(context.chat[context.chat.length - 1]);
|
||||
}
|
||||
updateUiAudioPlayState();
|
||||
}
|
||||
@ -376,6 +407,7 @@ function completeCurrentAudioJob() {
|
||||
currentAudioJob = null;
|
||||
talkingAnimation(false); //stop lip animation
|
||||
// updateUiPlayState();
|
||||
wrapper.update();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -466,7 +498,7 @@ async function processTtsQueue() {
|
||||
}
|
||||
|
||||
if (extension_settings.tts.skip_tags) {
|
||||
text = text.replace(/<.*?>.*?<\/.*?>/g, '').trim();
|
||||
text = text.replace(/<.*?>[\s\S]*?<\/.*?>/g, '').trim();
|
||||
}
|
||||
|
||||
if (!extension_settings.tts.pass_asterisks) {
|
||||
@ -569,6 +601,7 @@ function loadSettings() {
|
||||
$('#tts_narrate_quoted').prop('checked', extension_settings.tts.narrate_quoted_only);
|
||||
$('#tts_auto_generation').prop('checked', extension_settings.tts.auto_generation);
|
||||
$('#tts_periodic_auto_generation').prop('checked', extension_settings.tts.periodic_auto_generation);
|
||||
$('#tts_narrate_by_paragraphs').prop('checked', extension_settings.tts.narrate_by_paragraphs);
|
||||
$('#tts_narrate_translated_only').prop('checked', extension_settings.tts.narrate_translated_only);
|
||||
$('#tts_narrate_user').prop('checked', extension_settings.tts.narrate_user);
|
||||
$('#tts_pass_asterisks').prop('checked', extension_settings.tts.pass_asterisks);
|
||||
@ -638,6 +671,11 @@ function onPeriodicAutoGenerationClick() {
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
function onNarrateByParagraphsClick() {
|
||||
extension_settings.tts.narrate_by_paragraphs = !!$('#tts_narrate_by_paragraphs').prop('checked');
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
|
||||
function onNarrateDialoguesClick() {
|
||||
extension_settings.tts.narrate_dialogues_only = !!$('#tts_narrate_dialogues').prop('checked');
|
||||
@ -816,7 +854,12 @@ async function onMessageEvent(messageId, lastCharIndex) {
|
||||
lastChatId = context.chatId;
|
||||
|
||||
console.debug(`Adding message from ${message.name} for TTS processing: "${message.mes}"`);
|
||||
|
||||
if (extension_settings.tts.periodic_auto_generation) {
|
||||
ttsJobQueue.push(message);
|
||||
} else {
|
||||
processAndQueueTtsMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
async function onMessageDeleted() {
|
||||
@ -1156,6 +1199,7 @@ jQuery(async function () {
|
||||
$('#tts_pass_asterisks').on('click', onPassAsterisksClick);
|
||||
$('#tts_auto_generation').on('click', onAutoGenerationClick);
|
||||
$('#tts_periodic_auto_generation').on('click', onPeriodicAutoGenerationClick);
|
||||
$('#tts_narrate_by_paragraphs').on('click', onNarrateByParagraphsClick);
|
||||
$('#tts_narrate_user').on('click', onNarrateUserClick);
|
||||
|
||||
$('#playback_rate').on('input', function () {
|
||||
@ -1177,7 +1221,6 @@ jQuery(async function () {
|
||||
loadSettings(); // Depends on Extension Controls and loadTtsProvider
|
||||
loadTtsProvider(extension_settings.tts.currentProvider); // No dependencies
|
||||
addAudioControl(); // Depends on Extension Controls
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
setInterval(wrapper.update.bind(wrapper), UPDATE_INTERVAL); // Init depends on all the things
|
||||
eventSource.on(event_types.MESSAGE_SWIPED, resetTtsPlayback);
|
||||
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
|
||||
|
@ -30,6 +30,10 @@
|
||||
<input type="checkbox" id="tts_periodic_auto_generation">
|
||||
<small data-i18n="Narrate by paragraphs (when streaming)">Narrate by paragraphs (when streaming)</small>
|
||||
</label>
|
||||
<label class="checkbox_label" for="tts_narrate_by_paragraphs">
|
||||
<input type="checkbox" id="tts_narrate_by_paragraphs">
|
||||
<small data-i18n="Narrate by paragraphs (when not streaming)">Narrate by paragraphs (when not streaming)</small>
|
||||
</label>
|
||||
<label class="checkbox_label" for="tts_narrate_quoted">
|
||||
<input type="checkbox" id="tts_narrate_quoted">
|
||||
<small data-i18n="Only narrate quotes">Only narrate "quotes"</small>
|
||||
|
@ -27,21 +27,42 @@ export async function hideLoader() {
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// Spinner blurs/fades out
|
||||
$('#load-spinner').on('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function () {
|
||||
const spinner = $('#load-spinner');
|
||||
if (!spinner.length) {
|
||||
console.warn('Spinner element not found, skipping animation');
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if transitions are enabled
|
||||
const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s';
|
||||
const hasTransitions = parseFloat(transitionDuration) > 0;
|
||||
|
||||
if (hasTransitions) {
|
||||
Promise.race([
|
||||
new Promise((r) => setTimeout(r, 500)), // Fallback timeout
|
||||
new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)),
|
||||
]).finally(cleanup);
|
||||
} else {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
$('#loader').remove();
|
||||
// Yoink preloader entirely; it only exists to cover up unstyled content while loading JS
|
||||
// If it's present, we remove it once and then it's gone.
|
||||
yoinkPreloader();
|
||||
|
||||
loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE).then(() => {
|
||||
loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE)
|
||||
.catch((err) => console.error('Error completing loaderPopup:', err))
|
||||
.finally(() => {
|
||||
loaderPopup = null;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$('#load-spinner')
|
||||
.css({
|
||||
// Apply the styles
|
||||
spinner.css({
|
||||
'filter': 'blur(15px)',
|
||||
'opacity': '0',
|
||||
});
|
||||
|
@ -258,7 +258,7 @@ const default_settings = {
|
||||
ai21_model: 'jamba-1.5-large',
|
||||
mistralai_model: 'mistral-large-latest',
|
||||
cohere_model: 'command-r-plus',
|
||||
perplexity_model: 'llama-3.1-70b-instruct',
|
||||
perplexity_model: 'sonar-pro',
|
||||
groq_model: 'llama-3.1-70b-versatile',
|
||||
nanogpt_model: 'gpt-4o-mini',
|
||||
zerooneai_model: 'yi-large',
|
||||
@ -337,7 +337,7 @@ const oai_settings = {
|
||||
ai21_model: 'jamba-1.5-large',
|
||||
mistralai_model: 'mistral-large-latest',
|
||||
cohere_model: 'command-r-plus',
|
||||
perplexity_model: 'llama-3.1-70b-instruct',
|
||||
perplexity_model: 'sonar-pro',
|
||||
groq_model: 'llama-3.1-70b-versatile',
|
||||
nanogpt_model: 'gpt-4o-mini',
|
||||
zerooneai_model: 'yi-large',
|
||||
@ -4380,28 +4380,19 @@ async function onModelChange() {
|
||||
if (oai_settings.max_context_unlocked) {
|
||||
$('#openai_max_context').attr('max', unlocked_max);
|
||||
}
|
||||
else if (['sonar', 'sonar-reasoning'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', 127000);
|
||||
}
|
||||
else if (['sonar-pro'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', 200000);
|
||||
}
|
||||
else if (oai_settings.perplexity_model.includes('llama-3.1')) {
|
||||
const isOnline = oai_settings.perplexity_model.includes('online');
|
||||
const contextSize = isOnline ? 128 * 1024 - 4000 : 128 * 1024;
|
||||
$('#openai_max_context').attr('max', contextSize);
|
||||
}
|
||||
else if (['llama-3-sonar-small-32k-chat', 'llama-3-sonar-large-32k-chat'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', max_32k);
|
||||
}
|
||||
else if (['llama-3-sonar-small-32k-online', 'llama-3-sonar-large-32k-online'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', 28000);
|
||||
}
|
||||
else if (['sonar-small-chat', 'sonar-medium-chat', 'codellama-70b-instruct', 'mistral-7b-instruct', 'mixtral-8x7b-instruct', 'mixtral-8x22b-instruct'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', max_16k);
|
||||
}
|
||||
else if (['llama-3-8b-instruct', 'llama-3-70b-instruct'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', max_8k);
|
||||
}
|
||||
else if (['sonar-small-online', 'sonar-medium-online'].includes(oai_settings.perplexity_model)) {
|
||||
$('#openai_max_context').attr('max', 12000);
|
||||
}
|
||||
else {
|
||||
$('#openai_max_context').attr('max', max_4k);
|
||||
$('#openai_max_context').attr('max', max_128k);
|
||||
}
|
||||
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
|
@ -254,6 +254,7 @@ let power_user = {
|
||||
},
|
||||
|
||||
reasoning: {
|
||||
auto_parse: false,
|
||||
add_to_prompts: false,
|
||||
prefix: '<think>\n',
|
||||
suffix: '\n</think>',
|
||||
@ -2916,6 +2917,46 @@ export function flushEphemeralStoppingStrings() {
|
||||
EPHEMERAL_STOPPING_STRINGS.splice(0, EPHEMERAL_STOPPING_STRINGS.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the generated text should be filtered based on the auto-swipe settings.
|
||||
* @param {string} text The text to check
|
||||
* @returns {boolean} If the generated text should be filtered
|
||||
*/
|
||||
export function generatedTextFiltered(text) {
|
||||
/**
|
||||
* Checks if the given text contains any of the blacklisted words.
|
||||
* @param {string} text The text to check
|
||||
* @param {string[]} blacklist The list of blacklisted words
|
||||
* @param {number} threshold The number of blacklisted words that need to be present to trigger the check
|
||||
* @returns {boolean} Whether the text contains blacklisted words
|
||||
*/
|
||||
function containsBlacklistedWords(text, blacklist, threshold) {
|
||||
const regex = new RegExp(`\\b(${blacklist.join('|')})\\b`, 'gi');
|
||||
const matches = text.match(regex) || [];
|
||||
return matches.length >= threshold;
|
||||
}
|
||||
|
||||
// Make sure a generated text is non-empty
|
||||
// Otherwise we might get in a loop with a broken API
|
||||
text = text.trim();
|
||||
if (text.length > 0) {
|
||||
if (power_user.auto_swipe_minimum_length) {
|
||||
if (text.length < power_user.auto_swipe_minimum_length) {
|
||||
console.log('Generated text size too small');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (power_user.auto_swipe_blacklist.length && power_user.auto_swipe_blacklist_threshold) {
|
||||
if (containsBlacklistedWords(text, power_user.auto_swipe_blacklist, power_user.auto_swipe_blacklist_threshold)) {
|
||||
console.log('Generated text has blacklisted words');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the custom stopping strings from the power user settings.
|
||||
* @param {number | undefined} limit Number of strings to return. If 0 or undefined, returns all strings.
|
||||
@ -4072,4 +4113,45 @@ $(document).ready(() => {
|
||||
],
|
||||
helpString: 'activates a movingUI preset by name',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'stop-strings',
|
||||
aliases: ['stopping-strings', 'custom-stopping-strings', 'custom-stop-strings'],
|
||||
helpString: `
|
||||
<div>
|
||||
Sets a list of custom stopping strings. Gets the list if no value is provided.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Examples:</strong>
|
||||
</div>
|
||||
<ul>
|
||||
<li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li>
|
||||
<li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li>
|
||||
</ul>
|
||||
`,
|
||||
returns: ARGUMENT_TYPE.LIST,
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'list of strings',
|
||||
typeList: [ARGUMENT_TYPE.LIST],
|
||||
acceptsMultiple: false,
|
||||
isRequired: false,
|
||||
}),
|
||||
],
|
||||
callback: (_, value) => {
|
||||
if (String(value ?? '').trim()) {
|
||||
const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value);
|
||||
if (!parsedValue || !Array.isArray(parsedValue)) {
|
||||
throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.');
|
||||
}
|
||||
parsedValue.forEach((item, index) => {
|
||||
parsedValue[index] = String(item);
|
||||
});
|
||||
power_user.custom_stopping_strings = JSON.stringify(parsedValue);
|
||||
$('#custom_stopping_strings').val(power_user.custom_stopping_strings);
|
||||
saveSettingsDebounced();
|
||||
}
|
||||
|
||||
return power_user.custom_stopping_strings;
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
|
||||
import { chat, closeMessageEditor, event_types, eventSource, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
|
||||
import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
|
||||
import { t } from './i18n.js';
|
||||
import { MacrosParser } from './macros.js';
|
||||
import { Popup } from './popup.js';
|
||||
@ -7,7 +8,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { copyText } from './utils.js';
|
||||
import { copyText, escapeRegex, isFalseBoolean } from './utils.js';
|
||||
|
||||
/**
|
||||
* Gets a message from a jQuery element.
|
||||
@ -49,11 +50,12 @@ export class PromptReasoning {
|
||||
* Add reasoning to a message according to the power user settings.
|
||||
* @param {string} content Message content
|
||||
* @param {string} reasoning Message reasoning
|
||||
* @param {boolean} isPrefix Whether this is the last message prefix
|
||||
* @returns {string} Message content with reasoning
|
||||
*/
|
||||
addToMessage(content, reasoning) {
|
||||
addToMessage(content, reasoning, isPrefix) {
|
||||
// Disabled or reached limit of additions
|
||||
if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {
|
||||
if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) {
|
||||
return content;
|
||||
}
|
||||
|
||||
@ -70,6 +72,11 @@ export class PromptReasoning {
|
||||
const separator = substituteParams(power_user.reasoning.separator || '');
|
||||
const suffix = substituteParams(power_user.reasoning.suffix || '');
|
||||
|
||||
// Combine parts with reasoning only
|
||||
if (isPrefix && !content) {
|
||||
return `${prefix}${reasoning}`;
|
||||
}
|
||||
|
||||
// Combine parts with reasoning and content
|
||||
return `${prefix}${reasoning}${suffix}${separator}${content}`;
|
||||
}
|
||||
@ -105,11 +112,18 @@ function loadReasoningSettings() {
|
||||
power_user.reasoning.max_additions = Number($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#reasoning_auto_parse').prop('checked', power_user.reasoning.auto_parse);
|
||||
$('#reasoning_auto_parse').on('change', function () {
|
||||
power_user.reasoning.auto_parse = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
|
||||
function registerReasoningSlashCommands() {
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'reasoning-get',
|
||||
aliases: ['get-reasoning'],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
helpString: t`Get the contents of a reasoning block of a message. Returns an empty string if the message does not have a reasoning block.`,
|
||||
unnamedArgumentList: [
|
||||
@ -129,6 +143,7 @@ function registerReasoningSlashCommands() {
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'reasoning-set',
|
||||
aliases: ['set-reasoning'],
|
||||
returns: ARGUMENT_TYPE.STRING,
|
||||
helpString: t`Set the reasoning block of a message. Returns the reasoning block content.`,
|
||||
namedArgumentList: [
|
||||
@ -146,7 +161,7 @@ function registerReasoningSlashCommands() {
|
||||
}),
|
||||
],
|
||||
callback: async (args, value) => {
|
||||
const messageId = !isNaN(Number(args[0])) ? Number(args[0]) : chat.length - 1;
|
||||
const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1;
|
||||
const message = chat[messageId];
|
||||
if (!message?.extra) {
|
||||
return '';
|
||||
@ -160,6 +175,50 @@ function registerReasoningSlashCommands() {
|
||||
return message.extra.reasoning;
|
||||
},
|
||||
}));
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'reasoning-parse',
|
||||
aliases: ['parse-reasoning'],
|
||||
returns: 'reasoning string',
|
||||
helpString: t`Extracts the reasoning block from a string using the Reasoning Formatting settings.`,
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({
|
||||
name: 'regex',
|
||||
description: 'Whether to apply regex scripts to the reasoning content.',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'true',
|
||||
isRequired: false,
|
||||
enumProvider: commonEnumProviders.boolean('trueFalse'),
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({
|
||||
description: 'input string',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
callback: (args, value) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
|
||||
toastr.warning(t`Both prefix and suffix must be set in the Reasoning Formatting settings.`);
|
||||
return String(value);
|
||||
}
|
||||
|
||||
const parsedReasoning = parseReasoningFromString(String(value));
|
||||
|
||||
if (!parsedReasoning) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const applyRegex = !isFalseBoolean(String(args.regex ?? ''));
|
||||
return applyRegex
|
||||
? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING)
|
||||
: parsedReasoning.reasoning;
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function registerReasoningMacros() {
|
||||
@ -168,7 +227,7 @@ function registerReasoningMacros() {
|
||||
MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`);
|
||||
}
|
||||
|
||||
function setReasoningEventHandlers(){
|
||||
function setReasoningEventHandlers() {
|
||||
$(document).on('click', '.mes_reasoning_copy', (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@ -224,7 +283,7 @@ function setReasoningEventHandlers(){
|
||||
}
|
||||
|
||||
const textarea = messageBlock.find('.reasoning_edit_textarea');
|
||||
const reasoning = String(textarea.val());
|
||||
const reasoning = getRegexedString(String(textarea.val()), regex_placement.REASONING, { isEdit: true });
|
||||
message.extra.reasoning = reasoning;
|
||||
await saveChatConditional();
|
||||
updateMessageBlock(messageId, message);
|
||||
@ -289,9 +348,101 @@ function setReasoningEventHandlers(){
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses reasoning from a string using the power user reasoning settings.
|
||||
* @typedef {Object} ParsedReasoning
|
||||
* @property {string} reasoning Reasoning block
|
||||
* @property {string} content Message content
|
||||
* @param {string} str Content of the message
|
||||
* @returns {ParsedReasoning|null} Parsed reasoning block and message content
|
||||
*/
|
||||
function parseReasoningFromString(str) {
|
||||
// Both prefix and suffix must be defined
|
||||
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const regex = new RegExp(`${escapeRegex(power_user.reasoning.prefix)}(.*?)${escapeRegex(power_user.reasoning.suffix)}`, 's');
|
||||
|
||||
let didReplace = false;
|
||||
let reasoning = '';
|
||||
let content = String(str).replace(regex, (_match, captureGroup) => {
|
||||
didReplace = true;
|
||||
reasoning = captureGroup;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (didReplace && power_user.trim_spaces) {
|
||||
reasoning = reasoning.trim();
|
||||
content = content.trim();
|
||||
}
|
||||
|
||||
return { reasoning, content };
|
||||
} catch (error) {
|
||||
console.error('[Reasoning] Error parsing reasoning block', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function registerReasoningAppEvents() {
|
||||
eventSource.makeFirst(event_types.MESSAGE_RECEIVED, (/** @type {number} */ idx) => {
|
||||
if (!power_user.reasoning.auto_parse) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('[Reasoning] Auto-parsing reasoning block for message', idx);
|
||||
const message = chat[idx];
|
||||
|
||||
if (!message) {
|
||||
console.warn('[Reasoning] Message not found', idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!message.mes || message.mes === '...') {
|
||||
console.debug('[Reasoning] Message content is empty or a placeholder', idx);
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsedReasoning = parseReasoningFromString(message.mes);
|
||||
|
||||
// No reasoning block found
|
||||
if (!parsedReasoning) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the message has an extra object
|
||||
if (!message.extra || typeof message.extra !== 'object') {
|
||||
message.extra = {};
|
||||
}
|
||||
|
||||
const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes;
|
||||
|
||||
// If reasoning was found, add it to the message
|
||||
if (parsedReasoning.reasoning) {
|
||||
message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING);
|
||||
}
|
||||
|
||||
// Update the message text if it was changed
|
||||
if (parsedReasoning.content !== message.mes) {
|
||||
message.mes = parsedReasoning.content;
|
||||
}
|
||||
|
||||
// Find if a message already exists in DOM and must be updated
|
||||
if (contentUpdated) {
|
||||
const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null;
|
||||
if (messageRendered) {
|
||||
console.debug('[Reasoning] Updating message block', idx);
|
||||
updateMessageBlock(idx, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initReasoning() {
|
||||
loadReasoningSettings();
|
||||
setReasoningEventHandlers();
|
||||
registerReasoningSlashCommands();
|
||||
registerReasoningMacros();
|
||||
registerReasoningAppEvents();
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import {
|
||||
showMoreMessages,
|
||||
stopGeneration,
|
||||
substituteParams,
|
||||
syncCurrentSwipeInfoExtras,
|
||||
system_avatar,
|
||||
system_message_types,
|
||||
this_chid,
|
||||
@ -2870,8 +2871,11 @@ async function addSwipeCallback(args, value) {
|
||||
const newSwipeId = lastMessage.swipes.length - 1;
|
||||
|
||||
if (isTrueBoolean(args.switch)) {
|
||||
// Make sure ad-hoc changes to extras are saved before swiping away
|
||||
syncCurrentSwipeInfoExtras();
|
||||
lastMessage.swipe_id = newSwipeId;
|
||||
lastMessage.mes = lastMessage.swipes[newSwipeId];
|
||||
lastMessage.extra = structuredClone(lastMessage.swipe_info?.[newSwipeId]?.extra ?? lastMessage.extra ?? {});
|
||||
}
|
||||
|
||||
await saveChatConditional();
|
||||
|
@ -69,6 +69,7 @@ import { textgenerationwebui_settings } from './textgen-settings.js';
|
||||
import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
|
||||
import { ToolManager } from './tool-calling.js';
|
||||
import { timestampToMoment, uuidv4 } from './utils.js';
|
||||
import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
|
||||
|
||||
export function getContext() {
|
||||
return {
|
||||
@ -175,6 +176,16 @@ export function getContext() {
|
||||
humanizedDateTime,
|
||||
updateMessageBlock,
|
||||
appendMediaToMessage,
|
||||
variables: {
|
||||
local: {
|
||||
get: getLocalVariable,
|
||||
set: setLocalVariable,
|
||||
},
|
||||
global: {
|
||||
get: getGlobalVariable,
|
||||
set: setGlobalVariable,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,12 @@ const OPENROUTER_PROVIDERS = [
|
||||
'xAI',
|
||||
'Cloudflare',
|
||||
'SF Compute',
|
||||
'Minimax',
|
||||
'Nineteen',
|
||||
'Liquid',
|
||||
'Nebius',
|
||||
'Chutes',
|
||||
'Kluster',
|
||||
'01.AI',
|
||||
'HuggingFace',
|
||||
'Mancer',
|
||||
|
@ -501,7 +501,7 @@ export function loadTextGenSettings(data, loadedSettings) {
|
||||
for (const [type, selector] of Object.entries(SERVER_INPUTS)) {
|
||||
const control = $(selector);
|
||||
control.val(settings.server_urls[type] ?? '').on('input', function () {
|
||||
settings.server_urls[type] = String($(this).val());
|
||||
settings.server_urls[type] = String($(this).val()).trim();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
}
|
||||
|
@ -679,6 +679,9 @@ export function getTokenizerModel() {
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.PERPLEXITY) {
|
||||
if (oai_settings.perplexity_model.includes('sonar-reasoning')) {
|
||||
return deepseekTokenizer;
|
||||
}
|
||||
if (oai_settings.perplexity_model.includes('llama-3') || oai_settings.perplexity_model.includes('llama3')) {
|
||||
return llama3Tokenizer;
|
||||
}
|
||||
|
@ -1733,17 +1733,17 @@ export function hasAnimation(control) {
|
||||
|
||||
/**
|
||||
* Run an action once an animation on a control ends. If the control has no animation, the action will be executed immediately.
|
||||
*
|
||||
* The action will be executed after the animation ends or after the timeout, whichever comes first.
|
||||
* @param {HTMLElement} control - The control element to listen for animation end event
|
||||
* @param {(control:*?) => void} callback - The callback function to be executed when the animation ends
|
||||
* @param {number} [timeout=500] - The timeout in milliseconds to wait for the animation to end before executing the callback
|
||||
*/
|
||||
export function runAfterAnimation(control, callback) {
|
||||
export function runAfterAnimation(control, callback, timeout = 500) {
|
||||
if (hasAnimation(control)) {
|
||||
const onAnimationEnd = () => {
|
||||
control.removeEventListener('animationend', onAnimationEnd);
|
||||
callback(control);
|
||||
};
|
||||
control.addEventListener('animationend', onAnimationEnd);
|
||||
Promise.race([
|
||||
new Promise((r) => setTimeout(r, timeout)), // Fallback timeout
|
||||
new Promise((r) => control.addEventListener('animationend', r, { once: true })),
|
||||
]).finally(() => callback(control));
|
||||
} else {
|
||||
callback(control);
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import { isFalseBoolean, convertValueType, isTrueBoolean } from './utils.js';
|
||||
|
||||
const MAX_LOOPS = 100;
|
||||
|
||||
function getLocalVariable(name, args = {}) {
|
||||
export function getLocalVariable(name, args = {}) {
|
||||
if (!chat_metadata.variables) {
|
||||
chat_metadata.variables = {};
|
||||
}
|
||||
@ -45,7 +45,7 @@ function getLocalVariable(name, args = {}) {
|
||||
return (localVariable?.trim?.() === '' || isNaN(Number(localVariable))) ? (localVariable || '') : Number(localVariable);
|
||||
}
|
||||
|
||||
function setLocalVariable(name, value, args = {}) {
|
||||
export function setLocalVariable(name, value, args = {}) {
|
||||
if (!name) {
|
||||
throw new Error('Variable name cannot be empty or undefined.');
|
||||
}
|
||||
@ -80,7 +80,7 @@ function setLocalVariable(name, value, args = {}) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function getGlobalVariable(name, args = {}) {
|
||||
export function getGlobalVariable(name, args = {}) {
|
||||
let globalVariable = extension_settings.variables.global[args.key ?? name];
|
||||
if (args.index !== undefined) {
|
||||
try {
|
||||
@ -102,7 +102,7 @@ function getGlobalVariable(name, args = {}) {
|
||||
return (globalVariable?.trim?.() === '' || isNaN(Number(globalVariable))) ? (globalVariable || '') : Number(globalVariable);
|
||||
}
|
||||
|
||||
function setGlobalVariable(name, value, args = {}) {
|
||||
export function setGlobalVariable(name, value, args = {}) {
|
||||
if (!name) {
|
||||
throw new Error('Variable name cannot be empty or undefined.');
|
||||
}
|
||||
|
@ -399,6 +399,11 @@ input[type='checkbox']:focus-visible {
|
||||
color: var(--SmartThemeEmColor);
|
||||
}
|
||||
|
||||
.mes_text q i,
|
||||
.mes_text q em {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mes_text u,
|
||||
.mes_reasoning u {
|
||||
color: var(--SmartThemeUnderlineColor);
|
||||
|
@ -533,6 +533,7 @@ comfy.post('/delete-workflow', jsonParser, async (request, response) => {
|
||||
|
||||
comfy.post('/generate', jsonParser, async (request, response) => {
|
||||
try {
|
||||
let item;
|
||||
const url = new URL(urlJoin(request.body.url, '/prompt'));
|
||||
|
||||
const controller = new AbortController();
|
||||
@ -557,7 +558,6 @@ comfy.post('/generate', jsonParser, async (request, response) => {
|
||||
/** @type {any} */
|
||||
const data = await promptResult.json();
|
||||
const id = data.prompt_id;
|
||||
let item;
|
||||
const historyUrl = new URL(urlJoin(request.body.url, '/history'));
|
||||
while (true) {
|
||||
const result = await fetch(historyUrl);
|
||||
|
Reference in New Issue
Block a user