@gmickel/gno 0.3.4 → 0.4.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.
Files changed (57) hide show
  1. package/README.md +194 -53
  2. package/assets/badges/license.svg +12 -0
  3. package/assets/badges/npm.svg +13 -0
  4. package/assets/badges/twitter.svg +22 -0
  5. package/assets/badges/website.svg +22 -0
  6. package/package.json +30 -1
  7. package/src/cli/commands/ask.ts +11 -186
  8. package/src/cli/commands/models/pull.ts +9 -4
  9. package/src/cli/commands/serve.ts +19 -0
  10. package/src/cli/program.ts +28 -0
  11. package/src/llm/registry.ts +3 -1
  12. package/src/pipeline/answer.ts +191 -0
  13. package/src/serve/CLAUDE.md +91 -0
  14. package/src/serve/bunfig.toml +2 -0
  15. package/src/serve/context.ts +181 -0
  16. package/src/serve/index.ts +7 -0
  17. package/src/serve/public/app.tsx +56 -0
  18. package/src/serve/public/components/ai-elements/code-block.tsx +176 -0
  19. package/src/serve/public/components/ai-elements/conversation.tsx +98 -0
  20. package/src/serve/public/components/ai-elements/inline-citation.tsx +285 -0
  21. package/src/serve/public/components/ai-elements/loader.tsx +96 -0
  22. package/src/serve/public/components/ai-elements/message.tsx +443 -0
  23. package/src/serve/public/components/ai-elements/prompt-input.tsx +1421 -0
  24. package/src/serve/public/components/ai-elements/sources.tsx +75 -0
  25. package/src/serve/public/components/ai-elements/suggestion.tsx +51 -0
  26. package/src/serve/public/components/preset-selector.tsx +403 -0
  27. package/src/serve/public/components/ui/badge.tsx +46 -0
  28. package/src/serve/public/components/ui/button-group.tsx +82 -0
  29. package/src/serve/public/components/ui/button.tsx +62 -0
  30. package/src/serve/public/components/ui/card.tsx +92 -0
  31. package/src/serve/public/components/ui/carousel.tsx +244 -0
  32. package/src/serve/public/components/ui/collapsible.tsx +31 -0
  33. package/src/serve/public/components/ui/command.tsx +181 -0
  34. package/src/serve/public/components/ui/dialog.tsx +141 -0
  35. package/src/serve/public/components/ui/dropdown-menu.tsx +255 -0
  36. package/src/serve/public/components/ui/hover-card.tsx +42 -0
  37. package/src/serve/public/components/ui/input-group.tsx +167 -0
  38. package/src/serve/public/components/ui/input.tsx +21 -0
  39. package/src/serve/public/components/ui/progress.tsx +28 -0
  40. package/src/serve/public/components/ui/scroll-area.tsx +56 -0
  41. package/src/serve/public/components/ui/select.tsx +188 -0
  42. package/src/serve/public/components/ui/separator.tsx +26 -0
  43. package/src/serve/public/components/ui/table.tsx +114 -0
  44. package/src/serve/public/components/ui/textarea.tsx +18 -0
  45. package/src/serve/public/components/ui/tooltip.tsx +59 -0
  46. package/src/serve/public/globals.css +226 -0
  47. package/src/serve/public/hooks/use-api.ts +112 -0
  48. package/src/serve/public/index.html +13 -0
  49. package/src/serve/public/pages/Ask.tsx +442 -0
  50. package/src/serve/public/pages/Browse.tsx +270 -0
  51. package/src/serve/public/pages/Dashboard.tsx +202 -0
  52. package/src/serve/public/pages/DocView.tsx +302 -0
  53. package/src/serve/public/pages/Search.tsx +335 -0
  54. package/src/serve/routes/api.ts +763 -0
  55. package/src/serve/server.ts +249 -0
  56. package/src/store/sqlite/adapter.ts +47 -0
  57. package/src/store/types.ts +10 -0
