dsh-quick-toc 0.4.1 → 0.5.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/CHANGELOG.en.md +23 -0
- package/CHANGELOG.md +23 -0
- package/README.en.md +18 -9
- package/README.md +18 -9
- package/lib/client.js +1229 -234
- package/package.json +5 -3
package/lib/client.js
CHANGED
|
@@ -21,6 +21,57 @@ window.__ModuleLoader__.load({
|
|
|
21
21
|
var Z_BASE = 500;
|
|
22
22
|
var EASE = "cubic-bezier(0.22, 0.9, 0.3, 1)"; // smooth non-linear slide
|
|
23
23
|
|
|
24
|
+
// ---------- UI strings ----------
|
|
25
|
+
// Every string the panel renders goes through T(). 0.5.0 ships the Chinese
|
|
26
|
+
// column only — the visible text is byte-identical to 0.4.1 — so 0.6.0 can
|
|
27
|
+
// add English (or feed the same keys into the host locale table) without
|
|
28
|
+
// hunting for hard-coded literals a second time.
|
|
29
|
+
var DICTS = {
|
|
30
|
+
zh: {
|
|
31
|
+
"panel.title": "对话大纲",
|
|
32
|
+
"error.panel": "dsh-quick-toc 面板出错: ",
|
|
33
|
+
"handle.dragY": "按住拖动调整位置",
|
|
34
|
+
"handle.dockLeft": "移到左侧",
|
|
35
|
+
"handle.dockRight": "移到右侧",
|
|
36
|
+
"handle.collapse": "收起",
|
|
37
|
+
"handle.expand": "展开大纲",
|
|
38
|
+
"levels.tip": "标题层级筛选",
|
|
39
|
+
"levels.show": "显示 H",
|
|
40
|
+
"levels.hide": "隐藏 H",
|
|
41
|
+
"search.open": "搜索标题",
|
|
42
|
+
"search.word": "搜索",
|
|
43
|
+
"search.tail": ",回车定位…",
|
|
44
|
+
"search.scope.title": "标题",
|
|
45
|
+
"search.scope.full": "全文",
|
|
46
|
+
"search.scope.tipTitle": "当前:仅搜索标题。点击切换为全文搜索",
|
|
47
|
+
"search.scope.tipFull": "当前:全文搜索。点击切换为仅标题",
|
|
48
|
+
"search.fuzzy": "模糊",
|
|
49
|
+
"search.fuzzy.tipOn": "模糊匹配已开启:允许关键字中间夹少量其他文字,命中更多",
|
|
50
|
+
"search.fuzzy.tipOff": "模糊匹配已关闭:只匹配连续的文字。点击开启",
|
|
51
|
+
"search.empty": "没有匹配",
|
|
52
|
+
"search.hintMore": "向上滚动可加载更早的消息",
|
|
53
|
+
"search.hintOldest": "已经是最早的消息",
|
|
54
|
+
"hint.bottom": "已经到底了",
|
|
55
|
+
"turn.jumpReply": "跳转到该回合的模型回答开头",
|
|
56
|
+
"turn.jumpTurn": "跳转到该回合开头",
|
|
57
|
+
"turn.loading": "正在加载这个回合…",
|
|
58
|
+
"turn.unloaded": "未加载",
|
|
59
|
+
"turn.loadTip": "点击加载这个回合并跳转过去",
|
|
60
|
+
"preview.truncated": "预览由宿主提供,可能被截断",
|
|
61
|
+
"resize.w": "拖拽调整宽度",
|
|
62
|
+
"resize.h": "拖拽调整高度",
|
|
63
|
+
"resize.wh": "拖拽同时调整宽高"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
// The dictionary the current render reads. Kept as a module-level binding so
|
|
67
|
+
// the module-scope render helpers (renderItem/renderResultRow/...) can localise
|
|
68
|
+
// without threading a `t` argument through every call.
|
|
69
|
+
var activeDict = DICTS.zh;
|
|
70
|
+
function T(key) {
|
|
71
|
+
var s = activeDict[key];
|
|
72
|
+
return s === undefined ? key : s;
|
|
73
|
+
}
|
|
74
|
+
|
|
24
75
|
// ---------- markdown helpers ----------
|
|
25
76
|
function extractReplyText(node) {
|
|
26
77
|
var blocks = node && node.data && node.data.blocks;
|
|
@@ -78,6 +129,153 @@ window.__ModuleLoader__.load({
|
|
|
78
129
|
return first;
|
|
79
130
|
}
|
|
80
131
|
|
|
132
|
+
// ---------- normalized matching (fold, plus an optional fuzzy pass) ----------
|
|
133
|
+
// Folding ignores differences that never change meaning: letter case,
|
|
134
|
+
// full-width vs half-width forms (AB12,), and runs of whitespace. `map`
|
|
135
|
+
// keeps every folded character's ORIGINAL index, so a match found in folded
|
|
136
|
+
// space still highlights the untouched DOM text. Folding is ALWAYS on — it is
|
|
137
|
+
// correctness, not fuzziness: pasting "全角" text must find "全角" content.
|
|
138
|
+
function foldWithMap(text) {
|
|
139
|
+
var src = String(text === undefined || text === null ? "" : text);
|
|
140
|
+
var out = "";
|
|
141
|
+
var map = [];
|
|
142
|
+
var prevSpace = false;
|
|
143
|
+
for (var i = 0; i < src.length; i++) {
|
|
144
|
+
var ch = src.charAt(i);
|
|
145
|
+
var code = src.charCodeAt(i);
|
|
146
|
+
if (code >= 0xFF01 && code <= 0xFF5E) ch = String.fromCharCode(code - 0xFEE0); // full-width ASCII
|
|
147
|
+
else if (code === 0x3000) ch = " "; // ideographic space
|
|
148
|
+
var lower = ch.toLowerCase();
|
|
149
|
+
if (/\s/.test(lower)) {
|
|
150
|
+
if (prevSpace) continue; // collapse a whitespace run to one space
|
|
151
|
+
prevSpace = true;
|
|
152
|
+
out += " ";
|
|
153
|
+
map.push(i);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
prevSpace = false;
|
|
157
|
+
out += lower;
|
|
158
|
+
map.push(i);
|
|
159
|
+
}
|
|
160
|
+
map.push(src.length); // end sentinel: source index just past the last character
|
|
161
|
+
return { text: out, map: map };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function foldQuery(q) {
|
|
165
|
+
return foldWithMap(q).text.trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Contiguous occurrences of the folded query — the panel's original
|
|
169
|
+
// behaviour (case-insensitive, non-overlapping), now fold-aware.
|
|
170
|
+
function exactMatches(folded, fq) {
|
|
171
|
+
var hits = [];
|
|
172
|
+
if (!fq) return hits;
|
|
173
|
+
var at = 0;
|
|
174
|
+
while ((at = folded.text.indexOf(fq, at)) !== -1) {
|
|
175
|
+
hits.push({ ranges: [[folded.map[at], folded.map[at + fq.length]]] });
|
|
176
|
+
at += fq.length;
|
|
177
|
+
}
|
|
178
|
+
return hits;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// How far apart the matched characters of one fuzzy hit may sit. Without a
|
|
182
|
+
// bound, "提交" would match any text that happens to contain 提…交 somewhere,
|
|
183
|
+
// which buries the real hits; the span keeps fuzzy useful for "关键字中间夹
|
|
184
|
+
// 了别的字" without turning the list into noise.
|
|
185
|
+
function fuzzySpan(fq) {
|
|
186
|
+
return Math.max(12, fq.length * 2 + 8);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Subsequence occurrences (fuzzy mode): the query's characters must appear in
|
|
190
|
+
// order, each hit is the shortest window found from the current position, and
|
|
191
|
+
// hits never overlap. Adjacent matched characters are merged into one range so
|
|
192
|
+
// a contiguous hit still highlights as a whole word.
|
|
193
|
+
function fuzzyMatches(folded, fq) {
|
|
194
|
+
var hits = [];
|
|
195
|
+
if (!fq) return hits;
|
|
196
|
+
var span = fuzzySpan(fq);
|
|
197
|
+
var from = 0;
|
|
198
|
+
for (;;) {
|
|
199
|
+
var start = -1;
|
|
200
|
+
var at = from;
|
|
201
|
+
var ranges = [];
|
|
202
|
+
var ok = true;
|
|
203
|
+
for (var i = 0; i < fq.length; i++) {
|
|
204
|
+
var hit = folded.text.indexOf(fq.charAt(i), at);
|
|
205
|
+
if (hit === -1 || (start !== -1 && hit - start > span)) { ok = false; break; }
|
|
206
|
+
if (start === -1) start = hit;
|
|
207
|
+
var raw = [folded.map[hit], folded.map[hit + 1]];
|
|
208
|
+
var last = ranges.length > 0 ? ranges[ranges.length - 1] : null;
|
|
209
|
+
if (last !== null && last[1] === raw[0] && folded.text.charAt(hit - 1) !== " ") last[1] = raw[1];
|
|
210
|
+
else ranges.push(raw);
|
|
211
|
+
at = hit + 1;
|
|
212
|
+
}
|
|
213
|
+
if (!ok) break;
|
|
214
|
+
hits.push({ ranges: ranges });
|
|
215
|
+
from = at;
|
|
216
|
+
if (from >= folded.text.length) break;
|
|
217
|
+
}
|
|
218
|
+
return hits;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function findMatches(folded, fq, fuzzy) {
|
|
222
|
+
if (!fq) return [];
|
|
223
|
+
return fuzzy ? fuzzyMatches(folded, fq) : exactMatches(folded, fq);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Number of hits of the folded query in `text` (used for ×N and n/N counts).
|
|
227
|
+
function countOccurrences(text, q, fuzzy) {
|
|
228
|
+
var fq = q && q.folded !== undefined ? q.folded : foldQuery(q);
|
|
229
|
+
if (!text || !fq) return 0;
|
|
230
|
+
return findMatches(foldWithMap(text), fq, !!fuzzy).length;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------- section bodies: subtitle + hover preview ----------
|
|
234
|
+
// Body lines of one heading: everything after it up to the next heading in
|
|
235
|
+
// the same message.
|
|
236
|
+
function sectionPreview(lines, from, to, max) {
|
|
237
|
+
if (!lines || from >= to) return "";
|
|
238
|
+
var out = "";
|
|
239
|
+
var fence = null;
|
|
240
|
+
for (var i = from; i < to; i++) {
|
|
241
|
+
var line = lines[i];
|
|
242
|
+
var fm = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
243
|
+
if (fence !== null) {
|
|
244
|
+
if (fm && fm[1].charAt(0) === fence.char && fm[1].length >= fence.len && fm[2].trim() === "") fence = null;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (fm) { fence = { char: fm[1].charAt(0), len: fm[1].length }; continue; }
|
|
248
|
+
var t = line.trim();
|
|
249
|
+
if (t === "") continue;
|
|
250
|
+
out += (out === "" ? "" : " ") + t;
|
|
251
|
+
if (out.length >= max) return out.slice(0, max).trim() + "…";
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// First readable sentence of a section — the outline row's subtitle. Skips
|
|
257
|
+
// blank lines, code fences, table rules and bare bullet/emphasis markers so a
|
|
258
|
+
// "###" block that opens with a table still gets a meaningful line.
|
|
259
|
+
function firstSentence(lines, from, to, max) {
|
|
260
|
+
if (!lines || from >= to) return "";
|
|
261
|
+
var fence = null;
|
|
262
|
+
for (var i = from; i < to; i++) {
|
|
263
|
+
var line = lines[i];
|
|
264
|
+
var fm = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
265
|
+
if (fence !== null) {
|
|
266
|
+
if (fm && fm[1].charAt(0) === fence.char && fm[1].length >= fence.len && fm[2].trim() === "") fence = null;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (fm) { fence = { char: fm[1].charAt(0), len: fm[1].length }; continue; }
|
|
270
|
+
var t = line.trim().replace(/^[>+\-*]\s+/, "").replace(/^\d+[.)]\s+/, "");
|
|
271
|
+
if (t === "" || /^[|\-=*_\s]+$/.test(t)) continue;
|
|
272
|
+
t = cleanTitle(t);
|
|
273
|
+
if (t === "") continue;
|
|
274
|
+
return t.length > max ? t.slice(0, max).trim() + "…" : t;
|
|
275
|
+
}
|
|
276
|
+
return "";
|
|
277
|
+
}
|
|
278
|
+
|
|
81
279
|
// Headings are collected line by line so FENCED CODE BLOCKS can be skipped:
|
|
82
280
|
// a "```" block that documents markdown (or shows a shell comment like
|
|
83
281
|
// "# install") contains lines that look like headings but are code — they
|
|
@@ -99,18 +297,18 @@ window.__ModuleLoader__.load({
|
|
|
99
297
|
}
|
|
100
298
|
if (fm) { fence = { char: fm[1].charAt(0), len: fm[1].length }; continue; }
|
|
101
299
|
var m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
102
|
-
if (m !== null) items.push({ level: m[1].length, title: cleanTitle(m[2].trim()) });
|
|
300
|
+
if (m !== null) items.push({ level: m[1].length, title: cleanTitle(m[2].trim()), line: i });
|
|
103
301
|
}
|
|
104
302
|
return items;
|
|
105
303
|
}
|
|
106
304
|
|
|
107
|
-
function buildTree(headings) {
|
|
305
|
+
function buildTree(headings, time) {
|
|
108
306
|
var root = { level: 0, children: [] };
|
|
109
307
|
var stack = [root];
|
|
110
308
|
for (var i = 0; i < headings.length; i++) {
|
|
111
309
|
var h = headings[i];
|
|
112
310
|
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) stack.pop();
|
|
113
|
-
var node = { level: h.level, title: h.title, key: h.key, idx: h.idx, children: [] };
|
|
311
|
+
var node = { level: h.level, title: h.title, key: h.key, idx: h.idx, sub: h.sub, preview: h.preview, time: time, children: [] };
|
|
114
312
|
stack[stack.length - 1].children.push(node);
|
|
115
313
|
stack.push(node);
|
|
116
314
|
}
|
|
@@ -122,6 +320,7 @@ window.__ModuleLoader__.load({
|
|
|
122
320
|
// so a streaming update only re-joins/re-parses the node that actually
|
|
123
321
|
// changed instead of every message in the history.
|
|
124
322
|
var EMPTY_HEADINGS = [];
|
|
323
|
+
var EMPTY_LINES = [];
|
|
125
324
|
var nodeInfoCache = new Map();
|
|
126
325
|
function nodeInfo(key, node) {
|
|
127
326
|
var isUser = node.kind === "user";
|
|
@@ -132,12 +331,24 @@ window.__ModuleLoader__.load({
|
|
|
132
331
|
return cached;
|
|
133
332
|
}
|
|
134
333
|
var text = isUser ? extractUserText(node) : extractReplyText(node);
|
|
334
|
+
var lines = text ? text.split("\n") : EMPTY_LINES;
|
|
335
|
+
var headings = node.kind === "assistant-step" ? parseHeadings(text) : EMPTY_HEADINGS;
|
|
336
|
+
// per-heading subtitle (first sentence) + hover preview (section opening) —
|
|
337
|
+
// computed here, with the parse, so streaming updates only pay for the node
|
|
338
|
+
// that actually changed. `line` marks where the heading sits in `lines`, and
|
|
339
|
+
// its section runs to the next heading of ANY level (or the message end).
|
|
340
|
+
for (var hi = 0; hi < headings.length; hi++) {
|
|
341
|
+
var toLine = hi + 1 < headings.length ? headings[hi + 1].line : lines.length;
|
|
342
|
+
headings[hi].sub = firstSentence(lines, headings[hi].line + 1, toLine, 44);
|
|
343
|
+
headings[hi].preview = sectionPreview(lines, headings[hi].line + 1, toLine, 260);
|
|
344
|
+
}
|
|
135
345
|
var info = {
|
|
136
346
|
node: node,
|
|
137
347
|
blocks: blocks,
|
|
138
348
|
user: isUser,
|
|
139
349
|
text: text,
|
|
140
|
-
|
|
350
|
+
lines: lines,
|
|
351
|
+
headings: headings
|
|
141
352
|
};
|
|
142
353
|
nodeInfoCache.set(key, info);
|
|
143
354
|
return info;
|
|
@@ -155,33 +366,44 @@ window.__ModuleLoader__.load({
|
|
|
155
366
|
return h.key + "#" + h.idx;
|
|
156
367
|
}
|
|
157
368
|
|
|
158
|
-
// Split `text` into runs around
|
|
159
|
-
//
|
|
160
|
-
|
|
369
|
+
// Split `text` into runs around the hits of `q`, so the caller can render the
|
|
370
|
+
// hits distinctly. Returns [{ text, hit }, ...]. Runs on folded text, so a
|
|
371
|
+
// full-width or differently-cased match is highlighted where it really sits,
|
|
372
|
+
// and a fuzzy hit paints each matched character run.
|
|
373
|
+
function highlightParts(text, q, fuzzy) {
|
|
161
374
|
var parts = [];
|
|
162
375
|
if (!text) return parts;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
var
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
376
|
+
var fq = foldQuery(q);
|
|
377
|
+
if (!fq) return [{ text: text, hit: false }];
|
|
378
|
+
var hits = findMatches(foldWithMap(text), fq, !!fuzzy);
|
|
379
|
+
if (hits.length === 0) return [{ text: text, hit: false }];
|
|
380
|
+
var cursor = 0;
|
|
381
|
+
for (var i = 0; i < hits.length; i++) {
|
|
382
|
+
var ranges = hits[i].ranges;
|
|
383
|
+
for (var r = 0; r < ranges.length; r++) {
|
|
384
|
+
var from = ranges[r][0];
|
|
385
|
+
var to = ranges[r][1];
|
|
386
|
+
if (from < cursor || to <= from) continue;
|
|
387
|
+
if (from > cursor) parts.push({ text: text.slice(cursor, from), hit: false });
|
|
388
|
+
parts.push({ text: text.slice(from, to), hit: true });
|
|
389
|
+
cursor = to;
|
|
390
|
+
}
|
|
172
391
|
}
|
|
173
|
-
if (
|
|
392
|
+
if (cursor < text.length) parts.push({ text: text.slice(cursor), hit: false });
|
|
174
393
|
return parts;
|
|
175
394
|
}
|
|
176
395
|
|
|
177
396
|
// A one-line window around the first hit of `q` in `text` (for search results).
|
|
178
|
-
function snippetAround(text, q, span) {
|
|
397
|
+
function snippetAround(text, q, span, fuzzy) {
|
|
179
398
|
if (!text) return "";
|
|
180
399
|
var flat = text.replace(/\s+/g, " ").trim();
|
|
181
|
-
var
|
|
182
|
-
|
|
400
|
+
var fq = foldQuery(q);
|
|
401
|
+
var folded = fq ? foldWithMap(flat) : null;
|
|
402
|
+
var hits = folded ? findMatches(folded, fq, !!fuzzy) : [];
|
|
403
|
+
if (hits.length === 0) return previewText(flat, span * 2);
|
|
404
|
+
var at = folded.map[hits[0].ranges[0][0]];
|
|
183
405
|
var start = Math.max(0, at - span);
|
|
184
|
-
var end = Math.min(flat.length, at +
|
|
406
|
+
var end = Math.min(flat.length, at + span * 2);
|
|
185
407
|
return (start > 0 ? "…" : "") + flat.slice(start, end) + (end < flat.length ? "…" : "");
|
|
186
408
|
}
|
|
187
409
|
|
|
@@ -228,21 +450,6 @@ window.__ModuleLoader__.load({
|
|
|
228
450
|
}
|
|
229
451
|
|
|
230
452
|
// count every occurrence of q (case-insensitive) inside text
|
|
231
|
-
function countOccurrences(text, q) {
|
|
232
|
-
// an empty needle would make indexOf return 0 forever and freeze the tab;
|
|
233
|
-
// every caller passes a trimmed non-empty query today, but the guard keeps
|
|
234
|
-
// a future call site from hanging the UI.
|
|
235
|
-
if (!text || !q) return 0;
|
|
236
|
-
var lower = text.toLowerCase();
|
|
237
|
-
var count = 0;
|
|
238
|
-
var idx = 0;
|
|
239
|
-
while ((idx = lower.indexOf(q, idx)) !== -1) {
|
|
240
|
-
count++;
|
|
241
|
-
idx += q.length;
|
|
242
|
-
}
|
|
243
|
-
return count;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
453
|
// find the conversation's "load older messages" button (scoped to the
|
|
247
454
|
// conversation scrollport so panel buttons are never matched)
|
|
248
455
|
function findLoadOlderButton() {
|
|
@@ -320,10 +527,17 @@ window.__ModuleLoader__.load({
|
|
|
320
527
|
highlightSpans = [];
|
|
321
528
|
}
|
|
322
529
|
|
|
323
|
-
// wrap every
|
|
324
|
-
//
|
|
325
|
-
|
|
530
|
+
// wrap every hit of q inside row's text nodes; the hit at `currentOcc` gets a
|
|
531
|
+
// distinct "current" highlight. Matching runs on FOLDED text (case/full-width/
|
|
532
|
+
// whitespace insensitive) and, in fuzzy mode, on subsequences — a hit is then a
|
|
533
|
+
// set of character ranges, so one fuzzy hit can paint several spans.
|
|
534
|
+
// NOTE: these fills sit ON TOP OF conversation text, so they stay deliberately
|
|
535
|
+
// stronger than the panel's UI tints (C.chip / C.groupTint); only the current
|
|
536
|
+
// hit's outline reuses the shared accent value.
|
|
537
|
+
function highlightRow(row, q, currentOcc, fuzzy) {
|
|
326
538
|
if (!row || !q) return;
|
|
539
|
+
var fq = foldQuery(q);
|
|
540
|
+
if (!fq) return;
|
|
327
541
|
var walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT, null);
|
|
328
542
|
var textNodes = [];
|
|
329
543
|
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
@@ -332,38 +546,41 @@ window.__ModuleLoader__.load({
|
|
|
332
546
|
var node = textNodes[i];
|
|
333
547
|
var text = node.nodeValue;
|
|
334
548
|
if (!text) continue;
|
|
335
|
-
var
|
|
336
|
-
if (
|
|
549
|
+
var hits = findMatches(foldWithMap(text), fq, !!fuzzy);
|
|
550
|
+
if (hits.length === 0) continue;
|
|
337
551
|
var frag = document.createDocumentFragment();
|
|
338
|
-
// EVERY
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
552
|
+
// EVERY hit inside this text node must be wrapped, in order: the caller
|
|
553
|
+
// addresses hits by their global index (`occ`), which is counted over the
|
|
554
|
+
// message text. Wrapping only the first one per text node made a second
|
|
555
|
+
// hit on the same line un-markable, so stepping to it found no
|
|
342
556
|
// `.dqt-current` and fell back to a plain scroll with no highlight.
|
|
343
|
-
var
|
|
344
|
-
var
|
|
345
|
-
|
|
346
|
-
var before = text.slice(from, idx);
|
|
347
|
-
if (before) frag.appendChild(document.createTextNode(before));
|
|
348
|
-
var mark = document.createElement("span");
|
|
349
|
-
if (occ === currentOcc) {
|
|
350
|
-
mark.className = "dqt-current";
|
|
351
|
-
mark.style.background = "rgba(79,140,255,0.55)";
|
|
352
|
-
mark.style.boxShadow = "0 0 0 1px rgba(79,140,255,0.85)";
|
|
353
|
-
} else {
|
|
354
|
-
mark.style.background = "rgba(79,140,255,0.32)";
|
|
355
|
-
}
|
|
356
|
-
mark.style.borderRadius = "2px";
|
|
357
|
-
mark.style.color = "inherit";
|
|
358
|
-
mark.textContent = text.slice(idx, idx + q.length);
|
|
359
|
-
frag.appendChild(mark);
|
|
360
|
-
highlightSpans.push(mark);
|
|
557
|
+
var cursor = 0;
|
|
558
|
+
for (var h = 0; h < hits.length; h++) {
|
|
559
|
+
var isCurrent = occ === currentOcc;
|
|
361
560
|
occ++;
|
|
362
|
-
|
|
363
|
-
|
|
561
|
+
var ranges = hits[h].ranges;
|
|
562
|
+
for (var r = 0; r < ranges.length; r++) {
|
|
563
|
+
var from = ranges[r][0];
|
|
564
|
+
var to = ranges[r][1];
|
|
565
|
+
if (from < cursor || to <= from) continue;
|
|
566
|
+
if (from > cursor) frag.appendChild(document.createTextNode(text.slice(cursor, from)));
|
|
567
|
+
var mark = document.createElement("span");
|
|
568
|
+
if (isCurrent) {
|
|
569
|
+
mark.className = "dqt-current";
|
|
570
|
+
mark.style.background = "rgba(79,140,255,0.55)";
|
|
571
|
+
mark.style.boxShadow = "0 0 0 1px rgba(79,140,255,0.85)";
|
|
572
|
+
} else {
|
|
573
|
+
mark.style.background = "rgba(79,140,255,0.32)";
|
|
574
|
+
}
|
|
575
|
+
mark.style.borderRadius = "2px";
|
|
576
|
+
mark.style.color = "inherit";
|
|
577
|
+
mark.textContent = text.slice(from, to);
|
|
578
|
+
frag.appendChild(mark);
|
|
579
|
+
highlightSpans.push(mark);
|
|
580
|
+
cursor = to;
|
|
581
|
+
}
|
|
364
582
|
}
|
|
365
|
-
|
|
366
|
-
if (after) frag.appendChild(document.createTextNode(after));
|
|
583
|
+
if (cursor < text.length) frag.appendChild(document.createTextNode(text.slice(cursor)));
|
|
367
584
|
node.parentNode.replaceChild(frag, node);
|
|
368
585
|
}
|
|
369
586
|
}
|
|
@@ -376,7 +593,14 @@ window.__ModuleLoader__.load({
|
|
|
376
593
|
muted: "var(--dsw-alias-label-secondary, #9aa0ab)",
|
|
377
594
|
accent: "var(--dsw-alias-brand-primary, #4f8cff)",
|
|
378
595
|
hover: "var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))",
|
|
379
|
-
|
|
596
|
+
// ONE source for every "this control is on / this button is open / this match
|
|
597
|
+
// is current" tint, so the panel cannot drift into five near-identical blues.
|
|
598
|
+
chip: "rgba(79, 140, 255, 0.18)",
|
|
599
|
+
// the followed group is a LARGE surface: same hue, lighter fill, with a
|
|
600
|
+
// closing outline + accent bar so the block reads as a finished shape.
|
|
601
|
+
groupTint: "rgba(79, 140, 255, 0.10)",
|
|
602
|
+
groupEdge: "rgba(79, 140, 255, 0.85)",
|
|
603
|
+
groupEdgeSoft: "rgba(79, 140, 255, 0.35)"
|
|
380
604
|
};
|
|
381
605
|
|
|
382
606
|
// ---- theme scheme detection (for the panel's 3D inner shadow) ----
|
|
@@ -444,7 +668,7 @@ window.__ModuleLoader__.load({
|
|
|
444
668
|
maxWidth: 340,
|
|
445
669
|
wordBreak: "break-all"
|
|
446
670
|
},
|
|
447
|
-
children: "
|
|
671
|
+
children: T("error.panel") + msg
|
|
448
672
|
});
|
|
449
673
|
}
|
|
450
674
|
return this.props.children;
|
|
@@ -466,6 +690,68 @@ window.__ModuleLoader__.load({
|
|
|
466
690
|
var order = useChat(function (s) { return s.order; });
|
|
467
691
|
var nodes = useChat(function (s) { return s.nodes; });
|
|
468
692
|
|
|
693
|
+
// Whole-log turn index: the host's `turnOutline` projection
|
|
694
|
+
// (@deepseek-ai/dsh-session-turn-outline) names EVERY turn of the session —
|
|
695
|
+
// including turns the paged event window has not loaded yet — with a bounded
|
|
696
|
+
// prompt/response preview and the `turn/start` seq that pages history to it.
|
|
697
|
+
// On a host without that projection the value is undefined and the panel
|
|
698
|
+
// behaves exactly like 0.4.1 (loaded turns only).
|
|
699
|
+
var useProjection = props.useProjection;
|
|
700
|
+
var rawOutline = useProjection ? useProjection("turnOutline") : null;
|
|
701
|
+
// { sessions() } bridge to the host session face, injected by apply().
|
|
702
|
+
// It is a LOOKUP (ctx.get), not a cached reference: the service may be
|
|
703
|
+
// registered after this package loads, and an unloaded provider must not
|
|
704
|
+
// leave a stale handle behind.
|
|
705
|
+
var tocHost = props.tocHost;
|
|
706
|
+
var sessionId = props.sessionId;
|
|
707
|
+
var hostSessions = function () {
|
|
708
|
+
if (!tocHost || typeof tocHost.sessions !== "function") return null;
|
|
709
|
+
try {
|
|
710
|
+
return tocHost.sessions();
|
|
711
|
+
} catch (e) {
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
// Which stage of the bridge is actually ready — reported once at mount so a
|
|
716
|
+
// missing jump loader can be diagnosed from the console instead of guessed.
|
|
717
|
+
var jumpLoaderState = function () {
|
|
718
|
+
var sessions = hostSessions();
|
|
719
|
+
if (!sessions) return "no-sessions-service";
|
|
720
|
+
if (typeof sessions.binding !== "function") return "no-binding-api";
|
|
721
|
+
var binding = null;
|
|
722
|
+
try {
|
|
723
|
+
binding = sessions.binding(sessionId);
|
|
724
|
+
} catch (e) {
|
|
725
|
+
return "binding-threw";
|
|
726
|
+
}
|
|
727
|
+
var face = binding && binding.session;
|
|
728
|
+
if (!face) return "no-binding-for-session";
|
|
729
|
+
return typeof face.loadThrough === "function" ? "ready" : "no-loadThrough";
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
// Structural narrowing of that projection (its value crosses the wire):
|
|
733
|
+
// `turn` and `seq` are load-bearing — an entry without them cannot be
|
|
734
|
+
// shown or jumped to — while the previews are decorative and degrade to "".
|
|
735
|
+
var outlineTurns = react.useMemo(function () {
|
|
736
|
+
var list = Array.isArray(rawOutline) ? rawOutline
|
|
737
|
+
: (rawOutline && Array.isArray(rawOutline.turns) ? rawOutline.turns : null);
|
|
738
|
+
if (!list) return null; // projection unavailable: loaded turns only
|
|
739
|
+
var out = [];
|
|
740
|
+
for (var i = 0; i < list.length; i++) {
|
|
741
|
+
var e = list[i];
|
|
742
|
+
if (!e || typeof e !== "object") continue;
|
|
743
|
+
if (typeof e.turn !== "number" || !isFinite(e.turn) || e.turn < 0) continue;
|
|
744
|
+
if (typeof e.seq !== "number" || !isFinite(e.seq) || e.seq < 0) continue;
|
|
745
|
+
out.push({
|
|
746
|
+
turn: e.turn,
|
|
747
|
+
seq: e.seq,
|
|
748
|
+
prompt: typeof e.prompt === "string" ? e.prompt : "",
|
|
749
|
+
response: typeof e.response === "string" ? e.response : ""
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
return out;
|
|
753
|
+
}, [rawOutline]);
|
|
754
|
+
|
|
469
755
|
// ---- hooks (ALL before any conditional return) ----
|
|
470
756
|
// collapsed by default; user expands via the edge handle (default dock: left)
|
|
471
757
|
var _s1 = react.useState(false);
|
|
@@ -663,6 +949,79 @@ window.__ModuleLoader__.load({
|
|
|
663
949
|
var listRef = react.useRef(null);
|
|
664
950
|
var didInitScroll = react.useRef(false);
|
|
665
951
|
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
952
|
+
var lastScrollTopRef = react.useRef(0);
|
|
953
|
+
var lastHostClickRef = react.useRef(0); // throttle for the "load earlier" click
|
|
954
|
+
|
|
955
|
+
// ---- "scroll up to load earlier messages" hint (search mode) -----------
|
|
956
|
+
// Armed by a search when older history is still reachable; dismissed the
|
|
957
|
+
// moment the reader scrolls up (which is when the older page starts loading,
|
|
958
|
+
// so the hint has done its job). "" | "more" | "oldest"
|
|
959
|
+
var _sHint = react.useState("");
|
|
960
|
+
var hint = _sHint[0];
|
|
961
|
+
var setHint = _sHint[1];
|
|
962
|
+
|
|
963
|
+
// ---- transient edge banner (outline mode) ------------------------------
|
|
964
|
+
// Reaching either end of the outline flashes a short bar over the bottom of
|
|
965
|
+
// the list: fade in, hold a few seconds, fade out. Deliberately NOT the same
|
|
966
|
+
// element as the search hint above (which stays until the reader acts).
|
|
967
|
+
var _sToast = react.useState(null);
|
|
968
|
+
var toast = _sToast[0];
|
|
969
|
+
var setToast = _sToast[1];
|
|
970
|
+
var toastTimersRef = react.useRef({ hold: null, out: null });
|
|
971
|
+
var showBanner = function (text) {
|
|
972
|
+
if (toastTimersRef.current.hold) clearTimeout(toastTimersRef.current.hold);
|
|
973
|
+
if (toastTimersRef.current.out) clearTimeout(toastTimersRef.current.out);
|
|
974
|
+
setToast({ text: text, closing: false });
|
|
975
|
+
toastTimersRef.current.hold = setTimeout(function () {
|
|
976
|
+
setToast(function (cur) { return cur === null ? null : { text: cur.text, closing: true }; });
|
|
977
|
+
toastTimersRef.current.out = setTimeout(function () { setToast(null); }, 460);
|
|
978
|
+
}, 2600);
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
// ---- hover preview card -------------------------------------------------
|
|
982
|
+
// Hovering an outline/result row opens a card with the section's opening
|
|
983
|
+
// lines; it lives in the body portal so the scrolling list cannot clip it.
|
|
984
|
+
var _sHov = react.useState(null);
|
|
985
|
+
var hoverCard = _sHov[0];
|
|
986
|
+
var setHoverCard = _sHov[1];
|
|
987
|
+
var _sHovOut = react.useState(false);
|
|
988
|
+
var hoverClosing = _sHovOut[0];
|
|
989
|
+
var setHoverClosing = _sHovOut[1];
|
|
990
|
+
var hoverTimerRef = react.useRef(null);
|
|
991
|
+
var hoverCloseTimerRef = react.useRef(null);
|
|
992
|
+
var hoverStart = function (info, el) {
|
|
993
|
+
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
994
|
+
hoverTimerRef.current = setTimeout(function () {
|
|
995
|
+
var r = el && el.getBoundingClientRect ? el.getBoundingClientRect() : null;
|
|
996
|
+
if (!r || r.width <= 0) return;
|
|
997
|
+
if (hoverCloseTimerRef.current) {
|
|
998
|
+
clearTimeout(hoverCloseTimerRef.current);
|
|
999
|
+
hoverCloseTimerRef.current = null;
|
|
1000
|
+
}
|
|
1001
|
+
setHoverClosing(false);
|
|
1002
|
+
setHoverCard({
|
|
1003
|
+
title: info.title,
|
|
1004
|
+
sub: info.sub,
|
|
1005
|
+
preview: info.preview,
|
|
1006
|
+
meta: info.meta,
|
|
1007
|
+
ghost: info.ghost,
|
|
1008
|
+
top: r.top,
|
|
1009
|
+
left: r.left,
|
|
1010
|
+
right: r.right
|
|
1011
|
+
});
|
|
1012
|
+
}, 260);
|
|
1013
|
+
};
|
|
1014
|
+
// leaving fades the card out instead of dropping it on the next frame
|
|
1015
|
+
var hoverEnd = function () {
|
|
1016
|
+
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
1017
|
+
if (hoverCloseTimerRef.current) clearTimeout(hoverCloseTimerRef.current);
|
|
1018
|
+
setHoverClosing(true);
|
|
1019
|
+
hoverCloseTimerRef.current = setTimeout(function () {
|
|
1020
|
+
hoverCloseTimerRef.current = null;
|
|
1021
|
+
setHoverCard(null);
|
|
1022
|
+
setHoverClosing(false);
|
|
1023
|
+
}, 240);
|
|
1024
|
+
};
|
|
666
1025
|
|
|
667
1026
|
// ---- the outline belongs to the CHAT view only: when the center column
|
|
668
1027
|
// switches to another view (trajectory / context / plugin views), fade the
|
|
@@ -734,6 +1093,21 @@ window.__ModuleLoader__.load({
|
|
|
734
1093
|
if (scopeTimerRef.current) clearTimeout(scopeTimerRef.current);
|
|
735
1094
|
scopeTimerRef.current = setTimeout(function () { setPrevScope(null); }, 300);
|
|
736
1095
|
};
|
|
1096
|
+
// ---- fuzzy matching: an independent switch beside the scope pill --------
|
|
1097
|
+
// Off (default) = the folded CONTIGUOUS match. On = subsequence matching, so
|
|
1098
|
+
// keywords tolerate text wedged in between ("模糊匹配" also finds "模糊的匹配").
|
|
1099
|
+
var FUZZY_KEY = "dsh-quick-toc.fuzzy.v1";
|
|
1100
|
+
var _sFz = react.useState(function () {
|
|
1101
|
+
try { return localStorage.getItem(FUZZY_KEY) === "1"; } catch (e) { return false; }
|
|
1102
|
+
});
|
|
1103
|
+
var fuzzy = _sFz[0];
|
|
1104
|
+
var setFuzzy = _sFz[1];
|
|
1105
|
+
var toggleFuzzy = function () {
|
|
1106
|
+
var next = !fuzzy;
|
|
1107
|
+
setFuzzy(next);
|
|
1108
|
+
setMatchIdx(0);
|
|
1109
|
+
try { localStorage.setItem(FUZZY_KEY, next ? "1" : "0"); } catch (e) {}
|
|
1110
|
+
};
|
|
737
1111
|
var openSearch = function () {
|
|
738
1112
|
setSearchOpen(true);
|
|
739
1113
|
setSearchAnim("enter");
|
|
@@ -789,9 +1163,9 @@ window.__ModuleLoader__.load({
|
|
|
789
1163
|
}
|
|
790
1164
|
return false;
|
|
791
1165
|
};
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1166
|
+
var viaHost = function () {
|
|
1167
|
+
var btn = findLoadOlderButton();
|
|
1168
|
+
if (!btn || btn.disabled) return false;
|
|
795
1169
|
btn.click();
|
|
796
1170
|
// once the conversation loads more, expand the outline window too
|
|
797
1171
|
setTimeout(function () {
|
|
@@ -804,7 +1178,34 @@ window.__ModuleLoader__.load({
|
|
|
804
1178
|
});
|
|
805
1179
|
}
|
|
806
1180
|
}, 1200);
|
|
807
|
-
|
|
1181
|
+
return true;
|
|
1182
|
+
};
|
|
1183
|
+
var viaHostThrottled = function () {
|
|
1184
|
+
var now = Date.now();
|
|
1185
|
+
if (now - lastHostClickRef.current < 900) return false;
|
|
1186
|
+
if (!findLoadOlderButton()) return false;
|
|
1187
|
+
lastHostClickRef.current = now;
|
|
1188
|
+
return viaHost();
|
|
1189
|
+
};
|
|
1190
|
+
// In SEARCH mode the list shows matches, not the outline: paging the
|
|
1191
|
+
// outline's own window would change nothing the reader can see, whereas
|
|
1192
|
+
// loading the conversation's older messages extends what the search can
|
|
1193
|
+
// cover at all (the whole-log index only carries bounded previews). So the
|
|
1194
|
+
// host's own "load earlier" comes first while a query is active.
|
|
1195
|
+
if (query.trim() !== "") return viaHost() || grow();
|
|
1196
|
+
// Outline mode keeps the ORIGINAL contract: scrolling the outline up also
|
|
1197
|
+
// pulls the conversation's older messages in (that is what makes the outline
|
|
1198
|
+
// a substitute for scrolling the transcript), not merely more index entries.
|
|
1199
|
+
// The host click is throttled so one continuous scroll cannot hammer the
|
|
1200
|
+
// pager; grow() still runs first so the index never lags behind the window.
|
|
1201
|
+
var grew = grow();
|
|
1202
|
+
return viaHostThrottled() || grew;
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// older history still reachable? the outline can always page its own window
|
|
1206
|
+
// further, and beyond that the conversation's own "load older" button decides
|
|
1207
|
+
var canLoadOlder = function () {
|
|
1208
|
+
return visibleCount < groups.length || !!findLoadOlderButton();
|
|
808
1209
|
};
|
|
809
1210
|
|
|
810
1211
|
// wheel up (toward older) loads more when the list is at its top or has
|
|
@@ -812,17 +1213,52 @@ window.__ModuleLoader__.load({
|
|
|
812
1213
|
// always refreshes older turns, even without a visible scrollbar
|
|
813
1214
|
var onListWheel = function (e) {
|
|
814
1215
|
outlineTouchRef.current = Date.now();
|
|
815
|
-
if (e.deltaY >= 0) return;
|
|
816
1216
|
var el = listRef.current;
|
|
817
1217
|
if (!el) return;
|
|
818
|
-
|
|
1218
|
+
hoverEnd();
|
|
1219
|
+
if (e.deltaY >= 0) {
|
|
1220
|
+
// still scrolling down at the very end of the outline: say so. Outline
|
|
1221
|
+
// mode only — the search list has its own persistent hint line.
|
|
1222
|
+
var max = el.scrollHeight - el.clientHeight;
|
|
1223
|
+
if (max > 4 && el.scrollTop >= max - 4 && query.trim() === "") showBanner(T("hint.bottom"));
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
if (hint === "more") setHint(""); // scrolling up is what the hint asked for
|
|
1227
|
+
if (el.scrollTop <= 1) {
|
|
1228
|
+
var progressed = loadOlderOutline(el);
|
|
1229
|
+
if (!progressed) {
|
|
1230
|
+
if (query.trim() !== "") {
|
|
1231
|
+
if (hint !== "oldest") setHint("oldest");
|
|
1232
|
+
} else {
|
|
1233
|
+
showBanner(T("search.hintOldest"));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
819
1237
|
};
|
|
820
1238
|
|
|
821
1239
|
// scroll to the top edge also loads older groups (keeps the visual position)
|
|
822
1240
|
var onListScroll = function (e) {
|
|
823
1241
|
outlineTouchRef.current = Date.now();
|
|
824
1242
|
var el = e.currentTarget;
|
|
825
|
-
|
|
1243
|
+
var upward = el.scrollTop < lastScrollTopRef.current - 2;
|
|
1244
|
+
if (upward) {
|
|
1245
|
+
if (hint === "more") setHint("");
|
|
1246
|
+
if (hoverCard) hoverEnd();
|
|
1247
|
+
}
|
|
1248
|
+
lastScrollTopRef.current = el.scrollTop;
|
|
1249
|
+
if (el.scrollTop <= 24) {
|
|
1250
|
+
// Reaching the top is the ONLY place the exhausted case can be told
|
|
1251
|
+
// apart from "keep scrolling": after a search the list is parked at the
|
|
1252
|
+
// bottom, so the first wheel-up merely scrolls and never gets here.
|
|
1253
|
+
var progressed = loadOlderOutline(el);
|
|
1254
|
+
if (!progressed && upward) {
|
|
1255
|
+
if (query.trim() !== "") {
|
|
1256
|
+
if (hint !== "oldest") setHint("oldest");
|
|
1257
|
+
} else {
|
|
1258
|
+
showBanner(T("search.hintOldest"));
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
826
1262
|
};
|
|
827
1263
|
|
|
828
1264
|
// ---- resize drags (right edge = width, bottom edge = height) ----
|
|
@@ -977,14 +1413,53 @@ window.__ModuleLoader__.load({
|
|
|
977
1413
|
var info = nodeInfo(key, node);
|
|
978
1414
|
current.msgs.push({ key: key, text: info.text });
|
|
979
1415
|
for (var k = 0; k < info.headings.length; k++) {
|
|
980
|
-
current.headings.push({
|
|
1416
|
+
current.headings.push({
|
|
1417
|
+
level: info.headings[k].level,
|
|
1418
|
+
title: info.headings[k].title,
|
|
1419
|
+
key: key,
|
|
1420
|
+
idx: k,
|
|
1421
|
+
sub: info.headings[k].sub,
|
|
1422
|
+
preview: info.headings[k].preview
|
|
1423
|
+
});
|
|
981
1424
|
}
|
|
982
1425
|
}
|
|
983
1426
|
pruneNodeInfo(seen);
|
|
984
1427
|
// keep a turn when it has headings OR a time — turns without headings
|
|
985
1428
|
// still get a standalone time entry in the outline (click to jump)
|
|
986
|
-
|
|
987
|
-
|
|
1429
|
+
var ready = result.filter(function (g) { return g.headings.length > 0 || g.time !== ""; });
|
|
1430
|
+
if (outlineTurns === null) return ready;
|
|
1431
|
+
// ---- merge the whole-log turn index ---------------------------------
|
|
1432
|
+
// Loaded groups keep their order (a group whose node carries no turn
|
|
1433
|
+
// location stays where it was found); every turn the paged window has not
|
|
1434
|
+
// loaded yet becomes an "unloaded" entry in the gap where it belongs, so
|
|
1435
|
+
// the outline covers the whole session and heading-less turns stay
|
|
1436
|
+
// reachable by number. Preview text comes from the projection and is
|
|
1437
|
+
// bounded by it (host-side 50/120 character budgets).
|
|
1438
|
+
var merged = [];
|
|
1439
|
+
var li = 0;
|
|
1440
|
+
for (var ei = 0; ei < outlineTurns.length; ei++) {
|
|
1441
|
+
var entry = outlineTurns[ei];
|
|
1442
|
+
while (li < ready.length && (ready[li].turn === null || ready[li].turn < entry.turn)) {
|
|
1443
|
+
merged.push(ready[li]);
|
|
1444
|
+
li++;
|
|
1445
|
+
}
|
|
1446
|
+
if (li < ready.length && ready[li].turn === entry.turn) continue; // emitted above
|
|
1447
|
+
merged.push({
|
|
1448
|
+
turn: entry.turn,
|
|
1449
|
+
seq: entry.seq,
|
|
1450
|
+
ghost: true,
|
|
1451
|
+
time: "",
|
|
1452
|
+
userKey: "",
|
|
1453
|
+
userText: entry.prompt,
|
|
1454
|
+
userFull: "",
|
|
1455
|
+
msgs: [],
|
|
1456
|
+
headings: [],
|
|
1457
|
+
preview: entry.response
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
while (li < ready.length) { merged.push(ready[li]); li++; }
|
|
1461
|
+
return merged;
|
|
1462
|
+
}, [order, nodes, outlineTurns]);
|
|
988
1463
|
// first time content appears, scroll the list to the bottom (newest).
|
|
989
1464
|
// NOTE: must stay BELOW the `groups` memo — a deps array is evaluated
|
|
990
1465
|
// during render, so reading `groups` before that `var` is assigned would
|
|
@@ -994,14 +1469,25 @@ window.__ModuleLoader__.load({
|
|
|
994
1469
|
didInitScroll.current = true;
|
|
995
1470
|
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
996
1471
|
}, [groups.length]);
|
|
1472
|
+
// one-line diagnostic so the loaded build AND the host capabilities it can
|
|
1473
|
+
// actually reach are identifiable from the browser console
|
|
1474
|
+
var mountLogRef = react.useRef(false);
|
|
1475
|
+
react.useEffect(function () {
|
|
1476
|
+
if (mountLogRef.current) return;
|
|
1477
|
+
mountLogRef.current = true;
|
|
1478
|
+
console.log("[dsh-quick-toc] panel mounted · turnOutline=" +
|
|
1479
|
+
(outlineTurns ? outlineTurns.length + " turns" : "unavailable") +
|
|
1480
|
+
" · jumpLoader=" + jumpLoaderState());
|
|
1481
|
+
}, [outlineTurns]);
|
|
1482
|
+
|
|
997
1483
|
var groupTrees = react.useMemo(function () {
|
|
998
1484
|
return groups.map(function (g) {
|
|
999
|
-
if (levels.length === 6) return buildTree(g.headings);
|
|
1485
|
+
if (levels.length === 6) return buildTree(g.headings, g.time);
|
|
1000
1486
|
var kept = [];
|
|
1001
1487
|
for (var i = 0; i < g.headings.length; i++) {
|
|
1002
1488
|
if (levelSet[g.headings[i].level]) kept.push(g.headings[i]);
|
|
1003
1489
|
}
|
|
1004
|
-
return buildTree(kept);
|
|
1490
|
+
return buildTree(kept, g.time);
|
|
1005
1491
|
});
|
|
1006
1492
|
}, [groups, levels, levelSet]);
|
|
1007
1493
|
// pagination slice: the latest `visibleCount` groups
|
|
@@ -1009,12 +1495,22 @@ window.__ModuleLoader__.load({
|
|
|
1009
1495
|
var shownTrees = groupTrees.slice(groupTrees.length - shownGroups.length);
|
|
1010
1496
|
|
|
1011
1497
|
// the group currently being read (the turn under the middle of the
|
|
1012
|
-
// CONVERSATION viewport) stays bright in the outline; others are dimmed
|
|
1498
|
+
// CONVERSATION viewport) stays bright in the outline; others are dimmed.
|
|
1499
|
+
// Every node key of every loaded group -> its group index: mapping ONLY the
|
|
1500
|
+
// heading-bearing messages meant a turn with no markdown headings had no
|
|
1501
|
+
// entry at all, so a jump that landed on it (the host's own turn rail, for
|
|
1502
|
+
// one) could never light up or follow in the outline.
|
|
1013
1503
|
var keyToGroup = react.useMemo(function () {
|
|
1014
1504
|
var m = {};
|
|
1015
1505
|
for (var i = 0; i < groups.length; i++) {
|
|
1016
|
-
|
|
1017
|
-
|
|
1506
|
+
var g = groups[i];
|
|
1507
|
+
if (g.ghost) continue; // no loaded rows to match
|
|
1508
|
+
if (g.userKey) m[g.userKey] = i;
|
|
1509
|
+
for (var j = 0; j < g.headings.length; j++) {
|
|
1510
|
+
m[g.headings[j].key] = i;
|
|
1511
|
+
}
|
|
1512
|
+
for (var k = 0; k < g.msgs.length; k++) {
|
|
1513
|
+
m[g.msgs[k].key] = i;
|
|
1018
1514
|
}
|
|
1019
1515
|
}
|
|
1020
1516
|
return m;
|
|
@@ -1045,8 +1541,9 @@ window.__ModuleLoader__.load({
|
|
|
1045
1541
|
// search result rows: one row per matched heading/message (with an
|
|
1046
1542
|
// occurrence count), carrying its heading path, turn time and a snippet.
|
|
1047
1543
|
var resultRows = react.useMemo(function () {
|
|
1048
|
-
var
|
|
1049
|
-
if (!
|
|
1544
|
+
var fq = foldQuery(query);
|
|
1545
|
+
if (!fq) return [];
|
|
1546
|
+
var needle = { folded: fq }; // one folded query, reused for every text
|
|
1050
1547
|
var rows = [];
|
|
1051
1548
|
var index = {};
|
|
1052
1549
|
var push = function (row, n) {
|
|
@@ -1059,43 +1556,83 @@ window.__ModuleLoader__.load({
|
|
|
1059
1556
|
};
|
|
1060
1557
|
for (var gi = 0; gi < groups.length; gi++) {
|
|
1061
1558
|
var g = groups[gi];
|
|
1559
|
+
// a turn the paged window has not loaded: searchable through the host
|
|
1560
|
+
// outline's bounded previews, and clicking it pages the turn in first
|
|
1561
|
+
if (g.ghost) {
|
|
1562
|
+
if (searchScope === "full") {
|
|
1563
|
+
var gtext = (g.userText || "") + "\n" + (g.preview || "");
|
|
1564
|
+
var ng = countOccurrences(gtext, needle, fuzzy);
|
|
1565
|
+
if (ng > 0) {
|
|
1566
|
+
push({
|
|
1567
|
+
gi: gi, key: "", idx: undefined,
|
|
1568
|
+
title: previewText(g.userText || g.preview || "", 40),
|
|
1569
|
+
level: 0, path: "", time: "",
|
|
1570
|
+
snippet: snippetAround(g.preview || g.userText || "", fq, 36, fuzzy),
|
|
1571
|
+
ghost: true, turn: g.turn, seq: g.seq
|
|
1572
|
+
}, ng);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1062
1577
|
for (var j = 0; j < g.headings.length; j++) {
|
|
1063
1578
|
var h = g.headings[j];
|
|
1064
|
-
var nh = countOccurrences(h.title,
|
|
1579
|
+
var nh = countOccurrences(h.title, needle, fuzzy);
|
|
1065
1580
|
if (nh > 0) {
|
|
1066
1581
|
var hp = headingPaths[headingId(h)];
|
|
1067
1582
|
push({
|
|
1068
1583
|
gi: gi, key: h.key, idx: h.idx, title: h.title, level: h.level,
|
|
1069
|
-
|
|
1584
|
+
// the subtitle identifies same-titled headings ("which one?")
|
|
1585
|
+
path: hp ? hp.path : "", time: g.time, snippet: h.sub || ""
|
|
1070
1586
|
}, nh);
|
|
1071
1587
|
}
|
|
1072
1588
|
}
|
|
1073
1589
|
if (searchScope === "full") {
|
|
1074
|
-
var nu = g.userKey && g.userFull ? countOccurrences(g.userFull,
|
|
1590
|
+
var nu = g.userKey && g.userFull ? countOccurrences(g.userFull, needle, fuzzy) : 0;
|
|
1075
1591
|
if (nu > 0) {
|
|
1076
1592
|
push({
|
|
1077
1593
|
gi: gi, key: g.userKey, idx: undefined, title: previewText(g.userFull, 40),
|
|
1078
|
-
level: 0, path: "", time: g.time, snippet: snippetAround(g.userFull,
|
|
1594
|
+
level: 0, path: "", time: g.time, snippet: snippetAround(g.userFull, fq, 36, fuzzy)
|
|
1079
1595
|
}, nu);
|
|
1080
1596
|
}
|
|
1081
1597
|
for (var m = 0; m < g.msgs.length; m++) {
|
|
1082
1598
|
var msg = g.msgs[m];
|
|
1083
1599
|
if (!msg.text) continue;
|
|
1084
|
-
var nm = countOccurrences(msg.text,
|
|
1600
|
+
var nm = countOccurrences(msg.text, needle, fuzzy);
|
|
1085
1601
|
if (nm === 0) continue;
|
|
1086
1602
|
push({
|
|
1087
1603
|
gi: gi, key: msg.key, idx: undefined, title: previewText(msg.text, 40),
|
|
1088
|
-
level: 0, path: "", time: g.time, snippet: snippetAround(msg.text,
|
|
1604
|
+
level: 0, path: "", time: g.time, snippet: snippetAround(msg.text, fq, 36, fuzzy)
|
|
1089
1605
|
}, nm);
|
|
1090
1606
|
}
|
|
1091
1607
|
}
|
|
1092
1608
|
}
|
|
1093
1609
|
return rows;
|
|
1094
|
-
}, [groups, query, searchScope, headingPaths]);
|
|
1610
|
+
}, [groups, query, searchScope, headingPaths, fuzzy]);
|
|
1611
|
+
// ---- auto-follow: which turn is the reader looking at -------------------
|
|
1612
|
+
// This used to hang off ONE scroll listener bound to the scrollport element
|
|
1613
|
+
// captured when the effect ran. DSH can replace that element (a jump that
|
|
1614
|
+
// repages the window remounts the list), and the listener then stays on the
|
|
1615
|
+
// detached node — the outline silently stops following. So it re-queries the
|
|
1616
|
+
// scrollport on every run, listens on the DOCUMENT in the capture phase
|
|
1617
|
+
// (scroll does not bubble; capture still sees every descendant), and keeps a
|
|
1618
|
+
// slow poll for programmatic anchor adjustments that emit no scroll event.
|
|
1619
|
+
//
|
|
1620
|
+
// The outline's own scroll is INSTANT and starts on the next frame: a following
|
|
1621
|
+
// rail that animates its way to the target reads as permanently one step behind.
|
|
1095
1622
|
react.useEffect(function () {
|
|
1096
|
-
var
|
|
1097
|
-
|
|
1098
|
-
var
|
|
1623
|
+
var followFrame = null;
|
|
1624
|
+
var followTimer = null;
|
|
1625
|
+
var lastRun = 0;
|
|
1626
|
+
var update = function (force) {
|
|
1627
|
+
// scroll frames are hot and a document-level capture listener sees every
|
|
1628
|
+
// scroll in the app; the rail only changes when a different turn crosses
|
|
1629
|
+
// the viewport, so cap the row measurement at ~16/s
|
|
1630
|
+
var now = Date.now();
|
|
1631
|
+
if (!force && now - lastRun < 60) return;
|
|
1632
|
+
lastRun = now;
|
|
1633
|
+
if (!chatViewRef.current) return;
|
|
1634
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
1635
|
+
if (!sp) return;
|
|
1099
1636
|
var lr = sp.getBoundingClientRect();
|
|
1100
1637
|
var vTop = lr.top;
|
|
1101
1638
|
var vBottom = lr.top + lr.height;
|
|
@@ -1111,37 +1648,43 @@ window.__ModuleLoader__.load({
|
|
|
1111
1648
|
}
|
|
1112
1649
|
}
|
|
1113
1650
|
var sig = actives.slice().sort().join(",");
|
|
1114
|
-
if (sig
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}, 120);
|
|
1138
|
-
}
|
|
1651
|
+
if (sig === activeSigRef.current) return;
|
|
1652
|
+
activeSigRef.current = sig;
|
|
1653
|
+
setActiveGroup(actives);
|
|
1654
|
+
if (actives.length === 0) return;
|
|
1655
|
+
// pause right after the reader touched the outline themselves —
|
|
1656
|
+
// otherwise paging older turns gets yanked back to the newest group
|
|
1657
|
+
if (Date.now() - outlineTouchRef.current <= 1500) return;
|
|
1658
|
+
var gi0 = actives[0];
|
|
1659
|
+
setVisibleCount(function (prev) {
|
|
1660
|
+
var need = groups.length - gi0 + 3;
|
|
1661
|
+
return Math.max(prev, Math.min(groups.length, need));
|
|
1662
|
+
});
|
|
1663
|
+
if (followFrame) cancelAnimationFrame(followFrame);
|
|
1664
|
+
if (followTimer) clearTimeout(followTimer);
|
|
1665
|
+
var place = function () {
|
|
1666
|
+
var el = listRef.current;
|
|
1667
|
+
if (!el) return;
|
|
1668
|
+
var node = el.querySelector('[data-group-idx="' + gi0 + '"]');
|
|
1669
|
+
if (!node) return;
|
|
1670
|
+
var er = node.getBoundingClientRect();
|
|
1671
|
+
var lr2 = el.getBoundingClientRect();
|
|
1672
|
+
if (er.top < lr2.top - 2 || er.bottom > lr2.bottom + 2) {
|
|
1673
|
+
el.scrollTop = el.scrollTop + (er.top - lr2.top) - el.clientHeight / 2 + node.offsetHeight / 2;
|
|
1139
1674
|
}
|
|
1140
|
-
}
|
|
1675
|
+
};
|
|
1676
|
+
followFrame = requestAnimationFrame(function () { followFrame = requestAnimationFrame(place); });
|
|
1677
|
+
followTimer = setTimeout(place, 90); // the row may only exist after the expansion commits
|
|
1678
|
+
};
|
|
1679
|
+
update(true);
|
|
1680
|
+
document.addEventListener("scroll", update, { passive: true, capture: true });
|
|
1681
|
+
var poll = setInterval(function () { update(true); }, 250);
|
|
1682
|
+
return function () {
|
|
1683
|
+
document.removeEventListener("scroll", update, { capture: true });
|
|
1684
|
+
clearInterval(poll);
|
|
1685
|
+
if (followFrame) cancelAnimationFrame(followFrame);
|
|
1686
|
+
if (followTimer) clearTimeout(followTimer);
|
|
1141
1687
|
};
|
|
1142
|
-
update();
|
|
1143
|
-
sp.addEventListener("scroll", update, { passive: true });
|
|
1144
|
-
return function () { sp.removeEventListener("scroll", update); };
|
|
1145
1688
|
}, [keyToGroup]);
|
|
1146
1689
|
|
|
1147
1690
|
// ---- search matches ----
|
|
@@ -1150,21 +1693,32 @@ window.__ModuleLoader__.load({
|
|
|
1150
1693
|
// Every occurrence counts (multiple hits inside one message = multiple
|
|
1151
1694
|
// matches), so the n/N counter reflects the real total.
|
|
1152
1695
|
var matches = react.useMemo(function () {
|
|
1153
|
-
var
|
|
1154
|
-
if (!
|
|
1696
|
+
var fq = foldQuery(query);
|
|
1697
|
+
if (!fq) return [];
|
|
1698
|
+
var needle = { folded: fq };
|
|
1155
1699
|
var out = [];
|
|
1156
1700
|
for (var gi = 0; gi < groups.length; gi++) {
|
|
1157
1701
|
var g = groups[gi];
|
|
1702
|
+
if (g.ghost) {
|
|
1703
|
+
if (searchScope === "full") {
|
|
1704
|
+
var gtext = (g.userText || "") + "\n" + (g.preview || "");
|
|
1705
|
+
var ng = countOccurrences(gtext, needle, fuzzy);
|
|
1706
|
+
for (var cg = 0; cg < ng; cg++) {
|
|
1707
|
+
out.push({ gi: gi, title: g.userText || "", key: "", idx: undefined, ghost: true, turn: g.turn, seq: g.seq });
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1158
1712
|
for (var j = 0; j < g.headings.length; j++) {
|
|
1159
1713
|
var h = g.headings[j];
|
|
1160
|
-
var n = countOccurrences(h.title,
|
|
1714
|
+
var n = countOccurrences(h.title, needle, fuzzy);
|
|
1161
1715
|
for (var c = 0; c < n; c++) {
|
|
1162
1716
|
out.push({ gi: gi, title: h.title, key: h.key, idx: h.idx });
|
|
1163
1717
|
}
|
|
1164
1718
|
}
|
|
1165
1719
|
if (searchScope === "full") {
|
|
1166
1720
|
if (g.userKey && g.userFull) {
|
|
1167
|
-
var nu = countOccurrences(g.userFull,
|
|
1721
|
+
var nu = countOccurrences(g.userFull, needle, fuzzy);
|
|
1168
1722
|
for (var cu = 0; cu < nu; cu++) {
|
|
1169
1723
|
out.push({ gi: gi, title: previewText(g.userFull, 30), key: g.userKey, idx: undefined });
|
|
1170
1724
|
}
|
|
@@ -1172,7 +1726,7 @@ window.__ModuleLoader__.load({
|
|
|
1172
1726
|
for (var m = 0; m < g.msgs.length; m++) {
|
|
1173
1727
|
var msg = g.msgs[m];
|
|
1174
1728
|
if (!msg.text) continue;
|
|
1175
|
-
var nm = countOccurrences(msg.text,
|
|
1729
|
+
var nm = countOccurrences(msg.text, needle, fuzzy);
|
|
1176
1730
|
for (var cm = 0; cm < nm; cm++) {
|
|
1177
1731
|
out.push({ gi: gi, title: previewText(msg.text, 30), key: msg.key, idx: undefined });
|
|
1178
1732
|
}
|
|
@@ -1180,7 +1734,7 @@ window.__ModuleLoader__.load({
|
|
|
1180
1734
|
}
|
|
1181
1735
|
}
|
|
1182
1736
|
return out;
|
|
1183
|
-
}, [groups, query, searchScope]);
|
|
1737
|
+
}, [groups, query, searchScope, fuzzy]);
|
|
1184
1738
|
|
|
1185
1739
|
// which result row holds the current (n/N) occurrence (needs `matches`)
|
|
1186
1740
|
var activeResultRow = -1;
|
|
@@ -1203,10 +1757,13 @@ window.__ModuleLoader__.load({
|
|
|
1203
1757
|
var q = query.trim();
|
|
1204
1758
|
if (searchPosRef.current === q) return;
|
|
1205
1759
|
searchPosRef.current = q;
|
|
1206
|
-
if (!q) return;
|
|
1760
|
+
if (!q) { setHint(""); return; }
|
|
1207
1761
|
var el = listRef.current;
|
|
1208
1762
|
if (el) el.scrollTop = el.scrollHeight;
|
|
1209
1763
|
if (matches.length > 0) setMatchIdx(matches.length - 1);
|
|
1764
|
+
// arm the "scroll up for older messages" hint: the result list only covers
|
|
1765
|
+
// what the window holds, and older turns are one upward scroll away
|
|
1766
|
+
setHint(canLoadOlder() ? "more" : "");
|
|
1210
1767
|
}, [query, matches.length]);
|
|
1211
1768
|
|
|
1212
1769
|
// keep the current row in view while stepping with Enter
|
|
@@ -1277,15 +1834,8 @@ window.__ModuleLoader__.load({
|
|
|
1277
1834
|
// ---- jump: smooth glide to the exact heading element. When glued to the
|
|
1278
1835
|
// bottom, lift just past DSH's 25px stick-to-bottom threshold first so
|
|
1279
1836
|
// the glide is not yanked back. ----
|
|
1280
|
-
var
|
|
1281
|
-
var
|
|
1282
|
-
if (!row) return;
|
|
1283
|
-
var el = row;
|
|
1284
|
-
if (idx !== undefined && idx !== null) {
|
|
1285
|
-
var hs = row.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
1286
|
-
if (hs.length > 0) el = hs[Math.min(idx, hs.length - 1)] || row;
|
|
1287
|
-
}
|
|
1288
|
-
var sp = row.closest ? row.closest("[data-conversation-scroll]") : null;
|
|
1837
|
+
var glideTo = function (el) {
|
|
1838
|
+
var sp = el.closest ? el.closest("[data-conversation-scroll]") : null;
|
|
1289
1839
|
if (!sp) {
|
|
1290
1840
|
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1291
1841
|
return;
|
|
@@ -1298,6 +1848,104 @@ window.__ModuleLoader__.load({
|
|
|
1298
1848
|
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
1299
1849
|
};
|
|
1300
1850
|
|
|
1851
|
+
var jump = function (key, idx) {
|
|
1852
|
+
var row = findRow(key);
|
|
1853
|
+
if (!row) return;
|
|
1854
|
+
var el = row;
|
|
1855
|
+
if (idx !== undefined && idx !== null) {
|
|
1856
|
+
var hs = row.querySelectorAll("h1, h2, h3, h4, h5, h6");
|
|
1857
|
+
if (hs.length > 0) el = hs[Math.min(idx, hs.length - 1)] || row;
|
|
1858
|
+
}
|
|
1859
|
+
glideTo(el);
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
// ---- unloaded turns: page history to the turn, then land on it ----------
|
|
1863
|
+
// The host turn outline carries each turn's `turn/start` seq and the session
|
|
1864
|
+
// face's loadThrough(seq) is the documented jump loader ("page history
|
|
1865
|
+
// backwards until the window covers seq"). Once it resolves the turn's nodes
|
|
1866
|
+
// exist, so we poll briefly for its row — the native turn rail waits for the
|
|
1867
|
+
// same settle.
|
|
1868
|
+
var turnRow = function (turn) {
|
|
1869
|
+
try {
|
|
1870
|
+
return document.querySelector('[data-chat-turn="' + String(turn) + '"]');
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
return null;
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1876
|
+
// Fallback locator for the poll below: once the paged window covers the
|
|
1877
|
+
// turn, OUR OWN rebuilt outline has a loaded group for it, which carries the
|
|
1878
|
+
// anchor keys of that turn's nodes. That path does not depend on the host
|
|
1879
|
+
// keeping a `data-chat-turn` attribute on the row.
|
|
1880
|
+
var groupsRef = react.useRef([]);
|
|
1881
|
+
react.useEffect(function () { groupsRef.current = groups; }, [groups]);
|
|
1882
|
+
var loadedGroupRow = function (turn) {
|
|
1883
|
+
var list = groupsRef.current || [];
|
|
1884
|
+
for (var i = 0; i < list.length; i++) {
|
|
1885
|
+
var g = list[i];
|
|
1886
|
+
if (g.ghost || g.turn !== turn) continue;
|
|
1887
|
+
var key = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : g.userKey;
|
|
1888
|
+
var row = key ? findRow(key) : null;
|
|
1889
|
+
if (row) return row;
|
|
1890
|
+
}
|
|
1891
|
+
return null;
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
var landOnTurn = function (turn) {
|
|
1895
|
+
var tries = 0;
|
|
1896
|
+
var step = function () {
|
|
1897
|
+
tries++;
|
|
1898
|
+
var row = turnRow(turn) || loadedGroupRow(turn);
|
|
1899
|
+
if (row) {
|
|
1900
|
+
glideTo(row);
|
|
1901
|
+
// a jump that started from a search hit keeps the hit highlighted
|
|
1902
|
+
if (query.trim()) {
|
|
1903
|
+
clearHighlights();
|
|
1904
|
+
highlightRow(row, query.trim(), 0, fuzzy);
|
|
1905
|
+
}
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
if (tries < 25) setTimeout(step, 80);
|
|
1909
|
+
};
|
|
1910
|
+
setTimeout(step, 40);
|
|
1911
|
+
};
|
|
1912
|
+
|
|
1913
|
+
// returns false when the host exposes no jump loader (older DSH builds)
|
|
1914
|
+
var openTurn = function (turn, seq) {
|
|
1915
|
+
var sessions = hostSessions();
|
|
1916
|
+
if (!sessions) {
|
|
1917
|
+
console.warn("[dsh-quick-toc] ctx.get(\"sessions\") returned nothing: this host has no session service, cannot open turn " + turn);
|
|
1918
|
+
return false;
|
|
1919
|
+
}
|
|
1920
|
+
var binding = typeof sessions.binding === "function" ? sessions.binding(sessionId) : null;
|
|
1921
|
+
var face = binding && binding.session;
|
|
1922
|
+
if (!face || typeof face.loadThrough !== "function") {
|
|
1923
|
+
console.warn("[dsh-quick-toc] session " + String(sessionId) + " exposes no loadThrough jump loader, cannot open turn " + turn);
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
setHoverCard(null);
|
|
1927
|
+
Promise.resolve(face.loadThrough(seq)).then(function () {
|
|
1928
|
+
landOnTurn(turn);
|
|
1929
|
+
}, function (err) {
|
|
1930
|
+
console.warn("[dsh-quick-toc] loadThrough failed:", err);
|
|
1931
|
+
});
|
|
1932
|
+
return true;
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
// reveal one group inside the outline list (shared by Enter-stepping and by
|
|
1936
|
+
// jumps into a turn that had to be loaded first)
|
|
1937
|
+
var revealGroupInOutline = function (gi) {
|
|
1938
|
+
setVisibleCount(function (prev) {
|
|
1939
|
+
return Math.max(prev, Math.min(groups.length, groups.length - gi));
|
|
1940
|
+
});
|
|
1941
|
+
setTimeout(function () {
|
|
1942
|
+
var el = listRef.current;
|
|
1943
|
+
if (!el) return;
|
|
1944
|
+
var node = el.querySelector('[data-group-idx="' + gi + '"]');
|
|
1945
|
+
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1946
|
+
}, 150);
|
|
1947
|
+
};
|
|
1948
|
+
|
|
1301
1949
|
// jump to the n-th match, cycling; also reveal the group in the outline.
|
|
1302
1950
|
// ONE smooth scroll straight to the current occurrence (no competing
|
|
1303
1951
|
// scrolls): highlight first, then position the mark at the upper-middle
|
|
@@ -1306,14 +1954,21 @@ window.__ModuleLoader__.load({
|
|
|
1306
1954
|
var i = ((n % matches.length) + matches.length) % matches.length;
|
|
1307
1955
|
setMatchIdx(i);
|
|
1308
1956
|
var m = matches[i];
|
|
1309
|
-
var q = query.trim()
|
|
1957
|
+
var q = query.trim();
|
|
1310
1958
|
clearHighlights();
|
|
1959
|
+
if (m.ghost) {
|
|
1960
|
+
// the hit lives in a turn the paged window has not loaded: page it in
|
|
1961
|
+
// and land on it (landOnTurn re-applies the highlight there)
|
|
1962
|
+
openTurn(m.turn, m.seq);
|
|
1963
|
+
revealGroupInOutline(m.gi);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1311
1966
|
var r = findRowStrict(m.key);
|
|
1312
1967
|
if (r && q) {
|
|
1313
1968
|
// occurrence index within the target message (consecutive in matches)
|
|
1314
1969
|
var occ = 0;
|
|
1315
1970
|
for (var p = i - 1; p >= 0 && matches[p].key === m.key; p--) occ++;
|
|
1316
|
-
highlightRow(r, q, occ);
|
|
1971
|
+
highlightRow(r, q, occ, fuzzy);
|
|
1317
1972
|
var markEl = r.querySelector(".dqt-current");
|
|
1318
1973
|
var sp = r.closest ? r.closest("[data-conversation-scroll]") : null;
|
|
1319
1974
|
if (sp && markEl) {
|
|
@@ -1330,15 +1985,7 @@ window.__ModuleLoader__.load({
|
|
|
1330
1985
|
} else {
|
|
1331
1986
|
jump(m.key, m.idx);
|
|
1332
1987
|
}
|
|
1333
|
-
|
|
1334
|
-
return Math.max(prev, Math.min(groups.length, groups.length - m.gi));
|
|
1335
|
-
});
|
|
1336
|
-
setTimeout(function () {
|
|
1337
|
-
var el = listRef.current;
|
|
1338
|
-
if (!el) return;
|
|
1339
|
-
var node = el.querySelector('[data-group-idx="' + m.gi + '"]');
|
|
1340
|
-
if (node) node.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
1341
|
-
}, 150);
|
|
1988
|
+
revealGroupInOutline(m.gi);
|
|
1342
1989
|
};
|
|
1343
1990
|
|
|
1344
1991
|
// clicking result row `i` = make its FIRST occurrence the current match, so
|
|
@@ -1395,6 +2042,47 @@ window.__ModuleLoader__.load({
|
|
|
1395
2042
|
};
|
|
1396
2043
|
|
|
1397
2044
|
// ---- panel ----
|
|
2045
|
+
// what the module-scope render helpers need from this render
|
|
2046
|
+
var renderExtra = {
|
|
2047
|
+
paths: headingPaths,
|
|
2048
|
+
fuzzy: fuzzy,
|
|
2049
|
+
onOpenTurn: openTurn,
|
|
2050
|
+
hoverStart: hoverStart,
|
|
2051
|
+
hoverEnd: hoverEnd
|
|
2052
|
+
};
|
|
2053
|
+
// Transient banner pinned to the bottom of the panel (the panel is
|
|
2054
|
+
// position:fixed, so this anchors to the list's lower edge without joining
|
|
2055
|
+
// the list's scroll flow). pointerEvents:none keeps it from eating clicks.
|
|
2056
|
+
var toastEl = toast ? react_jsx_runtime.jsx("div", {
|
|
2057
|
+
className: "dqt-toast" + (toast.closing ? " dqt-toast-closing" : ""),
|
|
2058
|
+
style: {
|
|
2059
|
+
position: "absolute",
|
|
2060
|
+
left: 8,
|
|
2061
|
+
right: 8,
|
|
2062
|
+
bottom: 8,
|
|
2063
|
+
display: "flex",
|
|
2064
|
+
justifyContent: "center",
|
|
2065
|
+
pointerEvents: "none",
|
|
2066
|
+
zIndex: 3
|
|
2067
|
+
},
|
|
2068
|
+
children: react_jsx_runtime.jsx("span", {
|
|
2069
|
+
style: {
|
|
2070
|
+
maxWidth: "100%",
|
|
2071
|
+
padding: "3px 10px",
|
|
2072
|
+
borderRadius: 999,
|
|
2073
|
+
cornerShape: "round",
|
|
2074
|
+
fontSize: 11,
|
|
2075
|
+
color: C.text,
|
|
2076
|
+
background: C.panelBg,
|
|
2077
|
+
border: "1px solid " + C.panelBorder,
|
|
2078
|
+
boxShadow: "0 2px 10px rgba(0,0,0,0.22)",
|
|
2079
|
+
whiteSpace: "nowrap",
|
|
2080
|
+
overflow: "hidden",
|
|
2081
|
+
textOverflow: "ellipsis"
|
|
2082
|
+
},
|
|
2083
|
+
children: toast.text
|
|
2084
|
+
})
|
|
2085
|
+
}) : null;
|
|
1398
2086
|
// panel: opening slides in with a slow fade; closing slides quickly to
|
|
1399
2087
|
// the dock edge, clipped by the sidebar line (looks covered, not
|
|
1400
2088
|
// dissolving) and only fades at the very end. No box-shadow: a shadow
|
|
@@ -1419,7 +2107,12 @@ window.__ModuleLoader__.load({
|
|
|
1419
2107
|
}
|
|
1420
2108
|
}
|
|
1421
2109
|
// faded out entirely while another center-column view is active
|
|
1422
|
-
|
|
2110
|
+
// Idle transparency used to be 0.45, which stacked with the outline's own
|
|
2111
|
+
// inactive-group dimming (0.6) into a wall of grey that left the panel barely
|
|
2112
|
+
// readable whenever the pointer was elsewhere. Readability now comes from
|
|
2113
|
+
// CONTRAST on the group being read (tinted row + accent bar, see
|
|
2114
|
+
// renderGroups) instead of from fading everything else away.
|
|
2115
|
+
var panelOpacity = open && chatViewActive ? (hovered ? 1 : 0.72) : 0;
|
|
1423
2116
|
|
|
1424
2117
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
1425
2118
|
style: {
|
|
@@ -1469,7 +2162,7 @@ window.__ModuleLoader__.load({
|
|
|
1469
2162
|
cursor: "grab",
|
|
1470
2163
|
userSelect: "none"
|
|
1471
2164
|
},
|
|
1472
|
-
title: "
|
|
2165
|
+
title: T("handle.dragY"),
|
|
1473
2166
|
onPointerDown: onHandleDown,
|
|
1474
2167
|
onMouseEnter: function () { handleBright(true); },
|
|
1475
2168
|
onMouseLeave: function () { handleBright(false); },
|
|
@@ -1502,7 +2195,7 @@ window.__ModuleLoader__.load({
|
|
|
1502
2195
|
// three-bar outline mark (long / medium / short) — no title text
|
|
1503
2196
|
react_jsx_runtime.jsx("div", {
|
|
1504
2197
|
style: { display: "flex", alignItems: "center", color: C.muted, flex: "none" },
|
|
1505
|
-
title: "
|
|
2198
|
+
title: T("panel.title"),
|
|
1506
2199
|
children: react_jsx_runtime.jsx("svg", {
|
|
1507
2200
|
width: 18,
|
|
1508
2201
|
height: 14,
|
|
@@ -1526,7 +2219,7 @@ window.__ModuleLoader__.load({
|
|
|
1526
2219
|
react_jsx_runtime.jsx("button", {
|
|
1527
2220
|
className: "dqt-levels-btn",
|
|
1528
2221
|
onClick: function () { levelsOpen ? closeLevels() : setLevelsOpen(true); },
|
|
1529
|
-
title: "
|
|
2222
|
+
title: T("levels.tip"),
|
|
1530
2223
|
style: {
|
|
1531
2224
|
width: 24,
|
|
1532
2225
|
height: 24,
|
|
@@ -1539,8 +2232,10 @@ window.__ModuleLoader__.load({
|
|
|
1539
2232
|
display: "flex",
|
|
1540
2233
|
alignItems: "center",
|
|
1541
2234
|
justifyContent: "center",
|
|
1542
|
-
|
|
1543
|
-
|
|
2235
|
+
// open (or still animating shut) uses the SAME tint as every
|
|
2236
|
+
// other "on" control, so the header reads consistently
|
|
2237
|
+
background: (levelsOpen || levelsClosing) ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
2238
|
+
color: (levelsOpen || levelsClosing) ? C.accent : C.muted,
|
|
1544
2239
|
transition: "background 0.15s ease, color 0.15s ease"
|
|
1545
2240
|
},
|
|
1546
2241
|
onMouseEnter: function (e) {
|
|
@@ -1548,8 +2243,8 @@ window.__ModuleLoader__.load({
|
|
|
1548
2243
|
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
1549
2244
|
},
|
|
1550
2245
|
onMouseLeave: function (e) {
|
|
1551
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
1552
|
-
e.currentTarget.style.color = C.muted;
|
|
2246
|
+
e.currentTarget.style.background = (levelsOpen || levelsClosing) ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
2247
|
+
e.currentTarget.style.color = (levelsOpen || levelsClosing) ? C.accent : C.muted;
|
|
1553
2248
|
},
|
|
1554
2249
|
children: react_jsx_runtime.jsx("svg", {
|
|
1555
2250
|
width: 14,
|
|
@@ -1570,7 +2265,7 @@ window.__ModuleLoader__.load({
|
|
|
1570
2265
|
// magnifier button (SVG, matches the other buttons' style)
|
|
1571
2266
|
react_jsx_runtime.jsx("button", {
|
|
1572
2267
|
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
1573
|
-
title: "
|
|
2268
|
+
title: T("search.open"),
|
|
1574
2269
|
style: {
|
|
1575
2270
|
width: 24,
|
|
1576
2271
|
height: 24,
|
|
@@ -1579,9 +2274,9 @@ window.__ModuleLoader__.load({
|
|
|
1579
2274
|
justifyContent: "center",
|
|
1580
2275
|
borderRadius: "50%",
|
|
1581
2276
|
cornerShape: "round",
|
|
1582
|
-
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
2277
|
+
background: searchOpen ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
1583
2278
|
border: "none",
|
|
1584
|
-
color: C.muted,
|
|
2279
|
+
color: searchOpen ? C.accent : C.muted,
|
|
1585
2280
|
cursor: "pointer"
|
|
1586
2281
|
},
|
|
1587
2282
|
onMouseEnter: function (e) {
|
|
@@ -1589,8 +2284,8 @@ window.__ModuleLoader__.load({
|
|
|
1589
2284
|
e.currentTarget.style.color = "var(--dsw-alias-brand-primary, #4f8cff)";
|
|
1590
2285
|
},
|
|
1591
2286
|
onMouseLeave: function (e) {
|
|
1592
|
-
e.currentTarget.style.background = "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
1593
|
-
e.currentTarget.style.color = C.muted;
|
|
2287
|
+
e.currentTarget.style.background = searchOpen ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))";
|
|
2288
|
+
e.currentTarget.style.color = searchOpen ? C.accent : C.muted;
|
|
1594
2289
|
},
|
|
1595
2290
|
children: react_jsx_runtime.jsx("svg", {
|
|
1596
2291
|
width: 15,
|
|
@@ -1610,11 +2305,11 @@ window.__ModuleLoader__.load({
|
|
|
1610
2305
|
})
|
|
1611
2306
|
}),
|
|
1612
2307
|
// triangle tips toward the side it will move TO: shift slightly up + toward the tip
|
|
1613
|
-
iconBtn(toggleDock, dockRight ? "
|
|
2308
|
+
iconBtn(toggleDock, dockRight ? T("handle.dockLeft") : T("handle.dockRight"), dockRight ? "◀" : "▶", 12, dockRight ? { x: -1, y: -1 } : { x: 1, y: -1 }),
|
|
1614
2309
|
// close: thick SVG cross, nudged slightly down
|
|
1615
2310
|
react_jsx_runtime.jsx("button", {
|
|
1616
2311
|
onClick: function () { setOpen(false); },
|
|
1617
|
-
title: "
|
|
2312
|
+
title: T("handle.collapse"),
|
|
1618
2313
|
style: {
|
|
1619
2314
|
width: 24,
|
|
1620
2315
|
height: 24,
|
|
@@ -1726,23 +2421,23 @@ window.__ModuleLoader__.load({
|
|
|
1726
2421
|
color: "var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))"
|
|
1727
2422
|
},
|
|
1728
2423
|
children: [
|
|
1729
|
-
"
|
|
2424
|
+
T("search.word"),
|
|
1730
2425
|
react_jsx_runtime.jsx("span", {
|
|
1731
2426
|
style: { position: "relative", display: "inline-block" },
|
|
1732
2427
|
children: [
|
|
1733
2428
|
react_jsx_runtime.jsx("span", {
|
|
1734
2429
|
key: searchScope,
|
|
1735
2430
|
style: { display: "inline-block", animation: "dqt-fade-in 0.25s linear" },
|
|
1736
|
-
children: searchScope === "title" ? "
|
|
2431
|
+
children: searchScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1737
2432
|
}),
|
|
1738
2433
|
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1739
2434
|
key: "old-" + prevScope,
|
|
1740
2435
|
style: { position: "absolute", left: 0, top: 0, opacity: 0, animation: "dqt-fade-out 0.25s linear forwards" },
|
|
1741
|
-
children: prevScope === "title" ? "
|
|
2436
|
+
children: prevScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1742
2437
|
}) : null
|
|
1743
2438
|
]
|
|
1744
2439
|
}),
|
|
1745
|
-
"
|
|
2440
|
+
T("search.tail")
|
|
1746
2441
|
]
|
|
1747
2442
|
}) : null,
|
|
1748
2443
|
]
|
|
@@ -1771,7 +2466,7 @@ window.__ModuleLoader__.load({
|
|
|
1771
2466
|
// label text cross-fades old->new)
|
|
1772
2467
|
react_jsx_runtime.jsx("button", {
|
|
1773
2468
|
onClick: toggleScope,
|
|
1774
|
-
title: searchScope === "title" ? "
|
|
2469
|
+
title: searchScope === "title" ? T("search.scope.tipTitle") : T("search.scope.tipFull"),
|
|
1775
2470
|
style: {
|
|
1776
2471
|
flex: "none",
|
|
1777
2472
|
width: 34,
|
|
@@ -1783,7 +2478,7 @@ window.__ModuleLoader__.load({
|
|
|
1783
2478
|
cornerShape: "round",
|
|
1784
2479
|
fontSize: 11,
|
|
1785
2480
|
color: searchScope === "full" ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
1786
|
-
background: searchScope === "full" ?
|
|
2481
|
+
background: searchScope === "full" ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1787
2482
|
border: "none",
|
|
1788
2483
|
cursor: "pointer",
|
|
1789
2484
|
whiteSpace: "nowrap",
|
|
@@ -1795,15 +2490,40 @@ window.__ModuleLoader__.load({
|
|
|
1795
2490
|
react_jsx_runtime.jsx("span", {
|
|
1796
2491
|
key: searchScope,
|
|
1797
2492
|
style: { animation: "dqt-fade-in 0.25s linear", display: "block" },
|
|
1798
|
-
children: searchScope === "title" ? "
|
|
2493
|
+
children: searchScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1799
2494
|
}),
|
|
1800
2495
|
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1801
2496
|
key: "old-" + prevScope,
|
|
1802
2497
|
style: { position: "absolute", opacity: 0, animation: "dqt-fade-out 0.25s linear forwards", display: "block" },
|
|
1803
|
-
children: prevScope === "title" ? "
|
|
2498
|
+
children: prevScope === "title" ? T("search.scope.title") : T("search.scope.full")
|
|
1804
2499
|
}) : null
|
|
1805
2500
|
]
|
|
1806
2501
|
})
|
|
2502
|
+
}),
|
|
2503
|
+
// fuzzy switch: an independent toggle beside the scope pill (its label
|
|
2504
|
+
// is the same either way, so only the colour carries the state)
|
|
2505
|
+
react_jsx_runtime.jsx("button", {
|
|
2506
|
+
onClick: toggleFuzzy,
|
|
2507
|
+
title: fuzzy ? T("search.fuzzy.tipOn") : T("search.fuzzy.tipOff"),
|
|
2508
|
+
"data-fuzzy": fuzzy ? "on" : "off",
|
|
2509
|
+
style: {
|
|
2510
|
+
flex: "none",
|
|
2511
|
+
width: 34,
|
|
2512
|
+
height: 34,
|
|
2513
|
+
display: "flex",
|
|
2514
|
+
alignItems: "center",
|
|
2515
|
+
justifyContent: "center",
|
|
2516
|
+
borderRadius: "50%",
|
|
2517
|
+
cornerShape: "round",
|
|
2518
|
+
fontSize: 11,
|
|
2519
|
+
color: fuzzy ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
2520
|
+
background: fuzzy ? C.chip : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
2521
|
+
border: "none",
|
|
2522
|
+
cursor: "pointer",
|
|
2523
|
+
whiteSpace: "nowrap",
|
|
2524
|
+
transition: "background 0.25s ease, color 0.25s ease"
|
|
2525
|
+
},
|
|
2526
|
+
children: T("search.fuzzy")
|
|
1807
2527
|
})
|
|
1808
2528
|
]
|
|
1809
2529
|
})
|
|
@@ -1830,10 +2550,32 @@ window.__ModuleLoader__.load({
|
|
|
1830
2550
|
children: react_jsx_runtime.jsx("div", {
|
|
1831
2551
|
style: { direction: "ltr" },
|
|
1832
2552
|
children: query.trim()
|
|
1833
|
-
?
|
|
1834
|
-
|
|
2553
|
+
? [
|
|
2554
|
+
react_jsx_runtime.jsx("div", {
|
|
2555
|
+
key: "results",
|
|
2556
|
+
children: renderResults(resultRows, query.trim(), C, goToRow, activeResultRow, renderExtra)
|
|
2557
|
+
}),
|
|
2558
|
+
// older history is one upward scroll away — say so until the
|
|
2559
|
+
// reader actually scrolls up (then the hint has done its job)
|
|
2560
|
+
hint ? react_jsx_runtime.jsx("div", {
|
|
2561
|
+
key: "hint",
|
|
2562
|
+
style: {
|
|
2563
|
+
display: "flex",
|
|
2564
|
+
alignItems: "center",
|
|
2565
|
+
justifyContent: "center",
|
|
2566
|
+
padding: "10px 6px 4px",
|
|
2567
|
+
fontSize: 11,
|
|
2568
|
+
color: C.muted,
|
|
2569
|
+
opacity: 0.85,
|
|
2570
|
+
textAlign: "center"
|
|
2571
|
+
},
|
|
2572
|
+
children: hint === "oldest" ? T("search.hintOldest") : T("search.hintMore")
|
|
2573
|
+
}) : null
|
|
2574
|
+
]
|
|
2575
|
+
: renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup, renderExtra)
|
|
1835
2576
|
})
|
|
1836
2577
|
}),
|
|
2578
|
+
toastEl,
|
|
1837
2579
|
// resize handles (right edge: width, bottom edge: height)
|
|
1838
2580
|
react_jsx_runtime.jsx("div", {
|
|
1839
2581
|
style: {
|
|
@@ -1847,7 +2589,7 @@ window.__ModuleLoader__.load({
|
|
|
1847
2589
|
zIndex: 2
|
|
1848
2590
|
},
|
|
1849
2591
|
onPointerDown: onResizeWDown,
|
|
1850
|
-
title: "
|
|
2592
|
+
title: T("resize.w")
|
|
1851
2593
|
}),
|
|
1852
2594
|
react_jsx_runtime.jsx("div", {
|
|
1853
2595
|
style: {
|
|
@@ -1861,7 +2603,7 @@ window.__ModuleLoader__.load({
|
|
|
1861
2603
|
zIndex: 2
|
|
1862
2604
|
},
|
|
1863
2605
|
onPointerDown: onResizeHDown,
|
|
1864
|
-
title: "
|
|
2606
|
+
title: T("resize.h")
|
|
1865
2607
|
}),
|
|
1866
2608
|
// corner handle: resize width AND height at once
|
|
1867
2609
|
react_jsx_runtime.jsx("div", {
|
|
@@ -1876,7 +2618,7 @@ window.__ModuleLoader__.load({
|
|
|
1876
2618
|
zIndex: 3
|
|
1877
2619
|
},
|
|
1878
2620
|
onPointerDown: onResizeCornerDown,
|
|
1879
|
-
title: "
|
|
2621
|
+
title: T("resize.wh")
|
|
1880
2622
|
}),
|
|
1881
2623
|
// heading-level picker: pops down from the round header button,
|
|
1882
2624
|
// right-aligned, with an outer shadow so it reads as a floating layer.
|
|
@@ -1908,7 +2650,7 @@ window.__ModuleLoader__.load({
|
|
|
1908
2650
|
var active = !!levelSet[lv];
|
|
1909
2651
|
return react_jsx_runtime.jsx("button", {
|
|
1910
2652
|
onClick: function () { toggleLevel(lv); },
|
|
1911
|
-
title: active ? "
|
|
2653
|
+
title: active ? T("levels.hide") + lv : T("levels.show") + lv,
|
|
1912
2654
|
style: {
|
|
1913
2655
|
height: 22,
|
|
1914
2656
|
padding: "0 7px",
|
|
@@ -1920,9 +2662,9 @@ window.__ModuleLoader__.load({
|
|
|
1920
2662
|
lineHeight: "22px",
|
|
1921
2663
|
fontFamily: "inherit",
|
|
1922
2664
|
background: active
|
|
1923
|
-
?
|
|
2665
|
+
? C.chip
|
|
1924
2666
|
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1925
|
-
color: active ?
|
|
2667
|
+
color: active ? C.accent : C.muted,
|
|
1926
2668
|
fontWeight: active ? 600 : 400
|
|
1927
2669
|
},
|
|
1928
2670
|
children: "H" + lv
|
|
@@ -1952,7 +2694,7 @@ window.__ModuleLoader__.load({
|
|
|
1952
2694
|
? { right: viewport ? viewport.right + 52 : 60 } // clear of the milestone rail
|
|
1953
2695
|
: { left: viewport ? viewport.left : 0 })
|
|
1954
2696
|
},
|
|
1955
|
-
title: "
|
|
2697
|
+
title: T("handle.expand"),
|
|
1956
2698
|
onClick: function () { setOpen(true); },
|
|
1957
2699
|
onMouseEnter: function () { if (edgeRef.current) edgeRef.current.style.opacity = "1"; },
|
|
1958
2700
|
onMouseLeave: function () { if (edgeRef.current) edgeRef.current.style.opacity = "0.5"; },
|
|
@@ -1987,29 +2729,119 @@ window.__ModuleLoader__.load({
|
|
|
1987
2729
|
})
|
|
1988
2730
|
}) : null;
|
|
1989
2731
|
|
|
2732
|
+
// ---- hover preview card ------------------------------------------------
|
|
2733
|
+
// Rendered INSIDE the body portal (position: fixed) so the outline's own
|
|
2734
|
+
// scroll container can never clip it, and pointer-events: none so it can
|
|
2735
|
+
// never steal the hover that opened it.
|
|
2736
|
+
var hoverEl = null;
|
|
2737
|
+
if (hoverCard && open) {
|
|
2738
|
+
var cardW = 264;
|
|
2739
|
+
var cardTop = Math.max(8, Math.min(hoverCard.top - 4, window.innerHeight - 210));
|
|
2740
|
+
var cardLeft = dockRight
|
|
2741
|
+
? Math.max(8, hoverCard.left - cardW - 10)
|
|
2742
|
+
: Math.min(window.innerWidth - cardW - 8, hoverCard.right + 10);
|
|
2743
|
+
var cardBody = hoverCard.preview || hoverCard.sub || "";
|
|
2744
|
+
var cardChildren = [
|
|
2745
|
+
react_jsx_runtime.jsx("div", {
|
|
2746
|
+
style: { fontSize: 12, fontWeight: 600, color: C.text, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
2747
|
+
children: hoverCard.title
|
|
2748
|
+
}, "hc-t")
|
|
2749
|
+
];
|
|
2750
|
+
if (hoverCard.meta) {
|
|
2751
|
+
cardChildren.push(react_jsx_runtime.jsx("div", {
|
|
2752
|
+
style: { fontSize: 10, color: C.muted, opacity: 0.8, marginTop: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
2753
|
+
children: hoverCard.meta
|
|
2754
|
+
}, "hc-m"));
|
|
2755
|
+
}
|
|
2756
|
+
cardChildren.push(react_jsx_runtime.jsx("div", {
|
|
2757
|
+
style: { fontSize: 11, lineHeight: "16px", color: C.text, opacity: 0.9, marginTop: 4, maxHeight: 116, overflow: "hidden" },
|
|
2758
|
+
children: cardBody || (hoverCard.ghost ? T("turn.loadTip") : "")
|
|
2759
|
+
}, "hc-b"));
|
|
2760
|
+
if (hoverCard.ghost) {
|
|
2761
|
+
cardChildren.push(react_jsx_runtime.jsx("div", {
|
|
2762
|
+
style: { fontSize: 10, color: C.muted, opacity: 0.7, marginTop: 4 },
|
|
2763
|
+
children: T("preview.truncated")
|
|
2764
|
+
}, "hc-n"));
|
|
2765
|
+
}
|
|
2766
|
+
hoverEl = react_jsx_runtime.jsx("div", {
|
|
2767
|
+
className: "dqt-hover" + (hoverClosing ? " dqt-hover-closing" : ""),
|
|
2768
|
+
style: {
|
|
2769
|
+
position: "fixed",
|
|
2770
|
+
top: cardTop,
|
|
2771
|
+
left: cardLeft,
|
|
2772
|
+
width: cardW,
|
|
2773
|
+
padding: "8px 10px",
|
|
2774
|
+
borderRadius: 8,
|
|
2775
|
+
cornerShape: "round",
|
|
2776
|
+
background: C.panelBg,
|
|
2777
|
+
color: C.text,
|
|
2778
|
+
border: "1px solid " + C.panelBorder,
|
|
2779
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.28)",
|
|
2780
|
+
zIndex: Z_BASE + 1,
|
|
2781
|
+
pointerEvents: "none"
|
|
2782
|
+
},
|
|
2783
|
+
children: cardChildren
|
|
2784
|
+
}, "hover-card");
|
|
2785
|
+
}
|
|
2786
|
+
|
|
1990
2787
|
return react_dom.createPortal(
|
|
1991
|
-
react_jsx_runtime.jsx(ErrorBoundary, { children: [panelEl, edgeEl] }),
|
|
2788
|
+
react_jsx_runtime.jsx(ErrorBoundary, { children: [panelEl, edgeEl, hoverEl] }),
|
|
1992
2789
|
document.body
|
|
1993
2790
|
);
|
|
1994
2791
|
}
|
|
1995
2792
|
|
|
1996
|
-
function renderItem(n, depth, jump, C, uid) {
|
|
2793
|
+
function renderItem(n, depth, jump, C, uid, extra) {
|
|
1997
2794
|
var hasChildren = !!(n.children && n.children.length);
|
|
1998
|
-
var
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2795
|
+
var path = extra && extra.paths ? extra.paths[headingId(n)] : null;
|
|
2796
|
+
var hoverInfo = {
|
|
2797
|
+
title: n.title,
|
|
2798
|
+
sub: n.sub || "",
|
|
2799
|
+
preview: n.preview || "",
|
|
2800
|
+
meta: [n.time, path ? path.path : ""].filter(Boolean).join(" · "),
|
|
2801
|
+
ghost: false
|
|
2802
|
+
};
|
|
2803
|
+
var rows = [
|
|
2804
|
+
react_jsx_runtime.jsx("div", {
|
|
2805
|
+
style: { display: "flex", alignItems: "center", gap: 2, minWidth: 0 },
|
|
2806
|
+
children: react_jsx_runtime.jsx("span", {
|
|
2807
|
+
style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" },
|
|
2808
|
+
children: n.title
|
|
2809
|
+
})
|
|
2810
|
+
}, uid + "-l")
|
|
2811
|
+
];
|
|
2812
|
+
// Subtitle = the section's first sentence. This is what makes two headings
|
|
2813
|
+
// with the SAME text distinguishable without hovering ("which one is this?").
|
|
2814
|
+
if (n.sub) {
|
|
2815
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
2816
|
+
style: {
|
|
2817
|
+
fontSize: 10,
|
|
2818
|
+
lineHeight: "13px",
|
|
2819
|
+
color: C.muted,
|
|
2820
|
+
opacity: 0.75,
|
|
2821
|
+
whiteSpace: "nowrap",
|
|
2822
|
+
overflow: "hidden",
|
|
2823
|
+
textOverflow: "ellipsis"
|
|
2824
|
+
},
|
|
2825
|
+
children: n.sub
|
|
2826
|
+
}, uid + "-s"));
|
|
2827
|
+
}
|
|
2003
2828
|
return react_jsx_runtime.jsx(
|
|
2004
2829
|
"div",
|
|
2005
2830
|
{
|
|
2006
2831
|
onClick: function () { jump(n.key, n.idx); },
|
|
2832
|
+
onMouseEnter: function (e) {
|
|
2833
|
+
e.currentTarget.style.background = C.hover;
|
|
2834
|
+
if (extra && extra.hoverStart) extra.hoverStart(hoverInfo, e.currentTarget);
|
|
2835
|
+
},
|
|
2836
|
+
onMouseLeave: function (e) {
|
|
2837
|
+
e.currentTarget.style.background = "transparent";
|
|
2838
|
+
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
2839
|
+
},
|
|
2007
2840
|
"data-jump-key": n.key,
|
|
2008
2841
|
"data-jump-idx": n.idx !== undefined ? String(n.idx) : "0",
|
|
2009
2842
|
style: {
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
gap: 2,
|
|
2843
|
+
// no fixed height: a row grows by one line when it has a subtitle
|
|
2844
|
+
display: "block",
|
|
2013
2845
|
padding: "2px 6px",
|
|
2014
2846
|
paddingLeft: (hasChildren ? 2 : 6) + (n.level - 1) * 12,
|
|
2015
2847
|
margin: "1px 0",
|
|
@@ -2019,26 +2851,23 @@ window.__ModuleLoader__.load({
|
|
|
2019
2851
|
color: n.level <= 2 ? C.text : C.muted,
|
|
2020
2852
|
fontWeight: n.level <= 2 ? 600 : 400,
|
|
2021
2853
|
lineHeight: "18px",
|
|
2022
|
-
height: 22,
|
|
2023
2854
|
whiteSpace: "nowrap",
|
|
2024
2855
|
overflow: "hidden"
|
|
2025
2856
|
},
|
|
2026
|
-
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
2027
|
-
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
2028
2857
|
title: n.title,
|
|
2029
|
-
children:
|
|
2858
|
+
children: rows
|
|
2030
2859
|
},
|
|
2031
2860
|
uid + "-" + n.level + "-" + (n.key || "")
|
|
2032
2861
|
);
|
|
2033
2862
|
}
|
|
2034
2863
|
|
|
2035
|
-
function renderNodes(nodes, depth, jump, C, uid) {
|
|
2864
|
+
function renderNodes(nodes, depth, jump, C, uid, extra) {
|
|
2036
2865
|
var out = [];
|
|
2037
2866
|
for (var i = 0; i < nodes.length; i++) {
|
|
2038
2867
|
var n = nodes[i];
|
|
2039
|
-
out.push(renderItem(n, depth, jump, C, uid + "-" + i));
|
|
2868
|
+
out.push(renderItem(n, depth, jump, C, uid + "-" + i, extra));
|
|
2040
2869
|
if (n.children && n.children.length) {
|
|
2041
|
-
out.push(react_jsx_runtime.jsx("div", { children: renderNodes(n.children, depth + 1, jump, C, uid + "-" + i) }, uid + "-c-" + i));
|
|
2870
|
+
out.push(react_jsx_runtime.jsx("div", { children: renderNodes(n.children, depth + 1, jump, C, uid + "-" + i, extra) }, uid + "-c-" + i));
|
|
2042
2871
|
}
|
|
2043
2872
|
}
|
|
2044
2873
|
return out;
|
|
@@ -2048,8 +2877,8 @@ window.__ModuleLoader__.load({
|
|
|
2048
2877
|
// While a query is present the list shows every matched heading/message
|
|
2049
2878
|
// (grouped per message, with an occurrence count) instead of the outline, so
|
|
2050
2879
|
// search is "see them all, then jump" rather than stepping blindly.
|
|
2051
|
-
function hitSpans(text, q) {
|
|
2052
|
-
var parts = highlightParts(text, q);
|
|
2880
|
+
function hitSpans(text, q, fuzzy) {
|
|
2881
|
+
var parts = highlightParts(text, q, fuzzy);
|
|
2053
2882
|
return parts.map(function (p, i) {
|
|
2054
2883
|
return p.hit
|
|
2055
2884
|
? react_jsx_runtime.jsx("span", {
|
|
@@ -2060,8 +2889,10 @@ window.__ModuleLoader__.load({
|
|
|
2060
2889
|
});
|
|
2061
2890
|
}
|
|
2062
2891
|
|
|
2063
|
-
function renderResultRow(r, i, q, C, onRow, isActive) {
|
|
2892
|
+
function renderResultRow(r, i, q, C, onRow, isActive, extra) {
|
|
2893
|
+
var fuzzy = !!(extra && extra.fuzzy);
|
|
2064
2894
|
var meta = [];
|
|
2895
|
+
if (r.ghost) meta.push(T("turn.unloaded"));
|
|
2065
2896
|
if (r.time) meta.push(r.time);
|
|
2066
2897
|
if (r.path) meta.push(r.path);
|
|
2067
2898
|
if (r.count > 1) meta.push("×" + r.count);
|
|
@@ -2075,7 +2906,7 @@ window.__ModuleLoader__.load({
|
|
|
2075
2906
|
overflow: "hidden",
|
|
2076
2907
|
textOverflow: "ellipsis"
|
|
2077
2908
|
},
|
|
2078
|
-
children: hitSpans(r.title, q)
|
|
2909
|
+
children: hitSpans(r.title, q, fuzzy)
|
|
2079
2910
|
}, "t" + i)
|
|
2080
2911
|
];
|
|
2081
2912
|
if (r.snippet) {
|
|
@@ -2088,7 +2919,7 @@ window.__ModuleLoader__.load({
|
|
|
2088
2919
|
overflow: "hidden",
|
|
2089
2920
|
textOverflow: "ellipsis"
|
|
2090
2921
|
},
|
|
2091
|
-
children: hitSpans(r.snippet, q)
|
|
2922
|
+
children: hitSpans(r.snippet, q, fuzzy)
|
|
2092
2923
|
}, "s" + i));
|
|
2093
2924
|
}
|
|
2094
2925
|
if (meta.length) {
|
|
@@ -2102,9 +2933,23 @@ window.__ModuleLoader__.load({
|
|
|
2102
2933
|
// keyword in the conversation and scrolls there, exactly like Enter
|
|
2103
2934
|
// stepping — a row that only scrolled (no highlight) was the old bug
|
|
2104
2935
|
onClick: function () { onRow(i); },
|
|
2936
|
+
onMouseEnter: function (e) {
|
|
2937
|
+
e.currentTarget.style.background = C.hover;
|
|
2938
|
+
if (extra && extra.hoverStart) {
|
|
2939
|
+
extra.hoverStart({
|
|
2940
|
+
title: r.title,
|
|
2941
|
+
sub: r.snippet || "",
|
|
2942
|
+
preview: r.snippet || "",
|
|
2943
|
+
meta: meta.join(" · "),
|
|
2944
|
+
ghost: !!r.ghost
|
|
2945
|
+
}, e.currentTarget);
|
|
2946
|
+
}
|
|
2947
|
+
},
|
|
2948
|
+
onMouseLeave: function (e) {
|
|
2949
|
+
e.currentTarget.style.background = isActive ? C.chip : "transparent";
|
|
2950
|
+
if (extra && extra.hoverEnd) extra.hoverEnd();
|
|
2951
|
+
},
|
|
2105
2952
|
"data-result-idx": i,
|
|
2106
|
-
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
2107
|
-
onMouseLeave: function (e) { e.currentTarget.style.background = isActive ? "rgba(79,140,255,0.16)" : "transparent"; },
|
|
2108
2953
|
title: r.path || r.title,
|
|
2109
2954
|
style: {
|
|
2110
2955
|
padding: "4px 6px",
|
|
@@ -2112,23 +2957,23 @@ window.__ModuleLoader__.load({
|
|
|
2112
2957
|
borderRadius: 6,
|
|
2113
2958
|
cursor: "pointer",
|
|
2114
2959
|
minWidth: 0,
|
|
2115
|
-
background: isActive ?
|
|
2960
|
+
background: isActive ? C.chip : "transparent",
|
|
2116
2961
|
transition: "background 0.15s ease"
|
|
2117
2962
|
},
|
|
2118
2963
|
children: children
|
|
2119
2964
|
}, "res-" + i);
|
|
2120
2965
|
}
|
|
2121
2966
|
|
|
2122
|
-
function renderResults(rows, q, C, onRow, activeRow) {
|
|
2967
|
+
function renderResults(rows, q, C, onRow, activeRow, extra) {
|
|
2123
2968
|
if (!rows || rows.length === 0) {
|
|
2124
2969
|
return react_jsx_runtime.jsx("div", {
|
|
2125
2970
|
style: { padding: "12px 8px", fontSize: 12, color: C.muted, textAlign: "center" },
|
|
2126
|
-
children: "
|
|
2127
|
-
});
|
|
2971
|
+
children: T("search.empty")
|
|
2972
|
+
}, "empty");
|
|
2128
2973
|
}
|
|
2129
2974
|
var out = [];
|
|
2130
2975
|
for (var i = 0; i < rows.length; i++) {
|
|
2131
|
-
out.push(renderResultRow(rows[i], i, q, C, onRow, i === activeRow));
|
|
2976
|
+
out.push(renderResultRow(rows[i], i, q, C, onRow, i === activeRow, extra));
|
|
2132
2977
|
}
|
|
2133
2978
|
return out;
|
|
2134
2979
|
}
|
|
@@ -2139,9 +2984,8 @@ window.__ModuleLoader__.load({
|
|
|
2139
2984
|
// Implemented as a function (not an inline closure in a loop) so each
|
|
2140
2985
|
// header captures its own (g, gi) — the var-in-loop closure bug would
|
|
2141
2986
|
// otherwise make every header jump to the last group.
|
|
2142
|
-
function renderGroupHeader(g, gi, jump, C) {
|
|
2987
|
+
function renderGroupHeader(g, gi, jump, C, isActive, headingless) {
|
|
2143
2988
|
var replyKey = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : "";
|
|
2144
|
-
var targetKey = replyKey || g.userKey;
|
|
2145
2989
|
var jumpToTurn = function (e) {
|
|
2146
2990
|
e.stopPropagation();
|
|
2147
2991
|
// prefer the model's reply; fall back to the user message, then to the
|
|
@@ -2152,30 +2996,52 @@ window.__ModuleLoader__.load({
|
|
|
2152
2996
|
};
|
|
2153
2997
|
return react_jsx_runtime.jsx("div", {
|
|
2154
2998
|
style: {
|
|
2155
|
-
padding: "1px 4px 2px",
|
|
2156
|
-
height: 18,
|
|
2999
|
+
padding: headingless ? "3px 4px" : "1px 4px 2px",
|
|
3000
|
+
height: headingless ? undefined : 18,
|
|
2157
3001
|
display: "flex",
|
|
2158
3002
|
alignItems: "center",
|
|
2159
3003
|
minWidth: 0,
|
|
2160
3004
|
// sticky: while you scroll the outline, the header of the group you are
|
|
2161
|
-
// inside stays pinned at the top
|
|
2162
|
-
// the
|
|
3005
|
+
// inside stays pinned at the top. It must be opaque to cover the rows
|
|
3006
|
+
// scrolling underneath, so its base colour is the panel background in both
|
|
3007
|
+
// states — the "current group" tint rides on its own layer inside, which
|
|
3008
|
+
// can FADE (a background-image gradient cannot be transitioned).
|
|
2163
3009
|
position: "sticky",
|
|
2164
3010
|
top: 0,
|
|
2165
3011
|
zIndex: 2,
|
|
2166
3012
|
background: C.panelBg
|
|
2167
3013
|
},
|
|
2168
|
-
children:
|
|
3014
|
+
children: [
|
|
3015
|
+
react_jsx_runtime.jsx("div", {
|
|
3016
|
+
style: {
|
|
3017
|
+
position: "absolute",
|
|
3018
|
+
left: 0,
|
|
3019
|
+
right: 0,
|
|
3020
|
+
top: 0,
|
|
3021
|
+
bottom: 0,
|
|
3022
|
+
background: C.groupTint,
|
|
3023
|
+
opacity: isActive ? 1 : 0,
|
|
3024
|
+
transition: "opacity 0.18s ease",
|
|
3025
|
+
pointerEvents: "none"
|
|
3026
|
+
}
|
|
3027
|
+
}, "g-h-tint-" + gi),
|
|
3028
|
+
react_jsx_runtime.jsx("span", {
|
|
2169
3029
|
onClick: jumpToTurn,
|
|
2170
|
-
title: replyKey ? "
|
|
3030
|
+
title: replyKey ? T("turn.jumpReply") : T("turn.jumpTurn"),
|
|
2171
3031
|
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
2172
3032
|
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
2173
3033
|
style: {
|
|
2174
|
-
|
|
2175
|
-
|
|
3034
|
+
// A turn with no headings has ONLY this row, so it carries the whole
|
|
3035
|
+
// entry: readable size, weight and colour. No extra pill background —
|
|
3036
|
+
// the "current group" tint belongs to the group, so headed and
|
|
3037
|
+
// heading-less turns look the same when they are being read.
|
|
3038
|
+
fontSize: headingless ? 12 : 11,
|
|
3039
|
+
fontWeight: headingless ? 600 : 400,
|
|
3040
|
+
color: headingless ? C.text : C.muted,
|
|
2176
3041
|
cursor: "pointer",
|
|
2177
|
-
padding: "1px 5px",
|
|
3042
|
+
padding: headingless ? "2px 6px" : "1px 5px",
|
|
2178
3043
|
borderRadius: 4,
|
|
3044
|
+
background: "transparent",
|
|
2179
3045
|
transition: "background 0.15s ease",
|
|
2180
3046
|
display: "inline-flex",
|
|
2181
3047
|
alignItems: "center",
|
|
@@ -2187,18 +3053,81 @@ window.__ModuleLoader__.load({
|
|
|
2187
3053
|
children: [
|
|
2188
3054
|
react_jsx_runtime.jsx("span", { style: { fontWeight: 600, flex: "none" }, children: g.time || " " }),
|
|
2189
3055
|
g.userText ? react_jsx_runtime.jsx("span", {
|
|
2190
|
-
style: {
|
|
3056
|
+
style: {
|
|
3057
|
+
fontWeight: headingless ? 500 : 400,
|
|
3058
|
+
opacity: headingless ? 0.95 : 0.75,
|
|
3059
|
+
overflow: "hidden",
|
|
3060
|
+
textOverflow: "ellipsis",
|
|
3061
|
+
whiteSpace: "nowrap",
|
|
3062
|
+
minWidth: 0
|
|
3063
|
+
},
|
|
2191
3064
|
children: g.userText
|
|
2192
3065
|
}) : null
|
|
2193
3066
|
]
|
|
2194
3067
|
})
|
|
3068
|
+
]
|
|
2195
3069
|
}, "g-h-" + gi);
|
|
2196
3070
|
}
|
|
2197
3071
|
|
|
3072
|
+
// A turn the paged event window has not loaded yet: the host outline still
|
|
3073
|
+
// names it (number + bounded previews), so the outline covers the whole
|
|
3074
|
+
// session. Clicking pages that turn in through the session's jump loader.
|
|
3075
|
+
function renderGhostGroup(g, gi, C, extra) {
|
|
3076
|
+
extra = extra || {};
|
|
3077
|
+
var children = [
|
|
3078
|
+
react_jsx_runtime.jsx("div", {
|
|
3079
|
+
style: { display: "flex", alignItems: "center", gap: 5, minWidth: 0 },
|
|
3080
|
+
children: [
|
|
3081
|
+
react_jsx_runtime.jsx("span", {
|
|
3082
|
+
style: { fontSize: 10, fontWeight: 600, flex: "none", color: C.muted, opacity: 0.8, border: "1px solid " + C.panelBorder, borderRadius: 4, padding: "0 4px" },
|
|
3083
|
+
children: T("turn.unloaded")
|
|
3084
|
+
}),
|
|
3085
|
+
react_jsx_runtime.jsx("span", {
|
|
3086
|
+
style: { fontSize: 11, color: C.muted, fontWeight: 400, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", minWidth: 0 },
|
|
3087
|
+
children: g.userText || "#" + g.turn
|
|
3088
|
+
})
|
|
3089
|
+
]
|
|
3090
|
+
})
|
|
3091
|
+
];
|
|
3092
|
+
if (g.preview) {
|
|
3093
|
+
children.push(react_jsx_runtime.jsx("div", {
|
|
3094
|
+
style: { fontSize: 10, lineHeight: "13px", color: C.muted, opacity: 0.65, marginTop: 1, overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical" },
|
|
3095
|
+
children: g.preview
|
|
3096
|
+
}));
|
|
3097
|
+
}
|
|
3098
|
+
return react_jsx_runtime.jsx("div", {
|
|
3099
|
+
onClick: function () { extra.onOpenTurn(g.turn, g.seq); },
|
|
3100
|
+
onMouseEnter: function (e) {
|
|
3101
|
+
e.currentTarget.style.background = C.hover;
|
|
3102
|
+
e.currentTarget.style.opacity = "1";
|
|
3103
|
+
extra.hoverStart({ title: g.userText || "#" + g.turn, sub: "", preview: g.preview || "", meta: T("turn.unloaded"), ghost: true }, e.currentTarget);
|
|
3104
|
+
},
|
|
3105
|
+
onMouseLeave: function (e) {
|
|
3106
|
+
e.currentTarget.style.background = "transparent";
|
|
3107
|
+
e.currentTarget.style.opacity = "0.85";
|
|
3108
|
+
extra.hoverEnd();
|
|
3109
|
+
},
|
|
3110
|
+
"data-group-idx": gi,
|
|
3111
|
+
"data-ghost-turn": g.turn,
|
|
3112
|
+
title: T("turn.loadTip"),
|
|
3113
|
+
style: {
|
|
3114
|
+
padding: "3px 6px",
|
|
3115
|
+
margin: "1px 0",
|
|
3116
|
+
borderRadius: 6,
|
|
3117
|
+
cursor: "pointer",
|
|
3118
|
+
minWidth: 0,
|
|
3119
|
+
opacity: 0.85,
|
|
3120
|
+
transition: "background 0.15s ease, opacity 0.15s ease"
|
|
3121
|
+
},
|
|
3122
|
+
children: children
|
|
3123
|
+
}, "g-ghost-" + gi);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
2198
3126
|
// render each conversation turn as its own block: solid divider + time header
|
|
2199
3127
|
// offset = global group index of the first rendered group (stable React keys)
|
|
2200
3128
|
// activeIdx = array of groups being read (full opacity); others are dimmed
|
|
2201
|
-
function renderGroups(groups, trees, jump, C, offset, activeIdx) {
|
|
3129
|
+
function renderGroups(groups, trees, jump, C, offset, activeIdx, extra) {
|
|
3130
|
+
extra = extra || {};
|
|
2202
3131
|
var base = offset || 0;
|
|
2203
3132
|
var activeSet = {};
|
|
2204
3133
|
if (Array.isArray(activeIdx)) {
|
|
@@ -2208,24 +3137,55 @@ window.__ModuleLoader__.load({
|
|
|
2208
3137
|
for (var i = 0; i < groups.length; i++) {
|
|
2209
3138
|
var g = groups[i];
|
|
2210
3139
|
var gi = base + i;
|
|
2211
|
-
var
|
|
2212
|
-
|
|
3140
|
+
var isActive = !!activeSet[gi];
|
|
3141
|
+
var rows = g.ghost ? [] : trees[i];
|
|
3142
|
+
// The divider between two groups stays OUTSIDE the highlight box: inside it
|
|
3143
|
+
// would draw a rule straight across the tinted block.
|
|
3144
|
+
out.push(react_jsx_runtime.jsx("div", {
|
|
2213
3145
|
style: { borderTop: "1px solid " + C.panelBorder, margin: "7px 2px 3px", height: 0 }
|
|
2214
3146
|
}, "g-sep-" + gi));
|
|
2215
|
-
items
|
|
2216
|
-
|
|
2217
|
-
|
|
3147
|
+
var items = [];
|
|
3148
|
+
if (g.ghost) {
|
|
3149
|
+
items.push(renderGhostGroup(g, gi, C, extra));
|
|
3150
|
+
} else {
|
|
3151
|
+
items.push(renderGroupHeader(g, gi, jump, C, isActive, !rows || rows.length === 0));
|
|
3152
|
+
items.push(renderNodes(rows, 0, jump, C, "g" + gi, extra));
|
|
3153
|
+
}
|
|
3154
|
+
// Inactive groups used to fade to 0.6, which — multiplied by the panel's own
|
|
3155
|
+
// idle opacity — turned the whole list into grey. 0.85 keeps every row
|
|
3156
|
+
// readable, and "the group being read" is marked by a CLOSED tinted box
|
|
3157
|
+
// (fill + outline + accent bar) instead, which stays visible on top of any
|
|
3158
|
+
// panel transparency and no longer looks cut off on the right.
|
|
3159
|
+
var dim = !isActive && !g.ghost;
|
|
2218
3160
|
out.push(react_jsx_runtime.jsx("div", {
|
|
2219
3161
|
"data-group-idx": gi,
|
|
2220
|
-
|
|
3162
|
+
"data-active-group": isActive ? "on" : "off",
|
|
3163
|
+
style: {
|
|
3164
|
+
opacity: dim ? 0.85 : 1,
|
|
3165
|
+
// the box fades in AND out: colour transitions only, and the left accent
|
|
3166
|
+
// keeps its 3px width in both states so activating a group never reflows
|
|
3167
|
+
// (a width change would shift every row by 2px)
|
|
3168
|
+
transition: "opacity 0.15s ease, background-color 0.18s ease, border-color 0.18s ease",
|
|
3169
|
+
backgroundColor: isActive ? C.groupTint : "transparent",
|
|
3170
|
+
borderTop: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
3171
|
+
borderRight: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
3172
|
+
borderBottom: "1px solid " + (isActive ? C.groupEdgeSoft : "transparent"),
|
|
3173
|
+
borderLeft: "3px solid " + (isActive ? C.groupEdge : "transparent"),
|
|
3174
|
+
borderRadius: 6,
|
|
3175
|
+
margin: "2px -5px",
|
|
3176
|
+
paddingLeft: 2
|
|
3177
|
+
},
|
|
2221
3178
|
children: items
|
|
2222
3179
|
}, "g-" + gi));
|
|
2223
3180
|
}
|
|
2224
3181
|
return out;
|
|
2225
3182
|
}
|
|
2226
3183
|
|
|
3184
|
+
// Host-facing locale namespace: the slot declares `locale: "dsh-quick-toc"`,
|
|
3185
|
+
// which is what hands the panel its `t` prop. The panel's own visible strings
|
|
3186
|
+
// live in DICTS and are kept in sync with this table.
|
|
2227
3187
|
var zh = {
|
|
2228
|
-
"panel.title": "
|
|
3188
|
+
"panel.title": DICTS.zh["panel.title"]
|
|
2229
3189
|
};
|
|
2230
3190
|
var en = {
|
|
2231
3191
|
"panel.title": "Conversation Outline"
|
|
@@ -2237,6 +3197,29 @@ window.__ModuleLoader__.load({
|
|
|
2237
3197
|
ctx.effect(function () {
|
|
2238
3198
|
return ctx.locale.register("dsh-quick-toc", { zh: zh, en: en });
|
|
2239
3199
|
}, "dsh-quick-toc: dictionaries");
|
|
3200
|
+
|
|
3201
|
+
// Bridge to the host session face, used to open a turn whose events the
|
|
3202
|
+
// paged window has not loaded: ctx.sessions.binding(id).session.loadThrough(seq)
|
|
3203
|
+
// is the host's documented "page history backwards until the window covers
|
|
3204
|
+
// seq" jump loader.
|
|
3205
|
+
//
|
|
3206
|
+
// The client plugin facade offers two ways in: a direct `ctx.sessions`
|
|
3207
|
+
// property read (which demands `inject: ["sessions"]` on the returned plugin
|
|
3208
|
+
// object, and the runtime then PARKS the whole package whenever that provider
|
|
3209
|
+
// is absent) and `ctx.get(name)`, the declaration-free lookup. This uses the
|
|
3210
|
+
// lookup — deliberately optional, so a host without the session controller
|
|
3211
|
+
// still gets a working outline, and callers report the missing loader
|
|
3212
|
+
// instead of the panel disappearing.
|
|
3213
|
+
var host = {
|
|
3214
|
+
sessions: function () {
|
|
3215
|
+
try {
|
|
3216
|
+
return typeof ctx.get === "function" ? (ctx.get("sessions") || null) : null;
|
|
3217
|
+
} catch (e) {
|
|
3218
|
+
return null;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
};
|
|
3222
|
+
|
|
2240
3223
|
// Recent DSH (0.1.5-rc.1): session-scoped hooks (useChat/useSession/sessionId) only arrive
|
|
2241
3224
|
// inside a declared session slot. Register the panel into the session-scoped
|
|
2242
3225
|
// conversation.input.overlay (list/additive) so it receives useChat; the panel
|
|
@@ -2248,7 +3231,10 @@ window.__ModuleLoader__.load({
|
|
|
2248
3231
|
id: "quick-toc",
|
|
2249
3232
|
order: 90,
|
|
2250
3233
|
locale: "dsh-quick-toc"
|
|
2251
|
-
},
|
|
3234
|
+
}, function OutlinePanelWithHost(props) {
|
|
3235
|
+
// the panel needs the host bridge plus the session id it was registered for
|
|
3236
|
+
return OutlinePanel(Object.assign({}, props, { tocHost: host }));
|
|
3237
|
+
});
|
|
2252
3238
|
});
|
|
2253
3239
|
}
|
|
2254
3240
|
|
|
@@ -2267,6 +3253,15 @@ window.__ModuleLoader__.load({
|
|
|
2267
3253
|
// the level picker's enter/exit animations live here as classes so React
|
|
2268
3254
|
// re-renders (which rewrite inline styles) cannot restart or cancel them
|
|
2269
3255
|
".dqt-levels-pop{animation:dqt-pop-in 0.22s cubic-bezier(0.22, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
3256
|
+
// hover preview card: fades in place, and fades OUT when the pointer
|
|
3257
|
+
// leaves (both as stylesheet classes so re-renders cannot cancel them)
|
|
3258
|
+
".dqt-hover{animation:dqt-fade-in 0.12s linear both}" +
|
|
3259
|
+
".dqt-hover-closing{animation:dqt-fade-out 0.16s cubic-bezier(0.2, 0.9, 0.3, 1) both}" +
|
|
3260
|
+
// transient edge banner over the bottom of the outline list
|
|
3261
|
+
"@keyframes dqt-toast-in{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}" +
|
|
3262
|
+
"@keyframes dqt-toast-out{from{opacity:1;transform:none}to{opacity:0;transform:none}}" +
|
|
3263
|
+
".dqt-toast{animation:dqt-toast-in 0.2s cubic-bezier(0.22, 0.9, 0.3, 1) both}" +
|
|
3264
|
+
".dqt-toast-closing{animation:dqt-toast-out 0.42s linear both}" +
|
|
2270
3265
|
// exit: starts moving immediately (fast attack), so the dismiss feels snappy
|
|
2271
3266
|
".dqt-levels-pop-closing{animation:dqt-pop-out 0.14s cubic-bezier(0.2, 0.9, 0.3, 1) both;will-change:transform,opacity}" +
|
|
2272
3267
|
// Hide the native scrollbar entirely (no arrow buttons, no grey bar) —
|