@max-null/dsh-chat-rail 0.2.0 → 0.3.1
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 +3 -2
- package/lib/client.js +283 -34
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +29 -3
- package/package.json +7 -14
package/README.md
CHANGED
|
@@ -48,9 +48,10 @@ dsh plugin --profile <name> add @max-null/dsh-chat-rail
|
|
|
48
48
|
## 交互
|
|
49
49
|
|
|
50
50
|
- **hover 导航条**:画卷展开,显示编号 / 相对时间 / 消息预览
|
|
51
|
-
- **hover 消息项**:左侧弹出完整内容 tip
|
|
51
|
+
- **hover 消息项**:左侧弹出完整内容 tip 面板(含图消息在文本上方显示图片缩略图)
|
|
52
52
|
- **点击消息项**:跳转到会话中对应消息(未加载的历史自动翻页补载)
|
|
53
53
|
- **滚动会话**:高亮点跟随,导航条自动滚动保持居中
|
|
54
|
+
- **含图消息**:展开态显示柔和图片角标(收起态不显示,保持竖条干净)
|
|
54
55
|
|
|
55
56
|
## 架构
|
|
56
57
|
|
|
@@ -60,7 +61,7 @@ dsh plugin --profile <name> add @max-null/dsh-chat-rail
|
|
|
60
61
|
|
|
61
62
|
## SSID 系列
|
|
62
63
|
|
|
63
|
-
本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-chat-rail` · `dsh-
|
|
64
|
+
本插件属于 **`@max-null/*` 插件系列**——这一系列共同构成 **[SSID(思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** 桌面体验。SSID 是整合它们的盒:`dsh-chat-rail` · `dsh-chinese-thinking` · `dsh-draft-polish` · `dsh-guardian` · `dsh-habit` · `dsh-header-unify` · `dsh-memory` · `dsh-node-appearance` · `dsh-plugin-center` · `dsh-skill-mcp-center` · `dsh-ssid-panels` · `dsh-ssid-zh-ui`。
|
|
64
65
|
|
|
65
66
|
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.
|
|
66
67
|
|
package/lib/client.js
CHANGED
|
@@ -98,8 +98,9 @@ window.__ModuleLoader__.load({
|
|
|
98
98
|
"@keyframes crl-fade{from{opacity:0}to{opacity:1}}",
|
|
99
99
|
".crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}",
|
|
100
100
|
".crl_show .crl_ind{margin-left:8px}",
|
|
101
|
-
".crl_img{flex-shrink:0;
|
|
102
|
-
".crl_img
|
|
101
|
+
".crl_img{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-left:6px;color:rgba(0,0,0,.38)}",
|
|
102
|
+
".crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}",
|
|
103
|
+
"body[data-ds-dark-theme] .crl_img,[data-theme='dark'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}",
|
|
103
104
|
".crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}",
|
|
104
105
|
".crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}",
|
|
105
106
|
".crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}",
|
|
@@ -108,7 +109,15 @@ window.__ModuleLoader__.load({
|
|
|
108
109
|
"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)}",
|
|
109
110
|
".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:none}",
|
|
110
111
|
"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)))}",
|
|
111
|
-
"
|
|
112
|
+
".crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}",
|
|
113
|
+
".crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}",
|
|
114
|
+
".crl_tipImg{display:block;max-width:100%;max-height:150px;width:auto;height:auto;object-fit:contain;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)))}",
|
|
115
|
+
".crl_tipImgCount{position:absolute;right:6px;bottom:6px;padding:0 6px;font-size:10px;line-height:16px;border-radius:9px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.95);pointer-events:none}",
|
|
116
|
+
".crl_tipImgPh{flex-shrink:0;height:56px;border-radius:8px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.05)));animation:crl-pulse 1.2s ease-in-out infinite}",
|
|
117
|
+
"body[data-ds-dark-theme] .crl_tipImg,[data-theme='dark'] .crl_tipImg,.dark .crl_tipImg{border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}",
|
|
118
|
+
".crl_tipBadge{display:inline-block;margin-bottom:8px;padding:0 7px;font-size:10px;line-height:16px;border-radius:9px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.06)));color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.45)))}",
|
|
119
|
+
"@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}",
|
|
120
|
+
"@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}"
|
|
112
121
|
].join("");
|
|
113
122
|
const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
|
|
114
123
|
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) === null) {
|
|
@@ -131,23 +140,66 @@ window.__ModuleLoader__.load({
|
|
|
131
140
|
img: "crl_img",
|
|
132
141
|
loading: "crl_loading",
|
|
133
142
|
loadingLabel: "crl_loadingLabel",
|
|
134
|
-
tip: "crl_tip"
|
|
143
|
+
tip: "crl_tip",
|
|
144
|
+
tipImgs: "crl_tipImgs",
|
|
145
|
+
tipImgWrap: "crl_tipImgWrap",
|
|
146
|
+
tipImg: "crl_tipImg",
|
|
147
|
+
tipImgCount: "crl_tipImgCount",
|
|
148
|
+
tipImgPh: "crl_tipImgPh",
|
|
149
|
+
tipBadge: "crl_tipBadge"
|
|
135
150
|
};
|
|
136
151
|
const NOOP_STORE = {
|
|
137
152
|
getSnapshot: () => void 0,
|
|
138
153
|
subscribe: () => () => {}
|
|
139
154
|
};
|
|
140
|
-
/** Extract
|
|
141
|
-
function
|
|
142
|
-
if (!Array.isArray(content)) return
|
|
143
|
-
|
|
144
|
-
for (const block of content)
|
|
145
|
-
|
|
155
|
+
/** Extract stored-image references from a ContentBlock list (reference form). */
|
|
156
|
+
function nodeImagesOf(content) {
|
|
157
|
+
if (!Array.isArray(content)) return [];
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const block of content) {
|
|
160
|
+
if (block === null || typeof block !== "object") continue;
|
|
161
|
+
const b = block;
|
|
162
|
+
if (b.type !== "image") continue;
|
|
163
|
+
const a = b.attachment;
|
|
164
|
+
if (a === null || typeof a !== "object") continue;
|
|
165
|
+
const ref = a;
|
|
166
|
+
if (typeof ref.attachmentId !== "string" || ref.attachmentId === "") continue;
|
|
167
|
+
out.push({
|
|
168
|
+
attachmentId: ref.attachmentId,
|
|
169
|
+
mediaType: typeof ref.mediaType === "string" ? ref.mediaType : "image/png",
|
|
170
|
+
width: typeof ref.width === "number" ? ref.width : 0,
|
|
171
|
+
height: typeof ref.height === "number" ? ref.height : 0
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
146
175
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
/** Convert a ContentBlock list to displayable image specs; inline base64
|
|
177
|
+
* blocks (rare in replayed history) become data URLs on the spot. */
|
|
178
|
+
function imageSpecsOfContent(content) {
|
|
179
|
+
const specs = [];
|
|
180
|
+
if (!Array.isArray(content)) return specs;
|
|
181
|
+
for (const block of content) {
|
|
182
|
+
if (block === null || typeof block !== "object") continue;
|
|
183
|
+
const b = block;
|
|
184
|
+
if (b.type !== "image") continue;
|
|
185
|
+
const a = b.attachment;
|
|
186
|
+
if (a !== null && typeof a === "object") {
|
|
187
|
+
const ref = a;
|
|
188
|
+
if (typeof ref.attachmentId === "string" && ref.attachmentId !== "") {
|
|
189
|
+
specs.push({
|
|
190
|
+
kind: "ref",
|
|
191
|
+
attachmentId: ref.attachmentId,
|
|
192
|
+
mediaType: typeof ref.mediaType === "string" ? ref.mediaType : "image/png"
|
|
193
|
+
});
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (typeof b.data === "string" && b.data !== "") specs.push({
|
|
198
|
+
kind: "data",
|
|
199
|
+
src: `data:${typeof b.mediaType === "string" ? b.mediaType : "image/png"};base64,${b.data}`
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return specs;
|
|
151
203
|
}
|
|
152
204
|
/** Normalize one projection entry to a rail message. */
|
|
153
205
|
function normalize(m) {
|
|
@@ -155,15 +207,41 @@ window.__ModuleLoader__.load({
|
|
|
155
207
|
const o = m;
|
|
156
208
|
if (typeof o.seq !== "number") return null;
|
|
157
209
|
const text = typeof o.text === "string" ? o.text : typeof o.preview === "string" ? o.preview : "";
|
|
210
|
+
const images = Array.isArray(o.images) ? o.images.map(nodeImagesOfEntry).filter((i) => i !== null) : void 0;
|
|
158
211
|
return {
|
|
159
212
|
seq: o.seq,
|
|
160
213
|
time: typeof o.time === "number" ? o.time : 0,
|
|
161
214
|
text,
|
|
162
215
|
hasImage: o.hasImage === true,
|
|
216
|
+
...images !== void 0 && images.length > 0 ? { images } : {},
|
|
163
217
|
...typeof o.key === "string" ? { key: o.key } : {},
|
|
164
218
|
...typeof o.id === "string" ? { id: o.id } : {}
|
|
165
219
|
};
|
|
166
220
|
}
|
|
221
|
+
/** Normalize one projection entry of the `images` array (wire form). */
|
|
222
|
+
function nodeImagesOfEntry(v) {
|
|
223
|
+
if (v === null || typeof v !== "object") return null;
|
|
224
|
+
const o = v;
|
|
225
|
+
if (typeof o.attachmentId !== "string" || o.attachmentId === "") return null;
|
|
226
|
+
return {
|
|
227
|
+
attachmentId: o.attachmentId,
|
|
228
|
+
mediaType: typeof o.mediaType === "string" ? o.mediaType : "image/png",
|
|
229
|
+
width: typeof o.width === "number" ? o.width : 0,
|
|
230
|
+
height: typeof o.height === "number" ? o.height : 0
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Extract preview text from a user message's ContentBlock list. */
|
|
234
|
+
function userTextOf(content) {
|
|
235
|
+
if (!Array.isArray(content)) return "";
|
|
236
|
+
let out = "";
|
|
237
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
238
|
+
return out.trim().slice(0, 80);
|
|
239
|
+
}
|
|
240
|
+
/** Whether a ContentBlock list carries an image block (rc.8 attachments). */
|
|
241
|
+
function userHasImage(content) {
|
|
242
|
+
if (!Array.isArray(content)) return false;
|
|
243
|
+
return content.some((block) => block !== null && typeof block === "object" && block.type === "image");
|
|
244
|
+
}
|
|
167
245
|
/** Fallback collector: enumerate user messages from the loaded chat nodes. */
|
|
168
246
|
function collectFromNodes(snapshot) {
|
|
169
247
|
const out = [];
|
|
@@ -179,11 +257,13 @@ window.__ModuleLoader__.load({
|
|
|
179
257
|
if (typeof data.time !== "number" || !Array.isArray(data.content)) continue;
|
|
180
258
|
const key = typeof n.key === "string" ? n.key : void 0;
|
|
181
259
|
if (key === void 0) continue;
|
|
260
|
+
const images = nodeImagesOf(data.content);
|
|
182
261
|
out.push({
|
|
183
262
|
seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
|
|
184
263
|
time: data.time,
|
|
185
264
|
text: userTextOf(data.content),
|
|
186
265
|
hasImage: userHasImage(data.content),
|
|
266
|
+
...images.length > 0 ? { images } : {},
|
|
187
267
|
key
|
|
188
268
|
});
|
|
189
269
|
}
|
|
@@ -195,6 +275,33 @@ window.__ModuleLoader__.load({
|
|
|
195
275
|
if (typeof m.key === "string" && m.key !== "") return m.key;
|
|
196
276
|
if (typeof m.id === "string" && m.id !== "") return "13:input-message" + m.id;
|
|
197
277
|
}
|
|
278
|
+
/** Full text of a rail message from the loaded chat nodes (uncapped),
|
|
279
|
+
* falling back to the projection preview when the node is not mounted. */
|
|
280
|
+
function fullTextOf(m, nodeSnapshot) {
|
|
281
|
+
const key = anchorKeyOf(m);
|
|
282
|
+
const nodes = nodeSnapshot?.chat?.nodes;
|
|
283
|
+
const content = (key === void 0 ? void 0 : nodes?.get(key))?.data?.content;
|
|
284
|
+
if (Array.isArray(content)) {
|
|
285
|
+
let out = "";
|
|
286
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
287
|
+
const full = out.trim();
|
|
288
|
+
if (full !== "") return full;
|
|
289
|
+
}
|
|
290
|
+
return m.text;
|
|
291
|
+
}
|
|
292
|
+
/** Displayable tip images for one rail message: host projection references
|
|
293
|
+
* first (works even before the node window covers the message), then the
|
|
294
|
+
* loaded chat node's content blocks. */
|
|
295
|
+
function tipImagesOf(m, nodeSnapshot) {
|
|
296
|
+
if (Array.isArray(m.images) && m.images.length > 0) return m.images.map((img) => ({
|
|
297
|
+
kind: "ref",
|
|
298
|
+
attachmentId: img.attachmentId,
|
|
299
|
+
mediaType: img.mediaType
|
|
300
|
+
}));
|
|
301
|
+
const key = anchorKeyOf(m);
|
|
302
|
+
const nodes = nodeSnapshot?.chat?.nodes;
|
|
303
|
+
return imageSpecsOfContent((key === void 0 ? void 0 : nodes?.get(key))?.data?.content);
|
|
304
|
+
}
|
|
198
305
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
199
306
|
/**
|
|
200
307
|
* Ensure the message node is loaded into the visible window, then scroll to it.
|
|
@@ -252,6 +359,78 @@ window.__ModuleLoader__.load({
|
|
|
252
359
|
});
|
|
253
360
|
return true;
|
|
254
361
|
}
|
|
362
|
+
/** Session-scoped browser URL cache for resolved attachment bytes (one entry
|
|
363
|
+
* per session:attachment; a failed load evicts itself so the next hover
|
|
364
|
+
* retries instead of showing a permanently broken thumbnail). */
|
|
365
|
+
const thumbUrls = /* @__PURE__ */ new Map();
|
|
366
|
+
/** Fallback data URL when the browser cannot mint object URLs. */
|
|
367
|
+
function bytesToDataUrl(data, mediaType) {
|
|
368
|
+
let binary = "";
|
|
369
|
+
const chunk = 32768;
|
|
370
|
+
for (let offset = 0; offset < data.length; offset += chunk) binary += String.fromCharCode(...data.subarray(offset, offset + chunk));
|
|
371
|
+
return `data:${mediaType};base64,${btoa(binary)}`;
|
|
372
|
+
}
|
|
373
|
+
/** Resolve (and cache) one stored image to a displayable URL. */
|
|
374
|
+
function resolveThumb(sessionId, read, image) {
|
|
375
|
+
const key = `${sessionId}:${image.attachmentId}`;
|
|
376
|
+
let pending = thumbUrls.get(key);
|
|
377
|
+
if (pending === void 0) {
|
|
378
|
+
pending = read(image.attachmentId).then((result) => {
|
|
379
|
+
if (!result.ok || result.value === void 0) throw new Error(result.error?.message ?? result.error?.code ?? "readAttachment failed");
|
|
380
|
+
const data = result.value.data;
|
|
381
|
+
if (data === void 0) throw new Error("readAttachment resolved no bytes");
|
|
382
|
+
const mediaType = result.value.attachment?.mediaType ?? image.mediaType;
|
|
383
|
+
if (typeof URL.createObjectURL === "function") return URL.createObjectURL(new Blob([data], { type: mediaType }));
|
|
384
|
+
return bytesToDataUrl(data, mediaType);
|
|
385
|
+
});
|
|
386
|
+
pending.catch(() => {
|
|
387
|
+
thumbUrls.delete(key);
|
|
388
|
+
});
|
|
389
|
+
thumbUrls.set(key, pending);
|
|
390
|
+
}
|
|
391
|
+
return pending;
|
|
392
|
+
}
|
|
393
|
+
/** One tip thumbnail: resolves its attachment lazily, shows a pulsing
|
|
394
|
+
* placeholder while loading, and removes itself on failure. */
|
|
395
|
+
function TipThumb({ spec, sessionId, read }) {
|
|
396
|
+
const [src, setSrc] = (0, react.useState)(null);
|
|
397
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
398
|
+
const specKey = spec.kind === "ref" ? `ref:${spec.attachmentId}` : "data";
|
|
399
|
+
(0, react.useEffect)(() => {
|
|
400
|
+
let alive = true;
|
|
401
|
+
setSrc(null);
|
|
402
|
+
setFailed(false);
|
|
403
|
+
if (spec.kind === "data") {
|
|
404
|
+
setSrc(spec.src);
|
|
405
|
+
return () => {
|
|
406
|
+
alive = false;
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
resolveThumb(sessionId, read, spec).then((url) => {
|
|
410
|
+
if (alive) setSrc(url);
|
|
411
|
+
}).catch(() => {
|
|
412
|
+
if (alive) setFailed(true);
|
|
413
|
+
});
|
|
414
|
+
return () => {
|
|
415
|
+
alive = false;
|
|
416
|
+
};
|
|
417
|
+
}, [
|
|
418
|
+
specKey,
|
|
419
|
+
sessionId,
|
|
420
|
+
read
|
|
421
|
+
]);
|
|
422
|
+
if (failed) return null;
|
|
423
|
+
if (src === null) return (0, react.createElement)("div", {
|
|
424
|
+
className: S.tipImgPh,
|
|
425
|
+
"aria-hidden": true
|
|
426
|
+
});
|
|
427
|
+
return (0, react.createElement)("img", {
|
|
428
|
+
className: S.tipImg,
|
|
429
|
+
src,
|
|
430
|
+
alt: "",
|
|
431
|
+
draggable: false
|
|
432
|
+
});
|
|
433
|
+
}
|
|
255
434
|
/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */
|
|
256
435
|
function langStrings() {
|
|
257
436
|
const lang = typeof document !== "undefined" ? (document.documentElement.lang || "zh").toLowerCase() : "zh";
|
|
@@ -272,8 +451,21 @@ window.__ModuleLoader__.load({
|
|
|
272
451
|
const [tip, setTip] = (0, react.useState)(null);
|
|
273
452
|
const navRef = (0, react.useRef)(null);
|
|
274
453
|
const expandedRef = (0, react.useRef)(false);
|
|
454
|
+
const lastPointerRef = (0, react.useRef)(null);
|
|
275
455
|
const jumpAbortRef = (0, react.useRef)(null);
|
|
276
456
|
(0, react.useEffect)(() => () => jumpAbortRef.current?.abort(), []);
|
|
457
|
+
(0, react.useEffect)(() => {
|
|
458
|
+
const el = navRef.current;
|
|
459
|
+
if (el === null) return;
|
|
460
|
+
const onMove = (e) => {
|
|
461
|
+
lastPointerRef.current = {
|
|
462
|
+
x: e.clientX,
|
|
463
|
+
y: e.clientY
|
|
464
|
+
};
|
|
465
|
+
};
|
|
466
|
+
el.addEventListener("pointermove", onMove, { passive: true });
|
|
467
|
+
return () => el.removeEventListener("pointermove", onMove);
|
|
468
|
+
}, []);
|
|
277
469
|
/** Position the tip against the item's CURRENT (post-expand) rect. */
|
|
278
470
|
const positionTip = (index) => {
|
|
279
471
|
if (index < 0) return;
|
|
@@ -306,28 +498,37 @@ window.__ModuleLoader__.load({
|
|
|
306
498
|
}
|
|
307
499
|
const el = navRef.current;
|
|
308
500
|
if (el === null) return;
|
|
309
|
-
|
|
310
|
-
|
|
501
|
+
let settled = false;
|
|
502
|
+
const settle = () => {
|
|
503
|
+
if (settled) return;
|
|
504
|
+
settled = true;
|
|
311
505
|
expandedRef.current = true;
|
|
312
506
|
el.removeEventListener("transitionend", onTransitionEnd);
|
|
507
|
+
clearTimeout(timer);
|
|
508
|
+
const p = lastPointerRef.current;
|
|
509
|
+
if (p !== null) {
|
|
510
|
+
const hit = document.elementFromPoint(p.x, p.y);
|
|
511
|
+
const item = hit === null ? null : hit.closest("[data-crl-index]");
|
|
512
|
+
const idx = item === null ? -1 : Number(item.getAttribute("data-crl-index"));
|
|
513
|
+
if (Number.isInteger(idx) && idx >= 0) positionTip(idx);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
const onTransitionEnd = (e) => {
|
|
517
|
+
if (e.propertyName !== "width") return;
|
|
518
|
+
settle();
|
|
313
519
|
};
|
|
314
520
|
el.addEventListener("transitionend", onTransitionEnd);
|
|
315
|
-
|
|
521
|
+
const timer = setTimeout(settle, 300);
|
|
522
|
+
return () => {
|
|
523
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
524
|
+
clearTimeout(timer);
|
|
525
|
+
};
|
|
316
526
|
}, [show]);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const content = (key === void 0 ? void 0 : nodes?.get(key))?.data?.content;
|
|
323
|
-
if (Array.isArray(content)) {
|
|
324
|
-
let out = "";
|
|
325
|
-
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
326
|
-
const full = out.trim();
|
|
327
|
-
if (full !== "") return full;
|
|
328
|
-
}
|
|
329
|
-
return m.text;
|
|
330
|
-
};
|
|
527
|
+
const readThumb = (0, react.useMemo)(() => {
|
|
528
|
+
if (sessionId === void 0) return void 0;
|
|
529
|
+
const s = sessionsService.binding(sessionId)?.session;
|
|
530
|
+
return s?.readAttachment === void 0 ? void 0 : (id) => s.readAttachment(id);
|
|
531
|
+
}, [sessionId, sessionsService]);
|
|
331
532
|
(0, react.useEffect)(() => {
|
|
332
533
|
if (session === void 0) return;
|
|
333
534
|
if (Array.isArray(projected?.messages) && projected.messages.length > 0) return;
|
|
@@ -440,9 +641,31 @@ window.__ModuleLoader__.load({
|
|
|
440
641
|
(0, react.createElement)("span", { className: S.title }, m.text === "" ? t.noText : m.text),
|
|
441
642
|
m.hasImage ? (0, react.createElement)("span", {
|
|
442
643
|
className: S.img,
|
|
443
|
-
|
|
644
|
+
role: "img",
|
|
444
645
|
"aria-label": t.hasImage
|
|
445
|
-
}
|
|
646
|
+
}, (0, react.createElement)("svg", {
|
|
647
|
+
viewBox: "0 0 16 16",
|
|
648
|
+
width: 12,
|
|
649
|
+
height: 12,
|
|
650
|
+
fill: "none",
|
|
651
|
+
stroke: "currentColor",
|
|
652
|
+
strokeWidth: 1.4,
|
|
653
|
+
strokeLinecap: "round",
|
|
654
|
+
strokeLinejoin: "round",
|
|
655
|
+
"aria-hidden": true
|
|
656
|
+
}, (0, react.createElement)("rect", {
|
|
657
|
+
x: 1.7,
|
|
658
|
+
y: 2.7,
|
|
659
|
+
width: 12.6,
|
|
660
|
+
height: 10.6,
|
|
661
|
+
rx: 2.2
|
|
662
|
+
}), (0, react.createElement)("circle", {
|
|
663
|
+
cx: 5.6,
|
|
664
|
+
cy: 6.4,
|
|
665
|
+
r: 1.15,
|
|
666
|
+
fill: "currentColor",
|
|
667
|
+
stroke: "none"
|
|
668
|
+
}), (0, react.createElement)("path", { d: "M2.5 11.6l3.6-3.1 2.6 2.2 2.1-1.9 3.3 2.8" }))) : null,
|
|
446
669
|
(0, react.createElement)("span", { className: S.time }, relativeTime(m.time, t)),
|
|
447
670
|
(0, react.createElement)("span", {
|
|
448
671
|
className: S.ind,
|
|
@@ -469,7 +692,31 @@ window.__ModuleLoader__.load({
|
|
|
469
692
|
top: `${tip.y}px`,
|
|
470
693
|
transform: "translateX(-100%)"
|
|
471
694
|
}
|
|
472
|
-
},
|
|
695
|
+
}, (() => {
|
|
696
|
+
const m = messages[tip.index];
|
|
697
|
+
const specs = tipImagesOf(m, nodeSnapshot);
|
|
698
|
+
const children = [];
|
|
699
|
+
if (specs.length > 0 && readThumb !== void 0) children.push((0, react.createElement)("div", {
|
|
700
|
+
className: S.tipImgs,
|
|
701
|
+
key: "imgs"
|
|
702
|
+
}, (0, react.createElement)("div", {
|
|
703
|
+
className: S.tipImgWrap,
|
|
704
|
+
key: "img"
|
|
705
|
+
}, (0, react.createElement)(TipThumb, {
|
|
706
|
+
spec: specs[0],
|
|
707
|
+
sessionId,
|
|
708
|
+
read: readThumb
|
|
709
|
+
}), specs.length > 1 ? (0, react.createElement)("span", {
|
|
710
|
+
className: S.tipImgCount,
|
|
711
|
+
"aria-hidden": true
|
|
712
|
+
}, `+${specs.length - 1}`) : null)));
|
|
713
|
+
else if (m.hasImage) children.push((0, react.createElement)("span", {
|
|
714
|
+
className: S.tipBadge,
|
|
715
|
+
key: "badge"
|
|
716
|
+
}, t.hasImage));
|
|
717
|
+
children.push((0, react.createElement)("span", { key: "text" }, fullTextOf(m, nodeSnapshot) || t.noText));
|
|
718
|
+
return children;
|
|
719
|
+
})()) : null], document.body);
|
|
473
720
|
}
|
|
474
721
|
function apply(ctx) {
|
|
475
722
|
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
@@ -482,7 +729,9 @@ window.__ModuleLoader__.load({
|
|
|
482
729
|
//#endregion
|
|
483
730
|
exports.TimelineRail = TimelineRail;
|
|
484
731
|
exports.apply = apply;
|
|
732
|
+
exports.imageSpecsOfContent = imageSpecsOfContent;
|
|
485
733
|
exports.inject = inject;
|
|
734
|
+
exports.normalize = normalize;
|
|
486
735
|
return module.exports;
|
|
487
736
|
}
|
|
488
737
|
});
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["useSyncExternalStore","useState","useRef","createElement","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions']\r\n\r\n// ---- i18n (DSH locale-aware, zh/en) ----\r\ntype LocaleId = 'zh' | 'en'\r\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\r\n zh: {\r\n railLabel: '消息导航',\r\n roleUser: '用户',\r\n noText: '(无文本内容)',\r\n ariaJump: '跳转到消息',\r\n hasImage: '含图片',\r\n loading: '加载中…',\r\n timeJustNow: '刚刚',\r\n timeMinutes: '{n}分钟前',\r\n timeHours: '{n}小时前',\r\n timeDays: '{n}天前',\r\n },\r\n en: {\r\n railLabel: 'Message rail',\r\n roleUser: 'User',\r\n noText: '(no text)',\r\n ariaJump: 'Jump to message',\r\n hasImage: 'Has image',\r\n loading: 'Loading…',\r\n timeJustNow: 'just now',\r\n timeMinutes: '{n}m ago',\r\n timeHours: '{n}h ago',\r\n timeDays: '{n}d ago',\r\n },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 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:10px 0;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:thin;scrollbar-color:rgba(0,0,0,.18) transparent;transition:width .25s cubic-bezier(.4,0,.2,1),right var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),top var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),background .2s ease,border-color .2s ease,box-shadow .2s ease}',\r\n '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);scrollbar-color:rgba(255,255,255,.25) transparent}',\r\n '.crl_nav.crl_show{width:280px;overflow-y:auto;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)}',\r\n '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)}',\r\n '.crl_nav::-webkit-scrollbar{width:4px}',\r\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.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}',\r\n '.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}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:flex-end;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}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n '.crl_img{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:12px;height:12px;margin-left:6px;border:1px solid currentColor;border-radius:2px;opacity:.65}',\r\n '.crl_img::before{content:\"\";width:6px;height:6px;border:1px solid currentColor;border-radius:1px;background:radial-gradient(circle at 30% 30%,currentColor 0 1px,transparent 1.5px)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n '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)}',\r\n '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)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.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:none}',\r\n '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)))}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}}',\r\n].join('')\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n key?: string\r\n id?: string\r\n}\r\n\r\n/** Extract preview text from a user message's ContentBlock list. */\r\nfunction userTextOf(content: unknown): string {\r\n if (!Array.isArray(content)) return ''\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n return out.trim().slice(0, 80)\r\n}\r\n\r\n/** Whether a ContentBlock list carries an image block (rc.8 attachments). */\r\nfunction userHasImage(content: unknown): boolean {\r\n if (!Array.isArray(content)) return false\r\n return content.some(block => block !== null && typeof block === 'object'\r\n && (block as { type?: unknown }).type === 'image')\r\n}\r\n\r\n/** Normalize one projection entry to a rail message. */\r\nfunction normalize(m: unknown): RailMessage | null {\r\n if (m === null || typeof m !== 'object') return null\r\n const o = m as Record<string, unknown>\r\n if (typeof o.seq !== 'number') return null\r\n // Host projection emits `text`; keep `preview` as a defensive fallback in\r\n // case an older projection payload is still cached in the browser.\r\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\r\n return {\r\n seq: o.seq,\r\n time: typeof o.time === 'number' ? o.time : 0,\r\n text,\r\n // Older projections lack hasImage; defensive default false.\r\n hasImage: o.hasImage === true,\r\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes. */\r\nfunction collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n if (snapshot === undefined || (snapshot as { chat?: unknown }).chat === undefined) return out\r\n const chat = (snapshot as { chat: { nodes?: Map<unknown, unknown> } }).chat\r\n if (!chat.nodes) return out\r\n for (const node of chat.nodes.values()) {\r\n if (node === null || typeof node !== 'object') continue\r\n const n = node as { kind?: unknown; key?: unknown; anchorSeq?: unknown; data?: unknown }\r\n if (n.kind !== 'user') continue\r\n const data = n.data as { time?: unknown; content?: unknown } | null\r\n if (data === null || typeof data !== 'object') continue\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) continue\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) continue\r\n out.push({ seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0, time: data.time, text: userTextOf(data.content), hasImage: userHasImage(data.content), key })\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\r\n * in a long session needs many pages. Report page count through onProgress so\r\n * the UI can show a loading state instead of appearing frozen.\r\n *\r\n * The paged-in data lands in the session snapshot synchronously with\r\n * loadOlder's resolution, but the chat rows render through React — the DOM row\r\n * for the target key appears a beat later. We therefore poll for the row (with\r\n * a timeout) instead of querying once and giving up; otherwise a multi-page\r\n * jump would \"finish loading\" without scrolling and need a second click.\r\n */\r\nasync function jumpToMessage(\r\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\r\n sessionId: string,\r\n key: string,\r\n onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n let pages = 0\r\n let guard = 0\r\n let loaded = false\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> }; hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (snapshot?.chat?.nodes?.get(key) !== undefined) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\r\n return true\r\n}\r\n\r\n// ---- component ----\r\ninterface TimelineRailProps {\r\n useProjection: (key: string) => { messages?: unknown[] } | undefined\r\n sessionId?: SessionId\r\n sessionsService: ISessions\r\n}\r\n\r\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\r\nfunction langStrings(): Record<string, string> {\r\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\r\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\r\n}\r\n\r\nfunction TimelineRail({ useProjection, sessionId, sessionsService }: TimelineRailProps): ReactNode {\r\n const t = langStrings()\r\n const projected = useProjection('chatRail')\r\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\r\n const fallbackStore = session === undefined ? NOOP_STORE : session\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n const [activeIndex, setActiveIndex] = useState(-1)\r\n const [show, setShow] = useState(false)\r\n const [jumping, setJumping] = useState(false)\r\n // Hover full-content panel: index of the hovered item + fixed position.\r\n // The tip only triggers after the expand animation settles — in the\r\n // collapsed state the item rect is only 36px wide, so a tip positioned\r\n // there would be wrong once the rail expands.\r\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\r\n const navRef = useRef<HTMLDivElement | null>(null)\r\n // True only after the expand animation has fully settled; the width\r\n // transition takes ~250ms after `show` flips, and item rects are only\r\n // stable once it finishes. Tip positioning must wait for this.\r\n const expandedRef = useRef(false)\r\n // Aborts an in-flight jump (component unmount or a newer click superseding\r\n // an older one), so the loadOlder/DOM-poll loops stop promptly.\r\n const jumpAbortRef = useRef<AbortController | null>(null)\r\n useEffect(() => () => jumpAbortRef.current?.abort(), [])\r\n\r\n /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n return () => el.removeEventListener('transitionend', onTransitionEnd)\r\n }, [show])\r\n\r\n /** Full text of a rail message from the loaded chat nodes (uncapped),\r\n * falling back to the projection preview when the node is not mounted. */\r\n const fullTextOf = (m: RailMessage): string => {\r\n const key = anchorKeyOf(m)\r\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\r\n const content = node?.data?.content\r\n if (Array.isArray(content)) {\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n const full = out.trim()\r\n if (full !== '') return full\r\n }\r\n return m.text\r\n }\r\n\r\n // Background full-history load: follow the runtime's authoritative hasMore\r\n // flag, but STOP as soon as the projection delivers.\r\n useEffect(() => {\r\n if (session === undefined) return\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n let cancelled = false\r\n const run = async () => {\r\n let guard = 0\r\n while (!cancelled && guard++ < 120) {\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\r\n if (snap?.hasMore !== true) return\r\n if (snap.loadingOlder === true) { await delay(50); continue }\r\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\r\n }\r\n }\r\n run().catch(() => {})\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\r\n\r\n // Track the reading position (active item): nearest row to the 40% viewport line.\r\n useEffect(() => {\r\n if (messages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < messages.length; i++) {\r\n const key = anchorKeyOf(messages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, messages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n const items = messages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n setJumping(true)\r\n void jumpToMessage(sessionsService as never, sessionId as string, key, undefined, controller.signal)\r\n .finally(() => setJumping(false))\r\n },\r\n onMouseEnter: () => handleItemEnter(i),\r\n onMouseLeave: () => handleItemLeave(i),\r\n children: [\r\n createElement('span', { className: S.num }, `#${i + 1}`),\r\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage ? createElement('span', { className: S.img, title: t.hasImage, 'aria-label': t.hasImage }) : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item.\r\n tip !== null && tip.index >= 0 && tip.index < messages.length\r\n ? createElement('div', {\r\n className: S.tip,\r\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\r\n }, fullTextOf(messages[tip.index]) || t.noText)\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\r\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\r\n name: 'conversation.input.dock',\r\n id: 'chat-rail',\r\n order: 40,\r\n inject: () => ({ sessionsService: ctx.sessions }),\r\n }, TimelineRail))\r\n}\r\n\r\nexport { apply, TimelineRail }\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["useState","createElement","useSyncExternalStore","useRef","useMemo","createPortal"],"sources":["../src/client/index.tsx"],"sourcesContent":["/**\r\n * @max-null/dsh-chat-rail — web client half.\r\n *\r\n * Right-edge conversation navigation rail: one indicator per user message,\r\n * scroll-spy highlight of the reading position, hover preview, click to jump.\r\n *\r\n * Two deliberate fixes over the dsh-chat-timeline reference:\r\n *\r\n * 1. ANIMATION SYNC with dsh-better-sidebar. better-sidebar drives its layout\r\n * push through the `--dsh-sidebar-width` CSS variable on `:root` (the\r\n * `#root` margin-right transition reads the same variable). The rail's\r\n * `right` is `calc(var(--dsh-sidebar-width, 0px) + 12px)` with the same\r\n * transition timing, so rail and scrollport move together while the panel\r\n * expands — no \"panel first, scrollbar later\" lag.\r\n *\r\n * 2. VISIBILITY. The reference rail is 34px wide with 8×2px rgba(0,0,0,.16)\r\n * lines — nearly invisible on a light background. Here the rail keeps a\r\n * constant translucent capsule, thicker/darker indicator lines, and an\r\n * always-visible collapsed state (no \"reveal on pointer proximity\").\r\n *\r\n * Data: host `chatRail` projection first, loaded chat nodes fallback, then a\r\n * background loadOlder loop (stopped as soon as the projection delivers).\r\n * Mounted in conversation.input.dock, portal-rendered to body.\r\n */\r\n\r\nimport { createElement, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactNode } from 'react'\r\nimport { createPortal } from 'react-dom'\r\nimport type { ClientContext, SessionId } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport type { ISessions } from '@deepseek-ai/dsh-client-runtime/client'\r\n// Type-only: pulls the ui-conversation SlotMap merge (the input.dock entry).\r\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\r\n\r\nexport const inject = ['slots', 'sessions']\r\n\r\n// ---- i18n (DSH locale-aware, zh/en) ----\r\ntype LocaleId = 'zh' | 'en'\r\nconst STRINGS: Record<LocaleId, Record<string, string>> = {\r\n zh: {\r\n railLabel: '消息导航',\r\n roleUser: '用户',\r\n noText: '(无文本内容)',\r\n ariaJump: '跳转到消息',\r\n hasImage: '含图片',\r\n loading: '加载中…',\r\n timeJustNow: '刚刚',\r\n timeMinutes: '{n}分钟前',\r\n timeHours: '{n}小时前',\r\n timeDays: '{n}天前',\r\n },\r\n en: {\r\n railLabel: 'Message rail',\r\n roleUser: 'User',\r\n noText: '(no text)',\r\n ariaJump: 'Jump to message',\r\n hasImage: 'Has image',\r\n loading: 'Loading…',\r\n timeJustNow: 'just now',\r\n timeMinutes: '{n}m ago',\r\n timeHours: '{n}h ago',\r\n timeDays: '{n}d ago',\r\n },\r\n}\r\n\r\n/** Compact relative time for a message timestamp (zh/en via template). */\r\nfunction relativeTime(ts: number, s: Record<string, string>): string {\r\n const diff = Date.now() - ts\r\n if (diff < 60_000) return s.timeJustNow\r\n if (diff < 3_600_000) return s.timeMinutes.replace('{n}', String(Math.floor(diff / 60_000)))\r\n if (diff < 86_400_000) return s.timeHours.replace('{n}', String(Math.floor(diff / 3_600_000)))\r\n if (diff < 7 * 86_400_000) return s.timeDays.replace('{n}', String(Math.floor(diff / 86_400_000)))\r\n const d = new Date(ts)\r\n return `${d.getMonth() + 1}/${d.getDate()}`\r\n}\r\n\r\n// ---- styles ----\r\n// 画卷式导航:单个容器,折叠态 36px 竖条;hover 时宽度从右往左展开到 280px\r\n// (打开画卷效果)。容器垂直方向在「去掉底栏后的可用区域」内居中:\r\n// top = (100vh - 底栏高)/2 + translateY(-50%),底栏收起时正中、展开时上移避让。\r\n// right 跟随 --dsh-sidebar-width、top 跟随 --dsh-sidebar-height,与 better-sidebar\r\n// 面板共享同一 CSS 变量 + transition,动画同步。\r\nconst css = [\r\n '.crl_nav{user-select:none;z-index:100;position:fixed;right:calc(var(--dsh-sidebar-width,0px) + 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:10px 0;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:thin;scrollbar-color:rgba(0,0,0,.18) transparent;transition:width .25s cubic-bezier(.4,0,.2,1),right var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),top var(--ds-transition-duration-slow,0.3s) var(--ds-ease-in-out,ease-in-out),background .2s ease,border-color .2s ease,box-shadow .2s ease}',\r\n '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);scrollbar-color:rgba(255,255,255,.25) transparent}',\r\n '.crl_nav.crl_show{width:280px;overflow-y:auto;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)}',\r\n '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)}',\r\n '.crl_nav::-webkit-scrollbar{width:4px}',\r\n '.crl_nav::-webkit-scrollbar-thumb{background:rgba(0,0,0,.18);border-radius:4px}',\r\n // Jump-in-progress indicator: sticky row pinned at the rail top. The spinner\r\n // icon is always visible; the \"Loading…\" label only appears once expanded\r\n // (in the collapsed 36px rail the text would overflow the capsule).\r\n '.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}',\r\n '.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}',\r\n '.crl_loading .crl_loadingLabel{display:none}',\r\n '.crl_show .crl_loading .crl_loadingLabel{display:inline}',\r\n '@keyframes crl-spin{to{transform:rotate(360deg)}}',\r\n // Item row: always flex-end (the indicator rides the right edge through the\r\n // width animation, never jumping). The collapsed padding (0 9px) centers the\r\n // 18px indicator in the 36px capsule; the expanded padding (0 14px) clears\r\n // the text. Padding transitions with the same timing as the width, so the\r\n // indicator glides smoothly instead of snapping when text hides/shows.\r\n '.crl_item{cursor:pointer;flex-shrink:0;height:32px;min-height:32px;display:flex;justify-content:flex-end;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}',\r\n '.crl_show .crl_item{padding:0 14px}',\r\n '.crl_item:hover{color:rgba(0,0,0,.95)}',\r\n '.crl_item.crl_active{color:var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_item,[data-theme=\\'dark\\'] .crl_item,.dark .crl_item{color:rgba(255,255,255,.68)}',\r\n 'body[data-ds-dark-theme] .crl_item:hover,[data-theme=\\'dark\\'] .crl_item:hover,.dark .crl_item:hover{color:rgba(255,255,255,.95)}',\r\n // Title / ordinal / time: display:none in collapsed state (so they take no\r\n // flex space and the indicator line stays centered), revealed on expand.\r\n // The ordinal has a fixed width (right-aligned) so message text starts at\r\n // the same x across items; time sits AFTER the title (no fixed width, so it\r\n // never shrinks the text area).\r\n '.crl_title{display:none;font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;flex:1;min-width:0;text-align:left;overflow:hidden;color:inherit}',\r\n '.crl_show .crl_title{display:block;animation:crl-fade .18s ease}',\r\n '.crl_item.crl_active .crl_title{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:500}',\r\n '.crl_num{display:none;flex-shrink:0;width:28px;font-size:10px;line-height:20px;color:rgba(0,0,0,.35);margin-right:8px;text-align:right;user-select:none}',\r\n '.crl_show .crl_num{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_num,[data-theme=\\'dark\\'] .crl_num,.dark .crl_num{color:rgba(255,255,255,.35)}',\r\n '.crl_time{display:none;flex-shrink:0;font-size:10px;line-height:20px;color:rgba(0,0,0,.28);margin-right:10px;user-select:none;white-space:nowrap}',\r\n '.crl_show .crl_time{display:block;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_time,[data-theme=\\'dark\\'] .crl_time,.dark .crl_time{color:rgba(255,255,255,.28)}',\r\n '@keyframes crl-fade{from{opacity:0}to{opacity:1}}',\r\n // Indicator line: 10×3px, clearly visible in both themes.\r\n '.crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}',\r\n '.crl_show .crl_ind{margin-left:8px}',\r\n // Image badge: hidden in the collapsed state (the rail is a clean 36px\r\n // capsule of indicator lines; an icon there would crowd and distract) and\r\n // revealed with the other labels on expand. A quiet stroke glyph in a muted\r\n // tone that ignores the active-blue text color.\r\n '.crl_img{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-left:6px;color:rgba(0,0,0,.38)}',\r\n '.crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}',\r\n 'body[data-ds-dark-theme] .crl_img,[data-theme=\\'dark\\'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}',\r\n '.crl_line{background-color:rgba(0,0,0,.45);border-radius:4px;flex-shrink:0;width:10px;height:3px;transition:background-color .2s ease,transform .2s ease}',\r\n '.crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}',\r\n '.crl_item.crl_active .crl_line{background-color:var(--dsw-alias-state-business-primary,#4d6bfe);transform-origin:50%;transform:scale(1.4);box-shadow:0 0 6px var(--dsw-alias-state-business-primary,#4d6bfe)}',\r\n 'body[data-ds-dark-theme] .crl_line,[data-theme=\\'dark\\'] .crl_line,.dark .crl_line{background-color:rgba(255,255,255,.5)}',\r\n '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)}',\r\n '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)}',\r\n // Full-content hover panel: floats to the LEFT of the expanded rail list.\r\n '.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:none}',\r\n '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)))}',\r\n // Tip thumbnail gallery: stacked above the text, one row per image, capped\r\n // height so a tall screenshot cannot swallow the preview panel. The\r\n // placeholder pulses while session.readAttachment resolves the bytes; a\r\n // failed load removes the row entirely rather than showing a broken image.\r\n '.crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}',\r\n '.crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}',\r\n '.crl_tipImg{display:block;max-width:100%;max-height:150px;width:auto;height:auto;object-fit:contain;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,var(--border-default,rgba(0,0,0,.12)))}',\r\n '.crl_tipImgCount{position:absolute;right:6px;bottom:6px;padding:0 6px;font-size:10px;line-height:16px;border-radius:9px;background:rgba(0,0,0,.55);color:rgba(255,255,255,.95);pointer-events:none}',\r\n '.crl_tipImgPh{flex-shrink:0;height:56px;border-radius:8px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.05)));animation:crl-pulse 1.2s ease-in-out infinite}',\r\n 'body[data-ds-dark-theme] .crl_tipImg,[data-theme=\\'dark\\'] .crl_tipImg,.dark .crl_tipImg{border-color:var(--dsw-alias-border-l2,var(--border-default,rgba(255,255,255,.14)))}',\r\n '.crl_tipBadge{display:inline-block;margin-bottom:8px;padding:0 7px;font-size:10px;line-height:16px;border-radius:9px;background:var(--dsw-alias-surface-sunken,var(--bg-muted,rgba(0,0,0,.06)));color:var(--dsw-alias-label-secondary,var(--text-muted,rgba(0,0,0,.45)))}',\r\n '@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}',\r\n '@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}',\r\n].join('')\r\n\r\nconst STYLE_ID = '@max-null/dsh-chat-rail/styles.module.css'\r\nif (typeof document !== 'undefined' && document.querySelector(`style[data-plugin-css=\"${STYLE_ID}\"]`) === null) {\r\n const tag = document.createElement('style')\r\n tag.dataset.plugin = 'dsh-chat-rail'\r\n tag.dataset.pluginCss = STYLE_ID\r\n tag.textContent = css\r\n document.head.appendChild(tag)\r\n}\r\n\r\nconst S = {\r\n nav: 'crl_nav',\r\n navShow: 'crl_show',\r\n item: 'crl_item',\r\n itemActive: 'crl_active',\r\n title: 'crl_title',\r\n num: 'crl_num',\r\n time: 'crl_time',\r\n ind: 'crl_ind',\r\n line: 'crl_line',\r\n img: 'crl_img',\r\n loading: 'crl_loading',\r\n loadingLabel: 'crl_loadingLabel',\r\n tip: 'crl_tip',\r\n tipImgs: 'crl_tipImgs',\r\n tipImgWrap: 'crl_tipImgWrap',\r\n tipImg: 'crl_tipImg',\r\n tipImgCount: 'crl_tipImgCount',\r\n tipImgPh: 'crl_tipImgPh',\r\n tipBadge: 'crl_tipBadge',\r\n}\r\n\r\n// ---- data helpers ----\r\nconst NOOP_STORE = { getSnapshot: () => undefined, subscribe: () => () => {} }\r\n\r\ninterface RailImage {\r\n attachmentId: string\r\n mediaType: string\r\n width: number\r\n height: number\r\n}\r\n\r\ninterface RailMessage {\r\n seq: number\r\n time: number\r\n text: string\r\n hasImage: boolean\r\n /** Stored-image references from the host projection (empty when inline-only). */\r\n images?: RailImage[]\r\n key?: string\r\n id?: string\r\n}\r\n\r\n/** One displayable tip image: either a durable reference (resolved lazily) or\r\n * an inline base64 payload already usable as an <img> src. */\r\ntype ImageSpec =\r\n | { kind: 'ref'; attachmentId: string; mediaType: string }\r\n | { kind: 'data'; src: string }\r\n\r\n/** Extract stored-image references from a ContentBlock list (reference form). */\r\nfunction nodeImagesOf(content: unknown): RailImage[] {\r\n if (!Array.isArray(content)) return []\r\n const out: RailImage[] = []\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a === null || typeof a !== 'object') continue\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown; width?: unknown; height?: unknown }\r\n if (typeof ref.attachmentId !== 'string' || ref.attachmentId === '') continue\r\n out.push({\r\n attachmentId: ref.attachmentId,\r\n mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png',\r\n width: typeof ref.width === 'number' ? ref.width : 0,\r\n height: typeof ref.height === 'number' ? ref.height : 0,\r\n })\r\n }\r\n return out\r\n}\r\n\r\n/** Convert a ContentBlock list to displayable image specs; inline base64\r\n * blocks (rare in replayed history) become data URLs on the spot. */\r\nfunction imageSpecsOfContent(content: unknown): ImageSpec[] {\r\n const specs: ImageSpec[] = []\r\n if (!Array.isArray(content)) return specs\r\n for (const block of content) {\r\n if (block === null || typeof block !== 'object') continue\r\n const b = block as { type?: unknown; attachment?: unknown; data?: unknown; mediaType?: unknown }\r\n if (b.type !== 'image') continue\r\n const a = b.attachment\r\n if (a !== null && typeof a === 'object') {\r\n const ref = a as { attachmentId?: unknown; mediaType?: unknown }\r\n if (typeof ref.attachmentId === 'string' && ref.attachmentId !== '') {\r\n specs.push({ kind: 'ref', attachmentId: ref.attachmentId, mediaType: typeof ref.mediaType === 'string' ? ref.mediaType : 'image/png' })\r\n continue\r\n }\r\n }\r\n if (typeof b.data === 'string' && b.data !== '') {\r\n specs.push({ kind: 'data', src: `data:${typeof b.mediaType === 'string' ? b.mediaType : 'image/png'};base64,${b.data}` })\r\n }\r\n }\r\n return specs\r\n}\r\n\r\n/** Normalize one projection entry to a rail message. */\r\nfunction normalize(m: unknown): RailMessage | null {\r\n if (m === null || typeof m !== 'object') return null\r\n const o = m as Record<string, unknown>\r\n if (typeof o.seq !== 'number') return null\r\n // Host projection emits `text`; keep `preview` as a defensive fallback in\r\n // case an older projection payload is still cached in the browser.\r\n const text = typeof o.text === 'string' ? o.text : typeof o.preview === 'string' ? o.preview : ''\r\n const images = Array.isArray(o.images)\r\n ? o.images.map(nodeImagesOfEntry).filter((i): i is RailImage => i !== null)\r\n : undefined\r\n return {\r\n seq: o.seq,\r\n time: typeof o.time === 'number' ? o.time : 0,\r\n text,\r\n // Older projections lack hasImage; defensive default false.\r\n hasImage: o.hasImage === true,\r\n ...(images !== undefined && images.length > 0 ? { images } : {}),\r\n ...(typeof o.key === 'string' ? { key: o.key } : {}),\r\n ...(typeof o.id === 'string' ? { id: o.id } : {}),\r\n }\r\n}\r\n\r\n/** Normalize one projection entry of the `images` array (wire form). */\r\nfunction nodeImagesOfEntry(v: unknown): RailImage | null {\r\n if (v === null || typeof v !== 'object') return null\r\n const o = v as Record<string, unknown>\r\n if (typeof o.attachmentId !== 'string' || o.attachmentId === '') return null\r\n return {\r\n attachmentId: o.attachmentId,\r\n mediaType: typeof o.mediaType === 'string' ? o.mediaType : 'image/png',\r\n width: typeof o.width === 'number' ? o.width : 0,\r\n height: typeof o.height === 'number' ? o.height : 0,\r\n }\r\n}\r\n\r\n/** Extract preview text from a user message's ContentBlock list. */\r\nfunction userTextOf(content: unknown): string {\r\n if (!Array.isArray(content)) return ''\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n return out.trim().slice(0, 80)\r\n}\r\n\r\n/** Whether a ContentBlock list carries an image block (rc.8 attachments). */\r\nfunction userHasImage(content: unknown): boolean {\r\n if (!Array.isArray(content)) return false\r\n return content.some(block => block !== null && typeof block === 'object'\r\n && (block as { type?: unknown }).type === 'image')\r\n}\r\n\r\n/** Fallback collector: enumerate user messages from the loaded chat nodes. */\r\nfunction collectFromNodes(snapshot: unknown): RailMessage[] {\r\n const out: RailMessage[] = []\r\n if (snapshot === undefined || (snapshot as { chat?: unknown }).chat === undefined) return out\r\n const chat = (snapshot as { chat: { nodes?: Map<unknown, unknown> } }).chat\r\n if (!chat.nodes) return out\r\n for (const node of chat.nodes.values()) {\r\n if (node === null || typeof node !== 'object') continue\r\n const n = node as { kind?: unknown; key?: unknown; anchorSeq?: unknown; data?: unknown }\r\n if (n.kind !== 'user') continue\r\n const data = n.data as { time?: unknown; content?: unknown } | null\r\n if (data === null || typeof data !== 'object') continue\r\n if (typeof data.time !== 'number' || !Array.isArray(data.content)) continue\r\n const key = typeof n.key === 'string' ? n.key : undefined\r\n if (key === undefined) continue\r\n const images = nodeImagesOf(data.content)\r\n out.push({\r\n seq: typeof n.anchorSeq === 'number' ? n.anchorSeq : 0,\r\n time: data.time,\r\n text: userTextOf(data.content),\r\n hasImage: userHasImage(data.content),\r\n ...(images.length > 0 ? { images } : {}),\r\n key,\r\n })\r\n }\r\n out.sort((a, b) => a.seq - b.seq)\r\n return out\r\n}\r\n\r\n/** Resolve the chat node's data-chat-anchor-key (direct key or id-reconstructed). */\r\nfunction anchorKeyOf(m: RailMessage): string | undefined {\r\n if (typeof m.key === 'string' && m.key !== '') return m.key\r\n if (typeof m.id === 'string' && m.id !== '') return '13:input-message' + m.id\r\n return undefined\r\n}\r\n\r\n/** Full text of a rail message from the loaded chat nodes (uncapped),\r\n * falling back to the projection preview when the node is not mounted. */\r\nfunction fullTextOf(m: RailMessage, nodeSnapshot: unknown): string {\r\n const key = anchorKeyOf(m)\r\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\r\n const content = node?.data?.content\r\n if (Array.isArray(content)) {\r\n let out = ''\r\n for (const block of content) {\r\n if (block !== null && typeof block === 'object' && (block as { type?: unknown }).type === 'text'\r\n && typeof (block as { text?: unknown }).text === 'string') {\r\n out += (block as { text: string }).text\r\n }\r\n }\r\n const full = out.trim()\r\n if (full !== '') return full\r\n }\r\n return m.text\r\n}\r\n\r\n/** Displayable tip images for one rail message: host projection references\r\n * first (works even before the node window covers the message), then the\r\n * loaded chat node's content blocks. */\r\nfunction tipImagesOf(m: RailMessage, nodeSnapshot: unknown): ImageSpec[] {\r\n if (Array.isArray(m.images) && m.images.length > 0) {\r\n return m.images.map(img => ({ kind: 'ref', attachmentId: img.attachmentId, mediaType: img.mediaType }))\r\n }\r\n const key = anchorKeyOf(m)\r\n const nodes = (nodeSnapshot as { chat?: { nodes?: Map<string, { data?: { content?: unknown } }> } } | undefined)?.chat?.nodes\r\n const node = key === undefined ? undefined : nodes?.get(key)\r\n return imageSpecsOfContent(node?.data?.content)\r\n}\r\n\r\nconst delay = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms))\r\n\r\n/**\r\n * Ensure the message node is loaded into the visible window, then scroll to it.\r\n * loadOlder pages 50 messages at a time (DSH PAGE_MESSAGES), so a jump far back\r\n * in a long session needs many pages. Report page count through onProgress so\r\n * the UI can show a loading state instead of appearing frozen.\r\n *\r\n * The paged-in data lands in the session snapshot synchronously with\r\n * loadOlder's resolution, but the chat rows render through React — the DOM row\r\n * for the target key appears a beat later. We therefore poll for the row (with\r\n * a timeout) instead of querying once and giving up; otherwise a multi-page\r\n * jump would \"finish loading\" without scrolling and need a second click.\r\n */\r\nasync function jumpToMessage(\r\n sessionsService: { binding: (id: string) => { session?: { getSnapshot(): unknown; loadOlder(): Promise<unknown>; hasMore?: boolean; loadingOlder?: boolean } } | undefined },\r\n sessionId: string,\r\n key: string,\r\n onProgress?: (pages: number) => void,\r\n signal?: AbortSignal,\r\n): Promise<boolean> {\r\n const session = sessionsService.binding(sessionId)?.session\r\n if (session === undefined) return false\r\n let pages = 0\r\n let guard = 0\r\n let loaded = false\r\n while (guard++ < 120) {\r\n if (signal?.aborted) return false\r\n const snapshot = session.getSnapshot() as { chat?: { nodes?: Map<string, unknown> }; hasMore?: boolean; loadingOlder?: boolean } | undefined\r\n if (snapshot?.chat?.nodes?.get(key) !== undefined) {\r\n loaded = true\r\n break\r\n }\r\n if (snapshot?.hasMore !== true) break\r\n if (snapshot.loadingOlder === true) {\r\n // Another loader owns the current page; wait for it without busy-spinning.\r\n await delay(50)\r\n continue\r\n }\r\n await session.loadOlder()\r\n pages++\r\n // Report progress only on page boundaries, not every loop iteration.\r\n onProgress?.(pages)\r\n }\r\n // Page guard exhausted (or hasMore ran out) without the node materializing:\r\n // fail fast with context instead of burning the DOM poll below.\r\n if (!loaded) {\r\n console.warn(`[chat-rail] jumpToMessage: node \"${key}\" not loaded after ${pages} page(s)`)\r\n return false\r\n }\r\n const scrollport = typeof document !== 'undefined' ? document.querySelector('[data-conversation-scroll]') : null\r\n if (scrollport === null) return false\r\n // The snapshot is authoritative for \"loaded\", but the row's DOM node only\r\n // appears once React renders the prepended page. Poll for it (up to ~5s) so a\r\n // freshly paged-in jump scrolls on the same click.\r\n let row: Element | null = null\r\n let waited = 0\r\n while (waited++ < 100) {\r\n if (signal?.aborted) return false\r\n row = scrollport.querySelector(`[data-chat-anchor-key=\"${CSS.escape(key)}\"]`)\r\n if (row !== null) break\r\n await delay(50)\r\n }\r\n if (row === null) return false\r\n const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches\r\n row.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth', block: 'center' })\r\n return true\r\n}\r\n\r\n// ---- tip image thumbnails ----\r\n\r\n/** Session-scoped browser URL cache for resolved attachment bytes (one entry\r\n * per session:attachment; a failed load evicts itself so the next hover\r\n * retries instead of showing a permanently broken thumbnail). */\r\nconst thumbUrls = new Map<string, Promise<string>>()\r\n\r\n/** Minimal RpcResult shape of session.readAttachment (contract level). */\r\ninterface ReadAttachmentResult {\r\n ok: boolean\r\n value?: { attachment?: { mediaType?: string }; data?: Uint8Array }\r\n error?: { code?: string; message?: string }\r\n}\r\n\r\ntype ReadAttachmentFn = (attachmentId: string) => Promise<ReadAttachmentResult>\r\n\r\n/** Fallback data URL when the browser cannot mint object URLs. */\r\nfunction bytesToDataUrl(data: Uint8Array, mediaType: string): string {\r\n let binary = ''\r\n const chunk = 0x8000\r\n for (let offset = 0; offset < data.length; offset += chunk) {\r\n binary += String.fromCharCode(...data.subarray(offset, offset + chunk))\r\n }\r\n return `data:${mediaType};base64,${btoa(binary)}`\r\n}\r\n\r\n/** Resolve (and cache) one stored image to a displayable URL. */\r\nfunction resolveThumb(sessionId: string, read: ReadAttachmentFn, image: Pick<RailImage, 'attachmentId' | 'mediaType'>): Promise<string> {\r\n const key = `${sessionId}:${image.attachmentId}`\r\n let pending = thumbUrls.get(key)\r\n if (pending === undefined) {\r\n pending = read(image.attachmentId).then((result): string => {\r\n if (!result.ok || result.value === undefined) {\r\n throw new Error(result.error?.message ?? result.error?.code ?? 'readAttachment failed')\r\n }\r\n const data = result.value.data\r\n if (data === undefined) throw new Error('readAttachment resolved no bytes')\r\n const mediaType = result.value.attachment?.mediaType ?? image.mediaType\r\n if (typeof URL.createObjectURL === 'function') {\r\n return URL.createObjectURL(new Blob([data as BlobPart], { type: mediaType }))\r\n }\r\n return bytesToDataUrl(data, mediaType)\r\n })\r\n void pending.catch(() => { thumbUrls.delete(key) })\r\n thumbUrls.set(key, pending)\r\n }\r\n return pending\r\n}\r\n\r\n/** One tip thumbnail: resolves its attachment lazily, shows a pulsing\r\n * placeholder while loading, and removes itself on failure. */\r\nfunction TipThumb({ spec, sessionId, read }: {\r\n spec: ImageSpec\r\n sessionId: string\r\n read: ReadAttachmentFn\r\n}): ReactNode {\r\n const [src, setSrc] = useState<string | null>(null)\r\n const [failed, setFailed] = useState(false)\r\n const specKey = spec.kind === 'ref' ? `ref:${spec.attachmentId}` : 'data'\r\n useEffect(() => {\r\n let alive = true\r\n setSrc(null)\r\n setFailed(false)\r\n if (spec.kind === 'data') {\r\n setSrc(spec.src)\r\n return () => { alive = false }\r\n }\r\n resolveThumb(sessionId, read, spec)\r\n .then((url) => { if (alive) setSrc(url) })\r\n .catch(() => { if (alive) setFailed(true) })\r\n return () => { alive = false }\r\n // spec is a fresh object each render; the string key is the stable identity.\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [specKey, sessionId, read])\r\n if (failed) return null\r\n if (src === null) return createElement('div', { className: S.tipImgPh, 'aria-hidden': true })\r\n return createElement('img', { className: S.tipImg, src, alt: '', draggable: false })\r\n}\r\n\r\n// ---- component ----\r\ninterface TimelineRailProps {\r\n useProjection: (key: string) => { messages?: unknown[] } | undefined\r\n sessionId?: SessionId\r\n sessionsService: ISessions\r\n}\r\n\r\n/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */\r\nfunction langStrings(): Record<string, string> {\r\n const lang = typeof document !== 'undefined' ? (document.documentElement.lang || 'zh').toLowerCase() : 'zh'\r\n return STRINGS[lang.startsWith('zh') ? 'zh' : 'en']\r\n}\r\n\r\nfunction TimelineRail({ useProjection, sessionId, sessionsService }: TimelineRailProps): ReactNode {\r\n const t = langStrings()\r\n const projected = useProjection('chatRail')\r\n const session = sessionId === undefined ? undefined : (sessionsService.binding(sessionId) as { session?: { subscribe(cb: () => void): () => void; getSnapshot(): unknown } } | undefined)?.session\r\n const fallbackStore = session === undefined ? NOOP_STORE : session\r\n const nodeSnapshot = useSyncExternalStore(\r\n (cb) => fallbackStore.subscribe(cb),\r\n () => fallbackStore.getSnapshot(),\r\n )\r\n\r\n let messages: RailMessage[] = []\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) {\r\n messages = projected.messages.map(normalize).filter((m): m is RailMessage => m !== null)\r\n }\r\n if (messages.length === 0) {\r\n messages = collectFromNodes(nodeSnapshot)\r\n }\r\n\r\n const [activeIndex, setActiveIndex] = useState(-1)\r\n const [show, setShow] = useState(false)\r\n const [jumping, setJumping] = useState(false)\r\n // Hover full-content panel: index of the hovered item + fixed position.\r\n // The tip only triggers after the expand animation settles — in the\r\n // collapsed state the item rect is only 36px wide, so a tip positioned\r\n // there would be wrong once the rail expands.\r\n const [tip, setTip] = useState<{ index: number; x: number; y: number } | null>(null)\r\n const navRef = useRef<HTMLDivElement | null>(null) // True only after the expand animation has fully settled; the width\r\n // transition takes ~250ms after `show` flips, and item rects are only\r\n // stable once it finishes. Tip positioning must wait for this.\r\n const expandedRef = useRef(false)\r\n // Last observed pointer position over the rail. A mouseenter that arrives\r\n // before the width transition cannot show the tip (rects unstable), yet the\r\n // enter event is already gone by the time the animation ends — the settle\r\n // handler re-hit-tests the cursor against this position to show the tip for\r\n // the node still under the pointer.\r\n const lastPointerRef = useRef<{ x: number; y: number } | null>(null)\r\n // Aborts an in-flight jump (component unmount or a newer click superseding\r\n // an older one), so the loadOlder/DOM-poll loops stop promptly.\r\n const jumpAbortRef = useRef<AbortController | null>(null)\r\n useEffect(() => () => jumpAbortRef.current?.abort(), [])\r\n\r\n // Track the cursor over the rail for the expand-settled tip re-hit-test\r\n // (the `show` effect's settle handler reads lastPointerRef). Native\r\n // listener: plain DOM events keep the callback typed without React\r\n // synthetic-event generics in the hand-written createElement props.\r\n useEffect(() => {\r\n const el = navRef.current\r\n if (el === null) return\r\n const onMove = (e: PointerEvent) => { lastPointerRef.current = { x: e.clientX, y: e.clientY } }\r\n el.addEventListener('pointermove', onMove, { passive: true })\r\n return () => el.removeEventListener('pointermove', onMove)\r\n }, [])\r\n\r\n /** Position the tip against the item's CURRENT (post-expand) rect. */\r\n const positionTip = (index: number) => {\r\n if (index < 0) return\r\n const el = navRef.current?.querySelector<HTMLElement>(`[data-crl-index=\"${index}\"]`)\r\n if (el === null || el === undefined) return\r\n const rect = el.getBoundingClientRect()\r\n setTip({ index, x: rect.left - 12, y: rect.top })\r\n }\r\n\r\n /** Handle item hover: the collapsed state only expands the rail; the tip\r\n * only triggers once the expand animation has settled (item rects are then\r\n * stable, so the tip is positioned correctly). */\r\n const handleItemEnter = (index: number) => {\r\n if (!expandedRef.current) {\r\n setShow(true)\r\n return\r\n }\r\n positionTip(index)\r\n }\r\n\r\n const handleItemLeave = (index: number) => {\r\n setTip((prev) => (prev?.index === index ? null : prev))\r\n }\r\n\r\n // Track the settled expand state: when `show` flips true, wait for the width\r\n // transition to end before allowing tip triggers; when it flips false, the\r\n // rail is collapsing — treat as unexpanded immediately.\r\n useEffect(() => {\r\n if (!show) {\r\n expandedRef.current = false\r\n return\r\n }\r\n const el = navRef.current\r\n if (el === null) return\r\n let settled = false\r\n const settle = () => {\r\n if (settled) return\r\n settled = true\r\n expandedRef.current = true\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n // Re-hit-test the cursor: a mouseenter that fired before the width\r\n // transition could not show the tip (rects were still unstable), and\r\n // the enter event is long gone once the animation ends. If the pointer\r\n // still sits on a rail item, position that item's tip now.\r\n const p = lastPointerRef.current\r\n if (p !== null) {\r\n const hit = document.elementFromPoint(p.x, p.y)\r\n const item = hit === null ? null : hit.closest('[data-crl-index]')\r\n const idx = item === null ? -1 : Number(item.getAttribute('data-crl-index'))\r\n if (Number.isInteger(idx) && idx >= 0) positionTip(idx)\r\n }\r\n }\r\n const onTransitionEnd = (e: TransitionEvent) => {\r\n if (e.propertyName !== 'width') return\r\n settle()\r\n }\r\n el.addEventListener('transitionend', onTransitionEnd)\r\n // Fallback for `prefers-reduced-motion: reduce` (the stylesheet disables\r\n // the width transition, so `transitionend` never fires) and for any lost\r\n // event: settle after the transition duration regardless.\r\n const timer = setTimeout(settle, 300)\r\n return () => {\r\n el.removeEventListener('transitionend', onTransitionEnd)\r\n clearTimeout(timer)\r\n }\r\n }, [show])\r\n\r\n // Session-scoped attachment reader for tip thumbnails (stable identity so\r\n // TipThumb effects do not re-run every render; resolveThumb caches anyway).\r\n const readThumb = useMemo<ReadAttachmentFn | undefined>(() => {\r\n if (sessionId === undefined) return undefined\r\n const s = (sessionsService.binding(sessionId) as unknown as\r\n { session?: { readAttachment?: ReadAttachmentFn } } | undefined)?.session\r\n return s?.readAttachment === undefined\r\n ? undefined\r\n : (id: string) => s.readAttachment!(id)\r\n }, [sessionId, sessionsService])\r\n\r\n // Background full-history load: follow the runtime's authoritative hasMore\r\n // flag, but STOP as soon as the projection delivers.\r\n useEffect(() => {\r\n if (session === undefined) return\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n let cancelled = false\r\n const run = async () => {\r\n let guard = 0\r\n while (!cancelled && guard++ < 120) {\r\n if (Array.isArray(projected?.messages) && projected.messages.length > 0) return\r\n const snap = session.getSnapshot() as { hasMore?: boolean; loadingOlder?: boolean }\r\n if (snap?.hasMore !== true) return\r\n if (snap.loadingOlder === true) { await delay(50); continue }\r\n await (session as unknown as { loadOlder(): Promise<unknown> }).loadOlder()\r\n }\r\n }\r\n run().catch(() => {})\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, session === undefined ? 'none' : 'ready', Array.isArray(projected?.messages) && projected.messages.length > 0 ? 'have' : 'none'])\r\n\r\n // Track the reading position (active item): nearest row to the 40% viewport line.\r\n useEffect(() => {\r\n if (messages.length === 0) return\r\n const indexByKey = new Map<string, number>()\r\n for (let i = 0; i < messages.length; i++) {\r\n const key = anchorKeyOf(messages[i])\r\n if (key !== undefined) indexByKey.set(key, i)\r\n }\r\n const updateActive = () => {\r\n const sp = document.querySelector('[data-conversation-scroll]')\r\n if (sp === null) return\r\n const rect = sp.getBoundingClientRect()\r\n if (rect.height === 0) return\r\n const line = rect.top + rect.height * 0.4\r\n const rows = sp.querySelectorAll('[data-chat-anchor-key^=\"13:input-message\"]')\r\n let best = -1\r\n let bestDist = Infinity\r\n for (const row of rows) {\r\n const key = row.getAttribute('data-chat-anchor-key')\r\n if (key === null) continue\r\n const idx = indexByKey.get(key) ?? -1\r\n if (idx === -1) continue\r\n const r = row.getBoundingClientRect()\r\n const dist = Math.abs(r.top + r.height / 2 - line)\r\n if (dist < bestDist) { bestDist = dist; best = idx }\r\n }\r\n setActiveIndex(best)\r\n }\r\n updateActive()\r\n const el = document.querySelector('[data-conversation-scroll]')\r\n let scrollTimer: ReturnType<typeof setTimeout> | null = null\r\n const onScroll = () => {\r\n if (scrollTimer !== null) return\r\n scrollTimer = setTimeout(() => { scrollTimer = null; updateActive() }, 60)\r\n }\r\n el?.addEventListener('scroll', onScroll, { passive: true })\r\n const timer = window.setInterval(updateActive, 2000)\r\n return () => {\r\n el?.removeEventListener('scroll', onScroll)\r\n window.clearInterval(timer)\r\n if (scrollTimer !== null) clearTimeout(scrollTimer)\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [sessionId, messages.length])\r\n\r\n // Scroll the rail so the active item stays centered in its visible area:\r\n // follows the conversation's reading position (and re-centers after a\r\n // session switch, where activeIndex is recomputed for the new session).\r\n useEffect(() => {\r\n if (activeIndex < 0) return\r\n const el = navRef.current\r\n if (el === null) return\r\n const item = el.querySelector<HTMLElement>(`[data-crl-index=\"${activeIndex}\"]`)\r\n if (item === null) return\r\n const target = item.offsetTop - el.clientHeight / 2 + item.clientHeight / 2\r\n el.scrollTo({ top: Math.max(0, target), behavior: 'smooth' })\r\n }, [activeIndex])\r\n\r\n if (sessionId === undefined || messages.length < 2) return null\r\n\r\n const items = messages.map((m, i) => {\r\n const key = anchorKeyOf(m)\r\n return createElement('button', {\r\n type: 'button',\r\n key: m.seq,\r\n 'data-crl-index': String(i),\r\n className: S.item + (activeIndex === i ? ` ${S.itemActive}` : ''),\r\n 'aria-label': `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,\r\n 'aria-current': activeIndex === i ? 'location' : undefined,\r\n disabled: jumping,\r\n onClick: () => {\r\n if (key === undefined || jumping) return\r\n jumpAbortRef.current?.abort()\r\n const controller = new AbortController()\r\n jumpAbortRef.current = controller\r\n setJumping(true)\r\n void jumpToMessage(sessionsService as never, sessionId as string, key, undefined, controller.signal)\r\n .finally(() => setJumping(false))\r\n },\r\n onMouseEnter: () => handleItemEnter(i),\r\n onMouseLeave: () => handleItemLeave(i),\r\n children: [\r\n createElement('span', { className: S.num }, `#${i + 1}`),\r\n createElement('span', { className: S.title }, m.text === '' ? t.noText : m.text),\r\n m.hasImage\r\n ? createElement('span', { className: S.img, role: 'img', 'aria-label': t.hasImage },\r\n createElement('svg', {\r\n viewBox: '0 0 16 16',\r\n width: 12,\r\n height: 12,\r\n fill: 'none',\r\n stroke: 'currentColor',\r\n strokeWidth: 1.4,\r\n strokeLinecap: 'round',\r\n strokeLinejoin: 'round',\r\n 'aria-hidden': true,\r\n },\r\n createElement('rect', { x: 1.7, y: 2.7, width: 12.6, height: 10.6, rx: 2.2 }),\r\n createElement('circle', { cx: 5.6, cy: 6.4, r: 1.15, fill: 'currentColor', stroke: 'none' }),\r\n createElement('path', { d: 'M2.5 11.6l3.6-3.1 2.6 2.2 2.1-1.9 3.3 2.8' })))\r\n : null,\r\n createElement('span', { className: S.time }, relativeTime(m.time, t)),\r\n createElement('span', { className: S.ind, 'aria-hidden': true },\r\n createElement('span', { className: S.line })),\r\n ],\r\n })\r\n })\r\n\r\n return createPortal(\r\n // Fragment: the nav (transformed) and the tip must be siblings — a fixed\r\n // child inside a transformed ancestor is positioned relative to that\r\n // ancestor, breaking the viewport coordinates the tip uses.\r\n [createElement('div', {\r\n ref: navRef,\r\n className: S.nav + (show ? ` ${S.navShow}` : ''),\r\n role: 'navigation',\r\n 'aria-label': t.railLabel,\r\n onMouseEnter: () => setShow(true),\r\n onMouseLeave: () => setShow(false),\r\n children: [\r\n jumping ? createElement('div', { className: S.loading, key: 'loading' },\r\n createElement('span', { className: S.loadingLabel }, t.loading)) : null,\r\n ...items,\r\n ],\r\n }),\r\n // Full-content panel: anchored to the left of the hovered item. Images\r\n // stack above the text; a message whose image cannot be resolved (no\r\n // projection refs and unloaded node) shows a quiet \"has image\" badge.\r\n tip !== null && tip.index >= 0 && tip.index < messages.length\r\n ? createElement('div', {\r\n className: S.tip,\r\n style: { left: `${tip.x}px`, top: `${tip.y}px`, transform: 'translateX(-100%)' },\r\n }, (() => {\r\n const m = messages[tip.index]\r\n const specs = tipImagesOf(m, nodeSnapshot)\r\n const children: ReactNode[] = []\r\n if (specs.length > 0 && readThumb !== undefined) {\r\n // Only the first image renders; the rest collapse into a \"+N\"\r\n // badge so a many-image message cannot push the tip past its\r\n // viewport cap (the stacked thumbnails did).\r\n children.push(createElement('div', { className: S.tipImgs, key: 'imgs' },\r\n createElement('div', { className: S.tipImgWrap, key: 'img' },\r\n createElement(TipThumb, { spec: specs[0], sessionId: sessionId as string, read: readThumb }),\r\n specs.length > 1\r\n ? createElement('span', { className: S.tipImgCount, 'aria-hidden': true }, `+${specs.length - 1}`)\r\n : null)))\r\n } else if (m.hasImage) {\r\n children.push(createElement('span', { className: S.tipBadge, key: 'badge' }, t.hasImage))\r\n }\r\n children.push(createElement('span', { key: 'text' }, fullTextOf(m, nodeSnapshot) || t.noText))\r\n return children\r\n })())\r\n : null],\r\n document.body,\r\n )\r\n}\r\n\r\nfunction apply(ctx: ClientContext): void {\r\n ctx.slots.inject('conversation.input.dock', () => ctx.slots.register({\r\n name: 'conversation.input.dock',\r\n id: 'chat-rail',\r\n order: 40,\r\n inject: () => ({ sessionsService: ctx.sessions }),\r\n }, TimelineRail))\r\n}\r\n\r\nexport { apply, TimelineRail, imageSpecsOfContent, normalize }\r\n"],"mappings":"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"}
|
package/lib/index.mjs
CHANGED
|
@@ -29,9 +29,31 @@ function hasImageBlock(content) {
|
|
|
29
29
|
if (!Array.isArray(content)) return false;
|
|
30
30
|
return content.some((block) => block !== null && typeof block === "object" && block.type === "image");
|
|
31
31
|
}
|
|
32
|
+
/** Collect stored-image references from a ContentBlock list (reference form only:
|
|
33
|
+
* inline base64 data stays out of the payload — it can be megabytes). */
|
|
34
|
+
function imageRefsOf(content) {
|
|
35
|
+
if (!Array.isArray(content)) return [];
|
|
36
|
+
const refs = [];
|
|
37
|
+
for (const block of content) {
|
|
38
|
+
if (block === null || typeof block !== "object") continue;
|
|
39
|
+
const b = block;
|
|
40
|
+
if (b.type !== "image") continue;
|
|
41
|
+
const a = b.attachment;
|
|
42
|
+
if (a === null || typeof a !== "object") continue;
|
|
43
|
+
const ref = a;
|
|
44
|
+
if (typeof ref.attachmentId !== "string" || ref.attachmentId === "") continue;
|
|
45
|
+
refs.push({
|
|
46
|
+
attachmentId: ref.attachmentId,
|
|
47
|
+
mediaType: typeof ref.mediaType === "string" ? ref.mediaType : "image/png",
|
|
48
|
+
width: typeof ref.width === "number" ? ref.width : 0,
|
|
49
|
+
height: typeof ref.height === "number" ? ref.height : 0
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return refs;
|
|
53
|
+
}
|
|
32
54
|
const messageIndexProjectionDefinition = {
|
|
33
55
|
key: PROJECTION_KEY,
|
|
34
|
-
|
|
56
|
+
stateSchema: { parse: (val) => val },
|
|
35
57
|
init: () => ({ messages: [] }),
|
|
36
58
|
apply: (state, event) => {
|
|
37
59
|
if (event.type === "user/message") {
|
|
@@ -46,13 +68,17 @@ const messageIndexProjectionDefinition = {
|
|
|
46
68
|
time: event.time,
|
|
47
69
|
text,
|
|
48
70
|
hasImage,
|
|
71
|
+
images: imageRefsOf(data.content),
|
|
49
72
|
id
|
|
50
73
|
}] };
|
|
51
74
|
}
|
|
52
75
|
return state;
|
|
53
76
|
},
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
wire: {
|
|
78
|
+
viewSchema: { parse: (val) => val },
|
|
79
|
+
view: (state) => state
|
|
80
|
+
},
|
|
81
|
+
stateVersion: 6
|
|
56
82
|
};
|
|
57
83
|
const Config = { "~standard": {
|
|
58
84
|
version: 1,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-chat-rail",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "画卷式消息导航栏:右侧竖条 hover 展开画卷,scroll-spy 高亮居中跟随,编号+时间+完整预览,与 better-sidebar 双面板动画同步避让",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
@@ -58,27 +58,20 @@
|
|
|
58
58
|
],
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
62
|
-
"dsh-better-sidebar": ">=0.12.0",
|
|
61
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.1-rc.1",
|
|
63
62
|
"react": "^18.2.0",
|
|
64
63
|
"react-dom": "^18.2.0"
|
|
65
64
|
},
|
|
66
|
-
"peerDependenciesMeta": {
|
|
67
|
-
"dsh-better-sidebar": {
|
|
68
|
-
"optional": true
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
65
|
"devDependencies": {
|
|
72
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
73
|
-
"@deepseek-ai/dsh-client-runtime": "^0.1.
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
76
|
-
"@deepseek-ai/dsh-session-projection": "^0.1.
|
|
66
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-session-projection": "^0.1.1-rc.1",
|
|
77
71
|
"@types/node": "^22.0.0",
|
|
78
72
|
"@types/react": "~18.3.1",
|
|
79
73
|
"@types/react-dom": "^18.3.7",
|
|
80
74
|
"cordis": "^4.0.0-rc.7",
|
|
81
|
-
"dsh-better-sidebar": "^0.13.0",
|
|
82
75
|
"react": "^18.2.0",
|
|
83
76
|
"react-dom": "^18.2.0",
|
|
84
77
|
"tsdown": "^0.22.2",
|