@gmickel/gno 0.7.0 → 0.8.1
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 +90 -50
- package/THIRD_PARTY_NOTICES.md +22 -0
- package/assets/screenshots/webui-ask-answer.png +0 -0
- package/assets/screenshots/webui-collections.png +0 -0
- package/assets/screenshots/webui-editor.png +0 -0
- package/assets/screenshots/webui-home.png +0 -0
- package/assets/skill/SKILL.md +12 -12
- package/assets/skill/cli-reference.md +59 -57
- package/assets/skill/examples.md +8 -7
- package/assets/skill/mcp-reference.md +8 -4
- package/package.json +31 -24
- package/src/app/constants.ts +43 -42
- package/src/cli/colors.ts +1 -1
- package/src/cli/commands/ask.ts +44 -43
- package/src/cli/commands/cleanup.ts +9 -8
- package/src/cli/commands/collection/add.ts +12 -12
- package/src/cli/commands/collection/index.ts +4 -4
- package/src/cli/commands/collection/list.ts +26 -25
- package/src/cli/commands/collection/remove.ts +10 -10
- package/src/cli/commands/collection/rename.ts +10 -10
- package/src/cli/commands/context/add.ts +1 -1
- package/src/cli/commands/context/check.ts +17 -17
- package/src/cli/commands/context/index.ts +4 -4
- package/src/cli/commands/context/list.ts +11 -11
- package/src/cli/commands/context/rm.ts +1 -1
- package/src/cli/commands/doctor.ts +86 -84
- package/src/cli/commands/embed.ts +30 -28
- package/src/cli/commands/get.ts +27 -26
- package/src/cli/commands/index-cmd.ts +9 -9
- package/src/cli/commands/index.ts +16 -16
- package/src/cli/commands/init.ts +13 -12
- package/src/cli/commands/ls.ts +20 -19
- package/src/cli/commands/mcp/config.ts +30 -28
- package/src/cli/commands/mcp/index.ts +4 -4
- package/src/cli/commands/mcp/install.ts +17 -17
- package/src/cli/commands/mcp/paths.ts +133 -133
- package/src/cli/commands/mcp/status.ts +21 -21
- package/src/cli/commands/mcp/uninstall.ts +13 -13
- package/src/cli/commands/mcp.ts +2 -2
- package/src/cli/commands/models/clear.ts +12 -11
- package/src/cli/commands/models/index.ts +5 -5
- package/src/cli/commands/models/list.ts +31 -30
- package/src/cli/commands/models/path.ts +1 -1
- package/src/cli/commands/models/pull.ts +19 -18
- package/src/cli/commands/models/use.ts +4 -4
- package/src/cli/commands/multi-get.ts +38 -36
- package/src/cli/commands/query.ts +21 -20
- package/src/cli/commands/ref-parser.ts +10 -10
- package/src/cli/commands/reset.ts +40 -39
- package/src/cli/commands/search.ts +14 -13
- package/src/cli/commands/serve.ts +4 -4
- package/src/cli/commands/shared.ts +11 -10
- package/src/cli/commands/skill/index.ts +5 -5
- package/src/cli/commands/skill/install.ts +18 -17
- package/src/cli/commands/skill/paths-cmd.ts +11 -10
- package/src/cli/commands/skill/paths.ts +23 -23
- package/src/cli/commands/skill/show.ts +13 -12
- package/src/cli/commands/skill/uninstall.ts +16 -15
- package/src/cli/commands/status.ts +25 -24
- package/src/cli/commands/update.ts +3 -3
- package/src/cli/commands/vsearch.ts +17 -16
- package/src/cli/context.ts +5 -5
- package/src/cli/errors.ts +3 -3
- package/src/cli/format/search-results.ts +37 -37
- package/src/cli/options.ts +43 -43
- package/src/cli/program.ts +455 -459
- package/src/cli/progress.ts +1 -1
- package/src/cli/run.ts +24 -23
- package/src/collection/add.ts +9 -8
- package/src/collection/index.ts +3 -3
- package/src/collection/remove.ts +7 -6
- package/src/collection/types.ts +6 -6
- package/src/config/defaults.ts +1 -1
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +19 -18
- package/src/config/paths.ts +9 -8
- package/src/config/saver.ts +14 -13
- package/src/config/types.ts +53 -52
- package/src/converters/adapters/markitdownTs/adapter.ts +21 -19
- package/src/converters/adapters/officeparser/adapter.ts +18 -16
- package/src/converters/canonicalize.ts +12 -12
- package/src/converters/errors.ts +26 -22
- package/src/converters/index.ts +8 -8
- package/src/converters/mime.ts +25 -25
- package/src/converters/native/markdown.ts +10 -9
- package/src/converters/native/plaintext.ts +8 -7
- package/src/converters/path.ts +2 -2
- package/src/converters/pipeline.ts +11 -10
- package/src/converters/registry.ts +8 -8
- package/src/converters/types.ts +14 -14
- package/src/converters/versions.ts +4 -4
- package/src/index.ts +4 -4
- package/src/ingestion/chunker.ts +10 -9
- package/src/ingestion/index.ts +6 -6
- package/src/ingestion/language.ts +62 -62
- package/src/ingestion/sync.ts +50 -49
- package/src/ingestion/types.ts +10 -10
- package/src/ingestion/walker.ts +14 -13
- package/src/llm/cache.ts +51 -49
- package/src/llm/errors.ts +40 -36
- package/src/llm/index.ts +9 -9
- package/src/llm/lockfile.ts +6 -6
- package/src/llm/nodeLlamaCpp/adapter.ts +13 -12
- package/src/llm/nodeLlamaCpp/embedding.ts +9 -8
- package/src/llm/nodeLlamaCpp/generation.ts +7 -6
- package/src/llm/nodeLlamaCpp/lifecycle.ts +11 -10
- package/src/llm/nodeLlamaCpp/rerank.ts +6 -5
- package/src/llm/policy.ts +5 -5
- package/src/llm/registry.ts +6 -5
- package/src/llm/types.ts +2 -2
- package/src/mcp/resources/index.ts +15 -13
- package/src/mcp/server.ts +25 -23
- package/src/mcp/tools/get.ts +25 -23
- package/src/mcp/tools/index.ts +32 -29
- package/src/mcp/tools/multi-get.ts +34 -32
- package/src/mcp/tools/query.ts +29 -27
- package/src/mcp/tools/search.ts +14 -12
- package/src/mcp/tools/status.ts +12 -11
- package/src/mcp/tools/vsearch.ts +26 -24
- package/src/pipeline/answer.ts +9 -9
- package/src/pipeline/chunk-lookup.ts +1 -1
- package/src/pipeline/contextual.ts +4 -4
- package/src/pipeline/expansion.ts +23 -21
- package/src/pipeline/explain.ts +21 -21
- package/src/pipeline/fusion.ts +9 -9
- package/src/pipeline/hybrid.ts +41 -42
- package/src/pipeline/index.ts +10 -10
- package/src/pipeline/query-language.ts +39 -39
- package/src/pipeline/rerank.ts +8 -7
- package/src/pipeline/search.ts +22 -22
- package/src/pipeline/types.ts +8 -8
- package/src/pipeline/vsearch.ts +21 -24
- package/src/serve/CLAUDE.md +21 -15
- package/src/serve/config-sync.ts +9 -8
- package/src/serve/context.ts +19 -18
- package/src/serve/index.ts +1 -1
- package/src/serve/jobs.ts +7 -7
- package/src/serve/public/app.tsx +79 -25
- package/src/serve/public/components/AddCollectionDialog.tsx +382 -0
- package/src/serve/public/components/CaptureButton.tsx +60 -0
- package/src/serve/public/components/CaptureModal.tsx +365 -0
- package/src/serve/public/components/IndexingProgress.tsx +333 -0
- package/src/serve/public/components/ShortcutHelpModal.tsx +106 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +42 -32
- package/src/serve/public/components/ai-elements/conversation.tsx +16 -14
- package/src/serve/public/components/ai-elements/inline-citation.tsx +33 -32
- package/src/serve/public/components/ai-elements/loader.tsx +5 -4
- package/src/serve/public/components/ai-elements/message.tsx +39 -37
- package/src/serve/public/components/ai-elements/prompt-input.tsx +97 -95
- package/src/serve/public/components/ai-elements/sources.tsx +12 -10
- package/src/serve/public/components/ai-elements/suggestion.tsx +10 -9
- package/src/serve/public/components/editor/CodeMirrorEditor.tsx +142 -0
- package/src/serve/public/components/editor/MarkdownPreview.tsx +311 -0
- package/src/serve/public/components/editor/index.ts +6 -0
- package/src/serve/public/components/preset-selector.tsx +29 -28
- package/src/serve/public/components/ui/badge.tsx +13 -12
- package/src/serve/public/components/ui/button-group.tsx +13 -12
- package/src/serve/public/components/ui/button.tsx +23 -22
- package/src/serve/public/components/ui/card.tsx +16 -16
- package/src/serve/public/components/ui/carousel.tsx +36 -35
- package/src/serve/public/components/ui/collapsible.tsx +1 -1
- package/src/serve/public/components/ui/command.tsx +17 -15
- package/src/serve/public/components/ui/dialog.tsx +13 -12
- package/src/serve/public/components/ui/dropdown-menu.tsx +13 -12
- package/src/serve/public/components/ui/hover-card.tsx +6 -5
- package/src/serve/public/components/ui/input-group.tsx +45 -43
- package/src/serve/public/components/ui/input.tsx +6 -6
- package/src/serve/public/components/ui/progress.tsx +5 -4
- package/src/serve/public/components/ui/scroll-area.tsx +11 -10
- package/src/serve/public/components/ui/select.tsx +19 -18
- package/src/serve/public/components/ui/separator.tsx +6 -5
- package/src/serve/public/components/ui/table.tsx +18 -18
- package/src/serve/public/components/ui/textarea.tsx +4 -4
- package/src/serve/public/components/ui/tooltip.tsx +5 -4
- package/src/serve/public/globals.css +27 -4
- package/src/serve/public/hooks/use-api.ts +8 -8
- package/src/serve/public/hooks/useCaptureModal.tsx +83 -0
- package/src/serve/public/hooks/useKeyboardShortcuts.ts +85 -0
- package/src/serve/public/index.html +4 -4
- package/src/serve/public/lib/utils.ts +6 -0
- package/src/serve/public/pages/Ask.tsx +27 -26
- package/src/serve/public/pages/Browse.tsx +28 -27
- package/src/serve/public/pages/Collections.tsx +439 -0
- package/src/serve/public/pages/Dashboard.tsx +166 -40
- package/src/serve/public/pages/DocView.tsx +258 -73
- package/src/serve/public/pages/DocumentEditor.tsx +510 -0
- package/src/serve/public/pages/Search.tsx +80 -58
- package/src/serve/routes/api.ts +272 -155
- package/src/serve/security.ts +4 -4
- package/src/serve/server.ts +66 -48
- package/src/store/index.ts +5 -5
- package/src/store/migrations/001-initial.ts +24 -23
- package/src/store/migrations/002-documents-fts.ts +7 -6
- package/src/store/migrations/index.ts +4 -4
- package/src/store/migrations/runner.ts +17 -15
- package/src/store/sqlite/adapter.ts +123 -121
- package/src/store/sqlite/fts5-snowball.ts +24 -23
- package/src/store/sqlite/index.ts +1 -1
- package/src/store/sqlite/setup.ts +12 -12
- package/src/store/sqlite/types.ts +4 -4
- package/src/store/types.ts +19 -19
- package/src/store/vector/index.ts +3 -3
- package/src/store/vector/sqlite-vec.ts +23 -20
- package/src/store/vector/stats.ts +10 -8
- package/src/store/vector/types.ts +2 -2
- package/vendor/fts5-snowball/README.md +6 -6
- package/assets/screenshots/webui-ask-answer.jpg +0 -0
- package/assets/screenshots/webui-home.jpg +0 -0
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { ArrowLeft, ChevronRight, FileText, FolderOpen } from
|
|
2
|
-
import { useEffect, useState } from
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { ArrowLeft, ChevronRight, FileText, FolderOpen } from "lucide-react";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
import { Loader } from "../components/ai-elements/loader";
|
|
5
|
+
import { Badge } from "../components/ui/badge";
|
|
6
|
+
import { Button } from "../components/ui/button";
|
|
6
7
|
import {
|
|
7
8
|
Select,
|
|
8
9
|
SelectContent,
|
|
9
10
|
SelectItem,
|
|
10
11
|
SelectTrigger,
|
|
11
12
|
SelectValue,
|
|
12
|
-
} from
|
|
13
|
+
} from "../components/ui/select";
|
|
13
14
|
import {
|
|
14
15
|
Table,
|
|
15
16
|
TableBody,
|
|
@@ -17,8 +18,8 @@ import {
|
|
|
17
18
|
TableHead,
|
|
18
19
|
TableHeader,
|
|
19
20
|
TableRow,
|
|
20
|
-
} from
|
|
21
|
-
import { apiFetch } from
|
|
21
|
+
} from "../components/ui/table";
|
|
22
|
+
import { apiFetch } from "../hooks/use-api";
|
|
22
23
|
|
|
23
24
|
interface PageProps {
|
|
24
25
|
navigate: (to: string | number) => void;
|
|
@@ -47,7 +48,7 @@ interface DocsResponse {
|
|
|
47
48
|
|
|
48
49
|
export default function Browse({ navigate }: PageProps) {
|
|
49
50
|
const [collections, setCollections] = useState<Collection[]>([]);
|
|
50
|
-
const [selected, setSelected] = useState<string>(
|
|
51
|
+
const [selected, setSelected] = useState<string>("");
|
|
51
52
|
const [docs, setDocs] = useState<Document[]>([]);
|
|
52
53
|
const [total, setTotal] = useState(0);
|
|
53
54
|
const [offset, setOffset] = useState(0);
|
|
@@ -58,14 +59,14 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
58
59
|
// Parse collection from URL on mount
|
|
59
60
|
useEffect(() => {
|
|
60
61
|
const params = new URLSearchParams(window.location.search);
|
|
61
|
-
const collection = params.get(
|
|
62
|
+
const collection = params.get("collection");
|
|
62
63
|
if (collection) {
|
|
63
64
|
setSelected(collection);
|
|
64
65
|
}
|
|
65
66
|
}, []);
|
|
66
67
|
|
|
67
68
|
useEffect(() => {
|
|
68
|
-
apiFetch<Collection[]>(
|
|
69
|
+
void apiFetch<Collection[]>("/api/collections").then(({ data }) => {
|
|
69
70
|
if (data) {
|
|
70
71
|
setCollections(data);
|
|
71
72
|
}
|
|
@@ -78,7 +79,7 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
78
79
|
? `/api/docs?collection=${encodeURIComponent(selected)}&limit=${limit}&offset=${offset}`
|
|
79
80
|
: `/api/docs?limit=${limit}&offset=${offset}`;
|
|
80
81
|
|
|
81
|
-
apiFetch<DocsResponse>(url).then(({ data }) => {
|
|
82
|
+
void apiFetch<DocsResponse>(url).then(({ data }) => {
|
|
82
83
|
setLoading(false);
|
|
83
84
|
setInitialLoad(false);
|
|
84
85
|
if (data) {
|
|
@@ -91,15 +92,15 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
91
92
|
}, [selected, offset]);
|
|
92
93
|
|
|
93
94
|
const handleCollectionChange = (value: string) => {
|
|
94
|
-
const newSelected = value ===
|
|
95
|
+
const newSelected = value === "all" ? "" : value;
|
|
95
96
|
setSelected(newSelected);
|
|
96
97
|
setOffset(0);
|
|
97
98
|
setDocs([]);
|
|
98
99
|
// Update URL for shareable deep-links
|
|
99
100
|
const url = newSelected
|
|
100
101
|
? `/browse?collection=${encodeURIComponent(newSelected)}`
|
|
101
|
-
:
|
|
102
|
-
window.history.pushState({},
|
|
102
|
+
: "/browse";
|
|
103
|
+
window.history.pushState({}, "", url);
|
|
103
104
|
};
|
|
104
105
|
|
|
105
106
|
const handleLoadMore = () => {
|
|
@@ -108,16 +109,16 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
108
109
|
|
|
109
110
|
const getExtBadgeVariant = (ext: string) => {
|
|
110
111
|
switch (ext.toLowerCase()) {
|
|
111
|
-
case
|
|
112
|
-
case
|
|
113
|
-
return
|
|
114
|
-
case
|
|
115
|
-
return
|
|
116
|
-
case
|
|
117
|
-
case
|
|
118
|
-
return
|
|
112
|
+
case ".md":
|
|
113
|
+
case ".markdown":
|
|
114
|
+
return "default";
|
|
115
|
+
case ".pdf":
|
|
116
|
+
return "destructive";
|
|
117
|
+
case ".docx":
|
|
118
|
+
case ".doc":
|
|
119
|
+
return "secondary";
|
|
119
120
|
default:
|
|
120
|
-
return
|
|
121
|
+
return "outline";
|
|
121
122
|
}
|
|
122
123
|
};
|
|
123
124
|
|
|
@@ -141,7 +142,7 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
141
142
|
<div className="flex items-center gap-4">
|
|
142
143
|
<Select
|
|
143
144
|
onValueChange={handleCollectionChange}
|
|
144
|
-
value={selected ||
|
|
145
|
+
value={selected || "all"}
|
|
145
146
|
>
|
|
146
147
|
<SelectTrigger className="w-[200px]">
|
|
147
148
|
<FolderOpen className="mr-2 size-4 text-muted-foreground" />
|
|
@@ -179,8 +180,8 @@ export default function Browse({ navigate }: PageProps) {
|
|
|
179
180
|
<h3 className="mb-2 font-medium text-lg">No documents found</h3>
|
|
180
181
|
<p className="text-muted-foreground">
|
|
181
182
|
{selected
|
|
182
|
-
?
|
|
183
|
-
:
|
|
183
|
+
? "This collection is empty"
|
|
184
|
+
: "Index some documents to get started"}
|
|
184
185
|
</p>
|
|
185
186
|
</div>
|
|
186
187
|
)}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections page - List and manage document collections.
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Grid of collection cards with stats
|
|
6
|
+
* - Re-index action per collection
|
|
7
|
+
* - Remove collection with confirmation
|
|
8
|
+
* - Refresh button
|
|
9
|
+
* - Empty state
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
AlertCircleIcon,
|
|
14
|
+
DatabaseIcon,
|
|
15
|
+
FileTextIcon,
|
|
16
|
+
FolderIcon,
|
|
17
|
+
FolderMinusIcon,
|
|
18
|
+
FolderPlusIcon,
|
|
19
|
+
LayersIcon,
|
|
20
|
+
Loader2Icon,
|
|
21
|
+
MoreVerticalIcon,
|
|
22
|
+
RefreshCwIcon,
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { useCallback, useEffect, useState } from "react";
|
|
25
|
+
|
|
26
|
+
import { AddCollectionDialog } from "../components/AddCollectionDialog";
|
|
27
|
+
import { Loader } from "../components/ai-elements/loader";
|
|
28
|
+
import { Badge } from "../components/ui/badge";
|
|
29
|
+
import { Button } from "../components/ui/button";
|
|
30
|
+
import {
|
|
31
|
+
Card,
|
|
32
|
+
CardContent,
|
|
33
|
+
CardHeader,
|
|
34
|
+
CardTitle,
|
|
35
|
+
} from "../components/ui/card";
|
|
36
|
+
import {
|
|
37
|
+
Dialog,
|
|
38
|
+
DialogContent,
|
|
39
|
+
DialogDescription,
|
|
40
|
+
DialogFooter,
|
|
41
|
+
DialogHeader,
|
|
42
|
+
DialogTitle,
|
|
43
|
+
} from "../components/ui/dialog";
|
|
44
|
+
import {
|
|
45
|
+
DropdownMenu,
|
|
46
|
+
DropdownMenuContent,
|
|
47
|
+
DropdownMenuItem,
|
|
48
|
+
DropdownMenuSeparator,
|
|
49
|
+
DropdownMenuTrigger,
|
|
50
|
+
} from "../components/ui/dropdown-menu";
|
|
51
|
+
import {
|
|
52
|
+
Tooltip,
|
|
53
|
+
TooltipContent,
|
|
54
|
+
TooltipProvider,
|
|
55
|
+
TooltipTrigger,
|
|
56
|
+
} from "../components/ui/tooltip";
|
|
57
|
+
import { apiFetch } from "../hooks/use-api";
|
|
58
|
+
|
|
59
|
+
interface PageProps {
|
|
60
|
+
navigate: (to: string | number) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface CollectionStats {
|
|
64
|
+
name: string;
|
|
65
|
+
path: string;
|
|
66
|
+
documentCount: number;
|
|
67
|
+
chunkCount: number;
|
|
68
|
+
embeddedCount: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface StatusResponse {
|
|
72
|
+
collections: CollectionStats[];
|
|
73
|
+
totalDocuments: number;
|
|
74
|
+
lastUpdated: string | null;
|
|
75
|
+
healthy: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface SyncResponse {
|
|
79
|
+
jobId: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface CollectionCardProps {
|
|
83
|
+
collection: CollectionStats;
|
|
84
|
+
onReindex: () => void;
|
|
85
|
+
onRemove: () => void;
|
|
86
|
+
isReindexing: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatNumber(n: number): string {
|
|
90
|
+
if (n >= 1000000) {
|
|
91
|
+
return `${(n / 1000000).toFixed(1)}M`;
|
|
92
|
+
}
|
|
93
|
+
if (n >= 1000) {
|
|
94
|
+
return `${(n / 1000).toFixed(1)}K`;
|
|
95
|
+
}
|
|
96
|
+
return n.toString();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function truncatePath(path: string, maxLength = 40): string {
|
|
100
|
+
if (path.length <= maxLength) return path;
|
|
101
|
+
const start = path.slice(0, 15);
|
|
102
|
+
const end = path.slice(-22);
|
|
103
|
+
return `${start}...${end}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function CollectionCard({
|
|
107
|
+
collection,
|
|
108
|
+
onReindex,
|
|
109
|
+
onRemove,
|
|
110
|
+
isReindexing,
|
|
111
|
+
}: CollectionCardProps) {
|
|
112
|
+
const embedPercent =
|
|
113
|
+
collection.chunkCount > 0
|
|
114
|
+
? Math.round((collection.embeddedCount / collection.chunkCount) * 100)
|
|
115
|
+
: 100;
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Card className="group relative overflow-hidden transition-all hover:border-primary/30">
|
|
119
|
+
<CardHeader className="pb-2">
|
|
120
|
+
<div className="flex items-start justify-between gap-2">
|
|
121
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
122
|
+
<div className="rounded-lg bg-primary/10 p-2">
|
|
123
|
+
<FolderIcon className="size-5 text-primary" />
|
|
124
|
+
</div>
|
|
125
|
+
<CardTitle className="truncate text-lg">
|
|
126
|
+
{collection.name}
|
|
127
|
+
</CardTitle>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<DropdownMenu>
|
|
131
|
+
<DropdownMenuTrigger asChild>
|
|
132
|
+
<Button
|
|
133
|
+
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
|
|
134
|
+
size="icon-sm"
|
|
135
|
+
variant="ghost"
|
|
136
|
+
>
|
|
137
|
+
<MoreVerticalIcon className="size-4" />
|
|
138
|
+
</Button>
|
|
139
|
+
</DropdownMenuTrigger>
|
|
140
|
+
<DropdownMenuContent align="end">
|
|
141
|
+
<DropdownMenuItem disabled={isReindexing} onClick={onReindex}>
|
|
142
|
+
{isReindexing ? (
|
|
143
|
+
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
|
144
|
+
) : (
|
|
145
|
+
<RefreshCwIcon className="mr-2 size-4" />
|
|
146
|
+
)}
|
|
147
|
+
Re-index
|
|
148
|
+
</DropdownMenuItem>
|
|
149
|
+
<DropdownMenuSeparator />
|
|
150
|
+
<DropdownMenuItem
|
|
151
|
+
className="text-destructive focus:text-destructive"
|
|
152
|
+
onClick={onRemove}
|
|
153
|
+
>
|
|
154
|
+
<FolderMinusIcon className="mr-2 size-4" />
|
|
155
|
+
Remove
|
|
156
|
+
</DropdownMenuItem>
|
|
157
|
+
</DropdownMenuContent>
|
|
158
|
+
</DropdownMenu>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<TooltipProvider>
|
|
162
|
+
<Tooltip>
|
|
163
|
+
<TooltipTrigger asChild>
|
|
164
|
+
<p className="truncate font-mono text-muted-foreground text-xs">
|
|
165
|
+
{truncatePath(collection.path)}
|
|
166
|
+
</p>
|
|
167
|
+
</TooltipTrigger>
|
|
168
|
+
<TooltipContent className="max-w-xs break-all">
|
|
169
|
+
<p className="font-mono text-xs">{collection.path}</p>
|
|
170
|
+
</TooltipContent>
|
|
171
|
+
</Tooltip>
|
|
172
|
+
</TooltipProvider>
|
|
173
|
+
</CardHeader>
|
|
174
|
+
|
|
175
|
+
<CardContent className="pt-2">
|
|
176
|
+
<div className="grid grid-cols-3 gap-3">
|
|
177
|
+
<div className="flex items-center gap-2">
|
|
178
|
+
<FileTextIcon className="size-4 text-muted-foreground" />
|
|
179
|
+
<div>
|
|
180
|
+
<div className="font-medium text-sm">
|
|
181
|
+
{formatNumber(collection.documentCount)}
|
|
182
|
+
</div>
|
|
183
|
+
<div className="text-muted-foreground text-xs">documents</div>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="flex items-center gap-2">
|
|
187
|
+
<LayersIcon className="size-4 text-muted-foreground" />
|
|
188
|
+
<div>
|
|
189
|
+
<div className="font-medium text-sm">
|
|
190
|
+
{formatNumber(collection.chunkCount)}
|
|
191
|
+
</div>
|
|
192
|
+
<div className="text-muted-foreground text-xs">chunks</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="flex items-center gap-2">
|
|
196
|
+
<DatabaseIcon className="size-4 text-muted-foreground" />
|
|
197
|
+
<div>
|
|
198
|
+
<div className="font-medium text-sm">{embedPercent}%</div>
|
|
199
|
+
<div className="text-muted-foreground text-xs">embedded</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{isReindexing && (
|
|
205
|
+
<div className="mt-3 flex items-center gap-2 text-muted-foreground text-sm">
|
|
206
|
+
<Loader2Icon className="size-4 animate-spin" />
|
|
207
|
+
<span>Re-indexing...</span>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</CardContent>
|
|
211
|
+
</Card>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export default function Collections({ navigate: _navigate }: PageProps) {
|
|
216
|
+
const [collections, setCollections] = useState<CollectionStats[]>([]);
|
|
217
|
+
const [loading, setLoading] = useState(true);
|
|
218
|
+
const [error, setError] = useState<string | null>(null);
|
|
219
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
220
|
+
const [reindexingCollections, setReindexingCollections] = useState<
|
|
221
|
+
Set<string>
|
|
222
|
+
>(new Set());
|
|
223
|
+
const [removeDialog, setRemoveDialog] = useState<CollectionStats | null>(
|
|
224
|
+
null
|
|
225
|
+
);
|
|
226
|
+
const [removing, setRemoving] = useState(false);
|
|
227
|
+
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
228
|
+
|
|
229
|
+
const loadCollections = useCallback(async () => {
|
|
230
|
+
const { data, error: err } = await apiFetch<StatusResponse>("/api/status");
|
|
231
|
+
if (err) {
|
|
232
|
+
setError(err);
|
|
233
|
+
} else if (data) {
|
|
234
|
+
setCollections(data.collections);
|
|
235
|
+
setError(null);
|
|
236
|
+
}
|
|
237
|
+
}, []);
|
|
238
|
+
|
|
239
|
+
// Initial load
|
|
240
|
+
useEffect(() => {
|
|
241
|
+
void loadCollections().finally(() => setLoading(false));
|
|
242
|
+
}, [loadCollections]);
|
|
243
|
+
|
|
244
|
+
// Refresh
|
|
245
|
+
const handleRefresh = async () => {
|
|
246
|
+
setRefreshing(true);
|
|
247
|
+
await loadCollections();
|
|
248
|
+
setRefreshing(false);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
// Re-index collection
|
|
252
|
+
const handleReindex = async (name: string) => {
|
|
253
|
+
setReindexingCollections((prev) => new Set([...prev, name]));
|
|
254
|
+
|
|
255
|
+
const { error: err } = await apiFetch<SyncResponse>("/api/sync", {
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: JSON.stringify({ collection: name }),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (err) {
|
|
261
|
+
// Show error briefly then remove from reindexing state
|
|
262
|
+
setTimeout(() => {
|
|
263
|
+
setReindexingCollections((prev) => {
|
|
264
|
+
const next = new Set(prev);
|
|
265
|
+
next.delete(name);
|
|
266
|
+
return next;
|
|
267
|
+
});
|
|
268
|
+
}, 1000);
|
|
269
|
+
} else {
|
|
270
|
+
// Poll for completion (simple approach - could use IndexingProgress component)
|
|
271
|
+
setTimeout(async () => {
|
|
272
|
+
await loadCollections();
|
|
273
|
+
setReindexingCollections((prev) => {
|
|
274
|
+
const next = new Set(prev);
|
|
275
|
+
next.delete(name);
|
|
276
|
+
return next;
|
|
277
|
+
});
|
|
278
|
+
}, 3000);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Remove collection
|
|
283
|
+
const handleRemove = async () => {
|
|
284
|
+
if (!removeDialog) return;
|
|
285
|
+
|
|
286
|
+
setRemoving(true);
|
|
287
|
+
const { error: err } = await apiFetch(
|
|
288
|
+
`/api/collections/${encodeURIComponent(removeDialog.name)}`,
|
|
289
|
+
{ method: "DELETE" }
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
setRemoving(false);
|
|
293
|
+
setRemoveDialog(null);
|
|
294
|
+
|
|
295
|
+
if (!err) {
|
|
296
|
+
await loadCollections();
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// Loading state
|
|
301
|
+
if (loading) {
|
|
302
|
+
return (
|
|
303
|
+
<div className="flex min-h-screen items-center justify-center">
|
|
304
|
+
<div className="flex flex-col items-center gap-4">
|
|
305
|
+
<Loader className="text-primary" size={32} />
|
|
306
|
+
<p className="text-muted-foreground">Loading collections...</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return (
|
|
313
|
+
<div className="min-h-screen">
|
|
314
|
+
{/* Header */}
|
|
315
|
+
<header className="glass sticky top-0 z-10 border-border/50 border-b">
|
|
316
|
+
<div className="flex items-center justify-between px-8 py-4">
|
|
317
|
+
<div className="flex items-center gap-3">
|
|
318
|
+
<FolderIcon className="size-5 text-primary" />
|
|
319
|
+
<h1 className="font-semibold text-xl">Collections</h1>
|
|
320
|
+
<Badge className="font-mono" variant="outline">
|
|
321
|
+
{collections.length}
|
|
322
|
+
</Badge>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
<div className="flex items-center gap-2">
|
|
326
|
+
<Button
|
|
327
|
+
disabled={refreshing}
|
|
328
|
+
onClick={handleRefresh}
|
|
329
|
+
size="sm"
|
|
330
|
+
variant="outline"
|
|
331
|
+
>
|
|
332
|
+
{refreshing ? (
|
|
333
|
+
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
334
|
+
) : (
|
|
335
|
+
<RefreshCwIcon className="mr-1.5 size-4" />
|
|
336
|
+
)}
|
|
337
|
+
Refresh
|
|
338
|
+
</Button>
|
|
339
|
+
<Button onClick={() => setAddDialogOpen(true)} size="sm">
|
|
340
|
+
<FolderPlusIcon className="mr-1.5 size-4" />
|
|
341
|
+
Add Collection
|
|
342
|
+
</Button>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
</header>
|
|
346
|
+
|
|
347
|
+
<main className="p-8">
|
|
348
|
+
{/* Error */}
|
|
349
|
+
{error && (
|
|
350
|
+
<Card className="mx-auto mb-6 max-w-md border-destructive bg-destructive/10">
|
|
351
|
+
<CardContent className="py-4 text-center">
|
|
352
|
+
<AlertCircleIcon className="mx-auto mb-2 size-8 text-destructive" />
|
|
353
|
+
<p className="text-destructive">{error}</p>
|
|
354
|
+
<Button
|
|
355
|
+
className="mt-3"
|
|
356
|
+
onClick={() => void loadCollections()}
|
|
357
|
+
size="sm"
|
|
358
|
+
variant="outline"
|
|
359
|
+
>
|
|
360
|
+
Retry
|
|
361
|
+
</Button>
|
|
362
|
+
</CardContent>
|
|
363
|
+
</Card>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{/* Empty state */}
|
|
367
|
+
{!error && collections.length === 0 && (
|
|
368
|
+
<div className="mx-auto max-w-md py-16 text-center">
|
|
369
|
+
<div className="mx-auto mb-4 flex size-16 items-center justify-center rounded-full bg-muted">
|
|
370
|
+
<FolderIcon className="size-8 text-muted-foreground" />
|
|
371
|
+
</div>
|
|
372
|
+
<h2 className="mb-2 font-semibold text-xl">No collections yet</h2>
|
|
373
|
+
<p className="mb-6 text-muted-foreground">
|
|
374
|
+
Add your first collection to start indexing documents.
|
|
375
|
+
</p>
|
|
376
|
+
<Button onClick={() => setAddDialogOpen(true)}>
|
|
377
|
+
<FolderPlusIcon className="mr-2 size-4" />
|
|
378
|
+
Add Collection
|
|
379
|
+
</Button>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{/* Collections grid */}
|
|
384
|
+
{!error && collections.length > 0 && (
|
|
385
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
386
|
+
{collections.map((collection) => (
|
|
387
|
+
<CollectionCard
|
|
388
|
+
collection={collection}
|
|
389
|
+
isReindexing={reindexingCollections.has(collection.name)}
|
|
390
|
+
key={collection.name}
|
|
391
|
+
onReindex={() => void handleReindex(collection.name)}
|
|
392
|
+
onRemove={() => setRemoveDialog(collection)}
|
|
393
|
+
/>
|
|
394
|
+
))}
|
|
395
|
+
</div>
|
|
396
|
+
)}
|
|
397
|
+
</main>
|
|
398
|
+
|
|
399
|
+
{/* Add collection dialog */}
|
|
400
|
+
<AddCollectionDialog
|
|
401
|
+
onOpenChange={setAddDialogOpen}
|
|
402
|
+
onSuccess={() => void loadCollections()}
|
|
403
|
+
open={addDialogOpen}
|
|
404
|
+
/>
|
|
405
|
+
|
|
406
|
+
{/* Remove confirmation dialog */}
|
|
407
|
+
<Dialog
|
|
408
|
+
onOpenChange={(open) => !open && setRemoveDialog(null)}
|
|
409
|
+
open={!!removeDialog}
|
|
410
|
+
>
|
|
411
|
+
<DialogContent>
|
|
412
|
+
<DialogHeader>
|
|
413
|
+
<DialogTitle>Remove collection</DialogTitle>
|
|
414
|
+
<DialogDescription>
|
|
415
|
+
Are you sure you want to remove{" "}
|
|
416
|
+
<strong>{removeDialog?.name}</strong>? Indexed documents will be
|
|
417
|
+
kept in the database but won't appear in searches.
|
|
418
|
+
</DialogDescription>
|
|
419
|
+
</DialogHeader>
|
|
420
|
+
<DialogFooter className="gap-2 sm:gap-0">
|
|
421
|
+
<Button onClick={() => setRemoveDialog(null)} variant="outline">
|
|
422
|
+
Cancel
|
|
423
|
+
</Button>
|
|
424
|
+
<Button
|
|
425
|
+
disabled={removing}
|
|
426
|
+
onClick={handleRemove}
|
|
427
|
+
variant="destructive"
|
|
428
|
+
>
|
|
429
|
+
{removing && (
|
|
430
|
+
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
431
|
+
)}
|
|
432
|
+
Remove
|
|
433
|
+
</Button>
|
|
434
|
+
</DialogFooter>
|
|
435
|
+
</DialogContent>
|
|
436
|
+
</Dialog>
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
}
|