@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
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckCircle2Icon,
|
|
3
|
+
ChevronLeftIcon,
|
|
4
|
+
ChevronRightIcon,
|
|
5
|
+
DownloadIcon,
|
|
6
|
+
FolderPlusIcon,
|
|
7
|
+
RefreshCwIcon,
|
|
8
|
+
SparklesIcon,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { type ReactNode, useEffect, useState } from "react";
|
|
11
|
+
|
|
12
|
+
import type { AppStatusResponse, OnboardingStep } from "../../status-model";
|
|
13
|
+
|
|
14
|
+
import { cn } from "../lib/utils";
|
|
15
|
+
import { AIModelSelector } from "./AIModelSelector";
|
|
16
|
+
import { IndexingProgress } from "./IndexingProgress";
|
|
17
|
+
import { Badge } from "./ui/badge";
|
|
18
|
+
import { Button } from "./ui/button";
|
|
19
|
+
import {
|
|
20
|
+
Card,
|
|
21
|
+
CardContent,
|
|
22
|
+
CardDescription,
|
|
23
|
+
CardFooter,
|
|
24
|
+
CardHeader,
|
|
25
|
+
CardTitle,
|
|
26
|
+
} from "./ui/card";
|
|
27
|
+
import { Progress } from "./ui/progress";
|
|
28
|
+
import { ScrollArea } from "./ui/scroll-area";
|
|
29
|
+
import { Separator } from "./ui/separator";
|
|
30
|
+
|
|
31
|
+
interface FirstRunWizardProps {
|
|
32
|
+
onboarding: AppStatusResponse["onboarding"];
|
|
33
|
+
onAddCollection: (path?: string) => void;
|
|
34
|
+
onDownloadModels: () => void;
|
|
35
|
+
onEmbed: () => void;
|
|
36
|
+
onSync: () => void;
|
|
37
|
+
onSyncComplete?: () => void;
|
|
38
|
+
embedding?: boolean;
|
|
39
|
+
syncJobId?: string | null;
|
|
40
|
+
syncing?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function WizardPanel({
|
|
44
|
+
children,
|
|
45
|
+
className,
|
|
46
|
+
}: {
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
className?: string;
|
|
49
|
+
}) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
"rounded-2xl border border-border/70 bg-card/80 shadow-sm",
|
|
54
|
+
className
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function WizardPanelHeader({
|
|
63
|
+
badge,
|
|
64
|
+
children,
|
|
65
|
+
title,
|
|
66
|
+
}: {
|
|
67
|
+
title: string;
|
|
68
|
+
children?: ReactNode;
|
|
69
|
+
badge?: ReactNode;
|
|
70
|
+
}) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="border-border/50 border-b" style={{ padding: "24px 28px" }}>
|
|
73
|
+
<div className="min-w-0 flex-1">
|
|
74
|
+
<div className="mb-2 font-medium text-base">{title}</div>
|
|
75
|
+
{children}
|
|
76
|
+
</div>
|
|
77
|
+
{badge}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function WizardMiniCard({ body, title }: { title: string; body: string }) {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className="rounded-xl border border-border/60 bg-background/80 shadow-none"
|
|
86
|
+
style={{ padding: "20px" }}
|
|
87
|
+
>
|
|
88
|
+
<div className="mb-2 font-medium text-sm">{title}</div>
|
|
89
|
+
<p className="text-muted-foreground text-sm leading-6">{body}</p>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function WizardActionPanel({
|
|
95
|
+
action,
|
|
96
|
+
body,
|
|
97
|
+
chrome,
|
|
98
|
+
title,
|
|
99
|
+
}: {
|
|
100
|
+
title: string;
|
|
101
|
+
body: string;
|
|
102
|
+
action: ReactNode;
|
|
103
|
+
chrome?: ReactNode;
|
|
104
|
+
}) {
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
className="rounded-2xl border border-secondary/20 bg-secondary/5 shadow-sm"
|
|
108
|
+
style={{ padding: "24px 28px" }}
|
|
109
|
+
>
|
|
110
|
+
<div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-center">
|
|
111
|
+
<div className="min-w-0">
|
|
112
|
+
<div className="mb-2 font-medium text-sm">{title}</div>
|
|
113
|
+
<p className="text-muted-foreground text-sm leading-6">{body}</p>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="flex flex-wrap items-center gap-3 pt-2 lg:justify-self-end lg:pt-0">
|
|
116
|
+
{action}
|
|
117
|
+
{chrome}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getRecommendedStepId(
|
|
125
|
+
onboarding: AppStatusResponse["onboarding"]
|
|
126
|
+
): string {
|
|
127
|
+
const current =
|
|
128
|
+
onboarding.steps.find((step) => step.status === "current") ??
|
|
129
|
+
onboarding.steps.find((step) => step.status !== "complete") ??
|
|
130
|
+
onboarding.steps[0];
|
|
131
|
+
|
|
132
|
+
return current?.id ?? "folders";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getStepProgress(onboarding: AppStatusResponse["onboarding"]): number {
|
|
136
|
+
if (onboarding.steps.length === 0) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const completeCount = onboarding.steps.filter(
|
|
141
|
+
(step) => step.status === "complete"
|
|
142
|
+
).length;
|
|
143
|
+
const hasCurrent = onboarding.steps.some((step) => step.status === "current");
|
|
144
|
+
|
|
145
|
+
return Math.round(
|
|
146
|
+
((completeCount + (hasCurrent ? 0.5 : 0)) / onboarding.steps.length) * 100
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function getStepTone(step: OnboardingStep): {
|
|
151
|
+
badge: string;
|
|
152
|
+
className: string;
|
|
153
|
+
} {
|
|
154
|
+
if (step.status === "complete") {
|
|
155
|
+
return {
|
|
156
|
+
badge: "Done",
|
|
157
|
+
className:
|
|
158
|
+
"border-emerald-500/25 bg-emerald-500/10 text-emerald-500 hover:border-emerald-500/40",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (step.status === "current") {
|
|
163
|
+
return {
|
|
164
|
+
badge: "Now",
|
|
165
|
+
className:
|
|
166
|
+
"border-primary/35 bg-primary/10 text-primary hover:border-primary/50",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
badge: "Later",
|
|
172
|
+
className:
|
|
173
|
+
"border-border/60 bg-background/70 text-muted-foreground hover:border-border hover:text-foreground",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderStepIcon(stepId: string) {
|
|
178
|
+
if (stepId === "folders") {
|
|
179
|
+
return <FolderPlusIcon className="size-4" />;
|
|
180
|
+
}
|
|
181
|
+
if (stepId === "preset") {
|
|
182
|
+
return <SparklesIcon className="size-4" />;
|
|
183
|
+
}
|
|
184
|
+
if (stepId === "models") {
|
|
185
|
+
return <DownloadIcon className="size-4" />;
|
|
186
|
+
}
|
|
187
|
+
return <RefreshCwIcon className="size-4" />;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getStepActionLabel(stepId: string): string {
|
|
191
|
+
if (stepId === "folders") {
|
|
192
|
+
return "Add folder";
|
|
193
|
+
}
|
|
194
|
+
if (stepId === "models") {
|
|
195
|
+
return "Download models";
|
|
196
|
+
}
|
|
197
|
+
if (stepId === "indexing") {
|
|
198
|
+
return "Run sync";
|
|
199
|
+
}
|
|
200
|
+
return "Review preset";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function FirstRunWizard({
|
|
204
|
+
onboarding,
|
|
205
|
+
onAddCollection,
|
|
206
|
+
onDownloadModels,
|
|
207
|
+
onEmbed,
|
|
208
|
+
onSync,
|
|
209
|
+
onSyncComplete,
|
|
210
|
+
embedding = false,
|
|
211
|
+
syncJobId = null,
|
|
212
|
+
syncing = false,
|
|
213
|
+
}: FirstRunWizardProps) {
|
|
214
|
+
const recommendedStepId = getRecommendedStepId(onboarding);
|
|
215
|
+
const [activeStepId, setActiveStepId] = useState(recommendedStepId);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
setActiveStepId((current) => {
|
|
219
|
+
if (onboarding.steps.some((step) => step.id === current)) {
|
|
220
|
+
return current;
|
|
221
|
+
}
|
|
222
|
+
return recommendedStepId;
|
|
223
|
+
});
|
|
224
|
+
}, [onboarding.steps, recommendedStepId]);
|
|
225
|
+
|
|
226
|
+
const steps = onboarding.steps;
|
|
227
|
+
const foldersStep = steps.find((step) => step.id === "folders");
|
|
228
|
+
const foldersConnected = foldersStep?.status === "complete";
|
|
229
|
+
const activeStep =
|
|
230
|
+
steps.find((step) => step.id === activeStepId) ?? steps[0] ?? null;
|
|
231
|
+
const activeIndex = activeStep
|
|
232
|
+
? steps.findIndex((step) => step.id === activeStep.id)
|
|
233
|
+
: 0;
|
|
234
|
+
const previousStep = activeIndex > 0 ? steps[activeIndex - 1] : null;
|
|
235
|
+
const nextStep =
|
|
236
|
+
activeIndex >= 0 && activeIndex < steps.length - 1
|
|
237
|
+
? steps[activeIndex + 1]
|
|
238
|
+
: null;
|
|
239
|
+
const progressValue = getStepProgress(onboarding);
|
|
240
|
+
const isShowingRecommended = activeStep?.id === recommendedStepId;
|
|
241
|
+
const indexingNeedsEmbeddings =
|
|
242
|
+
onboarding.stage === "indexing" &&
|
|
243
|
+
onboarding.detail.toLowerCase().includes("embedding");
|
|
244
|
+
|
|
245
|
+
const runRecommendedAction = () => {
|
|
246
|
+
if (!activeStep) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (activeStep.id === "folders") {
|
|
250
|
+
onAddCollection();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (activeStep.id === "models") {
|
|
254
|
+
onDownloadModels();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (activeStep.id === "indexing" && indexingNeedsEmbeddings && !embedding) {
|
|
258
|
+
onEmbed();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (activeStep.id === "indexing" && !syncing) {
|
|
262
|
+
onSync();
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const renderStepBody = () => {
|
|
267
|
+
if (!activeStep) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (activeStep.id === "folders") {
|
|
272
|
+
return (
|
|
273
|
+
<div className="space-y-5">
|
|
274
|
+
<div className="flex flex-wrap gap-3">
|
|
275
|
+
<Button onClick={() => onAddCollection()} size="lg">
|
|
276
|
+
<FolderPlusIcon className="mr-2 size-4" />
|
|
277
|
+
{foldersConnected ? "Add another folder" : "Add first folder"}
|
|
278
|
+
</Button>
|
|
279
|
+
<Button
|
|
280
|
+
onClick={() => onAddCollection()}
|
|
281
|
+
size="lg"
|
|
282
|
+
variant="outline"
|
|
283
|
+
>
|
|
284
|
+
{foldersConnected
|
|
285
|
+
? "Browse for more folders"
|
|
286
|
+
: "Browse for another folder"}
|
|
287
|
+
</Button>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
{onboarding.suggestedCollections.length > 0 && (
|
|
291
|
+
<div className="space-y-3">
|
|
292
|
+
<div className="flex items-center justify-between gap-3">
|
|
293
|
+
<div>
|
|
294
|
+
<h3 className="font-medium">
|
|
295
|
+
{foldersConnected
|
|
296
|
+
? "Suggested next folders"
|
|
297
|
+
: "Recommended starting points"}
|
|
298
|
+
</h3>
|
|
299
|
+
<p className="text-muted-foreground text-sm">
|
|
300
|
+
{foldersConnected
|
|
301
|
+
? "Add another source quickly from the common local spots below."
|
|
302
|
+
: "Pick one to prefill the add-folder dialog."}
|
|
303
|
+
</p>
|
|
304
|
+
</div>
|
|
305
|
+
<Badge variant="outline">Quick picks</Badge>
|
|
306
|
+
</div>
|
|
307
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
308
|
+
{onboarding.suggestedCollections.map((suggestion) => (
|
|
309
|
+
<button
|
|
310
|
+
className="rounded-xl border border-border/70 bg-background/80 p-4 text-left shadow-sm transition-all hover:border-primary/35 hover:bg-background hover:shadow-md"
|
|
311
|
+
key={suggestion.path}
|
|
312
|
+
onClick={() => onAddCollection(suggestion.path)}
|
|
313
|
+
type="button"
|
|
314
|
+
>
|
|
315
|
+
<div className="mb-2 flex items-center justify-between gap-3">
|
|
316
|
+
<span className="font-medium">{suggestion.label}</span>
|
|
317
|
+
<ChevronRightIcon className="size-4 text-muted-foreground" />
|
|
318
|
+
</div>
|
|
319
|
+
<p className="font-mono text-muted-foreground text-xs">
|
|
320
|
+
{suggestion.path}
|
|
321
|
+
</p>
|
|
322
|
+
<p className="mt-3 text-muted-foreground text-sm">
|
|
323
|
+
{suggestion.reason}
|
|
324
|
+
</p>
|
|
325
|
+
</button>
|
|
326
|
+
))}
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
)}
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (activeStep.id === "preset") {
|
|
335
|
+
return (
|
|
336
|
+
<div className="space-y-5">
|
|
337
|
+
<WizardPanel className="border-secondary/25 bg-secondary/5">
|
|
338
|
+
<WizardPanelHeader
|
|
339
|
+
badge={<Badge variant="outline">Safe to switch</Badge>}
|
|
340
|
+
title="Preset selector"
|
|
341
|
+
>
|
|
342
|
+
<p className="max-w-3xl text-muted-foreground text-sm leading-6">
|
|
343
|
+
Pick the preset here. The active card below explains the
|
|
344
|
+
trade-off, model footprint, and whether a tuned preset is
|
|
345
|
+
available.
|
|
346
|
+
</p>
|
|
347
|
+
</WizardPanelHeader>
|
|
348
|
+
<div style={{ padding: "24px 28px" }}>
|
|
349
|
+
<AIModelSelector showDetails showDownloadAction={false} />
|
|
350
|
+
</div>
|
|
351
|
+
</WizardPanel>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (activeStep.id === "models") {
|
|
357
|
+
return (
|
|
358
|
+
<div className="space-y-5">
|
|
359
|
+
<WizardPanel>
|
|
360
|
+
<WizardPanelHeader
|
|
361
|
+
badge={<Badge variant="outline">Background download</Badge>}
|
|
362
|
+
title="Local model readiness"
|
|
363
|
+
>
|
|
364
|
+
<p className="max-w-2xl text-muted-foreground text-sm leading-6">
|
|
365
|
+
{activeStep.detail}
|
|
366
|
+
</p>
|
|
367
|
+
</WizardPanelHeader>
|
|
368
|
+
<div style={{ padding: "24px 28px" }}>
|
|
369
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
370
|
+
<WizardMiniCard
|
|
371
|
+
body="Core search is already usable. You can keep exploring while the heavier local model roles download."
|
|
372
|
+
title="What you have now"
|
|
373
|
+
/>
|
|
374
|
+
<WizardMiniCard
|
|
375
|
+
body="Better reranking, stronger local answers, and a cleaner first run experience once the active preset is fully cached."
|
|
376
|
+
title="What improves next"
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
</WizardPanel>
|
|
381
|
+
|
|
382
|
+
<WizardActionPanel
|
|
383
|
+
action={
|
|
384
|
+
<Button onClick={onDownloadModels} size="lg">
|
|
385
|
+
<DownloadIcon className="mr-2 size-4" />
|
|
386
|
+
Download local models
|
|
387
|
+
</Button>
|
|
388
|
+
}
|
|
389
|
+
body="Safe to start now. You do not need to stay on this step."
|
|
390
|
+
chrome={
|
|
391
|
+
<Badge className="px-3 py-1" variant="outline">
|
|
392
|
+
Runs in background
|
|
393
|
+
</Badge>
|
|
394
|
+
}
|
|
395
|
+
title="Download active preset"
|
|
396
|
+
/>
|
|
397
|
+
</div>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<div className="space-y-5">
|
|
403
|
+
<WizardPanel>
|
|
404
|
+
<WizardPanelHeader
|
|
405
|
+
badge={<Badge variant="outline">Safe to rerun</Badge>}
|
|
406
|
+
title="Finish first indexing"
|
|
407
|
+
>
|
|
408
|
+
<p className="max-w-2xl text-muted-foreground text-sm leading-6">
|
|
409
|
+
{activeStep.detail}
|
|
410
|
+
</p>
|
|
411
|
+
</WizardPanelHeader>
|
|
412
|
+
<div style={{ padding: "24px 28px" }}>
|
|
413
|
+
<WizardMiniCard
|
|
414
|
+
body="Pulls the current folder state into the index and reconciles the first-run workspace so search, browse, and health data line up."
|
|
415
|
+
title="What this does"
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
</WizardPanel>
|
|
419
|
+
|
|
420
|
+
<WizardActionPanel
|
|
421
|
+
action={
|
|
422
|
+
<Button
|
|
423
|
+
disabled={indexingNeedsEmbeddings ? embedding : syncing}
|
|
424
|
+
onClick={indexingNeedsEmbeddings ? onEmbed : onSync}
|
|
425
|
+
size="lg"
|
|
426
|
+
>
|
|
427
|
+
<RefreshCwIcon className="mr-2 size-4" />
|
|
428
|
+
{indexingNeedsEmbeddings
|
|
429
|
+
? embedding
|
|
430
|
+
? "Embedding..."
|
|
431
|
+
: "Finish embeddings"
|
|
432
|
+
: syncing
|
|
433
|
+
? "Syncing..."
|
|
434
|
+
: "Run first sync"}
|
|
435
|
+
</Button>
|
|
436
|
+
}
|
|
437
|
+
body={
|
|
438
|
+
indexingNeedsEmbeddings
|
|
439
|
+
? "Your files are indexed. One more embedding pass will unlock semantic search and local answers."
|
|
440
|
+
: "Good last step before you move into normal use."
|
|
441
|
+
}
|
|
442
|
+
title={
|
|
443
|
+
indexingNeedsEmbeddings
|
|
444
|
+
? "Finish semantic indexing"
|
|
445
|
+
: "Index the current workspace"
|
|
446
|
+
}
|
|
447
|
+
/>
|
|
448
|
+
|
|
449
|
+
{syncJobId && (
|
|
450
|
+
<WizardPanel>
|
|
451
|
+
<WizardPanelHeader title="Sync progress">
|
|
452
|
+
<p className="max-w-2xl text-muted-foreground text-sm leading-6">
|
|
453
|
+
Your first indexing run is in progress. The wizard will advance
|
|
454
|
+
once the job finishes and embeddings catch up.
|
|
455
|
+
</p>
|
|
456
|
+
</WizardPanelHeader>
|
|
457
|
+
<div style={{ padding: "24px 28px" }}>
|
|
458
|
+
<IndexingProgress jobId={syncJobId} onComplete={onSyncComplete} />
|
|
459
|
+
</div>
|
|
460
|
+
</WizardPanel>
|
|
461
|
+
)}
|
|
462
|
+
</div>
|
|
463
|
+
);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<section className="grid gap-6 xl:grid-cols-[280px_minmax(0,1fr)]">
|
|
468
|
+
<Card className="border-primary/15 bg-gradient-to-b from-card via-card to-primary/5">
|
|
469
|
+
<CardHeader className="space-y-4">
|
|
470
|
+
<div className="flex items-center justify-between gap-3">
|
|
471
|
+
<div className="flex items-center gap-2 text-primary">
|
|
472
|
+
<SparklesIcon className="size-4" />
|
|
473
|
+
<span className="font-medium text-sm tracking-wide uppercase">
|
|
474
|
+
Setup wizard
|
|
475
|
+
</span>
|
|
476
|
+
</div>
|
|
477
|
+
<Badge variant="outline">{progressValue}%</Badge>
|
|
478
|
+
</div>
|
|
479
|
+
<div className="space-y-2">
|
|
480
|
+
<CardTitle className="text-2xl tracking-tight">
|
|
481
|
+
{onboarding.headline}
|
|
482
|
+
</CardTitle>
|
|
483
|
+
<CardDescription>{onboarding.detail}</CardDescription>
|
|
484
|
+
</div>
|
|
485
|
+
<Progress value={progressValue} />
|
|
486
|
+
</CardHeader>
|
|
487
|
+
<CardContent className="pt-0">
|
|
488
|
+
<ScrollArea className="max-h-[420px] pr-3">
|
|
489
|
+
<div className="space-y-3">
|
|
490
|
+
{steps.map((step, index) => {
|
|
491
|
+
const tone = getStepTone(step);
|
|
492
|
+
const isActive = step.id === activeStep?.id;
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<button
|
|
496
|
+
className={cn(
|
|
497
|
+
"w-full rounded-2xl border p-4 text-left shadow-sm transition-all",
|
|
498
|
+
tone.className,
|
|
499
|
+
isActive &&
|
|
500
|
+
"ring-2 ring-primary/20 ring-offset-2 ring-offset-background"
|
|
501
|
+
)}
|
|
502
|
+
key={step.id}
|
|
503
|
+
onClick={() => setActiveStepId(step.id)}
|
|
504
|
+
type="button"
|
|
505
|
+
>
|
|
506
|
+
<div className="mb-3 flex items-start justify-between gap-3">
|
|
507
|
+
<div className="flex items-center gap-3">
|
|
508
|
+
<div
|
|
509
|
+
className={cn(
|
|
510
|
+
"flex size-9 shrink-0 items-center justify-center rounded-full border text-sm",
|
|
511
|
+
step.status === "complete"
|
|
512
|
+
? "border-emerald-500/30 bg-emerald-500/15 text-emerald-500"
|
|
513
|
+
: step.status === "current"
|
|
514
|
+
? "border-primary/30 bg-primary/10 text-primary"
|
|
515
|
+
: "border-border/70 bg-background/70 text-muted-foreground"
|
|
516
|
+
)}
|
|
517
|
+
>
|
|
518
|
+
{step.status === "complete" ? (
|
|
519
|
+
<CheckCircle2Icon className="size-4" />
|
|
520
|
+
) : (
|
|
521
|
+
index + 1
|
|
522
|
+
)}
|
|
523
|
+
</div>
|
|
524
|
+
<div>
|
|
525
|
+
<div className="font-medium">{step.title}</div>
|
|
526
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
527
|
+
Step {index + 1}
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
531
|
+
<Badge variant="outline">{tone.badge}</Badge>
|
|
532
|
+
</div>
|
|
533
|
+
<p className="text-sm text-muted-foreground">
|
|
534
|
+
{step.detail}
|
|
535
|
+
</p>
|
|
536
|
+
</button>
|
|
537
|
+
);
|
|
538
|
+
})}
|
|
539
|
+
</div>
|
|
540
|
+
</ScrollArea>
|
|
541
|
+
</CardContent>
|
|
542
|
+
</Card>
|
|
543
|
+
|
|
544
|
+
<Card className="border-primary/20 bg-gradient-to-br from-primary/8 via-card to-card">
|
|
545
|
+
<CardHeader className="space-y-4 border-border/50 border-b bg-background/40">
|
|
546
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
547
|
+
<div className="flex items-center gap-3">
|
|
548
|
+
<div className="flex size-10 items-center justify-center rounded-full border border-primary/25 bg-primary/10 text-primary">
|
|
549
|
+
{activeStep ? renderStepIcon(activeStep.id) : null}
|
|
550
|
+
</div>
|
|
551
|
+
<div>
|
|
552
|
+
<div className="mb-1 flex items-center gap-2">
|
|
553
|
+
<Badge variant="outline">
|
|
554
|
+
Step {Math.max(activeIndex + 1, 1)} of {steps.length || 1}
|
|
555
|
+
</Badge>
|
|
556
|
+
{isShowingRecommended && (
|
|
557
|
+
<Badge className="border-primary/30 bg-primary/10 text-primary hover:bg-primary/10">
|
|
558
|
+
Recommended now
|
|
559
|
+
</Badge>
|
|
560
|
+
)}
|
|
561
|
+
</div>
|
|
562
|
+
<CardTitle className="text-3xl tracking-tight">
|
|
563
|
+
{activeStep?.id === "preset"
|
|
564
|
+
? "Choose how GNO should feel"
|
|
565
|
+
: (activeStep?.title ?? onboarding.headline)}
|
|
566
|
+
</CardTitle>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
{activeStep && activeStep.id !== "preset" && (
|
|
570
|
+
<Button
|
|
571
|
+
disabled={
|
|
572
|
+
activeStep.id === "indexing"
|
|
573
|
+
? indexingNeedsEmbeddings
|
|
574
|
+
? embedding
|
|
575
|
+
: syncing
|
|
576
|
+
: false
|
|
577
|
+
}
|
|
578
|
+
onClick={runRecommendedAction}
|
|
579
|
+
size="sm"
|
|
580
|
+
variant={isShowingRecommended ? "default" : "outline"}
|
|
581
|
+
>
|
|
582
|
+
{activeStep.id === "indexing" && indexingNeedsEmbeddings
|
|
583
|
+
? embedding
|
|
584
|
+
? "Embedding..."
|
|
585
|
+
: "Finish embeddings"
|
|
586
|
+
: activeStep.id === "indexing" && syncing
|
|
587
|
+
? "Syncing..."
|
|
588
|
+
: getStepActionLabel(activeStep.id)}
|
|
589
|
+
</Button>
|
|
590
|
+
)}
|
|
591
|
+
</div>
|
|
592
|
+
<CardDescription className="max-w-3xl text-base">
|
|
593
|
+
{activeStep?.detail ?? onboarding.detail}
|
|
594
|
+
</CardDescription>
|
|
595
|
+
</CardHeader>
|
|
596
|
+
<CardContent className="space-y-6 p-6">{renderStepBody()}</CardContent>
|
|
597
|
+
<Separator />
|
|
598
|
+
<CardFooter className="flex flex-wrap items-center justify-between gap-3 p-6">
|
|
599
|
+
<Button
|
|
600
|
+
disabled={!previousStep}
|
|
601
|
+
onClick={() => previousStep && setActiveStepId(previousStep.id)}
|
|
602
|
+
variant="outline"
|
|
603
|
+
>
|
|
604
|
+
<ChevronLeftIcon className="mr-2 size-4" />
|
|
605
|
+
Previous
|
|
606
|
+
</Button>
|
|
607
|
+
<div className="text-muted-foreground text-sm">
|
|
608
|
+
You can jump between steps without losing progress.
|
|
609
|
+
</div>
|
|
610
|
+
<Button
|
|
611
|
+
disabled={!nextStep}
|
|
612
|
+
onClick={() => nextStep && setActiveStepId(nextStep.id)}
|
|
613
|
+
variant="outline"
|
|
614
|
+
>
|
|
615
|
+
Next
|
|
616
|
+
<ChevronRightIcon className="ml-2 size-4" />
|
|
617
|
+
</Button>
|
|
618
|
+
</CardFooter>
|
|
619
|
+
</Card>
|
|
620
|
+
</section>
|
|
621
|
+
);
|
|
622
|
+
}
|