astro-tractstack 2.0.0-rc.45 → 2.0.0-rc.47
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/package.json
CHANGED
|
@@ -189,7 +189,7 @@ export default function SearchModal({
|
|
|
189
189
|
{selectedTerms.map((term, index) => (
|
|
190
190
|
<div
|
|
191
191
|
key={index}
|
|
192
|
-
className="flex items-center gap-1 rounded-full border border-blue-200 bg-blue-100 px-3 py-1 text-sm font-
|
|
192
|
+
className="flex items-center gap-1 rounded-full border border-blue-200 bg-blue-100 px-3 py-1 text-sm font-bold text-blue-800"
|
|
193
193
|
>
|
|
194
194
|
<span>{term}</span>
|
|
195
195
|
<button
|
|
@@ -205,26 +205,25 @@ export default function SearchModal({
|
|
|
205
205
|
)}
|
|
206
206
|
|
|
207
207
|
{!showResults && (
|
|
208
|
-
<div className="relative">
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
</div>
|
|
208
|
+
<div className="relative w-full px-6 py-2">
|
|
209
|
+
{showCompletion && (
|
|
210
|
+
<div className="pointer-events-none absolute left-0 top-0 flex h-full w-full items-center px-6 py-2 text-xl text-gray-400">
|
|
211
|
+
<span style={{ visibility: 'hidden' }}>{query}</span>
|
|
212
|
+
{bestCompletion
|
|
213
|
+
.slice(query.length)
|
|
214
|
+
.replace(/ /g, '\u00A0')}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
<input
|
|
218
|
+
ref={inputRef}
|
|
219
|
+
type="text"
|
|
220
|
+
value={query}
|
|
221
|
+
onChange={handleInputChange}
|
|
222
|
+
onKeyDown={handleKeyDown}
|
|
223
|
+
placeholder="Search content..."
|
|
224
|
+
className="text-mydarkgrey relative z-10 w-full border-none bg-transparent text-xl placeholder-gray-500 outline-none"
|
|
225
|
+
style={{ background: 'transparent', padding: '0' }}
|
|
226
|
+
/>
|
|
228
227
|
</div>
|
|
229
228
|
)}
|
|
230
229
|
<button
|
|
@@ -286,7 +285,7 @@ export default function SearchModal({
|
|
|
286
285
|
{/* Suggestion Pills */}
|
|
287
286
|
{showSuggestions && (
|
|
288
287
|
<div className="w-full p-6">
|
|
289
|
-
<p className="text-mydarkgrey mb-4 text-sm font-
|
|
288
|
+
<p className="text-mydarkgrey mb-4 text-sm font-bold">
|
|
290
289
|
Suggestions ({suggestions.length})
|
|
291
290
|
</p>
|
|
292
291
|
<div className="flex flex-wrap gap-2">
|
|
@@ -294,7 +293,7 @@ export default function SearchModal({
|
|
|
294
293
|
<button
|
|
295
294
|
key={index}
|
|
296
295
|
onClick={() => handleSuggestionClick(suggestion)}
|
|
297
|
-
className={`inline-flex items-center rounded-full border px-3 py-1.5 text-sm font-
|
|
296
|
+
className={`inline-flex items-center rounded-full border px-3 py-1.5 text-sm font-bold transition-all hover:shadow-md ${getTypeColor(suggestion.type)}`}
|
|
298
297
|
>
|
|
299
298
|
<span>{suggestion.term}</span>
|
|
300
299
|
</button>
|
|
@@ -342,7 +341,6 @@ export default function SearchModal({
|
|
|
342
341
|
<SearchResults
|
|
343
342
|
results={searchResults}
|
|
344
343
|
contentMap={contentMap}
|
|
345
|
-
onResultClick={handleClose}
|
|
346
344
|
/>
|
|
347
345
|
)}
|
|
348
346
|
</div>
|
|
@@ -14,7 +14,6 @@ const VERBOSE = false;
|
|
|
14
14
|
interface SearchResultsProps {
|
|
15
15
|
results: CategorizedResults;
|
|
16
16
|
contentMap: FullContentMapItem[];
|
|
17
|
-
onResultClick: () => void;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
interface ResultItem {
|
|
@@ -36,7 +35,6 @@ const ITEMS_PER_PAGE = 10;
|
|
|
36
35
|
export default function SearchResults({
|
|
37
36
|
results,
|
|
38
37
|
contentMap,
|
|
39
|
-
onResultClick,
|
|
40
38
|
}: SearchResultsProps) {
|
|
41
39
|
const [currentPage, setCurrentPage] = useState(1);
|
|
42
40
|
|
|
@@ -238,7 +236,7 @@ export default function SearchResults({
|
|
|
238
236
|
key={item.id}
|
|
239
237
|
className="rounded-lg border border-gray-200 p-4 transition-colors hover:bg-gray-100"
|
|
240
238
|
>
|
|
241
|
-
<a href={item.url}
|
|
239
|
+
<a href={item.url} className="group block">
|
|
242
240
|
<div className="flex flex-col md:flex-row md:items-start md:gap-4">
|
|
243
241
|
{/* Mobile: Full width image with overlay badge */}
|
|
244
242
|
<div
|
|
@@ -23,7 +23,7 @@ interface UseSearchReturn {
|
|
|
23
23
|
clearAll: () => void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const DEBOUNCE_MS =
|
|
26
|
+
const DEBOUNCE_MS = 150;
|
|
27
27
|
const BACKEND_THROTTLE_MS = 1200;
|
|
28
28
|
|
|
29
29
|
export function useSearch(): UseSearchReturn {
|
|
@@ -39,10 +39,11 @@ export function useSearch(): UseSearchReturn {
|
|
|
39
39
|
const [isRetrieving, setIsRetrieving] = useState(false);
|
|
40
40
|
const [retrieveError, setRetrieveError] = useState<string | null>(null);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
const
|
|
42
|
+
// --- REVISED STATE FOR SEARCH LOGIC ---
|
|
43
|
+
const searchTimerRef = useRef<NodeJS.Timeout>();
|
|
44
|
+
const lastExecutionTimeRef = useRef<number>(0);
|
|
44
45
|
const pendingQueryRef = useRef<string | null>(null);
|
|
45
|
-
const
|
|
46
|
+
const inflightQueryRef = useRef<string | null>(null);
|
|
46
47
|
const api = useMemo(() => new TractStackAPI(), []);
|
|
47
48
|
|
|
48
49
|
const performDiscovery = useCallback(
|
|
@@ -52,26 +53,37 @@ export function useSearch(): UseSearchReturn {
|
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
if (inflightQueryRef.current === query.trim()) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
inflightQueryRef.current = query.trim();
|
|
55
61
|
setIsDiscovering(true);
|
|
56
62
|
setDiscoverError(null);
|
|
57
|
-
lastSearchTimeRef.current = Date.now();
|
|
58
63
|
|
|
59
64
|
try {
|
|
60
65
|
const response = await api.discover(query.trim());
|
|
61
66
|
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
if (inflightQueryRef.current === query.trim()) {
|
|
68
|
+
if (response.success && response.data) {
|
|
69
|
+
setSuggestions(response.data.suggestions);
|
|
70
|
+
} else {
|
|
71
|
+
setDiscoverError(response.error || 'Discovery failed');
|
|
72
|
+
setSuggestions([]);
|
|
73
|
+
}
|
|
67
74
|
}
|
|
68
75
|
} catch (err) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
if (inflightQueryRef.current === query.trim()) {
|
|
77
|
+
setDiscoverError(
|
|
78
|
+
err instanceof Error ? err.message : 'Discovery failed'
|
|
79
|
+
);
|
|
80
|
+
setSuggestions([]);
|
|
81
|
+
}
|
|
73
82
|
} finally {
|
|
74
|
-
|
|
83
|
+
if (inflightQueryRef.current === query.trim()) {
|
|
84
|
+
inflightQueryRef.current = null;
|
|
85
|
+
setIsDiscovering(false);
|
|
86
|
+
}
|
|
75
87
|
}
|
|
76
88
|
},
|
|
77
89
|
[api]
|
|
@@ -103,24 +115,11 @@ export function useSearch(): UseSearchReturn {
|
|
|
103
115
|
[api]
|
|
104
116
|
);
|
|
105
117
|
|
|
106
|
-
const executePendingSearch = useCallback(() => {
|
|
107
|
-
if (pendingQueryRef.current) {
|
|
108
|
-
const queryToExecute = pendingQueryRef.current;
|
|
109
|
-
pendingQueryRef.current = null;
|
|
110
|
-
performDiscovery(queryToExecute);
|
|
111
|
-
}
|
|
112
|
-
}, [performDiscovery]);
|
|
113
|
-
|
|
114
118
|
const discoverTerms = useCallback(
|
|
115
119
|
(query: string) => {
|
|
116
|
-
// Clear existing
|
|
117
|
-
if (
|
|
118
|
-
clearTimeout(
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Clear existing throttle timer
|
|
122
|
-
if (throttleTimeoutRef.current) {
|
|
123
|
-
clearTimeout(throttleTimeoutRef.current);
|
|
120
|
+
// Clear any existing timer.
|
|
121
|
+
if (searchTimerRef.current) {
|
|
122
|
+
clearTimeout(searchTimerRef.current);
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
// Clear results when starting new discovery
|
|
@@ -133,31 +132,37 @@ export function useSearch(): UseSearchReturn {
|
|
|
133
132
|
setDiscoverError(null);
|
|
134
133
|
setIsDiscovering(false);
|
|
135
134
|
pendingQueryRef.current = null;
|
|
135
|
+
inflightQueryRef.current = null;
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Always store the latest query
|
|
139
|
+
// Always store the latest query for the next execution
|
|
140
140
|
pendingQueryRef.current = query;
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const now = Date.now();
|
|
145
|
-
const timeSinceLastSearch = now - lastSearchTimeRef.current;
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const timeSinceLastSearch = now - lastExecutionTimeRef.current;
|
|
146
144
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
// Start with the basic debounce delay
|
|
146
|
+
let delay = DEBOUNCE_MS;
|
|
147
|
+
|
|
148
|
+
// If we are inside the throttle window, we must wait longer
|
|
149
|
+
if (timeSinceLastSearch < BACKEND_THROTTLE_MS) {
|
|
150
|
+
const remainingThrottle = BACKEND_THROTTLE_MS - timeSinceLastSearch;
|
|
151
|
+
delay = Math.max(delay, remainingThrottle);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
searchTimerRef.current = setTimeout(() => {
|
|
155
|
+
// Double check there's a query to run
|
|
156
|
+
if (pendingQueryRef.current !== null) {
|
|
157
|
+
const queryToExecute = pendingQueryRef.current;
|
|
158
|
+
|
|
159
|
+
// Update execution time as soon as the search is initiated
|
|
160
|
+
lastExecutionTimeRef.current = Date.now();
|
|
161
|
+
performDiscovery(queryToExecute);
|
|
157
162
|
}
|
|
158
|
-
},
|
|
163
|
+
}, delay);
|
|
159
164
|
},
|
|
160
|
-
[
|
|
165
|
+
[performDiscovery]
|
|
161
166
|
);
|
|
162
167
|
|
|
163
168
|
const selectSuggestion = useCallback(
|
|
@@ -191,12 +196,9 @@ export function useSearch(): UseSearchReturn {
|
|
|
191
196
|
);
|
|
192
197
|
|
|
193
198
|
const clearAll = useCallback(() => {
|
|
194
|
-
// Clear
|
|
195
|
-
if (
|
|
196
|
-
clearTimeout(
|
|
197
|
-
}
|
|
198
|
-
if (throttleTimeoutRef.current) {
|
|
199
|
-
clearTimeout(throttleTimeoutRef.current);
|
|
199
|
+
// Clear the main search timer
|
|
200
|
+
if (searchTimerRef.current) {
|
|
201
|
+
clearTimeout(searchTimerRef.current);
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
// Reset all state
|
|
@@ -207,6 +209,8 @@ export function useSearch(): UseSearchReturn {
|
|
|
207
209
|
setIsRetrieving(false);
|
|
208
210
|
setRetrieveError(null);
|
|
209
211
|
pendingQueryRef.current = null;
|
|
212
|
+
inflightQueryRef.current = null;
|
|
213
|
+
lastExecutionTimeRef.current = 0; // Reset throttle timer
|
|
210
214
|
}, []);
|
|
211
215
|
|
|
212
216
|
return {
|
|
@@ -273,8 +273,8 @@ const enableBunny = import.meta.env.PUBLIC_ENABLE_BUNNY === 'true';
|
|
|
273
273
|
}
|
|
274
274
|
<div
|
|
275
275
|
id="loading-indicator"
|
|
276
|
-
class="bg-myorange fixed left-0 top-0
|
|
277
|
-
style="opacity: 0.5; filter: blur(0.5px);"
|
|
276
|
+
class="bg-myorange fixed left-0 top-0 h-1 w-full scale-x-0 transform transition-transform duration-300 ease-out"
|
|
277
|
+
style="opacity: 0.5; filter: blur(0.5px); z-index: 10070;"
|
|
278
278
|
>
|
|
279
279
|
</div>
|
|
280
280
|
|