Move reasoning-specific code into its own module

eb798fa4f191892bffe07d40bd255b69dddf2d4f

Cohee <18619528+Cohee1207@users.noreply.github.com>

4 files changed, +231 -222Showing whitespace changes
public/script.js+3 -1
@@ -238,7 +238,7 @@ import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_set
238import { hideLoader, showLoader } from './scripts/loader.js';238import { hideLoader, showLoader } from './scripts/loader.js';
239import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';239import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
240import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js';240import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js';
241import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, PromptReasoning } from './scripts/chats.js';241import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat } from './scripts/chats.js';
242import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';242import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
243import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';243import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
244import { currentUser, setUserControls } from './scripts/user.js';244import { currentUser, setUserControls } from './scripts/user.js';
@@ -267,6 +267,7 @@ import { initSettingsSearch } from './scripts/setting-search.js';
267import { initBulkEdit } from './scripts/bulk-edit.js';267import { initBulkEdit } from './scripts/bulk-edit.js';
268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';268import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
269import { getContext } from './scripts/st-context.js';269import { getContext } from './scripts/st-context.js';
270import { initReasoning, PromptReasoning } from './scripts/reasoning.js';
270271
271// API OBJECT FOR EXTERNAL WIRING272// API OBJECT FOR EXTERNAL WIRING
272globalThis.SillyTavern = {273globalThis.SillyTavern = {
@@ -982,6 +983,7 @@ async function firstLoadInit() {
982 initServerHistory();983 initServerHistory();
983 initSettingsSearch();984 initSettingsSearch();
984 initBulkEdit();985 initBulkEdit();
986 initReasoning();
985 await initScrapers();987 await initScrapers();
986 doDailyExtensionUpdatesCheck();988 doDailyExtensionUpdatesCheck();
987 await hideLoader();989 await hideLoader();
public/scripts/chats.js+0 -188
@@ -23,9 +23,6 @@ import {
23 neutralCharacterName,23 neutralCharacterName,
24 updateChatMetadata,24 updateChatMetadata,
25 system_message_types,25 system_message_types,
26 updateMessageBlock,
27 closeMessageEditor,
28 substituteParams,
29} from '../script.js';26} from '../script.js';
30import { selected_group } from './group-chats.js';27import { selected_group } from './group-chats.js';
31import { power_user } from './power-user.js';28import { power_user } from './power-user.js';
@@ -40,7 +37,6 @@ import {
40 saveBase64AsFile,37 saveBase64AsFile,
41 extractTextFromOffice,38 extractTextFromOffice,
42 download,39 download,
43 copyText,
44} from './utils.js';40} from './utils.js';
45import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';41import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
46import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';42import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -1420,72 +1416,7 @@ export function registerFileConverter(mimeType, converter) {
1420 converters[mimeType] = converter;1416 converters[mimeType] = converter;
1421}1417}
14221418
1423/**
1424 * Helper class for adding reasoning to messages.
1425 * Keeps track of the number of reasoning additions.
1426 */
1427export class PromptReasoning {
1428 static REASONING_PLACEHOLDER = '\u200B';
1429
1430 constructor() {
1431 this.counter = 0;
1432 }
1433
1434 /**
1435 * Checks if the limit of reasoning additions has been reached.
1436 * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
1437 */
1438 isLimitReached() {
1439 if (!power_user.reasoning.add_to_prompts) {
1440 return true;
1441 }
1442
1443 return this.counter >= power_user.reasoning.max_additions;
1444 }
1445
1446 /**
1447 * Add reasoning to a message according to the power user settings.
1448 * @param {string} content Message content
1449 * @param {string} reasoning Message reasoning
1450 * @returns {string} Message content with reasoning
1451 */
1452 addToMessage(content, reasoning) {
1453 // Disabled or reached limit of additions
1454 if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {
1455 return content;
1456 }
1457
1458 // No reasoning provided or a placeholder
1459 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
1460 return content;
1461 }
1462
1463 // Increment the counter
1464 this.counter++;
1465
1466 // Substitute macros in variable parts
1467 const prefix = substituteParams(power_user.reasoning.prefix || '');
1468 const separator = substituteParams(power_user.reasoning.separator || '');
1469 const suffix = substituteParams(power_user.reasoning.suffix || '');
1470
1471 // Combine parts with reasoning and content
1472 return `${prefix}${reasoning}${suffix}${separator}${content}`;
1473 }
1474}
1475
1476jQuery(function () {1419jQuery(function () {
1477 /**
1478 * Gets a message from a jQuery element.
1479 * @param {Element} element
1480 * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
1481 */
1482 const getMessageFromJquery = function (element) {
1483 const messageBlock = $(element).closest('.mes');
1484 const messageId = Number(messageBlock.attr('mesid'));
1485 const message = chat[messageId];
1486 return { messageId: messageId, message, messageBlock };
1487 };
1488
1489 $(document).on('click', '.mes_hide', async function () {1420 $(document).on('click', '.mes_hide', async function () {
1490 const messageBlock = $(this).closest('.mes');1421 const messageBlock = $(this).closest('.mes');
1491 const messageId = Number(messageBlock.attr('mesid'));1422 const messageId = Number(messageBlock.attr('mesid'));
@@ -1636,125 +1567,6 @@ jQuery(function () {
1636 $(document).on('click', '.mes_img_enlarge', enlargeMessageImage);1567 $(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
1637 $(document).on('click', '.mes_img_delete', deleteMessageImage);1568 $(document).on('click', '.mes_img_delete', deleteMessageImage);
16381569
1639 $(document).on('click', '.mes_reasoning_copy', (e) => {
1640 e.stopPropagation();
1641 e.preventDefault();
1642 });
1643
1644 $(document).on('click', '.mes_reasoning_edit', function (e) {
1645 e.stopPropagation();
1646 e.preventDefault();
1647 const { message, messageBlock } = getMessageFromJquery(this);
1648 if (!message?.extra) {
1649 return;
1650 }
1651
1652 const reasoning = message?.extra?.reasoning;
1653 const chatElement = document.getElementById('chat');
1654 const textarea = document.createElement('textarea');
1655 const reasoningBlock = messageBlock.find('.mes_reasoning');
1656 textarea.classList.add('reasoning_edit_textarea');
1657 textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning;
1658 $(textarea).insertBefore(reasoningBlock);
1659
1660 if (!CSS.supports('field-sizing', 'content')) {
1661 const resetHeight = function () {
1662 const scrollTop = chatElement.scrollTop;
1663 textarea.style.height = '0px';
1664 textarea.style.height = `${textarea.scrollHeight}px`;
1665 chatElement.scrollTop = scrollTop;
1666 };
1667
1668 textarea.addEventListener('input', resetHeight);
1669 resetHeight();
1670 }
1671
1672 textarea.focus();
1673 textarea.setSelectionRange(textarea.value.length, textarea.value.length);
1674
1675 const textareaRect = textarea.getBoundingClientRect();
1676 const chatRect = chatElement.getBoundingClientRect();
1677
1678 // Scroll if textarea bottom is below visible area
1679 if (textareaRect.bottom > chatRect.bottom) {
1680 const scrollOffset = textareaRect.bottom - chatRect.bottom;
1681 chatElement.scrollTop += scrollOffset;
1682 }
1683 });
1684
1685 $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
1686 e.stopPropagation();
1687 e.preventDefault();
1688 const { message, messageId, messageBlock } = getMessageFromJquery(this);
1689 if (!message?.extra) {
1690 return;
1691 }
1692
1693 const textarea = messageBlock.find('.reasoning_edit_textarea');
1694 const reasoning = String(textarea.val());
1695 message.extra.reasoning = reasoning;
1696 await saveChatConditional();
1697 updateMessageBlock(messageId, message);
1698 textarea.remove();
1699 });
1700
1701 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
1702 e.stopPropagation();
1703 e.preventDefault();
1704
1705 const { messageBlock } = getMessageFromJquery(this);
1706 const textarea = messageBlock.find('.reasoning_edit_textarea');
1707 textarea.remove();
1708 });
1709
1710 $(document).on('click', '.mes_edit_add_reasoning', async function () {
1711 const { message, messageId } = getMessageFromJquery(this);
1712 if (!message?.extra) {
1713 return;
1714 }
1715
1716 if (message.extra.reasoning) {
1717 toastr.info(t`Reasoning already exists.`, t`Edit Message`);
1718 return;
1719 }
1720
1721 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
1722 await saveChatConditional();
1723 closeMessageEditor();
1724 updateMessageBlock(messageId, message);
1725 });
1726
1727 $(document).on('click', '.mes_reasoning_delete', async function (e) {
1728 e.stopPropagation();
1729 e.preventDefault();
1730
1731 const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
1732
1733 if (!confirm) {
1734 return;
1735 }
1736
1737 const { message, messageId } = getMessageFromJquery(this);
1738 if (!message?.extra) {
1739 return;
1740 }
1741 message.extra.reasoning = '';
1742 await saveChatConditional();
1743 updateMessageBlock(messageId, message);
1744 });
1745
1746 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
1747 const { message } = getMessageFromJquery(this);
1748 const reasoning = message?.extra?.reasoning;
1749
1750 if (!reasoning) {
1751 return;
1752 }
1753
1754 await copyText(reasoning);
1755 toastr.info(t`Copied!`, '', { timeOut: 2000 });
1756 });
1757
1758 $('#file_form_input').on('change', async () => {1570 $('#file_form_input').on('change', async () => {
1759 const fileInput = document.getElementById('file_form_input');1571 const fileInput = document.getElementById('file_form_input');
1760 if (!(fileInput instanceof HTMLInputElement)) return;1572 if (!(fileInput instanceof HTMLInputElement)) return;
public/scripts/power-user.js+0 -33
@@ -1621,7 +1621,6 @@ async function loadPowerUserSettings(settings, data) {
1621 loadMovingUIState();1621 loadMovingUIState();
1622 loadCharListState();1622 loadCharListState();
1623 toggleMDHotkeyIconDisplay();1623 toggleMDHotkeyIconDisplay();
1624 loadReasoningSettings();
1625}1624}
16261625
1627function toggleMDHotkeyIconDisplay() {1626function toggleMDHotkeyIconDisplay() {
@@ -1638,38 +1637,6 @@ function loadCharListState() {
1638 document.body.classList.toggle('charListGrid', power_user.charListGrid);1637 document.body.classList.toggle('charListGrid', power_user.charListGrid);
1639}1638}
16401639
1641function loadReasoningSettings() {
1642 $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
1643 $('#reasoning_add_to_prompts').on('change', function () {
1644 power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
1645 saveSettingsDebounced();
1646 });
1647
1648 $('#reasoning_prefix').val(power_user.reasoning.prefix);
1649 $('#reasoning_prefix').on('input', function () {
1650 power_user.reasoning.prefix = String($(this).val());
1651 saveSettingsDebounced();
1652 });
1653
1654 $('#reasoning_suffix').val(power_user.reasoning.suffix);
1655 $('#reasoning_suffix').on('input', function () {
1656 power_user.reasoning.suffix = String($(this).val());
1657 saveSettingsDebounced();
1658 });
1659
1660 $('#reasoning_separator').val(power_user.reasoning.separator);
1661 $('#reasoning_separator').on('input', function () {
1662 power_user.reasoning.separator = String($(this).val());
1663 saveSettingsDebounced();
1664 });
1665
1666 $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
1667 $('#reasoning_max_additions').on('input', function () {
1668 power_user.reasoning.max_additions = Number($(this).val());
1669 saveSettingsDebounced();
1670 });
1671}
1672
1673function loadMovingUIState() {1640function loadMovingUIState() {
1674 if (!isMobile()1641 if (!isMobile()
1675 && power_user.movingUIState1642 && power_user.movingUIState
public/scripts/reasoning.js+228 -0
@@ -0,0 +1,228 @@
1import { chat, closeMessageEditor, saveChatConditional, saveSettingsDebounced, substituteParams, updateMessageBlock } from '../script.js';
2import { t } from './i18n.js';
3import { Popup } from './popup.js';
4import { power_user } from './power-user.js';
5import { copyText } from './utils.js';
6
7/**
8 * Gets a message from a jQuery element.
9 * @param {Element} element
10 * @returns {{messageId: number, message: object, messageBlock: JQuery<HTMLElement>}}
11 */
12function getMessageFromJquery(element) {
13 const messageBlock = $(element).closest('.mes');
14 const messageId = Number(messageBlock.attr('mesid'));
15 const message = chat[messageId];
16 return { messageId: messageId, message, messageBlock };
17}
18
19/**
20 * Helper class for adding reasoning to messages.
21 * Keeps track of the number of reasoning additions.
22 */
23export class PromptReasoning {
24 static REASONING_PLACEHOLDER = '\u200B';
25
26 constructor() {
27 this.counter = 0;
28 }
29
30 /**
31 * Checks if the limit of reasoning additions has been reached.
32 * @returns {boolean} True if the limit of reasoning additions has been reached, false otherwise.
33 */
34 isLimitReached() {
35 if (!power_user.reasoning.add_to_prompts) {
36 return true;
37 }
38
39 return this.counter >= power_user.reasoning.max_additions;
40 }
41
42 /**
43 * Add reasoning to a message according to the power user settings.
44 * @param {string} content Message content
45 * @param {string} reasoning Message reasoning
46 * @returns {string} Message content with reasoning
47 */
48 addToMessage(content, reasoning) {
49 // Disabled or reached limit of additions
50 if (!power_user.reasoning.add_to_prompts || this.counter >= power_user.reasoning.max_additions) {
51 return content;
52 }
53
54 // No reasoning provided or a placeholder
55 if (!reasoning || reasoning === PromptReasoning.REASONING_PLACEHOLDER) {
56 return content;
57 }
58
59 // Increment the counter
60 this.counter++;
61
62 // Substitute macros in variable parts
63 const prefix = substituteParams(power_user.reasoning.prefix || '');
64 const separator = substituteParams(power_user.reasoning.separator || '');
65 const suffix = substituteParams(power_user.reasoning.suffix || '');
66
67 // Combine parts with reasoning and content
68 return `${prefix}${reasoning}${suffix}${separator}${content}`;
69 }
70}
71
72function loadReasoningSettings() {
73 $('#reasoning_add_to_prompts').prop('checked', power_user.reasoning.add_to_prompts);
74 $('#reasoning_add_to_prompts').on('change', function () {
75 power_user.reasoning.add_to_prompts = !!$(this).prop('checked');
76 saveSettingsDebounced();
77 });
78
79 $('#reasoning_prefix').val(power_user.reasoning.prefix);
80 $('#reasoning_prefix').on('input', function () {
81 power_user.reasoning.prefix = String($(this).val());
82 saveSettingsDebounced();
83 });
84
85 $('#reasoning_suffix').val(power_user.reasoning.suffix);
86 $('#reasoning_suffix').on('input', function () {
87 power_user.reasoning.suffix = String($(this).val());
88 saveSettingsDebounced();
89 });
90
91 $('#reasoning_separator').val(power_user.reasoning.separator);
92 $('#reasoning_separator').on('input', function () {
93 power_user.reasoning.separator = String($(this).val());
94 saveSettingsDebounced();
95 });
96
97 $('#reasoning_max_additions').val(power_user.reasoning.max_additions);
98 $('#reasoning_max_additions').on('input', function () {
99 power_user.reasoning.max_additions = Number($(this).val());
100 saveSettingsDebounced();
101 });
102}
103
104function setReasoningEventHandlers(){
105 $(document).on('click', '.mes_reasoning_copy', (e) => {
106 e.stopPropagation();
107 e.preventDefault();
108 });
109
110 $(document).on('click', '.mes_reasoning_edit', function (e) {
111 e.stopPropagation();
112 e.preventDefault();
113 const { message, messageBlock } = getMessageFromJquery(this);
114 if (!message?.extra) {
115 return;
116 }
117
118 const reasoning = message?.extra?.reasoning;
119 const chatElement = document.getElementById('chat');
120 const textarea = document.createElement('textarea');
121 const reasoningBlock = messageBlock.find('.mes_reasoning');
122 textarea.classList.add('reasoning_edit_textarea');
123 textarea.value = reasoning === PromptReasoning.REASONING_PLACEHOLDER ? '' : reasoning;
124 $(textarea).insertBefore(reasoningBlock);
125
126 if (!CSS.supports('field-sizing', 'content')) {
127 const resetHeight = function () {
128 const scrollTop = chatElement.scrollTop;
129 textarea.style.height = '0px';
130 textarea.style.height = `${textarea.scrollHeight}px`;
131 chatElement.scrollTop = scrollTop;
132 };
133
134 textarea.addEventListener('input', resetHeight);
135 resetHeight();
136 }
137
138 textarea.focus();
139 textarea.setSelectionRange(textarea.value.length, textarea.value.length);
140
141 const textareaRect = textarea.getBoundingClientRect();
142 const chatRect = chatElement.getBoundingClientRect();
143
144 // Scroll if textarea bottom is below visible area
145 if (textareaRect.bottom > chatRect.bottom) {
146 const scrollOffset = textareaRect.bottom - chatRect.bottom;
147 chatElement.scrollTop += scrollOffset;
148 }
149 });
150
151 $(document).on('click', '.mes_reasoning_edit_done', async function (e) {
152 e.stopPropagation();
153 e.preventDefault();
154 const { message, messageId, messageBlock } = getMessageFromJquery(this);
155 if (!message?.extra) {
156 return;
157 }
158
159 const textarea = messageBlock.find('.reasoning_edit_textarea');
160 const reasoning = String(textarea.val());
161 message.extra.reasoning = reasoning;
162 await saveChatConditional();
163 updateMessageBlock(messageId, message);
164 textarea.remove();
165 });
166
167 $(document).on('click', '.mes_reasoning_edit_cancel', function (e) {
168 e.stopPropagation();
169 e.preventDefault();
170
171 const { messageBlock } = getMessageFromJquery(this);
172 const textarea = messageBlock.find('.reasoning_edit_textarea');
173 textarea.remove();
174 });
175
176 $(document).on('click', '.mes_edit_add_reasoning', async function () {
177 const { message, messageId } = getMessageFromJquery(this);
178 if (!message?.extra) {
179 return;
180 }
181
182 if (message.extra.reasoning) {
183 toastr.info(t`Reasoning already exists.`, t`Edit Message`);
184 return;
185 }
186
187 message.extra.reasoning = PromptReasoning.REASONING_PLACEHOLDER;
188 await saveChatConditional();
189 closeMessageEditor();
190 updateMessageBlock(messageId, message);
191 });
192
193 $(document).on('click', '.mes_reasoning_delete', async function (e) {
194 e.stopPropagation();
195 e.preventDefault();
196
197 const confirm = await Popup.show.confirm(t`Are you sure you want to clear the reasoning?`, t`Visible message contents will stay intact.`);
198
199 if (!confirm) {
200 return;
201 }
202
203 const { message, messageId } = getMessageFromJquery(this);
204 if (!message?.extra) {
205 return;
206 }
207 message.extra.reasoning = '';
208 await saveChatConditional();
209 updateMessageBlock(messageId, message);
210 });
211
212 $(document).on('pointerup', '.mes_reasoning_copy', async function () {
213 const { message } = getMessageFromJquery(this);
214 const reasoning = message?.extra?.reasoning;
215
216 if (!reasoning) {
217 return;
218 }
219
220 await copyText(reasoning);
221 toastr.info(t`Copied!`, '', { timeOut: 2000 });
222 });
223}
224
225export function initReasoning() {
226 loadReasoningSettings();
227 setReasoningEventHandlers();
228}