|
import { |
|
moment, |
|
} from '../lib.js'; |
|
import { chat, closeMessageEditor, event_types, eventSource, main_api, messageFormatting, saveChatConditional, saveChatDebounced, saveSettingsDebounced, substituteParams, syncMesToSwipe, updateMessageBlock } from '../script.js'; |
|
import { getRegexedString, regex_placement } from './extensions/regex/engine.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 { performFuzzySearch, power_user } from './power-user.js'; |
|
import { getPresetManager } from './preset-manager.js'; |
|
import { SlashCommand } from './slash-commands/SlashCommand.js'; |
|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.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, isTrueBoolean, setDatasetProperty, trimSpaces } from './utils.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const reasoning_templates = []; |
|
|
|
export const DEFAULT_REASONING_TEMPLATE = 'DeepSeek'; |
|
|
|
|
|
|
|
|
|
|
|
const UI = { |
|
$select: $('#reasoning_select'), |
|
$suffix: $('#reasoning_suffix'), |
|
$prefix: $('#reasoning_prefix'), |
|
$separator: $('#reasoning_separator'), |
|
$autoParse: $('#reasoning_auto_parse'), |
|
$autoExpand: $('#reasoning_auto_expand'), |
|
$showHidden: $('#reasoning_show_hidden'), |
|
$addToPrompts: $('#reasoning_add_to_prompts'), |
|
$maxAdditions: $('#reasoning_max_additions'), |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
export const ReasoningType = { |
|
Model: 'model', |
|
Parsed: 'parsed', |
|
Manual: 'manual', |
|
Edited: 'edited', |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
function getMessageFromJquery(element) { |
|
const messageBlock = $(element).closest('.mes'); |
|
const messageId = Number(messageBlock.attr('mesid')); |
|
const message = chat[messageId]; |
|
return { messageId: messageId, message, messageBlock }; |
|
} |
|
|
|
|
|
|
|
|
|
function toggleReasoningAutoExpand() { |
|
const reasoningBlocks = document.querySelectorAll('details.mes_reasoning_details'); |
|
reasoningBlocks.forEach((block) => { |
|
if (block instanceof HTMLDetailsElement) { |
|
block.open = power_user.reasoning.auto_expand; |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function extractReasoningFromData(data, { |
|
mainApi = null, |
|
ignoreShowThoughts = false, |
|
textGenType = null, |
|
chatCompletionSource = null, |
|
} = {}) { |
|
switch (mainApi ?? main_api) { |
|
case 'textgenerationwebui': |
|
switch (textGenType ?? textgenerationwebui_settings.type) { |
|
case textgen_types.OPENROUTER: |
|
return data?.choices?.[0]?.reasoning ?? ''; |
|
} |
|
break; |
|
|
|
case 'openai': |
|
if (!ignoreShowThoughts && !oai_settings.show_thoughts) break; |
|
|
|
switch (chatCompletionSource ?? oai_settings.chat_completion_source) { |
|
case chat_completion_sources.DEEPSEEK: |
|
return data?.choices?.[0]?.message?.reasoning_content ?? ''; |
|
case chat_completion_sources.XAI: |
|
return data?.choices?.[0]?.message?.reasoning_content ?? ''; |
|
case chat_completion_sources.OPENROUTER: |
|
return data?.choices?.[0]?.message?.reasoning ?? ''; |
|
case chat_completion_sources.MAKERSUITE: |
|
case chat_completion_sources.VERTEXAI: |
|
return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? ''; |
|
case chat_completion_sources.CLAUDE: |
|
return data?.content?.find(part => part.type === 'thinking')?.thinking ?? ''; |
|
case chat_completion_sources.CUSTOM: { |
|
return data?.choices?.[0]?.message?.reasoning_content |
|
?? data?.choices?.[0]?.message?.reasoning |
|
?? ''; |
|
} |
|
} |
|
break; |
|
} |
|
|
|
return ''; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function isHiddenReasoningModel() { |
|
if (main_api !== 'openai') { |
|
return false; |
|
} |
|
|
|
|
|
|
|
const FUNCS = { |
|
equals: (currentModel, supportedModel) => currentModel === supportedModel, |
|
startsWith: (currentModel, supportedModel) => currentModel.startsWith(supportedModel), |
|
}; |
|
|
|
|
|
const hiddenReasoningModels = [ |
|
{ name: 'gpt-4.5', func: FUNCS.startsWith }, |
|
{ name: 'o1', func: FUNCS.startsWith }, |
|
{ name: 'o3', func: FUNCS.startsWith }, |
|
{ name: 'gemini-2.0-flash-thinking-exp', func: FUNCS.startsWith }, |
|
{ name: 'gemini-2.0-pro-exp', func: FUNCS.startsWith }, |
|
]; |
|
|
|
const model = getChatCompletionModel() || ''; |
|
|
|
const isHidden = hiddenReasoningModels.some(({ name, func }) => func(model, name)); |
|
return isHidden; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function updateReasoningUI(messageIdOrElement, { reset = false } = {}) { |
|
const handler = new ReasoningHandler(); |
|
handler.initHandleMessage(messageIdOrElement, { reset }); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const ReasoningState = { |
|
None: 'none', |
|
Thinking: 'thinking', |
|
Done: 'done', |
|
Hidden: 'hidden', |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
export class ReasoningHandler { |
|
|
|
#isHiddenReasoningModel; |
|
|
|
#isParsingReasoning = false; |
|
|
|
#parsingReasoningMesStartIndex = null; |
|
|
|
|
|
|
|
|
|
constructor(timeStarted = null) { |
|
|
|
this.state = ReasoningState.None; |
|
|
|
this.type = null; |
|
|
|
this.reasoning = ''; |
|
|
|
this.reasoningDisplayText = null; |
|
|
|
this.startTime = null; |
|
|
|
this.endTime = null; |
|
|
|
|
|
this.initialTime = timeStarted ?? new Date(); |
|
|
|
this.#isHiddenReasoningModel = isHiddenReasoningModel(); |
|
|
|
|
|
|
|
this.messageDom = null; |
|
|
|
this.messageReasoningDetailsDom = null; |
|
|
|
this.messageReasoningContentDom = null; |
|
|
|
this.messageReasoningHeaderDom = null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
initContinue(promptReasoning) { |
|
this.reasoning = promptReasoning.prefixReasoning; |
|
this.state = promptReasoning.prefixIncomplete ? ReasoningState.None : ReasoningState.Done; |
|
this.startTime = this.initialTime; |
|
this.endTime = promptReasoning.prefixDuration ? new Date(this.initialTime.getTime() + promptReasoning.prefixDuration) : null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
initHandleMessage(messageIdOrElement, { reset = false } = {}) { |
|
|
|
const messageElement = typeof messageIdOrElement === 'number' |
|
? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`) |
|
: messageIdOrElement instanceof HTMLElement |
|
? messageIdOrElement |
|
: $(messageIdOrElement)[0]; |
|
const messageId = Number(messageElement.getAttribute('mesid')); |
|
|
|
if (isNaN(messageId) || !chat[messageId]) return; |
|
|
|
if (!chat[messageId].extra) { |
|
chat[messageId].extra = {}; |
|
} |
|
const extra = chat[messageId].extra; |
|
|
|
if (extra.reasoning) { |
|
this.state = ReasoningState.Done; |
|
} else if (extra.reasoning_duration) { |
|
this.state = ReasoningState.Hidden; |
|
} |
|
|
|
this.type = extra?.reasoning_type; |
|
this.reasoning = extra?.reasoning ?? ''; |
|
this.reasoningDisplayText = extra?.reasoning_display_text ?? null; |
|
|
|
if (this.state !== ReasoningState.None) { |
|
this.initialTime = new Date(chat[messageId].gen_started); |
|
this.startTime = this.initialTime; |
|
this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0)); |
|
} |
|
|
|
|
|
this.messageDom = messageElement; |
|
|
|
|
|
if (reset) { |
|
this.state = this.#isHiddenReasoningModel ? ReasoningState.Thinking : ReasoningState.None; |
|
this.type = null; |
|
this.reasoning = ''; |
|
this.reasoningDisplayText = null; |
|
this.initialTime = new Date(); |
|
this.startTime = null; |
|
this.endTime = null; |
|
} |
|
|
|
this.updateDom(messageId); |
|
|
|
if (power_user.reasoning.auto_expand && this.state !== ReasoningState.Hidden) { |
|
this.messageReasoningDetailsDom.open = true; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
getDuration() { |
|
if (this.startTime && this.endTime) { |
|
return this.endTime.getTime() - this.startTime.getTime(); |
|
} |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateReasoning(messageId, reasoning = null, { persist = false, allowReset = false } = {}) { |
|
if (messageId == -1 || !chat[messageId]) { |
|
return false; |
|
} |
|
|
|
reasoning = allowReset ? reasoning ?? this.reasoning : reasoning || this.reasoning; |
|
reasoning = trimSpaces(reasoning); |
|
|
|
|
|
if (!chat[messageId].extra) { |
|
chat[messageId].extra = {}; |
|
} |
|
const extra = chat[messageId].extra; |
|
|
|
const reasoningChanged = extra.reasoning !== reasoning; |
|
this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING); |
|
|
|
this.type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model; |
|
|
|
if (persist) { |
|
|
|
extra.reasoning = this.reasoning; |
|
extra.reasoning_duration = this.getDuration(); |
|
extra.reasoning_type = (this.#isParsingReasoning || this.#parsingReasoningMesStartIndex) ? ReasoningType.Parsed : ReasoningType.Model; |
|
} |
|
|
|
return reasoningChanged; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async process(messageId, mesChanged, promptReasoning) { |
|
mesChanged = this.#autoParseReasoningFromMessage(messageId, mesChanged, promptReasoning); |
|
|
|
if (!this.reasoning && !this.#isHiddenReasoningModel) |
|
return; |
|
|
|
|
|
const reasoningChanged = this.updateReasoning(messageId, null, { persist: true }); |
|
|
|
if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) { |
|
this.state = ReasoningState.Thinking; |
|
this.startTime = this.initialTime; |
|
} |
|
if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) { |
|
this.endTime = new Date(); |
|
await this.finish(messageId); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#autoParseReasoningFromMessage(messageId, mesChanged, promptReasoning) { |
|
if (!power_user.reasoning.auto_parse) |
|
return; |
|
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) |
|
return mesChanged; |
|
|
|
|
|
const message = chat[messageId]; |
|
if (!message) return mesChanged; |
|
|
|
const parseTarget = promptReasoning?.prefixIncomplete ? (promptReasoning.prefixReasoningFormatted + message.mes) : message.mes; |
|
|
|
|
|
if (this.#parsingReasoningMesStartIndex) { |
|
message.mes = trimSpaces(parseTarget.slice(this.#parsingReasoningMesStartIndex)); |
|
return mesChanged; |
|
} |
|
|
|
if (this.state === ReasoningState.None || this.#isHiddenReasoningModel) { |
|
|
|
if (parseTarget.startsWith(power_user.reasoning.prefix) && parseTarget.length > power_user.reasoning.prefix.length) { |
|
this.#isParsingReasoning = true; |
|
|
|
|
|
this.state = ReasoningState.Thinking; |
|
this.startTime = this.startTime ?? this.initialTime; |
|
this.endTime = null; |
|
} |
|
} |
|
|
|
if (!this.#isParsingReasoning) |
|
return mesChanged; |
|
|
|
|
|
this.reasoning = parseTarget.slice(power_user.reasoning.prefix.length); |
|
message.mes = ''; |
|
|
|
|
|
if (this.reasoning.includes(power_user.reasoning.suffix)) { |
|
this.reasoning = this.reasoning.slice(0, this.reasoning.indexOf(power_user.reasoning.suffix)); |
|
this.#parsingReasoningMesStartIndex = parseTarget.indexOf(power_user.reasoning.suffix) + power_user.reasoning.suffix.length; |
|
message.mes = trimSpaces(parseTarget.slice(this.#parsingReasoningMesStartIndex)); |
|
this.#isParsingReasoning = false; |
|
} |
|
|
|
|
|
return message.mes.length ? mesChanged : false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async finish(messageId) { |
|
if (this.state === ReasoningState.None) return; |
|
|
|
|
|
if (this.startTime !== null && this.endTime === null) { |
|
this.endTime = new Date(); |
|
} |
|
|
|
if (this.state === ReasoningState.Thinking) { |
|
this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done; |
|
this.updateReasoning(messageId, null, { persist: true }); |
|
await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state); |
|
} |
|
|
|
this.updateDom(messageId); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
updateDom(messageId) { |
|
this.#checkDomElements(messageId); |
|
|
|
|
|
this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None); |
|
|
|
|
|
setDatasetProperty(this.messageDom, 'reasoningState', this.state !== ReasoningState.None ? this.state : null); |
|
setDatasetProperty(this.messageReasoningDetailsDom, 'state', this.state); |
|
setDatasetProperty(this.messageReasoningDetailsDom, 'type', this.type); |
|
|
|
|
|
const reasoning = trimSpaces(this.reasoningDisplayText ?? this.reasoning); |
|
const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true); |
|
this.messageReasoningContentDom.innerHTML = displayReasoning; |
|
|
|
|
|
|
|
const button = this.messageDom.querySelector('.mes_edit_add_reasoning'); |
|
button.title = this.state === ReasoningState.Hidden ? t`Hidden reasoning - Add reasoning block` : t`Add reasoning block`; |
|
|
|
|
|
if (this.state === ReasoningState.Hidden) { |
|
this.messageReasoningDetailsDom.open = false; |
|
} |
|
|
|
|
|
this.#updateReasoningTimeUI(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
#checkDomElements(messageId) { |
|
|
|
if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) { |
|
this.messageDom = null; |
|
} |
|
|
|
|
|
if (this.messageDom === null) { |
|
this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`); |
|
if (this.messageDom === null) throw new Error('message dom does not exist'); |
|
} |
|
if (this.messageReasoningDetailsDom === null) { |
|
this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details'); |
|
} |
|
if (this.messageReasoningContentDom === null) { |
|
this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning'); |
|
} |
|
if (this.messageReasoningHeaderDom === null) { |
|
this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#updateReasoningTimeUI() { |
|
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 }); |
|
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'; |
|
} else { |
|
element.textContent = t`Thinking...`; |
|
data = null; |
|
} |
|
|
|
if (this.type && this.type !== ReasoningType.Model) { |
|
title += ` [${translate(this.type)}]`; |
|
title = title.trim(); |
|
} |
|
element.title = title; |
|
|
|
setDatasetProperty(this.messageReasoningDetailsDom, 'duration', data); |
|
setDatasetProperty(element, 'duration', data); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export class PromptReasoning { |
|
|
|
|
|
|
|
|
|
static #LATEST = null; |
|
|
|
|
|
|
|
|
|
static REASONING_PLACEHOLDER = '\u200B'; |
|
|
|
|
|
|
|
|
|
|
|
static getLatestPrefix() { |
|
if (!PromptReasoning.#LATEST) { |
|
return ''; |
|
} |
|
|
|
if (!PromptReasoning.#LATEST.prefixIncomplete) { |
|
return ''; |
|
} |
|
|
|
return PromptReasoning.#LATEST.prefixReasoningFormatted; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
static clearLatest() { |
|
PromptReasoning.#LATEST = null; |
|
} |
|
|
|
constructor() { |
|
PromptReasoning.#LATEST = this; |
|
|
|
|
|
this.counter = 0; |
|
|
|
this.prefixLength = -1; |
|
|
|
this.prefixReasoning = ''; |
|
|
|
this.prefixReasoningFormatted = ''; |
|
|
|
this.prefixDuration = null; |
|
|
|
this.prefixIncomplete = false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
isLimitReached() { |
|
if (!power_user.reasoning.add_to_prompts) { |
|
return true; |
|
} |
|
|
|
return this.counter >= power_user.reasoning.max_additions; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addToMessage(content, reasoning, isPrefix, duration) { |
|
|
|
if (!isPrefix && (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions)) { |
|
return content; |
|
} |
|
|
|
|
|
if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) { |
|
return content; |
|
} |
|
|
|
|
|
this.counter++; |
|
|
|
|
|
const prefix = substituteParams(power_user.reasoning.prefix || ''); |
|
const separator = substituteParams(power_user.reasoning.separator || ''); |
|
const suffix = substituteParams(power_user.reasoning.suffix || ''); |
|
|
|
|
|
if (isPrefix && !content) { |
|
const formattedReasoning = `${prefix}${reasoning}`; |
|
if (isPrefix) { |
|
this.prefixReasoning = reasoning; |
|
this.prefixReasoningFormatted = formattedReasoning; |
|
this.prefixLength = formattedReasoning.length; |
|
this.prefixDuration = duration; |
|
this.prefixIncomplete = true; |
|
} |
|
return formattedReasoning; |
|
} |
|
|
|
|
|
const formattedReasoning = `${prefix}${reasoning}${suffix}${separator}`; |
|
if (isPrefix) { |
|
this.prefixReasoning = reasoning; |
|
this.prefixReasoningFormatted = formattedReasoning; |
|
this.prefixLength = formattedReasoning.length; |
|
this.prefixDuration = duration; |
|
this.prefixIncomplete = false; |
|
} |
|
return `${formattedReasoning}${content}`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
removePrefix(content) { |
|
if (this.prefixLength > 0) { |
|
return content.slice(this.prefixLength); |
|
} |
|
return content; |
|
} |
|
} |
|
|
|
function loadReasoningSettings() { |
|
UI.$addToPrompts.prop('checked', power_user.reasoning.add_to_prompts); |
|
UI.$addToPrompts.on('change', function () { |
|
power_user.reasoning.add_to_prompts = !!$(this).prop('checked'); |
|
saveSettingsDebounced(); |
|
}); |
|
|
|
UI.$prefix.val(power_user.reasoning.prefix); |
|
UI.$prefix.on('input', function () { |
|
power_user.reasoning.prefix = String($(this).val()); |
|
saveSettingsDebounced(); |
|
}); |
|
|
|
UI.$suffix.val(power_user.reasoning.suffix); |
|
UI.$suffix.on('input', function () { |
|
power_user.reasoning.suffix = String($(this).val()); |
|
saveSettingsDebounced(); |
|
}); |
|
|
|
UI.$separator.val(power_user.reasoning.separator); |
|
UI.$separator.on('input', function () { |
|
power_user.reasoning.separator = String($(this).val()); |
|
saveSettingsDebounced(); |
|
}); |
|
|
|
UI.$maxAdditions.val(power_user.reasoning.max_additions); |
|
UI.$maxAdditions.on('input', function () { |
|
power_user.reasoning.max_additions = Number($(this).val()); |
|
saveSettingsDebounced(); |
|
}); |
|
|
|
UI.$autoParse.prop('checked', power_user.reasoning.auto_parse); |
|
UI.$autoParse.on('change', function () { |
|
power_user.reasoning.auto_parse = !!$(this).prop('checked'); |
|
saveSettingsDebounced(); |
|
}); |
|
|
|
UI.$autoExpand.prop('checked', power_user.reasoning.auto_expand); |
|
UI.$autoExpand.on('change', function () { |
|
power_user.reasoning.auto_expand = !!$(this).prop('checked'); |
|
toggleReasoningAutoExpand(); |
|
saveSettingsDebounced(); |
|
}); |
|
toggleReasoningAutoExpand(); |
|
|
|
UI.$showHidden.prop('checked', power_user.reasoning.show_hidden); |
|
UI.$showHidden.on('change', function () { |
|
power_user.reasoning.show_hidden = !!$(this).prop('checked'); |
|
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); |
|
saveSettingsDebounced(); |
|
}); |
|
$('#chat').attr('data-show-hidden-reasoning', power_user.reasoning.show_hidden ? 'true' : null); |
|
|
|
UI.$select.on('change', async function () { |
|
const name = String($(this).val()); |
|
const template = reasoning_templates.find(p => p.name === name); |
|
if (!template) { |
|
return; |
|
} |
|
|
|
UI.$prefix.val(template.prefix); |
|
UI.$suffix.val(template.suffix); |
|
UI.$separator.val(template.separator); |
|
|
|
power_user.reasoning.name = name; |
|
power_user.reasoning.prefix = template.prefix; |
|
power_user.reasoning.suffix = template.suffix; |
|
power_user.reasoning.separator = template.separator; |
|
|
|
saveSettingsDebounced(); |
|
}); |
|
} |
|
|
|
function selectReasoningTemplateCallback(args, name) { |
|
if (!name) { |
|
return power_user.reasoning.name ?? ''; |
|
} |
|
|
|
const quiet = isTrueBoolean(args?.quiet); |
|
const templateNames = reasoning_templates.map(preset => preset.name); |
|
let foundName = templateNames.find(x => x.toLowerCase() === name.toLowerCase()); |
|
|
|
if (!foundName) { |
|
const result = performFuzzySearch('reasoning-templates', templateNames, [], name); |
|
|
|
if (result.length === 0) { |
|
!quiet && toastr.warning(`Reasoning template "${name}" not found`); |
|
return ''; |
|
} |
|
|
|
foundName = result[0].item; |
|
} |
|
|
|
UI.$select.val(foundName).trigger('change'); |
|
!quiet && toastr.success(`Reasoning template "${foundName}" selected`); |
|
return foundName; |
|
|
|
} |
|
|
|
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: [ |
|
SlashCommandArgument.fromProps({ |
|
description: 'Message ID. If not provided, the message ID of the last message is used.', |
|
typeList: ARGUMENT_TYPE.NUMBER, |
|
enumProvider: commonEnumProviders.messages(), |
|
}), |
|
], |
|
callback: (_args, value) => { |
|
const messageId = !isNaN(parseInt(value.toString())) ? parseInt(value.toString()) : chat.length - 1; |
|
const message = chat[messageId]; |
|
const reasoning = String(message?.extra?.reasoning ?? ''); |
|
return reasoning; |
|
}, |
|
})); |
|
|
|
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: [ |
|
SlashCommandNamedArgument.fromProps({ |
|
name: 'at', |
|
description: 'Message ID. If not provided, the message ID of the last message is used.', |
|
typeList: ARGUMENT_TYPE.NUMBER, |
|
enumProvider: commonEnumProviders.messages(), |
|
}), |
|
SlashCommandNamedArgument.fromProps({ |
|
name: 'collapse', |
|
description: 'Whether to collapse the reasoning block. (If not provided, uses the default expand setting)', |
|
typeList: [ARGUMENT_TYPE.BOOLEAN], |
|
enumList: commonEnumProviders.boolean('trueFalse')(), |
|
}), |
|
], |
|
unnamedArgumentList: [ |
|
SlashCommandArgument.fromProps({ |
|
description: 'Reasoning block content.', |
|
typeList: ARGUMENT_TYPE.STRING, |
|
}), |
|
], |
|
callback: async (args, value) => { |
|
const messageId = !isNaN(Number(args.at)) ? Number(args.at) : chat.length - 1; |
|
const message = chat[messageId]; |
|
if (!message) { |
|
return ''; |
|
} |
|
|
|
if (!message.extra || typeof message.extra !== 'object') { |
|
message.extra = {}; |
|
} |
|
|
|
message.extra.reasoning = String(value ?? ''); |
|
message.extra.reasoning_type = ReasoningType.Manual; |
|
await saveChatConditional(); |
|
|
|
closeMessageEditor('reasoning'); |
|
updateMessageBlock(messageId, message); |
|
|
|
if (isTrueBoolean(String(args.collapse))) $(`#chat [mesid="${messageId}"] .mes_reasoning_details`).removeAttr('open'); |
|
if (isFalseBoolean(String(args.collapse))) $(`#chat [mesid="${messageId}"] .mes_reasoning_details`).attr('open', ''); |
|
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, |
|
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: [ |
|
SlashCommandArgument.fromProps({ |
|
description: 'input string', |
|
typeList: [ARGUMENT_TYPE.STRING], |
|
}), |
|
], |
|
callback: (args, 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.`, 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 returnMessage = args.return === 'content'; |
|
|
|
const parsedReasoning = parseReasoningFromString(value, { strict: !isFalseBoolean(String(args.strict ?? '')) }); |
|
if (!parsedReasoning) { |
|
return returnMessage ? value : ''; |
|
} |
|
|
|
if (returnMessage) { |
|
return parsedReasoning.content; |
|
} |
|
|
|
const applyRegex = !isFalseBoolean(String(args.regex ?? '')); |
|
return applyRegex |
|
? getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING) |
|
: parsedReasoning.reasoning; |
|
}, |
|
})); |
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
|
name: 'reasoning-template', |
|
aliases: ['reasoning-formatting', 'reasoning-preset'], |
|
callback: selectReasoningTemplateCallback, |
|
returns: 'template name', |
|
namedArgumentList: [ |
|
SlashCommandNamedArgument.fromProps({ |
|
name: 'quiet', |
|
description: 'Suppress the toast message on template change', |
|
typeList: [ARGUMENT_TYPE.BOOLEAN], |
|
defaultValue: 'false', |
|
enumList: commonEnumProviders.boolean('trueFalse')(), |
|
}), |
|
], |
|
unnamedArgumentList: [ |
|
SlashCommandArgument.fromProps({ |
|
description: 'reasoning template name', |
|
typeList: [ARGUMENT_TYPE.STRING], |
|
enumProvider: () => reasoning_templates.map(x => new SlashCommandEnumValue(x.name, null, enumTypes.enum, enumIcons.preset)), |
|
}), |
|
], |
|
helpString: ` |
|
<div> |
|
Selects a reasoning template by name, using fuzzy search to find the closest match. |
|
Gets the current template if no name is provided. |
|
</div> |
|
<div> |
|
<strong>Example:</strong> |
|
<ul> |
|
<li> |
|
<pre><code class="language-stscript">/reasoning-template DeepSeek</code></pre> |
|
</li> |
|
</ul> |
|
</div> |
|
`, |
|
})); |
|
} |
|
|
|
function registerReasoningMacros() { |
|
MacrosParser.registerMacro('reasoningPrefix', () => power_user.reasoning.prefix, t`Reasoning Prefix`); |
|
MacrosParser.registerMacro('reasoningSuffix', () => power_user.reasoning.suffix, t`Reasoning Suffix`); |
|
MacrosParser.registerMacro('reasoningSeparator', () => power_user.reasoning.separator, t`Reasoning Separator`); |
|
} |
|
|
|
function setReasoningEventHandlers() { |
|
|
|
|
|
|
|
|
|
|
|
function updateReasoningFromValue(message, value) { |
|
const reasoning = getRegexedString(value, regex_placement.REASONING, { isEdit: true }); |
|
message.extra.reasoning = reasoning; |
|
message.extra.reasoning_type = message.extra.reasoning_type ? ReasoningType.Edited : ReasoningType.Manual; |
|
} |
|
|
|
$(document).on('click', '.mes_reasoning_details', function (e) { |
|
if (!e.target.closest('.mes_reasoning_actions') && !e.target.closest('.mes_reasoning_header')) { |
|
e.preventDefault(); |
|
} |
|
}); |
|
|
|
$(document).on('click', '.mes_reasoning_header', function (e) { |
|
const details = $(this).closest('.mes_reasoning_details'); |
|
|
|
if (details.find('.mes_reasoning').is(':empty')) { |
|
e.preventDefault(); |
|
return; |
|
} |
|
|
|
|
|
const mes = $(this).closest('.mes'); |
|
const mesEditArea = mes.find('#curEditTextarea'); |
|
if (mesEditArea.length) { |
|
const summary = $(mes).find('.mes_reasoning_summary'); |
|
if (!summary.attr('open')) { |
|
summary.find('.mes_reasoning_edit').trigger('click'); |
|
} |
|
} |
|
}); |
|
|
|
$(document).on('click', '.mes_reasoning_copy', (e) => { |
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
}); |
|
|
|
$(document).on('click', '.mes_reasoning_edit', function (e) { |
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
const { message, messageBlock } = getMessageFromJquery(this); |
|
if (!message?.extra) { |
|
return; |
|
} |
|
|
|
const reasoning = String(message?.extra?.reasoning ?? ''); |
|
const chatElement = document.getElementById('chat'); |
|
const textarea = document.createElement('textarea'); |
|
const reasoningBlock = messageBlock.find('.mes_reasoning'); |
|
textarea.classList.add('reasoning_edit_textarea'); |
|
textarea.value = reasoning; |
|
$(textarea).insertBefore(reasoningBlock); |
|
|
|
if (!CSS.supports('field-sizing', 'content')) { |
|
const resetHeight = function () { |
|
const scrollTop = chatElement.scrollTop; |
|
textarea.style.height = '0px'; |
|
textarea.style.height = `${textarea.scrollHeight}px`; |
|
chatElement.scrollTop = scrollTop; |
|
}; |
|
|
|
textarea.addEventListener('input', resetHeight); |
|
resetHeight(); |
|
} |
|
|
|
textarea.focus(); |
|
textarea.setSelectionRange(textarea.value.length, textarea.value.length); |
|
|
|
const textareaRect = textarea.getBoundingClientRect(); |
|
const chatRect = chatElement.getBoundingClientRect(); |
|
|
|
|
|
if (textareaRect.bottom > chatRect.bottom) { |
|
const scrollOffset = textareaRect.bottom - chatRect.bottom; |
|
chatElement.scrollTop += scrollOffset; |
|
} |
|
}); |
|
|
|
$(document).on('click', '.mes_reasoning_edit_done', async function (e) { |
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
const { message, messageId, messageBlock } = getMessageFromJquery(this); |
|
if (!message?.extra) { |
|
return; |
|
} |
|
|
|
const textarea = messageBlock.find('.reasoning_edit_textarea'); |
|
const newReasoning = String(textarea.val()); |
|
textarea.remove(); |
|
if (newReasoning === message.extra.reasoning) { |
|
return; |
|
} |
|
updateReasoningFromValue(message, newReasoning); |
|
await saveChatConditional(); |
|
updateMessageBlock(messageId, message); |
|
|
|
messageBlock.find('.mes_edit_done:visible').trigger('click'); |
|
await eventSource.emit(event_types.MESSAGE_REASONING_EDITED, messageId); |
|
}); |
|
|
|
$(document).on('click', '.mes_reasoning_edit_cancel', function (e) { |
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
|
|
const { messageBlock } = getMessageFromJquery(this); |
|
const textarea = messageBlock.find('.reasoning_edit_textarea'); |
|
textarea.remove(); |
|
|
|
messageBlock.find('.mes_reasoning_edit_cancel:visible').trigger('click'); |
|
|
|
updateReasoningUI(messageBlock); |
|
}); |
|
|
|
$(document).on('click', '.mes_edit_add_reasoning', async function () { |
|
const { message, messageBlock } = getMessageFromJquery(this); |
|
if (!message?.extra) { |
|
return; |
|
} |
|
|
|
if (message.extra.reasoning) { |
|
toastr.info(t`Reasoning already exists.`, t`Edit Message`); |
|
return; |
|
} |
|
|
|
messageBlock.addClass('reasoning'); |
|
|
|
|
|
|
|
if (messageBlock.attr('data-reasoning-state') === ReasoningState.Hidden) { |
|
messageBlock.attr('data-reasoning-state', ReasoningState.Done); |
|
} |
|
|
|
|
|
messageBlock.find('.mes_reasoning_details').attr('open', ''); |
|
messageBlock.find('.mes_reasoning_edit').trigger('click'); |
|
await saveChatConditional(); |
|
}); |
|
|
|
$(document).on('click', '.mes_reasoning_delete', async function (e) { |
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
|
|
const confirm = await Popup.show.confirm(t`Remove Reasoning`, t`Are you sure you want to clear the reasoning?<br />Visible message contents will stay intact.`); |
|
|
|
if (!confirm) { |
|
return; |
|
} |
|
|
|
const { message, messageId, messageBlock } = getMessageFromJquery(this); |
|
if (!message?.extra) { |
|
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'); |
|
textarea.remove(); |
|
await eventSource.emit(event_types.MESSAGE_REASONING_DELETED, messageId); |
|
}); |
|
|
|
$(document).on('pointerup', '.mes_reasoning_copy', async function () { |
|
const { message } = getMessageFromJquery(this); |
|
const reasoning = String(message?.extra?.reasoning ?? ''); |
|
|
|
if (!reasoning) { |
|
return; |
|
} |
|
|
|
await copyText(reasoning); |
|
toastr.info(t`Copied!`, '', { timeOut: 2000 }); |
|
}); |
|
|
|
$(document).on('input', '.reasoning_edit_textarea', function () { |
|
if (!power_user.auto_save_msg_edits) { |
|
return; |
|
} |
|
|
|
const { message, messageBlock } = getMessageFromJquery(this); |
|
if (!message?.extra) { |
|
return; |
|
} |
|
|
|
updateReasoningFromValue(message, String($(this).val())); |
|
updateReasoningUI(messageBlock); |
|
saveChatDebounced(); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function removeReasoningFromString(str) { |
|
if (!power_user.reasoning.auto_parse) { |
|
return str; |
|
} |
|
|
|
const parsedReasoning = parseReasoningFromString(str); |
|
return parsedReasoning?.content ?? str; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function parseReasoningFromString(str, { strict = true } = {}) { |
|
|
|
if (!power_user.reasoning.prefix || !power_user.reasoning.suffix) { |
|
return null; |
|
} |
|
|
|
try { |
|
const regex = new RegExp(`${(strict ? '^\\s*?' : '')}${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) { |
|
reasoning = trimSpaces(reasoning); |
|
content = trimSpaces(content); |
|
} |
|
|
|
return { reasoning, content }; |
|
} catch (error) { |
|
console.error('[Reasoning] Error parsing reasoning block', error); |
|
return null; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function parseReasoningInSwipes(swipes, swipeInfoArray, duration) { |
|
if (!power_user.reasoning.auto_parse) { |
|
return; |
|
} |
|
|
|
|
|
if (!Array.isArray(swipes) || !Array.isArray(swipeInfoArray) || swipes.length !== swipeInfoArray.length) { |
|
return; |
|
} |
|
|
|
for (let index = 0; index < swipes.length; index++) { |
|
const parsedReasoning = parseReasoningFromString(swipes[index]); |
|
if (parsedReasoning) { |
|
swipes[index] = getRegexedString(parsedReasoning.content, regex_placement.REASONING); |
|
swipeInfoArray[index].extra.reasoning = parsedReasoning.reasoning; |
|
swipeInfoArray[index].extra.reasoning_duration = duration; |
|
swipeInfoArray[index].extra.reasoning_type = ReasoningType.Parsed; |
|
} |
|
} |
|
} |
|
|
|
function registerReasoningAppEvents() { |
|
const eventHandler = ( type, idx) => { |
|
if (!power_user.reasoning.auto_parse) { |
|
return; |
|
} |
|
|
|
console.debug('[Reasoning] Auto-parsing reasoning block for message', idx); |
|
const prefix = type === event_types.MESSAGE_RECEIVED ? PromptReasoning.getLatestPrefix() : ''; |
|
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; |
|
} |
|
|
|
if (message.extra?.reasoning && !prefix) { |
|
console.debug('[Reasoning] Message already has reasoning', idx); |
|
return null; |
|
} |
|
|
|
const parsedReasoning = parseReasoningFromString(prefix + message.mes); |
|
|
|
|
|
if (!parsedReasoning) { |
|
return; |
|
} |
|
|
|
|
|
if (!message.extra || typeof message.extra !== 'object') { |
|
message.extra = {}; |
|
} |
|
|
|
const contentUpdated = !!parsedReasoning.reasoning || parsedReasoning.content !== message.mes; |
|
|
|
|
|
if (parsedReasoning.reasoning) { |
|
message.extra.reasoning = getRegexedString(parsedReasoning.reasoning, regex_placement.REASONING); |
|
message.extra.reasoning_type = ReasoningType.Parsed; |
|
} |
|
|
|
|
|
if (parsedReasoning.content !== message.mes) { |
|
message.mes = parsedReasoning.content; |
|
} |
|
|
|
if (contentUpdated) { |
|
syncMesToSwipe(); |
|
saveChatDebounced(); |
|
|
|
|
|
const messageRendered = document.querySelector(`.mes[mesid="${idx}"]`) !== null; |
|
if (messageRendered) { |
|
console.debug('[Reasoning] Updating message block', idx); |
|
updateMessageBlock(idx, message); |
|
} |
|
} |
|
}; |
|
|
|
for (const event of [event_types.MESSAGE_RECEIVED, event_types.MESSAGE_UPDATED]) { |
|
eventSource.on(event, ( idx) => eventHandler(event, idx)); |
|
} |
|
|
|
for (const event of [event_types.GENERATION_STOPPED, event_types.GENERATION_ENDED, event_types.CHAT_CHANGED]) { |
|
eventSource.on(event, () => PromptReasoning.clearLatest()); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function loadReasoningTemplates(data) { |
|
if (data.reasoning !== undefined) { |
|
reasoning_templates.splice(0, reasoning_templates.length, ...data.reasoning); |
|
} |
|
|
|
for (const template of reasoning_templates) { |
|
$('<option>').val(template.name).text(template.name).appendTo(UI.$select); |
|
} |
|
|
|
|
|
if (power_user.reasoning.name === undefined) { |
|
const defaultTemplate = reasoning_templates.find(p => p.name === DEFAULT_REASONING_TEMPLATE); |
|
if (defaultTemplate) { |
|
|
|
if (power_user.reasoning.prefix !== defaultTemplate.prefix || power_user.reasoning.suffix !== defaultTemplate.suffix || power_user.reasoning.separator !== defaultTemplate.separator) { |
|
|
|
const data = { |
|
name: '[Migrated] Custom', |
|
prefix: power_user.reasoning.prefix, |
|
suffix: power_user.reasoning.suffix, |
|
separator: power_user.reasoning.separator, |
|
}; |
|
await getPresetManager('reasoning')?.savePreset(data.name, data); |
|
power_user.reasoning.name = data.name; |
|
} else { |
|
power_user.reasoning.name = defaultTemplate.name; |
|
} |
|
} else { |
|
|
|
power_user.reasoning.name = ''; |
|
} |
|
|
|
saveSettingsDebounced(); |
|
} |
|
|
|
UI.$select.val(power_user.reasoning.name); |
|
} |
|
|
|
|
|
|
|
|
|
export function initReasoning() { |
|
loadReasoningSettings(); |
|
setReasoningEventHandlers(); |
|
registerReasoningSlashCommands(); |
|
registerReasoningMacros(); |
|
registerReasoningAppEvents(); |
|
} |
|
|