dsh-session-find 0.0.0-stage → 1.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.md +29 -0
- package/LICENSE +21 -0
- package/README.en.md +72 -0
- package/README.md +102 -2
- package/assets/options.png +0 -0
- package/assets/overview.png +0 -0
- package/client.js +1808 -0
- package/cordis.patch.yml +3 -0
- package/index.js +2 -0
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +56 -4
package/client.js
ADDED
|
@@ -0,0 +1,1808 @@
|
|
|
1
|
+
// dsh-session-find - Ctrl+F full-text find inside the current conversation.
|
|
2
|
+
// Hand-written in the Client module-loader format. No Harness Client package is imported;
|
|
3
|
+
// styling uses --dsw-alias-* theme tokens only, with literal fallbacks.
|
|
4
|
+
window.__ModuleLoader__.load({
|
|
5
|
+
id: 'dsh-session-find',
|
|
6
|
+
factory(require) {
|
|
7
|
+
var React = require('react');
|
|
8
|
+
var h = React.createElement;
|
|
9
|
+
var NS = 'dsh-session-find';
|
|
10
|
+
var MAX_RESULTS = 300;
|
|
11
|
+
var SEARCH_DEBOUNCE_MS = 140;
|
|
12
|
+
var LF = String.fromCharCode(10);
|
|
13
|
+
var QUOTE = String.fromCharCode(34);
|
|
14
|
+
var BACKSLASH = String.fromCharCode(92);
|
|
15
|
+
|
|
16
|
+
var ZH = {
|
|
17
|
+
open: '查找',
|
|
18
|
+
placeholder: '在对话中查找…',
|
|
19
|
+
hint: 'Enter 下一个 · Shift+Enter 上一个 · ↑↓ 历史 · Esc 关闭',
|
|
20
|
+
count: '{n} 处匹配',
|
|
21
|
+
limited: '(仅列前 {n} 条)',
|
|
22
|
+
none: '没有匹配',
|
|
23
|
+
empty: '输入关键词开始查找',
|
|
24
|
+
prev: '上一个',
|
|
25
|
+
next: '下一个',
|
|
26
|
+
close: '关闭',
|
|
27
|
+
jumpFailed: '没能定位到该条',
|
|
28
|
+
jumpApproximate: '已跳到所在回合(该条在折叠步骤里)',
|
|
29
|
+
jumpHidden: '已定位到该条 · 命中文字在思考/工具详情里,当前视图只显示摘要',
|
|
30
|
+
sameText: '同文 ×{n}',
|
|
31
|
+
payloadMore: '另有 {n} 条在源码/工具内容里 · 显示',
|
|
32
|
+
payloadHide: '隐藏 {n} 条源码/工具内容命中',
|
|
33
|
+
payloadOnly: ' · 命中都在源码/工具内容里',
|
|
34
|
+
merged: ' · 已合并 {n} 条同文',
|
|
35
|
+
turn: '第 {n} 轮',
|
|
36
|
+
hits: '命中 {n} 处',
|
|
37
|
+
filterAll: '全部',
|
|
38
|
+
scopeProse: '只在正文里搜',
|
|
39
|
+
scopeAll: '搜全部内容',
|
|
40
|
+
scopeHint: '不含思考 / 工具 / 系统',
|
|
41
|
+
scopeExcluded: '已排除 {n} 处',
|
|
42
|
+
scopeMiss: '正文无匹配,另有 {n} 处',
|
|
43
|
+
scopeSwitchAll: '切到搜全部内容',
|
|
44
|
+
groupTurn: '第 {n} 轮',
|
|
45
|
+
payloadMoreProse: '另有 {n} 条在源码形态里 · 显示',
|
|
46
|
+
payloadHideProse: '隐藏 {n} 条源码形态命中',
|
|
47
|
+
payloadOnlyProse: ' · 命中都在源码形态里',
|
|
48
|
+
roleUser: '你',
|
|
49
|
+
roleAssistant: '助手',
|
|
50
|
+
roleReasoning: '思考',
|
|
51
|
+
roleToolCall: '工具调用',
|
|
52
|
+
roleToolResult: '工具输出',
|
|
53
|
+
roleSystem: '系统',
|
|
54
|
+
roleOther: '其它',
|
|
55
|
+
optRegex: '正则',
|
|
56
|
+
optRegexHint: '按正则表达式匹配,例如 m0.4、^第、\\d+' ,
|
|
57
|
+
optMatchCase: '区分大小写',
|
|
58
|
+
optMatchCaseHint: 'Aa:大小写敏感',
|
|
59
|
+
optWholeWord: '全词',
|
|
60
|
+
optWholeWordHint: 'ab:只匹配完整的英文/数字词',
|
|
61
|
+
optHint: '.* 正则 · Aa 区分大小写 · ab 全词',
|
|
62
|
+
regexInvalid: '正则表达式无效',
|
|
63
|
+
regexHintBad: '检查括号与转义',
|
|
64
|
+
exportCopy: '复制结果为 Markdown',
|
|
65
|
+
exportDownload: '导出 Markdown 文件',
|
|
66
|
+
exportPrompt: '已生成 Markdown,请在保存对话框中确认',
|
|
67
|
+
exportTitle: '检索:{q}',
|
|
68
|
+
exportMeta: '{n} 处匹配 · 列出 {rows} 条',
|
|
69
|
+
exportCopied: '已复制 Markdown 到剪贴板',
|
|
70
|
+
exportFailed: '复制/导出失败,请重试',
|
|
71
|
+
user: '你',
|
|
72
|
+
assistant: 'AI',
|
|
73
|
+
tool: '工具',
|
|
74
|
+
other: '其它'
|
|
75
|
+
};
|
|
76
|
+
var EN = {
|
|
77
|
+
open: 'Find',
|
|
78
|
+
placeholder: 'Find in conversation…',
|
|
79
|
+
hint: 'Enter next · Shift+Enter previous · ↑↓ history · Esc close',
|
|
80
|
+
count: '{n} matches',
|
|
81
|
+
limited: '(first {n} only)',
|
|
82
|
+
none: 'No matches',
|
|
83
|
+
empty: 'Type to search this conversation',
|
|
84
|
+
prev: 'Previous',
|
|
85
|
+
next: 'Next',
|
|
86
|
+
close: 'Close',
|
|
87
|
+
jumpFailed: 'Could not locate that message',
|
|
88
|
+
jumpApproximate: 'Landed on the turn (the message sits in a collapsed step)',
|
|
89
|
+
jumpHidden: 'Landed · the match lives in reasoning/tool details, which this view summarises',
|
|
90
|
+
sameText: 'same text ×{n}',
|
|
91
|
+
payloadMore: '{n} more in source/tool payloads · show',
|
|
92
|
+
payloadHide: 'hide {n} source/tool hits',
|
|
93
|
+
payloadOnly: ' · all hits are in source/tool payloads',
|
|
94
|
+
merged: ' · merged {n} duplicates',
|
|
95
|
+
turn: 'Turn {n}',
|
|
96
|
+
hits: '{n} hits',
|
|
97
|
+
filterAll: 'All',
|
|
98
|
+
scopeProse: 'Message text only',
|
|
99
|
+
scopeAll: 'Search everything',
|
|
100
|
+
scopeHint: 'skips reasoning, tools, system',
|
|
101
|
+
scopeExcluded: '{n} elsewhere',
|
|
102
|
+
scopeMiss: 'no message hits; {n} elsewhere',
|
|
103
|
+
scopeSwitchAll: 'search everything',
|
|
104
|
+
groupTurn: 'Turn {n}',
|
|
105
|
+
payloadMoreProse: '{n} more in source form · show',
|
|
106
|
+
payloadHideProse: 'hide {n} source-form hits',
|
|
107
|
+
payloadOnlyProse: ' · all hits are in source form',
|
|
108
|
+
roleUser: 'You',
|
|
109
|
+
roleAssistant: 'Assistant',
|
|
110
|
+
roleReasoning: 'Reasoning',
|
|
111
|
+
roleToolCall: 'Tool call',
|
|
112
|
+
roleToolResult: 'Tool output',
|
|
113
|
+
roleSystem: 'System',
|
|
114
|
+
roleOther: 'Other',
|
|
115
|
+
optRegex: 'Regex',
|
|
116
|
+
optRegexHint: 'Match as a regular expression, e.g. m0.4',
|
|
117
|
+
optMatchCase: 'Match case',
|
|
118
|
+
optMatchCaseHint: 'Aa: case-sensitive match',
|
|
119
|
+
optWholeWord: 'Whole word',
|
|
120
|
+
optWholeWordHint: 'ab: whole ASCII words only',
|
|
121
|
+
optHint: '.* regex · Aa case · ab whole word',
|
|
122
|
+
regexInvalid: 'Invalid regular expression',
|
|
123
|
+
regexHintBad: 'check brackets and escapes',
|
|
124
|
+
exportCopy: 'Copy results as Markdown',
|
|
125
|
+
exportDownload: 'Download Markdown',
|
|
126
|
+
exportPrompt: 'Markdown ready — confirm it in the save dialog',
|
|
127
|
+
exportTitle: 'Search: {q}',
|
|
128
|
+
exportMeta: '{n} matches · {rows} rows listed',
|
|
129
|
+
exportCopied: 'Markdown copied',
|
|
130
|
+
exportFailed: 'Copy/download failed, please retry',
|
|
131
|
+
user: 'You',
|
|
132
|
+
assistant: 'Assistant',
|
|
133
|
+
tool: 'Tool',
|
|
134
|
+
other: 'Other'
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Object keys whose values are identifiers or enums, not readable content. */
|
|
138
|
+
var SKIP_KEYS = { key: 1, id: 1, messageId: 1, rpcId: 1, seq: 1, time: 1, kind: 1, type: 1, status: 1, role: 1, provider: 1, model: 1, requestId: 1, sessionId: 1, createdAt: 1, toolCallId: 1, callId: 1, source: 1, turn: 1, index: 1, form: 1, placement: 1 };
|
|
139
|
+
var textCache = new WeakMap();
|
|
140
|
+
var partsCache = new WeakMap();
|
|
141
|
+
var roleHitCache = new WeakMap();
|
|
142
|
+
/** Length of the readable prefix of each node text, so a hit can be flagged as
|
|
143
|
+
* "the form the page actually renders" rather than the JSON/markdown source. */
|
|
144
|
+
var projectionCache = new WeakMap();
|
|
145
|
+
/** Message text only: the visible prose, without reasoning, tool payloads or system nodes. */
|
|
146
|
+
var proseCache = new WeakMap();
|
|
147
|
+
|
|
148
|
+
/** Where a hit came from: the user, the assistant's prose, the model's hidden
|
|
149
|
+
* reasoning, a tool call (arguments) or a tool result (output), or system plumbing. */
|
|
150
|
+
var ROLE_BY_BLOCK = { reasoning: 'reasoning', 'tool-call': 'toolCall', 'tool-result': 'toolResult', text: 'assistant' };
|
|
151
|
+
var ROLE_BY_KEY = { reasoning: 'reasoning', toolCalls: 'toolCall', toolCall: 'toolCall', toolResults: 'toolResult', toolResult: 'toolResult', args: 'toolCall', arguments: 'toolCall' };
|
|
152
|
+
var ROLE_BY_KIND = {
|
|
153
|
+
user: 'user', steering: 'user', assistant: 'assistant', 'assistant-step': 'assistant',
|
|
154
|
+
'system-prompt': 'system', context: 'system', 'turn-trigger': 'system', command: 'system',
|
|
155
|
+
compaction: 'system', 'manual-compaction': 'system', 'model-retry': 'system',
|
|
156
|
+
'turn-error': 'system', 'turn-max-tokens': 'system', 'turn-process': 'other',
|
|
157
|
+
'turn-tail': 'other', unknown: 'other'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
function roleOfKind(kind) {
|
|
161
|
+
var name = String(kind === undefined || kind === null ? '' : kind);
|
|
162
|
+
if (ROLE_BY_KIND[name] !== undefined) return ROLE_BY_KIND[name];
|
|
163
|
+
if (name.indexOf('tool') >= 0) return name.indexOf('result') >= 0 ? 'toolResult' : 'toolCall';
|
|
164
|
+
return 'other';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function projectionLengthOf(node) {
|
|
168
|
+
var cached = projectionCache.get(node);
|
|
169
|
+
return cached === undefined ? 0 : cached;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isSpace(code) {
|
|
173
|
+
return code === 32 || code === 9 || code === 10 || code === 13;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function collapse(value) {
|
|
177
|
+
var out = '';
|
|
178
|
+
var pending = false;
|
|
179
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
180
|
+
var ch = value.charAt(i);
|
|
181
|
+
if (isSpace(value.charCodeAt(i))) { pending = true; continue; }
|
|
182
|
+
if (pending && out.length > 0) out += ' ';
|
|
183
|
+
pending = false;
|
|
184
|
+
out += ch;
|
|
185
|
+
}
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function escapeAttribute(value) {
|
|
190
|
+
var text = String(value);
|
|
191
|
+
var out = '';
|
|
192
|
+
for (var i = 0; i < text.length; i += 1) {
|
|
193
|
+
var ch = text.charAt(i);
|
|
194
|
+
if (ch === QUOTE || text.charCodeAt(i) === 92) out += BACKSLASH;
|
|
195
|
+
out += ch;
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Strings of a node, each tagged with the role of the block it belongs to. The
|
|
201
|
+
* order is unchanged from the original walk, so `textOf` still sees the same text. */
|
|
202
|
+
function collectParts(value, out, depth, role) {
|
|
203
|
+
if (depth > 7 || out.length > 600) return;
|
|
204
|
+
if (typeof value === 'string') {
|
|
205
|
+
var text = value.trim();
|
|
206
|
+
if (text.length >= 2) out.push({ text: text, role: role });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(value)) {
|
|
210
|
+
for (var i = 0; i < value.length && out.length <= 600; i += 1) collectParts(value[i], out, depth + 1, role);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (value !== null && typeof value === 'object') {
|
|
214
|
+
var next = role;
|
|
215
|
+
var blockKind = value.kind;
|
|
216
|
+
if (typeof blockKind === 'string' && ROLE_BY_BLOCK[blockKind] !== undefined) next = ROLE_BY_BLOCK[blockKind];
|
|
217
|
+
var keys = Object.keys(value);
|
|
218
|
+
for (var k = 0; k < keys.length && out.length <= 600; k += 1) {
|
|
219
|
+
var key = keys[k];
|
|
220
|
+
if (SKIP_KEYS[key] === 1) continue;
|
|
221
|
+
var keyRole = ROLE_BY_KEY[key];
|
|
222
|
+
collectParts(value[key], out, depth + 1, keyRole !== undefined && next === 'assistant' ? keyRole : next);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function nodeParts(node) {
|
|
228
|
+
if (node === null || typeof node !== 'object') return [];
|
|
229
|
+
var cached = partsCache.get(node);
|
|
230
|
+
if (cached !== undefined) return cached;
|
|
231
|
+
var parts = [];
|
|
232
|
+
collectParts(node, parts, 0, roleOfKind(node.kind));
|
|
233
|
+
partsCache.set(node, parts);
|
|
234
|
+
return parts;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Only the blocks the transcript renders as a message: user text and assistant prose. */
|
|
238
|
+
function proseTextOf(node) {
|
|
239
|
+
if (node === null || typeof node !== 'object') return '';
|
|
240
|
+
var cached = proseCache.get(node);
|
|
241
|
+
if (cached !== undefined) return cached;
|
|
242
|
+
var parts = nodeParts(node);
|
|
243
|
+
var flat = [];
|
|
244
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
245
|
+
var role = parts[i].role;
|
|
246
|
+
if (role === 'user' || role === 'assistant') flat.push(parts[i].text);
|
|
247
|
+
}
|
|
248
|
+
var text = foldQuotes(collapse(deescape(flat.join(LF))));
|
|
249
|
+
proseCache.set(node, text);
|
|
250
|
+
return text;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function textOf(node) {
|
|
254
|
+
if (node === null || typeof node !== 'object') return '';
|
|
255
|
+
var cached = textCache.get(node);
|
|
256
|
+
if (cached !== undefined) return cached;
|
|
257
|
+
var parts = nodeParts(node);
|
|
258
|
+
var flat = [];
|
|
259
|
+
for (var i = 0; i < parts.length; i += 1) flat.push(parts[i].text);
|
|
260
|
+
var raw = flat.join(LF);
|
|
261
|
+
var text = foldQuotes(collapse(deescape(raw)));
|
|
262
|
+
var projection = foldQuotes(collapse(proseProjection(raw)));
|
|
263
|
+
if (projection.length > 0) text = projection + ' ' + text;
|
|
264
|
+
textCache.set(node, text);
|
|
265
|
+
projectionCache.set(node, projection.length);
|
|
266
|
+
return text;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The role of the block that actually contains the needle: tool arguments and tool
|
|
270
|
+
* output can both live inside the same assistant step, so the node kind is not enough. */
|
|
271
|
+
function roleAt(node, needle) {
|
|
272
|
+
if (node === null || typeof node !== 'object') return 'other';
|
|
273
|
+
var base = roleOfKind(node.kind);
|
|
274
|
+
if (needle.length === 0) return base;
|
|
275
|
+
var cached = roleHitCache.get(node);
|
|
276
|
+
if (cached === undefined) { cached = new Map(); roleHitCache.set(node, cached); }
|
|
277
|
+
if (cached.has(needle)) return cached.get(needle);
|
|
278
|
+
var parts = nodeParts(node);
|
|
279
|
+
var found = null;
|
|
280
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
281
|
+
if (normalize(parts[i].text).indexOf(needle) >= 0) { found = parts[i].role; break; }
|
|
282
|
+
}
|
|
283
|
+
if (found === null) found = base;
|
|
284
|
+
cached.set(needle, found);
|
|
285
|
+
return found;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function snippetOf(text, at, length) {
|
|
289
|
+
var start = Math.max(0, at - 70);
|
|
290
|
+
var end = Math.min(text.length, at + length + 90);
|
|
291
|
+
var body = collapse(text.slice(start, end));
|
|
292
|
+
return (start > 0 ? '…' : '') + body + (end < text.length ? '…' : '');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function searchNodes(nodes, query, limit, proseOnly, turns, options) {
|
|
296
|
+
var items = [];
|
|
297
|
+
var truncated = false;
|
|
298
|
+
var matcher = buildMatcher(query, options);
|
|
299
|
+
if (matcher === null || !Array.isArray(nodes)) return { items: items, truncated: truncated };
|
|
300
|
+
if (matcher.invalid === true) return { items: items, truncated: truncated, invalid: true, reason: matcher.reason };
|
|
301
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
302
|
+
var node = nodes[i];
|
|
303
|
+
var text = proseOnly === true ? proseTextOf(node) : textOf(node);
|
|
304
|
+
if (text.length === 0) continue;
|
|
305
|
+
var first = matcher.find(text, 0);
|
|
306
|
+
if (first === null) continue;
|
|
307
|
+
var count = 1;
|
|
308
|
+
var from = first.at + Math.max(1, first.len);
|
|
309
|
+
while (count < 50) {
|
|
310
|
+
var next = matcher.find(text, from);
|
|
311
|
+
if (next === null) break;
|
|
312
|
+
count += 1;
|
|
313
|
+
from = next.at + Math.max(1, next.len);
|
|
314
|
+
}
|
|
315
|
+
var matchText = text.slice(first.at, first.at + first.len);
|
|
316
|
+
var hitRole = proseOnly === true
|
|
317
|
+
? (roleOfKind(node.kind) === 'user' ? 'user' : 'assistant')
|
|
318
|
+
: roleAt(node, normalize(matchText.length > 0 ? matchText : matcher.query));
|
|
319
|
+
var derivedTurn = Array.isArray(turns) && Number.isFinite(Number(turns[i])) ? Number(turns[i]) : node.turn;
|
|
320
|
+
items.push({ key: node.key, kind: node.kind, turn: derivedTurn, index: i, count: count, text: text, at: first.at, len: first.len, match: matchText, proseForm: proseOnly === true ? false : first.at < projectionLengthOf(node), role: hitRole });
|
|
321
|
+
if (items.length >= limit) { truncated = true; break; }
|
|
322
|
+
}
|
|
323
|
+
return { items: items, truncated: truncated };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Kinds whose text the transcript renders as prose; payload-only kinds rank later. */
|
|
327
|
+
var MESSAGE_KINDS = ['user', 'assistant-step', 'assistant', 'steering', 'context', 'compaction', 'manual-compaction', 'system-prompt', 'turn-trigger', 'command'];
|
|
328
|
+
|
|
329
|
+
function isPayloadKind(kind) { var n = String(kind === undefined || kind === null ? '' : kind); return n.indexOf('tool') >= 0 || kindRank(kind) >= MESSAGE_KINDS.length; }
|
|
330
|
+
|
|
331
|
+
/** Markers that say: this line is source code, not what the page draws. A strong marker
|
|
332
|
+
* never shows up in prose, so one is enough; the weak ones need corroboration. */
|
|
333
|
+
var STRONG_SOURCE = ['+Q+', 'StaticText', 'data-chat-', '=>'];
|
|
334
|
+
var WEAK_SOURCE = ['{' + QUOTE, QUOTE + ':' + QUOTE, BACKSLASH + QUOTE, 'function (', QUOTE + ',' + QUOTE, QUOTE + ':[', '; ', 'const ', 'await '];
|
|
335
|
+
|
|
336
|
+
function looksLikeSource(item) {
|
|
337
|
+
var text = String(item.text === undefined ? '' : item.text);
|
|
338
|
+
var at = Number(item.at);
|
|
339
|
+
if (!Number.isFinite(at)) at = 0;
|
|
340
|
+
var slice = text.slice(Math.max(0, at - 200), at + 200);
|
|
341
|
+
var strong = 0;
|
|
342
|
+
var weak = 0;
|
|
343
|
+
for (var i = 0; i < STRONG_SOURCE.length; i += 1) {
|
|
344
|
+
if (slice.indexOf(STRONG_SOURCE[i]) >= 0) strong += 1;
|
|
345
|
+
}
|
|
346
|
+
if (strong > 0) return true;
|
|
347
|
+
for (var j = 0; j < WEAK_SOURCE.length; j += 1) {
|
|
348
|
+
if (slice.indexOf(WEAK_SOURCE[j]) >= 0) weak += 1;
|
|
349
|
+
}
|
|
350
|
+
return weak >= 2;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function kindRank(kind) {
|
|
354
|
+
var name = String(kind === undefined || kind === null ? '' : kind);
|
|
355
|
+
for (var i = 0; i < MESSAGE_KINDS.length; i += 1) if (name.indexOf(MESSAGE_KINDS[i]) >= 0) return i;
|
|
356
|
+
return MESSAGE_KINDS.length + 1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** One visible sentence lives in several nodes (assistant step, turn tail, tool arguments),
|
|
360
|
+
* so the same line would otherwise be listed once per node. Fold hits that would render the
|
|
361
|
+
* same snippet into one row, keeping the node that actually shows prose as the representative. */
|
|
362
|
+
function dedupeHits(found, queryLength) {
|
|
363
|
+
var items = found.items;
|
|
364
|
+
if (!Array.isArray(items) || items.length < 2) return { items: items, truncated: found.truncated, folded: 0, invalid: found.invalid === true, reason: found.reason };
|
|
365
|
+
var groups = new Map();
|
|
366
|
+
var keys = [];
|
|
367
|
+
for (var i = 0; i < items.length; i += 1) {
|
|
368
|
+
var item = items[i];
|
|
369
|
+
var hitLength = Number(item.len);
|
|
370
|
+
var signature = normalize(snippetOf(String(item.text), Number(item.at), Number.isFinite(hitLength) && hitLength > 0 ? hitLength : Number(queryLength)));
|
|
371
|
+
var bucket = groups.get(signature);
|
|
372
|
+
if (bucket === undefined) { groups.set(signature, { best: item, folded: 0, members: [item], turn: item.turn }); keys.push(signature); continue; }
|
|
373
|
+
bucket.folded += 1;
|
|
374
|
+
bucket.members.push(item);
|
|
375
|
+
var candidate = kindRank(item.kind);
|
|
376
|
+
var current = kindRank(bucket.best.kind);
|
|
377
|
+
if (candidate < current || (candidate === current && Number(item.index) < Number(bucket.best.index))) bucket.best = item;
|
|
378
|
+
if (item.proseForm === true && bucket.best.proseForm !== true) bucket.best = item;
|
|
379
|
+
}
|
|
380
|
+
var out = [];
|
|
381
|
+
var folded = 0;
|
|
382
|
+
for (var k = 0; k < keys.length; k += 1) {
|
|
383
|
+
var entry = groups.get(keys[k]);
|
|
384
|
+
/** Grouping follows where the snippet first appears, so a folded row promoted from a
|
|
385
|
+
* later turn cannot make the turn headings jump backwards. */
|
|
386
|
+
if (entry.best !== undefined) entry.best.turn = entry.turn;
|
|
387
|
+
folded += entry.folded;
|
|
388
|
+
var chosen = entry.best;
|
|
389
|
+
var siblings = [];
|
|
390
|
+
for (var m = 0; m < entry.members.length && siblings.length < 6; m += 1) {
|
|
391
|
+
if (entry.members[m].key !== chosen.key) siblings.push(entry.members[m].key);
|
|
392
|
+
}
|
|
393
|
+
out.push({ key: chosen.key, kind: chosen.kind, turn: chosen.turn, index: chosen.index, count: chosen.count, text: chosen.text, at: chosen.at, len: chosen.len, match: chosen.match, duplicates: entry.folded, alts: siblings, proseForm: chosen.proseForm === true, role: chosen.role });
|
|
394
|
+
}
|
|
395
|
+
/** Prose hits first, payload-only hits after: the visible ones are what the user means. */
|
|
396
|
+
out.sort(function (left, right) {
|
|
397
|
+
if (left.proseForm !== right.proseForm) return left.proseForm ? -1 : 1;
|
|
398
|
+
var delta = kindRank(left.kind) - kindRank(right.kind);
|
|
399
|
+
return delta !== 0 ? delta : Number(left.index) - Number(right.index);
|
|
400
|
+
});
|
|
401
|
+
return { items: out, truncated: found.truncated, folded: folded, invalid: found.invalid === true, reason: found.reason };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
function cssEscape(value) {
|
|
406
|
+
var w = typeof window === 'undefined' ? undefined : window;
|
|
407
|
+
if (w !== undefined && w.CSS !== undefined && typeof w.CSS.escape === 'function') {
|
|
408
|
+
try { return w.CSS.escape(String(value)); } catch (error) { /* fall through to the local escaper */ }
|
|
409
|
+
}
|
|
410
|
+
return escapeAttribute(value);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
var HIGHLIGHT_NAME = 'dsh-session-find';
|
|
415
|
+
var HIGHLIGHT_ALL_NAME = 'dsh-session-find-all';
|
|
416
|
+
var HISTORY_KEY = 'dsh-session-find:history';
|
|
417
|
+
var LAST_QUERY_KEY = 'dsh-session-find:last-query';
|
|
418
|
+
var PROSE_KEY = 'dsh-session-find:prose-only';
|
|
419
|
+
var OPTIONS_KEY = 'dsh-session-find:search-options';
|
|
420
|
+
/** Ranges of the current highlight, re-measured after every correction. */
|
|
421
|
+
var activeMarks = [];
|
|
422
|
+
|
|
423
|
+
/** Typographic variants folded 1:1 onto what people actually type, so curly quotes and
|
|
424
|
+
* full-width spaces still match. Length is preserved, so offsets stay usable. */
|
|
425
|
+
var FOLD = {};
|
|
426
|
+
var FOLD_PAIRS = [[8216, 39], [8217, 39], [8218, 39], [8219, 39], [8242, 39], [8220, 34], [8221, 34], [8222, 34], [8223, 34], [8243, 34], [8208, 45], [8209, 45], [8210, 45], [8211, 45], [8212, 45], [8213, 45], [8722, 45], [160, 32], [12288, 32], [65282, 34], [65287, 39], [12300, 34], [12301, 34], [12302, 34], [12303, 34], [12296, 34], [12297, 34], [12298, 34], [12299, 34]];
|
|
427
|
+
for (var foldIndex = 0; foldIndex < FOLD_PAIRS.length; foldIndex += 1) {
|
|
428
|
+
FOLD[String.fromCharCode(FOLD_PAIRS[foldIndex][0])] = String.fromCharCode(FOLD_PAIRS[foldIndex][1]);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function foldQuotes(value) {
|
|
432
|
+
var text = String(value === null || value === undefined ? '' : value);
|
|
433
|
+
var out = '';
|
|
434
|
+
for (var i = 0; i < text.length; i += 1) {
|
|
435
|
+
var ch = text.charAt(i);
|
|
436
|
+
out += FOLD[ch] === undefined ? ch : FOLD[ch];
|
|
437
|
+
}
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Session nodes carry markdown and JSON source, so a rendered sentence can appear as
|
|
442
|
+
* `\"quoted\"`. Undo the escaping before matching, or searching what the page shows finds nothing. */
|
|
443
|
+
var DEESCAPE = { 110: 32, 114: 32, 116: 32, 34: 34, 39: 39, 47: 47, 92: 92 };
|
|
444
|
+
function deescape(value) {
|
|
445
|
+
var text = String(value === null || value === undefined ? '' : value);
|
|
446
|
+
if (text.indexOf(BACKSLASH) < 0) return text;
|
|
447
|
+
var out = '';
|
|
448
|
+
var i = 0;
|
|
449
|
+
while (i < text.length) {
|
|
450
|
+
var ch = text.charAt(i);
|
|
451
|
+
if (ch !== BACKSLASH || i + 1 >= text.length) { out += ch; i += 1; continue; }
|
|
452
|
+
var code = text.charCodeAt(i + 1);
|
|
453
|
+
if (DEESCAPE[code] !== undefined) { out += String.fromCharCode(DEESCAPE[code]); i += 2; continue; }
|
|
454
|
+
if ((code === 117 || code === 85) && i + 5 < text.length) {
|
|
455
|
+
var hex = text.slice(i + 2, i + 6);
|
|
456
|
+
if (/^[0-9a-fA-F]{4}$/.test(hex)) { out += String.fromCharCode(parseInt(hex, 16)); i += 6; continue; }
|
|
457
|
+
}
|
|
458
|
+
out += ch; i += 1;
|
|
459
|
+
}
|
|
460
|
+
return out;
|
|
461
|
+
}
|
|
462
|
+
/** A dsh-ui card is authored as JSON but drawn as prose, so index the strings inside it:
|
|
463
|
+
* the user searches what the page shows, not the source that renders it. */
|
|
464
|
+
/** True when a string looks like something a person reads, not a key or an identifier. */
|
|
465
|
+
function hasWideText(value) {
|
|
466
|
+
for (var i = 0; i < value.length; i += 1) {
|
|
467
|
+
var code = value.charCodeAt(i);
|
|
468
|
+
if (code >= 19968 && code <= 40959) return true;
|
|
469
|
+
if (code === 32) return true;
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function proseProjection(raw) {
|
|
475
|
+
if (raw.indexOf('{"') < 0 && raw.indexOf(BACKSLASH + QUOTE) < 0) return '';
|
|
476
|
+
var runs = [];
|
|
477
|
+
var i = 0;
|
|
478
|
+
while (i < raw.length && runs.length < 300) {
|
|
479
|
+
var open = raw.indexOf(QUOTE, i);
|
|
480
|
+
if (open < 0) break;
|
|
481
|
+
var close = open + 1;
|
|
482
|
+
while (close < raw.length) {
|
|
483
|
+
var ch = raw.charAt(close);
|
|
484
|
+
if (ch === BACKSLASH) { close += 2; continue; }
|
|
485
|
+
if (ch === QUOTE) break;
|
|
486
|
+
close += 1;
|
|
487
|
+
}
|
|
488
|
+
if (close >= raw.length) break;
|
|
489
|
+
var run = raw.slice(open + 1, close);
|
|
490
|
+
if (run.length >= 6 && hasWideText(run)) runs.push(deescape(run));
|
|
491
|
+
i = close + 1;
|
|
492
|
+
}
|
|
493
|
+
return runs.join(' · ');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function normalize(value) { return foldQuotes(collapse(String(value === null || value === undefined ? '' : value))).toLowerCase(); }
|
|
497
|
+
|
|
498
|
+
/** The same folding the scan applies, minus lowercasing: a case-sensitive search must
|
|
499
|
+
* not compare text that was already flattened. */
|
|
500
|
+
function foldForSearch(value) { return foldQuotes(collapse(String(value === null || value === undefined ? '' : value))); }
|
|
501
|
+
|
|
502
|
+
function wordBoundaryOk(text, at, length) {
|
|
503
|
+
var word = /[A-Za-z0-9_]/;
|
|
504
|
+
var before = at > 0 ? text.charAt(at - 1) : '';
|
|
505
|
+
var after = at + length < text.length ? text.charAt(at + length) : '';
|
|
506
|
+
return (before === '' || !word.test(before)) && (after === '' || !word.test(after));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** One scan flavour: literal (optionally case-sensitive and whole-word) or regular
|
|
510
|
+
* expression. Offsets always index the folded node text, which is what the scan sees. */
|
|
511
|
+
function buildMatcher(query, options) {
|
|
512
|
+
var folded = foldForSearch(query);
|
|
513
|
+
var opts = options === undefined || options === null ? {} : options;
|
|
514
|
+
var matchCase = opts.matchCase === true;
|
|
515
|
+
var wholeWord = opts.wholeWord === true;
|
|
516
|
+
if (folded.length === 0) return null;
|
|
517
|
+
if (opts.regex === true) {
|
|
518
|
+
var flags = 'g' + (matchCase ? '' : 'i');
|
|
519
|
+
var source = wholeWord ? '(?<![A-Za-z0-9_])(?:' + folded + ')(?![A-Za-z0-9_])' : folded;
|
|
520
|
+
var pattern = null;
|
|
521
|
+
try { pattern = new RegExp(source, flags); } catch (error) { return { invalid: true, reason: String(error && error.message ? error.message : error) }; }
|
|
522
|
+
return {
|
|
523
|
+
regex: true, literal: false, query: folded,
|
|
524
|
+
find: function (text, from) {
|
|
525
|
+
if (from > text.length) return null;
|
|
526
|
+
pattern.lastIndex = from;
|
|
527
|
+
var match = pattern.exec(text);
|
|
528
|
+
while (match !== null) {
|
|
529
|
+
if (match[0].length > 0) return { at: match.index, len: match[0].length };
|
|
530
|
+
pattern.lastIndex = match.index + 1;
|
|
531
|
+
if (pattern.lastIndex > text.length) return null;
|
|
532
|
+
match = pattern.exec(text);
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
var needle = matchCase ? folded : folded.toLowerCase();
|
|
539
|
+
var asciiWord = wholeWord && /^[A-Za-z0-9_]+$/.test(folded);
|
|
540
|
+
return {
|
|
541
|
+
regex: false, literal: true, query: folded, needle: needle,
|
|
542
|
+
find: function (text, from) {
|
|
543
|
+
var hay = matchCase ? text : text.toLowerCase();
|
|
544
|
+
var index = Math.max(0, from);
|
|
545
|
+
while (index <= hay.length - needle.length) {
|
|
546
|
+
var at = hay.indexOf(needle, index);
|
|
547
|
+
if (at < 0) return null;
|
|
548
|
+
if (!asciiWord || wordBoundaryOk(text, at, needle.length)) return { at: at, len: needle.length };
|
|
549
|
+
index = at + 1;
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function textNodeFilter() { return typeof NodeFilter === 'undefined' ? 4 : NodeFilter.SHOW_TEXT; }
|
|
557
|
+
|
|
558
|
+
/** The CSS Custom Highlight registry, read off the global so the local stylesheet
|
|
559
|
+
* string can never shadow it. */
|
|
560
|
+
function highlightRegistry() {
|
|
561
|
+
if (typeof window === 'undefined' || window.CSS === undefined || window.CSS === null) return null;
|
|
562
|
+
var registry = window.CSS.highlights;
|
|
563
|
+
return registry === undefined || registry === null ? null : registry;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function highlightConstructor() {
|
|
567
|
+
if (typeof window === 'undefined') return null;
|
|
568
|
+
return typeof window.Highlight === 'function' ? window.Highlight : null;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Concatenate an element's text nodes into one string plus the map back to them.
|
|
572
|
+
* Markdown splits a phrase across inline nodes, so matching must not be per node. */
|
|
573
|
+
function textMapOf(element) {
|
|
574
|
+
var map = [];
|
|
575
|
+
var parts = [];
|
|
576
|
+
var length = 0;
|
|
577
|
+
if (typeof document === 'undefined' || typeof document.createTreeWalker !== 'function') return { text: '', map: map };
|
|
578
|
+
var walker = document.createTreeWalker(element, textNodeFilter(), null);
|
|
579
|
+
var node = walker.nextNode();
|
|
580
|
+
while (node !== null) {
|
|
581
|
+
var value = String(node.nodeValue === null || node.nodeValue === undefined ? '' : node.nodeValue);
|
|
582
|
+
if (value.length > 0) {
|
|
583
|
+
map.push({ node: node, start: length, end: length + value.length });
|
|
584
|
+
parts.push(value);
|
|
585
|
+
length += value.length;
|
|
586
|
+
}
|
|
587
|
+
node = walker.nextNode();
|
|
588
|
+
}
|
|
589
|
+
return { text: parts.join(''), map: map };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** One Range over the concatenated offsets, possibly spanning several text nodes. */
|
|
593
|
+
function rangeAt(map, from, to) {
|
|
594
|
+
var start = null;
|
|
595
|
+
var end = null;
|
|
596
|
+
for (var i = 0; i < map.length; i += 1) {
|
|
597
|
+
var entry = map[i];
|
|
598
|
+
if (start === null && from >= entry.start && from < entry.end) start = { node: entry.node, offset: from - entry.start };
|
|
599
|
+
if (to > entry.start && to <= entry.end) { end = { node: entry.node, offset: to - entry.start }; break; }
|
|
600
|
+
}
|
|
601
|
+
if (start === null || end === null) return null;
|
|
602
|
+
var range = document.createRange();
|
|
603
|
+
try {
|
|
604
|
+
range.setStart(start.node, start.offset);
|
|
605
|
+
range.setEnd(end.node, end.offset);
|
|
606
|
+
} catch (error) { return null; }
|
|
607
|
+
return range;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** Every occurrence of the typed query inside one element, else of the longer
|
|
611
|
+
* locator snippet: the query is what the user must see coloured. */
|
|
612
|
+
function collectMarks(element, marks) {
|
|
613
|
+
var out = [];
|
|
614
|
+
if (element === null || element === undefined || marks === undefined || marks === null) return out;
|
|
615
|
+
if (typeof document === 'undefined' || typeof document.createRange !== 'function') return out;
|
|
616
|
+
var candidates = [];
|
|
617
|
+
if (marks.query) candidates.push(marks.query);
|
|
618
|
+
if (marks.needle) candidates.push(marks.needle);
|
|
619
|
+
for (var c = 0; c < candidates.length && out.length === 0; c += 1) {
|
|
620
|
+
var needle = normalize(candidates[c]);
|
|
621
|
+
if (needle.length < 2) continue;
|
|
622
|
+
var scanned = textMapOf(element);
|
|
623
|
+
var haystack = foldQuotes(scanned.text).toLowerCase();
|
|
624
|
+
var from = haystack.indexOf(needle);
|
|
625
|
+
while (from >= 0 && out.length < 80) {
|
|
626
|
+
var range = rangeAt(scanned.map, from, from + needle.length);
|
|
627
|
+
if (range !== null) out.push(range);
|
|
628
|
+
from = haystack.indexOf(needle, from + needle.length);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function applyHighlight(ranges) {
|
|
635
|
+
activeMarks = Array.isArray(ranges) ? ranges : [];
|
|
636
|
+
var registry = highlightRegistry();
|
|
637
|
+
var Highlight = highlightConstructor();
|
|
638
|
+
if (registry === null || Highlight === null || activeMarks.length === 0) return;
|
|
639
|
+
try { registry.set(HIGHLIGHT_NAME, new Highlight(...activeMarks)); } catch (error) { /* ignore */ }
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function clearHighlight() {
|
|
643
|
+
activeMarks = [];
|
|
644
|
+
var registry = highlightRegistry();
|
|
645
|
+
if (registry === null || typeof registry.delete !== 'function') return;
|
|
646
|
+
try { registry.delete(HIGHLIGHT_NAME); } catch (error) { /* a host without the API keeps its plain text */ }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Faintly mark every visible occurrence of the query, so the result list maps onto the
|
|
650
|
+
* transcript at a glance; the current hit keeps its stronger highlight on top. */
|
|
651
|
+
function paintAllMarks(query) {
|
|
652
|
+
var registry = highlightRegistry();
|
|
653
|
+
var Highlight = highlightConstructor();
|
|
654
|
+
if (registry === null || Highlight === null || typeof document === 'undefined') return;
|
|
655
|
+
var needle = normalize(query);
|
|
656
|
+
var ranges = [];
|
|
657
|
+
if (needle.length >= 2) {
|
|
658
|
+
var rows = document.querySelectorAll('[data-chat-node-key]');
|
|
659
|
+
for (var i = 0; i < rows.length && ranges.length < 240; i += 1) {
|
|
660
|
+
var found = collectMarks(rows[i], { query: query });
|
|
661
|
+
for (var k = 0; k < found.length && ranges.length < 240; k += 1) ranges.push(found[k]);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
if (ranges.length === 0) registry.delete(HIGHLIGHT_ALL_NAME);
|
|
666
|
+
else registry.set(HIGHLIGHT_ALL_NAME, new Highlight(...ranges));
|
|
667
|
+
} catch (error) { /* ignore */ }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function clearAllMarks() {
|
|
671
|
+
var registry = highlightRegistry();
|
|
672
|
+
if (registry === null || typeof registry.delete !== 'function') return;
|
|
673
|
+
try { registry.delete(HIGHLIGHT_ALL_NAME); } catch (error) { /* ignore */ }
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function readSearchOptions() {
|
|
677
|
+
var fallback = { regex: false, matchCase: false, wholeWord: false };
|
|
678
|
+
try {
|
|
679
|
+
var raw = window.localStorage.getItem(OPTIONS_KEY);
|
|
680
|
+
if (typeof raw !== 'string' || raw.length === 0) return fallback;
|
|
681
|
+
var parsed = JSON.parse(raw);
|
|
682
|
+
return { regex: parsed.regex === true, matchCase: parsed.matchCase === true, wholeWord: parsed.wholeWord === true };
|
|
683
|
+
} catch (error) { return fallback; }
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function readHistory() {
|
|
687
|
+
try {
|
|
688
|
+
var raw = window.localStorage.getItem(HISTORY_KEY);
|
|
689
|
+
var parsed = raw === null ? [] : JSON.parse(raw);
|
|
690
|
+
if (!Array.isArray(parsed)) return [];
|
|
691
|
+
return parsed.filter(function (value) { return typeof value === 'string' && value.length > 0; }).slice(0, 12);
|
|
692
|
+
} catch (error) { return []; }
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Rectangle of the highlighted match itself, so tall rows are centred on the hit
|
|
696
|
+
* rather than on the message. */
|
|
697
|
+
function currentRect(element) {
|
|
698
|
+
var range = activeMarks[0];
|
|
699
|
+
if (range !== undefined && range !== null && typeof range.getBoundingClientRect === 'function') {
|
|
700
|
+
try {
|
|
701
|
+
/** Only trust the mark rect when that mark lives inside this row: a sibling row
|
|
702
|
+
* rendering the same sentence must not move the landing. */
|
|
703
|
+
var probe = range.commonAncestorContainer !== undefined ? range.commonAncestorContainer : range.startContainer;
|
|
704
|
+
var inside = element !== null && element !== undefined && typeof element.contains === 'function' && probe !== undefined && probe !== null && (element === probe || element.contains(probe));
|
|
705
|
+
var markRect = range.getBoundingClientRect();
|
|
706
|
+
var elRect2 = element !== null && element !== undefined && typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : null;
|
|
707
|
+
var invisible = elRect2 === null || elRect2 === undefined || elRect2.height === 0;
|
|
708
|
+
if ((inside || invisible) && markRect !== null && markRect !== undefined && markRect.height > 0) return markRect;
|
|
709
|
+
if (inside) {
|
|
710
|
+
if (markRect !== null && markRect !== undefined && markRect.height > 0) return markRect;
|
|
711
|
+
}
|
|
712
|
+
} catch (error) { /* fall back to the element */ }
|
|
713
|
+
}
|
|
714
|
+
if (element === null || element === undefined || typeof element.getBoundingClientRect !== 'function') return null;
|
|
715
|
+
return element.getBoundingClientRect();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** One node can be drawn across several rows, so the searched sentence may be rendered by
|
|
719
|
+
* a sibling row of the same turn. Highlight it there, but never move the landing: every
|
|
720
|
+
* result must jump to its own node. */
|
|
721
|
+
function sameTurnHighlight(element, marks) {
|
|
722
|
+
if (element === null || element === undefined || typeof element.closest !== 'function') return [];
|
|
723
|
+
var scope = element.closest('[data-chat-turn]');
|
|
724
|
+
if (scope === null || scope === undefined) scope = element.parentElement;
|
|
725
|
+
while (scope !== null && scope !== undefined && scope.querySelectorAll !== undefined && scope.querySelectorAll('[data-chat-node-key]').length < 2 && scope.parentElement !== null) scope = scope.parentElement;
|
|
726
|
+
if (scope === null || scope === undefined || scope === element) return [];
|
|
727
|
+
/** collectMarks walks all text nodes and concatenates them, so cross-node text
|
|
728
|
+
* (card titles split by inline elements) is matched correctly. */
|
|
729
|
+
var found = collectMarks(scope, marks);
|
|
730
|
+
if (found.length > 0) return found;
|
|
731
|
+
/** GenUI cards can render in a portal outside the turn container, so fall back to
|
|
732
|
+
* the whole scroller. This only paints a highlight; it never moves the landing. */
|
|
733
|
+
var scroller = chatScroller();
|
|
734
|
+
if (scroller !== null && scroller !== undefined && scroller !== scope) return collectMarks(scroller, marks);
|
|
735
|
+
return [];
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function placeInto(box, element) {
|
|
739
|
+
if (box === null || box === undefined || typeof box.getBoundingClientRect !== 'function') return;
|
|
740
|
+
var target = currentRect(element);
|
|
741
|
+
if (target === null) return;
|
|
742
|
+
var boxRect = box.getBoundingClientRect();
|
|
743
|
+
var delta = (target.top + target.height / 2) - (boxRect.top + boxRect.height / 2);
|
|
744
|
+
if (Math.abs(delta) > 1) box.scrollTop = box.scrollTop + delta;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** The app may re-anchor the list right after a programmatic scroll: measure the
|
|
748
|
+
* match again and correct once, so the hit really ends up on screen. */
|
|
749
|
+
function verifyPlacement(box, element, marks) {
|
|
750
|
+
if (box === null || element === null || element === undefined) return Promise.resolve();
|
|
751
|
+
return settle().then(function () {
|
|
752
|
+
if (box === null || typeof box.getBoundingClientRect !== 'function') return;
|
|
753
|
+
var boxRect = box.getBoundingClientRect();
|
|
754
|
+
var target = currentRect(element);
|
|
755
|
+
if (target === null) return;
|
|
756
|
+
if (target.top >= boxRect.top && target.bottom <= boxRect.bottom) return;
|
|
757
|
+
var delta = (target.top + target.height / 2) - (boxRect.top + boxRect.height / 2);
|
|
758
|
+
if (Math.abs(delta) > 1) box.scrollTop = box.scrollTop + delta;
|
|
759
|
+
var own = collectMarks(element, marks);
|
|
760
|
+
applyHighlight(own.length > 0 ? own : sameTurnHighlight(element, marks));
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** Highlight, centre the hit, then confirm it stayed centred. */
|
|
765
|
+
function show(box, element, flashIt, marks) {
|
|
766
|
+
if (element === null || element === undefined) return Promise.resolve(null);
|
|
767
|
+
applyHighlight(collectMarks(element, marks));
|
|
768
|
+
var away = false;
|
|
769
|
+
if (activeMarks.length === 0) {
|
|
770
|
+
var nearby = sameTurnHighlight(element, marks);
|
|
771
|
+
if (nearby.length > 0) { applyHighlight(nearby); away = true; }
|
|
772
|
+
}
|
|
773
|
+
/** Landing always centres on this result own row; a nearby highlight is only painted,
|
|
774
|
+
* never allowed to drag the scroll position (or several results pile up on one spot). */
|
|
775
|
+
placeInto(box, element);
|
|
776
|
+
if (flashIt !== false) flash(element);
|
|
777
|
+
traceVerbose('text textLen=' + (typeof element.textContent === 'string' ? element.textContent.length : -1) + ' hasQuery=' + (typeof element.textContent === 'string' && String(marks && marks.query).length > 0 ? element.textContent.indexOf(String(marks.query)) >= 0 : false) + ' hasNeedle=' + (typeof element.textContent === 'string' && marks && marks.needle ? element.textContent.indexOf(String(marks.needle).slice(0, 12)) >= 0 : false) + ' collapsed=' + (typeof element.querySelectorAll === 'function' ? element.querySelectorAll('button[aria-expanded=false]').length : -1) + ' textNodes=' + (function () { try { var w = document.createTreeWalker(element, typeof NodeFilter === 'undefined' ? 4 : NodeFilter.SHOW_TEXT, null); var n = 0; var x = w.nextNode(); while (x !== null && n < 5000) { n += 1; x = w.nextNode(); } return n; } catch (e) { return -1; } })());
|
|
778
|
+
var rect = currentRect(element);
|
|
779
|
+
trace('show exact=' + (flashIt !== false) + ' away=' + away + ' marks=' + activeMarks.length + ' rectTop=' + (rect === null ? 'null' : Math.round(rect.top)) + ' rectH=' + (rect === null ? -1 : Math.round(rect.height)) + ' boxRect=' + JSON.stringify(box === null ? null : (function () { var r = box.getBoundingClientRect(); return { top: Math.round(r.top), h: Math.round(r.height), scroll: Math.round(box.scrollTop), sh: box.scrollHeight, ch: box.clientHeight }; })()) + ' rowKey=' + (typeof element.getAttribute === 'function' ? String(element.getAttribute('data-chat-node-key')) : '?'));
|
|
780
|
+
return verifyPlacement(box, element, marks).then(function () {
|
|
781
|
+
trace('after-verify top=' + (box === null ? -1 : Math.round(box.scrollTop)) + ' marks=' + activeMarks.length);
|
|
782
|
+
return { exact: flashIt !== false, highlighted: activeMarks.length > 0, element: element };
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function flash(element) {
|
|
787
|
+
if (element === null || element === undefined || element.classList === undefined) return;
|
|
788
|
+
element.classList.add('dsh-session-find-flash');
|
|
789
|
+
window.setTimeout(function () { element.classList.remove('dsh-session-find-flash'); }, 1600);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function findNodeElement(key) {
|
|
793
|
+
if (typeof document === 'undefined') return null;
|
|
794
|
+
try { return document.querySelector('[data-chat-node-key=' + QUOTE + cssEscape(key) + QUOTE + ']'); } catch (error) { return null; }
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function chatScroller() {
|
|
798
|
+
if (typeof document === 'undefined') return null;
|
|
799
|
+
var flow = document.querySelector('[data-chat-flow]');
|
|
800
|
+
var el = flow === null ? null : flow.parentElement;
|
|
801
|
+
while (el !== null && el !== undefined && el !== document.body) {
|
|
802
|
+
var style = window.getComputedStyle(el);
|
|
803
|
+
if (/(auto|scroll)/.test(style.overflowY) && el.scrollHeight - el.clientHeight > 8) return el;
|
|
804
|
+
el = el.parentElement;
|
|
805
|
+
}
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function settle() {
|
|
810
|
+
return new Promise(function (resolve) {
|
|
811
|
+
requestAnimationFrame(function () {
|
|
812
|
+
requestAnimationFrame(function () { setTimeout(resolve, 30); });
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** Mounted Turn range, the direction signal for the deep search. */
|
|
818
|
+
function mountedTurnWindow(box) {
|
|
819
|
+
if (box === null || box === undefined || typeof box.querySelectorAll !== 'function') return null;
|
|
820
|
+
var rows = box.querySelectorAll('[data-chat-turn]');
|
|
821
|
+
var min = Infinity;
|
|
822
|
+
var max = -Infinity;
|
|
823
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
824
|
+
var value = Number(rows[i].getAttribute('data-chat-turn'));
|
|
825
|
+
if (!Number.isFinite(value)) continue;
|
|
826
|
+
if (value < min) min = value;
|
|
827
|
+
if (value > max) max = value;
|
|
828
|
+
}
|
|
829
|
+
return min === Infinity ? null : { min: min, max: max };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Nearest Turn around one node index: plenty of rows carry no Turn of their own. */
|
|
833
|
+
function nearestTurn(nodes, index) {
|
|
834
|
+
if (!Array.isArray(nodes) || !Number.isFinite(index)) return null;
|
|
835
|
+
for (var distance = 0; distance < nodes.length; distance += 1) {
|
|
836
|
+
var before = index - distance;
|
|
837
|
+
var after = index + distance;
|
|
838
|
+
if (before >= 0 && nodes[before] !== null && nodes[before] !== undefined) {
|
|
839
|
+
var a = Number(nodes[before].turn);
|
|
840
|
+
if (Number.isFinite(a)) return a;
|
|
841
|
+
}
|
|
842
|
+
if (after < nodes.length && nodes[after] !== null && nodes[after] !== undefined) {
|
|
843
|
+
var b = Number(nodes[after].turn);
|
|
844
|
+
if (Number.isFinite(b)) return b;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/** Render order of the chat rows, cached by the array identity the store hands out. */
|
|
851
|
+
var rankCache = { order: null, map: null };
|
|
852
|
+
|
|
853
|
+
function rankMapOf(order) {
|
|
854
|
+
if (!Array.isArray(order)) return null;
|
|
855
|
+
if (rankCache.order === order && rankCache.map !== null) return rankCache.map;
|
|
856
|
+
var map = new Map();
|
|
857
|
+
for (var i = 0; i < order.length; i += 1) map.set(String(order[i]), i);
|
|
858
|
+
rankCache.order = order;
|
|
859
|
+
rankCache.map = map;
|
|
860
|
+
return map;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function rowKeyOf(row) {
|
|
864
|
+
if (row === null || row === undefined || typeof row.getAttribute !== 'function') return null;
|
|
865
|
+
var key = row.getAttribute('data-chat-node-key');
|
|
866
|
+
return key === null || key === undefined ? null : String(key);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** Mounted render-order window: the precise direction signal, independent of text. */
|
|
870
|
+
function mountedRankWindow(box, rankOf) {
|
|
871
|
+
if (box === null || box === undefined || typeof box.querySelectorAll !== 'function' || rankOf === null) return null;
|
|
872
|
+
var rows = box.querySelectorAll('[data-chat-node-key]');
|
|
873
|
+
var min = Infinity;
|
|
874
|
+
var max = -Infinity;
|
|
875
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
876
|
+
var rank = rankOf.get(rowKeyOf(rows[i]));
|
|
877
|
+
if (rank === undefined) continue;
|
|
878
|
+
if (rank < min) min = rank;
|
|
879
|
+
if (rank > max) max = rank;
|
|
880
|
+
}
|
|
881
|
+
return min === Infinity ? null : { min: min, max: max };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function turnRowElement(turn) {
|
|
885
|
+
if (!Number.isFinite(Number(turn)) || typeof document === 'undefined') return null;
|
|
886
|
+
try { return document.querySelector('[data-chat-turn=' + QUOTE + cssEscape(String(Number(turn))) + QUOTE + ']'); } catch (error) { return null; }
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Rows whose text contains the snippet (else the query). Matching by text is only a
|
|
891
|
+
* fallback, so it must not collapse duplicates onto one row: rows inside the target
|
|
892
|
+
* Turn win, and among them the row whose render rank is nearest the hit's own rank.
|
|
893
|
+
*/
|
|
894
|
+
function findByContent(box, marks, turn, wantRank) {
|
|
895
|
+
if (box === null || box === undefined || typeof box.querySelectorAll !== 'function') return null;
|
|
896
|
+
if (marks === undefined || marks === null) return null;
|
|
897
|
+
var candidates = [];
|
|
898
|
+
if (marks.needle) candidates.push(marks.needle);
|
|
899
|
+
if (marks.query) candidates.push(marks.query);
|
|
900
|
+
var rows = box.querySelectorAll('[data-chat-node-key]');
|
|
901
|
+
var wanted = Number.isFinite(Number(turn)) ? '[data-chat-turn=' + QUOTE + cssEscape(String(Number(turn))) + QUOTE + ']' : null;
|
|
902
|
+
var rankOf = rankMapOf(marks.order);
|
|
903
|
+
var rankWanted = Number(wantRank);
|
|
904
|
+
for (var c = 0; c < candidates.length; c += 1) {
|
|
905
|
+
var target = normalize(candidates[c]);
|
|
906
|
+
if (target.length < 2) continue;
|
|
907
|
+
var best = null;
|
|
908
|
+
var bestScore = Infinity;
|
|
909
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
910
|
+
var row = rows[i];
|
|
911
|
+
if (normalize(row.textContent).indexOf(target) < 0) continue;
|
|
912
|
+
var inTurn = wanted === null ? true : (typeof row.closest === 'function' && row.closest(wanted) !== null);
|
|
913
|
+
var score = inTurn ? 0 : 100000;
|
|
914
|
+
if (rankOf !== null && Number.isFinite(rankWanted)) {
|
|
915
|
+
var rank = rankOf.get(rowKeyOf(row));
|
|
916
|
+
if (rank !== undefined) score += Math.abs(rank - rankWanted);
|
|
917
|
+
}
|
|
918
|
+
if (score < bestScore) { best = row; bestScore = score; }
|
|
919
|
+
}
|
|
920
|
+
if (best !== null) return best;
|
|
921
|
+
}
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** Slice of the hit text used to find the row again among the mounted ones. */
|
|
926
|
+
function needleOf(text, at, length) {
|
|
927
|
+
var size = Math.max(24, Math.min(80, Number(length) + 40));
|
|
928
|
+
var start = Math.max(0, Math.min(Number(at), text.length - 1));
|
|
929
|
+
return text.slice(start, Math.min(text.length, start + size));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** Open collapsed disclosure controls inside one row so its hidden members render.
|
|
933
|
+
* Only aria-expanded toggles are touched: that attribute belongs to disclosures. */
|
|
934
|
+
function expandCollapsed(element, limit) {
|
|
935
|
+
if (element === null || element === undefined || typeof element.querySelectorAll !== 'function') return 0;
|
|
936
|
+
var toggles = element.querySelectorAll('button[aria-expanded=false]');
|
|
937
|
+
var opened = 0;
|
|
938
|
+
var most = limit === undefined ? 4 : limit;
|
|
939
|
+
for (var i = 0; i < toggles.length && opened < most; i += 1) {
|
|
940
|
+
try { toggles[i].click(); opened += 1; } catch (error) { /* a hostile control must not stop the jump */ }
|
|
941
|
+
}
|
|
942
|
+
return opened;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/** Expand what the row hides, then look again: the member node only exists in the DOM
|
|
946
|
+
* while its disclosure is open, and its own key is the exact identity to trust. */
|
|
947
|
+
function openThenFind(box, row, key, marks, turn, wantRank, depth) {
|
|
948
|
+
var opened = expandCollapsed(row);
|
|
949
|
+
var attempt = function () {
|
|
950
|
+
var exact = findNodeElement(key);
|
|
951
|
+
if (exact !== null) return show(box, exact, true, marks);
|
|
952
|
+
if (opened === 0) return Promise.resolve(null);
|
|
953
|
+
var nearby = findByContent(box, marks, turn, wantRank);
|
|
954
|
+
if (nearby !== null) return show(box, nearby, false, marks);
|
|
955
|
+
if (depth < 3) return openThenFind(box, row, key, marks, turn, wantRank, depth + 1);
|
|
956
|
+
return Promise.resolve(null);
|
|
957
|
+
};
|
|
958
|
+
if (opened === 0) return attempt();
|
|
959
|
+
return settle().then(attempt);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** Per-step tracing is off by default; flip to true to diagnose a real-machine jump. */
|
|
963
|
+
var VERBOSE = false;
|
|
964
|
+
|
|
965
|
+
function emit(message, enabled) {
|
|
966
|
+
try {
|
|
967
|
+
if (!enabled) return;
|
|
968
|
+
if (typeof console === 'undefined' || typeof console.log !== 'function') return;
|
|
969
|
+
console.log('[next-ui-diagnostic] session-find ' + String(message).slice(0, 900));
|
|
970
|
+
} catch (error) { /* tracing must never break the search */ }
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/** Key events only: one or two lines per jump, so a failure leaves evidence without spam.
|
|
974
|
+
* The prefix is the one main.js forwards into ~/.dsh/logs/desktop-next.log. */
|
|
975
|
+
function trace(message) { emit(message, true); }
|
|
976
|
+
|
|
977
|
+
/** Step-by-step detail for real-machine diagnosis. */
|
|
978
|
+
function traceVerbose(message) { emit(message, VERBOSE); }
|
|
979
|
+
|
|
980
|
+
function rowKeysBrief(rows, limit) {
|
|
981
|
+
var out = [];
|
|
982
|
+
if (rows === null || rows === undefined || typeof rows.length !== 'number') return out;
|
|
983
|
+
for (var i = 0; i < rows.length && out.length < (limit === undefined ? 5 : limit); i += 1) {
|
|
984
|
+
var key = rowKeyOf(rows[i]);
|
|
985
|
+
out.push(key === null ? '?' : key);
|
|
986
|
+
}
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Ancestor chain of the chat flow: overflowY plus scrollable overflow, to prove the scroller. */
|
|
991
|
+
function scrollerChain() {
|
|
992
|
+
if (typeof document === 'undefined') return 'no-document';
|
|
993
|
+
var flow = document.querySelector('[data-chat-flow]');
|
|
994
|
+
if (flow === null) return 'no-flow';
|
|
995
|
+
var el = flow.parentElement;
|
|
996
|
+
var out = [];
|
|
997
|
+
var depth = 0;
|
|
998
|
+
while (el !== null && el !== undefined && el !== document.body && depth < 6) {
|
|
999
|
+
var style = window.getComputedStyle(el);
|
|
1000
|
+
out.push(style.overflowY + '+' + (el.scrollHeight - el.clientHeight));
|
|
1001
|
+
el = el.parentElement;
|
|
1002
|
+
depth += 1;
|
|
1003
|
+
}
|
|
1004
|
+
return out.join(' > ');
|
|
1005
|
+
}
|
|
1006
|
+
/** One node can render as several rows (reasoning gets its own row), so the row that owns
|
|
1007
|
+
* the key often holds only part of the text. Prefer whichever mounted row actually shows
|
|
1008
|
+
* the matched text: otherwise there is nothing to highlight and centring looks wrong. */
|
|
1009
|
+
/** Where is the matched text actually rendered? The transcript splits one node across
|
|
1010
|
+
* several rows (reasoning renders as its own 思考 row), and those rows do not always carry
|
|
1011
|
+
* a node key, so search the mounted text nodes and take the element nearest the hit row. */
|
|
1012
|
+
function findTextOwner(box, marks, anchor) {
|
|
1013
|
+
if (box === null || box === undefined || typeof document === 'undefined') return null;
|
|
1014
|
+
if (typeof document.createTreeWalker !== 'function') return null;
|
|
1015
|
+
var wanted = [];
|
|
1016
|
+
if (marks !== undefined && marks !== null) {
|
|
1017
|
+
if (marks.query) wanted.push(normalize(marks.query));
|
|
1018
|
+
if (marks.needle) wanted.push(normalize(marks.needle).slice(0, 20));
|
|
1019
|
+
}
|
|
1020
|
+
var anchorRect = anchor !== null && anchor !== undefined && typeof anchor.getBoundingClientRect === "function" ? anchor.getBoundingClientRect() : null;
|
|
1021
|
+
var walker = document.createTreeWalker(box, textNodeFilter(), null);
|
|
1022
|
+
var node = walker.nextNode();
|
|
1023
|
+
var best = null;
|
|
1024
|
+
var bestDistance = Infinity;
|
|
1025
|
+
var scanned = 0;
|
|
1026
|
+
while (node !== null && scanned < 20000) {
|
|
1027
|
+
scanned += 1;
|
|
1028
|
+
var value = normalize(node.nodeValue);
|
|
1029
|
+
if (value.length >= 2) {
|
|
1030
|
+
for (var w = 0; w < wanted.length; w += 1) {
|
|
1031
|
+
if (wanted[w].length >= 2 && value.indexOf(wanted[w]) >= 0) {
|
|
1032
|
+
var host = node.parentElement;
|
|
1033
|
+
if (host === null || host === undefined) break;
|
|
1034
|
+
var row = typeof host.closest === "function" ? (host.closest('[data-chat-node-key]') || host) : host;
|
|
1035
|
+
var rect = typeof row.getBoundingClientRect === "function" ? row.getBoundingClientRect() : null;
|
|
1036
|
+
var distance = anchorRect === null || rect === null ? 0 : Math.abs((rect.top + rect.height / 2) - (anchorRect.top + anchorRect.height / 2));
|
|
1037
|
+
if (distance < bestDistance) { bestDistance = distance; best = row; }
|
|
1038
|
+
break;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
node = walker.nextNode();
|
|
1043
|
+
}
|
|
1044
|
+
return best;
|
|
1045
|
+
}
|
|
1046
|
+
/** One reveal at a time: a newer jump invalidates the running one. */
|
|
1047
|
+
var revealToken = 0;
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Reveal one hit. The search only trusts the node's own key while scrolling; text
|
|
1051
|
+
* matching is confined to the target Turn and disambiguated by render rank, because
|
|
1052
|
+
* injected context repeats the same text in many nodes.
|
|
1053
|
+
*/
|
|
1054
|
+
function revealNode(key, context) {
|
|
1055
|
+
var token = ++revealToken;
|
|
1056
|
+
var stale = function () { return token !== revealToken; };
|
|
1057
|
+
var ctx = context === undefined || context === null ? {} : context;
|
|
1058
|
+
var marks = { query: ctx.query, needle: ctx.needle, order: ctx.order };
|
|
1059
|
+
var rankOf = rankMapOf(ctx.order);
|
|
1060
|
+
var wantRank = rankOf === null ? undefined : rankOf.get(String(key));
|
|
1061
|
+
var turn = Number(ctx.turn);
|
|
1062
|
+
if (!Number.isFinite(turn)) turn = nearestTurn(ctx.nodes, Number(ctx.index));
|
|
1063
|
+
|
|
1064
|
+
var direct = findNodeElement(key);
|
|
1065
|
+
var directBox = direct === null ? null : chatScroller();
|
|
1066
|
+
if (direct !== null) {
|
|
1067
|
+
/** A merged row stands for several nodes. If this node does not render the sentence,
|
|
1068
|
+
* a folded sibling usually does — try those, then accept a text-less landing. */
|
|
1069
|
+
var ownMarks = collectMarks(direct, marks).length;
|
|
1070
|
+
var foldedAlts = Array.isArray(ctx.alts) ? ctx.alts : [];
|
|
1071
|
+
var taken = Array.isArray(ctx.primaries) ? ctx.primaries : [];
|
|
1072
|
+
if (ownMarks === 0) {
|
|
1073
|
+
for (var ai = 0; ai < foldedAlts.length && ai < 6; ai += 1) {
|
|
1074
|
+
var altRow = findNodeElement(foldedAlts[ai]);
|
|
1075
|
+
if (taken.indexOf(foldedAlts[ai]) >= 0) continue;
|
|
1076
|
+
if (altRow !== null && collectMarks(altRow, marks).length > 0) {
|
|
1077
|
+
trace('alt hit key=' + String(key) + ' -> ' + String(foldedAlts[ai]));
|
|
1078
|
+
return show(directBox, altRow, true, marks);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
trace('direct hit key=' + String(key) + ' marks=' + ownMarks);
|
|
1083
|
+
return show(directBox, direct, true, marks);
|
|
1084
|
+
}
|
|
1085
|
+
var box = chatScroller();
|
|
1086
|
+
if (box === null) { trace('env box=0 chain ' + scrollerChain()); return Promise.resolve(null); }
|
|
1087
|
+
var span = box.scrollHeight - box.clientHeight;
|
|
1088
|
+
if (span <= 0) return Promise.resolve(null);
|
|
1089
|
+
var startRows = box.querySelectorAll('[data-chat-node-key]');
|
|
1090
|
+
var keysInOrder = 0;
|
|
1091
|
+
for (var sr = 0; sr < startRows.length; sr += 1) if (rankOf !== null && rankOf.get(rowKeyOf(startRows[sr])) !== undefined) keysInOrder += 1;
|
|
1092
|
+
trace('env box=1 span=' + span + ' clientH=' + box.clientHeight + ' rows=' + startRows.length + ' keysInOrder=' + keysInOrder + ' wantRank=' + String(wantRank) + ' turn=' + String(turn) + ' orderLen=' + (Array.isArray(ctx.order) ? ctx.order.length : -1) + ' nodes=' + (Array.isArray(ctx.nodes) ? ctx.nodes.length : -1));
|
|
1093
|
+
traceVerbose('env chain ' + scrollerChain());
|
|
1094
|
+
|
|
1095
|
+
var lo = 0;
|
|
1096
|
+
var hi = span;
|
|
1097
|
+
var guess = Math.round(span / 2);
|
|
1098
|
+
var turnMax = Number(ctx.turnMax);
|
|
1099
|
+
if (wantRank !== undefined && Array.isArray(ctx.order) && ctx.order.length > 1) guess = Math.round(span * (wantRank / (ctx.order.length - 1)));
|
|
1100
|
+
else if (Number.isFinite(turn) && Number.isFinite(turnMax) && turnMax > 1) guess = Math.round(span * ((turn - 1) / (turnMax - 1)));
|
|
1101
|
+
else if (Number.isFinite(Number(ctx.index)) && Number(ctx.total) > 1) guess = Math.round(span * (Number(ctx.index) / (Number(ctx.total) - 1)));
|
|
1102
|
+
|
|
1103
|
+
var probeSeq = 0;
|
|
1104
|
+
var probe = function (offset) {
|
|
1105
|
+
probeSeq += 1;
|
|
1106
|
+
var seq = probeSeq;
|
|
1107
|
+
box.scrollTop = Math.max(0, Math.min(span, offset));
|
|
1108
|
+
var want = Math.round(box.scrollTop);
|
|
1109
|
+
return settle().then(function () {
|
|
1110
|
+
if (stale()) return null;
|
|
1111
|
+
var hit = findNodeElement(key);
|
|
1112
|
+
traceVerbose('probe#' + seq + ' want=' + want + ' now=' + Math.round(box.scrollTop) + ' keyFound=' + (hit !== null));
|
|
1113
|
+
if (hit !== null) return show(box, hit, true, marks);
|
|
1114
|
+
return undefined;
|
|
1115
|
+
});
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
var step = function (round) {
|
|
1119
|
+
if (stale() || round > 12) return Promise.resolve(null);
|
|
1120
|
+
return probe(guess).then(function (found) {
|
|
1121
|
+
if (found) return found;
|
|
1122
|
+
if (found === null || stale()) return null;
|
|
1123
|
+
var seenRows = box.querySelectorAll('[data-chat-node-key]');
|
|
1124
|
+
traceVerbose('mounted rows=' + seenRows.length + ' ' + rowKeysBrief(seenRows).join('|') + ' turns=' + JSON.stringify(mountedTurnWindow(box)) + ' rankWin=' + JSON.stringify(wantRank !== undefined ? mountedRankWindow(box, rankOf) : null));
|
|
1125
|
+
var unit = null;
|
|
1126
|
+
var low = null;
|
|
1127
|
+
var high = null;
|
|
1128
|
+
if (wantRank !== undefined) {
|
|
1129
|
+
var ranks = mountedRankWindow(box, rankOf);
|
|
1130
|
+
if (ranks !== null) { low = ranks.min; high = ranks.max; unit = wantRank; }
|
|
1131
|
+
}
|
|
1132
|
+
if (low === null && Number.isFinite(turn)) {
|
|
1133
|
+
var turns = mountedTurnWindow(box);
|
|
1134
|
+
if (turns !== null) { low = turns.min; high = turns.max; unit = turn; }
|
|
1135
|
+
}
|
|
1136
|
+
if (low === null) {
|
|
1137
|
+
var anyRows = box.querySelectorAll('[data-chat-node-key]');
|
|
1138
|
+
if (anyRows.length === 0) return null;
|
|
1139
|
+
return openThenFind(box, anyRows[0], key, marks, turn, wantRank, 0);
|
|
1140
|
+
}
|
|
1141
|
+
if (unit < low) hi = Math.min(hi, guess);
|
|
1142
|
+
else if (unit > high) lo = Math.max(lo, guess);
|
|
1143
|
+
else {
|
|
1144
|
+
var row = turnRowElement(turn);
|
|
1145
|
+
if (row === null) {
|
|
1146
|
+
var rowsNow = box.querySelectorAll('[data-chat-node-key]');
|
|
1147
|
+
row = rowsNow.length === 0 ? null : rowsNow[0];
|
|
1148
|
+
}
|
|
1149
|
+
if (row === null) return null;
|
|
1150
|
+
return openThenFind(box, row, key, marks, turn, wantRank, 0);
|
|
1151
|
+
}
|
|
1152
|
+
if (hi - lo <= 8) return null;
|
|
1153
|
+
var next = Math.round((lo + hi) / 2);
|
|
1154
|
+
if (next === guess) return null;
|
|
1155
|
+
guess = next;
|
|
1156
|
+
return step(round + 1);
|
|
1157
|
+
});
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
return step(0).then(function (outcome) {
|
|
1161
|
+
trace('end=' + (outcome === null ? 'none' : (outcome.exact ? 'exact' : 'approx')) + ' top=' + Math.round(box.scrollTop) + ' keyInDom=' + (findNodeElement(key) !== null));
|
|
1162
|
+
if (outcome || stale()) return outcome;
|
|
1163
|
+
var row = turnRowElement(turn);
|
|
1164
|
+
if (row === null) {
|
|
1165
|
+
var nearby = findByContent(box, marks, turn, wantRank);
|
|
1166
|
+
return nearby === null ? null : show(box, nearby, false, marks);
|
|
1167
|
+
}
|
|
1168
|
+
return openThenFind(box, row, key, marks, turn, wantRank, 0);
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
var STYLES = '.dsh-session-find-more{display:block;width:100%;margin:2px 0 0;padding:6px 8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa0a6);font-size:12px;text-align:left;cursor:pointer}.dsh-session-find-more:hover{background:var(--dsw-alias-bg-hover,rgba(255,255,255,.06))}.dsh-session-find-btn{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa0a6);cursor:pointer;padding:0}.dsh-session-find-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e8eaed)}.dsh-session-find-panel{position:fixed;top:88px;right:20px;width:min(820px,calc(100vw - 40px));max-height:calc(100vh - 120px);z-index:2147483000;display:flex;flex-direction:column;gap:8px;padding:12px;border-radius:14px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.28));background:var(--dsw-alias-bg-layer-2,#202124);box-shadow:0 16px 40px rgba(0,0,0,.36);color:var(--dsw-alias-label-primary,#e8eaed);font-size:13px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif}.dsh-session-find-row{display:flex;align-items:center;gap:6px}.dsh-session-find-input{flex:1;min-width:0;height:30px;padding:0 9px;border-radius:7px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.28));background:var(--dsw-alias-bg-base,transparent);color:inherit;font:inherit;outline:none}.dsh-session-find-input:focus{border-color:var(--dsw-alias-border-l1,rgba(127,127,127,.5))}.dsh-session-find-icon{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa0a6);cursor:pointer;padding:0}.dsh-session-find-icon:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16));color:var(--dsw-alias-label-primary,#e8eaed)}.dsh-session-find-icon[disabled]{opacity:.4;cursor:default}.dsh-session-find-meta{display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;color:var(--dsw-alias-label-tertiary,#8b9096)}.dsh-session-find-scope{display:flex;flex-wrap:wrap;align-items:center;gap:8px}.dsh-session-find-scope-btn{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.28));border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa0a6);font:inherit;font-size:11.5px;line-height:1.6;cursor:pointer}.dsh-session-find-scope-btn:hover{color:var(--dsw-alias-label-primary,#e8eaed)}.dsh-session-find-scope-btn[data-on=true]{border-color:rgba(96,165,250,.55);background:rgba(96,165,250,.14);color:#8ab4f8}.dsh-session-find-scope-dot{flex:none;width:7px;height:7px;border-radius:50%;background:currentColor;opacity:.55}.dsh-session-find-scope-btn[data-on=true] .dsh-session-find-scope-dot{opacity:1}.dsh-session-find-scope-hint{font-size:11px;color:var(--dsw-alias-label-tertiary,#8b9096)}.dsh-session-find-filters{display:flex;flex-wrap:wrap;align-items:center;gap:6px}.dsh-session-find-filter{border:1px solid transparent;border-radius:999px;background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.12));color:var(--dsw-alias-label-secondary,#9aa0a6);font:inherit;font-size:11.5px;line-height:19px;padding:0 9px;cursor:pointer}.dsh-session-find-filter:hover{color:var(--dsw-alias-label-primary,#e8eaed)}.dsh-session-find-filter[data-on=true]{background:var(--dsw-alias-bg-layer-2,#2b2d31);border-color:var(--dsw-alias-border-l1,rgba(127,127,127,.5));color:var(--dsw-alias-label-primary,#e8eaed);font-weight:600}.dsh-session-find-group{list-style:none;margin:8px 0 0;padding:0 4px;font-size:11px;line-height:16px;letter-spacing:.03em;color:var(--dsw-alias-label-tertiary,#8b9096)}.dsh-session-find-meta-link{border:0;background:transparent;padding:0;font:inherit;color:#8ab4f8;cursor:pointer;text-decoration:underline;text-underline-offset:2px}.dsh-session-find-meta-link:hover{color:#aecbfa}.dsh-session-find-list{margin:0;padding:0;list-style:none;max-height:min(64vh,760px);overflow:auto;display:flex;flex-direction:column;gap:4px}.dsh-session-find-hit{display:block;width:100%;text-align:left;border:1px solid transparent;border-radius:9px;background:transparent;color:inherit;cursor:pointer;padding:7px 10px 9px;font-size:13px;line-height:19px}.dsh-session-find-hit:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.16))}.dsh-session-find-hit[data-active=true]{background:var(--dsw-alias-interactive-bg-active,rgba(127,127,127,.24));border-color:var(--dsw-alias-border-l2,rgba(127,127,127,.28))}.dsh-session-find-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:3px}.dsh-session-find-chip{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-radius:999px;background:var(--dsw-alias-bg-layer-3,rgba(127,127,127,.14));color:var(--dsw-alias-label-secondary,#9aa0a6);font-size:11px;line-height:17px;padding:0 7px}.dsh-session-find-chip[data-role=user]{background:rgba(96,165,250,.18);color:#8ab4f8}.dsh-session-find-chip[data-role=assistant]{background:rgba(74,222,128,.16);color:#81c995}.dsh-session-find-chip[data-role=reasoning]{background:rgba(167,139,250,.18);color:#c4b5fd}.dsh-session-find-chip[data-role=toolCall]{background:rgba(251,191,36,.18);color:#f7c948}.dsh-session-find-chip[data-role=toolResult]{background:rgba(45,212,191,.18);color:#5eead4}.dsh-session-find-chip[data-role=system]{background:rgba(148,163,184,.18);color:#a8b3c1}.dsh-session-find-snippet{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;word-break:break-word;color:var(--dsw-alias-label-primary,#e8eaed)}.dsh-session-find-q{background-color:rgba(255,190,60,.42);border-radius:3px;color:inherit;font-weight:600}::highlight(dsh-session-find){background-color:rgba(255,190,60,.45);color:inherit}::highlight(dsh-session-find-all){background-color:rgba(255,190,60,.18)}.dsh-session-find-flash{animation:dsh-session-find-pulse 1.6s ease-out 1}@keyframes dsh-session-find-pulse{0%{background-color:var(--dsw-alias-interactive-bg-active,rgba(120,170,255,.35))}100%{background-color:transparent}}.dsh-session-find-bad{color:#f28b82}.dsh-session-find-opt-glyph{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:10.5px;opacity:.85}';
|
|
1173
|
+
|
|
1174
|
+
function makeFindBar(t, ui) {
|
|
1175
|
+
function iconButton(label, path, onClick, disabled) {
|
|
1176
|
+
return h('button', {
|
|
1177
|
+
type: 'button', className: 'dsh-session-find-icon', title: label, 'aria-label': label,
|
|
1178
|
+
disabled: disabled === true, onClick: onClick
|
|
1179
|
+
}, h('svg', { viewBox: '0 0 16 16', width: 15, height: 15, 'aria-hidden': true, fill: 'none', stroke: 'currentColor', strokeWidth: '1.6', strokeLinecap: 'round' }, path));
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
var ROLES = ['user', 'assistant', 'reasoning', 'toolCall', 'toolResult', 'system', 'other'];
|
|
1183
|
+
|
|
1184
|
+
function roleLabel(role) {
|
|
1185
|
+
if (role === 'user') return t('roleUser');
|
|
1186
|
+
if (role === 'assistant') return t('roleAssistant');
|
|
1187
|
+
if (role === 'reasoning') return t('roleReasoning');
|
|
1188
|
+
if (role === 'toolCall') return t('roleToolCall');
|
|
1189
|
+
if (role === 'toolResult') return t('roleToolResult');
|
|
1190
|
+
if (role === 'system') return t('roleSystem');
|
|
1191
|
+
return t('roleOther');
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function roleOfResult(result) {
|
|
1195
|
+
if (result === null || result === undefined) return 'other';
|
|
1196
|
+
if (result.role !== undefined && result.role !== null && String(result.role) !== '') return String(result.role);
|
|
1197
|
+
return roleOfKind(result.kind);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function HitButton(props) {
|
|
1201
|
+
var result = props.result;
|
|
1202
|
+
var hitLength = Number(result.len);
|
|
1203
|
+
if (!Number.isFinite(hitLength) || hitLength <= 0) hitLength = normalize(props.query).length;
|
|
1204
|
+
var needle = normalize(result.match !== undefined && result.match !== null && String(result.match).length > 0 ? result.match : props.query);
|
|
1205
|
+
var snippet = snippetOf(result.text, result.at, hitLength);
|
|
1206
|
+
var at = needle.length === 0 ? -1 : snippet.toLowerCase().indexOf(needle);
|
|
1207
|
+
var body = [];
|
|
1208
|
+
if (at < 0) body.push(snippet);
|
|
1209
|
+
else {
|
|
1210
|
+
body.push(snippet.slice(0, at));
|
|
1211
|
+
body.push(h('span', { key: 'q', className: 'dsh-session-find-q' }, snippet.slice(at, at + (hitLength > 0 ? hitLength : needle.length))));
|
|
1212
|
+
body.push(snippet.slice(at + (hitLength > 0 ? hitLength : needle.length)));
|
|
1213
|
+
}
|
|
1214
|
+
var role = roleOfResult(result);
|
|
1215
|
+
var chips = [];
|
|
1216
|
+
chips.push(h('span', { key: 'role', className: 'dsh-session-find-chip', 'data-role': role }, roleLabel(role)));
|
|
1217
|
+
if (Number.isFinite(Number(result.turn))) {
|
|
1218
|
+
chips.push(h('span', { key: 'turn', className: 'dsh-session-find-chip' }, t('turn', { n: Number(result.turn) })));
|
|
1219
|
+
}
|
|
1220
|
+
if (Number(result.duplicates) > 0) {
|
|
1221
|
+
chips.push(h('span', { key: 'dup', className: 'dsh-session-find-chip' }, t('sameText', { n: Number(result.duplicates) + 1 })));
|
|
1222
|
+
}
|
|
1223
|
+
if (Number(result.count) > 1) {
|
|
1224
|
+
chips.push(h('span', { key: 'count', className: 'dsh-session-find-chip' }, t('hits', { n: Number(result.count) })));
|
|
1225
|
+
}
|
|
1226
|
+
return h('button', {
|
|
1227
|
+
type: 'button', className: 'dsh-session-find-hit', 'data-active': props.active ? 'true' : 'false',
|
|
1228
|
+
onClick: function () { props.onPick(props.index); }
|
|
1229
|
+
}, h('span', { className: 'dsh-session-find-chips' }, chips),
|
|
1230
|
+
h('span', { className: 'dsh-session-find-snippet' }, body));
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return function SessionFind(props) {
|
|
1234
|
+
var useChat = props.useChat;
|
|
1235
|
+
var order = typeof useChat === 'function'
|
|
1236
|
+
? useChat(function (state) { return state !== null && typeof state === 'object' ? state.order : undefined; })
|
|
1237
|
+
: undefined;
|
|
1238
|
+
var nodeStore = typeof useChat === 'function'
|
|
1239
|
+
? useChat(function (state) { return state !== null && typeof state === 'object' ? state.nodes : undefined; })
|
|
1240
|
+
: undefined;
|
|
1241
|
+
var legacyNodes = typeof useChat === 'function'
|
|
1242
|
+
? useChat(function (state) { return state !== null && typeof state === 'object' && state.legacy !== undefined ? state.legacy.nodes : undefined; })
|
|
1243
|
+
: undefined;
|
|
1244
|
+
var nodesCacheRef = React.useRef({ order: null, store: null, legacy: null, nodes: [], turns: [] });
|
|
1245
|
+
var nodeCache = nodesCacheRef.current;
|
|
1246
|
+
if (nodeCache.order !== order || nodeCache.store !== nodeStore || nodeCache.legacy !== legacyNodes) {
|
|
1247
|
+
var canonical = [];
|
|
1248
|
+
if (Array.isArray(order) && nodeStore !== null && nodeStore !== undefined && typeof nodeStore.get === 'function') {
|
|
1249
|
+
for (var nodeIndex = 0; nodeIndex < order.length; nodeIndex += 1) {
|
|
1250
|
+
var canonicalNode = nodeStore.get(order[nodeIndex]);
|
|
1251
|
+
if (canonicalNode !== null && canonicalNode !== undefined) canonical.push(canonicalNode);
|
|
1252
|
+
}
|
|
1253
|
+
} else if (Array.isArray(legacyNodes)) canonical = legacyNodes;
|
|
1254
|
+
var turnMarks = new Array(canonical.length);
|
|
1255
|
+
var userTurns = 0;
|
|
1256
|
+
for (var turnIndex = 0; turnIndex < canonical.length; turnIndex += 1) {
|
|
1257
|
+
var turnNode = canonical[turnIndex];
|
|
1258
|
+
if (turnNode !== null && turnNode !== undefined && String(turnNode.kind) === 'user') userTurns += 1;
|
|
1259
|
+
turnMarks[turnIndex] = userTurns;
|
|
1260
|
+
}
|
|
1261
|
+
nodeCache = { order: order, store: nodeStore, legacy: legacyNodes, nodes: canonical, turns: turnMarks };
|
|
1262
|
+
nodesCacheRef.current = nodeCache;
|
|
1263
|
+
}
|
|
1264
|
+
var nodes = nodeCache.nodes;
|
|
1265
|
+
var revisionState = React.useState(0);
|
|
1266
|
+
var setRevision = revisionState[1];
|
|
1267
|
+
var queryState = React.useState(function () {
|
|
1268
|
+
try {
|
|
1269
|
+
var remembered = window.localStorage.getItem(LAST_QUERY_KEY);
|
|
1270
|
+
return typeof remembered === 'string' ? remembered : '';
|
|
1271
|
+
} catch (error) { return ''; }
|
|
1272
|
+
});
|
|
1273
|
+
var query = queryState[0];
|
|
1274
|
+
var setQuery = queryState[1];
|
|
1275
|
+
var resultState = React.useState({ items: [], truncated: false });
|
|
1276
|
+
var results = resultState[0];
|
|
1277
|
+
var setResults = resultState[1];
|
|
1278
|
+
var activeState = React.useState(0);
|
|
1279
|
+
var active = activeState[0];
|
|
1280
|
+
var setActive = activeState[1];
|
|
1281
|
+
var noticeState = React.useState('');
|
|
1282
|
+
var payloadState = React.useState(false);
|
|
1283
|
+
var notice = noticeState[0];
|
|
1284
|
+
var setNotice = noticeState[1];
|
|
1285
|
+
var showPayload = payloadState[0];
|
|
1286
|
+
var setPayload = payloadState[1];
|
|
1287
|
+
var filterState = React.useState('');
|
|
1288
|
+
var roleFilterOn = filterState[0];
|
|
1289
|
+
var setFilter = filterState[1];
|
|
1290
|
+
var proseState = React.useState(function () {
|
|
1291
|
+
try {
|
|
1292
|
+
var stored = window.localStorage.getItem('dsh-session-find:prose-only');
|
|
1293
|
+
return stored === null ? true : stored === '1';
|
|
1294
|
+
} catch (error) { return true; }
|
|
1295
|
+
});
|
|
1296
|
+
var proseOnly = proseState[0];
|
|
1297
|
+
var setProseOnly = proseState[1];
|
|
1298
|
+
var outsideState = React.useState(0);
|
|
1299
|
+
var outside = outsideState[0];
|
|
1300
|
+
var setOutside = outsideState[1];
|
|
1301
|
+
var optionsState = React.useState(readSearchOptions);
|
|
1302
|
+
var options = optionsState[0];
|
|
1303
|
+
var setOptions = optionsState[1];
|
|
1304
|
+
var patternState = React.useState('');
|
|
1305
|
+
var patternBad = patternState[0];
|
|
1306
|
+
var setPatternBad = patternState[1];
|
|
1307
|
+
var historyState = React.useState(readHistory);
|
|
1308
|
+
var history = historyState[0];
|
|
1309
|
+
var setHistory = historyState[1];
|
|
1310
|
+
var historyPos = React.useRef(-1);
|
|
1311
|
+
var rememberQuery = function (value) {
|
|
1312
|
+
var text = String(value === undefined || value === null ? '' : value).trim();
|
|
1313
|
+
if (text.length === 0) return;
|
|
1314
|
+
var list = readHistory().filter(function (item) { return item !== text; });
|
|
1315
|
+
list.unshift(text);
|
|
1316
|
+
if (list.length > 12) list = list.slice(0, 12);
|
|
1317
|
+
try { window.localStorage.setItem(HISTORY_KEY, JSON.stringify(list)); } catch (error) { /* private mode */ }
|
|
1318
|
+
setHistory(list);
|
|
1319
|
+
};
|
|
1320
|
+
var inputRef = React.useRef(null);
|
|
1321
|
+
var listRef = React.useRef(null);
|
|
1322
|
+
var navRef = React.useRef([]);
|
|
1323
|
+
var panelRef = React.useRef(null);
|
|
1324
|
+
var triggerRef = React.useRef(null);
|
|
1325
|
+
var open = ui.isOpen();
|
|
1326
|
+
|
|
1327
|
+
React.useEffect(function () {
|
|
1328
|
+
var stop = ui.subscribe(function () { setRevision(function (value) { return value + 1; }); });
|
|
1329
|
+
setRevision(function (value) { return value + 1; });
|
|
1330
|
+
return stop;
|
|
1331
|
+
}, []);
|
|
1332
|
+
|
|
1333
|
+
React.useEffect(function () {
|
|
1334
|
+
if (!open) return undefined;
|
|
1335
|
+
var timer = window.setTimeout(function () {
|
|
1336
|
+
var queryLength = query.trim().length;
|
|
1337
|
+
var flags = { regex: options.regex === true, matchCase: options.matchCase === true, wholeWord: options.wholeWord === true };
|
|
1338
|
+
if (proseOnly) {
|
|
1339
|
+
var allHits = searchNodes(nodes, query, MAX_RESULTS, false, nodeCache.turns, flags);
|
|
1340
|
+
var allTotal = 0;
|
|
1341
|
+
for (var ai = 0; ai < allHits.items.length; ai += 1) allTotal += allHits.items[ai].count;
|
|
1342
|
+
var proseHits = dedupeHits(searchNodes(nodes, query, MAX_RESULTS, true, nodeCache.turns, flags), queryLength);
|
|
1343
|
+
var proseTotal = 0;
|
|
1344
|
+
for (var pj = 0; pj < proseHits.items.length; pj += 1) proseTotal += proseHits.items[pj].count;
|
|
1345
|
+
setOutside(allTotal > proseTotal ? allTotal - proseTotal : 0);
|
|
1346
|
+
setPatternBad(allHits.invalid === true ? String(allHits.reason || '') : '');
|
|
1347
|
+
setResults(proseHits);
|
|
1348
|
+
} else {
|
|
1349
|
+
var all = searchNodes(nodes, query, MAX_RESULTS, false, nodeCache.turns, flags);
|
|
1350
|
+
setOutside(0);
|
|
1351
|
+
setPatternBad(all.invalid === true ? String(all.reason || '') : '');
|
|
1352
|
+
setResults(dedupeHits(all, queryLength));
|
|
1353
|
+
}
|
|
1354
|
+
setActive(0);
|
|
1355
|
+
setFilter('');
|
|
1356
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
1357
|
+
return function () { window.clearTimeout(timer); };
|
|
1358
|
+
}, [open, query, nodes, proseOnly, options.regex, options.matchCase, options.wholeWord]);
|
|
1359
|
+
|
|
1360
|
+
React.useEffect(function () {
|
|
1361
|
+
if (!open) return undefined;
|
|
1362
|
+
var text = query.trim();
|
|
1363
|
+
if (text.length === 0) return undefined;
|
|
1364
|
+
try { window.localStorage.setItem(LAST_QUERY_KEY, text); } catch (error) { /* private mode */ }
|
|
1365
|
+
return undefined;
|
|
1366
|
+
}, [open, query]);
|
|
1367
|
+
|
|
1368
|
+
React.useEffect(function () {
|
|
1369
|
+
if (open) return undefined;
|
|
1370
|
+
rememberQuery(query);
|
|
1371
|
+
historyPos.current = -1;
|
|
1372
|
+
return undefined;
|
|
1373
|
+
}, [open]);
|
|
1374
|
+
|
|
1375
|
+
React.useEffect(function () {
|
|
1376
|
+
if (open) return undefined;
|
|
1377
|
+
clearHighlight();
|
|
1378
|
+
clearAllMarks();
|
|
1379
|
+
return undefined;
|
|
1380
|
+
}, [open]);
|
|
1381
|
+
|
|
1382
|
+
React.useEffect(function () {
|
|
1383
|
+
if (!open) return undefined;
|
|
1384
|
+
paintAllMarks(query.trim());
|
|
1385
|
+
return undefined;
|
|
1386
|
+
}, [open, query, results]);
|
|
1387
|
+
|
|
1388
|
+
React.useEffect(function () {
|
|
1389
|
+
if (!open) return undefined;
|
|
1390
|
+
var timer = window.setTimeout(function () {
|
|
1391
|
+
var input = inputRef.current;
|
|
1392
|
+
if (input !== null) { input.focus(); input.select(); }
|
|
1393
|
+
}, 0);
|
|
1394
|
+
return function () { window.clearTimeout(timer); };
|
|
1395
|
+
}, [open]);
|
|
1396
|
+
|
|
1397
|
+
React.useEffect(function () {
|
|
1398
|
+
if (!open || typeof document === 'undefined') return undefined;
|
|
1399
|
+
var onPointerDown = function (event) {
|
|
1400
|
+
var target = event.target;
|
|
1401
|
+
var panelElement = panelRef.current;
|
|
1402
|
+
var triggerElement = triggerRef.current;
|
|
1403
|
+
if (panelElement !== null && typeof panelElement.contains === 'function' && panelElement.contains(target)) return;
|
|
1404
|
+
if (triggerElement !== null && typeof triggerElement.contains === 'function' && triggerElement.contains(target)) return;
|
|
1405
|
+
ui.setOpen(false);
|
|
1406
|
+
};
|
|
1407
|
+
document.addEventListener('pointerdown', onPointerDown, true);
|
|
1408
|
+
return function () { document.removeEventListener('pointerdown', onPointerDown, true); };
|
|
1409
|
+
}, [open]);
|
|
1410
|
+
|
|
1411
|
+
React.useEffect(function () {
|
|
1412
|
+
if (!open) return;
|
|
1413
|
+
var list = listRef.current;
|
|
1414
|
+
if (list === null) return;
|
|
1415
|
+
var rows = list.querySelectorAll('[data-row="true"]');
|
|
1416
|
+
var row = rows.length === 0 ? undefined : rows[Math.min(active, rows.length - 1)];
|
|
1417
|
+
if (row === undefined || row === null) return;
|
|
1418
|
+
var listRect = list.getBoundingClientRect();
|
|
1419
|
+
var rowRect = row.getBoundingClientRect();
|
|
1420
|
+
if (rowRect.bottom > listRect.bottom) list.scrollTop = list.scrollTop + (rowRect.bottom - listRect.bottom) + 4;
|
|
1421
|
+
else if (rowRect.top < listRect.top) list.scrollTop = list.scrollTop - (listRect.top - rowRect.top) - 4;
|
|
1422
|
+
}, [active, open, results, roleFilterOn]);
|
|
1423
|
+
|
|
1424
|
+
var goTo = function (index) {
|
|
1425
|
+
var items = navRef.current;
|
|
1426
|
+
if (items.length === 0) return;
|
|
1427
|
+
var next = ((index % items.length) + items.length) % items.length;
|
|
1428
|
+
setActive(next);
|
|
1429
|
+
setNotice('');
|
|
1430
|
+
var hit = items[next];
|
|
1431
|
+
var turnMax = 0;
|
|
1432
|
+
if (Array.isArray(nodeCache.turns) && nodeCache.turns.length > 0) {
|
|
1433
|
+
var lastTurn = Number(nodeCache.turns[nodeCache.turns.length - 1]);
|
|
1434
|
+
if (Number.isFinite(lastTurn)) turnMax = lastTurn;
|
|
1435
|
+
}
|
|
1436
|
+
var context = {
|
|
1437
|
+
index: hit.index,
|
|
1438
|
+
total: Array.isArray(nodes) ? nodes.length : 0,
|
|
1439
|
+
turn: hit.turn,
|
|
1440
|
+
turnMax: turnMax,
|
|
1441
|
+
nodes: nodes,
|
|
1442
|
+
needle: hit.match !== undefined && hit.match !== null && String(hit.match).length >= 2
|
|
1443
|
+
? String(hit.match)
|
|
1444
|
+
: needleOf(hit.text, hit.at, Number(hit.len) > 0 ? Number(hit.len) : normalize(query).length),
|
|
1445
|
+
alts: Array.isArray(hit.alts) ? hit.alts : [],
|
|
1446
|
+
primaries: items.map(function (row) { return row.key; }),
|
|
1447
|
+
query: query.trim(),
|
|
1448
|
+
order: order
|
|
1449
|
+
};
|
|
1450
|
+
trace('jump key=' + String(hit.key) + ' kind=' + String(hit.kind) + ' turn=' + String(hit.turn) + ' idx=' + String(hit.index) + ' q=' + JSON.stringify(String(query.trim())));
|
|
1451
|
+
revealNode(hit.key, context).then(function (outcome) {
|
|
1452
|
+
paintAllMarks(query.trim());
|
|
1453
|
+
if (outcome === null) { setNotice(t('jumpFailed')); return; }
|
|
1454
|
+
if (outcome.exact === false) { setNotice(t('jumpApproximate')); return; }
|
|
1455
|
+
setNotice(outcome.highlighted === false ? t('jumpHidden') : '');
|
|
1456
|
+
});
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
/** F3 / Ctrl+G reach the panel through the global key handler below; it calls these. */
|
|
1460
|
+
ui.next = function () { goTo(activeIndex + 1); };
|
|
1461
|
+
ui.prev = function () { goTo(activeIndex - 1); };
|
|
1462
|
+
|
|
1463
|
+
var onInputKeyDown = function (event) {
|
|
1464
|
+
if (event.key === 'Enter') { event.preventDefault(); goTo(activeIndex + (event.shiftKey ? -1 : 1)); return; }
|
|
1465
|
+
if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); ui.setOpen(false); return; }
|
|
1466
|
+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
|
1467
|
+
if (history.length === 0) return;
|
|
1468
|
+
var input = inputRef.current;
|
|
1469
|
+
if (input === null) return;
|
|
1470
|
+
var caret = Number(input.selectionStart);
|
|
1471
|
+
var caretEnd = Number(input.selectionEnd);
|
|
1472
|
+
if (caret !== caretEnd) return;
|
|
1473
|
+
if (event.key === 'ArrowUp' && caret !== 0) return;
|
|
1474
|
+
if (event.key === 'ArrowDown' && caret !== String(query).length) return;
|
|
1475
|
+
event.preventDefault();
|
|
1476
|
+
var step = event.key === 'ArrowUp' ? 1 : -1;
|
|
1477
|
+
var pos = historyPos.current < 0 ? (step > 0 ? 0 : history.length - 1) : ((historyPos.current + step) % history.length + history.length) % history.length;
|
|
1478
|
+
historyPos.current = pos;
|
|
1479
|
+
setQuery(history[pos]);
|
|
1480
|
+
}
|
|
1481
|
+
};
|
|
1482
|
+
|
|
1483
|
+
var trigger = h('button', {
|
|
1484
|
+
ref: triggerRef,
|
|
1485
|
+
type: 'button', className: 'dsh-session-find-btn', title: t('open') + ' (Ctrl+F)', 'aria-label': t('open'),
|
|
1486
|
+
onClick: function () { ui.setOpen(!open); }
|
|
1487
|
+
}, h('svg', { viewBox: '0 0 16 16', width: 15, height: 15, 'aria-hidden': true, fill: 'none', stroke: 'currentColor', strokeWidth: '1.5' },
|
|
1488
|
+
h('circle', { cx: 7, cy: 7, r: 4.5 }), h('path', { d: 'M10.5 10.5 L14 14', strokeLinecap: 'round' })));
|
|
1489
|
+
|
|
1490
|
+
if (!open) return h(React.Fragment, null, h('style', { key: 's' }, STYLES), trigger);
|
|
1491
|
+
|
|
1492
|
+
var total = 0;
|
|
1493
|
+
for (var i = 0; i < results.items.length; i += 1) total += results.items[i].count;
|
|
1494
|
+
/** Payload-only hits (tool arguments, mirrors of an answer) never render as prose, so
|
|
1495
|
+
* listing them beside the visible message reads as "one result repeated several times". */
|
|
1496
|
+
var proseItems = [];
|
|
1497
|
+
var payloadCount = 0;
|
|
1498
|
+
for (var pi = 0; pi < results.items.length; pi += 1) {
|
|
1499
|
+
var row = results.items[pi];
|
|
1500
|
+
if (isPayloadKind(row.kind) || looksLikeSource(row)) payloadCount += 1;
|
|
1501
|
+
else proseItems.push(row);
|
|
1502
|
+
}
|
|
1503
|
+
var payloadOnly = proseItems.length === 0 && payloadCount > 0;
|
|
1504
|
+
var shown = showPayload || payloadOnly ? results.items : proseItems;
|
|
1505
|
+
var hiddenPayload = shown === results.items ? 0 : payloadCount;
|
|
1506
|
+
|
|
1507
|
+
/** Group the visible rows by where they came from, so the bar can show counts per
|
|
1508
|
+
* source and the user can filter down to one of them. */
|
|
1509
|
+
var roleCounts = {};
|
|
1510
|
+
var rolePresent = [];
|
|
1511
|
+
for (var ri = 0; ri < shown.length; ri += 1) {
|
|
1512
|
+
var rowRole = roleOfResult(shown[ri]);
|
|
1513
|
+
if (roleCounts[rowRole] === undefined) { roleCounts[rowRole] = 0; rolePresent.push(rowRole); }
|
|
1514
|
+
roleCounts[rowRole] += 1;
|
|
1515
|
+
}
|
|
1516
|
+
var roleFilter = roleFilterOn !== '' && roleCounts[roleFilterOn] !== undefined ? roleFilterOn : '';
|
|
1517
|
+
var listed = roleFilter === '' ? shown : shown.filter(function (item) { return roleOfResult(item) === roleFilter; });
|
|
1518
|
+
navRef.current = listed;
|
|
1519
|
+
var activeIndex = listed.length === 0 ? 0 : Math.min(active, listed.length - 1);
|
|
1520
|
+
var meta = query.trim().length === 0
|
|
1521
|
+
? t('empty')
|
|
1522
|
+
: (patternBad !== ''
|
|
1523
|
+
? t('regexInvalid')
|
|
1524
|
+
: (results.items.length === 0
|
|
1525
|
+
? t('none')
|
|
1526
|
+
: t('count', { n: total }) + (results.folded > 0 ? t('merged', { n: results.folded }) : '') + (payloadOnly ? t(proseOnly ? 'payloadOnlyProse' : 'payloadOnly') : '') + (results.truncated ? ' ' + t('limited', { n: MAX_RESULTS }) : '')));
|
|
1527
|
+
var scopeSuffix = null;
|
|
1528
|
+
if (proseOnly && query.trim().length > 0 && outside > 0) {
|
|
1529
|
+
scopeSuffix = h(React.Fragment, null, ' · ', h('button', {
|
|
1530
|
+
type: 'button', className: 'dsh-session-find-meta-link', title: t('scopeSwitchAll'),
|
|
1531
|
+
onClick: function () { setScope(false); }
|
|
1532
|
+
}, results.items.length === 0 ? t('scopeMiss', { n: outside }) : t('scopeExcluded', { n: outside })));
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
var setScope = function (next) {
|
|
1536
|
+
setProseOnly(next);
|
|
1537
|
+
setFilter('');
|
|
1538
|
+
setActive(0);
|
|
1539
|
+
try { window.localStorage.setItem(PROSE_KEY, next ? '1' : '0'); } catch (error) { /* private mode */ }
|
|
1540
|
+
};
|
|
1541
|
+
var scope = h('div', { className: 'dsh-session-find-scope' },
|
|
1542
|
+
h('button', {
|
|
1543
|
+
type: 'button', className: 'dsh-session-find-scope-btn', 'data-on': proseOnly ? 'true' : 'false',
|
|
1544
|
+
title: t('scopeHint'),
|
|
1545
|
+
onClick: function () { setScope(!proseOnly); }
|
|
1546
|
+
}, h('span', { className: 'dsh-session-find-scope-dot' }), t(proseOnly ? 'scopeProse' : 'scopeAll')),
|
|
1547
|
+
h('span', { className: 'dsh-session-find-scope-hint' }, t('scopeHint')));
|
|
1548
|
+
|
|
1549
|
+
var setOption = function (name, next) {
|
|
1550
|
+
var merged = {
|
|
1551
|
+
regex: options.regex === true,
|
|
1552
|
+
matchCase: options.matchCase === true,
|
|
1553
|
+
wholeWord: options.wholeWord === true
|
|
1554
|
+
};
|
|
1555
|
+
merged[name] = next === true;
|
|
1556
|
+
setOptions(merged);
|
|
1557
|
+
setActive(0);
|
|
1558
|
+
setFilter('');
|
|
1559
|
+
try { window.localStorage.setItem(OPTIONS_KEY, JSON.stringify(merged)); } catch (error) { /* private mode */ }
|
|
1560
|
+
};
|
|
1561
|
+
var optionChip = function (name, glyph, label, hint) {
|
|
1562
|
+
var on = options[name] === true;
|
|
1563
|
+
return h('button', {
|
|
1564
|
+
key: name, type: 'button', className: 'dsh-session-find-scope-btn', 'data-on': on ? 'true' : 'false',
|
|
1565
|
+
title: hint, 'aria-label': hint, 'aria-pressed': on ? 'true' : 'false',
|
|
1566
|
+
onClick: function () { setOption(name, on !== true); }
|
|
1567
|
+
}, h('span', { className: 'dsh-session-find-opt-glyph' }, glyph), label);
|
|
1568
|
+
};
|
|
1569
|
+
var optionsRow = h('div', { className: 'dsh-session-find-scope' },
|
|
1570
|
+
optionChip('regex', '.*', t('optRegex'), t('optRegexHint')),
|
|
1571
|
+
optionChip('matchCase', 'Aa', t('optMatchCase'), t('optMatchCaseHint')),
|
|
1572
|
+
optionChip('wholeWord', 'ab', t('optWholeWord'), t('optWholeWordHint')),
|
|
1573
|
+
h('span', {
|
|
1574
|
+
className: patternBad !== '' ? 'dsh-session-find-scope-hint dsh-session-find-bad' : 'dsh-session-find-scope-hint',
|
|
1575
|
+
title: patternBad !== '' ? patternBad : undefined
|
|
1576
|
+
}, patternBad !== '' ? t('regexInvalid') + ' · ' + t('regexHintBad') : t('optHint')));
|
|
1577
|
+
|
|
1578
|
+
var hitLengthOf = function (item) {
|
|
1579
|
+
var value = Number(item.len);
|
|
1580
|
+
return Number.isFinite(value) && value > 0 ? value : normalize(query).length;
|
|
1581
|
+
};
|
|
1582
|
+
var exportMarkdown = function () {
|
|
1583
|
+
var lines = [];
|
|
1584
|
+
var typed = query.trim();
|
|
1585
|
+
lines.push('# ' + (typed.length === 0 ? t('empty') : t('exportTitle', { q: typed })));
|
|
1586
|
+
lines.push('');
|
|
1587
|
+
var bits = [proseOnly ? t('scopeProse') : t('scopeAll')];
|
|
1588
|
+
if (options.regex === true) bits.push(t('optRegex'));
|
|
1589
|
+
if (options.matchCase === true) bits.push(t('optMatchCase'));
|
|
1590
|
+
if (options.wholeWord === true) bits.push(t('optWholeWord'));
|
|
1591
|
+
bits.push(new Date().toLocaleString());
|
|
1592
|
+
lines.push('> ' + bits.join(' · '));
|
|
1593
|
+
lines.push('> ' + t('exportMeta', { n: total, rows: listed.length }));
|
|
1594
|
+
var lastTurn = null;
|
|
1595
|
+
for (var i = 0; i < listed.length; i += 1) {
|
|
1596
|
+
var item = listed[i];
|
|
1597
|
+
var turnNo = Number(item.turn);
|
|
1598
|
+
if (Number.isFinite(turnNo) && turnNo > 0 && turnNo !== lastTurn) {
|
|
1599
|
+
lastTurn = turnNo;
|
|
1600
|
+
lines.push('');
|
|
1601
|
+
lines.push('### ' + t('turn', { n: turnNo }));
|
|
1602
|
+
lines.push('');
|
|
1603
|
+
}
|
|
1604
|
+
var chips = '**' + roleLabel(roleOfResult(item)) + '**';
|
|
1605
|
+
if (Number(item.count) > 1) chips += ' · ' + t('hits', { n: Number(item.count) });
|
|
1606
|
+
if (Number(item.duplicates) > 0) chips += ' · ' + t('sameText', { n: Number(item.duplicates) + 1 });
|
|
1607
|
+
lines.push('- ' + chips + ' — ' + snippetOf(String(item.text), Number(item.at), hitLengthOf(item)));
|
|
1608
|
+
}
|
|
1609
|
+
return lines.join(LF) + LF;
|
|
1610
|
+
};
|
|
1611
|
+
var copyFallback = function (value) {
|
|
1612
|
+
try {
|
|
1613
|
+
var area = document.createElement('textarea');
|
|
1614
|
+
area.value = value;
|
|
1615
|
+
area.setAttribute('readonly', 'readonly');
|
|
1616
|
+
area.style.position = 'fixed';
|
|
1617
|
+
area.style.top = '-1000px';
|
|
1618
|
+
document.body.appendChild(area);
|
|
1619
|
+
area.select();
|
|
1620
|
+
var ok = document.execCommand('copy');
|
|
1621
|
+
document.body.removeChild(area);
|
|
1622
|
+
return ok === true;
|
|
1623
|
+
} catch (error) { return false; }
|
|
1624
|
+
};
|
|
1625
|
+
var copyResults = function () {
|
|
1626
|
+
var value = exportMarkdown();
|
|
1627
|
+
var done = function (ok) { setNotice(ok ? t('exportCopied') : t('exportFailed')); };
|
|
1628
|
+
try {
|
|
1629
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard !== undefined && typeof navigator.clipboard.writeText === 'function') {
|
|
1630
|
+
navigator.clipboard.writeText(value).then(function () { done(true); }, function () { done(copyFallback(value)); });
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
} catch (error) { /* fall through */ }
|
|
1634
|
+
done(copyFallback(value));
|
|
1635
|
+
};
|
|
1636
|
+
var downloadResults = function () {
|
|
1637
|
+
var value = exportMarkdown();
|
|
1638
|
+
var stamp = new Date();
|
|
1639
|
+
var pad = function (n) { return n < 10 ? '0' + String(n) : String(n); };
|
|
1640
|
+
var slug = query.trim().replace(/[\\/:*?"<>|\s]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
1641
|
+
var name = 'session-find-' + (slug.length > 0 ? slug + '-' : '') + String(stamp.getFullYear()) + pad(stamp.getMonth() + 1) + pad(stamp.getDate()) + '-' + pad(stamp.getHours()) + pad(stamp.getMinutes()) + '.md';
|
|
1642
|
+
var fallback = function () {
|
|
1643
|
+
try {
|
|
1644
|
+
var blob = new Blob([value], { type: 'text/markdown;charset=utf-8' });
|
|
1645
|
+
var url = URL.createObjectURL(blob);
|
|
1646
|
+
var anchor = document.createElement('a');
|
|
1647
|
+
anchor.href = url;
|
|
1648
|
+
anchor.download = name;
|
|
1649
|
+
anchor.style.display = 'none';
|
|
1650
|
+
document.body.appendChild(anchor);
|
|
1651
|
+
anchor.click();
|
|
1652
|
+
document.body.removeChild(anchor);
|
|
1653
|
+
window.setTimeout(function () { try { URL.revokeObjectURL(url); } catch (error) { /* ignore */ } }, 4000);
|
|
1654
|
+
setNotice(t('exportPrompt'));
|
|
1655
|
+
} catch (error) { setNotice(t('exportFailed')); }
|
|
1656
|
+
};
|
|
1657
|
+
fallback();
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
var filters = query.trim().length === 0 || rolePresent.length === 0 ? null : h('div', { className: 'dsh-session-find-filters' },
|
|
1661
|
+
h('button', {
|
|
1662
|
+
type: 'button', className: 'dsh-session-find-filter', 'data-on': roleFilter === '' ? 'true' : 'false',
|
|
1663
|
+
onClick: function () { setFilter(''); setActive(0); }
|
|
1664
|
+
}, t('filterAll') + ' ' + String(shown.length)),
|
|
1665
|
+
rolePresent.map(function (name) {
|
|
1666
|
+
return h('button', {
|
|
1667
|
+
key: name, type: 'button', className: 'dsh-session-find-filter',
|
|
1668
|
+
'data-on': roleFilter === name ? 'true' : 'false',
|
|
1669
|
+
onClick: function () { setFilter(name); setActive(0); }
|
|
1670
|
+
}, roleLabel(name) + ' ' + String(roleCounts[name]));
|
|
1671
|
+
}));
|
|
1672
|
+
|
|
1673
|
+
var rows = [];
|
|
1674
|
+
var lastGroup = -1;
|
|
1675
|
+
listed.forEach(function (item, index) {
|
|
1676
|
+
var turnNo = Number(item.turn);
|
|
1677
|
+
if (Number.isFinite(turnNo) && turnNo > 0 && turnNo !== lastGroup) {
|
|
1678
|
+
lastGroup = turnNo;
|
|
1679
|
+
rows.push(h('li', { key: 'group-' + String(turnNo), className: 'dsh-session-find-group', role: 'presentation' }, t('groupTurn', { n: turnNo })));
|
|
1680
|
+
}
|
|
1681
|
+
rows.push(h('li', { key: (item.key === undefined ? String(index) : String(item.key)) + ':' + String(index), 'data-row': 'true' },
|
|
1682
|
+
h(HitButton, { result: item, index: index, active: index === activeIndex, query: query, onPick: goTo })));
|
|
1683
|
+
});
|
|
1684
|
+
|
|
1685
|
+
var panel = h('div', {
|
|
1686
|
+
ref: panelRef, className: 'dsh-session-find-panel', role: 'dialog', 'aria-label': t('open'),
|
|
1687
|
+
onKeyDown: function (event) {
|
|
1688
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
|
|
1689
|
+
if (event.target === inputRef.current) return;
|
|
1690
|
+
if (listed.length === 0) return;
|
|
1691
|
+
event.preventDefault();
|
|
1692
|
+
goTo(activeIndex + (event.key === 'ArrowDown' ? 1 : -1));
|
|
1693
|
+
}
|
|
1694
|
+
},
|
|
1695
|
+
h('div', { className: 'dsh-session-find-row' },
|
|
1696
|
+
h('input', {
|
|
1697
|
+
ref: inputRef, className: 'dsh-session-find-input', type: 'text', value: query,
|
|
1698
|
+
placeholder: t('placeholder'), 'aria-label': t('placeholder'),
|
|
1699
|
+
onChange: function (event) { historyPos.current = -1; setQuery(event.target.value); },
|
|
1700
|
+
onKeyDown: onInputKeyDown
|
|
1701
|
+
}),
|
|
1702
|
+
iconButton(t('prev'), h('path', { d: 'M4 10 L8 6 L12 10' }), function () { goTo(activeIndex - 1); }, listed.length === 0),
|
|
1703
|
+
iconButton(t('next'), h('path', { d: 'M4 6 L8 10 L12 6' }), function () { goTo(activeIndex + 1); }, listed.length === 0),
|
|
1704
|
+
iconButton(t('exportCopy'), h('g', null,
|
|
1705
|
+
h('rect', { x: 5.5, y: 5.5, width: 7, height: 7, rx: 1.5 }),
|
|
1706
|
+
h('path', { d: 'M3.5 10.5 V3.5 a1 1 0 0 1 1 -1 h6' })), copyResults, listed.length === 0),
|
|
1707
|
+
iconButton(t('exportDownload'), h('g', null,
|
|
1708
|
+
h('path', { d: 'M8 2.5 V9.5 M5 7 L8 10 L11 7' }),
|
|
1709
|
+
h('path', { d: 'M3 12.5 h10' })), downloadResults, listed.length === 0),
|
|
1710
|
+
iconButton(t('close'), h('path', { d: 'M4 4 L12 12 M12 4 L4 12' }), function () { ui.setOpen(false); }, false)),
|
|
1711
|
+
h('div', { className: 'dsh-session-find-meta' },
|
|
1712
|
+
h('span', null, meta, scopeSuffix),
|
|
1713
|
+
h('span', null, notice !== '' ? notice : t('hint'))),
|
|
1714
|
+
scope,
|
|
1715
|
+
optionsRow,
|
|
1716
|
+
filters,
|
|
1717
|
+
payloadCount === 0 || payloadOnly ? null : h('button', {
|
|
1718
|
+
type: 'button', className: 'dsh-session-find-more',
|
|
1719
|
+
onClick: function () { setPayload(!showPayload); }
|
|
1720
|
+
}, hiddenPayload > 0 ? t(proseOnly ? 'payloadMoreProse' : 'payloadMore', { n: hiddenPayload }) : t(proseOnly ? 'payloadHideProse' : 'payloadHide', { n: payloadCount })),
|
|
1721
|
+
listed.length === 0 ? null : h('ul', { ref: listRef, className: 'dsh-session-find-list' }, rows));
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
return h(React.Fragment, null, h('style', { key: 's' }, STYLES), trigger, panel);
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
return {
|
|
1729
|
+
inject: ['slots'],
|
|
1730
|
+
apply(ctx) {
|
|
1731
|
+
var current = ZH;
|
|
1732
|
+
try {
|
|
1733
|
+
if (typeof navigator !== 'undefined' && String(navigator.language || '').toLowerCase().indexOf('zh') !== 0) current = EN;
|
|
1734
|
+
} catch (error) { /* keep ZH */ }
|
|
1735
|
+
var t = function (key, vars) {
|
|
1736
|
+
var text = current[key] !== undefined ? current[key] : key;
|
|
1737
|
+
if (vars !== undefined) {
|
|
1738
|
+
Object.keys(vars).forEach(function (name) {
|
|
1739
|
+
text = text.split('{' + name + '}').join(String(vars[name]));
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
return text;
|
|
1743
|
+
};
|
|
1744
|
+
ctx.inject(['locale'], function (scoped) {
|
|
1745
|
+
try {
|
|
1746
|
+
scoped.effect(function () { return scoped.locale.register(NS, { zh: ZH, en: EN }); }, 'session-find: dictionaries');
|
|
1747
|
+
t = scoped.locale.bind(NS);
|
|
1748
|
+
} catch (error) { /* keep the local table */ }
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
var listeners = new Set();
|
|
1752
|
+
var open = false;
|
|
1753
|
+
var ui = {
|
|
1754
|
+
subscribe: function (listener) { listeners.add(listener); return function () { listeners.delete(listener); }; },
|
|
1755
|
+
isOpen: function () { return open; },
|
|
1756
|
+
next: function () {},
|
|
1757
|
+
prev: function () {},
|
|
1758
|
+
setOpen: function (next) {
|
|
1759
|
+
if (open === next) return;
|
|
1760
|
+
open = next;
|
|
1761
|
+
listeners.forEach(function (listener) {
|
|
1762
|
+
try { listener(); } catch (error) { /* one listener must not break the rest */ }
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
|
|
1767
|
+
ctx.effect(function () {
|
|
1768
|
+
var onKeyDown = function (event) {
|
|
1769
|
+
if (event.defaultPrevented || event.isComposing) return;
|
|
1770
|
+
var target = event.target;
|
|
1771
|
+
var tag = target === null || target === undefined ? '' : String(target.tagName || '');
|
|
1772
|
+
var editing = target !== null && target !== undefined && (target.isContentEditable === true || /^(input|textarea)$/i.test(tag));
|
|
1773
|
+
if (event.key === 'Escape' && open) {
|
|
1774
|
+
event.preventDefault();
|
|
1775
|
+
event.stopPropagation();
|
|
1776
|
+
ui.setOpen(false);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
if ((event.ctrlKey || event.metaKey) && !event.altKey && !event.shiftKey && (event.key === 'f' || event.key === 'F')) {
|
|
1780
|
+
if (editing && open) return;
|
|
1781
|
+
event.preventDefault();
|
|
1782
|
+
event.stopPropagation();
|
|
1783
|
+
ui.setOpen(true);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
var f3 = event.key === 'F3' || event.keyCode === 114;
|
|
1787
|
+
if (open && (f3 || ((event.ctrlKey || event.metaKey) && !event.altKey && (event.key === 'g' || event.key === 'G')))) {
|
|
1788
|
+
event.preventDefault();
|
|
1789
|
+
event.stopPropagation();
|
|
1790
|
+
if (event.shiftKey) ui.prev(); else ui.next();
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
window.addEventListener('keydown', onKeyDown, true);
|
|
1794
|
+
return function () { window.removeEventListener('keydown', onKeyDown, true); };
|
|
1795
|
+
}, 'session-find: shortcut');
|
|
1796
|
+
|
|
1797
|
+
var FindBar = makeFindBar(function (key, vars) { return t(key, vars); }, ui);
|
|
1798
|
+
ctx.slots.inject('conversation.session.header.utilities', function () {
|
|
1799
|
+
return ctx.slots.register({
|
|
1800
|
+
name: 'conversation.session.header.utilities',
|
|
1801
|
+
id: 'session-find',
|
|
1802
|
+
order: 60
|
|
1803
|
+
}, FindBar);
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
});
|