@gmickel/gno 0.16.0 → 0.17.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/README.md +36 -1
- package/package.json +4 -1
- package/src/cli/commands/ask.ts +9 -0
- package/src/cli/commands/query.ts +3 -2
- package/src/cli/pager.ts +1 -1
- package/src/cli/program.ts +89 -0
- package/src/core/links.ts +92 -20
- package/src/ingestion/sync.ts +267 -23
- package/src/ingestion/types.ts +2 -0
- package/src/ingestion/walker.ts +2 -1
- package/src/mcp/tools/index.ts +30 -1
- package/src/mcp/tools/query.ts +22 -2
- package/src/mcp/tools/search.ts +8 -0
- package/src/mcp/tools/vsearch.ts +8 -0
- package/src/pipeline/answer.ts +324 -7
- package/src/pipeline/expansion.ts +243 -7
- package/src/pipeline/explain.ts +93 -5
- package/src/pipeline/hybrid.ts +240 -57
- package/src/pipeline/query-modes.ts +125 -0
- package/src/pipeline/rerank.ts +34 -13
- package/src/pipeline/search.ts +41 -3
- package/src/pipeline/temporal.ts +257 -0
- package/src/pipeline/types.ts +58 -0
- package/src/pipeline/vsearch.ts +107 -9
- package/src/serve/public/app.tsx +1 -3
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/lib/retrieval-filters.ts +167 -0
- package/src/serve/public/pages/Ask.tsx +339 -109
- package/src/serve/public/pages/Browse.tsx +71 -5
- package/src/serve/public/pages/DocView.tsx +2 -21
- package/src/serve/public/pages/Search.tsx +507 -120
- package/src/serve/routes/api.ts +202 -2
- package/src/store/migrations/006-document-metadata.ts +104 -0
- package/src/store/migrations/007-document-date-fields.ts +24 -0
- package/src/store/migrations/index.ts +3 -1
- package/src/store/sqlite/adapter.ts +218 -5
- package/src/store/types.ts +46 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ArrowLeft,
|
|
3
3
|
BookOpen,
|
|
4
|
+
ChevronDown,
|
|
4
5
|
CornerDownLeft,
|
|
5
6
|
FileText,
|
|
7
|
+
SlidersHorizontal,
|
|
6
8
|
Sparkles,
|
|
7
9
|
} from "lucide-react";
|
|
8
10
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -22,9 +24,24 @@ import {
|
|
|
22
24
|
import { Badge } from "../components/ui/badge";
|
|
23
25
|
import { Button } from "../components/ui/button";
|
|
24
26
|
import { Card, CardContent } from "../components/ui/card";
|
|
27
|
+
import {
|
|
28
|
+
Collapsible,
|
|
29
|
+
CollapsibleContent,
|
|
30
|
+
CollapsibleTrigger,
|
|
31
|
+
} from "../components/ui/collapsible";
|
|
32
|
+
import { Input } from "../components/ui/input";
|
|
33
|
+
import {
|
|
34
|
+
Select,
|
|
35
|
+
SelectContent,
|
|
36
|
+
SelectItem,
|
|
37
|
+
SelectTrigger,
|
|
38
|
+
SelectValue,
|
|
39
|
+
} from "../components/ui/select";
|
|
25
40
|
import { Textarea } from "../components/ui/textarea";
|
|
26
41
|
import { apiFetch } from "../hooks/use-api";
|
|
27
42
|
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
|
43
|
+
import { parseTagsCsv, type TagMode } from "../lib/retrieval-filters";
|
|
44
|
+
import { cn } from "../lib/utils";
|
|
28
45
|
|
|
29
46
|
interface PageProps {
|
|
30
47
|
navigate: (to: string | number) => void;
|
|
@@ -80,6 +97,10 @@ interface ConversationEntry {
|
|
|
80
97
|
error?: string;
|
|
81
98
|
}
|
|
82
99
|
|
|
100
|
+
interface Collection {
|
|
101
|
+
name: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
83
104
|
const THOROUGHNESS_ORDER: Thoroughness[] = ["fast", "balanced", "thorough"];
|
|
84
105
|
|
|
85
106
|
/**
|
|
@@ -100,7 +121,6 @@ function renderAnswer(
|
|
|
100
121
|
|
|
101
122
|
// oxlint-disable-next-line no-cond-assign -- Standard regex match pattern
|
|
102
123
|
while ((match = citationRegex.exec(answer)) !== null) {
|
|
103
|
-
// Add text before citation
|
|
104
124
|
if (match.index > lastIndex) {
|
|
105
125
|
parts.push(answer.slice(lastIndex, match.index));
|
|
106
126
|
}
|
|
@@ -128,7 +148,6 @@ function renderAnswer(
|
|
|
128
148
|
lastIndex = match.index + match[0].length;
|
|
129
149
|
}
|
|
130
150
|
|
|
131
|
-
// Add remaining text
|
|
132
151
|
if (lastIndex < answer.length) {
|
|
133
152
|
parts.push(answer.slice(lastIndex));
|
|
134
153
|
}
|
|
@@ -140,105 +159,152 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
140
159
|
const [query, setQuery] = useState("");
|
|
141
160
|
const [conversation, setConversation] = useState<ConversationEntry[]>([]);
|
|
142
161
|
const [capabilities, setCapabilities] = useState<Capabilities | null>(null);
|
|
162
|
+
const [collections, setCollections] = useState<Collection[]>([]);
|
|
143
163
|
const [thoroughness, setThoroughness] = useState<Thoroughness>("balanced");
|
|
164
|
+
|
|
165
|
+
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
166
|
+
const [selectedCollection, setSelectedCollection] = useState("");
|
|
167
|
+
const [since, setSince] = useState("");
|
|
168
|
+
const [until, setUntil] = useState("");
|
|
169
|
+
const [category, setCategory] = useState("");
|
|
170
|
+
const [author, setAuthor] = useState("");
|
|
171
|
+
const [tagMode, setTagMode] = useState<TagMode>("any");
|
|
172
|
+
const [tagsInput, setTagsInput] = useState("");
|
|
173
|
+
|
|
144
174
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
145
175
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
146
176
|
|
|
147
|
-
|
|
177
|
+
const hybridAvailable = capabilities?.hybrid ?? false;
|
|
178
|
+
|
|
148
179
|
useEffect(() => {
|
|
149
|
-
async function
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
async function bootstrap(): Promise<void> {
|
|
181
|
+
const [capsResult, collectionsResult] = await Promise.all([
|
|
182
|
+
apiFetch<Capabilities>("/api/capabilities"),
|
|
183
|
+
apiFetch<Collection[]>("/api/collections"),
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
if (capsResult.data) {
|
|
187
|
+
const caps = capsResult.data;
|
|
188
|
+
setCapabilities(caps);
|
|
189
|
+
setThoroughness(caps.hybrid ? "balanced" : "fast");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (collectionsResult.data) {
|
|
193
|
+
setCollections(collectionsResult.data);
|
|
159
194
|
}
|
|
160
195
|
}
|
|
161
|
-
|
|
196
|
+
|
|
197
|
+
void bootstrap();
|
|
162
198
|
}, []);
|
|
163
199
|
|
|
164
|
-
// Cycle thoroughness with 't' key
|
|
165
200
|
const cycleThoroughness = useCallback(() => {
|
|
166
201
|
setThoroughness((current) => {
|
|
202
|
+
if (!hybridAvailable) {
|
|
203
|
+
return "fast";
|
|
204
|
+
}
|
|
167
205
|
const currentIdx = THOROUGHNESS_ORDER.indexOf(current);
|
|
168
206
|
const nextIdx = (currentIdx + 1) % THOROUGHNESS_ORDER.length;
|
|
169
207
|
return THOROUGHNESS_ORDER[nextIdx];
|
|
170
208
|
});
|
|
171
|
-
}, []);
|
|
209
|
+
}, [hybridAvailable]);
|
|
172
210
|
|
|
173
211
|
const shortcuts = useMemo(
|
|
174
212
|
() => [{ key: "t", action: cycleThoroughness }],
|
|
175
213
|
[cycleThoroughness]
|
|
176
214
|
);
|
|
177
|
-
|
|
178
215
|
useKeyboardShortcuts(shortcuts);
|
|
179
216
|
|
|
180
|
-
// Scroll to bottom when conversation updates
|
|
181
217
|
useEffect(() => {
|
|
182
218
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
183
|
-
}, []);
|
|
219
|
+
}, [conversation]);
|
|
184
220
|
|
|
185
|
-
const handleSubmit =
|
|
186
|
-
e.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
221
|
+
const handleSubmit = useCallback(
|
|
222
|
+
async (e: React.FormEvent) => {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
if (!query.trim()) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
190
227
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Add entry to conversation
|
|
195
|
-
setConversation((prev) => [
|
|
196
|
-
...prev,
|
|
197
|
-
{ id: entryId, query: currentQuery, response: null, loading: true },
|
|
198
|
-
]);
|
|
199
|
-
setQuery("");
|
|
200
|
-
|
|
201
|
-
// Build request body with thoroughness-mapped params
|
|
202
|
-
// fast: BM25-only via noExpand + noRerank
|
|
203
|
-
// balanced: with reranking, no expansion
|
|
204
|
-
// thorough: full pipeline
|
|
205
|
-
const requestBody: Record<string, unknown> = {
|
|
206
|
-
query: currentQuery,
|
|
207
|
-
limit: 5,
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
if (thoroughness === "fast") {
|
|
211
|
-
requestBody.noExpand = true;
|
|
212
|
-
requestBody.noRerank = true;
|
|
213
|
-
} else if (thoroughness === "balanced") {
|
|
214
|
-
requestBody.noExpand = true;
|
|
215
|
-
requestBody.noRerank = false;
|
|
216
|
-
} else {
|
|
217
|
-
// thorough - full pipeline
|
|
218
|
-
requestBody.noExpand = false;
|
|
219
|
-
requestBody.noRerank = false;
|
|
220
|
-
}
|
|
228
|
+
const entryId = crypto.randomUUID();
|
|
229
|
+
const currentQuery = query.trim();
|
|
221
230
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
231
|
+
setConversation((prev) => [
|
|
232
|
+
...prev,
|
|
233
|
+
{ id: entryId, query: currentQuery, response: null, loading: true },
|
|
234
|
+
]);
|
|
235
|
+
setQuery("");
|
|
227
236
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
const requestBody: Record<string, unknown> = {
|
|
238
|
+
query: currentQuery,
|
|
239
|
+
limit: 5,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
if (selectedCollection) {
|
|
243
|
+
requestBody.collection = selectedCollection;
|
|
244
|
+
}
|
|
245
|
+
if (since) {
|
|
246
|
+
requestBody.since = since;
|
|
247
|
+
}
|
|
248
|
+
if (until) {
|
|
249
|
+
requestBody.until = until;
|
|
250
|
+
}
|
|
251
|
+
if (category.trim()) {
|
|
252
|
+
requestBody.category = category.trim();
|
|
253
|
+
}
|
|
254
|
+
if (author.trim()) {
|
|
255
|
+
requestBody.author = author.trim();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const normalizedTags = parseTagsCsv(tagsInput);
|
|
259
|
+
if (normalizedTags.length > 0) {
|
|
260
|
+
if (tagMode === "all") {
|
|
261
|
+
requestBody.tagsAll = normalizedTags.join(",");
|
|
262
|
+
} else {
|
|
263
|
+
requestBody.tagsAny = normalizedTags.join(",");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (thoroughness === "fast") {
|
|
268
|
+
requestBody.noExpand = true;
|
|
269
|
+
requestBody.noRerank = true;
|
|
270
|
+
} else if (thoroughness === "balanced") {
|
|
271
|
+
requestBody.noExpand = true;
|
|
272
|
+
requestBody.noRerank = false;
|
|
273
|
+
} else {
|
|
274
|
+
requestBody.noExpand = false;
|
|
275
|
+
requestBody.noRerank = false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const { data, error } = await apiFetch<AskResponse>("/api/ask", {
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: JSON.stringify(requestBody),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
setConversation((prev) =>
|
|
284
|
+
prev.map((entry) =>
|
|
285
|
+
entry.id === entryId
|
|
286
|
+
? {
|
|
287
|
+
...entry,
|
|
288
|
+
response: data ?? null,
|
|
289
|
+
loading: false,
|
|
290
|
+
error: error ?? undefined,
|
|
291
|
+
}
|
|
292
|
+
: entry
|
|
293
|
+
)
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
[
|
|
297
|
+
author,
|
|
298
|
+
category,
|
|
299
|
+
query,
|
|
300
|
+
selectedCollection,
|
|
301
|
+
since,
|
|
302
|
+
tagMode,
|
|
303
|
+
tagsInput,
|
|
304
|
+
thoroughness,
|
|
305
|
+
until,
|
|
306
|
+
]
|
|
307
|
+
);
|
|
242
308
|
|
|
243
309
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
244
310
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
@@ -247,11 +313,31 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
247
313
|
}
|
|
248
314
|
};
|
|
249
315
|
|
|
316
|
+
const clearFilters = () => {
|
|
317
|
+
setSelectedCollection("");
|
|
318
|
+
setSince("");
|
|
319
|
+
setUntil("");
|
|
320
|
+
setCategory("");
|
|
321
|
+
setAuthor("");
|
|
322
|
+
setTagsInput("");
|
|
323
|
+
setTagMode("any");
|
|
324
|
+
};
|
|
325
|
+
|
|
250
326
|
const answerAvailable = capabilities?.answer ?? false;
|
|
251
327
|
|
|
328
|
+
const activeFilterPills = [
|
|
329
|
+
selectedCollection ? `collection:${selectedCollection}` : null,
|
|
330
|
+
since ? `since:${since}` : null,
|
|
331
|
+
until ? `until:${until}` : null,
|
|
332
|
+
category.trim() ? `category:${category.trim()}` : null,
|
|
333
|
+
author.trim() ? `author:${author.trim()}` : null,
|
|
334
|
+
parseTagsCsv(tagsInput).length > 0
|
|
335
|
+
? `${tagMode}:${parseTagsCsv(tagsInput).join(",")}`
|
|
336
|
+
: null,
|
|
337
|
+
].filter((pill): pill is string => Boolean(pill));
|
|
338
|
+
|
|
252
339
|
return (
|
|
253
|
-
<div className="flex min-h-
|
|
254
|
-
{/* Header */}
|
|
340
|
+
<div className="flex h-dvh min-h-0 flex-col overflow-hidden">
|
|
255
341
|
<header className="glass sticky top-0 z-10 border-border/50 border-b">
|
|
256
342
|
<div className="flex items-center gap-4 px-8 py-4">
|
|
257
343
|
<Button
|
|
@@ -265,20 +351,16 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
265
351
|
</Button>
|
|
266
352
|
<h1 className="font-semibold text-xl">Ask</h1>
|
|
267
353
|
<div className="ml-auto flex items-center gap-4">
|
|
268
|
-
{/* Search depth selector */}
|
|
269
354
|
<ThoroughnessSelector
|
|
270
355
|
disabled={!capabilities?.hybrid}
|
|
271
356
|
onChange={setThoroughness}
|
|
272
357
|
value={thoroughness}
|
|
273
358
|
/>
|
|
274
359
|
|
|
275
|
-
{/* Divider */}
|
|
276
360
|
<div className="h-6 w-px bg-border/40" />
|
|
277
361
|
|
|
278
|
-
{/* AI model selector */}
|
|
279
362
|
<AIModelSelector />
|
|
280
363
|
|
|
281
|
-
{/* Capability badges */}
|
|
282
364
|
{capabilities && (
|
|
283
365
|
<div className="flex items-center gap-2">
|
|
284
366
|
{capabilities.vector && (
|
|
@@ -297,12 +379,168 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
297
379
|
</div>
|
|
298
380
|
</header>
|
|
299
381
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
382
|
+
<main className="min-h-0 flex-1 overflow-y-auto px-6 py-6 md:p-8">
|
|
383
|
+
<div className="mx-auto max-w-3xl space-y-5">
|
|
384
|
+
<Collapsible onOpenChange={setShowAdvanced} open={showAdvanced}>
|
|
385
|
+
<CollapsibleTrigger asChild>
|
|
386
|
+
<Button
|
|
387
|
+
className="gap-2"
|
|
388
|
+
size="sm"
|
|
389
|
+
type="button"
|
|
390
|
+
variant="outline"
|
|
391
|
+
>
|
|
392
|
+
<SlidersHorizontal className="size-4" />
|
|
393
|
+
Advanced Retrieval
|
|
394
|
+
<ChevronDown
|
|
395
|
+
className={cn(
|
|
396
|
+
"size-4 transition-transform",
|
|
397
|
+
showAdvanced && "rotate-180"
|
|
398
|
+
)}
|
|
399
|
+
/>
|
|
400
|
+
</Button>
|
|
401
|
+
</CollapsibleTrigger>
|
|
402
|
+
<CollapsibleContent className="pt-3">
|
|
403
|
+
<Card className="border-border/50 bg-card/50">
|
|
404
|
+
<CardContent className="space-y-4 pt-4">
|
|
405
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
406
|
+
<div>
|
|
407
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
408
|
+
Collection
|
|
409
|
+
</p>
|
|
410
|
+
<Select
|
|
411
|
+
onValueChange={(value) =>
|
|
412
|
+
setSelectedCollection(value === "all" ? "" : value)
|
|
413
|
+
}
|
|
414
|
+
value={selectedCollection || "all"}
|
|
415
|
+
>
|
|
416
|
+
<SelectTrigger className="w-full">
|
|
417
|
+
<SelectValue placeholder="All collections" />
|
|
418
|
+
</SelectTrigger>
|
|
419
|
+
<SelectContent>
|
|
420
|
+
<SelectItem value="all">All collections</SelectItem>
|
|
421
|
+
{collections.map((collection) => (
|
|
422
|
+
<SelectItem
|
|
423
|
+
key={collection.name}
|
|
424
|
+
value={collection.name}
|
|
425
|
+
>
|
|
426
|
+
{collection.name}
|
|
427
|
+
</SelectItem>
|
|
428
|
+
))}
|
|
429
|
+
</SelectContent>
|
|
430
|
+
</Select>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
<div>
|
|
434
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
435
|
+
Author
|
|
436
|
+
</p>
|
|
437
|
+
<Input
|
|
438
|
+
onChange={(e) => setAuthor(e.target.value)}
|
|
439
|
+
placeholder="gordon"
|
|
440
|
+
value={author}
|
|
441
|
+
/>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<div>
|
|
445
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
446
|
+
Category
|
|
447
|
+
</p>
|
|
448
|
+
<Input
|
|
449
|
+
onChange={(e) => setCategory(e.target.value)}
|
|
450
|
+
placeholder="engineering, research"
|
|
451
|
+
value={category}
|
|
452
|
+
/>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
456
|
+
<div>
|
|
457
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
458
|
+
Since
|
|
459
|
+
</p>
|
|
460
|
+
<Input
|
|
461
|
+
onChange={(e) => setSince(e.target.value)}
|
|
462
|
+
type="date"
|
|
463
|
+
value={since}
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
466
|
+
<div>
|
|
467
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
468
|
+
Until
|
|
469
|
+
</p>
|
|
470
|
+
<Input
|
|
471
|
+
onChange={(e) => setUntil(e.target.value)}
|
|
472
|
+
type="date"
|
|
473
|
+
value={until}
|
|
474
|
+
/>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<div className="md:col-span-2">
|
|
479
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
480
|
+
Tags (comma separated)
|
|
481
|
+
</p>
|
|
482
|
+
<Input
|
|
483
|
+
onChange={(e) => setTagsInput(e.target.value)}
|
|
484
|
+
placeholder="project/alpha, urgent"
|
|
485
|
+
value={tagsInput}
|
|
486
|
+
/>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
491
|
+
<span className="text-muted-foreground text-xs">
|
|
492
|
+
Tag match:
|
|
493
|
+
</span>
|
|
494
|
+
<Button
|
|
495
|
+
onClick={() => setTagMode("any")}
|
|
496
|
+
size="sm"
|
|
497
|
+
type="button"
|
|
498
|
+
variant={tagMode === "any" ? "default" : "outline"}
|
|
499
|
+
>
|
|
500
|
+
Any
|
|
501
|
+
</Button>
|
|
502
|
+
<Button
|
|
503
|
+
onClick={() => setTagMode("all")}
|
|
504
|
+
size="sm"
|
|
505
|
+
type="button"
|
|
506
|
+
variant={tagMode === "all" ? "default" : "outline"}
|
|
507
|
+
>
|
|
508
|
+
All
|
|
509
|
+
</Button>
|
|
510
|
+
<Button
|
|
511
|
+
className="ml-auto"
|
|
512
|
+
onClick={clearFilters}
|
|
513
|
+
size="sm"
|
|
514
|
+
type="button"
|
|
515
|
+
variant="ghost"
|
|
516
|
+
>
|
|
517
|
+
Clear filters
|
|
518
|
+
</Button>
|
|
519
|
+
</div>
|
|
520
|
+
</CardContent>
|
|
521
|
+
</Card>
|
|
522
|
+
</CollapsibleContent>
|
|
523
|
+
</Collapsible>
|
|
524
|
+
|
|
525
|
+
{activeFilterPills.length > 0 && (
|
|
526
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
527
|
+
<span className="font-mono text-muted-foreground text-xs">
|
|
528
|
+
Filters:
|
|
529
|
+
</span>
|
|
530
|
+
{activeFilterPills.map((pill) => (
|
|
531
|
+
<Badge
|
|
532
|
+
className="font-mono text-[10px]"
|
|
533
|
+
key={pill}
|
|
534
|
+
variant="outline"
|
|
535
|
+
>
|
|
536
|
+
{pill}
|
|
537
|
+
</Badge>
|
|
538
|
+
))}
|
|
539
|
+
</div>
|
|
540
|
+
)}
|
|
541
|
+
|
|
304
542
|
{conversation.length === 0 && (
|
|
305
|
-
<div className="py-
|
|
543
|
+
<div className="py-10 text-center md:py-14">
|
|
306
544
|
<Sparkles className="mx-auto mb-4 size-12 text-primary/60" />
|
|
307
545
|
<h2 className="mb-2 font-medium text-lg">Ask anything</h2>
|
|
308
546
|
<p className="text-muted-foreground">
|
|
@@ -313,17 +551,14 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
313
551
|
</div>
|
|
314
552
|
)}
|
|
315
553
|
|
|
316
|
-
{/* Conversation entries */}
|
|
317
554
|
{conversation.map((entry) => (
|
|
318
555
|
<div className="space-y-4" key={entry.id}>
|
|
319
|
-
{/* User query */}
|
|
320
556
|
<div className="flex justify-end">
|
|
321
557
|
<div className="max-w-[80%] rounded-lg bg-secondary px-4 py-3">
|
|
322
558
|
<p className="text-foreground">{entry.query}</p>
|
|
323
559
|
</div>
|
|
324
560
|
</div>
|
|
325
561
|
|
|
326
|
-
{/* AI response */}
|
|
327
562
|
<div className="space-y-3">
|
|
328
563
|
{entry.loading && (
|
|
329
564
|
<div className="flex items-center gap-3">
|
|
@@ -344,7 +579,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
344
579
|
|
|
345
580
|
{entry.response && (
|
|
346
581
|
<>
|
|
347
|
-
{/* Answer */}
|
|
348
582
|
{entry.response.answer && (
|
|
349
583
|
<div className="prose prose-sm prose-invert max-w-none rounded-lg bg-card/50 p-4">
|
|
350
584
|
<p className="whitespace-pre-wrap leading-relaxed">
|
|
@@ -357,7 +591,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
357
591
|
</div>
|
|
358
592
|
)}
|
|
359
593
|
|
|
360
|
-
{/* Citations */}
|
|
361
594
|
{entry.response.citations &&
|
|
362
595
|
entry.response.citations.length > 0 && (
|
|
363
596
|
<Sources defaultOpen>
|
|
@@ -365,26 +598,26 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
365
598
|
count={entry.response.citations.length}
|
|
366
599
|
/>
|
|
367
600
|
<SourcesContent>
|
|
368
|
-
{entry.response.citations.map((
|
|
601
|
+
{entry.response.citations.map((citation, i) => (
|
|
369
602
|
<Source
|
|
370
603
|
href="#"
|
|
371
|
-
key={`${
|
|
372
|
-
onClick={(
|
|
373
|
-
|
|
604
|
+
key={`${citation.docid}-${i}`}
|
|
605
|
+
onClick={(event) => {
|
|
606
|
+
event.preventDefault();
|
|
374
607
|
navigate(
|
|
375
|
-
`/doc?uri=${encodeURIComponent(
|
|
608
|
+
`/doc?uri=${encodeURIComponent(citation.uri)}`
|
|
376
609
|
);
|
|
377
610
|
}}
|
|
378
|
-
title={`[${i + 1}] ${
|
|
611
|
+
title={`[${i + 1}] ${citation.uri.split("/").pop()}`}
|
|
379
612
|
>
|
|
380
613
|
<BookOpen className="size-4 shrink-0" />
|
|
381
614
|
<span className="truncate">
|
|
382
|
-
[{i + 1}] {
|
|
615
|
+
[{i + 1}] {citation.uri.split("/").pop()}
|
|
383
616
|
</span>
|
|
384
|
-
{
|
|
617
|
+
{citation.startLine && (
|
|
385
618
|
<span className="ml-1 font-mono text-[10px] text-muted-foreground/60">
|
|
386
|
-
L{
|
|
387
|
-
{
|
|
619
|
+
L{citation.startLine}
|
|
620
|
+
{citation.endLine && `-${citation.endLine}`}
|
|
388
621
|
</span>
|
|
389
622
|
)}
|
|
390
623
|
</Source>
|
|
@@ -393,7 +626,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
393
626
|
</Sources>
|
|
394
627
|
)}
|
|
395
628
|
|
|
396
|
-
{/* Meta info */}
|
|
397
629
|
<div className="flex items-center gap-2 text-muted-foreground/60 text-xs">
|
|
398
630
|
<span>{entry.response.results.length} results</span>
|
|
399
631
|
{entry.response.meta.vectorsUsed && (
|
|
@@ -414,20 +646,19 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
414
646
|
)}
|
|
415
647
|
</div>
|
|
416
648
|
|
|
417
|
-
{/* Show retrieved sources if no answer */}
|
|
418
649
|
{!entry.response.answer &&
|
|
419
650
|
entry.response.results.length > 0 && (
|
|
420
651
|
<div className="space-y-2">
|
|
421
652
|
<p className="font-medium text-muted-foreground text-sm">
|
|
422
653
|
Search results:
|
|
423
654
|
</p>
|
|
424
|
-
{entry.response.results.map((
|
|
655
|
+
{entry.response.results.map((result, i) => (
|
|
425
656
|
<Card
|
|
426
657
|
className="cursor-pointer transition-colors hover:border-primary/50"
|
|
427
|
-
key={`${
|
|
658
|
+
key={`${result.docid}-${i}`}
|
|
428
659
|
onClick={() =>
|
|
429
660
|
navigate(
|
|
430
|
-
`/doc?uri=${encodeURIComponent(
|
|
661
|
+
`/doc?uri=${encodeURIComponent(result.uri)}`
|
|
431
662
|
)
|
|
432
663
|
}
|
|
433
664
|
>
|
|
@@ -435,17 +666,18 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
435
666
|
<div className="flex items-start justify-between gap-2">
|
|
436
667
|
<div className="min-w-0">
|
|
437
668
|
<p className="font-medium text-primary text-sm">
|
|
438
|
-
{
|
|
669
|
+
{result.title ||
|
|
670
|
+
result.uri.split("/").pop()}
|
|
439
671
|
</p>
|
|
440
672
|
<p className="line-clamp-2 text-muted-foreground text-xs">
|
|
441
|
-
{
|
|
673
|
+
{result.snippet.slice(0, 200)}...
|
|
442
674
|
</p>
|
|
443
675
|
</div>
|
|
444
676
|
<Badge
|
|
445
677
|
className="shrink-0 font-mono text-[10px]"
|
|
446
678
|
variant="secondary"
|
|
447
679
|
>
|
|
448
|
-
{(
|
|
680
|
+
{(result.score * 100).toFixed(0)}%
|
|
449
681
|
</Badge>
|
|
450
682
|
</div>
|
|
451
683
|
</CardContent>
|
|
@@ -454,7 +686,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
454
686
|
</div>
|
|
455
687
|
)}
|
|
456
688
|
|
|
457
|
-
{/* No results */}
|
|
458
689
|
{!entry.response.answer &&
|
|
459
690
|
entry.response.results.length === 0 && (
|
|
460
691
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
@@ -474,7 +705,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
474
705
|
</div>
|
|
475
706
|
</main>
|
|
476
707
|
|
|
477
|
-
{/* Input area */}
|
|
478
708
|
<footer className="glass sticky bottom-0 border-border/50 border-t">
|
|
479
709
|
<form
|
|
480
710
|
className="mx-auto flex max-w-3xl items-end gap-3 p-4"
|