dsh-smooth-stream 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +9 -14
- package/README.md +9 -14
- package/lib/client.js +265 -68
- package/lib/client.js.map +1 -1
- package/package.json +7 -2
- package/src/client/TypewriterAssistantNodeView.tsx +226 -49
- package/src/client/teleprompterGlide.ts +64 -1
package/README.en.md
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.md)
|
|
4
4
|
|
|
5
|
-
[
|
|
6
|
-
|
|
7
|
-
**dsh-smooth-stream** is a [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) community plugin for **silky streaming** in the Web UI: arrival-tracking typewriter reveal, glide-in wraps, no flicker. It is not part of the official DeepSeek distribution.
|
|
5
|
+
**dsh-smooth-stream** brings fluid streaming rendering and silky scrolling to the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`) Web UI. Text, Markdown, code blocks, tables, and tool results appear as the reply arrives; as the content grows, the page follows along in one continuous visual rhythm.
|
|
8
6
|
|
|
9
7
|
Project homepage: <https://laplace-bit.github.io/dsh-smooth-stream/>
|
|
10
8
|
|
|
@@ -14,17 +12,12 @@ Left: default Web UI. Right: dsh-smooth-stream.
|
|
|
14
12
|
|
|
15
13
|

|
|
16
14
|
|
|
17
|
-
##
|
|
15
|
+
## Core experience
|
|
18
16
|
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **Conversation chrome stays fixed.** `Deep diving...`, the composer, and the to-bottom button never ride the message transform. Fast output and low-frame-rate catch-up cannot paint through the status row or disappear behind the composer.
|
|
24
|
-
- **One continuous spring owns motion.** The engine carries velocity and displacement between frames instead of repeatedly starting native smooth-scroll calls. Line wraps, code, tables, and growing tool rows converge along the same trajectory; completion lands on the natural floor and retires temporary state without a flash or overshoot rebound.
|
|
25
|
-
- **Reader input wins immediately.** A small upward wheel, touch, or keyboard gesture releases automatic follow. Ownership returns only after the reader actually reaches the bottom again.
|
|
26
|
-
- **Think respects the user's preference.** With auto-expand enabled it keeps the Harness disclosure interaction and collapses when thinking ends. With it disabled, collapsed reasoning can keep updating without fake height motion, and a manual toggle is not wrestled back by stream state.
|
|
27
|
-
- **Performance guards preserve the final position.** `prefers-reduced-motion` shows complete content without taking follow. Off-screen DOM commits pause under low FPS, then catch up under control and still finish exactly at the bottom.
|
|
17
|
+
- **Fluid rendering.** Text appears as it arrives while Markdown structure stays active, keeping headings, lists, code blocks, and tables readable throughout the stream.
|
|
18
|
+
- **Silky scrolling.** As the content grows, the page follows along one continuous scroll path and keeps the reader close to the generated output.
|
|
19
|
+
- **Consistent transitions.** Line wraps, code blocks, tables, and tool results use the same motion treatment, so text, reasoning, and tools flow together.
|
|
20
|
+
- **Adaptive cadence.** Reveal speed responds to arrival rate and pending content, staying measured for slow output and catching up with fast output.
|
|
28
21
|
|
|
29
22
|
## Install
|
|
30
23
|
|
|
@@ -89,7 +82,7 @@ dsh plugin --profile web update dsh-smooth-stream
|
|
|
89
82
|
## FAQ
|
|
90
83
|
|
|
91
84
|
**Is this an official DeepSeek plugin?**
|
|
92
|
-
No. It is
|
|
85
|
+
No. It is independently maintained, MIT-licensed software for the DeepSeek Harness (`dsh`) Web UI and is not affiliated with DeepSeek.
|
|
93
86
|
|
|
94
87
|
**How do I install a DeepSeek Harness plugin?**
|
|
95
88
|
Use the built-in plugin command: `dsh plugin --profile web add dsh-smooth-stream` from a dsh source checkout (see [Install](#install)).
|
|
@@ -100,6 +93,8 @@ Yes — `dsh-smooth-stream` is published to [npm](https://www.npmjs.com/package/
|
|
|
100
93
|
**Does it respect `prefers-reduced-motion`?**
|
|
101
94
|
Yes. With reduced motion enabled the finished text is shown at once and the plugin does not take over follow. If the frame rate drops below 30 fps while the reply is off-screen, reveal pauses and catches up later.
|
|
102
95
|
|
|
96
|
+
[](https://whyihaveyou.github.io/dsh-suite/)
|
|
97
|
+
|
|
103
98
|
## License
|
|
104
99
|
|
|
105
100
|
[MIT](LICENSE)
|
package/README.md
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.en.md) | 中文
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**dsh-smooth-stream** 是 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的社区插件,给 Web 对话做**丝滑流式渲染**:字跟着模型走、换行滑入、不闪。不是官方发行的一部分。
|
|
5
|
+
**dsh-smooth-stream** 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)Web UI 带来流畅的流式渲染和丝滑滚动。文字、Markdown、代码块、表格以及工具结果会随着输出自然呈现;内容逐步增长时,页面平稳跟随,整轮回复保持连贯的视觉节奏。
|
|
8
6
|
|
|
9
7
|
项目主页:<https://laplace-bit.github.io/dsh-smooth-stream/>
|
|
10
8
|
|
|
@@ -14,17 +12,12 @@
|
|
|
14
12
|
|
|
15
13
|

|
|
16
14
|
|
|
17
|
-
##
|
|
15
|
+
## 核心体验
|
|
18
16
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- **状态区域保持稳定。** `Deep diving...`、输入框和「滚动到底部」按钮不参与消息 transform;高速输出或低帧率下,正文也不会越过状态区域或藏到输入框后面。
|
|
24
|
-
- **滚动使用持续弹簧而不是反复启动原生平滑滚动。** 每帧保留速度和位移状态,换行、代码块、表格及工具卡片增高都沿同一轨迹收敛;结束时落在自然底部并安静撤销临时状态,不闪烁、不越位回弹。
|
|
25
|
-
- **用户输入拥有最高优先级。** 向上滚轮、触控拖动或键盘滚动会在轻微手势时立即解除自动跟随;只有用户真正回到底部后才重新接管。
|
|
26
|
-
- **Think 尊重用户设置。** 自动展开开启时沿用 Harness 的 disclosure 交互并在思考结束后收起;关闭时折叠内容持续更新但不引发虚假高度和上下闪动,手动展开也不会被流状态抢回。
|
|
27
|
-
- **性能保护不会破坏最终状态。** `prefers-reduced-motion` 直接显示完整内容且不接管跟随;低帧率且回复在屏外时暂停 DOM 提交,恢复后受控追赶,最终仍准确停在底部。
|
|
17
|
+
- **流畅渲染。** 文字边到边呈现,Markdown 结构持续更新,标题、列表、代码块和表格在流式过程中保持自然的阅读状态。
|
|
18
|
+
- **丝滑滚动。** 内容逐步变高时,页面沿连续的滚动轨迹平稳跟随,视线始终贴着正在生成的内容。
|
|
19
|
+
- **统一过渡。** 换行、代码块、表格和工具结果使用一致的过渡方式,正文、思考过程与工具输出衔接自然。
|
|
20
|
+
- **自适应节奏。** 渲染速度会根据输出速度和待显示内容调整,让慢速输出从容呈现,快速输出及时跟上。
|
|
28
21
|
|
|
29
22
|
## 安装
|
|
30
23
|
|
|
@@ -89,7 +82,7 @@ dsh plugin --profile web update dsh-smooth-stream
|
|
|
89
82
|
## 常见问题
|
|
90
83
|
|
|
91
84
|
**这是 DeepSeek 官方插件吗?**
|
|
92
|
-
|
|
85
|
+
不是。它是面向 DeepSeek Harness(`dsh`)Web UI 的独立维护项目,采用 MIT 许可证,和 DeepSeek 没有从属关系。
|
|
93
86
|
|
|
94
87
|
**dsh 插件怎么安装?**
|
|
95
88
|
用内置插件命令:在 dsh 源码目录运行 `dsh plugin --profile web add dsh-smooth-stream`(见[安装](#安装))。
|
|
@@ -100,6 +93,8 @@ dsh plugin --profile web update dsh-smooth-stream
|
|
|
100
93
|
**支持 `prefers-reduced-motion` 吗?**
|
|
101
94
|
支持。系统开启减少动态效果时直接显示完整文本、不接管跟随;帧率低于 30 fps 且回复在屏外时,揭示自动暂停、恢复后再补上。
|
|
102
95
|
|
|
96
|
+
[](https://whyihaveyou.github.io/dsh-suite/)
|
|
97
|
+
|
|
103
98
|
## 许可证
|
|
104
99
|
|
|
105
100
|
[MIT](LICENSE)
|
package/lib/client.js
CHANGED
|
@@ -23,27 +23,27 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
tag.textContent = css$1;
|
|
24
24
|
}
|
|
25
25
|
var TypewriterAssistantNodeView_module_css_default = {
|
|
26
|
-
"
|
|
27
|
-
"
|
|
26
|
+
"thinkBody": "I17U7q_thinkBody",
|
|
27
|
+
"body": "I17U7q_body",
|
|
28
28
|
"thinkRow": "I17U7q_thinkRow",
|
|
29
|
-
"
|
|
30
|
-
"disclosureRow": "I17U7q_disclosureRow",
|
|
29
|
+
"thinkLeading": "I17U7q_thinkLeading",
|
|
31
30
|
"disclosureLeading": "I17U7q_disclosureLeading",
|
|
32
|
-
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
33
31
|
"disclosureTitle": "I17U7q_disclosureTitle",
|
|
34
|
-
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
35
|
-
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
36
|
-
"thinkSummary": "I17U7q_thinkSummary",
|
|
37
32
|
"follow": "I17U7q_follow",
|
|
33
|
+
"disclosureRoot": "I17U7q_disclosureRoot",
|
|
34
|
+
"disclosureIconIdle": "I17U7q_disclosureIconIdle",
|
|
38
35
|
"thinkTitle": "I17U7q_thinkTitle",
|
|
39
|
-
"
|
|
40
|
-
"thinkBody": "I17U7q_thinkBody",
|
|
41
|
-
"thinkLeading": "I17U7q_thinkLeading",
|
|
36
|
+
"disclosureRow": "I17U7q_disclosureRow",
|
|
42
37
|
"disclosureChevronHover": "I17U7q_disclosureChevronHover",
|
|
43
|
-
"stopped": "I17U7q_stopped",
|
|
44
38
|
"visuallyHidden": "I17U7q_visuallyHidden",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
39
|
+
"thinkSeparator": "I17U7q_thinkSeparator",
|
|
40
|
+
"root": "I17U7q_root",
|
|
41
|
+
"think": "I17U7q_think",
|
|
42
|
+
"dsh-smooth-stream-think-sweep": "I17U7q_dsh-smooth-stream-think-sweep",
|
|
43
|
+
"thinkChevron": "I17U7q_thinkChevron",
|
|
44
|
+
"stopped": "I17U7q_stopped",
|
|
45
|
+
"thinkSummary": "I17U7q_thinkSummary",
|
|
46
|
+
"disclosureContent": "I17U7q_disclosureContent"
|
|
47
47
|
};
|
|
48
48
|
//#endregion
|
|
49
49
|
//#region src/client/AnimatedDisclosure.tsx
|
|
@@ -667,7 +667,15 @@ window.__ModuleLoader__.load({
|
|
|
667
667
|
}
|
|
668
668
|
return height;
|
|
669
669
|
}
|
|
670
|
-
|
|
670
|
+
/**
|
|
671
|
+
* The plugin can be reinjected without replacing the conversation DOM. Keep
|
|
672
|
+
* runway ownership in the page realm so a fresh bundle adopts the existing
|
|
673
|
+
* margin instead of treating it as host layout and adding another 48px.
|
|
674
|
+
*/
|
|
675
|
+
const FOLLOW_RUNWAYS_SYMBOL = Symbol.for("dsh-smooth-stream.follow-runways");
|
|
676
|
+
const followRunwayRegistry = globalThis;
|
|
677
|
+
const followRunways = followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] ?? /* @__PURE__ */ new WeakMap();
|
|
678
|
+
followRunwayRegistry[FOLLOW_RUNWAYS_SYMBOL] = followRunways;
|
|
671
679
|
const followPaintLimits = /* @__PURE__ */ new WeakMap();
|
|
672
680
|
function invalidatePaintLimit(port) {
|
|
673
681
|
followPaintLimits.delete(port);
|
|
@@ -681,9 +689,39 @@ window.__ModuleLoader__.load({
|
|
|
681
689
|
followRunways.delete(port);
|
|
682
690
|
invalidatePaintLimit(port);
|
|
683
691
|
}
|
|
692
|
+
function isLegacyRunway(value) {
|
|
693
|
+
if (value === "") return false;
|
|
694
|
+
const terms = [...value.matchAll(/([\d.]+)px/g)];
|
|
695
|
+
if (terms.length === 0 || value.replaceAll(/calc|px|[\d.+()\s]/g, "") !== "") return false;
|
|
696
|
+
return terms.every(([, raw]) => {
|
|
697
|
+
const px = Number(raw);
|
|
698
|
+
return Number.isFinite(px) && px >= 48 && Math.abs(px % 48) <= Number.EPSILON;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
/** Remove unowned runway residue written by v0.3.3 and earlier bundles. */
|
|
702
|
+
function migrateLegacyRunway(port, surfaces, status, composer) {
|
|
703
|
+
if (followRunways.has(port)) return;
|
|
704
|
+
let migrated = false;
|
|
705
|
+
if (status !== null && isLegacyRunway(status.style.marginTop)) {
|
|
706
|
+
status.style.marginTop = "";
|
|
707
|
+
migrated = true;
|
|
708
|
+
}
|
|
709
|
+
const last = surfaces.at(-1);
|
|
710
|
+
if (status === null && composer !== null && last !== void 0 && isLegacyRunway(last.style.marginBottom)) {
|
|
711
|
+
last.style.marginBottom = "";
|
|
712
|
+
migrated = true;
|
|
713
|
+
}
|
|
714
|
+
if (migrated) invalidatePaintLimit(port);
|
|
715
|
+
}
|
|
684
716
|
function ensureRunway(port, surfaces) {
|
|
685
717
|
const status = turnStatusOf(port);
|
|
686
718
|
const composer = port.querySelector("[data-composer-seat]");
|
|
719
|
+
migrateLegacyRunway(port, surfaces, status, composer);
|
|
720
|
+
const naturalHeight = Math.max(0, port.scrollHeight - runwayOffsetOf(port));
|
|
721
|
+
if (port.clientHeight <= 0 || naturalHeight <= port.clientHeight) {
|
|
722
|
+
restoreRunway(port);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
687
725
|
const target = status === null ? {
|
|
688
726
|
element: composer === null ? void 0 : surfaces.at(-1),
|
|
689
727
|
property: "marginBottom"
|
|
@@ -1258,6 +1296,8 @@ window.__ModuleLoader__.load({
|
|
|
1258
1296
|
}
|
|
1259
1297
|
/** Conservative fallback before the streaming Markdown tail has geometry. */
|
|
1260
1298
|
const PREDICTIVE_WRAP_FALLBACK_CHARS = 32;
|
|
1299
|
+
const STREAM_ANNOUNCEMENT_INTERVAL_MS = 800;
|
|
1300
|
+
const STREAM_ANNOUNCEMENT_MAX_CHARS = 320;
|
|
1261
1301
|
function approximateInlineWidth(text, emPx) {
|
|
1262
1302
|
let width = 0;
|
|
1263
1303
|
for (const char of text) if (/\s/u.test(char)) width += emPx * .33;
|
|
@@ -1265,36 +1305,189 @@ window.__ModuleLoader__.load({
|
|
|
1265
1305
|
else width += emPx;
|
|
1266
1306
|
return width;
|
|
1267
1307
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1308
|
+
function measurePendingTextGeometry(root, visibleText) {
|
|
1309
|
+
if (typeof document.createTreeWalker !== "function" || typeof NodeFilter === "undefined") return {
|
|
1310
|
+
root,
|
|
1311
|
+
visibleText,
|
|
1312
|
+
fontSize: 14,
|
|
1313
|
+
wrapThresholdWidth: null
|
|
1314
|
+
};
|
|
1274
1315
|
const rootRect = root.getBoundingClientRect();
|
|
1275
1316
|
const rootWidth = Math.max(0, rootRect.width, rootRect.right - rootRect.left, root.clientWidth);
|
|
1276
|
-
if (rootWidth <= 0) return
|
|
1317
|
+
if (rootWidth <= 0) return {
|
|
1318
|
+
root,
|
|
1319
|
+
visibleText,
|
|
1320
|
+
fontSize: 14,
|
|
1321
|
+
wrapThresholdWidth: null
|
|
1322
|
+
};
|
|
1277
1323
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
1278
1324
|
let tail = null;
|
|
1279
1325
|
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) if ((node.textContent ?? "").length > 0) tail = node;
|
|
1280
1326
|
const parent = tail?.parentElement ?? root;
|
|
1281
1327
|
const fontSize = Number.parseFloat(getComputedStyle(parent).fontSize) || 14;
|
|
1282
|
-
|
|
1283
|
-
|
|
1328
|
+
if (tail === null || typeof document.createRange !== "function") return {
|
|
1329
|
+
root,
|
|
1330
|
+
visibleText,
|
|
1331
|
+
fontSize,
|
|
1332
|
+
wrapThresholdWidth: rootWidth
|
|
1333
|
+
};
|
|
1284
1334
|
try {
|
|
1285
1335
|
const length = tail.textContent?.length ?? 0;
|
|
1286
|
-
if (length <= 0) return
|
|
1336
|
+
if (length <= 0) return {
|
|
1337
|
+
root,
|
|
1338
|
+
visibleText,
|
|
1339
|
+
fontSize,
|
|
1340
|
+
wrapThresholdWidth: rootWidth
|
|
1341
|
+
};
|
|
1287
1342
|
const range = document.createRange();
|
|
1288
1343
|
range.setStart(tail, Math.max(0, length - 1));
|
|
1289
1344
|
range.setEnd(tail, length);
|
|
1290
1345
|
const tailRect = range.getBoundingClientRect();
|
|
1291
1346
|
const contentRight = rootRect.right;
|
|
1292
|
-
if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return
|
|
1293
|
-
|
|
1347
|
+
if (!Number.isFinite(tailRect.right) || tailRect.right <= rootRect.left || contentRight <= rootRect.left) return {
|
|
1348
|
+
root,
|
|
1349
|
+
visibleText,
|
|
1350
|
+
fontSize,
|
|
1351
|
+
wrapThresholdWidth: rootWidth
|
|
1352
|
+
};
|
|
1353
|
+
return {
|
|
1354
|
+
root,
|
|
1355
|
+
visibleText,
|
|
1356
|
+
fontSize,
|
|
1357
|
+
wrapThresholdWidth: Math.max(0, contentRight - tailRect.right) + fontSize * .35
|
|
1358
|
+
};
|
|
1294
1359
|
} catch {
|
|
1295
|
-
return
|
|
1360
|
+
return {
|
|
1361
|
+
root,
|
|
1362
|
+
visibleText,
|
|
1363
|
+
fontSize,
|
|
1364
|
+
wrapThresholdWidth: rootWidth
|
|
1365
|
+
};
|
|
1296
1366
|
}
|
|
1297
1367
|
}
|
|
1368
|
+
/** Whether buffered source can reach a new visual line before it drains. */
|
|
1369
|
+
function pendingTextCanGrow(root, visibleText, pending, geometryRef) {
|
|
1370
|
+
if (pending === "") return false;
|
|
1371
|
+
if (/[\r\n]/u.test(pending)) return true;
|
|
1372
|
+
const pendingChars = [...pending];
|
|
1373
|
+
if (root === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
|
|
1374
|
+
let geometry = geometryRef.current;
|
|
1375
|
+
if (geometry?.root !== root || geometry.visibleText !== visibleText) {
|
|
1376
|
+
geometry = measurePendingTextGeometry(root, visibleText);
|
|
1377
|
+
geometryRef.current = geometry;
|
|
1378
|
+
}
|
|
1379
|
+
if (geometry.wrapThresholdWidth === null) return pendingChars.length >= PREDICTIVE_WRAP_FALLBACK_CHARS;
|
|
1380
|
+
return approximateInlineWidth(pending, geometry.fontSize) >= geometry.wrapThresholdWidth;
|
|
1381
|
+
}
|
|
1382
|
+
function announcementChunkEnd(source, start) {
|
|
1383
|
+
const hardEnd = Math.min(source.length, start + STREAM_ANNOUNCEMENT_MAX_CHARS);
|
|
1384
|
+
if (hardEnd === source.length) return hardEnd;
|
|
1385
|
+
const softStart = start + Math.floor(STREAM_ANNOUNCEMENT_MAX_CHARS * .6);
|
|
1386
|
+
for (let index = hardEnd - 1; index >= softStart; index -= 1) if (/[\s.,;:!?]/u.test(source[index] ?? "")) return index + 1;
|
|
1387
|
+
return hardEnd;
|
|
1388
|
+
}
|
|
1389
|
+
/** A commit-driven live region isolated from the visible Markdown subtree. */
|
|
1390
|
+
const StreamAnnouncement = (0, react.memo)(function StreamAnnouncement({ text, active }) {
|
|
1391
|
+
const [announcement, setAnnouncement] = (0, react.useState)({
|
|
1392
|
+
text: "",
|
|
1393
|
+
revision: 0,
|
|
1394
|
+
present: active
|
|
1395
|
+
});
|
|
1396
|
+
const sourceRef = (0, react.useRef)(text);
|
|
1397
|
+
const activeRef = (0, react.useRef)(active);
|
|
1398
|
+
const announcedOffsetRef = (0, react.useRef)(active ? 0 : text.length);
|
|
1399
|
+
const drainSourceRef = (0, react.useRef)(null);
|
|
1400
|
+
const timerRef = (0, react.useRef)(null);
|
|
1401
|
+
(0, react.useEffect)(() => {
|
|
1402
|
+
const clearTimer = () => {
|
|
1403
|
+
if (timerRef.current === null) return;
|
|
1404
|
+
clearTimeout(timerRef.current);
|
|
1405
|
+
timerRef.current = null;
|
|
1406
|
+
};
|
|
1407
|
+
const publishNext = (source) => {
|
|
1408
|
+
const start = Math.min(announcedOffsetRef.current, source.length);
|
|
1409
|
+
const end = announcementChunkEnd(source, start);
|
|
1410
|
+
if (end <= start) return false;
|
|
1411
|
+
announcedOffsetRef.current = end;
|
|
1412
|
+
setAnnouncement((previous) => ({
|
|
1413
|
+
text: source.slice(start, end),
|
|
1414
|
+
revision: previous.revision + 1,
|
|
1415
|
+
present: true
|
|
1416
|
+
}));
|
|
1417
|
+
return end < source.length;
|
|
1418
|
+
};
|
|
1419
|
+
const hideAfterLinger = () => {
|
|
1420
|
+
timerRef.current = setTimeout(() => {
|
|
1421
|
+
timerRef.current = null;
|
|
1422
|
+
if (activeRef.current) return;
|
|
1423
|
+
drainSourceRef.current = null;
|
|
1424
|
+
setAnnouncement((previous) => ({
|
|
1425
|
+
...previous,
|
|
1426
|
+
present: false
|
|
1427
|
+
}));
|
|
1428
|
+
}, STREAM_ANNOUNCEMENT_INTERVAL_MS);
|
|
1429
|
+
};
|
|
1430
|
+
const drainNext = () => {
|
|
1431
|
+
const source = drainSourceRef.current;
|
|
1432
|
+
if (source === null) return;
|
|
1433
|
+
if (!publishNext(source)) {
|
|
1434
|
+
hideAfterLinger();
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
timerRef.current = setTimeout(() => {
|
|
1438
|
+
timerRef.current = null;
|
|
1439
|
+
if (activeRef.current) return;
|
|
1440
|
+
drainNext();
|
|
1441
|
+
}, STREAM_ANNOUNCEMENT_INTERVAL_MS);
|
|
1442
|
+
};
|
|
1443
|
+
const scheduleLive = () => {
|
|
1444
|
+
if (timerRef.current !== null || announcedOffsetRef.current >= sourceRef.current.length) return;
|
|
1445
|
+
timerRef.current = setTimeout(() => {
|
|
1446
|
+
timerRef.current = null;
|
|
1447
|
+
if (!activeRef.current) return;
|
|
1448
|
+
const source = sourceRef.current;
|
|
1449
|
+
if (publishNext(source)) scheduleLive();
|
|
1450
|
+
}, STREAM_ANNOUNCEMENT_INTERVAL_MS);
|
|
1451
|
+
};
|
|
1452
|
+
const wasActive = activeRef.current;
|
|
1453
|
+
const previousSource = sourceRef.current;
|
|
1454
|
+
if (!(!active && !wasActive && drainSourceRef.current !== null) && (!text.startsWith(previousSource) || announcedOffsetRef.current > text.length)) announcedOffsetRef.current = 0;
|
|
1455
|
+
sourceRef.current = text;
|
|
1456
|
+
activeRef.current = active;
|
|
1457
|
+
if (active) {
|
|
1458
|
+
if (!wasActive) {
|
|
1459
|
+
clearTimer();
|
|
1460
|
+
drainSourceRef.current = null;
|
|
1461
|
+
setAnnouncement((previous) => ({
|
|
1462
|
+
text: "",
|
|
1463
|
+
revision: previous.revision + 1,
|
|
1464
|
+
present: true
|
|
1465
|
+
}));
|
|
1466
|
+
}
|
|
1467
|
+
scheduleLive();
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (!wasActive) {
|
|
1471
|
+
if (drainSourceRef.current === null) announcedOffsetRef.current = text.length;
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
clearTimer();
|
|
1475
|
+
drainSourceRef.current = text;
|
|
1476
|
+
drainNext();
|
|
1477
|
+
}, [active, text]);
|
|
1478
|
+
(0, react.useEffect)(() => () => {
|
|
1479
|
+
if (timerRef.current !== null) clearTimeout(timerRef.current);
|
|
1480
|
+
timerRef.current = null;
|
|
1481
|
+
}, []);
|
|
1482
|
+
if (!active && !announcement.present) return null;
|
|
1483
|
+
const activating = active && !activeRef.current;
|
|
1484
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1485
|
+
className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
|
|
1486
|
+
"aria-live": "polite",
|
|
1487
|
+
"aria-atomic": "true",
|
|
1488
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: activating ? "" : announcement.text }, announcement.revision)
|
|
1489
|
+
});
|
|
1490
|
+
});
|
|
1298
1491
|
/**
|
|
1299
1492
|
* Smooth streaming text arm. While the reply runs, the accumulated source is
|
|
1300
1493
|
* revealed through the smoother at a rate that tracks the model's arrival
|
|
@@ -1306,13 +1499,14 @@ window.__ModuleLoader__.load({
|
|
|
1306
1499
|
* drains, the settled full parse (KaTeX math, fence highlighting, file
|
|
1307
1500
|
* mentions) swaps in exactly once.
|
|
1308
1501
|
*/
|
|
1309
|
-
function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming,
|
|
1502
|
+
function AnimatedMarkdownText({ text, codeLabels, fileMentions, streaming, ownFollow, followSpeedCpsRef, followRevealScaleRef, onPredictiveChange, preset, shouldHoldBack }) {
|
|
1310
1503
|
const reduced = usePrefersReducedMotion();
|
|
1311
1504
|
const [typing, setTyping] = (0, react.useState)(streaming);
|
|
1312
1505
|
const localSpeedCpsRef = (0, react.useRef)(35);
|
|
1313
1506
|
const followRootRef = (0, react.useRef)(null);
|
|
1314
1507
|
const predictionSourceRef = (0, react.useRef)(null);
|
|
1315
1508
|
const predictionStateRef = (0, react.useRef)(false);
|
|
1509
|
+
const predictionGeometryRef = (0, react.useRef)(null);
|
|
1316
1510
|
const speedCpsRef = followSpeedCpsRef ?? localSpeedCpsRef;
|
|
1317
1511
|
const displayed = useSmoothStreamContent(text, {
|
|
1318
1512
|
enabled: typing && !reduced,
|
|
@@ -1324,11 +1518,22 @@ window.__ModuleLoader__.load({
|
|
|
1324
1518
|
});
|
|
1325
1519
|
const shown = reduced ? text : displayed;
|
|
1326
1520
|
const live = typing && !reduced;
|
|
1521
|
+
(0, react.useEffect)(() => {
|
|
1522
|
+
const root = followRootRef.current;
|
|
1523
|
+
if (root === null || typeof ResizeObserver === "undefined") return;
|
|
1524
|
+
const observer = new ResizeObserver(() => {
|
|
1525
|
+
if (predictionGeometryRef.current?.root === root) predictionGeometryRef.current = null;
|
|
1526
|
+
});
|
|
1527
|
+
observer.observe(root);
|
|
1528
|
+
return () => {
|
|
1529
|
+
observer.disconnect();
|
|
1530
|
+
};
|
|
1531
|
+
}, []);
|
|
1327
1532
|
(0, react.useLayoutEffect)(() => {
|
|
1328
1533
|
if (onPredictiveChange === void 0) return;
|
|
1329
1534
|
const pending = text.slice(shown.length);
|
|
1330
1535
|
const sourceChanged = predictionSourceRef.current !== text;
|
|
1331
|
-
const next = !live || !streaming || pending === "" ? false : sourceChanged ? pendingTextCanGrow(followRootRef.current, pending) : predictionStateRef.current;
|
|
1536
|
+
const next = !live || !streaming || pending === "" ? false : sourceChanged ? pendingTextCanGrow(followRootRef.current, shown, pending, predictionGeometryRef) : predictionStateRef.current;
|
|
1332
1537
|
predictionSourceRef.current = text;
|
|
1333
1538
|
predictionStateRef.current = next;
|
|
1334
1539
|
onPredictiveChange(next);
|
|
@@ -1347,11 +1552,7 @@ window.__ModuleLoader__.load({
|
|
|
1347
1552
|
text,
|
|
1348
1553
|
typing
|
|
1349
1554
|
]);
|
|
1350
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1351
|
-
className: TypewriterAssistantNodeView_module_css_default.visuallyHidden,
|
|
1352
|
-
"aria-live": "polite",
|
|
1353
|
-
children: text
|
|
1354
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1555
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1355
1556
|
active: live && ownFollow,
|
|
1356
1557
|
speedCpsRef,
|
|
1357
1558
|
revealScaleRef: followRevealScaleRef,
|
|
@@ -1363,7 +1564,7 @@ window.__ModuleLoader__.load({
|
|
|
1363
1564
|
codeLabels,
|
|
1364
1565
|
fileMentions: live ? void 0 : fileMentions
|
|
1365
1566
|
})
|
|
1366
|
-
})
|
|
1567
|
+
});
|
|
1367
1568
|
}
|
|
1368
1569
|
function imageLabels(t) {
|
|
1369
1570
|
return {
|
|
@@ -1396,7 +1597,7 @@ window.__ModuleLoader__.load({
|
|
|
1396
1597
|
* or the assistant node settling — not when the rest of the reply is
|
|
1397
1598
|
* still streaming.
|
|
1398
1599
|
*/
|
|
1399
|
-
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef,
|
|
1600
|
+
function AnimatedReasoning({ text, running, preset, thinkAutoExpand, shouldHoldBack, followSpeedCpsRef, followRevealScaleRef, t }) {
|
|
1400
1601
|
const reduced = usePrefersReducedMotion();
|
|
1401
1602
|
const [expanded, setExpanded] = (0, react.useState)(running && thinkAutoExpand);
|
|
1402
1603
|
const summaryRef = (0, react.useRef)(null);
|
|
@@ -1412,9 +1613,6 @@ window.__ModuleLoader__.load({
|
|
|
1412
1613
|
(0, react.useLayoutEffect)(() => {
|
|
1413
1614
|
if (thinkAutoExpand) setExpanded(running);
|
|
1414
1615
|
}, [running, thinkAutoExpand]);
|
|
1415
|
-
(0, react.useLayoutEffect)(() => {
|
|
1416
|
-
onExpandedChange?.(expanded);
|
|
1417
|
-
}, [expanded, onExpandedChange]);
|
|
1418
1616
|
(0, react.useEffect)(() => {
|
|
1419
1617
|
const element = summaryRef.current;
|
|
1420
1618
|
if (element === null) return;
|
|
@@ -1479,13 +1677,10 @@ window.__ModuleLoader__.load({
|
|
|
1479
1677
|
const rootPredictiveRef = (0, react.useRef)(false);
|
|
1480
1678
|
const previousReasoningTailRef = (0, react.useRef)(-1);
|
|
1481
1679
|
if (reasoningTailIndex !== previousReasoningTailRef.current) {
|
|
1482
|
-
rootPredictiveRef.current =
|
|
1680
|
+
rootPredictiveRef.current = false;
|
|
1483
1681
|
if (!reasoningOwnsSpeed) rootSpeedRef.current = 35;
|
|
1484
1682
|
previousReasoningTailRef.current = reasoningTailIndex;
|
|
1485
1683
|
}
|
|
1486
|
-
const updateReasoningExpanded = (0, react.useMemo)(() => (expanded) => {
|
|
1487
|
-
rootPredictiveRef.current = expanded;
|
|
1488
|
-
}, []);
|
|
1489
1684
|
const updateTextPrediction = (0, react.useMemo)(() => (predictive) => {
|
|
1490
1685
|
rootPredictiveRef.current = predictive;
|
|
1491
1686
|
}, []);
|
|
@@ -1514,6 +1709,7 @@ window.__ModuleLoader__.load({
|
|
|
1514
1709
|
throw new Error(t("image.serviceUnavailable"));
|
|
1515
1710
|
});
|
|
1516
1711
|
if (!(streaming || data.status === "interrupted" || data.blocks.some((block) => block.kind !== "tool-call"))) return null;
|
|
1712
|
+
const announcementText = data.blocks.filter((block) => block.kind === "text").map((block) => block.text).join("\n");
|
|
1517
1713
|
const rendered = [];
|
|
1518
1714
|
const last = data.blocks.length - 1;
|
|
1519
1715
|
let lastFollow = -1;
|
|
@@ -1531,7 +1727,6 @@ window.__ModuleLoader__.load({
|
|
|
1531
1727
|
codeLabels,
|
|
1532
1728
|
fileMentions: mentions,
|
|
1533
1729
|
streaming,
|
|
1534
|
-
announce: index === last,
|
|
1535
1730
|
ownFollow: !streaming && index === lastFollow,
|
|
1536
1731
|
followSpeedCpsRef: index === lastFollow ? rootSpeedRef : void 0,
|
|
1537
1732
|
followRevealScaleRef: index === lastFollow ? rootRevealScaleRef : void 0,
|
|
@@ -1549,7 +1744,6 @@ window.__ModuleLoader__.load({
|
|
|
1549
1744
|
shouldHoldBack,
|
|
1550
1745
|
followSpeedCpsRef: reasoningOwnsSpeed && index === last ? rootSpeedRef : void 0,
|
|
1551
1746
|
followRevealScaleRef: reasoningOwnsSpeed && index === last ? rootRevealScaleRef : void 0,
|
|
1552
|
-
onExpandedChange: index === reasoningTailIndex ? updateReasoningExpanded : void 0,
|
|
1553
1747
|
t
|
|
1554
1748
|
}, index));
|
|
1555
1749
|
break;
|
|
@@ -1578,11 +1772,14 @@ window.__ModuleLoader__.load({
|
|
|
1578
1772
|
}, index));
|
|
1579
1773
|
}
|
|
1580
1774
|
}
|
|
1581
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1775
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1582
1776
|
ref: guardRef,
|
|
1583
1777
|
className: TypewriterAssistantNodeView_module_css_default.root,
|
|
1584
1778
|
"data-streaming": streaming || void 0,
|
|
1585
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1779
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(StreamAnnouncement, {
|
|
1780
|
+
text: announcementText,
|
|
1781
|
+
active: streaming && !reduced
|
|
1782
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FollowHost, {
|
|
1586
1783
|
active: streaming && !reduced,
|
|
1587
1784
|
speedCpsRef: rootSpeedRef,
|
|
1588
1785
|
revealScaleRef: rootRevealScaleRef,
|
|
@@ -1594,7 +1791,7 @@ window.__ModuleLoader__.load({
|
|
|
1594
1791
|
children: t("message.stopped")
|
|
1595
1792
|
})]
|
|
1596
1793
|
})
|
|
1597
|
-
})
|
|
1794
|
+
})]
|
|
1598
1795
|
});
|
|
1599
1796
|
});
|
|
1600
1797
|
//#endregion
|
|
@@ -1967,31 +2164,31 @@ window.__ModuleLoader__.load({
|
|
|
1967
2164
|
tag.textContent = css;
|
|
1968
2165
|
}
|
|
1969
2166
|
var SmoothStreamCard_module_css_default = {
|
|
1970
|
-
"
|
|
1971
|
-
"hint": "XNGeRG_hint",
|
|
1972
|
-
"failed": "XNGeRG_failed",
|
|
1973
|
-
"field": "XNGeRG_field",
|
|
1974
|
-
"chevron": "XNGeRG_chevron",
|
|
1975
|
-
"headText": "XNGeRG_headText",
|
|
2167
|
+
"header": "XNGeRG_header",
|
|
1976
2168
|
"version": "XNGeRG_version",
|
|
1977
|
-
"body": "XNGeRG_body",
|
|
1978
|
-
"fieldHead": "XNGeRG_fieldHead",
|
|
1979
|
-
"toggle": "XNGeRG_toggle",
|
|
1980
2169
|
"readOnly": "XNGeRG_readOnly",
|
|
1981
|
-
"header": "XNGeRG_header",
|
|
1982
|
-
"pending": "XNGeRG_pending",
|
|
1983
|
-
"label": "XNGeRG_label",
|
|
1984
|
-
"update": "XNGeRG_update",
|
|
1985
|
-
"discard": "XNGeRG_discard",
|
|
1986
2170
|
"footer": "XNGeRG_footer",
|
|
2171
|
+
"label": "XNGeRG_label",
|
|
2172
|
+
"hint": "XNGeRG_hint",
|
|
2173
|
+
"fieldHead": "XNGeRG_fieldHead",
|
|
1987
2174
|
"failure": "XNGeRG_failure",
|
|
1988
|
-
"
|
|
1989
|
-
"
|
|
2175
|
+
"update": "XNGeRG_update",
|
|
2176
|
+
"field": "XNGeRG_field",
|
|
2177
|
+
"save": "XNGeRG_save",
|
|
2178
|
+
"pending": "XNGeRG_pending",
|
|
2179
|
+
"card": "XNGeRG_card",
|
|
1990
2180
|
"cardOpen": "XNGeRG_cardOpen",
|
|
1991
|
-
"
|
|
2181
|
+
"chevron": "XNGeRG_chevron",
|
|
2182
|
+
"toggle": "XNGeRG_toggle",
|
|
2183
|
+
"body": "XNGeRG_body",
|
|
2184
|
+
"name": "XNGeRG_name",
|
|
2185
|
+
"failed": "XNGeRG_failed",
|
|
1992
2186
|
"updateRow": "XNGeRG_updateRow",
|
|
1993
|
-
"
|
|
1994
|
-
"
|
|
2187
|
+
"discard": "XNGeRG_discard",
|
|
2188
|
+
"description": "XNGeRG_description",
|
|
2189
|
+
"headText": "XNGeRG_headText",
|
|
2190
|
+
"updateCopy": "XNGeRG_updateCopy",
|
|
2191
|
+
"chevronOpen": "XNGeRG_chevronOpen"
|
|
1995
2192
|
};
|
|
1996
2193
|
//#endregion
|
|
1997
2194
|
//#region src/client/SmoothStreamCard.tsx
|