nexus/ui/src/components/onboarding/OnboardingSummaryStep.tsx
Nexus Dev 1683e88b9f feat(32-01): create OnboardingSummaryStep component and tests
- Read-only summary card with hardware, mode, provider, root dir rows
- SummaryRow helper component with optional mono styling
- Start chatting CTA with spinner and disabled state
- 6 unit tests covering rendering, empty root dir, error, click, loading
2026-04-04 03:55:49 +00:00

129 lines
3.6 KiB
TypeScript

// [nexus] Summary screen for onboarding wizard step 5 — read-only review before starting
import type { HardwareInfo, HardwareTier, NexusMode } from "@/api/hardware";
import { Button } from "@/components/ui/button";
interface OnboardingSummaryStepProps {
hardwareInfo: HardwareInfo | undefined;
selectedMode: NexusMode;
providerLabel: string;
rootDir: string;
loading: boolean;
error: string | null;
onStartChat: () => void;
onBack: () => void;
}
interface SummaryRowProps {
label: string;
value: string;
mono?: boolean;
}
function SummaryRow({ label, value, mono }: SummaryRowProps) {
return (
<div className="flex items-start justify-between gap-4">
<span className="text-sm text-muted-foreground shrink-0">{label}</span>
<span className={mono ? "font-mono text-sm text-right" : "text-sm text-right"}>{value}</span>
</div>
);
}
const HARDWARE_TIER_LABELS: Record<HardwareTier, string> = {
gpu: "GPU",
apple_silicon: "Apple Silicon",
cpu_only: "CPU Only",
};
const MODE_LABELS: Record<NexusMode, string> = {
personal_ai: "Personal AI Assistant",
project_builder: "Project Builder",
both: "Both (recommended)",
};
export function OnboardingSummaryStep({
hardwareInfo,
selectedMode,
providerLabel,
rootDir,
loading,
error,
onStartChat,
onBack,
}: OnboardingSummaryStepProps) {
const hardwareLabel = hardwareInfo
? (HARDWARE_TIER_LABELS[hardwareInfo.hardwareTier] ?? "Unknown")
: "Unknown";
const modeLabel = MODE_LABELS[selectedMode];
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">Ready to go</h1>
<p className="text-sm text-muted-foreground">Review your setup before starting.</p>
</div>
{/* Summary card */}
<div className="rounded-lg border border-border p-4 flex flex-col gap-3">
<SummaryRow label="Hardware" value={hardwareLabel} />
<SummaryRow label="Mode" value={modeLabel} />
<SummaryRow label="Provider" value={providerLabel} />
{rootDir && <SummaryRow label="Root directory" value={rootDir} mono />}
</div>
{/* Error message */}
{error && (
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
{error}
</p>
)}
{/* Actions */}
<div className="flex flex-col gap-2">
<Button
type="button"
onClick={onStartChat}
disabled={loading}
className="w-full"
>
{loading ? (
<span className="flex items-center gap-2">
<svg
className="h-4 w-4 animate-spin"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Setting up...
</span>
) : (
"Start chatting"
)}
</Button>
<Button
type="button"
variant="ghost"
onClick={onBack}
disabled={loading}
className="w-full"
>
Back
</Button>
</div>
</div>
);
}