@@ -0,0 +1,270 @@
1
+ import { ArrowLeft, ChevronRight, FileText, FolderOpen } from 'lucide-react';
2
+ import { useEffect, useState } from 'react';
3
+ import { Loader } from '../components/ai-elements/loader';
4
+ import { Badge } from '../components/ui/badge';
5
+ import { Button } from '../components/ui/button';
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from '../components/ui/select';
13
+ import {
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableHead,
18
+ TableHeader,
19
+ TableRow,
20
+ } from '../components/ui/table';
21
+ import { apiFetch } from '../hooks/use-api';
22
+
23
+ interface PageProps {
24
+ navigate: (to: string | number) => void;
25
+ }
26
+
27
+ interface Collection {
28
+ name: string;
29
+ path: string;
30
+ }
31
+
32
+ interface Document {
33
+ docid: string;
34
+ uri: string;
35
+ title: string | null;
36
+ collection: string;
37
+ relPath: string;
38
+ sourceExt: string;
39
+ }
40
+
41
+ interface DocsResponse {
42
+ documents: Document[];
43
+ total: number;
44
+ limit: number;
45
+ offset: number;
46
+ }
47
+
48
+ export default function Browse({ navigate }: PageProps) {
49
+ const [collections, setCollections] = useState<Collection[]>([]);
50
+ const [selected, setSelected] = useState<string>('');
51
+ const [docs, setDocs] = useState<Document[]>([]);
52
+ const [total, setTotal] = useState(0);
53
+ const [offset, setOffset] = useState(0);
54
+ const [loading, setLoading] = useState(false);
55
+ const [initialLoad, setInitialLoad] = useState(true);
56
+ const limit = 25;
57
+
58
+ // Parse collection from URL on mount
59
+ useEffect(() => {
60
+ const params = new URLSearchParams(window.location.search);
61
+ const collection = params.get('collection');
62
+ if (collection) {
63
+ setSelected(collection);
64
+ }
65
+ }, []);
66
+
67
+ useEffect(() => {
68
+ apiFetch<Collection[]>('/api/collections').then(({ data }) => {
69
+ if (data) {
70
+ setCollections(data);
71
+ }
72
+ });
73
+ }, []);
74
+
75
+ useEffect(() => {
76
+ setLoading(true);
77
+ const url = selected
78
+ ? `/api/docs?collection=${encodeURIComponent(selected)}&limit=${limit}&offset=${offset}`
79
+ : `/api/docs?limit=${limit}&offset=${offset}`;
80
+
81
+ apiFetch<DocsResponse>(url).then(({ data }) => {
82
+ setLoading(false);
83
+ setInitialLoad(false);
84
+ if (data) {
85
+ setDocs((prev) =>
86
+ offset === 0 ? data.documents : [...prev, ...data.documents]
87
+ );
88
+ setTotal(data.total);
89
+ }
90
+ });
91
+ }, [selected, offset]);
92
+
93
+ const handleCollectionChange = (value: string) => {
94
+ const newSelected = value === 'all' ? '' : value;
95
+ setSelected(newSelected);
96
+ setOffset(0);
97
+ setDocs([]);
98
+ // Update URL for shareable deep-links
99
+ const url = newSelected
100
+ ? `/browse?collection=${encodeURIComponent(newSelected)}`
101
+ : '/browse';
102
+ window.history.pushState({}, '', url);
103
+ };
104
+
105
+ const handleLoadMore = () => {
106
+ setOffset((prev) => prev + limit);
107
+ };
108
+
109
+ const getExtBadgeVariant = (ext: string) => {
110
+ switch (ext.toLowerCase()) {
111
+ case '.md':
112
+ case '.markdown':
113
+ return 'default';
114
+ case '.pdf':
115
+ return 'destructive';
116
+ case '.docx':
117
+ case '.doc':
118
+ return 'secondary';
119
+ default:
120
+ return 'outline';
121
+ }
122
+ };
123
+
124
+ return (
125
+ <div className="min-h-screen">
126
+ {/* Header */}
127
+ <header className="glass sticky top-0 z-10 border-border/50 border-b">
128
+ <div className="flex items-center justify-between px-8 py-4">
129
+ <div className="flex items-center gap-4">
130
+ <Button
131
+ className="gap-2"
132
+ onClick={() => navigate(-1)}
133
+ size="sm"
134
+ variant="ghost"
135
+ >
136
+ <ArrowLeft className="size-4" />
137
+ Back
138
+ </Button>
139
+ <h1 className="font-semibold text-xl">Browse</h1>
140
+ </div>
141
+ <div className="flex items-center gap-4">
142
+ <Select
143
+ onValueChange={handleCollectionChange}
144
+ value={selected || 'all'}
145
+ >
146
+ <SelectTrigger className="w-[200px]">
147
+ <FolderOpen className="mr-2 size-4 text-muted-foreground" />
148
+ <SelectValue placeholder="All Collections" />
149
+ </SelectTrigger>
150
+ <SelectContent>
151
+ <SelectItem value="all">All Collections</SelectItem>
152
+ {collections.map((c) => (
153
+ <SelectItem key={c.name} value={c.name}>
154
+ {c.name}
155
+ </SelectItem>
156
+ ))}
157
+ </SelectContent>
158
+ </Select>
159
+ <Badge className="font-mono" variant="outline">
160
+ {total.toLocaleString()} docs
161
+ </Badge>
162
+ </div>
163
+ </div>
164
+ </header>
165
+
166
+ <main className="mx-auto max-w-6xl p-8">
167
+ {/* Initial loading */}
168
+ {initialLoad && loading && (
169
+ <div className="flex flex-col items-center justify-center gap-4 py-20">
170
+ <Loader className="text-primary" size={32} />
171
+ <p className="text-muted-foreground">Loading documents...</p>
172
+ </div>
173
+ )}
174
+
175
+ {/* Empty state */}
176
+ {!loading && docs.length === 0 && (
177
+ <div className="py-20 text-center">
178
+ <FileText className="mx-auto mb-4 size-12 text-muted-foreground" />
179
+ <h3 className="mb-2 font-medium text-lg">No documents found</h3>
180
+ <p className="text-muted-foreground">
181
+ {selected
182
+ ? 'This collection is empty'
183
+ : 'Index some documents to get started'}
184
+ </p>
185
+ </div>
186
+ )}
187
+
188
+ {/* Document Table */}
189
+ {docs.length > 0 && (
190
+ <div className="animate-fade-in opacity-0">
191
+ <Table>
192
+ <TableHeader>
193
+ <TableRow>
194
+ <TableHead className="w-[50%]">Document</TableHead>
195
+ <TableHead>Collection</TableHead>
196
+ <TableHead className="text-right">Type</TableHead>
197
+ </TableRow>
198
+ </TableHeader>
199
+ <TableBody>
200
+ {docs.map((doc, _i) => (
201
+ <TableRow
202
+ className="group cursor-pointer"
203
+ key={doc.docid}
204
+ onClick={() =>
205
+ navigate(`/doc?uri=${encodeURIComponent(doc.uri)}`)
206
+ }
207
+ >
208
+ <TableCell>
209
+ <div className="flex items-center gap-2">
210
+ <FileText className="size-4 shrink-0 text-muted-foreground" />
211
+ <div className="min-w-0">
212
+ <div className="truncate font-medium transition-colors group-hover:text-primary">
213
+ {doc.title || doc.relPath}
214
+ </div>
215
+ <div className="truncate font-mono text-muted-foreground text-xs">
216
+ {doc.relPath}
217
+ </div>
218
+ </div>
219
+ <ChevronRight className="ml-auto size-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
220
+ </div>
221
+ </TableCell>
222
+ <TableCell>
223
+ <Badge className="font-mono text-xs" variant="outline">
224
+ {doc.collection}
225
+ </Badge>
226
+ </TableCell>
227
+ <TableCell className="text-right">
228
+ <Badge
229
+ className="font-mono text-xs"
230
+ variant={getExtBadgeVariant(doc.sourceExt)}
231
+ >
232
+ {doc.sourceExt}
233
+ </Badge>
234
+ </TableCell>
235
+ </TableRow>
236
+ ))}
237
+ </TableBody>
238
+ </Table>
239
+
240
+ {/* Load More */}
241
+ {offset + limit < total && (
242
+ <div className="mt-8 text-center">
243
+ <Button
244
+ className="gap-2"
245
+ disabled={loading}
246
+ onClick={handleLoadMore}
247
+ variant="outline"
248
+ >
249
+ {loading ? (
250
+ <>
251
+ <Loader size={16} />
252
+ Loading...
253
+ </>
254
+ ) : (
255
+ <>
256
+ Load More
257
+ <Badge className="ml-1" variant="secondary">
258
+ {Math.min(limit, total - docs.length)} remaining
259
+ </Badge>
260
+ </>
261
+ )}
262
+ </Button>
263
+ </div>
264
+ )}
265
+ </div>
266
+ )}
267
+ </main>
268
+ </div>
269
+ );
270
+ }
@@ -0,0 +1,202 @@
1
+ import {
2
+ BookOpen,
3
+ Database,
4
+ Layers,
5
+ MessageSquare,
6
+ Search,
7
+ Sparkles,
8
+ } from 'lucide-react';
9
+ import { useEffect, useState } from 'react';
10
+ import { PresetSelector } from '../components/preset-selector';
11
+ import { Badge } from '../components/ui/badge';
12
+ import { Button } from '../components/ui/button';
13
+ import {
14
+ Card,
15
+ CardContent,
16
+ CardDescription,
17
+ CardHeader,
18
+ } from '../components/ui/card';
19
+ import { apiFetch } from '../hooks/use-api';
20
+
21
+ interface PageProps {
22
+ navigate: (to: string | number) => void;
23
+ }
24
+
25
+ interface StatusData {
26
+ indexName: string;
27
+ totalDocuments: number;
28
+ totalChunks: number;
29
+ embeddingBacklog: number;
30
+ healthy: boolean;
31
+ collections: Array<{
32
+ name: string;
33
+ path: string;
34
+ documentCount: number;
35
+ chunkCount: number;
36
+ embeddedCount: number;
37
+ }>;
38
+ }
39
+
40
+ export default function Dashboard({ navigate }: PageProps) {
41
+ const [status, setStatus] = useState<StatusData | null>(null);
42
+ const [error, setError] = useState<string | null>(null);
43
+
44
+ useEffect(() => {
45
+ apiFetch<StatusData>('/api/status').then(({ data, error }) => {
46
+ if (error) {
47
+ setError(error);
48
+ } else {
49
+ setStatus(data);
50
+ }
51
+ });
52
+ }, []);
53
+
54
+ return (
55
+ <div className="min-h-screen">
56
+ {/* Header with aurora glow */}
57
+ <header className="relative border-border/50 border-b bg-card/50 backdrop-blur-sm">
58
+ <div className="aurora-glow absolute inset-0 opacity-30" />
59
+ <div className="relative px-8 py-12">
60
+ <div className="mb-2 flex items-center justify-between">
61
+ <div className="flex items-center gap-3">
62
+ <Sparkles className="size-8 text-primary" />
63
+ <h1 className="font-bold text-4xl text-primary tracking-tight">
64
+ GNO
65
+ </h1>
66
+ </div>
67
+ <PresetSelector />
68
+ </div>
69
+ <p className="text-lg text-muted-foreground">
70
+ Your Local Knowledge Index
71
+ </p>
72
+ </div>
73
+ </header>
74
+
75
+ <main className="mx-auto max-w-6xl p-8">
76
+ {/* Navigation */}
77
+ <nav className="mb-10 flex gap-4">
78
+ <Button
79
+ className="gap-2"
80
+ onClick={() => navigate('/search')}
81
+ size="lg"
82
+ >
83
+ <Search className="size-4" />
84
+ Search
85
+ </Button>
86
+ <Button
87
+ className="gap-2"
88
+ onClick={() => navigate('/ask')}
89
+ size="lg"
90
+ variant="secondary"
91
+ >
92
+ <MessageSquare className="size-4" />
93
+ Ask
94
+ </Button>
95
+ <Button
96
+ className="gap-2"
97
+ onClick={() => navigate('/browse')}
98
+ size="lg"
99
+ variant="outline"
100
+ >
101
+ <BookOpen className="size-4" />
102
+ Browse
103
+ </Button>
104
+ </nav>
105
+
106
+ {/* Error state */}
107
+ {error && (
108
+ <Card className="mb-6 border-destructive bg-destructive/10">
109
+ <CardContent className="py-4 text-destructive">{error}</CardContent>
110
+ </Card>
111
+ )}
112
+
113
+ {/* Stats Grid */}
114
+ {status && (
115
+ <div className="mb-10 grid animate-fade-in gap-6 opacity-0 md:grid-cols-3">
116
+ <Card className="group transition-colors hover:border-primary/50">
117
+ <CardHeader className="pb-2">
118
+ <CardDescription className="flex items-center gap-2">
119
+ <Database className="size-4" />
120
+ Documents
121
+ </CardDescription>
122
+ </CardHeader>
123
+ <CardContent>
124
+ <div className="font-bold text-4xl tracking-tight">
125
+ {status.totalDocuments.toLocaleString()}
126
+ </div>
127
+ </CardContent>
128
+ </Card>
129
+
130
+ <Card className="group stagger-1 animate-fade-in opacity-0 transition-colors hover:border-primary/50">
131
+ <CardHeader className="pb-2">
132
+ <CardDescription className="flex items-center gap-2">
133
+ <Layers className="size-4" />
134
+ Chunks
135
+ </CardDescription>
136
+ </CardHeader>
137
+ <CardContent>
138
+ <div className="font-bold text-4xl tracking-tight">
139
+ {status.totalChunks.toLocaleString()}
140
+ </div>
141
+ </CardContent>
142
+ </Card>
143
+
144
+ <Card className="group stagger-2 animate-fade-in opacity-0 transition-colors hover:border-primary/50">
145
+ <CardHeader className="pb-2">
146
+ <CardDescription>Status</CardDescription>
147
+ </CardHeader>
148
+ <CardContent>
149
+ <Badge
150
+ className="px-3 py-1 text-lg"
151
+ variant={status.healthy ? 'default' : 'secondary'}
152
+ >
153
+ {status.healthy ? '● Healthy' : '○ Degraded'}
154
+ </Badge>
155
+ </CardContent>
156
+ </Card>
157
+ </div>
158
+ )}
159
+
160
+ {/* Collections */}
161
+ {status && status.collections.length > 0 && (
162
+ <section className="stagger-3 animate-fade-in opacity-0">
163
+ <h2 className="mb-6 border-border/50 border-b pb-3 font-semibold text-2xl">
164
+ Collections
165
+ </h2>
166
+ <div className="space-y-3">
167
+ {status.collections.map((c, i) => (
168
+ <Card
169
+ className="group animate-fade-in cursor-pointer opacity-0 transition-all hover:border-primary/50 hover:bg-card/80"
170
+ key={c.name}
171
+ onClick={() =>
172
+ navigate(`/browse?collection=${encodeURIComponent(c.name)}`)
173
+ }
174
+ style={{ animationDelay: `${0.4 + i * 0.1}s` }}
175
+ >
176
+ <CardContent className="flex items-center justify-between py-4">
177
+ <div>
178
+ <div className="font-medium text-lg transition-colors group-hover:text-primary">
179
+ {c.name}
180
+ </div>
181
+ <div className="font-mono text-muted-foreground text-sm">
182
+ {c.path}
183
+ </div>
184
+ </div>
185
+ <div className="text-right">
186
+ <div className="font-medium">
187
+ {c.documentCount.toLocaleString()} docs
188
+ </div>
189
+ <div className="text-muted-foreground text-sm">
190
+ {c.chunkCount.toLocaleString()} chunks
191
+ </div>
192
+ </div>
193
+ </CardContent>
194
+ </Card>
195
+ ))}
196
+ </div>
197
+ </section>
198
+ )}
199
+ </main>
200
+ </div>
201
+ );
202
+ }