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 (
{title}
{children}
{badge}
); } function WizardMiniCard({ body, title }: { title: string; body: string }) { return (
{title}

{body}

); } function WizardActionPanel({ action, body, chrome, title, }: { title: string; body: string; action: ReactNode; chrome?: ReactNode; }) { return (
{title}

{body}

{action} {chrome}
); } 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 (
{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) => ( ))}
)}
); } 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 ( ); })}
{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?.detail ?? onboarding.detail}
{renderStepBody()}
You can jump between steps without losing progress.
); }