@gmickel/gno 0.16.0 → 0.18.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 +55 -2
- package/package.json +4 -1
- package/src/cli/commands/ask.ts +13 -0
- package/src/cli/commands/models/use.ts +1 -0
- package/src/cli/commands/query.ts +3 -2
- package/src/cli/pager.ts +1 -1
- package/src/cli/program.ts +107 -0
- package/src/config/types.ts +2 -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/llm/nodeLlamaCpp/generation.ts +3 -1
- package/src/llm/registry.ts +1 -0
- package/src/llm/types.ts +2 -0
- package/src/mcp/tools/index.ts +34 -1
- package/src/mcp/tools/query.ts +26 -2
- package/src/mcp/tools/search.ts +10 -0
- package/src/mcp/tools/vsearch.ts +10 -0
- package/src/pipeline/answer.ts +324 -7
- package/src/pipeline/expansion.ts +282 -11
- package/src/pipeline/explain.ts +93 -5
- package/src/pipeline/hybrid.ts +273 -70
- package/src/pipeline/intent.ts +152 -0
- package/src/pipeline/query-modes.ts +125 -0
- package/src/pipeline/rerank.ts +109 -51
- package/src/pipeline/search.ts +58 -4
- package/src/pipeline/temporal.ts +257 -0
- package/src/pipeline/types.ts +67 -0
- package/src/pipeline/vsearch.ts +121 -10
- 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 +174 -0
- package/src/serve/public/pages/Ask.tsx +378 -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 +561 -120
- package/src/serve/routes/api.ts +247 -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,162 @@ 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 [intent, setIntent] = useState("");
|
|
168
|
+
const [candidateLimit, setCandidateLimit] = useState("");
|
|
169
|
+
const [since, setSince] = useState("");
|
|
170
|
+
const [until, setUntil] = useState("");
|
|
171
|
+
const [category, setCategory] = useState("");
|
|
172
|
+
const [author, setAuthor] = useState("");
|
|
173
|
+
const [tagMode, setTagMode] = useState<TagMode>("any");
|
|
174
|
+
const [tagsInput, setTagsInput] = useState("");
|
|
175
|
+
|
|
144
176
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
|
145
177
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
146
178
|
|
|
147
|
-
|
|
179
|
+
const hybridAvailable = capabilities?.hybrid ?? false;
|
|
180
|
+
|
|
148
181
|
useEffect(() => {
|
|
149
|
-
async function
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
182
|
+
async function bootstrap(): Promise<void> {
|
|
183
|
+
const [capsResult, collectionsResult] = await Promise.all([
|
|
184
|
+
apiFetch<Capabilities>("/api/capabilities"),
|
|
185
|
+
apiFetch<Collection[]>("/api/collections"),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
if (capsResult.data) {
|
|
189
|
+
const caps = capsResult.data;
|
|
190
|
+
setCapabilities(caps);
|
|
191
|
+
setThoroughness(caps.hybrid ? "balanced" : "fast");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (collectionsResult.data) {
|
|
195
|
+
setCollections(collectionsResult.data);
|
|
159
196
|
}
|
|
160
197
|
}
|
|
161
|
-
|
|
198
|
+
|
|
199
|
+
void bootstrap();
|
|
162
200
|
}, []);
|
|
163
201
|
|
|
164
|
-
// Cycle thoroughness with 't' key
|
|
165
202
|
const cycleThoroughness = useCallback(() => {
|
|
166
203
|
setThoroughness((current) => {
|
|
204
|
+
if (!hybridAvailable) {
|
|
205
|
+
return "fast";
|
|
206
|
+
}
|
|
167
207
|
const currentIdx = THOROUGHNESS_ORDER.indexOf(current);
|
|
168
208
|
const nextIdx = (currentIdx + 1) % THOROUGHNESS_ORDER.length;
|
|
169
209
|
return THOROUGHNESS_ORDER[nextIdx];
|
|
170
210
|
});
|
|
171
|
-
}, []);
|
|
211
|
+
}, [hybridAvailable]);
|
|
172
212
|
|
|
173
213
|
const shortcuts = useMemo(
|
|
174
214
|
() => [{ key: "t", action: cycleThoroughness }],
|
|
175
215
|
[cycleThoroughness]
|
|
176
216
|
);
|
|
177
|
-
|
|
178
217
|
useKeyboardShortcuts(shortcuts);
|
|
179
218
|
|
|
180
|
-
// Scroll to bottom when conversation updates
|
|
181
219
|
useEffect(() => {
|
|
182
220
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
183
|
-
}, []);
|
|
221
|
+
}, [conversation]);
|
|
184
222
|
|
|
185
|
-
const handleSubmit =
|
|
186
|
-
e.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
223
|
+
const handleSubmit = useCallback(
|
|
224
|
+
async (e: React.FormEvent) => {
|
|
225
|
+
e.preventDefault();
|
|
226
|
+
if (!query.trim()) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
190
229
|
|
|
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
|
-
}
|
|
230
|
+
const entryId = crypto.randomUUID();
|
|
231
|
+
const currentQuery = query.trim();
|
|
221
232
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
233
|
+
setConversation((prev) => [
|
|
234
|
+
...prev,
|
|
235
|
+
{ id: entryId, query: currentQuery, response: null, loading: true },
|
|
236
|
+
]);
|
|
237
|
+
setQuery("");
|
|
227
238
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
)
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
const requestBody: Record<string, unknown> = {
|
|
240
|
+
query: currentQuery,
|
|
241
|
+
limit: 5,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (selectedCollection) {
|
|
245
|
+
requestBody.collection = selectedCollection;
|
|
246
|
+
}
|
|
247
|
+
if (intent.trim()) {
|
|
248
|
+
requestBody.intent = intent.trim();
|
|
249
|
+
}
|
|
250
|
+
if (candidateLimit.trim()) {
|
|
251
|
+
requestBody.candidateLimit = Number(candidateLimit);
|
|
252
|
+
}
|
|
253
|
+
if (since) {
|
|
254
|
+
requestBody.since = since;
|
|
255
|
+
}
|
|
256
|
+
if (until) {
|
|
257
|
+
requestBody.until = until;
|
|
258
|
+
}
|
|
259
|
+
if (category.trim()) {
|
|
260
|
+
requestBody.category = category.trim();
|
|
261
|
+
}
|
|
262
|
+
if (author.trim()) {
|
|
263
|
+
requestBody.author = author.trim();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const normalizedTags = parseTagsCsv(tagsInput);
|
|
267
|
+
if (normalizedTags.length > 0) {
|
|
268
|
+
if (tagMode === "all") {
|
|
269
|
+
requestBody.tagsAll = normalizedTags.join(",");
|
|
270
|
+
} else {
|
|
271
|
+
requestBody.tagsAny = normalizedTags.join(",");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (thoroughness === "fast") {
|
|
276
|
+
requestBody.noExpand = true;
|
|
277
|
+
requestBody.noRerank = true;
|
|
278
|
+
} else if (thoroughness === "balanced") {
|
|
279
|
+
requestBody.noExpand = true;
|
|
280
|
+
requestBody.noRerank = false;
|
|
281
|
+
} else {
|
|
282
|
+
requestBody.noExpand = false;
|
|
283
|
+
requestBody.noRerank = false;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { data, error } = await apiFetch<AskResponse>("/api/ask", {
|
|
287
|
+
method: "POST",
|
|
288
|
+
body: JSON.stringify(requestBody),
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
setConversation((prev) =>
|
|
292
|
+
prev.map((entry) =>
|
|
293
|
+
entry.id === entryId
|
|
294
|
+
? {
|
|
295
|
+
...entry,
|
|
296
|
+
response: data ?? null,
|
|
297
|
+
loading: false,
|
|
298
|
+
error: error ?? undefined,
|
|
299
|
+
}
|
|
300
|
+
: entry
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
},
|
|
304
|
+
[
|
|
305
|
+
author,
|
|
306
|
+
candidateLimit,
|
|
307
|
+
category,
|
|
308
|
+
intent,
|
|
309
|
+
query,
|
|
310
|
+
selectedCollection,
|
|
311
|
+
since,
|
|
312
|
+
tagMode,
|
|
313
|
+
tagsInput,
|
|
314
|
+
thoroughness,
|
|
315
|
+
until,
|
|
316
|
+
]
|
|
317
|
+
);
|
|
242
318
|
|
|
243
319
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
244
320
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
@@ -247,11 +323,35 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
247
323
|
}
|
|
248
324
|
};
|
|
249
325
|
|
|
326
|
+
const clearFilters = () => {
|
|
327
|
+
setSelectedCollection("");
|
|
328
|
+
setIntent("");
|
|
329
|
+
setCandidateLimit("");
|
|
330
|
+
setSince("");
|
|
331
|
+
setUntil("");
|
|
332
|
+
setCategory("");
|
|
333
|
+
setAuthor("");
|
|
334
|
+
setTagsInput("");
|
|
335
|
+
setTagMode("any");
|
|
336
|
+
};
|
|
337
|
+
|
|
250
338
|
const answerAvailable = capabilities?.answer ?? false;
|
|
251
339
|
|
|
340
|
+
const activeFilterPills = [
|
|
341
|
+
selectedCollection ? `collection:${selectedCollection}` : null,
|
|
342
|
+
intent.trim() ? `intent:${intent.trim()}` : null,
|
|
343
|
+
candidateLimit.trim() ? `candidates:${candidateLimit.trim()}` : null,
|
|
344
|
+
since ? `since:${since}` : null,
|
|
345
|
+
until ? `until:${until}` : null,
|
|
346
|
+
category.trim() ? `category:${category.trim()}` : null,
|
|
347
|
+
author.trim() ? `author:${author.trim()}` : null,
|
|
348
|
+
parseTagsCsv(tagsInput).length > 0
|
|
349
|
+
? `${tagMode}:${parseTagsCsv(tagsInput).join(",")}`
|
|
350
|
+
: null,
|
|
351
|
+
].filter((pill): pill is string => Boolean(pill));
|
|
352
|
+
|
|
252
353
|
return (
|
|
253
|
-
<div className="flex min-h-
|
|
254
|
-
{/* Header */}
|
|
354
|
+
<div className="flex h-dvh min-h-0 flex-col overflow-hidden">
|
|
255
355
|
<header className="glass sticky top-0 z-10 border-border/50 border-b">
|
|
256
356
|
<div className="flex items-center gap-4 px-8 py-4">
|
|
257
357
|
<Button
|
|
@@ -265,20 +365,16 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
265
365
|
</Button>
|
|
266
366
|
<h1 className="font-semibold text-xl">Ask</h1>
|
|
267
367
|
<div className="ml-auto flex items-center gap-4">
|
|
268
|
-
{/* Search depth selector */}
|
|
269
368
|
<ThoroughnessSelector
|
|
270
369
|
disabled={!capabilities?.hybrid}
|
|
271
370
|
onChange={setThoroughness}
|
|
272
371
|
value={thoroughness}
|
|
273
372
|
/>
|
|
274
373
|
|
|
275
|
-
{/* Divider */}
|
|
276
374
|
<div className="h-6 w-px bg-border/40" />
|
|
277
375
|
|
|
278
|
-
{/* AI model selector */}
|
|
279
376
|
<AIModelSelector />
|
|
280
377
|
|
|
281
|
-
{/* Capability badges */}
|
|
282
378
|
{capabilities && (
|
|
283
379
|
<div className="flex items-center gap-2">
|
|
284
380
|
{capabilities.vector && (
|
|
@@ -297,12 +393,193 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
297
393
|
</div>
|
|
298
394
|
</header>
|
|
299
395
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
396
|
+
<main className="min-h-0 flex-1 overflow-y-auto px-6 py-6 md:p-8">
|
|
397
|
+
<div className="mx-auto max-w-3xl space-y-5">
|
|
398
|
+
<Collapsible onOpenChange={setShowAdvanced} open={showAdvanced}>
|
|
399
|
+
<CollapsibleTrigger asChild>
|
|
400
|
+
<Button
|
|
401
|
+
className="gap-2"
|
|
402
|
+
size="sm"
|
|
403
|
+
type="button"
|
|
404
|
+
variant="outline"
|
|
405
|
+
>
|
|
406
|
+
<SlidersHorizontal className="size-4" />
|
|
407
|
+
Advanced Retrieval
|
|
408
|
+
<ChevronDown
|
|
409
|
+
className={cn(
|
|
410
|
+
"size-4 transition-transform",
|
|
411
|
+
showAdvanced && "rotate-180"
|
|
412
|
+
)}
|
|
413
|
+
/>
|
|
414
|
+
</Button>
|
|
415
|
+
</CollapsibleTrigger>
|
|
416
|
+
<CollapsibleContent className="pt-3">
|
|
417
|
+
<Card className="border-border/50 bg-card/50">
|
|
418
|
+
<CardContent className="space-y-4 pt-4">
|
|
419
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
420
|
+
<div>
|
|
421
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
422
|
+
Collection
|
|
423
|
+
</p>
|
|
424
|
+
<Select
|
|
425
|
+
onValueChange={(value) =>
|
|
426
|
+
setSelectedCollection(value === "all" ? "" : value)
|
|
427
|
+
}
|
|
428
|
+
value={selectedCollection || "all"}
|
|
429
|
+
>
|
|
430
|
+
<SelectTrigger className="w-full">
|
|
431
|
+
<SelectValue placeholder="All collections" />
|
|
432
|
+
</SelectTrigger>
|
|
433
|
+
<SelectContent>
|
|
434
|
+
<SelectItem value="all">All collections</SelectItem>
|
|
435
|
+
{collections.map((collection) => (
|
|
436
|
+
<SelectItem
|
|
437
|
+
key={collection.name}
|
|
438
|
+
value={collection.name}
|
|
439
|
+
>
|
|
440
|
+
{collection.name}
|
|
441
|
+
</SelectItem>
|
|
442
|
+
))}
|
|
443
|
+
</SelectContent>
|
|
444
|
+
</Select>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div>
|
|
448
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
449
|
+
Author
|
|
450
|
+
</p>
|
|
451
|
+
<Input
|
|
452
|
+
onChange={(e) => setAuthor(e.target.value)}
|
|
453
|
+
placeholder="gordon"
|
|
454
|
+
value={author}
|
|
455
|
+
/>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
<div className="md:col-span-2">
|
|
459
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
460
|
+
Intent
|
|
461
|
+
</p>
|
|
462
|
+
<Input
|
|
463
|
+
onChange={(e) => setIntent(e.target.value)}
|
|
464
|
+
placeholder="Disambiguate ambiguous questions without searching on this text"
|
|
465
|
+
value={intent}
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
<div>
|
|
470
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
471
|
+
Category
|
|
472
|
+
</p>
|
|
473
|
+
<Input
|
|
474
|
+
onChange={(e) => setCategory(e.target.value)}
|
|
475
|
+
placeholder="engineering, research"
|
|
476
|
+
value={category}
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
481
|
+
<div>
|
|
482
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
483
|
+
Since
|
|
484
|
+
</p>
|
|
485
|
+
<Input
|
|
486
|
+
onChange={(e) => setSince(e.target.value)}
|
|
487
|
+
type="date"
|
|
488
|
+
value={since}
|
|
489
|
+
/>
|
|
490
|
+
</div>
|
|
491
|
+
<div>
|
|
492
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
493
|
+
Until
|
|
494
|
+
</p>
|
|
495
|
+
<Input
|
|
496
|
+
onChange={(e) => setUntil(e.target.value)}
|
|
497
|
+
type="date"
|
|
498
|
+
value={until}
|
|
499
|
+
/>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<div>
|
|
504
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
505
|
+
Candidate limit
|
|
506
|
+
</p>
|
|
507
|
+
<Input
|
|
508
|
+
inputMode="numeric"
|
|
509
|
+
min="1"
|
|
510
|
+
onChange={(e) => setCandidateLimit(e.target.value)}
|
|
511
|
+
placeholder="20"
|
|
512
|
+
type="number"
|
|
513
|
+
value={candidateLimit}
|
|
514
|
+
/>
|
|
515
|
+
</div>
|
|
516
|
+
|
|
517
|
+
<div className="md:col-span-2">
|
|
518
|
+
<p className="mb-1 text-muted-foreground text-xs">
|
|
519
|
+
Tags (comma separated)
|
|
520
|
+
</p>
|
|
521
|
+
<Input
|
|
522
|
+
onChange={(e) => setTagsInput(e.target.value)}
|
|
523
|
+
placeholder="project/alpha, urgent"
|
|
524
|
+
value={tagsInput}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
530
|
+
<span className="text-muted-foreground text-xs">
|
|
531
|
+
Tag match:
|
|
532
|
+
</span>
|
|
533
|
+
<Button
|
|
534
|
+
onClick={() => setTagMode("any")}
|
|
535
|
+
size="sm"
|
|
536
|
+
type="button"
|
|
537
|
+
variant={tagMode === "any" ? "default" : "outline"}
|
|
538
|
+
>
|
|
539
|
+
Any
|
|
540
|
+
</Button>
|
|
541
|
+
<Button
|
|
542
|
+
onClick={() => setTagMode("all")}
|
|
543
|
+
size="sm"
|
|
544
|
+
type="button"
|
|
545
|
+
variant={tagMode === "all" ? "default" : "outline"}
|
|
546
|
+
>
|
|
547
|
+
All
|
|
548
|
+
</Button>
|
|
549
|
+
<Button
|
|
550
|
+
className="ml-auto"
|
|
551
|
+
onClick={clearFilters}
|
|
552
|
+
size="sm"
|
|
553
|
+
type="button"
|
|
554
|
+
variant="ghost"
|
|
555
|
+
>
|
|
556
|
+
Clear filters
|
|
557
|
+
</Button>
|
|
558
|
+
</div>
|
|
559
|
+
</CardContent>
|
|
560
|
+
</Card>
|
|
561
|
+
</CollapsibleContent>
|
|
562
|
+
</Collapsible>
|
|
563
|
+
|
|
564
|
+
{activeFilterPills.length > 0 && (
|
|
565
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
566
|
+
<span className="font-mono text-muted-foreground text-xs">
|
|
567
|
+
Filters:
|
|
568
|
+
</span>
|
|
569
|
+
{activeFilterPills.map((pill) => (
|
|
570
|
+
<Badge
|
|
571
|
+
className="font-mono text-[10px]"
|
|
572
|
+
key={pill}
|
|
573
|
+
variant="outline"
|
|
574
|
+
>
|
|
575
|
+
{pill}
|
|
576
|
+
</Badge>
|
|
577
|
+
))}
|
|
578
|
+
</div>
|
|
579
|
+
)}
|
|
580
|
+
|
|
304
581
|
{conversation.length === 0 && (
|
|
305
|
-
<div className="py-
|
|
582
|
+
<div className="py-10 text-center md:py-14">
|
|
306
583
|
<Sparkles className="mx-auto mb-4 size-12 text-primary/60" />
|
|
307
584
|
<h2 className="mb-2 font-medium text-lg">Ask anything</h2>
|
|
308
585
|
<p className="text-muted-foreground">
|
|
@@ -313,17 +590,14 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
313
590
|
</div>
|
|
314
591
|
)}
|
|
315
592
|
|
|
316
|
-
{/* Conversation entries */}
|
|
317
593
|
{conversation.map((entry) => (
|
|
318
594
|
<div className="space-y-4" key={entry.id}>
|
|
319
|
-
{/* User query */}
|
|
320
595
|
<div className="flex justify-end">
|
|
321
596
|
<div className="max-w-[80%] rounded-lg bg-secondary px-4 py-3">
|
|
322
597
|
<p className="text-foreground">{entry.query}</p>
|
|
323
598
|
</div>
|
|
324
599
|
</div>
|
|
325
600
|
|
|
326
|
-
{/* AI response */}
|
|
327
601
|
<div className="space-y-3">
|
|
328
602
|
{entry.loading && (
|
|
329
603
|
<div className="flex items-center gap-3">
|
|
@@ -344,7 +618,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
344
618
|
|
|
345
619
|
{entry.response && (
|
|
346
620
|
<>
|
|
347
|
-
{/* Answer */}
|
|
348
621
|
{entry.response.answer && (
|
|
349
622
|
<div className="prose prose-sm prose-invert max-w-none rounded-lg bg-card/50 p-4">
|
|
350
623
|
<p className="whitespace-pre-wrap leading-relaxed">
|
|
@@ -357,7 +630,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
357
630
|
</div>
|
|
358
631
|
)}
|
|
359
632
|
|
|
360
|
-
{/* Citations */}
|
|
361
633
|
{entry.response.citations &&
|
|
362
634
|
entry.response.citations.length > 0 && (
|
|
363
635
|
<Sources defaultOpen>
|
|
@@ -365,26 +637,26 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
365
637
|
count={entry.response.citations.length}
|
|
366
638
|
/>
|
|
367
639
|
<SourcesContent>
|
|
368
|
-
{entry.response.citations.map((
|
|
640
|
+
{entry.response.citations.map((citation, i) => (
|
|
369
641
|
<Source
|
|
370
642
|
href="#"
|
|
371
|
-
key={`${
|
|
372
|
-
onClick={(
|
|
373
|
-
|
|
643
|
+
key={`${citation.docid}-${i}`}
|
|
644
|
+
onClick={(event) => {
|
|
645
|
+
event.preventDefault();
|
|
374
646
|
navigate(
|
|
375
|
-
`/doc?uri=${encodeURIComponent(
|
|
647
|
+
`/doc?uri=${encodeURIComponent(citation.uri)}`
|
|
376
648
|
);
|
|
377
649
|
}}
|
|
378
|
-
title={`[${i + 1}] ${
|
|
650
|
+
title={`[${i + 1}] ${citation.uri.split("/").pop()}`}
|
|
379
651
|
>
|
|
380
652
|
<BookOpen className="size-4 shrink-0" />
|
|
381
653
|
<span className="truncate">
|
|
382
|
-
[{i + 1}] {
|
|
654
|
+
[{i + 1}] {citation.uri.split("/").pop()}
|
|
383
655
|
</span>
|
|
384
|
-
{
|
|
656
|
+
{citation.startLine && (
|
|
385
657
|
<span className="ml-1 font-mono text-[10px] text-muted-foreground/60">
|
|
386
|
-
L{
|
|
387
|
-
{
|
|
658
|
+
L{citation.startLine}
|
|
659
|
+
{citation.endLine && `-${citation.endLine}`}
|
|
388
660
|
</span>
|
|
389
661
|
)}
|
|
390
662
|
</Source>
|
|
@@ -393,7 +665,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
393
665
|
</Sources>
|
|
394
666
|
)}
|
|
395
667
|
|
|
396
|
-
{/* Meta info */}
|
|
397
668
|
<div className="flex items-center gap-2 text-muted-foreground/60 text-xs">
|
|
398
669
|
<span>{entry.response.results.length} results</span>
|
|
399
670
|
{entry.response.meta.vectorsUsed && (
|
|
@@ -414,20 +685,19 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
414
685
|
)}
|
|
415
686
|
</div>
|
|
416
687
|
|
|
417
|
-
{/* Show retrieved sources if no answer */}
|
|
418
688
|
{!entry.response.answer &&
|
|
419
689
|
entry.response.results.length > 0 && (
|
|
420
690
|
<div className="space-y-2">
|
|
421
691
|
<p className="font-medium text-muted-foreground text-sm">
|
|
422
692
|
Search results:
|
|
423
693
|
</p>
|
|
424
|
-
{entry.response.results.map((
|
|
694
|
+
{entry.response.results.map((result, i) => (
|
|
425
695
|
<Card
|
|
426
696
|
className="cursor-pointer transition-colors hover:border-primary/50"
|
|
427
|
-
key={`${
|
|
697
|
+
key={`${result.docid}-${i}`}
|
|
428
698
|
onClick={() =>
|
|
429
699
|
navigate(
|
|
430
|
-
`/doc?uri=${encodeURIComponent(
|
|
700
|
+
`/doc?uri=${encodeURIComponent(result.uri)}`
|
|
431
701
|
)
|
|
432
702
|
}
|
|
433
703
|
>
|
|
@@ -435,17 +705,18 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
435
705
|
<div className="flex items-start justify-between gap-2">
|
|
436
706
|
<div className="min-w-0">
|
|
437
707
|
<p className="font-medium text-primary text-sm">
|
|
438
|
-
{
|
|
708
|
+
{result.title ||
|
|
709
|
+
result.uri.split("/").pop()}
|
|
439
710
|
</p>
|
|
440
711
|
<p className="line-clamp-2 text-muted-foreground text-xs">
|
|
441
|
-
{
|
|
712
|
+
{result.snippet.slice(0, 200)}...
|
|
442
713
|
</p>
|
|
443
714
|
</div>
|
|
444
715
|
<Badge
|
|
445
716
|
className="shrink-0 font-mono text-[10px]"
|
|
446
717
|
variant="secondary"
|
|
447
718
|
>
|
|
448
|
-
{(
|
|
719
|
+
{(result.score * 100).toFixed(0)}%
|
|
449
720
|
</Badge>
|
|
450
721
|
</div>
|
|
451
722
|
</CardContent>
|
|
@@ -454,7 +725,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
454
725
|
</div>
|
|
455
726
|
)}
|
|
456
727
|
|
|
457
|
-
{/* No results */}
|
|
458
728
|
{!entry.response.answer &&
|
|
459
729
|
entry.response.results.length === 0 && (
|
|
460
730
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
@@ -474,7 +744,6 @@ export default function Ask({ navigate }: PageProps) {
|
|
|
474
744
|
</div>
|
|
475
745
|
</main>
|
|
476
746
|
|
|
477
|
-
{/* Input area */}
|
|
478
747
|
<footer className="glass sticky bottom-0 border-border/50 border-t">
|
|
479
748
|
<form
|
|
480
749
|
className="mx-auto flex max-w-3xl items-end gap-3 p-4"
|