@max-null/dsh-chat-rail 0.2.0 → 0.4.0
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 +17 -7
- package/docs/shots/rail-fav-1.png +0 -0
- package/docs/shots/rail-fav-2.png +0 -0
- package/lib/client.js +658 -50
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +99 -6
- package/package.json +10 -16
- package/docs/shots/rail-1.png +0 -0
- package/docs/shots/rail-2.png +0 -0
package/lib/client.js
CHANGED
|
@@ -31,7 +31,11 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
* background loadOlder loop (stopped as soon as the projection delivers).
|
|
32
32
|
* Mounted in conversation.input.dock, portal-rendered to body.
|
|
33
33
|
*/
|
|
34
|
-
const inject = [
|
|
34
|
+
const inject = [
|
|
35
|
+
"slots",
|
|
36
|
+
"sessions",
|
|
37
|
+
"conversation"
|
|
38
|
+
];
|
|
35
39
|
const STRINGS = {
|
|
36
40
|
zh: {
|
|
37
41
|
railLabel: "消息导航",
|
|
@@ -43,7 +47,12 @@ window.__ModuleLoader__.load({
|
|
|
43
47
|
timeJustNow: "刚刚",
|
|
44
48
|
timeMinutes: "{n}分钟前",
|
|
45
49
|
timeHours: "{n}小时前",
|
|
46
|
-
timeDays: "{n}天前"
|
|
50
|
+
timeDays: "{n}天前",
|
|
51
|
+
fav: "收藏消息",
|
|
52
|
+
unfav: "取消收藏",
|
|
53
|
+
fill: "填充到输入框",
|
|
54
|
+
filled: "已填入输入框",
|
|
55
|
+
favOnly: "只显示收藏"
|
|
47
56
|
},
|
|
48
57
|
en: {
|
|
49
58
|
railLabel: "Message rail",
|
|
@@ -55,7 +64,12 @@ window.__ModuleLoader__.load({
|
|
|
55
64
|
timeJustNow: "just now",
|
|
56
65
|
timeMinutes: "{n}m ago",
|
|
57
66
|
timeHours: "{n}h ago",
|
|
58
|
-
timeDays: "{n}d ago"
|
|
67
|
+
timeDays: "{n}d ago",
|
|
68
|
+
fav: "Bookmark message",
|
|
69
|
+
unfav: "Remove bookmark",
|
|
70
|
+
fill: "Fill into input",
|
|
71
|
+
filled: "Filled into input",
|
|
72
|
+
favOnly: "Bookmarks only"
|
|
59
73
|
}
|
|
60
74
|
};
|
|
61
75
|
/** Compact relative time for a message timestamp (zh/en via template). */
|
|
@@ -98,8 +112,9 @@ window.__ModuleLoader__.load({
|
|
|
98
112
|
"@keyframes crl-fade{from{opacity:0}to{opacity:1}}",
|
|
99
113
|
".crl_ind{flex-shrink:0;display:flex;justify-content:center;align-items:center;width:18px;height:20px}",
|
|
100
114
|
".crl_show .crl_ind{margin-left:8px}",
|
|
101
|
-
".crl_img{flex-shrink:0;
|
|
102
|
-
".crl_img
|
|
115
|
+
".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)}",
|
|
116
|
+
".crl_show .crl_img{display:inline-flex;animation:crl-fade .18s ease}",
|
|
117
|
+
"body[data-ds-dark-theme] .crl_img,[data-theme='dark'] .crl_img,.dark .crl_img{color:rgba(255,255,255,.38)}",
|
|
103
118
|
".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
119
|
".crl_item:hover .crl_line{background-color:rgba(0,0,0,.9)}",
|
|
105
120
|
".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 +123,38 @@ window.__ModuleLoader__.load({
|
|
|
108
123
|
"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
124
|
".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
125
|
"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
|
-
"
|
|
126
|
+
".crl_tipImgs{display:flex;flex-direction:column;gap:6px;margin-bottom:8px}",
|
|
127
|
+
".crl_tipImgWrap{position:relative;width:fit-content;max-width:100%;max-height:150px}",
|
|
128
|
+
".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)))}",
|
|
129
|
+
".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}",
|
|
130
|
+
".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}",
|
|
131
|
+
"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)))}",
|
|
132
|
+
".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)))}",
|
|
133
|
+
"@keyframes crl-pulse{0%,100%{opacity:.45}50%{opacity:.85}}",
|
|
134
|
+
".crl_msgAct{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;margin:0;padding:6px;border:none;border-radius:28px;background:transparent;color:var(--dsw-alias-label-tertiary,rgba(0,0,0,.42));cursor:pointer;transition:background .15s ease,color .15s ease}",
|
|
135
|
+
".crl_msgAct:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-secondary,rgba(0,0,0,.72))}",
|
|
136
|
+
".crl_msgAct:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#4d6bfe);outline-offset:2px}",
|
|
137
|
+
".crl_msgAct[data-tip]:hover::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);padding:5px 10px;border-radius:8px;background:rgba(24,28,36,.94);color:rgba(255,255,255,.95);font-size:12px;line-height:1.4;white-space:nowrap;pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}",
|
|
138
|
+
".crl_msgAct[data-tip]:hover::before{content:\"\";position:absolute;left:50%;bottom:calc(100% + 3px);transform:translateX(-50%);border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid rgba(24,28,36,.94);pointer-events:none;opacity:0;animation:crl-tip-in .12s ease .55s forwards;z-index:3000}",
|
|
139
|
+
"@keyframes crl-tip-in{to{opacity:1}}",
|
|
140
|
+
".crl_msgAct.crl_fav.crl_on{color:#ffd166}",
|
|
141
|
+
".crl_msgAct.crl_fav.crl_on svg{fill:currentColor}",
|
|
142
|
+
".crl_msgAct.crl_fav.crl_on:hover{color:#ffd166}",
|
|
143
|
+
".crl_favStar{display:none;flex-shrink:0;align-items:center;justify-content:center;width:14px;height:14px;margin-right:6px;color:#ffd166}",
|
|
144
|
+
".crl_show .crl_favStar{display:inline-flex;animation:crl-fade .18s ease}",
|
|
145
|
+
".crl_item.crl_favItem .crl_line{background-color:#ffd166}",
|
|
146
|
+
"body[data-ds-dark-theme] .crl_item.crl_favItem .crl_line,[data-theme='dark'] .crl_item.crl_favItem .crl_line,.dark .crl_item.crl_favItem .crl_line{background-color:#ffd166}",
|
|
147
|
+
".crl_item.crl_favItem.crl_active .crl_line{background-color:#ffd166;box-shadow:0 0 6px #ffd166}",
|
|
148
|
+
".crl_favToggle{position:sticky;top:0;z-index:3;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;width:26px;height:26px;margin:0 0 6px;padding:0;border:none;border-radius:13px;background:inherit;color:rgba(0,0,0,.4);cursor:pointer;font-size:11px;line-height:1;white-space:nowrap;transition:background .15s ease,color .15s ease}",
|
|
149
|
+
".crl_favToggle:hover{background:rgba(0,0,0,.07);color:rgba(0,0,0,.75)}",
|
|
150
|
+
".crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.14)}",
|
|
151
|
+
".crl_show .crl_favToggle{width:auto;height:26px;margin:0 6px 8px;padding:0 11px;border-radius:13px;align-self:flex-start;text-align:left}",
|
|
152
|
+
".crl_favToggleLabel{display:none}",
|
|
153
|
+
".crl_show .crl_favToggleLabel{display:inline-block;min-width:0;overflow:hidden;text-overflow:ellipsis;vertical-align:middle}",
|
|
154
|
+
"body[data-ds-dark-theme] .crl_favToggle,[data-theme='dark'] .crl_favToggle,.dark .crl_favToggle{color:rgba(255,255,255,.4)}",
|
|
155
|
+
"body[data-ds-dark-theme] .crl_favToggle:hover,[data-theme='dark'] .crl_favToggle:hover,.dark .crl_favToggle:hover{background:rgba(255,255,255,.1);color:rgba(255,255,255,.85)}",
|
|
156
|
+
"body[data-ds-dark-theme] .crl_favToggle.crl_on,[data-theme='dark'] .crl_favToggle.crl_on,.dark .crl_favToggle.crl_on{color:#ffd166;background:rgba(255,209,102,.18)}",
|
|
157
|
+
"@media (prefers-reduced-motion:reduce){.crl_nav,.crl_title,.crl_num,.crl_time,.crl_line{transition:none}.crl_tipImgPh{animation:none}}"
|
|
112
158
|
].join("");
|
|
113
159
|
const STYLE_ID = "@max-null/dsh-chat-rail/styles.module.css";
|
|
114
160
|
if (typeof document !== "undefined" && document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`) === null) {
|
|
@@ -131,23 +177,281 @@ window.__ModuleLoader__.load({
|
|
|
131
177
|
img: "crl_img",
|
|
132
178
|
loading: "crl_loading",
|
|
133
179
|
loadingLabel: "crl_loadingLabel",
|
|
134
|
-
tip: "crl_tip"
|
|
180
|
+
tip: "crl_tip",
|
|
181
|
+
tipImgs: "crl_tipImgs",
|
|
182
|
+
tipImgWrap: "crl_tipImgWrap",
|
|
183
|
+
tipImg: "crl_tipImg",
|
|
184
|
+
tipImgCount: "crl_tipImgCount",
|
|
185
|
+
tipImgPh: "crl_tipImgPh",
|
|
186
|
+
tipBadge: "crl_tipBadge",
|
|
187
|
+
favStar: "crl_favStar",
|
|
188
|
+
favItem: "crl_favItem",
|
|
189
|
+
favToggle: "crl_favToggle",
|
|
190
|
+
favToggleLabel: "crl_favToggleLabel"
|
|
135
191
|
};
|
|
136
192
|
const NOOP_STORE = {
|
|
137
193
|
getSnapshot: () => void 0,
|
|
138
194
|
subscribe: () => () => {}
|
|
139
195
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
196
|
+
const FAVORITES_KEY = "@max-null/dsh-chat-rail:favorites";
|
|
197
|
+
const FAVORITES_API = "/chat-rail/api/favorites";
|
|
198
|
+
/** 模块级快照缓存(host 加载后充当源 truth)+ 订阅者集合。 */
|
|
199
|
+
let favoritesCache = null;
|
|
200
|
+
const favoritesListeners = /* @__PURE__ */ new Set();
|
|
201
|
+
/** 从 localStorage 读旧数据(迁移用/离线兜底)。 */
|
|
202
|
+
function readFavoritesLocal() {
|
|
203
|
+
if (typeof localStorage === "undefined") return {};
|
|
204
|
+
try {
|
|
205
|
+
const raw = localStorage.getItem(FAVORITES_KEY);
|
|
206
|
+
if (raw === null) return {};
|
|
207
|
+
const value = JSON.parse(raw);
|
|
208
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
209
|
+
} catch {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
146
212
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
213
|
+
async function fetchFavoritesMap() {
|
|
214
|
+
try {
|
|
215
|
+
const res = await fetch(FAVORITES_API, { headers: { Accept: "application/json" } });
|
|
216
|
+
if (!res.ok) return {};
|
|
217
|
+
const value = (await res.json())?.value;
|
|
218
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
219
|
+
} catch {
|
|
220
|
+
return {};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/** 模块加载时同步 host;旧 localStorage 数据迁移一次(host 为空且本地有值)。 */
|
|
224
|
+
(async () => {
|
|
225
|
+
const host = await fetchFavoritesMap();
|
|
226
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
227
|
+
const legacy = readFavoritesLocal();
|
|
228
|
+
if (Object.keys(host).length === 0 && Object.keys(legacy).length > 0) await fetch(FAVORITES_API, {
|
|
229
|
+
method: "PUT",
|
|
230
|
+
headers: { "Content-Type": "application/json" },
|
|
231
|
+
body: JSON.stringify({ favorites: legacy })
|
|
232
|
+
}).catch(() => {});
|
|
233
|
+
try {
|
|
234
|
+
localStorage.removeItem(FAVORITES_KEY);
|
|
235
|
+
} catch {}
|
|
236
|
+
favoritesCache = Object.keys(host).length === 0 ? legacy : host;
|
|
237
|
+
for (const listener of favoritesListeners) listener();
|
|
238
|
+
} else favoritesCache = host;
|
|
239
|
+
})();
|
|
240
|
+
/** Read the favorites map(内存镜像;未加载完成时回退 localStorage)。 */
|
|
241
|
+
function readFavorites() {
|
|
242
|
+
if (favoritesCache !== null) return favoritesCache;
|
|
243
|
+
return readFavoritesLocal();
|
|
244
|
+
}
|
|
245
|
+
/** Write the favorites map;更新内存并 fire-and-forget 持久化到 host(host-only)。 */
|
|
246
|
+
function writeFavorites(map) {
|
|
247
|
+
favoritesCache = map;
|
|
248
|
+
fetch(FAVORITES_API, {
|
|
249
|
+
method: "PUT",
|
|
250
|
+
headers: { "Content-Type": "application/json" },
|
|
251
|
+
body: JSON.stringify({ favorites: map })
|
|
252
|
+
}).catch(() => {});
|
|
253
|
+
}
|
|
254
|
+
/** Normalized id list for one session (drops non-string entries). */
|
|
255
|
+
function favoriteIdsOf(map, sessionId) {
|
|
256
|
+
const list = map[sessionId];
|
|
257
|
+
return Array.isArray(list) ? list.filter((id) => typeof id === "string") : [];
|
|
258
|
+
}
|
|
259
|
+
/** Toggle one id in a list (stable new array; no in-place mutation). */
|
|
260
|
+
function toggleFavoriteId(list, id) {
|
|
261
|
+
return list.includes(id) ? list.filter((candidate) => candidate !== id) : [...list, id];
|
|
262
|
+
}
|
|
263
|
+
/** Whether one message is favorited in one session. */
|
|
264
|
+
function isFavorite(map, sessionId, messageId) {
|
|
265
|
+
return favoriteIdsOf(map, sessionId).includes(messageId);
|
|
266
|
+
}
|
|
267
|
+
/** Stable snapshot for useSyncExternalStore (loaded once, cached until toggle). */
|
|
268
|
+
function favoritesSnapshot() {
|
|
269
|
+
if (favoritesCache === null) favoritesCache = readFavorites();
|
|
270
|
+
return favoritesCache;
|
|
271
|
+
}
|
|
272
|
+
/** Subscribe to favorites changes; returns the unsubscribe. */
|
|
273
|
+
function subscribeFavorites(listener) {
|
|
274
|
+
favoritesListeners.add(listener);
|
|
275
|
+
return () => {
|
|
276
|
+
favoritesListeners.delete(listener);
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
/** Toggle one message's favorite state: snapshot → new map → persist → notify. */
|
|
280
|
+
function toggleFavorite(sessionId, messageId) {
|
|
281
|
+
const next = { ...favoritesSnapshot() };
|
|
282
|
+
next[sessionId] = toggleFavoriteId(favoriteIdsOf(favoritesSnapshot(), sessionId), messageId);
|
|
283
|
+
favoritesCache = next;
|
|
284
|
+
writeFavorites(next);
|
|
285
|
+
for (const listener of [...favoritesListeners]) listener();
|
|
286
|
+
}
|
|
287
|
+
/** Remember the messageId read off a DOM anchor key (`13:input-message<id>`). */
|
|
288
|
+
function messageIdOfAnchorKey(key) {
|
|
289
|
+
return key.startsWith("13:input-message") ? key.slice(16) : key;
|
|
290
|
+
}
|
|
291
|
+
/** The favorite key of one rail message: the durable id when present (host
|
|
292
|
+
* projection), otherwise the id reconstructed from the node anchor (the
|
|
293
|
+
* loaded-node fallback path). */
|
|
294
|
+
function favoriteIdOfMessage(m) {
|
|
295
|
+
if (typeof m.id === "string" && m.id !== "") return m.id;
|
|
296
|
+
if (typeof m.key === "string") return messageIdOfAnchorKey(m.key);
|
|
297
|
+
return "";
|
|
298
|
+
}
|
|
299
|
+
const actionCtx = {
|
|
300
|
+
sessionId: void 0,
|
|
301
|
+
onToggleFavorite: () => {},
|
|
302
|
+
onFill: () => {}
|
|
303
|
+
};
|
|
304
|
+
/** aria-label values of the copy button across shipped locales (button sits
|
|
305
|
+
* in the user row's IconActions; its parent is the row's action host). */
|
|
306
|
+
const COPY_ARIA_LABELS = /* @__PURE__ */ new Set([
|
|
307
|
+
"复制",
|
|
308
|
+
"Copy",
|
|
309
|
+
"已复制",
|
|
310
|
+
"Copied"
|
|
311
|
+
]);
|
|
312
|
+
/** The copy button inside one user message row (null until rendered). */
|
|
313
|
+
function copyButtonOf(row) {
|
|
314
|
+
for (const button of row.querySelectorAll("button")) {
|
|
315
|
+
const label = button.getAttribute("aria-label");
|
|
316
|
+
if (label !== null && COPY_ARIA_LABELS.has(label)) return button;
|
|
317
|
+
}
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
/** Star SVG path (milestone-compatible glyph, 24-unit viewBox). */
|
|
321
|
+
const STAR_PATH = "m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z";
|
|
322
|
+
/** Build the favorite (star) button for one user message row. */
|
|
323
|
+
function favoriteButtonOf(messageId, lang) {
|
|
324
|
+
const button = document.createElement("button");
|
|
325
|
+
button.type = "button";
|
|
326
|
+
button.className = "crl_msgAct crl_fav";
|
|
327
|
+
button.dataset.crlStar = messageId;
|
|
328
|
+
const initialLabel = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? "", messageId) ? lang.unfav : lang.fav;
|
|
329
|
+
button.setAttribute("aria-pressed", String(isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? "", messageId)));
|
|
330
|
+
button.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? "", messageId) ? "true" : void 0;
|
|
331
|
+
button.classList.toggle("crl_on", isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? "", messageId));
|
|
332
|
+
button.setAttribute("data-tip", initialLabel);
|
|
333
|
+
button.setAttribute("aria-label", initialLabel);
|
|
334
|
+
button.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="${STAR_PATH}"/></svg>`;
|
|
335
|
+
button.addEventListener("click", (event) => {
|
|
336
|
+
event.preventDefault();
|
|
337
|
+
event.stopPropagation();
|
|
338
|
+
const id = button.dataset.crlStar;
|
|
339
|
+
if (id !== void 0) {
|
|
340
|
+
actionCtx.onToggleFavorite(id);
|
|
341
|
+
refreshFavoriteButton(button, lang);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
return button;
|
|
345
|
+
}
|
|
346
|
+
/** Build the fill-to-input (plus) button for one user message row. */
|
|
347
|
+
function fillButtonOf(messageId, lang) {
|
|
348
|
+
const button = document.createElement("button");
|
|
349
|
+
button.type = "button";
|
|
350
|
+
button.className = "crl_msgAct crl_fill";
|
|
351
|
+
button.dataset.crlFill = messageId;
|
|
352
|
+
button.setAttribute("data-tip", lang.fill);
|
|
353
|
+
button.setAttribute("aria-label", lang.fill);
|
|
354
|
+
button.innerHTML = "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" aria-hidden=\"true\"><path d=\"M8 3.5v9M3.5 8h9\"/></svg>";
|
|
355
|
+
button.addEventListener("click", (event) => {
|
|
356
|
+
event.preventDefault();
|
|
357
|
+
event.stopPropagation();
|
|
358
|
+
const id = button.dataset.crlFill;
|
|
359
|
+
if (id !== void 0) actionCtx.onFill(id);
|
|
360
|
+
});
|
|
361
|
+
return button;
|
|
362
|
+
}
|
|
363
|
+
/** Sync one star button to its message's current favorite state. */
|
|
364
|
+
function refreshFavoriteButton(button, lang) {
|
|
365
|
+
const id = button.dataset.crlStar;
|
|
366
|
+
if (id === void 0) return;
|
|
367
|
+
const starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? "", id);
|
|
368
|
+
button.setAttribute("aria-pressed", String(starred));
|
|
369
|
+
button.dataset.starred = starred ? "true" : void 0;
|
|
370
|
+
button.classList.toggle("crl_on", starred);
|
|
371
|
+
const label = starred ? lang.unfav : lang.fav;
|
|
372
|
+
button.setAttribute("data-tip", label);
|
|
373
|
+
button.setAttribute("aria-label", label);
|
|
374
|
+
}
|
|
375
|
+
/** Refresh every injected star button (session switch / external toggle). */
|
|
376
|
+
function refreshAllFavoriteButtons(lang) {
|
|
377
|
+
for (const button of document.querySelectorAll("[data-crl-star]")) refreshFavoriteButton(button, lang);
|
|
378
|
+
}
|
|
379
|
+
/** Inject star + plus buttons into one user message row (idempotent). */
|
|
380
|
+
function injectMessageActions(row, lang) {
|
|
381
|
+
if (row.dataset.crlActions === "1") {
|
|
382
|
+
if (row.querySelector("[data-crl-star]") !== null && row.querySelector("[data-crl-fill]") !== null) return;
|
|
383
|
+
}
|
|
384
|
+
const copy = copyButtonOf(row);
|
|
385
|
+
const messageId = messageIdOfAnchorKey(row.getAttribute("data-chat-anchor-key") ?? "");
|
|
386
|
+
if (copy === null || messageId === "") return;
|
|
387
|
+
const star = favoriteButtonOf(messageId, lang);
|
|
388
|
+
star.dataset.starred = isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? "", messageId) ? "true" : void 0;
|
|
389
|
+
star.classList.toggle("crl_on", isFavorite(favoritesSnapshot(), actionCtx.sessionId ?? "", messageId));
|
|
390
|
+
const fill = fillButtonOf(messageId, lang);
|
|
391
|
+
copy.before(star, fill);
|
|
392
|
+
row.dataset.crlActions = "1";
|
|
393
|
+
}
|
|
394
|
+
/** Start observing new user rows so injected actions follow history paging. */
|
|
395
|
+
function startActionInjector(lang) {
|
|
396
|
+
const scan = () => {
|
|
397
|
+
for (const row of document.querySelectorAll("[data-chat-anchor-key^=\"13:input-message\"]")) injectMessageActions(row, lang);
|
|
398
|
+
};
|
|
399
|
+
const observer = new MutationObserver(scan);
|
|
400
|
+
if (typeof document !== "undefined" && document.body !== null) observer.observe(document.body, {
|
|
401
|
+
childList: true,
|
|
402
|
+
subtree: true
|
|
403
|
+
});
|
|
404
|
+
scan();
|
|
405
|
+
return () => observer.disconnect();
|
|
406
|
+
}
|
|
407
|
+
/** Extract stored-image references from a ContentBlock list (reference form). */
|
|
408
|
+
function nodeImagesOf(content) {
|
|
409
|
+
if (!Array.isArray(content)) return [];
|
|
410
|
+
const out = [];
|
|
411
|
+
for (const block of content) {
|
|
412
|
+
if (block === null || typeof block !== "object") continue;
|
|
413
|
+
const b = block;
|
|
414
|
+
if (b.type !== "image") continue;
|
|
415
|
+
const a = b.attachment;
|
|
416
|
+
if (a === null || typeof a !== "object") continue;
|
|
417
|
+
const ref = a;
|
|
418
|
+
if (typeof ref.attachmentId !== "string" || ref.attachmentId === "") continue;
|
|
419
|
+
out.push({
|
|
420
|
+
attachmentId: ref.attachmentId,
|
|
421
|
+
mediaType: typeof ref.mediaType === "string" ? ref.mediaType : "image/png",
|
|
422
|
+
width: typeof ref.width === "number" ? ref.width : 0,
|
|
423
|
+
height: typeof ref.height === "number" ? ref.height : 0
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return out;
|
|
427
|
+
}
|
|
428
|
+
/** Convert a ContentBlock list to displayable image specs; inline base64
|
|
429
|
+
* blocks (rare in replayed history) become data URLs on the spot. */
|
|
430
|
+
function imageSpecsOfContent(content) {
|
|
431
|
+
const specs = [];
|
|
432
|
+
if (!Array.isArray(content)) return specs;
|
|
433
|
+
for (const block of content) {
|
|
434
|
+
if (block === null || typeof block !== "object") continue;
|
|
435
|
+
const b = block;
|
|
436
|
+
if (b.type !== "image") continue;
|
|
437
|
+
const a = b.attachment;
|
|
438
|
+
if (a !== null && typeof a === "object") {
|
|
439
|
+
const ref = a;
|
|
440
|
+
if (typeof ref.attachmentId === "string" && ref.attachmentId !== "") {
|
|
441
|
+
specs.push({
|
|
442
|
+
kind: "ref",
|
|
443
|
+
attachmentId: ref.attachmentId,
|
|
444
|
+
mediaType: typeof ref.mediaType === "string" ? ref.mediaType : "image/png"
|
|
445
|
+
});
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (typeof b.data === "string" && b.data !== "") specs.push({
|
|
450
|
+
kind: "data",
|
|
451
|
+
src: `data:${typeof b.mediaType === "string" ? b.mediaType : "image/png"};base64,${b.data}`
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return specs;
|
|
151
455
|
}
|
|
152
456
|
/** Normalize one projection entry to a rail message. */
|
|
153
457
|
function normalize(m) {
|
|
@@ -155,15 +459,41 @@ window.__ModuleLoader__.load({
|
|
|
155
459
|
const o = m;
|
|
156
460
|
if (typeof o.seq !== "number") return null;
|
|
157
461
|
const text = typeof o.text === "string" ? o.text : typeof o.preview === "string" ? o.preview : "";
|
|
462
|
+
const images = Array.isArray(o.images) ? o.images.map(nodeImagesOfEntry).filter((i) => i !== null) : void 0;
|
|
158
463
|
return {
|
|
159
464
|
seq: o.seq,
|
|
160
465
|
time: typeof o.time === "number" ? o.time : 0,
|
|
161
466
|
text,
|
|
162
467
|
hasImage: o.hasImage === true,
|
|
468
|
+
...images !== void 0 && images.length > 0 ? { images } : {},
|
|
163
469
|
...typeof o.key === "string" ? { key: o.key } : {},
|
|
164
470
|
...typeof o.id === "string" ? { id: o.id } : {}
|
|
165
471
|
};
|
|
166
472
|
}
|
|
473
|
+
/** Normalize one projection entry of the `images` array (wire form). */
|
|
474
|
+
function nodeImagesOfEntry(v) {
|
|
475
|
+
if (v === null || typeof v !== "object") return null;
|
|
476
|
+
const o = v;
|
|
477
|
+
if (typeof o.attachmentId !== "string" || o.attachmentId === "") return null;
|
|
478
|
+
return {
|
|
479
|
+
attachmentId: o.attachmentId,
|
|
480
|
+
mediaType: typeof o.mediaType === "string" ? o.mediaType : "image/png",
|
|
481
|
+
width: typeof o.width === "number" ? o.width : 0,
|
|
482
|
+
height: typeof o.height === "number" ? o.height : 0
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/** Extract preview text from a user message's ContentBlock list. */
|
|
486
|
+
function userTextOf(content) {
|
|
487
|
+
if (!Array.isArray(content)) return "";
|
|
488
|
+
let out = "";
|
|
489
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
490
|
+
return out.trim().slice(0, 80);
|
|
491
|
+
}
|
|
492
|
+
/** Whether a ContentBlock list carries an image block (rc.8 attachments). */
|
|
493
|
+
function userHasImage(content) {
|
|
494
|
+
if (!Array.isArray(content)) return false;
|
|
495
|
+
return content.some((block) => block !== null && typeof block === "object" && block.type === "image");
|
|
496
|
+
}
|
|
167
497
|
/** Fallback collector: enumerate user messages from the loaded chat nodes. */
|
|
168
498
|
function collectFromNodes(snapshot) {
|
|
169
499
|
const out = [];
|
|
@@ -179,11 +509,13 @@ window.__ModuleLoader__.load({
|
|
|
179
509
|
if (typeof data.time !== "number" || !Array.isArray(data.content)) continue;
|
|
180
510
|
const key = typeof n.key === "string" ? n.key : void 0;
|
|
181
511
|
if (key === void 0) continue;
|
|
512
|
+
const images = nodeImagesOf(data.content);
|
|
182
513
|
out.push({
|
|
183
514
|
seq: typeof n.anchorSeq === "number" ? n.anchorSeq : 0,
|
|
184
515
|
time: data.time,
|
|
185
516
|
text: userTextOf(data.content),
|
|
186
517
|
hasImage: userHasImage(data.content),
|
|
518
|
+
...images.length > 0 ? { images } : {},
|
|
187
519
|
key
|
|
188
520
|
});
|
|
189
521
|
}
|
|
@@ -195,6 +527,33 @@ window.__ModuleLoader__.load({
|
|
|
195
527
|
if (typeof m.key === "string" && m.key !== "") return m.key;
|
|
196
528
|
if (typeof m.id === "string" && m.id !== "") return "13:input-message" + m.id;
|
|
197
529
|
}
|
|
530
|
+
/** Full text of a rail message from the loaded chat nodes (uncapped),
|
|
531
|
+
* falling back to the projection preview when the node is not mounted. */
|
|
532
|
+
function fullTextOf(m, nodeSnapshot) {
|
|
533
|
+
const key = anchorKeyOf(m);
|
|
534
|
+
const nodes = nodeSnapshot?.chat?.nodes;
|
|
535
|
+
const content = (key === void 0 ? void 0 : nodes?.get(key))?.data?.content;
|
|
536
|
+
if (Array.isArray(content)) {
|
|
537
|
+
let out = "";
|
|
538
|
+
for (const block of content) if (block !== null && typeof block === "object" && block.type === "text" && typeof block.text === "string") out += block.text;
|
|
539
|
+
const full = out.trim();
|
|
540
|
+
if (full !== "") return full;
|
|
541
|
+
}
|
|
542
|
+
return m.text;
|
|
543
|
+
}
|
|
544
|
+
/** Displayable tip images for one rail message: host projection references
|
|
545
|
+
* first (works even before the node window covers the message), then the
|
|
546
|
+
* loaded chat node's content blocks. */
|
|
547
|
+
function tipImagesOf(m, nodeSnapshot) {
|
|
548
|
+
if (Array.isArray(m.images) && m.images.length > 0) return m.images.map((img) => ({
|
|
549
|
+
kind: "ref",
|
|
550
|
+
attachmentId: img.attachmentId,
|
|
551
|
+
mediaType: img.mediaType
|
|
552
|
+
}));
|
|
553
|
+
const key = anchorKeyOf(m);
|
|
554
|
+
const nodes = nodeSnapshot?.chat?.nodes;
|
|
555
|
+
return imageSpecsOfContent((key === void 0 ? void 0 : nodes?.get(key))?.data?.content);
|
|
556
|
+
}
|
|
198
557
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
199
558
|
/**
|
|
200
559
|
* Ensure the message node is loaded into the visible window, then scroll to it.
|
|
@@ -252,12 +611,84 @@ window.__ModuleLoader__.load({
|
|
|
252
611
|
});
|
|
253
612
|
return true;
|
|
254
613
|
}
|
|
614
|
+
/** Session-scoped browser URL cache for resolved attachment bytes (one entry
|
|
615
|
+
* per session:attachment; a failed load evicts itself so the next hover
|
|
616
|
+
* retries instead of showing a permanently broken thumbnail). */
|
|
617
|
+
const thumbUrls = /* @__PURE__ */ new Map();
|
|
618
|
+
/** Fallback data URL when the browser cannot mint object URLs. */
|
|
619
|
+
function bytesToDataUrl(data, mediaType) {
|
|
620
|
+
let binary = "";
|
|
621
|
+
const chunk = 32768;
|
|
622
|
+
for (let offset = 0; offset < data.length; offset += chunk) binary += String.fromCharCode(...data.subarray(offset, offset + chunk));
|
|
623
|
+
return `data:${mediaType};base64,${btoa(binary)}`;
|
|
624
|
+
}
|
|
625
|
+
/** Resolve (and cache) one stored image to a displayable URL. */
|
|
626
|
+
function resolveThumb(sessionId, read, image) {
|
|
627
|
+
const key = `${sessionId}:${image.attachmentId}`;
|
|
628
|
+
let pending = thumbUrls.get(key);
|
|
629
|
+
if (pending === void 0) {
|
|
630
|
+
pending = read(image.attachmentId).then((result) => {
|
|
631
|
+
if (!result.ok || result.value === void 0) throw new Error(result.error?.message ?? result.error?.code ?? "readAttachment failed");
|
|
632
|
+
const data = result.value.data;
|
|
633
|
+
if (data === void 0) throw new Error("readAttachment resolved no bytes");
|
|
634
|
+
const mediaType = result.value.attachment?.mediaType ?? image.mediaType;
|
|
635
|
+
if (typeof URL.createObjectURL === "function") return URL.createObjectURL(new Blob([data], { type: mediaType }));
|
|
636
|
+
return bytesToDataUrl(data, mediaType);
|
|
637
|
+
});
|
|
638
|
+
pending.catch(() => {
|
|
639
|
+
thumbUrls.delete(key);
|
|
640
|
+
});
|
|
641
|
+
thumbUrls.set(key, pending);
|
|
642
|
+
}
|
|
643
|
+
return pending;
|
|
644
|
+
}
|
|
645
|
+
/** One tip thumbnail: resolves its attachment lazily, shows a pulsing
|
|
646
|
+
* placeholder while loading, and removes itself on failure. */
|
|
647
|
+
function TipThumb({ spec, sessionId, read }) {
|
|
648
|
+
const [src, setSrc] = (0, react.useState)(null);
|
|
649
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
650
|
+
const specKey = spec.kind === "ref" ? `ref:${spec.attachmentId}` : "data";
|
|
651
|
+
(0, react.useEffect)(() => {
|
|
652
|
+
let alive = true;
|
|
653
|
+
setSrc(null);
|
|
654
|
+
setFailed(false);
|
|
655
|
+
if (spec.kind === "data") {
|
|
656
|
+
setSrc(spec.src);
|
|
657
|
+
return () => {
|
|
658
|
+
alive = false;
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
resolveThumb(sessionId, read, spec).then((url) => {
|
|
662
|
+
if (alive) setSrc(url);
|
|
663
|
+
}).catch(() => {
|
|
664
|
+
if (alive) setFailed(true);
|
|
665
|
+
});
|
|
666
|
+
return () => {
|
|
667
|
+
alive = false;
|
|
668
|
+
};
|
|
669
|
+
}, [
|
|
670
|
+
specKey,
|
|
671
|
+
sessionId,
|
|
672
|
+
read
|
|
673
|
+
]);
|
|
674
|
+
if (failed) return null;
|
|
675
|
+
if (src === null) return (0, react.createElement)("div", {
|
|
676
|
+
className: S.tipImgPh,
|
|
677
|
+
"aria-hidden": true
|
|
678
|
+
});
|
|
679
|
+
return (0, react.createElement)("img", {
|
|
680
|
+
className: S.tipImg,
|
|
681
|
+
src,
|
|
682
|
+
alt: "",
|
|
683
|
+
draggable: false
|
|
684
|
+
});
|
|
685
|
+
}
|
|
255
686
|
/** Resolve copy for the current UI language (document lang, DSH sets zh/en). */
|
|
256
687
|
function langStrings() {
|
|
257
688
|
const lang = typeof document !== "undefined" ? (document.documentElement.lang || "zh").toLowerCase() : "zh";
|
|
258
689
|
return STRINGS[lang.startsWith("zh") ? "zh" : "en"];
|
|
259
690
|
}
|
|
260
|
-
function TimelineRail({ useProjection, sessionId, sessionsService }) {
|
|
691
|
+
function TimelineRail({ useProjection, sessionId, sessionsService, inputActions, conversation }) {
|
|
261
692
|
const t = langStrings();
|
|
262
693
|
const projected = useProjection("chatRail");
|
|
263
694
|
const session = sessionId === void 0 ? void 0 : sessionsService.binding(sessionId)?.session;
|
|
@@ -266,14 +697,34 @@ window.__ModuleLoader__.load({
|
|
|
266
697
|
let messages = [];
|
|
267
698
|
if (Array.isArray(projected?.messages) && projected.messages.length > 0) messages = projected.messages.map(normalize).filter((m) => m !== null);
|
|
268
699
|
if (messages.length === 0) messages = collectFromNodes(nodeSnapshot);
|
|
700
|
+
const favorites = (0, react.useSyncExternalStore)(subscribeFavorites, favoritesSnapshot);
|
|
701
|
+
const [favOnly, setFavOnly] = (0, react.useState)(false);
|
|
702
|
+
const showFavToggle = (sessionId === void 0 ? 0 : messages.filter((m) => isFavorite(favorites, sessionId, favoriteIdOfMessage(m))).length) > 0;
|
|
703
|
+
const effectiveMessages = favOnly ? messages.filter((m) => sessionId !== void 0 && isFavorite(favorites, sessionId, favoriteIdOfMessage(m))) : messages;
|
|
704
|
+
(0, react.useEffect)(() => {
|
|
705
|
+
if (favOnly && !showFavToggle) setFavOnly(false);
|
|
706
|
+
}, [favOnly, showFavToggle]);
|
|
269
707
|
const [activeIndex, setActiveIndex] = (0, react.useState)(-1);
|
|
270
708
|
const [show, setShow] = (0, react.useState)(false);
|
|
271
709
|
const [jumping, setJumping] = (0, react.useState)(false);
|
|
272
710
|
const [tip, setTip] = (0, react.useState)(null);
|
|
273
711
|
const navRef = (0, react.useRef)(null);
|
|
274
712
|
const expandedRef = (0, react.useRef)(false);
|
|
713
|
+
const lastPointerRef = (0, react.useRef)(null);
|
|
275
714
|
const jumpAbortRef = (0, react.useRef)(null);
|
|
276
715
|
(0, react.useEffect)(() => () => jumpAbortRef.current?.abort(), []);
|
|
716
|
+
(0, react.useEffect)(() => {
|
|
717
|
+
const el = navRef.current;
|
|
718
|
+
if (el === null) return;
|
|
719
|
+
const onMove = (e) => {
|
|
720
|
+
lastPointerRef.current = {
|
|
721
|
+
x: e.clientX,
|
|
722
|
+
y: e.clientY
|
|
723
|
+
};
|
|
724
|
+
};
|
|
725
|
+
el.addEventListener("pointermove", onMove, { passive: true });
|
|
726
|
+
return () => el.removeEventListener("pointermove", onMove);
|
|
727
|
+
}, []);
|
|
277
728
|
/** Position the tip against the item's CURRENT (post-expand) rect. */
|
|
278
729
|
const positionTip = (index) => {
|
|
279
730
|
if (index < 0) return;
|
|
@@ -306,28 +757,88 @@ window.__ModuleLoader__.load({
|
|
|
306
757
|
}
|
|
307
758
|
const el = navRef.current;
|
|
308
759
|
if (el === null) return;
|
|
309
|
-
|
|
310
|
-
|
|
760
|
+
let settled = false;
|
|
761
|
+
const settle = () => {
|
|
762
|
+
if (settled) return;
|
|
763
|
+
settled = true;
|
|
311
764
|
expandedRef.current = true;
|
|
312
765
|
el.removeEventListener("transitionend", onTransitionEnd);
|
|
766
|
+
clearTimeout(timer);
|
|
767
|
+
const p = lastPointerRef.current;
|
|
768
|
+
if (p !== null) {
|
|
769
|
+
const hit = document.elementFromPoint(p.x, p.y);
|
|
770
|
+
const item = hit === null ? null : hit.closest("[data-crl-index]");
|
|
771
|
+
const idx = item === null ? -1 : Number(item.getAttribute("data-crl-index"));
|
|
772
|
+
if (Number.isInteger(idx) && idx >= 0) positionTip(idx);
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
const onTransitionEnd = (e) => {
|
|
776
|
+
if (e.propertyName !== "width") return;
|
|
777
|
+
settle();
|
|
313
778
|
};
|
|
314
779
|
el.addEventListener("transitionend", onTransitionEnd);
|
|
315
|
-
|
|
780
|
+
const timer = setTimeout(settle, 300);
|
|
781
|
+
return () => {
|
|
782
|
+
el.removeEventListener("transitionend", onTransitionEnd);
|
|
783
|
+
clearTimeout(timer);
|
|
784
|
+
};
|
|
316
785
|
}, [show]);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
786
|
+
const readThumb = (0, react.useMemo)(() => {
|
|
787
|
+
if (sessionId === void 0) return void 0;
|
|
788
|
+
const s = sessionsService.binding(sessionId)?.session;
|
|
789
|
+
return s?.readAttachment === void 0 ? void 0 : (id) => s.readAttachment(id);
|
|
790
|
+
}, [sessionId, sessionsService]);
|
|
791
|
+
const fillToInput = (0, react.useCallback)((messageId) => {
|
|
792
|
+
const m = messages.find((candidate) => favoriteIdOfMessage(candidate) === messageId);
|
|
793
|
+
if (m === void 0 || inputActions === void 0 || sessionId === void 0) return;
|
|
794
|
+
const text = fullTextOf(m, nodeSnapshot) || m.text;
|
|
795
|
+
if (inputActions.setDraft !== void 0) inputActions.setDraft(text);
|
|
796
|
+
const specs = tipImagesOf(m, nodeSnapshot).filter((spec) => spec.kind === "ref");
|
|
797
|
+
if (specs.length === 0 || conversation === void 0 || readThumb === void 0) return;
|
|
798
|
+
(async () => {
|
|
799
|
+
const files = [];
|
|
800
|
+
for (const spec of specs) try {
|
|
801
|
+
const result = await readThumb(spec.attachmentId);
|
|
802
|
+
if (result?.ok !== true || result.value?.data === void 0) continue;
|
|
803
|
+
const mediaType = result.value.attachment?.mediaType ?? spec.mediaType;
|
|
804
|
+
files.push(new File([result.value.data], `attachment-${spec.attachmentId}${mediaType.includes("jpeg") ? ".jpg" : ".png"}`, { type: mediaType }));
|
|
805
|
+
} catch {}
|
|
806
|
+
if (files.length === 0) return;
|
|
807
|
+
const ids = conversation.createDraftImages(files).map((draft) => draft.id).filter((id) => id !== void 0);
|
|
808
|
+
if (ids.length > 0) try {
|
|
809
|
+
inputActions.addImages?.(ids);
|
|
810
|
+
} catch {}
|
|
811
|
+
})();
|
|
812
|
+
}, [
|
|
813
|
+
conversation,
|
|
814
|
+
inputActions,
|
|
815
|
+
messages,
|
|
816
|
+
nodeSnapshot,
|
|
817
|
+
readThumb,
|
|
818
|
+
sessionId
|
|
819
|
+
]);
|
|
820
|
+
(0, react.useEffect)(() => {
|
|
821
|
+
actionCtx.sessionId = sessionId;
|
|
822
|
+
actionCtx.onToggleFavorite = (messageId) => {
|
|
823
|
+
if (sessionId !== void 0) toggleFavorite(sessionId, messageId);
|
|
824
|
+
};
|
|
825
|
+
actionCtx.onFill = fillToInput;
|
|
826
|
+
refreshAllFavoriteButtons(t);
|
|
827
|
+
return () => {
|
|
828
|
+
if (actionCtx.sessionId === sessionId) actionCtx.sessionId = void 0;
|
|
829
|
+
};
|
|
830
|
+
}, [
|
|
831
|
+
sessionId,
|
|
832
|
+
fillToInput,
|
|
833
|
+
t
|
|
834
|
+
]);
|
|
835
|
+
(0, react.useEffect)(() => {
|
|
836
|
+
refreshAllFavoriteButtons(t);
|
|
837
|
+
}, [favorites, t]);
|
|
838
|
+
(0, react.useEffect)(() => {
|
|
839
|
+
if (typeof document === "undefined" || document.body === null) return;
|
|
840
|
+
return startActionInjector(t);
|
|
841
|
+
}, [t]);
|
|
331
842
|
(0, react.useEffect)(() => {
|
|
332
843
|
if (session === void 0) return;
|
|
333
844
|
if (Array.isArray(projected?.messages) && projected.messages.length > 0) return;
|
|
@@ -355,10 +866,10 @@ window.__ModuleLoader__.load({
|
|
|
355
866
|
Array.isArray(projected?.messages) && projected.messages.length > 0 ? "have" : "none"
|
|
356
867
|
]);
|
|
357
868
|
(0, react.useEffect)(() => {
|
|
358
|
-
if (
|
|
869
|
+
if (effectiveMessages.length === 0) return;
|
|
359
870
|
const indexByKey = /* @__PURE__ */ new Map();
|
|
360
|
-
for (let i = 0; i <
|
|
361
|
-
const key = anchorKeyOf(
|
|
871
|
+
for (let i = 0; i < effectiveMessages.length; i++) {
|
|
872
|
+
const key = anchorKeyOf(effectiveMessages[i]);
|
|
362
873
|
if (key !== void 0) indexByKey.set(key, i);
|
|
363
874
|
}
|
|
364
875
|
const updateActive = () => {
|
|
@@ -401,7 +912,7 @@ window.__ModuleLoader__.load({
|
|
|
401
912
|
window.clearInterval(timer);
|
|
402
913
|
if (scrollTimer !== null) clearTimeout(scrollTimer);
|
|
403
914
|
};
|
|
404
|
-
}, [sessionId,
|
|
915
|
+
}, [sessionId, effectiveMessages.length]);
|
|
405
916
|
(0, react.useEffect)(() => {
|
|
406
917
|
if (activeIndex < 0) return;
|
|
407
918
|
const el = navRef.current;
|
|
@@ -415,13 +926,15 @@ window.__ModuleLoader__.load({
|
|
|
415
926
|
});
|
|
416
927
|
}, [activeIndex]);
|
|
417
928
|
if (sessionId === void 0 || messages.length < 2) return null;
|
|
418
|
-
const
|
|
929
|
+
const tipIndex = tip === null ? -1 : favOnly ? messages.findIndex((m) => effectiveMessages[tip.index] !== void 0 && favoriteIdOfMessage(m) === favoriteIdOfMessage(effectiveMessages[tip.index])) : tip.index;
|
|
930
|
+
const items = effectiveMessages.map((m, i) => {
|
|
419
931
|
const key = anchorKeyOf(m);
|
|
932
|
+
const starred = favoriteIdOfMessage(m) !== "" && isFavorite(favorites, sessionId, favoriteIdOfMessage(m));
|
|
420
933
|
return (0, react.createElement)("button", {
|
|
421
934
|
type: "button",
|
|
422
935
|
key: m.seq,
|
|
423
936
|
"data-crl-index": String(i),
|
|
424
|
-
className: S.item + (activeIndex === i ? ` ${S.itemActive}` : ""),
|
|
937
|
+
className: S.item + (activeIndex === i ? ` ${S.itemActive}` : "") + (starred ? ` ${S.favItem}` : ""),
|
|
425
938
|
"aria-label": `${t.roleUser}: ${m.text.slice(0, 60) || t.noText} (${t.ariaJump})`,
|
|
426
939
|
"aria-current": activeIndex === i ? "location" : void 0,
|
|
427
940
|
disabled: jumping,
|
|
@@ -437,12 +950,50 @@ window.__ModuleLoader__.load({
|
|
|
437
950
|
onMouseLeave: () => handleItemLeave(i),
|
|
438
951
|
children: [
|
|
439
952
|
(0, react.createElement)("span", { className: S.num }, `#${i + 1}`),
|
|
953
|
+
starred ? (0, react.createElement)("span", {
|
|
954
|
+
className: S.favStar,
|
|
955
|
+
role: "img",
|
|
956
|
+
"aria-label": t.fav,
|
|
957
|
+
"aria-hidden": void 0
|
|
958
|
+
}, (0, react.createElement)("svg", {
|
|
959
|
+
viewBox: "0 0 24 24",
|
|
960
|
+
width: 13,
|
|
961
|
+
height: 13,
|
|
962
|
+
fill: "currentColor",
|
|
963
|
+
stroke: "none",
|
|
964
|
+
"aria-hidden": true
|
|
965
|
+
}, (0, react.createElement)("path", {
|
|
966
|
+
d: STAR_PATH,
|
|
967
|
+
"aria-hidden": true
|
|
968
|
+
}))) : null,
|
|
440
969
|
(0, react.createElement)("span", { className: S.title }, m.text === "" ? t.noText : m.text),
|
|
441
970
|
m.hasImage ? (0, react.createElement)("span", {
|
|
442
971
|
className: S.img,
|
|
443
|
-
|
|
972
|
+
role: "img",
|
|
444
973
|
"aria-label": t.hasImage
|
|
445
|
-
}
|
|
974
|
+
}, (0, react.createElement)("svg", {
|
|
975
|
+
viewBox: "0 0 16 16",
|
|
976
|
+
width: 12,
|
|
977
|
+
height: 12,
|
|
978
|
+
fill: "none",
|
|
979
|
+
stroke: "currentColor",
|
|
980
|
+
strokeWidth: 1.4,
|
|
981
|
+
strokeLinecap: "round",
|
|
982
|
+
strokeLinejoin: "round",
|
|
983
|
+
"aria-hidden": true
|
|
984
|
+
}, (0, react.createElement)("rect", {
|
|
985
|
+
x: 1.7,
|
|
986
|
+
y: 2.7,
|
|
987
|
+
width: 12.6,
|
|
988
|
+
height: 10.6,
|
|
989
|
+
rx: 2.2
|
|
990
|
+
}), (0, react.createElement)("circle", {
|
|
991
|
+
cx: 5.6,
|
|
992
|
+
cy: 6.4,
|
|
993
|
+
r: 1.15,
|
|
994
|
+
fill: "currentColor",
|
|
995
|
+
stroke: "none"
|
|
996
|
+
}), (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
997
|
(0, react.createElement)("span", { className: S.time }, relativeTime(m.time, t)),
|
|
447
998
|
(0, react.createElement)("span", {
|
|
448
999
|
className: S.ind,
|
|
@@ -458,31 +1009,88 @@ window.__ModuleLoader__.load({
|
|
|
458
1009
|
"aria-label": t.railLabel,
|
|
459
1010
|
onMouseEnter: () => setShow(true),
|
|
460
1011
|
onMouseLeave: () => setShow(false),
|
|
461
|
-
children: [
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1012
|
+
children: [
|
|
1013
|
+
showFavToggle ? (0, react.createElement)("button", {
|
|
1014
|
+
type: "button",
|
|
1015
|
+
key: "favToggle",
|
|
1016
|
+
className: S.favToggle + (favOnly ? " crl_on" : ""),
|
|
1017
|
+
"aria-pressed": favOnly,
|
|
1018
|
+
"aria-label": t.favOnly,
|
|
1019
|
+
title: t.favOnly,
|
|
1020
|
+
onMouseEnter: (e) => e.stopPropagation(),
|
|
1021
|
+
onClick: () => setFavOnly((v) => !v),
|
|
1022
|
+
children: [(0, react.createElement)("svg", {
|
|
1023
|
+
viewBox: "0 0 24 24",
|
|
1024
|
+
width: 14,
|
|
1025
|
+
height: 14,
|
|
1026
|
+
fill: favOnly ? "currentColor" : "none",
|
|
1027
|
+
stroke: "currentColor",
|
|
1028
|
+
strokeWidth: 2,
|
|
1029
|
+
strokeLinejoin: "round",
|
|
1030
|
+
"aria-hidden": true
|
|
1031
|
+
}, (0, react.createElement)("path", { d: STAR_PATH })), (0, react.createElement)("span", { className: S.favToggleLabel }, t.favOnly)]
|
|
1032
|
+
}) : null,
|
|
1033
|
+
jumping ? (0, react.createElement)("div", {
|
|
1034
|
+
className: S.loading,
|
|
1035
|
+
key: "loading"
|
|
1036
|
+
}, (0, react.createElement)("span", { className: S.loadingLabel }, t.loading)) : null,
|
|
1037
|
+
...items
|
|
1038
|
+
]
|
|
1039
|
+
}), tip !== null && tipIndex >= 0 && tipIndex < messages.length ? (0, react.createElement)("div", {
|
|
466
1040
|
className: S.tip,
|
|
467
1041
|
style: {
|
|
468
1042
|
left: `${tip.x}px`,
|
|
469
1043
|
top: `${tip.y}px`,
|
|
470
1044
|
transform: "translateX(-100%)"
|
|
471
1045
|
}
|
|
472
|
-
},
|
|
1046
|
+
}, (() => {
|
|
1047
|
+
const m = messages[tipIndex];
|
|
1048
|
+
const specs = tipImagesOf(m, nodeSnapshot);
|
|
1049
|
+
const children = [];
|
|
1050
|
+
if (specs.length > 0 && readThumb !== void 0) children.push((0, react.createElement)("div", {
|
|
1051
|
+
className: S.tipImgs,
|
|
1052
|
+
key: "imgs"
|
|
1053
|
+
}, (0, react.createElement)("div", {
|
|
1054
|
+
className: S.tipImgWrap,
|
|
1055
|
+
key: "img"
|
|
1056
|
+
}, (0, react.createElement)(TipThumb, {
|
|
1057
|
+
spec: specs[0],
|
|
1058
|
+
sessionId,
|
|
1059
|
+
read: readThumb
|
|
1060
|
+
}), specs.length > 1 ? (0, react.createElement)("span", {
|
|
1061
|
+
className: S.tipImgCount,
|
|
1062
|
+
"aria-hidden": true
|
|
1063
|
+
}, `+${specs.length - 1}`) : null)));
|
|
1064
|
+
else if (m.hasImage) children.push((0, react.createElement)("span", {
|
|
1065
|
+
className: S.tipBadge,
|
|
1066
|
+
key: "badge"
|
|
1067
|
+
}, t.hasImage));
|
|
1068
|
+
children.push((0, react.createElement)("span", { key: "text" }, fullTextOf(m, nodeSnapshot) || t.noText));
|
|
1069
|
+
return children;
|
|
1070
|
+
})()) : null], document.body);
|
|
473
1071
|
}
|
|
474
1072
|
function apply(ctx) {
|
|
475
1073
|
ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
|
|
476
1074
|
name: "conversation.input.dock",
|
|
477
1075
|
id: "chat-rail",
|
|
478
1076
|
order: 40,
|
|
479
|
-
inject: () => ({
|
|
1077
|
+
inject: () => ({
|
|
1078
|
+
sessionsService: ctx.sessions,
|
|
1079
|
+
conversation: ctx.conversation
|
|
1080
|
+
})
|
|
480
1081
|
}, TimelineRail));
|
|
481
1082
|
}
|
|
482
1083
|
//#endregion
|
|
483
1084
|
exports.TimelineRail = TimelineRail;
|
|
484
1085
|
exports.apply = apply;
|
|
1086
|
+
exports.favoriteIdsOf = favoriteIdsOf;
|
|
1087
|
+
exports.imageSpecsOfContent = imageSpecsOfContent;
|
|
485
1088
|
exports.inject = inject;
|
|
1089
|
+
exports.isFavorite = isFavorite;
|
|
1090
|
+
exports.messageIdOfAnchorKey = messageIdOfAnchorKey;
|
|
1091
|
+
exports.normalize = normalize;
|
|
1092
|
+
exports.readFavorites = readFavorites;
|
|
1093
|
+
exports.toggleFavoriteId = toggleFavoriteId;
|
|
486
1094
|
return module.exports;
|
|
487
1095
|
}
|
|
488
1096
|
});
|