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({
2223 isUser,2223 isUser,
2224 avatarImg,2224 avatarImg,
2225 bias,2225 bias,
2226 reasoning,
2227 reasoningDuration,
2228 isSystem,2226 isSystem,
2229 title,2227 title,
2230 timerValue,2228 timerValue,
@@ -2256,7 +2254,7 @@ function getMessageFromTemplate({
2256 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);2254 timerValue && mes.find('.mes_timer').attr('title', timerTitle).text(timerValue);
2257 bookmarkLink && updateBookmarkDisplay(mes);2255 bookmarkLink && updateBookmarkDisplay(mes);
22582256
2259 updateReasoningUI(mes, reasoning, reasoningDuration, { forceEnd: true });2257 updateReasoningUI(mes);
22602258
2261 if (power_user.timestamp_model_icon && extra?.api) {2259 if (power_user.timestamp_model_icon && extra?.api) {
2262 insertSVGIcon(mes, extra);2260 insertSVGIcon(mes, extra);
@@ -2279,7 +2277,7 @@ export function updateMessageBlock(messageId, message, { rerenderMessage = true
2279 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));2277 messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user, messageId, {}, false));
2280 }2278 }
22812279
2282 updateReasoningUI(messageElement, message?.extra?.reasoning, message?.extra?.reasoning_duration, { forceEnd: true });2280 updateReasoningUI(messageElement);
22832281
2284 addCopyToCodeBlocks(messageElement);2282 addCopyToCodeBlocks(messageElement);
2285 appendMediaToMessage(message, messageElement);2283 appendMediaToMessage(message, messageElement);
@@ -2440,7 +2438,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2440 false,2438 false,
2441 );2439 );
2442 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);2440 const bias = messageFormatting(mes.extra?.bias ?? '', '', false, false, -1, {}, false);
2443 const reasoning = messageFormatting(mes.extra?.reasoning ?? '', '', false, false, chat.indexOf(mes), {}, true);
2444 let bookmarkLink = mes?.extra?.bookmark_link ?? '';2441 let bookmarkLink = mes?.extra?.bookmark_link ?? '';
24452442
2446 let params = {2443 let params = {
@@ -2450,8 +2447,6 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
2450 isUser: mes.is_user,2447 isUser: mes.is_user,
2451 avatarImg: avatarImg,2448 avatarImg: avatarImg,
2452 bias: bias,2449 bias: bias,
2453 reasoning: reasoning,
2454 reasoningDuration: mes.extra?.reasoning_duration,
2455 isSystem: isSystem,2450 isSystem: isSystem,
2456 title: title,2451 title: title,
2457 bookmarkLink: bookmarkLink,2452 bookmarkLink: bookmarkLink,
@@ -3144,7 +3139,7 @@ class StreamingProcessor {
3144 this.messageLogprobs = [];3139 this.messageLogprobs = [];
3145 this.toolCalls = [];3140 this.toolCalls = [];
3146 // Initialize reasoning in its own handler3141 // Initialize reasoning in its own handler
3147 this.reasoningHandler = new ReasoningHandler(type, timeStarted);3142 this.reasoningHandler = new ReasoningHandler(timeStarted);
3148 }3143 }
31493144
3150 #checkDomElements(messageId) {3145 #checkDomElements(messageId) {
@@ -3154,7 +3149,7 @@ class StreamingProcessor {
3154 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');3149 this.messageTimerDom = this.messageDom?.querySelector('.mes_timer');
3155 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');3150 this.messageTokenCounterDom = this.messageDom?.querySelector('.tokenCounterDisplay');
3156 }3151 }
3157 this.reasoningHandler.checkDomElements(messageId);3152 this.reasoningHandler.updateDom(messageId);
3158 }3153 }
31593154
3160 #updateMessageBlockVisibility() {3155 #updateMessageBlockVisibility() {
@@ -3378,7 +3373,7 @@ class StreamingProcessor {
3378 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));3373 this.messageLogprobs.push(...(Array.isArray(logprobs) ? logprobs : [logprobs]));
3379 }3374 }
3380 // Get the updated reasoning string into the handler3375 // Get the updated reasoning string into the handler
3381 this.reasoningHandler.updateReasoning(state?.reasoning ?? '');3376 this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning ?? '');
3382 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);3377 await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
3383 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));3378 await sw.tick(async () => await this.onProgressStreaming(this.messageId, this.continueMessage + text));
3384 }3379 }
@@ -8727,7 +8722,7 @@ const swipe_right = () => {
8727 // resets the timer8722 // resets the timer
8728 swipeMessage.find('.mes_timer').html('');8723 swipeMessage.find('.mes_timer').html('');
8729 swipeMessage.find('.tokenCounterDisplay').text('');8724 swipeMessage.find('.tokenCounterDisplay').text('');
8730 updateReasoningUI(swipeMessage, null);8725 updateReasoningUI(swipeMessage);
8731 } else {8726 } else {
8732 //console.log('showing previously generated swipe candidate, or "..."');8727 //console.log('showing previously generated swipe candidate, or "..."');
8733 //console.log('onclick right swipe calling addOneMessage');8728 //console.log('onclick right swipe calling addOneMessage');
public/scripts/reasoning.js+186 -119
@@ -117,53 +117,20 @@ export function isHiddenReasoningModel() {
117}117}
118118
119/**119/**
120 * Updates the Reasoning UI.120 * Updates the Reasoning UI for a specific message
121 * @param {number|JQuery<HTMLElement>|HTMLElement} messageIdOrElement The message ID or the message element.121 * @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.
126 */122 */
127export function updateReasoningUI(messageIdOrElement, reasoning = null, reasoningDuration = null, { forceEnd = false } = {}) {123export 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 });
145}126}
146127
128
147/**129/**
148 * Updates the Reasoning controls130 * Enum for representing the state of reasoning
149 * @param {HTMLElement} element The element to update131 * @enum {string}
150 * @param {number?} duration The duration of the reasoning in milliseconds132 * @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
153 */133 */
154function 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} */
167export const ReasoningState = {134export const ReasoningState = {
168 None: 'none',135 None: 'none',
169 Thinking: 'thinking',136 Thinking: 'thinking',
@@ -173,16 +140,15 @@ export const ReasoningState = {
173140
174/**141/**
175 * Handles reasoning-specific logic and DOM updates for messages.142 * 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.
177 */144 */
178export class ReasoningHandler {145export class ReasoningHandler {
179 #isHidden;146 #isHiddenReasoningModel;
180147
181 /**148 /**
182 * @param {string} type - The streaming type149 * @param {Date?} [timeStarted=null] - When the generation started
183 * @param {Date} timeStarted - When the generation started
184 */150 */
185 constructor(type, timeStarted) {151 constructor(timeStarted = null) {
186 /** @type {ReasoningState} The current state of the reasoning process */152 /** @type {ReasoningState} The current state of the reasoning process */
187 this.state = ReasoningState.None;153 this.state = ReasoningState.None;
188 /** @type {string} The reasoning output */154 /** @type {string} The reasoning output */
@@ -192,28 +158,69 @@ export class ReasoningHandler {
192 /** @type {Date} When the reasoning ended */158 /** @type {Date} When the reasoning ended */
193 this.endTime = null;159 this.endTime = null;
194160
195 /** @type {string} Generation type (normal, continue, impersonation, etc) */
196 this.type = type;
197 /** @type {Date} Initial starting time of the generation */161 /** @type {Date} Initial starting time of the generation */
198 this.initialTime = timeStarted;162 this.initialTime = timeStarted ?? new Date();
199163
200 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */164 /** @type {boolean} True if the model supports reasoning, but hides the reasoning output */
201 this.#isHidden = isHiddenReasoningModel();165 this.#isHiddenReasoningModel = isHiddenReasoningModel();
202166
203 // Cached DOM elements for reasoning167 // Cached DOM elements for reasoning
204 /** @type {HTMLElement} Main message DOM element `.mes` */168 /** @type {HTMLElement} Main message DOM element `.mes` */
205 this.messageDom = null;169 this.messageDom = null;
206 /** @type {HTMLElement} Reasoning details DOM element `.mes_reasoning_details` */170 /** @type {HTMLElement} Reasoning details DOM element `.mes_reasoning_details` */
207 this.messageReasoningDetailsDom = null;171 this.messageReasoningDetailsDom = null;
208 /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning_content` */172 /** @type {HTMLElement} Reasoning content DOM element `.mes_reasoning` */
209 this.messageReasoningContentDom = null;173 this.messageReasoningContentDom = null;
210 /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header` */174 /** @type {HTMLElement} Reasoning header DOM element `.mes_reasoning_header_title` */
211 this.messageReasoningHeaderDom = null;175 this.messageReasoningHeaderDom = null;
212 }176 }
213177
214 /**178 /**
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 /**
215 * Gets the duration of the reasoning in milliseconds.221 * 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
217 */224 */
218 getDuration() {225 getDuration() {
219 if (this.startTime && this.endTime) {226 if (this.startTime && this.endTime) {
@@ -223,97 +230,157 @@ export class ReasoningHandler {
223 }230 }
224231
225 /**232 /**
226 * Finds and caches reasoning-related DOM elements for the given message.233 * Updates the reasoning text/string for a message.
227 * @param {number} messageId The message ID234 *
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
228 */240 */
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 once246 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 state252 // 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);
253 }259 }
254260
255 updateReasoning(reasoning = null) {
256 reasoning = reasoning ?? this.reasoning;
257 this.reasoning = getRegexedString(reasoning ?? '', regex_placement.REASONING);
258 }
259261
260 /**262 /**
261 * Processes and updates reasoning info for the message.263 * Handles processing of reasoning for a message.
262 * @param {number} messageId - The ID of the message.264 *
263 * @param {boolean} mesChanged - True if the message text changed.265 * This is usually called by the message processor 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>}
265 */270 */
266 async process(messageId, mesChanged, currentTime) {271 async process(messageId, mesChanged) {
267 if (!this.reasoning && !this.#isHidden) return;272 if (!this.reasoning && !this.#isHiddenReasoningModel) 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
280 if ((this.#isHidden || reasoningChanged) && this.startTime === null) {277 if ((this.#isHiddenReasoningModel || reasoningChanged) && this.state === ReasoningState.None) {
278 this.state = ReasoningState.Thinking;
281 this.startTime = this.initialTime;279 this.startTime = this.initialTime;
282 }280 }
283 if ((this.#isHidden || !reasoningChanged) && mesChanged && this.startTime !== null && this.endTime === null) {281 if ((this.#isHiddenReasoningModel || !reasoningChanged) && mesChanged && this.state === ReasoningState.Thinking) {
284 this.endTime = currentTime;282 this.endTime = new 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');
294 }284 }
295 }285 }
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 */
297 async finish(messageId) {296 async finish(messageId) {
297 if (this.state === ReasoningState.None) return;
298
298 // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming299 // Make sure the finish time is recorded if a reasoning was in process and it wasn't ended correctly during streaming
299 if (this.startTime !== null && this.endTime === null) {300 if (this.startTime !== null && this.endTime === null) {
300 this.endTime = new Date();301 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');
304 }363 }
305 }364 }
306365
307 /**366 /**
308 * Updates the reasoning duration in the UI.367 * Updates the reasoning time display in the UI.
309 * @param {number} messageId - The ID of the message368 *
310 * @param {object} [options={}] - Optional argument369 * 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 exists370 * Displays "Thinking..." if still processing, or a generic message otherwise.
312 */371 */
313 async updateTime(messageId, { forceEnd = false } = {}) {372 #updateReasoningTimeUI() {
373 const element = this.messageReasoningHeaderDom;
314 const duration = this.getDuration();374 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 }
317 }384 }
318}385}
319386