opencode-replay 1.0.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/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/assets/highlight.js +300 -0
- package/dist/assets/prism.css +273 -0
- package/dist/assets/search.js +445 -0
- package/dist/assets/styles.css +3384 -0
- package/dist/assets/theme.js +111 -0
- package/dist/index.js +2569 -0
- package/package.json +44 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism.js Theme - GitHub inspired
|
|
3
|
+
* Optimized for OpenCode Replay with both light and dark mode support
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
code[class*="language-"],
|
|
7
|
+
pre[class*="language-"] {
|
|
8
|
+
color: #24292e;
|
|
9
|
+
background: none;
|
|
10
|
+
font-family: var(--font-mono);
|
|
11
|
+
font-size: var(--font-size-sm);
|
|
12
|
+
text-align: left;
|
|
13
|
+
white-space: pre;
|
|
14
|
+
word-spacing: normal;
|
|
15
|
+
word-break: normal;
|
|
16
|
+
word-wrap: normal;
|
|
17
|
+
line-height: 1.5;
|
|
18
|
+
tab-size: 2;
|
|
19
|
+
hyphens: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Code blocks */
|
|
23
|
+
pre[class*="language-"] {
|
|
24
|
+
padding: var(--spacing-md);
|
|
25
|
+
margin: var(--spacing-md) 0;
|
|
26
|
+
overflow: auto;
|
|
27
|
+
border-radius: var(--radius-md);
|
|
28
|
+
background: var(--code-bg);
|
|
29
|
+
border: 1px solid var(--code-border);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Inline code */
|
|
33
|
+
:not(pre) > code[class*="language-"] {
|
|
34
|
+
padding: 2px 6px;
|
|
35
|
+
border-radius: var(--radius-sm);
|
|
36
|
+
background: var(--code-bg);
|
|
37
|
+
border: 1px solid var(--code-border);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Token colors - Light mode */
|
|
41
|
+
.token.comment,
|
|
42
|
+
.token.prolog,
|
|
43
|
+
.token.doctype,
|
|
44
|
+
.token.cdata {
|
|
45
|
+
color: #6a737d;
|
|
46
|
+
font-style: italic;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.token.punctuation {
|
|
50
|
+
color: #24292e;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.token.namespace {
|
|
54
|
+
opacity: 0.7;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.token.property,
|
|
58
|
+
.token.tag,
|
|
59
|
+
.token.boolean,
|
|
60
|
+
.token.number,
|
|
61
|
+
.token.constant,
|
|
62
|
+
.token.symbol,
|
|
63
|
+
.token.deleted {
|
|
64
|
+
color: #005cc5;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.token.selector,
|
|
68
|
+
.token.attr-name,
|
|
69
|
+
.token.string,
|
|
70
|
+
.token.char,
|
|
71
|
+
.token.builtin,
|
|
72
|
+
.token.inserted {
|
|
73
|
+
color: #22863a;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.token.operator,
|
|
77
|
+
.token.entity,
|
|
78
|
+
.token.url,
|
|
79
|
+
.language-css .token.string,
|
|
80
|
+
.style .token.string {
|
|
81
|
+
color: #d73a49;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.token.atrule,
|
|
85
|
+
.token.attr-value,
|
|
86
|
+
.token.keyword {
|
|
87
|
+
color: #d73a49;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.token.function,
|
|
91
|
+
.token.class-name {
|
|
92
|
+
color: #6f42c1;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.token.regex,
|
|
96
|
+
.token.important,
|
|
97
|
+
.token.variable {
|
|
98
|
+
color: #e36209;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.token.important,
|
|
102
|
+
.token.bold {
|
|
103
|
+
font-weight: bold;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.token.italic {
|
|
107
|
+
font-style: italic;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.token.entity {
|
|
111
|
+
cursor: help;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* Line highlighting */
|
|
115
|
+
.line-highlight {
|
|
116
|
+
background: rgba(255, 255, 0, 0.1);
|
|
117
|
+
border-left: 3px solid #f1c40f;
|
|
118
|
+
margin-left: calc(-1 * var(--spacing-md) - 3px);
|
|
119
|
+
padding-left: calc(var(--spacing-md) + 3px);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Line numbers */
|
|
123
|
+
.line-numbers .line-numbers-rows {
|
|
124
|
+
position: absolute;
|
|
125
|
+
pointer-events: none;
|
|
126
|
+
top: 0;
|
|
127
|
+
font-size: 100%;
|
|
128
|
+
left: -3.8em;
|
|
129
|
+
width: 3em;
|
|
130
|
+
letter-spacing: -1px;
|
|
131
|
+
border-right: 1px solid var(--color-border);
|
|
132
|
+
user-select: none;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.line-numbers-rows > span {
|
|
136
|
+
display: block;
|
|
137
|
+
counter-increment: linenumber;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.line-numbers-rows > span:before {
|
|
141
|
+
content: counter(linenumber);
|
|
142
|
+
color: var(--color-text-muted);
|
|
143
|
+
display: block;
|
|
144
|
+
padding-right: 0.8em;
|
|
145
|
+
text-align: right;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ============================================================================
|
|
149
|
+
Dark Mode
|
|
150
|
+
============================================================================ */
|
|
151
|
+
|
|
152
|
+
:root[data-theme="dark"] code[class*="language-"],
|
|
153
|
+
:root[data-theme="dark"] pre[class*="language-"] {
|
|
154
|
+
color: #e6edf3;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
:root[data-theme="dark"] .token.comment,
|
|
158
|
+
:root[data-theme="dark"] .token.prolog,
|
|
159
|
+
:root[data-theme="dark"] .token.doctype,
|
|
160
|
+
:root[data-theme="dark"] .token.cdata {
|
|
161
|
+
color: #8b949e;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:root[data-theme="dark"] .token.punctuation {
|
|
165
|
+
color: #e6edf3;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
:root[data-theme="dark"] .token.property,
|
|
169
|
+
:root[data-theme="dark"] .token.tag,
|
|
170
|
+
:root[data-theme="dark"] .token.boolean,
|
|
171
|
+
:root[data-theme="dark"] .token.number,
|
|
172
|
+
:root[data-theme="dark"] .token.constant,
|
|
173
|
+
:root[data-theme="dark"] .token.symbol,
|
|
174
|
+
:root[data-theme="dark"] .token.deleted {
|
|
175
|
+
color: #79c0ff;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
:root[data-theme="dark"] .token.selector,
|
|
179
|
+
:root[data-theme="dark"] .token.attr-name,
|
|
180
|
+
:root[data-theme="dark"] .token.string,
|
|
181
|
+
:root[data-theme="dark"] .token.char,
|
|
182
|
+
:root[data-theme="dark"] .token.builtin,
|
|
183
|
+
:root[data-theme="dark"] .token.inserted {
|
|
184
|
+
color: #a5d6ff;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
:root[data-theme="dark"] .token.operator,
|
|
188
|
+
:root[data-theme="dark"] .token.entity,
|
|
189
|
+
:root[data-theme="dark"] .token.url,
|
|
190
|
+
:root[data-theme="dark"] .language-css .token.string,
|
|
191
|
+
:root[data-theme="dark"] .style .token.string {
|
|
192
|
+
color: #ff7b72;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
:root[data-theme="dark"] .token.atrule,
|
|
196
|
+
:root[data-theme="dark"] .token.attr-value,
|
|
197
|
+
:root[data-theme="dark"] .token.keyword {
|
|
198
|
+
color: #ff7b72;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
:root[data-theme="dark"] .token.function,
|
|
202
|
+
:root[data-theme="dark"] .token.class-name {
|
|
203
|
+
color: #d2a8ff;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
:root[data-theme="dark"] .token.regex,
|
|
207
|
+
:root[data-theme="dark"] .token.important,
|
|
208
|
+
:root[data-theme="dark"] .token.variable {
|
|
209
|
+
color: #ffa657;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Auto dark mode */
|
|
213
|
+
@media (prefers-color-scheme: dark) {
|
|
214
|
+
:root:not([data-theme="light"]) code[class*="language-"],
|
|
215
|
+
:root:not([data-theme="light"]) pre[class*="language-"] {
|
|
216
|
+
color: #e6edf3;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
:root:not([data-theme="light"]) .token.comment,
|
|
220
|
+
:root:not([data-theme="light"]) .token.prolog,
|
|
221
|
+
:root:not([data-theme="light"]) .token.doctype,
|
|
222
|
+
:root:not([data-theme="light"]) .token.cdata {
|
|
223
|
+
color: #8b949e;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
:root:not([data-theme="light"]) .token.punctuation {
|
|
227
|
+
color: #e6edf3;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
:root:not([data-theme="light"]) .token.property,
|
|
231
|
+
:root:not([data-theme="light"]) .token.tag,
|
|
232
|
+
:root:not([data-theme="light"]) .token.boolean,
|
|
233
|
+
:root:not([data-theme="light"]) .token.number,
|
|
234
|
+
:root:not([data-theme="light"]) .token.constant,
|
|
235
|
+
:root:not([data-theme="light"]) .token.symbol,
|
|
236
|
+
:root:not([data-theme="light"]) .token.deleted {
|
|
237
|
+
color: #79c0ff;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
:root:not([data-theme="light"]) .token.selector,
|
|
241
|
+
:root:not([data-theme="light"]) .token.attr-name,
|
|
242
|
+
:root:not([data-theme="light"]) .token.string,
|
|
243
|
+
:root:not([data-theme="light"]) .token.char,
|
|
244
|
+
:root:not([data-theme="light"]) .token.builtin,
|
|
245
|
+
:root:not([data-theme="light"]) .token.inserted {
|
|
246
|
+
color: #a5d6ff;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
:root:not([data-theme="light"]) .token.operator,
|
|
250
|
+
:root:not([data-theme="light"]) .token.entity,
|
|
251
|
+
:root:not([data-theme="light"]) .token.url,
|
|
252
|
+
:root:not([data-theme="light"]) .language-css .token.string,
|
|
253
|
+
:root:not([data-theme="light"]) .style .token.string {
|
|
254
|
+
color: #ff7b72;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
:root:not([data-theme="light"]) .token.atrule,
|
|
258
|
+
:root:not([data-theme="light"]) .token.attr-value,
|
|
259
|
+
:root:not([data-theme="light"]) .token.keyword {
|
|
260
|
+
color: #ff7b72;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
:root:not([data-theme="light"]) .token.function,
|
|
264
|
+
:root:not([data-theme="light"]) .token.class-name {
|
|
265
|
+
color: #d2a8ff;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
:root:not([data-theme="light"]) .token.regex,
|
|
269
|
+
:root:not([data-theme="light"]) .token.important,
|
|
270
|
+
:root:not([data-theme="light"]) .token.variable {
|
|
271
|
+
color: #ffa657;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Replay - Client-side Search
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Fetches paginated HTML pages on-demand
|
|
6
|
+
* - Searches within .message elements using case-insensitive substring matching
|
|
7
|
+
* - Displays results as they're found (streaming UX)
|
|
8
|
+
* - Highlights matches with <mark> tags
|
|
9
|
+
* - Supports URL fragment for shareable searches (#search=query)
|
|
10
|
+
* - Progressive enhancement - hidden for file:// protocol (CORS)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
(function() {
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
// Configuration
|
|
17
|
+
var BATCH_SIZE = 3; // Pages to fetch in parallel
|
|
18
|
+
var MAX_RESULTS = 100; // Maximum results to display
|
|
19
|
+
|
|
20
|
+
// State
|
|
21
|
+
var modal = null;
|
|
22
|
+
var searchInput = null;
|
|
23
|
+
var searchStatus = null;
|
|
24
|
+
var searchResults = null;
|
|
25
|
+
var isSearching = false;
|
|
26
|
+
|
|
27
|
+
// Detect page context
|
|
28
|
+
var isSessionPage = window.location.pathname.includes('/sessions/');
|
|
29
|
+
var isIndexPage = !isSessionPage;
|
|
30
|
+
|
|
31
|
+
// Get total pages from data attribute (set by template)
|
|
32
|
+
var totalPages = parseInt(document.body.dataset.totalPages || '0', 10);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if we can perform search (CORS limitation for file://)
|
|
36
|
+
*/
|
|
37
|
+
function canSearch() {
|
|
38
|
+
return window.location.protocol !== 'file:';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Escape HTML special characters
|
|
43
|
+
*/
|
|
44
|
+
function escapeHtml(str) {
|
|
45
|
+
var div = document.createElement('div');
|
|
46
|
+
div.textContent = str;
|
|
47
|
+
return div.innerHTML;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Escape regex special characters
|
|
52
|
+
*/
|
|
53
|
+
function escapeRegex(str) {
|
|
54
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create the search modal HTML
|
|
59
|
+
*/
|
|
60
|
+
function createModal() {
|
|
61
|
+
var modalHtml = '<dialog id="search-modal" class="search-modal">' +
|
|
62
|
+
'<div class="search-modal-content">' +
|
|
63
|
+
'<div class="search-modal-header">' +
|
|
64
|
+
'<div class="search-input-wrapper">' +
|
|
65
|
+
'<span class="search-input-icon">🔍</span>' +
|
|
66
|
+
'<input type="text" id="search-modal-input" placeholder="Search messages..." autocomplete="off" />' +
|
|
67
|
+
'</div>' +
|
|
68
|
+
'<button type="button" class="search-modal-close" aria-label="Close search">×</button>' +
|
|
69
|
+
'</div>' +
|
|
70
|
+
'<div id="search-status" class="search-status"></div>' +
|
|
71
|
+
'<div id="search-results" class="search-results"></div>' +
|
|
72
|
+
'</div>' +
|
|
73
|
+
'</dialog>';
|
|
74
|
+
|
|
75
|
+
var container = document.createElement('div');
|
|
76
|
+
container.innerHTML = modalHtml;
|
|
77
|
+
document.body.appendChild(container.firstChild);
|
|
78
|
+
|
|
79
|
+
modal = document.getElementById('search-modal');
|
|
80
|
+
searchInput = document.getElementById('search-modal-input');
|
|
81
|
+
searchStatus = document.getElementById('search-status');
|
|
82
|
+
searchResults = document.getElementById('search-results');
|
|
83
|
+
|
|
84
|
+
// Event listeners
|
|
85
|
+
var closeBtn = modal.querySelector('.search-modal-close');
|
|
86
|
+
closeBtn.addEventListener('click', closeModal);
|
|
87
|
+
|
|
88
|
+
modal.addEventListener('click', function(e) {
|
|
89
|
+
if (e.target === modal) {
|
|
90
|
+
closeModal();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
modal.addEventListener('cancel', function(e) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
closeModal();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
var debounceTimer = null;
|
|
100
|
+
searchInput.addEventListener('input', function() {
|
|
101
|
+
clearTimeout(debounceTimer);
|
|
102
|
+
debounceTimer = setTimeout(function() {
|
|
103
|
+
performSearch(searchInput.value.trim());
|
|
104
|
+
}, 200);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
searchInput.addEventListener('keydown', function(e) {
|
|
108
|
+
if (e.key === 'Escape') {
|
|
109
|
+
closeModal();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Open the search modal
|
|
116
|
+
*/
|
|
117
|
+
function openModal(initialQuery) {
|
|
118
|
+
if (!modal) {
|
|
119
|
+
createModal();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
modal.showModal();
|
|
123
|
+
searchInput.value = initialQuery || '';
|
|
124
|
+
searchInput.focus();
|
|
125
|
+
|
|
126
|
+
if (initialQuery) {
|
|
127
|
+
performSearch(initialQuery);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Close the search modal
|
|
133
|
+
*/
|
|
134
|
+
function closeModal() {
|
|
135
|
+
if (modal && modal.open) {
|
|
136
|
+
modal.close();
|
|
137
|
+
isSearching = false;
|
|
138
|
+
// Clear URL hash
|
|
139
|
+
if (window.location.hash.startsWith('#search=')) {
|
|
140
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Update URL hash with search query
|
|
147
|
+
*/
|
|
148
|
+
function updateUrlHash(query) {
|
|
149
|
+
if (query) {
|
|
150
|
+
history.replaceState(null, '', window.location.pathname + window.location.search + '#search=' + encodeURIComponent(query));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the URL for fetching a page
|
|
156
|
+
*/
|
|
157
|
+
function getPageFetchUrl(pageFile) {
|
|
158
|
+
// Pages are in the same directory as index.html for sessions
|
|
159
|
+
return pageFile;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the URL for linking to a result
|
|
164
|
+
*/
|
|
165
|
+
function getPageLinkUrl(pageFile) {
|
|
166
|
+
return pageFile;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Highlight search term in text by escaping HTML first, then wrapping matches
|
|
171
|
+
* This prevents XSS by ensuring the query is never directly injected as HTML
|
|
172
|
+
*/
|
|
173
|
+
function highlightMatches(text, searchTerm) {
|
|
174
|
+
if (!text || !searchTerm) return escapeHtml(text || '');
|
|
175
|
+
|
|
176
|
+
// First escape the entire text to prevent XSS
|
|
177
|
+
var escaped = escapeHtml(text);
|
|
178
|
+
// Also escape the search term for HTML (in case it contains < > etc)
|
|
179
|
+
var escapedTerm = escapeHtml(searchTerm);
|
|
180
|
+
|
|
181
|
+
// Now highlight matches in the escaped text
|
|
182
|
+
var regex = new RegExp('(' + escapeRegex(escapedTerm) + ')', 'gi');
|
|
183
|
+
return escaped.replace(regex, '<mark>$1</mark>');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Process a single page and find matches
|
|
188
|
+
*/
|
|
189
|
+
function processPage(pageFile, html, query) {
|
|
190
|
+
var parser = new DOMParser();
|
|
191
|
+
var doc = parser.parseFromString(html, 'text/html');
|
|
192
|
+
var results = [];
|
|
193
|
+
|
|
194
|
+
// Find all message blocks
|
|
195
|
+
var messages = doc.querySelectorAll('.message');
|
|
196
|
+
messages.forEach(function(msg) {
|
|
197
|
+
var text = msg.textContent || '';
|
|
198
|
+
if (text.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
|
|
199
|
+
var msgId = msg.id || '';
|
|
200
|
+
var link = getPageLinkUrl(pageFile) + (msgId ? '#' + msgId : '');
|
|
201
|
+
|
|
202
|
+
// Get a preview of the content
|
|
203
|
+
var contentEl = msg.querySelector('.message-content');
|
|
204
|
+
var preview = contentEl ? contentEl.textContent.trim() : text.trim();
|
|
205
|
+
preview = preview.slice(0, 300);
|
|
206
|
+
|
|
207
|
+
// Determine message role
|
|
208
|
+
var role = msg.classList.contains('message-user') ? 'user' : 'assistant';
|
|
209
|
+
|
|
210
|
+
results.push({
|
|
211
|
+
link: link,
|
|
212
|
+
role: role,
|
|
213
|
+
preview: preview,
|
|
214
|
+
element: msg.cloneNode(true),
|
|
215
|
+
pageFile: pageFile
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Search within session pages
|
|
225
|
+
*/
|
|
226
|
+
async function searchSessionPages(query) {
|
|
227
|
+
if (totalPages === 0) {
|
|
228
|
+
searchStatus.textContent = 'No pages to search';
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
var resultsFound = 0;
|
|
233
|
+
var pagesSearched = 0;
|
|
234
|
+
|
|
235
|
+
// Build list of pages to fetch
|
|
236
|
+
var pagesToFetch = [];
|
|
237
|
+
for (var i = 1; i <= totalPages; i++) {
|
|
238
|
+
pagesToFetch.push('page-' + String(i).padStart(3, '0') + '.html');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Process pages in batches
|
|
242
|
+
for (var i = 0; i < pagesToFetch.length && isSearching; i += BATCH_SIZE) {
|
|
243
|
+
var batch = pagesToFetch.slice(i, i + BATCH_SIZE);
|
|
244
|
+
|
|
245
|
+
var promises = batch.map(function(pageFile) {
|
|
246
|
+
return fetch(getPageFetchUrl(pageFile))
|
|
247
|
+
.then(function(response) {
|
|
248
|
+
if (!response.ok) throw new Error('Failed to fetch ' + pageFile);
|
|
249
|
+
return response.text();
|
|
250
|
+
})
|
|
251
|
+
.then(function(html) {
|
|
252
|
+
var results = processPage(pageFile, html, query);
|
|
253
|
+
pagesSearched++;
|
|
254
|
+
|
|
255
|
+
// Update status
|
|
256
|
+
searchStatus.textContent = 'Found ' + (resultsFound + results.length) + ' result(s) in ' + pagesSearched + '/' + totalPages + ' pages...';
|
|
257
|
+
|
|
258
|
+
// Display results
|
|
259
|
+
results.forEach(function(result) {
|
|
260
|
+
if (resultsFound < MAX_RESULTS) {
|
|
261
|
+
displayResult(result, query);
|
|
262
|
+
resultsFound++;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return results.length;
|
|
267
|
+
})
|
|
268
|
+
.catch(function(err) {
|
|
269
|
+
console.error('Error fetching page:', err);
|
|
270
|
+
pagesSearched++;
|
|
271
|
+
return 0;
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await Promise.all(promises);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Final status
|
|
279
|
+
if (isSearching) {
|
|
280
|
+
if (resultsFound === 0) {
|
|
281
|
+
searchStatus.textContent = 'No results found';
|
|
282
|
+
searchResults.innerHTML = '<div class="search-no-results">No matches found for "' + escapeHtml(query) + '"</div>';
|
|
283
|
+
} else {
|
|
284
|
+
searchStatus.textContent = 'Found ' + resultsFound + ' result(s) in ' + totalPages + ' pages';
|
|
285
|
+
if (resultsFound >= MAX_RESULTS) {
|
|
286
|
+
searchStatus.textContent += ' (showing first ' + MAX_RESULTS + ')';
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Search the index page (session titles and previews)
|
|
294
|
+
*/
|
|
295
|
+
function searchIndexPage(query) {
|
|
296
|
+
var sessionCards = document.querySelectorAll('.session-card');
|
|
297
|
+
var resultsFound = 0;
|
|
298
|
+
|
|
299
|
+
sessionCards.forEach(function(card) {
|
|
300
|
+
var title = card.querySelector('.session-title');
|
|
301
|
+
var summary = card.querySelector('.session-summary');
|
|
302
|
+
var text = (title ? title.textContent : '') + ' ' + (summary ? summary.textContent : '');
|
|
303
|
+
|
|
304
|
+
if (text.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
|
|
305
|
+
var link = card.getAttribute('href') || '#';
|
|
306
|
+
var titleText = title ? title.textContent : 'Untitled Session';
|
|
307
|
+
var summaryText = summary ? summary.textContent : '';
|
|
308
|
+
|
|
309
|
+
displayResult({
|
|
310
|
+
link: link,
|
|
311
|
+
role: 'session',
|
|
312
|
+
preview: summaryText.slice(0, 200),
|
|
313
|
+
title: titleText,
|
|
314
|
+
pageFile: null
|
|
315
|
+
}, query);
|
|
316
|
+
|
|
317
|
+
resultsFound++;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (resultsFound === 0) {
|
|
322
|
+
searchStatus.textContent = 'No results found';
|
|
323
|
+
searchResults.innerHTML = '<div class="search-no-results">No sessions match "' + escapeHtml(query) + '"</div>';
|
|
324
|
+
} else {
|
|
325
|
+
searchStatus.textContent = 'Found ' + resultsFound + ' session(s)';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Display a single search result
|
|
331
|
+
*/
|
|
332
|
+
function displayResult(result, query) {
|
|
333
|
+
var resultDiv = document.createElement('a');
|
|
334
|
+
resultDiv.href = result.link;
|
|
335
|
+
resultDiv.className = 'search-result';
|
|
336
|
+
|
|
337
|
+
var roleLabel = result.role === 'user' ? 'USER' :
|
|
338
|
+
result.role === 'assistant' ? 'ASSISTANT' :
|
|
339
|
+
'SESSION';
|
|
340
|
+
var roleClass = 'search-result-role search-result-role-' + result.role;
|
|
341
|
+
|
|
342
|
+
var title = result.title || '';
|
|
343
|
+
var preview = result.preview || '';
|
|
344
|
+
|
|
345
|
+
// Highlight matches in preview (XSS-safe: escapes HTML before highlighting)
|
|
346
|
+
var highlightedPreview = highlightMatches(preview, query);
|
|
347
|
+
|
|
348
|
+
var html = '<div class="search-result-header">' +
|
|
349
|
+
'<span class="' + roleClass + '">' + roleLabel + '</span>';
|
|
350
|
+
|
|
351
|
+
if (result.pageFile) {
|
|
352
|
+
html += '<span class="search-result-page">' + escapeHtml(result.pageFile) + '</span>';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
html += '</div>';
|
|
356
|
+
|
|
357
|
+
if (title) {
|
|
358
|
+
// Highlight matches in title (XSS-safe)
|
|
359
|
+
var highlightedTitle = highlightMatches(title, query);
|
|
360
|
+
html += '<div class="search-result-title">' + highlightedTitle + '</div>';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
html += '<div class="search-result-preview">' + highlightedPreview + '</div>';
|
|
364
|
+
|
|
365
|
+
resultDiv.innerHTML = html;
|
|
366
|
+
searchResults.appendChild(resultDiv);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Main search function
|
|
371
|
+
*/
|
|
372
|
+
async function performSearch(query) {
|
|
373
|
+
if (!query || query.length < 2) {
|
|
374
|
+
searchStatus.textContent = 'Type at least 2 characters to search';
|
|
375
|
+
searchResults.innerHTML = '';
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Cancel any ongoing search
|
|
380
|
+
isSearching = false;
|
|
381
|
+
await new Promise(function(resolve) { setTimeout(resolve, 50); });
|
|
382
|
+
|
|
383
|
+
// Start new search
|
|
384
|
+
isSearching = true;
|
|
385
|
+
searchStatus.textContent = 'Searching...';
|
|
386
|
+
searchResults.innerHTML = '';
|
|
387
|
+
updateUrlHash(query);
|
|
388
|
+
|
|
389
|
+
if (isSessionPage) {
|
|
390
|
+
await searchSessionPages(query);
|
|
391
|
+
} else {
|
|
392
|
+
searchIndexPage(query);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Initialize search functionality
|
|
398
|
+
*/
|
|
399
|
+
function init() {
|
|
400
|
+
// Check if search can work (not file:// protocol)
|
|
401
|
+
if (!canSearch()) {
|
|
402
|
+
// Hide search triggers since they won't work
|
|
403
|
+
var triggers = document.querySelectorAll('.search-trigger');
|
|
404
|
+
triggers.forEach(function(trigger) {
|
|
405
|
+
trigger.style.display = 'none';
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Keyboard shortcut: Cmd/Ctrl + K
|
|
411
|
+
document.addEventListener('keydown', function(e) {
|
|
412
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
413
|
+
e.preventDefault();
|
|
414
|
+
openModal();
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Search trigger button click
|
|
419
|
+
var triggers = document.querySelectorAll('.search-trigger');
|
|
420
|
+
triggers.forEach(function(trigger) {
|
|
421
|
+
trigger.addEventListener('click', function(e) {
|
|
422
|
+
e.preventDefault();
|
|
423
|
+
openModal();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Check for search in URL hash on page load
|
|
428
|
+
if (window.location.hash.startsWith('#search=')) {
|
|
429
|
+
var query = decodeURIComponent(window.location.hash.substring(8));
|
|
430
|
+
if (query) {
|
|
431
|
+
// Delay to ensure page is ready
|
|
432
|
+
setTimeout(function() {
|
|
433
|
+
openModal(query);
|
|
434
|
+
}, 100);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Initialize when DOM is ready
|
|
440
|
+
if (document.readyState === 'loading') {
|
|
441
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
442
|
+
} else {
|
|
443
|
+
init();
|
|
444
|
+
}
|
|
445
|
+
})();
|