feat(30-02): API client, hook, ModeSelector, and HardwareSummaryStep

- Add ui/src/api/hardware.ts with fetchHardwareInfo, fetchNexusSettings, updateNexusSettings
- Add ui/src/hooks/useHardwareInfo.ts with useQuery wrapper
- Add queryKeys.hardware.info to ui/src/lib/queryKeys.ts
- Add ModeSelector with three-card layout and selected state styling
- Add HardwareSummaryStep with skeleton loading, tier-appropriate labels, privacy frame
This commit is contained in:
Nexus Dev 2026-04-02 23:27:21 +00:00
parent 545edec89c
commit f44235460a
5 changed files with 195 additions and 0 deletions

34
ui/src/api/hardware.ts Normal file
View file

@ -0,0 +1,34 @@
// [nexus] Hardware detection and nexus settings API client
import { api } from "./client";
export type HardwareTier = "gpu" | "apple_silicon" | "cpu_only";
export interface HardwareInfo {
totalGb: number;
freeGb: number;
usableGb: number;
platform: string;
gpuName: string | null;
gpuVramGb: number | null;
unifiedMemory: boolean;
hardwareTier: HardwareTier;
cpuModel: string | null;
}
export type NexusMode = "personal_ai" | "project_builder" | "both";
export interface NexusSettings {
mode: NexusMode;
}
export function fetchHardwareInfo(): Promise<HardwareInfo> {
return api.get<HardwareInfo>("/system/providers");
}
export function fetchNexusSettings(): Promise<NexusSettings> {
return api.get<NexusSettings>("/nexus/settings");
}
export function updateNexusSettings(settings: Partial<NexusSettings>): Promise<NexusSettings> {
return api.patch<NexusSettings>("/nexus/settings", settings);
}

View file

@ -0,0 +1,95 @@
// [nexus] Hardware summary display for onboarding wizard step 1
import { Skeleton } from "@/components/ui/skeleton";
import type { HardwareInfo } from "@/api/hardware";
interface HardwareSummaryStepProps {
hardwareInfo: HardwareInfo | undefined;
isLoading: boolean;
isError: boolean;
}
interface StatRowProps {
label: string;
value: string | number | null | undefined;
}
function StatRow({ label, value }: StatRowProps) {
return (
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-sm font-medium">{value}</span>
</div>
);
}
export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: HardwareSummaryStepProps) {
if (isLoading) {
return (
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-full rounded" />
</div>
);
}
if (isError) {
return (
<p className="text-sm text-muted-foreground">
Could not detect hardware. You can still continue.
</p>
);
}
if (!hardwareInfo) {
return (
<p className="text-sm text-muted-foreground">
Could not detect hardware. You can still continue.
</p>
);
}
const { hardwareTier } = hardwareInfo;
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
{hardwareTier === "apple_silicon" && (
<>
<StatRow label="Unified memory" value={`${hardwareInfo.totalGb} GB`} />
<StatRow label="Available" value={`${hardwareInfo.usableGb} GB`} />
<StatRow label="CPU" value={hardwareInfo.cpuModel} />
</>
)}
{hardwareTier === "gpu" && (
<>
<StatRow label="GPU" value={hardwareInfo.gpuName} />
<StatRow label="GPU VRAM" value={`${hardwareInfo.gpuVramGb} GB`} />
<StatRow label="System RAM" value={`${hardwareInfo.totalGb} GB`} />
</>
)}
{hardwareTier === "cpu_only" && (
<>
<StatRow label="System RAM" value={`${hardwareInfo.totalGb} GB`} />
<StatRow label="CPU" value={hardwareInfo.cpuModel} />
<p className="text-xs text-muted-foreground">
Slower than GPU-accelerated models -- cloud AI recommended
</p>
</>
)}
</div>
{hardwareTier !== "cpu_only" && (
<div className="flex flex-col gap-1 pt-2">
<span className="text-sm font-medium">Local AI (recommended for privacy)</span>
<span className="text-xs text-muted-foreground">
Runs entirely on your machine.{"\n"}
No accounts. No tracking. Works offline.
</span>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,49 @@
// [nexus] Three-card mode selector for onboarding wizard step 2
import { cn } from "@/lib/utils";
import type { NexusMode } from "@/api/hardware";
interface ModeSelectorProps {
value: NexusMode;
onChange: (mode: NexusMode) => void;
}
const MODES: { id: NexusMode; label: string; description: string }[] = [
{
id: "personal_ai",
label: "Personal AI Assistant",
description: "Always available, persistent memory, private.",
},
{
id: "project_builder",
label: "Project Builder",
description: "Brainstorm -> PM -> Engineer -> shipped product.",
},
{
id: "both",
label: "Both (recommended)",
description: "A conversation becomes a project with one click.",
},
];
export function ModeSelector({ value, onChange }: ModeSelectorProps) {
return (
<div className="grid gap-3">
{MODES.map((mode) => (
<button
key={mode.id}
type="button"
onClick={() => onChange(mode.id)}
className={cn(
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
value === mode.id
? "border-primary bg-primary/5"
: "border-border hover:border-muted-foreground/50"
)}
>
<span className="font-medium text-sm">{mode.label}</span>
<span className="text-xs text-muted-foreground">{mode.description}</span>
</button>
))}
</div>
);
}

View file

@ -0,0 +1,14 @@
// [nexus] React Query hook for hardware detection data
import { useQuery } from "@tanstack/react-query";
import { fetchHardwareInfo, type HardwareInfo } from "../api/hardware";
import { queryKeys } from "../lib/queryKeys";
export function useHardwareInfo(enabled = true) {
return useQuery<HardwareInfo>({
queryKey: queryKeys.hardware.info,
queryFn: fetchHardwareInfo,
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes — matches server cache TTL
retry: 1,
});
}

View file

@ -146,6 +146,9 @@ export const queryKeys = {
agentSkills: (agentId: string) => ["skill-groups", "agent", agentId, "skills"] as const,
agentEffective: (agentId: string) => ["skill-groups", "agent", agentId, "effective"] as const,
},
hardware: {
info: ["hardware", "info"] as const,
},
plugins: {
all: ["plugins"] as const,
examples: ["plugins", "examples"] as const,