import {
CheckCircle2Icon,
ChevronLeftIcon,
ChevronRightIcon,
DownloadIcon,
FolderPlusIcon,
RefreshCwIcon,
SparklesIcon,
} from "lucide-react";
import { type ReactNode, useEffect, useState } from "react";
import type { AppStatusResponse, OnboardingStep } from "../../status-model";
import { cn } from "../lib/utils";
import { AIModelSelector } from "./AIModelSelector";
import { IndexingProgress } from "./IndexingProgress";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "./ui/card";
import { Progress } from "./ui/progress";
import { ScrollArea } from "./ui/scroll-area";
import { Separator } from "./ui/separator";
interface FirstRunWizardProps {
onboarding: AppStatusResponse["onboarding"];
onAddCollection: (path?: string) => void;
onDownloadModels: () => void;
onEmbed: () => void;
onSync: () => void;
onSyncComplete?: () => void;
embedding?: boolean;
syncJobId?: string | null;
syncing?: boolean;
}
function WizardPanel({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
{children}
);
}
function WizardPanelHeader({
badge,
children,
title,
}: {
title: string;
children?: ReactNode;
badge?: ReactNode;
}) {
return (
);
}
function WizardMiniCard({ body, title }: { title: string; body: string }) {
return (
);
}
function WizardActionPanel({
action,
body,
chrome,
title,
}: {
title: string;
body: string;
action: ReactNode;
chrome?: ReactNode;
}) {
return (
);
}
function getRecommendedStepId(
onboarding: AppStatusResponse["onboarding"]
): string {
const current =
onboarding.steps.find((step) => step.status === "current") ??
onboarding.steps.find((step) => step.status !== "complete") ??
onboarding.steps[0];
return current?.id ?? "folders";
}
function getStepProgress(onboarding: AppStatusResponse["onboarding"]): number {
if (onboarding.steps.length === 0) {
return 0;
}
const completeCount = onboarding.steps.filter(
(step) => step.status === "complete"
).length;
const hasCurrent = onboarding.steps.some((step) => step.status === "current");
return Math.round(
((completeCount + (hasCurrent ? 0.5 : 0)) / onboarding.steps.length) * 100
);
}
function getStepTone(step: OnboardingStep): {
badge: string;
className: string;
} {
if (step.status === "complete") {
return {
badge: "Done",
className:
"border-emerald-500/25 bg-emerald-500/10 text-emerald-500 hover:border-emerald-500/40",
};
}
if (step.status === "current") {
return {
badge: "Now",
className:
"border-primary/35 bg-primary/10 text-primary hover:border-primary/50",
};
}
return {
badge: "Later",
className:
"border-border/60 bg-background/70 text-muted-foreground hover:border-border hover:text-foreground",
};
}
function renderStepIcon(stepId: string) {
if (stepId === "folders") {
return ;
}
if (stepId === "preset") {
return ;
}
if (stepId === "models") {
return ;
}
return ;
}
function getStepActionLabel(stepId: string): string {
if (stepId === "folders") {
return "Add folder";
}
if (stepId === "models") {
return "Download models";
}
if (stepId === "indexing") {
return "Run sync";
}
return "Review preset";
}
export function FirstRunWizard({
onboarding,
onAddCollection,
onDownloadModels,
onSync,
onSyncComplete,
syncJobId = null,
syncing = false,
}: FirstRunWizardProps) {
const recommendedStepId = getRecommendedStepId(onboarding);
const [activeStepId, setActiveStepId] = useState(recommendedStepId);
useEffect(() => {
setActiveStepId((current) => {
if (onboarding.steps.some((step) => step.id === current)) {
return current;
}
return recommendedStepId;
});
}, [onboarding.steps, recommendedStepId]);
const steps = onboarding.steps;
const foldersStep = steps.find((step) => step.id === "folders");
const foldersConnected = foldersStep?.status === "complete";
const activeStep =
steps.find((step) => step.id === activeStepId) ?? steps[0] ?? null;
const activeIndex = activeStep
? steps.findIndex((step) => step.id === activeStep.id)
: 0;
const previousStep = activeIndex > 0 ? steps[activeIndex - 1] : null;
const nextStep =
activeIndex >= 0 && activeIndex < steps.length - 1
? steps[activeIndex + 1]
: null;
const progressValue = getStepProgress(onboarding);
const isShowingRecommended = activeStep?.id === recommendedStepId;
const runRecommendedAction = () => {
if (!activeStep) {
return;
}
if (activeStep.action === "add-collection") {
onAddCollection();
return;
}
if (activeStep.action === "download-models") {
onDownloadModels();
return;
}
if (activeStep.action === "sync" && !syncing) {
onSync();
}
};
const renderStepBody = () => {
if (!activeStep) {
return null;
}
if (activeStep.id === "folders") {
return (
onAddCollection()} size="lg">
{foldersConnected ? "Add another folder" : "Add first folder"}
onAddCollection()}
size="lg"
variant="outline"
>
{foldersConnected
? "Browse for more folders"
: "Browse for another folder"}
{onboarding.suggestedCollections.length > 0 && (
{foldersConnected
? "Suggested next folders"
: "Recommended starting points"}
{foldersConnected
? "Add another source quickly from the common local spots below."
: "Pick one to prefill the add-folder dialog."}
Quick picks
{onboarding.suggestedCollections.map((suggestion) => (
onAddCollection(suggestion.path)}
type="button"
>
{suggestion.label}
{suggestion.path}
{suggestion.reason}
))}
)}
);
}
if (activeStep.id === "preset") {
return (
Safe to switch}
title="Preset selector"
>
Pick the preset here. The active card below explains the
trade-off, model footprint, and whether a tuned preset is
available.
);
}
if (activeStep.id === "models") {
return (
Background download}
title="Local model readiness"
>
{activeStep.detail}
Download local models
}
body="Safe to start now. You do not need to stay on this step."
chrome={
Runs in background
}
title="Download active preset"
/>
);
}
return (
Safe to rerun}
title="Prove lexical retrieval"
>
{activeStep.detail}
{syncing ? "Syncing..." : "Run first sync"}
}
body="Builds the local lexical index and proves that this exact folder can return corpus-derived evidence."
title="Index and verify the current workspace"
/>
{syncJobId && (
Your first indexing run is in progress. The wizard will advance
once the job finishes and lexical retrieval is proven.
)}
);
};
return (
Setup wizard
{progressValue}%
{onboarding.headline}
{onboarding.detail}
{steps.map((step, index) => {
const tone = getStepTone(step);
const isActive = step.id === activeStep?.id;
return (
setActiveStepId(step.id)}
type="button"
>
{step.status === "complete" ? (
) : (
index + 1
)}
{step.title}
Step {index + 1}
{tone.badge}
{step.detail}
);
})}
{activeStep ? renderStepIcon(activeStep.id) : null}
Step {Math.max(activeIndex + 1, 1)} of {steps.length || 1}
{isShowingRecommended && (
Recommended now
)}
{activeStep?.id === "preset"
? "Choose how GNO should feel"
: (activeStep?.title ?? onboarding.headline)}
{activeStep && activeStep.id !== "preset" && (
{activeStep.action === "sync" && syncing
? "Syncing..."
: getStepActionLabel(activeStep.id)}
)}
{activeStep?.detail ?? onboarding.detail}
{renderStepBody()}
previousStep && setActiveStepId(previousStep.id)}
variant="outline"
>
Previous
You can jump between steps without losing progress.
nextStep && setActiveStepId(nextStep.id)}
variant="outline"
>
Next
);
}