Feat: Add toggle to exclude think/reason blocks from smooth streaming (#4849) * Feat: Add toggle to exclude think/reason blocks from smooth streaming * Fix: Adjust new setting name to properly reflect what it does * Sync data-i18n with text content * Add reasoning stream flags for Mistral, Claude and Google models * Update layout * Fix: Enhance reasoning handling in parseStreamData function --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

6c8eb4d9ac6b9e76fd5ed53ad6e2739f112131bf

Kristian Schlikow <Dakraid@users.noreply.github.com>

Signed
4 files changed, +61 -16Ignore whitespace
public/css/toggle-dependent.css+2 -5
@@ -477,14 +477,11 @@ body.expandMessageActions .mes .mes_buttons .extraMesButtonsHint {
477 display: none !important;477 display: none !important;
478}478}
479479
480#smooth_streaming:not(:checked)~#smooth_streaming_speed_control {480#smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_speed_control,
481#smooth_streaming_control:has(#smooth_streaming:not(:checked))~#smooth_streaming_no_think_control {
481 display: none;482 display: none;
482}483}
483484
484#smooth_streaming:checked~#smooth_streaming_speed_control {
485 display: block;
486}
487
488.mdhotkey_icon {485.mdhotkey_icon {
489 opacity: 0.6;486 opacity: 0.6;
490}487}
public/index.html+15 -9
@@ -5029,22 +5029,28 @@
5029 <small data-i18n="Clean-Up">Clean-Up</small>5029 <small data-i18n="Clean-Up">Clean-Up</small>
5030 </div>5030 </div>
5031 </div>5031 </div>
5032 <label class="checkbox_label flexWrap" for="smooth_streaming">5032 <label id="smooth_streaming_control" class="checkbox_label" for="smooth_streaming">
5033 <input id="smooth_streaming" type="checkbox" />5033 <input id="smooth_streaming" type="checkbox" />
5034 <div class="flex-container alignItemsBaseline">5034 <div class="flex-container alignItemsBaseline">
5035 <small data-i18n="Smooth Streaming">5035 <small data-i18n="Smooth Streaming">
5036 Smooth Streaming5036 Smooth Streaming
5037 </small>5037 </small>
5038 </div>5038 </div>
5039 <div id="smooth_streaming_speed_control" class="flexBasis100p wide100p">
5040 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
5041 <div class="slider_hint">
5042 <span data-i18n="Slow">Slow</span>
5043 <span></span>
5044 <span data-i18n="Fast">Fast</span>
5045 </div>
5046 </div>
5047 </label>5039 </label>
5040 <label id="smooth_streaming_no_think_control" class="checkbox_label" for="smooth_streaming_no_think" title="Bypass smooth streaming in reasoning blocks." data-i18n="[title]Bypass smooth streaming in reasoning blocks.">
5041 <input id="smooth_streaming_no_think" type="checkbox" />
5042 <small data-i18n="Exclude 'Thinking...'">
5043 Exclude 'Thinking...'
5044 </small>
5045 </label>
5046 <div id="smooth_streaming_speed_control" class="wide100p">
5047 <input type="range" id="smooth_streaming_speed" name="smooth_streaming_speed" min="0" max="100" step="10" value="50">
5048 <div class="slider_hint">
5049 <span data-i18n="Slow">Slow</span>
5050 <span></span>
5051 <span data-i18n="Fast">Fast</span>
5052 </div>
5053 </div>
5048 <label class="checkbox_label" for="stream_fade_in" title="Fade in streamed text when it appears, instead of it just popping in." data-i18n="[title]Fade in streamed text when it appears, instead of it just popping in">5054 <label class="checkbox_label" for="stream_fade_in" title="Fade in streamed text when it appears, instead of it just popping in." data-i18n="[title]Fade in streamed text when it appears, instead of it just popping in">
5049 <input id="stream_fade_in" type="checkbox" />5055 <input id="stream_fade_in" type="checkbox" />
5050 <small data-i18n="Stream Fade-In">Stream Fade-In</small>5056 <small data-i18n="Stream Fade-In">Stream Fade-In</small>
public/scripts/power-user.js+7 -0
@@ -139,6 +139,7 @@ export const power_user = {
139 chat_truncation: 100,139 chat_truncation: 100,
140 streaming_fps: 30,140 streaming_fps: 30,
141 smooth_streaming: false,141 smooth_streaming: false,
142 smooth_streaming_no_think: false,
142 smooth_streaming_speed: 50,143 smooth_streaming_speed: 50,
143 stream_fade_in: false,144 stream_fade_in: false,
144145
@@ -1745,6 +1746,7 @@ export async function loadPowerUserSettings(settings, data) {
1745 $('#streaming_fps_counter').val(power_user.streaming_fps);1746 $('#streaming_fps_counter').val(power_user.streaming_fps);
17461747
1747 $('#smooth_streaming').prop('checked', power_user.smooth_streaming);1748 $('#smooth_streaming').prop('checked', power_user.smooth_streaming);
1749 $('#smooth_streaming_no_think').prop('checked', power_user.smooth_streaming_no_think);
1748 $('#smooth_streaming_speed').val(power_user.smooth_streaming_speed);1750 $('#smooth_streaming_speed').val(power_user.smooth_streaming_speed);
17491751
1750 $('#stream_fade_in').prop('checked', power_user.stream_fade_in);1752 $('#stream_fade_in').prop('checked', power_user.stream_fade_in);
@@ -3550,6 +3552,11 @@ jQuery(() => {
3550 saveSettingsDebounced();3552 saveSettingsDebounced();
3551 });3553 });
35523554
3555 $('#smooth_streaming_no_think').on('input', function () {
3556 power_user.smooth_streaming_no_think = !!$(this).prop('checked');
3557 saveSettingsDebounced();
3558 });
3559
3553 $('#smooth_streaming_speed').on('input', function () {3560 $('#smooth_streaming_speed').on('input', function () {
3554 power_user.smooth_streaming_speed = Number($('#smooth_streaming_speed').val());3561 power_user.smooth_streaming_speed = Number($('#smooth_streaming_speed').val());
3555 saveSettingsDebounced();3562 saveSettingsDebounced();
public/scripts/sse-stream.js+37 -2
@@ -105,7 +105,7 @@ function getDelay(s) {
105/**105/**
106 * Parses the stream data and returns the parsed data and the chunk to be sent.106 * Parses the stream data and returns the parsed data and the chunk to be sent.
107 * @param {object} json The JSON data.107 * @param {object} json The JSON data.
108 * @returns {AsyncGenerator<{data: object, chunk: string}>} The parsed data and the chunk to be sent.108 * @returns {AsyncGenerator<{data: object, chunk: string, reasoning?: boolean}>} The parsed data and the chunk to be sent.
109 */109 */
110async function* parseStreamData(json) {110async function* parseStreamData(json) {
111 // Cohere111 // Cohere
@@ -133,6 +133,19 @@ async function* parseStreamData(json) {
133 }133 }
134 return;134 return;
135 }135 }
136 else if (typeof json.delta === 'object' && typeof json.delta.thinking === 'string') {
137 if (json.delta.thinking.length > 0) {
138 for (let i = 0; i < json.delta.thinking.length; i++) {
139 const str = json.delta.thinking[i];
140 yield {
141 data: { ...json, delta: { thinking: str } },
142 chunk: str,
143 reasoning: true,
144 };
145 }
146 }
147 return;
148 }
136 // MakerSuite149 // MakerSuite
137 else if (Array.isArray(json.candidates)) {150 else if (Array.isArray(json.candidates)) {
138 for (let i = 0; i < json.candidates.length; i++) {151 for (let i = 0; i < json.candidates.length; i++) {
@@ -159,9 +172,11 @@ async function* parseStreamData(json) {
159 candidateClone.content.parts[j].text = str;172 candidateClone.content.parts[j].text = str;
160 candidateClone.content.parts = [candidateClone.content.parts[j]];173 candidateClone.content.parts = [candidateClone.content.parts[j]];
161 const candidates = [candidateClone];174 const candidates = [candidateClone];
175 const reasoning = json.candidates[i].content.parts[j].thought ?? false;
162 yield {176 yield {
163 data: { ...json, candidates },177 data: { ...json, candidates },
164 chunk: str,178 chunk: str,
179 reasoning,
165 };180 };
166 }181 }
167 }182 }
@@ -237,6 +252,7 @@ async function* parseStreamData(json) {
237 yield {252 yield {
238 data: { ...json, choices },253 data: { ...json, choices },
239 chunk: str,254 chunk: str,
255 reasoning: true,
240 };256 };
241 }257 }
242 return;258 return;
@@ -252,6 +268,7 @@ async function* parseStreamData(json) {
252 yield {268 yield {
253 data: { ...json, choices },269 data: { ...json, choices },
254 chunk: str,270 chunk: str,
271 reasoning: true,
255 };272 };
256 }273 }
257 return;274 return;
@@ -269,6 +286,24 @@ async function* parseStreamData(json) {
269 }286 }
270 return;287 return;
271 }288 }
289 else if (Array.isArray(json.choices[0].delta.content) && json.choices[0].delta.content.length > 0) {
290 if (Array.isArray(json.choices[0].delta.content[0].thinking) && json.choices[0].delta.content[0].thinking.length > 0) {
291 if (typeof json.choices[0].delta.content[0].thinking[0].text === 'string' && json.choices[0].delta.content[0].thinking[0].text.length > 0) {
292 for (let j = 0; j < json.choices[0].delta.content[0].thinking[0].text.length; j++) {
293 const str = json.choices[0].delta.content[0].thinking[0].text[j];
294 const choiceClone = structuredClone(json.choices[0]);
295 choiceClone.delta.content[0].thinking[0].text = str;
296 const choices = [choiceClone];
297 yield {
298 data: { ...json, choices },
299 chunk: str,
300 reasoning: true,
301 };
302 }
303 return;
304 }
305 }
306 }
272 }307 }
273 else if (typeof json.choices[0].message === 'object') {308 else if (typeof json.choices[0].message === 'object') {
274 if (typeof json.choices[0].message.content === 'string' && json.choices[0].message.content.length > 0) {309 if (typeof json.choices[0].message.content === 'string' && json.choices[0].message.content.length > 0) {
@@ -317,7 +352,7 @@ export class SmoothEventSourceStream extends EventSourceStream {
317 }352 }
318353
319 for await (const parsed of parseStreamData(json)) {354 for await (const parsed of parseStreamData(json)) {
320 hasFocus && await delay(getDelay(lastStr));355 !(power_user.smooth_streaming_no_think && parsed.reasoning) && hasFocus && await delay(getDelay(lastStr));
321 controller.enqueue(new MessageEvent(event.type, { data: JSON.stringify(parsed.data) }));356 controller.enqueue(new MessageEvent(event.type, { data: JSON.stringify(parsed.data) }));
322 lastStr = parsed.chunk;357 lastStr = parsed.chunk;
323 }358 }