dsh-quick-toc 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +104 -0
- package/CHANGELOG.md +82 -46
- package/README.en.md +36 -27
- package/README.md +35 -26
- package/lib/client.js +694 -62
- package/package.json +8 -7
package/lib/client.js
CHANGED
|
@@ -7,7 +7,7 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
let react = require("react");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
9
|
let react_dom = require("react-dom"); // createPortal -> panel stays a top-level overlay (highest pointer priority)
|
|
10
|
-
// DSH 0.1.
|
|
10
|
+
// Recent DSH (0.1.5-rc.1): the old @deepseek-ai/dsh-client-runtime package is gone.
|
|
11
11
|
// Runtime hooks (useChat etc.) now arrive as session-scope slot props —
|
|
12
12
|
// no runtime package to require here anymore.
|
|
13
13
|
|
|
@@ -78,12 +78,28 @@ window.__ModuleLoader__.load({
|
|
|
78
78
|
return first;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Headings are collected line by line so FENCED CODE BLOCKS can be skipped:
|
|
82
|
+
// a "```" block that documents markdown (or shows a shell comment like
|
|
83
|
+
// "# install") contains lines that look like headings but are code — they
|
|
84
|
+
// must not become outline entries (they have no element to jump to, and they
|
|
85
|
+
// shifted the index of the real headings).
|
|
81
86
|
function parseHeadings(text) {
|
|
82
87
|
var items = [];
|
|
83
|
-
|
|
84
|
-
var
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
if (!text) return items;
|
|
89
|
+
var lines = text.split("\n");
|
|
90
|
+
var fence = null; // { char, len } while inside a fenced code block
|
|
91
|
+
for (var i = 0; i < lines.length; i++) {
|
|
92
|
+
var line = lines[i];
|
|
93
|
+
var fm = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
94
|
+
if (fence !== null) {
|
|
95
|
+
// a closing fence uses the same character, is at least as long and has
|
|
96
|
+
// no info string after it
|
|
97
|
+
if (fm && fm[1].charAt(0) === fence.char && fm[1].length >= fence.len && fm[2].trim() === "") fence = null;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (fm) { fence = { char: fm[1].charAt(0), len: fm[1].length }; continue; }
|
|
101
|
+
var m = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
|
|
102
|
+
if (m !== null) items.push({ level: m[1].length, title: cleanTitle(m[2].trim()) });
|
|
87
103
|
}
|
|
88
104
|
return items;
|
|
89
105
|
}
|
|
@@ -101,6 +117,74 @@ window.__ModuleLoader__.load({
|
|
|
101
117
|
return root.children;
|
|
102
118
|
}
|
|
103
119
|
|
|
120
|
+
// ---- per-node extraction cache ----
|
|
121
|
+
// The conversation store keeps the same node objects for unchanged messages,
|
|
122
|
+
// so a streaming update only re-joins/re-parses the node that actually
|
|
123
|
+
// changed instead of every message in the history.
|
|
124
|
+
var EMPTY_HEADINGS = [];
|
|
125
|
+
var nodeInfoCache = new Map();
|
|
126
|
+
function nodeInfo(key, node) {
|
|
127
|
+
var isUser = node.kind === "user";
|
|
128
|
+
var blocks = node.data ? node.data.blocks : undefined;
|
|
129
|
+
var cached = nodeInfoCache.get(key);
|
|
130
|
+
if (cached !== undefined &&
|
|
131
|
+
(cached.node === node || (blocks !== undefined && cached.blocks === blocks && cached.user === isUser))) {
|
|
132
|
+
return cached;
|
|
133
|
+
}
|
|
134
|
+
var text = isUser ? extractUserText(node) : extractReplyText(node);
|
|
135
|
+
var info = {
|
|
136
|
+
node: node,
|
|
137
|
+
blocks: blocks,
|
|
138
|
+
user: isUser,
|
|
139
|
+
text: text,
|
|
140
|
+
headings: node.kind === "assistant-step" ? parseHeadings(text) : EMPTY_HEADINGS
|
|
141
|
+
};
|
|
142
|
+
nodeInfoCache.set(key, info);
|
|
143
|
+
return info;
|
|
144
|
+
}
|
|
145
|
+
// Drop cache records whose node left the conversation (bounded growth).
|
|
146
|
+
function pruneNodeInfo(seen) {
|
|
147
|
+
if (nodeInfoCache.size <= 200) return;
|
|
148
|
+
nodeInfoCache.forEach(function (value, key) {
|
|
149
|
+
if (!seen[key]) nodeInfoCache.delete(key);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// stable id for one heading occurrence (a message can hold several)
|
|
154
|
+
function headingId(h) {
|
|
155
|
+
return h.key + "#" + h.idx;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Split `text` into runs around case-insensitive occurrences of `q`, so the
|
|
159
|
+
// caller can render the hits distinctly. Returns [{ text, hit }, ...].
|
|
160
|
+
function highlightParts(text, q) {
|
|
161
|
+
var parts = [];
|
|
162
|
+
if (!text) return parts;
|
|
163
|
+
if (!q) return [{ text: text, hit: false }];
|
|
164
|
+
var lower = text.toLowerCase();
|
|
165
|
+
var from = 0;
|
|
166
|
+
for (;;) {
|
|
167
|
+
var at = lower.indexOf(q, from);
|
|
168
|
+
if (at === -1) break;
|
|
169
|
+
if (at > from) parts.push({ text: text.slice(from, at), hit: false });
|
|
170
|
+
parts.push({ text: text.slice(at, at + q.length), hit: true });
|
|
171
|
+
from = at + q.length;
|
|
172
|
+
}
|
|
173
|
+
if (from < text.length) parts.push({ text: text.slice(from), hit: false });
|
|
174
|
+
return parts;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// A one-line window around the first hit of `q` in `text` (for search results).
|
|
178
|
+
function snippetAround(text, q, span) {
|
|
179
|
+
if (!text) return "";
|
|
180
|
+
var flat = text.replace(/\s+/g, " ").trim();
|
|
181
|
+
var at = flat.toLowerCase().indexOf(q);
|
|
182
|
+
if (at === -1) return previewText(flat, span * 2);
|
|
183
|
+
var start = Math.max(0, at - span);
|
|
184
|
+
var end = Math.min(flat.length, at + q.length + span);
|
|
185
|
+
return (start > 0 ? "…" : "") + flat.slice(start, end) + (end < flat.length ? "…" : "");
|
|
186
|
+
}
|
|
187
|
+
|
|
104
188
|
// strict lookup: exact dataset match or CSS-escaped selector only —
|
|
105
189
|
// no fuzzy contains matching (avoids landing on the wrong row);
|
|
106
190
|
// hidden rows (zero rect, e.g. duplicate/hidden copies) are skipped
|
|
@@ -145,6 +229,10 @@ window.__ModuleLoader__.load({
|
|
|
145
229
|
|
|
146
230
|
// count every occurrence of q (case-insensitive) inside text
|
|
147
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;
|
|
148
236
|
var lower = text.toLowerCase();
|
|
149
237
|
var count = 0;
|
|
150
238
|
var idx = 0;
|
|
@@ -168,6 +256,56 @@ window.__ModuleLoader__.load({
|
|
|
168
256
|
return null;
|
|
169
257
|
}
|
|
170
258
|
|
|
259
|
+
// ---- which view is the conversation area showing? ----
|
|
260
|
+
// The center column hosts ONE view at a time (chat / trajectory / context /
|
|
261
|
+
// any plugin view): DSH renders only the selected view entry, but the
|
|
262
|
+
// scrollport is shared, so our overlay would otherwise float over every view.
|
|
263
|
+
// The tab list (rendered when there is more than one view) marks the active
|
|
264
|
+
// view with aria-selected; ui-chat registers the chat view with order 0, so
|
|
265
|
+
// its tab comes first. Anything unknowable degrades to "chat" (visible).
|
|
266
|
+
var CHAT_VIEW_LABELS = ["对话", "chat"];
|
|
267
|
+
// Cached so the fast poll costs one isConnected check in the common case.
|
|
268
|
+
var _chatTablistCache = null;
|
|
269
|
+
function conversationTablist() {
|
|
270
|
+
var cached = _chatTablistCache;
|
|
271
|
+
if (cached && cached.isConnected) return cached;
|
|
272
|
+
var found = null;
|
|
273
|
+
// Scope the lookup to the conversation area: walk up from the shared
|
|
274
|
+
// scrollport and take the first tab list an ancestor owns, so an
|
|
275
|
+
// unrelated tab list (e.g. inside a settings dialog) can never match.
|
|
276
|
+
var sp = document.querySelector("[data-conversation-scroll]");
|
|
277
|
+
if (sp) {
|
|
278
|
+
var node = sp;
|
|
279
|
+
for (var d = 0; d < 6 && node && !found; d++) {
|
|
280
|
+
node = node.parentElement;
|
|
281
|
+
if (node) found = node.querySelector('[role="tablist"]');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!found) found = document.querySelector('[role="tablist"]');
|
|
285
|
+
_chatTablistCache = found;
|
|
286
|
+
return found;
|
|
287
|
+
}
|
|
288
|
+
function isChatViewActive() {
|
|
289
|
+
try {
|
|
290
|
+
var tablist = conversationTablist();
|
|
291
|
+
if (!tablist) return true; // a single view: no tab list, and it is chat
|
|
292
|
+
var tabs = tablist.querySelectorAll('button[role="tab"]');
|
|
293
|
+
if (tabs.length < 2) return true;
|
|
294
|
+
var activeIdx = -1;
|
|
295
|
+
var chatIdx = -1;
|
|
296
|
+
for (var i = 0; i < tabs.length; i++) {
|
|
297
|
+
var label = (tabs[i].textContent || "").trim().toLowerCase();
|
|
298
|
+
if (chatIdx === -1 && CHAT_VIEW_LABELS.indexOf(label) !== -1) chatIdx = i;
|
|
299
|
+
if (activeIdx === -1 && tabs[i].getAttribute("aria-selected") === "true") activeIdx = i;
|
|
300
|
+
}
|
|
301
|
+
if (activeIdx === -1) return true; // no explicit selection -> assume chat
|
|
302
|
+
if (chatIdx === -1) chatIdx = 0; // chat is order 0 -> the first tab
|
|
303
|
+
return activeIdx === chatIdx;
|
|
304
|
+
} catch (e) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
171
309
|
// ---- search keyword highlight in the conversation ----
|
|
172
310
|
var highlightSpans = [];
|
|
173
311
|
|
|
@@ -195,26 +333,36 @@ window.__ModuleLoader__.load({
|
|
|
195
333
|
var text = node.nodeValue;
|
|
196
334
|
if (!text) continue;
|
|
197
335
|
var lower = text.toLowerCase();
|
|
198
|
-
|
|
199
|
-
if (idx < 0) continue;
|
|
336
|
+
if (lower.indexOf(q) < 0) continue;
|
|
200
337
|
var frag = document.createDocumentFragment();
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
338
|
+
// EVERY occurrence inside this text node must be wrapped, in order: the
|
|
339
|
+
// caller addresses hits by their global index (`occ`), which is counted
|
|
340
|
+
// over the message text. Wrapping only the first one per text node made
|
|
341
|
+
// a second hit on the same line un-markable, so stepping to it found no
|
|
342
|
+
// `.dqt-current` and fell back to a plain scroll with no highlight.
|
|
343
|
+
var from = 0;
|
|
344
|
+
var idx = lower.indexOf(q, from);
|
|
345
|
+
while (idx !== -1) {
|
|
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);
|
|
361
|
+
occ++;
|
|
362
|
+
from = idx + q.length;
|
|
363
|
+
idx = lower.indexOf(q, from);
|
|
210
364
|
}
|
|
211
|
-
|
|
212
|
-
mark.style.color = "inherit";
|
|
213
|
-
mark.textContent = text.slice(idx, idx + q.length);
|
|
214
|
-
frag.appendChild(mark);
|
|
215
|
-
highlightSpans.push(mark);
|
|
216
|
-
occ++;
|
|
217
|
-
var after = text.slice(idx + q.length);
|
|
365
|
+
var after = text.slice(from);
|
|
218
366
|
if (after) frag.appendChild(document.createTextNode(after));
|
|
219
367
|
node.parentNode.replaceChild(frag, node);
|
|
220
368
|
}
|
|
@@ -306,13 +454,13 @@ window.__ModuleLoader__.load({
|
|
|
306
454
|
|
|
307
455
|
// ---------- component ----------
|
|
308
456
|
function OutlinePanel(props) {
|
|
309
|
-
// DSH 0.1.
|
|
457
|
+
// Recent DSH (0.1.5-rc.1): the conversation moved out of the session snapshot —
|
|
310
458
|
// it is now the session-scope `chat` hook (ChatSnapshot: order + nodes
|
|
311
459
|
// map + legacy projection), contributed by dsh-client-ui-chat. Same node
|
|
312
460
|
// shape as before (kind user/assistant-step, location.turn, data.blocks).
|
|
313
461
|
var useChat = props.useChat;
|
|
314
462
|
if (!useChat) {
|
|
315
|
-
console.warn("[dsh-quick-toc] useChat prop missing (requires DSH >= 0.1.
|
|
463
|
+
console.warn("[dsh-quick-toc] useChat prop missing (requires DSH >= 0.1.5-rc.1)");
|
|
316
464
|
return null;
|
|
317
465
|
}
|
|
318
466
|
var order = useChat(function (s) { return s.order; });
|
|
@@ -425,6 +573,72 @@ window.__ModuleLoader__.load({
|
|
|
425
573
|
var panelH = _s8[0];
|
|
426
574
|
var setPanelH = _s8[1];
|
|
427
575
|
|
|
576
|
+
// ---- heading level filter (persisted) ----
|
|
577
|
+
// An arbitrary SET of levels, not a "show up to N" prefix: every chip is an
|
|
578
|
+
// independent on/off switch, so H1 + H3 without H2 is a valid view. Turning
|
|
579
|
+
// the last remaining level off restores all six (the panel is never empty).
|
|
580
|
+
var LEVELS_KEY = "dsh-quick-toc.levels.v1";
|
|
581
|
+
var OLD_MAX_LEVEL_KEY = "dsh-quick-toc.maxLevel.v1";
|
|
582
|
+
var ALL_LEVELS = [1, 2, 3, 4, 5, 6];
|
|
583
|
+
var _sLv = react.useState(function () {
|
|
584
|
+
try {
|
|
585
|
+
var raw = localStorage.getItem(LEVELS_KEY);
|
|
586
|
+
if (raw !== null) {
|
|
587
|
+
var arr = JSON.parse(raw);
|
|
588
|
+
if (Array.isArray(arr)) {
|
|
589
|
+
var kept = ALL_LEVELS.filter(function (lv) { return arr.indexOf(lv) !== -1; });
|
|
590
|
+
if (kept.length > 0) return kept;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// migrate the "show up to N" value written by earlier builds
|
|
594
|
+
var v = Number(localStorage.getItem(OLD_MAX_LEVEL_KEY));
|
|
595
|
+
if (isFinite(v) && v >= 1 && v <= 6) {
|
|
596
|
+
return ALL_LEVELS.filter(function (lv) { return lv <= v; });
|
|
597
|
+
}
|
|
598
|
+
} catch (e) {}
|
|
599
|
+
return ALL_LEVELS.slice();
|
|
600
|
+
});
|
|
601
|
+
var levels = _sLv[0];
|
|
602
|
+
var setLevels = _sLv[1];
|
|
603
|
+
var levelSet = react.useMemo(function () {
|
|
604
|
+
var m = {};
|
|
605
|
+
for (var i = 0; i < levels.length; i++) m[levels[i]] = true;
|
|
606
|
+
return m;
|
|
607
|
+
}, [levels]);
|
|
608
|
+
var toggleLevel = function (lv) {
|
|
609
|
+
var next;
|
|
610
|
+
if (levelSet[lv]) {
|
|
611
|
+
next = levels.filter(function (x) { return x !== lv; });
|
|
612
|
+
if (next.length === 0) next = ALL_LEVELS.slice(); // never leave the panel empty
|
|
613
|
+
} else {
|
|
614
|
+
next = ALL_LEVELS.filter(function (x) { return x === lv || levelSet[x]; });
|
|
615
|
+
}
|
|
616
|
+
setLevels(next);
|
|
617
|
+
try { localStorage.setItem(LEVELS_KEY, JSON.stringify(next)); } catch (e) {}
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
// ---- level-filter row (the H1–H6 chips) can be collapsed away; the toggle
|
|
621
|
+
// sits next to the search button in the panel header and the choice is
|
|
622
|
+
// remembered. The filter itself keeps applying while the row is hidden.
|
|
623
|
+
var LEVELS_ROW_KEY = "dsh-quick-toc.levelsRow.v1";
|
|
624
|
+
var _sLr = react.useState(function () {
|
|
625
|
+
try { return localStorage.getItem(LEVELS_ROW_KEY) !== "0"; } catch (e) { return true; }
|
|
626
|
+
});
|
|
627
|
+
var showLevelsRow = _sLr[0];
|
|
628
|
+
var setShowLevelsRow = _sLr[1];
|
|
629
|
+
var toggleLevelsRow = function () {
|
|
630
|
+
var next = !showLevelsRow;
|
|
631
|
+
setShowLevelsRow(next);
|
|
632
|
+
try { localStorage.setItem(LEVELS_ROW_KEY, next ? "1" : "0"); } catch (e) {}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// ---- breadcrumb: the heading the reader is currently inside ----
|
|
636
|
+
var _sCrumb = react.useState(null);
|
|
637
|
+
var crumb = _sCrumb[0];
|
|
638
|
+
var setCrumb = _sCrumb[1];
|
|
639
|
+
var crumbRef = react.useRef(null);
|
|
640
|
+
var headingPathsRef = react.useRef({});
|
|
641
|
+
|
|
428
642
|
// ---- pagination: show the latest N groups; scrolling to the top loads older ----
|
|
429
643
|
var PAGE_SIZE = 6;
|
|
430
644
|
var _s9 = react.useState(PAGE_SIZE);
|
|
@@ -434,6 +648,39 @@ window.__ModuleLoader__.load({
|
|
|
434
648
|
var didInitScroll = react.useRef(false);
|
|
435
649
|
var outlineTouchRef = react.useRef(0); // last time the user touched the outline
|
|
436
650
|
|
|
651
|
+
// ---- the outline belongs to the CHAT view only: when the center column
|
|
652
|
+
// switches to another view (trajectory / context / plugin views), fade the
|
|
653
|
+
// panel and its collapsed handle out. Polled lightly because view switches
|
|
654
|
+
// are user clicks and the tab DOM is remounted on session/view changes.
|
|
655
|
+
var _sView = react.useState(true);
|
|
656
|
+
var chatViewActive = _sView[0];
|
|
657
|
+
var setChatViewActive = _sView[1];
|
|
658
|
+
var chatViewRef = react.useRef(true);
|
|
659
|
+
// True briefly after a view switch: lets the fade use its own fast
|
|
660
|
+
// transition instead of the slower dock/collapse one.
|
|
661
|
+
var _sFade = react.useState(false);
|
|
662
|
+
var viewFading = _sFade[0];
|
|
663
|
+
var setViewFading = _sFade[1];
|
|
664
|
+
var viewFadeTimerRef = react.useRef(null);
|
|
665
|
+
react.useEffect(function () {
|
|
666
|
+
var detect = function () {
|
|
667
|
+
var v = isChatViewActive();
|
|
668
|
+
if (v !== chatViewRef.current) {
|
|
669
|
+
chatViewRef.current = v;
|
|
670
|
+
setChatViewActive(v);
|
|
671
|
+
setViewFading(true);
|
|
672
|
+
if (viewFadeTimerRef.current) clearTimeout(viewFadeTimerRef.current);
|
|
673
|
+
viewFadeTimerRef.current = setTimeout(function () { setViewFading(false); }, 380);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
detect();
|
|
677
|
+
var timer = setInterval(detect, 120);
|
|
678
|
+
return function () {
|
|
679
|
+
clearInterval(timer);
|
|
680
|
+
if (viewFadeTimerRef.current) clearTimeout(viewFadeTimerRef.current);
|
|
681
|
+
};
|
|
682
|
+
}, []);
|
|
683
|
+
|
|
437
684
|
// ---- search: header button opens a keyword box; Enter cycles through
|
|
438
685
|
// matching headings and jumps to each ----
|
|
439
686
|
var _s13 = react.useState(false);
|
|
@@ -510,13 +757,6 @@ window.__ModuleLoader__.load({
|
|
|
510
757
|
var setActiveGroup = _s10[1];
|
|
511
758
|
var activeSigRef = react.useRef("");
|
|
512
759
|
|
|
513
|
-
// first time content appears, scroll the list to the bottom (newest)
|
|
514
|
-
react.useEffect(function () {
|
|
515
|
-
if (didInitScroll.current || !listRef.current || !groups || groups.length === 0) return;
|
|
516
|
-
didInitScroll.current = true;
|
|
517
|
-
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
518
|
-
}, [groups ? groups.length : 0]);
|
|
519
|
-
|
|
520
760
|
// load older outline groups; when everything is loaded, click the
|
|
521
761
|
// conversation's own "load older" button so older turns keep appearing
|
|
522
762
|
var loadOlderOutline = function (el) {
|
|
@@ -677,6 +917,7 @@ window.__ModuleLoader__.load({
|
|
|
677
917
|
var turnUserText = {};
|
|
678
918
|
var turnUserFull = {};
|
|
679
919
|
if (!order || !nodes) return result;
|
|
920
|
+
var seen = {};
|
|
680
921
|
// first pass: per-turn time — the LAST message of the turn wins (end time);
|
|
681
922
|
// also remember each turn's user message key + first-line preview + full text
|
|
682
923
|
for (var i = 0; i < order.length; i++) {
|
|
@@ -686,10 +927,12 @@ window.__ModuleLoader__.load({
|
|
|
686
927
|
var l0 = n0.location;
|
|
687
928
|
var tid0 = l0 && (l0.kind === "turn" || l0.kind === "step") && l0.turn ? l0.turn.turn : null;
|
|
688
929
|
if (tid0 === null) continue;
|
|
930
|
+
seen[k0] = true;
|
|
931
|
+
var info0 = nodeInfo(k0, n0);
|
|
689
932
|
if (n0.kind === "user" && turnUserKey[tid0] === undefined) {
|
|
690
933
|
turnUserKey[tid0] = k0;
|
|
691
|
-
turnUserText[tid0] = previewText(
|
|
692
|
-
turnUserFull[tid0] =
|
|
934
|
+
turnUserText[tid0] = previewText(info0.text, 30);
|
|
935
|
+
turnUserFull[tid0] = info0.text;
|
|
693
936
|
}
|
|
694
937
|
var t0 = getNodeTime(n0);
|
|
695
938
|
if (t0) turnTimes[tid0] = t0;
|
|
@@ -714,22 +957,37 @@ window.__ModuleLoader__.load({
|
|
|
714
957
|
};
|
|
715
958
|
result.push(current);
|
|
716
959
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
var
|
|
721
|
-
|
|
722
|
-
current.headings.push({ level: parsed[k].level, title: parsed[k].title, key: key, idx: hIdx });
|
|
723
|
-
hIdx++;
|
|
960
|
+
seen[key] = true;
|
|
961
|
+
var info = nodeInfo(key, node);
|
|
962
|
+
current.msgs.push({ key: key, text: info.text });
|
|
963
|
+
for (var k = 0; k < info.headings.length; k++) {
|
|
964
|
+
current.headings.push({ level: info.headings[k].level, title: info.headings[k].title, key: key, idx: k });
|
|
724
965
|
}
|
|
725
966
|
}
|
|
967
|
+
pruneNodeInfo(seen);
|
|
726
968
|
// keep a turn when it has headings OR a time — turns without headings
|
|
727
969
|
// still get a standalone time entry in the outline (click to jump)
|
|
728
970
|
return result.filter(function (g) { return g.headings.length > 0 || g.time !== ""; });
|
|
729
971
|
}, [order, nodes]);
|
|
972
|
+
// first time content appears, scroll the list to the bottom (newest).
|
|
973
|
+
// NOTE: must stay BELOW the `groups` memo — a deps array is evaluated
|
|
974
|
+
// during render, so reading `groups` before that `var` is assigned would
|
|
975
|
+
// freeze the deps at 0 and the effect would only ever run on mount.
|
|
976
|
+
react.useEffect(function () {
|
|
977
|
+
if (didInitScroll.current || !listRef.current || groups.length === 0) return;
|
|
978
|
+
didInitScroll.current = true;
|
|
979
|
+
listRef.current.scrollTop = listRef.current.scrollHeight;
|
|
980
|
+
}, [groups.length]);
|
|
730
981
|
var groupTrees = react.useMemo(function () {
|
|
731
|
-
return groups.map(function (g) {
|
|
732
|
-
|
|
982
|
+
return groups.map(function (g) {
|
|
983
|
+
if (levels.length === 6) return buildTree(g.headings);
|
|
984
|
+
var kept = [];
|
|
985
|
+
for (var i = 0; i < g.headings.length; i++) {
|
|
986
|
+
if (levelSet[g.headings[i].level]) kept.push(g.headings[i]);
|
|
987
|
+
}
|
|
988
|
+
return buildTree(kept);
|
|
989
|
+
});
|
|
990
|
+
}, [groups, levels, levelSet]);
|
|
733
991
|
// pagination slice: the latest `visibleCount` groups
|
|
734
992
|
var shownGroups = groups.slice(Math.max(0, groups.length - visibleCount));
|
|
735
993
|
var shownTrees = groupTrees.slice(groupTrees.length - shownGroups.length);
|
|
@@ -745,6 +1003,80 @@ window.__ModuleLoader__.load({
|
|
|
745
1003
|
}
|
|
746
1004
|
return m;
|
|
747
1005
|
}, [groups]);
|
|
1006
|
+
|
|
1007
|
+
// ancestor path of every heading occurrence (breadcrumb + result rows):
|
|
1008
|
+
// id -> { path, title, level, gi }
|
|
1009
|
+
var headingPaths = react.useMemo(function () {
|
|
1010
|
+
var byId = {};
|
|
1011
|
+
var lastTitle = {};
|
|
1012
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
1013
|
+
var g = groups[gi];
|
|
1014
|
+
for (var j = 0; j < g.headings.length; j++) {
|
|
1015
|
+
var h = g.headings[j];
|
|
1016
|
+
for (var deeper = h.level + 1; deeper <= 6; deeper++) lastTitle[deeper] = undefined;
|
|
1017
|
+
var parts = [];
|
|
1018
|
+
for (var up = 1; up < h.level; up++) {
|
|
1019
|
+
if (lastTitle[up]) parts.push(lastTitle[up]);
|
|
1020
|
+
}
|
|
1021
|
+
parts.push(h.title);
|
|
1022
|
+
byId[headingId(h)] = { path: parts.join(" › "), title: h.title, level: h.level, gi: gi };
|
|
1023
|
+
lastTitle[h.level] = h.title;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return byId;
|
|
1027
|
+
}, [groups]);
|
|
1028
|
+
|
|
1029
|
+
// search result rows: one row per matched heading/message (with an
|
|
1030
|
+
// occurrence count), carrying its heading path, turn time and a snippet.
|
|
1031
|
+
var resultRows = react.useMemo(function () {
|
|
1032
|
+
var q = query.trim().toLowerCase();
|
|
1033
|
+
if (!q) return [];
|
|
1034
|
+
var rows = [];
|
|
1035
|
+
var index = {};
|
|
1036
|
+
var push = function (row, n) {
|
|
1037
|
+
var id = row.key + "#" + (row.idx === undefined ? "-" : row.idx);
|
|
1038
|
+
var known = index[id];
|
|
1039
|
+
if (known !== undefined) { known.count += n; return; }
|
|
1040
|
+
row.count = n;
|
|
1041
|
+
index[id] = row;
|
|
1042
|
+
rows.push(row);
|
|
1043
|
+
};
|
|
1044
|
+
for (var gi = 0; gi < groups.length; gi++) {
|
|
1045
|
+
var g = groups[gi];
|
|
1046
|
+
for (var j = 0; j < g.headings.length; j++) {
|
|
1047
|
+
var h = g.headings[j];
|
|
1048
|
+
var nh = countOccurrences(h.title, q);
|
|
1049
|
+
if (nh > 0) {
|
|
1050
|
+
var hp = headingPaths[headingId(h)];
|
|
1051
|
+
push({
|
|
1052
|
+
gi: gi, key: h.key, idx: h.idx, title: h.title, level: h.level,
|
|
1053
|
+
path: hp ? hp.path : "", time: g.time, snippet: ""
|
|
1054
|
+
}, nh);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (searchScope === "full") {
|
|
1058
|
+
var nu = g.userKey && g.userFull ? countOccurrences(g.userFull, q) : 0;
|
|
1059
|
+
if (nu > 0) {
|
|
1060
|
+
push({
|
|
1061
|
+
gi: gi, key: g.userKey, idx: undefined, title: previewText(g.userFull, 40),
|
|
1062
|
+
level: 0, path: "", time: g.time, snippet: snippetAround(g.userFull, q, 36)
|
|
1063
|
+
}, nu);
|
|
1064
|
+
}
|
|
1065
|
+
for (var m = 0; m < g.msgs.length; m++) {
|
|
1066
|
+
var msg = g.msgs[m];
|
|
1067
|
+
if (!msg.text) continue;
|
|
1068
|
+
var nm = countOccurrences(msg.text, q);
|
|
1069
|
+
if (nm === 0) continue;
|
|
1070
|
+
push({
|
|
1071
|
+
gi: gi, key: msg.key, idx: undefined, title: previewText(msg.text, 40),
|
|
1072
|
+
level: 0, path: "", time: g.time, snippet: snippetAround(msg.text, q, 36)
|
|
1073
|
+
}, nm);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return rows;
|
|
1078
|
+
}, [groups, query, searchScope, headingPaths]);
|
|
1079
|
+
headingPathsRef.current = headingPaths;
|
|
748
1080
|
react.useEffect(function () {
|
|
749
1081
|
var sp = document.querySelector("[data-conversation-scroll]");
|
|
750
1082
|
if (!sp) return;
|
|
@@ -791,6 +1123,43 @@ window.__ModuleLoader__.load({
|
|
|
791
1123
|
}
|
|
792
1124
|
}
|
|
793
1125
|
}
|
|
1126
|
+
// breadcrumb: the heading of the section that occupies most of the
|
|
1127
|
+
// viewport — i.e. the deepest heading above the viewport's MIDDLE line.
|
|
1128
|
+
// Walking backwards lets a heading we cannot map (e.g. one inside a tool
|
|
1129
|
+
// result) fall back to the nearest mapped ancestor heading.
|
|
1130
|
+
//
|
|
1131
|
+
// The middle line is what makes this consistent with every jump: a jump
|
|
1132
|
+
// (outline click, breadcrumb click, match stepping) always lands its
|
|
1133
|
+
// target ABOVE the middle, so clicking the breadcrumb never changes
|
|
1134
|
+
// which heading the breadcrumb shows.
|
|
1135
|
+
var crumbLine = vTop + lr.height * 0.5;
|
|
1136
|
+
var heads = sp.querySelectorAll("h1,h2,h3,h4,h5,h6");
|
|
1137
|
+
var nextCrumb = null;
|
|
1138
|
+
var paths = headingPathsRef.current;
|
|
1139
|
+
for (var hi = heads.length - 1; hi >= 0; hi--) {
|
|
1140
|
+
var hr = heads[hi].getBoundingClientRect();
|
|
1141
|
+
if (hr.top > crumbLine) continue;
|
|
1142
|
+
var rowEl = heads[hi].closest ? heads[hi].closest("[data-chat-anchor-key]") : null;
|
|
1143
|
+
if (!rowEl || !rowEl.dataset) continue;
|
|
1144
|
+
var rowKey = rowEl.dataset.chatAnchorKey;
|
|
1145
|
+
var rowHeads = rowEl.querySelectorAll("h1,h2,h3,h4,h5,h6");
|
|
1146
|
+
var idxInRow = -1;
|
|
1147
|
+
for (var rh = 0; rh < rowHeads.length; rh++) {
|
|
1148
|
+
if (rowHeads[rh] === heads[hi]) { idxInRow = rh; break; }
|
|
1149
|
+
}
|
|
1150
|
+
var meta = idxInRow >= 0 ? paths[rowKey + "#" + idxInRow] : undefined;
|
|
1151
|
+
if (meta !== undefined) {
|
|
1152
|
+
nextCrumb = { path: meta.path, key: rowKey, idx: idxInRow };
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
var prev = crumbRef.current;
|
|
1157
|
+
var same = (prev === null && nextCrumb === null) ||
|
|
1158
|
+
(prev !== null && nextCrumb !== null && prev.path === nextCrumb.path && prev.key === nextCrumb.key && prev.idx === nextCrumb.idx);
|
|
1159
|
+
if (!same) {
|
|
1160
|
+
crumbRef.current = nextCrumb;
|
|
1161
|
+
setCrumb(nextCrumb);
|
|
1162
|
+
}
|
|
794
1163
|
};
|
|
795
1164
|
update();
|
|
796
1165
|
sp.addEventListener("scroll", update, { passive: true });
|
|
@@ -835,6 +1204,46 @@ window.__ModuleLoader__.load({
|
|
|
835
1204
|
return out;
|
|
836
1205
|
}, [groups, query, searchScope]);
|
|
837
1206
|
|
|
1207
|
+
// which result row holds the current (n/N) occurrence (needs `matches`)
|
|
1208
|
+
var activeResultRow = -1;
|
|
1209
|
+
if (resultRows.length > 0 && matches.length > 0) {
|
|
1210
|
+
var acc = 0;
|
|
1211
|
+
var target = matchIdx % matches.length;
|
|
1212
|
+
for (var ri = 0; ri < resultRows.length; ri++) {
|
|
1213
|
+
acc += resultRows[ri].count;
|
|
1214
|
+
if (target < acc) { activeResultRow = ri; break; }
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// The result list follows the outline's convention: OLDEST hit at the top,
|
|
1219
|
+
// NEWEST at the bottom, and a fresh search starts scrolled to the bottom —
|
|
1220
|
+
// scroll UP from there to walk back through earlier hits. The newest hit is
|
|
1221
|
+
// also made the current one, so the highlighted row is the one on screen
|
|
1222
|
+
// (otherwise n/N points at a row that is off-view).
|
|
1223
|
+
var searchPosRef = react.useRef("");
|
|
1224
|
+
react.useEffect(function () {
|
|
1225
|
+
var q = query.trim();
|
|
1226
|
+
if (searchPosRef.current === q) return;
|
|
1227
|
+
searchPosRef.current = q;
|
|
1228
|
+
if (!q) return;
|
|
1229
|
+
var el = listRef.current;
|
|
1230
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
1231
|
+
if (matches.length > 0) setMatchIdx(matches.length - 1);
|
|
1232
|
+
}, [query, matches.length]);
|
|
1233
|
+
|
|
1234
|
+
// keep the current row in view while stepping with Enter
|
|
1235
|
+
react.useEffect(function () {
|
|
1236
|
+
if (!query.trim() || activeResultRow < 0) return;
|
|
1237
|
+
var el = listRef.current;
|
|
1238
|
+
if (!el || !el.querySelector) return;
|
|
1239
|
+
var node = el.querySelector('[data-result-idx="' + activeResultRow + '"]');
|
|
1240
|
+
if (!node) return;
|
|
1241
|
+
var er = node.getBoundingClientRect();
|
|
1242
|
+
var lr2 = el.getBoundingClientRect();
|
|
1243
|
+
if (er.top < lr2.top + 2) el.scrollTop += er.top - lr2.top - 2;
|
|
1244
|
+
else if (er.bottom > lr2.bottom - 2) el.scrollTop += er.bottom - lr2.bottom + 2;
|
|
1245
|
+
}, [activeResultRow, query]);
|
|
1246
|
+
|
|
838
1247
|
// ---- nothing to show without headings ----
|
|
839
1248
|
if (groups.length === 0) return null;
|
|
840
1249
|
|
|
@@ -866,15 +1275,19 @@ window.__ModuleLoader__.load({
|
|
|
866
1275
|
if (e.button !== 0) return;
|
|
867
1276
|
var startY = e.clientY;
|
|
868
1277
|
var origY = panelY;
|
|
1278
|
+
var lastY = panelY;
|
|
869
1279
|
var move = function (ev) {
|
|
870
1280
|
var next = origY + (ev.clientY - startY);
|
|
871
1281
|
next = Math.max(-(viewport ? viewport.top : 0), Math.min(window.innerHeight - 90, next));
|
|
1282
|
+
lastY = next;
|
|
872
1283
|
setPanelY(next);
|
|
873
1284
|
};
|
|
874
1285
|
var up = function () {
|
|
875
1286
|
window.removeEventListener("pointermove", move);
|
|
876
1287
|
window.removeEventListener("pointerup", up);
|
|
877
|
-
|
|
1288
|
+
// persist the DRAGGED value: `panelY` here is the value captured when
|
|
1289
|
+
// the drag started (the state setter has not re-rendered this closure)
|
|
1290
|
+
try { localStorage.setItem(PANEL_Y_KEY, String(lastY)); } catch (e2) {}
|
|
878
1291
|
};
|
|
879
1292
|
window.addEventListener("pointermove", move);
|
|
880
1293
|
window.addEventListener("pointerup", up);
|
|
@@ -950,6 +1363,15 @@ window.__ModuleLoader__.load({
|
|
|
950
1363
|
}, 150);
|
|
951
1364
|
};
|
|
952
1365
|
|
|
1366
|
+
// clicking result row `i` = make its FIRST occurrence the current match, so
|
|
1367
|
+
// it highlights + scrolls exactly like Enter stepping does (rows are deduped
|
|
1368
|
+
// one-per-message, so row i starts at the sum of the previous rows' counts)
|
|
1369
|
+
var goToRow = function (i) {
|
|
1370
|
+
var start = 0;
|
|
1371
|
+
for (var k = 0; k < i && k < resultRows.length; k++) start += resultRows[k].count;
|
|
1372
|
+
goToMatch(start);
|
|
1373
|
+
};
|
|
1374
|
+
|
|
953
1375
|
// ---- circular icon button (glyph optical offset: ox/oy px) ----
|
|
954
1376
|
var iconBtn = function (onClick, tip, glyph, fontSize, offset) {
|
|
955
1377
|
var ox = offset ? (offset.x || 0) : 0;
|
|
@@ -1018,7 +1440,8 @@ window.__ModuleLoader__.load({
|
|
|
1018
1440
|
panelClip = "inset(0 0 0 " + (panelW + 8) + "px)";
|
|
1019
1441
|
}
|
|
1020
1442
|
}
|
|
1021
|
-
|
|
1443
|
+
// faded out entirely while another center-column view is active
|
|
1444
|
+
var panelOpacity = open && chatViewActive ? (hovered ? 0.95 : 0.45) : 0;
|
|
1022
1445
|
|
|
1023
1446
|
var panelEl = react_jsx_runtime.jsx("div", {
|
|
1024
1447
|
style: {
|
|
@@ -1042,8 +1465,10 @@ window.__ModuleLoader__.load({
|
|
|
1042
1465
|
// soft shade in light) — outer shadow would be clipped by the collapse
|
|
1043
1466
|
// clip-path and the panel edges.
|
|
1044
1467
|
boxShadow: innerShadow(),
|
|
1045
|
-
|
|
1046
|
-
|
|
1468
|
+
// A view switch swaps in a short, delay-free opacity fade; the dock and
|
|
1469
|
+
// collapse animations keep their own (slower) timings.
|
|
1470
|
+
transition: viewFading ? "opacity 0.3s ease" : panelTransition,
|
|
1471
|
+
pointerEvents: chatViewActive ? "auto" : "none"
|
|
1047
1472
|
},
|
|
1048
1473
|
onMouseEnter: function () { setHovered(true); },
|
|
1049
1474
|
onMouseLeave: function () { setHovered(false); },
|
|
@@ -1115,6 +1540,29 @@ window.__ModuleLoader__.load({
|
|
|
1115
1540
|
react_jsx_runtime.jsx("div", {
|
|
1116
1541
|
style: { display: "flex", alignItems: "center", gap: 6, flex: "none" },
|
|
1117
1542
|
children: [
|
|
1543
|
+
// collapse/expand the H1–H6 level-filter row (sits directly
|
|
1544
|
+
// left of the search button). Labelled, not a bare icon.
|
|
1545
|
+
react_jsx_runtime.jsx("button", {
|
|
1546
|
+
onClick: toggleLevelsRow,
|
|
1547
|
+
title: showLevelsRow ? "收起 H1–H6 层级筛选那一行" : "展开 H1–H6 层级筛选那一行",
|
|
1548
|
+
style: {
|
|
1549
|
+
height: 20,
|
|
1550
|
+
padding: "0 6px",
|
|
1551
|
+
border: "none",
|
|
1552
|
+
borderRadius: 5,
|
|
1553
|
+
cornerShape: "round",
|
|
1554
|
+
fontFamily: "inherit",
|
|
1555
|
+
fontSize: 11,
|
|
1556
|
+
lineHeight: "20px",
|
|
1557
|
+
cursor: "pointer",
|
|
1558
|
+
flex: "none",
|
|
1559
|
+
background: showLevelsRow
|
|
1560
|
+
? "rgba(79,140,255,0.16)"
|
|
1561
|
+
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.16))",
|
|
1562
|
+
color: showLevelsRow ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted
|
|
1563
|
+
},
|
|
1564
|
+
children: "层级"
|
|
1565
|
+
}),
|
|
1118
1566
|
// magnifier button (SVG, matches the other buttons' style)
|
|
1119
1567
|
react_jsx_runtime.jsx("button", {
|
|
1120
1568
|
onClick: function () { searchOpen ? closeSearch() : openSearch(); },
|
|
@@ -1206,6 +1654,67 @@ window.__ModuleLoader__.load({
|
|
|
1206
1654
|
})
|
|
1207
1655
|
]
|
|
1208
1656
|
}),
|
|
1657
|
+
// level filter row (H1–H6): can be collapsed away with the 层级 button
|
|
1658
|
+
// in the panel header
|
|
1659
|
+
showLevelsRow ? react_jsx_runtime.jsx("div", {
|
|
1660
|
+
style: {
|
|
1661
|
+
flex: "none",
|
|
1662
|
+
display: "flex",
|
|
1663
|
+
alignItems: "center",
|
|
1664
|
+
justifyContent: "space-between",
|
|
1665
|
+
flexWrap: "wrap",
|
|
1666
|
+
rowGap: 2,
|
|
1667
|
+
gap: 6,
|
|
1668
|
+
padding: "3px 10px 3px",
|
|
1669
|
+
borderBottom: crumb ? "none" : "1px solid " + C.panelBorder
|
|
1670
|
+
},
|
|
1671
|
+
children: [
|
|
1672
|
+
react_jsx_runtime.jsx("div", {
|
|
1673
|
+
style: { display: "flex", alignItems: "center", gap: 2, flex: "none" },
|
|
1674
|
+
children: [1, 2, 3, 4, 5, 6].map(function (lv) {
|
|
1675
|
+
var active = !!levelSet[lv];
|
|
1676
|
+
return react_jsx_runtime.jsx("button", {
|
|
1677
|
+
onClick: function () { toggleLevel(lv); },
|
|
1678
|
+
title: active ? "隐藏 H" + lv : "显示 H" + lv,
|
|
1679
|
+
style: {
|
|
1680
|
+
width: 17,
|
|
1681
|
+
height: 17,
|
|
1682
|
+
padding: 0,
|
|
1683
|
+
border: "none",
|
|
1684
|
+
borderRadius: 4,
|
|
1685
|
+
cornerShape: "round",
|
|
1686
|
+
cursor: "pointer",
|
|
1687
|
+
fontSize: 10,
|
|
1688
|
+
lineHeight: "17px",
|
|
1689
|
+
background: active
|
|
1690
|
+
? "rgba(79,140,255,0.22)"
|
|
1691
|
+
: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1692
|
+
color: active ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
1693
|
+
fontWeight: active ? 600 : 400
|
|
1694
|
+
},
|
|
1695
|
+
children: lv
|
|
1696
|
+
}, "lv" + lv);
|
|
1697
|
+
})
|
|
1698
|
+
})
|
|
1699
|
+
]
|
|
1700
|
+
}) : null,
|
|
1701
|
+
// breadcrumb: the heading path you are currently reading
|
|
1702
|
+
crumb ? react_jsx_runtime.jsx("div", {
|
|
1703
|
+
onClick: function () { jump(crumb.key, crumb.idx); },
|
|
1704
|
+
title: crumb.path,
|
|
1705
|
+
style: {
|
|
1706
|
+
flex: "none",
|
|
1707
|
+
padding: "0 10px 4px",
|
|
1708
|
+
borderBottom: "1px solid " + C.panelBorder,
|
|
1709
|
+
fontSize: 10,
|
|
1710
|
+
color: C.muted,
|
|
1711
|
+
whiteSpace: "nowrap",
|
|
1712
|
+
overflow: "hidden",
|
|
1713
|
+
textOverflow: "ellipsis",
|
|
1714
|
+
cursor: "pointer"
|
|
1715
|
+
},
|
|
1716
|
+
children: crumb.path
|
|
1717
|
+
}) : null,
|
|
1209
1718
|
// search input row: outer grid row animates 0fr<->1fr so the row
|
|
1210
1719
|
// collapses/expands to its EXACT natural height (no max-height
|
|
1211
1720
|
// overshoot stutter); the outline below moves smoothly with it
|
|
@@ -1364,7 +1873,11 @@ window.__ModuleLoader__.load({
|
|
|
1364
1873
|
className: "dqt-list",
|
|
1365
1874
|
style: {
|
|
1366
1875
|
overflowY: "auto",
|
|
1367
|
-
padding:
|
|
1876
|
+
// NO top padding: a sticky child is confined to its containing
|
|
1877
|
+
// block, so a padded scroll container would hold the pinned group
|
|
1878
|
+
// header that many pixels below the toolbar (a visible gap). Spacing
|
|
1879
|
+
// between groups comes from the per-group divider instead.
|
|
1880
|
+
padding: "0 8px 6px",
|
|
1368
1881
|
flex: "1 1 auto",
|
|
1369
1882
|
minHeight: 0,
|
|
1370
1883
|
direction: dockRight ? "ltr" : "rtl"
|
|
@@ -1373,7 +1886,9 @@ window.__ModuleLoader__.load({
|
|
|
1373
1886
|
onWheel: onListWheel,
|
|
1374
1887
|
children: react_jsx_runtime.jsx("div", {
|
|
1375
1888
|
style: { direction: "ltr" },
|
|
1376
|
-
children:
|
|
1889
|
+
children: query.trim()
|
|
1890
|
+
? renderResults(resultRows, query.trim().toLowerCase(), C, goToRow, activeResultRow)
|
|
1891
|
+
: renderGroups(shownGroups, shownTrees, jump, C, Math.max(0, groups.length - visibleCount), activeGroup)
|
|
1377
1892
|
})
|
|
1378
1893
|
}),
|
|
1379
1894
|
// resize handles (right edge: width, bottom edge: height)
|
|
@@ -1436,7 +1951,9 @@ window.__ModuleLoader__.load({
|
|
|
1436
1951
|
zIndex: Z_BASE,
|
|
1437
1952
|
top: (viewport ? viewport.top + viewport.height / 2 - 46 : "50%"),
|
|
1438
1953
|
transform: handleShown ? "translateX(0)" : (dockRight ? "translateX(16px)" : "translateX(-16px)"),
|
|
1439
|
-
|
|
1954
|
+
opacity: chatViewActive ? 1 : 0,
|
|
1955
|
+
pointerEvents: chatViewActive ? "auto" : "none",
|
|
1956
|
+
transition: "transform 0.4s " + EASE + ", opacity 0.26s ease",
|
|
1440
1957
|
...(dockRight
|
|
1441
1958
|
? { right: viewport ? viewport.right + 52 : 60 } // clear of the milestone rail
|
|
1442
1959
|
: { left: viewport ? viewport.left : 0 })
|
|
@@ -1483,6 +2000,12 @@ window.__ModuleLoader__.load({
|
|
|
1483
2000
|
}
|
|
1484
2001
|
|
|
1485
2002
|
function renderItem(n, depth, jump, C, uid) {
|
|
2003
|
+
var hasChildren = !!(n.children && n.children.length);
|
|
2004
|
+
var parts = [];
|
|
2005
|
+
parts.push(react_jsx_runtime.jsx("span", {
|
|
2006
|
+
style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" },
|
|
2007
|
+
children: n.title
|
|
2008
|
+
}, uid + "-l"));
|
|
1486
2009
|
return react_jsx_runtime.jsx(
|
|
1487
2010
|
"div",
|
|
1488
2011
|
{
|
|
@@ -1490,8 +2013,11 @@ window.__ModuleLoader__.load({
|
|
|
1490
2013
|
"data-jump-key": n.key,
|
|
1491
2014
|
"data-jump-idx": n.idx !== undefined ? String(n.idx) : "0",
|
|
1492
2015
|
style: {
|
|
2016
|
+
display: "flex",
|
|
2017
|
+
alignItems: "center",
|
|
2018
|
+
gap: 2,
|
|
1493
2019
|
padding: "2px 6px",
|
|
1494
|
-
paddingLeft:
|
|
2020
|
+
paddingLeft: (hasChildren ? 2 : 6) + (n.level - 1) * 12,
|
|
1495
2021
|
margin: "1px 0",
|
|
1496
2022
|
borderRadius: 6,
|
|
1497
2023
|
cursor: "pointer",
|
|
@@ -1501,13 +2027,12 @@ window.__ModuleLoader__.load({
|
|
|
1501
2027
|
lineHeight: "18px",
|
|
1502
2028
|
height: 22,
|
|
1503
2029
|
whiteSpace: "nowrap",
|
|
1504
|
-
overflow: "hidden"
|
|
1505
|
-
textOverflow: "ellipsis"
|
|
2030
|
+
overflow: "hidden"
|
|
1506
2031
|
},
|
|
1507
2032
|
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
1508
2033
|
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
1509
2034
|
title: n.title,
|
|
1510
|
-
children:
|
|
2035
|
+
children: parts
|
|
1511
2036
|
},
|
|
1512
2037
|
uid + "-" + n.level + "-" + (n.key || "")
|
|
1513
2038
|
);
|
|
@@ -1525,23 +2050,130 @@ window.__ModuleLoader__.load({
|
|
|
1525
2050
|
return out;
|
|
1526
2051
|
}
|
|
1527
2052
|
|
|
1528
|
-
//
|
|
2053
|
+
// ---- search results view ----
|
|
2054
|
+
// While a query is present the list shows every matched heading/message
|
|
2055
|
+
// (grouped per message, with an occurrence count) instead of the outline, so
|
|
2056
|
+
// search is "see them all, then jump" rather than stepping blindly.
|
|
2057
|
+
function hitSpans(text, q) {
|
|
2058
|
+
var parts = highlightParts(text, q);
|
|
2059
|
+
return parts.map(function (p, i) {
|
|
2060
|
+
return p.hit
|
|
2061
|
+
? react_jsx_runtime.jsx("span", {
|
|
2062
|
+
style: { background: "rgba(255,196,0,0.32)", borderRadius: 2, color: "inherit" },
|
|
2063
|
+
children: p.text
|
|
2064
|
+
}, "hp" + i)
|
|
2065
|
+
: p.text;
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function renderResultRow(r, i, q, C, onRow, isActive) {
|
|
2070
|
+
var meta = [];
|
|
2071
|
+
if (r.time) meta.push(r.time);
|
|
2072
|
+
if (r.path) meta.push(r.path);
|
|
2073
|
+
if (r.count > 1) meta.push("×" + r.count);
|
|
2074
|
+
var children = [
|
|
2075
|
+
react_jsx_runtime.jsx("div", {
|
|
2076
|
+
style: {
|
|
2077
|
+
fontSize: 12,
|
|
2078
|
+
fontWeight: r.level > 0 && r.level <= 2 ? 600 : 400,
|
|
2079
|
+
color: r.level > 0 ? C.text : C.muted,
|
|
2080
|
+
whiteSpace: "nowrap",
|
|
2081
|
+
overflow: "hidden",
|
|
2082
|
+
textOverflow: "ellipsis"
|
|
2083
|
+
},
|
|
2084
|
+
children: hitSpans(r.title, q)
|
|
2085
|
+
}, "t" + i)
|
|
2086
|
+
];
|
|
2087
|
+
if (r.snippet) {
|
|
2088
|
+
children.push(react_jsx_runtime.jsx("div", {
|
|
2089
|
+
style: {
|
|
2090
|
+
fontSize: 11,
|
|
2091
|
+
color: C.muted,
|
|
2092
|
+
marginTop: 1,
|
|
2093
|
+
whiteSpace: "nowrap",
|
|
2094
|
+
overflow: "hidden",
|
|
2095
|
+
textOverflow: "ellipsis"
|
|
2096
|
+
},
|
|
2097
|
+
children: hitSpans(r.snippet, q)
|
|
2098
|
+
}, "s" + i));
|
|
2099
|
+
}
|
|
2100
|
+
if (meta.length) {
|
|
2101
|
+
children.push(react_jsx_runtime.jsx("div", {
|
|
2102
|
+
style: { fontSize: 10, color: C.muted, opacity: 0.8, marginTop: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" },
|
|
2103
|
+
children: meta.join(" · ")
|
|
2104
|
+
}, "m" + i));
|
|
2105
|
+
}
|
|
2106
|
+
return react_jsx_runtime.jsx("div", {
|
|
2107
|
+
// clicking a row makes that hit the CURRENT one: it highlights the
|
|
2108
|
+
// keyword in the conversation and scrolls there, exactly like Enter
|
|
2109
|
+
// stepping — a row that only scrolled (no highlight) was the old bug
|
|
2110
|
+
onClick: function () { onRow(i); },
|
|
2111
|
+
"data-result-idx": i,
|
|
2112
|
+
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
2113
|
+
onMouseLeave: function (e) { e.currentTarget.style.background = isActive ? "rgba(79,140,255,0.16)" : "transparent"; },
|
|
2114
|
+
title: r.path || r.title,
|
|
2115
|
+
style: {
|
|
2116
|
+
padding: "4px 6px",
|
|
2117
|
+
margin: "1px 0",
|
|
2118
|
+
borderRadius: 6,
|
|
2119
|
+
cursor: "pointer",
|
|
2120
|
+
minWidth: 0,
|
|
2121
|
+
background: isActive ? "rgba(79,140,255,0.16)" : "transparent",
|
|
2122
|
+
transition: "background 0.15s ease"
|
|
2123
|
+
},
|
|
2124
|
+
children: children
|
|
2125
|
+
}, "res-" + i);
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
function renderResults(rows, q, C, onRow, activeRow) {
|
|
2129
|
+
if (!rows || rows.length === 0) {
|
|
2130
|
+
return react_jsx_runtime.jsx("div", {
|
|
2131
|
+
style: { padding: "12px 8px", fontSize: 12, color: C.muted, textAlign: "center" },
|
|
2132
|
+
children: "没有匹配"
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
var out = [];
|
|
2136
|
+
for (var i = 0; i < rows.length; i++) {
|
|
2137
|
+
out.push(renderResultRow(rows[i], i, q, C, onRow, i === activeRow));
|
|
2138
|
+
}
|
|
2139
|
+
return out;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
// a group's header (the time row): clicking it jumps to the TOP OF THE
|
|
2143
|
+
// MODEL'S REPLY for that turn — the first assistant step — which also makes
|
|
2144
|
+
// heading-less turns jumpable, since they only have this row.
|
|
1529
2145
|
// Implemented as a function (not an inline closure in a loop) so each
|
|
1530
2146
|
// header captures its own (g, gi) — the var-in-loop closure bug would
|
|
1531
2147
|
// otherwise make every header jump to the last group.
|
|
1532
2148
|
function renderGroupHeader(g, gi, jump, C) {
|
|
2149
|
+
var replyKey = (g.msgs && g.msgs.length > 0) ? g.msgs[0].key : "";
|
|
2150
|
+
var targetKey = replyKey || g.userKey;
|
|
1533
2151
|
var jumpToTurn = function (e) {
|
|
1534
2152
|
e.stopPropagation();
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
if (
|
|
1538
|
-
|
|
2153
|
+
// prefer the model's reply; fall back to the user message, then to the
|
|
2154
|
+
// turn's first heading, so the row always does something useful
|
|
2155
|
+
if (replyKey && findRow(replyKey)) { jump(replyKey); return; }
|
|
2156
|
+
if (g.userKey && findRow(g.userKey)) { jump(g.userKey); return; }
|
|
2157
|
+
if (g.headings.length > 0) jump(g.headings[0].key, g.headings[0].idx);
|
|
1539
2158
|
};
|
|
1540
2159
|
return react_jsx_runtime.jsx("div", {
|
|
1541
|
-
style: {
|
|
2160
|
+
style: {
|
|
2161
|
+
padding: "1px 4px 2px",
|
|
2162
|
+
height: 18,
|
|
2163
|
+
display: "flex",
|
|
2164
|
+
alignItems: "center",
|
|
2165
|
+
minWidth: 0,
|
|
2166
|
+
// sticky: while you scroll the outline, the header of the group you are
|
|
2167
|
+
// inside stays pinned at the top (background must be opaque to cover
|
|
2168
|
+
// the rows scrolling underneath).
|
|
2169
|
+
position: "sticky",
|
|
2170
|
+
top: 0,
|
|
2171
|
+
zIndex: 2,
|
|
2172
|
+
background: C.panelBg
|
|
2173
|
+
},
|
|
1542
2174
|
children: react_jsx_runtime.jsx("span", {
|
|
1543
2175
|
onClick: jumpToTurn,
|
|
1544
|
-
title:
|
|
2176
|
+
title: replyKey ? "跳转到该回合的模型回答开头" : "跳转到该回合开头",
|
|
1545
2177
|
onMouseEnter: function (e) { e.currentTarget.style.background = C.hover; },
|
|
1546
2178
|
onMouseLeave: function (e) { e.currentTarget.style.background = "transparent"; },
|
|
1547
2179
|
style: {
|
|
@@ -1611,7 +2243,7 @@ window.__ModuleLoader__.load({
|
|
|
1611
2243
|
ctx.effect(function () {
|
|
1612
2244
|
return ctx.locale.register("dsh-quick-toc", { zh: zh, en: en });
|
|
1613
2245
|
}, "dsh-quick-toc: dictionaries");
|
|
1614
|
-
// DSH 0.1.
|
|
2246
|
+
// Recent DSH (0.1.5-rc.1): session-scoped hooks (useChat/useSession/sessionId) only arrive
|
|
1615
2247
|
// inside a declared session slot. Register the panel into the session-scoped
|
|
1616
2248
|
// conversation.input.overlay (list/additive) so it receives useChat; the panel
|
|
1617
2249
|
// itself renders a fixed, frame-floating dock, so the overlay seat is just the
|