More coding on ReasoningHandler, fixing stuff

9697322f1e35c58d9026d8eeec1d8d307817c2ea

Wolfsblvt <wolfsblvt@gmail.com>

2 files changed, +192 -130Ignore whitespace
public/script.js+6 -11
@@ -2223,8 +2223,6 @@ function getMessageFromTemplate({
22232223 isUser,
22242224 avatarImg,
22252225 bias,
2226- reasoning,
2227- reasoningDuration,
22282226 isSystem,
22292227 title,
22302228 timerValue,
@@ -2256,7 +2254,7 @@ function getMessageFromTemplate({
22562254 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
22572255 bookmarkLink && updateBookmarkDisplay(mes);
22582256
2259- updateReasoningUI(mes, reasoning, reasoningDuration, { forceEnd: true });
2257+ updateReasoningUI(mes);
22602258
22612259 if (power_user.timestamp_model_icon && extra?.api) {
22622260 insertSVGIcon(mes, extra);
@@ -2279,7 +2277,7 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true
22792277 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
22802278 }
22812279
2282- updateReasoningUI(messageElement, message?.extra?.reasoning, message?.extra?.reasoning_duration, { forceEnd: true });
2280+ updateReasoningUI(messageElement);
22832281
22842282 addCopyToCodeBlocks(messageElement);
22852283 appendMediaToMessage(message, messageElement);
@@ -2440,7 +2438,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24402438 false,
24412439 );
24422440 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2443- const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
24442441 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24452442
24462443 let params = {
@@ -2450,8 +2447,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
24502447 isUser: mes.is_user,
24512448 avatarImg: avatarImg,
24522449 bias: bias,
2453- reasoning: reasoning,
2454- reasoningDuration: mes.extra?.reasoning_duration,
24552450 isSystem: isSystem,
24562451 title: title,
24572452 bookmarkLink: bookmarkLink,
@@ -3144,7 +3139,7 @@ class StreamingProcessor {
31443139 this.messageLogprobs = [];
31453140 this.toolCalls = [];
31463141 // Initialize reasoning in its own handler
31473142 this.reasoningHandler = new ReasoningHandler(type, timeStarted);
31483143 }
31493144
31503145 #checkDomElements(messageId) {
@@ -3154,7 +3149,7 @@ class StreamingProcessor {
31543149 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
31553150 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
31563151 }
31573152 this.reasoningHandler.checkDomElementsupdateDom(messageId);
31583153 }
31593154
31603155 #updateMessageBlockVisibility() {
@@ -3378,7 +3373,7 @@ class StreamingProcessor {
33783373 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
33793374 }
33803375 // Get the updated reasoning string into the handler
33813376 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
33823377 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
33833378 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
33843379 }
@@ -8727,7 +8722,7 @@ const swipe_right = () => {
87278722 // resets the timer
87288723 swipeMessage.find('.mes_timer').html('');
87298724 swipeMessage.find('.tokenCounterDisplay').text('');
87308725 updateReasoningUI(swipeMessage, null);
87318726 } else {
87328727 //console.log('showing previously generated swipe candidate, or "..."');
87338728 //console.log('onclick right swipe calling addOneMessage');
public/scripts/reasoning.js+186 -119
@@ -117,53 +117,20 @@ export function isHiddenReasoningModel() {
117117}
118118
119119/**
120120 * Updates the Reasoning UI. for a specific message
121121 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element.
122- * @param {string|null} [reasoning=null] The reasoning content.
123- * @param {number|null} [reasoningDuration=null] The duration of the reasoning in milliseconds.
124- * @param {object} [options={}] Options for the function.
125- * @param {boolean} [options.forceEnd=false] If true, there will be no "Thinking..." when no duration exists.
126122 */
127-export function updateReasoningUI(messageIdOrElement, reasoning = null, reasoningDuration = null, { forceEnd = false } = {}) {
123+export function updateReasoningUI(messageIdOrElement) {
128- const messageElement = typeof messageIdOrElement === 'number'
124+ const handler = new ReasoningHandler();
129- ? $(`#chat [mesid="${messageIdOrElement}"]`)
125+ handler.initHandleMessage(messageIdOrElement);
130- : $(messageIdOrElement);
131- const mesReasoningElement = messageElement.find('.mes_reasoning');
132- const mesReasoningHeaderTitle = messageElement.find('.mes_reasoning_header_title');
133- const mesId = Number(messageElement.attr('mesid'));
134-
135- mesReasoningElement.html(messageFormatting(reasoning ?? '', '', false, false, mesId, {}, true));
136- const reasoningText = mesReasoningElement.text().trim();
137-
138- const hasReasoningText = !!reasoningText;
139- const isReasoningHidden = (!!reasoningDuration && !hasReasoningText) || (!forceEnd && isHiddenReasoningModel());
140- const isReasoning = hasReasoningText || isReasoningHidden;
141-
142- messageElement.toggleClass('reasoning', isReasoning);
143- messageElement.toggleClass('reasoning_hidden', isReasoningHidden);
144- updateReasoningTimeUI(mesReasoningHeaderTitle[0], reasoningDuration, { forceEnd });
145126}
146127
128+
147129/**
148130 * UpdatesEnum for representing the Reasoningstate controlsof reasoning
149131 * @paramenum {HTMLElementstring} element The element to update
150- * @param {number?} duration The duration of the reasoning in milliseconds
132+ * @readonly
151- * @param {object} [options={}] Options for the function
152- * @param {boolean} [options.forceEnd=false] If true, there will be no "Thinking..." when no duration exists
153133 */
154-function updateReasoningTimeUI(element, duration, { forceEnd = false } = {}) {
155- if (duration) {
156- const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
157- const secondsStr = moment.duration(duration).asSeconds();
158- element.innerHTML = t`Thought for <span title="${secondsStr} seconds">${durationStr}</span>`;
159- } else if (forceEnd) {
160- element.textContent = t`Thought for some time`;
161- } else {
162- element.textContent = t`Thinking...`;
163- }
164-}
165-
166-/** @enum {string} */
167134export const ReasoningState = {
168135 None: 'none',
169136 Thinking: 'thinking',
@@ -173,16 +140,15 @@ export const ReasoningState = {
173140
174141/**
175142 * Handles reasoning-specific logic and DOM updates for messages.
176- * Used inside the @see {StreamingProcessor}
143+ * This class is used inside the {@link StreamingProcessor} to manage reasoning states and UI updates.
177144 */
178145export class ReasoningHandler {
179146 #isHiddenisHiddenReasoningModel;
180147
181148 /**
182149 * @param {stringDate?} type[timeStarted=null] - TheWhen streamingthe typegeneration started
183- * @param {Date} timeStarted - When the generation started
184150 */
185151 constructor(type, timeStarted = null) {
186152 /** @type {ReasoningState} The current state of the reasoning process */
187153 this.state = ReasoningState.None;
188154 /** @type {string} The reasoning output */
@@ -192,28 +158,69 @@ export class ReasoningHandler {
192158 /** @type {Date} When the reasoning ended */
193159 this.endTime = null;
194160
195- /** @type {string} Generation type (normal, continue, impersonation, etc) */
196- this.type = type;
197161 /** @type {Date} Initial starting time of the generation */
198162 this.initialTime = timeStarted ?? new Date();
199163
200164 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
201165 this.#isHiddenisHiddenReasoningModel = isHiddenReasoningModel();
202166
203167 // Cached DOM elements for reasoning
204168 /** @type {HTMLElement} Main message DOM element `.mes` */
205169 this.messageDom = null;
206170 /** @type {HTMLElement} Reasoning details DOM element `.mes_reasoning_details` */
207171 this.messageReasoningDetailsDom = null;
208172 /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning_contentmes_reasoning` */
209173 this.messageReasoningContentDom = null;
210174 /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_headermes_reasoning_header_title` */
211175 this.messageReasoningHeaderDom = null;
212176 }
213177
214178 /**
179+ * Initializes the reasoning handler for a specific message.
180+ *
181+ * Can be used to update the DOM elements or read other reasoning states.
182+ * It will internally take the message-saved data and write the states back into the handler, as if during streaming of the message.
183+ * The state will always be either done/hidden or none.
184+ *
185+ * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement - The message ID or the message element
186+ */
187+ initHandleMessage(messageIdOrElement) {
188+ /** @type {HTMLElement} */
189+ const messageElement = typeof messageIdOrElement === 'number'
190+ ? document.querySelector(`#chat [mesid="${messageIdOrElement}"]`)
191+ : messageIdOrElement instanceof HTMLElement
192+ ? messageIdOrElement
193+ : $(messageIdOrElement)[0];
194+ const messageId = Number(messageElement.getAttribute('mesid'));
195+
196+ if (isNaN(messageId)) return;
197+
198+ const extra = chat[messageId]['extra'];
199+
200+ if (extra.reasoning) {
201+ this.state = ReasoningState.Done;
202+ } else if (extra.reasoning_duration) {
203+ this.state = ReasoningState.Hidden;
204+ }
205+
206+ this.reasoning = extra?.reasoning ?? '';
207+
208+ if (this.state !== ReasoningState.None) {
209+ this.initialTime = new Date(chat[messageId].gen_started);
210+ this.startTime = this.initialTime;
211+ this.endTime = new Date(this.startTime.getTime() + (extra?.reasoning_duration ?? 0));
212+ }
213+
214+ // Prefill main dom element, as message might not have been rendered yet
215+ this.messageDom = messageElement;
216+
217+ this.updateDom(messageId);
218+ }
219+
220+ /**
215221 * Gets the duration of the reasoning in milliseconds.
216- * @returns {number|null} The duration in milliseconds, or null if the start or end time is not set.
222+ *
223+ * @returns {number?} The duration in milliseconds, or null if the start or end time is not set
217224 */
218225 getDuration() {
219226 if (this.startTime && this.endTime) {
@@ -223,97 +230,157 @@ export class ReasoningHandler {
223230 }
224231
225232 /**
226233 * Finds andUpdates cachesthe reasoning-related DOM elementstext/string for the givena message.
227- * @param {number} messageId The message ID
234+ *
235+ * @param {number} messageId - The ID of the message to update
236+ * @param {string?} [reasoning=null] - The reasoning text to update - If null, uses the current reasoning
237+ * @param {Object} [options={}] - Optional arguments
238+ * @param {boolean} [options.persist=false] - Whether to persist the reasoning to the message object
239+ * @returns {boolean} - Returns true if the reasoning was changed, otherwise false
228240 */
229- checkDomElements(messageId) {
241+ updateReasoning(messageId, reasoning = null, { persist = false } = {}) {
230- // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
242+ reasoning = reasoning ?? this.reasoning;
231- if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
243+ const reasoningChanged = this.reasoning !== reasoning;
232- this.messageDom = null;
244+ this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
233- }
234245
235- // Cache the DOM elements once
246+ if (persist) {
236- if (this.messageDom === null) {
247+ // Ensure the chat extra exists
237- this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
248+ if (!chat[messageId]['extra']) {
238- this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
249+ chat[messageId]['extra'] = {};
239- this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
250+ }
240- this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
241- // Update the DOM with the current reasoning state.
242- this.messageDom.dataset.state = this.state;
243- this.messageDom.classList.toggle('reasoning_hidden', this.#isHidden);
244- }
245251
246- // Update main DOM state
252+ // Build and save the reasoning data to message extras
247- this.#updateDomState();
253+ const extra = chat[messageId]['extra'];
248- }
254+ extra['reasoning'] = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
255+ extra['reasoning_duration'] = this.getDuration();
256+ }
249257
250- #updateDomState() {
258+ return reasoningChanged;
251- this.messageDom.dataset.state = this.state;
252- this.messageDom.classList.toggle('reasoning_hidden', this.#isHidden);
253259 }
254260
255- updateReasoning(reasoning = null) {
256- reasoning = reasoning ?? this.reasoning;
257- this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
258- }
259261
260262 /**
261263 * ProcessesHandles andprocessing updatesof reasoning info for thea message.
262- * @param {number} messageId - The ID of the message.
264+ *
263265 * @param {boolean}This mesChangedis -usually Truecalled ifby the message textprocessor when a message is changed.
264- * @param {Date} currentTime - The current time.
266+ *
267+ * @param {number} messageId - The ID of the message to process
268+ * @param {boolean} mesChanged - Whether the message has changed
269+ * @returns {Promise<void>}
265270 */
266271 async process(messageId, mesChanged, currentTime) {
267272 if (!this.reasoning && !this.#isHiddenisHiddenReasoningModel) return;
268-
269- this.updateReasoning();
270273
271- // Ensure the chat extra exists.
274+ // Ensure reasoning string is updated and regexes are applied correctly
272- if (!chat[messageId]['extra']) {
275+ const reasoningChanged = this.updateReasoning(messageId, null, { persist: true });
273- chat[messageId]['extra'] = {};
274- }
275- const extra = chat[messageId]['extra'];
276- const finalReasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
277- const reasoningChanged = extra['reasoning'] !== finalReasoning;
278- extra['reasoning'] = finalReasoning;
279276
280277 if ((this.#isHiddenisHiddenReasoningModel || reasoningChanged) && this.startTimestate === nullReasoningState.None) {
278+ this.state = ReasoningState.Thinking;
281279 this.startTime = this.initialTime;
282280 }
283281 if ((this.#isHiddenisHiddenReasoningModel || !reasoningChanged) && mesChanged && this.startTime !== null && this.endTimestate === nullReasoningState.Thinking) {
284282 this.endTime = currentTimenew Date();
285- await eventSource.emit(event_types.STREAM_REASONING_DONE, finalReasoning, () => this.getDuration());
283+ await this.finish(messageId);
286- }
287- await this.updateTime(messageId);
288- if (this.messageReasoningContentDom instanceof HTMLElement) {
289- const formattedReasoning = messageFormatting(finalReasoning, '', false, false, messageId, {}, true);
290- this.messageReasoningContentDom.innerHTML = formattedReasoning;
291- }
292- if (this.messageDom instanceof HTMLElement) {
293- this.messageDom.classList.add('reasoning');
294284 }
295285 }
296286
287+ /**
288+ * Completes the reasoning process for a message.
289+ *
290+ * Records the finish time if it was not set during streaming and updates the reasoning state.
291+ * Emits an event to signal the completion of reasoning and updates the DOM elements accordingly.
292+ *
293+ * @param {number} messageId - The ID of the message to complete reasoning for
294+ * @returns {Promise<void>}
295+ */
297296 async finish(messageId) {
297+ if (this.state === ReasoningState.None) return;
298+
298299 // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
299300 if (this.startTime !== null && this.endTime === null) {
300301 this.endTime = new Date();
301- const finalReasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
302+ }
302- await eventSource.emit(event_types.STREAM_REASONING_DONE, finalReasoning, () => this.getDuration());
303+
303- await this.updateTime(messageId);
304+ if (this.state === ReasoningState.Thinking) {
305+ this.state = this.#isHiddenReasoningModel ? ReasoningState.Hidden : ReasoningState.Done;
306+ this.updateReasoning(messageId, null, { persist: true });
307+ await eventSource.emit(event_types.STREAM_REASONING_DONE, this.reasoning, this.getDuration(), messageId, this.state);
308+ }
309+
310+ this.updateDom(messageId);
311+ }
312+
313+ /**
314+ * Updates the reasoning UI elements for a message.
315+ *
316+ * Toggles the CSS class, updates states, reasoning message, and duration.
317+ *
318+ * @param {number} messageId - The ID of the message to update
319+ */
320+ updateDom(messageId) {
321+ this.#checkDomElements(messageId);
322+
323+ // Main CSS class to show this message includes reasoning
324+ this.messageDom.classList.toggle('reasoning', this.state !== ReasoningState.None);
325+
326+ // Update states to the relevant DOM elements
327+ this.messageDom.dataset.state = this.state !== ReasoningState.None ? this.state : null;
328+ this.messageReasoningDetailsDom.dataset.state = this.state;
329+
330+ // Update the reasoning message
331+ const reasoning = power_user.trim_spaces ? this.reasoning.trim() : this.reasoning;
332+ const displayReasoning = messageFormatting(reasoning, '', false, false, messageId, {}, true);
333+ this.messageReasoningContentDom.innerHTML = displayReasoning;
334+
335+ // Update the reasoning duration in the UI
336+ this.#updateReasoningTimeUI();
337+ }
338+
339+ /**
340+ * Finds and caches reasoning-related DOM elements for the given message.
341+ *
342+ * @param {number} messageId - The ID of the message to cache the DOM elements for
343+ */
344+ #checkDomElements(messageId) {
345+ // Make sure we reset dom elements if we are checking for a different message (shouldn't happen, but be sure)
346+ if (this.messageDom !== null && this.messageDom.getAttribute('mesid') !== messageId.toString()) {
347+ this.messageDom = null;
348+ }
349+
350+ // Cache the DOM elements once
351+ if (this.messageDom === null) {
352+ this.messageDom = document.querySelector(`#chat .mes[mesid="${messageId}"]`);
353+ if (this.messageDom === null) throw new Error('message dom does not exist');
354+ }
355+ if (this.messageReasoningDetailsDom === null) {
356+ this.messageReasoningDetailsDom = this.messageDom.querySelector('.mes_reasoning_details');
357+ }
358+ if (this.messageReasoningContentDom === null) {
359+ this.messageReasoningContentDom = this.messageDom.querySelector('.mes_reasoning');
360+ }
361+ if (this.messageReasoningHeaderDom === null) {
362+ this.messageReasoningHeaderDom = this.messageDom.querySelector('.mes_reasoning_header_title');
304363 }
305364 }
306365
307366 /**
308367 * Updates the reasoning durationtime display in the UI.
309- * @param {number} messageId - The ID of the message
368+ *
310- * @param {object} [options={}] - Optional argument
369+ * Shows the duration in a human-readable format with a tooltip for exact seconds.
311- * @param {boolean} [options.forceEnd=false] - If true, there will be no "Thinking..." when no duration exists
370+ * Displays "Thinking..." if still processing, or a generic message otherwise.
312371 */
313- async updateTime(messageId, { forceEnd = false } = {}) {
372+ #updateReasoningTimeUI() {
373+ const element = this.messageReasoningHeaderDom;
314374 const duration = this.getDuration();
315- chat[messageId]['extra']['reasoning_duration'] = duration;
375+ if (duration) {
316- updateReasoningUI(this.messageDom, this.reasoning, duration, { forceEnd });
376+ const durationStr = moment.duration(duration).locale(getCurrentLocale()).humanize({ s: 50, ss: 3 });
377+ const secondsStr = moment.duration(duration).asSeconds();
378+ element.innerHTML = t`Thought for <span title="${secondsStr} seconds">${durationStr}</span>`;
379+ } else if (this.state === ReasoningState.Thinking) {
380+ element.textContent = t`Thinking...`;
381+ } else {
382+ element.textContent = t`Thought for some time`;
383+ }
317384 }
318385}
319386