@gmickel/gno 0.28.2 → 0.29.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/README.md +10 -2
- package/package.json +1 -1
- package/src/app/constants.ts +4 -2
- package/src/cli/commands/mcp/install.ts +4 -4
- package/src/cli/commands/mcp/status.ts +7 -7
- package/src/cli/commands/skill/install.ts +5 -5
- package/src/cli/program.ts +2 -2
- package/src/collection/add.ts +10 -0
- package/src/collection/types.ts +1 -0
- package/src/config/types.ts +12 -2
- package/src/core/depth-policy.ts +1 -1
- package/src/core/file-ops.ts +203 -1
- package/src/llm/registry.ts +20 -4
- package/src/serve/AGENTS.md +16 -16
- package/src/serve/CLAUDE.md +16 -16
- package/src/serve/config-sync.ts +32 -1
- package/src/serve/connectors.ts +243 -0
- package/src/serve/context.ts +9 -0
- package/src/serve/doc-events.ts +31 -1
- package/src/serve/embed-scheduler.ts +12 -0
- package/src/serve/import-preview.ts +173 -0
- package/src/serve/public/app.tsx +101 -7
- package/src/serve/public/components/AIModelSelector.tsx +383 -145
- package/src/serve/public/components/AddCollectionDialog.tsx +123 -7
- package/src/serve/public/components/BootstrapStatus.tsx +133 -0
- package/src/serve/public/components/CaptureModal.tsx +5 -2
- package/src/serve/public/components/CollectionsEmptyState.tsx +63 -0
- package/src/serve/public/components/FirstRunWizard.tsx +622 -0
- package/src/serve/public/components/HealthCenter.tsx +128 -0
- package/src/serve/public/components/IndexingProgress.tsx +21 -2
- package/src/serve/public/components/QuickSwitcher.tsx +62 -36
- package/src/serve/public/components/TagInput.tsx +5 -1
- package/src/serve/public/components/WikiLinkAutocomplete.tsx +15 -6
- package/src/serve/public/components/WorkspaceTabs.tsx +60 -0
- package/src/serve/public/hooks/use-doc-events.ts +48 -4
- package/src/serve/public/lib/local-history.ts +40 -7
- package/src/serve/public/lib/navigation-state.ts +156 -0
- package/src/serve/public/lib/workspace-tabs.ts +235 -0
- package/src/serve/public/pages/Ask.tsx +11 -1
- package/src/serve/public/pages/Browse.tsx +73 -0
- package/src/serve/public/pages/Collections.tsx +29 -13
- package/src/serve/public/pages/Connectors.tsx +178 -0
- package/src/serve/public/pages/Dashboard.tsx +493 -67
- package/src/serve/public/pages/DocView.tsx +192 -34
- package/src/serve/public/pages/DocumentEditor.tsx +127 -5
- package/src/serve/public/pages/Search.tsx +12 -1
- package/src/serve/routes/api.ts +541 -62
- package/src/serve/server.ts +79 -2
- package/src/serve/status-model.ts +149 -0
- package/src/serve/status.ts +706 -0
- package/src/serve/watch-service.ts +73 -8
- package/src/types/electrobun-shell.d.ts +43 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BookOpen,
|
|
3
3
|
CheckCircle2Icon,
|
|
4
|
+
CpuIcon,
|
|
4
5
|
Database,
|
|
6
|
+
DownloadIcon,
|
|
7
|
+
FolderHeartIcon,
|
|
5
8
|
FolderIcon,
|
|
6
9
|
GitForkIcon,
|
|
7
10
|
Layers,
|
|
@@ -10,11 +13,19 @@ import {
|
|
|
10
13
|
PenIcon,
|
|
11
14
|
RefreshCwIcon,
|
|
12
15
|
Search,
|
|
16
|
+
StarIcon,
|
|
13
17
|
} from "lucide-react";
|
|
14
18
|
import { useCallback, useEffect, useState } from "react";
|
|
15
19
|
|
|
20
|
+
import type { AppStatusResponse, HealthActionKind } from "../../status-model";
|
|
21
|
+
|
|
22
|
+
import { AddCollectionDialog } from "../components/AddCollectionDialog";
|
|
23
|
+
import { AIModelSelector } from "../components/AIModelSelector";
|
|
24
|
+
import { BootstrapStatus } from "../components/BootstrapStatus";
|
|
16
25
|
import { CaptureButton } from "../components/CaptureButton";
|
|
26
|
+
import { FirstRunWizard } from "../components/FirstRunWizard";
|
|
17
27
|
import { GnoLogo } from "../components/GnoLogo";
|
|
28
|
+
import { HealthCenter } from "../components/HealthCenter";
|
|
18
29
|
import { IndexingProgress } from "../components/IndexingProgress";
|
|
19
30
|
import { Button } from "../components/ui/button";
|
|
20
31
|
import {
|
|
@@ -23,55 +34,157 @@ import {
|
|
|
23
34
|
CardDescription,
|
|
24
35
|
CardHeader,
|
|
25
36
|
} from "../components/ui/card";
|
|
37
|
+
import { Progress } from "../components/ui/progress";
|
|
26
38
|
import { apiFetch } from "../hooks/use-api";
|
|
27
39
|
import { useCaptureModal } from "../hooks/useCaptureModal";
|
|
40
|
+
import {
|
|
41
|
+
loadFavoriteCollections,
|
|
42
|
+
loadFavoriteDocuments,
|
|
43
|
+
loadRecentDocuments,
|
|
44
|
+
toggleFavoriteCollection,
|
|
45
|
+
type FavoriteCollection,
|
|
46
|
+
type FavoriteDoc,
|
|
47
|
+
type RecentDoc,
|
|
48
|
+
} from "../lib/navigation-state";
|
|
28
49
|
|
|
29
50
|
interface SyncResponse {
|
|
30
51
|
jobId: string;
|
|
31
52
|
}
|
|
32
53
|
|
|
33
|
-
interface
|
|
34
|
-
|
|
54
|
+
interface EmbedResponse {
|
|
55
|
+
embedded?: number;
|
|
56
|
+
errors?: number;
|
|
57
|
+
running?: boolean;
|
|
58
|
+
pendingCount?: number;
|
|
59
|
+
note?: string;
|
|
35
60
|
}
|
|
36
61
|
|
|
37
|
-
interface
|
|
38
|
-
indexName: string;
|
|
39
|
-
totalDocuments: number;
|
|
40
|
-
totalChunks: number;
|
|
41
|
-
embeddingBacklog: number;
|
|
42
|
-
healthy: boolean;
|
|
62
|
+
interface SyncResultSummary {
|
|
43
63
|
collections: Array<{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
collection: string;
|
|
65
|
+
filesProcessed: number;
|
|
66
|
+
filesAdded: number;
|
|
67
|
+
filesUpdated: number;
|
|
68
|
+
filesUnchanged: number;
|
|
69
|
+
filesErrored: number;
|
|
70
|
+
durationMs: number;
|
|
49
71
|
}>;
|
|
72
|
+
totalDurationMs: number;
|
|
73
|
+
totalFilesProcessed: number;
|
|
74
|
+
totalFilesAdded: number;
|
|
75
|
+
totalFilesUpdated: number;
|
|
76
|
+
totalFilesErrored: number;
|
|
77
|
+
totalFilesSkipped: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface DownloadProgressState {
|
|
81
|
+
downloadedBytes: number;
|
|
82
|
+
totalBytes: number;
|
|
83
|
+
percent: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface ModelDownloadStatus {
|
|
87
|
+
active: boolean;
|
|
88
|
+
currentType: string | null;
|
|
89
|
+
progress: DownloadProgressState | null;
|
|
90
|
+
completed: string[];
|
|
91
|
+
failed: Array<{ type: string; error: string }>;
|
|
92
|
+
startedAt: number | null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface PageProps {
|
|
96
|
+
navigate: (to: string | number) => void;
|
|
50
97
|
}
|
|
51
98
|
|
|
52
99
|
export default function Dashboard({ navigate }: PageProps) {
|
|
53
|
-
const [status, setStatus] = useState<
|
|
100
|
+
const [status, setStatus] = useState<AppStatusResponse | null>(null);
|
|
54
101
|
const [error, setError] = useState<string | null>(null);
|
|
55
102
|
const [syncing, setSyncing] = useState(false);
|
|
56
103
|
const [syncJobId, setSyncJobId] = useState<string | null>(null);
|
|
104
|
+
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
|
105
|
+
const [initialCollectionPath, setInitialCollectionPath] = useState<
|
|
106
|
+
string | undefined
|
|
107
|
+
>(undefined);
|
|
108
|
+
const [busyAction, setBusyAction] = useState<HealthActionKind | null>(null);
|
|
109
|
+
const [recentDocs, setRecentDocs] = useState<RecentDoc[]>([]);
|
|
110
|
+
const [favoriteDocs, setFavoriteDocs] = useState<FavoriteDoc[]>([]);
|
|
111
|
+
const [favoriteCollections, setFavoriteCollections] = useState<
|
|
112
|
+
FavoriteCollection[]
|
|
113
|
+
>([]);
|
|
114
|
+
const [modelDownloadStatus, setModelDownloadStatus] =
|
|
115
|
+
useState<ModelDownloadStatus | null>(null);
|
|
57
116
|
const { openCapture } = useCaptureModal();
|
|
117
|
+
|
|
58
118
|
const openCollections = () => navigate("/collections");
|
|
59
119
|
|
|
60
120
|
const loadStatus = useCallback(async () => {
|
|
61
|
-
const { data, error: err } =
|
|
121
|
+
const { data, error: err } =
|
|
122
|
+
await apiFetch<AppStatusResponse>("/api/status");
|
|
62
123
|
if (err) {
|
|
63
124
|
setError(err);
|
|
64
|
-
|
|
65
|
-
setStatus(data);
|
|
66
|
-
setError(null);
|
|
125
|
+
return;
|
|
67
126
|
}
|
|
127
|
+
|
|
128
|
+
setStatus(data);
|
|
129
|
+
setError(null);
|
|
68
130
|
}, []);
|
|
69
131
|
|
|
70
132
|
useEffect(() => {
|
|
71
133
|
void loadStatus();
|
|
72
134
|
}, [loadStatus]);
|
|
73
135
|
|
|
74
|
-
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
setRecentDocs(loadRecentDocuments());
|
|
138
|
+
setFavoriteDocs(loadFavoriteDocuments());
|
|
139
|
+
setFavoriteCollections(loadFavoriteCollections());
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
let cancelled = false;
|
|
144
|
+
let intervalId: ReturnType<typeof setInterval> | null = null;
|
|
145
|
+
|
|
146
|
+
async function pollModelStatus(): Promise<void> {
|
|
147
|
+
const { data } =
|
|
148
|
+
await apiFetch<ModelDownloadStatus>("/api/models/status");
|
|
149
|
+
if (cancelled || !data) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setModelDownloadStatus(data);
|
|
154
|
+
|
|
155
|
+
if (!data.active && intervalId) {
|
|
156
|
+
clearInterval(intervalId);
|
|
157
|
+
intervalId = null;
|
|
158
|
+
void loadStatus();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
void pollModelStatus();
|
|
163
|
+
|
|
164
|
+
if (
|
|
165
|
+
busyAction === "download-models" ||
|
|
166
|
+
status?.bootstrap.models.downloading ||
|
|
167
|
+
modelDownloadStatus?.active
|
|
168
|
+
) {
|
|
169
|
+
intervalId = setInterval(() => {
|
|
170
|
+
void pollModelStatus();
|
|
171
|
+
}, 1000);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return () => {
|
|
175
|
+
cancelled = true;
|
|
176
|
+
if (intervalId) {
|
|
177
|
+
clearInterval(intervalId);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}, [
|
|
181
|
+
busyAction,
|
|
182
|
+
loadStatus,
|
|
183
|
+
modelDownloadStatus?.active,
|
|
184
|
+
status?.bootstrap.models.downloading,
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const handleSync = useCallback(async () => {
|
|
75
188
|
setSyncing(true);
|
|
76
189
|
setSyncJobId(null);
|
|
77
190
|
|
|
@@ -88,47 +201,213 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
88
201
|
if (data?.jobId) {
|
|
89
202
|
setSyncJobId(data.jobId);
|
|
90
203
|
}
|
|
91
|
-
};
|
|
204
|
+
}, []);
|
|
92
205
|
|
|
93
|
-
const handleSyncComplete = () => {
|
|
206
|
+
const handleSyncComplete = (result?: SyncResultSummary) => {
|
|
94
207
|
setSyncing(false);
|
|
95
|
-
setSyncJobId(null);
|
|
96
208
|
void loadStatus();
|
|
209
|
+
|
|
210
|
+
const isNoop =
|
|
211
|
+
!!result &&
|
|
212
|
+
result.totalFilesProcessed === 0 &&
|
|
213
|
+
result.totalFilesAdded === 0 &&
|
|
214
|
+
result.totalFilesUpdated === 0 &&
|
|
215
|
+
result.totalFilesErrored === 0;
|
|
216
|
+
|
|
217
|
+
window.setTimeout(
|
|
218
|
+
() => {
|
|
219
|
+
setSyncJobId(null);
|
|
220
|
+
},
|
|
221
|
+
isNoop ? 2500 : 1200
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const handleOpenAddCollection = (path?: string) => {
|
|
226
|
+
setInitialCollectionPath(path);
|
|
227
|
+
setAddDialogOpen(true);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const handleDownloadModels = useCallback(async () => {
|
|
231
|
+
setBusyAction("download-models");
|
|
232
|
+
const { error: err } = await apiFetch("/api/models/pull", {
|
|
233
|
+
method: "POST",
|
|
234
|
+
});
|
|
235
|
+
setBusyAction(null);
|
|
236
|
+
|
|
237
|
+
if (err) {
|
|
238
|
+
setError(err);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { data } = await apiFetch<ModelDownloadStatus>("/api/models/status");
|
|
243
|
+
if (data) {
|
|
244
|
+
setModelDownloadStatus(data);
|
|
245
|
+
}
|
|
246
|
+
void loadStatus();
|
|
247
|
+
}, [loadStatus]);
|
|
248
|
+
|
|
249
|
+
const handleEmbedNow = useCallback(async () => {
|
|
250
|
+
setBusyAction("embed");
|
|
251
|
+
const { data, error: err } = await apiFetch<EmbedResponse>("/api/embed", {
|
|
252
|
+
method: "POST",
|
|
253
|
+
});
|
|
254
|
+
setBusyAction(null);
|
|
255
|
+
|
|
256
|
+
if (err) {
|
|
257
|
+
setError(err);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (data?.errors && data.errors > 0) {
|
|
262
|
+
setError(`Embedding failed for ${data.errors} chunks.`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
void loadStatus();
|
|
266
|
+
}, [loadStatus]);
|
|
267
|
+
|
|
268
|
+
const isModelDownloadBlocking =
|
|
269
|
+
busyAction === "download-models" || modelDownloadStatus?.active;
|
|
270
|
+
|
|
271
|
+
const handleHealthAction = (action: HealthActionKind) => {
|
|
272
|
+
if (action === "add-collection") {
|
|
273
|
+
handleOpenAddCollection();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (action === "open-collections") {
|
|
278
|
+
openCollections();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (action === "sync") {
|
|
283
|
+
void handleSync();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (action === "embed") {
|
|
288
|
+
void handleEmbedNow();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (action === "download-models") {
|
|
293
|
+
void handleDownloadModels();
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const handleToggleFavoriteCollection = (
|
|
298
|
+
collection: AppStatusResponse["collections"][number]
|
|
299
|
+
) => {
|
|
300
|
+
setFavoriteCollections(
|
|
301
|
+
toggleFavoriteCollection({
|
|
302
|
+
name: collection.name,
|
|
303
|
+
href: `/browse?collection=${encodeURIComponent(collection.name)}`,
|
|
304
|
+
label: collection.name,
|
|
305
|
+
})
|
|
306
|
+
);
|
|
97
307
|
};
|
|
98
308
|
|
|
99
309
|
return (
|
|
100
310
|
<div className="min-h-screen">
|
|
101
|
-
{
|
|
311
|
+
{isModelDownloadBlocking && (
|
|
312
|
+
<div className="fixed inset-0 z-[90] bg-background/85 backdrop-blur-sm">
|
|
313
|
+
<div className="flex min-h-screen items-center justify-center px-6">
|
|
314
|
+
<Card className="w-full max-w-2xl border-primary/30 bg-card/95 shadow-2xl">
|
|
315
|
+
<CardHeader className="space-y-3">
|
|
316
|
+
<div className="flex items-center gap-3">
|
|
317
|
+
<div className="flex size-11 items-center justify-center rounded-full bg-primary/12 text-primary">
|
|
318
|
+
<DownloadIcon className="size-5" />
|
|
319
|
+
</div>
|
|
320
|
+
<div>
|
|
321
|
+
<div className="font-semibold text-xl">
|
|
322
|
+
Downloading local models
|
|
323
|
+
</div>
|
|
324
|
+
<CardDescription className="mt-1 text-sm">
|
|
325
|
+
GNO is preparing the active preset. Keep this page open
|
|
326
|
+
until the download finishes.
|
|
327
|
+
</CardDescription>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
</CardHeader>
|
|
331
|
+
<CardContent className="space-y-5">
|
|
332
|
+
<div className="rounded-xl border border-border/60 bg-background/70 p-4">
|
|
333
|
+
<div className="mb-2 flex items-center justify-between gap-3">
|
|
334
|
+
<span className="font-medium text-sm">
|
|
335
|
+
{modelDownloadStatus?.currentType
|
|
336
|
+
? `Current step: ${modelDownloadStatus.currentType}`
|
|
337
|
+
: "Preparing download"}
|
|
338
|
+
</span>
|
|
339
|
+
<span className="font-mono text-muted-foreground text-xs">
|
|
340
|
+
{modelDownloadStatus?.progress
|
|
341
|
+
? `${modelDownloadStatus.progress.percent.toFixed(0)}%`
|
|
342
|
+
: "Starting..."}
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
<Progress
|
|
346
|
+
value={modelDownloadStatus?.progress?.percent ?? 5}
|
|
347
|
+
/>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
351
|
+
<div className="rounded-xl border border-border/60 bg-background/70 p-4">
|
|
352
|
+
<div className="mb-2 font-medium text-sm">Completed</div>
|
|
353
|
+
<p className="text-muted-foreground text-sm">
|
|
354
|
+
{modelDownloadStatus?.completed.length
|
|
355
|
+
? modelDownloadStatus.completed.join(", ")
|
|
356
|
+
: "Nothing completed yet."}
|
|
357
|
+
</p>
|
|
358
|
+
</div>
|
|
359
|
+
<div className="rounded-xl border border-border/60 bg-background/70 p-4">
|
|
360
|
+
<div className="mb-2 font-medium text-sm">
|
|
361
|
+
Why this blocks
|
|
362
|
+
</div>
|
|
363
|
+
<p className="text-muted-foreground text-sm">
|
|
364
|
+
Search can partially work while files stream in, but
|
|
365
|
+
results and first-run status are clearer once the preset
|
|
366
|
+
download finishes.
|
|
367
|
+
</p>
|
|
368
|
+
</div>
|
|
369
|
+
</div>
|
|
370
|
+
</CardContent>
|
|
371
|
+
</Card>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
|
|
102
376
|
<header className="relative border-border/50 border-b bg-card/50 backdrop-blur-sm">
|
|
103
377
|
<div className="aurora-glow absolute inset-0 opacity-30" />
|
|
104
378
|
<div className="relative px-8 py-12">
|
|
105
|
-
<div className="
|
|
106
|
-
<div className="
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
379
|
+
<div className="grid gap-6 md:grid-cols-[minmax(0,1fr)_auto] md:items-start">
|
|
380
|
+
<div className="min-w-0">
|
|
381
|
+
<div className="flex items-center gap-3">
|
|
382
|
+
<GnoLogo className="size-8 shrink-0 text-primary" />
|
|
383
|
+
<h1 className="font-bold text-4xl text-primary tracking-tight">
|
|
384
|
+
GNO
|
|
385
|
+
</h1>
|
|
386
|
+
</div>
|
|
387
|
+
<p className="mt-4 text-lg text-muted-foreground">
|
|
388
|
+
Your Local Knowledge Index
|
|
389
|
+
</p>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<div className="flex flex-wrap items-center gap-3 md:justify-end">
|
|
393
|
+
<AIModelSelector showLabel={false} />
|
|
394
|
+
<Button
|
|
395
|
+
disabled={syncing}
|
|
396
|
+
onClick={() => void handleSync()}
|
|
397
|
+
size="sm"
|
|
398
|
+
variant="outline"
|
|
399
|
+
>
|
|
400
|
+
{syncing ? (
|
|
401
|
+
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
402
|
+
) : (
|
|
403
|
+
<RefreshCwIcon className="mr-1.5 size-4" />
|
|
404
|
+
)}
|
|
405
|
+
{syncing ? "Syncing..." : "Update All"}
|
|
406
|
+
</Button>
|
|
111
407
|
</div>
|
|
112
|
-
<Button
|
|
113
|
-
disabled={syncing}
|
|
114
|
-
onClick={handleSync}
|
|
115
|
-
size="sm"
|
|
116
|
-
variant="outline"
|
|
117
|
-
>
|
|
118
|
-
{syncing ? (
|
|
119
|
-
<Loader2Icon className="mr-1.5 size-4 animate-spin" />
|
|
120
|
-
) : (
|
|
121
|
-
<RefreshCwIcon className="mr-1.5 size-4" />
|
|
122
|
-
)}
|
|
123
|
-
{syncing ? "Syncing..." : "Update All"}
|
|
124
|
-
</Button>
|
|
125
408
|
</div>
|
|
126
|
-
<p className="text-lg text-muted-foreground">
|
|
127
|
-
Your Local Knowledge Index
|
|
128
|
-
</p>
|
|
129
409
|
|
|
130
|
-
{
|
|
131
|
-
{syncJobId && (
|
|
410
|
+
{syncJobId && (!status || status.onboarding.ready) && (
|
|
132
411
|
<div className="mt-4 rounded-lg border border-border/50 bg-background/50 p-4">
|
|
133
412
|
<IndexingProgress
|
|
134
413
|
jobId={syncJobId}
|
|
@@ -144,7 +423,22 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
144
423
|
</header>
|
|
145
424
|
|
|
146
425
|
<main className="mx-auto max-w-6xl p-8">
|
|
147
|
-
{
|
|
426
|
+
{status && !status.onboarding.ready && (
|
|
427
|
+
<section className="mb-10">
|
|
428
|
+
<FirstRunWizard
|
|
429
|
+
onboarding={status.onboarding}
|
|
430
|
+
onAddCollection={handleOpenAddCollection}
|
|
431
|
+
onDownloadModels={() => void handleDownloadModels()}
|
|
432
|
+
onEmbed={() => void handleEmbedNow()}
|
|
433
|
+
onSync={() => void handleSync()}
|
|
434
|
+
onSyncComplete={handleSyncComplete}
|
|
435
|
+
embedding={busyAction === "embed"}
|
|
436
|
+
syncJobId={syncJobId}
|
|
437
|
+
syncing={syncing}
|
|
438
|
+
/>
|
|
439
|
+
</section>
|
|
440
|
+
)}
|
|
441
|
+
|
|
148
442
|
<nav className="mb-10 flex flex-wrap gap-4">
|
|
149
443
|
<Button
|
|
150
444
|
className="gap-2"
|
|
@@ -181,6 +475,15 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
181
475
|
<FolderIcon className="size-4" />
|
|
182
476
|
Collections
|
|
183
477
|
</Button>
|
|
478
|
+
<Button
|
|
479
|
+
className="gap-2"
|
|
480
|
+
onClick={() => navigate("/connectors")}
|
|
481
|
+
size="lg"
|
|
482
|
+
variant="outline"
|
|
483
|
+
>
|
|
484
|
+
<CpuIcon className="size-4" />
|
|
485
|
+
Connectors
|
|
486
|
+
</Button>
|
|
184
487
|
<Button
|
|
185
488
|
className="gap-2"
|
|
186
489
|
onClick={() => navigate("/graph")}
|
|
@@ -192,17 +495,118 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
192
495
|
</Button>
|
|
193
496
|
</nav>
|
|
194
497
|
|
|
195
|
-
{/* Error state */}
|
|
196
498
|
{error && (
|
|
197
499
|
<Card className="mb-6 border-destructive bg-destructive/10">
|
|
198
500
|
<CardContent className="py-4 text-destructive">{error}</CardContent>
|
|
199
501
|
</Card>
|
|
200
502
|
)}
|
|
201
503
|
|
|
202
|
-
{
|
|
504
|
+
{status && (
|
|
505
|
+
<div className="mb-10">
|
|
506
|
+
<HealthCenter
|
|
507
|
+
busyAction={busyAction}
|
|
508
|
+
health={status.health}
|
|
509
|
+
onAction={handleHealthAction}
|
|
510
|
+
/>
|
|
511
|
+
</div>
|
|
512
|
+
)}
|
|
513
|
+
|
|
514
|
+
{status && (
|
|
515
|
+
<div className="mb-10">
|
|
516
|
+
<BootstrapStatus
|
|
517
|
+
bootstrap={status.bootstrap}
|
|
518
|
+
onDownloadModels={() => void handleDownloadModels()}
|
|
519
|
+
/>
|
|
520
|
+
</div>
|
|
521
|
+
)}
|
|
522
|
+
|
|
523
|
+
{(recentDocs.length > 0 ||
|
|
524
|
+
favoriteDocs.length > 0 ||
|
|
525
|
+
favoriteCollections.length > 0) && (
|
|
526
|
+
<section className="mb-10 grid gap-4 xl:grid-cols-3">
|
|
527
|
+
<Card className="border-border/60 bg-card/70">
|
|
528
|
+
<CardHeader className="pb-3">
|
|
529
|
+
<div className="flex items-center gap-2">
|
|
530
|
+
<StarIcon className="size-4 text-primary" />
|
|
531
|
+
<CardDescription>Favorite Documents</CardDescription>
|
|
532
|
+
</div>
|
|
533
|
+
</CardHeader>
|
|
534
|
+
<CardContent className="space-y-2">
|
|
535
|
+
{favoriteDocs.length === 0 ? (
|
|
536
|
+
<p className="text-muted-foreground text-sm">
|
|
537
|
+
Favorite a document from Browse to keep it here.
|
|
538
|
+
</p>
|
|
539
|
+
) : (
|
|
540
|
+
favoriteDocs.slice(0, 5).map((doc) => (
|
|
541
|
+
<Button
|
|
542
|
+
className="w-full justify-start"
|
|
543
|
+
key={doc.href}
|
|
544
|
+
onClick={() => navigate(doc.href)}
|
|
545
|
+
variant="ghost"
|
|
546
|
+
>
|
|
547
|
+
{doc.label}
|
|
548
|
+
</Button>
|
|
549
|
+
))
|
|
550
|
+
)}
|
|
551
|
+
</CardContent>
|
|
552
|
+
</Card>
|
|
553
|
+
<Card className="border-border/60 bg-card/70">
|
|
554
|
+
<CardHeader className="pb-3">
|
|
555
|
+
<div className="flex items-center gap-2">
|
|
556
|
+
<FolderHeartIcon className="size-4 text-primary" />
|
|
557
|
+
<CardDescription>Pinned Collections</CardDescription>
|
|
558
|
+
</div>
|
|
559
|
+
</CardHeader>
|
|
560
|
+
<CardContent className="space-y-2">
|
|
561
|
+
{favoriteCollections.length === 0 ? (
|
|
562
|
+
<p className="text-muted-foreground text-sm">
|
|
563
|
+
Pin collections from the dashboard cards.
|
|
564
|
+
</p>
|
|
565
|
+
) : (
|
|
566
|
+
favoriteCollections.slice(0, 5).map((collection) => (
|
|
567
|
+
<Button
|
|
568
|
+
className="w-full justify-start"
|
|
569
|
+
key={collection.name}
|
|
570
|
+
onClick={() => navigate(collection.href)}
|
|
571
|
+
variant="ghost"
|
|
572
|
+
>
|
|
573
|
+
{collection.label}
|
|
574
|
+
</Button>
|
|
575
|
+
))
|
|
576
|
+
)}
|
|
577
|
+
</CardContent>
|
|
578
|
+
</Card>
|
|
579
|
+
<Card className="border-border/60 bg-card/70">
|
|
580
|
+
<CardHeader className="pb-3">
|
|
581
|
+
<div className="flex items-center gap-2">
|
|
582
|
+
<Search className="size-4 text-primary" />
|
|
583
|
+
<CardDescription>Recent Documents</CardDescription>
|
|
584
|
+
</div>
|
|
585
|
+
</CardHeader>
|
|
586
|
+
<CardContent className="space-y-2">
|
|
587
|
+
{recentDocs.length === 0 ? (
|
|
588
|
+
<p className="text-muted-foreground text-sm">
|
|
589
|
+
Open a few notes and they will show up here.
|
|
590
|
+
</p>
|
|
591
|
+
) : (
|
|
592
|
+
recentDocs.slice(0, 5).map((doc) => (
|
|
593
|
+
<Button
|
|
594
|
+
className="w-full justify-start"
|
|
595
|
+
key={doc.href}
|
|
596
|
+
onClick={() => navigate(doc.href)}
|
|
597
|
+
variant="ghost"
|
|
598
|
+
>
|
|
599
|
+
{doc.label}
|
|
600
|
+
</Button>
|
|
601
|
+
))
|
|
602
|
+
)}
|
|
603
|
+
</CardContent>
|
|
604
|
+
</Card>
|
|
605
|
+
</section>
|
|
606
|
+
)}
|
|
607
|
+
|
|
203
608
|
{status && (
|
|
204
609
|
<div className="mb-10 grid animate-fade-in gap-6 opacity-0 md:grid-cols-4">
|
|
205
|
-
{/* Hero Documents Card */}
|
|
206
610
|
<Card className="group relative overflow-hidden border-primary/30 bg-gradient-to-br from-primary/10 via-primary/5 to-transparent transition-all duration-300 hover:border-primary/50 hover:shadow-[0_0_30px_-10px_hsl(var(--primary)/0.3)]">
|
|
207
611
|
<div className="pointer-events-none absolute -top-12 -right-12 size-32 rounded-full bg-primary/10 blur-2xl" />
|
|
208
612
|
<CardHeader className="relative pb-2">
|
|
@@ -268,7 +672,6 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
268
672
|
</CardContent>
|
|
269
673
|
</Card>
|
|
270
674
|
|
|
271
|
-
{/* Quick Capture Card */}
|
|
272
675
|
<Card
|
|
273
676
|
className="group stagger-3 animate-fade-in cursor-pointer opacity-0 transition-all duration-200 hover:-translate-y-0.5 hover:border-secondary/50 hover:bg-secondary/5 hover:shadow-lg"
|
|
274
677
|
onClick={() => openCapture()}
|
|
@@ -293,7 +696,6 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
293
696
|
</div>
|
|
294
697
|
)}
|
|
295
698
|
|
|
296
|
-
{/* Collections */}
|
|
297
699
|
{status && status.collections.length > 0 && (
|
|
298
700
|
<section className="stagger-3 animate-fade-in opacity-0">
|
|
299
701
|
<div className="mb-6 flex items-center justify-between gap-4 border-border/50 border-b pb-3">
|
|
@@ -303,42 +705,61 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
303
705
|
</Button>
|
|
304
706
|
</div>
|
|
305
707
|
<div className="space-y-3">
|
|
306
|
-
{status.collections.map((
|
|
708
|
+
{status.collections.map((collection, index) => (
|
|
307
709
|
<Card
|
|
308
710
|
className="group animate-fade-in cursor-pointer opacity-0 transition-all hover:border-primary/50 hover:bg-card/80"
|
|
309
|
-
key={
|
|
711
|
+
key={collection.name}
|
|
310
712
|
onClick={() =>
|
|
311
|
-
navigate(
|
|
713
|
+
navigate(
|
|
714
|
+
`/browse?collection=${encodeURIComponent(collection.name)}`
|
|
715
|
+
)
|
|
312
716
|
}
|
|
313
|
-
style={{ animationDelay: `${0.4 +
|
|
717
|
+
style={{ animationDelay: `${0.4 + index * 0.1}s` }}
|
|
314
718
|
>
|
|
315
719
|
<CardContent className="flex items-center justify-between py-4">
|
|
316
720
|
<div className="flex items-center gap-3">
|
|
317
|
-
{/* Health indicator */}
|
|
318
721
|
{syncing ? (
|
|
319
722
|
<Loader2Icon className="size-4 animate-spin text-amber-500" />
|
|
320
|
-
) :
|
|
723
|
+
) : collection.embeddedCount >= collection.chunkCount ? (
|
|
321
724
|
<CheckCircle2Icon className="size-4 text-green-500" />
|
|
322
725
|
) : (
|
|
323
726
|
<div className="size-4 rounded-full border-2 border-amber-500" />
|
|
324
727
|
)}
|
|
325
728
|
<div>
|
|
326
729
|
<div className="font-medium text-lg transition-colors group-hover:text-primary">
|
|
327
|
-
{
|
|
730
|
+
{collection.name}
|
|
328
731
|
</div>
|
|
329
732
|
<div className="font-mono text-muted-foreground text-sm">
|
|
330
|
-
{
|
|
733
|
+
{collection.path}
|
|
331
734
|
</div>
|
|
332
735
|
</div>
|
|
333
736
|
</div>
|
|
334
|
-
<div className="text-right">
|
|
737
|
+
<div className="flex items-center gap-3 text-right">
|
|
738
|
+
<Button
|
|
739
|
+
onClick={(event) => {
|
|
740
|
+
event.stopPropagation();
|
|
741
|
+
handleToggleFavoriteCollection(collection);
|
|
742
|
+
}}
|
|
743
|
+
size="icon-sm"
|
|
744
|
+
variant="ghost"
|
|
745
|
+
>
|
|
746
|
+
<StarIcon
|
|
747
|
+
className={`size-4 ${
|
|
748
|
+
favoriteCollections.some(
|
|
749
|
+
(entry) => entry.name === collection.name
|
|
750
|
+
)
|
|
751
|
+
? "fill-current text-secondary"
|
|
752
|
+
: "text-muted-foreground"
|
|
753
|
+
}`}
|
|
754
|
+
/>
|
|
755
|
+
</Button>
|
|
335
756
|
<div className="font-medium">
|
|
336
|
-
{
|
|
757
|
+
{collection.documentCount.toLocaleString()} docs
|
|
337
758
|
</div>
|
|
338
759
|
<div className="text-muted-foreground text-sm">
|
|
339
|
-
{
|
|
340
|
-
? `${
|
|
341
|
-
: `${
|
|
760
|
+
{collection.embeddedCount === collection.chunkCount
|
|
761
|
+
? `${collection.chunkCount.toLocaleString()} chunks`
|
|
762
|
+
: `${collection.embeddedCount}/${collection.chunkCount} embedded`}
|
|
342
763
|
</div>
|
|
343
764
|
</div>
|
|
344
765
|
</CardContent>
|
|
@@ -349,8 +770,13 @@ export default function Dashboard({ navigate }: PageProps) {
|
|
|
349
770
|
)}
|
|
350
771
|
</main>
|
|
351
772
|
|
|
352
|
-
{/* Floating Action Button */}
|
|
353
773
|
<CaptureButton onClick={() => openCapture()} />
|
|
774
|
+
<AddCollectionDialog
|
|
775
|
+
initialPath={initialCollectionPath}
|
|
776
|
+
onOpenChange={setAddDialogOpen}
|
|
777
|
+
onSuccess={() => void loadStatus()}
|
|
778
|
+
open={addDialogOpen}
|
|
779
|
+
/>
|
|
354
780
|
</div>
|
|
355
781
|
);
|
|
356
782
|
}
|