@max-null/dsh-chat-rail 0.6.1 → 0.6.3
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.md +20 -14
- package/docs/shots/rail-fav-1.png +0 -0
- package/docs/shots/rail-fav-2.png +0 -0
- package/lib/client.js +268 -33
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
|
|
6
6
|
|
|
7
|
-
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**画卷式消息导航栏**:会话面板右侧的一条竖排导航,每条用户消息一个指示点,scroll-spy 跟随阅读位置,hover
|
|
7
|
+
面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的**画卷式消息导航栏**:会话面板右侧的一条竖排导航,每条用户消息一个指示点,scroll-spy 跟随阅读位置,hover 展开画卷查看消息列表,点击跳转任意历史消息;消息 hover tip 内嵌「提问&回答」直达区,收藏 + 一键回填输入框。
|
|
8
8
|
|
|
9
9
|
> 不同于市面上"面板弹出式"的导航插件,dsh-chat-rail 采用**画卷式展开**:收起时是一条干净的竖条,hover 时从右往左平滑展开,像打开画卷一样。
|
|
10
10
|
|
|
11
11
|
## 截图
|
|
12
12
|
|
|
13
|
-
|
|
|
13
|
+
| 画卷展开:只显示收藏(通栏 header + 黄色收藏条) | 消息操作按钮(收藏 ★ / 填充 ➕ / 复制)+ 折叠态导航条 |
|
|
14
14
|
|---|---|
|
|
15
15
|
|  |  |
|
|
16
16
|
|
|
@@ -20,24 +20,28 @@ This plugin belongs to the **`@max-null/*` family** — a set of plugins that to
|
|
|
20
20
|
|
|
21
21
|
| 能力 | 市面同类 | dsh-chat-rail |
|
|
22
22
|
|---|---|---|
|
|
23
|
-
| **与侧边栏/底栏共存** | ❌ 展开时与 better-sidebar 错位、遮挡 | ✅
|
|
23
|
+
| **与侧边栏/底栏共存** | ❌ 展开时与 better-sidebar 错位、遮挡 | ✅ **锚定会话区右列 + 底栏变量避让**:导航条实时跟随会话列右缘,官方右侧边栏 push 展开时会话列左移、导航**同步左移**(共享同一段过渡动画),不再被压在面板下面;底栏展开收起按 `--dsh-sidebar-height` 垂直避让,互不遮挡 |
|
|
24
|
+
| **空间不足** | ❌ 依然悬浮,遮挡会话 | ✅ **自动退场**:会话区剩余宽度不足(<260px,如窄屏右侧边栏完全遮住会话)时导航条整体淡出,空间恢复即回来 |
|
|
24
25
|
| **展开形态** | ❌ 面板突兀弹出,高度错位 | ✅ **画卷式展开**:单容器从右往左平滑展开,高度天然一致 |
|
|
25
26
|
| **阅读位置跟随** | 高亮点会跑出显示区 | ✅ **导航条自动滚动,高亮点持续居中**:无论会话滚到哪里,当前消息的指示点始终在导航可视区中央 |
|
|
26
27
|
| **切换会话定位** | 回到顶部 | ✅ 自动定位到新会话当前阅读位置 |
|
|
27
|
-
| **消息预览** | 浏览器原生 title(与悬停双重提示) | ✅ **自绘 tip 面板**:完整内容、动画结束后精准定位、无原生 title
|
|
28
|
+
| **消息预览** | 浏览器原生 title(与悬停双重提示) | ✅ **自绘 tip 面板**:完整内容、动画结束后精准定位、无原生 title 干扰;含图消息在文本上方显示缩略图,tip 底部还有**「提问&回答」直达区** |
|
|
28
29
|
| **消息信息** | 只有预览文本 | ✅ **编号 + 相对时间 + 文本三列对齐**:编号固定宽、时间靠右,文本区域最大化且整齐 |
|
|
29
30
|
| **长会话跳转** | 干等卡死 | ✅ **加载反馈**:跳转远距离消息时显示 spinner + "加载中",收起态只留图标 |
|
|
30
|
-
| **消息收藏** | ❌ 大多数只能看不能存 | ✅ **收藏 +
|
|
31
|
+
| **消息收藏** | ❌ 大多数只能看不能存 | ✅ **收藏 + 快速填充**:消息行星标收藏(黄色导航条联动)、「只显示收藏」为面板**通栏 header**(折叠/展开同构、贴顶不遮条目)、加号一键全文+附件回填输入框 |
|
|
31
32
|
| **主题适配** | 部分 | ✅ 全部使用 DSH 主题变量(`--dsw-alias-*`),浅深色无缝适配 |
|
|
32
|
-
| **无侧边栏兜底** | ❌ 依赖特定插件 | ✅ 纯 DSH
|
|
33
|
+
| **无侧边栏兜底** | ❌ 依赖特定插件 | ✅ 纯 DSH 环境开箱即用,锚定会话区右缘 + 垂直居中 |
|
|
33
34
|
|
|
34
35
|
## 兼容性(重点)
|
|
35
36
|
|
|
36
|
-
- **与 dsh-better-sidebar
|
|
37
|
-
-
|
|
38
|
-
-
|
|
37
|
+
- **与 dsh-better-sidebar 深度兼容**:底栏展开/收起时导航按 `--dsh-sidebar-height` 垂直避让,与面板**共享同一段 transition 动画同步移动**,永不错位(包括底栏右上角的收起按钮)。
|
|
38
|
+
- **右缘锚定会话区右列**:导航条实时测量会话列右缘定位——官方右侧边栏默认 push 展开时会话列左移,导航**同步左移**(同段过渡动画),不再被压在文件面板下面;不再依赖恒为 0 的 `--dsh-sidebar-width`。
|
|
39
|
+
- **空间不足自动退场**:会话区剩余宽度 < 260px 时(如窄屏右侧边栏完全遮住会话),导航条整体淡出,空间恢复即回来。
|
|
40
|
+
- **会话区滚动条隐藏**:会话区(`[data-conversation-scroll]`)自带的 Y 轴滚动条被本插件画成透明——它与导航条的位置指示功能重复,官方内核 `0.1.5` 起又把 bar 从「紧贴内容边缘」改为「离边缘 2px」,观感上更显眼。只覆盖 thumb 颜色、**保留官方 8px 槽位**(`scrollbar-gutter: stable` 是官方为输入卡跨视图不位移而保留的),滚轮/触控板滚动不受影响。
|
|
41
|
+
- **无 better-sidebar 也能用**:变量缺失时走 CSS fallback(垂直居中 + 视口右缘),纯 DSH 环境开箱即用。
|
|
42
|
+
- **与 DSH 自带轮次导航协同**:DSH `0.1.2-alpha.1+` 自带 TurnNavigator(轮次导航竖轨)。安装本插件后自动隐藏官方竖轨(aria-label 双文案锚点,不依赖构建 hash),由本插件统一承担会话导航;未安装本插件时官方导航照常显示。设置卡片另提供**「使用官方轮次导航条(对比模式)」**开关,可随时切回官方竖轨对比(免重启生效)。
|
|
39
43
|
- **深历史跳转(DSH `0.1.2-alpha.3+` 内核)**:会话历史按窗口分页加载(`hasMore`)时,点击跳转走官方 `loadThrough(seq)` 精确跳转装载器(200 条/页一次到位),替代旧版逐页 `loadOlder`(50 条/页)循环;旧内核无 `loadThrough` 时自动回退逐页循环。
|
|
40
|
-
- 需要 DSH ≥ `0.1.0-rc.6
|
|
44
|
+
- 需要 DSH ≥ `0.1.0-rc.6`;随 SSiD v0.3.0(内核 `0.1.5-rc.2`)完整适配验证。
|
|
41
45
|
|
|
42
46
|
## 安装
|
|
43
47
|
|
|
@@ -55,27 +59,29 @@ dsh plugin --profile <name> add @max-null/dsh-chat-rail
|
|
|
55
59
|
## 交互
|
|
56
60
|
|
|
57
61
|
- **hover 导航条**:画卷展开,显示编号 / 相对时间 / 消息预览
|
|
58
|
-
- **hover 消息项**:左侧弹出完整内容 tip
|
|
62
|
+
- **hover 消息项**:左侧弹出完整内容 tip 面板(含图消息在文本上方显示图片缩略图;tip 底部**「提问&回答」区**列出该消息内的提问——SVG 图标 + 单行问题,点击直达提问/回答位置)
|
|
59
63
|
- **点击消息项**:跳转到会话中对应消息(未加载的历史自动补载;DSH `0.1.2-alpha.3+` 走官方 `loadThrough` 精确跳转,旧内核回退逐页循环)
|
|
60
64
|
- **滚动会话**:高亮点跟随,导航条自动滚动保持居中
|
|
61
65
|
- **含图消息**:展开态显示柔和图片角标(收起态不显示,保持竖条干净)
|
|
62
66
|
|
|
63
|
-
###
|
|
67
|
+
### 收藏与快速填充
|
|
64
68
|
|
|
65
69
|
- **消息操作按钮**:每条用户消息的复制按钮左侧新增两个按钮
|
|
66
70
|
- **星 ★**:收藏/取消收藏该消息(收藏后变成黄色实心星);收藏状态按会话持久化(localStorage),刷新与重启后保留
|
|
67
71
|
- **加号 +**:一键把该消息**全文 + 附件**填入输入框(草稿设为消息全文,历史图片经官方附件通道回填入输入框)
|
|
68
72
|
- **导航条联动**:收藏的消息指示条显示为**黄色**;展开态在标题前显示小星标
|
|
69
|
-
-
|
|
73
|
+
- **只显示收藏**:过滤开关是导航面板的**通栏 header**(星标 + 文案 + 底部分隔线,折叠/展开同构、贴顶),点击点亮(黄色实心)后导航栏只显示已收藏的消息;再点恢复全部。列表在 header 之下独立滚动,开关不压住条目;无收藏时不显示该开关,过滤中取消最后一条收藏会自动退出过滤
|
|
70
74
|
|
|
71
75
|
## 架构
|
|
72
76
|
|
|
73
77
|
- **host 半端**(`lib/index.mjs`):注册 `chatRail` 会话投影——从 session log 折叠用户消息锚点 `{ seq, time, text, id }`(排除插件/工具注入的上下文行)
|
|
74
|
-
- **client 半端**(`lib/client.js`):`conversation.input.dock` 槽注入,portal 渲染到 body
|
|
78
|
+
- **client 半端**(`lib/client.js`):`conversation.input.dock` 槽注入,portal 渲染到 body;右缘实时测量会话列定位(ResizeObserver + 兜底采样),侧边栏 push 展开时跟随移动,空间不足时淡出
|
|
75
79
|
- 数据源优先序:`chatRail` 投影 → 已加载聊天节点 → 后台 loadOlder 循环
|
|
76
80
|
|
|
77
81
|
## SSID 系列
|
|
78
82
|
|
|
83
|
+
本插件是 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 全家桶的一员;也可以单独安装到任意 DSH profile——除设置卡片外不依赖其它同系列插件(右侧边栏避让、导航条定位都走官方 DOM 契约,缺失时自动降级)。
|
|
84
|
+
|
|
79
85
|
## 开发
|
|
80
86
|
|
|
81
87
|
```bash
|
|
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,18 +93,21 @@ 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:
|
|
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
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;
|
|
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:0;height:0}",
|
|
101
|
+
".crl_nav::-webkit-scrollbar,.crl_list::-webkit-scrollbar{width:0;height:0}",
|
|
102
|
+
"[data-conversation-scroll]::-webkit-scrollbar-thumb,[data-conversation-scroll]::-webkit-scrollbar-thumb:hover{background:transparent}",
|
|
103
|
+
".crl_show .crl_list{overflow-y:auto;align-items:stretch}",
|
|
102
104
|
".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}",
|
|
103
105
|
".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}",
|
|
104
106
|
".crl_loading .crl_loadingLabel{display:none}",
|
|
105
107
|
".crl_show .crl_loading .crl_loadingLabel{display:inline}",
|
|
106
108
|
"@keyframes crl-spin{to{transform:rotate(360deg)}}",
|
|
107
|
-
".crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:
|
|
108
|
-
".crl_show .crl_item{padding:0 14px}",
|
|
109
|
+
".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}",
|
|
110
|
+
".crl_show .crl_item{justify-content:flex-end;padding:0 14px}",
|
|
109
111
|
".crl_item:hover{color:rgba(0,0,0,.95)}",
|
|
110
112
|
".crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
111
113
|
"body[data-ds-dark-theme] .crl_item,[data-theme='dark'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}",
|
|
@@ -131,7 +133,12 @@ window.__ModuleLoader__.load({
|
|
|
131
133
|
"body[data-ds-dark-theme] .crl_line,[data-theme='dark'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}",
|
|
132
134
|
"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)}",
|
|
133
135
|
"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)}",
|
|
134
|
-
".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:
|
|
136
|
+
".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}",
|
|
137
|
+
".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}",
|
|
138
|
+
".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}",
|
|
139
|
+
".crl_tipQa:hover{background:rgba(77,107,254,.16)}",
|
|
140
|
+
".crl_tipQaIcon{flex:none;display:block;color:var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
141
|
+
".crl_tipQaText{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
135
142
|
"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)))}",
|
|
136
143
|
".crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}",
|
|
137
144
|
".crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}",
|
|
@@ -155,14 +162,16 @@ window.__ModuleLoader__.load({
|
|
|
155
162
|
".crl_item.crl_favItem .crl_line{background-color:#ffd166}",
|
|
156
163
|
"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}",
|
|
157
164
|
".crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}",
|
|
158
|
-
".crl_favToggle{
|
|
165
|
+
".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)}",
|
|
159
166
|
".crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}",
|
|
160
167
|
".crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}",
|
|
161
|
-
".crl_show .crl_favToggle{width:
|
|
168
|
+
".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)}",
|
|
162
169
|
".crl_favToggleLabel{display:none}",
|
|
163
170
|
".crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}",
|
|
164
|
-
"body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}",
|
|
171
|
+
"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)}",
|
|
165
172
|
"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)}",
|
|
173
|
+
"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)}",
|
|
174
|
+
".crl_nav.crl_navHidden{opacity:0 !important;pointer-events:none !important}",
|
|
166
175
|
"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)}",
|
|
167
176
|
"@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}",
|
|
168
177
|
".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}",
|
|
@@ -189,6 +198,14 @@ window.__ModuleLoader__.load({
|
|
|
189
198
|
* CSS module 类名(如 hkplfa_slot,hash 随构建内容漂移)。 */
|
|
190
199
|
const HIDE_OFFICIAL_CSS = "nav[aria-label=\"轮次导航\"],nav[aria-label=\"Turn navigation\"]{display:none !important}";
|
|
191
200
|
const HIDE_OFFICIAL_STYLE_ID = "data-crl-hide-official";
|
|
201
|
+
/**
|
|
202
|
+
* `data-plugin` 归属标记:DSH 的 client 模块系统只把**没有** `data-plugin` 的 `<style>`
|
|
203
|
+
* 认领给正在 materialize 的模块(`claimStyles`),而 HMR 重载时按
|
|
204
|
+
* `data-plugin === 模块 id` 逐字匹配删除。裸注入的 style 会被任意模块认领走,
|
|
205
|
+
* 随后随那个模块的一次重载被物理删除——元素还在、样式消失(悬浮球样式事故的机制)。
|
|
206
|
+
* 这里用一个独立于模块 id 的稳定值:既不进别人的账,也不随本模块的进出被清掉。
|
|
207
|
+
*/
|
|
208
|
+
const HIDE_OFFICIAL_PLUGIN_TAG = "dsh-chat-rail-hide-official";
|
|
192
209
|
/** 按设置同步「屏蔽官方 TurnNavigator」样式(showOfficial=true=官方模式:不屏蔽)。 */
|
|
193
210
|
function syncOfficialHide(showOfficial) {
|
|
194
211
|
const el = document.head.querySelector(`style[${HIDE_OFFICIAL_STYLE_ID}]`);
|
|
@@ -199,10 +216,19 @@ window.__ModuleLoader__.load({
|
|
|
199
216
|
if (el === null) {
|
|
200
217
|
const tag = document.createElement("style");
|
|
201
218
|
tag.setAttribute(HIDE_OFFICIAL_STYLE_ID, "");
|
|
219
|
+
tag.setAttribute("data-plugin", HIDE_OFFICIAL_PLUGIN_TAG);
|
|
202
220
|
tag.textContent = HIDE_OFFICIAL_CSS;
|
|
203
221
|
document.head.appendChild(tag);
|
|
204
|
-
}
|
|
222
|
+
} else if (el.textContent !== HIDE_OFFICIAL_CSS) el.textContent = HIDE_OFFICIAL_CSS;
|
|
205
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* 会话区剩余宽度低于此值时隐藏导航条(px)。
|
|
226
|
+
*
|
|
227
|
+
* 36px 是 rail 自身宽度,留 ~220px 给消息区——再窄时指示点已无参考意义。
|
|
228
|
+
*/
|
|
229
|
+
const RAIL_MIN_SPACE = 260;
|
|
230
|
+
/** 导航条隐藏类(空间不足时由 React 加上;用类而非内联 display,避免与官方模式冲突)。 */
|
|
231
|
+
const NAV_HIDDEN_CLASS = "crl_navHidden";
|
|
206
232
|
/** 移除所有已注入的行内收藏/填充按钮(官方模式下的回溯清理)。 */
|
|
207
233
|
function clearRowActions() {
|
|
208
234
|
for (const row of document.querySelectorAll("[data-crl-actions=\"1\"]")) {
|
|
@@ -219,16 +245,20 @@ window.__ModuleLoader__.load({
|
|
|
219
245
|
/** No-op subscribe for useSyncExternalStore when the scope is not yet bound. */
|
|
220
246
|
const NOOP_SUB = (() => () => {});
|
|
221
247
|
const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
|
|
222
|
-
if (typeof document !== "undefined"
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
if (typeof document !== "undefined") {
|
|
249
|
+
const existing = document.head.querySelector(`style[data-plugin-css="${STYLE_ID}"]`);
|
|
250
|
+
if (existing === null) {
|
|
251
|
+
const tag = document.createElement("style");
|
|
252
|
+
tag.dataset.plugin = "dsh-chat-rail";
|
|
253
|
+
tag.dataset.pluginCss = STYLE_ID;
|
|
254
|
+
tag.textContent = css;
|
|
255
|
+
document.head.appendChild(tag);
|
|
256
|
+
} else if (existing.textContent !== css) existing.textContent = css;
|
|
228
257
|
}
|
|
229
258
|
const S = {
|
|
230
259
|
nav: "crl_nav",
|
|
231
260
|
navShow: "crl_show",
|
|
261
|
+
list: "crl_list",
|
|
232
262
|
item: "crl_item",
|
|
233
263
|
itemActive: "crl_active",
|
|
234
264
|
title: "crl_title",
|
|
@@ -240,6 +270,10 @@ window.__ModuleLoader__.load({
|
|
|
240
270
|
loading: "crl_loading",
|
|
241
271
|
loadingLabel: "crl_loadingLabel",
|
|
242
272
|
tip: "crl_tip",
|
|
273
|
+
tipQaWrap: "crl_tipQaWrap",
|
|
274
|
+
tipQa: "crl_tipQa",
|
|
275
|
+
tipQaIcon: "crl_tipQaIcon",
|
|
276
|
+
tipQaText: "crl_tipQaText",
|
|
243
277
|
tipImgs: "crl_tipImgs",
|
|
244
278
|
tipImgWrap: "crl_tipImgWrap",
|
|
245
279
|
tipImg: "crl_tipImg",
|
|
@@ -611,11 +645,109 @@ window.__ModuleLoader__.load({
|
|
|
611
645
|
out.sort((a, b) => a.seq - b.seq);
|
|
612
646
|
return out;
|
|
613
647
|
}
|
|
614
|
-
/**
|
|
648
|
+
/** 任一数据平面的节点集合(与 collectFromNodes 同一套取值规则)。 */
|
|
649
|
+
function nodeValuesOf(snapshot) {
|
|
650
|
+
const store = snapshot?.nodes;
|
|
651
|
+
if (store !== void 0 && typeof store.values === "function") return [...store.values()];
|
|
652
|
+
const chat = snapshot?.chat;
|
|
653
|
+
if (chat?.nodes === void 0) return [];
|
|
654
|
+
return [...chat.nodes.values()];
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* 「提问&回答」也要能被导航。
|
|
658
|
+
*
|
|
659
|
+
* 它**不是** chat node:`ask_user_question` 是助手消息里的一个 tool-call 块,
|
|
660
|
+
* 用户的答案落在同一个 callId 的 `tool/result`(实测会话日志:有 `tool/call
|
|
661
|
+
* name=ask_user_question` 与对应结果,**没有任何带问答痕迹的 user/message**)。
|
|
662
|
+
* 所以 `railMessageOfNode` 的 `kind==='user'` 判定永远覆盖不到它。
|
|
663
|
+
*
|
|
664
|
+
* 但官方给工具调用也设了 DOM 锚点(`ui-tool/ToolCallTree.tsx:36` →
|
|
665
|
+
* `data-chat-anchor-key={`call:${callId}`}`),所以**可跳转**——只要把
|
|
666
|
+
* 助手节点里的问答块收集成 rail 条目即可。
|
|
667
|
+
*
|
|
668
|
+
* 范围:只覆盖已加载的 chat node(不做 loadThrough 拉取未加载历史)——
|
|
669
|
+
* 用户 2026-09-14 拍板。
|
|
670
|
+
*
|
|
671
|
+
* @param snapshot - 当前 chat node 快照(任一平面)。
|
|
672
|
+
* @returns 按 seq 升序的问答条目。
|
|
673
|
+
*/
|
|
674
|
+
function collectQaFromNodes(snapshot) {
|
|
675
|
+
const out = [];
|
|
676
|
+
for (const node of nodeValuesOf(snapshot)) {
|
|
677
|
+
if (node === null || typeof node !== "object") continue;
|
|
678
|
+
const n = node;
|
|
679
|
+
if (n.kind !== "tool-call") continue;
|
|
680
|
+
const root = n.data?.root;
|
|
681
|
+
if (root === null || typeof root !== "object") continue;
|
|
682
|
+
const r = root;
|
|
683
|
+
const callId = typeof r.callId === "string" ? r.callId : "";
|
|
684
|
+
if (callId === "") continue;
|
|
685
|
+
if ((typeof r.name === "string" ? r.name : typeof r.call?.name === "string" ? r.call.name : "") !== "ask_user_question") continue;
|
|
686
|
+
const asked = questionsTextOf(typeof r.argsRaw === "string" ? r.argsRaw : typeof r.call?.argsRaw === "string" ? r.call.argsRaw : "");
|
|
687
|
+
if (asked === "") continue;
|
|
688
|
+
out.push({
|
|
689
|
+
seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
|
|
690
|
+
time: typeof r.time === "number" ? r.time : 0,
|
|
691
|
+
text: asked,
|
|
692
|
+
hasImage: false,
|
|
693
|
+
key: typeof n.key === "string" ? n.key : void 0,
|
|
694
|
+
anchor: `call:${callId}`,
|
|
695
|
+
qa: true
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
699
|
+
return out;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* 属于第 `index` 条消息区间的问答——**只归给它前面最近的那条用户消息**。
|
|
703
|
+
*
|
|
704
|
+
* 区间取 `[本条 seq, 下一条消息的 seq)`,最后一条消息无上界。
|
|
705
|
+
* 判据不能只写 `qa.seq >= m.seq`:那会让同一条提问落进**所有更早消息**的 tip
|
|
706
|
+
* (2026-09-14 用户报告的归属 bug)。依据是「提问一定在某条消息的回答中」——
|
|
707
|
+
* 它必然夹在两条用户消息之间,因此上界就是下一条消息。
|
|
708
|
+
*
|
|
709
|
+
* @param qaMessages - 已按 seq 升序的问答条目(collectQaFromNodes 的产出)。
|
|
710
|
+
* @param messages - 完整的用户消息列表(**不是**收藏过滤后的列表:区间按消息序列划分)。
|
|
711
|
+
* @param index - 目标消息在 `messages` 中的下标。
|
|
712
|
+
* @returns 落在该区间内的问答条目(保持输入顺序)。
|
|
713
|
+
*/
|
|
714
|
+
function qaWithinMessage(qaMessages, messages, index) {
|
|
715
|
+
const self = messages[index];
|
|
716
|
+
if (self === void 0) return [];
|
|
717
|
+
const next = messages[index + 1];
|
|
718
|
+
const upper = next === void 0 ? void 0 : next.seq;
|
|
719
|
+
return qaMessages.filter((q) => q.seq >= self.seq && (upper === void 0 || q.seq < upper));
|
|
720
|
+
}
|
|
721
|
+
/** 问答条目的预览文本:取 `arguments` JSON 里的问题标题(多问用 ` / ` 连)。 */
|
|
722
|
+
function questionsTextOf(argsRaw) {
|
|
723
|
+
if (typeof argsRaw !== "string" || argsRaw.trim() === "") return "";
|
|
724
|
+
let parsed;
|
|
725
|
+
try {
|
|
726
|
+
parsed = JSON.parse(argsRaw);
|
|
727
|
+
} catch {
|
|
728
|
+
return "";
|
|
729
|
+
}
|
|
730
|
+
const list = parsed?.questions;
|
|
731
|
+
if (!Array.isArray(list)) return "";
|
|
732
|
+
const parts = [];
|
|
733
|
+
for (const q of list) {
|
|
734
|
+
if (q === null || typeof q !== "object") continue;
|
|
735
|
+
const o = q;
|
|
736
|
+
const text = typeof o.question === "string" && o.question !== "" ? o.question : typeof o.header === "string" ? o.header : "";
|
|
737
|
+
if (text !== "") parts.push(text);
|
|
738
|
+
}
|
|
739
|
+
return parts.join(" / ").trim().slice(0, 80);
|
|
740
|
+
}
|
|
741
|
+
/** Resolve the DOM anchor key (`data-chat-anchor-key`) for one rail message. */
|
|
615
742
|
function anchorKeyOf(m) {
|
|
743
|
+
if (typeof m.anchor === "string" && m.anchor !== "") return m.anchor;
|
|
616
744
|
if (typeof m.key === "string" && m.key !== "") return m.key;
|
|
617
745
|
if (typeof m.id === "string" && m.id !== "") return "13:input-message" + m.id;
|
|
618
746
|
}
|
|
747
|
+
/** Resolve the **chat node key** for one rail message (used for load checks). */
|
|
748
|
+
function nodeKeyOf(m) {
|
|
749
|
+
return typeof m.key === "string" && m.key !== "" ? m.key : anchorKeyOf(m);
|
|
750
|
+
}
|
|
619
751
|
/** Full text of a rail message from the loaded chat nodes (uncapped),
|
|
620
752
|
* falling back to the projection preview when the node is not mounted. */
|
|
621
753
|
function fullTextOf(m, nodeSnapshot) {
|
|
@@ -736,13 +868,13 @@ window.__ModuleLoader__.load({
|
|
|
736
868
|
if (row === null) return false;
|
|
737
869
|
onJump?.("landed");
|
|
738
870
|
const reducedMotion = typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
739
|
-
const scrollToRow = () => {
|
|
871
|
+
const scrollToRow = (instant = false) => {
|
|
740
872
|
const viewRect = scrollport.getBoundingClientRect();
|
|
741
873
|
const rowRect = row.getBoundingClientRect();
|
|
742
874
|
const target = Math.max(0, scrollport.scrollTop + (rowRect.top - viewRect.top) - (viewRect.height - rowRect.height) / 2);
|
|
743
875
|
scrollport.scrollTo({
|
|
744
876
|
top: target,
|
|
745
|
-
behavior: reducedMotion ? "auto" : "smooth"
|
|
877
|
+
behavior: instant || reducedMotion ? "auto" : "smooth"
|
|
746
878
|
});
|
|
747
879
|
};
|
|
748
880
|
const rowDelta = () => {
|
|
@@ -773,7 +905,7 @@ window.__ModuleLoader__.load({
|
|
|
773
905
|
await delay(600);
|
|
774
906
|
if (signal?.aborted) return false;
|
|
775
907
|
if (rowDelta() <= Math.max(120, scrollport.getBoundingClientRect().height * .2)) break;
|
|
776
|
-
scrollToRow();
|
|
908
|
+
scrollToRow(true);
|
|
777
909
|
}
|
|
778
910
|
return true;
|
|
779
911
|
}
|
|
@@ -863,6 +995,7 @@ window.__ModuleLoader__.load({
|
|
|
863
995
|
let messages = [];
|
|
864
996
|
if (Array.isArray(projected?.messages) && projected.messages.length > 0) messages = projected.messages.map(normalize).filter((m) => m !== null);
|
|
865
997
|
if (messages.length === 0) messages = collectFromNodes(nodeSnapshot);
|
|
998
|
+
const qaMessages = collectQaFromNodes(nodeSnapshot);
|
|
866
999
|
const showOfficial = (0, react.useSyncExternalStore)(railSettingsScope !== null ? (cb) => railSettingsScope.subscribe(cb) : NOOP_SUB, () => railSettingsScope?.getSnapshot() ?? { value: void 0 }).value?.showOfficialNavigator === true;
|
|
867
1000
|
const applyOfficialMode = (on) => {
|
|
868
1001
|
syncOfficialHide(on);
|
|
@@ -874,6 +1007,39 @@ window.__ModuleLoader__.load({
|
|
|
874
1007
|
(0, react.useEffect)(() => {
|
|
875
1008
|
applyOfficialMode(showOfficial);
|
|
876
1009
|
}, [showOfficial]);
|
|
1010
|
+
const [anchor, setAnchor] = (0, react.useState)(null);
|
|
1011
|
+
(0, react.useEffect)(() => {
|
|
1012
|
+
const measure = () => {
|
|
1013
|
+
if (typeof document === "undefined") return;
|
|
1014
|
+
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
1015
|
+
if (scrollport === null) {
|
|
1016
|
+
setAnchor(null);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
const rect = scrollport.getBoundingClientRect();
|
|
1020
|
+
if (rect.width === 0) {
|
|
1021
|
+
setAnchor(null);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const next = {
|
|
1025
|
+
right: Math.max(0, Math.round(window.innerWidth - rect.right)),
|
|
1026
|
+
width: Math.round(rect.width)
|
|
1027
|
+
};
|
|
1028
|
+
setAnchor((prev) => prev !== null && prev.right === next.right && prev.width === next.width ? prev : next);
|
|
1029
|
+
};
|
|
1030
|
+
measure();
|
|
1031
|
+
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
1032
|
+
const observer = typeof ResizeObserver === "undefined" || scrollport === null ? null : new ResizeObserver(measure);
|
|
1033
|
+
if (observer !== null && scrollport !== null) observer.observe(scrollport);
|
|
1034
|
+
window.addEventListener("resize", measure);
|
|
1035
|
+
const timer = window.setInterval(measure, 400);
|
|
1036
|
+
return () => {
|
|
1037
|
+
if (observer !== null) observer.disconnect();
|
|
1038
|
+
window.removeEventListener("resize", measure);
|
|
1039
|
+
window.clearInterval(timer);
|
|
1040
|
+
};
|
|
1041
|
+
}, [sessionId]);
|
|
1042
|
+
const spaceTight = anchor !== null && anchor.width < RAIL_MIN_SPACE;
|
|
877
1043
|
const favorites = (0, react.useSyncExternalStore)(subscribeFavorites, favoritesSnapshot);
|
|
878
1044
|
const [favOnly, setFavOnly] = (0, react.useState)(false);
|
|
879
1045
|
const showFavToggle = (sessionId === void 0 ? 0 : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length) > 0;
|
|
@@ -886,6 +1052,7 @@ window.__ModuleLoader__.load({
|
|
|
886
1052
|
const [jumping, setJumping] = (0, react.useState)(false);
|
|
887
1053
|
const [tip, setTip] = (0, react.useState)(null);
|
|
888
1054
|
const navRef = (0, react.useRef)(null);
|
|
1055
|
+
const listRef = (0, react.useRef)(null);
|
|
889
1056
|
const expandedRef = (0, react.useRef)(false);
|
|
890
1057
|
const lastPointerRef = (0, react.useRef)(null);
|
|
891
1058
|
const jumpAbortRef = (0, react.useRef)(null);
|
|
@@ -924,9 +1091,21 @@ window.__ModuleLoader__.load({
|
|
|
924
1091
|
}
|
|
925
1092
|
positionTip(index);
|
|
926
1093
|
};
|
|
1094
|
+
const tipCloseTimerRef = (0, react.useRef)(null);
|
|
1095
|
+
const cancelTipClose = () => {
|
|
1096
|
+
if (tipCloseTimerRef.current !== null) {
|
|
1097
|
+
clearTimeout(tipCloseTimerRef.current);
|
|
1098
|
+
tipCloseTimerRef.current = null;
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
927
1101
|
const handleItemLeave = (index) => {
|
|
928
|
-
|
|
1102
|
+
cancelTipClose();
|
|
1103
|
+
tipCloseTimerRef.current = setTimeout(() => {
|
|
1104
|
+
tipCloseTimerRef.current = null;
|
|
1105
|
+
setTip((prev) => prev?.index === index ? null : prev);
|
|
1106
|
+
}, 180);
|
|
929
1107
|
};
|
|
1108
|
+
(0, react.useEffect)(() => cancelTipClose, []);
|
|
930
1109
|
(0, react.useEffect)(() => {
|
|
931
1110
|
if (!show) {
|
|
932
1111
|
expandedRef.current = false;
|
|
@@ -1056,7 +1235,7 @@ window.__ModuleLoader__.load({
|
|
|
1056
1235
|
const rect = sp.getBoundingClientRect();
|
|
1057
1236
|
if (rect.height === 0) return;
|
|
1058
1237
|
const line = rect.top + rect.height * .4;
|
|
1059
|
-
const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"]");
|
|
1238
|
+
const rows = sp.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"],[data-chat-anchor-key^=\"call:\"]");
|
|
1060
1239
|
let best = -1;
|
|
1061
1240
|
let bestDist = Infinity;
|
|
1062
1241
|
for (const row of rows) {
|
|
@@ -1093,7 +1272,7 @@ window.__ModuleLoader__.load({
|
|
|
1093
1272
|
}, [sessionId, effectiveMessages.length]);
|
|
1094
1273
|
(0, react.useEffect)(() => {
|
|
1095
1274
|
if (activeIndex < 0) return;
|
|
1096
|
-
const el =
|
|
1275
|
+
const el = listRef.current;
|
|
1097
1276
|
if (el === null) return;
|
|
1098
1277
|
const item = el.querySelector(`[data-crl-index="${activeIndex}"]`);
|
|
1099
1278
|
if (item === null) return;
|
|
@@ -1112,6 +1291,8 @@ window.__ModuleLoader__.load({
|
|
|
1112
1291
|
type: "button",
|
|
1113
1292
|
key: m.seq,
|
|
1114
1293
|
"data-crl-index": String(i),
|
|
1294
|
+
...key !== void 0 ? { "data-crl-anchor": key } : {},
|
|
1295
|
+
...m.qa === true ? { "data-crl-qa": "true" } : {},
|
|
1115
1296
|
className: S.item + (activeIndex === i ? ` ${S.itemActive}` : "") + (starred ? ` ${S.favItem}` : ""),
|
|
1116
1297
|
"aria-label": `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,
|
|
1117
1298
|
"aria-current": activeIndex === i ? "location" : void 0,
|
|
@@ -1121,7 +1302,7 @@ window.__ModuleLoader__.load({
|
|
|
1121
1302
|
jumpAbortRef.current?.abort();
|
|
1122
1303
|
const controller = new AbortController();
|
|
1123
1304
|
jumpAbortRef.current = controller;
|
|
1124
|
-
jumpToMessage(sessionsService, sessionId,
|
|
1305
|
+
jumpToMessage(sessionsService, sessionId, nodeKeyOf(m), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, m.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
|
|
1125
1306
|
},
|
|
1126
1307
|
onMouseEnter: () => handleItemEnter(i),
|
|
1127
1308
|
onMouseLeave: () => handleItemLeave(i),
|
|
@@ -1181,7 +1362,8 @@ window.__ModuleLoader__.load({
|
|
|
1181
1362
|
});
|
|
1182
1363
|
return (0, react_dom.createPortal)([(0, react.createElement)("div", {
|
|
1183
1364
|
ref: navRef,
|
|
1184
|
-
className: S.nav + (show ? ` ${S.navShow}` : ""),
|
|
1365
|
+
className: S.nav + (show ? ` ${S.navShow}` : "") + (spaceTight ? ` ${NAV_HIDDEN_CLASS}` : ""),
|
|
1366
|
+
...anchor !== null ? { style: { right: `${anchor.right + 3}px` } } : {},
|
|
1185
1367
|
role: "navigation",
|
|
1186
1368
|
"aria-label": t.railLabel,
|
|
1187
1369
|
onMouseEnter: () => setShow(true),
|
|
@@ -1211,7 +1393,11 @@ window.__ModuleLoader__.load({
|
|
|
1211
1393
|
className: S.loading,
|
|
1212
1394
|
key: "loading"
|
|
1213
1395
|
}, (0, react.createElement)("span", { className: S.loadingLabel }, t.loading)) : null,
|
|
1214
|
-
|
|
1396
|
+
(0, react.createElement)("div", {
|
|
1397
|
+
ref: listRef,
|
|
1398
|
+
key: "list",
|
|
1399
|
+
className: S.list
|
|
1400
|
+
}, ...items)
|
|
1215
1401
|
]
|
|
1216
1402
|
}), tip !== null && tipIndex >= 0 && tipIndex < messages.length ? (0, react.createElement)("div", {
|
|
1217
1403
|
className: S.tip,
|
|
@@ -1219,7 +1405,9 @@ window.__ModuleLoader__.load({
|
|
|
1219
1405
|
left: `${tip.x}px`,
|
|
1220
1406
|
top: `${tip.y}px`,
|
|
1221
1407
|
transform: "translateX(-100%)"
|
|
1222
|
-
}
|
|
1408
|
+
},
|
|
1409
|
+
onMouseEnter: () => cancelTipClose(),
|
|
1410
|
+
onMouseLeave: () => setTip(null)
|
|
1223
1411
|
}, (() => {
|
|
1224
1412
|
const m = messages[tipIndex];
|
|
1225
1413
|
const specs = tipImagesOf(m, nodeSnapshot);
|
|
@@ -1243,6 +1431,51 @@ window.__ModuleLoader__.load({
|
|
|
1243
1431
|
key: "badge"
|
|
1244
1432
|
}, t.hasImage));
|
|
1245
1433
|
children.push((0, react.createElement)("span", { key: "text" }, fullTextOf(m, nodeSnapshot) || t.noText));
|
|
1434
|
+
const qaForTip = qaWithinMessage(qaMessages, messages, tipIndex);
|
|
1435
|
+
if (qaForTip.length > 0) children.push((0, react.createElement)("div", {
|
|
1436
|
+
className: S.tipQaWrap,
|
|
1437
|
+
key: "qa"
|
|
1438
|
+
}, qaForTip.map((q) => (0, react.createElement)("button", {
|
|
1439
|
+
type: "button",
|
|
1440
|
+
key: q.key ?? q.anchor,
|
|
1441
|
+
className: S.tipQa,
|
|
1442
|
+
title: q.text,
|
|
1443
|
+
onClick: (e) => {
|
|
1444
|
+
e.stopPropagation();
|
|
1445
|
+
if (jumping) return;
|
|
1446
|
+
jumpAbortRef.current?.abort();
|
|
1447
|
+
const controller = new AbortController();
|
|
1448
|
+
jumpAbortRef.current = controller;
|
|
1449
|
+
jumpToMessage(sessionsService, sessionId, nodeKeyOf(q), (k) => chatNodeOf(fallbackStore.getSnapshot(), k), void 0, controller.signal, q.seq, (phase) => setJumping(phase === "paging")).finally(() => setJumping(false));
|
|
1450
|
+
},
|
|
1451
|
+
children: [(0, react.createElement)("svg", {
|
|
1452
|
+
key: "icon",
|
|
1453
|
+
className: S.tipQaIcon,
|
|
1454
|
+
viewBox: "0 0 16 16",
|
|
1455
|
+
width: 13,
|
|
1456
|
+
height: 13,
|
|
1457
|
+
fill: "none",
|
|
1458
|
+
stroke: "currentColor",
|
|
1459
|
+
strokeWidth: 1.6,
|
|
1460
|
+
strokeLinecap: "round",
|
|
1461
|
+
strokeLinejoin: "round",
|
|
1462
|
+
"aria-hidden": true
|
|
1463
|
+
}, (0, react.createElement)("circle", {
|
|
1464
|
+
cx: 8,
|
|
1465
|
+
cy: 8,
|
|
1466
|
+
r: 6.5,
|
|
1467
|
+
"aria-hidden": true
|
|
1468
|
+
}), (0, react.createElement)("path", {
|
|
1469
|
+
d: "M6.06 6a2 2 0 0 1 3.89.67c0 1.33-2 2-2 2",
|
|
1470
|
+
"aria-hidden": true
|
|
1471
|
+
}), (0, react.createElement)("path", {
|
|
1472
|
+
d: "M8 11.33h.01",
|
|
1473
|
+
"aria-hidden": true
|
|
1474
|
+
})), (0, react.createElement)("span", {
|
|
1475
|
+
className: S.tipQaText,
|
|
1476
|
+
key: "txt"
|
|
1477
|
+
}, q.text)]
|
|
1478
|
+
}))));
|
|
1246
1479
|
return children;
|
|
1247
1480
|
})()) : null], document.body);
|
|
1248
1481
|
}
|
|
@@ -1319,6 +1552,7 @@ window.__ModuleLoader__.load({
|
|
|
1319
1552
|
exports.chatNodeOf = chatNodeOf;
|
|
1320
1553
|
exports.clearRowActions = clearRowActions;
|
|
1321
1554
|
exports.collectFromNodes = collectFromNodes;
|
|
1555
|
+
exports.collectQaFromNodes = collectQaFromNodes;
|
|
1322
1556
|
exports.favoriteIdsOf = favoriteIdsOf;
|
|
1323
1557
|
exports.imageSpecsOfContent = imageSpecsOfContent;
|
|
1324
1558
|
exports.inject = inject;
|
|
@@ -1326,6 +1560,7 @@ window.__ModuleLoader__.load({
|
|
|
1326
1560
|
exports.jumpToMessage = jumpToMessage;
|
|
1327
1561
|
exports.messageIdOfAnchorKey = messageIdOfAnchorKey;
|
|
1328
1562
|
exports.normalize = normalize;
|
|
1563
|
+
exports.qaWithinMessage = qaWithinMessage;
|
|
1329
1564
|
exports.railMessageOfNode = railMessageOfNode;
|
|
1330
1565
|
exports.readFavorites = readFavorites;
|
|
1331
1566
|
exports.syncOfficialHide = syncOfficialHide;
|