dsh-quick-toc 0.2.0-beta → 0.2.2
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/lib/client.js +239 -25
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -135,6 +135,18 @@ window.__ModuleLoader__.load({
|
|
|
135
135
|
return null;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// count every occurrence of q (case-insensitive) inside text
|
|
139
|
+
function countOccurrences(text, q) {
|
|
140
|
+
var lower = text.toLowerCase();
|
|
141
|
+
var count = 0;
|
|
142
|
+
var idx = 0;
|
|
143
|
+
while ((idx = lower.indexOf(q, idx)) !== -1) {
|
|
144
|
+
count++;
|
|
145
|
+
idx += q.length;
|
|
146
|
+
}
|
|
147
|
+
return count;
|
|
148
|
+
}
|
|
149
|
+
|
|
138
150
|
// find the conversation's "load older messages" button (scoped to the
|
|
139
151
|
// conversation scrollport so panel buttons are never matched)
|
|
140
152
|
function findLoadOlderButton() {
|
|
@@ -148,6 +160,58 @@ window.__ModuleLoader__.load({
|
|
|
148
160
|
return null;
|
|
149
161
|
}
|
|
150
162
|
|
|
163
|
+
// ---- search keyword highlight in the conversation ----
|
|
164
|
+
var highlightSpans = [];
|
|
165
|
+
|
|
166
|
+
function clearHighlights() {
|
|
167
|
+
for (var i = 0; i < highlightSpans.length; i++) {
|
|
168
|
+
var s = highlightSpans[i];
|
|
169
|
+
if (s.parentNode) {
|
|
170
|
+
var t = document.createTextNode(s.textContent);
|
|
171
|
+
s.parentNode.replaceChild(t, s);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
highlightSpans = [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// wrap every occurrence of q (case-insensitive) inside row's text nodes;
|
|
178
|
+
// the occurrence at `currentOcc` gets a distinct "current" highlight
|
|
179
|
+
function highlightRow(row, q, currentOcc) {
|
|
180
|
+
if (!row || !q) return;
|
|
181
|
+
var walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT, null);
|
|
182
|
+
var textNodes = [];
|
|
183
|
+
while (walker.nextNode()) textNodes.push(walker.currentNode);
|
|
184
|
+
var occ = 0;
|
|
185
|
+
for (var i = 0; i < textNodes.length; i++) {
|
|
186
|
+
var node = textNodes[i];
|
|
187
|
+
var text = node.nodeValue;
|
|
188
|
+
if (!text) continue;
|
|
189
|
+
var lower = text.toLowerCase();
|
|
190
|
+
var idx = lower.indexOf(q);
|
|
191
|
+
if (idx < 0) continue;
|
|
192
|
+
var frag = document.createDocumentFragment();
|
|
193
|
+
var before = text.slice(0, idx);
|
|
194
|
+
if (before) frag.appendChild(document.createTextNode(before));
|
|
195
|
+
var mark = document.createElement("span");
|
|
196
|
+
if (occ === currentOcc) {
|
|
197
|
+
mark.className = "dqt-current";
|
|
198
|
+
mark.style.background = "rgba(79,140,255,0.55)";
|
|
199
|
+
mark.style.boxShadow = "0 0 0 1px rgba(79,140,255,0.85)";
|
|
200
|
+
} else {
|
|
201
|
+
mark.style.background = "rgba(79,140,255,0.32)";
|
|
202
|
+
}
|
|
203
|
+
mark.style.borderRadius = "2px";
|
|
204
|
+
mark.style.color = "inherit";
|
|
205
|
+
mark.textContent = text.slice(idx, idx + q.length);
|
|
206
|
+
frag.appendChild(mark);
|
|
207
|
+
highlightSpans.push(mark);
|
|
208
|
+
occ++;
|
|
209
|
+
var after = text.slice(idx + q.length);
|
|
210
|
+
if (after) frag.appendChild(document.createTextNode(after));
|
|
211
|
+
node.parentNode.replaceChild(frag, node);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
151
215
|
// ---------- theme colors: DSH CSS variables (adapt to light/dark) ----------
|
|
152
216
|
var C = {
|
|
153
217
|
panelBg: "var(--dsw-alias-bg-base, rgba(24, 28, 36, 0.96))",
|
|
@@ -343,6 +407,22 @@ window.__ModuleLoader__.load({
|
|
|
343
407
|
var searching = _s17[0];
|
|
344
408
|
var setSearching = _s17[1];
|
|
345
409
|
var searchTimerRef = react.useRef(null);
|
|
410
|
+
// search scope: "title" (default) or "full" (user messages + AI replies)
|
|
411
|
+
var _s18 = react.useState("title");
|
|
412
|
+
var searchScope = _s18[0];
|
|
413
|
+
var setSearchScope = _s18[1];
|
|
414
|
+
// previous scope kept briefly so the old label cross-fades with the new one
|
|
415
|
+
var _s19 = react.useState(null);
|
|
416
|
+
var prevScope = _s19[0];
|
|
417
|
+
var setPrevScope = _s19[1];
|
|
418
|
+
var scopeTimerRef = react.useRef(null);
|
|
419
|
+
var toggleScope = function () {
|
|
420
|
+
setPrevScope(searchScope);
|
|
421
|
+
setSearchScope(searchScope === "title" ? "full" : "title");
|
|
422
|
+
setMatchIdx(0);
|
|
423
|
+
if (scopeTimerRef.current) clearTimeout(scopeTimerRef.current);
|
|
424
|
+
scopeTimerRef.current = setTimeout(function () { setPrevScope(null); }, 300);
|
|
425
|
+
};
|
|
346
426
|
var openSearch = function () {
|
|
347
427
|
setSearchOpen(true);
|
|
348
428
|
setSearchAnim("enter");
|
|
@@ -350,6 +430,7 @@ window.__ModuleLoader__.load({
|
|
|
350
430
|
var closeSearch = function () {
|
|
351
431
|
setSearchAnim("out");
|
|
352
432
|
setQuery("");
|
|
433
|
+
clearHighlights();
|
|
353
434
|
setTimeout(function () {
|
|
354
435
|
setSearchOpen(false);
|
|
355
436
|
setSearchAnim("hidden");
|
|
@@ -369,6 +450,7 @@ window.__ModuleLoader__.load({
|
|
|
369
450
|
setQuery(v);
|
|
370
451
|
setMatchIdx(0);
|
|
371
452
|
setSearching(true);
|
|
453
|
+
clearHighlights();
|
|
372
454
|
if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
|
|
373
455
|
searchTimerRef.current = setTimeout(function () { setSearching(false); }, 220);
|
|
374
456
|
};
|
|
@@ -545,9 +627,10 @@ window.__ModuleLoader__.load({
|
|
|
545
627
|
var turnTimes = {};
|
|
546
628
|
var turnUserKey = {};
|
|
547
629
|
var turnUserText = {};
|
|
630
|
+
var turnUserFull = {};
|
|
548
631
|
if (!order || !nodes) return result;
|
|
549
632
|
// first pass: per-turn time — the LAST message of the turn wins (end time);
|
|
550
|
-
// also remember each turn's user message key + first-line preview
|
|
633
|
+
// also remember each turn's user message key + first-line preview + full text
|
|
551
634
|
for (var i = 0; i < order.length; i++) {
|
|
552
635
|
var k0 = order[i];
|
|
553
636
|
var n0 = nodes.get(k0);
|
|
@@ -558,11 +641,12 @@ window.__ModuleLoader__.load({
|
|
|
558
641
|
if (n0.kind === "user" && turnUserKey[tid0] === undefined) {
|
|
559
642
|
turnUserKey[tid0] = k0;
|
|
560
643
|
turnUserText[tid0] = previewText(extractUserText(n0), 30);
|
|
644
|
+
turnUserFull[tid0] = extractUserText(n0);
|
|
561
645
|
}
|
|
562
646
|
var t0 = getNodeTime(n0);
|
|
563
647
|
if (t0) turnTimes[tid0] = t0;
|
|
564
648
|
}
|
|
565
|
-
// second pass: group assistant headings by turn
|
|
649
|
+
// second pass: group assistant headings + full reply texts by turn
|
|
566
650
|
for (var j = 0; j < order.length; j++) {
|
|
567
651
|
var key = order[j];
|
|
568
652
|
var node = nodes.get(key);
|
|
@@ -576,11 +660,15 @@ window.__ModuleLoader__.load({
|
|
|
576
660
|
time: turnId !== null ? (turnTimes[turnId] || "") : "",
|
|
577
661
|
userKey: turnId !== null ? (turnUserKey[turnId] || "") : "",
|
|
578
662
|
userText: turnId !== null ? (turnUserText[turnId] || "") : "",
|
|
663
|
+
userFull: turnId !== null ? (turnUserFull[turnId] || "") : "",
|
|
664
|
+
msgs: [],
|
|
579
665
|
headings: []
|
|
580
666
|
};
|
|
581
667
|
result.push(current);
|
|
582
668
|
}
|
|
583
|
-
var
|
|
669
|
+
var text = extractReplyText(node);
|
|
670
|
+
current.msgs.push({ key: key, text: text });
|
|
671
|
+
var parsed = parseHeadings(text);
|
|
584
672
|
var hIdx = 0;
|
|
585
673
|
for (var k = 0; k < parsed.length; k++) {
|
|
586
674
|
current.headings.push({ level: parsed[k].level, title: parsed[k].title, key: key, idx: hIdx });
|
|
@@ -661,7 +749,11 @@ window.__ModuleLoader__.load({
|
|
|
661
749
|
return function () { sp.removeEventListener("scroll", update); };
|
|
662
750
|
}, [keyToGroup]);
|
|
663
751
|
|
|
664
|
-
// ---- search matches
|
|
752
|
+
// ---- search matches ----
|
|
753
|
+
// "title" scope: heading titles only; "full" scope: also the user
|
|
754
|
+
// message and every AI reply text of each turn.
|
|
755
|
+
// Every occurrence counts (multiple hits inside one message = multiple
|
|
756
|
+
// matches), so the n/N counter reflects the real total.
|
|
665
757
|
var matches = react.useMemo(function () {
|
|
666
758
|
var q = query.trim().toLowerCase();
|
|
667
759
|
if (!q) return [];
|
|
@@ -670,13 +762,30 @@ window.__ModuleLoader__.load({
|
|
|
670
762
|
var g = groups[gi];
|
|
671
763
|
for (var j = 0; j < g.headings.length; j++) {
|
|
672
764
|
var h = g.headings[j];
|
|
673
|
-
|
|
765
|
+
var n = countOccurrences(h.title, q);
|
|
766
|
+
for (var c = 0; c < n; c++) {
|
|
674
767
|
out.push({ gi: gi, title: h.title, key: h.key, idx: h.idx });
|
|
675
768
|
}
|
|
676
769
|
}
|
|
770
|
+
if (searchScope === "full") {
|
|
771
|
+
if (g.userKey && g.userFull) {
|
|
772
|
+
var nu = countOccurrences(g.userFull, q);
|
|
773
|
+
for (var cu = 0; cu < nu; cu++) {
|
|
774
|
+
out.push({ gi: gi, title: previewText(g.userFull, 30), key: g.userKey, idx: undefined });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
for (var m = 0; m < g.msgs.length; m++) {
|
|
778
|
+
var msg = g.msgs[m];
|
|
779
|
+
if (!msg.text) continue;
|
|
780
|
+
var nm = countOccurrences(msg.text, q);
|
|
781
|
+
for (var cm = 0; cm < nm; cm++) {
|
|
782
|
+
out.push({ gi: gi, title: previewText(msg.text, 30), key: msg.key, idx: undefined });
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
677
786
|
}
|
|
678
787
|
return out;
|
|
679
|
-
}, [groups, query]);
|
|
788
|
+
}, [groups, query, searchScope]);
|
|
680
789
|
|
|
681
790
|
// ---- nothing to show without headings ----
|
|
682
791
|
if (groups.length === 0) return null;
|
|
@@ -754,13 +863,38 @@ window.__ModuleLoader__.load({
|
|
|
754
863
|
sp.scrollTo({ top: t, behavior: "smooth" });
|
|
755
864
|
};
|
|
756
865
|
|
|
757
|
-
// jump to the n-th match, cycling; also reveal the group in the outline
|
|
866
|
+
// jump to the n-th match, cycling; also reveal the group in the outline.
|
|
867
|
+
// ONE smooth scroll straight to the current occurrence (no competing
|
|
868
|
+
// scrolls): highlight first, then position the mark at the upper-middle
|
|
758
869
|
var goToMatch = function (n) {
|
|
759
870
|
if (matches.length === 0) return;
|
|
760
871
|
var i = ((n % matches.length) + matches.length) % matches.length;
|
|
761
872
|
setMatchIdx(i);
|
|
762
873
|
var m = matches[i];
|
|
763
|
-
|
|
874
|
+
var q = query.trim().toLowerCase();
|
|
875
|
+
clearHighlights();
|
|
876
|
+
var r = findRowStrict(m.key);
|
|
877
|
+
if (r && q) {
|
|
878
|
+
// occurrence index within the target message (consecutive in matches)
|
|
879
|
+
var occ = 0;
|
|
880
|
+
for (var p = i - 1; p >= 0 && matches[p].key === m.key; p--) occ++;
|
|
881
|
+
highlightRow(r, q, occ);
|
|
882
|
+
var markEl = r.querySelector(".dqt-current");
|
|
883
|
+
var sp = r.closest ? r.closest("[data-conversation-scroll]") : null;
|
|
884
|
+
if (sp && markEl) {
|
|
885
|
+
var target = markEl.getBoundingClientRect().top - sp.getBoundingClientRect().top + sp.scrollTop - sp.clientHeight * 0.35;
|
|
886
|
+
// stick-to-bottom nudge (same as jump): break the 25px floor hold
|
|
887
|
+
var floor = Math.max(0, sp.scrollHeight - sp.clientHeight);
|
|
888
|
+
if (floor - sp.scrollTop <= 25 && Math.abs(target - sp.scrollTop) > 60) {
|
|
889
|
+
sp.scrollTop = Math.max(0, floor - 26);
|
|
890
|
+
}
|
|
891
|
+
sp.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
|
|
892
|
+
} else {
|
|
893
|
+
jump(m.key, m.idx);
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
jump(m.key, m.idx);
|
|
897
|
+
}
|
|
764
898
|
setVisibleCount(function (prev) {
|
|
765
899
|
return Math.max(prev, Math.min(groups.length, groups.length - m.gi));
|
|
766
900
|
});
|
|
@@ -1043,26 +1177,69 @@ window.__ModuleLoader__.load({
|
|
|
1043
1177
|
transition: "opacity 0.2s ease, transform 0.2s ease"
|
|
1044
1178
|
},
|
|
1045
1179
|
children: [
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
onChange: function (e) { onQueryChange(e.target.value); },
|
|
1050
|
-
onKeyDown: function (e) {
|
|
1051
|
-
if (e.key === "Enter") { goToMatch(matchIdx + 1); e.preventDefault(); }
|
|
1052
|
-
if (e.key === "Escape") { closeSearch(); }
|
|
1053
|
-
},
|
|
1054
|
-
placeholder: "搜索标题,回车定位…",
|
|
1180
|
+
// input with a custom animated placeholder: the box stays static,
|
|
1181
|
+
// only the placeholder text fades when the scope switches
|
|
1182
|
+
react_jsx_runtime.jsx("div", {
|
|
1055
1183
|
style: {
|
|
1184
|
+
position: "relative",
|
|
1056
1185
|
flex: 1,
|
|
1057
1186
|
minWidth: 0,
|
|
1187
|
+
display: "flex",
|
|
1188
|
+
alignItems: "center",
|
|
1058
1189
|
background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1190
|
+
borderRadius: 999,
|
|
1191
|
+
padding: "6px 12px"
|
|
1192
|
+
},
|
|
1193
|
+
children: [
|
|
1194
|
+
react_jsx_runtime.jsx("input", {
|
|
1195
|
+
ref: searchRef,
|
|
1196
|
+
value: query,
|
|
1197
|
+
onChange: function (e) { onQueryChange(e.target.value); },
|
|
1198
|
+
onKeyDown: function (e) {
|
|
1199
|
+
if (e.key === "Enter") { goToMatch(matchIdx + 1); e.preventDefault(); }
|
|
1200
|
+
if (e.key === "Escape") { closeSearch(); }
|
|
1201
|
+
},
|
|
1202
|
+
style: {
|
|
1203
|
+
flex: 1,
|
|
1204
|
+
minWidth: 0,
|
|
1205
|
+
width: "100%",
|
|
1206
|
+
background: "transparent",
|
|
1207
|
+
border: "none",
|
|
1208
|
+
outline: "none",
|
|
1209
|
+
fontSize: 13,
|
|
1210
|
+
color: C.text,
|
|
1211
|
+
padding: 0
|
|
1212
|
+
}
|
|
1213
|
+
}),
|
|
1214
|
+
(!query) ? react_jsx_runtime.jsx("span", {
|
|
1215
|
+
style: {
|
|
1216
|
+
position: "absolute",
|
|
1217
|
+
left: 12,
|
|
1218
|
+
pointerEvents: "none",
|
|
1219
|
+
fontSize: 13,
|
|
1220
|
+
color: "var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))"
|
|
1221
|
+
},
|
|
1222
|
+
children: [
|
|
1223
|
+
"搜索",
|
|
1224
|
+
react_jsx_runtime.jsx("span", {
|
|
1225
|
+
style: { position: "relative", display: "inline-block" },
|
|
1226
|
+
children: [
|
|
1227
|
+
react_jsx_runtime.jsx("span", {
|
|
1228
|
+
key: searchScope,
|
|
1229
|
+
style: { display: "inline-block", animation: "dqt-fade-in 0.25s linear" },
|
|
1230
|
+
children: searchScope === "title" ? "标题" : "全文"
|
|
1231
|
+
}),
|
|
1232
|
+
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1233
|
+
key: "old-" + prevScope,
|
|
1234
|
+
style: { position: "absolute", left: 0, top: 0, opacity: 0, animation: "dqt-fade-out 0.25s linear forwards" },
|
|
1235
|
+
children: prevScope === "title" ? "标题" : "全文"
|
|
1236
|
+
}) : null
|
|
1237
|
+
]
|
|
1238
|
+
}),
|
|
1239
|
+
",回车定位…"
|
|
1240
|
+
]
|
|
1241
|
+
}) : null,
|
|
1242
|
+
]
|
|
1066
1243
|
}),
|
|
1067
1244
|
searching ? react_jsx_runtime.jsx("span", {
|
|
1068
1245
|
style: { width: 13, height: 13, flex: "none", display: "flex", alignItems: "center", justifyContent: "center" },
|
|
@@ -1083,6 +1260,43 @@ window.__ModuleLoader__.load({
|
|
|
1083
1260
|
}) : react_jsx_runtime.jsx("span", {
|
|
1084
1261
|
style: { fontSize: 11, color: C.muted, flex: "none", minWidth: 30, textAlign: "center" },
|
|
1085
1262
|
children: matches.length > 0 ? ((matchIdx % matches.length) + 1) + "/" + matches.length : ""
|
|
1263
|
+
}),
|
|
1264
|
+
// scope toggle: 标题 <-> 全文 (round pill; background fades,
|
|
1265
|
+
// label text cross-fades old->new)
|
|
1266
|
+
react_jsx_runtime.jsx("button", {
|
|
1267
|
+
onClick: toggleScope,
|
|
1268
|
+
title: searchScope === "title" ? "当前:仅搜索标题。点击切换为全文搜索" : "当前:全文搜索。点击切换为仅标题",
|
|
1269
|
+
style: {
|
|
1270
|
+
flex: "none",
|
|
1271
|
+
width: 34,
|
|
1272
|
+
height: 34,
|
|
1273
|
+
display: "flex",
|
|
1274
|
+
alignItems: "center",
|
|
1275
|
+
justifyContent: "center",
|
|
1276
|
+
borderRadius: "50%",
|
|
1277
|
+
fontSize: 11,
|
|
1278
|
+
color: searchScope === "full" ? "var(--dsw-alias-brand-primary, #4f8cff)" : C.muted,
|
|
1279
|
+
background: searchScope === "full" ? "rgba(79,140,255,0.18)" : "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,0.14))",
|
|
1280
|
+
border: "none",
|
|
1281
|
+
cursor: "pointer",
|
|
1282
|
+
whiteSpace: "nowrap",
|
|
1283
|
+
transition: "background 0.25s ease"
|
|
1284
|
+
},
|
|
1285
|
+
children: react_jsx_runtime.jsx("span", {
|
|
1286
|
+
style: { position: "relative", display: "inline-flex", alignItems: "center", justifyContent: "center" },
|
|
1287
|
+
children: [
|
|
1288
|
+
react_jsx_runtime.jsx("span", {
|
|
1289
|
+
key: searchScope,
|
|
1290
|
+
style: { animation: "dqt-fade-in 0.25s linear", display: "block" },
|
|
1291
|
+
children: searchScope === "title" ? "标题" : "全文"
|
|
1292
|
+
}),
|
|
1293
|
+
(prevScope && prevScope !== searchScope) ? react_jsx_runtime.jsx("span", {
|
|
1294
|
+
key: "old-" + prevScope,
|
|
1295
|
+
style: { position: "absolute", opacity: 0, animation: "dqt-fade-out 0.25s linear forwards", display: "block" },
|
|
1296
|
+
children: prevScope === "title" ? "标题" : "全文"
|
|
1297
|
+
}) : null
|
|
1298
|
+
]
|
|
1299
|
+
})
|
|
1086
1300
|
})
|
|
1087
1301
|
]
|
|
1088
1302
|
})
|
|
@@ -1373,7 +1587,7 @@ window.__ModuleLoader__.load({
|
|
|
1373
1587
|
if (document.getElementById("dsh-quick-toc-css")) return;
|
|
1374
1588
|
var s = document.createElement("style");
|
|
1375
1589
|
s.id = "dsh-quick-toc-css";
|
|
1376
|
-
s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}";
|
|
1590
|
+
s.textContent = "@keyframes dqt-spin{to{transform:rotate(360deg)}}@keyframes dqt-fade-in{from{opacity:0}to{opacity:1}}@keyframes dqt-fade-out{from{opacity:1}to{opacity:0}}";
|
|
1377
1591
|
document.head.appendChild(s);
|
|
1378
1592
|
})();
|
|
1379
1593
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quick-toc",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Quick conversation TOC for DeepSeek Harness: markdown heading outline grouped by turn, with auto-follow highlight and smooth jump navigation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|