@max-null/dsh-chat-rail 0.6.0 → 0.6.2
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.
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -15,12 +15,11 @@ window.__ModuleLoader__.load({
|
|
|
15
15
|
*
|
|
16
16
|
* Two deliberate fixes over the dsh-chat-timeline reference:
|
|
17
17
|
*
|
|
18
|
-
* 1.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* expands — no "panel first, scrollbar later" lag.
|
|
18
|
+
* 1. POSITION ANCHOR. The rail's right edge follows the conversation column
|
|
19
|
+
* (`[data-conversation-scroll]`), not the viewport: the official right
|
|
20
|
+
* sidebar pushes the conversation narrower, and the rail must travel with
|
|
21
|
+
* it. `--dsh-sidebar-width` reads 0px on the current kernel, so a CSS-only
|
|
22
|
+
* offset would pin the rail underneath that sidebar.
|
|
24
23
|
*
|
|
25
24
|
* 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)
|
|
26
25
|
* lines — nearly invisible on a light background. Here the rail keeps a
|
|
@@ -94,19 +93,20 @@ window.__ModuleLoader__.load({
|
|
|
94
93
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
95
94
|
}
|
|
96
95
|
const css = [
|
|
97
|
-
".crl_nav{user-select:none;z-index:100;position:fixed;right:
|
|
98
|
-
"
|
|
99
|
-
".crl_nav.
|
|
96
|
+
".crl_nav{user-select:none;z-index:100;position:fixed;right:3px;top:calc((100vh - var(--dsh-sidebar-height,0px)) / 2);transform:translateY(-50%);width:36px;max-height:min(60vh,420px,calc(100vh - var(--dsh-sidebar-height,0px) - 40px));display:flex;flex-direction:column;align-items:center;box-sizing:border-box;padding:0 0 10px;border-radius:18px;overflow-y:hidden;overflow-x:hidden;background:rgba(255,255,255,.55);border:1px solid rgba(0,0,0,.07);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);scrollbar-width:none;transition:width .25s cubic-bezier(.4,0,.2,1),right .25s cubic-bezier(.4,0,.2,1),top .25s cubic-bezier(.4,0,.2,1),background .2s ease,border-color .2s ease,box-shadow .2s ease}",
|
|
97
|
+
".crl_list{display:flex;flex-direction:column;align-items:center;width:100%;min-height:0;flex:1 1 auto;position:relative;overflow-y:hidden;overflow-x:hidden;scrollbar-width:none}",
|
|
98
|
+
"body[data-ds-dark-theme] .crl_nav,[data-theme='dark'] .crl_nav,.dark .crl_nav{background:rgba(28,28,32,.6);border-color:rgba(255,255,255,.09)}",
|
|
99
|
+
".crl_nav.crl_show{width:280px;align-items:stretch;background:rgba(255,255,255,.94);border-color:rgba(0,0,0,.08);box-shadow:0 10px 30px rgba(0,0,0,.10),0 2px 8px rgba(0,0,0,.05)}",
|
|
100
100
|
"body[data-ds-dark-theme] .crl_nav.crl_show,[data-theme='dark'] .crl_nav.crl_show,.dark .crl_nav.crl_show{background:rgba(28,28,32,.96);border-color:rgba(255,255,255,.09);box-shadow:0 10px 30px rgba(0,0,0,.5),0 2px 8px rgba(0,0,0,.28)}",
|
|
101
|
-
".crl_nav::-webkit-scrollbar{width:
|
|
102
|
-
".
|
|
101
|
+
".crl_nav::-webkit-scrollbar,.crl_list::-webkit-scrollbar{width:0;height:0}",
|
|
102
|
+
".crl_show .crl_list{overflow-y:auto;align-items:stretch}",
|
|
103
103
|
".crl_loading{position:sticky;top:0;z-index:2;flex-shrink:0;width:100%;padding:4px 0;font-size:10px;line-height:1;text-align:center;color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.5)));background:inherit;pointer-events:none}",
|
|
104
104
|
".crl_loading::before{content:\"\";display:inline-block;width:8px;height:8px;margin-right:4px;border:1.5px solid var(--dsw-alias-state-business-primary,#4d6bfe);border-top-color:transparent;border-radius:50%;vertical-align:-1px;animation:crl-spin .8s linear infinite}",
|
|
105
105
|
".crl_loading .crl_loadingLabel{display:none}",
|
|
106
106
|
".crl_show .crl_loading .crl_loadingLabel{display:inline}",
|
|
107
107
|
"@keyframes crl-spin{to{transform:rotate(360deg)}}",
|
|
108
|
-
".crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:
|
|
109
|
-
".crl_show .crl_item{padding:0 14px}",
|
|
108
|
+
".crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:center;align-items:center;width:100%;box-sizing:border-box;padding:0 9px;line-height:20px;background:none;border:none;font:inherit;text-align:left;color:rgba(0,0,0,.68);transition:padding .25s cubic-bezier(.4,0,.2,1),color .15s ease}",
|
|
109
|
+
".crl_show .crl_item{justify-content:flex-end;padding:0 14px}",
|
|
110
110
|
".crl_item:hover{color:rgba(0,0,0,.95)}",
|
|
111
111
|
".crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
112
112
|
"body[data-ds-dark-theme] .crl_item,[data-theme='dark'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}",
|
|
@@ -132,7 +132,12 @@ window.__ModuleLoader__.load({
|
|
|
132
132
|
"body[data-ds-dark-theme] .crl_line,[data-theme='dark'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}",
|
|
133
133
|
"body[data-ds-dark-theme] .crl_item:hover .crl_line,[data-theme='dark'] .crl_item:hover .crl_line,.dark .crl_item:hover .crl_line{background-color:rgba(255,255,255,.95)}",
|
|
134
134
|
"body[data-ds-dark-theme] .crl_item.crl_active .crl_line,[data-theme='dark'] .crl_item.crl_active .crl_line,.dark .crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
135
|
-
".crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:
|
|
135
|
+
".crl_tip{position:fixed;z-index:200;max-width:360px;max-height:70vh;overflow-y:auto;padding:10px 12px;font-size:12px;line-height:1.6;color:var(--dsw-alias-label-primary,var(--text-primary,rgba(0,0,0,.85)));background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(255,255,255,.97)));border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)));border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16);white-space:pre-wrap;word-break:break-word;pointer-events:auto}",
|
|
136
|
+
".crl_tipQaWrap{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));display:flex;flex-direction:column;gap:4px;pointer-events:auto}",
|
|
137
|
+
".crl_tipQa{display:flex;align-items:center;gap:6px;width:100%;box-sizing:border-box;padding:5px 8px;border:none;border-radius:7px;background:rgba(77,107,254,.08);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .15s ease}",
|
|
138
|
+
".crl_tipQa:hover{background:rgba(77,107,254,.16)}",
|
|
139
|
+
".crl_tipQaIcon{flex:none;display:block;color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
140
|
+
".crl_tipQaText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
136
141
|
"body[data-ds-dark-theme] .crl_tip,[data-theme='dark'] .crl_tip,.dark .crl_tip{background:var(--dsw-alias-surface-raised,var(--bg-elevated,rgba(28,28,32,.97)));border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}",
|
|
137
142
|
".crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}",
|
|
138
143
|
".crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}",
|
|
@@ -156,14 +161,16 @@ window.__ModuleLoader__.load({
|
|
|
156
161
|
".crl_item.crl_favItem .crl_line{background-color:#ffd166}",
|
|
157
162
|
"body[data-ds-dark-theme] .crl_item.crl_favItem .crl_line,[data-theme='dark'] .crl_item.crl_favItem .crl_line,.dark .crl_item.crl_favItem .crl_line{background-color:#ffd166}",
|
|
158
163
|
".crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}",
|
|
159
|
-
".crl_favToggle{
|
|
164
|
+
".crl_favToggle{flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:34px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:0;background:transparent;border-bottom:1px solid rgba(0,0,0,.07);color:rgba(0,0,0,.42);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease,margin .25s cubic-bezier(.4,0,.2,1),left .25s cubic-bezier(.4,0,.2,1)}",
|
|
160
165
|
".crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}",
|
|
161
166
|
".crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}",
|
|
162
|
-
".crl_show .crl_favToggle{width:
|
|
167
|
+
".crl_show .crl_favToggle{width:100%;height:28px;margin:0 0 6px;padding:0 12px;border-radius:0;justify-content:flex-start;text-align:left;border-bottom:1px solid rgba(0,0,0,.07)}",
|
|
163
168
|
".crl_favToggleLabel{display:none}",
|
|
164
169
|
".crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}",
|
|
165
|
-
"body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}",
|
|
170
|
+
"body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4);border-bottom-color:rgba(255,255,255,.1)}",
|
|
166
171
|
"body[data-ds-dark-theme] .crl_favToggle:hover,[data-theme='dark'] .crl_favToggle:hover,.dark .crl_favToggle:hover{background:rgba(255,255,255,.1);color:rgba(255,255,255,.85)}",
|
|
172
|
+
"body[data-ds-dark-theme] .crl_show .crl_favToggle,[data-theme='dark'] .crl_show .crl_favToggle,.dark .crl_show .crl_favToggle{border-bottom-color:rgba(255,255,255,.1)}",
|
|
173
|
+
".crl_nav.crl_navHidden{opacity:0 !important;pointer-events:none !important}",
|
|
167
174
|
"body[data-ds-dark-theme] .crl_favToggle.crl_on,[data-theme='dark'] .crl_favToggle.crl_on,.dark .crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.18)}",
|
|
168
175
|
"@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}",
|
|
169
176
|
".crlSetCard{list-style:none;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s;cursor:pointer}",
|
|
@@ -190,6 +197,14 @@ window.__ModuleLoader__.load({
|
|
|
190
197
|
* CSS module 类名(如 hkplfa_slot,hash 随构建内容漂移)。 */
|
|
191
198
|
const HIDE_OFFICIAL_CSS = "nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}";
|
|
192
199
|
const HIDE_OFFICIAL_STYLE_ID = "data-crl-hide-official";
|
|
200
|
+
/**
|
|
201
|
+
* `data-plugin` 归属标记:DSH 的 client 模块系统只把**没有** `data-plugin` 的 `<style>`
|
|
202
|
+
* 认领给正在 materialize 的模块(`claimStyles`),而 HMR 重载时按
|
|
203
|
+
* `data-plugin === 模块 id` 逐字匹配删除。裸注入的 style 会被任意模块认领走,
|
|
204
|
+
* 随后随那个模块的一次重载被物理删除——元素还在、样式消失(悬浮球样式事故的机制)。
|
|
205
|
+
* 这里用一个独立于模块 id 的稳定值:既不进别人的账,也不随本模块的进出被清掉。
|
|
206
|
+
*/
|
|
207
|
+
const HIDE_OFFICIAL_PLUGIN_TAG = "dsh-chat-rail-hide-official";
|
|
193
208
|
/** 按设置同步「屏蔽官方 TurnNavigator」样式(showOfficial=true=官方模式:不屏蔽)。 */
|
|
194
209
|
function syncOfficialHide(showOfficial) {
|
|
195
210
|
const el = document.head.querySelector(`style[${HIDE_OFFICIAL_STYLE_ID}]`);
|
|
@@ -200,10 +215,19 @@ window.__ModuleLoader__.load({
|
|
|
200
215
|
if (el === null) {
|
|
201
216
|
const tag = document.createElement("style");
|
|
202
217
|
tag.setAttribute(HIDE_OFFICIAL_STYLE_ID, "");
|
|
218
|
+
tag.setAttribute("data-plugin", HIDE_OFFICIAL_PLUGIN_TAG);
|
|
203
219
|
tag.textContent = HIDE_OFFICIAL_CSS;
|
|
204
220
|
document.head.appendChild(tag);
|
|
205
|
-
}
|
|
221
|
+
} else if (el.textContent !== HIDE_OFFICIAL_CSS) el.textContent = HIDE_OFFICIAL_CSS;
|
|
206
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* 会话区剩余宽度低于此值时隐藏导航条(px)。
|
|
225
|
+
*
|
|
226
|
+
* 36px 是 rail 自身宽度,留 ~220px 给消息区——再窄时指示点已无参考意义。
|
|
227
|
+
*/
|
|
228
|
+
const RAIL_MIN_SPACE = 260;
|
|
229
|
+
/** 导航条隐藏类(空间不足时由 React 加上;用类而非内联 display,避免与官方模式冲突)。 */
|
|
230
|
+
const NAV_HIDDEN_CLASS = "crl_navHidden";
|
|
207
231
|
/** 移除所有已注入的行内收藏/填充按钮(官方模式下的回溯清理)。 */
|
|
208
232
|
function clearRowActions() {
|
|
209
233
|
for (const row of document.querySelectorAll("[data-crl-actions=\"1\"]")) {
|
|
@@ -220,16 +244,20 @@ window.__ModuleLoader__.load({
|
|
|
220
244
|
/** No-op subscribe for useSyncExternalStore when the scope is not yet bound. */
|
|
221
245
|
const NOOP_SUB = (() => () => {});
|
|
222
246
|
const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
|
|
223
|
-
if (typeof document !== "undefined"
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
247
|
+
if (typeof document !== "undefined") {
|
|
248
|
+
const existing = document.head.querySelector(`style[data-plugin-css="${STYLE_ID}"]`);
|
|
249
|
+
if (existing === null) {
|
|
250
|
+
const tag = document.createElement("style");
|
|
251
|
+
tag.dataset.plugin = "dsh-chat-rail";
|
|
252
|
+
tag.dataset.pluginCss = STYLE_ID;
|
|
253
|
+
tag.textContent = css;
|
|
254
|
+
document.head.appendChild(tag);
|
|
255
|
+
} else if (existing.textContent !== css) existing.textContent = css;
|
|
229
256
|
}
|
|
230
257
|
const S = {
|
|
231
258
|
nav: "crl_nav",
|
|
232
259
|
navShow: "crl_show",
|
|
260
|
+
list: "crl_list",
|
|
233
261
|
item: "crl_item",
|
|
234
262
|
itemActive: "crl_active",
|
|
235
263
|
title: "crl_title",
|
|
@@ -241,6 +269,10 @@ window.__ModuleLoader__.load({
|
|
|
241
269
|
loading: "crl_loading",
|
|
242
270
|
loadingLabel: "crl_loadingLabel",
|
|
243
271
|
tip: "crl_tip",
|
|
272
|
+
tipQaWrap: "crl_tipQaWrap",
|
|
273
|
+
tipQa: "crl_tipQa",
|
|
274
|
+
tipQaIcon: "crl_tipQaIcon",
|
|
275
|
+
tipQaText: "crl_tipQaText",
|
|
244
276
|
tipImgs: "crl_tipImgs",
|
|
245
277
|
tipImgWrap: "crl_tipImgWrap",
|
|
246
278
|
tipImg: "crl_tipImg",
|
|
@@ -612,11 +644,89 @@ window.__ModuleLoader__.load({
|
|
|
612
644
|
out.sort((a, b) => a.seq - b.seq);
|
|
613
645
|
return out;
|
|
614
646
|
}
|
|
615
|
-
/**
|
|
647
|
+
/** 任一数据平面的节点集合(与 collectFromNodes 同一套取值规则)。 */
|
|
648
|
+
function nodeValuesOf(snapshot) {
|
|
649
|
+
const store = snapshot?.nodes;
|
|
650
|
+
if (store !== void 0 && typeof store.values === "function") return [...store.values()];
|
|
651
|
+
const chat = snapshot?.chat;
|
|
652
|
+
if (chat?.nodes === void 0) return [];
|
|
653
|
+
return [...chat.nodes.values()];
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* 「提问&回答」也要能被导航。
|
|
657
|
+
*
|
|
658
|
+
* 它**不是** chat node:`ask_user_question` 是助手消息里的一个 tool-call 块,
|
|
659
|
+
* 用户的答案落在同一个 callId 的 `tool/result`(实测会话日志:有 `tool/call
|
|
660
|
+
* name=ask_user_question` 与对应结果,**没有任何带问答痕迹的 user/message**)。
|
|
661
|
+
* 所以 `railMessageOfNode` 的 `kind==='user'` 判定永远覆盖不到它。
|
|
662
|
+
*
|
|
663
|
+
* 但官方给工具调用也设了 DOM 锚点(`ui-tool/ToolCallTree.tsx:36` →
|
|
664
|
+
* `data-chat-anchor-key={`call:${callId}`}`),所以**可跳转**——只要把
|
|
665
|
+
* 助手节点里的问答块收集成 rail 条目即可。
|
|
666
|
+
*
|
|
667
|
+
* 范围:只覆盖已加载的 chat node(不做 loadThrough 拉取未加载历史)——
|
|
668
|
+
* 用户 2026-09-14 拍板。
|
|
669
|
+
*
|
|
670
|
+
* @param snapshot - 当前 chat node 快照(任一平面)。
|
|
671
|
+
* @returns 按 seq 升序的问答条目。
|
|
672
|
+
*/
|
|
673
|
+
function collectQaFromNodes(snapshot) {
|
|
674
|
+
const out = [];
|
|
675
|
+
for (const node of nodeValuesOf(snapshot)) {
|
|
676
|
+
if (node === null || typeof node !== "object") continue;
|
|
677
|
+
const n = node;
|
|
678
|
+
if (n.kind !== "tool-call") continue;
|
|
679
|
+
const root = n.data?.root;
|
|
680
|
+
if (root === null || typeof root !== "object") continue;
|
|
681
|
+
const r = root;
|
|
682
|
+
const callId = typeof r.callId === "string" ? r.callId : "";
|
|
683
|
+
if (callId === "") continue;
|
|
684
|
+
if ((typeof r.name === "string" ? r.name : typeof r.call?.name === "string" ? r.call.name : "") !== "ask_user_question") continue;
|
|
685
|
+
const asked = questionsTextOf(typeof r.argsRaw === "string" ? r.argsRaw : typeof r.call?.argsRaw === "string" ? r.call.argsRaw : "");
|
|
686
|
+
if (asked === "") continue;
|
|
687
|
+
out.push({
|
|
688
|
+
seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
|
|
689
|
+
time: typeof r.time === "number" ? r.time : 0,
|
|
690
|
+
text: asked,
|
|
691
|
+
hasImage: false,
|
|
692
|
+
key: typeof n.key === "string" ? n.key : void 0,
|
|
693
|
+
anchor: `call:${callId}`,
|
|
694
|
+
qa: true
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
698
|
+
return out;
|
|
699
|
+
}
|
|
700
|
+
/** 问答条目的预览文本:取 `arguments` JSON 里的问题标题(多问用 ` / ` 连)。 */
|
|
701
|
+
function questionsTextOf(argsRaw) {
|
|
702
|
+
if (typeof argsRaw !== "string" || argsRaw.trim() === "") return "";
|
|
703
|
+
let parsed;
|
|
704
|
+
try {
|
|
705
|
+
parsed = JSON.parse(argsRaw);
|
|
706
|
+
} catch {
|
|
707
|
+
return "";
|
|
708
|
+
}
|
|
709
|
+
const list = parsed?.questions;
|
|
710
|
+
if (!Array.isArray(list)) return "";
|
|
711
|
+
const parts = [];
|
|
712
|
+
for (const q of list) {
|
|
713
|
+
if (q === null || typeof q !== "object") continue;
|
|
714
|
+
const o = q;
|
|
715
|
+
const text = typeof o.question === "string" && o.question !== "" ? o.question : typeof o.header === "string" ? o.header : "";
|
|
716
|
+
if (text !== "") parts.push(text);
|
|
717
|
+
}
|
|
718
|
+
return parts.join(" / ").trim().slice(0, 80);
|
|
719
|
+
}
|
|
720
|
+
/** Resolve the DOM anchor key (`data-chat-anchor-key`) for one rail message. */
|
|
616
721
|
function anchorKeyOf(m) {
|
|
722
|
+
if (typeof m.anchor === "string" && m.anchor !== "") return m.anchor;
|
|
617
723
|
if (typeof m.key === "string" && m.key !== "") return m.key;
|
|
618
724
|
if (typeof m.id === "string" && m.id !== "") return "13:input-message" + m.id;
|
|
619
725
|
}
|
|
726
|
+
/** Resolve the **chat node key** for one rail message (used for load checks). */
|
|
727
|
+
function nodeKeyOf(m) {
|
|
728
|
+
return typeof m.key === "string" && m.key !== "" ? m.key : anchorKeyOf(m);
|
|
729
|
+
}
|
|
620
730
|
/** Full text of a rail message from the loaded chat nodes (uncapped),
|
|
621
731
|
* falling back to the projection preview when the node is not mounted. */
|
|
622
732
|
function fullTextOf(m, nodeSnapshot) {
|
|
@@ -663,37 +773,42 @@ window.__ModuleLoader__.load({
|
|
|
663
773
|
* DSH, where the Session snapshot no longer carries chat.nodes — falling back
|
|
664
774
|
* to the legacy Session snapshot's `chat.nodes` Map without it.
|
|
665
775
|
*/
|
|
666
|
-
async function jumpToMessage(sessionsService, sessionId, key, nodeOf, onProgress, signal, targetSeq) {
|
|
776
|
+
async function jumpToMessage(sessionsService, sessionId, key, nodeOf, onProgress, signal, targetSeq, onJump) {
|
|
667
777
|
const session = sessionsService.binding(sessionId)?.session;
|
|
668
778
|
if (session === void 0) return false;
|
|
669
779
|
const isLoaded = nodeOf === void 0 ? (k) => session.getSnapshot().chat?.nodes?.get(k) !== void 0 : nodeOf;
|
|
670
780
|
let pages = 0;
|
|
671
781
|
let loaded = false;
|
|
672
|
-
const
|
|
673
|
-
if (typeof
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
await jumpLoadThrough(targetSeq);
|
|
682
|
-
let spinNode = 0;
|
|
683
|
-
while (spinNode++ < 100) {
|
|
782
|
+
const jumpSession = session;
|
|
783
|
+
if (typeof jumpSession.loadThrough === "function" && targetSeq !== void 0 && Number.isSafeInteger(targetSeq)) {
|
|
784
|
+
if (!isLoaded(key)) {
|
|
785
|
+
let spin = 0;
|
|
786
|
+
while (spin++ < 120) {
|
|
787
|
+
if (signal?.aborted) return false;
|
|
788
|
+
if (session.getSnapshot()?.loadingOlder !== true) break;
|
|
789
|
+
await delay(50);
|
|
790
|
+
}
|
|
684
791
|
if (signal?.aborted) return false;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
792
|
+
onJump?.("paging");
|
|
793
|
+
await jumpSession.loadThrough(targetSeq);
|
|
794
|
+
let spinNode = 0;
|
|
795
|
+
while (spinNode++ < 100) {
|
|
796
|
+
if (signal?.aborted) return false;
|
|
797
|
+
if (isLoaded(key)) {
|
|
798
|
+
loaded = true;
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
await delay(50);
|
|
802
|
+
}
|
|
803
|
+
if (!loaded) {
|
|
804
|
+
console.warn(`[chat-rail] jumpToMessage: node "${key}" not loaded after loadThrough(${String(targetSeq)})`);
|
|
805
|
+
return false;
|
|
688
806
|
}
|
|
689
|
-
await delay(50);
|
|
690
|
-
}
|
|
691
|
-
if (!loaded) {
|
|
692
|
-
console.warn(`[chat-rail] jumpToMessage: node "${key}" not loaded after loadThrough(${String(targetSeq)})`);
|
|
693
|
-
return false;
|
|
694
807
|
}
|
|
808
|
+
loaded = true;
|
|
695
809
|
} else {
|
|
696
810
|
let guard = 0;
|
|
811
|
+
let reportedPaging = false;
|
|
697
812
|
while (guard++ < 120) {
|
|
698
813
|
if (signal?.aborted) return false;
|
|
699
814
|
const snapshot = session.getSnapshot();
|
|
@@ -706,6 +821,10 @@ window.__ModuleLoader__.load({
|
|
|
706
821
|
await delay(50);
|
|
707
822
|
continue;
|
|
708
823
|
}
|
|
824
|
+
if (!reportedPaging) {
|
|
825
|
+
onJump?.("paging");
|
|
826
|
+
reportedPaging = true;
|
|
827
|
+
}
|
|
709
828
|
await session.loadOlder();
|
|
710
829
|
pages++;
|
|
711
830
|
onProgress?.(pages);
|
|
@@ -726,15 +845,15 @@ window.__ModuleLoader__.load({
|
|
|
726
845
|
await delay(50);
|
|
727
846
|
}
|
|
728
847
|
if (row === null) return false;
|
|
848
|
+
onJump?.("landed");
|
|
729
849
|
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
730
|
-
const scrollToRow = () => {
|
|
850
|
+
const scrollToRow = (instant = false) => {
|
|
731
851
|
const viewRect = scrollport.getBoundingClientRect();
|
|
732
852
|
const rowRect = row.getBoundingClientRect();
|
|
733
|
-
const target = scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2;
|
|
734
|
-
if (target < 0) return;
|
|
853
|
+
const target = Math.max(0, scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2);
|
|
735
854
|
scrollport.scrollTo({
|
|
736
855
|
top: target,
|
|
737
|
-
behavior: reducedMotion ? "auto" : "smooth"
|
|
856
|
+
behavior: instant || reducedMotion ? "auto" : "smooth"
|
|
738
857
|
});
|
|
739
858
|
};
|
|
740
859
|
const rowDelta = () => {
|
|
@@ -765,7 +884,7 @@ window.__ModuleLoader__.load({
|
|
|
765
884
|
await delay(600);
|
|
766
885
|
if (signal?.aborted) return false;
|
|
767
886
|
if (rowDelta() <= Math.max(120, scrollport.getBoundingClientRect().height * .2)) break;
|
|
768
|
-
scrollToRow();
|
|
887
|
+
scrollToRow(true);
|
|
769
888
|
}
|
|
770
889
|
return true;
|
|
771
890
|
}
|
|
@@ -855,6 +974,7 @@ window.__ModuleLoader__.load({
|
|
|
855
974
|
let messages = [];
|
|
856
975
|
if (Array.isArray(projected?.messages) && projected.messages.length > 0) messages = projected.messages.map(normalize).filter((m) => m !== null);
|
|
857
976
|
if (messages.length === 0) messages = collectFromNodes(nodeSnapshot);
|
|
977
|
+
const qaMessages = collectQaFromNodes(nodeSnapshot);
|
|
858
978
|
const showOfficial = (0, react.useSyncExternalStore)(railSettingsScope !== null ? (cb) => railSettingsScope.subscribe(cb) : NOOP_SUB, () => railSettingsScope?.getSnapshot() ?? { value: void 0 }).value?.showOfficialNavigator === true;
|
|
859
979
|
const applyOfficialMode = (on) => {
|
|
860
980
|
syncOfficialHide(on);
|
|
@@ -866,6 +986,39 @@ window.__ModuleLoader__.load({
|
|
|
866
986
|
(0, react.useEffect)(() => {
|
|
867
987
|
applyOfficialMode(showOfficial);
|
|
868
988
|
}, [showOfficial]);
|
|
989
|
+
const [anchor, setAnchor] = (0, react.useState)(null);
|
|
990
|
+
(0, react.useEffect)(() => {
|
|
991
|
+
const measure = () => {
|
|
992
|
+
if (typeof document === "undefined") return;
|
|
993
|
+
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
994
|
+
if (scrollport === null) {
|
|
995
|
+
setAnchor(null);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const rect = scrollport.getBoundingClientRect();
|
|
999
|
+
if (rect.width === 0) {
|
|
1000
|
+
setAnchor(null);
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const next = {
|
|
1004
|
+
right: Math.max(0, Math.round(window.innerWidth - rect.right)),
|
|
1005
|
+
width: Math.round(rect.width)
|
|
1006
|
+
};
|
|
1007
|
+
setAnchor((prev) => prev !== null && prev.right === next.right && prev.width === next.width ? prev : next);
|
|
1008
|
+
};
|
|
1009
|
+
measure();
|
|
1010
|
+
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
1011
|
+
const observer = typeof ResizeObserver === "undefined" || scrollport === null ? null : new ResizeObserver(measure);
|
|
1012
|
+
if (observer !== null && scrollport !== null) observer.observe(scrollport);
|
|
1013
|
+
window.addEventListener("resize", measure);
|
|
1014
|
+
const timer = window.setInterval(measure, 400);
|
|
1015
|
+
return () => {
|
|
1016
|
+
if (observer !== null) observer.disconnect();
|
|
1017
|
+
window.removeEventListener("resize", measure);
|
|
1018
|
+
window.clearInterval(timer);
|
|
1019
|
+
};
|
|
1020
|
+
}, [sessionId]);
|
|
1021
|
+
const spaceTight = anchor !== null && anchor.width < RAIL_MIN_SPACE;
|
|
869
1022
|
const favorites = (0, react.useSyncExternalStore)(subscribeFavorites, favoritesSnapshot);
|
|
870
1023
|
const [favOnly, setFavOnly] = (0, react.useState)(false);
|
|
871
1024
|
const showFavToggle = (sessionId === void 0 ? 0 : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length) > 0;
|
|
@@ -878,6 +1031,7 @@ window.__ModuleLoader__.load({
|
|
|
878
1031
|
const [jumping, setJumping] = (0, react.useState)(false);
|
|
879
1032
|
const [tip, setTip] = (0, react.useState)(null);
|
|
880
1033
|
const navRef = (0, react.useRef)(null);
|
|
1034
|
+
const listRef = (0, react.useRef)(null);
|
|
881
1035
|
const expandedRef = (0, react.useRef)(false);
|
|
882
1036
|
const lastPointerRef = (0, react.useRef)(null);
|
|
883
1037
|
const jumpAbortRef = (0, react.useRef)(null);
|
|
@@ -916,9 +1070,21 @@ window.__ModuleLoader__.load({
|
|
|
916
1070
|
}
|
|
917
1071
|
positionTip(index);
|
|
918
1072
|
};
|
|
1073
|
+
const tipCloseTimerRef = (0, react.useRef)(null);
|
|
1074
|
+
const cancelTipClose = () => {
|
|
1075
|
+
if (tipCloseTimerRef.current !== null) {
|
|
1076
|
+
clearTimeout(tipCloseTimerRef.current);
|
|
1077
|
+
tipCloseTimerRef.current = null;
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
919
1080
|
const handleItemLeave = (index) => {
|
|
920
|
-
|
|
1081
|
+
cancelTipClose();
|
|
1082
|
+
tipCloseTimerRef.current = setTimeout(() => {
|
|
1083
|
+
tipCloseTimerRef.current = null;
|
|
1084
|
+
setTip((prev) => prev?.index === index ? null : prev);
|
|
1085
|
+
}, 180);
|
|
921
1086
|
};
|
|
1087
|
+
(0, react.useEffect)(() => cancelTipClose, []);
|
|
922
1088
|
(0, react.useEffect)(() => {
|
|
923
1089
|
if (!show) {
|
|
924
1090
|
expandedRef.current = false;
|
|
@@ -1048,7 +1214,7 @@ window.__ModuleLoader__.load({
|
|
|
1048
1214
|
const rect = sp.getBoundingClientRect();
|
|
1049
1215
|
if (rect.height === 0) return;
|
|
1050
1216
|
const line = rect.top + rect.height * .4;
|
|
1051
|
-
const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"]");
|
|
1217
|
+
const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"],[data-chat-anchor-key^=\"call:\"]");
|
|
1052
1218
|
let best = -1;
|
|
1053
1219
|
let bestDist = Infinity;
|
|
1054
1220
|
for (const row of rows) {
|
|
@@ -1085,7 +1251,7 @@ window.__ModuleLoader__.load({
|
|
|
1085
1251
|
}, [sessionId, effectiveMessages.length]);
|
|
1086
1252
|
(0, react.useEffect)(() => {
|
|
1087
1253
|
if (activeIndex < 0) return;
|
|
1088
|
-
const el =
|
|
1254
|
+
const el = listRef.current;
|
|
1089
1255
|
if (el === null) return;
|
|
1090
1256
|
const item = el.querySelector(`[data-crl-index="${activeIndex}"]`);
|
|
1091
1257
|
if (item === null) return;
|
|
@@ -1104,6 +1270,8 @@ window.__ModuleLoader__.load({
|
|
|
1104
1270
|
type: "button",
|
|
1105
1271
|
key: m.seq,
|
|
1106
1272
|
"data-crl-index": String(i),
|
|
1273
|
+
...key !== void 0 ? { "data-crl-anchor": key } : {},
|
|
1274
|
+
...m.qa === true ? { "data-crl-qa": "true" } : {},
|
|
1107
1275
|
className: S.item + (activeIndex === i ? ` ${S.itemActive}` : "") + (starred ? ` ${S.favItem}` : ""),
|
|
1108
1276
|
"aria-label": `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,
|
|
1109
1277
|
"aria-current": activeIndex === i ? "location" : void 0,
|
|
@@ -1113,8 +1281,7 @@ window.__ModuleLoader__.load({
|
|
|
1113
1281
|
jumpAbortRef.current?.abort();
|
|
1114
1282
|
const controller = new AbortController();
|
|
1115
1283
|
jumpAbortRef.current = controller;
|
|
1116
|
-
setJumping(
|
|
1117
|
-
jumpToMessage(sessionsService, sessionId, key, (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq).finally(() => setJumping(false));
|
|
1284
|
+
jumpToMessage(sessionsService, sessionId, nodeKeyOf(m), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
|
|
1118
1285
|
},
|
|
1119
1286
|
onMouseEnter: () => handleItemEnter(i),
|
|
1120
1287
|
onMouseLeave: () => handleItemLeave(i),
|
|
@@ -1174,7 +1341,8 @@ window.__ModuleLoader__.load({
|
|
|
1174
1341
|
});
|
|
1175
1342
|
return (0, react_dom.createPortal)([(0, react.createElement)("div", {
|
|
1176
1343
|
ref: navRef,
|
|
1177
|
-
className: S.nav + (show ? ` ${S.navShow}` : ""),
|
|
1344
|
+
className: S.nav + (show ? ` ${S.navShow}` : "") + (spaceTight ? ` ${NAV_HIDDEN_CLASS}` : ""),
|
|
1345
|
+
...anchor !== null ? { style: { right: `${anchor.right + 3}px` } } : {},
|
|
1178
1346
|
role: "navigation",
|
|
1179
1347
|
"aria-label": t.railLabel,
|
|
1180
1348
|
onMouseEnter: () => setShow(true),
|
|
@@ -1204,7 +1372,11 @@ window.__ModuleLoader__.load({
|
|
|
1204
1372
|
className: S.loading,
|
|
1205
1373
|
key: "loading"
|
|
1206
1374
|
}, (0, react.createElement)("span", { className: S.loadingLabel }, t.loading)) : null,
|
|
1207
|
-
|
|
1375
|
+
(0, react.createElement)("div", {
|
|
1376
|
+
ref: listRef,
|
|
1377
|
+
key: "list",
|
|
1378
|
+
className: S.list
|
|
1379
|
+
}, ...items)
|
|
1208
1380
|
]
|
|
1209
1381
|
}), tip !== null && tipIndex >= 0 && tipIndex < messages.length ? (0, react.createElement)("div", {
|
|
1210
1382
|
className: S.tip,
|
|
@@ -1212,7 +1384,9 @@ window.__ModuleLoader__.load({
|
|
|
1212
1384
|
left: `${tip.x}px`,
|
|
1213
1385
|
top: `${tip.y}px`,
|
|
1214
1386
|
transform: "translateX(-100%)"
|
|
1215
|
-
}
|
|
1387
|
+
},
|
|
1388
|
+
onMouseEnter: () => cancelTipClose(),
|
|
1389
|
+
onMouseLeave: () => setTip(null)
|
|
1216
1390
|
}, (() => {
|
|
1217
1391
|
const m = messages[tipIndex];
|
|
1218
1392
|
const specs = tipImagesOf(m, nodeSnapshot);
|
|
@@ -1236,6 +1410,52 @@ window.__ModuleLoader__.load({
|
|
|
1236
1410
|
key: "badge"
|
|
1237
1411
|
}, t.hasImage));
|
|
1238
1412
|
children.push((0, react.createElement)("span", { key: "text" }, fullTextOf(m, nodeSnapshot) || t.noText));
|
|
1413
|
+
const qaForTip = [];
|
|
1414
|
+
for (let qi = qaMessages.length - 1; qi >= 0; qi--) if (qaMessages[qi].seq >= m.seq) qaForTip.unshift(qaMessages[qi]);
|
|
1415
|
+
if (qaForTip.length > 0) children.push((0, react.createElement)("div", {
|
|
1416
|
+
className: S.tipQaWrap,
|
|
1417
|
+
key: "qa"
|
|
1418
|
+
}, qaForTip.map((q) => (0, react.createElement)("button", {
|
|
1419
|
+
type: "button",
|
|
1420
|
+
key: q.key ?? q.anchor,
|
|
1421
|
+
className: S.tipQa,
|
|
1422
|
+
title: q.text,
|
|
1423
|
+
onClick: (e) => {
|
|
1424
|
+
e.stopPropagation();
|
|
1425
|
+
if (jumping) return;
|
|
1426
|
+
jumpAbortRef.current?.abort();
|
|
1427
|
+
const controller = new AbortController();
|
|
1428
|
+
jumpAbortRef.current = controller;
|
|
1429
|
+
jumpToMessage(sessionsService, sessionId, nodeKeyOf(q), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, q.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
|
|
1430
|
+
},
|
|
1431
|
+
children: [(0, react.createElement)("svg", {
|
|
1432
|
+
key: "icon",
|
|
1433
|
+
className: S.tipQaIcon,
|
|
1434
|
+
viewBox: "0 0 16 16",
|
|
1435
|
+
width: 13,
|
|
1436
|
+
height: 13,
|
|
1437
|
+
fill: "none",
|
|
1438
|
+
stroke: "currentColor",
|
|
1439
|
+
strokeWidth: 1.6,
|
|
1440
|
+
strokeLinecap: "round",
|
|
1441
|
+
strokeLinejoin: "round",
|
|
1442
|
+
"aria-hidden": true
|
|
1443
|
+
}, (0, react.createElement)("circle", {
|
|
1444
|
+
cx: 8,
|
|
1445
|
+
cy: 8,
|
|
1446
|
+
r: 6.5,
|
|
1447
|
+
"aria-hidden": true
|
|
1448
|
+
}), (0, react.createElement)("path", {
|
|
1449
|
+
d: "M6.06 6a2 2 0 0 1 3.89.67c0 1.33-2 2-2 2",
|
|
1450
|
+
"aria-hidden": true
|
|
1451
|
+
}), (0, react.createElement)("path", {
|
|
1452
|
+
d: "M8 11.33h.01",
|
|
1453
|
+
"aria-hidden": true
|
|
1454
|
+
})), (0, react.createElement)("span", {
|
|
1455
|
+
className: S.tipQaText,
|
|
1456
|
+
key: "txt"
|
|
1457
|
+
}, q.text)]
|
|
1458
|
+
}))));
|
|
1239
1459
|
return children;
|
|
1240
1460
|
})()) : null], document.body);
|
|
1241
1461
|
}
|
|
@@ -1312,6 +1532,7 @@ window.__ModuleLoader__.load({
|
|
|
1312
1532
|
exports.chatNodeOf = chatNodeOf;
|
|
1313
1533
|
exports.clearRowActions = clearRowActions;
|
|
1314
1534
|
exports.collectFromNodes = collectFromNodes;
|
|
1535
|
+
exports.collectQaFromNodes = collectQaFromNodes;
|
|
1315
1536
|
exports.favoriteIdsOf = favoriteIdsOf;
|
|
1316
1537
|
exports.imageSpecsOfContent = imageSpecsOfContent;
|
|
1317
1538
|
exports.inject = inject;
|